Merge: Better LSP integration

Author Mitja Felicijan <mitja.felicijan@gmail.com> 2026-05-20 01:52:03 +0200
Committer Mitja Felicijan <mitja.felicijan@gmail.com> 2026-05-20 02:39:04 +0200
Commit 958e33ff5ca4e6d2e2fd4e29b11d01e7c08badb7 (patch)
-rw-r--r-- editor.go 216
-rw-r--r-- ftypes.go 34
-rw-r--r-- kevent.go 2
-rw-r--r-- lsp.go 506
4 files changed, 529 insertions, 229 deletions
diff --git a/editor.go b/editor.go
...
176
	return result.String()
176
	return result.String()
177
}
177
}
178
  
178
  
  
179
func (e *Editor) wrapText(text string, maxWidth int) []string {
  
180
	var lines []string
  
181
	paragraphs := strings.Split(text, "\n")
  
182
  
  
183
	for _, p := range paragraphs {
  
184
		words := strings.Fields(p)
  
185
		if len(words) == 0 {
  
186
			lines = append(lines, "")
  
187
			continue
  
188
		}
  
189
  
  
190
		currentLine := ""
  
191
		for _, word := range words {
  
192
			if len(currentLine)+len(word)+1 <= maxWidth {
  
193
				if currentLine == "" {
  
194
					currentLine = word
  
195
				} else {
  
196
					currentLine += " " + word
  
197
				}
  
198
			} else {
  
199
				if currentLine != "" {
  
200
					lines = append(lines, currentLine)
  
201
				}
  
202
				// If a single word is longer than maxWidth, we have to break it
  
203
				for len(word) > maxWidth {
  
204
					lines = append(lines, word[:maxWidth])
  
205
					word = word[maxWidth:]
  
206
				}
  
207
				currentLine = word
  
208
			}
  
209
		}
  
210
		if currentLine != "" {
  
211
			lines = append(lines, currentLine)
  
212
		}
  
213
	}
  
214
	return lines
  
215
}
  
216
  
