[lsp] Go to definition
This adds the "textDocument/definition" capability to the language
server. This capability only works for library names and top-level
declarations, i.e. not members or methods.
Test: go test ./...
Change-Id: I6de0ae3730ac54db7975b37f7136514bfa2255d0
Reviewed-on: https://fuchsia-review.googlesource.com/c/fidl-misc/+/395895
Reviewed-by: Pascal Perez <pascallouis@google.com>
Reviewed-by: Benjamin Prosnitz <bprosnitz@google.com>
diff --git a/fidl-lsp/analysis/analyzer.go b/fidl-lsp/analysis/analyzer.go
index e421843..8bbb4c7 100644
--- a/fidl-lsp/analysis/analyzer.go
+++ b/fidl-lsp/analysis/analyzer.go
@@ -8,6 +8,7 @@
"fmt"
"hash/fnv"
"io/ioutil"
+ "log"
"os"
fidlcommon "fidl-lsp/third_party/common"
@@ -15,10 +16,13 @@
"fidl-lsp/state"
)
-// Config keeps track of where the Analyzer can find FIDL tools.
+// Config keeps track of where the Analyzer can find FIDL tools, as well as the
+// root build directory, to which all locations in CompiledLibraries are
+// relative.
// This config will be exposed where possible as settings in FIDL LSP client IDE
// extensions.
type Config struct {
+ BuildRootDir string
FidlcPath string
FidlLintPath string
FidlFormatPath string
@@ -29,11 +33,13 @@
// exists (`json`), its deserialized JSON IR (`ir`), and any diagnostics on it.
// `deps` and `files` are treated as sets.
type Library struct {
- deps map[fidlcommon.LibraryName]bool
- files map[string]bool
- json string
- ir *FidlLibrary // nil if not yet imported
- diags map[state.FileID][]Diagnostic
+ deps map[fidlcommon.LibraryName]bool
+ // This library's constituent files in the form of absolute paths
+ files map[string]bool
+ json string
+ ir *FidlLibrary // nil if not yet imported
+ diags map[state.FileID][]Diagnostic
+ symbols symbolMap
}
// Analyzer compiles FIDL libraries and extracts analyses from them, including
@@ -71,17 +77,18 @@
inputFilesFIDL: make(map[state.FileID]string),
inputFilesJSON: make(map[fidlcommon.LibraryName]string),
}
- for libName, lib := range compiledLibraries {
- a.libs[fidlcommon.MustReadLibraryName(libName)] = &Library{
+ for name, lib := range compiledLibraries {
+ libName := fidlcommon.MustReadLibraryName(name)
+ a.libs[libName] = &Library{
deps: make(map[fidlcommon.LibraryName]bool, len(lib.Deps)),
files: make(map[string]bool, len(lib.Files)),
json: lib.JSON,
}
for _, dep := range lib.Deps {
- a.libs[fidlcommon.MustReadLibraryName(libName)].deps[fidlcommon.MustReadLibraryName(dep)] = true
+ a.libs[libName].deps[fidlcommon.MustReadLibraryName(dep)] = true
}
for _, file := range lib.Files {
- a.libs[fidlcommon.MustReadLibraryName(libName)].files[file] = true
+ a.libs[libName].files[file] = true
}
}
return a
@@ -115,12 +122,12 @@
lib := a.libs[libName]
// If the file currently being edited was already part of lib.files,
// delete that and replace it with the temporary/edited version.
- if absPath, err := state.FileIDToPath(path); err == nil {
+ if absPath, err := state.FileIDToPath(path); err != nil {
+ log.Printf("error parsing: %s\n", err)
+ } else {
if _, ok := lib.files[absPath]; ok {
delete(lib.files, absPath)
}
- } else {
- fmt.Fprint(os.Stderr, "error parsing\n")
}
lib.files[inputFilePath] = true
@@ -178,9 +185,9 @@
}
func (a *Analyzer) inputFileToFileID(inputFilePath string) (state.FileID, error) {
- for editorFile, inputFile := range a.inputFilesFIDL {
+ for fileID, inputFile := range a.inputFilesFIDL {
if inputFile == inputFilePath {
- return editorFile, nil
+ return fileID, nil
}
}
return "", fmt.Errorf("could not find input file `%s`", inputFilePath)
diff --git a/fidl-lsp/analysis/compile.go b/fidl-lsp/analysis/compile.go
index c71f851..4084400 100644
--- a/fidl-lsp/analysis/compile.go
+++ b/fidl-lsp/analysis/compile.go
@@ -10,34 +10,13 @@
"fmt"
"io/ioutil"
"os/exec"
+ "strings"
fidlcommon "fidl-lsp/third_party/common"
"fidl-lsp/state"
)
-// Read in the JSON IR at absolute filepath `jsonPath`.
-func (a *Analyzer) importLibrary(jsonPath string) error {
- data, err := ioutil.ReadFile(jsonPath)
- if err != nil {
- return err
- }
- var lib FidlLibrary
- if err := json.Unmarshal(data, &lib); err != nil {
- return err
- }
- // TODO: import more things? files + deps?
- if _, ok := a.libs[fidlcommon.MustReadLibraryName(lib.Name)]; !ok {
- a.libs[fidlcommon.MustReadLibraryName(lib.Name)] = &Library{
- json: jsonPath,
- }
- }
- a.libs[fidlcommon.MustReadLibraryName(lib.Name)].ir = &lib
-
- // TODO: construct symbol map from JSON IR for the file's library
- return nil
-}
-
type compileResult struct {
lib fidlcommon.LibraryName
diags map[state.FileID][]Diagnostic
@@ -75,29 +54,36 @@
if err := a.importLibrary(jsonPath); err != nil {
return compileResult{}, fmt.Errorf("error on adding compiled JSON: %s", err)
}
+ } else {
+ // TODO: there is no platform-independent way in Go to check an error
+ // status code, but if we can, we should somehow determine if fidlc
+ // exited with status code 1 (failure due to FIDL errors) or something
+ // else (internal error or command otherwise failed).
+ // If it's status code 1, we should continue, but anything else, we
+ // should return an error from `compile` to signal failure.
}
diags := make(map[state.FileID][]Diagnostic)
for fileName := range a.libs[libraryName].files {
- editorFile, err := a.inputFileToFileID(fileName)
+ fileID, err := a.inputFileToFileID(fileName)
if err == nil {
- diags[editorFile] = []Diagnostic{}
+ diags[fileID] = []Diagnostic{}
}
}
if errorsAndWarnings, err := a.fidlcDiagsFromStderr(stderr.Bytes()); err == nil {
- for editorFile, fileDiags := range errorsAndWarnings {
- if _, ok := diags[editorFile]; !ok {
- diags[editorFile] = []Diagnostic{}
+ for fileID, fileDiags := range errorsAndWarnings {
+ if _, ok := diags[fileID]; !ok {
+ diags[fileID] = []Diagnostic{}
}
- diags[editorFile] = append(diags[editorFile], fileDiags...)
+ diags[fileID] = append(diags[fileID], fileDiags...)
}
}
if lints, err := a.runFidlLint(a.inputFilesFIDL[path]); err == nil {
- for editorFile, fileDiags := range lints {
- if _, ok := diags[editorFile]; !ok {
- diags[editorFile] = []Diagnostic{}
+ for fileID, fileDiags := range lints {
+ if _, ok := diags[fileID]; !ok {
+ diags[fileID] = []Diagnostic{}
}
- diags[editorFile] = append(diags[editorFile], fileDiags...)
+ diags[fileID] = append(diags[fileID], fileDiags...)
}
}
@@ -106,3 +92,219 @@
diags: diags,
}, nil
}
+
+// Read in the JSON IR at absolute filepath `jsonPath`.
+func (a *Analyzer) importLibrary(jsonPath string) error {
+ data, err := ioutil.ReadFile(jsonPath)
+ if err != nil {
+ return err
+ }
+ var lib FidlLibrary
+ if err := json.Unmarshal(data, &lib); err != nil {
+ return err
+ }
+ // TODO: import more things? files + deps?
+ libName := fidlcommon.MustReadLibraryName(lib.Name)
+ if _, ok := a.libs[libName]; !ok {
+ a.libs[libName] = &Library{}
+ }
+ a.libs[libName].ir = &lib
+ a.libs[libName].json = jsonPath
+
+ // Construct symbol map from JSON IR for the file's library
+ symbols, err := a.genSymbolMap(lib)
+ if err != nil {
+ return fmt.Errorf("unable to generate symbol map from library %s: %s", lib.Name, err)
+ }
+ a.libs[libName].symbols = symbols
+
+ return nil
+}
+
+type symbolKind string
+
+const (
+ bitsKind symbolKind = "bits"
+ constKind = "const"
+ enumKind = "enum"
+ protocolKind = "protocol"
+ serviceKind = "service"
+ structKind = "struct"
+ tableKind = "table"
+ unionKind = "union"
+ typeAliasKind = "typeAlias"
+)
+
+type symbolInfo struct {
+ lib string
+ name string
+ definition state.Location
+ attrs []attribute
+ kind symbolKind
+ isMember bool
+ isMethod bool
+ typeInfo interface{}
+ maybeFromTypeAlias typeCtor
+}
+
+type symbolMap map[string]*symbolInfo
+
+func (a *Analyzer) genSymbolMap(l FidlLibrary) (symbolMap, error) {
+ // TODO: skip SomeLongAnonymousPrefix* structs? (are we double counting method request/response params?)
+ // TODO: how to handle const literals?
+
+ sm := make(symbolMap)
+
+ for _, d := range l.BitsDecls {
+ sm[d.Name] = &symbolInfo{
+ lib: l.Name,
+ name: d.Name,
+ definition: a.fidlLocToStateLoc(d.Loc),
+ kind: bitsKind,
+ typeInfo: d.Type,
+ maybeFromTypeAlias: d.FromTypeAlias,
+ attrs: d.Attrs,
+ }
+ a.addMembersToSymbolMap(sm, l.Name, d.Name, bitsKind, d.Members)
+ }
+ for _, d := range l.ConstDecls {
+ sm[d.Name] = &symbolInfo{
+ lib: l.Name,
+ name: d.Name,
+ definition: a.fidlLocToStateLoc(d.Loc),
+ kind: constKind,
+ typeInfo: d.Type,
+ maybeFromTypeAlias: d.FromTypeAlias,
+ attrs: d.Attrs,
+ }
+ }
+ for _, d := range l.EnumDecls {
+ sm[d.Name] = &symbolInfo{
+ lib: l.Name,
+ name: d.Name,
+ definition: a.fidlLocToStateLoc(d.Loc),
+ kind: enumKind,
+ typeInfo: d.Type,
+ maybeFromTypeAlias: d.FromTypeAlias,
+ attrs: d.Attrs,
+ }
+ a.addMembersToSymbolMap(sm, l.Name, d.Name, enumKind, d.Members)
+ }
+ for _, d := range l.ProtocolDecls {
+ sm[d.Name] = &symbolInfo{
+ lib: l.Name,
+ name: d.Name,
+ definition: a.fidlLocToStateLoc(d.Loc),
+ kind: protocolKind,
+ attrs: d.Attrs,
+ }
+ for _, m := range d.Methods {
+ methodName := fmt.Sprintf("%s.%s", d.Name, m.Name)
+ sm[methodName] = &symbolInfo{
+ lib: l.Name,
+ name: methodName,
+ definition: a.fidlLocToStateLoc(m.Loc),
+ kind: protocolKind,
+ isMethod: true,
+ attrs: m.Attrs,
+ }
+ if len(m.MaybeRequest) > 0 {
+ a.addMembersToSymbolMap(sm, l.Name, methodName, protocolKind, m.MaybeRequest)
+ }
+ if len(m.MaybeResponse) > 0 {
+ a.addMembersToSymbolMap(sm, l.Name, methodName, protocolKind, m.MaybeResponse)
+ }
+ }
+ }
+ for _, d := range l.ServiceDecls {
+ sm[d.Name] = &symbolInfo{
+ lib: l.Name,
+ name: d.Name,
+ definition: a.fidlLocToStateLoc(d.Loc),
+ kind: serviceKind,
+ attrs: d.Attrs,
+ }
+ a.addMembersToSymbolMap(sm, l.Name, d.Name, serviceKind, d.Members)
+ }
+ for _, d := range l.StructDecls {
+ sm[d.Name] = &symbolInfo{
+ lib: l.Name,
+ name: d.Name,
+ definition: a.fidlLocToStateLoc(d.Loc),
+ kind: structKind,
+ attrs: d.Attrs,
+ }
+ a.addMembersToSymbolMap(sm, l.Name, d.Name, structKind, d.Members)
+ }
+ for _, d := range l.TableDecls {
+ sm[d.Name] = &symbolInfo{
+ lib: l.Name,
+ name: d.Name,
+ definition: a.fidlLocToStateLoc(d.Loc),
+ kind: tableKind,
+ attrs: d.Attrs,
+ }
+ a.addMembersToSymbolMap(sm, l.Name, d.Name, tableKind, d.Members)
+ }
+ for _, d := range l.UnionDecls {
+ sm[d.Name] = &symbolInfo{
+ lib: l.Name,
+ name: d.Name,
+ definition: a.fidlLocToStateLoc(d.Loc),
+ kind: unionKind,
+ attrs: d.Attrs,
+ }
+ a.addMembersToSymbolMap(sm, l.Name, d.Name, unionKind, d.Members)
+ }
+ for _, d := range l.TypeAliasDecls {
+ sm[d.Name] = &symbolInfo{
+ lib: l.Name,
+ name: d.Name,
+ definition: a.fidlLocToStateLoc(d.Loc),
+ kind: typeAliasKind,
+ typeInfo: d.TypeCtor,
+ attrs: d.Attrs,
+ }
+ }
+
+ return sm, nil
+}
+
+func (a *Analyzer) addMembersToSymbolMap(sm symbolMap, libName string, declName string, kind symbolKind, members []member) {
+ for _, m := range members {
+ memberName := fmt.Sprintf("%s.%s", declName, m.Name)
+ sm[memberName] = &symbolInfo{
+ lib: libName,
+ name: memberName,
+ definition: a.fidlLocToStateLoc(m.Loc),
+ kind: kind,
+ isMember: true,
+ typeInfo: m.Type,
+ maybeFromTypeAlias: m.FromTypeAlias,
+ attrs: m.Attrs,
+ }
+ }
+}
+
+func (a *Analyzer) fidlLocToStateLoc(loc location) state.Location {
+ // If this JSON IR was compiled by the language server, the `Filename`s for
+ // for all the symbols will be tmp input files from the Analyzer. We want
+ // to point at the corresponding editor files for these tmp files.
+ fileID, err := a.inputFileToFileID(loc.Filename)
+ if err != nil {
+ // We assume `Filename` is a filepath to a precompiled JSON IR, provided
+ // in the CompiledLibraries on startup.
+ //
+ // The `Filename` in a decl's location is recorded relative to where
+ // fidlc was invoked. All the locations in CompiledLibraries are
+ // prepended with "../../" which can be replaced by $FUCHSIA_DIR/.
+ fileID = state.FileID(strings.Replace(loc.Filename, "../..", a.cfg.BuildRootDir, 1))
+ }
+ return state.Location{
+ FileID: fileID,
+ Range: state.Range{
+ Start: state.Position{Line: loc.Line - 1, Character: loc.Column - 1},
+ End: state.Position{Line: loc.Line - 1, Character: loc.Column - 1 + loc.Length},
+ },
+ }
+}
diff --git a/fidl-lsp/analysis/definition.go b/fidl-lsp/analysis/definition.go
new file mode 100644
index 0000000..3063f59
--- /dev/null
+++ b/fidl-lsp/analysis/definition.go
@@ -0,0 +1,154 @@
+// 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 analysis
+
+import (
+ "fmt"
+ "io/ioutil"
+ "strings"
+
+ fidlcommon "fidl-lsp/third_party/common"
+
+ "fidl-lsp/state"
+)
+
+// DefinitionOfSymbol returns the location (or locations) where the given symbol
+// is defined, as long as that location is available to the Analyzer (i.e. in
+// a.libs somewhere).
+//
+// For a library name, e.g. `fuchsia.foo`, DefinitionOfSymbol returns the
+// locations of the `library` declarations for each file in that library.
+//
+// DefinitionOfSymbol only works for library names and top-level names: enums,
+// bits, structs, protocols, etc. -- not for member names. For example, it can
+// find the definition of `Foo` in the following example:
+//
+// protocol P {
+// Method(Foo foo);
+// };
+//
+// But it can't find the definition of `B.FOO`:
+//
+// bits B {
+// FOO = 0;
+// }
+// const ZERO = B.FOO;
+func (a *Analyzer) DefinitionOfSymbol(fs *state.FileSystem, sym state.Symbol) ([]state.Location, error) {
+ // If `sym` is a library name, return locations pointing at all the files in
+ // the library (specifically, pointing at their `library` declarations).
+ libName, err := fidlcommon.ReadLibraryName(sym.Name)
+ if err == nil {
+ if _, isLib := a.libs[libName]; isLib {
+ return a.declarationsOfLibrary(fs, libName), nil
+ }
+ }
+
+ // Otherwise, we assume it is a local or fully-qualified name
+ name, err := a.symbolToFullyQualifiedName(fs, sym)
+ if err != nil {
+ return nil, fmt.Errorf(
+ "could not convert symbol `%s` to fully-qualified name: %s",
+ sym.Name,
+ err,
+ )
+ }
+
+ lib, ok := a.libs[name.LibraryName()]
+ if !ok {
+ return nil, fmt.Errorf("unknown library `%s`", name.LibraryName().FullyQualifiedName())
+ }
+ if lib.ir == nil {
+ if err := a.importLibrary(lib.json); err != nil {
+ return nil, fmt.Errorf(
+ "error importing library `%s`: %s",
+ name.LibraryName().FullyQualifiedName(),
+ err,
+ )
+ }
+ }
+
+ // Check that we have a symbolMap for this library, and look up this
+ // symbol's definition location in that symbolMap.
+ if lib.symbols == nil {
+ return nil, fmt.Errorf(
+ "no symbol map for library `%s`",
+ name.LibraryName().FullyQualifiedName(),
+ )
+ }
+ symInfo, ok := lib.symbols[name.FullyQualifiedName()]
+ if !ok {
+ return nil, fmt.Errorf("could not find definition of symbol `%s`", name.FullyQualifiedName())
+ }
+
+ return []state.Location{symInfo.definition}, nil
+}
+
+func (a *Analyzer) declarationsOfLibrary(fs *state.FileSystem, lib fidlcommon.LibraryName) []state.Location {
+ files := []state.FileID{}
+ for file := range a.libs[lib].files {
+ // These files are all absolute paths, but some are to temporary input
+ // files used by the Analyzer. For these, we want to convert the path
+ // to the document URI the editor knows about.
+ if fileID, err := a.inputFileToFileID(file); err != nil {
+ files = append(files, state.FileID(file))
+ } else {
+ files = append(files, fileID)
+ }
+ }
+
+ locs := []state.Location{}
+ // Find the `library` declaration in each file
+ for _, fileID := range files {
+ file, err := fs.File(fileID)
+ if err != nil {
+ // If we don't have the file in memory, read it in
+ bytes, err := ioutil.ReadFile(string(fileID))
+ if err != nil {
+ continue
+ }
+ file = string(bytes)
+ }
+
+ if libraryMatch, ok := state.ParseLibraryMatch(file); ok {
+ locs = append(locs, state.Location{
+ FileID: fileID,
+ Range: libraryMatch.Range,
+ })
+ }
+ }
+ return locs
+}
+
+func (a *Analyzer) symbolToFullyQualifiedName(fs *state.FileSystem, sym state.Symbol) (fidlcommon.Name, error) {
+ // If `sym` is a local name (not fully-qualified), we create a FQN by
+ // attaching its library name.
+ var fqn string
+ if !strings.Contains(sym.Name, ".") {
+ file, err := fs.File(sym.Location.FileID)
+ if err != nil {
+ return fidlcommon.Name{}, fmt.Errorf("could not open file `%s`", sym.Location.FileID)
+ }
+ libName, err := state.LibraryOfFile(file)
+ if err != nil {
+ return fidlcommon.Name{}, fmt.Errorf(
+ "could not find library of symbol `%s` in file `%s`",
+ sym.Name,
+ sym.Location.FileID,
+ )
+ }
+ fqn = libName.FullyQualifiedName() + "/" + sym.Name
+ } else {
+ // If the symbol contains '.', we assume it is a fully-qualified name.
+ i := strings.LastIndex(sym.Name, ".")
+ fqn = sym.Name[:i] + "/" + sym.Name[i+1:]
+ }
+
+ // Convert `fqn` to a fidlcommon.Name.
+ name, err := fidlcommon.ReadName(fqn)
+ if err != nil {
+ return fidlcommon.Name{}, fmt.Errorf("could not read fully-qualified name `%s`: %s", fqn, err)
+ }
+ return name, nil
+}
diff --git a/fidl-lsp/analysis/definition_test.go b/fidl-lsp/analysis/definition_test.go
new file mode 100644
index 0000000..be321e6
--- /dev/null
+++ b/fidl-lsp/analysis/definition_test.go
@@ -0,0 +1,326 @@
+// 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 analysis_test
+
+import (
+ "os"
+ "testing"
+
+ "fidl-lsp/analysis"
+ "fidl-lsp/state"
+)
+
+var (
+ fuchsiaDir = os.Getenv("FUCHSIA_DIR")
+ toolsPath = fuchsiaDir + "/out/default.zircon/tools/"
+ fidlcPath = toolsPath + "fidlc"
+ fidlLintPath = toolsPath + "fidl-lint"
+ fidlFormatPath = toolsPath + "fidl-format"
+)
+
+func defaultConfig() analysis.Config {
+ return analysis.Config{
+ BuildRootDir: fuchsiaDir,
+ FidlcPath: fidlcPath,
+ FidlLintPath: fidlLintPath,
+ FidlFormatPath: fidlFormatPath,
+ }
+}
+
+func TestDefinitionOfLibrary(t *testing.T) {
+ analyzer := analysis.NewAnalyzer(defaultConfig(), analysis.CompiledLibraries{})
+ fs := state.NewFileSystem()
+ fs.NewFile(
+ "file.fidl",
+ `library test;`,
+ )
+ if err := analyzer.Analyze(fs, "file.fidl"); err != nil {
+ t.Fatalf("failed to analyze file: %s", err)
+ }
+
+ locs, err := analyzer.DefinitionOfSymbol(fs, state.Symbol{
+ Name: "test",
+ Location: state.Location{
+ FileID: state.FileID("file.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 0, Character: 8},
+ End: state.Position{Line: 0, Character: 12},
+ },
+ },
+ })
+ if err != nil {
+ t.Fatalf("error getting definition of symbol: %s", err)
+ }
+
+ expLoc := state.Location{
+ FileID: state.FileID("file.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 0, Character: 8},
+ End: state.Position{Line: 0, Character: 12},
+ },
+ }
+ if len(locs) != 1 {
+ t.Fatalf("expected 1 definition location")
+ }
+ if locs[0] != expLoc {
+ t.Errorf("unexpected definition of symbol: expected %v, got %v", expLoc, locs[0])
+ }
+}
+
+func TestDefinitionOfLibraryMultipleFiles(t *testing.T) {
+ analyzer := analysis.NewAnalyzer(defaultConfig(), analysis.CompiledLibraries{})
+ fs := state.NewFileSystem()
+ fs.NewFile(
+ "file1.fidl",
+ `library test;`,
+ )
+ fs.NewFile(
+ "file2.fidl",
+ `library test;`,
+ )
+ if err := analyzer.Analyze(fs, "file1.fidl"); err != nil {
+ t.Fatalf("failed to analyze file: %s", err)
+ }
+ if err := analyzer.Analyze(fs, "file2.fidl"); err != nil {
+ t.Fatalf("failed to analyze file: %s", err)
+ }
+
+ locs, err := analyzer.DefinitionOfSymbol(fs, state.Symbol{
+ Name: "test",
+ Location: state.Location{
+ FileID: state.FileID("file1.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 0, Character: 8},
+ End: state.Position{Line: 0, Character: 12},
+ },
+ },
+ })
+ if err != nil {
+ t.Fatalf("error getting definition of symbol: %s", err)
+ }
+
+ expLocs := []state.Location{
+ {
+ FileID: state.FileID("file1.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 0, Character: 8},
+ End: state.Position{Line: 0, Character: 12},
+ },
+ },
+ {
+ FileID: state.FileID("file2.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 0, Character: 8},
+ End: state.Position{Line: 0, Character: 12},
+ },
+ },
+ }
+ if len(locs) != len(expLocs) {
+ t.Fatalf("expected %d definition locations", len(expLocs))
+ }
+ for _, expLoc := range expLocs {
+ found := false
+ for _, loc := range locs {
+ if expLoc == loc {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Errorf("expected but did not find definition of symbol: %v", expLoc)
+ }
+ }
+}
+
+func TestDefinitionOfLibraryImport(t *testing.T) {
+ analyzer := analysis.NewAnalyzer(defaultConfig(), analysis.CompiledLibraries{})
+ fs := state.NewFileSystem()
+ fs.NewFile(
+ "import.fidl",
+ `
+library fuchsia.import;
+
+const uint8 FOO = 0;
+`,
+ )
+ fs.NewFile(
+ "test.fidl",
+ `
+library test;
+using fuchsia.import;
+
+const uint8 BAR = fuchsia.import.FOO;
+`,
+ )
+ if err := analyzer.Analyze(fs, "import.fidl"); err != nil {
+ t.Fatalf("failed to analyze file: %s", err)
+ }
+ if err := analyzer.Analyze(fs, "test.fidl"); err != nil {
+ t.Fatalf("failed to analyze file: %s", err)
+ }
+
+ locs, err := analyzer.DefinitionOfSymbol(fs, state.Symbol{
+ Name: "fuchsia.import",
+ Location: state.Location{
+ FileID: state.FileID("test.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 3, Character: 6},
+ End: state.Position{Line: 3, Character: 20},
+ },
+ },
+ })
+ if err != nil {
+ t.Fatalf("error getting definition of symbol: %s", err)
+ }
+
+ expLoc := state.Location{
+ FileID: state.FileID("import.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 1, Character: 8},
+ End: state.Position{Line: 1, Character: 22},
+ },
+ }
+ if len(locs) != 1 {
+ t.Fatalf("expected 1 definition location")
+ }
+ if locs[0] != expLoc {
+ t.Errorf("unexpected definition of symbol: expected %v, got %v", expLoc, locs[0])
+ }
+}
+
+func TestDefinitionOfFullyQualifiedSymbol(t *testing.T) {
+ analyzer := analysis.NewAnalyzer(defaultConfig(), analysis.CompiledLibraries{})
+ fs := state.NewFileSystem()
+ fs.NewFile(
+ "import.fidl",
+ `
+library fuchsia.import;
+
+struct Foo {};
+`,
+ )
+ fs.NewFile(
+ "test.fidl",
+ `
+library test;
+using fuchsia.import;
+
+protocol Baz {
+ Method(fuchsia.import.Foo f);
+};
+`,
+ )
+ if err := analyzer.Analyze(fs, "import.fidl"); err != nil {
+ t.Fatalf("failed to analyze file: %s", err)
+ }
+ if err := analyzer.Analyze(fs, "test.fidl"); err != nil {
+ t.Fatalf("failed to analyze file: %s", err)
+ }
+
+ locs, err := analyzer.DefinitionOfSymbol(fs, state.Symbol{
+ Name: "fuchsia.import.Foo",
+ Location: state.Location{
+ FileID: state.FileID("test.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 5, Character: 11},
+ End: state.Position{Line: 5, Character: 29},
+ },
+ },
+ })
+ if err != nil {
+ t.Fatalf("error getting definition of symbol: %s", err)
+ }
+
+ expLoc := state.Location{
+ FileID: state.FileID("import.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 3, Character: 7},
+ End: state.Position{Line: 3, Character: 10},
+ },
+ }
+ if len(locs) != 1 {
+ t.Fatalf("expected 1 definition location")
+ }
+ if locs[0] != expLoc {
+ t.Errorf("unexpected definition of symbol: expected %v, got %v", expLoc, locs[0])
+ }
+}
+
+func TestDefinitionOfLocalSymbol(t *testing.T) {
+ analyzer := analysis.NewAnalyzer(defaultConfig(), analysis.CompiledLibraries{})
+ fs := state.NewFileSystem()
+ fs.NewFile(
+ "test.fidl",
+ `
+library test;
+
+struct Foo {};
+
+protocol Baz {
+ Method(Foo f);
+};
+`,
+ )
+ if err := analyzer.Analyze(fs, "test.fidl"); err != nil {
+ t.Fatalf("failed to analyze file: %s", err)
+ }
+
+ locs, err := analyzer.DefinitionOfSymbol(fs, state.Symbol{
+ Name: "Foo",
+ Location: state.Location{
+ FileID: state.FileID("test.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 6, Character: 11},
+ End: state.Position{Line: 6, Character: 14},
+ },
+ },
+ })
+ if err != nil {
+ t.Fatalf("error getting definition of symbol: %s", err)
+ }
+
+ expLoc := state.Location{
+ FileID: state.FileID("test.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 3, Character: 7},
+ End: state.Position{Line: 3, Character: 10},
+ },
+ }
+ if len(locs) != 1 {
+ t.Fatalf("expected 1 definition location")
+ }
+ if locs[0] != expLoc {
+ t.Errorf("unexpected definition of symbol: expected %v, got %v", expLoc, locs[0])
+ }
+}
+
+func TestDefinitionOfReservedKeyword(t *testing.T) {
+ analyzer := analysis.NewAnalyzer(defaultConfig(), analysis.CompiledLibraries{})
+ fs := state.NewFileSystem()
+ fs.NewFile(
+ "test.fidl",
+ `
+library test;
+
+struct Foo {};
+`,
+ )
+ if err := analyzer.Analyze(fs, "test.fidl"); err != nil {
+ t.Fatalf("failed to analyze file: %s", err)
+ }
+
+ if _, err := analyzer.DefinitionOfSymbol(fs, state.Symbol{
+ Name: "struct",
+ Location: state.Location{
+ FileID: state.FileID("test.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 3, Character: 0},
+ End: state.Position{Line: 3, Character: 6},
+ },
+ },
+ }); err == nil {
+ t.Fatalf("expect error on definition of reserved keyword")
+ }
+}
diff --git a/fidl-lsp/analysis/deps.go b/fidl-lsp/analysis/deps.go
index dda1100..506ecfa 100644
--- a/fidl-lsp/analysis/deps.go
+++ b/fidl-lsp/analysis/deps.go
@@ -30,8 +30,8 @@
JSON string
}
-// GetFidlProject builds a CompiledLibraries out of a fidl_project.json file.
-func GetFidlProject(fidlProjectPath string) (CompiledLibraries, error) {
+// LoadFidlProject builds a CompiledLibraries out of a fidl_project.json file.
+func LoadFidlProject(fidlProjectPath string) (CompiledLibraries, error) {
data, err := ioutil.ReadFile(fidlProjectPath)
if err != nil {
return nil, fmt.Errorf("failed to read file: %s", err)
diff --git a/fidl-lsp/analysis/deps_test.go b/fidl-lsp/analysis/deps_test.go
index 942946f..04e7140 100644
--- a/fidl-lsp/analysis/deps_test.go
+++ b/fidl-lsp/analysis/deps_test.go
@@ -75,7 +75,7 @@
// itself, which we don't check. So there should be (# of dependencies + 1)
// total --files args.
if len(fidlcArgs) != len(expArgs)+1 {
- t.Fatalf("incorrect number of fidlc --files args %d: expected %d", len(fidlcArgs), len(expArgs)+len(fs.Files()))
+ t.Fatalf("incorrect number of fidlc --files args %d: expected %d", len(fidlcArgs), len(expArgs)+fs.NFiles())
}
for _, expArg := range expArgs {
if indexOf(fidlcArgs, expArg) == -1 {
@@ -128,7 +128,7 @@
"--files",
}
if len(fidlcArgs) != len(expArgs)+1 {
- t.Fatalf("incorrect number of fidlc --files args %d: expected %d", len(fidlcArgs), len(expArgs)+len(fs.Files()))
+ t.Fatalf("incorrect number of fidlc --files args %d: expected %d", len(fidlcArgs), len(expArgs)+fs.NFiles())
}
for _, expArg := range expArgs {
if indexOf(fidlcArgs, expArg) == -1 {
@@ -194,7 +194,7 @@
}
if len(fidlcArgs) != len(expArgs)+2 {
t.Errorf("args: %v\n", fidlcArgs)
- t.Fatalf("incorrect number of fidlc --files args %d: expected %d", len(fidlcArgs), len(expArgs)+len(fs.Files()))
+ t.Fatalf("incorrect number of fidlc --files args %d: expected %d", len(fidlcArgs), len(expArgs)+fs.NFiles())
}
// We don't test the topological sort, just that all the required files are present.
for _, expArg := range expArgs {
diff --git a/fidl-lsp/analysis/diagnostics.go b/fidl-lsp/analysis/diagnostics.go
index eb93394..05a42b6 100644
--- a/fidl-lsp/analysis/diagnostics.go
+++ b/fidl-lsp/analysis/diagnostics.go
@@ -55,15 +55,15 @@
}
fileToDiags := make(map[state.FileID][]Diagnostic)
for i := range diags {
- editorFile, err := a.inputFileToFileID(diags[i].Path)
+ fileID, err := a.inputFileToFileID(diags[i].Path)
if err != nil {
continue
}
- if _, ok := fileToDiags[editorFile]; !ok {
- fileToDiags[editorFile] = []Diagnostic{}
+ if _, ok := fileToDiags[fileID]; !ok {
+ fileToDiags[fileID] = []Diagnostic{}
}
diags[i].Source = Fidlc
- fileToDiags[editorFile] = append(fileToDiags[editorFile], diags[i])
+ fileToDiags[fileID] = append(fileToDiags[fileID], diags[i])
}
return fileToDiags, nil
}
@@ -81,15 +81,15 @@
}
fileToDiags := make(map[state.FileID][]Diagnostic)
for i := range diags {
- editorFile, err := a.inputFileToFileID(diags[i].Path)
+ fileID, err := a.inputFileToFileID(diags[i].Path)
if err != nil {
continue
}
- if _, ok := fileToDiags[editorFile]; !ok {
- fileToDiags[editorFile] = []Diagnostic{}
+ if _, ok := fileToDiags[fileID]; !ok {
+ fileToDiags[fileID] = []Diagnostic{}
}
diags[i].Source = FidlLint
- fileToDiags[editorFile] = append(fileToDiags[editorFile], diags[i])
+ fileToDiags[fileID] = append(fileToDiags[fileID], diags[i])
}
return fileToDiags, nil
}
diff --git a/fidl-lsp/langserver/definition.go b/fidl-lsp/langserver/definition.go
new file mode 100644
index 0000000..d0468ea
--- /dev/null
+++ b/fidl-lsp/langserver/definition.go
@@ -0,0 +1,45 @@
+// 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"
+
+ "fidl-lsp/state"
+)
+
+func (h *LangHandler) handleDefinition(params lsp.TextDocumentPositionParams) ([]lsp.Location, error) {
+ sym, err := h.fs.SymbolAtPos(
+ state.FileID(params.TextDocument.URI),
+ state.Position{Line: params.Position.Line, Character: params.Position.Character},
+ )
+ if err != nil {
+ h.log.Printf(
+ "could not find symbol at position `%#v` in document `%s`\n",
+ params.Position,
+ params.TextDocument.URI,
+ )
+ return nil, err
+ }
+
+ locs, err := h.analyzer.DefinitionOfSymbol(h.fs, sym)
+ if err != nil {
+ h.log.Printf("error on definition: %s\n", err)
+ return nil, err
+ }
+
+ // Convert state.Locations --> lsp.Locations
+ lspLocs := make([]lsp.Location, len(locs))
+ for i, loc := range locs {
+ lspLocs[i] = lsp.Location{
+ URI: lsp.DocumentURI(loc.FileID),
+ Range: lsp.Range{
+ Start: lsp.Position{Line: loc.Range.Start.Line, Character: loc.Range.Start.Character},
+ End: lsp.Position{Line: loc.Range.End.Line, Character: loc.Range.End.Character},
+ },
+ }
+ }
+ return lspLocs, nil
+}
diff --git a/fidl-lsp/langserver/diagnostics.go b/fidl-lsp/langserver/diagnostics.go
index 8845107..01d1212 100644
--- a/fidl-lsp/langserver/diagnostics.go
+++ b/fidl-lsp/langserver/diagnostics.go
@@ -49,8 +49,8 @@
// Convert analysis.Diagnostics --> lsp.Diagnostics
lspDiags := make(map[lsp.DocumentURI][]lsp.Diagnostic)
- for editorFile, fileDiags := range diags {
- fileURI := lsp.DocumentURI(editorFile)
+ for fileID, fileDiags := range diags {
+ fileURI := lsp.DocumentURI(fileID)
lspDiags[fileURI] = []lsp.Diagnostic{}
for _, diag := range fileDiags {
diff --git a/fidl-lsp/langserver/handler.go b/fidl-lsp/langserver/handler.go
index 8ba6a13..f688369 100644
--- a/fidl-lsp/langserver/handler.go
+++ b/fidl-lsp/langserver/handler.go
@@ -52,7 +52,10 @@
type Config struct {
serverCapabilities serverCapabilities
- baseRefURI string
+
+ // The base URI for reference links. Currently we linkify Fuchsia platform
+ // library imports, and the links point to the SDK docs.
+ baseRefURI string
}
func (c *Config) FormatRefURI(libraryName fidlcommon.LibraryName) string {
@@ -63,6 +66,7 @@
func NewDefaultConfig() Config {
return Config{
serverCapabilities: serverCapabilities{
+ DefinitionProvider: true,
DocumentFormattingProvider: true,
DocumentLinkProvider: &documentLinkOptions{},
TextDocumentSync: &lsp.TextDocumentSyncOptionsOrKind{
@@ -223,6 +227,17 @@
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}
diff --git a/fidl-lsp/langserver/links.go b/fidl-lsp/langserver/links.go
index f189383..378bff8 100644
--- a/fidl-lsp/langserver/links.go
+++ b/fidl-lsp/langserver/links.go
@@ -15,10 +15,10 @@
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)
+ return nil, fmt.Errorf("could not find file `%s`", params.TextDocument.URI)
}
imports := state.ParsePlatformImportsMatch(file)
- links := make([]documentLink, 0)
+ links := []documentLink{}
for _, m := range imports {
links = append(links, documentLink{
Range: lsp.Range{
diff --git a/fidl-lsp/langserver/links_test.go b/fidl-lsp/langserver/links_test.go
index 65eccf6..2c65e7a 100644
--- a/fidl-lsp/langserver/links_test.go
+++ b/fidl-lsp/langserver/links_test.go
@@ -5,6 +5,7 @@
package langserver
import (
+ "strings"
"testing"
"github.com/sourcegraph/go-lsp"
@@ -19,6 +20,7 @@
using fuchsia.io;
using non.platform.import;
`
+ lines := strings.SplitAfter(fileText, "\n")
handler := NewHandlerWithFiles(TestFile{
Name: "test.fidl",
@@ -43,14 +45,14 @@
found := false
for _, link := range links {
start, err := state.OffsetInFile(
- fileText,
+ lines,
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,
+ lines,
state.Position{Line: link.Range.End.Line, Character: link.Range.End.Character},
)
if err != nil {
diff --git a/fidl-lsp/langserver/testutils.go b/fidl-lsp/langserver/testutils.go
index fb0a618..43ce4c5 100644
--- a/fidl-lsp/langserver/testutils.go
+++ b/fidl-lsp/langserver/testutils.go
@@ -31,6 +31,7 @@
log.New(os.Stderr, "[LSP Test] ", log.Lshortfile),
analysis.NewAnalyzer(
analysis.Config{
+ BuildRootDir: fuchsiaDir,
FidlcPath: fidlcPath,
FidlLintPath: fidlLintPath,
FidlFormatPath: fidlFormatPath,
diff --git a/fidl-lsp/main.go b/fidl-lsp/main.go
index aed5295..8a387bd 100644
--- a/fidl-lsp/main.go
+++ b/fidl-lsp/main.go
@@ -51,12 +51,13 @@
// fidl_project.json is generated by running
// `python3 scripts/gen_fidl_project.py path/to/fidl_project.json`
- fidlProject, err := analysis.GetFidlProject(fidlProjectPath)
+ fidlProject, err := analysis.LoadFidlProject(fidlProjectPath)
if err != nil {
trace.Fatalf("Failed to parse fidl_project.json at `%s`: %s\n", fidlProjectPath, err)
}
analyzer := analysis.NewAnalyzer(
analysis.Config{
+ BuildRootDir: fuchsiaDir,
FidlcPath: fidlcPath,
FidlLintPath: fidlLintPath,
FidlFormatPath: fidlFormatPath,
diff --git a/fidl-lsp/state/fs.go b/fidl-lsp/state/fs.go
index b018490..b6eb790 100644
--- a/fidl-lsp/state/fs.go
+++ b/fidl-lsp/state/fs.go
@@ -23,7 +23,12 @@
// files on the client.
type FileSystem struct {
// TODO: should this be a map[FileID][]byte instead?
- files map[FileID]string
+
+ // `files` is stored as a map of FileIDs to files that are split by line
+ // (but retaining their '\n' characters).
+ // This speeds up the common operation of searching for a symbol at a given
+ // (line, column) in the file as the file is indexed by line.
+ files map[FileID][]string
// TODO: RWLock for safe concurrency
}
@@ -42,6 +47,12 @@
Character int
}
+// Location represents a range in a particular file.
+type Location struct {
+ FileID FileID
+ Range Range
+}
+
// Change represents a single diff in a source file.
// Range is the range that is being changed. RangeLength is the length of the
// replacement text (0 if the range is being deleted). Text is the text to
@@ -54,27 +65,27 @@
// NewFileSystem returns an initialized empty FileSystem.
func NewFileSystem() *FileSystem {
return &FileSystem{
- files: make(map[FileID]string),
+ files: make(map[FileID][]string),
}
}
// File is a read-only accessor for the in-memory file with ID `path`.
func (fs *FileSystem) File(path FileID) (string, error) {
- if file, ok := fs.files[path]; ok {
- return file, nil
+ if lines, ok := fs.files[path]; ok {
+ return strings.Join(lines, ""), nil
}
return "", fmt.Errorf("file `%s` not in memory", path)
}
-// Files returns all of the in-memory files for traversal.
-func (fs *FileSystem) Files() map[FileID]string {
- return fs.files
+// NFiles returns the number of in-memory files.
+func (fs *FileSystem) NFiles() int {
+ return len(fs.files)
}
// NewFile copies `text` into the FileSystem's in-memory file system ("opening"
// the file), indexed by the FileID `path`.
func (fs *FileSystem) NewFile(path FileID, text string) {
- fs.files[path] = text
+ fs.files[path] = strings.SplitAfter(text, "\n")
}
// DeleteFile deletes the FileSystem's in-memory representation of the file
@@ -95,51 +106,47 @@
return err
}
+ file := strings.Join(fs.files[path], "")
+
// Copy file over to new buffer with inserted text change
var newFile strings.Builder
- newFile.WriteString(fs.files[path][:start])
+ newFile.WriteString(file[:start])
newFile.WriteString(change.NewContent)
- newFile.WriteString(fs.files[path][end:])
- fs.files[path] = newFile.String()
+ newFile.WriteString(file[end:])
+ fs.files[path] = strings.SplitAfter(newFile.String(), "\n")
}
return nil
}
// 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
- )
- for _, c := range contents {
- if line == pos.Line && col == pos.Character {
- return offset, nil
- }
- offset++
- if c == '\n' {
- line++
- col = 0
- } else {
- col++
+func OffsetInFile(lines []string, pos Position) (uint, error) {
+ var offset uint
+ for line, l := range lines {
+ for col := range l {
+ if line == pos.Line && col == pos.Character {
+ return offset, nil
+ }
+ offset++
}
}
// Check if pos is pointing at the end of the file
- if line == pos.Line && col == pos.Character {
+ lastLine := len(lines) - 1
+ if pos.Line == lastLine && pos.Character == len(lines[lastLine]) {
return offset, nil
}
return 0, fmt.Errorf(
"position %#v was out of bounds in file with %d lines, %d total chars",
pos,
- line+1,
+ len(lines),
offset,
)
}
// FileIDToPath converts a FileID, which is in the form of a file schema URI
// ("file:///"), to an absolute path.
-func FileIDToPath(editorFile FileID) (string, error) {
- fileURI, err := url.Parse(string(editorFile))
+func FileIDToPath(fileID FileID) (string, error) {
+ fileURI, err := url.Parse(string(fileID))
if err != nil {
return "", fmt.Errorf("could not parse FileID: %s", err)
}
diff --git a/fidl-lsp/state/parse.go b/fidl-lsp/state/parse.go
index 083f822..f91f9b3 100644
--- a/fidl-lsp/state/parse.go
+++ b/fidl-lsp/state/parse.go
@@ -103,3 +103,67 @@
},
}
}
+
+// https://fuchsia.dev/fuchsia-src/development/languages/fidl/reference/language.md#identifiers
+//
+// This also includes a '.' as long as it is not initial or trailing, so we can
+// capture fully-qualified names and library names.
+var identRegexp = regexp.MustCompile(`\b[a-zA-Z](?:[a-zA-Z0-9_.]*[a-zA-Z0-9])?\b`)
+
+// Symbol represents a FIDL identifier.
+// It could be a fully-qualified name, a local declaration name, a library name,
+// etc.
+type Symbol struct {
+ Name string
+ Location Location
+}
+
+// SymbolAtPos returns the FIDL identifier that is at `pos` in `file`.
+func (fs *FileSystem) SymbolAtPos(path FileID, pos Position) (Symbol, error) {
+ lines, ok := fs.files[path]
+ if !ok {
+ return Symbol{}, fmt.Errorf("could not find file `%s`", path)
+ }
+
+ if pos.Line >= len(lines) {
+ return Symbol{}, fmt.Errorf("position %#v in file `%s` is out of bounds", pos, path)
+ }
+ line := lines[pos.Line]
+ tokens := identRegexp.FindAllStringIndex(line, -1)
+ if len(tokens) == 0 {
+ return Symbol{}, fmt.Errorf("could not find any symbols in line %d in file `%s`", pos.Line, path)
+ }
+
+ var name string
+ r := Range{
+ Start: Position{Line: pos.Line},
+ End: Position{Line: pos.Line},
+ }
+ for _, token := range tokens {
+ if pos.Character >= token[0] &&
+ pos.Character <= token[1] {
+ name = line[token[0]:token[1]]
+ r.Start.Character = token[0]
+ r.End.Character = token[1]
+ break
+ }
+ }
+ if name == "" {
+ return Symbol{}, fmt.Errorf("could not find symbol at pos %#v in file `%s`", pos, path)
+ }
+
+ // TODO: could exclude all FIDL keywords, to save work
+
+ // TODO: check that the symbol is not part of a comment: if the line
+ // contains "//" and the symbol is after the "//", then don't return the
+ // symbol. "//" could also be part of a string const, but that also wouldn't
+ // be an identifer.
+
+ return Symbol{
+ Name: name,
+ Location: Location{
+ FileID: path,
+ Range: r,
+ },
+ }, nil
+}
diff --git a/fidl-lsp/state/parse_test.go b/fidl-lsp/state/parse_test.go
index cb050ee..6e32a0d 100644
--- a/fidl-lsp/state/parse_test.go
+++ b/fidl-lsp/state/parse_test.go
@@ -130,3 +130,158 @@
}
}
}
+
+func TestSymbolAtPos(t *testing.T) {
+ fs := state.NewFileSystem()
+ fs.NewFile("test.fidl", `
+library example;
+using test.import;
+
+struct Foo {
+ uint8 bar_baz;
+};
+`)
+
+ cases := []struct {
+ pos state.Position
+ sym state.Symbol
+ }{
+ {
+ pos: state.Position{Line: 1, Character: 0},
+ sym: state.Symbol{
+ Name: "library",
+ Location: state.Location{
+ FileID: state.FileID("test.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 1, Character: 0},
+ End: state.Position{Line: 1, Character: 7},
+ },
+ },
+ },
+ },
+ {
+ pos: state.Position{Line: 1, Character: 15},
+ sym: state.Symbol{
+ Name: "example",
+ Location: state.Location{
+ FileID: state.FileID("test.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 1, Character: 8},
+ End: state.Position{Line: 1, Character: 15},
+ },
+ },
+ },
+ },
+ {
+ pos: state.Position{Line: 2, Character: 11},
+ sym: state.Symbol{
+ Name: "test.import",
+ Location: state.Location{
+ FileID: state.FileID("test.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 2, Character: 6},
+ End: state.Position{Line: 2, Character: 17},
+ },
+ },
+ },
+ },
+ {
+ pos: state.Position{Line: 4, Character: 10},
+ sym: state.Symbol{
+ Name: "Foo",
+ Location: state.Location{
+ FileID: state.FileID("test.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 4, Character: 7},
+ End: state.Position{Line: 4, Character: 10},
+ },
+ },
+ },
+ },
+ {
+ pos: state.Position{Line: 5, Character: 13},
+ sym: state.Symbol{
+ Name: "bar_baz",
+ Location: state.Location{
+ FileID: state.FileID("test.fidl"),
+ Range: state.Range{
+ Start: state.Position{Line: 5, Character: 10},
+ End: state.Position{Line: 5, Character: 17},
+ },
+ },
+ },
+ },
+ }
+
+ for _, ex := range cases {
+ sym, err := fs.SymbolAtPos(
+ state.FileID("test.fidl"),
+ ex.pos,
+ )
+ if err != nil {
+ t.Fatalf("error finding symbol: %s", err)
+ }
+ if sym.Name != ex.sym.Name {
+ t.Fatalf("did not find correct symbol; expected %s, found %s", ex.sym.Name, sym.Name)
+ }
+ if sym.Location.FileID != ex.sym.Location.FileID {
+ t.Fatalf(
+ "incorrect file; expected %s, found %s",
+ ex.sym.Location.FileID,
+ sym.Location.FileID,
+ )
+ }
+ if sym.Location.Range != ex.sym.Location.Range {
+ t.Fatalf(
+ "incorrect range; expected %#v, found %#v",
+ ex.sym.Location.Range,
+ sym.Location.Range,
+ )
+ }
+ }
+}
+
+func TestNoSymbolAtPos(t *testing.T) {
+ fs := state.NewFileSystem()
+ fs.NewFile("test.fidl", `
+// ^ should not be a symbol
+library example;
+// ^ should not be a symbol
+
+struct Foo {
+// ^ should not be a symbol
+ uint8 foo;
+//^ should not be a symbol
+};
+
+.prepended.dot
+^ should not be a symbol ('prepended.dot' is a valid symbol)
+trailing.dot.
+ ^ should not be a symbol ('trailing.dot' is a valid symbol)
+0starts_with_a_number
+ ^ should not be a symbol
+ends_with_a_semicolon_
+ ^ should not be a symbol
+`)
+
+ cases := []state.Position{
+ {Line: 0, Character: 0},
+ {Line: 2, Character: 16},
+ {Line: 5, Character: 11},
+ {Line: 7, Character: 2},
+ {Line: 11, Character: 0},
+ {Line: 13, Character: 13},
+ {Line: 15, Character: 3},
+ {Line: 17, Character: 3},
+ }
+
+ for _, ex := range cases {
+ sym, err := fs.SymbolAtPos(
+ state.FileID("test.fidl"),
+ ex,
+ )
+ if err == nil {
+ t.Errorf("expected not to find symbol at position %v, but found %s", ex, sym.Name)
+ }
+ }
+}