Skip to content

Commit

Permalink
Completion bugfixes; add type/option completion for enums and services
Browse files Browse the repository at this point in the history
  • Loading branch information
kralicky committed Jan 20, 2024
1 parent d0614af commit eed5ed6
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 35 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ require (
github.com/bufbuild/protovalidate-go v0.4.3
github.com/google/cel-go v0.18.2
github.com/kralicky/gpkg v0.0.0-20240119195700-64f32830b14f
github.com/kralicky/protocompile v0.0.0-20240119214132-b305d384cad9
github.com/kralicky/protocompile v0.0.0-20240120215100-ab7a9f0a39c3
github.com/kralicky/tools-lite v0.0.0-20240104191314-c259ddd5a342
github.com/mattn/go-tty v0.0.5
github.com/spf13/cobra v1.8.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kralicky/gpkg v0.0.0-20240119195700-64f32830b14f h1:MsNe8A51V+7Fu5OMXSl8SK02erPJ40vFs2zDHn89w1g=
github.com/kralicky/gpkg v0.0.0-20240119195700-64f32830b14f/go.mod h1:vOkwMjs49XmP/7Xfo9ZL6eg2ei51lmtD/4U/Az5GTq8=
github.com/kralicky/protocompile v0.0.0-20240119214132-b305d384cad9 h1:wv7HTtoAfKG5IRMyMfG05b2QDCaOzJQCd8A8WhdF6hI=
github.com/kralicky/protocompile v0.0.0-20240119214132-b305d384cad9/go.mod h1:QKlDXp/yojhlpqgJfUHWhqzvD9gCD/baEPFvq89cpgE=
github.com/kralicky/protocompile v0.0.0-20240120215100-ab7a9f0a39c3 h1:f5yigNZE17Mn47G9uHtxdDl7iPXbT78hheKi1zrvw+M=
github.com/kralicky/protocompile v0.0.0-20240120215100-ab7a9f0a39c3/go.mod h1:QKlDXp/yojhlpqgJfUHWhqzvD9gCD/baEPFvq89cpgE=
github.com/kralicky/tools-lite v0.0.0-20240104191314-c259ddd5a342 h1:lZLWHXKHmOhTrs3oSZoCRtb8Y9a0mqUwCsaKut+Y1eU=
github.com/kralicky/tools-lite v0.0.0-20240104191314-c259ddd5a342/go.mod h1:NKsdxFI6awifvNvxDwtCU1YCaKRoSSPpbHXkKOMuq24=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
Expand Down
6 changes: 4 additions & 2 deletions pkg/format/protoprint/print.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ func (p *Printer) PrintProtosToFileSystem(fds []protoreflect.FileDescriptor, roo
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return nil, err
}
return os.OpenFile(fullPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
return os.OpenFile(fullPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o666)
})
}