179
// NewEditor creates a new editor instance with a default empty buffer.
217
// NewEditor creates a new editor instance with a default empty buffer.
180
func NewEditor(devMode bool) *Editor {
218
func NewEditor(devMode bool) *Editor {
181
	e := &Editor{
219
	e := &Editor{
...
1735
	}
1773
	}
1736
  
1774
  
1737
	e.pushJump()
1775
	e.pushJump()
  
1776
  
  
1777
	// Sync buffer content with LSP server before requesting definition.
  
1778
	b.lspClient.SendDidChange(b.toString())
1738
  
1779
  
1739
	locs, err := b.lspClient.Definition(b.PrimaryCursor().Y, b.PrimaryCursor().X)
1780
	locs, err := b.lspClient.Definition(b.PrimaryCursor().Y, b.PrimaryCursor().X)
1740
	if err != nil {
1781
	if err != nil {
...
4588
	e.message = "Requesting signature..."
4629
	e.message = "Requesting signature..."
4589
	e.draw()
4630
	e.draw()
4590
  
4631
  
  
4632
	// Sync buffer content with LSP server before requesting hover.
  
4633
	b.lspClient.SendDidChange(b.toString())
  
4634
  
4591
	cursor := b.PrimaryCursor()
4635
	cursor := b.PrimaryCursor()
4592
	content, err := b.lspClient.Hover(cursor.Y, cursor.X)
4636
	content, err := b.lspClient.Hover(cursor.Y, cursor.X)
4593
	if err != nil {
4637
	if err != nil {
...
4608
  
4652
  
4609
	e.message = "Requesting completions..."
4653
	e.message = "Requesting completions..."
4610
	e.draw()
4654
	e.draw()
  
4655
  
  
4656
	// Sync buffer content with LSP server before requesting completions.
  
4657
	b.lspClient.SendDidChange(b.toString())
4611
  
4658
  
4612
	cursor := b.PrimaryCursor()
4659
	cursor := b.PrimaryCursor()
4613
	items, err := b.lspClient.Completion(cursor.Y, cursor.X)
4660
	items, err := b.lspClient.Completion(cursor.Y, cursor.X)
...
4626
	e.autocompleteScroll = 0
4673
	e.autocompleteScroll = 0
4627
	e.showAutocomplete = true
4674
	e.showAutocomplete = true
4628
	e.message = ""
4675
	e.message = ""
  
4676
  
  
4677
	e.resolveSelectedCompletion()
  
4678
}
  
4679
  
  
4680
func (e *Editor) resolveSelectedCompletion() {
  
4681
	b := e.activeBuffer()
  
4682
	if b == nil || b.lspClient == nil {
  
4683
		return
  
4684
	}
  
4685
  
  
4686
	if e.autocompleteIndex < 0 || e.autocompleteIndex >= len(e.autocompleteItems) {
  
4687
		return
  
4688
	}
  
4689
  
  
4690
	item := e.autocompleteItems[e.autocompleteIndex]
  
4691
  
  
4692
	// If it already has documentation and it's not a resolve-only item, maybe skip?
  
4693
	// But clangd often sends partial info.
  
4694
  
  
4695
	go func(index int, it CompletionItem) {
  
4696
		resolved, err := b.lspClient.ResolveCompletion(it)
  
4697
		if err == nil {
  
4698
			// Update the item in the list if the index is still the same
  
4699
			if e.autocompleteIndex == index {
  
4700
				e.autocompleteItems[index] = resolved
  
4701
				termbox.Interrupt() // Redraw
  
4702
			}
  
4703
		}
  
4704
	}(e.autocompleteIndex, item)
4629
}
4705
}
4630
  
4706
  
4631
func (e *Editor) drawAutocompletePopup() {
4707
func (e *Editor) drawAutocompletePopup() {
...
4639
		return
4715
		return
4640
	}
4716
	}
4641
  
4717
  
4642
	// Calculate max label width for alignment
4718
	// Helper to get extra info (signature or type) for an item
4643
	maxLabelWidth := 0
4719
	getExtraInfo := func(item CompletionItem) string {
4644
	for _, item := range e.autocompleteItems {
4720
		info := ""
4645
		if len(item.Label) > maxLabelWidth {
4721
  
4646
			maxLabelWidth = len(item.Label)
4722
		// 1. Try LabelDetails (standard in newer LSP)
  
4723
		if item.LabelDetails != nil {
  
4724
			// For functions, Detail usually has the parameters: (a, b int)
  
4725
			// For variables, Description usually has the type: int
  
4726
			if item.Kind == 2 || item.Kind == 3 { // Method or Function
  
4727
				if item.LabelDetails.Detail != "" {
  
4728
					info = item.LabelDetails.Detail
  
4729
				} else if item.LabelDetails.Description != "" {
  
4730
					info = item.LabelDetails.Description
  
4731
				}
  
4732
			} else {
  
4733
				// For variables/others, prefer Description (type)
  
4734
				if item.LabelDetails.Description != "" {
  
4735
					info = item.LabelDetails.Description
  
4736
				} else if item.LabelDetails.Detail != "" {
  
4737
					info = item.LabelDetails.Detail
  
4738
				}
  
4739
			}
4647
		}
4740
		}
  
4741
  
  
4742
		// 2. Fallback to Detail if still empty
  
4743
		if info == "" && item.Detail != "" {
  
4744
			info = item.Detail
  
4745
			// Some servers put the whole label in the detail, strip it if so
  
4746
			cleanLabel := item.Label
  
4747
			for strings.HasPrefix(cleanLabel, "•") || strings.HasPrefix(cleanLabel, " ") {
  
4748
				cleanLabel = strings.TrimPrefix(cleanLabel, "•")
  
4749
				cleanLabel = strings.TrimPrefix(cleanLabel, " ")
  
4750
			}
  
4751
			if strings.HasPrefix(info, cleanLabel) {
  
4752
				info = strings.TrimSpace(strings.TrimPrefix(info, cleanLabel))
  
4753
			}
  
4754
		}
  
4755
  
  
4756
		return strings.TrimSpace(info)
4648
	}
4757
	}
4649
  
4758
  
4650
	// Calculate total width: label + separator + detail
4759
	// Calculate max width for the combined display
4651
	maxWidth := 0
4760
	maxTotalWidth := 0
4652
	for _, item := range e.autocompleteItems {
4761
	type itemDisplay struct {
4653
		displayText := item.Label
4762
		label string
4654
		if item.Detail != "" {
4763
		sig   string
4655
			// Pad label to align, then add arrow and detail
4764
	}
4656
			padding := maxLabelWidth - len(item.Label)
4765
	displayItems := make([]itemDisplay, len(e.autocompleteItems))
4657
			displayText = item.Label + strings.Repeat(" ", padding) + " " + item.Detail
4766
  
  
4767
	for i, item := range e.autocompleteItems {
  
4768
		label := item.Label
  
4769
		for strings.HasPrefix(label, "•") || strings.HasPrefix(label, " ") {
  
4770
			label = strings.TrimPrefix(label, "•")
  
4771
			label = strings.TrimPrefix(label, " ")
4658
		}
4772
		}
4659
		if len(displayText) > maxWidth {
4773
  
4660
			maxWidth = len(displayText)
4774
		extra := getExtraInfo(item)
  
4775
		displayItems[i] = itemDisplay{label: label, sig: extra}
  
4776
  
  
4777
		width := len(label)
  
4778
		if extra != "" {
  
4779
			width += len(extra) + 1 // +1 for spacing
  
4780
		}
  
4781
		if width > maxTotalWidth {
  
4782
			maxTotalWidth = width
4661
		}
4783
		}
4662
	}
4784
	}
4663
  
4785
  
4664
	// Cap width to terminal width
4786
	maxWidth := maxTotalWidth
4665
	if maxWidth > w-10 {
4787
	if maxWidth > 80 {
4666
		maxWidth = w - 10
4788
		maxWidth = 80
  
4789
	}
  
4790
	// Leave space for gutter and borders
  
4791
	if maxWidth > w-Config.GutterWidth-4 {
  
4792
		maxWidth = w - Config.GutterWidth - 4
4667
	}
4793
	}
4668
  
4794
  
4669
	popupWidth := maxWidth + 2
4795
	popupWidth := maxWidth + 2
...
4680
	startX := cursorScreenX
4806
	startX := cursorScreenX
4681
	startY := cursorScreenY + 1
4807
	startY := cursorScreenY + 1
4682
  
4808
  
4683
	// Adjust if out of bounds
  
4684
	if startY+popupHeight > h-1 {
4809
	if startY+popupHeight > h-1 {
4685
		startY = cursorScreenY - popupHeight
4810
		startY = cursorScreenY - popupHeight
4686
	}
4811
	}
...
4691
		startX = 0
4816
		startX = 0
4692
	}
4817
	}
4693
  
4818
  
4694
	fg, bg := GetThemeColor(ColorAutocompleteWindow)
4819
	_, bg := GetThemeColor(ColorAutocompleteWindow)
4695
	selFg, selBg := GetThemeColor(ColorAutocompleteSelected)
4820
	selFg, selBg := GetThemeColor(ColorAutocompleteSelected)
  
4821
  
  
4822
	blackFg := termbox.ColorBlack
  
4823
	darkGrayFg := termbox.Attribute(244) // Dark gray in 256-color palette
4696
  
4824
  
4697
	// Draw background and content
4825
	// Draw background and content
4698
	for y := 0; y < popupHeight; y++ {
4826
	for y := 0; y < popupHeight; y++ {
4699
		itemIdx := y + e.autocompleteScroll
4827
		itemIdx := y + e.autocompleteScroll
4700
		if itemIdx >= len(e.autocompleteItems) {
4828
		if itemIdx >= len(displayItems) {
4701
			break
4829
			break
4702
		}
4830
		}
4703
		item := e.autocompleteItems[itemIdx]
4831
		item := displayItems[itemIdx]
  
4832
  
  
4833
		currentBg := bg
  
4834
		labelFg := blackFg
  
4835
		sigFg := darkGrayFg
4704
  
4836
  
4705
		currentFg, currentBg := fg, bg
  
4706
		if itemIdx == e.autocompleteIndex {
4837
		if itemIdx == e.autocompleteIndex {
4707
			currentFg, currentBg = selFg, selBg
4838
			currentBg = selBg
  
4839
			labelFg = selFg
  
4840
			sigFg = selFg // Use selected foreground for both in selection
4708
		}
4841
		}
4709
  
4842
  
4710
		// Fill line
4843
		// Fill line
4711
		for x := 0; x < popupWidth; x++ {
4844
		for x := 0; x < popupWidth; x++ {
4712
			termbox.SetCell(startX+x, startY+y, ' ', currentFg, currentBg)
4845
			termbox.SetCell(startX+x, startY+y, ' ', labelFg, currentBg)
4713
		}
4846
		}
4714
  
4847
  
4715
		// Draw label and detail (signature) with alignment
4848
		// Draw label
4716
		displayText := item.Label
4849
		lx := 0
4717
		if item.Detail != "" {
4850
		for _, r := range item.label {
4718
			// Pad label to align with others
4851
			if lx < maxWidth {
4719
			padding := maxLabelWidth - len(item.Label)
4852
				termbox.SetCell(startX+1+lx, startY+y, r, labelFg, currentBg)
4720
			displayText = item.Label + strings.Repeat(" ", padding) + "  " + item.Detail
4853
				lx++
  
4854
			}
4721
		}
4855
		}
4722
		if len(displayText) > maxWidth {
4856
  
4723
			displayText = displayText[:maxWidth-3] + "..."
4857
		// Draw signature if it fits
4724
		}
4858
		if item.sig != "" && lx < maxWidth-1 {
4725
		for j, r := range displayText {
4859
			termbox.SetCell(startX+1+lx, startY+y, ' ', sigFg, currentBg)
4726
			termbox.SetCell(startX+1+j, startY+y, r, currentFg, currentBg)
4860
			lx++
  
4861
			for _, r := range item.sig {
  
4862
				if lx < maxWidth {
  
4863
					termbox.SetCell(startX+1+lx, startY+y, r, sigFg, currentBg)
  
4864
					lx++
  
4865
				}
  
4866
			}
4727
		}
4867
		}
4728
	}
4868
	}
4729
}
4869
}
...
4749
  
