lsp_setup.go

   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}