Skip to content

Commit 8deeabb

Browse files
committed
internal/lsp: support range formatting
Refactor code a bit to support range formatting as well document formatting. Also, separate view from server to clean up. Change-Id: Ica397c7a0fb92a7708ea247c2d5de83e5528d8d4 Reviewed-on: https://go-review.googlesource.com/138275 Reviewed-by: Alan Donovan <[email protected]>
1 parent 792c3e6 commit 8deeabb

File tree

3 files changed

+141
-62
lines changed

3 files changed

+141
-62
lines changed

internal/lsp/format.go

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package lsp
2+
3+
import (
4+
"fmt"
5+
"go/format"
6+
"strings"
7+
8+
"golang.org/x/tools/internal/lsp/protocol"
9+
)
10+
11+
// format formats a document with a given range.
12+
func (s *server) format(uri protocol.DocumentURI, rng *protocol.Range) ([]protocol.TextEdit, error) {
13+
data, err := s.readActiveFile(uri)
14+
if err != nil {
15+
return nil, err
16+
}
17+
if rng != nil {
18+
start, err := positionToOffset(data, int(rng.Start.Line), int(rng.Start.Character))
19+
if err != nil {
20+
return nil, err
21+
}
22+
end, err := positionToOffset(data, int(rng.End.Line), int(rng.End.Character))
23+
if err != nil {
24+
return nil, err
25+
}
26+
data = data[start:end]
27+
// format.Source will fail if the substring is not a balanced expression tree.
28+
// TODO(rstambler): parse the file and use astutil.PathEnclosingInterval to
29+
// find the largest ast.Node n contained within start:end, and format the
30+
// region n.Pos-n.End instead.
31+
}
32+
// format.Source changes slightly from one release to another, so the version
33+
// of Go used to build the LSP server will determine how it formats code.
34+
// This should be acceptable for all users, who likely be prompted to rebuild
35+
// the LSP server on each Go release.
36+
fmted, err := format.Source([]byte(data))
37+
if err != nil {
38+
return nil, err
39+
}
40+
if rng == nil {
41+
// Get the ending line and column numbers for the original file.
42+
line := strings.Count(data, "\n")
43+
col := len(data) - strings.LastIndex(data, "\n") - 1
44+
if col < 0 {
45+
col = 0
46+
}
47+
rng = &protocol.Range{
48+
Start: protocol.Position{0, 0},
49+
End: protocol.Position{float64(line), float64(col)},
50+
}
51+
}
52+
// TODO(rstambler): Compute text edits instead of replacing whole file.
53+
return []protocol.TextEdit{
54+
{
55+
Range: *rng,
56+
NewText: string(fmted),
57+
},
58+
}, nil
59+
}
60+
61+
// positionToOffset converts a 0-based line and column number in a file
62+
// to a byte offset value.
63+
func positionToOffset(contents string, line, col int) (int, error) {
64+
start := 0
65+
for i := 0; i < int(line); i++ {
66+
if start >= len(contents) {
67+
return 0, fmt.Errorf("file contains %v lines, not %v lines", i, line)
68+
}
69+
index := strings.IndexByte(contents[start:], '\n')
70+
if index == -1 {
71+
return 0, fmt.Errorf("file contains %v lines, not %v lines", i, line)
72+
}
73+
start += (index + 1)
74+
}
75+
offset := start + int(col)
76+
return offset, nil
77+
}

internal/lsp/server.go

+22-62
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@ package lsp
66

