diff --git a/server/handlers/ssh_config/analyzer/analyzer.go b/server/handlers/ssh_config/analyzer/analyzer.go index 74d54b1..48adb23 100644 --- a/server/handlers/ssh_config/analyzer/analyzer.go +++ b/server/handlers/ssh_config/analyzer/analyzer.go @@ -42,6 +42,8 @@ func Analyze( analyzeMatchBlocks(ctx) analyzeHostBlock(ctx) analyzeBlocks(ctx) + analyzeTagOptions(ctx) + analyzeTagImports(ctx) return ctx.diagnostics } diff --git a/server/handlers/ssh_config/analyzer/tag.go b/server/handlers/ssh_config/analyzer/tag.go deleted file mode 100644 index 4208d31..0000000 --- a/server/handlers/ssh_config/analyzer/tag.go +++ /dev/null @@ -1,28 +0,0 @@ -package analyzer - -import ( - "config-lsp/common" - "config-lsp/handlers/ssh_config/fields" - "fmt" - - protocol "github.com/tliron/glsp/protocol_3_16" -) - -var tagOption = fields.CreateNormalizedName("Tag") - -func analyzeTags( - ctx *analyzerContext, -) { - // Check if the specified tags actually exist - for _, options := range ctx.document.Indexes.AllOptionsPerName[tagOption] { - for _, option := range options { - if _, found := ctx.document.Indexes.Tags[option.OptionValue.Value.Value]; !found { - ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ - Range: option.OptionValue.ToLSPRange(), - Message: fmt.Sprintf("Unknown tag: %s", option.OptionValue.Value.Value), - Severity: &common.SeverityError, - }) - } - } - } -} diff --git a/server/handlers/ssh_config/analyzer/tag_imports.go b/server/handlers/ssh_config/analyzer/tag_imports.go new file mode 100644 index 0000000..ce0713b --- /dev/null +++ b/server/handlers/ssh_config/analyzer/tag_imports.go @@ -0,0 +1,33 @@ +package analyzer + +import ( + "config-lsp/common" + "fmt" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func analyzeTagImports( + ctx *analyzerContext, +) { + for name, info := range ctx.document.Indexes.Tags { + if _, found := ctx.document.Indexes.TagImports[name]; !found { + var diagnosticRange protocol.Range + + if len(info.Block.MatchValue.Entries) == 1 { + diagnosticRange = info.Block.MatchOption.ToLSPRange() + } else { + diagnosticRange = info.EntryValue.ToLSPRange() + } + + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Range: diagnosticRange, + Message: fmt.Sprintf("Tag %s is not used", name), + Severity: &common.SeverityWarning, + Tags: []protocol.DiagnosticTag{ + protocol.DiagnosticTagUnnecessary, + }, + }) + } + } +} diff --git a/server/handlers/ssh_config/analyzer/tag_imports_test.go b/server/handlers/ssh_config/analyzer/tag_imports_test.go new file mode 100644 index 0000000..d3783b7 --- /dev/null +++ b/server/handlers/ssh_config/analyzer/tag_imports_test.go @@ -0,0 +1,49 @@ +package analyzer + +import ( + testutils_test "config-lsp/handlers/ssh_config/test_utils" + "testing" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func TestUsedTagImportsExample( + t *testing.T, +) { + d := testutils_test.DocumentFromInput(t, ` +Host test.com + Tag auth + +Match tagged auth + User root +`) + ctx := &analyzerContext{ + document: d, + diagnostics: make([]protocol.Diagnostic, 0), + } + + analyzeTagImports(ctx) + + if len(ctx.diagnostics) > 0 { + t.Errorf("Expected no errors, got %v", len(ctx.diagnostics)) + } +} + +func TestUnusedTagImportsExample( + t *testing.T, +) { + d := testutils_test.DocumentFromInput(t, ` +Match tagged auth + User root +`) + ctx := &analyzerContext{ + document: d, + diagnostics: make([]protocol.Diagnostic, 0), + } + + analyzeTagImports(ctx) + + if !(len(ctx.diagnostics) == 1) { + t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics)) + } +} diff --git a/server/handlers/ssh_config/analyzer/tag_options.go b/server/handlers/ssh_config/analyzer/tag_options.go new file mode 100644 index 0000000..e94356c --- /dev/null +++ b/server/handlers/ssh_config/analyzer/tag_options.go @@ -0,0 +1,32 @@ +package analyzer + +import ( + "config-lsp/common" + "config-lsp/handlers/ssh_config/fields" + "fmt" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +var tagOption = fields.CreateNormalizedName("Tag") + +func analyzeTagOptions( + ctx *analyzerContext, +) { + // Check if the specified tags actually exist + for _, options := range ctx.document.Indexes.AllOptionsPerName[tagOption] { + for _, option := range options { + tag, found := ctx.document.Indexes.Tags[option.OptionValue.Value.Value] + + if found && tag.Block.Start.Line > option.Start.Line { + continue + } + + ctx.diagnostics = append(ctx.diagnostics, protocol.Diagnostic{ + Range: option.OptionValue.ToLSPRange(), + Message: fmt.Sprintf("Unknown tag: %s", option.OptionValue.Value.Value), + Severity: &common.SeverityError, + }) + } + } +} diff --git a/server/handlers/ssh_config/analyzer/tag_options_test.go b/server/handlers/ssh_config/analyzer/tag_options_test.go new file mode 100644 index 0000000..e7ef97c --- /dev/null +++ b/server/handlers/ssh_config/analyzer/tag_options_test.go @@ -0,0 +1,52 @@ +package analyzer + +import ( + testutils_test "config-lsp/handlers/ssh_config/test_utils" + "testing" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func TestValidTagExample( + t *testing.T, +) { + d := testutils_test.DocumentFromInput(t, ` +Host test.com + Tag auth + +Match tagged auth + User root +`) + ctx := &analyzerContext{ + document: d, + diagnostics: make([]protocol.Diagnostic, 0), + } + + analyzeTagOptions(ctx) + + if len(ctx.diagnostics) > 0 { + t.Errorf("Expected no errors, got %v", len(ctx.diagnostics)) + } +} + +func TestTagBlockBeforeExample( + t *testing.T, +) { + d := testutils_test.DocumentFromInput(t, ` +Match tagged auth + User root + +Host test.com + Tag auth +`) + ctx := &analyzerContext{ + document: d, + diagnostics: make([]protocol.Diagnostic, 0), + } + + analyzeTagOptions(ctx) + + if !(len(ctx.diagnostics) == 1) { + t.Errorf("Expected 1 error, got %v", len(ctx.diagnostics)) + } +} diff --git a/server/handlers/ssh_config/handlers/completions.go b/server/handlers/ssh_config/handlers/completions.go index 15a44f9..dff6464 100644 --- a/server/handlers/ssh_config/handlers/completions.go +++ b/server/handlers/ssh_config/handlers/completions.go @@ -65,6 +65,7 @@ func GetOptionCompletions( d *sshconfig.SSHDocument, entry *ast.SSHOption, block ast.SSHBlock, + line uint32, cursor common.CursorPosition, ) ([]protocol.CompletionItem, error) { option, found := fields.Options[entry.Key.Key] @@ -84,6 +85,7 @@ func GetOptionCompletions( if entry.Key.Key == tagOption { return getTagCompletions( d, + line, cursor, entry, ) @@ -94,13 +96,13 @@ func GetOptionCompletions( } // Hello wo|rld - line := entry.OptionValue.Value.Raw + lineValue := entry.OptionValue.Value.Raw // NEW: docvalues index return option.DeprecatedFetchCompletions( - line, + lineValue, common.DeprecatedImprovedCursorToIndex( cursor, - line, + lineValue, entry.OptionValue.Start.Character, ), ), nil diff --git a/server/handlers/ssh_config/handlers/completions_tag.go b/server/handlers/ssh_config/handlers/completions_tag.go index 61e922a..fb77d9d 100644 --- a/server/handlers/ssh_config/handlers/completions_tag.go +++ b/server/handlers/ssh_config/handlers/completions_tag.go @@ -5,8 +5,6 @@ import ( "config-lsp/common/formatting" sshconfig "config-lsp/handlers/ssh_config" "config-lsp/handlers/ssh_config/ast" - "config-lsp/handlers/ssh_config/indexes" - "config-lsp/utils" "fmt" protocol "github.com/tliron/glsp/protocol_3_16" @@ -14,24 +12,30 @@ import ( func getTagCompletions( d *sshconfig.SSHDocument, + line uint32, cursor common.CursorPosition, entry *ast.SSHOption, ) ([]protocol.CompletionItem, error) { - return utils.MapMapToSlice( - d.Indexes.Tags, - func(name string, info indexes.SSHIndexTagInfo) protocol.CompletionItem { - kind := protocol.CompletionItemKindModule - text := renderMatchBlock(info.Block) - return protocol.CompletionItem{ - Label: name, - Kind: &kind, - Documentation: protocol.MarkupContent{ - Kind: protocol.MarkupKindMarkdown, - Value: fmt.Sprintf("```sshconfig\n%s\n```", text), - }, - } - }, - ), nil + completions := make([]protocol.CompletionItem, 0) + + for name, info := range d.Indexes.Tags { + if info.Block.Start.Line < line { + continue + } + + kind := protocol.CompletionItemKindModule + text := renderMatchBlock(info.Block) + completions = append(completions, protocol.CompletionItem{ + Label: name, + Kind: &kind, + Documentation: protocol.MarkupContent{ + Kind: protocol.MarkupKindMarkdown, + Value: fmt.Sprintf("```sshconfig\n%s\n```", text), + }, + }) + } + + return completions, nil } func renderMatchBlock( diff --git a/server/handlers/ssh_config/lsp/text-document-completion.go b/server/handlers/ssh_config/lsp/text-document-completion.go index 56d6973..d3feab4 100644 --- a/server/handlers/ssh_config/lsp/text-document-completion.go +++ b/server/handlers/ssh_config/lsp/text-document-completion.go @@ -42,6 +42,7 @@ func TextDocumentCompletion(context *glsp.Context, params *protocol.CompletionPa d, option, block, + line, cursor, ) }