blob: 7f4eec75b0cc181182a3540dfd5a329c5976317e [file] [log] [blame]
// 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, &params); err != nil {
return nil, err
}
h.init = &params
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, &params); 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, &params); 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, &params); 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, &params); 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, &params); 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, &params); 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, &params); 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, &params); 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, &params); 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),
}
}
}