feat(ssh_config): Overall improvements

This commit is contained in:
Myzel394 2024-09-22 22:14:10 +02:00
parent 6608375b9e
commit 92b2f12e3f
No known key found for this signature in database
GPG Key ID: DEC4AAB876F73185
11 changed files with 313 additions and 23 deletions

View File

@ -152,6 +152,7 @@ func (s *sshParserListener) ExitEntry(ctx *parser.EntryContext) {
s.sshContext.currentKeyIsBlockOf = nil s.sshContext.currentKeyIsBlockOf = nil
s.sshContext.currentBlock = matchBlock s.sshContext.currentBlock = matchBlock
case SSHBlockTypeHost: case SSHBlockTypeHost:
var host *hostparser.Host var host *hostparser.Host

View File

@ -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)
}
}

View File

@ -18,6 +18,8 @@ type SSHBlock interface {
AddOption(option *SSHOption) AddOption(option *SSHOption)
SetEnd(common.Location) SetEnd(common.Location)
GetOptions() *treemap.Map GetOptions() *treemap.Map
GetEntryOption() *SSHOption
GetLocation() common.LocationRange
} }
func (b *SSHMatchBlock) GetBlockType() SSHBlockType { func (b *SSHMatchBlock) GetBlockType() SSHBlockType {
@ -36,6 +38,14 @@ func (b *SSHMatchBlock) GetOptions() *treemap.Map {
return b.Options 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 { func (b *SSHHostBlock) GetBlockType() SSHBlockType {
return SSHBlockTypeHost return SSHBlockTypeHost
} }
@ -52,6 +62,14 @@ func (b *SSHHostBlock) GetOptions() *treemap.Map {
return b.Options return b.Options
} }
func (b *SSHHostBlock) GetEntryOption() *SSHOption {
return b.HostOption
}
func (b *SSHHostBlock) GetLocation() common.LocationRange {
return b.LocationRange
}
type SSHType uint8 type SSHType uint8
const ( const (
@ -88,3 +106,94 @@ func (b *SSHHostBlock) GetType() SSHType {
func (b *SSHHostBlock) GetOption() *SSHOption { func (b *SSHHostBlock) GetOption() *SSHOption {
return b.HostOption 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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -23,12 +23,14 @@ type SSHIndexIncludeValue struct {
type SSHIndexIncludeLine struct { type SSHIndexIncludeLine struct {
Values []*SSHIndexIncludeValue Values []*SSHIndexIncludeValue
Option *ast.SSHOption Option *ast.SSHOption
Block *ast.SSHBlock Block ast.SSHBlock
} }
type SSHIndexes struct { type SSHIndexes struct {
AllOptionsPerName map[string](map[*ast.SSHBlock]([]*ast.SSHOption)) AllOptionsPerName map[string](map[ast.SSHBlock]([]*ast.SSHOption))
Includes []*SSHIndexIncludeLine Includes []*SSHIndexIncludeLine
BlockRanges map[uint32]ast.SSHBlock
} }

View File

@ -13,7 +13,7 @@ var whitespacePattern = regexp.MustCompile(`\S+`)
func NewSSHIndexes() *SSHIndexes { func NewSSHIndexes() *SSHIndexes {
return &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), Includes: make([]*SSHIndexIncludeLine, 0),
} }
} }
@ -34,13 +34,13 @@ func CreateIndexes(config ast.SSHConfig) (*SSHIndexes, []common.LSPError) {
case ast.SSHTypeHost: case ast.SSHTypeHost:
block := entry.(ast.SSHBlock) block := entry.(ast.SSHBlock)
errs = append(errs, addOption(indexes, entry.GetOption(), &block)...) errs = append(errs, addOption(indexes, entry.GetOption(), block)...)
it := block.GetOptions().Iterator() it := block.GetOptions().Iterator()
for it.Next() { for it.Next() {
option := it.Value().(*ast.SSHOption) 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( func addOption(
i *SSHIndexes, i *SSHIndexes,
option *ast.SSHOption, option *ast.SSHOption,
block *ast.SSHBlock, block ast.SSHBlock,
) []common.LSPError { ) []common.LSPError {
var errs []common.LSPError var errs []common.LSPError
@ -118,7 +118,7 @@ func addOption(
} }
} }
} else { } else {
i.AllOptionsPerName[option.Key.Key] = map[*ast.SSHBlock]([]*ast.SSHOption){ i.AllOptionsPerName[option.Key.Key] = map[ast.SSHBlock]([]*ast.SSHOption){
block: { block: {
option, option,
}, },

View File

@ -1,10 +1,50 @@
package lsp package lsp
import ( import (
"config-lsp/common"
sshconfig "config-lsp/handlers/ssh_config"
"config-lsp/handlers/ssh_config/handlers"
"regexp"
"github.com/tliron/glsp" "github.com/tliron/glsp"
protocol "github.com/tliron/glsp/protocol_3_16" protocol "github.com/tliron/glsp/protocol_3_16"
) )
var isEmptyPattern = regexp.MustCompile(`^\s*$`)
func TextDocumentCompletion(context *glsp.Context, params *protocol.CompletionParams) (any, error) { 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 return nil, nil
} }

View File

@ -1,8 +1,8 @@
package sshconfig package sshconfig
import ( import (
"config-lsp/handlers/ssh_config/ast"
"config-lsp/handlers/ssh_config/indexes" "config-lsp/handlers/ssh_config/indexes"
"config-lsp/handlers/ssh_config/ast"
protocol "github.com/tliron/glsp/protocol_3_16" protocol "github.com/tliron/glsp/protocol_3_16"
) )
@ -13,3 +13,4 @@ type SSHDocument struct {
} }
var DocumentParserMap = map[protocol.DocumentUri]*SSHDocument{} var DocumentParserMap = map[protocol.DocumentUri]*SSHDocument{}

View File

@ -62,7 +62,6 @@ func (c SSHDConfig) FindOption(line uint32) (*SSHDOption, *SSHDMatchBlock) {
} }
return nil, nil return nil, nil
} }
func (c SSHDConfig) GetAllOptions() []*SSHDOption { func (c SSHDConfig) GetAllOptions() []*SSHDOption {

View File

@ -20,23 +20,17 @@ func GetRootCompletions(
availableOptions := make(map[string]docvalues.DocumentationValue, 0) availableOptions := make(map[string]docvalues.DocumentationValue, 0)
if parentMatchBlock == nil { for key, option := range fields.Options {
for key, option := range fields.Options { var exists = false
if d.Indexes != nil && utils.KeyExists(d.Indexes.AllOptionsPerName, key) && !utils.KeyExists(fields.AllowedDuplicateOptions, key) {
continue
}
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
} }
} }