mirror of
https://github.com/Myzel394/config-lsp.git
synced 2025-06-18 23:15:26 +02:00
feat(ssh_config): Overall improvements
This commit is contained in:
parent
6608375b9e
commit
92b2f12e3f
@ -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
|
||||||
|
|
||||||
|
54
handlers/ssh_config/ast/ssh_cofig_fields_test.go
Normal file
54
handlers/ssh_config/ast/ssh_cofig_fields_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
31
handlers/ssh_config/document_fields.go
Normal file
31
handlers/ssh_config/document_fields.go
Normal 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
|
||||||
|
}
|
||||||
|
|
59
handlers/ssh_config/handlers/completions.go
Normal file
59
handlers/ssh_config/handlers/completions.go
Normal 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
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
},
|
},
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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{}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user