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