[lsp] Document links for FIDL library imports
This adds the "textDocument/documentLink" capability to the language
server. It linkifies platform imports into links to their SDK docs.
This CL also adds the `lsp` package, essentially a shim around go-lsp
that adds some missing LSP types.
Test: go test ./...
Change-Id: Id44a727b57c70d05663b5dfb5d263cb179f53556
Reviewed-on: https://fuchsia-review.googlesource.com/c/fidl-misc/+/395575
Reviewed-by: Benjamin Prosnitz <bprosnitz@google.com>
Reviewed-by: Pascal Perez <pascallouis@google.com>
diff --git a/fidl-lsp/analysis/library.go b/fidl-lsp/analysis/library.go
index dd74fa2..fbde7ef 100644
--- a/fidl-lsp/analysis/library.go
+++ b/fidl-lsp/analysis/library.go
@@ -4,14 +4,12 @@
package analysis
-import fidlcommon "fidl-lsp/third_party/common"
-
// Map from fully qualified name to type.kind, e.g.
// "fuchsia.hardware.camera/Device": "interface"
type decls map[string]string
type dependency struct {
- Name fidlcommon.LibraryName
+ Name string
Declarations decls
}
diff --git a/fidl-lsp/langserver/handler.go b/fidl-lsp/langserver/handler.go
index 62bda4e..8ba6a13 100644
--- a/fidl-lsp/langserver/handler.go
+++ b/fidl-lsp/langserver/handler.go
@@ -11,6 +11,7 @@
"log"
"os"
+ fidlcommon "fidl-lsp/third_party/common"
"github.com/sourcegraph/go-lsp"
"github.com/sourcegraph/jsonrpc2"
@@ -35,7 +36,7 @@
// LangHandler also has an *analysis.Analyzer,
// which is used to compile and extract insights from files being edited.
type LangHandler struct {
- cfg lsp.ServerCapabilities
+ cfg Config
init *lsp.InitializeParams
shutdown bool
@@ -49,22 +50,35 @@
// Assert that LangHandler implements the jsonrpc2.Handler interface
var _ jsonrpc2.Handler = (*LangHandler)(nil)
+type Config struct {
+ serverCapabilities serverCapabilities
+ baseRefURI 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() lsp.ServerCapabilities {
- return lsp.ServerCapabilities{
- DocumentFormattingProvider: true,
- TextDocumentSync: &lsp.TextDocumentSyncOptionsOrKind{
- Options: &lsp.TextDocumentSyncOptions{
- OpenClose: true,
- Change: lsp.TDSKIncremental,
+func NewDefaultConfig() Config {
+ return Config{
+ serverCapabilities: serverCapabilities{
+ DocumentFormattingProvider: true,
+ DocumentLinkProvider: &documentLinkOptions{},
+ TextDocumentSync: &lsp.TextDocumentSyncOptionsOrKind{
+ Options: &lsp.TextDocumentSyncOptions{
+ OpenClose: true,
+ Change: lsp.TDSKIncremental,
+ },
},
},
+ baseRefURI: "https://fuchsia.dev/reference/fidl/",
}
}
// NewLangHandler returns a LangHandler with the given configuration, logger,
// and set of CompiledLibraries.
-func NewLangHandler(cfg lsp.ServerCapabilities, trace *log.Logger, analyzer *analysis.Analyzer) *LangHandler {
+func NewLangHandler(cfg Config, trace *log.Logger, analyzer *analysis.Analyzer) *LangHandler {
return &LangHandler{
log: trace,
cfg: cfg,
@@ -143,7 +157,7 @@
return nil, err
}
h.init = ¶ms
- return lsp.InitializeResult{Capabilities: h.cfg}, nil
+ return initializeResult{Capabilities: h.cfg.serverCapabilities}, nil
case "initialized":
// A notification that the client is ready to receive requests. Ignore
@@ -209,6 +223,17 @@
h.publishDiagnostics(ctx, conn, params.TextDocument.URI)
return nil, nil
+ 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}
diff --git a/fidl-lsp/langserver/links.go b/fidl-lsp/langserver/links.go
new file mode 100644
index 0000000..f189383
--- /dev/null
+++ b/fidl-lsp/langserver/links.go
@@ -0,0 +1,42 @@
+// 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 (
+ "fmt"
+
+ "github.com/sourcegraph/go-lsp"
+
+ "fidl-lsp/state"
+)
+
+func (h *LangHandler) handleDocumentLinks(params documentLinkParams) ([]documentLink, error) {
+ file, err := h.fs.File(state.FileID(params.TextDocument.URI))
+ if err != nil {
+ return []documentLink{}, fmt.Errorf("could not find file `%s`", params.TextDocument.URI)
+ }
+ imports := state.ParsePlatformImportsMatch(file)
+ links := make([]documentLink, 0)
+ for _, m := range imports {
+ links = append(links, documentLink{
+ Range: lsp.Range{
+ Start: lsp.Position{
+ Line: m.Range.Start.Line,
+ Character: m.Range.Start.Character,
+ },
+ End: lsp.Position{
+ Line: m.Range.End.Line,
+ Character: m.Range.End.Character,
+ },
+ },
+ Target: lsp.DocumentURI(h.cfg.FormatRefURI(m.Lib)),
+ Tooltip: fmt.Sprintf("Fuchsia SDK Docs for library %s", m.Lib.FullyQualifiedName()),
+ })
+ }
+
+ return links, nil
+}
+
+// TODO: fxr/ --> fxrev.dev/, fxb/ --> fxbug.dev/?
diff --git a/fidl-lsp/langserver/links_test.go b/fidl-lsp/langserver/links_test.go
new file mode 100644
index 0000000..65eccf6
--- /dev/null
+++ b/fidl-lsp/langserver/links_test.go
@@ -0,0 +1,70 @@
+// 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 (
+ "testing"
+
+ "github.com/sourcegraph/go-lsp"
+
+ "fidl-lsp/state"
+)
+
+func TestLinks(t *testing.T) {
+ fileText := `
+library test;
+
+using fuchsia.io;
+using non.platform.import;
+`
+
+ handler := NewHandlerWithFiles(TestFile{
+ Name: "test.fidl",
+ Text: fileText,
+ })
+
+ links, err := handler.handleDocumentLinks(documentLinkParams{
+ TextDocument: lsp.TextDocumentIdentifier{
+ URI: lsp.DocumentURI("test.fidl"),
+ },
+ })
+ if err != nil {
+ t.Fatalf("failed to get document links: %s", err)
+ }
+ expLinks := map[string]lsp.DocumentURI{
+ "fuchsia.io": lsp.DocumentURI("https://fuchsia.dev/reference/fidl/fuchsia.io"),
+ }
+ if len(links) != len(expLinks) {
+ t.Fatalf("incorrect number of links; expected 1, actual %d", len(links))
+ }
+ for expSpan, expLink := range expLinks {
+ found := false
+ for _, link := range links {
+ start, err := state.OffsetInFile(
+ fileText,
+ state.Position{Line: link.Range.Start.Line, Character: link.Range.Start.Character},
+ )
+ if err != nil {
+ t.Fatalf("could not get offset of link in file: %s", err)
+ }
+ end, err := state.OffsetInFile(
+ fileText,
+ state.Position{Line: link.Range.End.Line, Character: link.Range.End.Character},
+ )
+ if err != nil {
+ t.Fatalf("could not get offset of link in file: %s", err)
+ }
+ span := fileText[start:end]
+ if span == expSpan && link.Target == expLink {
+ found = true
+ break
+ }
+ }
+ // Check if it's in links
+ if !found {
+ t.Fatalf("missing expected link `%s` on span `%s`", expLink, expSpan)
+ }
+ }
+}
diff --git a/fidl-lsp/langserver/lsp.go b/fidl-lsp/langserver/lsp.go
new file mode 100644
index 0000000..07e0f55
--- /dev/null
+++ b/fidl-lsp/langserver/lsp.go
@@ -0,0 +1,67 @@
+// 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 "github.com/sourcegraph/go-lsp"
+
+// `go-lsp` does not include all the necessary types for an LSP server. For
+// example, the types associated with the "textDocument/documentLink" language
+// feature are missing. This file includes those missing definitions.
+
+type serverCapabilities struct {
+ TextDocumentSync *lsp.TextDocumentSyncOptionsOrKind `json:"textDocumentSync,omitempty"`
+ HoverProvider bool `json:"hoverProvider,omitempty"`
+ CompletionProvider *lsp.CompletionOptions `json:"completionProvider,omitempty"`
+ SignatureHelpProvider *lsp.SignatureHelpOptions `json:"signatureHelpProvider,omitempty"`
+ DefinitionProvider bool `json:"definitionProvider,omitempty"`
+ TypeDefinitionProvider bool `json:"typeDefinitionProvider,omitempty"`
+ ReferencesProvider bool `json:"referencesProvider,omitempty"`
+ DocumentHighlightProvider bool `json:"documentHighlightProvider,omitempty"`
+ DocumentSymbolProvider bool `json:"documentSymbolProvider,omitempty"`
+ WorkspaceSymbolProvider bool `json:"workspaceSymbolProvider,omitempty"`
+ ImplementationProvider bool `json:"implementationProvider,omitempty"`
+ CodeActionProvider bool `json:"codeActionProvider,omitempty"`
+ CodeLensProvider *lsp.CodeLensOptions `json:"codeLensProvider,omitempty"`
+ DocumentFormattingProvider bool `json:"documentFormattingProvider,omitempty"`
+ DocumentRangeFormattingProvider bool `json:"documentRangeFormattingProvider,omitempty"`
+ DocumentOnTypeFormattingProvider *lsp.DocumentOnTypeFormattingOptions `json:"documentOnTypeFormattingProvider,omitempty"`
+ DocumentLinkProvider *documentLinkOptions `json:"documentLinkProvider,omitempty"`
+ RenameProvider bool `json:"renameProvider,omitempty"`
+ ExecuteCommandProvider *lsp.ExecuteCommandOptions `json:"executeCommandProvider,omitempty"`
+ SemanticHighlighting *lsp.SemanticHighlightingOptions `json:"semanticHighlighting,omitempty"`
+
+ // XWorkspaceReferencesProvider indicates the server provides support for
+ // xworkspace/references. This is a Sourcegraph extension.
+ XWorkspaceReferencesProvider bool `json:"xworkspaceReferencesProvider,omitempty"`
+
+ // XDefinitionProvider indicates the server provides support for
+ // textDocument/xdefinition. This is a Sourcegraph extension.
+ XDefinitionProvider bool `json:"xdefinitionProvider,omitempty"`
+
+ // XWorkspaceSymbolByProperties indicates the server provides support for
+ // querying symbols by properties with WorkspaceSymbolParams.symbol. This
+ // is a Sourcegraph extension.
+ XWorkspaceSymbolByProperties bool `json:"xworkspaceSymbolByProperties,omitempty"`
+
+ Experimental interface{} `json:"experimental,omitempty"`
+}
+
+type initializeResult struct {
+ Capabilities serverCapabilities `json:"capabilities,omitempty"`
+}
+
+type documentLinkOptions struct {
+ ResolveProvider bool `json:"resolveProvider,omitempty"`
+}
+
+type documentLinkParams struct {
+ TextDocument lsp.TextDocumentIdentifier `json:"textDocument"`
+}
+
+type documentLink struct {
+ Range lsp.Range `json:"range"`
+ Target lsp.DocumentURI `json:"target"`
+ Tooltip string `json:"tooltip"`
+}
diff --git a/fidl-lsp/state/fs.go b/fidl-lsp/state/fs.go
index 47b6db0..b018490 100644
--- a/fidl-lsp/state/fs.go
+++ b/fidl-lsp/state/fs.go
@@ -86,11 +86,11 @@
// ApplyChanges applies the specified changes to the relevant in-memory file.
func (fs *FileSystem) ApplyChanges(path FileID, changes []Change) error {
for _, change := range changes {
- start, err := offsetInFile(fs.files[path], change.Range.Start)
+ start, err := OffsetInFile(fs.files[path], change.Range.Start)
if err != nil {
return err
}
- end, err := offsetInFile(fs.files[path], change.Range.End)
+ end, err := OffsetInFile(fs.files[path], change.Range.End)
if err != nil {
return err
}
@@ -105,7 +105,9 @@
return nil
}
-func offsetInFile(contents string, pos Position) (uint, error) {
+// OffsetInFile converts a Position (line, character) to a rune offset in
+// `contents`.
+func OffsetInFile(contents string, pos Position) (uint, error) {
var (
line, col int
offset uint