From cd23ffcb7f5450cd6873b5b9f43191adffd4a1c4 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Wed, 2 Oct 2024 09:24:11 +0200 Subject: [PATCH] feat(ssh_config): Add code actiont o add option to unknown option --- common/formatting/formatting.go | 11 +- common/formatting/formatting_test.go | 19 ++ handlers/ssh_config/analyzer/values.go | 2 +- handlers/ssh_config/handlers/code-actions.go | 184 ++++++++++++++++++ .../ssh_config/handlers/fetch-code-actions.go | 47 +++++ handlers/ssh_config/indexes/indexes.go | 3 +- .../ssh_config/indexes/indexes_handlers.go | 2 +- handlers/ssh_config/lsp/README.md | 7 - .../lsp/text-document-code-action.go | 17 ++ .../lsp/workspace-execute-command.go | 25 +++ root-handler/text-document-code-action.go | 3 +- root-handler/workspace-execute-command.go | 3 + 12 files changed, 311 insertions(+), 12 deletions(-) create mode 100644 handlers/ssh_config/handlers/code-actions.go create mode 100644 handlers/ssh_config/handlers/fetch-code-actions.go delete mode 100644 handlers/ssh_config/lsp/README.md create mode 100644 handlers/ssh_config/lsp/text-document-code-action.go create mode 100644 handlers/ssh_config/lsp/workspace-execute-command.go diff --git a/common/formatting/formatting.go b/common/formatting/formatting.go index c72b639..65ad395 100644 --- a/common/formatting/formatting.go +++ b/common/formatting/formatting.go @@ -7,6 +7,12 @@ import ( protocol "github.com/tliron/glsp/protocol_3_16" ) +var DefaultFormattingOptions = protocol.FormattingOptions{ + "tabSize": float64(4), + "insertSpaces": false, + "trimTrailingWhitespace": true, +} + type FormatTemplate string func (f FormatTemplate) Format( @@ -84,7 +90,10 @@ func surroundWithQuotes(s string) string { innerValue := value[startPosition:endPosition] - if strings.Contains(innerValue, " ") { + if innerValue[0] == '"' && innerValue[len(innerValue)-1] == '"' && (len(innerValue) >= 2 || innerValue[len(innerValue)-2] != '\\') { + // Already surrounded + value = value[:startPosition-3] + innerValue + value[endPosition+3:] + } else if strings.Contains(innerValue, " ") { value = value[:startPosition-3] + "\"" + innerValue + "\"" + value[endPosition+3:] } else { value = value[:startPosition-3] + innerValue + value[endPosition+3:] diff --git a/common/formatting/formatting_test.go b/common/formatting/formatting_test.go index fc606aa..7ceba9a 100644 --- a/common/formatting/formatting_test.go +++ b/common/formatting/formatting_test.go @@ -116,3 +116,22 @@ func TestSurroundWithQuotesButNoSpaceExample( t.Errorf("Expected %q but got %q", expected, result) } } + +func TestSurroundWithQuotesButAlreadySurrounded( + t *testing.T, +) { + template := FormatTemplate("%s /!'%s/!'") + + options := protocol.FormattingOptions{ + "tabSize": float64(4), + "insertSpaces": false, + "trimTrailingWhitespace": true, + } + + result := template.Format(options, "PermitRootLogin", `"Hello World"`) + expected := `PermitRootLogin "Hello World"` + + if result != expected { + t.Errorf("Expected %q but got %q", expected, result) + } +} diff --git a/handlers/ssh_config/analyzer/values.go b/handlers/ssh_config/analyzer/values.go index 742a435..052ada3 100644 --- a/handlers/ssh_config/analyzer/values.go +++ b/handlers/ssh_config/analyzer/values.go @@ -32,7 +32,7 @@ func analyzeValuesAreValid( Severity: &common.SeverityError, }, ) - ctx.document.Indexes.UnknownOptions = append(ctx.document.Indexes.UnknownOptions, info) + ctx.document.Indexes.UnknownOptions[info.Option.Start.Line] = info continue } diff --git a/handlers/ssh_config/handlers/code-actions.go b/handlers/ssh_config/handlers/code-actions.go new file mode 100644 index 0000000..1627370 --- /dev/null +++ b/handlers/ssh_config/handlers/code-actions.go @@ -0,0 +1,184 @@ +package handlers + +import ( + "config-lsp/common/formatting" + sshconfig "config-lsp/handlers/ssh_config" + "config-lsp/handlers/ssh_config/ast" + "fmt" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +type codeActionName string + +const ( + CodeActionAddToUnknown codeActionName = "addToUnknown" +) + +type codeAction interface { + RunCommand(*sshconfig.SSHDocument) (*protocol.ApplyWorkspaceEditParams, error) +} + +type codeActionArgs interface{} + +type codeActionAddToUnknownArgs struct { + URI protocol.DocumentUri + // Where the option is defined + OptionLine uint32 + // Where the block is defined, if nil, option is globally defined + BlockLine *uint32 +} + +func CodeActionAddToUnknownArgsFromArguments(arguments map[string]interface{}) codeActionAddToUnknownArgs { + var blockLine *uint32 + + if arguments["BlockLine"] != nil { + blockLineValue := uint32(arguments["BlockLine"].(float64)) + blockLine = &blockLineValue + } + + return codeActionAddToUnknownArgs{ + URI: arguments["URI"].(protocol.DocumentUri), + OptionLine: uint32(arguments["OptionLine"].(float64)), + BlockLine: blockLine, + } +} + +var addToUnknownOptionTemplate = formatting.FormatTemplate("/!'%s/!'") + +func (args codeActionAddToUnknownArgs) RunCommand(d *sshconfig.SSHDocument) (*protocol.ApplyWorkspaceEditParams, error) { + var option *ast.SSHOption + var block ast.SSHBlock + + // Either this or `insertionLine` must be set + // `ignoreUnknownOption` is used if an `IgnoreUnknown` option is set already + // `insertionLine` is used if no `IgnoreUnknown` option is set + var ignoreUnknownOption *ast.SSHOption + var insertionLine uint32 + + if args.BlockLine == nil { + // Global + rawOption, found := d.Config.Options.Get(args.OptionLine) + + if !found { + return nil, fmt.Errorf("No option found at line %d", args.OptionLine) + } + + option = rawOption.(*ast.SSHOption) + + if ignoreOption, found := d.Indexes.IgnoredOptions[nil]; found { + ignoreUnknownOption = ignoreOption.OptionValue + } else { + insertionLine = 0 + } + } else { + // Block + rawBlock, found := d.Config.Options.Get(*args.BlockLine) + + if !found { + return nil, fmt.Errorf("No block found at line %d", *args.BlockLine) + } + + block = rawBlock.(ast.SSHBlock) + + rawOption, found := block.GetOptions().Get(args.OptionLine) + + if !found { + return nil, fmt.Errorf("No option found at line %d", args.OptionLine) + } + + option = rawOption.(*ast.SSHOption) + + if ignoreOption, found := d.Indexes.IgnoredOptions[block]; found { + ignoreUnknownOption = ignoreOption.OptionValue + } else { + insertionLine = block.GetEntryOption().Start.Line + 1 + } + } + + rawOptionName := option.Key.Value.Raw + println("rawOption:") + println(rawOptionName) + optionName := addToUnknownOptionTemplate.Format(formatting.DefaultFormattingOptions, rawOptionName) + + // We got everything, let's build the edit! + if ignoreUnknownOption == nil { + // Insert a completely new IgnoreUnknown option + + if block == nil { + // Global + label := fmt.Sprintf("Add %s to unknown options", option.Key.Key) + return &protocol.ApplyWorkspaceEditParams{ + Label: &label, + Edit: protocol.WorkspaceEdit{ + Changes: map[protocol.DocumentUri][]protocol.TextEdit{ + args.URI: { + { + Range: protocol.Range{ + Start: protocol.Position{ + Line: insertionLine, + Character: 0, + }, + End: protocol.Position{ + Line: insertionLine, + Character: 0, + }, + }, + NewText: fmt.Sprintf("IgnoreUnknown %s\n", optionName), + }, + }, + }, + }, + }, nil + } else { + label := fmt.Sprintf("Add %s to unknown options", option.Key.Key) + return &protocol.ApplyWorkspaceEditParams{ + Label: &label, + Edit: protocol.WorkspaceEdit{ + Changes: map[protocol.DocumentUri][]protocol.TextEdit{ + args.URI: { + { + Range: protocol.Range{ + Start: protocol.Position{ + Line: insertionLine, + Character: 0, + }, + End: protocol.Position{ + Line: insertionLine, + Character: 0, + }, + }, + NewText: fmt.Sprintf(" IgnoreUnknown %s\n", optionName), + }, + }, + }, + }, + }, nil + } + } else { + // Append to the existing IgnoreUnknown option + label := fmt.Sprintf("Add %s to unknown options", option.Key.Key) + return &protocol.ApplyWorkspaceEditParams{ + Label: &label, + Edit: protocol.WorkspaceEdit{ + Changes: map[protocol.DocumentUri][]protocol.TextEdit{ + args.URI: { + { + Range: protocol.Range{ + Start: protocol.Position{ + Line: ignoreUnknownOption.Start.Line, + Character: ignoreUnknownOption.End.Character, + }, + End: protocol.Position{ + Line: ignoreUnknownOption.Start.Line, + Character: ignoreUnknownOption.End.Character, + }, + }, + NewText: fmt.Sprintf(" %s", optionName), + }, + }, + }, + }, + }, nil + } +} diff --git a/handlers/ssh_config/handlers/fetch-code-actions.go b/handlers/ssh_config/handlers/fetch-code-actions.go new file mode 100644 index 0000000..a2a2759 --- /dev/null +++ b/handlers/ssh_config/handlers/fetch-code-actions.go @@ -0,0 +1,47 @@ +package handlers + +import ( + sshconfig "config-lsp/handlers/ssh_config" + "fmt" + + protocol "github.com/tliron/glsp/protocol_3_16" +) + +func FetchCodeActions( + 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, + }, + }, + } + codeAction := &protocol.CodeAction{ + Title: fmt.Sprintf("Add %s to unknown options", unknownOption.Option.Key.Key), + Command: &command, + } + + return []protocol.CodeAction{ + *codeAction, + } + } + + return nil +} diff --git a/handlers/ssh_config/indexes/indexes.go b/handlers/ssh_config/indexes/indexes.go index 175a061..dd2a54f 100644 --- a/handlers/ssh_config/indexes/indexes.go +++ b/handlers/ssh_config/indexes/indexes.go @@ -44,5 +44,6 @@ type SSHIndexes struct { // This is used for code actions. // This stores a list of unknown option, so that we can provide // a code action to add them to a "IgnoreUnknown" option - UnknownOptions []ast.AllOptionInfo + // This is a map of to