From e569516aae2bdec84fd6b5cf45392cffa3a5a902 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Mon, 17 Feb 2025 22:27:14 +0100 Subject: [PATCH] fix(server): Improve sshd_config --- .../handlers/sshd_config/analyzer/analyzer.go | 1 - .../handlers/sshd_config/analyzer/options.go | 22 ++-- .../{values_test.go => options_test.go} | 2 +- .../handlers/sshd_config/analyzer/values.go | 39 ------- .../handlers/fetch-code-actions.go | 20 ++++ .../handlers/fetch-code-actions_typos.go | 110 ++++++++++++++++++ .../lsp/text-document-code-action.go | 16 +++ 7 files changed, 160 insertions(+), 50 deletions(-) rename server/handlers/sshd_config/analyzer/{values_test.go => options_test.go} (96%) delete mode 100644 server/handlers/sshd_config/analyzer/values.go create mode 100644 server/handlers/sshd_config/handlers/fetch-code-actions.go create mode 100644 server/handlers/sshd_config/handlers/fetch-code-actions_typos.go create mode 100644 server/handlers/sshd_config/lsp/text-document-code-action.go diff --git a/server/handlers/sshd_config/analyzer/analyzer.go b/server/handlers/sshd_config/analyzer/analyzer.go index 72cb52e..0007b09 100644 --- a/server/handlers/sshd_config/analyzer/analyzer.go +++ b/server/handlers/sshd_config/analyzer/analyzer.go @@ -54,7 +54,6 @@ func Analyze( } } - analyzeValuesAreValid(ctx) analyzeMatchBlocks(ctx) analyzeTokens(ctx) diff --git a/server/handlers/sshd_config/analyzer/options.go b/server/handlers/sshd_config/analyzer/options.go index 62fc3c7..b15b16d 100644 --- a/server/handlers/sshd_config/analyzer/options.go +++ b/server/handlers/sshd_config/analyzer/options.go @@ -4,6 +4,7 @@ import ( "config-lsp/common" docvalues "config-lsp/doc-values" "config-lsp/handlers/sshd_config/ast" + "config-lsp/handlers/sshd_config/diagnostics" "config-lsp/handlers/sshd_config/fields" "fmt" @@ -20,7 +21,7 @@ func analyzeStructureIsValid( switch entry.(type) { case *ast.SSHDOption: - checkOption(ctx, entry.(*ast.SSHDOption), false) + checkOption(ctx, entry.(*ast.SSHDOption), nil) case *ast.SSHDMatchBlock: matchBlock := entry.(*ast.SSHDMatchBlock) checkMatchBlock(ctx, matchBlock) @@ -31,7 +32,7 @@ func analyzeStructureIsValid( func checkOption( ctx *analyzerContext, option *ast.SSHDOption, - isInMatchBlock bool, + matchBlock *ast.SSHDMatchBlock, ) { if option.Key == nil { return @@ -44,16 +45,19 @@ func checkOption( docOption, found := fields.Options[key] if !found { - ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ - Range: option.Key.ToLSPRange(), - Message: fmt.Sprintf("Unknown option: %s", option.Key.Key), - Severity: &common.SeverityError, - }) + ctx.diagnostics = append(ctx.diagnostics, diagnostics.GenerateUnknownOption( + option.Key.ToLSPRange(), + option.Key.Value.Value, + )) + ctx.document.Indexes.UnknownOptions[option.Start.Line] = ast.SSHDOptionInfo{ + Option: option, + MatchBlock: matchBlock, + } return } - if _, found := fields.MatchAllowedOptions[key]; !found && isInMatchBlock { + if _, found := fields.MatchAllowedOptions[key]; !found && matchBlock != nil { ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ Range: option.Key.ToLSPRange(), Message: fmt.Sprintf("Option '%s' is not allowed inside Match blocks", option.Key.Key), @@ -99,6 +103,6 @@ func checkMatchBlock( for it.Next() { option := it.Value().(*ast.SSHDOption) - checkOption(ctx, option, true) + checkOption(ctx, option, matchBlock) } } diff --git a/server/handlers/sshd_config/analyzer/values_test.go b/server/handlers/sshd_config/analyzer/options_test.go similarity index 96% rename from server/handlers/sshd_config/analyzer/values_test.go rename to server/handlers/sshd_config/analyzer/options_test.go index b09df8c..7997b1f 100644 --- a/server/handlers/sshd_config/analyzer/values_test.go +++ b/server/handlers/sshd_config/analyzer/options_test.go @@ -18,7 +18,7 @@ ThisOptionDoesNotExist okay diagnostics: make([]protocol.Diagnostic, 0), } - analyzeValuesAreValid(ctx) + analyzeStructureIsValid(ctx) if !(len(ctx.diagnostics) == 1) { t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics)) diff --git a/server/handlers/sshd_config/analyzer/values.go b/server/handlers/sshd_config/analyzer/values.go deleted file mode 100644 index d29d8c7..0000000 --- a/server/handlers/sshd_config/analyzer/values.go +++ /dev/null @@ -1,39 +0,0 @@ -package analyzer - -import ( - "config-lsp/handlers/sshd_config/diagnostics" - "config-lsp/handlers/sshd_config/fields" -) - -func analyzeValuesAreValid( - ctx *analyzerContext, -) { - // Check if there are unknown options - for _, info := range ctx.document.Config.GetAllOptions() { - normalizedName := fields.CreateNormalizedName(info.Option.Key.Value.Value) - - var isUnknown bool = true - - // Check if the option is unknown - if info.MatchBlock == nil { - // All options are allowed - if _, found := fields.Options[normalizedName]; found { - isUnknown = false - } - } else { - // Only `MatchAllowedOptions` are allowed - if _, found := fields.MatchAllowedOptions[normalizedName]; found { - isUnknown = false - } - } - - if isUnknown { - ctx.diagnostics = append(ctx.diagnostics, diagnostics.GenerateUnknownOption( - info.Option.Key.ToLSPRange(), - info.Option.Key.Value.Value, - )) - - ctx.document.Indexes.UnknownOptions[info.Option.Start.Line] = info - } - } -} diff --git a/server/handlers/sshd_config/handlers/fetch-code-actions.go b/server/handlers/sshd_config/handlers/fetch-code-actions.go new file mode 100644 index 0000000..3713dff --- /dev/null +++ b/server/handlers/sshd_config/handlers/fetch-code-actions.go @@ -0,0 +1,20 @@ +package handlers + +import ( + sshdconfig "config-lsp/handlers/sshd_config" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func FetchCodeActions( + d *sshdconfig.SSHDDocument, + params *protocol.CodeActionParams, +) []protocol.CodeAction { + if d.Indexes == nil { + return nil + } + + actions := getKeywordTypoFixes(d, params) + + return actions +} diff --git a/server/handlers/sshd_config/handlers/fetch-code-actions_typos.go b/server/handlers/sshd_config/handlers/fetch-code-actions_typos.go new file mode 100644 index 0000000..e449405 --- /dev/null +++ b/server/handlers/sshd_config/handlers/fetch-code-actions_typos.go @@ -0,0 +1,110 @@ +package handlers + +import ( + "config-lsp/common" + sshdconfig "config-lsp/handlers/sshd_config" + "config-lsp/handlers/sshd_config/diagnostics" + "config-lsp/handlers/sshd_config/fields" + "fmt" + + "github.com/hbollon/go-edlib" + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func getKeywordTypoFixes( + d *sshdconfig.SSHDDocument, + params *protocol.CodeActionParams, +) []protocol.CodeAction { + if common.ServerOptions.NoTypoSuggestions { + return nil + } + + line := params.Range.Start.Line + + if typoOption, found := d.Indexes.UnknownOptions[line]; found { + name := typoOption.Option.Key.Value.Value + + suggestedOptions := findSimilarOptions(name, typoOption.MatchBlock != nil) + + actions := make([]protocol.CodeAction, 0, len(suggestedOptions)) + + kind := protocol.CodeActionKindQuickFix + for index, normalizedOptionName := range suggestedOptions { + isPreferred := index == 0 + optionName := fields.FieldsNameFormattedMap[normalizedOptionName] + + actions = append(actions, protocol.CodeAction{ + Title: fmt.Sprintf("Typo Fix: %s", optionName), + IsPreferred: &isPreferred, + Kind: &kind, + Diagnostics: []protocol.Diagnostic{ + diagnostics.GenerateUnknownOption( + typoOption.Option.Key.ToLSPRange(), + typoOption.Option.Key.Value.Value, + ), + }, + Edit: &protocol.WorkspaceEdit{ + Changes: map[protocol.DocumentUri][]protocol.TextEdit{ + params.TextDocument.URI: { + { + Range: typoOption.Option.Key.ToLSPRange(), + NewText: optionName, + }, + }, + }, + }, + }) + } + + return actions + } + + return nil +} + +// Find options that are similar to the given option name. +// This is used to find typos & suggest the correct option name. +// Once an option is found that has a Damerau-Levenshtein distance of 1, it is immediately returned. +// If not, then the next 2 options of similarity 2, or 3 options of similarity 3 are returned. +// If no options with similarity <= 3 are found, then an empty slice is returned. +func findSimilarOptions( + optionName string, + restrictToMatchOptions bool, +) []fields.NormalizedOptionName { + normalizedOptionName := string(fields.CreateNormalizedName(optionName)) + + optionsPerSimilarity := map[uint8][]fields.NormalizedOptionName{ + 2: make([]fields.NormalizedOptionName, 0, 2), + 3: make([]fields.NormalizedOptionName, 0, 3), + } + + for name := range fields.Options { + if restrictToMatchOptions { + if _, found := fields.MatchAllowedOptions[name]; !found { + continue + } + } + + normalizedName := string(name) + similarity := edlib.DamerauLevenshteinDistance(normalizedName, normalizedOptionName) + + switch similarity { + case 1: + return []fields.NormalizedOptionName{name} + case 2: + optionsPerSimilarity[2] = append(optionsPerSimilarity[2], name) + + if len(optionsPerSimilarity[2]) >= 2 { + return optionsPerSimilarity[2] + } + case 3: + optionsPerSimilarity[3] = append(optionsPerSimilarity[3], name) + + if len(optionsPerSimilarity[3]) >= 3 { + return optionsPerSimilarity[3] + } + } + } + + return append(optionsPerSimilarity[2], optionsPerSimilarity[3]...) +} diff --git a/server/handlers/sshd_config/lsp/text-document-code-action.go b/server/handlers/sshd_config/lsp/text-document-code-action.go new file mode 100644 index 0000000..405b010 --- /dev/null +++ b/server/handlers/sshd_config/lsp/text-document-code-action.go @@ -0,0 +1,16 @@ +package lsp + +import ( + sshdconfig "config-lsp/handlers/sshd_config" + "config-lsp/handlers/sshd_config/handlers" + + "github.com/tliron/glsp" + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func TextDocumentCodeAction(context *glsp.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) { + d := sshdconfig.DocumentParserMap[params.TextDocument.URI] + actions := handlers.FetchCodeActions(d, params) + + return actions, nil +}