Detailed changes
@@ -257,6 +257,7 @@ func setupSubscriptions(app *app.App, parentCtx context.Context) (chan tea.Msg,
setupSubscriber(ctx, &wg, "messages", app.Messages.Subscribe, ch)
setupSubscriber(ctx, &wg, "permissions", app.Permissions.Subscribe, ch)
setupSubscriber(ctx, &wg, "coderAgent", app.CoderAgent.Subscribe, ch)
+ setupSubscriber(ctx, &wg, "lspSetup", app.LSPSetup.Subscribe, ch)
cleanupFunc := func() {
logging.Info("Cancelling all subscriptions")
@@ -16,6 +16,7 @@ import (
"github.com/opencode-ai/opencode/internal/llm/agent"
"github.com/opencode-ai/opencode/internal/logging"
"github.com/opencode-ai/opencode/internal/lsp"
+ "github.com/opencode-ai/opencode/internal/lsp/setup"
"github.com/opencode-ai/opencode/internal/message"
"github.com/opencode-ai/opencode/internal/permission"
"github.com/opencode-ai/opencode/internal/session"
@@ -27,6 +28,7 @@ type App struct {
Messages message.Service
History history.Service
Permissions permission.Service
+ LSPSetup setup.Service
CoderAgent agent.Service
@@ -47,12 +49,14 @@ func New(ctx context.Context, conn *sql.DB) (*App, error) {
sessions := session.NewService(q)
messages := message.NewService(q)
files := history.NewService(q, conn)
+ lspSetup := setup.NewService()
app := &App{
Sessions: sessions,
Messages: messages,
History: files,
Permissions: permission.NewPermissionService(),
+ LSPSetup: lspSetup,
LSPClients: make(map[string]*lsp.Client),
}
@@ -7,6 +7,8 @@ import (
"github.com/opencode-ai/opencode/internal/config"
"github.com/opencode-ai/opencode/internal/logging"
"github.com/opencode-ai/opencode/internal/lsp"
+ "github.com/opencode-ai/opencode/internal/lsp/protocol"
+ "github.com/opencode-ai/opencode/internal/lsp/setup"
"github.com/opencode-ai/opencode/internal/lsp/watcher"
)
@@ -34,6 +36,22 @@ func (app *App) CheckAndSetupLSP(ctx context.Context) bool {
return true
}
+// ConfigureLSP configures LSP with the provided servers
+func (app *App) ConfigureLSP(ctx context.Context, servers map[protocol.LanguageKind]setup.LSPServerInfo) error {
+ // Save the configuration using the LSP setup service
+ err := app.LSPSetup.SaveConfiguration(ctx, servers)
+ if err != nil {
+ logging.Error("Failed to save LSP configuration", err)
+ return err
+ }
+
+ // Initialize LSP clients with the new configuration
+ app.InitLSPClients(ctx)
+
+ logging.Info("LSP configuration updated successfully")
+ return nil
+}
+
// 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) {
logging.Info("Creating LSP client", "name", name, "command", command, "args", args)
@@ -5,10 +5,10 @@ import (
"fmt"
"io"
"os/exec"
- "runtime"
"strings"
- "github.com/opencode-ai/opencode/internal/logging"
+ "github.com/opencode-ai/opencode/internal/config"
+ "github.com/opencode-ai/opencode/internal/lsp/protocol"
)
// InstallationResult represents the result of an LSP server installation
@@ -121,197 +121,21 @@ func VerifyInstallation(serverName string) bool {
return err == nil
}
-// GetPackageManager returns the appropriate package manager command for the current OS
-func GetPackageManager() string {
- switch runtime.GOOS {
- case "darwin":
- // Check for Homebrew
- if _, err := exec.LookPath("brew"); err == nil {
- return "brew"
- }
- // Check for MacPorts
- if _, err := exec.LookPath("port"); err == nil {
- return "port"
- }
- case "linux":
- // Check for apt (Debian/Ubuntu)
- if _, err := exec.LookPath("apt"); err == nil {
- return "apt"
- }
- // Check for dnf (Fedora)
- if _, err := exec.LookPath("dnf"); err == nil {
- return "dnf"
- }
- // Check for yum (CentOS/RHEL)
- if _, err := exec.LookPath("yum"); err == nil {
- return "yum"
- }
- // Check for pacman (Arch)
- if _, err := exec.LookPath("pacman"); err == nil {
- return "pacman"
- }
- // Check for zypper (openSUSE)
- if _, err := exec.LookPath("zypper"); err == nil {
- return "zypper"
- }
- case "windows":
- // Check for Chocolatey
- if _, err := exec.LookPath("choco"); err == nil {
- return "choco"
- }
- // Check for Scoop
- if _, err := exec.LookPath("scoop"); err == nil {
- return "scoop"
- }
- }
-
- return ""
-}
-
-// GetSystemInstallCommand returns the system-specific installation command for a package
-func GetSystemInstallCommand(packageName string) string {
- packageManager := GetPackageManager()
-
- switch packageManager {
- case "brew":
- return fmt.Sprintf("brew install %s", packageName)
- case "port":
- return fmt.Sprintf("sudo port install %s", packageName)
- case "apt":
- return fmt.Sprintf("sudo apt install -y %s", packageName)
- case "dnf":
- return fmt.Sprintf("sudo dnf install -y %s", packageName)
- case "yum":
- return fmt.Sprintf("sudo yum install -y %s", packageName)
- case "pacman":
- return fmt.Sprintf("sudo pacman -S --noconfirm %s", packageName)
- case "zypper":
- return fmt.Sprintf("sudo zypper install -y %s", packageName)
- case "choco":
- return fmt.Sprintf("choco install -y %s", packageName)
- case "scoop":
- return fmt.Sprintf("scoop install %s", packageName)
- }
-
- return ""
-}
-
-// InstallDependencies installs common dependencies for LSP servers
-func InstallDependencies(ctx context.Context) []InstallationResult {
- results := []InstallationResult{}
-
- // Check for Node.js and npm
- if _, err := exec.LookPath("node"); err != nil {
- // Node.js is not installed, try to install it
- cmd := GetSystemInstallCommand("nodejs")
- if cmd == "" {
- results = append(results, InstallationResult{
- ServerName: "nodejs",
- Success: false,
- Error: fmt.Errorf("Node.js is not installed and could not determine how to install it"),
- Output: "Please install Node.js manually: https://nodejs.org/",
- })
- } else {
- // Execute the installation command
- installCmd, installArgs := parseInstallCommand(cmd)
- execCmd := exec.CommandContext(ctx, installCmd, installArgs...)
-
- output, err := execCmd.CombinedOutput()
- if err != nil {
- results = append(results, InstallationResult{
- ServerName: "nodejs",
- Success: false,
- Error: fmt.Errorf("failed to install Node.js: %w", err),
- Output: string(output),
- })
- } else {
- results = append(results, InstallationResult{
- ServerName: "nodejs",
- Success: true,
- Output: string(output),
- })
- }
- }
- }
-
- // Check for Python and pip
- pythonCmd := "python3"
- if runtime.GOOS == "windows" {
- pythonCmd = "python"
- }
-
- if _, err := exec.LookPath(pythonCmd); err != nil {
- // Python is not installed, try to install it
- cmd := GetSystemInstallCommand("python3")
- if cmd == "" {
- results = append(results, InstallationResult{
- ServerName: "python",
- Success: false,
- Error: fmt.Errorf("python is not installed and could not determine how to install it"),
- Output: "Please install Python manually: https://www.python.org/",
- })
- } else {
- // Execute the installation command
- installCmd, installArgs := parseInstallCommand(cmd)
- execCmd := exec.CommandContext(ctx, installCmd, installArgs...)
-
- output, err := execCmd.CombinedOutput()
- if err != nil {
- results = append(results, InstallationResult{
- ServerName: "python",
- Success: false,
- Error: fmt.Errorf("failed to install Python: %w", err),
- Output: string(output),
- })
- } else {
- results = append(results, InstallationResult{
- ServerName: "python",
- Success: true,
- Output: string(output),
- })
- }
- }
- }
-
- // Check for Go
- if _, err := exec.LookPath("go"); err != nil {
- // Go is not installed, try to install it
- cmd := GetSystemInstallCommand("golang")
- if cmd == "" {
- results = append(results, InstallationResult{
- ServerName: "go",
- Success: false,
- Error: fmt.Errorf("go is not installed and could not determine how to install it"),
- Output: "Please install Go manually: https://golang.org/",
- })
- } else {
- // Execute the installation command
- installCmd, installArgs := parseInstallCommand(cmd)
- execCmd := exec.CommandContext(ctx, installCmd, installArgs...)
-
- output, err := execCmd.CombinedOutput()
- if err != nil {
- results = append(results, InstallationResult{
- ServerName: "go",
- Success: false,
- Error: fmt.Errorf("failed to install Go: %w", err),
- Output: string(output),
- })
- } else {
- results = append(results, InstallationResult{
- ServerName: "go",
- Success: true,
- Output: string(output),
- })
- }
+// UpdateLSPConfig updates the LSP configuration in the config file
+func UpdateLSPConfig(servers map[protocol.LanguageKind]LSPServerInfo) error {
+ // Create a map for the LSP configuration
+ lspConfig := make(map[string]config.LSPConfig)
+
+ for lang, server := range servers {
+ langStr := string(lang)
+
+ lspConfig[langStr] = config.LSPConfig{
+ Disabled: false,
+ Command: server.Command,
+ Args: server.Args,
+ Options: server.Options,
}
}
- return results
-}
-
-// UpdateLSPConfig updates the LSP configuration in the config file
-func UpdateLSPConfig(servers LSPServerMap) error {
- logging.Info("Updating LSP configuration with", len(servers), "servers")
- return nil
+ return config.SaveLocalLSPConfig(lspConfig)
}
@@ -0,0 +1,172 @@
+package setup
+
+import (
+ "context"
+
+ "github.com/opencode-ai/opencode/internal/lsp/protocol"
+ "github.com/opencode-ai/opencode/internal/pubsub"
+)
+
+// LSPSetupEvent represents an event related to LSP setup
+type LSPSetupEvent struct {
+ Type LSPSetupEventType
+ Language protocol.LanguageKind
+ ServerName string
+ Success bool
+ Error error
+ Description string
+}
+
+// LSPSetupEventType defines the type of LSP setup event
+type LSPSetupEventType string
+
+const (
+ // EventLanguageDetected is emitted when a language is detected in the workspace
+ EventLanguageDetected LSPSetupEventType = "language_detected"
+ // EventServerDiscovered is emitted when an LSP server is discovered
+ EventServerDiscovered LSPSetupEventType = "server_discovered"
+ // EventServerInstalled is emitted when an LSP server is installed
+ EventServerInstalled LSPSetupEventType = "server_installed"
+ // EventServerInstallFailed is emitted when an LSP server installation fails
+ EventServerInstallFailed LSPSetupEventType = "server_install_failed"
+ // EventSetupCompleted is emitted when the LSP setup is completed
+ EventSetupCompleted LSPSetupEventType = "setup_completed"
+)
+
+// Service defines the interface for the LSP setup service
+type Service interface {
+ pubsub.Suscriber[LSPSetupEvent]
+
+ // DetectLanguages detects languages in the workspace
+ DetectLanguages(ctx context.Context, workspaceDir string) (map[protocol.LanguageKind]int, error)
+
+ // GetPrimaryLanguages returns the top N languages in the project
+ GetPrimaryLanguages(languages map[protocol.LanguageKind]int, limit int) []LanguageScore
+
+ // DetectMonorepo checks if the workspace is a monorepo
+ DetectMonorepo(ctx context.Context, workspaceDir string) (bool, []string)
+
+ // DiscoverInstalledLSPs discovers installed LSP servers
+ DiscoverInstalledLSPs(ctx context.Context) LSPServerMap
+
+ // GetRecommendedLSPServers returns recommended LSP servers for languages
+ GetRecommendedLSPServers(ctx context.Context, languages []LanguageScore) LSPServerMap
+
+ // InstallLSPServer installs an LSP server
+ InstallLSPServer(ctx context.Context, server LSPServerInfo) InstallationResult
+
+ // VerifyInstallation verifies that an LSP server is correctly installed
+ VerifyInstallation(ctx context.Context, serverName string) bool
+
+ // SaveConfiguration saves the LSP configuration
+ SaveConfiguration(ctx context.Context, servers map[protocol.LanguageKind]LSPServerInfo) error
+}
+
+type service struct {
+ *pubsub.Broker[LSPSetupEvent]
+}
+
+// NewService creates a new LSP setup service
+func NewService() Service {
+ broker := pubsub.NewBroker[LSPSetupEvent]()
+ return &service{
+ Broker: broker,
+ }
+}
+
+// DetectLanguages detects languages in the workspace
+func (s *service) DetectLanguages(ctx context.Context, workspaceDir string) (map[protocol.LanguageKind]int, error) {
+ languages, err := DetectProjectLanguages(workspaceDir)
+ if err != nil {
+ return nil, err
+ }
+
+ // Emit events for detected languages
+ for lang, score := range languages {
+ if lang != "" && score > 0 {
+ s.Publish(pubsub.CreatedEvent, LSPSetupEvent{
+ Type: EventLanguageDetected,
+ Language: lang,
+ Description: "Language detected in workspace",
+ })
+ }
+ }
+
+ return languages, nil
+}
+
+// GetPrimaryLanguages returns the top N languages in the project
+func (s *service) GetPrimaryLanguages(languages map[protocol.LanguageKind]int, limit int) []LanguageScore {
+ return GetPrimaryLanguages(languages, limit)
+}
+
+// DetectMonorepo checks if the workspace is a monorepo
+func (s *service) DetectMonorepo(ctx context.Context, workspaceDir string) (bool, []string) {
+ return DetectMonorepo(workspaceDir)
+}
+
+// DiscoverInstalledLSPs discovers installed LSP servers
+func (s *service) DiscoverInstalledLSPs(ctx context.Context) LSPServerMap {
+ servers := DiscoverInstalledLSPs()
+
+ // Emit events for discovered servers
+ for lang, serverList := range servers {
+ for _, server := range serverList {
+ s.Publish(pubsub.CreatedEvent, LSPSetupEvent{
+ Type: EventServerDiscovered,
+ Language: lang,
+ ServerName: server.Name,
+ Description: "LSP server discovered",
+ })
+ }
+ }
+
+ return servers
+}
+
+// GetRecommendedLSPServers returns recommended LSP servers for languages
+func (s *service) GetRecommendedLSPServers(ctx context.Context, languages []LanguageScore) LSPServerMap {
+ return GetRecommendedLSPServers(languages)
+}
+
+// InstallLSPServer installs an LSP server
+func (s *service) InstallLSPServer(ctx context.Context, server LSPServerInfo) InstallationResult {
+ result := InstallLSPServer(ctx, server)
+
+ // Emit event based on installation result
+ eventType := EventServerInstalled
+ if !result.Success {
+ eventType = EventServerInstallFailed
+ }
+
+ s.Publish(pubsub.CreatedEvent, LSPSetupEvent{
+ Type: eventType,
+ ServerName: server.Name,
+ Success: result.Success,
+ Error: result.Error,
+ Description: result.Output,
+ })
+
+ return result
+}
+
+// VerifyInstallation verifies that an LSP server is correctly installed
+func (s *service) VerifyInstallation(ctx context.Context, serverName string) bool {
+ return VerifyInstallation(serverName)
+}
+
+// SaveConfiguration saves the LSP configuration
+func (s *service) SaveConfiguration(ctx context.Context, servers map[protocol.LanguageKind]LSPServerInfo) error {
+ // Update the LSP configuration
+ err := UpdateLSPConfig(servers)
+
+ // Emit setup completed event
+ s.Publish(pubsub.CreatedEvent, LSPSetupEvent{
+ Type: EventSetupCompleted,
+ Success: err == nil,
+ Error: err,
+ Description: "LSP setup completed",
+ })
+
+ return err
+}
@@ -3,7 +3,6 @@ package dialog
import (
"context"
"fmt"
- "os/exec"
"sort"
"strings"
@@ -15,6 +14,7 @@ import (
"github.com/opencode-ai/opencode/internal/config"
"github.com/opencode-ai/opencode/internal/lsp/protocol"
"github.com/opencode-ai/opencode/internal/lsp/setup"
+ "github.com/opencode-ai/opencode/internal/pubsub"
utilComponents "github.com/opencode-ai/opencode/internal/tui/components/util"
"github.com/opencode-ai/opencode/internal/tui/styles"
"github.com/opencode-ai/opencode/internal/tui/theme"
@@ -52,6 +52,7 @@ type LSPSetupWizard struct {
keys lspSetupKeyMap
error string
program *tea.Program
+ setupService setup.Service
}
// LSPItem represents an item in the language or server list
@@ -99,7 +100,7 @@ func (i LSPItem) Render(selected bool, width int) string {
}
// NewLSPSetupWizard creates a new LSPSetupWizard
-func NewLSPSetupWizard(ctx context.Context) *LSPSetupWizard {
+func NewLSPSetupWizard(ctx context.Context, setupService setup.Service) *LSPSetupWizard {
s := spinner.New()
s.Spinner = spinner.Dot
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
@@ -113,6 +114,7 @@ func NewLSPSetupWizard(ctx context.Context) *LSPSetupWizard {
installOutput: make([]string, 0, 10), // Initialize with capacity for 10 lines
spinner: s,
keys: DefaultLSPSetupKeyMap(),
+ setupService: setupService,
}
}
@@ -181,18 +183,18 @@ func (m *LSPSetupWizard) Init() tea.Cmd {
// detectLanguages is a command that detects languages in the workspace
func (m *LSPSetupWizard) detectLanguages() tea.Msg {
- languages, err := setup.DetectProjectLanguages(config.WorkingDirectory())
+ languages, err := m.setupService.DetectLanguages(m.ctx, config.WorkingDirectory())
if err != nil {
return lspSetupErrorMsg{err: err}
}
- isMonorepo, projectDirs := setup.DetectMonorepo(config.WorkingDirectory())
+ isMonorepo, projectDirs := m.setupService.DetectMonorepo(m.ctx, config.WorkingDirectory())
- primaryLangs := setup.GetPrimaryLanguages(languages, 10)
+ primaryLangs := m.setupService.GetPrimaryLanguages(languages, 10)
- availableLSPs := setup.DiscoverInstalledLSPs()
+ availableLSPs := m.setupService.DiscoverInstalledLSPs(m.ctx)
- recommendedLSPs := setup.GetRecommendedLSPServers(primaryLangs)
+ recommendedLSPs := m.setupService.GetRecommendedLSPServers(m.ctx, primaryLangs)
for lang, servers := range recommendedLSPs {
if _, ok := availableLSPs[lang]; !ok {
availableLSPs[lang] = servers
@@ -299,6 +301,18 @@ func (m *LSPSetupWizard) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case lspSetupErrorMsg:
m.error = msg.err.Error()
+ m.installing = false
+
+ // If we're in the installation step, stay there to show the error
+ if m.step == StepInstallation {
+ m.step = StepInstallation
+ }
+ m.installing = false
+
+ // If we're in the installation step, stay there to show the error
+ if m.step == StepInstallation {
+ m.step = StepInstallation
+ }
return m, nil
case lspSetupInstallMsg:
@@ -316,14 +330,68 @@ func (m *LSPSetupWizard) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.addOutputLine(fmt.Sprintf("✗ Failed to install %s for %s", msg.result.ServerName, msg.language))
}
+ // Continue with the next installation
+ if len(m.installResults) < len(m.selectedLSPs) {
+ return m, tea.Batch(
+ m.spinner.Tick,
+ m.installNextServer(),
+ )
+ }
+
+ // All installations are complete
+ m.installing = false
+ m.step = StepInstallation
+ return m, nil
+
+ case lspSetupInstallDoneMsg:
m.installing = false
+ m.step = StepInstallation
+ return m, nil
- if len(m.installResults) == len(m.selectedLSPs) {
- // All installations are complete, move to the summary step
- m.step = StepInstallation
- } else {
- // Continue with the next installation
- return m, m.installNextServer()
+ case pubsub.Event[setup.LSPSetupEvent]:
+ // Handle LSP setup events
+ event := msg.Payload
+ switch event.Type {
+ case setup.EventLanguageDetected:
+ // Language detected, update UI if needed
+ m.addOutputLine(fmt.Sprintf("Detected language: %s", event.Language))
+ case setup.EventServerDiscovered:
+ // Server discovered, update UI if needed
+ m.addOutputLine(fmt.Sprintf("Discovered server: %s for %s", event.ServerName, event.Language))
+ case setup.EventServerInstalled:
+ // Server installed, update the installation results
+ if _, ok := m.installResults[event.Language]; !ok {
+ m.installResults[event.Language] = setup.InstallationResult{
+ ServerName: event.ServerName,
+ Success: event.Success,
+ Error: event.Error,
+ Output: event.Description,
+ }
+ m.addOutputLine(fmt.Sprintf("✓ Successfully installed %s for %s", event.ServerName, event.Language))
+ }
+ case setup.EventServerInstallFailed:
+ // Server installation failed, update the installation results
+ if _, ok := m.installResults[event.Language]; !ok {
+ m.installResults[event.Language] = setup.InstallationResult{
+ ServerName: event.ServerName,
+ Success: false,
+ Error: event.Error,
+ Output: event.Description,
+ }
+ m.addOutputLine(fmt.Sprintf("✗ Failed to install %s for %s: %s",
+ event.ServerName, event.Language, event.Error))
+ }
+ case setup.EventSetupCompleted:
+ // Setup completed, update UI if needed
+ if event.Success {
+ m.addOutputLine("LSP setup completed successfully")
+ // If we're in the installation step and all servers are installed, we can move to the next step
+ if m.installing && len(m.installResults) == len(m.selectedLSPs) {
+ m.installing = false
+ }
+ } else {
+ m.addOutputLine(fmt.Sprintf("LSP setup failed: %s", event.Error))
+ }
}
}
@@ -401,6 +469,10 @@ func (m *LSPSetupWizard) handleEnter() (tea.Model, tea.Cmd) {
// Start installation
m.step = StepInstallation
m.installing = true
+ m.installResults = make(map[protocol.LanguageKind]setup.InstallationResult)
+ m.installOutput = []string{} // Clear previous output
+ m.addOutputLine("Starting LSP server installation...")
+
// Start the spinner and begin installation
return m, tea.Batch(
m.spinner.Tick,
@@ -591,6 +663,7 @@ func (m *LSPSetupWizard) renderConfirmation(baseStyle lipgloss.Style, t theme.Th
)
}
+// renderInstallation renders the installation/summary step
// renderInstallation renders the installation/summary step
func (m *LSPSetupWizard) renderInstallation(baseStyle lipgloss.Style, t theme.Theme, maxWidth int) string {
if m.installing {
@@ -601,7 +674,48 @@ func (m *LSPSetupWizard) renderInstallation(baseStyle lipgloss.Style, t theme.Th
Width(maxWidth).
Padding(1, 1)
- spinnerText := m.spinner.View() + " Installing " + m.currentInstall + "..."
+ // Show progress for all servers
+ var progressLines []string
+
+ // Get languages in a sorted order for consistent display
+ var languages []protocol.LanguageKind
+ for lang := range m.selectedLSPs {
+ languages = append(languages, lang)
+ }
+
+ // Sort languages alphabetically
+ sort.Slice(languages, func(i, j int) bool {
+ return string(languages[i]) < string(languages[j])
+ })
+
+ for _, lang := range languages {
+ server := m.selectedLSPs[lang]
+ status := "⋯" // Pending
+ statusColor := t.TextMuted()
+
+ if result, ok := m.installResults[lang]; ok {
+ if result.Success {
+ status = "✓" // Success
+ statusColor = t.Success()
+ } else {
+ status = "✗" // Failed
+ statusColor = t.Error()
+ }
+ } else if m.currentInstall == fmt.Sprintf("%s for %s", server.Name, lang) {
+ status = m.spinner.View() // In progress
+ statusColor = t.Primary()
+ }
+
+ line := fmt.Sprintf("%s %s: %s",
+ baseStyle.Foreground(statusColor).Render(status),
+ lang,
+ server.Name)
+
+ progressLines = append(progressLines, line)
+ }
+
+ progressText := strings.Join(progressLines, "\n")
+ progressContent := spinnerStyle.Render(progressText)
// Show output if available
var content string
@@ -617,11 +731,11 @@ func (m *LSPSetupWizard) renderInstallation(baseStyle lipgloss.Style, t theme.Th
content = lipgloss.JoinVertical(
lipgloss.Left,
- spinnerStyle.Render(spinnerText),
+ progressContent,
outputContent,
)
} else {
- content = spinnerStyle.Render(spinnerText)
+ content = progressContent
}
return content
@@ -821,57 +935,47 @@ func (m *LSPSetupWizard) installNextServer() tea.Cmd {
return func() tea.Msg {
for lang, server := range m.selectedLSPs {
if _, ok := m.installResults[lang]; !ok {
- if _, err := exec.LookPath(server.Command); err == nil {
+ if m.setupService.VerifyInstallation(m.ctx, server.Command) {
// Server is already installed
output := fmt.Sprintf("%s is already installed", server.Name)
- m.installResults[lang] = setup.InstallationResult{
- ServerName: server.Name,
- Success: true,
- Output: output,
+ return lspSetupInstallMsg{
+ language: lang,
+ result: setup.InstallationResult{
+ ServerName: server.Name,
+ Success: true,
+ Output: output,
+ },
+ output: output,
}
-
- // Add output line
- m.addOutputLine(output)
-
- // Continue with next server immediately
- return m.installNextServer()()
}
// Install this server
m.installing = true
m.currentInstall = fmt.Sprintf("%s for %s", server.Name, lang)
-
- // Add initial output line
m.addOutputLine(fmt.Sprintf("Installing %s for %s...", server.Name, lang))
- // Create a channel to receive the installation result
- resultCh := make(chan setup.InstallationResult)
-
- go func(l protocol.LanguageKind, s setup.LSPServerInfo) {
- result := setup.InstallLSPServer(m.ctx, s)
- resultCh <- result
- }(lang, server)
-
- // Return a command that will wait for the installation to complete
- // and also keep the spinner updating
- return tea.Batch(
- m.spinner.Tick,
- func() tea.Msg {
- result := <-resultCh
- return lspSetupInstallMsg{
- language: lang,
- result: result,
- output: result.Output,
- }
- },
- )
+ // Return a command that will perform the installation
+ return installServerCmd(m.ctx, lang, server, m.setupService)
}
}
// All servers have been installed
- m.installing = false
- m.step = StepInstallation
- return nil
+ return lspSetupInstallDoneMsg{}
+ }
+}
+
+// installServerCmd creates a command that installs an LSP server
+func installServerCmd(ctx context.Context, lang protocol.LanguageKind, server setup.LSPServerInfo, setupService setup.Service) tea.Cmd {
+ return func() tea.Msg {
+ // Perform installation using the service
+ result := setupService.InstallLSPServer(ctx, server)
+
+ // Return result as a message
+ return lspSetupInstallMsg{
+ language: lang,
+ result: result,
+ output: result.Output,
+ }
}
}
@@ -932,6 +1036,9 @@ type lspSetupInstallMsg struct {
output string // Installation output
}
+// lspSetupInstallDoneMsg is sent when all installations are complete
+type lspSetupInstallDoneMsg struct{}
+
// CloseLSPSetupMsg is a message that is sent when the LSP setup wizard is closed
type CloseLSPSetupMsg struct {
Configure bool
@@ -12,7 +12,6 @@ import (
"github.com/opencode-ai/opencode/internal/config"
"github.com/opencode-ai/opencode/internal/llm/agent"
"github.com/opencode-ai/opencode/internal/logging"
- "github.com/opencode-ai/opencode/internal/lsp/protocol"
"github.com/opencode-ai/opencode/internal/permission"
"github.com/opencode-ai/opencode/internal/pubsub"
"github.com/opencode-ai/opencode/internal/session"
@@ -392,8 +391,8 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case dialog.ShowLSPSetupMsg:
a.showLSPSetupDialog = msg.Show
if a.showLSPSetupDialog {
- // Initialize the LSP setup wizard
- a.lspSetupDialog = dialog.NewLSPSetupWizard(context.Background())
+ // Initialize the LSP setup wizard with the app's LSP setup service
+ a.lspSetupDialog = dialog.NewLSPSetupWizard(context.Background(), a.app.LSPSetup)
a.lspSetupDialog.SetSize(a.width, a.height)
return a, a.lspSetupDialog.Init()
}
@@ -402,30 +401,13 @@ func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case dialog.CloseLSPSetupMsg:
a.showLSPSetupDialog = false
if msg.Configure && len(msg.Servers) > 0 {
- // Convert setup.LSPServerInfo to config.LSPServerInfo
- configServers := make(map[protocol.LanguageKind]config.LSPServerInfo)
- for lang, server := range msg.Servers {
- configServers[lang] = config.LSPServerInfo{
- Name: server.Name,
- Command: server.Command,
- Args: server.Args,
- InstallCmd: server.InstallCmd,
- Description: server.Description,
- Recommended: server.Recommended,
- Options: server.Options,
- }
- }
-
- // Update the LSP configuration
- err := config.UpdateLSPConfig(configServers)
+ // Use the app's ConfigureLSP method to handle the configuration
+ err := a.app.ConfigureLSP(context.Background(), msg.Servers)
if err != nil {
logging.Error("Failed to update LSP configuration", "error", err)
return a, util.ReportError(err)
}
- // Restart LSP clients
- go a.app.InitLSPClients(context.Background())
-
return a, util.ReportInfo("LSP configuration updated successfully")
}
return a, nil