4889
  
4750
	// Text to insert
4890
	// Text to insert
4751
	insertText := item.InsertText
4891
	insertText := item.InsertText
4752
	if insertText == "" {
4892
	if item.TextEdit != nil {
  
4893
		insertText = item.TextEdit.NewText
  
4894
	} else if insertText == "" {
4753
		insertText = item.Label
4895
		insertText = item.Label
4754
	}
4896
	}
4755
  
4897
  
...
diff --git a/ftypes.go b/ftypes.go
...
8
// FileType represents the configuration for a specific programming language.
8
// FileType represents the configuration for a specific programming language.
9
type FileType struct {
9
type FileType struct {
10
	Name             string   // Display name of the file type.
10
	Name             string   // Display name of the file type.
  
11
	LanguageID       string   // LSP language identifier (e.g., "cpp", "typescriptreact").
11
	Extensions       []string // File extensions (e.g., .go, .py) or filenames (e.g., Makefile).
12
	Extensions       []string // File extensions (e.g., .go, .py) or filenames (e.g., Makefile).
12
	UseTabs          bool     // Whether to use tabs for indentation.
13
	UseTabs          bool     // Whether to use tabs for indentation.
13
	Comment          string   // Single-line comment prefix (e.g., // or #).
14
	Comment          string   // Single-line comment prefix (e.g., // or #).
...
21
// fileTypes is a global list of all supported languages in the editor.
22
// fileTypes is a global list of all supported languages in the editor.
22
var fileTypes = []*FileType{
23
var fileTypes = []*FileType{
23
	{
24
	{
24
		Name:       "Go",
25
		Name:           "Go",
25
		Extensions: []string{".go"},
26
		LanguageID:     "go",
26
		UseTabs:    true,
27
		Extensions:     []string{".go"},
27
		Comment:    "//",
28
		UseTabs:        true,
28
		TabWidth:   Config.DefaultTabWidth,
29
		Comment:        "//",
29
		EnableLSP:  true,
30
		TabWidth:       Config.DefaultTabWidth,
30
		LSPCommand: "gopls",
31
		EnableLSP:      true,
  
32
		LSPCommand:     "gopls",
  
33
		LSPCommandArgs: []string{"serve"},
31
	},
34
	},
32
	{
35
	{
33
		Name:       "C",
36
		Name:       "C",
  
37
		LanguageID: "c",
34
		Extensions: []string{".c", ".h"},
38
		Extensions: []string{".c", ".h"},
35
		UseTabs:    true,
39
		UseTabs:    true,
36
		Comment:    "//",
40
		Comment:    "//",
...
40
	},
44
	},
41
	{
45
	{
42
		Name:       "C++",
46
		Name:       "C++",
  
47
		LanguageID: "cpp",
43
		Extensions: []string{".cpp", ".hpp", ".cc", ".hh", ".cxx", ".hxx"},
48
		Extensions: []string{".cpp", ".hpp", ".cc", ".hh", ".cxx", ".hxx"},
44
		UseTabs:    true,
49
		UseTabs:    true,
45
		Comment:    "//",
50
		Comment:    "//",
...
49
	},
54
	},
50
	{
55
	{
51
		Name:           "JavaScript",
56
		Name:           "JavaScript",
  
57
		LanguageID:     "javascript",
52
		Extensions:     []string{".js"},
58
		Extensions:     []string{".js"},
53
		UseTabs:        true,
59
		UseTabs:        true,
54
		Comment:        "//",
60
		Comment:        "//",
...
59
	},
65
	},
60
	{
66
	{
61
		Name:           "TypeScript",
67
		Name:           "TypeScript",
  
68
		LanguageID:     "typescript",
62
		Extensions:     []string{".ts"},
69
		Extensions:     []string{".ts"},
63
		UseTabs:        true,
70
		UseTabs:        true,
64
		Comment:        "//",
71
		Comment:        "//",
...
69
	},
76
	},
70
	{
77
	{
71
		Name:           "TSX",
78
		Name:           "TSX",
  
79
		LanguageID:     "typescriptreact",
72
		Extensions:     []string{".tsx"},
80
		Extensions:     []string{".tsx"},
73
		UseTabs:        true,
81
		UseTabs:        true,
74
		Comment:        "//",
82
		Comment:        "//",
...
79
	},
87
	},
80
	{
88
	{
81
		Name:           "Python",
89
		Name:           "Python",
  
90
		LanguageID:     "python",
82
		Extensions:     []string{".py"},
91
		Extensions:     []string{".py"},
83
		UseTabs:        false,
92
		UseTabs:        false,
84
		Comment:        "#",
93
		Comment:        "#",
85
		TabWidth:       Config.DefaultTabWidth,
94
		TabWidth:       Config.DefaultTabWidth,
  
95
		EnableLSP:      true,
86
		LSPCommand:     "pyright-langserver",
96
		LSPCommand:     "pyright-langserver",
87
		LSPCommandArgs: []string{"--stdio"},
97
		LSPCommandArgs: []string{"--stdio"},
88
	},
98
	},
89
	{
99
	{
90
		Name:       "Bash",
100
		Name:       "Bash",
  
101
		LanguageID: "shellscript",
91
		Extensions: []string{".sh"},
102
		Extensions: []string{".sh"},
92
		UseTabs:    true,
103
		UseTabs:    true,
93
		Comment:    "#",
104
		Comment:    "#",
...
95
	},
106
	},
96
	{
107
	{
97
		Name:       "CSS",
108
		Name:       "CSS",
  
109
		LanguageID: "css",
98
		Extensions: []string{".css"},
110
		Extensions: []string{".css"},
99
		UseTabs:    false,
111
		UseTabs:    false,
100
		Comment:    "//",
112
		Comment:    "//",
...
102
	},
114
	},
103
	{
115
	{
104
		Name:       "Dockerfile",
116
		Name:       "Dockerfile",
  
117
		LanguageID: "dockerfile",
105
		Extensions: []string{".dockerfile", "Dockerfile"},
118
		Extensions: []string{".dockerfile", "Dockerfile"},
106
		UseTabs:    false,
119
		UseTabs:    false,
107
		Comment:    "#",
120
		Comment:    "#",
...
109
	},
122
	},
110
	{
123
	{
111
		Name:       "HTML",
124
		Name:       "HTML",
  
125
		LanguageID: "html",
112
		Extensions: []string{".html", ".htm"},
126
		Extensions: []string{".html", ".htm"},
113
		UseTabs:    false,
127
		UseTabs:    false,
114
		Comment:    "",
128
		Comment:    "",
...
116
	},
130
	},
117
	{
131
	{
118
		Name:       "Lua",
132
		Name:       "Lua",
  
133
		LanguageID: "lua",
119
		Extensions: []string{".lua"},
134
		Extensions: []string{".lua"},
120
		UseTabs:    true,
135
		UseTabs:    true,
121
		Comment:    "--",
136
		Comment:    "--",
...
123
	},
138
	},
124
	{
139
	{
125
		Name:       "Markdown",
140
		Name:       "Markdown",
  
141
		LanguageID: "markdown",
126
		Extensions: []string{".md", ".markdown"},
142
		Extensions: []string{".md", ".markdown"},
127
		UseTabs:    false,
143
		UseTabs:    false,
128
		Comment:    "",
144
		Comment:    "",
...
130
	},
146
	},
131
	{
147
	{
132
		Name:       "PHP",
148
		Name:       "PHP",
  
149
		LanguageID: "php",
133
		Extensions: []string{".php"},
150
		Extensions: []string{".php"},
134
		UseTabs:    true,
151
		UseTabs:    true,
135
		Comment:    "//",
152
		Comment:    "//",
...
137
	},
154
	},
138
	{
155
	{
139
		Name:       "SQL",
156
		Name:       "SQL",
  
157
		LanguageID: "sql",
140
		Extensions: []string{".sql"},
158
		Extensions: []string{".sql"},
141
		UseTabs:    true,
159
		UseTabs:    true,
142
		Comment:    "--",
160
		Comment:    "--",
...
144
	},
162
	},
145
	{
163
	{
146
		Name:       "Makefile",
164
		Name:       "Makefile",
  
165
		LanguageID: "makefile",
147
		Extensions: []string{".make", "Makefile", "makefile"},
166
		Extensions: []string{".make", "Makefile", "makefile"},
148
		UseTabs:    true,
167
		UseTabs:    true,
149
		Comment:    "#",
168
		Comment:    "#",
...
151
	},
170
	},
152
	{
171
	{
153
		Name:       "Text",
172
		Name:       "Text",
  
173
		LanguageID: "plaintext",
154
		Extensions: []string{},
174
		Extensions: []string{},
155
		UseTabs:    false,
175
		UseTabs:    false,
156
		Comment:    "",
176
		Comment:    "",
...
diff --git a/kevent.go b/kevent.go
...
563
			if e.autocompleteIndex >= e.autocompleteScroll+10 {
563
			if e.autocompleteIndex >= e.autocompleteScroll+10 {
564
				e.autocompleteScroll = e.autocompleteIndex - 9
564
				e.autocompleteScroll = e.autocompleteIndex - 9
565
			}
565
			}
  
566
			e.resolveSelectedCompletion()
566
			return
567
			return
567
		case termbox.KeyArrowDown:
568
		case termbox.KeyArrowDown:
568
			e.autocompleteIndex++
569
			e.autocompleteIndex++
...
576
			if e.autocompleteIndex >= e.autocompleteScroll+10 {
577
			if e.autocompleteIndex >= e.autocompleteScroll+10 {
577
				e.autocompleteScroll = e.autocompleteIndex - 9
578
				e.autocompleteScroll = e.autocompleteIndex - 9
578
			}
579
			}
  
580
			e.resolveSelectedCompletion()
579
			return
581
			return
580
		case termbox.KeyEnter:
582
		case termbox.KeyEnter:
581
			e.insertCompletion(e.autocompleteItems[e.autocompleteIndex])
583
			e.insertCompletion(e.autocompleteItems[e.autocompleteIndex])
...
diff --git a/lsp.go b/lsp.go
...
37
  
37
  
38
	responses     map[int64]chan map[string]interface{} // Map of request IDs to response channels.
38
	responses     map[int64]chan map[string]interface{} // Map of request IDs to response channels.
39
	responseMutex sync.Mutex
39
	responseMutex sync.Mutex
40
	fileType      *FileType // Associated file type for language ID.
40
	writeMutex    sync.Mutex // Protects concurrent writes to stdin.
  
41
	fileType      *FileType  // Associated file type for language ID.
41
}
42
}
42
  
43
  
43
// Position in a document (0-based line and character).
44
// Position in a document (0-based line and character).
...
60
  
61
  
61
// CompletionItem represents a suggestion for completion.
62
// CompletionItem represents a suggestion for completion.
62
type CompletionItem struct {
63
type CompletionItem struct {
63
	Label         string `json:"label"`
64
	Label         string               `json:"label"`
64
	Kind          int    `json:"kind"`
65
	LabelDetails  *CompletionItemLabel `json:"labelDetails"`
65
	Detail        string `json:"detail"`
66
	Kind          int                  `json:"kind"`
66
	Documentation string `json:"documentation"`
67
	Detail        string               `json:"detail"`
67
	InsertText    string `json:"insertText"`
68
	Documentation interface{}          `json:"documentation"`
  
69
	InsertText    string               `json:"insertText"`
  
70
	FilterText    string               `json:"filterText"`
  
71
	TextEdit      *TextEdit            `json:"textEdit"`
  
72
	Data          interface{}          `json:"data"` // Opaque data for resolve request
  
73
}
  
74
  
  
75
type CompletionItemLabel struct {
  
76
	Detail      string `json:"detail"`      // e.g. (int a, int b)
  
77
	Description string `json:"description"` // e.g. int
  
78
}
  
79
  
  
80
type TextEdit struct {
  
81
	Range   Range  `json:"range"`
  
82
	NewText string `json:"newText"`
68
}
83
}
69
  
84
  
70
// CompletionList represents a collection of completion items.
85
// CompletionList represents a collection of completion items.
...
108
	// Launch the language server's executable.
123
	// Launch the language server's executable.
109
	client.cmd = exec.Command(ft.LSPCommand, ft.LSPCommandArgs...)
124
	client.cmd = exec.Command(ft.LSPCommand, ft.LSPCommandArgs...)
110
  
125
  
111
	// Suppress the server's own internal log messages (stderr).
126
	// Suppress the server's own internal log messages (stderr) and redirect to logCallback.
112
	devNull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0)
127
	stderr, err := client.cmd.StderrPipe()
113
	if err == nil {
128
	if err == nil {
114
		client.cmd.Stderr = devNull
129
		go func() {
  
130
			scanner := bufio.NewScanner(stderr)
  
131
			for scanner.Scan() {
  
132
				if client.logCallback != nil {
  
133
					client.logCallback("LSP-stderr", scanner.Text())
  
134
				}
  
135
			}
  
136
		}()
115
	}
137
	}
116
  
138
  
117
	client.stdin, err = client.cmd.StdinPipe()
139
	client.stdin, err = client.cmd.StdinPipe()
...
123
	if err != nil {
145
	if err != nil {
124
		return nil, err
146
		return nil, err
125
	}
147
	}
  
148
  
  
149
	// Set working directory to the project root to help server find config files and resolve relative paths.
  
150
	client.cmd.Dir = findProjectRoot(absPath)
126
  
151
  
127
	if err := client.cmd.Start(); err != nil {
152
	if err := client.cmd.Start(); err != nil {
128
		return nil, err
153
		return nil, err
...
150
	return atomic.AddInt64(&c.messageID, 1)
175
	return atomic.AddInt64(&c.messageID, 1)
151
}
176
}
152
  
177
  
  
178
// Request sends a JSON-RPC request and waits for a response (up to 5s).
  
179
func (c *LSPClient) Request(method string, params interface{}) (map[string]interface{}, error) {
  
180
	id := c.nextID()
  
181
	responseChan := make(chan map[string]interface{}, 1)
  
182
	c.responseMutex.Lock()
  
183
	c.responses[id] = responseChan
  
184
	c.responseMutex.Unlock()
  
185
  
  
186
	if err := c.sendRequestWithID(id, method, params); err != nil {
  
187
		c.responseMutex.Lock()
  
188
		delete(c.responses, id)
  
189
		c.responseMutex.Unlock()
  
190
		return nil, err
  
191
	}
  
192
  
  
193
	select {
  
194
	case resp := <-responseChan:
  
195
		if errVal, ok := resp["error"]; ok {
  
196
			return nil, fmt.Errorf("LSP error: %v", errVal)
  
197
		}
  
198
		return resp, nil
  
199
	case <-time.After(10 * time.Second):
  
200
		c.responseMutex.Lock()
  
201
		delete(c.responses, id)
  
202
		c.responseMutex.Unlock()
  
203
		return nil, fmt.Errorf("LSP request timeout: %s", method)
  
204
	}
  
205
}
  
206
  
153
// sendRequest sends a JSON-RPC request and expects a response.
207
// sendRequest sends a JSON-RPC request and expects a response.
154
func (c *LSPClient) sendRequest(method string, params interface{}) error {
208
func (c *LSPClient) sendRequest(method string, params interface{}) error {
155
	id := c.nextID()
209
	_, err := c.Request(method, params)
156
	request := map[string]interface{}{
210
	return err
157
		"jsonrpc": "2.0",
  
158
		"id":      id,
  
159
		"method":  method,
  
160
		"params":  params,
  
161
	}
  
162
	return c.sendMessage(request)
  
163
}
211
}
164
  
212
  
165
// sendNotification sends a JSON-RPC message without expecting a response.
213
// sendNotification sends a JSON-RPC message without expecting a response.
...
183
		return err
231
		return err
184
	}
232
	}
185
  
233
  
  
234
	if c.logCallback != nil {
  
235
		msgStr := string(data)
  
236
		if len(msgStr) > 500 {
  
237
			msgStr = msgStr[:500] + "..."
  
238
		}
  
239
		c.logCallback("LSP-send", msgStr)
  
240
	}
  
241
  
186
	// LSP messages use a header similar to HTTP: Content-Length followed by \r\n\r\n.
242
	// LSP messages use a header similar to HTTP: Content-Length followed by \r\n\r\n.
187
	content := fmt.Sprintf("Content-Length: %d\r\n\r\n%s", len(data), data)
243
	content := fmt.Sprintf("Content-Length: %d\r\n\r\n%s", len(data), data)
  
244
  
  
245
	c.writeMutex.Lock()
  
246
	defer c.writeMutex.Unlock()
188
	_, err = c.stdin.Write([]byte(content))
247
	_, err = c.stdin.Write([]byte(content))
189
	return err
248
	return err
190
}
249
}
...
211
				break