77
import (
88
"context"
9-
"fmt"
10-
"go/format"
119
"os"
12-
"strings"
1310
"sync"
1411

1512
"golang.org/x/tools/internal/jsonrpc2"
@@ -20,7 +17,7 @@ import (
2017
// stream is closed.
2118
func RunServer(ctx context.Context, stream jsonrpc2.Stream, opts ...interface{}) error {
2219
s := &server{
23-
activeFiles: make(map[protocol.DocumentURI]string),
20+
view: newView(),
2421
}
2522
conn, client := protocol.RunServer(ctx, stream, s, opts...)
2623
s.client = client
@@ -33,31 +30,7 @@ type server struct {
3330
initializedMu sync.Mutex
3431
initialized bool // set once the server has received "initialize" request
3532

36-
activeFilesMu sync.Mutex
37-
activeFiles map[protocol.DocumentURI]string // files
38-
}
39-
40-
func (s *server) cacheActiveFile(uri protocol.DocumentURI, changes []protocol.TextDocumentContentChangeEvent) error {
41-
s.activeFilesMu.Lock()
42-
defer s.activeFilesMu.Unlock()
43-
44-
for _, change := range changes {
45-
if change.RangeLength == 0 {
46-
s.activeFiles[uri] = change.Text
47-
}
48-
}
49-
return nil
50-
}
51-
52-
func (s *server) readActiveFile(uri protocol.DocumentURI) (string, error) {
53-
s.activeFilesMu.Lock()
54-
defer s.activeFilesMu.Unlock()
55-
56-
content, ok := s.activeFiles[uri]
57-
if !ok {
58-
return "", fmt.Errorf("file not found: %s", uri)
59-
}
60-
return content, nil
33+
*view
6134
}
6235

6336
func (s *server) Initialize(ctx context.Context, params *protocol.InitializeParams) (*protocol.InitializeResult, error) {
@@ -70,9 +43,11 @@ func (s *server) Initialize(ctx context.Context, params *protocol.InitializePara
7043
return &protocol.InitializeResult{
7144
Capabilities: protocol.ServerCapabilities{
7245
TextDocumentSync: protocol.TextDocumentSyncOptions{
73-
Change: float64(protocol.Full), // full contents of file sent on each update
46+
Change: float64(protocol.Full), // full contents of file sent on each update
47+
OpenClose: true,
7448
},
75-
DocumentFormattingProvider: true,
49+
DocumentFormattingProvider: true,
50+
DocumentRangeFormattingProvider: true,
7651
},
7752
}, nil
7853
}
@@ -119,12 +94,19 @@ func (s *server) ExecuteCommand(context.Context, *protocol.ExecuteCommandParams)
11994
return nil, notImplemented("ExecuteCommand")
12095
}
12196

122-
func (s *server) DidOpen(context.Context, *protocol.DidOpenTextDocumentParams) error {
123-
return notImplemented("DidOpen")
97+
func (s *server) DidOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error {
98+
s.cacheActiveFile(params.TextDocument.URI, params.TextDocument.Text)
99+
return nil
124100
}
125101

126102
func (s *server) DidChange(ctx context.Context, params *protocol.DidChangeTextDocumentParams) error {
127-
s.cacheActiveFile(params.TextDocument.URI, params.ContentChanges)
103+
if len(params.ContentChanges) < 1 {
104+
return jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "no content changes provided")
105+
}
106+
// We expect the full content of file, i.e. a single change with no range.
107+
if change := params.ContentChanges[0]; change.RangeLength == 0 {
108+
s.cacheActiveFile(params.TextDocument.URI, change.Text)
109+
}
128110
return nil
129111
}
130112

@@ -140,8 +122,9 @@ func (s *server) DidSave(context.Context, *protocol.DidSaveTextDocumentParams) e
140122
return notImplemented("DidSave")
141123
}
142124

143-
func (s *server) DidClose(context.Context, *protocol.DidCloseTextDocumentParams) error {
144-
return notImplemented("DidClose")
125+
func (s *server) DidClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error {
126+
s.clearActiveFile(params.TextDocument.URI)
127+
return nil
145128
}
146129

147130
func (s *server) Completion(context.Context, *protocol.CompletionParams) (*protocol.CompletionList, error) {
@@ -213,34 +196,11 @@ func (s *server) ColorPresentation(context.Context, *protocol.ColorPresentationP
213196
}
214197

215198
func (s *server) Formatting(ctx context.Context, params *protocol.DocumentFormattingParams) ([]protocol.TextEdit, error) {
216-
data, err := s.readActiveFile(params.TextDocument.URI)
217-
if err != nil {
218-
return nil, jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "unable to format %s: %v", params.TextDocument.URI, err)
219-
}
220-
fmted, err := format.Source([]byte(data))
221-
if err != nil {
222-
return nil, jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "unable to format %s: %v", params.TextDocument.URI, err)
223-
}
224-
// Get the ending line and column numbers for the original file.
225-
line := strings.Count(data, "\n")
226-
col := len(data) - strings.LastIndex(data, "\n")
227-
if col < 0 {
228-
col = 0
229-
}
230-
// TODO(rstambler): Compute text edits instead of replacing whole file.
231-
return []protocol.TextEdit{
232-
{
233-
Range: protocol.Range{
234-
Start: protocol.Position{0, 0},
235-
End: protocol.Position{float64(line), float64(col)},
236-
},
237-
NewText: string(fmted),
238-
},
239-
}, nil
199+
return s.format(params.TextDocument.URI, nil)
240200
}
241201

242-
func (s *server) RangeFormatting(context.Context, *protocol.DocumentRangeFormattingParams) ([]protocol.TextEdit, error) {
243-
return nil, notImplemented("RangeFormatting")
202+
func (s *server) RangeFormatting(ctx context.Context, params *protocol.DocumentRangeFormattingParams) ([]protocol.TextEdit, error) {
203+
return s.format(params.TextDocument.URI, &params.Range)
244204
}
245205

246206
func (s *server) OnTypeFormatting(context.Context, *protocol.DocumentOnTypeFormattingParams) ([]protocol.TextEdit, error) {

internal/lsp/view.go

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package lsp
2+
3+
import (
4+
"fmt"
5+
"sync"
6+
7+
"golang.org/x/tools/internal/lsp/protocol"
8+
)
9+
10+
type view struct {
11+
activeFilesMu sync.Mutex
12+
activeFiles map[protocol.DocumentURI]string
13+
}
14+
15+
func newView() *view {
16+
return &view{
17+
activeFiles: make(map[protocol.DocumentURI]string),
18+
}
19+
}
20+
21+
func (v *view) cacheActiveFile(uri protocol.DocumentURI, text string) {
22+
v.activeFilesMu.Lock()
23+
v.activeFiles[uri] = text
24+
v.activeFilesMu.Unlock()
25+
}
26+
27+
func (v *view) readActiveFile(uri protocol.DocumentURI) (string, error) {
28+
v.activeFilesMu.Lock()
29+
defer v.activeFilesMu.Unlock()
30+
31+
content, ok := v.activeFiles[uri]
32+
if !ok {
33+
return "", fmt.Errorf("file not found: %s", uri)
34+
}
35+
return content, nil
36+
}
37+
38+
func (v *view) clearActiveFile(uri protocol.DocumentURI) {
39+
v.activeFilesMu.Lock()
40+
delete(v.activeFiles, uri)
41+
v.activeFilesMu.Unlock()
42+
}

0 commit comments

Comments
 (0)