diff --git a/handlers/aliases/handlers/get-value.go b/handlers/aliases/handlers/get-value.go index 3e2347b..d9c0d12 100644 --- a/handlers/aliases/handlers/get-value.go +++ b/handlers/aliases/handlers/get-value.go @@ -16,15 +16,15 @@ func GetValueAtCursor( index, found := slices.BinarySearchFunc( entry.Values.Values, cursor, - func(entry ast.AliasValueInterface, pos uint32) int { - value := entry.GetAliasValue() + func(current ast.AliasValueInterface, target uint32) int { + value := current.GetAliasValue() - if pos > value.Location.End.Character { - return -1 + if target < value.Location.Start.Character { + return 1 } - if pos < value.Location.Start.Character { - return 1 + if target > value.Location.End.Character { + return -1 } return 0 diff --git a/handlers/sshd_config/fields/match-parser/full_test.go b/handlers/sshd_config/fields/match-parser/full_test.go new file mode 100644 index 0000000..33121ad --- /dev/null +++ b/handlers/sshd_config/fields/match-parser/full_test.go @@ -0,0 +1,74 @@ +package matchparser + +import ( + "testing" +) + +func TestFullExample( + t *testing.T, +) { + input := "User alice,adam Address 192.168.1.1 Host *.example.com" + match := NewMatch() + errs := match.Parse(input, 0, 0) + + if len(errs) > 0 { + t.Fatalf("Failed to parse match: %v", errs) + } + + entry := match.GetEntryByCursor(0) + if !(entry == match.Entries[0]) { + t.Errorf("Expected entry at 0 to be %v, but got %v", match.Entries[0], entry) + } + + entry = match.GetEntryByCursor(5) + if !(entry == match.Entries[0]) { + t.Errorf("Expected entry at 5 to be %v, but got %v", match.Entries[0], entry) + } + + entry = match.GetEntryByCursor(13) + if !(entry == match.Entries[0]) { + t.Errorf("Expected entry at 13 to be %v, but got %v", match.Entries[1], entry) + } + + entry = match.GetEntryByCursor(16) + if !(entry == match.Entries[1]) { + t.Errorf("Expected entry at 16 to be %v, but got %v", match.Entries[1], entry) + } + + entry = match.GetEntryByCursor(24) + if !(entry == match.Entries[1]) { + t.Errorf("Expected entry at 24 to be %v, but got %v", match.Entries[2], entry) + } + + entry = match.GetEntryByCursor(36) + if !(entry == match.Entries[2]) { + t.Errorf("Expected entry at 36 to be %v, but got %v", match.Entries[2], entry) + } +} + +func TestGetEntryForIncompleteExample( + t *testing.T, +) { + input := "User " + match := NewMatch() + errs := match.Parse(input, 0, 0) + + if len(errs) > 0 { + t.Fatalf("Failed to parse match: %v", errs) + } + + entry := match.GetEntryByCursor(0) + if !(entry == match.Entries[0]) { + t.Errorf("Expected entry at 0 to be %v, but got %v", match.Entries[0], entry) + } + + entry = match.GetEntryByCursor(4) + if !(entry == match.Entries[0]) { + t.Errorf("Expected entry at 4 to be %v, but got %v", match.Entries[0], entry) + } + + entry = match.GetEntryByCursor(5) + if !(entry == match.Entries[0]) { + t.Errorf("Expected entry at 5 to be %v, but got %v", match.Entries[0], entry) + } +} diff --git a/handlers/sshd_config/fields/match-parser/match_fields.go b/handlers/sshd_config/fields/match-parser/match_fields.go index 3dc12dd..f9c2d4a 100644 --- a/handlers/sshd_config/fields/match-parser/match_fields.go +++ b/handlers/sshd_config/fields/match-parser/match_fields.go @@ -6,12 +6,12 @@ func (m Match) GetEntryByCursor(cursor uint32) *MatchEntry { index, found := slices.BinarySearchFunc( m.Entries, cursor, - func(entry *MatchEntry, cursor uint32) int { - if cursor < entry.Start.Character { + func(current *MatchEntry, target uint32) int { + if target < current.Start.Character { return 1 } - if cursor > entry.End.Character { + if target > current.End.Character { return -1 } @@ -40,12 +40,12 @@ func (e MatchEntry) GetValueByCursor(cursor uint32) *MatchValue { index, found := slices.BinarySearchFunc( e.Values.Values, cursor, - func(value *MatchValue, cursor uint32) int { - if cursor < value.Start.Character { + func(current *MatchValue, target uint32) int { + if target < current.Start.Character { return 1 } - if cursor > value.End.Character { + if target > current.End.Character { return -1 } diff --git a/handlers/sshd_config/handlers/definition.go b/handlers/sshd_config/handlers/definition.go index 50de94d..bec6955 100644 --- a/handlers/sshd_config/handlers/definition.go +++ b/handlers/sshd_config/handlers/definition.go @@ -16,13 +16,13 @@ func GetIncludeOptionLocation( index, found := slices.BinarySearchFunc( include.Values, cursor, - func(i *indexes.SSHDIndexIncludeValue, cursor uint32) int { - if cursor < i.Start.Character { - return -1 + func(current *indexes.SSHDIndexIncludeValue, target uint32) int { + if target < current.Start.Character { + return 1 } - if cursor > i.End.Character { - return 1 + if target > current.End.Character { + return -1 } return 0 diff --git a/handlers/sshd_config/handlers/formatting.go b/handlers/sshd_config/handlers/formatting.go new file mode 100644 index 0000000..4fcc2de --- /dev/null +++ b/handlers/sshd_config/handlers/formatting.go @@ -0,0 +1,38 @@ +package handlers + +import ( + sshdconfig "config-lsp/handlers/sshd_config" + "config-lsp/handlers/sshd_config/ast" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func FormatDocument( + d *sshdconfig.SSHDocument, + textRange protocol.Range, + options protocol.FormattingOptions, +) ([]protocol.TextEdit, error) { + edits := make([]protocol.TextEdit, 0) + + it := d.Config.Options.Iterator() + for it.Next() { + line := it.Key().(uint32) + entry := it.Value().(ast.SSHDEntry) + + if !(line >= textRange.Start.Line && line <= textRange.End.Line) { + continue + } + + switch entry.(type) { + case *ast.SSHDOption: + option := entry.(*ast.SSHDOption) + edits = append(edits, formatSSHDOption(option, options)...) + case *ast.SSHDMatchBlock: + matchBlock := entry.(*ast.SSHDMatchBlock) + + edits = formatSSHDMatchBlock(matchBlock, options) + } + } + + return edits, nil +} diff --git a/handlers/sshd_config/handlers/formatting_nodes.go b/handlers/sshd_config/handlers/formatting_nodes.go new file mode 100644 index 0000000..da84cba --- /dev/null +++ b/handlers/sshd_config/handlers/formatting_nodes.go @@ -0,0 +1,107 @@ +package handlers + +import ( + "config-lsp/common/formatting" + "config-lsp/handlers/sshd_config/ast" + matchparser "config-lsp/handlers/sshd_config/fields/match-parser" + "config-lsp/utils" + "fmt" + "strings" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func formatSSHDOption( + option *ast.SSHDOption, + options protocol.FormattingOptions, +) []protocol.TextEdit { + template := formatting.FormatTemplate( + "%s/t%s", + ) + + var key string + + if option.Key != nil { + key = option.Key.Key + } else { + key = "" + } + + var value string + + if option.OptionValue != nil { + value = option.OptionValue.Value.Raw + } else { + value = "" + } + + return []protocol.TextEdit{ + { + Range: option.ToLSPRange(), + NewText: template.Format(options, key, value), + }, + } +} + +func formatSSHDMatchBlock( + matchBlock *ast.SSHDMatchBlock, + options protocol.FormattingOptions, +) []protocol.TextEdit { + edits := make([]protocol.TextEdit, 0) + + template := formatting.FormatTemplate( + "%s/t%s", + ) + edits = append(edits, protocol.TextEdit{ + Range: matchBlock.ToLSPRange(), + NewText: template.Format(options, matchBlock.MatchEntry.Key.Key, formatMatchToString(matchBlock.MatchValue, options)), + }) + + it := matchBlock.Options.Iterator() + for it.Next() { + option := it.Value().(*ast.SSHDOption) + + edits = append(edits, formatSSHDOption(option, options)...) + } + + return edits +} + +func formatMatchToString( + match *matchparser.Match, + options protocol.FormattingOptions, +) string { + entriesAsStrings := utils.Map( + match.Entries, + func(entry *matchparser.MatchEntry) string { + return formatMatchEntryToString(entry, options) + }, + ) + + return strings.Join(entriesAsStrings, " ") +} + +func formatMatchEntryToString( + entry *matchparser.MatchEntry, + options protocol.FormattingOptions, +) string { + return fmt.Sprintf( + "%s %s", + string(entry.Criteria.Type), + formatMatchValuesToString(entry.Values, options), + ) +} + +func formatMatchValuesToString( + values *matchparser.MatchValues, + options protocol.FormattingOptions, +) string { + valuesAsStrings := utils.Map( + values.Values, + func(value *matchparser.MatchValue) string { + return value.Value.Raw + }, + ) + + return strings.Join(valuesAsStrings, ",") +} diff --git a/handlers/sshd_config/handlers/formatting_test.go b/handlers/sshd_config/handlers/formatting_test.go new file mode 100644 index 0000000..3a15d89 --- /dev/null +++ b/handlers/sshd_config/handlers/formatting_test.go @@ -0,0 +1,62 @@ +package handlers + +import ( + sshdconfig "config-lsp/handlers/sshd_config" + "config-lsp/handlers/sshd_config/ast" + "config-lsp/handlers/sshd_config/indexes" + "config-lsp/utils" + "testing" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func TestSimpleFormattingExampleWorks( + t *testing.T, +) { + input := utils.Dedent(` + PermitRootLogin yes +a b +`) + config := ast.NewSSHConfig() + errors := config.Parse(input) + + if len(errors) > 0 { + t.Fatalf("Failed to parse SSHD config: %v", errors) + } + + i, errors := indexes.CreateIndexes(*config) + + if len(errors) > 0 { + t.Fatalf("Failed to create indexes: %v", errors) + } + + d := sshdconfig.SSHDocument{ + Config: config, + Indexes: i, + } + + options := protocol.FormattingOptions{} + options["insertSpaces"] = true + options["tabSize"] = float64(4) + + edits, err := FormatDocument( + &d, + protocol.Range{ + Start: protocol.Position{ + Line: 0, + Character: protocol.UInteger(0), + }, + End: protocol.Position{ + Line: 1, + Character: protocol.UInteger(0), + }, + }, + options, + ) + + if err != nil { + t.Errorf("Failed to format document: %v", err) + } + + _ = edits +} diff --git a/handlers/sshd_config/lsp/text-document-range-formatting.go b/handlers/sshd_config/lsp/text-document-range-formatting.go new file mode 100644 index 0000000..aeb76f8 --- /dev/null +++ b/handlers/sshd_config/lsp/text-document-range-formatting.go @@ -0,0 +1,22 @@ +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 TextDocumentRangeFormatting( + context *glsp.Context, + params *protocol.DocumentRangeFormattingParams, +) ([]protocol.TextEdit, error) { + d := sshdconfig.DocumentParserMap[params.TextDocument.URI] + + return handlers.FormatDocument( + d, + params.Range, + params.Options, + ) +}