From 67c7f7f4b7db7c245030307096f6a7caf22ae87d Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 16 Feb 2025 13:21:45 +0100 Subject: [PATCH 01/31] feat(server): Add NoTypoSuggestions to global options --- server/common/options.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/server/common/options.go b/server/common/options.go index 35773e0..8dc762d 100644 --- a/server/common/options.go +++ b/server/common/options.go @@ -13,6 +13,15 @@ type ServerOptionsType struct { // we show a native warning. The error message boxes just clutter // the interface. NoUndetectableErrors bool + + // If true, the server will not detect typos and suggest + // the correct keywords. + // Since the server finds typos using the Damerau-Levenshtein distance, + // and this is done each time code actions are requested + // (which happens quite often), these suggestions can eat a lot of resources. + // You may want to enable this option if you are dealing with little + // resources or if you're low on battery. + NoTypoSuggestions bool } var ServerOptions = new(ServerOptionsType) @@ -22,4 +31,9 @@ func InitServerOptions() { Log.Info("config-lsp will not return errors for undetectable files") ServerOptions.NoUndetectableErrors = true } + + if slices.Contains(os.Args, "--no-typo-suggestions") { + Log.Info("config-lsp will not detect typos for keywords") + ServerOptions.NoTypoSuggestions = true + } } From 026e0349a1b1479594bb49987b278eae9008c0aa Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 16 Feb 2025 14:43:14 +0100 Subject: [PATCH 02/31] feat(server): Add typo fix suggestion to ssh_config --- server/common/options.go | 3 + server/go.mod | 1 + server/go.sum | 2 + server/handlers/ssh_config/analyzer/values.go | 2 - .../ssh_config/handlers/code-action-typos.go | 103 ++++++++++++++++++ .../ssh_config/handlers/fetch-code-actions.go | 18 ++- .../lsp/text-document-did-open.go | 3 - 7 files changed, 123 insertions(+), 9 deletions(-) create mode 100644 server/handlers/ssh_config/handlers/code-action-typos.go diff --git a/server/common/options.go b/server/common/options.go index 8dc762d..3ac4edb 100644 --- a/server/common/options.go +++ b/server/common/options.go @@ -27,6 +27,9 @@ type ServerOptionsType struct { var ServerOptions = new(ServerOptionsType) func InitServerOptions() { + ServerOptions.NoUndetectableErrors = false + ServerOptions.NoTypoSuggestions = false + if slices.Contains(os.Args, "--no-undetectable-errors") { Log.Info("config-lsp will not return errors for undetectable files") ServerOptions.NoUndetectableErrors = true diff --git a/server/go.mod b/server/go.mod index 886d90f..45a560f 100644 --- a/server/go.mod +++ b/server/go.mod @@ -15,6 +15,7 @@ require ( require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/gorilla/websocket v1.5.3 // indirect + github.com/hbollon/go-edlib v1.6.0 // indirect github.com/iancoleman/strcase v0.3.0 // indirect github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect diff --git a/server/go.sum b/server/go.sum index a99cb8e..b8344b8 100644 --- a/server/go.sum +++ b/server/go.sum @@ -9,6 +9,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hbollon/go-edlib v1.6.0 h1:ga7AwwVIvP8mHm9GsPueC0d71cfRU/52hmPJ7Tprv4E= +github.com/hbollon/go-edlib v1.6.0/go.mod h1:wnt6o6EIVEzUfgbUZY7BerzQ2uvzp354qmS2xaLkrhM= github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM= diff --git a/server/handlers/ssh_config/analyzer/values.go b/server/handlers/ssh_config/analyzer/values.go index 49b7e1e..a339487 100644 --- a/server/handlers/ssh_config/analyzer/values.go +++ b/server/handlers/ssh_config/analyzer/values.go @@ -29,8 +29,6 @@ func analyzeValuesAreValid( ), ) ctx.document.Indexes.UnknownOptions[info.Option.Start.Line] = info - - continue } } } diff --git a/server/handlers/ssh_config/handlers/code-action-typos.go b/server/handlers/ssh_config/handlers/code-action-typos.go new file mode 100644 index 0000000..cbf4c3f --- /dev/null +++ b/server/handlers/ssh_config/handlers/code-action-typos.go @@ -0,0 +1,103 @@ +package handlers + +import ( + "config-lsp/common" + sshconfig "config-lsp/handlers/ssh_config" + "config-lsp/handlers/ssh_config/diagnostics" + "config-lsp/handlers/ssh_config/fields" + "fmt" + + "github.com/hbollon/go-edlib" + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func getKeywordTypoFixes( + d *sshconfig.SSHDocument, + 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) + + 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, +) []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/ssh_config/handlers/fetch-code-actions.go b/server/handlers/ssh_config/handlers/fetch-code-actions.go index 1adb69d..c27acf6 100644 --- a/server/handlers/ssh_config/handlers/fetch-code-actions.go +++ b/server/handlers/ssh_config/handlers/fetch-code-actions.go @@ -12,12 +12,22 @@ func FetchCodeActions( d *sshconfig.SSHDocument, params *protocol.CodeActionParams, ) []protocol.CodeAction { - line := params.Range.Start.Line - if d.Indexes == nil { return nil } + actions := getAddToUnknownCodeAction(d, params) + actions = append(actions, getKeywordTypoFixes(d, params)...) + + return actions +} + +func getAddToUnknownCodeAction( + d *sshconfig.SSHDocument, + params *protocol.CodeActionParams, +) []protocol.CodeAction { + line := params.Range.Start.Line + if unknownOption, found := d.Indexes.UnknownOptions[line]; found { var blockLine *uint32 @@ -39,7 +49,7 @@ func FetchCodeActions( }, } kind := protocol.CodeActionKindQuickFix - codeAction := &protocol.CodeAction{ + codeAction := protocol.CodeAction{ Title: fmt.Sprintf("Add %s to unknown options", unknownOption.Option.Key.Key), Command: &command, Kind: &kind, @@ -52,7 +62,7 @@ func FetchCodeActions( } return []protocol.CodeAction{ - *codeAction, + codeAction, } } diff --git a/server/root-handler/lsp/text-document-did-open.go b/server/root-handler/lsp/text-document-did-open.go index e1ddd28..842d136 100644 --- a/server/root-handler/lsp/text-document-did-open.go +++ b/server/root-handler/lsp/text-document-did-open.go @@ -61,9 +61,6 @@ func initFile( uri protocol.DocumentUri, advertisedLanguage string, ) (*shared.SupportedLanguage, error) { - println("Initializing the file") - println(advertisedLanguage) - println(uri) language, err := utils.DetectLanguage(content, advertisedLanguage, uri) if err != nil { From 3ac3ebbe50dfedbcbd7a6768b6f82ad0da6db0d4 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 16 Feb 2025 15:04:52 +0100 Subject: [PATCH 03/31] refactor(server): Outsource ssh_config code action: Add To Unknown into own file --- .../handlers/code-actions-add-to-unknown.go | 56 +++++++++++++++++++ .../ssh_config/handlers/fetch-code-actions.go | 49 ---------------- 2 files changed, 56 insertions(+), 49 deletions(-) create mode 100644 server/handlers/ssh_config/handlers/code-actions-add-to-unknown.go diff --git a/server/handlers/ssh_config/handlers/code-actions-add-to-unknown.go b/server/handlers/ssh_config/handlers/code-actions-add-to-unknown.go new file mode 100644 index 0000000..0c1abec --- /dev/null +++ b/server/handlers/ssh_config/handlers/code-actions-add-to-unknown.go @@ -0,0 +1,56 @@ +package handlers + +import ( + sshconfig "config-lsp/handlers/ssh_config" + "config-lsp/handlers/ssh_config/diagnostics" + "fmt" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func getAddToUnknownCodeAction( + d *sshconfig.SSHDocument, + params *protocol.CodeActionParams, +) []protocol.CodeAction { + line := params.Range.Start.Line + + if unknownOption, found := d.Indexes.UnknownOptions[line]; found { + var blockLine *uint32 + + if unknownOption.Block != nil { + blockLineValue := uint32(unknownOption.Block.GetLocation().Start.Line) + blockLine = &blockLineValue + } + + commandID := "sshconfig." + CodeActionAddToUnknown + command := protocol.Command{ + Title: fmt.Sprintf("Add %s to unknown options", unknownOption.Option.Key.Key), + Command: string(commandID), + Arguments: []any{ + codeActionAddToUnknownArgs{ + URI: params.TextDocument.URI, + OptionLine: unknownOption.Option.Start.Line, + BlockLine: blockLine, + }, + }, + } + kind := protocol.CodeActionKindQuickFix + codeAction := protocol.CodeAction{ + Title: fmt.Sprintf("Add %s to unknown options", unknownOption.Option.Key.Key), + Command: &command, + Kind: &kind, + Diagnostics: []protocol.Diagnostic{ + diagnostics.GenerateUnknownOption( + unknownOption.Option.Key.ToLSPRange(), + unknownOption.Option.Key.Value.Value, + ), + }, + } + + return []protocol.CodeAction{ + codeAction, + } + } + + return nil +} diff --git a/server/handlers/ssh_config/handlers/fetch-code-actions.go b/server/handlers/ssh_config/handlers/fetch-code-actions.go index c27acf6..27a2a7f 100644 --- a/server/handlers/ssh_config/handlers/fetch-code-actions.go +++ b/server/handlers/ssh_config/handlers/fetch-code-actions.go @@ -2,8 +2,6 @@ package handlers import ( sshconfig "config-lsp/handlers/ssh_config" - "config-lsp/handlers/ssh_config/diagnostics" - "fmt" protocol "github.com/tliron/glsp/protocol_3_16" ) @@ -21,50 +19,3 @@ func FetchCodeActions( return actions } - -func getAddToUnknownCodeAction( - d *sshconfig.SSHDocument, - params *protocol.CodeActionParams, -) []protocol.CodeAction { - line := params.Range.Start.Line - - if unknownOption, found := d.Indexes.UnknownOptions[line]; found { - var blockLine *uint32 - - if unknownOption.Block != nil { - blockLineValue := uint32(unknownOption.Block.GetLocation().Start.Line) - blockLine = &blockLineValue - } - - commandID := "sshconfig." + CodeActionAddToUnknown - command := protocol.Command{ - Title: fmt.Sprintf("Add %s to unknown options", unknownOption.Option.Key.Key), - Command: string(commandID), - Arguments: []any{ - codeActionAddToUnknownArgs{ - URI: params.TextDocument.URI, - OptionLine: unknownOption.Option.Start.Line, - BlockLine: blockLine, - }, - }, - } - kind := protocol.CodeActionKindQuickFix - codeAction := protocol.CodeAction{ - Title: fmt.Sprintf("Add %s to unknown options", unknownOption.Option.Key.Key), - Command: &command, - Kind: &kind, - Diagnostics: []protocol.Diagnostic{ - diagnostics.GenerateUnknownOption( - unknownOption.Option.Key.ToLSPRange(), - unknownOption.Option.Key.Value.Value, - ), - }, - } - - return []protocol.CodeAction{ - codeAction, - } - } - - return nil -} From 0c827b04cdd6fb5419b3640d152db5314f162b65 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Mon, 17 Feb 2025 21:50:56 +0100 Subject: [PATCH 04/31] feat(server): Improve sshd_config; Add unknown option detection --- .../handlers/sshd_config/analyzer/analyzer.go | 1 + .../handlers/sshd_config/analyzer/quotes.go | 10 ++--- .../handlers/sshd_config/analyzer/tokens.go | 10 ++--- .../handlers/sshd_config/analyzer/values.go | 39 +++++++++++++++++++ .../sshd_config/analyzer/values_test.go | 34 ++++++++++++++++ .../sshd_config/ast/sshd_config_ast_utils.go | 8 ++++ .../sshd_config/ast/sshd_config_fields.go | 24 +++++++----- .../sshd_config/diagnostics/diagnostics.go | 19 +++++++++ .../handlers/sshd_config/indexes/indexes.go | 2 + .../sshd_config/indexes/indexes_handlers.go | 1 + 10 files changed, 129 insertions(+), 19 deletions(-) create mode 100644 server/handlers/sshd_config/analyzer/values.go create mode 100644 server/handlers/sshd_config/analyzer/values_test.go create mode 100644 server/handlers/sshd_config/ast/sshd_config_ast_utils.go create mode 100644 server/handlers/sshd_config/diagnostics/diagnostics.go diff --git a/server/handlers/sshd_config/analyzer/analyzer.go b/server/handlers/sshd_config/analyzer/analyzer.go index 0007b09..72cb52e 100644 --- a/server/handlers/sshd_config/analyzer/analyzer.go +++ b/server/handlers/sshd_config/analyzer/analyzer.go @@ -54,6 +54,7 @@ func Analyze( } } + analyzeValuesAreValid(ctx) analyzeMatchBlocks(ctx) analyzeTokens(ctx) diff --git a/server/handlers/sshd_config/analyzer/quotes.go b/server/handlers/sshd_config/analyzer/quotes.go index a563bc5..e423510 100644 --- a/server/handlers/sshd_config/analyzer/quotes.go +++ b/server/handlers/sshd_config/analyzer/quotes.go @@ -11,12 +11,12 @@ import ( func analyzeQuotesAreValid( ctx *analyzerContext, ) { - for _, option := range ctx.document.Config.GetAllOptions() { - checkIsUsingDoubleQuotes(ctx, option.Key.Value, option.Key.LocationRange) - checkIsUsingDoubleQuotes(ctx, option.OptionValue.Value, option.OptionValue.LocationRange) + for _, info := range ctx.document.Config.GetAllOptions() { + checkIsUsingDoubleQuotes(ctx, info.Option.Key.Value, info.Option.Key.LocationRange) + checkIsUsingDoubleQuotes(ctx, info.Option.OptionValue.Value, info.Option.OptionValue.LocationRange) - checkQuotesAreClosed(ctx, option.Key.Value, option.Key.LocationRange) - checkQuotesAreClosed(ctx, option.OptionValue.Value, option.OptionValue.LocationRange) + checkQuotesAreClosed(ctx, info.Option.Key.Value, info.Option.Key.LocationRange) + checkQuotesAreClosed(ctx, info.Option.OptionValue.Value, info.Option.OptionValue.LocationRange) } } diff --git a/server/handlers/sshd_config/analyzer/tokens.go b/server/handlers/sshd_config/analyzer/tokens.go index ee642db..4c29b2d 100644 --- a/server/handlers/sshd_config/analyzer/tokens.go +++ b/server/handlers/sshd_config/analyzer/tokens.go @@ -13,13 +13,13 @@ import ( func analyzeTokens( ctx *analyzerContext, ) { - for _, option := range ctx.document.Config.GetAllOptions() { - if option.Key == nil || option.OptionValue == nil { + for _, info := range ctx.document.Config.GetAllOptions() { + if info.Option.Key == nil || info.Option.OptionValue == nil { continue } - key := option.Key.Key - text := option.OptionValue.Value.Value + key := info.Option.Key.Key + text := info.Option.OptionValue.Value.Value var tokens []string if foundTokens, found := fields.OptionsTokensMap[key]; found { @@ -39,7 +39,7 @@ func analyzeTokens( } ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ - Range: option.OptionValue.ToLSPRange(), + Range: info.Option.OptionValue.ToLSPRange(), Message: fmt.Sprintf("Token '%s' is not allowed for option '%s'", token, optionName), Severity: &common.SeverityError, }) diff --git a/server/handlers/sshd_config/analyzer/values.go b/server/handlers/sshd_config/analyzer/values.go new file mode 100644 index 0000000..d29d8c7 --- /dev/null +++ b/server/handlers/sshd_config/analyzer/values.go @@ -0,0 +1,39 @@ +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/analyzer/values_test.go b/server/handlers/sshd_config/analyzer/values_test.go new file mode 100644 index 0000000..b09df8c --- /dev/null +++ b/server/handlers/sshd_config/analyzer/values_test.go @@ -0,0 +1,34 @@ +package analyzer + +import ( + testutils_test "config-lsp/handlers/sshd_config/test_utils" + "testing" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func TestUnknownOptionExample( + t *testing.T, +) { + d := testutils_test.DocumentFromInput(t, ` +ThisOptionDoesNotExist okay +`) + ctx := &analyzerContext{ + document: d, + diagnostics: make([]protocol.Diagnostic, 0), + } + + analyzeValuesAreValid(ctx) + + if !(len(ctx.diagnostics) == 1) { + t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics)) + } + + if !(len(ctx.document.Indexes.UnknownOptions) == 1) { + t.Errorf("Expected 1 unknown option, got %v", len(ctx.document.Indexes.UnknownOptions)) + } + + if !(ctx.document.Indexes.UnknownOptions[0].Option.Key.Value.Value == "ThisOptionDoesNotExist") { + t.Errorf("Expected 'ThisOptionDoesNotExist', got %v", ctx.document.Indexes.UnknownOptions[0].Option.Key.Value.Value) + } +} diff --git a/server/handlers/sshd_config/ast/sshd_config_ast_utils.go b/server/handlers/sshd_config/ast/sshd_config_ast_utils.go new file mode 100644 index 0000000..aeebea8 --- /dev/null +++ b/server/handlers/sshd_config/ast/sshd_config_ast_utils.go @@ -0,0 +1,8 @@ +// Contains structs that are used as utilities, but are +// not used for the AST itself +package ast + +type SSHDOptionInfo struct { + MatchBlock *SSHDMatchBlock + Option *SSHDOption +} diff --git a/server/handlers/sshd_config/ast/sshd_config_fields.go b/server/handlers/sshd_config/ast/sshd_config_fields.go index 11546d4..c5960fc 100644 --- a/server/handlers/sshd_config/ast/sshd_config_fields.go +++ b/server/handlers/sshd_config/ast/sshd_config_fields.go @@ -64,26 +64,32 @@ func (c SSHDConfig) FindOption(line uint32) (*SSHDOption, *SSHDMatchBlock) { return nil, nil } -func (c SSHDConfig) GetAllOptions() []*SSHDOption { - options := make( - []*SSHDOption, +func (c SSHDConfig) GetAllOptions() []SSHDOptionInfo { + infos := make( + []SSHDOptionInfo, 0, // Approximation, this does not need to be exact c.Options.Size()+10, ) + var currentMatchBlock *SSHDMatchBlock = nil + for _, rawEntry := range c.Options.Values() { switch entry := rawEntry.(type) { case *SSHDOption: - options = append(options, entry) + infos = append(infos, SSHDOptionInfo{ + Option: entry, + MatchBlock: currentMatchBlock, + }) case *SSHDMatchBlock: - options = append(options, entry.MatchOption) + currentMatchBlock = entry - for _, rawOption := range entry.Options.Values() { - options = append(options, rawOption.(*SSHDOption)) - } + infos = append(infos, SSHDOptionInfo{ + Option: entry.MatchOption, + MatchBlock: currentMatchBlock, + }) } } - return options + return infos } diff --git a/server/handlers/sshd_config/diagnostics/diagnostics.go b/server/handlers/sshd_config/diagnostics/diagnostics.go new file mode 100644 index 0000000..86b96f0 --- /dev/null +++ b/server/handlers/sshd_config/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, + optionName string, +) protocol.Diagnostic { + return protocol.Diagnostic{ + Range: diagnosticRange, + Message: fmt.Sprintf("Unknown option: %s", optionName), + Severity: &common.SeverityError, + } +} diff --git a/server/handlers/sshd_config/indexes/indexes.go b/server/handlers/sshd_config/indexes/indexes.go index 236b359..656f36d 100644 --- a/server/handlers/sshd_config/indexes/indexes.go +++ b/server/handlers/sshd_config/indexes/indexes.go @@ -37,4 +37,6 @@ type SSHDIndexes struct { AllOptionsPerName map[fields.NormalizedOptionName](map[*ast.SSHDMatchBlock]([]*ast.SSHDOption)) Includes map[uint32]*SSHDIndexIncludeLine + + UnknownOptions map[uint32]ast.SSHDOptionInfo } diff --git a/server/handlers/sshd_config/indexes/indexes_handlers.go b/server/handlers/sshd_config/indexes/indexes_handlers.go index 5c90047..76d2cfb 100644 --- a/server/handlers/sshd_config/indexes/indexes_handlers.go +++ b/server/handlers/sshd_config/indexes/indexes_handlers.go @@ -18,6 +18,7 @@ func CreateIndexes(config ast.SSHDConfig) (*SSHDIndexes, []common.LSPError) { indexes := &SSHDIndexes{ AllOptionsPerName: make(map[fields.NormalizedOptionName](map[*ast.SSHDMatchBlock]([]*ast.SSHDOption))), Includes: make(map[uint32]*SSHDIndexIncludeLine), + UnknownOptions: make(map[uint32]ast.SSHDOptionInfo), } it := config.Options.Iterator() 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 05/31] 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 +} From dbf543db666562ab9addbd2eb93865d026d6fc8d Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Mon, 17 Feb 2025 22:27:52 +0100 Subject: [PATCH 06/31] fix(server): Improve ssh_config --- .../handlers/ssh_config/analyzer/analyzer.go | 1 - .../handlers/ssh_config/analyzer/options.go | 31 ++++++- .../ssh_config/analyzer/options_test.go | 76 +++++++++++++++++ server/handlers/ssh_config/analyzer/values.go | 34 -------- .../ssh_config/analyzer/values_test.go | 84 ------------------- ...o => fetch-code-actions_add_to_unknown.go} | 0 ...n-typos.go => fetch-code-actions_typos.go} | 0 7 files changed, 103 insertions(+), 123 deletions(-) delete mode 100644 server/handlers/ssh_config/analyzer/values.go delete mode 100644 server/handlers/ssh_config/analyzer/values_test.go rename server/handlers/ssh_config/handlers/{code-actions-add-to-unknown.go => fetch-code-actions_add_to_unknown.go} (100%) rename server/handlers/ssh_config/handlers/{code-action-typos.go => fetch-code-actions_typos.go} (100%) diff --git a/server/handlers/ssh_config/analyzer/analyzer.go b/server/handlers/ssh_config/analyzer/analyzer.go index 447bb34..b8b9ba7 100644 --- a/server/handlers/ssh_config/analyzer/analyzer.go +++ b/server/handlers/ssh_config/analyzer/analyzer.go @@ -54,7 +54,6 @@ func Analyze( } } - analyzeValuesAreValid(ctx) analyzeTokens(ctx) analyzeIgnoreUnknownHasNoUnnecessary(ctx) analyzeDependents(ctx) diff --git a/server/handlers/ssh_config/analyzer/options.go b/server/handlers/ssh_config/analyzer/options.go index cee2bd6..0356cf5 100644 --- a/server/handlers/ssh_config/analyzer/options.go +++ b/server/handlers/ssh_config/analyzer/options.go @@ -4,6 +4,7 @@ import ( "config-lsp/common" docvalues "config-lsp/doc-values" "config-lsp/handlers/ssh_config/ast" + "config-lsp/handlers/ssh_config/diagnostics" "config-lsp/handlers/ssh_config/fields" "config-lsp/utils" "fmt" @@ -41,11 +42,33 @@ func checkOption( checkIsUsingDoubleQuotes(ctx, option.Key.Value, option.Key.LocationRange) checkQuotesAreClosed(ctx, option.Key.Value, option.Key.LocationRange) - docOption, found := fields.Options[option.Key.Key] + docOption, optionFound := fields.Options[option.Key.Key] - if !found { + if !optionFound { // Diagnostics will be handled by `values.go` - return + if !ctx.document.Indexes.CanOptionBeIgnored(option, block) { + ctx.diagnostics = append( + ctx.diagnostics, + diagnostics.GenerateUnknownOption( + option.Key.ToLSPRange(), + option.Key.Value.Value, + ), + ) + ctx.document.Indexes.UnknownOptions[option.Start.Line] = ast.AllOptionInfo{ + Option: option, + Block: block, + } + + return + } + } + + if block != nil && block.GetBlockType() == ast.SSHBlockTypeHost && utils.KeyExists(fields.HostDisallowedOptions, option.Key.Key) { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Range: option.Key.LocationRange.ToLSPRange(), + Message: fmt.Sprintf("Option '%s' is not allowed in Host blocks", option.Key.Key), + Severity: &common.SeverityError, + }) } // Check for values that are not allowed in Host blocks @@ -59,7 +82,7 @@ func checkOption( } } - if option.OptionValue != nil { + if option.OptionValue != nil && optionFound { checkIsUsingDoubleQuotes(ctx, option.OptionValue.Value, option.OptionValue.LocationRange) checkQuotesAreClosed(ctx, option.OptionValue.Value, option.OptionValue.LocationRange) diff --git a/server/handlers/ssh_config/analyzer/options_test.go b/server/handlers/ssh_config/analyzer/options_test.go index f4ba881..5a98d7f 100644 --- a/server/handlers/ssh_config/analyzer/options_test.go +++ b/server/handlers/ssh_config/analyzer/options_test.go @@ -112,3 +112,79 @@ Match t.Fatalf("Expected 1 error, got %v", ctx.diagnostics) } } + +func TestUnknownOptionExample( + t *testing.T, +) { + d := testutils_test.DocumentFromInput(t, ` +ThisOptionDoesNotExist okay +`) + ctx := &analyzerContext{ + document: d, + diagnostics: make([]protocol.Diagnostic, 0), + } + + analyzeStructureIsValid(ctx) + + if !(len(ctx.diagnostics) == 1) { + t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics)) + } + + if !(len(ctx.document.Indexes.UnknownOptions) == 1) { + t.Errorf("Expected 1 unknown option, got %v", len(ctx.document.Indexes.UnknownOptions)) + } + + if !(ctx.document.Indexes.UnknownOptions[0].Option.Key.Value.Value == "ThisOptionDoesNotExist") { + t.Errorf("Expected 'ThisOptionDoesNotExist', got %v", ctx.document.Indexes.UnknownOptions[0].Option.Key.Value.Value) + } +} + +func TestUnknownOptionButIgnoredExample( + t *testing.T, +) { + d := testutils_test.DocumentFromInput(t, ` +IgnoreUnknown ThisOptionDoesNotExist +ThisOptionDoesNotExist okay +`) + ctx := &analyzerContext{ + document: d, + diagnostics: make([]protocol.Diagnostic, 0), + } + + analyzeStructureIsValid(ctx) + + if len(ctx.diagnostics) > 0 { + t.Fatalf("Expected no errors, but got %v", len(ctx.diagnostics)) + } + + if !(len(ctx.document.Indexes.UnknownOptions) == 0) { + t.Errorf("Expected 0 unknown options, got %v", len(ctx.document.Indexes.UnknownOptions)) + } +} + +func TestUnknownOptionIgnoredIsAfterDefinitionExample( + t *testing.T, +) { + d := testutils_test.DocumentFromInput(t, ` +ThisOptionDoesNotExist okay +IgnoreUnknown ThisOptionDoesNotExist +`) + ctx := &analyzerContext{ + document: d, + diagnostics: make([]protocol.Diagnostic, 0), + } + + analyzeStructureIsValid(ctx) + + if !(len(ctx.diagnostics) == 1) { + t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics)) + } + + if !(len(ctx.document.Indexes.UnknownOptions) == 1) { + t.Errorf("Expected 1 unknown option, got %v", len(ctx.document.Indexes.UnknownOptions)) + } + + if !(ctx.document.Indexes.UnknownOptions[0].Option.Key.Value.Value == "ThisOptionDoesNotExist") { + t.Errorf("Expected 'ThisOptionDoesNotExist', got %v", ctx.document.Indexes.UnknownOptions[0].Option.Key.Value.Value) + } +} diff --git a/server/handlers/ssh_config/analyzer/values.go b/server/handlers/ssh_config/analyzer/values.go deleted file mode 100644 index a339487..0000000 --- a/server/handlers/ssh_config/analyzer/values.go +++ /dev/null @@ -1,34 +0,0 @@ -package analyzer - -import ( - "config-lsp/handlers/ssh_config/diagnostics" - "config-lsp/handlers/ssh_config/fields" -) - -func analyzeValuesAreValid( - ctx *analyzerContext, -) { - // Check if there are unknown options - for _, info := range ctx.document.Config.GetAllOptions() { - option := info.Option - block := info.Block - - _, found := fields.Options[option.Key.Key] - - if !found { - if ctx.document.Indexes.CanOptionBeIgnored(option, block) { - // Skip - continue - } - - ctx.diagnostics = append( - ctx.diagnostics, - diagnostics.GenerateUnknownOption( - option.Key.ToLSPRange(), - option.Key.Value.Value, - ), - ) - ctx.document.Indexes.UnknownOptions[info.Option.Start.Line] = info - } - } -} diff --git a/server/handlers/ssh_config/analyzer/values_test.go b/server/handlers/ssh_config/analyzer/values_test.go deleted file mode 100644 index 88bffcc..0000000 --- a/server/handlers/ssh_config/analyzer/values_test.go +++ /dev/null @@ -1,84 +0,0 @@ -package analyzer - -import ( - testutils_test "config-lsp/handlers/ssh_config/test_utils" - "testing" - - protocol "github.com/tliron/glsp/protocol_3_16" -) - -func TestUnknownOptionExample( - t *testing.T, -) { - d := testutils_test.DocumentFromInput(t, ` -ThisOptionDoesNotExist okay -`) - ctx := &analyzerContext{ - document: d, - diagnostics: make([]protocol.Diagnostic, 0), - } - - analyzeValuesAreValid(ctx) - - if !(len(ctx.diagnostics) == 1) { - t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics)) - } - - if !(len(ctx.document.Indexes.UnknownOptions) == 1) { - t.Errorf("Expected 1 unknown option, got %v", len(ctx.document.Indexes.UnknownOptions)) - } - - if !(ctx.document.Indexes.UnknownOptions[0].Option.Key.Value.Value == "ThisOptionDoesNotExist") { - t.Errorf("Expected 'ThisOptionDoesNotExist', got %v", ctx.document.Indexes.UnknownOptions[0].Option.Key.Value.Value) - } -} - -func TestUnknownOptionButIgnoredExample( - t *testing.T, -) { - d := testutils_test.DocumentFromInput(t, ` -IgnoreUnknown ThisOptionDoesNotExist -ThisOptionDoesNotExist okay -`) - ctx := &analyzerContext{ - document: d, - diagnostics: make([]protocol.Diagnostic, 0), - } - - analyzeValuesAreValid(ctx) - - if len(ctx.diagnostics) > 0 { - t.Fatalf("Expected no errors, but got %v", len(ctx.diagnostics)) - } - - if !(len(ctx.document.Indexes.UnknownOptions) == 0) { - t.Errorf("Expected 0 unknown options, got %v", len(ctx.document.Indexes.UnknownOptions)) - } -} - -func TestUnknownOptionIgnoredIsAfterDefinitionExample( - t *testing.T, -) { - d := testutils_test.DocumentFromInput(t, ` -ThisOptionDoesNotExist okay -IgnoreUnknown ThisOptionDoesNotExist -`) - ctx := &analyzerContext{ - document: d, - diagnostics: make([]protocol.Diagnostic, 0), - } - - analyzeValuesAreValid(ctx) - - if !(len(ctx.diagnostics) == 1) { - t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics)) - } - - if !(len(ctx.document.Indexes.UnknownOptions) == 1) { - t.Errorf("Expected 1 unknown option, got %v", len(ctx.document.Indexes.UnknownOptions)) - } - - if !(ctx.document.Indexes.UnknownOptions[0].Option.Key.Value.Value == "ThisOptionDoesNotExist") { - t.Errorf("Expected 'ThisOptionDoesNotExist', got %v", ctx.document.Indexes.UnknownOptions[0].Option.Key.Value.Value) - } -} diff --git a/server/handlers/ssh_config/handlers/code-actions-add-to-unknown.go b/server/handlers/ssh_config/handlers/fetch-code-actions_add_to_unknown.go similarity index 100% rename from server/handlers/ssh_config/handlers/code-actions-add-to-unknown.go rename to server/handlers/ssh_config/handlers/fetch-code-actions_add_to_unknown.go diff --git a/server/handlers/ssh_config/handlers/code-action-typos.go b/server/handlers/ssh_config/handlers/fetch-code-actions_typos.go similarity index 100% rename from server/handlers/ssh_config/handlers/code-action-typos.go rename to server/handlers/ssh_config/handlers/fetch-code-actions_typos.go From 706b8137ddf6f77eecdf9a2cf472d646a437371d Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Mon, 17 Feb 2025 22:28:09 +0100 Subject: [PATCH 07/31] fix(server): Add code action LSP to root-handler --- server/root-handler/lsp/text-document-code-action.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/root-handler/lsp/text-document-code-action.go b/server/root-handler/lsp/text-document-code-action.go index 2b0a146..2a7027a 100644 --- a/server/root-handler/lsp/text-document-code-action.go +++ b/server/root-handler/lsp/text-document-code-action.go @@ -5,6 +5,7 @@ import ( aliases "config-lsp/handlers/aliases/lsp" hosts "config-lsp/handlers/hosts/lsp" sshconfig "config-lsp/handlers/ssh_config/lsp" + sshdconfig "config-lsp/handlers/sshd_config/lsp" wireguard "config-lsp/handlers/wireguard/lsp" "config-lsp/root-handler/shared" utils "config-lsp/root-handler/utils" @@ -32,7 +33,7 @@ func TextDocumentCodeAction(context *glsp.Context, params *protocol.CodeActionPa case shared.LanguageHosts: return hosts.TextDocumentCodeAction(context, params) case shared.LanguageSSHDConfig: - return nil, nil + return sshdconfig.TextDocumentCodeAction(context, params) case shared.LanguageSSHConfig: return sshconfig.TextDocumentCodeAction(context, params) case shared.LanguageWireguard: From 98f76fd839b6418ff73ab8952deaf18859abc049 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Mon, 17 Feb 2025 22:40:47 +0100 Subject: [PATCH 08/31] fix(server): Improve structure analyzer for ssh_config Signed-off-by: Myzel394 --- .../handlers/ssh_config/analyzer/options.go | 50 +++++++++---------- server/handlers/ssh_config/analyzer/quotes.go | 12 ----- .../ssh_config/analyzer/quotes_test.go | 20 ++++++-- 3 files changed, 40 insertions(+), 42 deletions(-) diff --git a/server/handlers/ssh_config/analyzer/options.go b/server/handlers/ssh_config/analyzer/options.go index 0356cf5..90dee52 100644 --- a/server/handlers/ssh_config/analyzer/options.go +++ b/server/handlers/ssh_config/analyzer/options.go @@ -39,9 +39,26 @@ func checkOption( option *ast.SSHOption, block ast.SSHBlock, ) { + if option.Key == nil { + return + } + + ///// General checks checkIsUsingDoubleQuotes(ctx, option.Key.Value, option.Key.LocationRange) checkQuotesAreClosed(ctx, option.Key.Value, option.Key.LocationRange) + if option.Separator == nil || option.Separator.Value.Value == "" { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Range: option.Key.LocationRange.ToLSPRange(), + Message: fmt.Sprintf("There should be a separator between an option and its value"), + Severity: &common.SeverityError, + }) + } else { + checkIsUsingDoubleQuotes(ctx, option.Separator.Value, option.Separator.LocationRange) + checkQuotesAreClosed(ctx, option.Separator.Value, option.Separator.LocationRange) + } + + ///// Check if the key is valid docOption, optionFound := fields.Options[option.Key.Key] if !optionFound { @@ -58,22 +75,13 @@ func checkOption( Option: option, Block: block, } - - return } - } - if block != nil && block.GetBlockType() == ast.SSHBlockTypeHost && utils.KeyExists(fields.HostDisallowedOptions, option.Key.Key) { - ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ - Range: option.Key.LocationRange.ToLSPRange(), - Message: fmt.Sprintf("Option '%s' is not allowed in Host blocks", option.Key.Key), - Severity: &common.SeverityError, - }) - } - - // Check for values that are not allowed in Host blocks - if block != nil && block.GetBlockType() == ast.SSHBlockTypeHost { - if utils.KeyExists(fields.HostDisallowedOptions, option.Key.Key) { + // Since we don't know the option, we can't verify the value + return + } else { + // Check for values that are not allowed in Host blocks + if block != nil && block.GetBlockType() == ast.SSHBlockTypeHost && utils.KeyExists(fields.HostDisallowedOptions, option.Key.Key) { ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ Range: option.Key.LocationRange.ToLSPRange(), Message: fmt.Sprintf("Option '%s' is not allowed in Host blocks", option.Key.Key), @@ -82,7 +90,8 @@ func checkOption( } } - if option.OptionValue != nil && optionFound { + ///// Check if the value is valid + if option.OptionValue != nil { checkIsUsingDoubleQuotes(ctx, option.OptionValue.Value, option.OptionValue.LocationRange) checkQuotesAreClosed(ctx, option.OptionValue.Value, option.OptionValue.LocationRange) @@ -98,17 +107,6 @@ func checkOption( }) } } - - if option.Separator == nil || option.Separator.Value.Value == "" { - ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ - Range: option.Key.LocationRange.ToLSPRange(), - Message: fmt.Sprintf("There should be a separator between an option and its value"), - Severity: &common.SeverityError, - }) - } else { - checkIsUsingDoubleQuotes(ctx, option.Separator.Value, option.Separator.LocationRange) - checkQuotesAreClosed(ctx, option.Separator.Value, option.Separator.LocationRange) - } } func checkBlock( diff --git a/server/handlers/ssh_config/analyzer/quotes.go b/server/handlers/ssh_config/analyzer/quotes.go index 50a3399..3835519 100644 --- a/server/handlers/ssh_config/analyzer/quotes.go +++ b/server/handlers/ssh_config/analyzer/quotes.go @@ -9,18 +9,6 @@ import ( protocol "github.com/tliron/glsp/protocol_3_16" ) -func analyzeQuotesAreValid( - ctx *analyzerContext, -) { - for _, info := range ctx.document.Config.GetAllOptions() { - checkIsUsingDoubleQuotes(ctx, info.Option.Key.Value, info.Option.Key.LocationRange) - checkIsUsingDoubleQuotes(ctx, info.Option.OptionValue.Value, info.Option.OptionValue.LocationRange) - - checkQuotesAreClosed(ctx, info.Option.Key.Value, info.Option.Key.LocationRange) - checkQuotesAreClosed(ctx, info.Option.OptionValue.Value, info.Option.OptionValue.LocationRange) - } -} - func checkIsUsingDoubleQuotes( ctx *analyzerContext, value commonparser.ParsedString, diff --git a/server/handlers/ssh_config/analyzer/quotes_test.go b/server/handlers/ssh_config/analyzer/quotes_test.go index c52a218..e5e0041 100644 --- a/server/handlers/ssh_config/analyzer/quotes_test.go +++ b/server/handlers/ssh_config/analyzer/quotes_test.go @@ -7,6 +7,18 @@ import ( protocol "github.com/tliron/glsp/protocol_3_16" ) +func testQuotes( + ctx *analyzerContext, +) { + for _, info := range ctx.document.Config.GetAllOptions() { + checkIsUsingDoubleQuotes(ctx, info.Option.Key.Value, info.Option.Key.LocationRange) + checkIsUsingDoubleQuotes(ctx, info.Option.OptionValue.Value, info.Option.OptionValue.LocationRange) + + checkQuotesAreClosed(ctx, info.Option.Key.Value, info.Option.Key.LocationRange) + checkQuotesAreClosed(ctx, info.Option.OptionValue.Value, info.Option.OptionValue.LocationRange) + } +} + func TestSimpleInvalidQuotesExample( t *testing.T, ) { @@ -17,7 +29,7 @@ PermitRootLogin 'yes' document: d, diagnostics: make([]protocol.Diagnostic, 0), } - analyzeQuotesAreValid(ctx) + testQuotes(ctx) if !(len(ctx.diagnostics) == 1) { t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics)) @@ -34,7 +46,7 @@ func TestSingleQuotesKeyAndOptionExample( document: d, diagnostics: make([]protocol.Diagnostic, 0), } - analyzeQuotesAreValid(ctx) + testQuotes(ctx) if !(len(ctx.diagnostics) == 2) { t.Errorf("Expected 2 ctx.diagnostics, got %v", len(ctx.diagnostics)) @@ -51,7 +63,7 @@ PermitRootLogin "yes document: d, diagnostics: make([]protocol.Diagnostic, 0), } - analyzeQuotesAreValid(ctx) + testQuotes(ctx) if !(len(ctx.diagnostics) == 1) { t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics)) @@ -68,7 +80,7 @@ func TestIncompleteQuotesExample( document: d, diagnostics: make([]protocol.Diagnostic, 0), } - analyzeQuotesAreValid(ctx) + testQuotes(ctx) if !(len(ctx.diagnostics) == 1) { t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics)) From bf05d07fc9f38a99109db3d699a5b5502d9fb3f4 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Mon, 17 Feb 2025 22:41:06 +0100 Subject: [PATCH 09/31] fix(server): Improve structure analyzer for sshd_config Signed-off-by: Myzel394 --- .../handlers/sshd_config/analyzer/options.go | 48 +++++++++++-------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/server/handlers/sshd_config/analyzer/options.go b/server/handlers/sshd_config/analyzer/options.go index b15b16d..c18ca4d 100644 --- a/server/handlers/sshd_config/analyzer/options.go +++ b/server/handlers/sshd_config/analyzer/options.go @@ -6,6 +6,7 @@ import ( "config-lsp/handlers/sshd_config/ast" "config-lsp/handlers/sshd_config/diagnostics" "config-lsp/handlers/sshd_config/fields" + "config-lsp/utils" "fmt" protocol "github.com/tliron/glsp/protocol_3_16" @@ -38,13 +39,25 @@ func checkOption( return } + ///// General checks checkIsUsingDoubleQuotes(ctx, option.Key.Value, option.Key.LocationRange) checkQuotesAreClosed(ctx, option.Key.Value, option.Key.LocationRange) - key := option.Key.Key - docOption, found := fields.Options[key] + if option.Separator == nil || option.Separator.Value.Value == "" { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Range: option.Key.LocationRange.ToLSPRange(), + Message: fmt.Sprintf("There should be a separator between an option and its value"), + Severity: &common.SeverityError, + }) + } else { + checkIsUsingDoubleQuotes(ctx, option.Separator.Value, option.Separator.LocationRange) + checkQuotesAreClosed(ctx, option.Separator.Value, option.Separator.LocationRange) + } - if !found { + ///// Check if the key is valid + docOption, optionFound := fields.Options[option.Key.Key] + + if !optionFound { ctx.diagnostics = append(ctx.diagnostics, diagnostics.GenerateUnknownOption( option.Key.ToLSPRange(), option.Key.Value.Value, @@ -54,17 +67,20 @@ func checkOption( MatchBlock: matchBlock, } + // Since we don't know the option, we can't verify the value return + } else { + // Check for values that are not allowed in Match blocks + if matchBlock != nil && !utils.KeyExists(fields.MatchAllowedOptions, option.Key.Key) { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Range: option.Key.ToLSPRange(), + Message: fmt.Sprintf("Option '%s' is not allowed in Match blocks", option.Key.Key), + Severity: &common.SeverityError, + }) + } } - 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), - Severity: &common.SeverityError, - }) - } - + ///// Check if the value is valid if option.OptionValue != nil { checkIsUsingDoubleQuotes(ctx, option.OptionValue.Value, option.OptionValue.LocationRange) checkQuotesAreClosed(ctx, option.OptionValue.Value, option.OptionValue.LocationRange) @@ -82,16 +98,6 @@ func checkOption( } } - if option.Separator == nil || option.Separator.Value.Value == "" { - ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ - Range: option.Key.LocationRange.ToLSPRange(), - Message: fmt.Sprintf("There should be a separator between an option and its value"), - Severity: &common.SeverityError, - }) - } else { - checkIsUsingDoubleQuotes(ctx, option.Separator.Value, option.Separator.LocationRange) - checkQuotesAreClosed(ctx, option.Separator.Value, option.Separator.LocationRange) - } } func checkMatchBlock( From ba056d6ae9d6c295a2dbb58b282ea9ec09fbcb2d Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 23 Feb 2025 18:44:06 +0100 Subject: [PATCH 10/31] refactor(server): Improve Wireguard config; Improve the parser Signed-off-by: Myzel394 --- server/handlers/wireguard/ast/parser.go | 212 ++++++++++++++++++ server/handlers/wireguard/ast/parser_test.go | 54 +++++ server/handlers/wireguard/ast/wireguard.go | 43 ++++ .../wireguard/ast/wireguard_fields.go | 39 ++++ server/handlers/wireguard/indexes/indexes.go | 1 + server/handlers/wireguard/shared.go | 1 + 6 files changed, 350 insertions(+) create mode 100644 server/handlers/wireguard/ast/parser.go create mode 100644 server/handlers/wireguard/ast/parser_test.go create mode 100644 server/handlers/wireguard/ast/wireguard.go create mode 100644 server/handlers/wireguard/ast/wireguard_fields.go create mode 100644 server/handlers/wireguard/indexes/indexes.go create mode 100644 server/handlers/wireguard/shared.go diff --git a/server/handlers/wireguard/ast/parser.go b/server/handlers/wireguard/ast/parser.go new file mode 100644 index 0000000..eb6c153 --- /dev/null +++ b/server/handlers/wireguard/ast/parser.go @@ -0,0 +1,212 @@ +package ast + +import ( + "config-lsp/common" + "config-lsp/utils" + "fmt" + "regexp" + "strings" +) + +func NewWGConfig() *WGConfig { + config := &WGConfig{} + config.Clear() + + return config +} + +func (c *WGConfig) Clear() { + c.Sections = make([]*WGSection, 0, 2) + c.CommentLines = make(map[uint32]struct{}) +} + +var commentPattern = regexp.MustCompile(`^\s*([;#])`) +var emptyPattern = regexp.MustCompile(`^\s*$`) +var headerPattern = regexp.MustCompile(`^\s*\[(\w+)]?`) +var linePattern = regexp.MustCompile(`^\s*(?P.+?)\s*(?P=)\s*(?P\S.*?)?\s*(?:[;#].*)?\s*$`) + +func (c *WGConfig) Parse(input string) []common.LSPError { + errors := make([]common.LSPError, 0) + lines := utils.SplitIntoLines(input) + + var currentSection *WGSection + + for rawLineNumber, line := range lines { + lineNumber := uint32(rawLineNumber) + + if emptyPattern.MatchString(line) { + continue + } + + if commentPattern.MatchString(line) { + c.CommentLines[lineNumber] = struct{}{} + + continue + } + + if headerPattern.MatchString(line) { + name := headerPattern.FindStringSubmatch(line)[1] + + currentSection = &WGSection{ + LocationRange: common.LocationRange{ + Start: common.Location{ + Line: lineNumber, + Character: 0, + }, + End: common.Location{ + Line: lineNumber, + Character: uint32(len(line)) + 1, + }, + }, + Header: WGHeader{ + LocationRange: common.LocationRange{ + Start: common.Location{ + Line: lineNumber, + Character: 0, + }, + End: common.Location{ + Line: lineNumber, + Character: uint32(len(line)) + 1, + }, + }, + Name: name, + }, + Properties: make(map[uint32]*WGProperty), + } + + c.Sections = append(c.Sections, currentSection) + + continue + } + + // Else property + if currentSection == nil { + // Root properties are not allowed + errors = append(errors, common.LSPError{ + Range: common.LocationRange{ + Start: common.Location{ + Line: lineNumber, + Character: 0, + }, + End: common.Location{ + Line: lineNumber, + Character: uint32(len(line)), + }, + }, + Err: fmt.Errorf("A header is missing before a property. This property has no header above it."), + }) + + continue + } + + if !strings.Contains(line, "=") { + // Incomplete property + indexes := utils.GetTrimIndex(line) + + currentSection.Properties[lineNumber] = &WGProperty{ + Key: WGPropertyKey{ + LocationRange: common.LocationRange{ + Start: common.Location{ + Line: lineNumber, + Character: uint32(indexes[0]), + }, + End: common.Location{ + Line: lineNumber, + Character: uint32(indexes[1]), + }, + }, + Name: line[indexes[0]:indexes[1]], + }, + } + } else { + // Fully written out property + + indexes := linePattern.FindStringSubmatchIndex(line) + + if indexes == nil || len(indexes) == 0 { + // Error + errors = append(errors, common.LSPError{ + Range: common.LocationRange{ + Start: common.Location{ + Line: lineNumber, + Character: 0, + }, + End: common.Location{ + Line: lineNumber, + Character: uint32(len(line)), + }, + }, + Err: fmt.Errorf("This property seems to be malformed"), + }) + + continue + } + + // Construct key + keyStart := uint32(indexes[2]) + keyEnd := uint32(indexes[3]) + key := WGPropertyKey{ + LocationRange: common.LocationRange{ + Start: common.Location{ + Line: lineNumber, + Character: keyStart, + }, + End: common.Location{ + Line: lineNumber, + Character: keyEnd, + }, + }, + Name: line[keyStart:keyEnd], + } + + // Construct separator + separatorStart := uint32(indexes[4]) + separatorEnd := uint32(indexes[5]) + separator := WGPropertySeparator{ + LocationRange: common.LocationRange{ + Start: common.Location{ + Line: lineNumber, + Character: separatorStart, + }, + End: common.Location{ + Line: lineNumber, + Character: separatorEnd, + }, + }, + } + + // Construct value + var value *WGPropertyValue + + if indexes[6] != -1 && indexes[7] != -1 { + // value exists + valueStart := uint32(indexes[6]) + valueEnd := uint32(indexes[7]) + + value = &WGPropertyValue{ + LocationRange: common.LocationRange{ + Start: common.Location{ + Line: lineNumber, + Character: valueStart, + }, + End: common.Location{ + Line: lineNumber, + Character: valueEnd, + }, + }, + Value: line[valueStart:valueEnd], + } + } + + // And lastly, add the property + currentSection.Properties[lineNumber] = &WGProperty{ + Key: key, + Separator: &separator, + Value: value, + } + } + + } + + return errors +} diff --git a/server/handlers/wireguard/ast/parser_test.go b/server/handlers/wireguard/ast/parser_test.go new file mode 100644 index 0000000..761baa8 --- /dev/null +++ b/server/handlers/wireguard/ast/parser_test.go @@ -0,0 +1,54 @@ +package ast + +import ( + "config-lsp/utils" + "testing" +) + +func TestExample1Works( + t *testing.T, +) { + sample := utils.Dedent(` +# A comment at the very top + + +[Interface] +PrivateKey = 1234567890 # Some comment +Address = 10.0.0.1 + + + +[Peer] +PublicKey = 1234567890 + +; I'm a comment +`) + + config := NewWGConfig() + + errors := config.Parse(sample) + + if len(errors) > 0 { + t.Fatalf("Parse: Expected no errors, but got %v", errors) + } + + if !(utils.KeyExists(config.CommentLines, 0) && utils.KeyExists(config.CommentLines, 12)) { + t.Errorf("Parse: Expected comments to be present on lines 0 and 12") + } + + if !(config.Sections[0].Header.Name == "Interface" && config.Sections[1].Header.Name == "Peer") { + t.Errorf("Parse: Expected sections to be present on lines 0, 1, and 2") + } + + if !(config.Sections[0].Properties[4].Key.Name == "PrivateKey" && config.Sections[0].Properties[4].Value.Value == "1234567890") { + t.Errorf("Parse: Expected property line 4 to be correct") + } + + if !(config.Sections[0].Properties[5].Key.Name == "Address" && config.Sections[0].Properties[5].Value.Value == "10.0.0.1") { + t.Errorf("Parse: Expected property line 5 to be correct") + } + + if !(config.Sections[1].Properties[10].Key.Name == "PublicKey" && config.Sections[1].Properties[10].Value.Value == "1234567890") { + t.Errorf("Parse: Expected property line 10 to be correct") + } +} diff --git a/server/handlers/wireguard/ast/wireguard.go b/server/handlers/wireguard/ast/wireguard.go new file mode 100644 index 0000000..c648b07 --- /dev/null +++ b/server/handlers/wireguard/ast/wireguard.go @@ -0,0 +1,43 @@ +package ast + +import ( + "config-lsp/common" +) + +type WGPropertyKey struct { + common.LocationRange + Name string +} + +type WGPropertyValue struct { + common.LocationRange + Value string +} + +type WGPropertySeparator struct { + common.LocationRange +} + +type WGProperty struct { + Key WGPropertyKey + Separator *WGPropertySeparator + Value *WGPropertyValue +} + +type WGHeader struct { + common.LocationRange + Name string +} + +type WGSection struct { + common.LocationRange + Header WGHeader + // map of: line number -> WGProperty + Properties map[uint32]*WGProperty +} + +type WGConfig struct { + Sections []*WGSection + // Used to identify where not to show diagnostics + CommentLines map[uint32]struct{} +} diff --git a/server/handlers/wireguard/ast/wireguard_fields.go b/server/handlers/wireguard/ast/wireguard_fields.go new file mode 100644 index 0000000..46460c3 --- /dev/null +++ b/server/handlers/wireguard/ast/wireguard_fields.go @@ -0,0 +1,39 @@ +package ast + +import ( + "slices" +) + +func (c *WGConfig) FindSectionByLine(line uint32) *WGSection { + index, found := slices.BinarySearchFunc( + c.Sections, + line, + func(current *WGSection, target uint32) int { + if target < current.Start.Line { + return 1 + } + + if target > current.Start.Line { + return -1 + } + + return 0 + }, + ) + + if !found { + return nil + } + + return c.Sections[index] +} + +func (c *WGConfig) FindPropertyByLine(line uint32) *WGProperty { + section := c.FindSectionByLine(line) + + if section == nil { + return nil + } + + return section.Properties[line] +} diff --git a/server/handlers/wireguard/indexes/indexes.go b/server/handlers/wireguard/indexes/indexes.go new file mode 100644 index 0000000..fce2081 --- /dev/null +++ b/server/handlers/wireguard/indexes/indexes.go @@ -0,0 +1 @@ +package indexes diff --git a/server/handlers/wireguard/shared.go b/server/handlers/wireguard/shared.go new file mode 100644 index 0000000..0b280a9 --- /dev/null +++ b/server/handlers/wireguard/shared.go @@ -0,0 +1 @@ +package wireguard From 36950fe27135c009d52a2fd2d1e388df3591c3e3 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 23 Feb 2025 21:48:08 +0100 Subject: [PATCH 11/31] refactor(server): Improve Wireguard AST, analyzer and indexes Signed-off-by: Myzel394 --- server/handlers/wireguard/ast/parser.go | 14 ++- server/handlers/wireguard/ast/wireguard.go | 1 + .../wireguard/ast/wireguard_fields.go | 10 ++ server/handlers/wireguard/lsp/shared.go | 8 -- .../wireguard/lsp/text-document-did-change.go | 14 +-- .../wireguard/lsp/text-document-did-close.go | 4 +- .../wireguard/lsp/text-document-did-open.go | 18 +++- server/handlers/wireguard/parser/wg-parser.go | 94 +++---------------- .../wireguard/parser/wg-parser_test.go | 10 +- .../handlers/wireguard/parser/wg-property.go | 82 ++-------------- .../handlers/wireguard/parser/wg-section.go | 61 ++---------- 11 files changed, 82 insertions(+), 234 deletions(-) delete mode 100644 server/handlers/wireguard/lsp/shared.go diff --git a/server/handlers/wireguard/ast/parser.go b/server/handlers/wireguard/ast/parser.go index eb6c153..8d2c066 100644 --- a/server/handlers/wireguard/ast/parser.go +++ b/server/handlers/wireguard/ast/parser.go @@ -22,7 +22,7 @@ func (c *WGConfig) Clear() { var commentPattern = regexp.MustCompile(`^\s*([;#])`) var emptyPattern = regexp.MustCompile(`^\s*$`) -var headerPattern = regexp.MustCompile(`^\s*\[(\w+)]?`) +var headerPattern = regexp.MustCompile(`^\s*\[(\w+)?]?`) var linePattern = regexp.MustCompile(`^\s*(?P.+?)\s*(?P=)\s*(?P\S.*?)?\s*(?:[;#].*)?\s*$`) func (c *WGConfig) Parse(input string) []common.LSPError { @@ -177,11 +177,13 @@ func (c *WGConfig) Parse(input string) []common.LSPError { // Construct value var value *WGPropertyValue + propertyEnd := uint32(len(line)) if indexes[6] != -1 && indexes[7] != -1 { // value exists valueStart := uint32(indexes[6]) valueEnd := uint32(indexes[7]) + propertyEnd = valueEnd value = &WGPropertyValue{ LocationRange: common.LocationRange{ @@ -200,6 +202,16 @@ func (c *WGConfig) Parse(input string) []common.LSPError { // And lastly, add the property currentSection.Properties[lineNumber] = &WGProperty{ + LocationRange: common.LocationRange{ + Start: common.Location{ + Line: lineNumber, + Character: keyStart, + }, + End: common.Location{ + Line: lineNumber, + Character: propertyEnd, + }, + }, Key: key, Separator: &separator, Value: value, diff --git a/server/handlers/wireguard/ast/wireguard.go b/server/handlers/wireguard/ast/wireguard.go index c648b07..9f1274c 100644 --- a/server/handlers/wireguard/ast/wireguard.go +++ b/server/handlers/wireguard/ast/wireguard.go @@ -19,6 +19,7 @@ type WGPropertySeparator struct { } type WGProperty struct { + common.LocationRange Key WGPropertyKey Separator *WGPropertySeparator Value *WGPropertyValue diff --git a/server/handlers/wireguard/ast/wireguard_fields.go b/server/handlers/wireguard/ast/wireguard_fields.go index 46460c3..3904220 100644 --- a/server/handlers/wireguard/ast/wireguard_fields.go +++ b/server/handlers/wireguard/ast/wireguard_fields.go @@ -37,3 +37,13 @@ func (c *WGConfig) FindPropertyByLine(line uint32) *WGProperty { return section.Properties[line] } + +func (s *WGSection) FindFirstPropertyByName(name string) *WGProperty { + for _, property := range s.Properties { + if property.Key.Name == name { + return property + } + } + + return nil +} diff --git a/server/handlers/wireguard/lsp/shared.go b/server/handlers/wireguard/lsp/shared.go deleted file mode 100644 index 377f850..0000000 --- a/server/handlers/wireguard/lsp/shared.go +++ /dev/null @@ -1,8 +0,0 @@ -package lsp - -import ( - "config-lsp/handlers/wireguard/parser" - protocol "github.com/tliron/glsp/protocol_3_16" -) - -var documentParserMap = map[protocol.DocumentUri]*parser.WireguardParser{} diff --git a/server/handlers/wireguard/lsp/text-document-did-change.go b/server/handlers/wireguard/lsp/text-document-did-change.go index c5ec400..1b58bbd 100644 --- a/server/handlers/wireguard/lsp/text-document-did-change.go +++ b/server/handlers/wireguard/lsp/text-document-did-change.go @@ -2,8 +2,10 @@ package lsp import ( "config-lsp/common" - "config-lsp/handlers/wireguard/handlers" + "config-lsp/handlers/wireguard" + "config-lsp/handlers/wireguard/analyzer" "config-lsp/utils" + "github.com/tliron/glsp" protocol "github.com/tliron/glsp/protocol_3_16" ) @@ -15,22 +17,22 @@ func TextDocumentDidChange( content := params.ContentChanges[0].(protocol.TextDocumentContentChangeEventWhole).Text common.ClearDiagnostics(context, params.TextDocument.URI) - p := documentParserMap[params.TextDocument.URI] - p.Clear() + document := wireguard.DocumentParserMap[params.TextDocument.URI] + document.Config.Clear() diagnostics := make([]protocol.Diagnostic, 0) - errors := p.ParseFromString(content) + errors := document.Config.Parse(content) if len(errors) > 0 { diagnostics = append(diagnostics, utils.Map( errors, - func(err common.ParseError) protocol.Diagnostic { + func(err common.LSPError) protocol.Diagnostic { return err.ToDiagnostic() }, )...) } - diagnostics = append(diagnostics, handlers.Analyze(*p)...) + diagnostics = append(diagnostics, analyzer.Analyze(document)...) if len(diagnostics) > 0 { common.SendDiagnostics(context, params.TextDocument.URI, diagnostics) diff --git a/server/handlers/wireguard/lsp/text-document-did-close.go b/server/handlers/wireguard/lsp/text-document-did-close.go index 3961658..318d28d 100644 --- a/server/handlers/wireguard/lsp/text-document-did-close.go +++ b/server/handlers/wireguard/lsp/text-document-did-close.go @@ -1,12 +1,14 @@ package lsp import ( + "config-lsp/handlers/wireguard" + "github.com/tliron/glsp" protocol "github.com/tliron/glsp/protocol_3_16" ) func TextDocumentDidClose(context *glsp.Context, params *protocol.DidCloseTextDocumentParams) error { - delete(documentParserMap, params.TextDocument.URI) + delete(wireguard.DocumentParserMap, params.TextDocument.URI) return nil } diff --git a/server/handlers/wireguard/lsp/text-document-did-open.go b/server/handlers/wireguard/lsp/text-document-did-open.go index fc9e0b2..49193b3 100644 --- a/server/handlers/wireguard/lsp/text-document-did-open.go +++ b/server/handlers/wireguard/lsp/text-document-did-open.go @@ -2,8 +2,12 @@ package lsp import ( "config-lsp/common" - "config-lsp/handlers/wireguard/parser" + "config-lsp/handlers/wireguard" + "config-lsp/handlers/wireguard/analyzer" + "config-lsp/handlers/wireguard/ast" + "config-lsp/handlers/wireguard/indexes" "config-lsp/utils" + "github.com/tliron/glsp" protocol "github.com/tliron/glsp/protocol_3_16" ) @@ -14,17 +18,21 @@ func TextDocumentDidOpen( ) error { common.ClearDiagnostics(context, params.TextDocument.URI) - p := parser.CreateWireguardParser() - documentParserMap[params.TextDocument.URI] = &p + document := &wireguard.WGDocument{ + Config: ast.NewWGConfig(), + Indexes: &indexes.WGIndexes{}, + } + wireguard.DocumentParserMap[params.TextDocument.URI] = document - errors := p.ParseFromString(params.TextDocument.Text) + errors := document.Config.Parse(params.TextDocument.Text) diagnostics := utils.Map( errors, - func(err common.ParseError) protocol.Diagnostic { + func(err common.LSPError) protocol.Diagnostic { return err.ToDiagnostic() }, ) + diagnostics = append(diagnostics, analyzer.Analyze(document)...) if len(diagnostics) > 0 { common.SendDiagnostics(context, params.TextDocument.URI, diagnostics) diff --git a/server/handlers/wireguard/parser/wg-parser.go b/server/handlers/wireguard/parser/wg-parser.go index a85d262..688dc5c 100644 --- a/server/handlers/wireguard/parser/wg-parser.go +++ b/server/handlers/wireguard/parser/wg-parser.go @@ -2,6 +2,7 @@ package parser import ( "config-lsp/common" + "config-lsp/handlers/wireguard/ast" "regexp" "slices" "strings" @@ -11,33 +12,7 @@ var commentPattern = regexp.MustCompile(`^\s*(;|#)`) var emptyLinePattern = regexp.MustCompile(`^\s*$`) var headerPattern = regexp.MustCompile(`^\s*\[`) -type CharacterLocation struct { - Start uint32 - End uint32 -} - -type wireguardLineIndex struct { - Type LineType - BelongingSection *WireguardSection -} - -type WireguardParser struct { - // : if nil then does not belong to a section - Sections []*WireguardSection - // Used to identify where not to show diagnostics - commentLines map[uint32]struct{} - - // Indexes - linesIndexes map[uint32]wireguardLineIndex -} - -func (p *WireguardParser) Clear() { - p.Sections = []*WireguardSection{} - p.commentLines = map[uint32]struct{}{} - p.linesIndexes = map[uint32]wireguardLineIndex{} -} - -func (p *WireguardParser) ParseFromString(input string) []common.ParseError { +func (p *ast.WGConfig) ParseFromString(input string) []common.ParseError { var errors []common.ParseError lines := strings.Split( input, @@ -116,7 +91,7 @@ func (p *WireguardParser) ParseFromString(input string) []common.ParseError { } } - var emptySection *WireguardSection + var emptySection *ast.WGSection if len(collectedProperties) > 0 { var endLine uint32 @@ -127,7 +102,7 @@ func (p *WireguardParser) ParseFromString(input string) []common.ParseError { endLine = p.Sections[len(p.Sections)-1].StartLine } - emptySection = &WireguardSection{ + emptySection = &ast.WGSection{ StartLine: 0, EndLine: endLine, Properties: collectedProperties, @@ -152,7 +127,7 @@ func (p *WireguardParser) ParseFromString(input string) []common.ParseError { // Add empty section if endLine != 0 { - emptySection = &WireguardSection{ + emptySection = &ast.WGSection{ StartLine: 0, EndLine: endLine, Properties: collectedProperties, @@ -203,7 +178,7 @@ func (p *WireguardParser) ParseFromString(input string) []common.ParseError { return errors } -func (p *WireguardParser) GetSectionByLine(line uint32) *WireguardSection { +func (p *ast.WGConfig) GetSectionByLine(line uint32) *ast.WGSection { for _, section := range p.Sections { if section.StartLine <= line && section.EndLine >= line { return section @@ -215,7 +190,7 @@ func (p *WireguardParser) GetSectionByLine(line uint32) *WireguardSection { // Search for a property by name // Returns (line number, property) -func (p *WireguardParser) FindFirstPropertyByName(name string) (*uint32, *WireguardProperty) { +func (p *ast.WGConfig) FindFirstPropertyByName(name string) (*uint32, *ast.WGProperty) { for _, section := range p.Sections { for lineNumber, property := range section.Properties { if property.Key.Name == name { @@ -227,9 +202,9 @@ func (p *WireguardParser) FindFirstPropertyByName(name string) (*uint32, *Wiregu return nil, nil } -func (p WireguardParser) GetInterfaceSection() (*WireguardSection, bool) { +func (p ast.WGConfig) GetInterfaceSection() (*ast.WGSection, bool) { for _, section := range p.Sections { - if section.Name != nil && *section.Name == "Interface" { + if section.Header != nil && *section.Header == "Interface" { return section, true } } @@ -237,7 +212,7 @@ func (p WireguardParser) GetInterfaceSection() (*WireguardSection, bool) { return nil, false } -func (p WireguardParser) GetTypeByLine(line uint32) LineType { +func (p ast.WGConfig) GetTypeByLine(line uint32) LineType { // Check if line is a comment if _, found := p.commentLines[line]; found { return LineTypeComment @@ -250,53 +225,8 @@ func (p WireguardParser) GetTypeByLine(line uint32) LineType { return LineTypeEmpty } -// Get the section that the line belongs to -// Example: -// [Interface] -// Address = 10.0.0.1 -// -// -// [Peer] -// -// This would return the section [Interface] -func (p *WireguardParser) GetBelongingSectionByLine(line uint32) *WireguardSection { - if info, found := p.linesIndexes[line]; found { - return info.BelongingSection - } - - return nil -} - -func (p *WireguardParser) GetPropertyByLine(line uint32) (*WireguardSection, *WireguardProperty) { - section := p.GetSectionByLine(line) - - if section == nil || section.Name == nil { - return nil, nil - } - - property, _ := section.GetPropertyByLine(line) - - if property == nil { - return nil, nil - } - - return section, property -} - -func (p *WireguardParser) GetSectionsByName(name string) []*WireguardSection { - var sections []*WireguardSection - - for _, section := range p.Sections { - if section.Name != nil && *section.Name == name { - sections = append(sections, section) - } - } - - return sections -} - -func CreateWireguardParser() WireguardParser { - parser := WireguardParser{} +func CreateWireguardParser() ast.WGConfig { + parser := ast.WGConfig{} parser.Clear() return parser diff --git a/server/handlers/wireguard/parser/wg-parser_test.go b/server/handlers/wireguard/parser/wg-parser_test.go index eeee672..c49fa54 100644 --- a/server/handlers/wireguard/parser/wg-parser_test.go +++ b/server/handlers/wireguard/parser/wg-parser_test.go @@ -35,7 +35,7 @@ PublicKey = 5555 t.Fatalf("parseFromString failed to collect comment lines %v", parser.commentLines) } - if !((len(parser.Sections) == 3) && (*parser.Sections[0].Name == "Interface") && (*parser.Sections[1].Name == "Peer") && (*parser.Sections[2].Name == "Peer")) { + if !((len(parser.Sections) == 3) && (*parser.Sections[0].Header == "Interface") && (*parser.Sections[1].Header == "Peer") && (*parser.Sections[2].Header == "Peer")) { t.Fatalf("parseFromString failed to collect sections %v", parser.Sections) } @@ -93,7 +93,7 @@ PublicKey = 1234567890 t.Fatalf("parseFromString failed with error %v", errors) } - if !((len(parser.Sections) == 2) && (*parser.Sections[0].Name == "Interface") && (*parser.Sections[1].Name == "Peer")) { + if !((len(parser.Sections) == 2) && (*parser.Sections[0].Header == "Interface") && (*parser.Sections[1].Header == "Peer")) { t.Fatalf("parseFromString failed to collect sections %v", parser.Sections) } @@ -120,7 +120,7 @@ PrivateKey = 1234567890 t.Fatalf("parseFromString failed with error %v", errors) } - if !((len(parser.Sections) == 2) && (*parser.Sections[0].Name == "Inteface") && (*parser.Sections[1].Name == "Peer")) { + if !((len(parser.Sections) == 2) && (*parser.Sections[0].Header == "Inteface") && (*parser.Sections[1].Header == "Peer")) { t.Fatalf("parseFromString failed to collect sections %v", parser.Sections) } @@ -168,7 +168,7 @@ PublicKey = 1234567890 t.Fatalf("parseFromString failed with error %v", errors) } - if !((len(parser.Sections) == 2) && (*parser.Sections[0].Name == "Inte") && (*parser.Sections[1].Name == "Peer")) { + if !((len(parser.Sections) == 2) && (*parser.Sections[0].Header == "Inte") && (*parser.Sections[1].Header == "Peer")) { t.Fatalf("parseFromString failed to collect sections: %v", parser.Sections) } @@ -202,7 +202,7 @@ PrivateKey = 1234567890 t.Fatalf("parseFromString failed with error: %v", errors) } - if !((len(parser.Sections) == 2) && (*parser.Sections[0].Name == "Inte") && (*parser.Sections[1].Name == "Peer")) { + if !((len(parser.Sections) == 2) && (*parser.Sections[0].Header == "Inte") && (*parser.Sections[1].Header == "Peer")) { t.Fatalf("parseFromString failed to collect sections: %v", parser.Sections) } diff --git a/server/handlers/wireguard/parser/wg-property.go b/server/handlers/wireguard/parser/wg-property.go index 5deae64..a96937b 100644 --- a/server/handlers/wireguard/parser/wg-property.go +++ b/server/handlers/wireguard/parser/wg-property.go @@ -2,6 +2,7 @@ package parser import ( docvalues "config-lsp/doc-values" + "config-lsp/handlers/wireguard/ast" "config-lsp/utils" "regexp" "strings" @@ -11,71 +12,8 @@ import ( var linePattern = regexp.MustCompile(`^\s*(?P.+?)\s*(?P=)\s*(?P\S.*?)?\s*(?:(?:;|#).*)?\s*$`) -type WireguardPropertyKey struct { - Location CharacterLocation - Name string -} - -type WireguardPropertyValue struct { - Location CharacterLocation - Value string -} - -type WireguardPropertySeparator struct { - Location CharacterLocation -} - -type WireguardProperty struct { - Key WireguardPropertyKey - Separator *WireguardPropertySeparator - Value *WireguardPropertyValue -} - -func (p WireguardProperty) String() string { - if p.Value == nil { - return p.Key.Name - } - - return p.Key.Name + "=" + p.Value.Value -} - -func (p WireguardProperty) GetLineRange(line uint32) protocol.Range { - return protocol.Range{ - Start: protocol.Position{ - Line: line, - Character: p.Key.Location.Start, - }, - End: protocol.Position{ - Line: line, - Character: p.Key.Location.End, - }, - } -} - -func (p WireguardProperty) GetInsertRange(line uint32) protocol.Range { - var insertPosition uint32 = p.Separator.Location.End - var length uint32 = 0 - - if p.Value != nil { - insertPosition = p.Value.Location.Start - 1 - // Length of the value; +1 because of the starting space - length = (p.Value.Location.End - p.Value.Location.Start) + 1 - } - - return protocol.Range{ - Start: protocol.Position{ - Line: line, - Character: insertPosition, - }, - End: protocol.Position{ - Line: line, - Character: insertPosition + length, - }, - } -} - // WireguardProperties []: -type WireguardProperties map[uint32]WireguardProperty +type WireguardProperties map[uint32]ast.WGProperty func (p *WireguardProperties) AddLine(lineNumber uint32, line string) error { property, err := CreateWireguardProperty(line) @@ -89,7 +27,7 @@ func (p *WireguardProperties) AddLine(lineNumber uint32, line string) error { return nil } -func CreateWireguardProperty(line string) (*WireguardProperty, error) { +func CreateWireguardProperty(line string) (*ast.WGProperty, error) { if !strings.Contains(line, "=") { indexes := utils.GetTrimIndex(line) @@ -98,8 +36,8 @@ func CreateWireguardProperty(line string) (*WireguardProperty, error) { return nil, &docvalues.MalformedLineError{} } - return &WireguardProperty{ - Key: WireguardPropertyKey{ + return &ast.WGProperty{ + Key: ast.WGPropertyKey{ Name: line[indexes[0]:indexes[1]], Location: CharacterLocation{ Start: uint32(indexes[0]), @@ -117,7 +55,7 @@ func CreateWireguardProperty(line string) (*WireguardProperty, error) { keyStart := uint32(indexes[2]) keyEnd := uint32(indexes[3]) - key := WireguardPropertyKey{ + key := ast.WGPropertyKey{ Location: CharacterLocation{ Start: keyStart, End: keyEnd, @@ -127,21 +65,21 @@ func CreateWireguardProperty(line string) (*WireguardProperty, error) { separatorStart := uint32(indexes[4]) separatorEnd := uint32(indexes[5]) - separator := WireguardPropertySeparator{ + separator := ast.WGPropertySeparator{ Location: CharacterLocation{ Start: separatorStart, End: separatorEnd, }, } - var value *WireguardPropertyValue + var value *ast.WGPropertyValue if indexes[6] != -1 && indexes[7] != -1 { // value exists valueStart := uint32(indexes[6]) valueEnd := uint32(indexes[7]) - value = &WireguardPropertyValue{ + value = &ast.WGPropertyValue{ Location: CharacterLocation{ Start: valueStart, End: valueEnd, @@ -150,7 +88,7 @@ func CreateWireguardProperty(line string) (*WireguardProperty, error) { } } - return &WireguardProperty{ + return &ast.WGProperty{ Key: key, Separator: &separator, Value: value, diff --git a/server/handlers/wireguard/parser/wg-section.go b/server/handlers/wireguard/parser/wg-section.go index 8e94a9e..8dc5097 100644 --- a/server/handlers/wireguard/parser/wg-section.go +++ b/server/handlers/wireguard/parser/wg-section.go @@ -1,10 +1,8 @@ package parser import ( - "fmt" + "config-lsp/handlers/wireguard/ast" "regexp" - - protocol "github.com/tliron/glsp/protocol_3_16" ) type PropertyNotFoundError struct{} @@ -19,52 +17,7 @@ func (e PropertyNotFullyTypedError) Error() string { return "Property not fully typed" } -type WireguardSection struct { - Name *string - StartLine uint32 - EndLine uint32 - Properties WireguardProperties -} - -func (s WireguardSection) String() string { - var name string - - if s.Name == nil { - name = "" - } else { - name = *s.Name - } - - return fmt.Sprintf("[%s]; %d-%d: %v", name, s.StartLine, s.EndLine, s.Properties) -} - -func (s WireguardSection) GetHeaderLineRange() protocol.Range { - return protocol.Range{ - Start: protocol.Position{ - Line: s.StartLine, - Character: 0, - }, - End: protocol.Position{ - Line: s.StartLine, - Character: 99999999, - }, - } -} - -func (s WireguardSection) GetRange() protocol.Range { - return protocol.Range{ - Start: protocol.Position{ - Line: s.StartLine, - Character: 0, - }, - End: protocol.Position{ - Line: s.EndLine, - Character: 99999999, - }, - } -} - -func (s WireguardSection) FetchFirstProperty(name string) (*uint32, *WireguardProperty) { +func (s ast.WGSection) FetchFirstProperty(name string) (*uint32, *ast.WGProperty) { for line, property := range s.Properties { if property.Key.Name == name { return &line, &property @@ -74,13 +27,13 @@ func (s WireguardSection) FetchFirstProperty(name string) (*uint32, *WireguardPr return nil, nil } -func (s WireguardSection) ExistsProperty(name string) bool { +func (s ast.WGSection) ExistsProperty(name string) bool { _, property := s.FetchFirstProperty(name) return property != nil } -func (s WireguardSection) GetPropertyByLine(lineNumber uint32) (*WireguardProperty, error) { +func (s ast.WGSection) GetPropertyByLine(lineNumber uint32) (*ast.WGProperty, error) { property, found := s.Properties[lineNumber] if !found { @@ -99,7 +52,7 @@ func CreateWireguardSection( endLine uint32, headerLine string, props WireguardProperties, -) WireguardSection { +) ast.WGSection { match := validHeaderPattern.FindStringSubmatch(headerLine) var header string @@ -111,8 +64,8 @@ func CreateWireguardSection( header = match[1] } - return WireguardSection{ - Name: &header, + return ast.WGSection{ + Header: &header, StartLine: startLine, EndLine: endLine, Properties: props, From eb076dbf53ba98e42baa15ead3d759db60b5937d Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 23 Feb 2025 21:51:45 +0100 Subject: [PATCH 12/31] refactor(server): Improve Wireguard analyzer Signed-off-by: Myzel394 --- .../handlers/wireguard/analyzer/analyzer.go | 45 ++++++ .../wireguard/analyzer/analyzer_test.go | 57 ++++++++ .../handlers/wireguard/analyzer/property.go | 136 ++++++++++++++++++ server/handlers/wireguard/analyzer/section.go | 18 +++ .../handlers/wireguard/analyzer/structure.go | 83 +++++++++++ 5 files changed, 339 insertions(+) create mode 100644 server/handlers/wireguard/analyzer/analyzer.go create mode 100644 server/handlers/wireguard/analyzer/analyzer_test.go create mode 100644 server/handlers/wireguard/analyzer/property.go create mode 100644 server/handlers/wireguard/analyzer/section.go create mode 100644 server/handlers/wireguard/analyzer/structure.go diff --git a/server/handlers/wireguard/analyzer/analyzer.go b/server/handlers/wireguard/analyzer/analyzer.go new file mode 100644 index 0000000..fad1cf4 --- /dev/null +++ b/server/handlers/wireguard/analyzer/analyzer.go @@ -0,0 +1,45 @@ +package analyzer + +import ( + "config-lsp/common" + "config-lsp/handlers/wireguard" + "config-lsp/handlers/wireguard/indexes" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +type analyzerContext struct { + document *wireguard.WGDocument + diagnostics []protocol.Diagnostic +} + +func Analyze( + d *wireguard.WGDocument, +) []protocol.Diagnostic { + ctx := &analyzerContext{ + document: d, + diagnostics: make([]protocol.Diagnostic, 0), + } + + analyzeStructureIsValid(ctx) + + if len(ctx.diagnostics) > 0 { + return ctx.diagnostics + } + + i, indexErrors := indexes.CreateIndexes(d.Config) + + if len(indexErrors) > 0 { + return common.ErrsToDiagnostics(indexErrors) + } + + d.Indexes = i + + analyzeInterfaceSection(ctx) + analyzeDNSPropertyContainsFallback(ctx) + analyzeKeepAlivePropertyIsSet(ctx) + analyzeSymmetricPropertiesSet(ctx) + analyzeDuplicateAllowedIPs(ctx) + + return ctx.diagnostics +} diff --git a/server/handlers/wireguard/analyzer/analyzer_test.go b/server/handlers/wireguard/analyzer/analyzer_test.go new file mode 100644 index 0000000..50ac57c --- /dev/null +++ b/server/handlers/wireguard/analyzer/analyzer_test.go @@ -0,0 +1,57 @@ +package analyzer + +import ( + "config-lsp/handlers/wireguard/parser" + "config-lsp/utils" + "testing" +) + +func TestMultipleIntefaces(t *testing.T) { + content := utils.Dedent(` +[Interface] +PrivateKey = abc + +[Interface] +PrivateKey = def +`) + p := parser.CreateWireguardParser() + p.ParseFromString(content) + + diagnostics := Analyze(p) + + if len(diagnostics) == 0 { + t.Errorf("Expected diagnostic errors, got %d", len(diagnostics)) + } +} + +func TestInvalidValue(t *testing.T) { + content := utils.Dedent(` +[Interface] +DNS = nope +`) + p := parser.CreateWireguardParser() + p.ParseFromString(content) + + diagnostics := Analyze(p) + + if len(diagnostics) == 0 { + t.Errorf("Expected diagnostic errors, got %d", len(diagnostics)) + } +} + +func TestDuplicateProperties(t *testing.T) { + content := utils.Dedent(` +[Interface] +PrivateKey = abc +DNS = 1.1.1.1 +PrivateKey = def +`) + p := parser.CreateWireguardParser() + p.ParseFromString(content) + + diagnostics := Analyze(p) + + if len(diagnostics) == 0 { + t.Errorf("Expected diagnostic errors, got %d", len(diagnostics)) + } +} diff --git a/server/handlers/wireguard/analyzer/property.go b/server/handlers/wireguard/analyzer/property.go new file mode 100644 index 0000000..2a80141 --- /dev/null +++ b/server/handlers/wireguard/analyzer/property.go @@ -0,0 +1,136 @@ +package analyzer + +import ( + "config-lsp/common" + "config-lsp/utils" + "context" + "fmt" + "net/netip" + "strings" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func analyzeDNSPropertyContainsFallback( + ctx *analyzerContext, +) { + sections, found := ctx.document.Indexes.SectionsByName["Interface"] + + if !found { + return + } + + interfaceSection := sections[0] + + property := interfaceSection.FindFirstPropertyByName("DNS") + + if property == nil { + return + } + + dnsAmount := len(strings.Split(property.Value.Value, ",")) + + if dnsAmount == 1 { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Message: "There is only one DNS server specified. It is recommended to set up fallback DNS servers", + Severity: &common.SeverityHint, + Range: property.Value.ToLSPRange(), + }) + } +} + +func analyzeKeepAlivePropertyIsSet( + ctx *analyzerContext, +) { + for _, section := range ctx.document.Indexes.SectionsByName["Peer"] { + // If an endpoint is set, then we should only check for the keepalive property + if section.FindFirstPropertyByName("Endpoint") != nil && section.FindFirstPropertyByName("PersistentKeepalive") == nil { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Message: "PersistentKeepalive is not set. It is recommended to set this property, as it helps to maintain the connection when users are behind NAT", + Severity: &common.SeverityHint, + Range: section.Header.ToLSPRange(), + }) + } + } +} + +func analyzeSymmetricPropertiesSet( + ctx *analyzerContext, +) { + for _, section := range ctx.document.Indexes.SectionsByName["Interface"] { + preUpProperty := section.FindFirstPropertyByName("PreUp") + preDownProperty := section.FindFirstPropertyByName("PreDown") + + postUpProperty := section.FindFirstPropertyByName("PostUp") + postDownProperty := section.FindFirstPropertyByName("PostDown") + + if preUpProperty != nil && preDownProperty == nil { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Message: "PreUp is set, but PreDown is not. It is recommended to set both properties symmetrically", + Range: preUpProperty.ToLSPRange(), + Severity: &common.SeverityHint, + }) + } else if preUpProperty == nil && preDownProperty != nil { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Message: "PreDown is set, but PreUp is not. It is recommended to set both properties symmetrically", + Range: preDownProperty.ToLSPRange(), + Severity: &common.SeverityHint, + }) + } + + if postUpProperty != nil && postDownProperty == nil { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Message: "PostUp is set, but PostDown is not. It is recommended to set both properties symmetrically", + Range: postUpProperty.ToLSPRange(), + Severity: &common.SeverityHint, + }) + } else if postUpProperty == nil && postDownProperty != nil { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Message: "PostDown is set, but PostUp is not. It is recommended to set both properties symmetrically", + Range: postDownProperty.ToLSPRange(), + Severity: &common.SeverityHint, + }) + } + } +} + +// Strategy +// Simply compare the host bits of the IP addresses. +// Use a binary tree to store the host bits. +func analyzeDuplicateAllowedIPs( + ctx *analyzerContext, +) { + ipHostSet := utils.CreateIPv4HostSet() + + for _, section := range ctx.document.Indexes.SectionsByName["Peer"] { + property := section.FindFirstPropertyByName("AllowedIPs") + + if property == nil { + continue + } + + ipAddress, err := netip.ParsePrefix(property.Value.Value) + + if err != nil { + // This should not happen... + continue + } + + if ipContext, _ := ipHostSet.ContainsIP(ipAddress); ipContext != nil { + definedLine := (*ipContext).Value("line").(uint32) + + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Message: fmt.Sprintf("This IP range is already covered on line %d", definedLine+1), + Severity: &common.SeverityError, + Range: property.ToLSPRange(), + }) + } else { + ipContext := context.WithValue(context.Background(), "line", property.Start.Line) + + ipHostSet.AddIP( + ipAddress, + ipContext, + ) + } + } +} diff --git a/server/handlers/wireguard/analyzer/section.go b/server/handlers/wireguard/analyzer/section.go new file mode 100644 index 0000000..2587183 --- /dev/null +++ b/server/handlers/wireguard/analyzer/section.go @@ -0,0 +1,18 @@ +package analyzer + +import ( + "config-lsp/common" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func analyzeInterfaceSection(ctx *analyzerContext) { + sections := ctx.document.Indexes.SectionsByName["Interface"] + if len(sections) > 1 { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Message: "Only one [Interface] section is allowed", + Severity: &common.SeverityError, + Range: sections[1].Header.ToLSPRange(), + }) + } +} diff --git a/server/handlers/wireguard/analyzer/structure.go b/server/handlers/wireguard/analyzer/structure.go new file mode 100644 index 0000000..58a9c9e --- /dev/null +++ b/server/handlers/wireguard/analyzer/structure.go @@ -0,0 +1,83 @@ +package analyzer + +import ( + "config-lsp/common" + "config-lsp/handlers/wireguard/ast" + "config-lsp/handlers/wireguard/fields" + "config-lsp/utils" + "fmt" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func analyzeStructureIsValid(ctx *analyzerContext) { + for _, section := range ctx.document.Config.Sections { + // Whether to check if the property is allowed in the section + checkAllowedProperty := true + + if section.Header.Name == "" { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Message: "This section is missing a name", + Range: section.Header.ToLSPRange(), + Severity: &common.SeverityError, + }) + } else if !utils.KeyExists(fields.OptionsHeaderMap, section.Header.Name) { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Message: fmt.Sprintf("Unknown section '%s'. It must be one of: [Interface], [Peer]", section.Header.Name), + Range: section.Header.ToLSPRange(), + Severity: &common.SeverityError, + }) + // Do not check as the section is unknown + checkAllowedProperty = false + } + + if len(section.Properties) == 0 { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Message: "This section is empty", + Range: section.Header.ToLSPRange(), + Severity: &common.SeverityInformation, + Tags: []protocol.DiagnosticTag{ + protocol.DiagnosticTagUnnecessary, + }, + }) + } else { + existingProperties := make(map[string]*ast.WGProperty) + + for _, property := range section.Properties { + 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.Value.ToLSPRange(), + Severity: &common.SeverityError, + }) + } + + if checkAllowedProperty { + options := fields.OptionsHeaderMap[section.Header.Name] + + if !utils.KeyExists(options, property.Key.Name) { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Message: fmt.Sprintf("Unknown property '%s'", property.Key.Name), + Range: property.Key.ToLSPRange(), + Severity: &common.SeverityError, + }) + } else if existingProperty, found := existingProperties[property.Key.Name]; 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(), + }) + } + } + } + } + } +} From 020cc8ad6760a4b9ad8f2e5863554cfa781a3998 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 23 Feb 2025 21:54:52 +0100 Subject: [PATCH 13/31] refactor(server): Improve Wireguard indexes Signed-off-by: Myzel394 --- server/handlers/wireguard/indexes/indexes.go | 7 ++++++ .../wireguard/indexes/indexes_handlers.go | 22 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 server/handlers/wireguard/indexes/indexes_handlers.go diff --git a/server/handlers/wireguard/indexes/indexes.go b/server/handlers/wireguard/indexes/indexes.go index fce2081..3c58f77 100644 --- a/server/handlers/wireguard/indexes/indexes.go +++ b/server/handlers/wireguard/indexes/indexes.go @@ -1 +1,8 @@ package indexes + +import "config-lsp/handlers/wireguard/ast" + +type WGIndexes struct { + // map of: section name -> WGSection + SectionsByName map[string][]*ast.WGSection +} diff --git a/server/handlers/wireguard/indexes/indexes_handlers.go b/server/handlers/wireguard/indexes/indexes_handlers.go new file mode 100644 index 0000000..c332e70 --- /dev/null +++ b/server/handlers/wireguard/indexes/indexes_handlers.go @@ -0,0 +1,22 @@ +package indexes + +import ( + "config-lsp/common" + "config-lsp/handlers/wireguard/ast" +) + +func CreateIndexes(config *ast.WGConfig) (*WGIndexes, []common.LSPError) { + errs := make([]common.LSPError, 0) + indexes := &WGIndexes{ + SectionsByName: make(map[string][]*ast.WGSection), + } + + for _, section := range config.Sections { + indexes.SectionsByName[section.Header.Name] = append( + indexes.SectionsByName[section.Header.Name], + section, + ) + } + + return indexes, errs +} From a0dca94b9d926c49225120abbd5ea7cf17bde52f Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 8 Mar 2025 14:44:59 +0100 Subject: [PATCH 14/31] refactor(server): Refactor Wireguard config; Improve completions + bunch of other stuff Signed-off-by: Myzel394 --- .../wireguard/analyzer/analyzer_test.go | 34 +- server/handlers/wireguard/ast/parser.go | 20 +- server/handlers/wireguard/ast/parser_test.go | 8 + server/handlers/wireguard/ast/wireguard.go | 1 + .../wireguard/ast/wireguard_fields.go | 6 +- .../handlers/wireguard/handlers/analyzer.go | 452 ------------------ .../wireguard/handlers/analyzer_test.go | 57 --- .../wireguard/handlers/code-actions.go | 12 +- .../wireguard/handlers/completions.go | 162 +------ .../wireguard/handlers/completions_body.go | 182 +++++++ .../wireguard/handlers/completions_header.go | 46 ++ .../wireguard/handlers/completions_test.go | 266 +++++++---- .../wireguard/handlers/fetch-code-actions.go | 10 +- server/handlers/wireguard/handlers/hover.go | 22 +- .../wireguard/lsp/text-document-completion.go | 56 +-- .../wireguard/parser/wg-parser-type_test.go | 127 ----- server/handlers/wireguard/parser/wg-parser.go | 258 ---------- .../wireguard/parser/wg-parser_test.go | 357 -------------- .../handlers/wireguard/parser/wg-property.go | 96 ---- .../handlers/wireguard/parser/wg-section.go | 73 --- server/handlers/wireguard/shared.go | 13 + 21 files changed, 506 insertions(+), 1752 deletions(-) delete mode 100644 server/handlers/wireguard/handlers/analyzer.go delete mode 100644 server/handlers/wireguard/handlers/analyzer_test.go create mode 100644 server/handlers/wireguard/handlers/completions_body.go create mode 100644 server/handlers/wireguard/handlers/completions_header.go delete mode 100644 server/handlers/wireguard/parser/wg-parser-type_test.go delete mode 100644 server/handlers/wireguard/parser/wg-parser.go delete mode 100644 server/handlers/wireguard/parser/wg-parser_test.go delete mode 100644 server/handlers/wireguard/parser/wg-property.go delete mode 100644 server/handlers/wireguard/parser/wg-section.go diff --git a/server/handlers/wireguard/analyzer/analyzer_test.go b/server/handlers/wireguard/analyzer/analyzer_test.go index 50ac57c..bf0aa05 100644 --- a/server/handlers/wireguard/analyzer/analyzer_test.go +++ b/server/handlers/wireguard/analyzer/analyzer_test.go @@ -1,7 +1,8 @@ package analyzer import ( - "config-lsp/handlers/wireguard/parser" + "config-lsp/handlers/wireguard" + "config-lsp/handlers/wireguard/ast" "config-lsp/utils" "testing" ) @@ -14,12 +15,14 @@ PrivateKey = abc [Interface] PrivateKey = def `) - p := parser.CreateWireguardParser() - p.ParseFromString(content) + d := &wireguard.WGDocument{ + Config: ast.NewWGConfig(), + } + d.Config.Parse(content) - diagnostics := Analyze(p) + diagnostics := Analyze(d) - if len(diagnostics) == 0 { + if !(len(diagnostics) > 0) { t.Errorf("Expected diagnostic errors, got %d", len(diagnostics)) } } @@ -29,12 +32,14 @@ func TestInvalidValue(t *testing.T) { [Interface] DNS = nope `) - p := parser.CreateWireguardParser() - p.ParseFromString(content) + d := &wireguard.WGDocument{ + Config: ast.NewWGConfig(), + } + d.Config.Parse(content) - diagnostics := Analyze(p) + diagnostics := Analyze(d) - if len(diagnostics) == 0 { + if !(len(diagnostics) > 0) { t.Errorf("Expected diagnostic errors, got %d", len(diagnostics)) } } @@ -46,12 +51,15 @@ PrivateKey = abc DNS = 1.1.1.1 PrivateKey = def `) - p := parser.CreateWireguardParser() - p.ParseFromString(content) - diagnostics := Analyze(p) + d := &wireguard.WGDocument{ + Config: ast.NewWGConfig(), + } + d.Config.Parse(content) - if len(diagnostics) == 0 { + diagnostics := Analyze(d) + + if !(len(diagnostics) > 0) { t.Errorf("Expected diagnostic errors, got %d", len(diagnostics)) } } diff --git a/server/handlers/wireguard/ast/parser.go b/server/handlers/wireguard/ast/parser.go index 8d2c066..441232c 100644 --- a/server/handlers/wireguard/ast/parser.go +++ b/server/handlers/wireguard/ast/parser.go @@ -35,12 +35,21 @@ func (c *WGConfig) Parse(input string) []common.LSPError { lineNumber := uint32(rawLineNumber) if emptyPattern.MatchString(line) { + // Set end of last section + if currentSection != nil { + currentSection.End.Line = lineNumber + currentSection.End.Character = 0 + } continue } if commentPattern.MatchString(line) { c.CommentLines[lineNumber] = struct{}{} - + // Set end of last section + if currentSection != nil { + currentSection.End.Line = lineNumber + currentSection.End.Character = uint32(len(line)) + } continue } @@ -80,6 +89,13 @@ func (c *WGConfig) Parse(input string) []common.LSPError { } // Else property + + // Set end of last section + if currentSection != nil { + currentSection.End.Line = lineNumber + currentSection.End.Character = uint32(len(line)) + } + if currentSection == nil { // Root properties are not allowed errors = append(errors, common.LSPError{ @@ -212,12 +228,12 @@ func (c *WGConfig) Parse(input string) []common.LSPError { Character: propertyEnd, }, }, + RawValue: line, Key: key, Separator: &separator, Value: value, } } - } return errors diff --git a/server/handlers/wireguard/ast/parser_test.go b/server/handlers/wireguard/ast/parser_test.go index 761baa8..2a0981b 100644 --- a/server/handlers/wireguard/ast/parser_test.go +++ b/server/handlers/wireguard/ast/parser_test.go @@ -36,6 +36,14 @@ PublicKey = 1234567890 t.Errorf("Parse: Expected comments to be present on lines 0 and 12") } + if !(config.Sections[0].Start.Line == 3 && config.Sections[0].End.Line == 8) { + t.Errorf("Parse: Expected section 0 to be present on lines 3 and 6, but it is: %v", config.Sections[0].End) + } + + if !(config.Sections[0].Start.Character == 0 && config.Sections[0].End.Character == 0) { + t.Errorf("Parse: Expected section 0 to be present on characters 0 and 0, but it is: %v", config.Sections[0].End) + } + if !(config.Sections[0].Header.Name == "Interface" && config.Sections[1].Header.Name == "Peer") { t.Errorf("Parse: Expected sections to be present on lines 0, 1, and 2") } diff --git a/server/handlers/wireguard/ast/wireguard.go b/server/handlers/wireguard/ast/wireguard.go index 9f1274c..ed29233 100644 --- a/server/handlers/wireguard/ast/wireguard.go +++ b/server/handlers/wireguard/ast/wireguard.go @@ -20,6 +20,7 @@ type WGPropertySeparator struct { type WGProperty struct { common.LocationRange + RawValue string Key WGPropertyKey Separator *WGPropertySeparator Value *WGPropertyValue diff --git a/server/handlers/wireguard/ast/wireguard_fields.go b/server/handlers/wireguard/ast/wireguard_fields.go index 3904220..68d9694 100644 --- a/server/handlers/wireguard/ast/wireguard_fields.go +++ b/server/handlers/wireguard/ast/wireguard_fields.go @@ -10,11 +10,11 @@ func (c *WGConfig) FindSectionByLine(line uint32) *WGSection { line, func(current *WGSection, target uint32) int { if target < current.Start.Line { - return 1 + return -1 } - if target > current.Start.Line { - return -1 + if target > current.End.Line { + return 1 } return 0 diff --git a/server/handlers/wireguard/handlers/analyzer.go b/server/handlers/wireguard/handlers/analyzer.go deleted file mode 100644 index 79e2734..0000000 --- a/server/handlers/wireguard/handlers/analyzer.go +++ /dev/null @@ -1,452 +0,0 @@ -package handlers - -import ( - docvalues "config-lsp/doc-values" - "config-lsp/handlers/wireguard/fields" - "config-lsp/handlers/wireguard/parser" - "config-lsp/utils" - "context" - "fmt" - "net/netip" - "slices" - "strings" - - protocol "github.com/tliron/glsp/protocol_3_16" -) - -func Analyze( - p parser.WireguardParser, -) []protocol.Diagnostic { - sectionsErrors := analyzeSections(p.Sections) - sectionsErrors = append(sectionsErrors, analyzeOnlyOneInterfaceSectionSpecified(p)...) - - if len(sectionsErrors) > 0 { - return sectionsErrors - } - - validCheckErrors := checkIfValuesAreValid(p.Sections) - - if len(validCheckErrors) > 0 { - return validCheckErrors - } - - diagnostics := make([]protocol.Diagnostic, 0) - diagnostics = append(diagnostics, analyzeParserForDuplicateProperties(p)...) - diagnostics = append(diagnostics, analyzeDNSContainsFallback(p)...) - diagnostics = append(diagnostics, analyzeKeepAliveIsSet(p)...) - diagnostics = append(diagnostics, analyzeSymmetricPropertiesExist(p)...) - diagnostics = append(diagnostics, analyzeDuplicateAllowedIPs(p)...) - - return diagnostics -} - -func analyzeSections( - sections []*parser.WireguardSection, -) []protocol.Diagnostic { - var diagnostics []protocol.Diagnostic - - for _, section := range sections { - sectionDiagnostics := analyzeSection(*section) - - if len(sectionDiagnostics) > 0 { - diagnostics = append(diagnostics, sectionDiagnostics...) - } - } - - if len(diagnostics) > 0 { - return diagnostics - } - - return diagnostics -} - -func analyzeOnlyOneInterfaceSectionSpecified( - p parser.WireguardParser, -) []protocol.Diagnostic { - var diagnostics []protocol.Diagnostic - alreadyFound := false - - for _, section := range p.GetSectionsByName("Interface") { - if alreadyFound { - severity := protocol.DiagnosticSeverityError - diagnostics = append(diagnostics, protocol.Diagnostic{ - Message: "Only one [Interface] section is allowed", - Severity: &severity, - Range: section.GetHeaderLineRange(), - }) - } - - alreadyFound = true - } - - return diagnostics -} - -func analyzeDNSContainsFallback( - p parser.WireguardParser, -) []protocol.Diagnostic { - lineNumber, property := p.FindFirstPropertyByName("DNS") - - if property == nil { - return []protocol.Diagnostic{} - } - - dnsAmount := len(strings.Split(property.Value.Value, ",")) - - if dnsAmount == 1 { - severity := protocol.DiagnosticSeverityHint - - return []protocol.Diagnostic{ - { - Message: "There is only one DNS server specified. It is recommended to set up fallback DNS servers", - Severity: &severity, - Range: protocol.Range{ - Start: protocol.Position{ - Line: *lineNumber, - Character: property.Value.Location.Start, - }, - End: protocol.Position{ - Line: *lineNumber, - Character: property.Value.Location.End, - }, - }, - }, - } - } - - return []protocol.Diagnostic{} -} - -func analyzeKeepAliveIsSet( - p parser.WireguardParser, -) []protocol.Diagnostic { - var diagnostics []protocol.Diagnostic - - for _, section := range p.GetSectionsByName("Peer") { - // If an endpoint is set, then we should only check for the keepalive property - if section.ExistsProperty("Endpoint") && !section.ExistsProperty("PersistentKeepalive") { - severity := protocol.DiagnosticSeverityHint - diagnostics = append(diagnostics, protocol.Diagnostic{ - Message: "PersistentKeepalive is not set. It is recommended to set this property, as it helps to maintain the connection when users are behind NAT", - Severity: &severity, - Range: section.GetRange(), - }) - } - } - - return diagnostics -} - -// Check if the values are valid. -// Assumes that sections have been analyzed already. -func checkIfValuesAreValid( - sections []*parser.WireguardSection, -) []protocol.Diagnostic { - var diagnostics []protocol.Diagnostic - - for _, section := range sections { - for lineNumber, property := range section.Properties { - diagnostics = append( - diagnostics, - analyzeProperty(property, section, lineNumber)..., - ) - } - } - - return diagnostics -} - -func analyzeSection( - s parser.WireguardSection, -) []protocol.Diagnostic { - var diagnostics []protocol.Diagnostic - - if s.Name == nil { - // No section name - severity := protocol.DiagnosticSeverityError - diagnostics = append(diagnostics, protocol.Diagnostic{ - Message: "This section is missing a name", - Severity: &severity, - Range: s.GetRange(), - }) - return diagnostics - } - - if _, found := fields.OptionsHeaderMap[*s.Name]; !found { - // Unknown section - severity := protocol.DiagnosticSeverityError - diagnostics = append(diagnostics, protocol.Diagnostic{ - Message: fmt.Sprintf("Unknown section '%s'. It must be one of: [Interface], [Peer]", *s.Name), - Severity: &severity, - Range: s.GetHeaderLineRange(), - }) - - return diagnostics - } - - return diagnostics -} - -// Check if the property is valid. -// Returns a list of diagnostics. -// `belongingSection` is the section to which the property belongs. This value is -// expected to be non-nil and expected to be a valid Wireguard section. -func analyzeProperty( - p parser.WireguardProperty, - belongingSection *parser.WireguardSection, - propertyLine uint32, -) []protocol.Diagnostic { - sectionOptions := fields.OptionsHeaderMap[*belongingSection.Name] - option, found := sectionOptions[p.Key.Name] - - if !found { - // Unknown property - severity := protocol.DiagnosticSeverityError - return []protocol.Diagnostic{ - { - Message: fmt.Sprintf("Unknown property '%s'", p.Key.Name), - Severity: &severity, - Range: protocol.Range{ - Start: protocol.Position{ - Line: propertyLine, - Character: p.Key.Location.Start, - }, - End: protocol.Position{ - Line: propertyLine, - Character: p.Key.Location.End, - }, - }, - }, - } - } - - if p.Value == nil { - // No value to check - severity := protocol.DiagnosticSeverityWarning - return []protocol.Diagnostic{ - { - Message: "Property is missing a value", - Severity: &severity, - Range: p.GetLineRange(propertyLine), - }, - } - } - - errors := option.DeprecatedCheckIsValid(p.Value.Value) - - return utils.Map(errors, func(err *docvalues.InvalidValue) protocol.Diagnostic { - severity := protocol.DiagnosticSeverityError - return protocol.Diagnostic{ - Message: err.GetMessage(), - Severity: &severity, - Range: protocol.Range{ - Start: protocol.Position{ - Line: propertyLine, - Character: p.Value.Location.Start + err.Start, - }, - End: protocol.Position{ - Line: propertyLine, - Character: p.Value.Location.Start + err.End, - }, - }, - } - }) -} - -func analyzeParserForDuplicateProperties( - p parser.WireguardParser, -) []protocol.Diagnostic { - diagnostics := make([]protocol.Diagnostic, 0) - - for _, section := range p.Sections { - diagnostics = append(diagnostics, analyzeDuplicateProperties(*section)...) - } - - return diagnostics -} - -func analyzeDuplicateProperties( - s parser.WireguardSection, -) []protocol.Diagnostic { - var diagnostics []protocol.Diagnostic - - existingProperties := make(map[string]uint32) - - lines := utils.KeysOfMap(s.Properties) - slices.Sort(lines) - - for _, currentLineNumber := range lines { - property := s.Properties[currentLineNumber] - var skipCheck = false - - if s.Name != nil { - switch *s.Name { - case "Interface": - if _, found := fields.InterfaceAllowedDuplicateFields[property.Key.Name]; found { - skipCheck = true - } - case "Peer": - if _, found := fields.PeerAllowedDuplicateFields[property.Key.Name]; found { - skipCheck = true - } - } - } - - if skipCheck { - continue - } - - if existingLineNumber, found := existingProperties[property.Key.Name]; found { - severity := protocol.DiagnosticSeverityError - diagnostics = append(diagnostics, protocol.Diagnostic{ - Message: fmt.Sprintf("Property '%s' is already defined on line %d", property.Key.Name, existingLineNumber+1), - Severity: &severity, - Range: protocol.Range{ - Start: protocol.Position{ - Line: currentLineNumber, - Character: 0, - }, - End: protocol.Position{ - Line: currentLineNumber, - Character: 99999, - }, - }, - }) - } else { - existingProperties[property.Key.Name] = currentLineNumber - } - } - - return diagnostics -} - -type propertyWithLine struct { - Line uint32 - Property parser.WireguardProperty - IpPrefix netip.Prefix -} - -func mapAllowedIPsToMasks(p parser.WireguardParser) map[uint8][]propertyWithLine { - ips := make(map[uint8][]propertyWithLine) - - for _, section := range p.GetSectionsByName("Peer") { - for lineNumber, property := range section.Properties { - if property.Key.Name == "AllowedIPs" { - ipAddress, err := netip.ParsePrefix(property.Value.Value) - - if err != nil { - // This should not happen... - continue - } - - hostBits := uint8(ipAddress.Bits()) - - if _, found := ips[uint8(hostBits)]; !found { - ips[hostBits] = make([]propertyWithLine, 0) - } - - ips[hostBits] = append(ips[hostBits], propertyWithLine{ - Line: uint32(lineNumber), - Property: property, - IpPrefix: ipAddress, - }) - } - } - } - - return ips -} - -// Strategy -// Simply compare the host bits of the IP addresses. -// Use a binary tree to store the host bits. -func analyzeDuplicateAllowedIPs(p parser.WireguardParser) []protocol.Diagnostic { - diagnostics := make([]protocol.Diagnostic, 0) - - maskedIPs := mapAllowedIPsToMasks(p) - hostBits := utils.KeysOfMap(maskedIPs) - slices.Sort(hostBits) - - ipHostSet := utils.CreateIPv4HostSet() - - for _, hostBit := range hostBits { - ips := maskedIPs[hostBit] - - for _, ipInfo := range ips { - if ctx, _ := ipHostSet.ContainsIP(ipInfo.IpPrefix); ctx != nil { - severity := protocol.DiagnosticSeverityError - definedLine := (*ctx).Value("line").(uint32) - - diagnostics = append(diagnostics, protocol.Diagnostic{ - Message: fmt.Sprintf("This IP range is already covered on line %d", definedLine), - Severity: &severity, - Range: protocol.Range{ - Start: protocol.Position{ - Line: ipInfo.Line, - Character: ipInfo.Property.Key.Location.Start, - }, - End: protocol.Position{ - Line: ipInfo.Line, - Character: ipInfo.Property.Value.Location.End, - }, - }, - }) - } else { - humanLineNumber := ipInfo.Line + 1 - ctx := context.WithValue(context.Background(), "line", humanLineNumber) - - ipHostSet.AddIP( - ipInfo.IpPrefix, - ctx, - ) - } - } - } - - return diagnostics -} - -func analyzeSymmetricPropertiesExist( - p parser.WireguardParser, -) []protocol.Diagnostic { - diagnostics := make([]protocol.Diagnostic, 0, 4) - severity := protocol.DiagnosticSeverityHint - - for _, section := range p.GetSectionsByName("Interface") { - preUpLine, preUpProperty := section.FetchFirstProperty("PreUp") - preDownLine, preDownProperty := section.FetchFirstProperty("PreDown") - - postUpLine, postUpProperty := section.FetchFirstProperty("PostUp") - postDownLine, postDownProperty := section.FetchFirstProperty("PostDown") - - if preUpProperty != nil && preDownProperty == nil { - diagnostics = append(diagnostics, protocol.Diagnostic{ - Message: "PreUp is set, but PreDown is not. It is recommended to set both properties symmetrically", - Range: preUpProperty.GetLineRange(*preUpLine), - Severity: &severity, - }) - } else if preUpProperty == nil && preDownProperty != nil { - diagnostics = append(diagnostics, protocol.Diagnostic{ - Message: "PreDown is set, but PreUp is not. It is recommended to set both properties symmetrically", - Range: preDownProperty.GetLineRange(*preDownLine), - Severity: &severity, - }) - } - - if postUpProperty != nil && postDownProperty == nil { - diagnostics = append(diagnostics, protocol.Diagnostic{ - Message: "PostUp is set, but PostDown is not. It is recommended to set both properties symmetrically", - Range: postUpProperty.GetLineRange(*postUpLine), - Severity: &severity, - }) - } else if postUpProperty == nil && postDownProperty != nil { - diagnostics = append(diagnostics, protocol.Diagnostic{ - Message: "PostDown is set, but PostUp is not. It is recommended to set both properties symmetrically", - Range: postDownProperty.GetLineRange(*postDownLine), - Severity: &severity, - }) - } - } - - return diagnostics -} diff --git a/server/handlers/wireguard/handlers/analyzer_test.go b/server/handlers/wireguard/handlers/analyzer_test.go deleted file mode 100644 index 345e3ad..0000000 --- a/server/handlers/wireguard/handlers/analyzer_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package handlers - -import ( - "config-lsp/handlers/wireguard/parser" - "config-lsp/utils" - "testing" -) - -func TestMultipleIntefaces(t *testing.T) { - content := utils.Dedent(` -[Interface] -PrivateKey = abc - -[Interface] -PrivateKey = def -`) - p := parser.CreateWireguardParser() - p.ParseFromString(content) - - diagnostics := Analyze(p) - - if len(diagnostics) == 0 { - t.Errorf("Expected diagnostic errors, got %d", len(diagnostics)) - } -} - -func TestInvalidValue(t *testing.T) { - content := utils.Dedent(` -[Interface] -DNS = nope -`) - p := parser.CreateWireguardParser() - p.ParseFromString(content) - - diagnostics := Analyze(p) - - if len(diagnostics) == 0 { - t.Errorf("Expected diagnostic errors, got %d", len(diagnostics)) - } -} - -func TestDuplicateProperties(t *testing.T) { - content := utils.Dedent(` -[Interface] -PrivateKey = abc -DNS = 1.1.1.1 -PrivateKey = def -`) - p := parser.CreateWireguardParser() - p.ParseFromString(content) - - diagnostics := Analyze(p) - - if len(diagnostics) == 0 { - t.Errorf("Expected diagnostic errors, got %d", len(diagnostics)) - } -} diff --git a/server/handlers/wireguard/handlers/code-actions.go b/server/handlers/wireguard/handlers/code-actions.go index ecb37cd..32dbf57 100644 --- a/server/handlers/wireguard/handlers/code-actions.go +++ b/server/handlers/wireguard/handlers/code-actions.go @@ -1,8 +1,9 @@ package handlers +/* import ( + "config-lsp/handlers/wireguard/ast" wgcommands "config-lsp/handlers/wireguard/commands" - "config-lsp/handlers/wireguard/parser" protocol "github.com/tliron/glsp/protocol_3_16" ) @@ -15,7 +16,7 @@ const ( ) type CodeAction interface { - RunCommand(*parser.WireguardParser) (*protocol.ApplyWorkspaceEditParams, error) + RunCommand(*ast.WGConfig) (*protocol.ApplyWorkspaceEditParams, error) } type CodeActionArgs interface{} @@ -32,7 +33,7 @@ func CodeActionGeneratePrivateKeyArgsFromArguments(arguments map[string]any) Cod } } -func (args CodeActionGeneratePrivateKeyArgs) RunCommand(p *parser.WireguardParser) (*protocol.ApplyWorkspaceEditParams, error) { +func (args CodeActionGeneratePrivateKeyArgs) RunCommand(p *ast.WGConfig) (*protocol.ApplyWorkspaceEditParams, error) { privateKey, err := wgcommands.CreateNewPrivateKey() if err != nil { @@ -73,7 +74,7 @@ func CodeActionGeneratePresharedKeyArgsFromArguments(arguments map[string]any) C } } -func (args CodeActionGeneratePresharedKeyArgs) RunCommand(p *parser.WireguardParser) (*protocol.ApplyWorkspaceEditParams, error) { +func (args CodeActionGeneratePresharedKeyArgs) RunCommand(p *ast.WGConfig) (*protocol.ApplyWorkspaceEditParams, error) { presharedKey, err := wgcommands.CreatePresharedKey() if err != nil { @@ -114,7 +115,7 @@ func CodeActionAddKeepaliveArgsFromArguments(arguments map[string]any) CodeActio } } -func (args CodeActionAddKeepaliveArgs) RunCommand(p *parser.WireguardParser) (*protocol.ApplyWorkspaceEditParams, error) { +func (args CodeActionAddKeepaliveArgs) RunCommand(p *ast.WGConfig) (*protocol.ApplyWorkspaceEditParams, error) { section := p.Sections[args.SectionIndex] label := "Add PersistentKeepalive" @@ -141,3 +142,4 @@ func (args CodeActionAddKeepaliveArgs) RunCommand(p *parser.WireguardParser) (*p }, }, nil } +*/ diff --git a/server/handlers/wireguard/handlers/completions.go b/server/handlers/wireguard/handlers/completions.go index 4ebecf6..96fd37d 100644 --- a/server/handlers/wireguard/handlers/completions.go +++ b/server/handlers/wireguard/handlers/completions.go @@ -1,160 +1,34 @@ package handlers import ( - docvalues "config-lsp/doc-values" - "config-lsp/handlers/wireguard/fields" - "config-lsp/handlers/wireguard/parser" - "config-lsp/utils" + "config-lsp/handlers/wireguard" + protocol "github.com/tliron/glsp/protocol_3_16" - "maps" ) -func getHeaderCompletion(name string, documentation string) protocol.CompletionItem { - textFormat := protocol.InsertTextFormatPlainText - kind := protocol.CompletionItemKindEnum - - insertText := "[" + name + "]\n" - - return protocol.CompletionItem{ - Label: "[" + name + "]", - InsertTextFormat: &textFormat, - InsertText: &insertText, - Kind: &kind, - Documentation: &documentation, - } -} - -func GetRootCompletionsForEmptyLine( - p parser.WireguardParser, +func SuggestCompletions( + d *wireguard.WGDocument, + params *protocol.CompletionParams, ) ([]protocol.CompletionItem, error) { - completions := make([]protocol.CompletionItem, 0) + lineNumber := params.Position.Line - if _, found := p.GetInterfaceSection(); !found { - completions = append(completions, getHeaderCompletion("Interface", fields.HeaderInterfaceEnum.Documentation)) - } - - completions = append(completions, getHeaderCompletion("Peer", fields.HeaderPeerEnum.Documentation)) - - return completions, nil -} - -func GetCompletionsForSectionEmptyLine( - s parser.WireguardSection, -) ([]protocol.CompletionItem, error) { - if s.Name == nil { + if _, found := d.Config.CommentLines[lineNumber]; found { return nil, nil } - options := make(map[string]docvalues.DocumentationValue) + section := d.Config.FindSectionByLine(lineNumber) + property := d.Config.FindPropertyByLine(lineNumber) - switch *s.Name { - case "Interface": - maps.Copy(options, fields.InterfaceOptions) - - // Remove existing options - for _, property := range s.Properties { - if _, found := fields.InterfaceAllowedDuplicateFields[property.Key.Name]; found { - continue - } - - delete(options, property.Key.Name) - } - case "Peer": - maps.Copy(options, fields.PeerOptions) - - // Remove existing options - for _, property := range s.Properties { - if _, found := fields.PeerAllowedDuplicateFields[property.Key.Name]; found { - continue - } - - delete(options, property.Key.Name) - } - } - - kind := protocol.CompletionItemKindProperty - - return utils.MapMapToSlice( - options, - func(optionName string, value docvalues.DocumentationValue) protocol.CompletionItem { - insertText := optionName + " = " - - return protocol.CompletionItem{ - Kind: &kind, - Documentation: value.Documentation, - Label: optionName, - InsertText: &insertText, - } - }, - ), nil -} - -func GetSeparatorCompletion(property parser.WireguardProperty, character uint32) ([]protocol.CompletionItem, error) { - var insertText string - - if character == property.Key.Location.End { - insertText = property.Key.Name + " = " - } else { - insertText = "= " - } - - kind := protocol.CompletionItemKindValue - - return []protocol.CompletionItem{ - { - Label: insertText, - InsertText: &insertText, - Kind: &kind, - }, - }, parser.PropertyNotFullyTypedError{} -} - -func GetCompletionsForSectionPropertyLine( - s parser.WireguardSection, - lineNumber uint32, - character uint32, -) ([]protocol.CompletionItem, error) { - property, err := s.GetPropertyByLine(lineNumber) - - if err != nil { - return nil, err - } - - if s.Name == nil { - return nil, parser.PropertyNotFoundError{} - } - - options, found := fields.OptionsHeaderMap[*s.Name] - - if !found { - return nil, parser.PropertyNotFoundError{} - } - - if property.Separator == nil { - if _, found := options[property.Key.Name]; found { - return GetSeparatorCompletion(*property, character) - } - // Get empty line completions - return nil, parser.PropertyNotFullyTypedError{} - } - - option, found := options[property.Key.Name] - - if !found { - if character < property.Separator.Location.Start { - return nil, parser.PropertyNotFullyTypedError{} + if section == nil { + // First, the user needs to define a section header + if property == nil { + return GetSectionHeaderCompletions(d) } else { - return nil, parser.PropertyNotFoundError{} + // However, if they start typing a property - we should not + // show anything to signal them that they can't write a property yet. + return nil, nil } + } else { + return GetSectionBodyCompletions(d, *section, property, params) } - - if property.Value == nil { - if character >= property.Separator.Location.End { - return option.DeprecatedFetchCompletions("", 0), nil - } - } - - relativeCursor := character - property.Value.Location.Start - - return option.DeprecatedFetchCompletions(property.Value.Value, relativeCursor), nil } diff --git a/server/handlers/wireguard/handlers/completions_body.go b/server/handlers/wireguard/handlers/completions_body.go new file mode 100644 index 0000000..e553dfc --- /dev/null +++ b/server/handlers/wireguard/handlers/completions_body.go @@ -0,0 +1,182 @@ +package handlers + +import ( + "config-lsp/common" + docvalues "config-lsp/doc-values" + "config-lsp/handlers/wireguard" + "config-lsp/handlers/wireguard/ast" + "config-lsp/handlers/wireguard/fields" + "config-lsp/utils" + "maps" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func GetSectionBodyCompletions( + d *wireguard.WGDocument, + section ast.WGSection, + property *ast.WGProperty, + params *protocol.CompletionParams, +) ([]protocol.CompletionItem, error) { + // These are the possible scenarios: + // | = Cursor position + /* + [Inter| + */ + /* + [Interface] + | + */ + /* + [Interface] + Add| + */ + /* + [Interface] + Address = 10.0.0.1/24 + + | + */ + + // First scenario, user is typing the section name + if params.Position.Line == section.Start.Line { + return GetSectionHeaderCompletions(d) + } + + // Third and fourth scenarios, the user wants to add a new property + completions, err := getPropertyCompletions(d, section, property, params) + + if err != nil { + // Something weird happened + return completions, err + } + + // Fourth scenario may arrive here, the user is typing a property name, but the previous line is empty. + // In this case, the user may want to add a property or add a new section. + // We should therefore suggest both options. + + // Check if previous line is empty + previousLineProperty := d.Config.FindPropertyByLine(params.Position.Line - 1) + + if previousLineProperty == nil && params.Position.Line-1 != section.Start.Line { + sectionCompletions, err := GetSectionHeaderCompletions(d) + + if err != nil { + return sectionCompletions, err + } + + completions = append(completions, sectionCompletions...) + } + + return completions, nil +} + +func getPropertyCompletions( + d *wireguard.WGDocument, + section ast.WGSection, + property *ast.WGProperty, + params *protocol.CompletionParams, +) ([]protocol.CompletionItem, error) { + // These are the possible scenarios: + /* Empty line / Key started / Separator missing: + Add| + Address | + */ + /* Value missing or started: + Address = 10.| + */ + + if property == nil || property.Separator == nil { + // First scenario + return getKeyCompletions(section), nil + } + + // Check if the cursor it outside the value + position := common.LSPCharacterAsCursorPosition(params.Position.Character) + if property.Value != nil && property.Value.IsPositionAfterEnd(position) { + // Then we don't show anything + return nil, nil + } + + // Otherwise, suggest value completions + return getValueCompletions(section, *property, position), nil +} + +func getKeyCompletions( + section ast.WGSection, +) []protocol.CompletionItem { + options := make(map[string]docvalues.DocumentationValue) + + switch section.Header.Name { + case "Interface": + maps.Copy(options, fields.InterfaceOptions) + + // Remove existing, non-duplicate options + for _, property := range section.Properties { + if _, found := fields.InterfaceAllowedDuplicateFields[property.Key.Name]; found { + continue + } + + delete(options, property.Key.Name) + } + case "Peer": + maps.Copy(options, fields.PeerOptions) + + // Remove existing, non-duplicate options + for _, property := range section.Properties { + if _, found := fields.PeerAllowedDuplicateFields[property.Key.Name]; found { + continue + } + + delete(options, property.Key.Name) + } + } + + kind := protocol.CompletionItemKindField + + return utils.MapMapToSlice( + options, + func(optionName string, value docvalues.DocumentationValue) protocol.CompletionItem { + insertText := optionName + " = " + + return protocol.CompletionItem{ + Kind: &kind, + Documentation: value.Documentation, + Label: optionName, + InsertText: &insertText, + } + }, + ) +} + +func getValueCompletions( + section ast.WGSection, + property ast.WGProperty, + cursorPosition common.CursorPosition, +) []protocol.CompletionItem { + // TODO: Normalize section header name + options, found := fields.OptionsHeaderMap[section.Header.Name] + + if !found { + return nil + } + + option, found := options[property.Key.Name] + + if !found { + return nil + } + + if property.Value == nil { + return option.DeprecatedFetchCompletions("", 0) + } else { + return option.DeprecatedFetchCompletions( + property.Value.Value, + common.DeprecatedImprovedCursorToIndex( + cursorPosition, + property.Value.Value, + property.Value.Start.Character, + ), + ) + } +} diff --git a/server/handlers/wireguard/handlers/completions_header.go b/server/handlers/wireguard/handlers/completions_header.go new file mode 100644 index 0000000..a7e11f0 --- /dev/null +++ b/server/handlers/wireguard/handlers/completions_header.go @@ -0,0 +1,46 @@ +package handlers + +import ( + "config-lsp/handlers/wireguard" + "config-lsp/handlers/wireguard/fields" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func getHeaderCompletion(name string, documentation string) protocol.CompletionItem { + textFormat := protocol.InsertTextFormatPlainText + kind := protocol.CompletionItemKindEnum + + insertText := "[" + name + "]\n" + + return protocol.CompletionItem{ + Label: "[" + name + "]", + InsertTextFormat: &textFormat, + InsertText: &insertText, + Kind: &kind, + Documentation: &documentation, + } +} + +func GetSectionHeaderCompletions( + d *wireguard.WGDocument, +) ([]protocol.CompletionItem, error) { + completions := make([]protocol.CompletionItem, 0) + + containsInterfaceSection := false + + for _, section := range d.Config.Sections { + if section.Header.Name == "Interface" { + containsInterfaceSection = true + break + } + } + + if !containsInterfaceSection { + completions = append(completions, getHeaderCompletion("Interface", fields.HeaderInterfaceEnum.Documentation)) + } + + completions = append(completions, getHeaderCompletion("Peer", fields.HeaderPeerEnum.Documentation)) + + return completions, nil +} diff --git a/server/handlers/wireguard/handlers/completions_test.go b/server/handlers/wireguard/handlers/completions_test.go index 5db4edc..c8f4118 100644 --- a/server/handlers/wireguard/handlers/completions_test.go +++ b/server/handlers/wireguard/handlers/completions_test.go @@ -1,10 +1,13 @@ package handlers import ( + "config-lsp/handlers/wireguard" + "config-lsp/handlers/wireguard/ast" "config-lsp/handlers/wireguard/fields" - "config-lsp/handlers/wireguard/parser" "config-lsp/utils" "testing" + + protocol "github.com/tliron/glsp/protocol_3_16" ) func TestSimplePropertyInInterface( @@ -14,17 +17,33 @@ func TestSimplePropertyInInterface( [Interface] `) - p := parser.CreateWireguardParser() - p.ParseFromString(sample) + p := ast.NewWGConfig() + parseErrors := p.Parse(sample) - completions, err := GetCompletionsForSectionEmptyLine(*p.Sections[0]) + if len(parseErrors) > 0 { + t.Fatalf("Parser failed with error %v", parseErrors) + } + + d := &wireguard.WGDocument{ + Config: p, + } + + params := &protocol.CompletionParams{ + TextDocumentPositionParams: protocol.TextDocumentPositionParams{ + Position: protocol.Position{ + Line: 1, + Character: 0, + }, + }, + } + completions, err := SuggestCompletions(d, params) if err != nil { - t.Fatalf("getCompletionsForEmptyLine failed with error: %v", err) + t.Errorf("getCompletionsForEmptyLine failed with error: %v", err) } if len(completions) != len(fields.InterfaceOptions) { - t.Fatalf("getCompletionsForEmptyLine: Expected %v completions, but got %v", len(fields.InterfaceOptions), len(completions)) + t.Errorf("getCompletionsForEmptyLine: Expected %v completions, but got %v", len(fields.InterfaceOptions), len(completions)) } } @@ -36,10 +55,26 @@ func TestSimpleOneExistingPropertyInInterface( PrivateKey = 1234567890 `) - p := parser.CreateWireguardParser() - p.ParseFromString(sample) + p := ast.NewWGConfig() + parseErrors := p.Parse(sample) - completions, err := GetCompletionsForSectionEmptyLine(*p.Sections[0]) + if len(parseErrors) > 0 { + t.Fatalf("Parser failed with error %v", parseErrors) + } + + d := &wireguard.WGDocument{ + Config: p, + } + + params := &protocol.CompletionParams{ + TextDocumentPositionParams: protocol.TextDocumentPositionParams{ + Position: protocol.Position{ + Line: 2, + Character: 0, + }, + }, + } + completions, err := SuggestCompletions(d, params) if err != nil { t.Fatalf("getCompletionsForEmptyLine failed with error: %v", err) @@ -51,124 +86,151 @@ PrivateKey = 1234567890 } } -func TestEmptyRootCompletionsWork( +func TestEmptyCompletions( t *testing.T, ) { sample := utils.Dedent(` - `) - p := parser.CreateWireguardParser() - p.ParseFromString(sample) +`) + p := ast.NewWGConfig() + parseErrors := p.Parse(sample) - completions, _ := GetRootCompletionsForEmptyLine(p) + if len(parseErrors) > 0 { + t.Fatalf("Parser failed with error %v", parseErrors) + } + + d := &wireguard.WGDocument{ + Config: p, + } + + params := &protocol.CompletionParams{ + TextDocumentPositionParams: protocol.TextDocumentPositionParams{ + Position: protocol.Position{ + Line: 0, + Character: 0, + }, + }, + } + completions, err := SuggestCompletions(d, params) + + if err != nil { + t.Fatalf("getCompletionsForEmptyLine failed with error: %v", err) + } if len(completions) != 2 { t.Fatalf("getRootCompletionsForEmptyLine: Expected 2 completions, but got %v", len(completions)) } } -func TestInterfaceSectionRootCompletionsBeforeWork( +func TestIncompletePropertyCompletions( t *testing.T, ) { sample := utils.Dedent(` - -[Interface] -`) - p := parser.CreateWireguardParser() - p.ParseFromString(sample) - - completions, _ := GetRootCompletionsForEmptyLine(p) - - if len(completions) != 1 { - t.Fatalf("getRootCompletionsForEmptyLine: Expected 1 completions, but got %v", len(completions)) - } -} - -func TestInterfaceAndPeerSectionRootCompletionsWork( - t *testing.T, -) { - sample := utils.Dedent(` -[Interface] - [Peer] +Add `) - p := parser.CreateWireguardParser() - p.ParseFromString(sample) + p := ast.NewWGConfig() + parseErrors := p.Parse(sample) - completions, _ := GetRootCompletionsForEmptyLine(p) - - if len(completions) != 1 { - t.Fatalf("getRootCompletionsForEmptyLine: Expected 1 completions, but got %v", len(completions)) - } -} - -func TestPropertyNoSepatorShouldCompleteSeparator( - t *testing.T, -) { - sample := utils.Dedent(` -[Interface] -DNS -`) - p := parser.CreateWireguardParser() - p.ParseFromString(sample) - - completions, err := GetCompletionsForSectionPropertyLine(*p.Sections[0], 1, 3) - - if err == nil { - t.Fatalf("getCompletionsForPropertyLine err is nil but should not be") + if len(parseErrors) > 0 { + t.Fatalf("Parser failed with error %v", parseErrors) } - if len(completions) != 1 { - t.Fatalf("getCompletionsForPropertyLine: Expected 1 completion, but got %v", len(completions)) + d := &wireguard.WGDocument{ + Config: p, } - if *completions[0].InsertText != "DNS = " { - t.Fatalf("getCompletionsForPropertyLine: Expected completion to be 'DNS = ', but got '%v'", completions[0].Label) + params := &protocol.CompletionParams{ + TextDocumentPositionParams: protocol.TextDocumentPositionParams{ + Position: protocol.Position{ + Line: 1, + Character: 3, + }, + }, } -} - -func TestPropertyNoSeparatorWithSpaceShouldCompleteSeparator( - t *testing.T, -) { - sample := utils.Dedent(` -[Interface] -DNS -`) - p := parser.CreateWireguardParser() - p.ParseFromString(sample) - - completions, err := GetCompletionsForSectionPropertyLine(*p.Sections[0], 1, 4) - - if err == nil { - t.Fatalf("getCompletionsForPropertyLine err is nil but should not be") - } - - if len(completions) != 1 { - t.Fatalf("getCompletionsForPropertyLine: Expected 1 completion, but got %v", len(completions)) - } - - if *completions[0].InsertText != "= " { - t.Fatalf("getCompletionsForPropertyLine: Expected completion to be '= ', but got '%v'", completions[0].Label) - } -} - -func TestHeaderButNoProperty( - t *testing.T, -) { - sample := utils.Dedent(` -[Interface] - -`) - p := parser.CreateWireguardParser() - p.ParseFromString(sample) - - completions, err := GetCompletionsForSectionEmptyLine(*p.Sections[0]) + completions, err := SuggestCompletions(d, params) if err != nil { - t.Fatalf("getCompletionsForEmptyLine failed with error: %v", err) + t.Errorf("getCompletionsForEmptyLine failed with error: %v", err) } - if len(completions) != len(fields.InterfaceOptions) { - t.Fatalf("getCompletionsForEmptyLine: Expected %v completions, but got %v", len(fields.InterfaceOptions), len(completions)) + if len(completions) != len(fields.PeerOptions) { + t.Errorf("getRootCompletionsForEmptyLine: Expected 1 completions, but got %v", len(completions)) + } +} + +func TestPropertyBeforeLineIsEmpty( + t *testing.T, +) { + sample := utils.Dedent(` +[Interface] +DNS = 1.1.1.1 + + +`) + p := ast.NewWGConfig() + parseErrors := p.Parse(sample) + + if len(parseErrors) > 0 { + t.Fatalf("Parser failed with error %v", parseErrors) + } + + d := &wireguard.WGDocument{ + Config: p, + } + + params := &protocol.CompletionParams{ + TextDocumentPositionParams: protocol.TextDocumentPositionParams{ + Position: protocol.Position{ + Line: 3, + Character: 0, + }, + }, + } + completions, err := SuggestCompletions(d, params) + + if err != nil { + t.Errorf("getCompletionsForPropertyLine failed with error: %v", err) + } + + if len(completions) != len(fields.InterfaceOptions)+1-1 { + t.Errorf("getCompletionsForPropertyLine: Expected completions, but got %v", len(completions)) + } +} + +func TestPropertyValueCompletions( + t *testing.T, +) { + sample := utils.Dedent(` +[Interface] +Table = +`) + p := ast.NewWGConfig() + parseErrors := p.Parse(sample) + + if len(parseErrors) > 0 { + t.Fatalf("Parser failed with error %v", parseErrors) + } + + d := &wireguard.WGDocument{ + Config: p, + } + + params := &protocol.CompletionParams{ + TextDocumentPositionParams: protocol.TextDocumentPositionParams{ + Position: protocol.Position{ + Line: 1, + Character: 8, + }, + }, + } + completions, err := SuggestCompletions(d, params) + + if err != nil { + t.Errorf("getCompletionsForPropertyLine failed with error: %v", err) + } + + if !(len(completions) == 2) { + t.Errorf("getCompletionsForPropertyLine: Expected completions, but got %v", len(completions)) } } diff --git a/server/handlers/wireguard/handlers/fetch-code-actions.go b/server/handlers/wireguard/handlers/fetch-code-actions.go index 84dfead..7a5a86b 100644 --- a/server/handlers/wireguard/handlers/fetch-code-actions.go +++ b/server/handlers/wireguard/handlers/fetch-code-actions.go @@ -1,19 +1,20 @@ package handlers +/* import ( + "config-lsp/handlers/wireguard/ast" "config-lsp/handlers/wireguard/commands" - "config-lsp/handlers/wireguard/parser" protocol "github.com/tliron/glsp/protocol_3_16" ) func GetKeepaliveCodeActions( - p *parser.WireguardParser, + p *ast.WGConfig, params *protocol.CodeActionParams, ) []protocol.CodeAction { line := params.Range.Start.Line for index, section := range p.Sections { - if section.StartLine >= line && line <= section.EndLine && section.Name != nil && *section.Name == "Peer" { + if section.StartLine >= line && line <= section.EndLine && section.Header != nil && *section.Header == "Peer" { if section.ExistsProperty("Endpoint") && !section.ExistsProperty("PersistentKeepalive") { commandID := "wireguard." + CodeActionAddKeepalive command := protocol.Command{ @@ -41,7 +42,7 @@ func GetKeepaliveCodeActions( } func GetKeyGenerationCodeActions( - p *parser.WireguardParser, + p *ast.WGConfig, params *protocol.CodeActionParams, ) []protocol.CodeAction { line := params.Range.Start.Line @@ -102,3 +103,4 @@ func GetKeyGenerationCodeActions( return nil } +*/ diff --git a/server/handlers/wireguard/handlers/hover.go b/server/handlers/wireguard/handlers/hover.go index 7ac6660..8d652d1 100644 --- a/server/handlers/wireguard/handlers/hover.go +++ b/server/handlers/wireguard/handlers/hover.go @@ -1,20 +1,21 @@ package handlers +/* import ( docvalues "config-lsp/doc-values" + "config-lsp/handlers/wireguard/ast" "config-lsp/handlers/wireguard/fields" - "config-lsp/handlers/wireguard/parser" "fmt" "strings" ) func getPropertyInfo( - p parser.WireguardProperty, + p ast.WGProperty, cursor uint32, - section parser.WireguardSection, + section ast.WGSection, ) []string { if cursor <= p.Key.Location.End { - options, found := fields.OptionsHeaderMap[*section.Name] + options, found := fields.OptionsHeaderMap[*section.Header] if !found { return []string{} @@ -29,7 +30,7 @@ func getPropertyInfo( return strings.Split(option.Documentation, "\n") } - options, found := fields.OptionsHeaderMap[*section.Name] + options, found := fields.OptionsHeaderMap[*section.Header] if !found { return []string{} @@ -42,19 +43,19 @@ func getPropertyInfo( return []string{} } -func getSectionInfo(s parser.WireguardSection) []string { - if s.Name == nil { +func getSectionInfo(s ast.WGSection) []string { + if s.Header == nil { return []string{} } contents := []string{ - fmt.Sprintf("## [%s]", *s.Name), + fmt.Sprintf("## [%s]", *s.Header), "", } var option *docvalues.EnumString = nil - switch *s.Name { + switch *s.Header { case "Interface": option = &fields.HeaderInterfaceEnum case "Peer": @@ -71,7 +72,7 @@ func getSectionInfo(s parser.WireguardSection) []string { } func GetHoverContent( - p parser.WireguardParser, + p ast.WGConfig, line uint32, cursor uint32, ) []string { @@ -103,3 +104,4 @@ func GetHoverContent( return contents } +*/ diff --git a/server/handlers/wireguard/lsp/text-document-completion.go b/server/handlers/wireguard/lsp/text-document-completion.go index 069cac9..28fdbfb 100644 --- a/server/handlers/wireguard/lsp/text-document-completion.go +++ b/server/handlers/wireguard/lsp/text-document-completion.go @@ -1,60 +1,18 @@ package lsp import ( + "config-lsp/handlers/wireguard" "config-lsp/handlers/wireguard/handlers" - "config-lsp/handlers/wireguard/parser" + "regexp" + "github.com/tliron/glsp" protocol "github.com/tliron/glsp/protocol_3_16" ) +var headerPattern = regexp.MustCompile(`^\s*\[(\w+)?]?`) + func TextDocumentCompletion(context *glsp.Context, params *protocol.CompletionParams) (any, error) { - p := documentParserMap[params.TextDocument.URI] + d := wireguard.DocumentParserMap[params.TextDocument.URI] - lineNumber := params.Position.Line - - section := p.GetBelongingSectionByLine(lineNumber) - lineType := p.GetTypeByLine(lineNumber) - - switch lineType { - case parser.LineTypeComment: - return nil, nil - case parser.LineTypeHeader: - return handlers.GetRootCompletionsForEmptyLine(*p) - case parser.LineTypeEmpty: - if section.Name == nil { - // Root completions - return handlers.GetRootCompletionsForEmptyLine(*p) - } - - completions, err := handlers.GetCompletionsForSectionEmptyLine(*section) - - // === Smart rules === - - // If previous line is empty too, maybe new section? - if lineNumber >= 1 && p.GetTypeByLine(lineNumber-1) == parser.LineTypeEmpty && len(p.GetBelongingSectionByLine(lineNumber).Properties) > 0 { - rootCompletions, err := handlers.GetRootCompletionsForEmptyLine(*p) - - if err == nil { - completions = append(completions, rootCompletions...) - } - } - - return completions, err - case parser.LineTypeProperty: - completions, err := handlers.GetCompletionsForSectionPropertyLine(*section, lineNumber, params.Position.Character) - - if completions == nil && err != nil { - switch err.(type) { - // Ignore - case parser.PropertyNotFullyTypedError: - return handlers.GetCompletionsForSectionEmptyLine(*section) - default: - return nil, err - } - } - - return completions, nil - } - - panic("TextDocumentCompletion: unexpected line type") + return handlers.SuggestCompletions(d, params) } diff --git a/server/handlers/wireguard/parser/wg-parser-type_test.go b/server/handlers/wireguard/parser/wg-parser-type_test.go deleted file mode 100644 index 53ee275..0000000 --- a/server/handlers/wireguard/parser/wg-parser-type_test.go +++ /dev/null @@ -1,127 +0,0 @@ -package parser - -import ( - "config-lsp/utils" - "testing" -) - -func TestGetLineTypeWorksCorrectly( - t *testing.T, -) { - sample := utils.Dedent(` -# A comment at the very top -Test=Hello - -[Interface] -PrivateKey = 1234567890 # Some comment -Address = 10.0.0.1 - - - -[Peer] -PublicKey = 1234567890 - -; I'm a comment -`) - - parser := CreateWireguardParser() - parser.ParseFromString(sample) - - lineType := parser.GetTypeByLine(0) - if lineType != LineTypeComment { - t.Fatalf("getTypeByLine: Expected line 0 to be a comment, but it is %v", lineType) - } - - lineType = parser.GetTypeByLine(1) - if lineType != LineTypeProperty { - t.Fatalf("getTypeByLine: Expected line 1 to be a property, but it is %v", lineType) - } - - lineType = parser.GetTypeByLine(2) - if lineType != LineTypeEmpty { - t.Fatalf("getTypeByLine: Expected line 2 to be empty, but it is %v", lineType) - } - - lineType = parser.GetTypeByLine(3) - if lineType != LineTypeHeader { - t.Fatalf("getTypeByLine: Expected line 3 to be a header, but it is %v", lineType) - } - - lineType = parser.GetTypeByLine(4) - if lineType != LineTypeProperty { - t.Fatalf("getTypeByLine: Expected line 4 to be a property, but it is %v", lineType) - } - - lineType = parser.GetTypeByLine(12) - if lineType != LineTypeComment { - t.Fatalf("getTypeByLine: Expected line 12 to be a comment, but it is %v", lineType) - } -} - -func TestGetBelongingSectionWorksCorrectly( - t *testing.T, -) { - sample := utils.Dedent(` -# A comment at the very top -Test=Hello - -[Interface] -PrivateKey = 1234567890 # Some comment -Address = 10.0.0.1 - - - -[Peer] -PublicKey = 1234567890 - -; I'm a comment -`) - - parser := CreateWireguardParser() - parser.ParseFromString(sample) - - section := parser.GetBelongingSectionByLine(0) - - // Comment - if section != nil { - t.Fatalf("getBelongingSectionByLine: Expected line 0 to be in no section, but it is in %v", section) - } - - section = parser.GetBelongingSectionByLine(1) - - if section != parser.Sections[1] { - t.Fatalf("getBelongingSectionByLine: Expected line 1 to be in global section, but it is in %v", section) - } - - section = parser.GetBelongingSectionByLine(2) - if section != parser.Sections[1] { - t.Fatalf("getBelongingSectionByLine: Expected line 2 to be in global section, but it is in %v", section) - } - - section = parser.GetBelongingSectionByLine(3) - if section != parser.Sections[2] { - t.Fatalf("getBelongingSectionByLine: Expected line 3 to be in section Interface, but it is in %v", section) - } - - section = parser.GetBelongingSectionByLine(4) - if section != parser.Sections[2] { - t.Fatalf("getBelongingSectionByLine: Expected line 4 to be in section Interface, but it is in %v", section) - } - - section = parser.GetBelongingSectionByLine(6) - if section != parser.Sections[2] { - t.Fatalf("getBelongingSectionByLine: Expected line 6 to be in section Interface, but it is in %v", section) - } - - section = parser.GetBelongingSectionByLine(10) - if section != parser.Sections[3] { - t.Fatalf("getBelongingSectionByLine: Expected line 10 to be in section Peer, but it is in %v", section) - } - - section = parser.GetBelongingSectionByLine(12) - - // Comment - if section != nil { - t.Fatalf("getBelongingSectionByLine: Expected line 12 to be in no section, but it is in %v", section) - } -} diff --git a/server/handlers/wireguard/parser/wg-parser.go b/server/handlers/wireguard/parser/wg-parser.go deleted file mode 100644 index 688dc5c..0000000 --- a/server/handlers/wireguard/parser/wg-parser.go +++ /dev/null @@ -1,258 +0,0 @@ -package parser - -import ( - "config-lsp/common" - "config-lsp/handlers/wireguard/ast" - "regexp" - "slices" - "strings" -) - -var commentPattern = regexp.MustCompile(`^\s*(;|#)`) -var emptyLinePattern = regexp.MustCompile(`^\s*$`) -var headerPattern = regexp.MustCompile(`^\s*\[`) - -func (p *ast.WGConfig) ParseFromString(input string) []common.ParseError { - var errors []common.ParseError - lines := strings.Split( - input, - "\n", - ) - - slices.Reverse(lines) - - collectedProperties := WireguardProperties{} - var lastPropertyLine *uint32 - - for index, line := range lines { - currentLineNumber := uint32(len(lines) - index - 1) - lineType := getLineType(line) - - switch lineType { - case LineTypeComment: - p.commentLines[currentLineNumber] = struct{}{} - p.linesIndexes[currentLineNumber] = wireguardLineIndex{ - Type: LineTypeComment, - BelongingSection: nil, - } - - case LineTypeEmpty: - continue - - case LineTypeProperty: - err := collectedProperties.AddLine(currentLineNumber, line) - - if err != nil { - errors = append(errors, common.ParseError{ - Line: currentLineNumber, - Err: err, - }) - continue - } - - if lastPropertyLine == nil { - lastPropertyLine = ¤tLineNumber - } - - case LineTypeHeader: - var lastLine uint32 - - if lastPropertyLine == nil { - // Current line - lastLine = currentLineNumber - } else { - lastLine = *lastPropertyLine - } - - section := CreateWireguardSection( - currentLineNumber, - lastLine, - line, - collectedProperties, - ) - - p.Sections = append(p.Sections, §ion) - - // Add indexes - for lineNumber := range collectedProperties { - p.linesIndexes[lineNumber] = wireguardLineIndex{ - Type: LineTypeProperty, - BelongingSection: §ion, - } - } - p.linesIndexes[currentLineNumber] = wireguardLineIndex{ - Type: LineTypeHeader, - BelongingSection: §ion, - } - - // Reset - collectedProperties = WireguardProperties{} - lastPropertyLine = nil - } - } - - var emptySection *ast.WGSection - - if len(collectedProperties) > 0 { - var endLine uint32 - - if len(p.Sections) == 0 { - endLine = uint32(len(lines)) - } else { - endLine = p.Sections[len(p.Sections)-1].StartLine - } - - emptySection = &ast.WGSection{ - StartLine: 0, - EndLine: endLine, - Properties: collectedProperties, - } - - p.Sections = append(p.Sections, emptySection) - - for lineNumber := range collectedProperties { - p.linesIndexes[lineNumber] = wireguardLineIndex{ - Type: LineTypeProperty, - BelongingSection: emptySection, - } - } - p.Sections = append(p.Sections, emptySection) - } else { - // Add empty section - var endLine = uint32(len(lines)) - - if len(p.Sections) > 0 { - endLine = p.Sections[len(p.Sections)-1].StartLine - } - - // Add empty section - if endLine != 0 { - emptySection = &ast.WGSection{ - StartLine: 0, - EndLine: endLine, - Properties: collectedProperties, - } - - p.Sections = append(p.Sections, emptySection) - - for newLine := uint32(0); newLine < endLine; newLine++ { - if _, found := p.linesIndexes[newLine]; found { - continue - } - - p.linesIndexes[newLine] = wireguardLineIndex{ - Type: LineTypeEmpty, - BelongingSection: emptySection, - } - } - } - } - - // Since we parse the content from bottom to top, we need to reverse the sections - // so its in correct order - slices.Reverse(p.Sections) - - // Fill empty lines between sections - for lineNumber, section := range p.Sections { - var endLine uint32 - - if len(p.Sections) > lineNumber+1 { - nextSection := p.Sections[lineNumber+1] - endLine = nextSection.StartLine - } else { - endLine = uint32(len(lines)) - } - - for newLine := section.StartLine; newLine < endLine; newLine++ { - if _, found := p.linesIndexes[newLine]; found { - continue - } - - p.linesIndexes[newLine] = wireguardLineIndex{ - Type: LineTypeEmpty, - BelongingSection: section, - } - } - } - - return errors -} - -func (p *ast.WGConfig) GetSectionByLine(line uint32) *ast.WGSection { - for _, section := range p.Sections { - if section.StartLine <= line && section.EndLine >= line { - return section - } - } - - return nil -} - -// Search for a property by name -// Returns (line number, property) -func (p *ast.WGConfig) FindFirstPropertyByName(name string) (*uint32, *ast.WGProperty) { - for _, section := range p.Sections { - for lineNumber, property := range section.Properties { - if property.Key.Name == name { - return &lineNumber, &property - } - } - } - - return nil, nil -} - -func (p ast.WGConfig) GetInterfaceSection() (*ast.WGSection, bool) { - for _, section := range p.Sections { - if section.Header != nil && *section.Header == "Interface" { - return section, true - } - } - - return nil, false -} - -func (p ast.WGConfig) GetTypeByLine(line uint32) LineType { - // Check if line is a comment - if _, found := p.commentLines[line]; found { - return LineTypeComment - } - - if info, found := p.linesIndexes[line]; found { - return info.Type - } - - return LineTypeEmpty -} - -func CreateWireguardParser() ast.WGConfig { - parser := ast.WGConfig{} - parser.Clear() - - return parser -} - -type LineType string - -const ( - LineTypeComment LineType = "comment" - LineTypeEmpty LineType = "empty" - LineTypeHeader LineType = "header" - LineTypeProperty LineType = "property" -) - -func getLineType(line string) LineType { - if commentPattern.MatchString(line) { - return LineTypeComment - } - - if emptyLinePattern.MatchString(line) { - return LineTypeEmpty - } - - if headerPattern.MatchString(line) { - return LineTypeHeader - } - - return LineTypeProperty -} diff --git a/server/handlers/wireguard/parser/wg-parser_test.go b/server/handlers/wireguard/parser/wg-parser_test.go deleted file mode 100644 index c49fa54..0000000 --- a/server/handlers/wireguard/parser/wg-parser_test.go +++ /dev/null @@ -1,357 +0,0 @@ -package parser - -import ( - "config-lsp/utils" - "testing" - - "github.com/k0kubun/pp" -) - -func TestValidWildTestWorksFine( - t *testing.T, -) { - sample := utils.Dedent(` -[Interface] -PrivateKey = 1234567890 -Address = 192.168.1.0/24 - -# I'm a comment -[Peer] -PublicKey = 1234567890 -Endpoint = 1.2.3.4 ; I'm just a comment - -[Peer] -PublicKey = 5555 - `) - - parser := CreateWireguardParser() - errors := parser.ParseFromString(sample) - - if len(errors) > 0 { - t.Fatalf("parseFromString failed with error %v", errors) - } - - if !(len(parser.commentLines) == 1 && utils.KeyExists(parser.commentLines, 4)) { - t.Fatalf("parseFromString failed to collect comment lines %v", parser.commentLines) - } - - if !((len(parser.Sections) == 3) && (*parser.Sections[0].Header == "Interface") && (*parser.Sections[1].Header == "Peer") && (*parser.Sections[2].Header == "Peer")) { - t.Fatalf("parseFromString failed to collect sections %v", parser.Sections) - } - - if !(parser.Sections[0].StartLine == 0 && parser.Sections[0].EndLine == 2 && parser.Sections[1].StartLine == 5 && parser.Sections[1].EndLine == 7 && parser.Sections[2].StartLine == 9 && parser.Sections[2].EndLine == 10) { - t.Fatalf("parseFromString: Invalid start and end lines %v", parser.Sections) - } - - if !((len(parser.Sections[0].Properties) == 2) && (len(parser.Sections[1].Properties) == 2) && (len(parser.Sections[2].Properties) == 1)) { - t.Fatalf("parseFromString: Invalid amount of properties %v", parser.Sections) - } - - if !((parser.Sections[0].Properties[1].Key.Name == "PrivateKey") && (parser.Sections[0].Properties[2].Key.Name == "Address")) { - t.Fatalf("parseFromString failed to collect properties of section 0 %v", parser.Sections[0].Properties) - } - - if !((parser.Sections[1].Properties[6].Key.Name == "PublicKey") && (parser.Sections[1].Properties[7].Key.Name == "Endpoint")) { - t.Fatalf("parseFromString failed to collect properties of section 1 %v", parser.Sections[1].Properties) - } - - if !(parser.Sections[2].Properties[10].Key.Name == "PublicKey") { - t.Fatalf("parseFromString failed to collect properties of section 2 %v", parser.Sections[2].Properties) - } - - // Check if line indexes are correct - if !(parser.linesIndexes[0].Type == LineTypeHeader && - parser.linesIndexes[1].Type == LineTypeProperty && - parser.linesIndexes[2].Type == LineTypeProperty && - parser.linesIndexes[3].Type == LineTypeEmpty && - parser.linesIndexes[4].Type == LineTypeComment && - parser.linesIndexes[5].Type == LineTypeHeader && - parser.linesIndexes[6].Type == LineTypeProperty && - parser.linesIndexes[7].Type == LineTypeProperty && - parser.linesIndexes[8].Type == LineTypeEmpty && - parser.linesIndexes[9].Type == LineTypeHeader && - parser.linesIndexes[10].Type == LineTypeProperty) { - pp.Println(parser.linesIndexes) - t.Fatal("parseFromString: Invalid line indexes") - } -} - -func TestEmptySectionAtStartWorksFine( - t *testing.T, -) { - sample := utils.Dedent(` -[Interface] - -[Peer] -PublicKey = 1234567890 -`) - - parser := CreateWireguardParser() - errors := parser.ParseFromString(sample) - - if len(errors) > 0 { - t.Fatalf("parseFromString failed with error %v", errors) - } - - if !((len(parser.Sections) == 2) && (*parser.Sections[0].Header == "Interface") && (*parser.Sections[1].Header == "Peer")) { - t.Fatalf("parseFromString failed to collect sections %v", parser.Sections) - } - - if !(len(parser.Sections[0].Properties) == 0 && len(parser.Sections[1].Properties) == 1) { - t.Fatalf("parseFromString failed to collect properties %v", parser.Sections) - } -} - -func TestEmptySectionAtEndWorksFine( - t *testing.T, -) { - sample := utils.Dedent(` -[Inteface] -PrivateKey = 1234567890 - -[Peer] -# Just sneaking in here, hehe -`) - - parser := CreateWireguardParser() - errors := parser.ParseFromString(sample) - - if len(errors) > 0 { - t.Fatalf("parseFromString failed with error %v", errors) - } - - if !((len(parser.Sections) == 2) && (*parser.Sections[0].Header == "Inteface") && (*parser.Sections[1].Header == "Peer")) { - t.Fatalf("parseFromString failed to collect sections %v", parser.Sections) - } - - if !(len(parser.Sections[0].Properties) == 1 && len(parser.Sections[1].Properties) == 0) { - t.Fatalf("parseFromString failed to collect properties %v", parser.Sections) - } - - if !(len(parser.commentLines) == 1 && utils.KeyExists(parser.commentLines, 4)) { - t.Fatalf("parseFromString failed to collect comment lines %v", parser.commentLines) - } -} - -func TestEmptyFileWorksFine( - t *testing.T, -) { - sample := utils.Dedent(` -`) - - parser := CreateWireguardParser() - errors := parser.ParseFromString(sample) - - if len(errors) > 0 { - t.Fatalf("parseFromString failed with error %v", errors) - } - - if !(len(parser.Sections) == 1) { - t.Fatalf("parseFromString failed to collect sections %v", parser.Sections) - } -} - -func TestPartialSectionWithNoPropertiesWorksFine( - t *testing.T, -) { - sample := utils.Dedent(` -[Inte - -[Peer] -PublicKey = 1234567890 -`) - - parser := CreateWireguardParser() - errors := parser.ParseFromString(sample) - - if len(errors) > 0 { - t.Fatalf("parseFromString failed with error %v", errors) - } - - if !((len(parser.Sections) == 2) && (*parser.Sections[0].Header == "Inte") && (*parser.Sections[1].Header == "Peer")) { - t.Fatalf("parseFromString failed to collect sections: %v", parser.Sections) - } - - if !(len(parser.Sections[0].Properties) == 0 && len(parser.Sections[1].Properties) == 1) { - t.Fatalf("parseFromString failed to collect properties: %v", parser.Sections) - } - - if !(len(parser.commentLines) == 0) { - t.Fatalf("parseFromString failed to collect comment lines: %v", parser.commentLines) - } - - if !(parser.Sections[1].Properties[3].Key.Name == "PublicKey") { - t.Fatalf("parseFromString failed to collect properties of section 1: %v", parser.Sections[1].Properties) - } -} - -func TestPartialSectionWithPropertiesWorksFine( - t *testing.T, -) { - sample := utils.Dedent(` -[Inte -PrivateKey = 1234567890 - -[Peer] -`) - - parser := CreateWireguardParser() - errors := parser.ParseFromString(sample) - - if len(errors) > 0 { - t.Fatalf("parseFromString failed with error: %v", errors) - } - - if !((len(parser.Sections) == 2) && (*parser.Sections[0].Header == "Inte") && (*parser.Sections[1].Header == "Peer")) { - t.Fatalf("parseFromString failed to collect sections: %v", parser.Sections) - } - - if !(len(parser.Sections[0].Properties) == 1 && len(parser.Sections[1].Properties) == 0) { - t.Fatalf("parseFromString failed to collect properties: %v", parser.Sections) - } - - if !(parser.Sections[0].Properties[1].Key.Name == "PrivateKey") { - t.Fatalf("parseFromString failed to collect properties of section 0: %v", parser.Sections[0].Properties) - } -} - -func TestFileWithOnlyComments( - t *testing.T, -) { - sample := utils.Dedent(` -# This is a comment -# Another comment -`) - parser := CreateWireguardParser() - errors := parser.ParseFromString(sample) - - if len(errors) > 0 { - t.Fatalf("parseFromString failed with error: %v", errors) - } - - if !(len(parser.Sections) == 1) { - t.Fatalf("parseFromString failed to collect sections: %v", parser.Sections) - } - - if !(len(parser.commentLines) == 2) { - t.Fatalf("parseFromString failed to collect comment lines: %v", parser.commentLines) - } - - if !(utils.KeyExists(parser.commentLines, 0) && utils.KeyExists(parser.commentLines, 1)) { - t.Fatalf("parseFromString failed to collect comment lines: %v", parser.commentLines) - } -} - -func TestMultipleSectionsNoProperties( - t *testing.T, -) { - sample := utils.Dedent(` -[Interface] -[Peer] -[Peer] -`) - - parser := CreateWireguardParser() - errors := parser.ParseFromString(sample) - - if len(errors) > 0 { - t.Fatalf("parseFromString failed with error: %v", errors) - } - - if !(len(parser.Sections) == 3) { - t.Fatalf("parseFromString failed to collect sections: %v", parser.Sections) - } - - for _, section := range parser.Sections { - if len(section.Properties) != 0 { - t.Fatalf("parseFromString failed to collect properties: %v", section.Properties) - } - } -} - -func TestWildTest1WorksCorrectly( - t *testing.T, -) { - sample := utils.Dedent(` -[Interface] -DNS=1.1.1.1 - - -`) - parser := CreateWireguardParser() - errors := parser.ParseFromString(sample) - - if len(errors) > 0 { - t.Fatalf("parseFromString failed with error: %v", errors) - } - - if !(len(parser.Sections) == 1) { - t.Fatalf("parseFromString failed to collect sections: %v", parser.Sections) - } - - if !(len(parser.Sections[0].Properties) == 1) { - t.Fatalf("parseFromString failed to collect properties: %v", parser.Sections[0].Properties) - } - - if !(parser.Sections[0].Properties[1].Key.Name == "DNS") { - t.Fatalf("parseFromString failed to collect properties of section 0: %v", parser.Sections[0].Properties) - } - - if !(parser.Sections[0].Properties[1].Value.Value == "1.1.1.1") { - t.Fatalf("parseFromString failed to collect properties of section 0: %v", parser.Sections[0].Properties) - } - - if !(len(parser.commentLines) == 0) { - t.Fatalf("parseFromString failed to collect comment lines: %v", parser.commentLines) - } - - if !(parser.Sections[0].StartLine == 0 && parser.Sections[0].EndLine == 1) { - t.Fatalf("parseFromString: Invalid start and end lines %v", parser.Sections) - } -} - -func TestPartialKeyWorksCorrectly( - t *testing.T, -) { - sample := utils.Dedent(` -[Interface] -DNS -`) - parser := CreateWireguardParser() - errors := parser.ParseFromString(sample) - - if len(errors) > 0 { - t.Fatalf("parseFromString failed with error: %v", errors) - } - - if !(parser.Sections[0].Properties[1].Key.Name == "DNS") { - t.Fatalf("parseFromString failed to collect properties of section 0: %v", parser.Sections[0].Properties) - } - - if !(parser.Sections[0].Properties[1].Separator == nil) { - t.Fatalf("parseFromString failed to collect properties of section 0: %v", parser.Sections[0].Properties) - } -} - -func TestPartialValueWithSeparatorWorksCorrectly( - t *testing.T, -) { - sample := utils.Dedent(` -[Interface] -DNS= -`) - parser := CreateWireguardParser() - errors := parser.ParseFromString(sample) - - if len(errors) > 0 { - t.Fatalf("parseFromString failed with error: %v", errors) - } - - if !(parser.Sections[0].Properties[1].Value == nil) { - t.Fatalf("parseFromString failed to collect properties of section 0: %v", parser.Sections[0].Properties) - } - - if !(parser.Sections[0].Properties[1].Separator != nil) { - t.Fatalf("parseFromString failed to collect properties of section 0: %v", parser.Sections[0].Properties) - } -} diff --git a/server/handlers/wireguard/parser/wg-property.go b/server/handlers/wireguard/parser/wg-property.go deleted file mode 100644 index a96937b..0000000 --- a/server/handlers/wireguard/parser/wg-property.go +++ /dev/null @@ -1,96 +0,0 @@ -package parser - -import ( - docvalues "config-lsp/doc-values" - "config-lsp/handlers/wireguard/ast" - "config-lsp/utils" - "regexp" - "strings" - - protocol "github.com/tliron/glsp/protocol_3_16" -) - -var linePattern = regexp.MustCompile(`^\s*(?P.+?)\s*(?P=)\s*(?P\S.*?)?\s*(?:(?:;|#).*)?\s*$`) - -// WireguardProperties []: -type WireguardProperties map[uint32]ast.WGProperty - -func (p *WireguardProperties) AddLine(lineNumber uint32, line string) error { - property, err := CreateWireguardProperty(line) - - if err != nil { - return err - } - - (*p)[lineNumber] = *property - - return nil -} - -func CreateWireguardProperty(line string) (*ast.WGProperty, error) { - if !strings.Contains(line, "=") { - indexes := utils.GetTrimIndex(line) - - if indexes == nil { - // weird, should not happen - return nil, &docvalues.MalformedLineError{} - } - - return &ast.WGProperty{ - Key: ast.WGPropertyKey{ - Name: line[indexes[0]:indexes[1]], - Location: CharacterLocation{ - Start: uint32(indexes[0]), - End: uint32(indexes[1]), - }, - }, - }, nil - } - - indexes := linePattern.FindStringSubmatchIndex(line) - - if indexes == nil || len(indexes) == 0 { - return nil, &docvalues.MalformedLineError{} - } - - keyStart := uint32(indexes[2]) - keyEnd := uint32(indexes[3]) - key := ast.WGPropertyKey{ - Location: CharacterLocation{ - Start: keyStart, - End: keyEnd, - }, - Name: line[keyStart:keyEnd], - } - - separatorStart := uint32(indexes[4]) - separatorEnd := uint32(indexes[5]) - separator := ast.WGPropertySeparator{ - Location: CharacterLocation{ - Start: separatorStart, - End: separatorEnd, - }, - } - - var value *ast.WGPropertyValue - - if indexes[6] != -1 && indexes[7] != -1 { - // value exists - valueStart := uint32(indexes[6]) - valueEnd := uint32(indexes[7]) - - value = &ast.WGPropertyValue{ - Location: CharacterLocation{ - Start: valueStart, - End: valueEnd, - }, - Value: line[valueStart:valueEnd], - } - } - - return &ast.WGProperty{ - Key: key, - Separator: &separator, - Value: value, - }, nil -} diff --git a/server/handlers/wireguard/parser/wg-section.go b/server/handlers/wireguard/parser/wg-section.go deleted file mode 100644 index 8dc5097..0000000 --- a/server/handlers/wireguard/parser/wg-section.go +++ /dev/null @@ -1,73 +0,0 @@ -package parser - -import ( - "config-lsp/handlers/wireguard/ast" - "regexp" -) - -type PropertyNotFoundError struct{} - -func (e PropertyNotFoundError) Error() string { - return "Property not found" -} - -type PropertyNotFullyTypedError struct{} - -func (e PropertyNotFullyTypedError) Error() string { - return "Property not fully typed" -} - -func (s ast.WGSection) FetchFirstProperty(name string) (*uint32, *ast.WGProperty) { - for line, property := range s.Properties { - if property.Key.Name == name { - return &line, &property - } - } - - return nil, nil -} - -func (s ast.WGSection) ExistsProperty(name string) bool { - _, property := s.FetchFirstProperty(name) - - return property != nil -} - -func (s ast.WGSection) GetPropertyByLine(lineNumber uint32) (*ast.WGProperty, error) { - property, found := s.Properties[lineNumber] - - if !found { - return nil, PropertyNotFoundError{} - } - - return &property, nil -} - -var validHeaderPattern = regexp.MustCompile(`^\s*\[(?P
.+?)\]\s*$`) - -// Create a new create section -// Return (, ) -func CreateWireguardSection( - startLine uint32, - endLine uint32, - headerLine string, - props WireguardProperties, -) ast.WGSection { - match := validHeaderPattern.FindStringSubmatch(headerLine) - - var header string - - if match == nil { - // Still typing it - header = headerLine[1:] - } else { - header = match[1] - } - - return ast.WGSection{ - Header: &header, - StartLine: startLine, - EndLine: endLine, - Properties: props, - } -} diff --git a/server/handlers/wireguard/shared.go b/server/handlers/wireguard/shared.go index 0b280a9..3c243e6 100644 --- a/server/handlers/wireguard/shared.go +++ b/server/handlers/wireguard/shared.go @@ -1 +1,14 @@ package wireguard + +import ( + "config-lsp/handlers/wireguard/ast" + "config-lsp/handlers/wireguard/indexes" + protocol "github.com/tliron/glsp/protocol_3_16" +) + +type WGDocument struct { + Config *ast.WGConfig + Indexes *indexes.WGIndexes +} + +var DocumentParserMap = map[protocol.DocumentUri]*WGDocument{} From ff9b5db18a16b8a86089ea726c47b2579224de52 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 8 Mar 2025 15:56:09 +0100 Subject: [PATCH 15/31] refactor(server): Refactor Wireguard config; Improve completions; Improve code actions Signed-off-by: Myzel394 --- .../handlers/wireguard/analyzer/structure.go | 2 +- .../wireguard/ast/wireguard_fields.go | 21 ++++++ .../wireguard/handlers/code-actions.go | 66 +++++++------------ .../wireguard/handlers/completions_body.go | 61 ++++++++++------- .../wireguard/handlers/fetch-code-actions.go | 64 +++++++++++------- .../lsp/text-document-code-action.go | 8 ++- .../wireguard/lsp/text-document-hover.go | 6 +- .../lsp/workspace-execute-command.go | 15 ++--- 8 files changed, 136 insertions(+), 107 deletions(-) diff --git a/server/handlers/wireguard/analyzer/structure.go b/server/handlers/wireguard/analyzer/structure.go index 58a9c9e..b399312 100644 --- a/server/handlers/wireguard/analyzer/structure.go +++ b/server/handlers/wireguard/analyzer/structure.go @@ -55,7 +55,7 @@ func analyzeStructureIsValid(ctx *analyzerContext) { if property.Value == nil || property.Value.Value == "" { ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ Message: "This property is missing a value", - Range: property.Value.ToLSPRange(), + Range: property.ToLSPRange(), Severity: &common.SeverityError, }) } diff --git a/server/handlers/wireguard/ast/wireguard_fields.go b/server/handlers/wireguard/ast/wireguard_fields.go index 68d9694..053283d 100644 --- a/server/handlers/wireguard/ast/wireguard_fields.go +++ b/server/handlers/wireguard/ast/wireguard_fields.go @@ -1,6 +1,7 @@ package ast import ( + "config-lsp/utils" "slices" ) @@ -47,3 +48,23 @@ func (s *WGSection) FindFirstPropertyByName(name string) *WGProperty { return nil } + +func (s *WGSection) FindPropertyByName(name string) *WGProperty { + for _, property := range s.Properties { + if property.Key.Name == name { + return property + } + } + + return nil +} + +func (s *WGSection) GetLastProperty() *WGProperty { + if len(s.Properties) == 0 { + return nil + } + + lastLine := utils.FindBiggestKey(s.Properties) + + return s.Properties[lastLine] +} diff --git a/server/handlers/wireguard/handlers/code-actions.go b/server/handlers/wireguard/handlers/code-actions.go index 32dbf57..bbdad88 100644 --- a/server/handlers/wireguard/handlers/code-actions.go +++ b/server/handlers/wireguard/handlers/code-actions.go @@ -1,9 +1,10 @@ package handlers -/* import ( + "config-lsp/handlers/wireguard" "config-lsp/handlers/wireguard/ast" wgcommands "config-lsp/handlers/wireguard/commands" + protocol "github.com/tliron/glsp/protocol_3_16" ) @@ -12,7 +13,6 @@ type CodeActionName string const ( CodeActionGeneratePrivateKey CodeActionName = "generatePrivateKey" CodeActionGeneratePresharedKey CodeActionName = "generatePresharedKey" - CodeActionAddKeepalive CodeActionName = "addKeepalive" ) type CodeAction interface { @@ -33,14 +33,15 @@ func CodeActionGeneratePrivateKeyArgsFromArguments(arguments map[string]any) Cod } } -func (args CodeActionGeneratePrivateKeyArgs) RunCommand(p *ast.WGConfig) (*protocol.ApplyWorkspaceEditParams, error) { +func (args CodeActionGeneratePrivateKeyArgs) RunCommand(d *wireguard.WGDocument) (*protocol.ApplyWorkspaceEditParams, error) { privateKey, err := wgcommands.CreateNewPrivateKey() if err != nil { return &protocol.ApplyWorkspaceEditParams{}, err } - section, property := p.GetPropertyByLine(args.Line) + section := d.Config.FindSectionByLine(args.Line) + property := d.Config.FindPropertyByLine(args.Line) if section == nil || property == nil { return nil, nil @@ -54,7 +55,16 @@ func (args CodeActionGeneratePrivateKeyArgs) RunCommand(p *ast.WGConfig) (*proto args.URI: { { NewText: " " + privateKey, - Range: property.GetInsertRange(args.Line), + Range: protocol.Range{ + Start: protocol.Position{ + Line: property.End.Line, + Character: property.End.Character, + }, + End: protocol.Position{ + Line: property.End.Line, + Character: property.End.Character, + }, + }, }, }, }, @@ -74,14 +84,15 @@ func CodeActionGeneratePresharedKeyArgsFromArguments(arguments map[string]any) C } } -func (args CodeActionGeneratePresharedKeyArgs) RunCommand(p *ast.WGConfig) (*protocol.ApplyWorkspaceEditParams, error) { +func (args CodeActionGeneratePresharedKeyArgs) RunCommand(d *wireguard.WGDocument) (*protocol.ApplyWorkspaceEditParams, error) { presharedKey, err := wgcommands.CreatePresharedKey() if err != nil { return &protocol.ApplyWorkspaceEditParams{}, err } - section, property := p.GetPropertyByLine(args.Line) + section := d.Config.FindSectionByLine(args.Line) + property := d.Config.FindPropertyByLine(args.Line) if section == nil || property == nil { return nil, nil @@ -95,45 +106,14 @@ func (args CodeActionGeneratePresharedKeyArgs) RunCommand(p *ast.WGConfig) (*pro args.URI: { { NewText: " " + presharedKey, - Range: property.GetInsertRange(args.Line), - }, - }, - }, - }, - }, nil -} - -type CodeActionAddKeepaliveArgs struct { - URI protocol.DocumentUri - SectionIndex uint32 -} - -func CodeActionAddKeepaliveArgsFromArguments(arguments map[string]any) CodeActionAddKeepaliveArgs { - return CodeActionAddKeepaliveArgs{ - URI: arguments["URI"].(protocol.DocumentUri), - SectionIndex: uint32(arguments["SectionIndex"].(float64)), - } -} - -func (args CodeActionAddKeepaliveArgs) RunCommand(p *ast.WGConfig) (*protocol.ApplyWorkspaceEditParams, error) { - section := p.Sections[args.SectionIndex] - - label := "Add PersistentKeepalive" - return &protocol.ApplyWorkspaceEditParams{ - Label: &label, - Edit: protocol.WorkspaceEdit{ - Changes: map[protocol.DocumentUri][]protocol.TextEdit{ - args.URI: { - { - NewText: "PersistentKeepalive = 25\n", Range: protocol.Range{ Start: protocol.Position{ - Line: section.EndLine + 1, - Character: 0, + Line: property.End.Line, + Character: property.End.Character, }, End: protocol.Position{ - Line: section.EndLine + 1, - Character: 0, + Line: property.End.Line, + Character: property.End.Character, }, }, }, @@ -142,4 +122,4 @@ func (args CodeActionAddKeepaliveArgs) RunCommand(p *ast.WGConfig) (*protocol.Ap }, }, nil } -*/ + diff --git a/server/handlers/wireguard/handlers/completions_body.go b/server/handlers/wireguard/handlers/completions_body.go index e553dfc..b844734 100644 --- a/server/handlers/wireguard/handlers/completions_body.go +++ b/server/handlers/wireguard/handlers/completions_body.go @@ -86,13 +86,20 @@ func getPropertyCompletions( Address = 10.| */ - if property == nil || property.Separator == nil { + currentLine := params.Position.Line + position := common.LSPCharacterAsCursorPosition(params.Position.Character) + + // Special case, key defined but separator missing + if property != nil && property.Separator == nil && !property.Key.ContainsPosition(position) { + return getKeyCompletions(section, true, currentLine), nil + } + + if property == nil || property.Separator == nil || property.Key.ContainsPosition(position) { // First scenario - return getKeyCompletions(section), nil + return getKeyCompletions(section, false, currentLine), nil } // Check if the cursor it outside the value - position := common.LSPCharacterAsCursorPosition(params.Position.Character) if property.Value != nil && property.Value.IsPositionAfterEnd(position) { // Then we don't show anything return nil, nil @@ -104,45 +111,55 @@ func getPropertyCompletions( func getKeyCompletions( section ast.WGSection, + onlySeparator bool, + currentLine uint32, ) []protocol.CompletionItem { options := make(map[string]docvalues.DocumentationValue) + allowedDuplicatedFields := make(map[string]struct{}) switch section.Header.Name { case "Interface": maps.Copy(options, fields.InterfaceOptions) - - // Remove existing, non-duplicate options - for _, property := range section.Properties { - if _, found := fields.InterfaceAllowedDuplicateFields[property.Key.Name]; found { - continue - } - - delete(options, property.Key.Name) - } + allowedDuplicatedFields = fields.InterfaceAllowedDuplicateFields case "Peer": maps.Copy(options, fields.PeerOptions) + allowedDuplicatedFields = fields.PeerAllowedDuplicateFields + } - // Remove existing, non-duplicate options - for _, property := range section.Properties { - if _, found := fields.PeerAllowedDuplicateFields[property.Key.Name]; found { - continue - } - - delete(options, property.Key.Name) + // Remove existing, non-duplicate options + for _, property := range section.Properties { + if _, found := allowedDuplicatedFields[property.Key.Name]; found { + continue } + + if property.Key.Start.Line == currentLine { + // The user is currently typing the key, thus we should suggest it + continue + } + + delete(options, property.Key.Name) } kind := protocol.CompletionItemKindField return utils.MapMapToSlice( options, - func(optionName string, value docvalues.DocumentationValue) protocol.CompletionItem { - insertText := optionName + " = " + func(rawOptionName string, value docvalues.DocumentationValue) protocol.CompletionItem { + var label string + var insertText string + + if onlySeparator { + label = rawOptionName + " = " + insertText = "= " + } else { + label = rawOptionName + insertText = rawOptionName + " = " + } return protocol.CompletionItem{ Kind: &kind, Documentation: value.Documentation, - Label: optionName, + Label: label, InsertText: &insertText, } }, diff --git a/server/handlers/wireguard/handlers/fetch-code-actions.go b/server/handlers/wireguard/handlers/fetch-code-actions.go index 7a5a86b..389ceba 100644 --- a/server/handlers/wireguard/handlers/fetch-code-actions.go +++ b/server/handlers/wireguard/handlers/fetch-code-actions.go @@ -1,37 +1,52 @@ package handlers -/* import ( - "config-lsp/handlers/wireguard/ast" + "config-lsp/handlers/wireguard" "config-lsp/handlers/wireguard/commands" + protocol "github.com/tliron/glsp/protocol_3_16" ) func GetKeepaliveCodeActions( - p *ast.WGConfig, + d *wireguard.WGDocument, params *protocol.CodeActionParams, ) []protocol.CodeAction { line := params.Range.Start.Line - for index, section := range p.Sections { - if section.StartLine >= line && line <= section.EndLine && section.Header != nil && *section.Header == "Peer" { - if section.ExistsProperty("Endpoint") && !section.ExistsProperty("PersistentKeepalive") { - commandID := "wireguard." + CodeActionAddKeepalive - command := protocol.Command{ - Title: "Add PersistentKeepalive", - Command: string(commandID), - Arguments: []any{ - CodeActionAddKeepaliveArgs{ - URI: params.TextDocument.URI, - SectionIndex: uint32(index), - }, - }, + for _, section := range d.Indexes.SectionsByName["Peer"] { + if section.Start.Line >= line && line <= section.End.Line { + if section.FindPropertyByName("Endpoint") != nil && section.FindFirstPropertyByName("PersistentKeepalive") == nil { + var insertionLine uint32 + lastProperty := section.GetLastProperty() + + if lastProperty == nil { + insertionLine = section.End.Line + } else { + insertionLine = lastProperty.End.Line + 1 } return []protocol.CodeAction{ { Title: "Add PersistentKeepalive", - Command: &command, + Edit: &protocol.WorkspaceEdit{ + Changes: map[protocol.DocumentUri][]protocol.TextEdit{ + params.TextDocument.URI: { + { + Range: protocol.Range{ + Start: protocol.Position{ + Line: insertionLine, + Character: 0, + }, + End: protocol.Position{ + Line: insertionLine, + Character: 0, + }, + }, + NewText: "PersistentKeepalive = 25\n", + }, + }, + }, + }, }, } } @@ -42,11 +57,17 @@ func GetKeepaliveCodeActions( } func GetKeyGenerationCodeActions( - p *ast.WGConfig, + d *wireguard.WGDocument, params *protocol.CodeActionParams, ) []protocol.CodeAction { + if !wgcommands.AreWireguardToolsAvailable() { + return nil + } + line := params.Range.Start.Line - section, property := p.GetPropertyByLine(line) + + section := d.Config.FindSectionByLine(line) + property := d.Config.FindPropertyByLine(line) if section == nil || property == nil || property.Separator == nil { return nil @@ -54,10 +75,6 @@ func GetKeyGenerationCodeActions( switch property.Key.Name { case "PrivateKey": - if !wgcommands.AreWireguardToolsAvailable() { - return nil - } - commandID := "wireguard." + CodeActionGeneratePrivateKey command := protocol.Command{ Title: "Generate Private Key", @@ -103,4 +120,3 @@ func GetKeyGenerationCodeActions( return nil } -*/ diff --git a/server/handlers/wireguard/lsp/text-document-code-action.go b/server/handlers/wireguard/lsp/text-document-code-action.go index fe006e8..98bda8b 100644 --- a/server/handlers/wireguard/lsp/text-document-code-action.go +++ b/server/handlers/wireguard/lsp/text-document-code-action.go @@ -1,18 +1,20 @@ package lsp import ( + "config-lsp/handlers/wireguard" "config-lsp/handlers/wireguard/handlers" + "github.com/tliron/glsp" protocol "github.com/tliron/glsp/protocol_3_16" ) func TextDocumentCodeAction(context *glsp.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) { - p := documentParserMap[params.TextDocument.URI] + d := wireguard.DocumentParserMap[params.TextDocument.URI] actions := make([]protocol.CodeAction, 0, 2) - actions = append(actions, handlers.GetKeyGenerationCodeActions(p, params)...) - actions = append(actions, handlers.GetKeepaliveCodeActions(p, params)...) + actions = append(actions, handlers.GetKeyGenerationCodeActions(d, params)...) + actions = append(actions, handlers.GetKeepaliveCodeActions(d, params)...) if len(actions) > 0 { return actions, nil diff --git a/server/handlers/wireguard/lsp/text-document-hover.go b/server/handlers/wireguard/lsp/text-document-hover.go index d75dab3..bc54cec 100644 --- a/server/handlers/wireguard/lsp/text-document-hover.go +++ b/server/handlers/wireguard/lsp/text-document-hover.go @@ -1,10 +1,6 @@ package lsp import ( - "config-lsp/handlers/wireguard/handlers" - "config-lsp/handlers/wireguard/parser" - "strings" - "github.com/tliron/glsp" protocol "github.com/tliron/glsp/protocol_3_16" ) @@ -13,6 +9,7 @@ func TextDocumentHover( context *glsp.Context, params *protocol.HoverParams, ) (*protocol.Hover, error) { + /* p := documentParserMap[params.TextDocument.URI] switch p.GetTypeByLine(params.Position.Line) { @@ -37,6 +34,7 @@ func TextDocumentHover( } return &hover, nil } + */ return nil, nil } diff --git a/server/handlers/wireguard/lsp/workspace-execute-command.go b/server/handlers/wireguard/lsp/workspace-execute-command.go index cbae011..19cd6fc 100644 --- a/server/handlers/wireguard/lsp/workspace-execute-command.go +++ b/server/handlers/wireguard/lsp/workspace-execute-command.go @@ -1,6 +1,7 @@ package lsp import ( + "config-lsp/handlers/wireguard" "config-lsp/handlers/wireguard/handlers" "strings" @@ -15,21 +16,15 @@ func WorkspaceExecuteCommand(context *glsp.Context, params *protocol.ExecuteComm case string(handlers.CodeActionGeneratePrivateKey): args := handlers.CodeActionGeneratePrivateKeyArgsFromArguments(params.Arguments[0].(map[string]any)) - p := documentParserMap[args.URI] + d := wireguard.DocumentParserMap[args.URI] - return args.RunCommand(p) + return args.RunCommand(d) case string(handlers.CodeActionGeneratePresharedKey): args := handlers.CodeActionGeneratePresharedKeyArgsFromArguments(params.Arguments[0].(map[string]any)) - parser := documentParserMap[args.URI] + d := wireguard.DocumentParserMap[args.URI] - return args.RunCommand(parser) - case string(handlers.CodeActionAddKeepalive): - args := handlers.CodeActionAddKeepaliveArgsFromArguments(params.Arguments[0].(map[string]any)) - - p := documentParserMap[args.URI] - - return args.RunCommand(p) + return args.RunCommand(d) } return nil, nil From d3f6122eefc1913f1b9223b06802855f8b359702 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 8 Mar 2025 16:03:06 +0100 Subject: [PATCH 16/31] feat(server): Add FindBiggestKey utils Signed-off-by: Myzel394 --- server/utils/slices.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/server/utils/slices.go b/server/utils/slices.go index 5f3b5ec..673a491 100644 --- a/server/utils/slices.go +++ b/server/utils/slices.go @@ -150,3 +150,15 @@ func Without[T comparable](a []T, b []T) []T { return !found }) } + +func FindBiggestKey[T any](values map[uint32]T) uint32 { + var biggestKey uint32 + + for k := range values { + if k > biggestKey { + biggestKey = k + } + } + + return biggestKey +} From 1227949f26264d7357c9ff4bc85c6ef914e0b288 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 9 Mar 2025 19:28:25 +0100 Subject: [PATCH 17/31] fix(server): Improve wireguard Signed-off-by: Myzel394 --- .../handlers/wireguard/analyzer/structure.go | 21 ++++--- server/handlers/wireguard/ast/parser.go | 12 +++- server/handlers/wireguard/ast/parser_test.go | 12 +++- server/handlers/wireguard/ast/wireguard.go | 5 +- .../wireguard/ast/wireguard_fields.go | 23 +++++--- server/handlers/wireguard/fields/common.go | 9 +++ .../{documentation-fields.go => fields.go} | 56 +++++++++---------- .../wireguard/fields/fields_formatted.go | 23 ++++++++ .../wireguard/handlers/code-actions.go | 1 - .../wireguard/handlers/completions_body.go | 32 +++++++---- .../wireguard/handlers/completions_test.go | 4 +- .../wireguard/handlers/fetch-code-actions.go | 2 +- .../wireguard/lsp/text-document-hover.go | 42 +++++++------- 13 files changed, 155 insertions(+), 87 deletions(-) create mode 100644 server/handlers/wireguard/fields/common.go rename server/handlers/wireguard/fields/{documentation-fields.go => fields.go} (94%) create mode 100644 server/handlers/wireguard/fields/fields_formatted.go diff --git a/server/handlers/wireguard/analyzer/structure.go b/server/handlers/wireguard/analyzer/structure.go index b399312..9f977a5 100644 --- a/server/handlers/wireguard/analyzer/structure.go +++ b/server/handlers/wireguard/analyzer/structure.go @@ -12,6 +12,7 @@ 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 @@ -21,7 +22,7 @@ func analyzeStructureIsValid(ctx *analyzerContext) { Range: section.Header.ToLSPRange(), Severity: &common.SeverityError, }) - } else if !utils.KeyExists(fields.OptionsHeaderMap, section.Header.Name) { + } else if !utils.KeyExists(fields.OptionsHeaderMap, normalizedHeaderName) { ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ Message: fmt.Sprintf("Unknown section '%s'. It must be one of: [Interface], [Peer]", section.Header.Name), Range: section.Header.ToLSPRange(), @@ -31,7 +32,7 @@ func analyzeStructureIsValid(ctx *analyzerContext) { checkAllowedProperty = false } - if len(section.Properties) == 0 { + if section.Properties.Size() == 0 { ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ Message: "This section is empty", Range: section.Header.ToLSPRange(), @@ -41,9 +42,13 @@ func analyzeStructureIsValid(ctx *analyzerContext) { }, }) } else { - existingProperties := make(map[string]*ast.WGProperty) + 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) - for _, property := range section.Properties { if property.Key.Name == "" { ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ Message: "This property is missing a name", @@ -61,21 +66,23 @@ func analyzeStructureIsValid(ctx *analyzerContext) { } if checkAllowedProperty { - options := fields.OptionsHeaderMap[section.Header.Name] + options := fields.OptionsHeaderMap[normalizedHeaderName] - if !utils.KeyExists(options, property.Key.Name) { + if !utils.KeyExists(options, normalizedPropertyName) { ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ Message: fmt.Sprintf("Unknown property '%s'", property.Key.Name), Range: property.Key.ToLSPRange(), Severity: &common.SeverityError, }) - } else if existingProperty, found := existingProperties[property.Key.Name]; found { + } else 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(), }) } + + existingProperties[normalizedPropertyName] = property } } } diff --git a/server/handlers/wireguard/ast/parser.go b/server/handlers/wireguard/ast/parser.go index 441232c..ebed861 100644 --- a/server/handlers/wireguard/ast/parser.go +++ b/server/handlers/wireguard/ast/parser.go @@ -6,6 +6,9 @@ import ( "fmt" "regexp" "strings" + + "github.com/emirpasic/gods/maps/treemap" + gods "github.com/emirpasic/gods/utils" ) func NewWGConfig() *WGConfig { @@ -80,7 +83,7 @@ func (c *WGConfig) Parse(input string) []common.LSPError { }, Name: name, }, - Properties: make(map[uint32]*WGProperty), + Properties: treemap.NewWith(gods.UInt32Comparator), } c.Sections = append(c.Sections, currentSection) @@ -119,7 +122,7 @@ func (c *WGConfig) Parse(input string) []common.LSPError { // Incomplete property indexes := utils.GetTrimIndex(line) - currentSection.Properties[lineNumber] = &WGProperty{ + newProperty := &WGProperty{ Key: WGPropertyKey{ LocationRange: common.LocationRange{ Start: common.Location{ @@ -134,6 +137,8 @@ func (c *WGConfig) Parse(input string) []common.LSPError { Name: line[indexes[0]:indexes[1]], }, } + + currentSection.Properties.Put(lineNumber, newProperty) } else { // Fully written out property @@ -217,7 +222,7 @@ func (c *WGConfig) Parse(input string) []common.LSPError { } // And lastly, add the property - currentSection.Properties[lineNumber] = &WGProperty{ + newProperty := &WGProperty{ LocationRange: common.LocationRange{ Start: common.Location{ Line: lineNumber, @@ -233,6 +238,7 @@ func (c *WGConfig) Parse(input string) []common.LSPError { Separator: &separator, Value: value, } + currentSection.Properties.Put(lineNumber, newProperty) } } diff --git a/server/handlers/wireguard/ast/parser_test.go b/server/handlers/wireguard/ast/parser_test.go index 2a0981b..e75f263 100644 --- a/server/handlers/wireguard/ast/parser_test.go +++ b/server/handlers/wireguard/ast/parser_test.go @@ -48,15 +48,21 @@ PublicKey = 1234567890 t.Errorf("Parse: Expected sections to be present on lines 0, 1, and 2") } - if !(config.Sections[0].Properties[4].Key.Name == "PrivateKey" && config.Sections[0].Properties[4].Value.Value == "1234567890") { + rawFourthProperty, _ := config.Sections[0].Properties.Get(uint32(4)) + fourthProperty := rawFourthProperty.(*WGProperty) + if !(fourthProperty.Key.Name == "PrivateKey" && fourthProperty.Value.Value == "1234567890") { t.Errorf("Parse: Expected property line 4 to be correct") } - if !(config.Sections[0].Properties[5].Key.Name == "Address" && config.Sections[0].Properties[5].Value.Value == "10.0.0.1") { + rawFifthProperty, _ := config.Sections[0].Properties.Get(uint32(5)) + fifthProperty := rawFifthProperty.(*WGProperty) + if !(fifthProperty.Key.Name == "Address" && fifthProperty.Value.Value == "10.0.0.1") { t.Errorf("Parse: Expected property line 5 to be correct") } - if !(config.Sections[1].Properties[10].Key.Name == "PublicKey" && config.Sections[1].Properties[10].Value.Value == "1234567890") { + rawTenthProperty, _ := config.Sections[1].Properties.Get(uint32(10)) + tenthProperty := rawTenthProperty.(*WGProperty) + if !(tenthProperty.Key.Name == "PublicKey" && tenthProperty.Value.Value == "1234567890") { t.Errorf("Parse: Expected property line 10 to be correct") } } diff --git a/server/handlers/wireguard/ast/wireguard.go b/server/handlers/wireguard/ast/wireguard.go index ed29233..5c3d520 100644 --- a/server/handlers/wireguard/ast/wireguard.go +++ b/server/handlers/wireguard/ast/wireguard.go @@ -2,6 +2,7 @@ package ast import ( "config-lsp/common" + "github.com/emirpasic/gods/maps/treemap" ) type WGPropertyKey struct { @@ -34,8 +35,8 @@ type WGHeader struct { type WGSection struct { common.LocationRange Header WGHeader - // map of: line number -> WGProperty - Properties map[uint32]*WGProperty + // [uint32]WGProperty: line number -> WGProperty + Properties *treemap.Map } type WGConfig struct { diff --git a/server/handlers/wireguard/ast/wireguard_fields.go b/server/handlers/wireguard/ast/wireguard_fields.go index 053283d..5135153 100644 --- a/server/handlers/wireguard/ast/wireguard_fields.go +++ b/server/handlers/wireguard/ast/wireguard_fields.go @@ -1,7 +1,6 @@ package ast import ( - "config-lsp/utils" "slices" ) @@ -36,11 +35,17 @@ func (c *WGConfig) FindPropertyByLine(line uint32) *WGProperty { return nil } - return section.Properties[line] + if property, found := section.Properties.Get(line); found { + return property.(*WGProperty) + } + + return nil } func (s *WGSection) FindFirstPropertyByName(name string) *WGProperty { - for _, property := range s.Properties { + it := s.Properties.Iterator() + for it.Next() { + property := it.Value().(*WGProperty) if property.Key.Name == name { return property } @@ -50,7 +55,9 @@ func (s *WGSection) FindFirstPropertyByName(name string) *WGProperty { } func (s *WGSection) FindPropertyByName(name string) *WGProperty { - for _, property := range s.Properties { + it := s.Properties.Iterator() + for it.Next() { + property := it.Value().(*WGProperty) if property.Key.Name == name { return property } @@ -60,11 +67,11 @@ func (s *WGSection) FindPropertyByName(name string) *WGProperty { } func (s *WGSection) GetLastProperty() *WGProperty { - if len(s.Properties) == 0 { + if s.Properties.Size() == 0 { return nil } - lastLine := utils.FindBiggestKey(s.Properties) - - return s.Properties[lastLine] + lastLine, _ := s.Properties.Max() + lastProperty, _ := s.Properties.Get(lastLine) + return lastProperty.(*WGProperty) } diff --git a/server/handlers/wireguard/fields/common.go b/server/handlers/wireguard/fields/common.go new file mode 100644 index 0000000..5881ab5 --- /dev/null +++ b/server/handlers/wireguard/fields/common.go @@ -0,0 +1,9 @@ +package fields + +import "strings" + +type NormalizedName string + +func CreateNormalizedName(s string) NormalizedName { + return NormalizedName(strings.ToLower(s)) +} diff --git a/server/handlers/wireguard/fields/documentation-fields.go b/server/handlers/wireguard/fields/fields.go similarity index 94% rename from server/handlers/wireguard/fields/documentation-fields.go rename to server/handlers/wireguard/fields/fields.go index 20204ce..6224d01 100644 --- a/server/handlers/wireguard/fields/documentation-fields.go +++ b/server/handlers/wireguard/fields/fields.go @@ -25,8 +25,8 @@ var maxPortValue = 65535 var minMTUValue = 68 var maxMTUValue = 1500 -var InterfaceOptions = map[string]docvalues.DocumentationValue{ - "Address": { +var InterfaceOptions = map[NormalizedName]docvalues.DocumentationValue{ + "address": { Documentation: `Defines what address range the local node should route traffic for. Depending on whether the node is a simple client joining the VPN subnet, or a bounce server that's relaying traffic between multiple clients, this can be set to a single IP of the node itself (specified with CIDR notation), e.g. 192.0.2.3/32), or a range of IPv4/IPv6 subnets that the node can route traffic for. ## Examples @@ -49,7 +49,7 @@ You can also specify multiple subnets or IPv6 subnets like so: AllowRange: true, }, }, - "ListenPort": { + "listenport": { Documentation: `When the node is acting as a public bounce server, it should hardcode a port to listen for incoming VPN connections from the public internet. Clients not acting as relays should not set this value. If not specified, chosen randomly. ## Examples @@ -66,14 +66,14 @@ Using custom WireGuard port Max: &maxPortValue, }, }, - "PrivateKey": { + "privatekey": { Documentation: `This is the private key for the local node, never shared with other servers. All nodes must have a private key set, regardless of whether they are public bounce servers relaying traffic, or simple clients joining the VPN. This key can be generated with [wg genkey > example.key] `, Value: docvalues.StringValue{}, }, - "DNS": { + "dns": { Documentation: `The DNS server(s) to announce to VPN clients via DHCP, most clients will use this server for DNS requests over the VPN, but clients can also override this value locally on their nodes The value can be left unconfigured to use the system's default DNS servers @@ -97,7 +97,7 @@ or multiple DNS servers can be provided }, }, }, - "Table": { + "table": { Documentation: `Optionally defines which routing table to use for the WireGuard routes, not necessary to configure for most setups. There are two special values: ‘off’ disables the creation of routes altogether, and ‘auto’ (the default) adds routes to the default table and enables special handling of default routes. @@ -127,7 +127,7 @@ https://git.zx2c4.com/WireGuard/about/src/tools/man/wg-quick.8 }, }, }, - "MTU": { + "mtu": { Documentation: `Optionally defines the maximum transmission unit (MTU, aka packet/frame size) to use when connecting to the peer, not necessary to configure for most setups. The MTU is automatically determined from the endpoint addresses or the system default route, which is usually a sane choice. @@ -142,7 +142,7 @@ https://git.zx2c4.com/WireGuard/about/src/tools/man/wg-quick.8 Max: &maxMTUValue, }, }, - "PreUp": { + "preup": { Documentation: `Optionally run a command before the interface is brought up. This option can be specified multiple times, with commands executed in the order they appear in the file. ## Examples @@ -152,7 +152,7 @@ Add an IP route PreUp = ip rule add ipproto tcp dport 22 table 1234 `, Value: docvalues.StringValue{}, }, - "PostUp": { + "postup": { Documentation: `Optionally run a command after the interface is brought up. This option can appear multiple times, as with PreUp ## Examples @@ -182,7 +182,7 @@ Force WireGuard to re-resolve IP address for peer domain `, Value: docvalues.StringValue{}, }, - "PreDown": { + "predown": { Documentation: `Optionally run a command before the interface is brought down. This option can appear multiple times, as with PreUp ## Examples @@ -196,7 +196,7 @@ Hit a webhook on another server `, Value: docvalues.StringValue{}, }, - "PostDown": { + "postdown": { Documentation: `Optionally run a command after the interface is brought down. This option can appear multiple times, as with PreUp ## Examples @@ -215,21 +215,21 @@ Remove the iptables rule that forwards packets on the WireGuard interface `, Value: docvalues.StringValue{}, }, - "FwMark": { + "fwmark": { Documentation: "a 32-bit fwmark for outgoing packets. If set to 0 or \"off\", this option is disabled. May be specified in hexadecimal by prepending \"0x\". Optional", Value: docvalues.StringValue{}, }, } -var InterfaceAllowedDuplicateFields = map[string]struct{}{ - "PreUp": {}, - "PostUp": {}, - "PreDown": {}, - "PostDown": {}, +var InterfaceAllowedDuplicateFields = map[NormalizedName]struct{}{ + "preup": {}, + "postup": {}, + "predown": {}, + "postdown": {}, } -var PeerOptions = map[string]docvalues.DocumentationValue{ - "Endpoint": { +var PeerOptions = map[NormalizedName]docvalues.DocumentationValue{ + "endpoint": { Documentation: `Defines the publicly accessible address for a remote peer. This should be left out for peers behind a NAT or peers that don't have a stable publicly accessible IP:PORT pair. Typically, this only needs to be defined on the main bounce server, but it can also be defined on other public nodes with stable IPs like public-server2 in the example config below. ## Examples @@ -243,7 +243,7 @@ Endpoint is a hostname/FQDN `, Value: docvalues.StringValue{}, }, - "AllowedIPs": { + "allowedips": { Documentation: `This defines the IP ranges for which a peer will route traffic. On simple clients, this is usually a single address (the VPN address of the simple client itself). For bounce servers this will be a range of the IPs or subnets that the relay server is capable of routing traffic for. Multiple IPs and subnets may be specified using comma-separated IPv4 or IPv6 CIDR notation (from a single /32 or /128 address, all the way up to 0.0.0.0/0 and ::/0 to indicate a default route to send all internet and VPN traffic through that peer). This option may be specified multiple times. When deciding how to route a packet, the system chooses the most specific route first, and falls back to broader routes. So for a packet destined to 192.0.2.3, the system would first look for a peer advertising 192.0.2.3/32 specifically, and would fall back to a peer advertising 192.0.2.1/24 or a larger range like 0.0.0.0/0 as a last resort. @@ -280,7 +280,7 @@ Peer is a relay server that routes to itself and all nodes on its local LAN }, }, }, - "PublicKey": { + "publickey": { Documentation: `This is the public key for the remote node, shareable with all peers. All nodes must have a public key set, regardless of whether they are public bounce servers relaying traffic, or simple clients joining the VPN. This key can be generated with wg pubkey < example.key > example.key.pub. (see above for how to generate the private key example.key) @@ -291,7 +291,7 @@ This key can be generated with wg pubkey < example.key > example.key.pub. (see a `, Value: docvalues.StringValue{}, }, - "PersistentKeepalive": { + "persistentkeepalive": { Documentation: `If the connection is going from a NAT-ed peer to a public peer, the node behind the NAT must regularly send an outgoing ping in order to keep the bidirectional connection alive in the NAT router's connection table. ## Examples @@ -310,17 +310,17 @@ Oocal NAT-ed node to remote public node `, Value: docvalues.PositiveNumberValue(), }, - "PresharedKey": { + "presharedkey": { Documentation: "Optionally defines a pre-shared key for the peer, used to authenticate the connection. This is not necessary, but strongly recommended for security.", Value: docvalues.StringValue{}, }, } -var PeerAllowedDuplicateFields = map[string]struct{}{ - "AllowedIPs": {}, +var PeerAllowedDuplicateFields = map[NormalizedName]struct{}{ + "allowedips": {}, } -var OptionsHeaderMap = map[string](map[string]docvalues.DocumentationValue){ - "Interface": InterfaceOptions, - "Peer": PeerOptions, +var OptionsHeaderMap = map[NormalizedName](map[NormalizedName]docvalues.DocumentationValue){ + "interface": InterfaceOptions, + "peer": PeerOptions, } diff --git a/server/handlers/wireguard/fields/fields_formatted.go b/server/handlers/wireguard/fields/fields_formatted.go new file mode 100644 index 0000000..fc4ed4b --- /dev/null +++ b/server/handlers/wireguard/fields/fields_formatted.go @@ -0,0 +1,23 @@ +package fields + +var AllOptionsFormatted = map[NormalizedName]string{ + // Interface + "address": "Address", + "listenport": "ListenPort", + "privatekey": "PrivateKey", + "dns": "DNS", + "table": "Table", + "mtu": "MTU", + "preup": "PreUp", + "postup": "PostUp", + "predown": "Predown", + "postdown": "PostDown", + "fwmark": "FwMark", + + // Peer Options + "endpoint": "Endpoint", + "allowedips": "AllowedIPs", + "publickey": "PublicKey", + "persistentkeepalive": "PersistentKeepalive", + "presharedkey": "PresharedKey", +} diff --git a/server/handlers/wireguard/handlers/code-actions.go b/server/handlers/wireguard/handlers/code-actions.go index bbdad88..566890b 100644 --- a/server/handlers/wireguard/handlers/code-actions.go +++ b/server/handlers/wireguard/handlers/code-actions.go @@ -122,4 +122,3 @@ func (args CodeActionGeneratePresharedKeyArgs) RunCommand(d *wireguard.WGDocumen }, }, nil } - diff --git a/server/handlers/wireguard/handlers/completions_body.go b/server/handlers/wireguard/handlers/completions_body.go index b844734..051f9c0 100644 --- a/server/handlers/wireguard/handlers/completions_body.go +++ b/server/handlers/wireguard/handlers/completions_body.go @@ -55,6 +55,11 @@ func GetSectionBodyCompletions( // In this case, the user may want to add a property or add a new section. // We should therefore suggest both options. + isLineEmpty := property == nil + if !isLineEmpty { + return completions, nil + } + // Check if previous line is empty previousLineProperty := d.Config.FindPropertyByLine(params.Position.Line - 1) @@ -114,8 +119,8 @@ func getKeyCompletions( onlySeparator bool, currentLine uint32, ) []protocol.CompletionItem { - options := make(map[string]docvalues.DocumentationValue) - allowedDuplicatedFields := make(map[string]struct{}) + options := make(map[fields.NormalizedName]docvalues.DocumentationValue) + allowedDuplicatedFields := make(map[fields.NormalizedName]struct{}) switch section.Header.Name { case "Interface": @@ -127,8 +132,11 @@ func getKeyCompletions( } // Remove existing, non-duplicate options - for _, property := range section.Properties { - if _, found := allowedDuplicatedFields[property.Key.Name]; found { + it := section.Properties.Iterator() + for it.Next() { + property := it.Value().(*ast.WGProperty) + normalizedName := fields.CreateNormalizedName(property.Key.Name) + if _, found := allowedDuplicatedFields[normalizedName]; found { continue } @@ -137,23 +145,24 @@ func getKeyCompletions( continue } - delete(options, property.Key.Name) + delete(options, normalizedName) } kind := protocol.CompletionItemKindField return utils.MapMapToSlice( options, - func(rawOptionName string, value docvalues.DocumentationValue) protocol.CompletionItem { + func(rawOptionName fields.NormalizedName, value docvalues.DocumentationValue) protocol.CompletionItem { + optionName := fields.AllOptionsFormatted[rawOptionName] var label string var insertText string if onlySeparator { - label = rawOptionName + " = " + label = optionName + " = " insertText = "= " } else { - label = rawOptionName - insertText = rawOptionName + " = " + label = optionName + insertText = optionName + " = " } return protocol.CompletionItem{ @@ -172,13 +181,14 @@ func getValueCompletions( cursorPosition common.CursorPosition, ) []protocol.CompletionItem { // TODO: Normalize section header name - options, found := fields.OptionsHeaderMap[section.Header.Name] + normalizedHeaderName := fields.CreateNormalizedName(section.Header.Name) + options, found := fields.OptionsHeaderMap[normalizedHeaderName] if !found { return nil } - option, found := options[property.Key.Name] + option, found := options[fields.CreateNormalizedName(property.Key.Name)] if !found { return nil diff --git a/server/handlers/wireguard/handlers/completions_test.go b/server/handlers/wireguard/handlers/completions_test.go index c8f4118..fce079e 100644 --- a/server/handlers/wireguard/handlers/completions_test.go +++ b/server/handlers/wireguard/handlers/completions_test.go @@ -227,10 +227,10 @@ Table = completions, err := SuggestCompletions(d, params) if err != nil { - t.Errorf("getCompletionsForPropertyLine failed with error: %v", err) + t.Errorf("SuggestComplete failed with error: %v", err) } if !(len(completions) == 2) { - t.Errorf("getCompletionsForPropertyLine: Expected completions, but got %v", len(completions)) + t.Errorf("Expected 2 completions, but got %v", len(completions)) } } diff --git a/server/handlers/wireguard/handlers/fetch-code-actions.go b/server/handlers/wireguard/handlers/fetch-code-actions.go index 389ceba..f4263f4 100644 --- a/server/handlers/wireguard/handlers/fetch-code-actions.go +++ b/server/handlers/wireguard/handlers/fetch-code-actions.go @@ -27,7 +27,7 @@ func GetKeepaliveCodeActions( return []protocol.CodeAction{ { - Title: "Add PersistentKeepalive", + Title: "Add PersistentKeepalive", Edit: &protocol.WorkspaceEdit{ Changes: map[protocol.DocumentUri][]protocol.TextEdit{ params.TextDocument.URI: { diff --git a/server/handlers/wireguard/lsp/text-document-hover.go b/server/handlers/wireguard/lsp/text-document-hover.go index bc54cec..1f2945f 100644 --- a/server/handlers/wireguard/lsp/text-document-hover.go +++ b/server/handlers/wireguard/lsp/text-document-hover.go @@ -10,30 +10,30 @@ func TextDocumentHover( params *protocol.HoverParams, ) (*protocol.Hover, error) { /* - p := documentParserMap[params.TextDocument.URI] + p := documentParserMap[params.TextDocument.URI] - switch p.GetTypeByLine(params.Position.Line) { - case parser.LineTypeComment: - return nil, nil - case parser.LineTypeEmpty: - return nil, nil - case parser.LineTypeHeader: - fallthrough - case parser.LineTypeProperty: - documentation := handlers.GetHoverContent( - *p, - params.Position.Line, - params.Position.Character, - ) + switch p.GetTypeByLine(params.Position.Line) { + case parser.LineTypeComment: + return nil, nil + case parser.LineTypeEmpty: + return nil, nil + case parser.LineTypeHeader: + fallthrough + case parser.LineTypeProperty: + documentation := handlers.GetHoverContent( + *p, + params.Position.Line, + params.Position.Character, + ) - hover := protocol.Hover{ - Contents: protocol.MarkupContent{ - Kind: protocol.MarkupKindMarkdown, - Value: strings.Join(documentation, "\n"), - }, + hover := protocol.Hover{ + Contents: protocol.MarkupContent{ + Kind: protocol.MarkupKindMarkdown, + Value: strings.Join(documentation, "\n"), + }, + } + return &hover, nil } - return &hover, nil - } */ return nil, nil From e14866bcdccd1ed4f1462703a515cfb4a697bc51 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 9 Mar 2025 19:49:23 +0100 Subject: [PATCH 18/31] fix(server): Improve wireguard Signed-off-by: Myzel394 --- .../handlers/wireguard/analyzer/structure.go | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/server/handlers/wireguard/analyzer/structure.go b/server/handlers/wireguard/analyzer/structure.go index 9f977a5..96bb79a 100644 --- a/server/handlers/wireguard/analyzer/structure.go +++ b/server/handlers/wireguard/analyzer/structure.go @@ -2,6 +2,7 @@ package analyzer import ( "config-lsp/common" + docvalues "config-lsp/doc-values" "config-lsp/handlers/wireguard/ast" "config-lsp/handlers/wireguard/fields" "config-lsp/utils" @@ -63,23 +64,39 @@ func analyzeStructureIsValid(ctx *analyzerContext) { Range: property.ToLSPRange(), Severity: &common.SeverityError, }) + checkAllowedProperty = false } if checkAllowedProperty { - options := fields.OptionsHeaderMap[normalizedHeaderName] + availableOptions := fields.OptionsHeaderMap[normalizedHeaderName] - if !utils.KeyExists(options, normalizedPropertyName) { - ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ - Message: fmt.Sprintf("Unknown property '%s'", property.Key.Name), - Range: property.Key.ToLSPRange(), - Severity: &common.SeverityError, - }) - } else if existingProperty, found := existingProperties[normalizedPropertyName]; found { + // 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 From b94d98756598d097c595b2bd81c616fb8868dbd7 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 9 Mar 2025 20:10:50 +0100 Subject: [PATCH 19/31] fix(server): Improve wireguard hover lsp Signed-off-by: Myzel394 --- server/handlers/wireguard/handlers/hover.go | 134 +++++++----------- .../wireguard/lsp/text-document-hover.go | 49 +++---- 2 files changed, 79 insertions(+), 104 deletions(-) diff --git a/server/handlers/wireguard/handlers/hover.go b/server/handlers/wireguard/handlers/hover.go index 8d652d1..c0765f4 100644 --- a/server/handlers/wireguard/handlers/hover.go +++ b/server/handlers/wireguard/handlers/hover.go @@ -1,107 +1,81 @@ package handlers -/* import ( + "config-lsp/common" docvalues "config-lsp/doc-values" + "config-lsp/handlers/wireguard" "config-lsp/handlers/wireguard/ast" "config-lsp/handlers/wireguard/fields" "fmt" "strings" + + protocol "github.com/tliron/glsp/protocol_3_16" ) -func getPropertyInfo( - p ast.WGProperty, - cursor uint32, +func GetPropertyHoverInfo( + d *wireguard.WGDocument, section ast.WGSection, -) []string { - if cursor <= p.Key.Location.End { - options, found := fields.OptionsHeaderMap[*section.Header] - - if !found { - return []string{} - } - - option, found := options[p.Key.Name] - - if !found { - return []string{} - } - - return strings.Split(option.Documentation, "\n") - } - - options, found := fields.OptionsHeaderMap[*section.Header] + property ast.WGProperty, + index common.IndexPosition, +) (*protocol.Hover, error) { + availableOptions, found := fields.OptionsHeaderMap[fields.CreateNormalizedName(section.Header.Name)] if !found { - return []string{} + return nil, nil } - if option, found := options[p.Key.Name]; found { - return option.GetTypeDescription() + option, found := availableOptions[fields.CreateNormalizedName(property.Key.Name)] + + if !found { + return nil, nil } - return []string{} + if property.Key.ContainsPosition(index) { + return &protocol.Hover{ + Contents: protocol.MarkupContent{ + Kind: protocol.MarkupKindMarkdown, + Value: option.Documentation, + }, + }, nil + } + + if property.Value != nil && property.Value.ContainsPosition(index) { + return &protocol.Hover{ + Contents: protocol.MarkupContent{ + Kind: protocol.MarkupKindMarkdown, + Value: strings.Join(option.GetTypeDescription(), "\n"), + }, + }, nil + } + + return nil, nil } -func getSectionInfo(s ast.WGSection) []string { - if s.Header == nil { - return []string{} - } +func GetSectionHoverInfo( + d *wireguard.WGDocument, + section ast.WGSection, +) (*protocol.Hover, error) { + var docValue *docvalues.EnumString = nil - contents := []string{ - fmt.Sprintf("## [%s]", *s.Header), - "", - } - - var option *docvalues.EnumString = nil - - switch *s.Header { + switch section.Header.Name { case "Interface": - option = &fields.HeaderInterfaceEnum + docValue = &fields.HeaderInterfaceEnum case "Peer": - option = &fields.HeaderPeerEnum + docValue = &fields.HeaderPeerEnum } - if option == nil { - return contents + if docValue == nil { + return nil, nil } - contents = append(contents, strings.Split(option.Documentation, "\n")...) - - return contents + return &protocol.Hover{ + Contents: protocol.MarkupContent{ + Kind: protocol.MarkupKindMarkdown, + Value: fmt.Sprintf( + "## [%s]\n\n%s", + section.Header.Name, + docValue.Documentation, + ), + }, + }, nil } - -func GetHoverContent( - p ast.WGConfig, - line uint32, - cursor uint32, -) []string { - section := p.GetSectionByLine(line) - - if section == nil { - return []string{} - } - - sectionInfo := getSectionInfo(*section) - - property, _ := section.GetPropertyByLine(line) - - if property == nil { - return sectionInfo - } - - propertyInfo := getPropertyInfo(*property, cursor, *section) - - if len(propertyInfo) == 0 { - return sectionInfo - } - - contents := append(sectionInfo, []string{ - "", - fmt.Sprintf("### %s", property.Key.Name), - }...) - contents = append(contents, propertyInfo...) - - return contents -} -*/ diff --git a/server/handlers/wireguard/lsp/text-document-hover.go b/server/handlers/wireguard/lsp/text-document-hover.go index 1f2945f..a1b4d9b 100644 --- a/server/handlers/wireguard/lsp/text-document-hover.go +++ b/server/handlers/wireguard/lsp/text-document-hover.go @@ -1,6 +1,10 @@ package lsp import ( + "config-lsp/common" + "config-lsp/handlers/wireguard" + "config-lsp/handlers/wireguard/handlers" + "github.com/tliron/glsp" protocol "github.com/tliron/glsp/protocol_3_16" ) @@ -9,32 +13,29 @@ func TextDocumentHover( context *glsp.Context, params *protocol.HoverParams, ) (*protocol.Hover, error) { - /* - p := documentParserMap[params.TextDocument.URI] + d := wireguard.DocumentParserMap[params.TextDocument.URI] + line := params.Position.Line - switch p.GetTypeByLine(params.Position.Line) { - case parser.LineTypeComment: - return nil, nil - case parser.LineTypeEmpty: - return nil, nil - case parser.LineTypeHeader: - fallthrough - case parser.LineTypeProperty: - documentation := handlers.GetHoverContent( - *p, - params.Position.Line, - params.Position.Character, - ) + section := d.Config.FindSectionByLine(line) + property := d.Config.FindPropertyByLine(line) - hover := protocol.Hover{ - Contents: protocol.MarkupContent{ - Kind: protocol.MarkupKindMarkdown, - Value: strings.Join(documentation, "\n"), - }, - } - return &hover, nil - } - */ + index := common.LSPCharacterAsIndexPosition(params.Position.Character) + + if property != nil && section != nil { + return handlers.GetPropertyHoverInfo( + d, + *section, + *property, + index, + ) + } + + if section != nil && section.Start.Line == line { + return handlers.GetSectionHoverInfo( + d, + *section, + ) + } return nil, nil } From 0b2690910ff984290b46bcb50679dc2c1e65d06d Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sun, 9 Mar 2025 20:11:29 +0100 Subject: [PATCH 20/31] fix(server): Improve aliases Signed-off-by: Myzel394 --- server/handlers/aliases/ast/listener.go | 2 +- server/handlers/aliases/lsp/text-document-hover.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/handlers/aliases/ast/listener.go b/server/handlers/aliases/ast/listener.go index 3a5c112..3939c70 100644 --- a/server/handlers/aliases/ast/listener.go +++ b/server/handlers/aliases/ast/listener.go @@ -68,7 +68,7 @@ func (s *aliasesParserListener) EnterValues(ctx *parser.ValuesContext) { } } -// === Value === // +// === Name === // func (s *aliasesParserListener) EnterUser(ctx *parser.UserContext) { location := common.CharacterRangeFromCtx(ctx.BaseParserRuleContext) diff --git a/server/handlers/aliases/lsp/text-document-hover.go b/server/handlers/aliases/lsp/text-document-hover.go index 638f524..2bf81cc 100644 --- a/server/handlers/aliases/lsp/text-document-hover.go +++ b/server/handlers/aliases/lsp/text-document-hover.go @@ -51,7 +51,7 @@ func TextDocumentHover( contents := []string{} contents = append(contents, handlers.GetAliasValueTypeInfo(value)...) contents = append(contents, "") - contents = append(contents, "#### Value") + contents = append(contents, "#### Name") contents = append(contents, handlers.GetAliasValueHoverInfo(*document.Indexes, value)) text := strings.Join(contents, "\n") From 1cfb9bbfba7663e842edb4ea852cd4bf18c67427 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 15 Mar 2025 22:40:31 +0100 Subject: [PATCH 21/31] refactor(server): ssh_config improvements Signed-off-by: Myzel394 --- .../handlers/ssh_config/analyzer/analyzer.go | 1 + .../handlers/ssh_config/analyzer/options.go | 34 ++------ .../ssh_config/analyzer/options_test.go | 76 ----------------- server/handlers/ssh_config/analyzer/values.go | 36 ++++++++ .../ssh_config/analyzer/values_test.go | 84 +++++++++++++++++++ .../ssh_config/handlers/formatting.go | 2 +- .../lsp/text-document-did-change.go | 1 + 7 files changed, 132 insertions(+), 102 deletions(-) create mode 100644 server/handlers/ssh_config/analyzer/values.go create mode 100644 server/handlers/ssh_config/analyzer/values_test.go diff --git a/server/handlers/ssh_config/analyzer/analyzer.go b/server/handlers/ssh_config/analyzer/analyzer.go index b8b9ba7..447bb34 100644 --- a/server/handlers/ssh_config/analyzer/analyzer.go +++ b/server/handlers/ssh_config/analyzer/analyzer.go @@ -54,6 +54,7 @@ func Analyze( } } + analyzeValuesAreValid(ctx) analyzeTokens(ctx) analyzeIgnoreUnknownHasNoUnnecessary(ctx) analyzeDependents(ctx) diff --git a/server/handlers/ssh_config/analyzer/options.go b/server/handlers/ssh_config/analyzer/options.go index 90dee52..47d8dc9 100644 --- a/server/handlers/ssh_config/analyzer/options.go +++ b/server/handlers/ssh_config/analyzer/options.go @@ -4,7 +4,6 @@ import ( "config-lsp/common" docvalues "config-lsp/doc-values" "config-lsp/handlers/ssh_config/ast" - "config-lsp/handlers/ssh_config/diagnostics" "config-lsp/handlers/ssh_config/fields" "config-lsp/utils" "fmt" @@ -63,31 +62,16 @@ func checkOption( if !optionFound { // Diagnostics will be handled by `values.go` - if !ctx.document.Indexes.CanOptionBeIgnored(option, block) { - ctx.diagnostics = append( - ctx.diagnostics, - diagnostics.GenerateUnknownOption( - option.Key.ToLSPRange(), - option.Key.Value.Value, - ), - ) - ctx.document.Indexes.UnknownOptions[option.Start.Line] = ast.AllOptionInfo{ - Option: option, - Block: block, - } - } - - // Since we don't know the option, we can't verify the value return - } else { - // Check for values that are not allowed in Host blocks - if block != nil && block.GetBlockType() == ast.SSHBlockTypeHost && utils.KeyExists(fields.HostDisallowedOptions, option.Key.Key) { - ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ - Range: option.Key.LocationRange.ToLSPRange(), - Message: fmt.Sprintf("Option '%s' is not allowed in Host blocks", option.Key.Key), - Severity: &common.SeverityError, - }) - } + } + + // Check for values that are not allowed in Host blocks + if block != nil && block.GetBlockType() == ast.SSHBlockTypeHost && utils.KeyExists(fields.HostDisallowedOptions, option.Key.Key) { + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Range: option.Key.LocationRange.ToLSPRange(), + Message: fmt.Sprintf("Option '%s' is not allowed in Host blocks", option.Key.Key), + Severity: &common.SeverityError, + }) } ///// Check if the value is valid diff --git a/server/handlers/ssh_config/analyzer/options_test.go b/server/handlers/ssh_config/analyzer/options_test.go index 5a98d7f..f4ba881 100644 --- a/server/handlers/ssh_config/analyzer/options_test.go +++ b/server/handlers/ssh_config/analyzer/options_test.go @@ -112,79 +112,3 @@ Match t.Fatalf("Expected 1 error, got %v", ctx.diagnostics) } } - -func TestUnknownOptionExample( - t *testing.T, -) { - d := testutils_test.DocumentFromInput(t, ` -ThisOptionDoesNotExist okay -`) - ctx := &analyzerContext{ - document: d, - diagnostics: make([]protocol.Diagnostic, 0), - } - - analyzeStructureIsValid(ctx) - - if !(len(ctx.diagnostics) == 1) { - t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics)) - } - - if !(len(ctx.document.Indexes.UnknownOptions) == 1) { - t.Errorf("Expected 1 unknown option, got %v", len(ctx.document.Indexes.UnknownOptions)) - } - - if !(ctx.document.Indexes.UnknownOptions[0].Option.Key.Value.Value == "ThisOptionDoesNotExist") { - t.Errorf("Expected 'ThisOptionDoesNotExist', got %v", ctx.document.Indexes.UnknownOptions[0].Option.Key.Value.Value) - } -} - -func TestUnknownOptionButIgnoredExample( - t *testing.T, -) { - d := testutils_test.DocumentFromInput(t, ` -IgnoreUnknown ThisOptionDoesNotExist -ThisOptionDoesNotExist okay -`) - ctx := &analyzerContext{ - document: d, - diagnostics: make([]protocol.Diagnostic, 0), - } - - analyzeStructureIsValid(ctx) - - if len(ctx.diagnostics) > 0 { - t.Fatalf("Expected no errors, but got %v", len(ctx.diagnostics)) - } - - if !(len(ctx.document.Indexes.UnknownOptions) == 0) { - t.Errorf("Expected 0 unknown options, got %v", len(ctx.document.Indexes.UnknownOptions)) - } -} - -func TestUnknownOptionIgnoredIsAfterDefinitionExample( - t *testing.T, -) { - d := testutils_test.DocumentFromInput(t, ` -ThisOptionDoesNotExist okay -IgnoreUnknown ThisOptionDoesNotExist -`) - ctx := &analyzerContext{ - document: d, - diagnostics: make([]protocol.Diagnostic, 0), - } - - analyzeStructureIsValid(ctx) - - if !(len(ctx.diagnostics) == 1) { - t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics)) - } - - if !(len(ctx.document.Indexes.UnknownOptions) == 1) { - t.Errorf("Expected 1 unknown option, got %v", len(ctx.document.Indexes.UnknownOptions)) - } - - if !(ctx.document.Indexes.UnknownOptions[0].Option.Key.Value.Value == "ThisOptionDoesNotExist") { - t.Errorf("Expected 'ThisOptionDoesNotExist', got %v", ctx.document.Indexes.UnknownOptions[0].Option.Key.Value.Value) - } -} diff --git a/server/handlers/ssh_config/analyzer/values.go b/server/handlers/ssh_config/analyzer/values.go new file mode 100644 index 0000000..49b7e1e --- /dev/null +++ b/server/handlers/ssh_config/analyzer/values.go @@ -0,0 +1,36 @@ +package analyzer + +import ( + "config-lsp/handlers/ssh_config/diagnostics" + "config-lsp/handlers/ssh_config/fields" +) + +func analyzeValuesAreValid( + ctx *analyzerContext, +) { + // Check if there are unknown options + for _, info := range ctx.document.Config.GetAllOptions() { + option := info.Option + block := info.Block + + _, found := fields.Options[option.Key.Key] + + if !found { + if ctx.document.Indexes.CanOptionBeIgnored(option, block) { + // Skip + continue + } + + ctx.diagnostics = append( + ctx.diagnostics, + diagnostics.GenerateUnknownOption( + option.Key.ToLSPRange(), + option.Key.Value.Value, + ), + ) + ctx.document.Indexes.UnknownOptions[info.Option.Start.Line] = info + + continue + } + } +} diff --git a/server/handlers/ssh_config/analyzer/values_test.go b/server/handlers/ssh_config/analyzer/values_test.go new file mode 100644 index 0000000..88bffcc --- /dev/null +++ b/server/handlers/ssh_config/analyzer/values_test.go @@ -0,0 +1,84 @@ +package analyzer + +import ( + testutils_test "config-lsp/handlers/ssh_config/test_utils" + "testing" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func TestUnknownOptionExample( + t *testing.T, +) { + d := testutils_test.DocumentFromInput(t, ` +ThisOptionDoesNotExist okay +`) + ctx := &analyzerContext{ + document: d, + diagnostics: make([]protocol.Diagnostic, 0), + } + + analyzeValuesAreValid(ctx) + + if !(len(ctx.diagnostics) == 1) { + t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics)) + } + + if !(len(ctx.document.Indexes.UnknownOptions) == 1) { + t.Errorf("Expected 1 unknown option, got %v", len(ctx.document.Indexes.UnknownOptions)) + } + + if !(ctx.document.Indexes.UnknownOptions[0].Option.Key.Value.Value == "ThisOptionDoesNotExist") { + t.Errorf("Expected 'ThisOptionDoesNotExist', got %v", ctx.document.Indexes.UnknownOptions[0].Option.Key.Value.Value) + } +} + +func TestUnknownOptionButIgnoredExample( + t *testing.T, +) { + d := testutils_test.DocumentFromInput(t, ` +IgnoreUnknown ThisOptionDoesNotExist +ThisOptionDoesNotExist okay +`) + ctx := &analyzerContext{ + document: d, + diagnostics: make([]protocol.Diagnostic, 0), + } + + analyzeValuesAreValid(ctx) + + if len(ctx.diagnostics) > 0 { + t.Fatalf("Expected no errors, but got %v", len(ctx.diagnostics)) + } + + if !(len(ctx.document.Indexes.UnknownOptions) == 0) { + t.Errorf("Expected 0 unknown options, got %v", len(ctx.document.Indexes.UnknownOptions)) + } +} + +func TestUnknownOptionIgnoredIsAfterDefinitionExample( + t *testing.T, +) { + d := testutils_test.DocumentFromInput(t, ` +ThisOptionDoesNotExist okay +IgnoreUnknown ThisOptionDoesNotExist +`) + ctx := &analyzerContext{ + document: d, + diagnostics: make([]protocol.Diagnostic, 0), + } + + analyzeValuesAreValid(ctx) + + if !(len(ctx.diagnostics) == 1) { + t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics)) + } + + if !(len(ctx.document.Indexes.UnknownOptions) == 1) { + t.Errorf("Expected 1 unknown option, got %v", len(ctx.document.Indexes.UnknownOptions)) + } + + if !(ctx.document.Indexes.UnknownOptions[0].Option.Key.Value.Value == "ThisOptionDoesNotExist") { + t.Errorf("Expected 'ThisOptionDoesNotExist', got %v", ctx.document.Indexes.UnknownOptions[0].Option.Key.Value.Value) + } +} diff --git a/server/handlers/ssh_config/handlers/formatting.go b/server/handlers/ssh_config/handlers/formatting.go index 169f325..9d1d91b 100644 --- a/server/handlers/ssh_config/handlers/formatting.go +++ b/server/handlers/ssh_config/handlers/formatting.go @@ -33,7 +33,7 @@ func FormatDocument( // it := d.Config.Options.Iterator() // for it.Next() { // line := it.Key().(uint32) - // entry := it.Value().(ast.SSHEntry) + // entry := it.Name().(ast.SSHEntry) // // if !(line >= textRange.Start.Line && line <= textRange.End.Line) { // continue diff --git a/server/handlers/ssh_config/lsp/text-document-did-change.go b/server/handlers/ssh_config/lsp/text-document-did-change.go index 118ace6..830315c 100644 --- a/server/handlers/ssh_config/lsp/text-document-did-change.go +++ b/server/handlers/ssh_config/lsp/text-document-did-change.go @@ -20,6 +20,7 @@ func TextDocumentDidChange( document := sshconfig.DocumentParserMap[params.TextDocument.URI] document.Config.Clear() + println("reparsing everything") diagnostics := make([]protocol.Diagnostic, 0) errors := document.Config.Parse(content) From f29bb12d84ce799394edd836c51c62a3d6873c72 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 15 Mar 2025 22:41:49 +0100 Subject: [PATCH 22/31] fix(server): Fix language initialization Signed-off-by: Myzel394 --- server/root-handler/lsp/text-document-did-change.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/root-handler/lsp/text-document-did-change.go b/server/root-handler/lsp/text-document-did-change.go index dc30ca7..8ea26fc 100644 --- a/server/root-handler/lsp/text-document-did-change.go +++ b/server/root-handler/lsp/text-document-did-change.go @@ -33,7 +33,7 @@ func TextDocumentDidChange(context *glsp.Context, params *protocol.DidChangeText } } - if newLanguage != language { + if *newLanguage != *language { language = newLanguage params := &protocol.DidOpenTextDocumentParams{ From 00976cec959658e6557ffa9c3aa3f67ee2fc006e Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 15 Mar 2025 22:42:12 +0100 Subject: [PATCH 23/31] feat: Add justfile Signed-off-by: Myzel394 --- flake.lock | 12 ++++++------ flake.nix | 1 + justfile | 25 +++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 6 deletions(-) create mode 100644 justfile diff --git a/flake.lock b/flake.lock index 35080e0..f1d265f 100644 --- a/flake.lock +++ b/flake.lock @@ -26,11 +26,11 @@ ] }, "locked": { - "lastModified": 1733668782, - "narHash": "sha256-tPsqU00FhgdFr0JiQUiBMgPVbl1jbPCY5gbFiJycL3I=", + "lastModified": 1741396135, + "narHash": "sha256-wqmdLr7h4Bk8gyKutgaApJKOM8JVvywI5P48NuqJ9Jg=", "owner": "tweag", "repo": "gomod2nix", - "rev": "514283ec89c39ad0079ff2f3b1437404e4cba608", + "rev": "0983848bf2a7ccbfe24d874065adb8fd0f23729b", "type": "github" }, "original": { @@ -41,11 +41,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1739214665, - "narHash": "sha256-26L8VAu3/1YRxS8MHgBOyOM8xALdo6N0I04PgorE7UM=", + "lastModified": 1741513245, + "narHash": "sha256-7rTAMNTY1xoBwz0h7ZMtEcd8LELk9R5TzBPoHuhNSCk=", "owner": "nixos", "repo": "nixpkgs", - "rev": "64e75cd44acf21c7933d61d7721e812eac1b5a0a", + "rev": "e3e32b642a31e6714ec1b712de8c91a3352ce7e1", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 3e85932..4ce0613 100644 --- a/flake.nix +++ b/flake.nix @@ -136,6 +136,7 @@ mailutils wireguard-tools antlr + just ]) ++ (if pkgs.stdenv.isLinux then with pkgs; [ postfix ] else []); diff --git a/justfile b/justfile new file mode 100644 index 0000000..a731785 --- /dev/null +++ b/justfile @@ -0,0 +1,25 @@ +#!/usr/bin/env just --justfile + +set dotenv-load := true + +default: + @just --list + +# Lint whole project +lint: + cd server && gofmt -s -w . + # cd vs-code-extension && yarn run lint + +# Build config-lsp and test it in nvim (config-lsp will be loaded automatically) +[working-directory: "./server"] +test-nvim file: + go build -o ./result/bin/config-lsp && rm -rf ~/.local/state/nvim/lsp.log && DOTFILES_IGNORE_CONFIG_LSP=1 nvim {{file}} -c ':source nvim-lsp-debug.lua' + +# Show Mason Logs +show-nvim-logs: + bat ~/.local/state/nvim/lsp.log + +[working-directory: "./server"] +test: + nix develop --command bash -c 'go test ./... -count=1' + From 437985d72f5116b26394316ef879cfb8c27128ef Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Fri, 14 Feb 2025 23:32:05 +0100 Subject: [PATCH 24/31] chore(ci-cd): Update .goreleaser.yaml config Signed-off-by: Myzel394 --- .goreleaser.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 4e7fece..7c1e02b 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -19,7 +19,7 @@ builds: dir: ./server archives: - - format: tar.gz + - formats: [ 'tar.gz' ] # this name template makes the OS and Arch compatible with the results of `uname`. name_template: >- {{ .ProjectName }}_ @@ -31,7 +31,7 @@ archives: # use zip for windows archives format_overrides: - goos: windows - format: zip + formats: [ 'zip' ] changelog: sort: asc From d81f9787716989efff61eec9f8f28d862b3e46e3 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Sat, 15 Feb 2025 01:00:15 +0100 Subject: [PATCH 25/31] fix(server): Improve spec field analyzer Signed-off-by: Myzel394 --- server/doc-values/value-path.go | 14 +++++---- server/handlers/fstab/analyzer/spec.go | 38 ++++++++++++++++++++++++ server/handlers/fstab/analyzer/values.go | 2 ++ server/handlers/fstab/fields/spec.go | 3 +- 4 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 server/handlers/fstab/analyzer/spec.go diff --git a/server/doc-values/value-path.go b/server/doc-values/value-path.go index 1aced7c..2ca9a8c 100644 --- a/server/doc-values/value-path.go +++ b/server/doc-values/value-path.go @@ -48,11 +48,15 @@ func (v PathValue) GetTypeDescription() []string { func (v PathValue) DeprecatedCheckIsValid(value string) []*InvalidValue { if !utils.DoesPathExist(value) { - return []*InvalidValue{{ - Err: PathDoesNotExistError{}, - Start: 0, - End: uint32(len(value)), - }} + if v.RequiredType == PathTypeExistenceOptional { + return nil + } else { + return []*InvalidValue{{ + Err: PathDoesNotExistError{}, + Start: 0, + End: uint32(len(value)), + }} + } } isValid := false diff --git a/server/handlers/fstab/analyzer/spec.go b/server/handlers/fstab/analyzer/spec.go new file mode 100644 index 0000000..4b3b382 --- /dev/null +++ b/server/handlers/fstab/analyzer/spec.go @@ -0,0 +1,38 @@ +package analyzer + +import ( + "config-lsp/common" + "config-lsp/handlers/fstab/ast" + "regexp" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +var volatileBlockFields = regexp.MustCompile(`^/dev/(sd|nvme|mmcblk|sr|vd|loop|cdrom)[a-zA-Z0-9]*$`) + +func analyzeSpecField( + ctx *analyzerContext, + field *ast.FstabField, +) { + if field == nil { + return + } + + if field.Value.Value == "" { + return + } + + if !volatileBlockFields.MatchString(field.Value.Value) { + return + } + + codeDescription := protocol.CodeDescription{ + HRef: protocol.URI("https://wiki.archlinux.org/title/Persistent_block_device_naming"), + } + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Range: field.ToLSPRange(), + Message: "Kernel name descriptors for block devices are not persistent and can change each boot, they should not be used in configuration files. Prefer device UUIDs or LABELs instead.", + CodeDescription: &codeDescription, + Severity: &common.SeverityWarning, + }) +} diff --git a/server/handlers/fstab/analyzer/values.go b/server/handlers/fstab/analyzer/values.go index 62d9fac..fcc1fbc 100644 --- a/server/handlers/fstab/analyzer/values.go +++ b/server/handlers/fstab/analyzer/values.go @@ -21,6 +21,8 @@ func analyzeValuesAreValid( checkField(ctx, entry.Fields.MountPoint, fields.MountPointField) checkField(ctx, entry.Fields.FilesystemType, fields.FileSystemTypeField) + analyzeSpecField(ctx, entry.Fields.Spec) + if entry.Fields.Options != nil { mountOptions := entry.FetchMountOptionsField(true) diff --git a/server/handlers/fstab/fields/spec.go b/server/handlers/fstab/fields/spec.go index df43f6d..0602cfa 100644 --- a/server/handlers/fstab/fields/spec.go +++ b/server/handlers/fstab/fields/spec.go @@ -6,7 +6,8 @@ import ( ) var UuidField = docvalues.RegexValue{ - Regex: *regexp.MustCompile(`[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}`), + // Can either be a UUID or UID + Regex: *regexp.MustCompile(`(?i)([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}|[a-f0-9]{4}-[a-f0-9]{4})`), } var LabelField = docvalues.RegexValue{ Regex: *regexp.MustCompile(`\S+`), From 8350458ae54b0e822b88d35bafa02ae3d341cb53 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2025 21:57:27 +0000 Subject: [PATCH 26/31] chore(deps): bump golang.org/x/crypto from 0.25.0 to 0.31.0 in /server Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.25.0 to 0.31.0. - [Commits](https://github.com/golang/crypto/compare/v0.25.0...v0.31.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-type: indirect ... Signed-off-by: dependabot[bot] Signed-off-by: Myzel394 --- server/go.mod | 6 +++--- server/go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/server/go.mod b/server/go.mod index 45a560f..ba46ef0 100644 --- a/server/go.mod +++ b/server/go.mod @@ -30,7 +30,7 @@ require ( github.com/segmentio/ksuid v1.0.4 // indirect github.com/sourcegraph/jsonrpc2 v0.2.0 // indirect github.com/tliron/kutil v0.3.24 // indirect - golang.org/x/crypto v0.25.0 // indirect - golang.org/x/sys v0.22.0 // indirect - golang.org/x/term v0.22.0 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/term v0.27.0 // indirect ) diff --git a/server/go.sum b/server/go.sum index b8344b8..3a19871 100644 --- a/server/go.sum +++ b/server/go.sum @@ -48,13 +48,13 @@ github.com/tliron/glsp v0.2.2 h1:IKPfwpE8Lu8yB6Dayta+IyRMAbTVunudeauEgjXBt+c= github.com/tliron/glsp v0.2.2/go.mod h1:GMVWDNeODxHzmDPvYbYTCs7yHVaEATfYtXiYJ9w1nBg= github.com/tliron/kutil v0.3.24 h1:LvaqizF4htpEef9tC0B//sqtvQzEjDu69A4a1HrY+ko= github.com/tliron/kutil v0.3.24/go.mod h1:2iSIhOnOe1reqczZQy6TauVHhItsq6xRLV2rVBvodpk= -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= -golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= From 15471894d40d41f3af01e5f066dd5c73cb9bffc5 Mon Sep 17 00:00:00 2001 From: Myzel394 Date: Sun, 16 Mar 2025 00:25:05 +0100 Subject: [PATCH 27/31] fix: Fix --- server/common/diagnostics.go | 5 +- server/fetch_tags.js | 27 +++++++ .../gitconfig/fields/nested-option.go | 78 +++++++++++++++++++ .../lsp/text-document-did-change.go | 63 +++++++-------- 4 files changed, 137 insertions(+), 36 deletions(-) create mode 100644 server/fetch_tags.js create mode 100644 server/handlers/gitconfig/fields/nested-option.go diff --git a/server/common/diagnostics.go b/server/common/diagnostics.go index fbd5fdb..5ebd92d 100644 --- a/server/common/diagnostics.go +++ b/server/common/diagnostics.go @@ -6,7 +6,10 @@ import ( ) func ClearDiagnostics(context *glsp.Context, uri protocol.DocumentUri) { - go context.Notify( + // Diagnostics are sent synchronously, as sending them async + // could result in a race condition when we send diagnostics + // to the client. + context.Notify( protocol.ServerTextDocumentPublishDiagnostics, protocol.PublishDiagnosticsParams{ URI: uri, diff --git a/server/fetch_tags.js b/server/fetch_tags.js new file mode 100644 index 0000000..fa11764 --- /dev/null +++ b/server/fetch_tags.js @@ -0,0 +1,27 @@ +// Creates a JSON object in the form of: +// { +// [