on main help sign in
Merge remote-tracking branch 'origin/main' into list
commitd38891e9ad143adcec97697f0ed32ed76b8a5fdd authorKujtim Hoxha <kujtimii.h@gmail.com> dateSat 26 Jul 2025, 12:27 (1 year ago) parent4b0f84f2d809a7
10 files changed+132−23
.goreleaser.yml+27 internal/config/load.go+1−1 internal/config/provider.go+2 internal/llm/tools/diagnostics.go+15−3 internal/lsp/client.go+11−2 internal/lsp/protocol/pattern_interfaces.go+18−2 internal/lsp/protocol/uri.go+18−7 internal/lsp/util/edit.go+27−5 internal/lsp/watcher/watcher.go+12−2 internal/tui/page/chat/chat.go+1−1
.goreleaser.yml+27
@@ -110,6 +110,7 @@ homebrew_casks:
110110 - repository:
111111 owner: charmbracelet
112112 name: homebrew-tap
113+ token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
113114
114115npms:
115116 - name: "@charmland/crush"
@@ -134,6 +135,32 @@ nfpms:
134135 - src: ./manpages/crush.1.gz
135136 dst: /usr/share/man/man1/crush.1.gz
136137
138+nix:
139+ - repository:
140+ owner: "charmbracelet"
141+ name: nur
142+ token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
143+ extra_install: |-
144+ installManPage ./manpages/crush.1.gz.
145+ installShellCompletion ./completions/*
146+
147+winget:
148+ - publisher: charmbracelet
149+ copyright: Charmbracelet, Inc
150+ repository:
151+ owner: "charmbracelet"
152+ name: winget-pkgs
153+ token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
154+ branch: "crush-{{.Version}}"
155+ pull_request:
156+ enabled: true
157+ draft: false
158+ check_boxes: true
159+ base:
160+ owner: microsoft
161+ name: winget-pkgs
162+ branch: master
163+
137164changelog:
138165 sort: asc
139166 disable: "{{ .IsNightly }}"
internal/config/load.go+1−1
@@ -17,7 +17,7 @@ import (
1717 "github.com/charmbracelet/crush/internal/log"
1818)
1919
20−const catwalkURL = "https://catwalk.charm.sh"
20+const defaultCatwalkURL = "https://catwalk.charm.sh"
2121
2222// LoadReader config via io.Reader.
2323func LoadReader(fd io.Reader) (*Config, error) {
internal/config/provider.go+2
@@ -1,6 +1,7 @@
11package config
22
33import (
4+ "cmp"
45 "encoding/json"
56 "fmt"
67 "log/slog"
@@ -74,6 +75,7 @@ func loadProvidersFromCache(path string) ([]catwalk.Provider, error) {
7475}
7576
7677func Providers() ([]catwalk.Provider, error) {
78+ catwalkURL := cmp.Or(os.Getenv("CATWALK_URL"), defaultCatwalkURL)
7779 client := catwalk.NewWithURL(catwalkURL)
7880 path := providerCacheFileData()
7981 return loadProvidersOnce(client, path)
internal/llm/tools/diagnostics.go+15−3
@@ -4,6 +4,7 @@ import (
44 "context"
55 "encoding/json"
66 "fmt"
7+ "log/slog"
78 "maps"
89 "sort"
910 "strings"
@@ -118,7 +119,13 @@ func waitForLspDiagnostics(ctx context.Context, filePath string, lsps map[string
118119 return
119120 }
120121
121− if diagParams.URI.Path() == filePath || hasDiagnosticsChanged(client.GetDiagnostics(), originalDiags) {
122+ path, err := diagParams.URI.Path()
123+ if err != nil {
124+ slog.Error("Failed to convert diagnostic URI to path", "uri", diagParams.URI, "error", err)
125+ return
126+ }
127+
128+ if path == filePath || hasDiagnosticsChanged(client.GetDiagnostics(), originalDiags) {
122129 select {
123130 case diagChan <- struct{}{}:
124131 default:
@@ -216,10 +223,15 @@ func getDiagnostics(filePath string, lsps map[string]*lsp.Client) string {
216223 diagnostics := client.GetDiagnostics()
217224 if len(diagnostics) > 0 {
218225 for location, diags := range diagnostics {
219− isCurrentFile := location.Path() == filePath
226+ path, err := location.Path()
227+ if err != nil {
228+ slog.Error("Failed to convert diagnostic location URI to path", "uri", location, "error", err)
229+ continue
230+ }
231+ isCurrentFile := path == filePath
220232
221233 for _, diag := range diags {
222− formattedDiag := formatDiagnostic(location.Path(), diag, lspName)
234+ formattedDiag := formatDiagnostic(path, diag, lspName)
223235
224236 if isCurrentFile {
225237 fileDiagnostics = append(fileDiagnostics, formattedDiag)
internal/lsp/client.go+11−2
@@ -449,7 +449,12 @@ func (c *Client) pingTypeScriptServer(ctx context.Context) error {
449449
450450 // If we have any open files, try to get document symbols for one
451451 for uri := range c.openFiles {
452− filePath := protocol.DocumentURI(uri).Path()
452+ filePath, err := protocol.DocumentURI(uri).Path()
453+ if err != nil {
454+ slog.Error("Failed to convert URI to path for TypeScript symbol collection", "uri", uri, "error", err)
455+ continue
456+ }
457+
453458 if strings.HasSuffix(filePath, ".ts") || strings.HasSuffix(filePath, ".js") ||
454459 strings.HasSuffix(filePath, ".tsx") || strings.HasSuffix(filePath, ".jsx") {
455460 var symbols []protocol.DocumentSymbol
@@ -712,7 +717,11 @@ func (c *Client) CloseAllFiles(ctx context.Context) {
712717 // First collect all URIs that need to be closed
713718 for uri := range c.openFiles {
714719 // Convert URI back to file path using proper URI handling
715− filePath := protocol.DocumentURI(uri).Path()
720+ filePath, err := protocol.DocumentURI(uri).Path()
721+ if err != nil {
722+ slog.Error("Failed to convert URI to path for file closing", "uri", uri, "error", err)
723+ continue
724+ }
716725 filesToClose = append(filesToClose, filePath)
717726 }
718727 c.openFilesMu.Unlock()
internal/lsp/protocol/pattern_interfaces.go+18−2
@@ -2,6 +2,7 @@ package protocol
22
33import (
44 "fmt"
5+ "log/slog"
56)
67
78// PatternInfo is an interface for types that represent glob patterns
@@ -36,21 +37,36 @@ func (g *GlobPattern) AsPattern() (PatternInfo, error) {
3637 return nil, fmt.Errorf("nil pattern")
3738 }
3839
40+ var err error
41+
3942 switch v := g.Value.(type) {
4043 case string:
4144 return StringPattern{Pattern: v}, nil
45+
4246 case RelativePattern:
4347 // Handle BaseURI which could be string or DocumentUri
4448 basePath := ""
4549 switch baseURI := v.BaseURI.Value.(type) {
4650 case string:
47− basePath = DocumentURI(baseURI).Path()
51+ basePath, err = DocumentURI(baseURI).Path()
52+ if err != nil {
53+ slog.Error("Failed to convert URI to path", "uri", baseURI, "error", err)
54+ return nil, fmt.Errorf("invalid URI: %s", baseURI)
55+ }
56+
4857 case DocumentURI:
49− basePath = baseURI.Path()
58+ basePath, err = baseURI.Path()
59+ if err != nil {
60+ slog.Error("Failed to convert DocumentURI to path", "uri", baseURI, "error", err)
61+ return nil, fmt.Errorf("invalid DocumentURI: %s", baseURI)
62+ }
63+
5064 default:
5165 return nil, fmt.Errorf("unknown BaseURI type: %T", v.BaseURI.Value)
5266 }
67+
5368 return RelativePatternInfo{RP: v, BasePath: basePath}, nil
69+
5470 default:
5571 return nil, fmt.Errorf("unknown pattern type: %T", g.Value)
5672 }
internal/lsp/protocol/uri.go+18−7
@@ -70,7 +70,7 @@ func (uri *DocumentURI) UnmarshalText(data []byte) (err error) {
7070// DocumentUri("").Path() returns the empty string.
7171//
7272// Path panics if called on a URI that is not a valid filename.
73−func (uri DocumentURI) Path() string {
73+func (uri DocumentURI) Path() (string, error) {
7474 filename, err := filename(uri)
7575 if err != nil {
7676 // e.g. ParseRequestURI failed.
@@ -79,22 +79,33 @@ func (uri DocumentURI) Path() string {
7979 // direct string manipulation; all DocumentUris
8080 // received from the client pass through
8181 // ParseRequestURI, which ensures validity.
82− panic(err)
82+ return "", fmt.Errorf("invalid URI %q: %w", uri, err)
8383 }
84− return filepath.FromSlash(filename)
84+ return filepath.FromSlash(filename), nil
8585}
8686
8787// Dir returns the URI for the directory containing the receiver.
88−func (uri DocumentURI) Dir() DocumentURI {
88+func (uri DocumentURI) Dir() (DocumentURI, error) {
89+ // XXX: Legacy comment:
8990 // This function could be more efficiently implemented by avoiding any call
9091 // to Path(), but at least consolidates URI manipulation.
91− return URIFromPath(uri.DirPath())
92+
93+ path, err := uri.DirPath()
94+ if err != nil {
95+ return "", fmt.Errorf("invalid URI %q: %w", uri, err)
96+ }
97+
98+ return URIFromPath(path), nil
9299}
93100
94101// DirPath returns the file path to the directory containing this URI, which
95102// must be a file URI.
96−func (uri DocumentURI) DirPath() string {
97− return filepath.Dir(uri.Path())
103+func (uri DocumentURI) DirPath() (string, error) {
104+ path, err := uri.Path()
105+ if err != nil {
106+ return "", err
107+ }
108+ return filepath.Dir(path), nil
98109}
99110
100111func filename(uri DocumentURI) (string, error) {
internal/lsp/util/edit.go+27−5
@@ -11,7 +11,10 @@ import (
1111)
1212
1313func applyTextEdits(uri protocol.DocumentURI, edits []protocol.TextEdit) error {
14− path := uri.Path()
14+ path, err := uri.Path()
15+ if err != nil {
16+ return fmt.Errorf("invalid URI: %w", err)
17+ }
1518
1619 // Read the file content
1720 content, err := os.ReadFile(path)
@@ -148,7 +151,11 @@ func applyTextEdit(lines []string, edit protocol.TextEdit) ([]string, error) {
148151// applyDocumentChange applies a DocumentChange (create/rename/delete operations)
149152func applyDocumentChange(change protocol.DocumentChange) error {
150153 if change.CreateFile != nil {
151− path := change.CreateFile.URI.Path()
154+ path, err := change.CreateFile.URI.Path()
155+ if err != nil {
156+ return fmt.Errorf("invalid URI: %w", err)
157+ }
158+
152159 if change.CreateFile.Options != nil {
153160 if change.CreateFile.Options.Overwrite {
154161 // Proceed with overwrite
@@ -164,7 +171,11 @@ func applyDocumentChange(change protocol.DocumentChange) error {
164171 }
165172
166173 if change.DeleteFile != nil {
167− path := change.DeleteFile.URI.Path()
174+ path, err := change.DeleteFile.URI.Path()
175+ if err != nil {
176+ return fmt.Errorf("invalid URI: %w", err)
177+ }
178+
168179 if change.DeleteFile.Options != nil && change.DeleteFile.Options.Recursive {
169180 if err := os.RemoveAll(path); err != nil {
170181 return fmt.Errorf("failed to delete directory recursively: %w", err)
@@ -177,8 +188,19 @@ func applyDocumentChange(change protocol.DocumentChange) error {
177188 }
178189
179190 if change.RenameFile != nil {
180− oldPath := change.RenameFile.OldURI.Path()
181− newPath := change.RenameFile.NewURI.Path()
191+ var newPath, oldPath string
192+ var err error
193+
194+ oldPath, err = change.RenameFile.OldURI.Path()
195+ if err != nil {
196+ return err
197+ }
198+
199+ newPath, err = change.RenameFile.NewURI.Path()
200+ if err != nil {
201+ return err
202+ }
203+
182204 if change.RenameFile.Options != nil {
183205 if !change.RenameFile.Options.Overwrite {
184206 if _, err := os.Stat(newPath); err == nil {
internal/lsp/watcher/watcher.go+12−2
@@ -617,7 +617,11 @@ func (w *WorkspaceWatcher) matchesPattern(path string, pattern protocol.GlobPatt
617617 return false
618618 }
619619 // For relative patterns
620− basePath = protocol.DocumentURI(basePath).Path()
620+ if basePath, err = protocol.DocumentURI(basePath).Path(); err != nil {
621+ // XXX: Do we want to return here, or send the error up the stack?
622+ slog.Error("Error converting base path to URI", "basePath", basePath, "error", err)
623+ }
624+
621625 basePath = filepath.ToSlash(basePath)
622626
623627 // Make path relative to basePath for matching
@@ -660,7 +664,13 @@ func (w *WorkspaceWatcher) debounceHandleFileEvent(ctx context.Context, uri stri
660664// handleFileEvent sends file change notifications
661665func (w *WorkspaceWatcher) handleFileEvent(ctx context.Context, uri string, changeType protocol.FileChangeType) {
662666 // If the file is open and it's a change event, use didChange notification
663− filePath := protocol.DocumentURI(uri).Path()
667+ filePath, err := protocol.DocumentURI(uri).Path()
668+ if err != nil {
669+ // XXX: Do we want to return here, or send the error up the stack?
670+ slog.Error("Error converting URI to path", "uri", uri, "error", err)
671+ return
672+ }
673+
664674 if changeType == protocol.FileChangeType(protocol.Deleted) {
665675 w.client.ClearDiagnosticsForURI(protocol.DocumentURI(uri))
666676 } else if changeType == protocol.FileChangeType(protocol.Changed) && w.client.IsFileOpen(filePath) {
internal/tui/page/chat/chat.go+1−1
@@ -843,7 +843,7 @@ func (p *chatPage) Help() help.KeyMap {
843843 ),
844844 key.NewBinding(
845845 key.WithKeys("g", "home"),
846− key.WithHelp("g", "hone"),
846+ key.WithHelp("g", "home"),
847847 ),
848848 key.NewBinding(
849849 key.WithKeys("G", "end"),