|
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/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. |
| ... |