feat(server): Add Wireguard code actions: Fix typo

This commit is contained in:
Myzel394 2025-03-29 16:12:28 +01:00
parent c5fefad56d
commit e4d7521a4c
No known key found for this signature in database
GPG Key ID: 3B955307C2FC2F11
9 changed files with 197 additions and 112 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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