Skip to content

Commit 8b86a79

Browse files
committed
fix: Fix completion errors in files without terminal newline
1 parent c3e665a commit 8b86a79

File tree

5 files changed

+141
-29
lines changed

5 files changed

+141
-29
lines changed

internal/app/handler.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func (h *Handler) Initialize(context.Context, lsp.ClientCapabilities) (*lsp.Serv
5656
func (h *Handler) Completion(_ context.Context, params lsp.CompletionParams) (*lsp.CompletionList, error) {
5757
currentWord, c, err := h.getCompletionContext(params)
5858
if err != nil {
59-
return nil, fmt.Errorf("failed to get current word: %w", err)
59+
return nil, fmt.Errorf("failed to get context: %w", err)
6060
}
6161

6262
// TODO(asnyder):
@@ -119,9 +119,15 @@ func (h *Handler) readBetweenPositions(doc *lsp.Document, startPos, endPos lsp.P
119119
if err != nil {
120120
return nil, fmt.Errorf("start position to offset: %w", err)
121121
}
122-
endOffset, err := doc.PositionToOffset(endPos)
123-
if err != nil {
124-
return nil, fmt.Errorf("end position to offset: %w", err)
122+
123+
var endOffset int
124+
if endPos.Line >= doc.Lines()-1 {
125+
endOffset = doc.Len()
126+
} else {
127+
endOffset, err = doc.PositionToOffset(endPos)
128+
if err != nil {
129+
return nil, fmt.Errorf("end position to offset: %w", err)
130+
}
125131
}
126132

127133
return io.ReadAll(io.NewSectionReader(doc, int64(startOffset), int64(endOffset-startOffset)))

internal/app/handler_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package app_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/google/go-cmp/cmp/cmpopts"
7+
. "github.com/onsi/gomega"
8+
9+
"github.com/armsnyder/gdshader-language-server/internal/app"
10+
"github.com/armsnyder/gdshader-language-server/internal/lsp"
11+
)
12+
13+
func TestHandler(t *testing.T) {
14+
t.Run("completion in empty document", func(t *testing.T) {
15+
g := NewWithT(t)
16+
var h app.Handler
17+
const uri = "file:///test.gdshader"
18+
19+
// Open the document.
20+
err := h.DidOpenTextDocument(t.Context(), lsp.DidOpenTextDocumentParams{
21+
TextDocument: lsp.TextDocumentItem{URI: uri},
22+
})
23+
g.Expect(err).ToNot(HaveOccurred(), "DidOpenTextDocument error")
24+
25+
// Type the first character.
26+
err = h.DidChangeTextDocument(t.Context(), lsp.DidChangeTextDocumentParams{
27+
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
28+
ContentChanges: []lsp.TextDocumentContentChangeEvent{{
29+
Range: &lsp.Range{},
30+
Text: "s",
31+
}},
32+
})
33+
g.Expect(err).ToNot(HaveOccurred(), "DidChangeTextDocument error")
34+
35+
// Get autocompletion list.
36+
list, err := h.Completion(t.Context(), lsp.CompletionParams{
37+
TextDocumentPositionParams: lsp.TextDocumentPositionParams{
38+
TextDocument: lsp.TextDocumentIdentifier{URI: uri},
39+
Position: lsp.Position{Character: 1},
40+
},
41+
})
42+
g.Expect(err).ToNot(HaveOccurred(), "Completion error")
43+
expectedItem := lsp.CompletionItem{Label: "shader_type", Kind: lsp.CompletionKeyword}
44+
ignoreFields := cmpopts.IgnoreFields(lsp.CompletionItem{}, "Detail", "Documentation")
45+
g.Expect(list.Items).To(ContainElement(BeComparableTo(expectedItem, ignoreFields)), "Missing expected completion item")
46+
})
47+
}

internal/ast/parser_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import (
3030

3131
"github.com/alecthomas/participle/v2/lexer"
3232
"github.com/armsnyder/gdshader-language-server/internal/ast"
33-
"github.com/google/go-cmp/cmp"
33+
"github.com/google/go-cmp/cmp/cmpopts"
3434
"github.com/samber/lo"
3535

3636
. "github.com/onsi/gomega"
@@ -81,4 +81,4 @@ func TestCanParseAllValidPrograms(t *testing.T) {
8181
}
8282
}
8383

84-
var IgnorePos = cmp.FilterValues(func(_, _ lexer.Position) bool { return true }, cmp.Ignore())
84+
var IgnorePos = cmpopts.IgnoreTypes(lexer.Position{})

internal/lsp/document_sync.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,13 @@ func utf16Width(r rune) int {
296296
return 2
297297
}
298298

299+
// Lines returns the number of lines in the document. A single line ending in
300+
// a newline character is counted as two lines. This is consistent with the
301+
// LSP specification.
302+
func (d *Document) Lines() int {
303+
return len(d.lineStart)
304+
}
305+
299306
// ArrayBuffer is the simplest implementation of Buffer, using a byte slice
300307
// for storage. It is optimized for reads. Insertions and deletions are O(n)
301308
// due to slice copying.

internal/lsp/document_sync_test.go

Lines changed: 75 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,17 @@ func parsePos(s string) lsp.Position {
8989
return lsp.Position{Line: line, Character: char}
9090
}
9191

92-
func TestDocument_ApplyChange(t *testing.T) {
92+
func TestDocument(t *testing.T) {
93+
for _, impl := range bufferImplementations {
94+
t.Run(bufferName(impl), func(t *testing.T) {
95+
t.Run("ApplyChange", func(t *testing.T) { testApplyChange(t, impl) })
96+
t.Run("ApplyChange_Error", func(t *testing.T) { testApplyChangeError(t, impl) })
97+
t.Run("PositionToOffset", func(t *testing.T) { testPositionToOffset(t, impl) })
98+
})
99+
}
100+
}
101+
102+
func testApplyChange(t *testing.T, impl lsp.Buffer) {
93103
tests := []struct {
94104
name string
95105
initial string
@@ -164,25 +174,71 @@ func TestDocument_ApplyChange(t *testing.T) {
164174
},
165175
}
166176

167-
for _, impl := range bufferImplementations {
168-
t.Run(bufferName(impl), func(t *testing.T) {
169-
for _, tt := range tests {
170-
t.Run(tt.name, func(t *testing.T) {
171-
g := NewWithT(t)
172-
testutil.SetupLogger(t)
173-
doc := lsp.NewDocument([]byte(tt.initial), newBuffer(impl))
174-
for i, change := range tt.changes {
175-
err := doc.ApplyChange(change)
176-
g.Expect(err).ToNot(HaveOccurred(), "ApplyChange #%d failed", i+1)
177-
}
178-
g.Expect(string(doc.Bytes())).To(BeComparableTo(tt.want))
179-
})
177+
for _, tt := range tests {
178+
t.Run(tt.name, func(t *testing.T) {
179+
g := NewWithT(t)
180+
testutil.SetupLogger(t)
181+
doc := lsp.NewDocument([]byte(tt.initial), newBuffer(impl))
182+
for i, change := range tt.changes {
183+
err := doc.ApplyChange(change)
184+
g.Expect(err).ToNot(HaveOccurred(), "ApplyChange #%d failed", i+1)
185+
}
186+
g.Expect(string(doc.Bytes())).To(BeComparableTo(tt.want))
187+
})
188+
}
189+
}
190+
191+
func testPositionToOffset(t *testing.T, impl lsp.Buffer) {
192+
tests := []struct {
193+
name string
194+
initial string
195+
position lsp.Position
196+
want int
197+
wantErr bool
198+
}{
199+
{
200+
name: "position in single line",
201+
initial: "hello world",
202+
position: lsp.Position{Line: 0, Character: 5},
203+
want: 5,
204+
},
205+
{
206+
name: "position in multi-line",
207+
initial: "hello\nworld",
208+
position: lsp.Position{Line: 1, Character: 2},
209+
want: 8,
210+
},
211+
{
212+
name: "position in multi-line with CRLF",
213+
initial: "hello\r\nworld",
214+
position: lsp.Position{Line: 1, Character: 2},
215+
want: 9,
216+
},
217+
{
218+
name: "position at end of document",
219+
initial: "hello world",
220+
position: lsp.Position{Line: 0, Character: 11},
221+
want: 11,
222+
},
223+
}
224+
225+
for _, tt := range tests {
226+
t.Run(tt.name, func(t *testing.T) {
227+
g := NewWithT(t)
228+
testutil.SetupLogger(t)
229+
doc := lsp.NewDocument([]byte(tt.initial), newBuffer(impl))
230+
offset, err := doc.PositionToOffset(tt.position)
231+
if tt.wantErr {
232+
g.Expect(err).To(HaveOccurred(), "expected error for position %v", tt.position)
233+
} else {
234+
g.Expect(err).ToNot(HaveOccurred(), "unexpected error for position %v", tt.position)
235+
g.Expect(offset).To(BeEquivalentTo(tt.want), "unexpected offset for position %v", tt.position)
180236
}
181237
})
182238
}
183239
}
184240

185-
func TestDocument_ApplyChange_Error(t *testing.T) {
241+
func testApplyChangeError(t *testing.T, impl lsp.Buffer) {
186242
tests := []struct {
187243
name string
188244
initial string
@@ -200,14 +256,10 @@ func TestDocument_ApplyChange_Error(t *testing.T) {
200256
},
201257
}
202258

203-
for _, impl := range bufferImplementations {
204-
t.Run(bufferName(impl), func(t *testing.T) {
205-
for _, tt := range tests {
206-
t.Run(tt.name, func(t *testing.T) {
207-
doc := lsp.NewDocument([]byte(tt.initial), newBuffer(impl))
208-
NewWithT(t).Expect(doc.ApplyChange(tt.change)).ToNot(Succeed())
209-
})
210-
}
259+
for _, tt := range tests {
260+
t.Run(tt.name, func(t *testing.T) {
261+
doc := lsp.NewDocument([]byte(tt.initial), newBuffer(impl))
262+
NewWithT(t).Expect(doc.ApplyChange(tt.change)).ToNot(Succeed())
211263
})
212264
}
213265
}

0 commit comments

Comments
 (0)