summaryrefslogtreecommitdiffhomepage
path: root/internal/app
diff options
context:
space:
mode:
authorKujtim Hoxha <[email protected]>2025-04-21 19:59:35 +0200
committerGitHub <[email protected]>2025-04-21 19:59:35 +0200
commitf33dff87725764af0b675b5e5b2e011b21c14c90 (patch)
tree4fe2c022305f13775f2cab3cdd80cd808259765b /internal/app
parent6b1c64bcc75b89c530294b6a2d4404682b435d56 (diff)
parent3a6a26981a8074b6ab0eaadb520db986e04799ff (diff)
downloadopencode-f33dff87725764af0b675b5e5b2e011b21c14c90.tar.gz
opencode-f33dff87725764af0b675b5e5b2e011b21c14c90.zip
Merge pull request #27 from kujtimiihoxha/opencode
OpenCode - Initial Implementation
Diffstat (limited to 'internal/app')
-rw-r--r--internal/app/app.go99
-rw-r--r--internal/app/lsp.go126
-rw-r--r--internal/app/services.go60
3 files changed, 225 insertions, 60 deletions
diff --git a/internal/app/app.go b/internal/app/app.go
new file mode 100644
index 000000000..36b1ca16f
--- /dev/null
+++ b/internal/app/app.go
@@ -0,0 +1,99 @@
+package app
+
+import (
+ "context"
+ "database/sql"
+ "maps"
+ "sync"
+ "time"
+
+ "github.com/kujtimiihoxha/opencode/internal/config"
+ "github.com/kujtimiihoxha/opencode/internal/db"
+ "github.com/kujtimiihoxha/opencode/internal/history"
+ "github.com/kujtimiihoxha/opencode/internal/llm/agent"
+ "github.com/kujtimiihoxha/opencode/internal/logging"
+ "github.com/kujtimiihoxha/opencode/internal/lsp"
+ "github.com/kujtimiihoxha/opencode/internal/message"
+ "github.com/kujtimiihoxha/opencode/internal/permission"
+ "github.com/kujtimiihoxha/opencode/internal/session"
+)
+
+type App struct {
+ Sessions session.Service
+ Messages message.Service
+ History history.Service
+ Permissions permission.Service
+
+ CoderAgent agent.Service
+
+ LSPClients map[string]*lsp.Client
+
+ clientsMutex sync.RWMutex
+
+ watcherCancelFuncs []context.CancelFunc
+ cancelFuncsMutex sync.Mutex
+ watcherWG sync.WaitGroup
+}
+
+func New(ctx context.Context, conn *sql.DB) (*App, error) {
+ q := db.New(conn)
+ sessions := session.NewService(q)
+ messages := message.NewService(q)
+ files := history.NewService(q, conn)
+
+ app := &App{
+ Sessions: sessions,
+ Messages: messages,
+ History: files,
+ Permissions: permission.NewPermissionService(),
+ LSPClients: make(map[string]*lsp.Client),
+ }
+
+ // Initialize LSP clients in the background
+ go app.initLSPClients(ctx)
+
+ var err error
+ app.CoderAgent, err = agent.NewAgent(
+ config.AgentCoder,
+ app.Sessions,
+ app.Messages,
+ agent.CoderAgentTools(
+ app.Permissions,
+ app.Sessions,
+ app.Messages,
+ app.History,
+ app.LSPClients,
+ ),
+ )
+ if err != nil {
+ logging.Error("Failed to create coder agent", err)
+ return nil, err
+ }
+
+ return app, nil
+}
+
+// Shutdown performs a clean shutdown of the application
+func (app *App) Shutdown() {
+ // Cancel all watcher goroutines
+ app.cancelFuncsMutex.Lock()
+ for _, cancel := range app.watcherCancelFuncs {
+ cancel()
+ }
+ app.cancelFuncsMutex.Unlock()
+ app.watcherWG.Wait()
+
+ // Perform additional cleanup for LSP clients
+ app.clientsMutex.RLock()
+ clients := make(map[string]*lsp.Client, len(app.LSPClients))
+ maps.Copy(clients, app.LSPClients)
+ app.clientsMutex.RUnlock()
+
+ for name, client := range clients {
+ shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ if err := client.Shutdown(shutdownCtx); err != nil {
+ logging.Error("Failed to shutdown LSP client", "name", name, "error", err)
+ }
+ cancel()
+ }
+}
diff --git a/internal/app/lsp.go b/internal/app/lsp.go
new file mode 100644
index 000000000..77feeb943
--- /dev/null
+++ b/internal/app/lsp.go
@@ -0,0 +1,126 @@
+package app
+
+import (
+ "context"
+ "time"
+
+ "github.com/kujtimiihoxha/opencode/internal/config"
+ "github.com/kujtimiihoxha/opencode/internal/logging"
+ "github.com/kujtimiihoxha/opencode/internal/lsp"
+ "github.com/kujtimiihoxha/opencode/internal/lsp/watcher"
+)
+
+func (app *App) initLSPClients(ctx context.Context) {
+ cfg := config.Get()
+
+ // Initialize LSP clients
+ for name, clientConfig := range cfg.LSP {
+ // Start each client initialization in its own goroutine
+ go app.createAndStartLSPClient(ctx, name, clientConfig.Command, clientConfig.Args...)
+ }
+ logging.Info("LSP clients initialization started in background")
+}
+
+// createAndStartLSPClient creates a new LSP client, initializes it, and starts its workspace watcher
+func (app *App) createAndStartLSPClient(ctx context.Context, name string, command string, args ...string) {
+ // Create a specific context for initialization with a timeout
+ logging.Info("Creating LSP client", "name", name, "command", command, "args", args)
+
+ // Create the LSP client
+ lspClient, err := lsp.NewClient(ctx, command, args...)
+ if err != nil {
+ logging.Error("Failed to create LSP client for", name, err)
+ return
+ }
+
+ // Create a longer timeout for initialization (some servers take time to start)
+ initCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
+ defer cancel()
+
+ // Initialize with the initialization context
+ _, err = lspClient.InitializeLSPClient(initCtx, config.WorkingDirectory())
+ if err != nil {
+ logging.Error("Initialize failed", "name", name, "error", err)
+ // Clean up the client to prevent resource leaks
+ lspClient.Close()
+ return
+ }
+
+ // Wait for the server to be ready
+ if err := lspClient.WaitForServerReady(initCtx); err != nil {
+ logging.Error("Server failed to become ready", "name", name, "error", err)
+ // We'll continue anyway, as some functionality might still work
+ lspClient.SetServerState(lsp.StateError)
+ } else {
+ logging.Info("LSP server is ready", "name", name)
+ lspClient.SetServerState(lsp.StateReady)
+ }
+
+ logging.Info("LSP client initialized", "name", name)
+
+ // Create a child context that can be canceled when the app is shutting down
+ watchCtx, cancelFunc := context.WithCancel(ctx)
+
+ // Create a context with the server name for better identification
+ watchCtx = context.WithValue(watchCtx, "serverName", name)
+
+ // Create the workspace watcher
+ workspaceWatcher := watcher.NewWorkspaceWatcher(lspClient)
+
+ // Store the cancel function to be called during cleanup
+ app.cancelFuncsMutex.Lock()
+ app.watcherCancelFuncs = append(app.watcherCancelFuncs, cancelFunc)
+ app.cancelFuncsMutex.Unlock()
+
+ // Add the watcher to a WaitGroup to track active goroutines
+ app.watcherWG.Add(1)
+
+ // Add to map with mutex protection before starting goroutine
+ app.clientsMutex.Lock()
+ app.LSPClients[name] = lspClient
+ app.clientsMutex.Unlock()
+
+ go app.runWorkspaceWatcher(watchCtx, name, workspaceWatcher)
+}
+
+// runWorkspaceWatcher executes the workspace watcher for an LSP client
+func (app *App) runWorkspaceWatcher(ctx context.Context, name string, workspaceWatcher *watcher.WorkspaceWatcher) {
+ defer app.watcherWG.Done()
+ defer logging.RecoverPanic("LSP-"+name, func() {
+ // Try to restart the client
+ app.restartLSPClient(ctx, name)
+ })
+
+ workspaceWatcher.WatchWorkspace(ctx, config.WorkingDirectory())
+ logging.Info("Workspace watcher stopped", "client", name)
+}
+
+// restartLSPClient attempts to restart a crashed or failed LSP client
+func (app *App) restartLSPClient(ctx context.Context, name string) {
+ // Get the original configuration
+ cfg := config.Get()
+ clientConfig, exists := cfg.LSP[name]
+ if !exists {
+ logging.Error("Cannot restart client, configuration not found", "client", name)
+ return
+ }
+
+ // Clean up the old client if it exists
+ app.clientsMutex.Lock()
+ oldClient, exists := app.LSPClients[name]
+ if exists {
+ delete(app.LSPClients, name) // Remove from map before potentially slow shutdown
+ }
+ app.clientsMutex.Unlock()
+
+ if exists && oldClient != nil {
+ // Try to shut it down gracefully, but don't block on errors
+ shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ _ = oldClient.Shutdown(shutdownCtx)
+ cancel()
+ }
+
+ // Create a new client using the shared function
+ app.createAndStartLSPClient(ctx, name, clientConfig.Command, clientConfig.Args...)
+ logging.Info("Successfully restarted LSP client", "client", name)
+}
diff --git a/internal/app/services.go b/internal/app/services.go
deleted file mode 100644
index 76b2226ae..000000000
--- a/internal/app/services.go
+++ /dev/null
@@ -1,60 +0,0 @@
-package app
-
-import (
- "context"
- "database/sql"
-
- "github.com/kujtimiihoxha/termai/internal/config"
- "github.com/kujtimiihoxha/termai/internal/db"
- "github.com/kujtimiihoxha/termai/internal/logging"
- "github.com/kujtimiihoxha/termai/internal/lsp"
- "github.com/kujtimiihoxha/termai/internal/lsp/watcher"
- "github.com/kujtimiihoxha/termai/internal/message"
- "github.com/kujtimiihoxha/termai/internal/permission"
- "github.com/kujtimiihoxha/termai/internal/session"
-)
-
-type App struct {
- Context context.Context
-
- Sessions session.Service
- Messages message.Service
- Permissions permission.Service
-
- LSPClients map[string]*lsp.Client
-}
-
-func New(ctx context.Context, conn *sql.DB) *App {
- cfg := config.Get()
- logging.Info("Debug mode enabled")
-
- q := db.New(conn)
- sessions := session.NewService(ctx, q)
- messages := message.NewService(ctx, q)
-
- app := &App{
- Context: ctx,
- Sessions: sessions,
- Messages: messages,
- Permissions: permission.NewPermissionService(),
- LSPClients: make(map[string]*lsp.Client),
- }
-
- for name, client := range cfg.LSP {
- lspClient, err := lsp.NewClient(ctx, client.Command, client.Args...)
- workspaceWatcher := watcher.NewWorkspaceWatcher(lspClient)
- if err != nil {
- logging.Error("Failed to create LSP client for", name, err)
- continue
- }
-
- _, err = lspClient.InitializeLSPClient(ctx, config.WorkingDirectory())
- if err != nil {
- logging.Error("Initialize failed", "error", err)
- continue
- }
- go workspaceWatcher.WatchWorkspace(ctx, config.WorkingDirectory())
- app.LSPClients[name] = lspClient
- }
- return app
-}