diff --git a/internal/app/app.go b/internal/app/app.go index 249b4a392a496c127c8adf03436f83b1372de1d5..dc75f19f30ee0a4b8dee37f5d58102fc5e1a7c11 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -35,6 +35,9 @@ type App struct { LSPClients *csync.Map[string, *lsp.Client] + lspStates *csync.Map[string, LSPClientInfo] + lspBroker *pubsub.Broker[LSPEvent] + config *config.Config serviceEventsWG *sync.WaitGroup @@ -65,6 +68,8 @@ func New(ctx context.Context, conn *sql.DB, cfg *config.Config) (*App, error) { History: files, Permissions: permission.NewPermissionService(cfg.WorkingDir(), skipPermissionsRequests, allowedTools), LSPClients: csync.NewMap[string, *lsp.Client](), + lspStates: csync.NewMap[string, LSPClientInfo](), + lspBroker: pubsub.NewBroker[LSPEvent](), globalCtx: ctx, @@ -220,7 +225,7 @@ func (app *App) setupEvents() { setupSubscriber(ctx, app.serviceEventsWG, "permissions-notifications", app.Permissions.SubscribeNotifications, app.events) setupSubscriber(ctx, app.serviceEventsWG, "history", app.History.Subscribe, app.events) setupSubscriber(ctx, app.serviceEventsWG, "mcp", agent.SubscribeMCPEvents, app.events) - setupSubscriber(ctx, app.serviceEventsWG, "lsp", SubscribeLSPEvents, app.events) + setupSubscriber(ctx, app.serviceEventsWG, "lsp", app.SubscribeLSPEvents, app.events) cleanupFunc := func() error { cancel() app.serviceEventsWG.Wait() diff --git a/internal/app/lsp.go b/internal/app/lsp.go index 057e9ce39363f3fd68c8c980ce22e3e8b0e78154..07db7f3420a5e30172da0698d95cabc307998ee2 100644 --- a/internal/app/lsp.go +++ b/internal/app/lsp.go @@ -28,23 +28,23 @@ func (app *App) createAndStartLSPClient(ctx context.Context, name string, config // Check if any root markers exist in the working directory (config now has defaults) if !lsp.HasRootMarkers(app.config.WorkingDir(), config.RootMarkers) { slog.Info("Skipping LSP client - no root markers found", "name", name, "rootMarkers", config.RootMarkers) - updateLSPState(name, lsp.StateDisabled, nil, nil, 0) + app.updateLSPState(name, lsp.StateDisabled, nil, 0) return } // Update state to starting - updateLSPState(name, lsp.StateStarting, nil, nil, 0) + app.updateLSPState(name, lsp.StateStarting, nil, 0) // Create LSP client. lspClient, err := lsp.New(ctx, name, config) if err != nil { slog.Error("Failed to create LSP client for", name, err) - updateLSPState(name, lsp.StateError, err, nil, 0) + app.updateLSPState(name, lsp.StateError, err, 0) return } // Set diagnostics callback - lspClient.SetDiagnosticsCallback(updateLSPDiagnostics) + lspClient.SetDiagnosticsCallback(app.updateLSPDiagnostics) // Increase initialization timeout as some servers take more time to start. initCtx, cancel := context.WithTimeout(ctx, 30*time.Second) @@ -54,7 +54,7 @@ func (app *App) createAndStartLSPClient(ctx context.Context, name string, config _, err = lspClient.Initialize(initCtx, app.config.WorkingDir()) if err != nil { slog.Error("Initialize failed", "name", name, "error", err) - updateLSPState(name, lsp.StateError, err, lspClient, 0) + app.updateLSPState(name, lsp.StateError, err, 0) lspClient.Close(ctx) return } @@ -65,12 +65,12 @@ func (app *App) createAndStartLSPClient(ctx context.Context, name string, config // Server never reached a ready state, but let's continue anyway, as // some functionality might still work. lspClient.SetServerState(lsp.StateError) - updateLSPState(name, lsp.StateError, err, lspClient, 0) + app.updateLSPState(name, lsp.StateError, err, 0) } else { // Server reached a ready state scuccessfully. slog.Info("LSP server is ready", "name", name) lspClient.SetServerState(lsp.StateReady) - updateLSPState(name, lsp.StateReady, nil, lspClient, 0) + app.updateLSPState(name, lsp.StateReady, nil, 0) } slog.Info("LSP client initialized", "name", name) diff --git a/internal/app/lsp_events.go b/internal/app/lsp_events.go index 8877a02a1a623af9339e660d5710881beefc75cf..9338357e8facd1ff14fdedaf16315aa7e99dd82a 100644 --- a/internal/app/lsp_events.go +++ b/internal/app/lsp_events.go @@ -5,7 +5,6 @@ import ( "maps" "time" - "github.com/charmbracelet/crush/internal/csync" "github.com/charmbracelet/crush/internal/lsp" "github.com/charmbracelet/crush/internal/pubsub" ) @@ -38,50 +37,43 @@ type LSPEvent struct { // LSPClientInfo holds information about an LSP client's state type LSPClientInfo struct { - Name string - State lsp.ServerState - Error error - Client *lsp.Client - DiagnosticCount int - ConnectedAt time.Time + Name string `json:"name"` + State lsp.ServerState `json:"state"` + Error error `json:"error,omitempty"` + DiagnosticCount int `json:"diagnostic_count,omitempty"` + ConnectedAt time.Time `json:"connected_at"` } -var ( - lspStates = csync.NewMap[string, LSPClientInfo]() - lspBroker = pubsub.NewBroker[LSPEvent]() -) - // SubscribeLSPEvents returns a channel for LSP events -func SubscribeLSPEvents(ctx context.Context) <-chan pubsub.Event[LSPEvent] { - return lspBroker.Subscribe(ctx) +func (a *App) SubscribeLSPEvents(ctx context.Context) <-chan pubsub.Event[LSPEvent] { + return a.lspBroker.Subscribe(ctx) } // GetLSPStates returns the current state of all LSP clients -func GetLSPStates() map[string]LSPClientInfo { - return maps.Collect(lspStates.Seq2()) +func (a *App) GetLSPStates() map[string]LSPClientInfo { + return maps.Collect(a.lspStates.Seq2()) } // GetLSPState returns the state of a specific LSP client -func GetLSPState(name string) (LSPClientInfo, bool) { - return lspStates.Get(name) +func (a *App) GetLSPState(name string) (LSPClientInfo, bool) { + return a.lspStates.Get(name) } // updateLSPState updates the state of an LSP client and publishes an event -func updateLSPState(name string, state lsp.ServerState, err error, client *lsp.Client, diagnosticCount int) { +func (a *App) updateLSPState(name string, state lsp.ServerState, err error, diagnosticCount int) { info := LSPClientInfo{ Name: name, State: state, Error: err, - Client: client, DiagnosticCount: diagnosticCount, } if state == lsp.StateReady { info.ConnectedAt = time.Now() } - lspStates.Set(name, info) + a.lspStates.Set(name, info) // Publish state change event - lspBroker.Publish(pubsub.UpdatedEvent, LSPEvent{ + a.lspBroker.Publish(pubsub.UpdatedEvent, LSPEvent{ Type: LSPEventStateChanged, Name: name, State: state, @@ -91,13 +83,13 @@ func updateLSPState(name string, state lsp.ServerState, err error, client *lsp.C } // updateLSPDiagnostics updates the diagnostic count for an LSP client and publishes an event -func updateLSPDiagnostics(name string, diagnosticCount int) { - if info, exists := lspStates.Get(name); exists { +func (a *App) updateLSPDiagnostics(name string, diagnosticCount int) { + if info, exists := a.lspStates.Get(name); exists { info.DiagnosticCount = diagnosticCount - lspStates.Set(name, info) + a.lspStates.Set(name, info) // Publish diagnostics change event - lspBroker.Publish(pubsub.UpdatedEvent, LSPEvent{ + a.lspBroker.Publish(pubsub.UpdatedEvent, LSPEvent{ Type: LSPEventDiagnosticsChanged, Name: name, State: info.State, diff --git a/internal/message/attachment.go b/internal/message/attachment.go index 6e89f001436ed120d52c08c05ade8c8a741cfb7a..9f6e64172c6a8912694d50e4eb029e1f8d54a3a9 100644 --- a/internal/message/attachment.go +++ b/internal/message/attachment.go @@ -1,8 +1,47 @@ package message +import ( + "encoding/base64" + "encoding/json" +) + type Attachment struct { - FilePath string - FileName string - MimeType string - Content []byte + FilePath string `json:"file_path"` + FileName string `json:"file_name"` + MimeType string `json:"mime_type"` + Content []byte `json:"content"` +} + +// MarshalJSON implements the [json.Marshaler] interface. +func (a Attachment) MarshalJSON() ([]byte, error) { + // Encode the content as a base64 string + type Alias Attachment + return json.Marshal(&struct { + Content string `json:"content"` + *Alias + }{ + Content: base64.StdEncoding.EncodeToString(a.Content), + Alias: (*Alias)(&a), + }) +} + +// UnmarshalJSON implements the [json.Unmarshaler] interface. +func (a *Attachment) UnmarshalJSON(data []byte) error { + // Decode the content from a base64 string + type Alias Attachment + aux := &struct { + Content string `json:"content"` + *Alias + }{ + Alias: (*Alias)(a), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + content, err := base64.StdEncoding.DecodeString(aux.Content) + if err != nil { + return err + } + a.Content = content + return nil }