Expand Down Expand Up @@ -970,7 +970,6 @@ func (p *Printer) printMessageBody(md protoreflect.MessageDescriptor, reg *proto
ood := d.ContainingOneof()
if ood == nil || ood.IsSynthetic() {
p.printField(d, reg, w, sourceInfo, childPath, string(scope), indent)

} else {
// print the one-of, including all of its fields
p.printOneOf(ood, elements, i, reg, w, sourceInfo, path, indent, int32(ood.Index()))
Expand Down Expand Up @@ -1589,6 +1588,9 @@ func (p *Printer) printOptionsLong(opts []option, reg *protoregistry.Types, w *w
return sourceInfo.Get(append(path, i))
},
func(w *writer, indent int, opt option, _ bool) {
if opt.val == nil {
return
}
p.indent(w, indent)
_, _ = fmt.Fprint(w, "option ")
p.printOption(reg, opt.name, opt.val, w, indent)
Expand Down
79 changes: 50 additions & 29 deletions pkg/lsp/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,11 @@ func (c *Cache) GetCompletions(params *protocol.CompletionParams) (result *proto
completions := []protocol.CompletionItem{}
desc, _, _ := deepPathSearch(path, searchTarget, maybeCurrentLinkRes)

scope, existingOpts := findCompletionScopeAndExistingOptions(path, maybeCurrentLinkRes)
scope := findCompletionScope(path, maybeCurrentLinkRes)
if scope == nil {
return nil, nil
}
existingOpts := findExistingOptions(scope)

switch node := path[len(path)-1].(type) {
case *ast.MessageNode:
Expand Down Expand Up @@ -180,17 +184,39 @@ func (c *Cache) GetCompletions(params *protocol.CompletionParams) (result *proto
}
}
}
case *ast.RPCTypeNode:
// complete message types
fileNode := searchTarget.AST()
var partialName, partialNameSuffix string
withinName := node.MessageType != nil && tokenAtOffset >= node.MessageType.Start() && tokenAtOffset <= node.MessageType.End()
if !withinName && tokenAtOffset == node.CloseParen.Token() {
// check if the cursor is before or after the paren
start := fileNode.NodeInfo(node.CloseParen).Start()
if start.Offset == posOffset {
withinName = true
}
}
if withinName {
var err error
partialName, partialNameSuffix, err = findPartialNames(searchTarget.AST(), node.MessageType, mapper, posOffset)
if err != nil {
return nil, err
}
}
completions = append(completions, completeTypeNames(c, partialName, partialNameSuffix, maybeCurrentLinkRes, desc.FullName(), params.Position)...)

case *ast.CompactOptionsNode:
completions = append(completions,
c.completeOptionOrExtensionName(scope, path, searchTarget.AST(), nil, 0, maybeCurrentLinkRes, existingOpts, mapper, posOffset, params.Position)...)
case *ast.OptionNode:
completions = append(completions,
c.completeOptionOrExtensionName(scope, path, searchTarget.AST(), nil, 0, maybeCurrentLinkRes, existingOpts, mapper, posOffset, params.Position)...)
c.completeOptionOrExtensionName(scope.Options().ProtoReflect().Descriptor(), path, searchTarget.AST(), nil, 0, maybeCurrentLinkRes, existingOpts, mapper, posOffset, params.Position)...)
case *ast.FieldReferenceNode:
nodeIdx := -1
scope := scope
switch prev := path[len(path)-2].(type) {
case *ast.OptionNameNode:
scope = scope.Options().ProtoReflect().Descriptor()
nodeIdx = slices.Index(prev.Parts, node)
case *ast.MessageFieldNode:
if desc == nil {
Expand Down Expand Up @@ -409,7 +435,11 @@ func (c *Cache) completeOptionOrExtensionName(
var err error
partialName, partialNameSuffix, err = findPartialNames(fileNode, node.Name, mapper, posOffset)
if err != nil {
return nil
// TODO: possible grammar issue here?
// this can happen in situations like 'option <cursor>\n'
if err.Error() != "column is beyond end of line" {
return nil
}
}
}
items, err := c.deepCompleteOptionNames(scope, partialName, partialNameSuffix, linkRes, existingOpts, node, pos)
Expand Down Expand Up @@ -616,40 +646,29 @@ var (
snippetMode = protocol.SnippetTextFormat
)

func findCompletionScopeAndExistingOptions(nodePath []ast.Node, linkRes linker.Result) (protoreflect.Descriptor, map[string]struct{}) {
func findCompletionScope(nodePath []ast.Node, linkRes linker.Result) protoreflect.Descriptor {
var scope protoreflect.Descriptor
existing := map[string]struct{}{}
LOOP:
for i := len(nodePath) - 1; i >= 0; i-- {
switch node := nodePath[i].(type) {
case ast.MessageDeclNode:
scope = (*descriptorpb.MessageOptions)(nil).ProtoReflect().Descriptor()
if desc := linkRes.MessageDescriptor(node); desc != nil && desc.Options != nil {
existing = existingOptions(desc.GetOptions().ProtoReflect())
}
break LOOP
case *ast.FieldNode:
scope = (*descriptorpb.FieldOptions)(nil).ProtoReflect().Descriptor()
if desc := linkRes.FieldDescriptor(node); desc != nil && desc.Options != nil {
existing = existingOptions(desc.GetOptions().ProtoReflect())
switch nodePath[i].(type) {
case *ast.MessageNode, *ast.FieldNode, *ast.RPCNode, *ast.EnumNode, *ast.ServiceNode, *ast.MessageFieldNode:
desc, _, err := deepPathSearch(nodePath[:i+1], linkRes, linkRes)
if err != nil || desc == nil {
return nil
}
scope = desc
break LOOP
case *ast.FileNode:
scope = (*descriptorpb.FileOptions)(nil).ProtoReflect().Descriptor()
if linkRes.Options() != nil {
existing = existingOptions(linkRes.Options().ProtoReflect())
}
case ast.FileDeclNode:
scope = linkRes
break LOOP
case *ast.MessageFieldNode:
linkRes.Descriptor(node)
}
}
return scope, existing
return scope
}

func existingOptions(opts protoreflect.Message) map[string]struct{} {
func findExistingOptions(scope protoreflect.Descriptor) map[string]struct{} {
existing := map[string]struct{}{}
opts.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
scope.Options().ProtoReflect().Range(func(fd protoreflect.FieldDescriptor, _ protoreflect.Value) bool {
existing[string(fd.FullName())] = struct{}{}
return true
})
Expand Down Expand Up @@ -734,6 +753,9 @@ func (c *Cache) deepCompleteOptionNames(
if shouldCompleteExtensions {
// find any messages extending prevMsg
for _, x := range c.FindExtensionsByMessage(prevMsg.FullName()) {
if strings.HasPrefix(string(x.FullName()), "gogoproto.") && !strings.HasPrefix(partialName, "gogo") {
continue
}
item := newExtensionFieldCompletionItem(x, linkRes, partialName, partialNameSuffix, pos)
open, close := "(", ")"
if existingFieldRef != nil {
Expand Down Expand Up @@ -829,14 +851,13 @@ func completeTypeNames(cache *Cache, partialName, partialNameSuffix string, link
continue
}
maybeResolveImport(&item, candidate, linkRes)
insertText := strings.TrimPrefix(item.Label, partialName)
replaceRange := protocol.Range{
Start: pos,
Start: adjustColumn(pos, -len(partialName)),
End: adjustColumn(pos, len(partialNameSuffix)),
}
item.TextEdit = &protocol.Or_CompletionItem_textEdit{
Value: protocol.InsertReplaceEdit{
NewText: insertText,
NewText: item.Label,
Insert: replaceRange,
Replace: replaceRange,
},
Expand Down
7 changes: 7 additions & 0 deletions pkg/lsp/rename.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,13 @@ func (c *Cache) Rename(params *protocol.RenameParams) (*protocol.WorkspaceEdit,
switch node := node.(type) {
case *ast.IdentNode:
editRange = ref.NodeInfo
case *ast.CompoundIdentNode:
info := ref.NodeInfo.Internal().ParentFile().NodeInfo(node.Components[len(node.Components)-1])
if info.IsValid() {
editRange = info
} else {
continue
}
case *ast.FieldReferenceNode:
// ensure only the name is replaced
switch name := node.Name.(type) {
Expand Down
22 changes: 21 additions & 1 deletion pkg/lsp/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,21 @@ var ErrNoDescriptorFound = fmt.Errorf("failed to find descriptor")
// for the original ast node.
func deepPathSearch(path []ast.Node, parseRes parser.Result, linkRes linker.Result) (protoreflect.Descriptor, protocol.Range, error) {
root := linkRes.AST()

if len(path) == 0 {
panic("bug: empty path")
}
if _, ok := path[0].(*ast.FileNode); !ok {
panic("bug: first path element is not an *ast.FileNode")
}

if len(path) == 1 {
return linkRes, toRange(root.NodeInfo(root)), nil
}

stack := stack{}

for i := len(path) - 1; i >= 0; i-- {
for i := len(path) - 1; i > 0; i-- {
currentNode := path[i]
switch currentNode.(type) {
// short-circuit for some nodes that we know don't map to descriptors -
Expand Down Expand Up @@ -156,6 +168,8 @@ func deepPathSearch(path []ast.Node, parseRes parser.Result, linkRes linker.Resu
return stack[0].desc, toRange(root.NodeInfo(stack[0].node)), nil
}

stack.push(path[0], linkRes)

for i := len(stack) - 1; i >= 0; i-- {
want := stack[i]
if want.isResolved() {
Expand Down Expand Up @@ -673,6 +687,12 @@ func findNarrowestEnclosingScope(parseRes parser.Result, tokenAtOffset ast.Token
}
return nil
},
DoVisitRPCTypeNode: func(node *ast.RPCTypeNode) error {
if intersectsLocationExclusive(node, node.CloseParen) {
paths = append(paths, slices.Clone(tracker.Path()))
}
return nil
},
DoVisitPackageNode: func(node *ast.PackageNode) error {
if intersectsLocationExclusive(node, node.Semicolon) {
paths = append(paths, slices.Clone(tracker.Path()))
Expand Down

0 comments on commit eed5ed6

Please sign in to comment.