diff --git a/components/completion.go b/components/completion.go index 965ec8d..29298c8 100644 --- a/components/completion.go +++ b/components/completion.go @@ -3,14 +3,24 @@ package components import ( "context" "pls/proto/view" + "strings" + "time" "github.com/TobiasYin/go-lsp/lsp/defines" ) -var protoKeywordCompletionItems []defines.CompletionItem +var ( + protoKeywordCompletionItems []defines.CompletionItem + + kindKeyword = defines.CompletionItemKindKeyword + kindModule = defines.CompletionItemKindModule + kindClass = defines.CompletionItemKindClass + kindEnum = defines.CompletionItemKindEnum + + defaultCompletionTimeout = time.Millisecond * 500 +) func init() { - kindKeyword := defines.CompletionItemKindKeyword for _, keyword := range []string{"string", "bytes", "double", "float", "int32", "int64", "uint32", "uint64", "sint32", "sint64", "fixed32", "fixed64", "sfixed32", "sfixed64", "bool", "message", "enum", "service", "rpc", "optional", "repeated", "required", @@ -29,41 +39,50 @@ func Completion(ctx context.Context, req *defines.CompletionParams) (*[]defines. if !view.IsProtoFile(req.TextDocument.Uri) { return nil, nil } + ctx, cancel := context.WithTimeout(ctx, defaultCompletionTimeout) + defer cancel() + proto_file, err := view.ViewManager.GetFile(req.TextDocument.Uri) if err != nil || proto_file.Proto() == nil { return nil, nil } line_str := proto_file.ReadLine(int(req.Position.Line)) - word := getWord(line_str, int(req.Position.Character-1), false) - if req.Context.TriggerKind != defines.CompletionTriggerKindTriggerCharacter { - res, err := CompletionInThisFile(proto_file) - - kindModule := defines.CompletionItemKindModule - for _, im := range proto_file.Proto().Imports() { - import_uri, err := view.GetDocumentUriFromImportPath(req.TextDocument.Uri, im.ProtoImport.Filename) - if err != nil { - continue - } - - file, err := view.ViewManager.GetFile(import_uri) - if err != nil { - continue - } - - if len(file.Proto().Packages()) > 0 { - *res = append(*res, defines.CompletionItem{ - Label: file.Proto().Packages()[0].ProtoPackage.Name, - Kind: &kindModule, - InsertText: &file.Proto().Packages()[0].ProtoPackage.Name, - }) - } + wordWithDot := getWord(line_str, int(req.Position.Character-1), true) + + var res []defines.CompletionItem + // suggest imported packages that match what user has typed so far + // + // word = "google" + // suggest = [ google.protobuf , google.api ] + for _, pkg := range GetImportedPackages(ctx, proto_file) { + if strings.HasPrefix(*pkg.InsertText, wordWithDot) { + res = append(res, pkg) } + } - return res, err + if req.Context.TriggerKind != defines.CompletionTriggerKindTriggerCharacter { + res = append(res, protoKeywordCompletionItems...) + res = append(res, CompletionInThisFile(ctx, proto_file)...) + return &res, err } + + packageName := strings.TrimSuffix(wordWithDot, ".") + res = append(res, CompletionInPackage(ctx, proto_file, packageName)...) + + return &res, nil +} + +func GetImportedPackages(ctx context.Context, proto_file view.ProtoFile) (res []defines.CompletionItem) { + unique := make(map[string]struct{}) for _, im := range proto_file.Proto().Imports() { - import_uri, err := view.GetDocumentUriFromImportPath(req.TextDocument.Uri, im.ProtoImport.Filename) + select { + case <-ctx.Done(): + return + default: + } + + import_uri, err := view.GetDocumentUriFromImportPath(proto_file.URI(), im.ProtoImport.Filename) if err != nil { continue } @@ -73,31 +92,100 @@ func Completion(ctx context.Context, req *defines.CompletionParams) (*[]defines. continue } - if len(file.Proto().Packages()) > 0 && file.Proto().Packages()[0].ProtoPackage.Name == word { - return CompletionInThisFile(file) + if len(file.Proto().Packages()) == 0 { + continue + } + + packageName := file.Proto().Packages()[0].ProtoPackage.Name + + if _, exist := unique[packageName]; exist { + continue + } + + unique[packageName] = struct{}{} + res = append(res, defines.CompletionItem{ + Label: packageName, + Kind: &kindModule, + InsertText: &packageName, + }) + + } + + return res +} + +func CompletionInPackage(ctx context.Context, file view.ProtoFile, packageName string) (res []defines.CompletionItem) { + for _, im := range file.Proto().Imports() { + select { + case <-ctx.Done(): + return + default: + } + + import_uri, err := view.GetDocumentUriFromImportPath(file.URI(), im.ProtoImport.Filename) + if err != nil { + continue + } + + imported_file, err := view.ViewManager.GetFile(import_uri) + if err != nil { + continue + } + + if len(imported_file.Proto().Packages()) == 0 { + continue + } + + importedPackage := imported_file.Proto().Packages()[0].ProtoPackage.Name + if importedPackage == packageName { + res = append(res, CompletionInThisFile(ctx, imported_file)...) } + } - return nil, nil + return res } -func CompletionInThisFile(file view.ProtoFile) (result *[]defines.CompletionItem, err error) { - kindEnum := defines.CompletionItemKindEnum - res := protoKeywordCompletionItems +func CompletionInThisFile(ctx context.Context, file view.ProtoFile) (res []defines.CompletionItem) { + select { + case <-ctx.Done(): + return + default: + } + for _, enums := range file.Proto().Enums() { + + doc := formatHover(SymbolDefinition{ + Type: DefinitionTypeEnum, + Enum: enums, + }) + res = append(res, defines.CompletionItem{ Label: enums.Protobuf().Name, Kind: &kindEnum, InsertText: &enums.Protobuf().Name, + Documentation: defines.MarkupContent{ + Kind: defines.MarkupKindMarkdown, + Value: doc, + }, }) } - kindClass := defines.CompletionItemKindClass for _, message := range file.Proto().Messages() { + + doc := formatHover(SymbolDefinition{ + Type: DefinitionTypeMessage, + Message: message, + }) + res = append(res, defines.CompletionItem{ Label: message.Protobuf().Name, Kind: &kindClass, InsertText: &message.Protobuf().Name, + Documentation: defines.MarkupContent{ + Kind: defines.MarkupKindMarkdown, + Value: doc, + }, }) } - return &res, nil + return res }