| // Copyright 2020 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package langserver |
| |
| import ( |
| "context" |
| "encoding/json" |
| "fmt" |
| "log" |
| "os" |
| "strings" |
| |
| fidlcommon "fidl-lsp/third_party/common" |
| "github.com/sourcegraph/go-lsp" |
| "github.com/sourcegraph/jsonrpc2" |
| |
| "fidl-lsp/analysis" |
| "fidl-lsp/state" |
| ) |
| |
| // LspServer is a wrapper around a LangHandler. |
| // It is the entry point for all LSP communication with the client. |
| type LspServer struct { |
| jsonrpc2.Handler |
| } |
| |
| // LangHandler implements jsonrpc2.Handler, an interface with one Handle method. |
| // It fields all LSP requests from the client, and sends back responses and |
| // notifications. |
| // |
| // LangHandler has a *state.FileSystem, which keeps track of the the language |
| // editor's open files (in the future, to support concurrency, this state can be |
| // wrapped in e.g. an RWLock). |
| // |
| // LangHandler also has an *analysis.Analyzer, |
| // which is used to compile and extract insights from files being edited. |
| type LangHandler struct { |
| cfg Config |
| init *lsp.InitializeParams |
| shutdown bool |
| |
| log *log.Logger |
| |
| fs *state.FileSystem |
| |
| analyzer *analysis.Analyzer |
| } |
| |
| // Assert that LangHandler implements the jsonrpc2.Handler interface |
| var _ jsonrpc2.Handler = (*LangHandler)(nil) |
| |
| type Config struct { |
| serverCapabilities serverCapabilities |
| |
| // The base URI for reference links. Currently we linkify Fuchsia platform |
| // library imports, and the links point to the SDK docs. |
| baseRefURI string |
| |
| // The absolute path to the fidl_project.json file. This is used as a |
| // default if the client does not specify a custom path in its configuration |
| // settings. |
| fidlProjectPath string |
| } |
| |
| func (c *Config) FormatRefURI(libraryName fidlcommon.LibraryName) string { |
| return c.baseRefURI + libraryName.FullyQualifiedName() |
| } |
| |
| // NewDefaultConfig returns a default configuration for the language server. |
| func NewDefaultConfig(fidlProjectPath string) Config { |
| return Config{ |
| serverCapabilities: serverCapabilities{ |
| DefinitionProvider: true, |
| DocumentFormattingProvider: true, |
| DocumentLinkProvider: &documentLinkOptions{}, |
| HoverProvider: true, |
| ReferencesProvider: true, |
| TextDocumentSync: &lsp.TextDocumentSyncOptionsOrKind{ |
| Options: &lsp.TextDocumentSyncOptions{ |
| OpenClose: true, |
| Change: lsp.TDSKIncremental, |
| }, |
| }, |
| }, |
| baseRefURI: "https://fuchsia.dev/reference/fidl/", |
| fidlProjectPath: fidlProjectPath, |
| } |
| } |
| |
| // NewLangHandler returns a LangHandler with the given configuration, logger, |
| // and set of CompiledLibraries. |
| func NewLangHandler(cfg Config, trace *log.Logger, analyzer *analysis.Analyzer) *LangHandler { |
| return &LangHandler{ |
| log: trace, |
| cfg: cfg, |
| fs: state.NewFileSystem(), |
| analyzer: analyzer, |
| } |
| } |
| |
| // Handle fields an LSP request or notification. |
| // If there is an internal server error, it will be logged and an empty response |
| // will be sent to the client. If there is an error in the request, an error |
| // reponse will be sent. |
| func (h *LangHandler) Handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) { |
| if resp, handleErr := h.handle(ctx, conn, req); handleErr == nil { |
| if resp != nil { |
| if err := conn.Reply(ctx, req.ID, resp); err != nil { |
| h.log.Printf("error on conn.Reply: %s\n", err) |
| } |
| } |
| } else { |
| switch handleErr.(type) { |
| case *jsonrpc2.Error: |
| h.log.Printf("got a request we couldn't handle: %s\n", req.Method) |
| if err := conn.ReplyWithError(ctx, req.ID, handleErr.(*jsonrpc2.Error)); err != nil { |
| h.log.Printf("error on conn.ReplyWithError: %s\n", err) |
| } |
| default: |
| // If the error is not a jsonrpc2.Error, this means that we had an |
| // internal server error and were unable to meet the request. Rather |
| // than pass that error on to the client, we should log the error |
| // and reply with an empty response so the client is not waiting for |
| // a response. |
| h.log.Printf("err on handling `%s`: %s\n", req.Method, handleErr) |
| if err := conn.Reply(ctx, req.ID, resp); err != nil { |
| h.log.Printf("error on conn.Reply: %s\n", err) |
| } |
| } |
| } |
| } |
| |
| // Notify sends an LSP notification. |
| func (h *LangHandler) Notify(ctx context.Context, conn *jsonrpc2.Conn, method string, notif interface{}) { |
| if err := conn.Notify(ctx, method, notif); err != nil { |
| h.log.Printf("error on conn.Reply: %s\n", err) |
| } |
| } |
| |
| // The general pattern of the `handle` callback is as follows: |
| // |
| // * receive a notification from the client indicating a change of state --> |
| // pass the change to the FileSystem --> |
| // trigger a re-analysis of the file that was changed --> |
| // publish diagnostics on that file. |
| // |
| // * receive a request from the client --> |
| // request the relevant analysis from the Analyzer, passing it a snapshot of |
| // the state of the FileSystem --> |
| // wrap the analysis in relevant LSP type/object and send to the client. |
| func (h *LangHandler) handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) (result interface{}, err error) { |
| // TODO: locking over `h` |
| switch req.Method { |
| case "initialize": |
| // TODO: if server receives any request before `initialize`, it should |
| // respond with error code: -32002 |
| if h.init != nil { |
| return nil, &jsonrpc2.Error{ |
| Code: jsonrpc2.CodeInvalidRequest, |
| Message: "language server is already initialized", |
| } |
| } |
| if req.Params == nil { |
| return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams} |
| } |
| var params lsp.InitializeParams |
| if err := json.Unmarshal(*req.Params, ¶ms); err != nil { |
| return nil, err |
| } |
| h.init = ¶ms |
| return initializeResult{Capabilities: h.cfg.serverCapabilities}, nil |
| |
| case "initialized": |
| // A notification that the client is ready to receive requests. |
| h.requestConfiguration(ctx, conn) |
| return nil, nil |
| |
| case "shutdown": |
| // TODO: if server receives any request after `shutdown`, it should |
| // respond with an error: InvalidRequest. |
| h.shutdown = true |
| h.analyzer.Cleanup() |
| return nil, nil |
| |
| case "exit": |
| if h.shutdown { |
| os.Exit(0) |
| } else { |
| os.Exit(1) |
| } |
| return nil, nil |
| |
| case "workspace/didChangeConfiguration": |
| if req.Params == nil { |
| return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams} |
| } |
| var params lsp.DidChangeConfigurationParams |
| if err := json.Unmarshal(*req.Params, ¶ms); err != nil { |
| return nil, err |
| } |
| |
| h.handleDidChangeConfiguration(ctx, conn, params) |
| return nil, nil |
| |
| case "textDocument/didOpen": |
| if req.Params == nil { |
| return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams} |
| } |
| var params lsp.DidOpenTextDocumentParams |
| if err := json.Unmarshal(*req.Params, ¶ms); err != nil { |
| return nil, err |
| } |
| |
| h.handleOpenFile(params) |
| if err := h.analyzer.Analyze(h.fs, state.FileID(params.TextDocument.URI)); err != nil { |
| h.log.Println(err) |
| } |
| h.publishDiagnostics(ctx, conn, params.TextDocument.URI) |
| return nil, nil |
| |
| case "textDocument/didClose": |
| if req.Params == nil { |
| return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams} |
| } |
| var params lsp.DidCloseTextDocumentParams |
| if err := json.Unmarshal(*req.Params, ¶ms); err != nil { |
| return nil, err |
| } |
| |
| h.handleCloseFile(params) |
| // TODO: remove diagnostics associated with this file? |
| return nil, nil |
| |
| case "textDocument/didChange": |
| if req.Params == nil { |
| return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams} |
| } |
| var params lsp.DidChangeTextDocumentParams |
| if err := json.Unmarshal(*req.Params, ¶ms); err != nil { |
| return nil, err |
| } |
| |
| h.handleDidChange(params) |
| if err := h.analyzer.Analyze(h.fs, state.FileID(params.TextDocument.URI)); err != nil { |
| h.log.Println(err) |
| } |
| h.publishDiagnostics(ctx, conn, params.TextDocument.URI) |
| return nil, nil |
| |
| case "textDocument/definition": |
| if req.Params == nil { |
| return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams} |
| } |
| var params lsp.TextDocumentPositionParams |
| if err := json.Unmarshal(*req.Params, ¶ms); err != nil { |
| return nil, err |
| } |
| |
| return h.handleDefinition(params) |
| |
| case "textDocument/documentLink": |
| if req.Params == nil { |
| return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams} |
| } |
| var params documentLinkParams |
| if err := json.Unmarshal(*req.Params, ¶ms); err != nil { |
| return nil, err |
| } |
| |
| return h.handleDocumentLinks(params) |
| |
| case "textDocument/formatting": |
| if req.Params == nil { |
| return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams} |
| } |
| var params lsp.DocumentFormattingParams |
| if err := json.Unmarshal(*req.Params, ¶ms); err != nil { |
| return nil, err |
| } |
| |
| return h.handleFormat(params) |
| |
| case "textDocument/hover": |
| if req.Params == nil { |
| return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams} |
| } |
| var params lsp.TextDocumentPositionParams |
| if err := json.Unmarshal(*req.Params, ¶ms); err != nil { |
| return nil, err |
| } |
| |
| return h.handleHover(params) |
| |
| case "textDocument/references": |
| if req.Params == nil { |
| return nil, &jsonrpc2.Error{Code: jsonrpc2.CodeInvalidParams} |
| } |
| var params lsp.TextDocumentPositionParams |
| if err := json.Unmarshal(*req.Params, ¶ms); err != nil { |
| return nil, err |
| } |
| |
| return h.handleReferences(params) |
| |
| default: |
| // Notification and requests whose methods start with ‘$/’ are messages |
| // which are protocol implementation dependent and might not be |
| // implementable in all clients or servers. |
| // If a server or client receives notifications starting with ‘$/’ it is |
| // free to ignore the notification |
| // |
| // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#dollarRequests |
| if strings.HasPrefix(req.Method, "$") && req.Notif { |
| return nil, nil |
| } |
| return nil, &jsonrpc2.Error{ |
| Code: jsonrpc2.CodeMethodNotFound, |
| Message: fmt.Sprintf("method not supported: %s", req.Method), |
| } |
| } |
| } |