diff --git a/doc-values/value-key-value-assignment.go b/doc-values/value-key-value-assignment.go index ec37ca7..cda5c3c 100644 --- a/doc-values/value-key-value-assignment.go +++ b/doc-values/value-key-value-assignment.go @@ -104,7 +104,7 @@ func (v KeyValueAssignmentValue) CheckIsValid(value string) []*InvalidValue { } func (v KeyValueAssignmentValue) FetchCompletions(line string, cursor uint32) []protocol.CompletionItem { - if cursor == 0 { + if cursor == 0 || line == "" { return v.Key.FetchCompletions(line, cursor) } diff --git a/handlers/sshd_config/analyzer/analyzer.go b/handlers/sshd_config/analyzer/analyzer.go index 4635a05..77cf4c2 100644 --- a/handlers/sshd_config/analyzer/analyzer.go +++ b/handlers/sshd_config/analyzer/analyzer.go @@ -1,12 +1,26 @@ package analyzer import ( + "config-lsp/common" "config-lsp/handlers/sshd_config" + "config-lsp/utils" + protocol "github.com/tliron/glsp/protocol_3_16" ) func Analyze( d *sshdconfig.SSHDocument, ) []protocol.Diagnostic { + errors := analyzeOptionsAreValid(d) + + if len(errors) > 0 { + return utils.Map( + errors, + func(err common.LSPError) protocol.Diagnostic { + return err.ToDiagnostic() + }, + ) + } + return nil } diff --git a/handlers/sshd_config/analyzer/options.go b/handlers/sshd_config/analyzer/options.go new file mode 100644 index 0000000..58132b3 --- /dev/null +++ b/handlers/sshd_config/analyzer/options.go @@ -0,0 +1,60 @@ +package analyzer + +import ( + "config-lsp/common" + docvalues "config-lsp/doc-values" + sshdconfig "config-lsp/handlers/sshd_config" + "config-lsp/handlers/sshd_config/ast" + "config-lsp/handlers/sshd_config/fields" + "config-lsp/utils" + "errors" + "fmt" +) + +func analyzeOptionsAreValid( + d *sshdconfig.SSHDocument, +) []common.LSPError { + errs := make([]common.LSPError, 0) + it := d.Config.Options.Iterator() + + for it.Next() { + line := it.Key().(uint32) + entry := it.Value().(ast.SSHEntry) + + option := entry.GetOption() + + if option.Key != nil { + docOption, found := fields.Options[option.Key.Value] + + if !found { + errs = append(errs, common.LSPError{ + Range: option.Key.LocationRange, + Err: errors.New(fmt.Sprintf("Unknown option: %s", option.Key.Value)), + }) + continue + } + + if option.OptionValue == nil { + continue + } + + invalidValues := docOption.CheckIsValid(option.OptionValue.Value) + + errs = append( + errs, + utils.Map( + invalidValues, + func(invalidValue *docvalues.InvalidValue) common.LSPError { + err := docvalues.LSPErrorFromInvalidValue(line, *invalidValue) + err.ShiftCharacter(option.OptionValue.Start.Character) + + return err + }, + )..., + ) + + } + } + + return errs +} diff --git a/handlers/sshd_config/ast/sshd_config.go b/handlers/sshd_config/ast/sshd_config.go index 25897e9..2f24577 100644 --- a/handlers/sshd_config/ast/sshd_config.go +++ b/handlers/sshd_config/ast/sshd_config.go @@ -25,6 +25,7 @@ const ( type SSHEntry interface { GetType() SSHEntryType + GetOption() SSHOption } type SSHSeparator struct { @@ -44,6 +45,10 @@ func (o SSHOption) GetType() SSHEntryType { return SSHEntryTypeOption } +func (o SSHOption) GetOption() SSHOption { + return o +} + type SSHMatchBlock struct { common.LocationRange MatchEntry *SSHOption @@ -56,6 +61,10 @@ func (m SSHMatchBlock) GetType() SSHEntryType { return SSHEntryTypeMatchBlock } +func (m SSHMatchBlock) GetOption() SSHOption { + return *m.MatchEntry +} + type SSHConfig struct { // [uint32]SSHOption -> line number -> *SSHEntry Options *treemap.Map diff --git a/handlers/sshd_config/handlers/completions.go b/handlers/sshd_config/handlers/completions.go index a23d656..8d7814a 100644 --- a/handlers/sshd_config/handlers/completions.go +++ b/handlers/sshd_config/handlers/completions.go @@ -13,23 +13,51 @@ import ( func GetRootCompletions( d *sshdconfig.SSHDocument, parentMatchBlock *ast.SSHMatchBlock, + suggestValue bool, ) ([]protocol.CompletionItem, error) { kind := protocol.CompletionItemKindField - format := protocol.InsertTextFormatSnippet return utils.MapMapToSlice( fields.Options, func(name string, rawValue docvalues.Value) protocol.CompletionItem { doc := rawValue.(docvalues.DocumentationValue) - insertText := name + " " + "${1:value}" - return protocol.CompletionItem{ - Label: name, - Kind: &kind, - Documentation: doc.Documentation, - InsertText: &insertText, - InsertTextFormat: &format, + 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 } + +func GetOptionCompletions( + d *sshdconfig.SSHDocument, + entry *ast.SSHOption, + cursor uint32, +) ([]protocol.CompletionItem, error) { + option, found := fields.Options[entry.Key.Value] + + if !found { + return nil, nil + } + + if entry.OptionValue == nil { + return option.FetchCompletions("", 0), nil + } + + relativeCursor := cursor - entry.OptionValue.Start.Character + line := entry.OptionValue.Value + + return option.FetchCompletions(line, relativeCursor), nil +} diff --git a/handlers/sshd_config/lsp/text-document-completion.go b/handlers/sshd_config/lsp/text-document-completion.go index 7f9c3a2..f207837 100644 --- a/handlers/sshd_config/lsp/text-document-completion.go +++ b/handlers/sshd_config/lsp/text-document-completion.go @@ -1,6 +1,7 @@ package lsp import ( + "config-lsp/common" sshdconfig "config-lsp/handlers/sshd_config" "config-lsp/handlers/sshd_config/handlers" "regexp" @@ -14,7 +15,6 @@ var containsSeparatorPattern = regexp.MustCompile(`\s+$`) func TextDocumentCompletion(context *glsp.Context, params *protocol.CompletionParams) (any, error) { line := params.Position.Line cursor := params.Position.Character - _ = cursor d := sshdconfig.DocumentParserMap[params.TextDocument.URI] @@ -24,11 +24,23 @@ func TextDocumentCompletion(context *glsp.Context, params *protocol.CompletionPa entry, matchBlock := d.Config.FindOption(line) - if entry == nil || entry.Separator == nil { + if entry == nil || + entry.Separator == nil || + entry.Key == nil || + (common.CursorToCharacterIndex(cursor)) <= entry.Key.End.Character { // Empty line return handlers.GetRootCompletions( d, matchBlock, + entry == nil || containsSeparatorPattern.Match([]byte(entry.Value)), + ) + } + + if entry.Separator != nil && cursor > entry.Separator.End.Character { + return handlers.GetOptionCompletions( + d, + entry, + cursor, ) }