1package dialog
2
3import (
4 "context"
5 "fmt"
6 "os/exec"
7 "sort"
8 "strings"
9
10 "github.com/charmbracelet/bubbles/key"
11 "github.com/charmbracelet/bubbles/spinner"
12 tea "github.com/charmbracelet/bubbletea"
13 "github.com/charmbracelet/lipgloss"
14
15 "github.com/opencode-ai/opencode/internal/config"
16 "github.com/opencode-ai/opencode/internal/lsp/protocol"
17 "github.com/opencode-ai/opencode/internal/lsp/setup"
18 utilComponents "github.com/opencode-ai/opencode/internal/tui/components/util"
19 "github.com/opencode-ai/opencode/internal/tui/styles"
20 "github.com/opencode-ai/opencode/internal/tui/theme"
21 "github.com/opencode-ai/opencode/internal/tui/util"
22)
23
24// LSPSetupStep represents the current step in the LSP setup wizard
25type LSPSetupStep int
26
27const (
28 StepIntroduction LSPSetupStep = iota
29 StepLanguageSelection
30 StepConfirmation
31 StepInstallation
32)
33
34// LSPSetupWizard is a component that guides users through LSP setup
35type LSPSetupWizard struct {
36 ctx context.Context
37 step LSPSetupStep
38 width, height int
39 languages map[protocol.LanguageKind]int
40 selectedLangs map[protocol.LanguageKind]bool
41 availableLSPs setup.LSPServerMap
42 selectedLSPs map[protocol.LanguageKind]setup.LSPServerInfo
43 installResults map[protocol.LanguageKind]setup.InstallationResult
44 isMonorepo bool
45 projectDirs []string
46 langList utilComponents.SimpleList[LSPItem]
47 serverList utilComponents.SimpleList[LSPItem]
48 spinner spinner.Model
49 installing bool
50 currentInstall string
51 installOutput []string // Store installation output
52 keys lspSetupKeyMap
53 error string
54 program *tea.Program
55}
56
57// LSPItem represents an item in the language or server list
58type LSPItem struct {
59 title string
60 description string
61 selected bool
62 language protocol.LanguageKind
63 server setup.LSPServerInfo
64}
65
66// Render implements SimpleListItem interface
67func (i LSPItem) Render(selected bool, width int) string {
68 t := theme.CurrentTheme()
69 baseStyle := styles.BaseStyle()
70
71 descStyle := baseStyle.Width(width).Foreground(t.TextMuted())
72 itemStyle := baseStyle.Width(width).
73 Foreground(t.Text()).
74 Background(t.Background())
75
76 if selected {
77 itemStyle = itemStyle.
78 Background(t.Primary()).
79 Foreground(t.Background()).
80 Bold(true)
81 descStyle = descStyle.
82 Background(t.Primary()).
83 Foreground(t.Background())
84 }
85
86 title := i.title
87 if i.selected {
88 title = "[x] " + i.title
89 } else {
90 title = "[ ] " + i.title
91 }
92
93 titleStr := itemStyle.Padding(0, 1).Render(title)
94 if i.description != "" {
95 description := descStyle.Padding(0, 1).Render(i.description)
96 return lipgloss.JoinVertical(lipgloss.Left, titleStr, description)
97 }
98 return titleStr
99}
100
101// NewLSPSetupWizard creates a new LSPSetupWizard
102func NewLSPSetupWizard(ctx context.Context) *LSPSetupWizard {
103 s := spinner.New()
104 s.Spinner = spinner.Dot
105 s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))
106
107 return &LSPSetupWizard{
108 ctx: ctx,
109 step: StepIntroduction,
110 selectedLangs: make(map[protocol.LanguageKind]bool),
111 selectedLSPs: make(map[protocol.LanguageKind]setup.LSPServerInfo),
112 installResults: make(map[protocol.LanguageKind]setup.InstallationResult),
113 installOutput: make([]string, 0, 10), // Initialize with capacity for 10 lines
114 spinner: s,
115 keys: DefaultLSPSetupKeyMap(),
116 }
117}
118
119type lspSetupKeyMap struct {
120 Up key.Binding
121 Down key.Binding
122 Select key.Binding
123 Next key.Binding
124 Back key.Binding
125 Quit key.Binding
126}
127
128// DefaultLSPSetupKeyMap returns the default key bindings for the LSP setup wizard
129func DefaultLSPSetupKeyMap() lspSetupKeyMap {
130 return lspSetupKeyMap{
131 Up: key.NewBinding(
132 key.WithKeys("up", "k"),
133 key.WithHelp("↑/k", "up"),
134 ),
135 Down: key.NewBinding(
136 key.WithKeys("down", "j"),
137 key.WithHelp("↓/j", "down"),
138 ),
139 Select: key.NewBinding(
140 key.WithKeys("space"),
141 key.WithHelp("space", "select"),
142 ),
143 Next: key.NewBinding(
144 key.WithKeys("enter"),
145 key.WithHelp("enter", "next"),
146 ),
147 Back: key.NewBinding(
148 key.WithKeys("esc"),
149 key.WithHelp("esc", "back/quit"),
150 ),
151 Quit: key.NewBinding(
152 key.WithKeys("ctrl+c", "q"),
153 key.WithHelp("ctrl+c/q", "quit"),
154 ),
155 }
156}
157
158// ShortHelp implements key.Map
159func (k lspSetupKeyMap) ShortHelp() []key.Binding {
160 return []key.Binding{
161 k.Up,
162 k.Down,
163 k.Select,
164 k.Next,
165 k.Back,
166 }
167}
168
169// FullHelp implements key.Map
170func (k lspSetupKeyMap) FullHelp() [][]key.Binding {
171 return [][]key.Binding{k.ShortHelp()}
172}
173
174// Init implements tea.Model
175func (m *LSPSetupWizard) Init() tea.Cmd {
176 return tea.Batch(
177 m.spinner.Tick,
178 m.detectLanguages,
179 )
180}
181
182// detectLanguages is a command that detects languages in the workspace
183func (m *LSPSetupWizard) detectLanguages() tea.Msg {
184 languages, err := setup.DetectProjectLanguages(config.WorkingDirectory())
185 if err != nil {
186 return lspSetupErrorMsg{err: err}
187 }
188
189 isMonorepo, projectDirs := setup.DetectMonorepo(config.WorkingDirectory())
190
191 primaryLangs := setup.GetPrimaryLanguages(languages, 10)
192
193 availableLSPs := setup.DiscoverInstalledLSPs()
194
195 recommendedLSPs := setup.GetRecommendedLSPServers(primaryLangs)
196 for lang, servers := range recommendedLSPs {
197 if _, ok := availableLSPs[lang]; !ok {
198 availableLSPs[lang] = servers
199 }
200 }
201
202 return lspSetupDetectMsg{
203 languages: languages,
204 primaryLangs: primaryLangs,
205 availableLSPs: availableLSPs,
206 isMonorepo: isMonorepo,
207 projectDirs: projectDirs,
208 }
209}
210
211// Update implements tea.Model
212func (m *LSPSetupWizard) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
213 var cmds []tea.Cmd
214
215 // Handle space key directly for language selection
216 if keyMsg, ok := msg.(tea.KeyMsg); ok && keyMsg.String() == " " && m.step == StepLanguageSelection {
217 item, idx := m.langList.GetSelectedItem()
218 if idx != -1 {
219 m.selectedLangs[item.language] = !m.selectedLangs[item.language]
220 return m, m.updateLanguageSelection()
221 }
222 }
223
224 switch msg := msg.(type) {
225 case tea.KeyMsg:
226 switch {
227 case key.Matches(msg, m.keys.Quit):
228 return m, util.CmdHandler(CloseLSPSetupMsg{Configure: false})
229 case key.Matches(msg, m.keys.Back):
230 if m.step > StepIntroduction {
231 m.step--
232 return m, nil
233 }
234 return m, util.CmdHandler(CloseLSPSetupMsg{Configure: false})
235 case key.Matches(msg, m.keys.Next):
236 return m.handleEnter()
237 }
238
239 case tea.WindowSizeMsg:
240 m.width = msg.Width
241 m.height = msg.Height
242
243 // Update list dimensions
244 if m.langList != nil {
245 m.langList.SetMaxWidth(min(80, m.width-10))
246 }
247 if m.serverList != nil {
248 m.serverList.SetMaxWidth(min(80, m.width-10))
249 }
250
251 case spinner.TickMsg:
252 var cmd tea.Cmd
253 m.spinner, cmd = m.spinner.Update(msg)
254 if m.installing {
255 // Only continue ticking if we're still installing
256 cmds = append(cmds, cmd)
257 }
258
259 case lspSetupDetectMsg:
260 m.languages = msg.languages
261 m.availableLSPs = msg.availableLSPs
262 m.isMonorepo = msg.isMonorepo
263 m.projectDirs = msg.projectDirs
264
265 // Create language list items - only for languages with available servers
266 items := []LSPItem{}
267 for _, lang := range msg.primaryLangs {
268 // Check if servers are available for this language
269 hasServers := false
270 if servers, ok := m.availableLSPs[lang.Language]; ok && len(servers) > 0 {
271 hasServers = true
272 }
273
274 // Only add languages that have servers available
275 if hasServers {
276 item := LSPItem{
277 title: string(lang.Language),
278 selected: false,
279 language: lang.Language,
280 }
281
282 items = append(items, item)
283
284 // Pre-select languages with high scores
285 if lang.Score > 10 {
286 m.selectedLangs[lang.Language] = true
287 }
288 }
289 }
290
291 // Create the language list
292 m.langList = utilComponents.NewSimpleList(items, 10, "No languages with available servers detected", true)
293
294 // Move to the next step
295 m.step = StepLanguageSelection
296
297 // Update the selection status in the list
298 return m, m.updateLanguageSelection()
299
300 case lspSetupErrorMsg:
301 m.error = msg.err.Error()
302 return m, nil
303
304 case lspSetupInstallMsg:
305 m.installResults[msg.language] = msg.result
306
307 // Add output from the installation result
308 if msg.output != "" {
309 m.addOutputLine(msg.output)
310 }
311
312 // Add success/failure message with clear formatting
313 if msg.result.Success {
314 m.addOutputLine(fmt.Sprintf("✓ Successfully installed %s for %s", msg.result.ServerName, msg.language))
315 } else {
316 m.addOutputLine(fmt.Sprintf("✗ Failed to install %s for %s", msg.result.ServerName, msg.language))
317 }
318
319 m.installing = false
320
321 if len(m.installResults) == len(m.selectedLSPs) {
322 // All installations are complete, move to the summary step
323 m.step = StepInstallation
324 } else {
325 // Continue with the next installation
326 return m, m.installNextServer()
327 }
328 }
329
330 // Handle list updates
331 if m.step == StepLanguageSelection {
332 u, cmd := m.langList.Update(msg)
333 m.langList = u.(utilComponents.SimpleList[LSPItem])
334 cmds = append(cmds, cmd)
335 }
336
337 return m, tea.Batch(cmds...)
338}
339
340// handleEnter handles the enter key press based on the current step
341func (m *LSPSetupWizard) handleEnter() (tea.Model, tea.Cmd) {
342 switch m.step {
343 case StepIntroduction: // Introduction
344 return m, m.detectLanguages
345
346 case StepLanguageSelection: // Language selection
347 // Check if any languages are selected
348 hasSelected := false
349
350 // Create a sorted list of languages for consistent ordering
351 var selectedLangs []protocol.LanguageKind
352 for lang, selected := range m.selectedLangs {
353 if selected {
354 selectedLangs = append(selectedLangs, lang)
355 hasSelected = true
356 }
357 }
358
359 // Sort languages alphabetically for consistent display
360 sort.Slice(selectedLangs, func(i, j int) bool {
361 return string(selectedLangs[i]) < string(selectedLangs[j])
362 })
363
364 // Auto-select servers for each language
365 for _, lang := range selectedLangs {
366 // Auto-select the recommended or first server for each language
367 if servers, ok := m.availableLSPs[lang]; ok && len(servers) > 0 {
368 // First try to find a recommended server
369 foundRecommended := false
370 for _, server := range servers {
371 if server.Recommended {
372 m.selectedLSPs[lang] = server
373 foundRecommended = true
374 break
375 }
376 }
377
378 // If no recommended server, use the first one
379 if !foundRecommended && len(servers) > 0 {
380 m.selectedLSPs[lang] = servers[0]
381 }
382 } else {
383 // No servers available for this language, deselect it
384 m.selectedLangs[lang] = false
385 // Update the UI to reflect this change
386 return m, m.updateLanguageSelection()
387 }
388 }
389
390 if !hasSelected {
391 // No language selected, show error
392 m.error = "Please select at least one language"
393 return m, nil
394 }
395
396 // Skip server selection and go directly to confirmation
397 m.step = StepConfirmation
398 return m, nil
399
400 case StepConfirmation: // Confirmation
401 // Start installation
402 m.step = StepInstallation
403 m.installing = true
404 // Start the spinner and begin installation
405 return m, tea.Batch(
406 m.spinner.Tick,
407 m.installNextServer(),
408 )
409
410 case StepInstallation: // Summary
411 // Save configuration and close
412 return m, util.CmdHandler(CloseLSPSetupMsg{
413 Configure: true,
414 Servers: m.selectedLSPs,
415 })
416 }
417
418 return m, nil
419}
420
421// View implements tea.Model
422func (m *LSPSetupWizard) View() string {
423 t := theme.CurrentTheme()
424 baseStyle := styles.BaseStyle()
425
426 // Calculate width needed for content
427 maxWidth := min(80, m.width-10)
428
429 title := baseStyle.
430 Foreground(t.Primary()).
431 Bold(true).
432 Width(maxWidth).
433 Padding(0, 1).
434 Render("LSP Setup Wizard")
435
436 var content string
437
438 switch m.step {
439 case StepIntroduction: // Introduction
440 content = m.renderIntroduction(baseStyle, t, maxWidth)
441 case StepLanguageSelection: // Language selection
442 content = m.renderLanguageSelection(baseStyle, t, maxWidth)
443 case StepConfirmation: // Confirmation
444 content = m.renderConfirmation(baseStyle, t, maxWidth)
445 case StepInstallation: // Installation/Summary
446 content = m.renderInstallation(baseStyle, t, maxWidth)
447 }
448
449 // Add error message if any
450 if m.error != "" {
451 errorMsg := baseStyle.
452 Foreground(t.Error()).
453 Width(maxWidth).
454 Padding(1, 1).
455 Render("Error: " + m.error)
456
457 content = lipgloss.JoinVertical(
458 lipgloss.Left,
459 content,
460 errorMsg,
461 )
462 }
463
464 // Add help text
465 helpText := baseStyle.
466 Foreground(t.TextMuted()).
467 Width(maxWidth).
468 Padding(1, 1).
469 Render(m.getHelpText())
470
471 fullContent := lipgloss.JoinVertical(
472 lipgloss.Left,
473 title,
474 baseStyle.Width(maxWidth).Render(""),
475 content,
476 helpText,
477 )
478
479 return baseStyle.Padding(1, 2).
480 Border(lipgloss.RoundedBorder()).
481 BorderBackground(t.Background()).
482 BorderForeground(t.BorderNormal()).
483 Width(lipgloss.Width(fullContent) + 4).
484 Render(fullContent)
485}
486
487// renderIntroduction renders the introduction step
488func (m *LSPSetupWizard) renderIntroduction(baseStyle lipgloss.Style, t theme.Theme, maxWidth int) string {
489 explanation := baseStyle.
490 Foreground(t.Text()).
491 Width(maxWidth).
492 Padding(0, 1).
493 Render("OpenCode can automatically configure Language Server Protocol (LSP) integration for your project. LSP provides code intelligence features like error checking, diagnostics, and more.")
494
495 if m.languages == nil {
496 // Show spinner while detecting languages
497 spinner := baseStyle.
498 Foreground(t.Primary()).
499 Width(maxWidth).
500 Padding(1, 1).
501 Render(m.spinner.View() + " Detecting languages in your project...")
502
503 return lipgloss.JoinVertical(
504 lipgloss.Left,
505 explanation,
506 spinner,
507 )
508 }
509
510 nextPrompt := baseStyle.
511 Foreground(t.Primary()).
512 Width(maxWidth).
513 Padding(1, 1).
514 Render("Press Enter to continue")
515
516 return lipgloss.JoinVertical(
517 lipgloss.Left,
518 explanation,
519 nextPrompt,
520 )
521}
522
523// renderLanguageSelection renders the language selection step
524func (m *LSPSetupWizard) renderLanguageSelection(baseStyle lipgloss.Style, t theme.Theme, maxWidth int) string {
525 explanation := baseStyle.
526 Foreground(t.Text()).
527 Width(maxWidth).
528 Padding(0, 1).
529 Render("Select the languages you want to configure LSP for. Only languages with available servers are shown. Use Space to toggle selection, Enter to continue.")
530
531 // Show monorepo info if detected
532 monorepoInfo := ""
533 if m.isMonorepo {
534 monorepoInfo = baseStyle.
535 Foreground(t.TextMuted()).
536 Width(maxWidth).
537 Padding(0, 1).
538 Render(fmt.Sprintf("Monorepo detected with %d projects", len(m.projectDirs)))
539 }
540
541 // Set max width for the list
542 m.langList.SetMaxWidth(maxWidth)
543
544 // Render the language list
545 listView := m.langList.View()
546
547 return lipgloss.JoinVertical(
548 lipgloss.Left,
549 explanation,
550 monorepoInfo,
551 listView,
552 )
553}
554
555// renderConfirmation renders the confirmation step
556func (m *LSPSetupWizard) renderConfirmation(baseStyle lipgloss.Style, t theme.Theme, maxWidth int) string {
557 explanation := baseStyle.
558 Foreground(t.Text()).
559 Width(maxWidth).
560 Padding(0, 1).
561 Render("Review your LSP configuration. Press Enter to install missing servers and save the configuration.")
562
563 // Get languages in a sorted order for consistent display
564 var languages []protocol.LanguageKind
565 for lang := range m.selectedLSPs {
566 languages = append(languages, lang)
567 }
568
569 // Sort languages alphabetically
570 sort.Slice(languages, func(i, j int) bool {
571 return string(languages[i]) < string(languages[j])
572 })
573
574 // Build the configuration summary
575 var configLines []string
576 for _, lang := range languages {
577 server := m.selectedLSPs[lang]
578 configLines = append(configLines, fmt.Sprintf("%s: %s", lang, server.Name))
579 }
580
581 configSummary := baseStyle.
582 Foreground(t.Text()).
583 Width(maxWidth).
584 Padding(1, 1).
585 Render(strings.Join(configLines, "\n"))
586
587 return lipgloss.JoinVertical(
588 lipgloss.Left,
589 explanation,
590 configSummary,
591 )
592}
593
594// renderInstallation renders the installation/summary step
595func (m *LSPSetupWizard) renderInstallation(baseStyle lipgloss.Style, t theme.Theme, maxWidth int) string {
596 if m.installing {
597 // Show installation progress with proper styling
598 spinnerStyle := baseStyle.
599 Foreground(t.Primary()).
600 Background(t.Background()).
601 Width(maxWidth).
602 Padding(1, 1)
603
604 spinnerText := m.spinner.View() + " Installing " + m.currentInstall + "..."
605
606 // Show output if available
607 var content string
608 if len(m.installOutput) > 0 {
609 outputStyle := baseStyle.
610 Foreground(t.TextMuted()).
611 Background(t.Background()).
612 Width(maxWidth).
613 Padding(1, 1)
614
615 outputText := strings.Join(m.installOutput, "\n")
616 outputContent := outputStyle.Render(outputText)
617
618 content = lipgloss.JoinVertical(
619 lipgloss.Left,
620 spinnerStyle.Render(spinnerText),
621 outputContent,
622 )
623 } else {
624 content = spinnerStyle.Render(spinnerText)
625 }
626
627 return content
628 }
629
630 // Show installation results
631 explanation := baseStyle.
632 Foreground(t.Text()).
633 Width(maxWidth).
634 Padding(0, 1).
635 Render("LSP server installation complete. Press Enter to save the configuration and exit.")
636
637 // Build the installation summary
638 var resultLines []string
639 for lang, result := range m.installResults {
640 status := "✓"
641 statusColor := t.Success()
642 if !result.Success {
643 status = "✗"
644 statusColor = t.Error()
645 }
646
647 line := fmt.Sprintf("%s %s: %s",
648 baseStyle.Foreground(statusColor).Render(status),
649 lang,
650 result.ServerName)
651
652 resultLines = append(resultLines, line)
653 }
654
655 // Style the result summary with a header
656 resultHeader := baseStyle.
657 Foreground(t.Primary()).
658 Bold(true).
659 Width(maxWidth).
660 Padding(1, 1).
661 Render("Installation Results:")
662
663 resultSummary := baseStyle.
664 Width(maxWidth).
665 Padding(0, 2). // Indent the results
666 Render(strings.Join(resultLines, "\n"))
667
668 // Show output if available
669 var content string
670 if len(m.installOutput) > 0 {
671 // Create a header for the output section
672 outputHeader := baseStyle.
673 Foreground(t.Primary()).
674 Bold(true).
675 Width(maxWidth).
676 Padding(1, 1).
677 Render("Installation Output:")
678
679 // Style the output
680 outputStyle := baseStyle.
681 Foreground(t.TextMuted()).
682 Background(t.Background()).
683 Width(maxWidth).
684 Padding(0, 2) // Indent the output
685
686 outputText := strings.Join(m.installOutput, "\n")
687 outputContent := outputStyle.Render(outputText)
688
689 content = lipgloss.JoinVertical(
690 lipgloss.Left,
691 explanation,
692 baseStyle.Render(""), // Add a blank line for spacing
693 resultHeader,
694 resultSummary,
695 baseStyle.Render(""), // Add a blank line for spacing
696 outputHeader,
697 outputContent,
698 )
699 } else {
700 content = lipgloss.JoinVertical(
701 lipgloss.Left,
702 explanation,
703 baseStyle.Render(""), // Add a blank line for spacing
704 resultHeader,
705 resultSummary,
706 )
707 }
708
709 return content
710}
711
712// getHelpText returns the help text for the current step
713func (m *LSPSetupWizard) getHelpText() string {
714 switch m.step {
715 case StepIntroduction:
716 return "Enter: Continue • Esc: Quit"
717 case StepLanguageSelection:
718 return "↑/↓: Navigate • Space: Toggle selection • Enter: Continue • Esc: Quit"
719 case StepConfirmation:
720 return "Enter: Install and configure • Esc: Back"
721 case StepInstallation:
722 if m.installing {
723 return "Installing LSP servers..."
724 }
725 return "Enter: Save and exit • Esc: Back"
726 }
727
728 return ""
729}
730
731// updateLanguageSelection updates the selection status in the language list
732func (m *LSPSetupWizard) updateLanguageSelection() tea.Cmd {
733 return func() tea.Msg {
734 items := m.langList.GetItems()
735 updatedItems := make([]LSPItem, 0, len(items))
736
737 for _, item := range items {
738 // Only update the selected state, preserve the item otherwise
739 newItem := item
740 newItem.selected = m.selectedLangs[item.language]
741 updatedItems = append(updatedItems, newItem)
742 }
743
744 // Set the items - the selected index will be preserved by the SimpleList implementation
745 m.langList.SetItems(updatedItems)
746
747 return nil
748 }
749}
750
751// createServerListForLanguage creates the server list for a specific language
752func (m *LSPSetupWizard) createServerListForLanguage(lang protocol.LanguageKind) tea.Cmd {
753 return func() tea.Msg {
754 items := []LSPItem{}
755
756 if servers, ok := m.availableLSPs[lang]; ok {
757 for _, server := range servers {
758 description := server.Description
759 if server.Recommended {
760 description += " (Recommended)"
761 }
762
763 items = append(items, LSPItem{
764 title: server.Name,
765 description: description,
766 language: lang,
767 server: server,
768 })
769 }
770 }
771
772 // If no servers available, add a placeholder
773 if len(items) == 0 {
774 items = append(items, LSPItem{
775 title: "No LSP servers available for " + string(lang),
776 description: "You'll need to install a server manually",
777 language: lang,
778 })
779 }
780
781 // Create the server list
782 m.serverList = utilComponents.NewSimpleList(items, 10, "No servers available", true)
783
784 // Move to the server selection step
785 m.step = 2
786
787 return nil
788 }
789}
790
791// getCurrentLanguage returns the language currently being configured
792func (m *LSPSetupWizard) getCurrentLanguage() protocol.LanguageKind {
793 items := m.serverList.GetItems()
794 if len(items) == 0 {
795 return ""
796 }
797 return items[0].language
798}
799
800// getNextLanguage returns the next language to configure after the current one
801func (m *LSPSetupWizard) getNextLanguage(currentLang protocol.LanguageKind) protocol.LanguageKind {
802 foundCurrent := false
803
804 for lang := range m.selectedLangs {
805 if m.selectedLangs[lang] {
806 if foundCurrent {
807 return lang
808 }
809
810 if lang == currentLang {
811 foundCurrent = true
812 }
813 }
814 }
815
816 return ""
817}
818
819// installNextServer installs the next server in the queue
820func (m *LSPSetupWizard) installNextServer() tea.Cmd {
821 return func() tea.Msg {
822 for lang, server := range m.selectedLSPs {
823 if _, ok := m.installResults[lang]; !ok {
824 if _, err := exec.LookPath(server.Command); err == nil {
825 // Server is already installed
826 output := fmt.Sprintf("%s is already installed", server.Name)
827 m.installResults[lang] = setup.InstallationResult{
828 ServerName: server.Name,
829 Success: true,
830 Output: output,
831 }
832
833 // Add output line
834 m.addOutputLine(output)
835
836 // Continue with next server immediately
837 return m.installNextServer()()
838 }
839
840 // Install this server
841 m.installing = true
842 m.currentInstall = fmt.Sprintf("%s for %s", server.Name, lang)
843
844 // Add initial output line
845 m.addOutputLine(fmt.Sprintf("Installing %s for %s...", server.Name, lang))
846
847 // Create a channel to receive the installation result
848 resultCh := make(chan setup.InstallationResult)
849
850 go func(l protocol.LanguageKind, s setup.LSPServerInfo) {
851 result := setup.InstallLSPServer(m.ctx, s)
852 resultCh <- result
853 }(lang, server)
854
855 // Return a command that will wait for the installation to complete
856 // and also keep the spinner updating
857 return tea.Batch(
858 m.spinner.Tick,
859 func() tea.Msg {
860 result := <-resultCh
861 return lspSetupInstallMsg{
862 language: lang,
863 result: result,
864 output: result.Output,
865 }
866 },
867 )
868 }
869 }
870
871 // All servers have been installed
872 m.installing = false
873 m.step = StepInstallation
874 return nil
875 }
876}
877
878// SetSize sets the size of the component
879func (m *LSPSetupWizard) SetSize(width, height int) {
880 m.width = width
881 m.height = height
882
883 // Update list max width if lists are initialized
884 if m.langList != nil {
885 m.langList.SetMaxWidth(min(80, width-10))
886 }
887 if m.serverList != nil {
888 m.serverList.SetMaxWidth(min(80, width-10))
889 }
890}
891
892// addOutputLine adds a line to the installation output, keeping only the last 10 lines
893func (m *LSPSetupWizard) addOutputLine(line string) {
894 // Split the line into multiple lines if it contains newlines
895 lines := strings.Split(line, "\n")
896 for _, l := range lines {
897 if l == "" {
898 continue
899 }
900
901 // Add the line to the output
902 m.installOutput = append(m.installOutput, l)
903
904 // Keep only the last 10 lines
905 if len(m.installOutput) > 10 {
906 m.installOutput = m.installOutput[len(m.installOutput)-10:]
907 }
908 }
909}
910
911// Bindings implements layout.Bindings
912func (m *LSPSetupWizard) Bindings() []key.Binding {
913 return m.keys.ShortHelp()
914}
915
916// Message types for the LSP setup wizard
917type lspSetupDetectMsg struct {
918 languages map[protocol.LanguageKind]int
919 primaryLangs []setup.LanguageScore
920 availableLSPs setup.LSPServerMap
921 isMonorepo bool
922 projectDirs []string
923}
924
925type lspSetupErrorMsg struct {
926 err error
927}
928
929type lspSetupInstallMsg struct {
930 language protocol.LanguageKind
931 result setup.InstallationResult
932 output string // Installation output
933}
934
935// CloseLSPSetupMsg is a message that is sent when the LSP setup wizard is closed
936type CloseLSPSetupMsg struct {
937 Configure bool
938 Servers map[protocol.LanguageKind]setup.LSPServerInfo
939}
940
941// ShowLSPSetupMsg is a message that is sent to show the LSP setup wizard
942type ShowLSPSetupMsg struct {
943 Show bool
944}