diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go index 75d22925755..eea102f666f 100644 --- a/internal/lsp/lsp_test.go +++ b/internal/lsp/lsp_test.go @@ -443,3 +443,31 @@ func (d definitions) collect(fset *token.FileSet, src, target packagestest.Range loc := toProtocolLocation(fset, source.Range(src)) d[loc] = toProtocolLocation(fset, source.Range(target)) } + +func TestBytesOffset(t *testing.T) { + tests := []struct { + text string + pos protocol.Position + want int + }{ + {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 0}, want: 0}, + {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 1}, want: 1}, + {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 2}, want: 1}, + {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 3}, want: 5}, + {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 4}, want: -1}, + {text: "aaa\nbbb\n", pos: protocol.Position{Line: 0, Character: 3}, want: 3}, + {text: "aaa\nbbb\n", pos: protocol.Position{Line: 0, Character: 4}, want: -1}, + {text: "aaa\nbbb\n", pos: protocol.Position{Line: 1, Character: 0}, want: 4}, + {text: "aaa\nbbb\n", pos: protocol.Position{Line: 1, Character: 3}, want: 7}, + {text: "aaa\nbbb\n", pos: protocol.Position{Line: 1, Character: 4}, want: -1}, + {text: "aaa\nbbb\n", pos: protocol.Position{Line: 2, Character: 0}, want: -1}, + {text: "aaa\nbbb\n\n", pos: protocol.Position{Line: 2, Character: 0}, want: 8}, + } + + for _, test := range tests { + got := bytesOffset([]byte(test.text), test.pos) + if got != test.want { + t.Errorf("want %d for %q(Line:%d,Character:%d), but got %d", test.want, test.text, int(test.pos.Line), int(test.pos.Character), got) + } + } +} diff --git a/internal/lsp/protocol/text.go b/internal/lsp/protocol/text.go index 83456a415b6..5d50b2a80d1 100644 --- a/internal/lsp/protocol/text.go +++ b/internal/lsp/protocol/text.go @@ -38,7 +38,7 @@ type TextDocumentContentChangeEvent struct { /** * The range of the document that changed. */ - Range Range `json:"range,omitempty"` + Range *Range `json:"range,omitempty"` /** * The length of the range that got replaced. diff --git a/internal/lsp/server.go b/internal/lsp/server.go index 3db22fff13c..121b721608d 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -5,6 +5,7 @@ package lsp import ( + "bytes" "context" "fmt" "go/ast" @@ -13,6 +14,7 @@ import ( "net" "os" "sync" + "unicode/utf8" "golang.org/x/tools/go/packages" "golang.org/x/tools/internal/jsonrpc2" @@ -122,7 +124,7 @@ func (s *server) Initialize(ctx context.Context, params *protocol.InitializePara TriggerCharacters: []string{"(", ","}, }, TextDocumentSync: protocol.TextDocumentSyncOptions{ - Change: float64(protocol.Full), // full contents of file sent on each update + Change: float64(protocol.Incremental), OpenClose: true, }, TypeDefinitionProvider: true, @@ -177,16 +179,82 @@ func (s *server) DidOpen(ctx context.Context, params *protocol.DidOpenTextDocume return nil } +func bytesOffset(content []byte, pos protocol.Position) int { + var line, char, offset int + + for len(content) > 0 { + if line == int(pos.Line) && char == int(pos.Character) { + return offset + } + r, size := utf8.DecodeRune(content) + char++ + // The offsets are based on a UTF-16 string representation. + // So the rune should be checked twice for two code units in UTF-16. + if r >= 0x10000 { + if line == int(pos.Line) && char == int(pos.Character) { + return offset + } + char++ + } + offset += size + content = content[size:] + if r == '\n' { + line++ + char = 0 + } + } + return -1 +} + +func (s *server) applyChanges(ctx context.Context, params *protocol.DidChangeTextDocumentParams) (string, error) { + if len(params.ContentChanges) == 1 && params.ContentChanges[0].Range == nil { + // If range is empty, we expect the full content of file, i.e. a single change with no range. + change := params.ContentChanges[0] + if change.RangeLength != 0 { + return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "unexpected change range provided") + } + return change.Text, nil + } + + sourceURI, err := fromProtocolURI(params.TextDocument.URI) + if err != nil { + return "", err + } + + file, err := s.view.GetFile(ctx, sourceURI) + if err != nil { + return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "file not found") + } + + content := file.GetContent() + for _, change := range params.ContentChanges { + start := bytesOffset(content, change.Range.Start) + if start == -1 { + return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "invalid range for content change") + } + end := bytesOffset(content, change.Range.End) + if end == -1 { + return "", jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "invalid range for content change") + } + var buf bytes.Buffer + buf.Write(content[:start]) + buf.WriteString(change.Text) + buf.Write(content[end:]) + content = buf.Bytes() + } + return string(content), nil +} + func (s *server) DidChange(ctx context.Context, params *protocol.DidChangeTextDocumentParams) error { if len(params.ContentChanges) < 1 { return jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "no content changes provided") } - // We expect the full content of file, i.e. a single change with no range. - change := params.ContentChanges[0] - if change.RangeLength != 0 { - return jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "unexpected change range provided") + + text, err := s.applyChanges(ctx, params) + if err != nil { + return err } - s.cacheAndDiagnose(ctx, params.TextDocument.URI, change.Text) + s.cacheAndDiagnose(ctx, params.TextDocument.URI, text) return nil }