From 8deeabbe2e5312c8a8c390e9cb04c61112f0c2f5 Mon Sep 17 00:00:00 2001 From: Rebecca Stambler Date: Thu, 27 Sep 2018 18:15:45 -0400 Subject: [PATCH] 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 --- internal/lsp/format.go | 77 ++++++++++++++++++++++++++++++++++++++ internal/lsp/server.go | 84 +++++++++++------------------------------- internal/lsp/view.go | 42 +++++++++++++++++++++ 3 files changed, 141 insertions(+), 62 deletions(-) create mode 100644 internal/lsp/format.go create mode 100644 internal/lsp/view.go diff --git a/internal/lsp/format.go b/internal/lsp/format.go new file mode 100644 index 00000000..02e2f038 --- /dev/null +++ b/internal/lsp/format.go @@ -0,0 +1,77 @@ +package lsp + +import ( + "fmt" + "go/format" + "strings" + + "golang.org/x/tools/internal/lsp/protocol" +) + +// format formats a document with a given range. +func (s *server) format(uri protocol.DocumentURI, rng *protocol.Range) ([]protocol.TextEdit, error) { + data, err := s.readActiveFile(uri) + if err != nil { + return nil, err + } + if rng != nil { + start, err := positionToOffset(data, int(rng.Start.Line), int(rng.Start.Character)) + if err != nil { + return nil, err + } + end, err := positionToOffset(data, int(rng.End.Line), int(rng.End.Character)) + if err != nil { + return nil, err + } + data = data[start:end] + // format.Source will fail if the substring is not a balanced expression tree. + // TODO(rstambler): parse the file and use astutil.PathEnclosingInterval to + // find the largest ast.Node n contained within start:end, and format the + // region n.Pos-n.End instead. + } + // format.Source changes slightly from one release to another, so the version + // of Go used to build the LSP server will determine how it formats code. + // This should be acceptable for all users, who likely be prompted to rebuild + // the LSP server on each Go release. + fmted, err := format.Source([]byte(data)) + if err != nil { + return nil, err + } + if rng == nil { + // Get the ending line and column numbers for the original file. + line := strings.Count(data, "\n") + col := len(data) - strings.LastIndex(data, "\n") - 1 + if col < 0 { + col = 0 + } + rng = &protocol.Range{ + Start: protocol.Position{0, 0}, + End: protocol.Position{float64(line), float64(col)}, + } + } + // TODO(rstambler): Compute text edits instead of replacing whole file. + return []protocol.TextEdit{ + { + Range: *rng, + NewText: string(fmted), + }, + }, nil +} + +// positionToOffset converts a 0-based line and column number in a file +// to a byte offset value. +func positionToOffset(contents string, line, col int) (int, error) { + start := 0 + for i := 0; i < int(line); i++ { + if start >= len(contents) { + return 0, fmt.Errorf("file contains %v lines, not %v lines", i, line) + } + index := strings.IndexByte(contents[start:], '\n') + if index == -1 { + return 0, fmt.Errorf("file contains %v lines, not %v lines", i, line) + } + start += (index + 1) + } + offset := start + int(col) + return offset, nil +} diff --git a/internal/lsp/server.go b/internal/lsp/server.go index f5611bee..3673ce3e 100644 --- a/internal/lsp/server.go +++ b/internal/lsp/server.go @@ -6,10 +6,7 @@ package lsp import ( "context" - "fmt" - "go/format" "os" - "strings" "sync" "golang.org/x/tools/internal/jsonrpc2" @@ -20,7 +17,7 @@ import ( // stream is closed. func RunServer(ctx context.Context, stream jsonrpc2.Stream, opts ...interface{}) error { s := &server{ - activeFiles: make(map[protocol.DocumentURI]string), + view: newView(), } conn, client := protocol.RunServer(ctx, stream, s, opts...) s.client = client @@ -33,31 +30,7 @@ type server struct { initializedMu sync.Mutex initialized bool // set once the server has received "initialize" request - activeFilesMu sync.Mutex - activeFiles map[protocol.DocumentURI]string // files -} - -func (s *server) cacheActiveFile(uri protocol.DocumentURI, changes []protocol.TextDocumentContentChangeEvent) error { - s.activeFilesMu.Lock() - defer s.activeFilesMu.Unlock() - - for _, change := range changes { - if change.RangeLength == 0 { - s.activeFiles[uri] = change.Text - } - } - return nil -} - -func (s *server) readActiveFile(uri protocol.DocumentURI) (string, error) { - s.activeFilesMu.Lock() - defer s.activeFilesMu.Unlock() - - content, ok := s.activeFiles[uri] - if !ok { - return "", fmt.Errorf("file not found: %s", uri) - } - return content, nil + *view } 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 return &protocol.InitializeResult{ Capabilities: protocol.ServerCapabilities{ TextDocumentSync: protocol.TextDocumentSyncOptions{ - Change: float64(protocol.Full), // full contents of file sent on each update + Change: float64(protocol.Full), // full contents of file sent on each update + OpenClose: true, }, - DocumentFormattingProvider: true, + DocumentFormattingProvider: true, + DocumentRangeFormattingProvider: true, }, }, nil } @@ -119,12 +94,19 @@ func (s *server) ExecuteCommand(context.Context, *protocol.ExecuteCommandParams) return nil, notImplemented("ExecuteCommand") } -func (s *server) DidOpen(context.Context, *protocol.DidOpenTextDocumentParams) error { - return notImplemented("DidOpen") +func (s *server) DidOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error { + s.cacheActiveFile(params.TextDocument.URI, params.TextDocument.Text) + return nil } func (s *server) DidChange(ctx context.Context, params *protocol.DidChangeTextDocumentParams) error { - s.cacheActiveFile(params.TextDocument.URI, params.ContentChanges) + 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. + if change := params.ContentChanges[0]; change.RangeLength == 0 { + s.cacheActiveFile(params.TextDocument.URI, change.Text) + } return nil } @@ -140,8 +122,9 @@ func (s *server) DidSave(context.Context, *protocol.DidSaveTextDocumentParams) e return notImplemented("DidSave") } -func (s *server) DidClose(context.Context, *protocol.DidCloseTextDocumentParams) error { - return notImplemented("DidClose") +func (s *server) DidClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error { + s.clearActiveFile(params.TextDocument.URI) + return nil } func (s *server) Completion(context.Context, *protocol.CompletionParams) (*protocol.CompletionList, error) { @@ -213,34 +196,11 @@ func (s *server) ColorPresentation(context.Context, *protocol.ColorPresentationP } func (s *server) Formatting(ctx context.Context, params *protocol.DocumentFormattingParams) ([]protocol.TextEdit, error) { - data, err := s.readActiveFile(params.TextDocument.URI) - if err != nil { - return nil, jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "unable to format %s: %v", params.TextDocument.URI, err) - } - fmted, err := format.Source([]byte(data)) - if err != nil { - return nil, jsonrpc2.NewErrorf(jsonrpc2.CodeInternalError, "unable to format %s: %v", params.TextDocument.URI, err) - } - // Get the ending line and column numbers for the original file. - line := strings.Count(data, "\n") - col := len(data) - strings.LastIndex(data, "\n") - if col < 0 { - col = 0 - } - // TODO(rstambler): Compute text edits instead of replacing whole file. - return []protocol.TextEdit{ - { - Range: protocol.Range{ - Start: protocol.Position{0, 0}, - End: protocol.Position{float64(line), float64(col)}, - }, - NewText: string(fmted), - }, - }, nil + return s.format(params.TextDocument.URI, nil) } -func (s *server) RangeFormatting(context.Context, *protocol.DocumentRangeFormattingParams) ([]protocol.TextEdit, error) { - return nil, notImplemented("RangeFormatting") +func (s *server) RangeFormatting(ctx context.Context, params *protocol.DocumentRangeFormattingParams) ([]protocol.TextEdit, error) { + return s.format(params.TextDocument.URI, ¶ms.Range) } func (s *server) OnTypeFormatting(context.Context, *protocol.DocumentOnTypeFormattingParams) ([]protocol.TextEdit, error) { diff --git a/internal/lsp/view.go b/internal/lsp/view.go new file mode 100644 index 00000000..1011ba98 --- /dev/null +++ b/internal/lsp/view.go @@ -0,0 +1,42 @@ +package lsp + +import ( + "fmt" + "sync" + + "golang.org/x/tools/internal/lsp/protocol" +) + +type view struct { + activeFilesMu sync.Mutex + activeFiles map[protocol.DocumentURI]string +} + +func newView() *view { + return &view{ + activeFiles: make(map[protocol.DocumentURI]string), + } +} + +func (v *view) cacheActiveFile(uri protocol.DocumentURI, text string) { + v.activeFilesMu.Lock() + v.activeFiles[uri] = text + v.activeFilesMu.Unlock() +} + +func (v *view) readActiveFile(uri protocol.DocumentURI) (string, error) { + v.activeFilesMu.Lock() + defer v.activeFilesMu.Unlock() + + content, ok := v.activeFiles[uri] + if !ok { + return "", fmt.Errorf("file not found: %s", uri) + } + return content, nil +} + +func (v *view) clearActiveFile(uri protocol.DocumentURI) { + v.activeFilesMu.Lock() + delete(v.activeFiles, uri) + v.activeFilesMu.Unlock() +}