270
				break
212
			}
271
			}
213
  
272
  
  
273
			lowerLine := strings.ToLower(line)
214
			var length int
274
			var length int
215
			if n, _ := fmt.Sscanf(line, "Content-Length: %d", &length); n == 1 {
275
			if strings.HasPrefix(lowerLine, "content-length:") {
216
				contentLength = length
276
				if n, _ := fmt.Sscanf(lowerLine, "content-length: %d", &length); n == 1 {
  
277
					contentLength = length
  
278
				}
217
			}
279
			}
218
		}
280
		}
219
  
281
  
...
233
			continue
295
			continue
234
		}
296
		}
235
  
297
  
236
		// If the message has an "id", it's a response to a request we sent.
298
		// If the message has an "id", it's either a response to a request we sent
  
299
		// or a request from the server to us.
237
		if idVal, hasID := msg["id"]; hasID {
300
		if idVal, hasID := msg["id"]; hasID {
238
			if c.logCallback != nil {
301
			method, isServerRequest := msg["method"].(string)
239
				c.logCallback("LSP", fmt.Sprintf("Received response with ID: %v (type: %T)", idVal, idVal))
302
  
240
			}
303
			if isServerRequest {
241
			if id, ok := idVal.(float64); ok {
  
242
				idInt := int64(id)
  
243
				if c.logCallback != nil {
304
				if c.logCallback != nil {
244
					c.logCallback("LSP", fmt.Sprintf("Looking for response channel with ID=%d", idInt))
305
					c.logCallback("LSP", fmt.Sprintf("Received server request: %s (ID: %v)", method, idVal))
245
				}
306
				}
246
				c.responseMutex.Lock()
307
				// Handle server-to-client requests.
247
				ch, exists := c.responses[idInt]
308
				c.handleServerRequest(method, idVal, msg["params"])
248
				if exists {
  
249
					if c.logCallback != nil {
  
250
						c.logCallback("LSP", fmt.Sprintf("Found channel for ID=%d, sending response", idInt))
  
251
					}
  
252
					delete(c.responses, idInt)
  
253
					c.responseMutex.Unlock()
  
254
					ch <- msg // Send response to the goroutine waiting for it.
  
255
				} else {
  
256
					if c.logCallback != nil {
  
257
						c.logCallback("LSP", fmt.Sprintf("No channel found for ID=%d", idInt))
  
258
					}
  
259
					c.responseMutex.Unlock()
  
260
				}
  
261
			} else {
309
			} else {
262
				if c.logCallback != nil {
310
				if c.logCallback != nil {
263
					c.logCallback("LSP", fmt.Sprintf("Failed to convert ID to int64: %v", idVal))
311
					c.logCallback("LSP", fmt.Sprintf("Received response for ID: %v", idVal))
  
312
				}
  
313
  
  
314
				var idInt int64
  
315
				validID := false
  
316
				switch v := idVal.(type) {
  
317
				case float64:
  
318
					idInt = int64(v)
  
319
					validID = true
  
320
				case string:
  
321
					fmt.Sscanf(v, "%d", &idInt)
  
322
					validID = true
  
323
				}
  
324
  
  
325
				if validID {
  
326
					c.responseMutex.Lock()
  
327
					ch, exists := c.responses[idInt]
  
328
					if exists {
  
329
						delete(c.responses, idInt)
  
330
						c.responseMutex.Unlock()
  
331
						ch <- msg
  
332
					} else {
  
333
						if c.logCallback != nil {
  
334
							c.logCallback("LSP", fmt.Sprintf("No channel found for ID=%d", idInt))
  
335
						}
  
336
						c.responseMutex.Unlock()
  
337
					}
264
				}
338
				}
265
			}
339
			}
266
		}
340
		}
