From e4d7521a4ca82d915481acfeaedc4254516346a9 Mon Sep 17 00:00:00 2001 From: Myzel394 Date: Sat, 29 Mar 2025 16:12:28 +0100 Subject: [PATCH] feat(server): Add Wireguard code actions: Fix typo --- .../handlers/fetch-code-actions_typos.go | 45 +--------- .../handlers/wireguard/analyzer/analyzer.go | 6 ++ .../handlers/wireguard/analyzer/properties.go | 89 +++++++++++++++++++ .../handlers/wireguard/analyzer/structure.go | 66 -------------- .../wireguard/diagnostics/diagnostics.go | 19 ++++ .../handlers/fetch-code-actions_typos.go | 66 ++++++++++++++ server/handlers/wireguard/indexes/indexes.go | 14 ++- .../wireguard/indexes/indexes_handlers.go | 3 +- .../lsp/text-document-code-action.go | 1 + 9 files changed, 197 insertions(+), 112 deletions(-) create mode 100644 server/handlers/wireguard/analyzer/properties.go create mode 100644 server/handlers/wireguard/diagnostics/diagnostics.go create mode 100644 server/handlers/wireguard/handlers/fetch-code-actions_typos.go diff --git a/server/handlers/ssh_config/handlers/fetch-code-actions_typos.go b/server/handlers/ssh_config/handlers/fetch-code-actions_typos.go index cbf4c3f..0b7bc73 100644 --- a/server/handlers/ssh_config/handlers/fetch-code-actions_typos.go +++ b/server/handlers/ssh_config/handlers/fetch-code-actions_typos.go @@ -5,9 +5,9 @@ import ( sshconfig "config-lsp/handlers/ssh_config" "config-lsp/handlers/ssh_config/diagnostics" "config-lsp/handlers/ssh_config/fields" + "config-lsp/utils" "fmt" - "github.com/hbollon/go-edlib" protocol "github.com/tliron/glsp/protocol_3_16" ) @@ -24,7 +24,8 @@ func getKeywordTypoFixes( if typoOption, found := d.Indexes.UnknownOptions[line]; found { name := typoOption.Option.Key.Value.Value - suggestedOptions := findSimilarOptions(name) + opts := utils.KeysOfMap(fields.Options) + suggestedOptions := common.FindSimilarItems(fields.CreateNormalizedName(name), opts) actions := make([]protocol.CodeAction, 0, len(suggestedOptions)) @@ -61,43 +62,3 @@ func getKeywordTypoFixes( 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, -) []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 { - 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/wireguard/analyzer/analyzer.go b/server/handlers/wireguard/analyzer/analyzer.go index fad1cf4..2dac98f 100644 --- a/server/handlers/wireguard/analyzer/analyzer.go +++ b/server/handlers/wireguard/analyzer/analyzer.go @@ -35,6 +35,12 @@ func Analyze( d.Indexes = i + analyzeProperties(ctx) + + if len(ctx.diagnostics) > 0 { + return ctx.diagnostics + } + analyzeInterfaceSection(ctx) analyzeDNSPropertyContainsFallback(ctx) analyzeKeepAlivePropertyIsSet(ctx) diff --git a/server/handlers/wireguard/analyzer/properties.go b/server/handlers/wireguard/analyzer/properties.go new file mode 100644 index 0000000..8aa2882 --- /dev/null +++ b/server/handlers/wireguard/analyzer/properties.go @@ -0,0 +1,89 @@ +package analyzer + +import ( + "config-lsp/common" + docvalues "config-lsp/doc-values" + "config-lsp/handlers/wireguard/ast" + "config-lsp/handlers/wireguard/diagnostics" + "config-lsp/handlers/wireguard/fields" + "config-lsp/handlers/wireguard/indexes" + "fmt" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func analyzeProperties( + ctx *analyzerContext, +) { + for _, section := range ctx.document.Config.Sections { + normalizedHeaderName := fields.CreateNormalizedName(section.Header.Name) + + // Whether to check if the property is allowed in the section + checkAllowedProperty := true + existingProperties := make(map[fields.NormalizedName]*ast.WGProperty) + + it := section.Properties.Iterator() + for it.Next() { + property := it.Value().(*ast.WGProperty) + normalizedPropertyName := fields.CreateNormalizedName(property.Key.Name) + + if property.Key.Name == "" { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Message: "This property is missing a name", + Range: property.Key.ToLSPRange(), + Severity: &common.SeverityError, + }) + } + + if property.Value == nil || property.Value.Value == "" { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Message: "This property is missing a value", + Range: property.ToLSPRange(), + Severity: &common.SeverityError, + }) + checkAllowedProperty = false + } + + if checkAllowedProperty { + availableOptions := fields.OptionsHeaderMap[normalizedHeaderName] + + // Duplicate check + if existingProperty, found := existingProperties[normalizedPropertyName]; found { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Message: fmt.Sprintf("Property '%s' has already been defined on line %d", property.Key.Name, existingProperty.Start.Line+1), + Severity: &common.SeverityError, + Range: existingProperty.ToLSPRange(), + }) + // Check if value is valid + } else if option, found := availableOptions[normalizedPropertyName]; found { + invalidValues := option.DeprecatedCheckIsValid(property.Value.Value) + + for _, invalidValue := range invalidValues { + err := docvalues.LSPErrorFromInvalidValue(property.Start.Line, *invalidValue).ShiftCharacter(property.Value.Start.Character) + + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Range: err.Range.ToLSPRange(), + Message: err.Err.Error(), + Severity: &common.SeverityError, + }) + } + // Unknown property + } else { + ctx.diagnostics = append(ctx.diagnostics, + diagnostics.GenerateUnknownOption( + property.ToLSPRange(), + property.Key.Name, + ), + ) + + ctx.document.Indexes.UnknownProperties[property.Key.Start.Line] = indexes.WGIndexPropertyInfo{ + Section: section, + Property: property, + } + } + + existingProperties[normalizedPropertyName] = property + } + } + } +} diff --git a/server/handlers/wireguard/analyzer/structure.go b/server/handlers/wireguard/analyzer/structure.go index 96bb79a..bbdecfb 100644 --- a/server/handlers/wireguard/analyzer/structure.go +++ b/server/handlers/wireguard/analyzer/structure.go @@ -2,8 +2,6 @@ package analyzer import ( "config-lsp/common" - docvalues "config-lsp/doc-values" - "config-lsp/handlers/wireguard/ast" "config-lsp/handlers/wireguard/fields" "config-lsp/utils" "fmt" @@ -14,8 +12,6 @@ import ( func analyzeStructureIsValid(ctx *analyzerContext) { for _, section := range ctx.document.Config.Sections { normalizedHeaderName := fields.CreateNormalizedName(section.Header.Name) - // Whether to check if the property is allowed in the section - checkAllowedProperty := true if section.Header.Name == "" { ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ @@ -29,8 +25,6 @@ func analyzeStructureIsValid(ctx *analyzerContext) { Range: section.Header.ToLSPRange(), Severity: &common.SeverityError, }) - // Do not check as the section is unknown - checkAllowedProperty = false } if section.Properties.Size() == 0 { @@ -42,66 +36,6 @@ func analyzeStructureIsValid(ctx *analyzerContext) { protocol.DiagnosticTagUnnecessary, }, }) - } else { - existingProperties := make(map[fields.NormalizedName]*ast.WGProperty) - - it := section.Properties.Iterator() - for it.Next() { - property := it.Value().(*ast.WGProperty) - normalizedPropertyName := fields.CreateNormalizedName(property.Key.Name) - - if property.Key.Name == "" { - ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ - Message: "This property is missing a name", - Range: property.Key.ToLSPRange(), - Severity: &common.SeverityError, - }) - } - - if property.Value == nil || property.Value.Value == "" { - ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ - Message: "This property is missing a value", - Range: property.ToLSPRange(), - Severity: &common.SeverityError, - }) - checkAllowedProperty = false - } - - if checkAllowedProperty { - availableOptions := fields.OptionsHeaderMap[normalizedHeaderName] - - // Duplicate check - if existingProperty, found := existingProperties[normalizedPropertyName]; found { - ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ - Message: fmt.Sprintf("Property '%s' has already been defined on line %d", property.Key.Name, existingProperty.Start.Line+1), - Severity: &common.SeverityError, - Range: existingProperty.ToLSPRange(), - }) - // Check if value is valid - } else if option, found := availableOptions[normalizedPropertyName]; found { - invalidValues := option.DeprecatedCheckIsValid(property.Value.Value) - - for _, invalidValue := range invalidValues { - err := docvalues.LSPErrorFromInvalidValue(property.Start.Line, *invalidValue).ShiftCharacter(property.Value.Start.Character) - - ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ - Range: err.Range.ToLSPRange(), - Message: err.Err.Error(), - Severity: &common.SeverityError, - }) - } - // Unknown property - } else { - ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ - Message: fmt.Sprintf("Unknown property '%s'", property.Key.Name), - Range: property.Key.ToLSPRange(), - Severity: &common.SeverityError, - }) - } - - existingProperties[normalizedPropertyName] = property - } - } } } } diff --git a/server/handlers/wireguard/diagnostics/diagnostics.go b/server/handlers/wireguard/diagnostics/diagnostics.go new file mode 100644 index 0000000..dcba9f5 --- /dev/null +++ b/server/handlers/wireguard/diagnostics/diagnostics.go @@ -0,0 +1,19 @@ +package diagnostics + +import ( + "config-lsp/common" + "fmt" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func GenerateUnknownOption( + diagnosticRange protocol.Range, + propertyName string, +) protocol.Diagnostic { + return protocol.Diagnostic{ + Range: diagnosticRange, + Message: fmt.Sprintf("Unknown property: %s", propertyName), + Severity: &common.SeverityError, + } +} diff --git a/server/handlers/wireguard/handlers/fetch-code-actions_typos.go b/server/handlers/wireguard/handlers/fetch-code-actions_typos.go new file mode 100644 index 0000000..c45b029 --- /dev/null +++ b/server/handlers/wireguard/handlers/fetch-code-actions_typos.go @@ -0,0 +1,66 @@ +package handlers + +import ( + "config-lsp/common" + "config-lsp/handlers/wireguard" + "config-lsp/handlers/wireguard/diagnostics" + "config-lsp/handlers/wireguard/fields" + "config-lsp/utils" + "fmt" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func GetPropertyKeywordTypoFixes( + d *wireguard.WGDocument, + params *protocol.CodeActionParams, +) []protocol.CodeAction { + if common.ServerOptions.NoTypoSuggestions { + return nil + } + + line := params.Range.Start.Line + + if typoInfo, found := d.Indexes.UnknownProperties[line]; found { + if options, found := fields.OptionsHeaderMap[fields.CreateNormalizedName(typoInfo.Section.Header.Name)]; found { + normalizedPropertyKey := fields.CreateNormalizedName(typoInfo.Property.Key.Name) + opts := utils.KeysOfMap(options) + + suggestedProperties := common.FindSimilarItems(normalizedPropertyKey, opts) + + actions := make([]protocol.CodeAction, 0, len(suggestedProperties)) + + kind := protocol.CodeActionKindQuickFix + for index, normalizedPropertyName := range suggestedProperties { + isPreferred := index == 0 + optionName := fields.AllOptionsFormatted[normalizedPropertyName] + + actions = append(actions, protocol.CodeAction{ + Title: fmt.Sprintf("Typo Fix: %s", optionName), + IsPreferred: &isPreferred, + Kind: &kind, + Diagnostics: []protocol.Diagnostic{ + diagnostics.GenerateUnknownOption( + typoInfo.Property.ToLSPRange(), + typoInfo.Property.Key.Name, + ), + }, + Edit: &protocol.WorkspaceEdit{ + Changes: map[protocol.DocumentUri][]protocol.TextEdit{ + params.TextDocument.URI: { + { + Range: typoInfo.Property.Key.ToLSPRange(), + NewText: optionName, + }, + }, + }, + }, + }) + } + + return actions + } + } + + return nil +} diff --git a/server/handlers/wireguard/indexes/indexes.go b/server/handlers/wireguard/indexes/indexes.go index 3c58f77..858226a 100644 --- a/server/handlers/wireguard/indexes/indexes.go +++ b/server/handlers/wireguard/indexes/indexes.go @@ -2,7 +2,15 @@ package indexes import "config-lsp/handlers/wireguard/ast" -type WGIndexes struct { - // map of: section name -> WGSection - SectionsByName map[string][]*ast.WGSection +type WGIndexPropertyInfo struct { + Section *ast.WGSection + Property *ast.WGProperty +} + +type WGIndexes struct { + // map of: section name -> *WGSection + SectionsByName map[string][]*ast.WGSection + + // map of: line number -> *WGIndexPropertyInfo + UnknownProperties map[uint32]WGIndexPropertyInfo } diff --git a/server/handlers/wireguard/indexes/indexes_handlers.go b/server/handlers/wireguard/indexes/indexes_handlers.go index c332e70..0366b90 100644 --- a/server/handlers/wireguard/indexes/indexes_handlers.go +++ b/server/handlers/wireguard/indexes/indexes_handlers.go @@ -8,7 +8,8 @@ import ( func CreateIndexes(config *ast.WGConfig) (*WGIndexes, []common.LSPError) { errs := make([]common.LSPError, 0) indexes := &WGIndexes{ - SectionsByName: make(map[string][]*ast.WGSection), + SectionsByName: make(map[string][]*ast.WGSection), + UnknownProperties: make(map[uint32]WGIndexPropertyInfo), } for _, section := range config.Sections { diff --git a/server/handlers/wireguard/lsp/text-document-code-action.go b/server/handlers/wireguard/lsp/text-document-code-action.go index 4c86674..f8de8c6 100644 --- a/server/handlers/wireguard/lsp/text-document-code-action.go +++ b/server/handlers/wireguard/lsp/text-document-code-action.go @@ -16,6 +16,7 @@ func TextDocumentCodeAction(context *glsp.Context, params *protocol.CodeActionPa actions = append(actions, handlers.GetKeyGenerationCodeActions(d, params)...) actions = append(actions, handlers.GetKeepaliveCodeActions(d, params)...) actions = append(actions, handlers.GetAddPeerLikeThis(d, params)...) + actions = append(actions, handlers.GetPropertyKeywordTypoFixes(d, params)...) if len(actions) > 0 { return actions, nil