lsp_setup.go

  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}