diff --git a/handlers/ssh_config/ast/listener.go b/handlers/ssh_config/ast/listener.go index f9dff36..0043422 100644 --- a/handlers/ssh_config/ast/listener.go +++ b/handlers/ssh_config/ast/listener.go @@ -152,6 +152,7 @@ func (s *sshParserListener) ExitEntry(ctx *parser.EntryContext) { s.sshContext.currentKeyIsBlockOf = nil s.sshContext.currentBlock = matchBlock + case SSHBlockTypeHost: var host *hostparser.Host diff --git a/handlers/ssh_config/ast/ssh_cofig_fields_test.go b/handlers/ssh_config/ast/ssh_cofig_fields_test.go new file mode 100644 index 0000000..eeba4a9 --- /dev/null +++ b/handlers/ssh_config/ast/ssh_cofig_fields_test.go @@ -0,0 +1,54 @@ +package ast + +import ( + "config-lsp/utils" + "testing" +) + +func TestComplexExampleRetrievesCorrectly( + t *testing.T, +) { + input := utils.Dedent(` +Port 22 + +Host laptop + HostName laptop.lan + +Match originalhost laptop exec "[[ $(/usr/bin/dig +short laptop.lan) == '' ]]" + HostName laptop.sdn + `) + + p := NewSSHConfig() + errors := p.Parse(input) + + if len(errors) != 0 { + t.Fatalf("Expected no errors, got %v", errors) + } + + firstOption, firstBlock := p.FindOption(0) + if !(firstOption.Value.Raw == "Port 22") { + t.Errorf("Expected Port 22, got %v", firstOption.Value.Raw) + } + + if !(firstBlock == nil) { + t.Errorf("Expected no block, got %v", firstBlock) + } + + secondOption, secondBlock := p.FindOption(3) + if !(secondOption.Value.Raw == " HostName laptop.lan") { + t.Errorf("Expected HostName laptop.lan, got %v", secondOption.Value.Raw) + } + + if !(secondBlock.GetLocation().Start.Line == 2) { + t.Errorf("Expected line 2, got %v", secondBlock.GetLocation().Start.Line) + } + + thirdOption, thirdBlock := p.FindOption(6) + if !(thirdOption.Value.Raw == " HostName laptop.sdn") { + t.Errorf("Expected HostName laptop.sdn, got %v", thirdOption.Value.Raw) + } + + if !(thirdBlock.GetLocation().Start.Line == 5) { + t.Errorf("Expected line 3, got %v", thirdBlock.GetLocation().Start.Line) + } +} diff --git a/handlers/ssh_config/ast/ssh_config_fields.go b/handlers/ssh_config/ast/ssh_config_fields.go index ec5ccd0..7902b21 100644 --- a/handlers/ssh_config/ast/ssh_config_fields.go +++ b/handlers/ssh_config/ast/ssh_config_fields.go @@ -18,6 +18,8 @@ type SSHBlock interface { AddOption(option *SSHOption) SetEnd(common.Location) GetOptions() *treemap.Map + GetEntryOption() *SSHOption + GetLocation() common.LocationRange } func (b *SSHMatchBlock) GetBlockType() SSHBlockType { @@ -36,6 +38,14 @@ func (b *SSHMatchBlock) GetOptions() *treemap.Map { return b.Options } +func (b *SSHMatchBlock) GetEntryOption() *SSHOption { + return b.MatchOption +} + +func (b *SSHMatchBlock) GetLocation() common.LocationRange { + return b.LocationRange +} + func (b *SSHHostBlock) GetBlockType() SSHBlockType { return SSHBlockTypeHost } @@ -52,6 +62,14 @@ func (b *SSHHostBlock) GetOptions() *treemap.Map { return b.Options } +func (b *SSHHostBlock) GetEntryOption() *SSHOption { + return b.HostOption +} + +func (b *SSHHostBlock) GetLocation() common.LocationRange { + return b.LocationRange +} + type SSHType uint8 const ( @@ -88,3 +106,94 @@ func (b *SSHHostBlock) GetType() SSHType { func (b *SSHHostBlock) GetOption() *SSHOption { return b.HostOption } + +func (c SSHConfig) FindBlock(line uint32) SSHBlock { + it := c.Options.Iterator() + for it.Next() { + entry := it.Value().(SSHEntry) + + if entry.GetType() == SSHTypeOption { + continue + } + + block := entry.(SSHBlock) + + if block.GetLocation().Start.Line <= line && block.GetLocation().End.Line >= line { + return block + } + } + + return nil +} + +func (c SSHConfig) FindOption(line uint32) (*SSHOption, SSHBlock) { + block := c.FindBlock(line) + + var option *SSHOption + + if block == nil { + if rawOption, found := c.Options.Get(line); found { + option = rawOption.(*SSHOption) + } + } else { + if rawOption, found := block.GetOptions().Get(line); found { + option = rawOption.(*SSHOption) + } + } + + return option, block +} + +type AllOptionInfo struct { + Block SSHBlock + Option *SSHOption +} + +func (c SSHConfig) GetAllOptions() []AllOptionInfo { + options := make([]AllOptionInfo, 0, 50) + + for _, rawEntry := range c.Options.Values() { + switch rawEntry.(type) { + case *SSHOption: + option := rawEntry.(*SSHOption) + options = append(options, AllOptionInfo{ + Block: nil, + Option: option, + }) + case *SSHMatchBlock: + block := rawEntry.(SSHBlock) + + options = append(options, AllOptionInfo{ + Block: block, + Option: block.GetEntryOption(), + }) + + for _, rawOption := range block.GetOptions().Values() { + option := rawOption.(*SSHOption) + options = append(options, AllOptionInfo{ + Block: nil, + Option: option, + }) + } + case *SSHHostBlock: + block := rawEntry.(SSHBlock) + + options = append(options, AllOptionInfo{ + Block: block, + Option: block.GetEntryOption(), + }) + + for _, rawOption := range block.GetOptions().Values() { + option := rawOption.(*SSHOption) + options = append(options, AllOptionInfo{ + Block: nil, + Option: option, + }) + } + } + + } + + return options +} + diff --git a/handlers/ssh_config/document_fields.go b/handlers/ssh_config/document_fields.go new file mode 100644 index 0000000..4c6795f --- /dev/null +++ b/handlers/ssh_config/document_fields.go @@ -0,0 +1,31 @@ +package sshconfig + +import "config-lsp/handlers/ssh_config/ast" + +func (d SSHDocument) FindOptionByNameAndBlock( + name string, + block ast.SSHBlock, +) *ast.AllOptionInfo { + for _, info := range d.FindOptionsByName(name) { + if info.Block == block { + return &info + } + } + + return nil +} + +func (d SSHDocument) FindOptionsByName( + name string, +) []ast.AllOptionInfo { + options := make([]ast.AllOptionInfo, 0, 5) + + for _, info := range d.Config.GetAllOptions() { + if info.Option.Key.Key == name { + options = append(options, info) + } + } + + return options +} + diff --git a/handlers/ssh_config/handlers/completions.go b/handlers/ssh_config/handlers/completions.go new file mode 100644 index 0000000..e6236c6 --- /dev/null +++ b/handlers/ssh_config/handlers/completions.go @@ -0,0 +1,59 @@ +package handlers + +import ( + docvalues "config-lsp/doc-values" + sshconfig "config-lsp/handlers/ssh_config" + "config-lsp/handlers/ssh_config/ast" + "config-lsp/handlers/ssh_config/fields" + "config-lsp/utils" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func GetRootCompletions( + d *sshconfig.SSHDocument, + parentBlock ast.SSHBlock, + suggestValue bool, +) ([]protocol.CompletionItem, error) { + kind := protocol.CompletionItemKindField + + availableOptions := make(map[string]docvalues.DocumentationValue, 0) + + for key, option := range fields.Options { + alreadyExists := d.FindOptionByNameAndBlock(key, parentBlock) != nil + + if !alreadyExists || utils.KeyExists(fields.AllowedDuplicateOptions, key) { + availableOptions[key] = option + } + } + + // Remove all fields that are already present and are not allowed to be duplicated + for _, info := range d.Config.GetAllOptions() { + if _, found := fields.AllowedDuplicateOptions[info.Option.Key.Key]; found { + continue + } + + delete(availableOptions, info.Option.Key.Key) + } + + return utils.MapMapToSlice( + availableOptions, + func(name string, doc docvalues.DocumentationValue) protocol.CompletionItem { + completion := &protocol.CompletionItem{ + Label: name, + Kind: &kind, + Documentation: doc.Documentation, + } + + if suggestValue { + format := protocol.InsertTextFormatSnippet + insertText := name + " " + "${1:value}" + + completion.InsertTextFormat = &format + completion.InsertText = &insertText + } + + return *completion + }, + ), nil +} diff --git a/handlers/ssh_config/indexes/indexes.go b/handlers/ssh_config/indexes/indexes.go index 35cc82d..841cbc3 100644 --- a/handlers/ssh_config/indexes/indexes.go +++ b/handlers/ssh_config/indexes/indexes.go @@ -23,12 +23,14 @@ type SSHIndexIncludeValue struct { type SSHIndexIncludeLine struct { Values []*SSHIndexIncludeValue Option *ast.SSHOption - Block *ast.SSHBlock + Block ast.SSHBlock } type SSHIndexes struct { - AllOptionsPerName map[string](map[*ast.SSHBlock]([]*ast.SSHOption)) + AllOptionsPerName map[string](map[ast.SSHBlock]([]*ast.SSHOption)) Includes []*SSHIndexIncludeLine + + BlockRanges map[uint32]ast.SSHBlock } diff --git a/handlers/ssh_config/indexes/handlers.go b/handlers/ssh_config/indexes/indexes_handlers.go similarity index 90% rename from handlers/ssh_config/indexes/handlers.go rename to handlers/ssh_config/indexes/indexes_handlers.go index 2502870..4c78283 100644 --- a/handlers/ssh_config/indexes/handlers.go +++ b/handlers/ssh_config/indexes/indexes_handlers.go @@ -13,7 +13,7 @@ var whitespacePattern = regexp.MustCompile(`\S+`) func NewSSHIndexes() *SSHIndexes { return &SSHIndexes{ - AllOptionsPerName: make(map[string](map[*ast.SSHBlock]([]*ast.SSHOption)), 0), + AllOptionsPerName: make(map[string](map[ast.SSHBlock]([]*ast.SSHOption)), 0), Includes: make([]*SSHIndexIncludeLine, 0), } } @@ -34,13 +34,13 @@ func CreateIndexes(config ast.SSHConfig) (*SSHIndexes, []common.LSPError) { case ast.SSHTypeHost: block := entry.(ast.SSHBlock) - errs = append(errs, addOption(indexes, entry.GetOption(), &block)...) + errs = append(errs, addOption(indexes, entry.GetOption(), block)...) it := block.GetOptions().Iterator() for it.Next() { option := it.Value().(*ast.SSHOption) - errs = append(errs, addOption(indexes, option, &block)...) + errs = append(errs, addOption(indexes, option, block)...) } } } @@ -90,7 +90,7 @@ func CreateIndexes(config ast.SSHConfig) (*SSHIndexes, []common.LSPError) { func addOption( i *SSHIndexes, option *ast.SSHOption, - block *ast.SSHBlock, + block ast.SSHBlock, ) []common.LSPError { var errs []common.LSPError @@ -118,7 +118,7 @@ func addOption( } } } else { - i.AllOptionsPerName[option.Key.Key] = map[*ast.SSHBlock]([]*ast.SSHOption){ + i.AllOptionsPerName[option.Key.Key] = map[ast.SSHBlock]([]*ast.SSHOption){ block: { option, }, diff --git a/handlers/ssh_config/lsp/text-document-completion.go b/handlers/ssh_config/lsp/text-document-completion.go index 5c5b64a..5813dae 100644 --- a/handlers/ssh_config/lsp/text-document-completion.go +++ b/handlers/ssh_config/lsp/text-document-completion.go @@ -1,10 +1,50 @@ package lsp import ( + "config-lsp/common" + sshconfig "config-lsp/handlers/ssh_config" + "config-lsp/handlers/ssh_config/handlers" + "regexp" + "github.com/tliron/glsp" protocol "github.com/tliron/glsp/protocol_3_16" ) +var isEmptyPattern = regexp.MustCompile(`^\s*$`) + func TextDocumentCompletion(context *glsp.Context, params *protocol.CompletionParams) (any, error) { + line := params.Position.Line + cursor := common.LSPCharacterAsCursorPosition(params.Position.Character) + + d := sshconfig.DocumentParserMap[params.TextDocument.URI] + + if _, found := d.Config.CommentLines[line]; found { + return nil, nil + } + + option, block := d.Config.FindOption(line) + + if option == nil || + option.Separator == nil || + option.Key == nil || + option.Key.IsPositionBeforeEnd(cursor) { + + return handlers.GetRootCompletions( + d, + block, + // Empty line, or currently typing a new key + option == nil || isEmptyPattern.Match([]byte(option.Value.Raw[cursor:])), + ) + } + + // if option.Separator != nil && option.OptionValue.IsPositionAfterStart(cursor) { + // return handlers.GetOptionCompletions( + // d, + // entry, + // matchBlock, + // cursor, + // ) + // } + return nil, nil } diff --git a/handlers/ssh_config/shared.go b/handlers/ssh_config/shared.go index 5aa8e06..60b905d 100644 --- a/handlers/ssh_config/shared.go +++ b/handlers/ssh_config/shared.go @@ -1,8 +1,8 @@ package sshconfig import ( - "config-lsp/handlers/ssh_config/ast" "config-lsp/handlers/ssh_config/indexes" + "config-lsp/handlers/ssh_config/ast" protocol "github.com/tliron/glsp/protocol_3_16" ) @@ -13,3 +13,4 @@ type SSHDocument struct { } var DocumentParserMap = map[protocol.DocumentUri]*SSHDocument{} + diff --git a/handlers/sshd_config/ast/sshd_config_fields.go b/handlers/sshd_config/ast/sshd_config_fields.go index 8c226bd..11546d4 100644 --- a/handlers/sshd_config/ast/sshd_config_fields.go +++ b/handlers/sshd_config/ast/sshd_config_fields.go @@ -62,7 +62,6 @@ func (c SSHDConfig) FindOption(line uint32) (*SSHDOption, *SSHDMatchBlock) { } return nil, nil - } func (c SSHDConfig) GetAllOptions() []*SSHDOption { diff --git a/handlers/sshd_config/handlers/completions.go b/handlers/sshd_config/handlers/completions.go index 827b636..0082417 100644 --- a/handlers/sshd_config/handlers/completions.go +++ b/handlers/sshd_config/handlers/completions.go @@ -20,23 +20,17 @@ func GetRootCompletions( availableOptions := make(map[string]docvalues.DocumentationValue, 0) - if parentMatchBlock == nil { - for key, option := range fields.Options { - if d.Indexes != nil && utils.KeyExists(d.Indexes.AllOptionsPerName, key) && !utils.KeyExists(fields.AllowedDuplicateOptions, key) { - continue - } + for key, option := range fields.Options { + var exists = false - availableOptions[key] = option + if optionsMap, found := d.Indexes.AllOptionsPerName[key]; found { + if _, found := optionsMap[parentMatchBlock]; found { + exists = true + } } - } else { - for key := range fields.MatchAllowedOptions { - if option, found := fields.Options[key]; found { - if d.Indexes != nil && utils.KeyExists(d.Indexes.AllOptionsPerName, key) && !utils.KeyExists(fields.AllowedDuplicateOptions, key) { - continue - } - availableOptions[key] = option - } + if !exists || utils.KeyExists(fields.AllowedDuplicateOptions, key) { + availableOptions[key] = option } }