| // 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/analysis" |
| "fidl-lsp/state" |
| ) |
| |
| // handleHover asks the FileSystem for the symbol at the location specified in |
| // `params`, and then asks the Analyzer for the type of that symbol. |
| // |
| // It then converts that type to a human-readable string representation that |
| // will be displayed as a tooltip, including doc comments, other attributes on |
| // the type, and the type itself. For example, given this FIDL: |
| // |
| // library example; |
| // |
| // /// Foo is a struct. |
| // [Attribute = "Value"] |
| // struct Foo {}; |
| // |
| // protocol Bar { |
| // Method(Foo foo); |
| // ~~~ |
| // } |
| // |
| // Hovering over the `~~~` would give you this tooltip: |
| // |
| // Foo is a struct. // In plain text |
| // [Attribute = "Value"] // In monospace font |
| // struct example/Foo |
| // |
| func (h *LangHandler) handleHover(params lsp.TextDocumentPositionParams) (lsp.Hover, 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 lsp.Hover{}, err |
| } |
| |
| symType, err := h.analyzer.TypeOfSymbol(h.fs, sym) |
| if err != nil { |
| h.log.Printf("could not get type of symbol `%s`: %s", sym.Name, err) |
| return lsp.Hover{}, err |
| } |
| |
| // Convert SymbolType --> []lsp.MarkedString |
| typeDescription, err := symbolTypeToMarkedStrings(sym.Name, symType) |
| if err != nil { |
| h.log.Printf("could not get string representation of symbol `%s`'s type: %s", sym.Name, err) |
| return lsp.Hover{}, err |
| } |
| |
| return lsp.Hover{ |
| Contents: typeDescription, |
| Range: &lsp.Range{ |
| Start: lsp.Position{ |
| Line: sym.Location.Range.Start.Line, |
| Character: sym.Location.Range.Start.Character, |
| }, |
| End: lsp.Position{ |
| Line: sym.Location.Range.End.Line, |
| Character: sym.Location.Range.End.Character, |
| }, |
| }, |
| }, nil |
| } |
| |
| func newFIDLString(s string) lsp.MarkedString { |
| return lsp.MarkedString{ |
| Language: "fidl", |
| Value: s, |
| } |
| } |
| |
| func symbolTypeToMarkedStrings(name string, symType analysis.Type) ([]lsp.MarkedString, error) { |
| if symType.IsLib { |
| return []lsp.MarkedString{{ |
| Language: "fidl", |
| Value: fmt.Sprintf("library %s", name), |
| }}, nil |
| } |
| |
| res := []lsp.MarkedString{} |
| |
| var docStr *lsp.MarkedString |
| attrList := "[" |
| firstAttr := true |
| // Add attributes as a marked string |
| for _, attr := range symType.Attrs { |
| if attr.Name == "Doc" { |
| rawMarkedStr := lsp.RawMarkedString(attr.Value) |
| docStr = &rawMarkedStr |
| } else { |
| if !firstAttr { |
| attrList += ", " |
| } |
| firstAttr = false |
| if attr.Value != "" { |
| attrList += fmt.Sprintf("%s = \"%s\"", attr.Name, attr.Value) |
| } else { |
| attrList += attr.Name |
| } |
| } |
| } |
| attrList += "]" |
| // Prepend doc string if there is one |
| if docStr != nil { |
| res = append([]lsp.MarkedString{*docStr}, res...) |
| } |
| if !firstAttr { |
| res = append(res, lsp.MarkedString{ |
| Language: "fidl", |
| Value: attrList, |
| }) |
| } |
| |
| switch symType.Kind { |
| default: |
| return nil, fmt.Errorf("unknown kind of symbol: %s", symType.Kind) |
| |
| case analysis.ArrayType: |
| if symType.Array == nil { |
| goto TypeInfoNotSet |
| } |
| res = append(res, newFIDLString(shortName(symType))) |
| |
| case analysis.VectorType: |
| if symType.Vector == nil { |
| goto TypeInfoNotSet |
| } |
| res = append(res, newFIDLString(shortName(symType))) |
| |
| case analysis.StringType: |
| if symType.String == nil { |
| goto TypeInfoNotSet |
| } |
| res = append(res, newFIDLString(shortName(symType))) |
| |
| case analysis.HandleType: |
| if symType.Handle == nil { |
| goto TypeInfoNotSet |
| } |
| res = append(res, newFIDLString(shortName(symType))) |
| |
| case analysis.RequestType: |
| if symType.Request == nil { |
| goto TypeInfoNotSet |
| } |
| res = append(res, newFIDLString(shortName(symType))) |
| |
| case analysis.PrimitiveType: |
| if symType.Primitive == nil { |
| goto TypeInfoNotSet |
| } |
| res = append(res, newFIDLString(shortName(symType))) |
| |
| case analysis.IdentifierType: |
| if symType.Identifier == nil { |
| goto TypeInfoNotSet |
| } |
| |
| if !symType.Identifier.IsDecl { |
| res = append(res, newFIDLString(shortName(symType))) |
| break |
| } |
| |
| switch symType.Identifier.Kind { |
| default: |
| return nil, fmt.Errorf("unknown identifier kind: %s", symType.Identifier.Kind) |
| |
| case analysis.BitsKind: |
| if symType.Identifier.Bits == nil { |
| goto IdentifierTypeInfoNotSet |
| } |
| res = append(res, newFIDLString( |
| fmt.Sprintf( |
| "bits %s: %s", |
| symType.Identifier.Name, |
| shortName(symType.Identifier.Bits.Type), |
| ), |
| )) |
| |
| case analysis.EnumKind: |
| if symType.Identifier.Enum == nil { |
| goto IdentifierTypeInfoNotSet |
| } |
| res = append(res, newFIDLString( |
| fmt.Sprintf( |
| "enum %s: %s", |
| symType.Identifier.Name, |
| symType.Identifier.Enum.Type, |
| ), |
| )) |
| |
| case analysis.ConstKind: |
| if symType.Identifier.Const == nil { |
| goto IdentifierTypeInfoNotSet |
| } |
| res = append(res, newFIDLString( |
| fmt.Sprintf( |
| "const %s %s = `%s`", |
| shortName(symType.Identifier.Const.Type), |
| symType.Identifier.Name, |
| symType.Identifier.Const.Value, |
| ), |
| )) |
| |
| case analysis.TypeAliasKind: |
| if symType.Identifier.TypeAlias == nil { |
| goto IdentifierTypeInfoNotSet |
| } |
| res = append(res, newFIDLString( |
| fmt.Sprintf( |
| "%s: alias to %s", |
| symType.Identifier.Name, |
| shortName(symType.Identifier.TypeAlias.Type), |
| ), |
| )) |
| |
| case analysis.ProtocolKind, |
| analysis.ServiceKind, |
| analysis.StructKind, |
| analysis.TableKind, |
| analysis.UnionKind: |
| res = append(res, newFIDLString( |
| fmt.Sprintf("%s %s", symType.Identifier.Kind, symType.Identifier.Name), |
| )) |
| } |
| } |
| |
| return res, nil |
| |
| TypeInfoNotSet: |
| return nil, fmt.Errorf( |
| "symbol is tagged as a `%s` but `%s` type info not set", |
| symType.Kind, |
| symType.Kind, |
| ) |
| IdentifierTypeInfoNotSet: |
| return nil, fmt.Errorf( |
| "symbol is tagged as a `%s` but `%s` type info not set", |
| symType.Identifier.Kind, |
| symType.Identifier.Kind, |
| ) |
| } |
| |
| func shortName(t analysis.Type) string { |
| switch t.Kind { |
| default: |
| return "unknown type" |
| |
| case analysis.ArrayType: |
| return fmt.Sprintf("array<%s>:%d", shortName(t.Array.ElementType), t.Array.ElementCount) |
| |
| case analysis.VectorType: |
| str := fmt.Sprintf("vector<%s>", shortName(t.Vector.ElementType)) |
| if t.Vector.ElementCount != nil { |
| str += fmt.Sprintf(":%d", *t.Vector.ElementCount) |
| } |
| if t.Vector.Nullable { |
| str += "?" |
| } |
| return str |
| |
| case analysis.StringType: |
| str := "string" |
| if t.String.ElementCount != nil { |
| str += fmt.Sprintf(":%d", *t.String.ElementCount) |
| } |
| if t.String.Nullable { |
| str += "?" |
| } |
| return str |
| |
| case analysis.RequestType: |
| str := fmt.Sprintf("request<%s>", t.Request.Subtype) |
| if t.Request.Nullable { |
| str += "?" |
| } |
| return str |
| |
| case analysis.HandleType: |
| str := fmt.Sprintf("handle<%s>", t.Handle.Subtype) |
| if t.Handle.Nullable { |
| str += "?" |
| } |
| return str |
| |
| case analysis.PrimitiveType: |
| return string(t.Primitive.Subtype) |
| |
| case analysis.IdentifierType: |
| var str string |
| if t.Identifier.IsDecl { |
| str = t.Identifier.Name |
| } else { |
| str = t.Identifier.Identifier |
| } |
| if t.Identifier.Nullable { |
| str += "?" |
| } |
| return str |
| } |
| } |