267
  
341
  
268
		// If it has no "id", it's an asynchronous notification (like updated diagnostics).
342
		// If it has no "id", it's an asynchronous notification.
269
		if _, hasID := msg["id"]; !hasID {
343
		if _, hasID := msg["id"]; !hasID {
270
			c.handleNotification(msg)
344
			c.handleNotification(msg)
271
		}
345
		}
272
	}
346
	}
  
347
}
  
348
  
  
349
// handleServerRequest responds to requests initiated by the server.
  
350
func (c *LSPClient) handleServerRequest(method string, id interface{}, params interface{}) {
  
351
	// For now, we provide minimal responses to keep the server happy.
  
352
	var result interface{} = nil
  
353
  
  
354
	if method == "workspace/configuration" {
  
355
		// Return empty settings for any requested scope.
  
356
		if p, ok := params.(map[string]interface{}); ok {
  
357
			if items, ok := p["items"].([]interface{}); ok {
  
358
				res := make([]interface{}, len(items))
  
359
				for i := range res {
  
360
					res[i] = map[string]interface{}{}
  
361
				}
  
362
				result = res
  
363
			}
  
364
		}
  
365
	}
  
366
  
  
367
	response := map[string]interface{}{
  
368
		"jsonrpc": "2.0",
  
369
		"id":      id,
  
370
		"result":  result,
  
371
	}
  
372
	c.sendMessage(response)
273
}
373
}
274
  
374
  
275
// handleNotification processes messages initiated by the server.
375
// handleNotification processes messages initiated by the server.
...
314
	}
414
	}
315
}
415
}
316
  
416
  
  
417
// findProjectRoot looks for a project root marker like .git, compile_commands.json, or .clangd.
  
418
func findProjectRoot(path string) string {
  
419
	dir := filepath.Dir(path)
  
420
	for {
  
421
		if _, err := os.Stat(filepath.Join(dir, ".git")); err == nil {
  
422
			return dir
  
423
		}
  
424
		if _, err := os.Stat(filepath.Join(dir, "compile_commands.json")); err == nil {
  
425
			return dir
  
426
		}
  
427
		if _, err := os.Stat(filepath.Join(dir, ".clangd")); err == nil {
  
428
			return dir
  
429
		}
  
430
		if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil {
  
431
			return dir
  
432
		}
  
433
  
  
434
		parent := filepath.Dir(dir)
  
435
		if parent == dir {
  
436
			break
  
437
		}
  
438
		dir = parent
  
439
	}
  
440
	return filepath.Dir(path)
  
441
}
  
442
  
317
// initialize sends the initial 'initialize' request to the server.
443
// initialize sends the initial 'initialize' request to the server.
318
func (c *LSPClient) initialize() error {
444
func (c *LSPClient) initialize() error {
319
	rootURI := "file://" + filepath.Dir(c.filename)
445
	rootPath := findProjectRoot(c.filename)
  
446
	rootURI := "file://" + rootPath
320
	params := map[string]interface{}{
447
	params := map[string]interface{}{
321
		"processId": os.Getpid(),
448
		"processId": os.Getpid(),
322
		"rootUri":   rootURI,
449
		"rootUri":   rootURI,
  
450
		"rootPath":  rootPath, // Deprecated but some servers still use it.
  
451
		"workspaceFolders": []map[string]interface{}{
  
452
			{
  
453
				"uri":  rootURI,
  
454
				"name": filepath.Base(rootPath),
  
455
			},
  
456
		},
323
		"capabilities": map[string]interface{}{
457
		"capabilities": map[string]interface{}{
324
			"textDocument": map[string]interface{}{
458
			"textDocument": map[string]interface{}{
  
459
				"synchronization": map[string]interface{}{
  
460
					"didSave":             true,
  
461
					"dynamicRegistration": false,
  
462
					"willSave":            false,
  
463
					"willSaveWaitUntil":   false,
  
464
				},
325
				"publishDiagnostics": map[string]interface{}{},
465
				"publishDiagnostics": map[string]interface{}{},
326
				"hover": map[string]interface{}{
466
				"hover": map[string]interface{}{
327
					"contentFormat": []string{"plaintext"},
467
					"contentFormat": []string{"plaintext"},
328
				},
468
				},
329
				"completion": map[string]interface{}{
469
				"completion": map[string]interface{}{
330
					"completionItem": map[string]interface{}{
470
					"completionItem": map[string]interface{}{
331
						"snippetSupport": false,
471
						"snippetSupport":          false,
  
472
						"resolveSupport":          map[string]interface{}{"properties": []string{"documentation", "detail"}},
  
473
						"insertReplaceSupport":    true,
  
474
						"labelDetailsSupport":     true,
  
475
						"deprecatedSupport":       true,
  
476
						"commitCharactersSupport": false,
332
					},
477
					},
  
478
					"contextSupport": true,
333
				},
479
				},
  
480
				"definition": map[string]interface{}{
  
481
					"dynamicRegistration": false,
  
482
					"linkSupport":         false,
  
483
				},
  
484
			},
  
485
			"workspace": map[string]interface{}{
  
486
				"configuration":    true,
  
487
				"workspaceFolders": true,
334
			},
488
			},
335
		},
489
		},
336
	}
490
	}
337
  
491
  
  
492
	// Move textDocumentSync to top level of capabilities if needed by some servers,
  
493
	// though it's technically under textDocument in some versions.
  
494
	// Actually, the spec says it should be under capabilities for server capabilities,
  
495
	// but for client capabilities it is under textDocument.
  
496
	// However, many servers like gopls prefer it at a certain location.
  
497
	// Let's add it to the top level of capabilities as well just in case.
  
498
	params["capabilities"].(map[string]interface{})["textDocumentSync"] = 1 // Full
  
499
  
338
	if err := c.sendRequest("initialize", params); err != nil {
500
	if err := c.sendRequest("initialize", params); err != nil {
339
		return err
501
		return err
340
	}
502
	}
...
344
  
506
  
345
// sendDidOpen notifies the server that a file has been opened.
507
// sendDidOpen notifies the server that a file has been opened.
346
func (c *LSPClient) sendDidOpen(content string) error {
508
func (c *LSPClient) sendDidOpen(content string) error {
347
	languageID := strings.ToLower(c.fileType.Name)
509
	languageID := c.fileType.LanguageID
  
510
	if languageID == "" {
  
511
		languageID = strings.ToLower(c.fileType.Name)
  
512
	}
348
	params := map[string]interface{}{
513
	params := map[string]interface{}{
349
		"textDocument": map[string]interface{}{
514
		"textDocument": map[string]interface{}{
350
			"uri":        c.uri,
515
			"uri":        c.uri,
...
384
  
549
  
385
// Definition requests the location of the definition of the symbol at cursor.
550
// Definition requests the location of the definition of the symbol at cursor.
386
func (c *LSPClient) Definition(line, character int) ([]Location, error) {
551
func (c *LSPClient) Definition(line, character int) ([]Location, error) {
387
	id := c.nextID()
  
388
	params := map[string]interface{}{
552
	params := map[string]interface{}{
389
		"textDocument": map[string]interface{}{
553
		"textDocument": map[string]interface{}{
390
			"uri": c.uri,
554
			"uri": c.uri,
...
395
		},
559
		},
396
	}
560
	}
397
  
561
  
398
	responseChan := make(chan map[string]interface{}, 1)
562
	resp, err := c.Request("textDocument/definition", params)
399
	c.responseMutex.Lock()
563
	if err != nil {
400
	c.responses[id] = responseChan
  
401
	c.responseMutex.Unlock()
  
402
  
  
403
	if err := c.sendRequestWithID(id, "textDocument/definition", params); err != nil {
  
404
		c.responseMutex.Lock()
  
405
		delete(c.responses, id)
  
406
		c.responseMutex.Unlock()
  
407
		return nil, err
564
		return nil, err
408
	}
565
	}
409
  
566
  
410
	select {
567
	result := resp["result"]
411
	case resp := <-responseChan:
568
	if result == nil {
412
		if err, ok := resp["error"]; ok {
569
		return nil, nil
413
			return nil, fmt.Errorf("LSP error: %v", err)
570
	}
414
		}
  
415
  
  
416
		result := resp["result"]
  
417
		if result == nil {
  
418
			return nil, nil
  
419
		}
  
420
  
  
421
		resJSON, _ := json.Marshal(result)
  
422
  
571
  
423
		// Definition can return a single Location or an array of them.
572
	resJSON, _ := json.Marshal(result)
424
		var loc Location
  
425
		if err := json.Unmarshal(resJSON, &loc); err == nil && loc.URI != "" {
  
426
			return []Location{loc}, nil
  
427
		}
  
428
  
573
  
429
		var locs []Location
574
	// Definition can return a single Location or an array of them.
430
		if err := json.Unmarshal(resJSON, &locs); err == nil {
575
	var loc Location
431
			return locs, nil
576
	if err := json.Unmarshal(resJSON, &loc); err == nil && loc.URI != "" {
432
		}
577
		return []Location{loc}, nil
  
578
	}
433
  
579
  
434
		return nil, nil
580
	var locs []Location
435
	case <-time.After(5 * time.Second):
581
	if err := json.Unmarshal(resJSON, &locs); err == nil {
436
		c.responseMutex.Lock()
582
		return locs, nil
437
		delete(c.responses, id)
  
438
		c.responseMutex.Unlock()
  
439
		return nil, fmt.Errorf("LSP request timeout")
  
440
	}
583
	}
  
584
  
  
585
	return nil, nil
441
}
586
}
442
  
587
  
443
// Hover requests documentation information for the symbol at cursor.
588
// Hover requests documentation information for the symbol at cursor.
444
func (c *LSPClient) Hover(line, character int) (string, error) {
589
func (c *LSPClient) Hover(line, character int) (string, error) {
445
	id := c.nextID()
  
446
	params := map[string]interface{}{
590
	params := map[string]interface{}{
447
		"textDocument": map[string]interface{}{
591
		"textDocument": map[string]interface{}{
448
			"uri": c.uri,
592
			"uri": c.uri,
...
453
		},
597
		},
454
	}
598
	}
455
  
599
  
456
	responseChan := make(chan map[string]interface{}, 1)
600
	resp, err := c.Request("textDocument/hover", params)
457
	c.responseMutex.Lock()
601
	if err != nil {
458
	c.responses[id] = responseChan
  
459
	c.responseMutex.Unlock()
  
460
  
  
461
	if err := c.sendRequestWithID(id, "textDocument/hover", params); err != nil {
  
462
		c.responseMutex.Lock()
  
463
		delete(c.responses, id)
  
464
		c.responseMutex.Unlock()
  
465
		return "", err
602
		return "", err
466
	}
603
	}
467
  
604
  
468
	select {
605
	result := resp["result"]
469
	case resp := <-responseChan:
606
	if result == nil {
470
		if err, ok := resp["error"]; ok {
607
		return "", nil
471
			return "", fmt.Errorf("LSP error: %v", err)
608
	}
472
		}
  
473
  
609
  
474
		result := resp["result"]
610
	// Hover responses are complex: they can be strings, objects, or arrays.
475
		if result == nil {
611
	resMap, ok := result.(map[string]interface{})
476
			return "", nil
612
	if !ok {
477
		}
613
		return "", nil
  
614
	}
478
  
615
  
479
		// Hover responses are complex: they can be strings, objects, or arrays.
616
	contents := resMap["contents"]
480
		resMap, ok := result.(map[string]interface{})
617
	if contents == nil {
481
		if !ok {
618
		return "", nil
482
			return "", nil
619
	}
483
		}
  
484
  
620
  
485
		contents := resMap["contents"]
621
	if mc, ok := contents.(map[string]interface{}); ok {
486
		if contents == nil {
622
		if val, ok := mc["value"].(string); ok {
487
			return "", nil
623
			return stripMarkdown(val), nil
488
		}
624
		}
  
625
	}
489
  
626
  
490
		if mc, ok := contents.(map[string]interface{}); ok {
627
	if s, ok := contents.(string); ok {
491
			if val, ok := mc["value"].(string); ok {
628
		return stripMarkdown(s), nil
492
				return stripMarkdown(val), nil
629
	}
493
			}
  
494
		}
  
495
  
630
  
496
		if s, ok := contents.(string); ok {
631
	if ss, ok := contents.([]interface{}); ok {
497
			return stripMarkdown(s), nil
632
		var result strings.Builder
498
		}
633
		for i, s := range ss {
499
  
634
			if str, ok := s.(string); ok {
500
		if ss, ok := contents.([]interface{}); ok {
635
				result.WriteString(stripMarkdown(str))
501
			var result strings.Builder
636
				if i < len(ss)-1 {
502
			for i, s := range ss {
637
					result.WriteString("\n")
503
				if str, ok := s.(string); ok {
638
				}
504
					result.WriteString(stripMarkdown(str))
639
			} else if m, ok := s.(map[string]interface{}); ok {
  
640
				if val, ok := m["value"].(string); ok {
  
641
					result.WriteString(stripMarkdown(val))
505
					if i < len(ss)-1 {
642
					if i < len(ss)-1 {
506
						result.WriteString("\n")
643
						result.WriteString("\n")
507
					}
644
					}
508
				} else if m, ok := s.(map[string]interface{}); ok {
  
509
					if val, ok := m["value"].(string); ok {
  
510
						result.WriteString(stripMarkdown(val))
  
511
						if i < len(ss)-1 {
  
512
							result.WriteString("\n")
  
513
						}
  
514
					}
  
515
				}
645
				}
516
			}
646
			}
517
			return strings.TrimSpace(result.String()), nil
  
518
		}
647
		}
  
648
		return strings.TrimSpace(result.String()), nil
  
649
	}
519
  
650
  
520
		return "", nil
651
	return "", nil
521
	case <-time.After(5 * time.Second):
652
}
522
		c.responseMutex.Lock()
653
  
523
		delete(c.responses, id)
654
// ResolveCompletion requests additional details for a completion item.
524
		c.responseMutex.Unlock()
655
func (c *LSPClient) ResolveCompletion(item CompletionItem) (CompletionItem, error) {
525
		return "", fmt.Errorf("LSP request timeout")
656
	resp, err := c.Request("completionItem/resolve", item)
  
657
	if err != nil {
  
658
		return item, err
  
659
	}
  
660
  
  
661
	result := resp["result"]
  
662
	if result == nil {
  
663
		return item, nil
  
664
	}
  
665
  
  
666
	resJSON, _ := json.Marshal(result)
  
667
	var resolvedItem CompletionItem
  
668
	if err := json.Unmarshal(resJSON, &resolvedItem); err != nil {
  
669
		return item, err
  
670
	}
  
671
  
  
672
	return resolvedItem, nil
  
673
}
  
674
  
  
675
// getDocumentationString extracts a plain string from the Documentation field.
  
676
func (c *LSPClient) getDocumentationString(doc interface{}) string {
  
677
	if doc == nil {
  
678
		return ""
  
679
	}
  
680
	if s, ok := doc.(string); ok {
  
681
		return stripMarkdown(s)
  
682
	}
  
683
	if m, ok := doc.(map[string]interface{}); ok {
  
684
		if val, ok := m["value"].(string); ok {
  
685
			return stripMarkdown(val)
  
686
		}
526
	}
687
	}
  
688
	return ""
527
}
689
}
528
  
690
  
529
// Completion requests a list of completion items for the symbol at cursor.
691
// Completion requests a list of completion items for the symbol at cursor.
530
func (c *LSPClient) Completion(line, character int) ([]CompletionItem, error) {
692
func (c *LSPClient) Completion(line, character int) ([]CompletionItem, error) {
531
	id := c.nextID()
  
532
	params := map[string]interface{}{
693
	params := map[string]interface{}{
533
		"textDocument": map[string]interface{}{
694
		"textDocument": map[string]interface{}{
534
			"uri": c.uri,
695
			"uri": c.uri,
...
537
			"line":      line,
698
			"line":      line,
538
			"character": character,
699
			"character": character,
539
		},
700
		},
540
	}
701
		"context": map[string]interface{}{
541
  
702
			"triggerKind": 1, // Invited
542
	if c.logCallback != nil {
703
		},
543
		c.logCallback("LSP", fmt.Sprintf("Requesting completion at %d:%d (ID=%d)", line, character, id))
  
544
	}
704
	}
545
  
705
  
546
	responseChan := make(chan map[string]interface{}, 1)
706
	resp, err := c.Request("textDocument/completion", params)
547
	c.responseMutex.Lock()
707
	if err != nil {
548
	c.responses[id] = responseChan
  
549
	c.responseMutex.Unlock()
  
550
  
  
551
	if err := c.sendRequestWithID(id, "textDocument/completion", params); err != nil {
  
552
		c.responseMutex.Lock()
  
553
		delete(c.responses, id)
  
554
		c.responseMutex.Unlock()
  
555
		return nil, err
708
		return nil, err
556
	}
709
	}
557
  
710
  
558
	select {
711
	result := resp["result"]
559
	case resp := <-responseChan:
712
	if result == nil {
560
		if c.logCallback != nil {
713
		return nil, nil
561
			c.logCallback("LSP", fmt.Sprintf("Received completion response (ID=%d)", id))
714
	}
562
		}
  
563
		if err, ok := resp["error"]; ok {
  
564
			return nil, fmt.Errorf("LSP error: %v", err)
  
565
		}
  
566
  
715
  
567
		result := resp["result"]
716
	resJSON, _ := json.Marshal(result)
568
		if result == nil {
  
569
			return nil, nil
  
570
		}
  
571
  
717
  
572
		resJSON, _ := json.Marshal(result)
718
	// Completion can return a CompletionList or an array of CompletionItems.
  
719
	// Try unmarshaling into array of items first as it's more direct.
  
720
	var compItems []CompletionItem
  
721
	if err := json.Unmarshal(resJSON, &compItems); err == nil && compItems != nil {
  
722
		return compItems, nil
  
723
	}
573
  
724
  
574
		// Completion can return a CompletionList or an array of CompletionItems.
725
	var compList CompletionList
575
		var compList CompletionList
726
	if err := json.Unmarshal(resJSON, &compList); err == nil {
576
		if err := json.Unmarshal(resJSON, &compList); err == nil {
727
		return compList.Items, nil
577
			return compList.Items, nil
728
	}
578
		}
  
579
  
729
  
580
		var compItems []CompletionItem
730
	return nil, nil
581
		if err := json.Unmarshal(resJSON, &compItems); err == nil {
  
582
			return compItems, nil
  
583
		}
  
584
  
  
585
		return nil, nil
  
586
	case <-time.After(10 * time.Second):
  
587
		if c.logCallback != nil {
  
588
			c.logCallback("LSP", fmt.Sprintf("Completion request timed out (ID=%d)", id))
  
589
		}
  
590
		c.responseMutex.Lock()
  
591
		delete(c.responses, id)
  
592
		c.responseMutex.Unlock()
  
593
		return nil, fmt.Errorf("LSP request timeout")
  
594
	}
  
595
}
731
}
596
  
732
  
597
// sendRequestWithID helper to send a request with a pre-generated ID.
733
// sendRequestWithID helper to send a request with a pre-generated ID.
...