From bd58ae3e58b858981c5cc51b0ceff055082aa76e Mon Sep 17 00:00:00 2001 From: ahert001 Date: Fri, 10 Jan 2025 12:26:35 +0100 Subject: [PATCH 1/6] add code actions --- AnyText/AnyText.Core/CodeActionInfo.cs | 60 +++++ AnyText/AnyText.Core/Grammars/Grammar.cs | 11 + AnyText/AnyText.Core/WorkspaceEdit.cs | 219 +++++++++++++++ AnyText/AnyText.Lsp/ILspServer.CodeAction.cs | 16 ++ AnyText/AnyText.Lsp/ILspServer.cs | 10 +- AnyText/AnyText.Lsp/LspServer.CodeAction.cs | 249 ++++++++++++++++++ .../AnyText.Lsp/LspServer.ExecuteCommand.cs | 46 ++++ AnyText/AnyText.Lsp/LspServer.Registration.cs | 7 +- AnyText/AnyText.Lsp/LspServer.cs | 14 +- .../Grammars/AnyTextGrammar.manual.Actions.cs | 100 +++++++ 10 files changed, 726 insertions(+), 6 deletions(-) create mode 100644 AnyText/AnyText.Core/CodeActionInfo.cs create mode 100644 AnyText/AnyText.Core/WorkspaceEdit.cs create mode 100644 AnyText/AnyText.Lsp/ILspServer.CodeAction.cs create mode 100644 AnyText/AnyText.Lsp/LspServer.CodeAction.cs create mode 100644 AnyText/AnyText.Lsp/LspServer.ExecuteCommand.cs create mode 100644 AnyText/AnyText/Grammars/AnyTextGrammar.manual.Actions.cs diff --git a/AnyText/AnyText.Core/CodeActionInfo.cs b/AnyText/AnyText.Core/CodeActionInfo.cs new file mode 100644 index 00000000..cdf28e95 --- /dev/null +++ b/AnyText/AnyText.Core/CodeActionInfo.cs @@ -0,0 +1,60 @@ +namespace NMF.AnyText +{ + /// + /// Represents the information about a code action. + /// + public class CodeActionInfo + { + /// + /// The title is typically displayed in the UI to describe the action. + /// + public string Title { get; set; } + + /// + /// Kind of the code action. + /// Possible values: + /// - "quickfix" + /// - "refactor" + /// - "refactor.extract" + /// - "refactor.inline" + /// - "refactor.rewrite" + /// - "source" + /// - "source.organizeImports" + /// + public string Kind { get; set; } + + /// + /// This array holds diagnostics for which this action is relevant. If no diagnostics are set, the action may apply generally. + /// + public string[] Diagnostics { get; set; } + + /// + /// A value of true indicates that the code action is preferred; otherwise, false or null if there's no preference. + /// + public bool IsPreferred { get; set; } + + /// + /// This is the text that describes the command to execute, which can be shown to the user. + /// + public string CommandTitle { get; set; } + + /// + /// The command is the identifier or name of the action to execute when the user selects it. + /// + public string Command { get; set; } + + /// + /// These are the parameters passed to the command when it is executed. + /// + public object[] Arguments { get; set; } + /// + /// Identifies the Diagnostic that this Action fixes + /// + public string DiagnosticIdentifier { get; set; } + /// + /// Defines the Workspace changes this action executes + /// + public WorkspaceEdit WorkspaceEdit { get; set; } + + } +} \ No newline at end of file diff --git a/AnyText/AnyText.Core/Grammars/Grammar.cs b/AnyText/AnyText.Core/Grammars/Grammar.cs index 0755638c..1cee514e 100644 --- a/AnyText/AnyText.Core/Grammars/Grammar.cs +++ b/AnyText/AnyText.Core/Grammars/Grammar.cs @@ -177,5 +177,16 @@ public Rule Root /// a context to resolve the root rule /// the root rule for this grammar protected abstract Rule GetRootRule(GrammarContext context); + + /// + /// Gets the list of code actions supported by this grammar. + /// + public virtual IEnumerable SupportedCodeActions { get; } = new List(); + + /// + /// Dictionary of executable actions. + /// The key is the action identifier, and the value is the action executor. + /// + public virtual Dictionary> ExecutableCodeActions { get; } = new Dictionary>(); } } diff --git a/AnyText/AnyText.Core/WorkspaceEdit.cs b/AnyText/AnyText.Core/WorkspaceEdit.cs new file mode 100644 index 00000000..f741af7a --- /dev/null +++ b/AnyText/AnyText.Core/WorkspaceEdit.cs @@ -0,0 +1,219 @@ +using System.Collections.Generic; + +namespace NMF.AnyText +{ + /// + /// Represents changes to a workspace, including text edits, document changes, and change annotations. + /// + public class WorkspaceEdit + { + /// + /// A dictionary of changes to text documents, keyed by document URI, with the value being the text edits for that document. + /// + public Dictionary Changes { get; set; } + + /// + /// A list of document-level changes (e.g., file creation, renaming, deletion). + /// + public List DocumentChanges { get; set; } + + /// + /// A dictionary of annotations associated with changes, keyed by annotation ID. + /// + public Dictionary ChangeAnnotations { get; set; } + } + + /// + /// Represents a change to a document, including text edits, file creation, renaming, or deletion. + /// + public class DocumentChange + { + /// + /// Text document edits (e.g., line insertions, deletions). + /// + public TextDocumentEdit TextDocumentEdit { get; set; } + + /// + /// Information for creating a new file. + /// + public CreateFile CreateFile { get; set; } + + /// + /// Information for renaming an existing file. + /// + public RenameFile RenameFile { get; set; } + + /// + /// Information for deleting an existing file. + /// + public DeleteFile DeleteFile { get; set; } + } + + /// + /// Represents metadata or instructions for an annotation associated with a change. + /// + public class ChangeAnnotation + { + /// + /// A label for the annotation (e.g., "Refactor"). + /// + public string Label { get; set; } + + /// + /// Indicates if the change requires user confirmation. + /// + public bool? NeedsConfirmation { get; set; } + + /// + /// A description or explanation of the annotation. + /// + public string Description { get; set; } + } + + /// + /// Represents the text document edit instructions, including the document and the edits. + /// + public class TextDocumentEdit + { + /// + /// Identifies the text document to edit, including optional version information. + /// + public OptionalVersionedTextDocumentIdentifier TextDocument { get; set; } + + /// + /// An Array of edits to perform on the document (e.g., insertions, deletions). + /// + public TextEdit[] Edits { get; set; } + } + + /// + /// Identifies a text document with optional versioning. + /// + public class OptionalVersionedTextDocumentIdentifier + { + /// + /// The URI of the text document. + /// + public string Uri { get; set; } + + /// + /// An optional version number for the document, if versioning is supported. + /// + public int? Version { get; set; } + } + + /// + /// Represents the information needed to create a new file. + /// + public class CreateFile + { + /// + /// The type of file creation (e.g., "create"). + /// + public string Kind { get; set; } + + /// + /// The URI of the file to be created. + /// + public string Uri { get; set; } + + /// + /// File options (e.g., whether to overwrite an existing file). + /// + public FileOptions Options { get; set; } + + /// + /// An optional annotation ID related to the file creation. + /// + public string AnnotationId { get; set; } + } + + /// + /// Represents the information needed to rename an existing file. + /// + public class RenameFile + { + /// + /// The type of file operation (e.g., "rename"). + /// + public string Kind { get; set; } + + /// + /// The URI of the old file name. + /// + public string OldUri { get; set; } + + /// + /// The URI of the new file name. + /// + public string NewUri { get; set; } + + /// + /// File options (e.g., whether to overwrite). + /// + public FileOptions Options { get; set; } + + /// + /// An optional annotation ID related to the file rename. + /// + public string AnnotationId { get; set; } + } + + /// + /// Represents the information needed to delete an existing file. + /// + public class DeleteFile + { + /// + /// The type of file operation (e.g., "delete"). + /// + public string Kind { get; set; } + + /// + /// The URI of the file to be deleted. + /// + public string Uri { get; set; } + + /// + /// File deletion options (e.g., whether to delete recursively). + /// + public DeleteFileOptions Options { get; set; } + + /// + /// An optional annotation ID related to the file deletion. + /// + public string AnnotationId { get; set; } + } + + /// + /// Options for creating or renaming files, such as overwrite behavior. + /// + public class FileOptions + { + /// + /// If true, overwrite an existing file. + /// + public bool? Overwrite { get; set; } + + /// + /// If true, ignore the operation if the file already exists. + /// + public bool? IgnoreIfExists { get; set; } + } + + /// + /// Options for deleting files, such as recursive deletion and handling missing files. + /// + public class DeleteFileOptions + { + /// + /// If true, delete directories recursively. + /// + public bool? Recursive { get; set; } + + /// + /// If true, ignore the operation if the file does not exist. + /// + public bool? IgnoreIfNotExists { get; set; } + } +} diff --git a/AnyText/AnyText.Lsp/ILspServer.CodeAction.cs b/AnyText/AnyText.Lsp/ILspServer.CodeAction.cs new file mode 100644 index 00000000..6f297aa8 --- /dev/null +++ b/AnyText/AnyText.Lsp/ILspServer.CodeAction.cs @@ -0,0 +1,16 @@ +using LspTypes; +using Newtonsoft.Json.Linq; +using StreamJsonRpc; + +namespace NMF.AnyText +{ + public partial interface ILspServer + { + /// + /// Handles the textDocument/codeAction request from the client. + /// + /// The JSON token containing the parameters of the request. (CodeActionParams) + [JsonRpcMethod(Methods.TextDocumentCodeActionName)] + CodeAction[] CodeAction(JToken arg); + } +} \ No newline at end of file diff --git a/AnyText/AnyText.Lsp/ILspServer.cs b/AnyText/AnyText.Lsp/ILspServer.cs index 43a0bc0c..70f1383a 100644 --- a/AnyText/AnyText.Lsp/ILspServer.cs +++ b/AnyText/AnyText.Lsp/ILspServer.cs @@ -42,11 +42,19 @@ public InitializeResult Initialize( void Shutdown(); /// - /// Handles the */setTrace request from the client. This is used to set the trace setting of the server. + /// Handles the $/setTrace request from the client. This is used to set the trace setting of the server. /// /// The JSON token containing the parameters of the request. (SetTraceParams) [JsonRpcMethod(MethodConstants.SetTrace)] public void SetTrace(JToken arg); + /// + /// Handles the workspace/ececuteCommand request from the client. This is used to execute an action on the + /// Server. + /// + /// The JSON token containing the parameters of the request. (ExceuteCommandParams) + [JsonRpcMethod(Methods.WorkspaceExecuteCommandName)] + public void ExecuteCommand(JToken arg); + } } \ No newline at end of file diff --git a/AnyText/AnyText.Lsp/LspServer.CodeAction.cs b/AnyText/AnyText.Lsp/LspServer.CodeAction.cs new file mode 100644 index 00000000..ae52878e --- /dev/null +++ b/AnyText/AnyText.Lsp/LspServer.CodeAction.cs @@ -0,0 +1,249 @@ +using LspTypes; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using Range = LspTypes.Range; + +namespace NMF.AnyText +{ + public partial class LspServer + { + /// + public CodeAction[] CodeAction(JToken arg) + { + var request = arg.ToObject(); + + var codeActions = new List(); + if (!_documents.TryGetValue(request.TextDocument.Uri, out var document)) + return codeActions.ToArray(); + + var codeActionCapabilities = _clientCapabilities?.TextDocument?.CodeAction; + var supportsIsPreferred = codeActionCapabilities?.IsPreferredSupport == true; + + var documentUri = request.TextDocument.Uri; + var diagnostics = request.Context.Diagnostics; + var range = request.Range; + var kindFilter = request.Context.Only; + + + var arguments = new[] { documentUri, range.Start, (object)range.End }; + var grammar = document.Context.Grammar; + foreach (var action in grammar.SupportedCodeActions) + { + var diagnosticIdentifier = action.DiagnosticIdentifier; + var relevantDiagnostics = diagnostics + .Where(d => d.Message.Contains(diagnosticIdentifier)) + .ToArray(); + if (!string.IsNullOrEmpty(diagnosticIdentifier) && relevantDiagnostics.Length == 0) + continue; + var actionKind = !string.IsNullOrEmpty(action.Kind) ? ParseLspCodeActionKind(action.Kind) : null; + if (kindFilter != null && kindFilter.Any() && actionKind != null && + !kindFilter.Contains(actionKind.Value)) continue; + codeActions.Add(new CodeAction + { + Title = action.Title, + Kind = actionKind, + Diagnostics = relevantDiagnostics.Length == 0 ? null : relevantDiagnostics, + Edit = action.WorkspaceEdit != null ? MapWorkspaceEdit(action.WorkspaceEdit) : null, + IsPreferred = supportsIsPreferred && action.IsPreferred ? true : null, + Command = action.Command != null + ? new Command + { + Title = action.CommandTitle, + CommandIdentifier = action.Command, + Arguments = arguments.Concat(action.Arguments).ToArray() + } + : null + }); + } + + return codeActions.ToArray(); + } + + private static readonly Dictionary KindMapping = new(StringComparer.OrdinalIgnoreCase) + { + { "", CodeActionKind.Empty }, + { "quickfix", CodeActionKind.QuickFix }, + { "refactor", CodeActionKind.Refactor }, + { "refactor.extract", CodeActionKind.RefactorExtract }, + { "refactor.inline", CodeActionKind.RefactorInline }, + { "refactor.rewrite", CodeActionKind.RefactorRewrite }, + { "source", CodeActionKind.Source }, + { "source.organizeImports", CodeActionKind.SourceOrganizeImports } + }; + + private static CodeActionKind? ParseLspCodeActionKind(string kind) + { + if (KindMapping.TryGetValue(kind, out var result)) return result; + + return null; + } + + private LspTypes.WorkspaceEdit MapWorkspaceEdit(WorkspaceEdit workspaceEdit) + { + return new LspTypes.WorkspaceEdit + { + Changes = MapChanges(workspaceEdit.Changes), + DocumentChanges = MapDocumentChanges(workspaceEdit.DocumentChanges), + ChangeAnnotations = MapChangeAnnotations(workspaceEdit.ChangeAnnotations) + }; + } + + private Dictionary MapChanges(Dictionary changes) + { + var lspChanges = new Dictionary(); + + foreach (var entry in changes) lspChanges.Add(entry.Key, MapTextEditsArray(entry.Value)); + + return lspChanges; + } + + private LspTypes.TextEdit[] MapTextEditsArray(TextEdit[] textEdits) + { + return textEdits.Select(e => MapTextEdit(e)).ToArray(); + } + + private LspTypes.TextEdit MapTextEdit(TextEdit textEdit) + { + return new LspTypes.TextEdit + { + Range = new Range + { + Start = new Position + { + Line = (uint)textEdit.Start.Line, + Character = (uint)textEdit.Start.Col + }, + End = new Position + { + Line = (uint)textEdit.End.Line, + Character = (uint)textEdit.End.Col + } + }, + NewText = string.Concat(textEdit.NewText) + }; + } + + private SumType[]> MapDocumentChanges(List documentChanges) + { + var lspDocumentChanges = documentChanges.Select(MapDocumentChange).ToList(); + return new SumType[]>(lspDocumentChanges.ToArray()); + } + + + private SumType + MapDocumentChange(DocumentChange docChange) + { + if (docChange.TextDocumentEdit != null) return MapTextDocumentEdit(docChange.TextDocumentEdit); + + if (docChange.CreateFile != null) return MapCreateFile(docChange.CreateFile); + + if (docChange.RenameFile != null) return MapRenameFile(docChange.RenameFile); + + if (docChange.DeleteFile != null) return MapDeleteFile(docChange.DeleteFile); + + throw new InvalidOperationException( + "DocumentChange must contain one of TextDocumentEdit, CreateFile, RenameFile, or DeleteFile."); + } + + private SumType + MapTextDocumentEdit(TextDocumentEdit textDocumentEdit) + { + var edits = textDocumentEdit.Edits + .Select(e => new SumType(MapTextEdit(e))).ToArray(); + var lspTextDocumentEdit = new LspTypes.TextDocumentEdit + { + TextDocument = new LspTypes.OptionalVersionedTextDocumentIdentifier + { + Uri = textDocumentEdit.TextDocument.Uri, + Version = textDocumentEdit.TextDocument.Version + }, + Edits = edits + }; + + return new + SumType( + lspTextDocumentEdit); + } + + private SumType + MapCreateFile(CreateFile createFile) + { + var lspCreateFile = new LspTypes.CreateFile + { + Kind = createFile.Kind, + Uri = createFile.Uri, + Options = new CreateFileOptions + { + Overwrite = createFile.Options?.Overwrite, + IgnoreIfExists = createFile.Options?.IgnoreIfExists + }, + AnnotationId = createFile.AnnotationId + }; + + return new + SumType( + lspCreateFile); + } + + private SumType + MapRenameFile(RenameFile renameFile) + { + var lspRenameFile = new LspTypes.RenameFile + { + Kind = renameFile.Kind, + OldUri = renameFile.OldUri, + NewUri = renameFile.NewUri, + Options = new RenameFileOptions + { + Overwrite = renameFile.Options?.Overwrite, + IgnoreIfExists = renameFile.Options?.IgnoreIfExists + }, + AnnotationId = renameFile.AnnotationId + }; + + return new + SumType( + lspRenameFile); + } + + private SumType + MapDeleteFile(DeleteFile deleteFile) + { + var lspDeleteFile = new LspTypes.DeleteFile + { + Kind = deleteFile.Kind, + Uri = deleteFile.Uri, + Options = new LspTypes.DeleteFileOptions + { + Recursive = deleteFile.Options?.Recursive, + IgnoreIfNotExists = deleteFile.Options?.IgnoreIfNotExists + }, + AnnotationId = deleteFile.AnnotationId + }; + + return new + SumType( + lspDeleteFile); + } + + private Dictionary MapChangeAnnotations( + Dictionary changeAnnotations) + { + var lspChangeAnnotations = changeAnnotations.ToDictionary( + entry => entry.Key, + entry => new LspTypes.ChangeAnnotation + { + Label = entry.Value.Label, + NeedsConfirmation = entry.Value.NeedsConfirmation, + Description = entry.Value.Description + } + ); + + return lspChangeAnnotations; + } + } +} \ No newline at end of file diff --git a/AnyText/AnyText.Lsp/LspServer.ExecuteCommand.cs b/AnyText/AnyText.Lsp/LspServer.ExecuteCommand.cs new file mode 100644 index 00000000..50418c83 --- /dev/null +++ b/AnyText/AnyText.Lsp/LspServer.ExecuteCommand.cs @@ -0,0 +1,46 @@ +using LspTypes; +using Newtonsoft.Json.Linq; +using System; +using System.Linq; + +namespace NMF.AnyText +{ + public partial class LspServer + { + /// + public void ExecuteCommand(JToken arg) + { + var request = arg.ToObject(); + if (!_languages.TryGetValue(_currentLanguageId, out var language)) + { + SendLogMessage(MessageType.Error, $"{_currentLanguageId} Language not found"); + return; + } + + if (!language.ExecutableCodeActions.TryGetValue(request.Command, out var executableCodeAction)) + { + SendLogMessage(MessageType.Error, $"{request.Command} Command not supported"); + return; + } + + executableCodeAction.Invoke(request.Arguments); + } + + private Registration CreateExecuteCommandRegistration(string languageId) + { + _languages.TryGetValue(languageId, out var language); + if (language == null) return null; + + var registrationOptions = new ExecuteCommandRegistrationOptions + { + Commands = language.ExecutableCodeActions.Keys.ToArray() + }; + return new Registration + { + RegisterOptions = registrationOptions, + Id = Guid.NewGuid().ToString(), + Method = Methods.WorkspaceExecuteCommandName + }; + } + } +} \ No newline at end of file diff --git a/AnyText/AnyText.Lsp/LspServer.Registration.cs b/AnyText/AnyText.Lsp/LspServer.Registration.cs index a005b221..ed5af5f8 100644 --- a/AnyText/AnyText.Lsp/LspServer.Registration.cs +++ b/AnyText/AnyText.Lsp/LspServer.Registration.cs @@ -6,6 +6,7 @@ namespace NMF.AnyText public partial class LspServer { private string _currentLanguageId; + /// /// Registers Server Capabilities with the client when a document is opened. /// @@ -14,11 +15,11 @@ public partial class LspServer private void RegisterCapabilitiesOnOpen(string languageId, Parser parser) { if (_currentLanguageId == languageId) return; - + var semanticRegistration = CreateSemanticTokenRegistration(languageId, parser); - RegisterCapabilities(new[] { semanticRegistration }); + var executeRegistration = CreateExecuteCommandRegistration(languageId); + RegisterCapabilities(new[] { semanticRegistration, executeRegistration }); _currentLanguageId = languageId; - } /// diff --git a/AnyText/AnyText.Lsp/LspServer.cs b/AnyText/AnyText.Lsp/LspServer.cs index 6e605314..85e46894 100644 --- a/AnyText/AnyText.Lsp/LspServer.cs +++ b/AnyText/AnyText.Lsp/LspServer.cs @@ -20,7 +20,7 @@ public partial class LspServer : ILspServer private readonly JsonRpc _rpc; private readonly Dictionary _documents = new Dictionary(); private readonly Dictionary _languages; - + private ClientCapabilities _clientCapabilities; /// /// Creates a new instance /// @@ -50,7 +50,8 @@ public InitializeResult Initialize( , WorkspaceFolder[] workspaceFolders , object InitializationOptions = null) { - + _clientCapabilities = capabilities; + var serverCapabilities = new ServerCapabilities { TextDocumentSync = new TextDocumentSyncOptions @@ -65,6 +66,15 @@ public InitializeResult Initialize( ReferencesProvider = new ReferenceOptions { WorkDoneProgress = false + }, + CodeActionProvider = new CodeActionOptions + { + CodeActionKinds = new[] + { + CodeActionKind.RefactorExtract, CodeActionKind.Empty, CodeActionKind.Refactor, + CodeActionKind.Source, CodeActionKind.QuickFix, CodeActionKind.RefactorInline, + CodeActionKind.RefactorRewrite, CodeActionKind.SourceOrganizeImports + } } }; UpdateTraceSource(trace); diff --git a/AnyText/AnyText/Grammars/AnyTextGrammar.manual.Actions.cs b/AnyText/AnyText/Grammars/AnyTextGrammar.manual.Actions.cs new file mode 100644 index 00000000..78e352d0 --- /dev/null +++ b/AnyText/AnyText/Grammars/AnyTextGrammar.manual.Actions.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.Json; + +namespace NMF.AnyText.Grammars +{ + public partial class AnyTextGrammar + { + /// + /// List of Possible Code Actions + /// + public override IEnumerable SupportedCodeActions { get; } = new List + { + new() + { + Title = "Generate comment header", + Kind = "refactor.extract", + CommandTitle = "Insert Comment Header", + WorkspaceEdit = null, + Diagnostics = new[] { "" }, + Command = "editor.action.addCommentHeader", + Arguments = new[] { "a" } + } + }; + + /// + /// Dictionary of Code Action Identifier and the Executable Action + /// + public override Dictionary> ExecutableCodeActions { get; } = new() + { + { + "editor.action.addCommentHeader", obj => + { + var arguments = obj; + if (arguments != null && arguments.Length > 0) + { + var documentUri = (string)arguments[0]; + var startRange = ParsePositionFromJson(arguments[1].ToString()); + var endRange = ParsePositionFromJson(arguments[2].ToString()); + + if (documentUri != null) + { + InsertCommentHeader(documentUri); + return "Comment header generated."; + } + + return "Invalid document URI."; + } + + return "No arguments provided."; + } + } + }; + + + private static void InsertCommentHeader(string filePath) + { + var uri = new Uri(Uri.UnescapeDataString(filePath), UriKind.RelativeOrAbsolute); + var localPath = uri.LocalPath; + var content = File.ReadAllLines(localPath); + var commentHeader = GenerateCommentHeader(); + + var updatedContent = new string[content.Length + commentHeader.Length]; + Array.Copy(commentHeader, updatedContent, commentHeader.Length); + Array.Copy(content, 0, updatedContent, commentHeader.Length, content.Length); + + SaveDocument(localPath, updatedContent); + } + + private static string[] GenerateCommentHeader() + { + var builder = new StringBuilder(); + builder.AppendLine("/*"); + builder.AppendLine(" * Description: "); + builder.AppendLine(" * Author: "); + builder.AppendLine(" * Date: " + DateTime.Now.ToString("yyyy-MM-dd")); + builder.AppendLine(" * Version: 1.0"); + builder.AppendLine(" */"); + + return builder.ToString().Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); + } + + private static void SaveDocument(string filePath, string[] updatedContent) + { + File.WriteAllLines(filePath, updatedContent); + } + + private static ParsePosition ParsePositionFromJson(string jsonString) + { + var jsonDocument = JsonDocument.Parse(jsonString); + + var line = jsonDocument.RootElement.GetProperty("line").GetInt32(); + var character = jsonDocument.RootElement.GetProperty("character").GetInt32(); + + return new ParsePosition(line, character); + } + } +} \ No newline at end of file From 5e1e1fb123069ac0374cf69ec0c3e122735576c6 Mon Sep 17 00:00:00 2001 From: ahert001 Date: Mon, 13 Jan 2025 12:47:36 +0100 Subject: [PATCH 2/6] Fix WorkspaceEdit Actions --- AnyText/AnyText.Lsp/LspServer.CodeAction.cs | 24 +++++++++++++++------ AnyText/AnyText.Lsp/LspServer.cs | 3 ++- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/AnyText/AnyText.Lsp/LspServer.CodeAction.cs b/AnyText/AnyText.Lsp/LspServer.CodeAction.cs index ae52878e..62aa87dd 100644 --- a/AnyText/AnyText.Lsp/LspServer.CodeAction.cs +++ b/AnyText/AnyText.Lsp/LspServer.CodeAction.cs @@ -84,9 +84,9 @@ private LspTypes.WorkspaceEdit MapWorkspaceEdit(WorkspaceEdit workspaceEdit) { return new LspTypes.WorkspaceEdit { - Changes = MapChanges(workspaceEdit.Changes), + Changes = workspaceEdit.Changes != null ? MapChanges(workspaceEdit.Changes) : null, DocumentChanges = MapDocumentChanges(workspaceEdit.DocumentChanges), - ChangeAnnotations = MapChangeAnnotations(workspaceEdit.ChangeAnnotations) + ChangeAnnotations = workspaceEdit.ChangeAnnotations != null ? MapChangeAnnotations(workspaceEdit.ChangeAnnotations) : null }; } @@ -152,13 +152,15 @@ private LspTypes.TextEdit MapTextEdit(TextEdit textEdit) private SumType MapTextDocumentEdit(TextDocumentEdit textDocumentEdit) { + string workspaceFolder = _workspaceFolders.FirstOrDefault()?.Uri; + string fileUri = $"{workspaceFolder}/{textDocumentEdit.TextDocument.Uri}"; var edits = textDocumentEdit.Edits .Select(e => new SumType(MapTextEdit(e))).ToArray(); var lspTextDocumentEdit = new LspTypes.TextDocumentEdit { TextDocument = new LspTypes.OptionalVersionedTextDocumentIdentifier { - Uri = textDocumentEdit.TextDocument.Uri, + Uri = fileUri, Version = textDocumentEdit.TextDocument.Version }, Edits = edits @@ -172,10 +174,12 @@ private LspTypes.TextEdit MapTextEdit(TextEdit textEdit) private SumType MapCreateFile(CreateFile createFile) { + string workspaceFolder = _workspaceFolders.FirstOrDefault()?.Uri; + string fileUri = $"{workspaceFolder}/{createFile.Uri}"; var lspCreateFile = new LspTypes.CreateFile { Kind = createFile.Kind, - Uri = createFile.Uri, + Uri = fileUri, Options = new CreateFileOptions { Overwrite = createFile.Options?.Overwrite, @@ -192,11 +196,15 @@ private LspTypes.TextEdit MapTextEdit(TextEdit textEdit) private SumType MapRenameFile(RenameFile renameFile) { + string workspaceFolder = _workspaceFolders.FirstOrDefault()?.Uri; + string oldfileUri = $"{workspaceFolder}/{renameFile.OldUri}"; + string newfileUri = $"{workspaceFolder}/{renameFile.NewUri}"; var lspRenameFile = new LspTypes.RenameFile { + Kind = renameFile.Kind, - OldUri = renameFile.OldUri, - NewUri = renameFile.NewUri, + OldUri = oldfileUri, + NewUri = newfileUri, Options = new RenameFileOptions { Overwrite = renameFile.Options?.Overwrite, @@ -213,10 +221,12 @@ private LspTypes.TextEdit MapTextEdit(TextEdit textEdit) private SumType MapDeleteFile(DeleteFile deleteFile) { + string workspaceFolder = _workspaceFolders.FirstOrDefault()?.Uri; + string fileUri = $"{workspaceFolder}/{deleteFile.Uri}"; var lspDeleteFile = new LspTypes.DeleteFile { Kind = deleteFile.Kind, - Uri = deleteFile.Uri, + Uri = fileUri, Options = new LspTypes.DeleteFileOptions { Recursive = deleteFile.Options?.Recursive, diff --git a/AnyText/AnyText.Lsp/LspServer.cs b/AnyText/AnyText.Lsp/LspServer.cs index 85e46894..89de780f 100644 --- a/AnyText/AnyText.Lsp/LspServer.cs +++ b/AnyText/AnyText.Lsp/LspServer.cs @@ -21,6 +21,7 @@ public partial class LspServer : ILspServer private readonly Dictionary _documents = new Dictionary(); private readonly Dictionary _languages; private ClientCapabilities _clientCapabilities; + private WorkspaceFolder[] _workspaceFolders; /// /// Creates a new instance /// @@ -51,7 +52,7 @@ public InitializeResult Initialize( , object InitializationOptions = null) { _clientCapabilities = capabilities; - + _workspaceFolders = workspaceFolders; var serverCapabilities = new ServerCapabilities { TextDocumentSync = new TextDocumentSyncOptions From 8a9c8b5849b09e652037e268fb87ad4463fc0afd Mon Sep 17 00:00:00 2001 From: ahert001 Date: Tue, 21 Jan 2025 11:03:43 +0100 Subject: [PATCH 3/6] Move CodeActions to Rules --- AnyText/AnyText.Core/CodeActionInfo.cs | 5 +- .../AnyText.Core/ExecuteCommandArguments.cs | 31 +++ AnyText/AnyText.Core/Grammars/Grammar.cs | 23 +- AnyText/AnyText.Core/Rules/Rule.cs | 5 + .../Workspace/ChangeAnnotation.cs | 23 ++ AnyText/AnyText.Core/Workspace/CreateFile.cs | 28 +++ AnyText/AnyText.Core/Workspace/DeleteFile.cs | 28 +++ .../Workspace/DeleteFileOptions.cs | 18 ++ .../AnyText.Core/Workspace/DocumentChange.cs | 28 +++ AnyText/AnyText.Core/Workspace/FileOptions.cs | 18 ++ ...OptionalVersionedTextDocumentIdentifier.cs | 18 ++ AnyText/AnyText.Core/Workspace/RenameFile.cs | 33 +++ .../Workspace/TextDocumentEdit.cs | 18 ++ .../AnyText.Core/Workspace/WorkspaceEdit.cs | 26 +++ AnyText/AnyText.Core/WorkspaceEdit.cs | 219 ------------------ AnyText/AnyText.Lsp/LspServer.CodeAction.cs | 77 ++++-- .../AnyText.Lsp/LspServer.ExecuteCommand.cs | 46 +++- .../Grammars/AnyTextGrammar.manual.Actions.cs | 128 ++++++---- 18 files changed, 481 insertions(+), 291 deletions(-) create mode 100644 AnyText/AnyText.Core/ExecuteCommandArguments.cs create mode 100644 AnyText/AnyText.Core/Workspace/ChangeAnnotation.cs create mode 100644 AnyText/AnyText.Core/Workspace/CreateFile.cs create mode 100644 AnyText/AnyText.Core/Workspace/DeleteFile.cs create mode 100644 AnyText/AnyText.Core/Workspace/DeleteFileOptions.cs create mode 100644 AnyText/AnyText.Core/Workspace/DocumentChange.cs create mode 100644 AnyText/AnyText.Core/Workspace/FileOptions.cs create mode 100644 AnyText/AnyText.Core/Workspace/OptionalVersionedTextDocumentIdentifier.cs create mode 100644 AnyText/AnyText.Core/Workspace/RenameFile.cs create mode 100644 AnyText/AnyText.Core/Workspace/TextDocumentEdit.cs create mode 100644 AnyText/AnyText.Core/Workspace/WorkspaceEdit.cs delete mode 100644 AnyText/AnyText.Core/WorkspaceEdit.cs diff --git a/AnyText/AnyText.Core/CodeActionInfo.cs b/AnyText/AnyText.Core/CodeActionInfo.cs index cdf28e95..c32d44e2 100644 --- a/AnyText/AnyText.Core/CodeActionInfo.cs +++ b/AnyText/AnyText.Core/CodeActionInfo.cs @@ -1,3 +1,6 @@ +using System.Collections.Generic; +using NMF.AnyText.Workspace; + namespace NMF.AnyText { /// @@ -46,7 +49,7 @@ public class CodeActionInfo /// /// These are the parameters passed to the command when it is executed. /// - public object[] Arguments { get; set; } + public Dictionary Arguments { get; set; } /// /// Identifies the Diagnostic that this Action fixes /// diff --git a/AnyText/AnyText.Core/ExecuteCommandArguments.cs b/AnyText/AnyText.Core/ExecuteCommandArguments.cs new file mode 100644 index 00000000..f6d2969c --- /dev/null +++ b/AnyText/AnyText.Core/ExecuteCommandArguments.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace NMF.AnyText +{ + /// + /// Represents the arguments for executing a command on a document. + /// + public class ExecuteCommandArguments + { + + /// + /// URI of the document. + /// + public string DocumentUri { get; set; } + + /// + /// Starting position of the Range. + /// + public ParsePosition Start { get; set; } + + /// + /// Ending position of the Range. + /// + public ParsePosition End { get; set; } + + /// + /// Additional options for the command execution. + /// + public Dictionary OtherOptions { get; set; } + } +} \ No newline at end of file diff --git a/AnyText/AnyText.Core/Grammars/Grammar.cs b/AnyText/AnyText.Core/Grammars/Grammar.cs index 1cee514e..130a179c 100644 --- a/AnyText/AnyText.Core/Grammars/Grammar.cs +++ b/AnyText/AnyText.Core/Grammars/Grammar.cs @@ -179,14 +179,27 @@ public Rule Root protected abstract Rule GetRootRule(GrammarContext context); /// - /// Gets the list of code actions supported by this grammar. + /// Dictionary of executable actions. + /// The key is the action identifier, and the value is the action executor. /// - public virtual IEnumerable SupportedCodeActions { get; } = new List(); + protected Dictionary> ExecutableCodeActions { get; } = new (); /// - /// Dictionary of executable actions. - /// The key is the action identifier, and the value is the action executor. + /// Adds a new code action to the dictionary. + /// + /// The identifier of the action. + /// The action executor. + protected void AddExecutableCodeAction(string actionIdentifier, Func executor) + { + ExecutableCodeActions.Add(actionIdentifier, executor); + } + /// + /// Retrieves the dictionary of executable actions as a read-only dictionary. /// - public virtual Dictionary> ExecutableCodeActions { get; } = new Dictionary>(); + /// A read-only view of the dictionary. + public IReadOnlyDictionary> GetExecutableCodeActions() + { + return ExecutableCodeActions; + } } } diff --git a/AnyText/AnyText.Core/Rules/Rule.cs b/AnyText/AnyText.Core/Rules/Rule.cs index 2a65ed3c..ef4f13a3 100644 --- a/AnyText/AnyText.Core/Rules/Rule.cs +++ b/AnyText/AnyText.Core/Rules/Rule.cs @@ -200,5 +200,10 @@ public uint? TokenModifierIndex get; internal set; } + + /// + /// Gets the list of code actions supported by this grammar. + /// + public virtual IEnumerable SupportedCodeActions => new List(); } } diff --git a/AnyText/AnyText.Core/Workspace/ChangeAnnotation.cs b/AnyText/AnyText.Core/Workspace/ChangeAnnotation.cs new file mode 100644 index 00000000..fdea0fbc --- /dev/null +++ b/AnyText/AnyText.Core/Workspace/ChangeAnnotation.cs @@ -0,0 +1,23 @@ +namespace NMF.AnyText.Workspace +{ + /// + /// Represents metadata or instructions for an annotation associated with a change. + /// + public class ChangeAnnotation + { + /// + /// A label for the annotation (e.g., "Refactor"). + /// + public string Label { get; set; } + + /// + /// Indicates if the change requires user confirmation. + /// + public bool? NeedsConfirmation { get; set; } + + /// + /// A description or explanation of the annotation. + /// + public string Description { get; set; } + } +} \ No newline at end of file diff --git a/AnyText/AnyText.Core/Workspace/CreateFile.cs b/AnyText/AnyText.Core/Workspace/CreateFile.cs new file mode 100644 index 00000000..d2b1ee38 --- /dev/null +++ b/AnyText/AnyText.Core/Workspace/CreateFile.cs @@ -0,0 +1,28 @@ +namespace NMF.AnyText.Workspace +{ + /// + /// Represents the information needed to create a new file. + /// + public class CreateFile + { + /// + /// The type of file creation (e.g., "create"). + /// + public string Kind { get; set; } + + /// + /// The URI of the file to be created. + /// + public string Uri { get; set; } + + /// + /// File options (e.g., whether to overwrite an existing file). + /// + public FileOptions Options { get; set; } + + /// + /// An optional annotation ID related to the file creation. + /// + public string AnnotationId { get; set; } + } +} \ No newline at end of file diff --git a/AnyText/AnyText.Core/Workspace/DeleteFile.cs b/AnyText/AnyText.Core/Workspace/DeleteFile.cs new file mode 100644 index 00000000..79bcff21 --- /dev/null +++ b/AnyText/AnyText.Core/Workspace/DeleteFile.cs @@ -0,0 +1,28 @@ +namespace NMF.AnyText.Workspace +{ + /// + /// Represents the information needed to delete an existing file. + /// + public class DeleteFile + { + /// + /// The type of file operation (e.g., "delete"). + /// + public string Kind { get; set; } + + /// + /// The URI of the file to be deleted. + /// + public string Uri { get; set; } + + /// + /// File deletion options (e.g., whether to delete recursively). + /// + public DeleteFileOptions Options { get; set; } + + /// + /// An optional annotation ID related to the file deletion. + /// + public string AnnotationId { get; set; } + } +} \ No newline at end of file diff --git a/AnyText/AnyText.Core/Workspace/DeleteFileOptions.cs b/AnyText/AnyText.Core/Workspace/DeleteFileOptions.cs new file mode 100644 index 00000000..37546c97 --- /dev/null +++ b/AnyText/AnyText.Core/Workspace/DeleteFileOptions.cs @@ -0,0 +1,18 @@ +namespace NMF.AnyText.Workspace +{ + /// + /// Options for deleting files, such as recursive deletion and handling missing files. + /// + public class DeleteFileOptions + { + /// + /// If true, delete directories recursively. + /// + public bool? Recursive { get; set; } + + /// + /// If true, ignore the operation if the file does not exist. + /// + public bool? IgnoreIfNotExists { get; set; } + } +} \ No newline at end of file diff --git a/AnyText/AnyText.Core/Workspace/DocumentChange.cs b/AnyText/AnyText.Core/Workspace/DocumentChange.cs new file mode 100644 index 00000000..49c134c8 --- /dev/null +++ b/AnyText/AnyText.Core/Workspace/DocumentChange.cs @@ -0,0 +1,28 @@ +namespace NMF.AnyText.Workspace +{ + /// + /// Represents a change to a document, including text edits, file creation, renaming, or deletion. + /// + public class DocumentChange + { + /// + /// Text document edits (e.g., line insertions, deletions). + /// + public TextDocumentEdit TextDocumentEdit { get; set; } + + /// + /// Information for creating a new file. + /// + public CreateFile CreateFile { get; set; } + + /// + /// Information for renaming an existing file. + /// + public RenameFile RenameFile { get; set; } + + /// + /// Information for deleting an existing file. + /// + public DeleteFile DeleteFile { get; set; } + } +} \ No newline at end of file diff --git a/AnyText/AnyText.Core/Workspace/FileOptions.cs b/AnyText/AnyText.Core/Workspace/FileOptions.cs new file mode 100644 index 00000000..c5b55509 --- /dev/null +++ b/AnyText/AnyText.Core/Workspace/FileOptions.cs @@ -0,0 +1,18 @@ +namespace NMF.AnyText.Workspace +{ + /// + /// Options for creating or renaming files, such as overwrite behavior. + /// + public class FileOptions + { + /// + /// If true, overwrite an existing file. + /// + public bool? Overwrite { get; set; } + + /// + /// If true, ignore the operation if the file already exists. + /// + public bool? IgnoreIfExists { get; set; } + } +} \ No newline at end of file diff --git a/AnyText/AnyText.Core/Workspace/OptionalVersionedTextDocumentIdentifier.cs b/AnyText/AnyText.Core/Workspace/OptionalVersionedTextDocumentIdentifier.cs new file mode 100644 index 00000000..1569094a --- /dev/null +++ b/AnyText/AnyText.Core/Workspace/OptionalVersionedTextDocumentIdentifier.cs @@ -0,0 +1,18 @@ +namespace NMF.AnyText.Workspace +{ + /// + /// Identifies a text document with optional versioning. + /// + public class OptionalVersionedTextDocumentIdentifier + { + /// + /// The URI of the text document. + /// + public string Uri { get; set; } + + /// + /// An optional version number for the document, if versioning is supported. + /// + public int? Version { get; set; } + } +} \ No newline at end of file diff --git a/AnyText/AnyText.Core/Workspace/RenameFile.cs b/AnyText/AnyText.Core/Workspace/RenameFile.cs new file mode 100644 index 00000000..e8ce8fd6 --- /dev/null +++ b/AnyText/AnyText.Core/Workspace/RenameFile.cs @@ -0,0 +1,33 @@ +namespace NMF.AnyText.Workspace +{ + /// + /// Represents the information needed to rename an existing file. + /// + public class RenameFile + { + /// + /// The type of file operation (e.g., "rename"). + /// + public string Kind { get; set; } + + /// + /// The URI of the old file name. + /// + public string OldUri { get; set; } + + /// + /// The URI of the new file name. + /// + public string NewUri { get; set; } + + /// + /// File options (e.g., whether to overwrite). + /// + public FileOptions Options { get; set; } + + /// + /// An optional annotation ID related to the file rename. + /// + public string AnnotationId { get; set; } + } +} \ No newline at end of file diff --git a/AnyText/AnyText.Core/Workspace/TextDocumentEdit.cs b/AnyText/AnyText.Core/Workspace/TextDocumentEdit.cs new file mode 100644 index 00000000..9def3db5 --- /dev/null +++ b/AnyText/AnyText.Core/Workspace/TextDocumentEdit.cs @@ -0,0 +1,18 @@ +namespace NMF.AnyText.Workspace +{ + /// + /// Represents the text document edit instructions, including the document and the edits. + /// + public class TextDocumentEdit + { + /// + /// Identifies the text document to edit, including optional version information. + /// + public OptionalVersionedTextDocumentIdentifier TextDocument { get; set; } + + /// + /// An Array of edits to perform on the document (e.g., insertions, deletions). + /// + public TextEdit[] Edits { get; set; } + } +} \ No newline at end of file diff --git a/AnyText/AnyText.Core/Workspace/WorkspaceEdit.cs b/AnyText/AnyText.Core/Workspace/WorkspaceEdit.cs new file mode 100644 index 00000000..534b36d3 --- /dev/null +++ b/AnyText/AnyText.Core/Workspace/WorkspaceEdit.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace NMF.AnyText.Workspace +{ + /// + /// Represents changes to a workspace, including text edits, document changes, and change annotations. + /// + public class WorkspaceEdit + { + /// + /// A dictionary of changes to text documents, keyed by document URI, with the value being the text edits for that + /// document. + /// + public Dictionary Changes { get; set; } + + /// + /// A list of document-level changes (e.g., file creation, renaming, deletion). + /// + public List DocumentChanges { get; set; } + + /// + /// A dictionary of annotations associated with changes, keyed by annotation ID. + /// + public Dictionary ChangeAnnotations { get; set; } + } +} \ No newline at end of file diff --git a/AnyText/AnyText.Core/WorkspaceEdit.cs b/AnyText/AnyText.Core/WorkspaceEdit.cs deleted file mode 100644 index f741af7a..00000000 --- a/AnyText/AnyText.Core/WorkspaceEdit.cs +++ /dev/null @@ -1,219 +0,0 @@ -using System.Collections.Generic; - -namespace NMF.AnyText -{ - /// - /// Represents changes to a workspace, including text edits, document changes, and change annotations. - /// - public class WorkspaceEdit - { - /// - /// A dictionary of changes to text documents, keyed by document URI, with the value being the text edits for that document. - /// - public Dictionary Changes { get; set; } - - /// - /// A list of document-level changes (e.g., file creation, renaming, deletion). - /// - public List DocumentChanges { get; set; } - - /// - /// A dictionary of annotations associated with changes, keyed by annotation ID. - /// - public Dictionary ChangeAnnotations { get; set; } - } - - /// - /// Represents a change to a document, including text edits, file creation, renaming, or deletion. - /// - public class DocumentChange - { - /// - /// Text document edits (e.g., line insertions, deletions). - /// - public TextDocumentEdit TextDocumentEdit { get; set; } - - /// - /// Information for creating a new file. - /// - public CreateFile CreateFile { get; set; } - - /// - /// Information for renaming an existing file. - /// - public RenameFile RenameFile { get; set; } - - /// - /// Information for deleting an existing file. - /// - public DeleteFile DeleteFile { get; set; } - } - - /// - /// Represents metadata or instructions for an annotation associated with a change. - /// - public class ChangeAnnotation - { - /// - /// A label for the annotation (e.g., "Refactor"). - /// - public string Label { get; set; } - - /// - /// Indicates if the change requires user confirmation. - /// - public bool? NeedsConfirmation { get; set; } - - /// - /// A description or explanation of the annotation. - /// - public string Description { get; set; } - } - - /// - /// Represents the text document edit instructions, including the document and the edits. - /// - public class TextDocumentEdit - { - /// - /// Identifies the text document to edit, including optional version information. - /// - public OptionalVersionedTextDocumentIdentifier TextDocument { get; set; } - - /// - /// An Array of edits to perform on the document (e.g., insertions, deletions). - /// - public TextEdit[] Edits { get; set; } - } - - /// - /// Identifies a text document with optional versioning. - /// - public class OptionalVersionedTextDocumentIdentifier - { - /// - /// The URI of the text document. - /// - public string Uri { get; set; } - - /// - /// An optional version number for the document, if versioning is supported. - /// - public int? Version { get; set; } - } - - /// - /// Represents the information needed to create a new file. - /// - public class CreateFile - { - /// - /// The type of file creation (e.g., "create"). - /// - public string Kind { get; set; } - - /// - /// The URI of the file to be created. - /// - public string Uri { get; set; } - - /// - /// File options (e.g., whether to overwrite an existing file). - /// - public FileOptions Options { get; set; } - - /// - /// An optional annotation ID related to the file creation. - /// - public string AnnotationId { get; set; } - } - - /// - /// Represents the information needed to rename an existing file. - /// - public class RenameFile - { - /// - /// The type of file operation (e.g., "rename"). - /// - public string Kind { get; set; } - - /// - /// The URI of the old file name. - /// - public string OldUri { get; set; } - - /// - /// The URI of the new file name. - /// - public string NewUri { get; set; } - - /// - /// File options (e.g., whether to overwrite). - /// - public FileOptions Options { get; set; } - - /// - /// An optional annotation ID related to the file rename. - /// - public string AnnotationId { get; set; } - } - - /// - /// Represents the information needed to delete an existing file. - /// - public class DeleteFile - { - /// - /// The type of file operation (e.g., "delete"). - /// - public string Kind { get; set; } - - /// - /// The URI of the file to be deleted. - /// - public string Uri { get; set; } - - /// - /// File deletion options (e.g., whether to delete recursively). - /// - public DeleteFileOptions Options { get; set; } - - /// - /// An optional annotation ID related to the file deletion. - /// - public string AnnotationId { get; set; } - } - - /// - /// Options for creating or renaming files, such as overwrite behavior. - /// - public class FileOptions - { - /// - /// If true, overwrite an existing file. - /// - public bool? Overwrite { get; set; } - - /// - /// If true, ignore the operation if the file already exists. - /// - public bool? IgnoreIfExists { get; set; } - } - - /// - /// Options for deleting files, such as recursive deletion and handling missing files. - /// - public class DeleteFileOptions - { - /// - /// If true, delete directories recursively. - /// - public bool? Recursive { get; set; } - - /// - /// If true, ignore the operation if the file does not exist. - /// - public bool? IgnoreIfNotExists { get; set; } - } -} diff --git a/AnyText/AnyText.Lsp/LspServer.CodeAction.cs b/AnyText/AnyText.Lsp/LspServer.CodeAction.cs index 62aa87dd..9bca913a 100644 --- a/AnyText/AnyText.Lsp/LspServer.CodeAction.cs +++ b/AnyText/AnyText.Lsp/LspServer.CodeAction.cs @@ -1,9 +1,18 @@ -using LspTypes; -using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; +using LspTypes; +using Newtonsoft.Json.Linq; +using NMF.AnyText.Workspace; using Range = LspTypes.Range; +using ChangeAnnotation = NMF.AnyText.Workspace.ChangeAnnotation; +using CreateFile = NMF.AnyText.Workspace.CreateFile; +using DeleteFile = NMF.AnyText.Workspace.DeleteFile; +using DeleteFileOptions = LspTypes.DeleteFileOptions; +using OptionalVersionedTextDocumentIdentifier = LspTypes.OptionalVersionedTextDocumentIdentifier; +using RenameFile = NMF.AnyText.Workspace.RenameFile; +using TextDocumentEdit = NMF.AnyText.Workspace.TextDocumentEdit; +using WorkspaceEdit = NMF.AnyText.Workspace.WorkspaceEdit; namespace NMF.AnyText { @@ -27,19 +36,41 @@ public CodeAction[] CodeAction(JToken arg) var kindFilter = request.Context.Only; - var arguments = new[] { documentUri, range.Start, (object)range.End }; - var grammar = document.Context.Grammar; - foreach (var action in grammar.SupportedCodeActions) + var startPosition = AsParsePosition(request.Range.Start); + var endPosition = AsParsePosition(request.Range.End); + + var ruleApp = document.Context.Matcher.GetRuleApplicationsAt(AsParsePosition(range.Start)) + .FirstOrDefault(r => r.Rule.IsLiteral); + + if (ruleApp == null) return codeActions.ToArray(); + + while (!(ruleApp.CurrentPosition <= startPosition && + ruleApp.CurrentPosition + ruleApp.Length >= endPosition)) + { + ruleApp = ruleApp.Parent; + if (ruleApp == null) + return codeActions.ToArray(); + } + + var actions = ruleApp.Rule.SupportedCodeActions; + + var arguments = new object[] + { documentUri, ruleApp.CurrentPosition, ruleApp.CurrentPosition + ruleApp.Length }; + + foreach (var action in actions) { var diagnosticIdentifier = action.DiagnosticIdentifier; var relevantDiagnostics = diagnostics .Where(d => d.Message.Contains(diagnosticIdentifier)) .ToArray(); + if (!string.IsNullOrEmpty(diagnosticIdentifier) && relevantDiagnostics.Length == 0) continue; + var actionKind = !string.IsNullOrEmpty(action.Kind) ? ParseLspCodeActionKind(action.Kind) : null; if (kindFilter != null && kindFilter.Any() && actionKind != null && !kindFilter.Contains(actionKind.Value)) continue; + codeActions.Add(new CodeAction { Title = action.Title, @@ -52,7 +83,9 @@ public CodeAction[] CodeAction(JToken arg) { Title = action.CommandTitle, CommandIdentifier = action.Command, - Arguments = arguments.Concat(action.Arguments).ToArray() + Arguments = action.Arguments != null + ? arguments.Concat(action.Arguments.Cast()).ToArray() + : arguments.ToArray() } : null }); @@ -86,15 +119,18 @@ private LspTypes.WorkspaceEdit MapWorkspaceEdit(WorkspaceEdit workspaceEdit) { Changes = workspaceEdit.Changes != null ? MapChanges(workspaceEdit.Changes) : null, DocumentChanges = MapDocumentChanges(workspaceEdit.DocumentChanges), - ChangeAnnotations = workspaceEdit.ChangeAnnotations != null ? MapChangeAnnotations(workspaceEdit.ChangeAnnotations) : null + ChangeAnnotations = workspaceEdit.ChangeAnnotations != null + ? MapChangeAnnotations(workspaceEdit.ChangeAnnotations) + : null }; } private Dictionary MapChanges(Dictionary changes) { var lspChanges = new Dictionary(); - - foreach (var entry in changes) lspChanges.Add(entry.Key, MapTextEditsArray(entry.Value)); + var workspaceFolder = _workspaceFolders.FirstOrDefault()?.Uri; + foreach (var entry in changes) + lspChanges.Add($"{workspaceFolder}/{entry.Key}", MapTextEditsArray(entry.Value)); return lspChanges; } @@ -152,13 +188,13 @@ private LspTypes.TextEdit MapTextEdit(TextEdit textEdit) private SumType MapTextDocumentEdit(TextDocumentEdit textDocumentEdit) { - string workspaceFolder = _workspaceFolders.FirstOrDefault()?.Uri; - string fileUri = $"{workspaceFolder}/{textDocumentEdit.TextDocument.Uri}"; + var workspaceFolder = _workspaceFolders.FirstOrDefault()?.Uri; + var fileUri = $"{workspaceFolder}/{textDocumentEdit.TextDocument.Uri}"; var edits = textDocumentEdit.Edits .Select(e => new SumType(MapTextEdit(e))).ToArray(); var lspTextDocumentEdit = new LspTypes.TextDocumentEdit { - TextDocument = new LspTypes.OptionalVersionedTextDocumentIdentifier + TextDocument = new OptionalVersionedTextDocumentIdentifier { Uri = fileUri, Version = textDocumentEdit.TextDocument.Version @@ -174,8 +210,8 @@ private LspTypes.TextEdit MapTextEdit(TextEdit textEdit) private SumType MapCreateFile(CreateFile createFile) { - string workspaceFolder = _workspaceFolders.FirstOrDefault()?.Uri; - string fileUri = $"{workspaceFolder}/{createFile.Uri}"; + var workspaceFolder = _workspaceFolders.FirstOrDefault()?.Uri; + var fileUri = $"{workspaceFolder}/{createFile.Uri}"; var lspCreateFile = new LspTypes.CreateFile { Kind = createFile.Kind, @@ -196,12 +232,11 @@ private LspTypes.TextEdit MapTextEdit(TextEdit textEdit) private SumType MapRenameFile(RenameFile renameFile) { - string workspaceFolder = _workspaceFolders.FirstOrDefault()?.Uri; - string oldfileUri = $"{workspaceFolder}/{renameFile.OldUri}"; - string newfileUri = $"{workspaceFolder}/{renameFile.NewUri}"; + var workspaceFolder = _workspaceFolders.FirstOrDefault()?.Uri; + var oldfileUri = $"{workspaceFolder}/{renameFile.OldUri}"; + var newfileUri = $"{workspaceFolder}/{renameFile.NewUri}"; var lspRenameFile = new LspTypes.RenameFile { - Kind = renameFile.Kind, OldUri = oldfileUri, NewUri = newfileUri, @@ -221,13 +256,13 @@ private LspTypes.TextEdit MapTextEdit(TextEdit textEdit) private SumType MapDeleteFile(DeleteFile deleteFile) { - string workspaceFolder = _workspaceFolders.FirstOrDefault()?.Uri; - string fileUri = $"{workspaceFolder}/{deleteFile.Uri}"; + var workspaceFolder = _workspaceFolders.FirstOrDefault()?.Uri; + var fileUri = $"{workspaceFolder}/{deleteFile.Uri}"; var lspDeleteFile = new LspTypes.DeleteFile { Kind = deleteFile.Kind, Uri = fileUri, - Options = new LspTypes.DeleteFileOptions + Options = new DeleteFileOptions { Recursive = deleteFile.Options?.Recursive, IgnoreIfNotExists = deleteFile.Options?.IgnoreIfNotExists diff --git a/AnyText/AnyText.Lsp/LspServer.ExecuteCommand.cs b/AnyText/AnyText.Lsp/LspServer.ExecuteCommand.cs index 50418c83..4d7ebe52 100644 --- a/AnyText/AnyText.Lsp/LspServer.ExecuteCommand.cs +++ b/AnyText/AnyText.Lsp/LspServer.ExecuteCommand.cs @@ -1,7 +1,10 @@ -using LspTypes; -using Newtonsoft.Json.Linq; using System; +using System.Collections.Generic; using System.Linq; +using System.Text.Json; +using LspTypes; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace NMF.AnyText { @@ -17,13 +20,35 @@ public void ExecuteCommand(JToken arg) return; } - if (!language.ExecutableCodeActions.TryGetValue(request.Command, out var executableCodeAction)) + + var actions = language.GetExecutableCodeActions(); + + if (!actions.TryGetValue(request.Command, out var action)) { SendLogMessage(MessageType.Error, $"{request.Command} Command not supported"); return; } - executableCodeAction.Invoke(request.Arguments); + var args = request.Arguments; + Dictionary dict = null; + if (args.Length > 3 && args[3] != null) + { + var jsonObject = JsonConvert.DeserializeObject(args[3].ToString()!); + dict = new Dictionary + { + { jsonObject.Key.ToString(), jsonObject.Value } + }; + } + + var executeCommandArguments = new ExecuteCommandArguments + + { + DocumentUri = args[0].ToString(), + Start = ParsePositionFromJson(args[1].ToString()), + End = ParsePositionFromJson(args[2].ToString()), + OtherOptions = dict + }; + action.Invoke(executeCommandArguments); } private Registration CreateExecuteCommandRegistration(string languageId) @@ -31,9 +56,10 @@ private Registration CreateExecuteCommandRegistration(string languageId) _languages.TryGetValue(languageId, out var language); if (language == null) return null; + var registrationOptions = new ExecuteCommandRegistrationOptions { - Commands = language.ExecutableCodeActions.Keys.ToArray() + Commands = language.GetExecutableCodeActions().Keys.ToArray() }; return new Registration { @@ -42,5 +68,15 @@ private Registration CreateExecuteCommandRegistration(string languageId) Method = Methods.WorkspaceExecuteCommandName }; } + + private static ParsePosition ParsePositionFromJson(string jsonString) + { + var jsonDocument = JsonDocument.Parse(jsonString); + + var line = jsonDocument.RootElement.GetProperty("Line").GetInt32(); + var col = jsonDocument.RootElement.GetProperty("Col").GetInt32(); + + return new ParsePosition(line, col); + } } } \ No newline at end of file diff --git a/AnyText/AnyText/Grammars/AnyTextGrammar.manual.Actions.cs b/AnyText/AnyText/Grammars/AnyTextGrammar.manual.Actions.cs index 78e352d0..28d7256e 100644 --- a/AnyText/AnyText/Grammars/AnyTextGrammar.manual.Actions.cs +++ b/AnyText/AnyText/Grammars/AnyTextGrammar.manual.Actions.cs @@ -3,58 +3,116 @@ using System.IO; using System.Text; using System.Text.Json; +using NMF.AnyText.Workspace; +using FileOptions = NMF.AnyText.Workspace.FileOptions; namespace NMF.AnyText.Grammars { public partial class AnyTextGrammar { - /// - /// List of Possible Code Actions - /// - public override IEnumerable SupportedCodeActions { get; } = new List + public AnyTextGrammar() + { + foreach (var exe in ExecutableCodeActions) AddExecutableCodeAction(exe.Key, exe.Value); + } + + public partial class ModelRuleRule { - new() + public override IEnumerable SupportedCodeActions => new List { - Title = "Generate comment header", - Kind = "refactor.extract", - CommandTitle = "Insert Comment Header", - WorkspaceEdit = null, - Diagnostics = new[] { "" }, - Command = "editor.action.addCommentHeader", - Arguments = new[] { "a" } - } - }; + new() + { + Title = "Copy to new File", + Kind = "quickfix", + WorkspaceEdit = new WorkspaceEdit + { + DocumentChanges = new List + { + new() + { + CreateFile = new CreateFile + { + Options = new FileOptions + { + IgnoreIfExists = false, + Overwrite = true + }, + AnnotationId = "createFile", + Kind = "create", + Uri = "newDocument.anytext" + } + }, + new() + { + TextDocumentEdit = new TextDocumentEdit + { + TextDocument = new OptionalVersionedTextDocumentIdentifier + { + Version = 1, + Uri = "newDocument.anytext" + }, + Edits = new[] + { + new TextEdit(new ParsePosition(0, 0), new ParsePosition(0, 0), + new[] { "Text in new File" }) + } + } + } + }, + ChangeAnnotations = new Dictionary + { + { + "createFile", new ChangeAnnotation + { + Description = "description", + Label = "label", + NeedsConfirmation = true + } + } + } + }, + Diagnostics = new[] { "" } + } + }; + } + + public partial class GrammarRule + { + public override IEnumerable SupportedCodeActions => new List + { + new() + { + Title = "Generate comment header", + Kind = "refactor.extract", + CommandTitle = "Insert Comment Header", + WorkspaceEdit = null, + Diagnostics = new[] { "" }, + Command = "editor.action.addCommentHeader" + } + }; + } /// /// Dictionary of Code Action Identifier and the Executable Action /// - public override Dictionary> ExecutableCodeActions { get; } = new() + public Dictionary> ExecutableCodeActions { get; } = new() { { "editor.action.addCommentHeader", obj => { - var arguments = obj; - if (arguments != null && arguments.Length > 0) + var documentUri = obj.DocumentUri; + var start = obj.Start; + var end = obj.End; + if (documentUri != null) { - var documentUri = (string)arguments[0]; - var startRange = ParsePositionFromJson(arguments[1].ToString()); - var endRange = ParsePositionFromJson(arguments[2].ToString()); - - if (documentUri != null) - { - InsertCommentHeader(documentUri); - return "Comment header generated."; - } - - return "Invalid document URI."; + InsertCommentHeader(documentUri); + return "Comment header generated."; } - return "No arguments provided."; + return "Invalid document URI."; } } }; - private static void InsertCommentHeader(string filePath) { var uri = new Uri(Uri.UnescapeDataString(filePath), UriKind.RelativeOrAbsolute); @@ -86,15 +144,5 @@ private static void SaveDocument(string filePath, string[] updatedContent) { File.WriteAllLines(filePath, updatedContent); } - - private static ParsePosition ParsePositionFromJson(string jsonString) - { - var jsonDocument = JsonDocument.Parse(jsonString); - - var line = jsonDocument.RootElement.GetProperty("line").GetInt32(); - var character = jsonDocument.RootElement.GetProperty("character").GetInt32(); - - return new ParsePosition(line, character); - } } } \ No newline at end of file From e7ec807cc607a46df4b86e8763e0bb10c1a15277 Mon Sep 17 00:00:00 2001 From: ahert001 Date: Wed, 22 Jan 2025 08:36:12 +0100 Subject: [PATCH 4/6] Add CodeLens --- AnyText/AnyText.Core/CodeLensInfo.cs | 41 +++++++++++++ AnyText/AnyText.Core/Grammars/Grammar.cs | 10 ++-- .../Rules/MultiRuleApplication.cs | 10 ++++ AnyText/AnyText.Core/Rules/Rule.cs | 7 ++- AnyText/AnyText.Core/Rules/RuleApplication.cs | 24 ++++++++ .../Rules/SingleRuleApplication.cs | 9 +++ AnyText/AnyText.Lsp/ILspServer.CodeLens.cs | 24 ++++++++ AnyText/AnyText.Lsp/LspServer.CodeLens.cs | 57 +++++++++++++++++++ .../AnyText.Lsp/LspServer.ExecuteCommand.cs | 14 +++-- AnyText/AnyText.Lsp/LspServer.cs | 7 ++- .../Grammars/AnyTextGrammar.manual.Actions.cs | 28 ++++++++- 11 files changed, 216 insertions(+), 15 deletions(-) create mode 100644 AnyText/AnyText.Core/CodeLensInfo.cs create mode 100644 AnyText/AnyText.Lsp/ILspServer.CodeLens.cs create mode 100644 AnyText/AnyText.Lsp/LspServer.CodeLens.cs diff --git a/AnyText/AnyText.Core/CodeLensInfo.cs b/AnyText/AnyText.Core/CodeLensInfo.cs new file mode 100644 index 00000000..211d2682 --- /dev/null +++ b/AnyText/AnyText.Core/CodeLensInfo.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; + +namespace NMF.AnyText +{ + /// + /// Represents a CodeLens item used for a Language Server Protocol (LSP) server. + /// CodeLens provides information or actions associated with specific locations in a text document. + /// + public class CodeLensInfo + { + /// + /// Gets or sets the title of the CodeLens item, typically a label displayed in the editor. + /// + public string Title { get; set; } + + /// + /// Gets or sets the identifier for the command to be executed when the CodeLens is activated. + /// + public string CommandIdentifier { get; set; } + + /// + /// Gets or sets the dictionary of arguments to be passed along with the command when invoked. + /// + public Dictionary Arguments { get; set; } + + /// + /// Gets or sets the start position of the text range that this CodeLens is associated with. + /// + public ParsePosition Start { get; set; } + + /// + /// Gets or sets the end position of the text range that this CodeLens is associated with. + /// + public ParsePosition End { get; set; } + + /// + /// Gets or sets additional data associated with this CodeLens, which can be used for custom functionality. + /// + public object Data { get; set; } + } +} \ No newline at end of file diff --git a/AnyText/AnyText.Core/Grammars/Grammar.cs b/AnyText/AnyText.Core/Grammars/Grammar.cs index 130a179c..6b648a48 100644 --- a/AnyText/AnyText.Core/Grammars/Grammar.cs +++ b/AnyText/AnyText.Core/Grammars/Grammar.cs @@ -182,24 +182,24 @@ public Rule Root /// Dictionary of executable actions. /// The key is the action identifier, and the value is the action executor. /// - protected Dictionary> ExecutableCodeActions { get; } = new (); + protected Dictionary> ExecutableActions { get; } = new (); /// /// Adds a new code action to the dictionary. /// /// The identifier of the action. /// The action executor. - protected void AddExecutableCodeAction(string actionIdentifier, Func executor) + protected void AddExecutableAction(string actionIdentifier, Func executor) { - ExecutableCodeActions.Add(actionIdentifier, executor); + ExecutableActions.Add(actionIdentifier, executor); } /// /// Retrieves the dictionary of executable actions as a read-only dictionary. /// /// A read-only view of the dictionary. - public IReadOnlyDictionary> GetExecutableCodeActions() + public IReadOnlyDictionary> GetExecutableActions() { - return ExecutableCodeActions; + return ExecutableActions; } } } diff --git a/AnyText/AnyText.Core/Rules/MultiRuleApplication.cs b/AnyText/AnyText.Core/Rules/MultiRuleApplication.cs index e0b3bd60..d9713408 100644 --- a/AnyText/AnyText.Core/Rules/MultiRuleApplication.cs +++ b/AnyText/AnyText.Core/Rules/MultiRuleApplication.cs @@ -58,6 +58,16 @@ public override void Deactivate(ParseContext context) } base.Deactivate(context); } + + /// + public override void AddCodeLenses(ICollection codeLenses) + { + foreach (var ruleApplication in Inner) + { + ruleApplication.AddCodeLenses(codeLenses); + } + base.AddCodeLenses(codeLenses); + } internal override RuleApplication MigrateTo(MultiRuleApplication multiRule, ParseContext context) { diff --git a/AnyText/AnyText.Core/Rules/Rule.cs b/AnyText/AnyText.Core/Rules/Rule.cs index ef4f13a3..d1e021c9 100644 --- a/AnyText/AnyText.Core/Rules/Rule.cs +++ b/AnyText/AnyText.Core/Rules/Rule.cs @@ -202,8 +202,13 @@ public uint? TokenModifierIndex } /// - /// Gets the list of code actions supported by this grammar. + /// Gets the list of code actions for this rule. /// public virtual IEnumerable SupportedCodeActions => new List(); + + /// + /// Gets the list of code lenses for this rule. + /// + public virtual IEnumerable SupportedCodeLenses => new List(); } } diff --git a/AnyText/AnyText.Core/Rules/RuleApplication.cs b/AnyText/AnyText.Core/Rules/RuleApplication.cs index ab2ee44e..fc30b8a4 100644 --- a/AnyText/AnyText.Core/Rules/RuleApplication.cs +++ b/AnyText/AnyText.Core/Rules/RuleApplication.cs @@ -172,7 +172,31 @@ public virtual void Deactivate(ParseContext context) /// /// A collection of parse errors public virtual IEnumerable CreateParseErrors() => Enumerable.Empty(); + + /// + /// Add all CodeLenses of this ruleApplication to a collection + /// + /// Collection of CodeLenses + public virtual void AddCodeLenses(ICollection codeLenses) + { + if (Rule.SupportedCodeLenses.Any()) + { + var ruleCodeLenses = Rule.SupportedCodeLenses.Select(a => new CodeLensInfo() + { + Arguments = a.Arguments, + CommandIdentifier = a.CommandIdentifier, + Data = a.Data, + Title = a.Title, + Start = CurrentPosition, + End = CurrentPosition + Length, + }); + foreach (var codeLens in ruleCodeLenses) + { + codeLenses.Add(codeLens); + } + } + } /// /// Gets called when the newPosition of the given rule application changes /// diff --git a/AnyText/AnyText.Core/Rules/SingleRuleApplication.cs b/AnyText/AnyText.Core/Rules/SingleRuleApplication.cs index a2bad5d0..a129abc4 100644 --- a/AnyText/AnyText.Core/Rules/SingleRuleApplication.cs +++ b/AnyText/AnyText.Core/Rules/SingleRuleApplication.cs @@ -47,6 +47,15 @@ public override void Deactivate(ParseContext context) } base.Deactivate(context); } + /// + public override void AddCodeLenses(ICollection codeLenses) + { + if (Inner != null && Inner.IsActive) + { + Inner.AddCodeLenses(codeLenses); + } + base.AddCodeLenses(codeLenses); + } internal override RuleApplication MigrateTo(SingleRuleApplication singleRule, ParseContext context) { diff --git a/AnyText/AnyText.Lsp/ILspServer.CodeLens.cs b/AnyText/AnyText.Lsp/ILspServer.CodeLens.cs new file mode 100644 index 00000000..99dd1487 --- /dev/null +++ b/AnyText/AnyText.Lsp/ILspServer.CodeLens.cs @@ -0,0 +1,24 @@ +using LspTypes; +using Newtonsoft.Json.Linq; +using StreamJsonRpc; + +namespace NMF.AnyText +{ + public partial interface ILspServer + { + /// + /// Handles the textDocument/codeLens request from the client. + /// + /// The JSON token containing the parameters of the request. (CodeLensParams) + /// A Array of objects containing the available CodeLenses of the document. + [JsonRpcMethod(Methods.TextDocumentCodeLensName)] + CodeLens[] CodeLens(JToken arg); + /// + /// Handles the codeLense/resolve request from the client. + /// + /// The JSON token containing the parameters of the request. (CodeLens) + /// A object containing the executed CodeLens + [JsonRpcMethod(Methods.CodeLensResolveName)] + CodeLens CodeLensResolve(JToken arg); + } +} \ No newline at end of file diff --git a/AnyText/AnyText.Lsp/LspServer.CodeLens.cs b/AnyText/AnyText.Lsp/LspServer.CodeLens.cs new file mode 100644 index 00000000..b2d4ea31 --- /dev/null +++ b/AnyText/AnyText.Lsp/LspServer.CodeLens.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.Linq; +using LspTypes; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NMF.Utilities; + +namespace NMF.AnyText +{ + public partial class LspServer + { + /// + public CodeLens[] CodeLens(JToken arg) + { + var request = arg.ToObject(); + + if (!_documents.TryGetValue(request.TextDocument.Uri, out var document)) + return new CodeLens[] {}; + + var codeLensInfos = new List(); + document.Context.RootRuleApplication.AddCodeLenses(codeLensInfos); + var codeLenses = codeLensInfos.Select(c => + { + + var arguments = new object[] { request.TextDocument.Uri, c.Start, c.End, }; + return new CodeLens + { + Command = new Command() + { + Title = c.Title, + CommandIdentifier = c.CommandIdentifier, + Arguments = c.Arguments != null + ? arguments.Concat(c.Arguments.Cast()).ToArray() + : arguments.ToArray() + }, + Data = c.Data, + Range = new Range() + { + Start = new Position((uint)c.Start.Line, (uint)c.Start.Col), + End = new Position((uint)c.End.Line, (uint)c.End.Col) + } + }; + }); + return codeLenses.ToArray(); + } + + /// + public CodeLens CodeLensResolve(JToken arg) + { + var request = arg.ToObject(); + + ExecuteCommand(request.Command.CommandIdentifier, request.Command.Arguments); + + return request; + } + } +} diff --git a/AnyText/AnyText.Lsp/LspServer.ExecuteCommand.cs b/AnyText/AnyText.Lsp/LspServer.ExecuteCommand.cs index 4d7ebe52..799e500b 100644 --- a/AnyText/AnyText.Lsp/LspServer.ExecuteCommand.cs +++ b/AnyText/AnyText.Lsp/LspServer.ExecuteCommand.cs @@ -14,6 +14,11 @@ public partial class LspServer public void ExecuteCommand(JToken arg) { var request = arg.ToObject(); + ExecuteCommand(request.Command, request.Arguments); + } + + private void ExecuteCommand(string commandIdentifier, object[] args) + { if (!_languages.TryGetValue(_currentLanguageId, out var language)) { SendLogMessage(MessageType.Error, $"{_currentLanguageId} Language not found"); @@ -21,15 +26,14 @@ public void ExecuteCommand(JToken arg) } - var actions = language.GetExecutableCodeActions(); + var actions = language.GetExecutableActions(); - if (!actions.TryGetValue(request.Command, out var action)) + if (!actions.TryGetValue(commandIdentifier, out var action)) { - SendLogMessage(MessageType.Error, $"{request.Command} Command not supported"); + SendLogMessage(MessageType.Error, $"{commandIdentifier} Command not supported"); return; } - var args = request.Arguments; Dictionary dict = null; if (args.Length > 3 && args[3] != null) { @@ -59,7 +63,7 @@ private Registration CreateExecuteCommandRegistration(string languageId) var registrationOptions = new ExecuteCommandRegistrationOptions { - Commands = language.GetExecutableCodeActions().Keys.ToArray() + Commands = language.GetExecutableActions().Keys.ToArray() }; return new Registration { diff --git a/AnyText/AnyText.Lsp/LspServer.cs b/AnyText/AnyText.Lsp/LspServer.cs index 89de780f..0d28029b 100644 --- a/AnyText/AnyText.Lsp/LspServer.cs +++ b/AnyText/AnyText.Lsp/LspServer.cs @@ -76,7 +76,12 @@ public InitializeResult Initialize( CodeActionKind.Source, CodeActionKind.QuickFix, CodeActionKind.RefactorInline, CodeActionKind.RefactorRewrite, CodeActionKind.SourceOrganizeImports } - } + }, + CodeLensProvider = new CodeLensOptions() + { + ResolveProvider = true + }, + }; UpdateTraceSource(trace); diff --git a/AnyText/AnyText/Grammars/AnyTextGrammar.manual.Actions.cs b/AnyText/AnyText/Grammars/AnyTextGrammar.manual.Actions.cs index 28d7256e..a7c31e4c 100644 --- a/AnyText/AnyText/Grammars/AnyTextGrammar.manual.Actions.cs +++ b/AnyText/AnyText/Grammars/AnyTextGrammar.manual.Actions.cs @@ -12,9 +12,24 @@ public partial class AnyTextGrammar { public AnyTextGrammar() { - foreach (var exe in ExecutableCodeActions) AddExecutableCodeAction(exe.Key, exe.Value); + foreach (var exe in ExecutableActions) AddExecutableAction(exe.Key, exe.Value); } + public partial class FragmentRuleRule + { + public override IEnumerable SupportedCodeLenses => new List() + { + new() + { + Title = "Run Test", + CommandIdentifier = "codelens.runTest", + Arguments = new Dictionary() + { + {"test","test"}, + } + } + }; + } public partial class ModelRuleRule { public override IEnumerable SupportedCodeActions => new List @@ -92,9 +107,9 @@ public partial class GrammarRule } /// - /// Dictionary of Code Action Identifier and the Executable Action + /// Dictionary of Code Identifier and the Executable Action /// - public Dictionary> ExecutableCodeActions { get; } = new() + public Dictionary> ExecutableActions { get; } = new() { { "editor.action.addCommentHeader", obj => @@ -110,6 +125,13 @@ public partial class GrammarRule return "Invalid document URI."; } + }, + { + "codelens.runTest", obj => + { + Console.Error.WriteLine("TestRun"); + return null; + } } }; From a58e8da594382e0c41ca934e7177774dc8f747c2 Mon Sep 17 00:00:00 2001 From: ahert001 Date: Wed, 22 Jan 2025 16:26:17 +0100 Subject: [PATCH 5/6] Change executable Actions and add filter for actions --- AnyText/AnyText.Core/CodeActionInfo.cs | 25 +++- AnyText/AnyText.Core/CodeLensInfo.cs | 8 +- .../AnyText.Core/ExecuteCommandArguments.cs | 5 +- AnyText/AnyText.Core/Grammars/Grammar.cs | 21 ++- AnyText/AnyText.Core/Parser.CodeAction.cs | 70 ++++++++++ .../Rules/MultiRuleApplication.cs | 6 +- AnyText/AnyText.Core/Rules/Rule.cs | 4 +- AnyText/AnyText.Core/Rules/RuleApplication.cs | 16 ++- .../Rules/SingleRuleApplication.cs | 8 +- AnyText/AnyText.Lsp/LspServer.CodeAction.cs | 37 +++--- .../AnyText.Lsp/LspServer.ExecuteCommand.cs | 11 +- .../Grammars/AnyTextGrammar.manual.Actions.cs | 124 ++++++++---------- 12 files changed, 220 insertions(+), 115 deletions(-) create mode 100644 AnyText/AnyText.Core/Parser.CodeAction.cs diff --git a/AnyText/AnyText.Core/CodeActionInfo.cs b/AnyText/AnyText.Core/CodeActionInfo.cs index c32d44e2..7b0e2579 100644 --- a/AnyText/AnyText.Core/CodeActionInfo.cs +++ b/AnyText/AnyText.Core/CodeActionInfo.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using NMF.AnyText.Workspace; @@ -44,20 +45,36 @@ public class CodeActionInfo /// /// The command is the identifier or name of the action to execute when the user selects it. /// - public string Command { get; set; } + public string CommandIdentifier { get; set; } /// /// These are the parameters passed to the command when it is executed. /// public Dictionary Arguments { get; set; } + /// /// Identifies the Diagnostic that this Action fixes /// public string DiagnosticIdentifier { get; set; } + /// - /// Defines the Workspace changes this action executes + /// Defines the how the WorkspaceEdit Object of this CodeAction is created /// - public WorkspaceEdit WorkspaceEdit { get; set; } - + public Func WorkspaceEdit { get; set; } + + /// + /// Start Position of the RuleApplication of this action + /// + public ParsePosition Start { get; set; } + + /// + /// End Position of the RuleApplication of this action + /// + public ParsePosition End { get; set; } + + /// + /// The actual execution of this CodeAction + /// + public Action Action { get; set; } } } \ No newline at end of file diff --git a/AnyText/AnyText.Core/CodeLensInfo.cs b/AnyText/AnyText.Core/CodeLensInfo.cs index 211d2682..4fe74b70 100644 --- a/AnyText/AnyText.Core/CodeLensInfo.cs +++ b/AnyText/AnyText.Core/CodeLensInfo.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace NMF.AnyText { @@ -37,5 +38,10 @@ public class CodeLensInfo /// Gets or sets additional data associated with this CodeLens, which can be used for custom functionality. /// public object Data { get; set; } + + /// + /// The actual execution of this CodeLens + /// + public Action Action { get; set; } } } \ No newline at end of file diff --git a/AnyText/AnyText.Core/ExecuteCommandArguments.cs b/AnyText/AnyText.Core/ExecuteCommandArguments.cs index f6d2969c..4da6b455 100644 --- a/AnyText/AnyText.Core/ExecuteCommandArguments.cs +++ b/AnyText/AnyText.Core/ExecuteCommandArguments.cs @@ -7,7 +7,10 @@ namespace NMF.AnyText /// public class ExecuteCommandArguments { - + /// + /// ParseContext of the Document + /// + public ParseContext Context { get; set; } /// /// URI of the document. /// diff --git a/AnyText/AnyText.Core/Grammars/Grammar.cs b/AnyText/AnyText.Core/Grammars/Grammar.cs index 6b648a48..5fed85c1 100644 --- a/AnyText/AnyText.Core/Grammars/Grammar.cs +++ b/AnyText/AnyText.Core/Grammars/Grammar.cs @@ -56,6 +56,9 @@ public void Initialize() foreach (var rule in allRules) { rule.Initialize(_context); + + AddActionsFromRule(rule); + if (rule.TokenType != null) { CalculateTokenIndices(tokenTypes, tokenModifiers, rule, out int tokenTypeIndex, out int tokenModifierIndex); @@ -73,6 +76,18 @@ public void Initialize() } } + private void AddActionsFromRule(Rule rule) + { + if (rule.SupportedCodeActions.Any()) + foreach (var actionInfo in rule.SupportedCodeActions) + if (!string.IsNullOrEmpty(actionInfo.CommandIdentifier)) + ExecutableActions.TryAdd(actionInfo.CommandIdentifier, actionInfo.Action); + + if (rule.SupportedCodeLenses.Any()) + foreach (var lensInfo in rule.SupportedCodeLenses) + ExecutableActions.TryAdd(lensInfo.CommandIdentifier, lensInfo.Action); + } + /// /// Gets an array of comment rules used by this grammar /// @@ -182,14 +197,14 @@ public Rule Root /// Dictionary of executable actions. /// The key is the action identifier, and the value is the action executor. /// - protected Dictionary> ExecutableActions { get; } = new (); + protected Dictionary> ExecutableActions { get; } = new (); /// /// Adds a new code action to the dictionary. /// /// The identifier of the action. /// The action executor. - protected void AddExecutableAction(string actionIdentifier, Func executor) + protected void AddExecutableAction(string actionIdentifier, Action executor) { ExecutableActions.Add(actionIdentifier, executor); } @@ -197,7 +212,7 @@ protected void AddExecutableAction(string actionIdentifier, Func /// A read-only view of the dictionary. - public IReadOnlyDictionary> GetExecutableActions() + public IReadOnlyDictionary> GetExecutableActions() { return ExecutableActions; } diff --git a/AnyText/AnyText.Core/Parser.CodeAction.cs b/AnyText/AnyText.Core/Parser.CodeAction.cs new file mode 100644 index 00000000..56ae1d67 --- /dev/null +++ b/AnyText/AnyText.Core/Parser.CodeAction.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NMF.AnyText.Rules; + +namespace NMF.AnyText +{ + public partial class Parser + { + /// + /// Retrieves code action information within a specified range of parse positions. + /// + /// The starting position of the range. + /// The ending position of the range. + /// An optional predicate to filter rule applications. + /// A collection of objects representing available code actions. + public IEnumerable GetCodeActionInfo(ParsePosition start, ParsePosition end, + Predicate predicate = null) + { + predicate ??= _ => true; + var codeActionInfos = new List(); + + var ruleApp = Context.Matcher.GetRuleApplicationsAt(start) + .FirstOrDefault(r => r.Rule.IsLiteral); + + if (ruleApp == null) return codeActionInfos.ToArray(); + + while (!(ruleApp.CurrentPosition <= start && + ruleApp.CurrentPosition + ruleApp.Length >= end)) + { + ruleApp = ruleApp.Parent; + if (ruleApp == null) + return codeActionInfos; + } + + CollectCodeActionsWithPositions(ruleApp, predicate, codeActionInfos); + + var parent = ruleApp.Parent; + while (parent != null && parent.Length == ruleApp.Length) + { + CollectCodeActionsWithPositions(parent, predicate, codeActionInfos); + parent = parent.Parent; + } + + + return codeActionInfos; + } + + private static void CollectCodeActionsWithPositions(RuleApplication ruleApp, Predicate predicate, + List codeActionInfos) + { + if (predicate.Invoke(ruleApp)) + codeActionInfos.AddRange(ruleApp.Rule.SupportedCodeActions.Select(a => new CodeActionInfo + { + Start = ruleApp.CurrentPosition, + End = ruleApp.CurrentPosition + ruleApp.Length, + Action = a.Action, + CommandIdentifier = a.CommandIdentifier, + Arguments = a.Arguments, + Diagnostics = a.Diagnostics, + Kind = a.Kind, + CommandTitle = a.CommandTitle, + Title = a.Title, + DiagnosticIdentifier = a.DiagnosticIdentifier, + WorkspaceEdit = a.WorkspaceEdit, + IsPreferred = a.IsPreferred + })); + } + } +} \ No newline at end of file diff --git a/AnyText/AnyText.Core/Rules/MultiRuleApplication.cs b/AnyText/AnyText.Core/Rules/MultiRuleApplication.cs index d9713408..b3afb2e6 100644 --- a/AnyText/AnyText.Core/Rules/MultiRuleApplication.cs +++ b/AnyText/AnyText.Core/Rules/MultiRuleApplication.cs @@ -60,13 +60,13 @@ public override void Deactivate(ParseContext context) } /// - public override void AddCodeLenses(ICollection codeLenses) + public override void AddCodeLenses(ICollection codeLenses, Predicate predicate = null) { foreach (var ruleApplication in Inner) { - ruleApplication.AddCodeLenses(codeLenses); + ruleApplication.AddCodeLenses(codeLenses, predicate); } - base.AddCodeLenses(codeLenses); + base.AddCodeLenses(codeLenses, predicate); } internal override RuleApplication MigrateTo(MultiRuleApplication multiRule, ParseContext context) diff --git a/AnyText/AnyText.Core/Rules/Rule.cs b/AnyText/AnyText.Core/Rules/Rule.cs index d1e021c9..9071779f 100644 --- a/AnyText/AnyText.Core/Rules/Rule.cs +++ b/AnyText/AnyText.Core/Rules/Rule.cs @@ -204,11 +204,11 @@ public uint? TokenModifierIndex /// /// Gets the list of code actions for this rule. /// - public virtual IEnumerable SupportedCodeActions => new List(); + public virtual IEnumerable SupportedCodeActions => Enumerable.Empty(); /// /// Gets the list of code lenses for this rule. /// - public virtual IEnumerable SupportedCodeLenses => new List(); + public virtual IEnumerable SupportedCodeLenses => Enumerable.Empty(); } } diff --git a/AnyText/AnyText.Core/Rules/RuleApplication.cs b/AnyText/AnyText.Core/Rules/RuleApplication.cs index fc30b8a4..c30d748c 100644 --- a/AnyText/AnyText.Core/Rules/RuleApplication.cs +++ b/AnyText/AnyText.Core/Rules/RuleApplication.cs @@ -174,13 +174,17 @@ public virtual void Deactivate(ParseContext context) public virtual IEnumerable CreateParseErrors() => Enumerable.Empty(); /// - /// Add all CodeLenses of this ruleApplication to a collection + /// Adds all CodeLens information of this to the provided collection. /// - /// Collection of CodeLenses - public virtual void AddCodeLenses(ICollection codeLenses) + /// The collection to which the objects will be added. + /// An optional predicate that filters which rule applications should have their CodeLenses added. Default is true for all. + public virtual void AddCodeLenses(ICollection codeLenses, Predicate predicate = null) { - if (Rule.SupportedCodeLenses.Any()) + predicate ??= _ => true; + + if (Rule.SupportedCodeLenses.Any() && predicate.Invoke(this)) { + var end = CurrentPosition + Length; var ruleCodeLenses = Rule.SupportedCodeLenses.Select(a => new CodeLensInfo() { Arguments = a.Arguments, @@ -188,15 +192,15 @@ public virtual void AddCodeLenses(ICollection codeLenses) Data = a.Data, Title = a.Title, Start = CurrentPosition, - End = CurrentPosition + Length, + End = end, }); foreach (var codeLens in ruleCodeLenses) { codeLenses.Add(codeLens); } - } } + /// /// Gets called when the newPosition of the given rule application changes /// diff --git a/AnyText/AnyText.Core/Rules/SingleRuleApplication.cs b/AnyText/AnyText.Core/Rules/SingleRuleApplication.cs index a129abc4..e15f093e 100644 --- a/AnyText/AnyText.Core/Rules/SingleRuleApplication.cs +++ b/AnyText/AnyText.Core/Rules/SingleRuleApplication.cs @@ -48,13 +48,13 @@ public override void Deactivate(ParseContext context) base.Deactivate(context); } /// - public override void AddCodeLenses(ICollection codeLenses) + public override void AddCodeLenses(ICollection codeLenses, Predicate predicate = null) { - if (Inner != null && Inner.IsActive) + if (Inner != null) { - Inner.AddCodeLenses(codeLenses); + Inner.AddCodeLenses(codeLenses, predicate); } - base.AddCodeLenses(codeLenses); + base.AddCodeLenses(codeLenses, predicate); } internal override RuleApplication MigrateTo(SingleRuleApplication singleRule, ParseContext context) diff --git a/AnyText/AnyText.Lsp/LspServer.CodeAction.cs b/AnyText/AnyText.Lsp/LspServer.CodeAction.cs index 9bca913a..bdde215c 100644 --- a/AnyText/AnyText.Lsp/LspServer.CodeAction.cs +++ b/AnyText/AnyText.Lsp/LspServer.CodeAction.cs @@ -32,30 +32,14 @@ public CodeAction[] CodeAction(JToken arg) var documentUri = request.TextDocument.Uri; var diagnostics = request.Context.Diagnostics; - var range = request.Range; var kindFilter = request.Context.Only; - var startPosition = AsParsePosition(request.Range.Start); var endPosition = AsParsePosition(request.Range.End); - var ruleApp = document.Context.Matcher.GetRuleApplicationsAt(AsParsePosition(range.Start)) - .FirstOrDefault(r => r.Rule.IsLiteral); + var actions = document.GetCodeActionInfo(startPosition, endPosition); - if (ruleApp == null) return codeActions.ToArray(); - - while (!(ruleApp.CurrentPosition <= startPosition && - ruleApp.CurrentPosition + ruleApp.Length >= endPosition)) - { - ruleApp = ruleApp.Parent; - if (ruleApp == null) - return codeActions.ToArray(); - } - - var actions = ruleApp.Rule.SupportedCodeActions; - - var arguments = new object[] - { documentUri, ruleApp.CurrentPosition, ruleApp.CurrentPosition + ruleApp.Length }; + foreach (var action in actions) { @@ -71,18 +55,29 @@ public CodeAction[] CodeAction(JToken arg) if (kindFilter != null && kindFilter.Any() && actionKind != null && !kindFilter.Contains(actionKind.Value)) continue; + var workspaceEdit = action.WorkspaceEdit?.Invoke(new ExecuteCommandArguments() + { + Context = document.Context, + DocumentUri = documentUri, + Start = action.Start, + End = action.End, + OtherOptions = action.Arguments + }); + + var arguments = new object[] + { documentUri, action.Start, action.End }; codeActions.Add(new CodeAction { Title = action.Title, Kind = actionKind, Diagnostics = relevantDiagnostics.Length == 0 ? null : relevantDiagnostics, - Edit = action.WorkspaceEdit != null ? MapWorkspaceEdit(action.WorkspaceEdit) : null, + Edit = action.WorkspaceEdit != null ? MapWorkspaceEdit(workspaceEdit) : null, IsPreferred = supportsIsPreferred && action.IsPreferred ? true : null, - Command = action.Command != null + Command = action.CommandIdentifier != null ? new Command { Title = action.CommandTitle, - CommandIdentifier = action.Command, + CommandIdentifier = action.CommandIdentifier, Arguments = action.Arguments != null ? arguments.Concat(action.Arguments.Cast()).ToArray() : arguments.ToArray() diff --git a/AnyText/AnyText.Lsp/LspServer.ExecuteCommand.cs b/AnyText/AnyText.Lsp/LspServer.ExecuteCommand.cs index 799e500b..06fcfb8e 100644 --- a/AnyText/AnyText.Lsp/LspServer.ExecuteCommand.cs +++ b/AnyText/AnyText.Lsp/LspServer.ExecuteCommand.cs @@ -34,6 +34,13 @@ private void ExecuteCommand(string commandIdentifier, object[] args) return; } + var uri = args[0].ToString(); + if (!_documents.TryGetValue(uri!, out var document)) + { + SendLogMessage(MessageType.Error, $"{commandIdentifier} no ParseContext found for URI {uri}"); + return; + } + Dictionary dict = null; if (args.Length > 3 && args[3] != null) { @@ -44,10 +51,12 @@ private void ExecuteCommand(string commandIdentifier, object[] args) }; } + var executeCommandArguments = new ExecuteCommandArguments { - DocumentUri = args[0].ToString(), + Context = document.Context, + DocumentUri = uri, Start = ParsePositionFromJson(args[1].ToString()), End = ParsePositionFromJson(args[2].ToString()), OtherOptions = dict diff --git a/AnyText/AnyText/Grammars/AnyTextGrammar.manual.Actions.cs b/AnyText/AnyText/Grammars/AnyTextGrammar.manual.Actions.cs index a7c31e4c..94ab7129 100644 --- a/AnyText/AnyText/Grammars/AnyTextGrammar.manual.Actions.cs +++ b/AnyText/AnyText/Grammars/AnyTextGrammar.manual.Actions.cs @@ -10,11 +10,6 @@ namespace NMF.AnyText.Grammars { public partial class AnyTextGrammar { - public AnyTextGrammar() - { - foreach (var exe in ExecutableActions) AddExecutableAction(exe.Key, exe.Value); - } - public partial class FragmentRuleRule { public override IEnumerable SupportedCodeLenses => new List() @@ -26,10 +21,16 @@ public partial class FragmentRuleRule Arguments = new Dictionary() { {"test","test"}, + }, + Action = args => + { + Console.Error.WriteLine("TestRun"); } + } }; } + public partial class ModelRuleRule { public override IEnumerable SupportedCodeActions => new List @@ -38,54 +39,58 @@ public partial class ModelRuleRule { Title = "Copy to new File", Kind = "quickfix", - WorkspaceEdit = new WorkspaceEdit + WorkspaceEdit = (args) => { - DocumentChanges = new List + return new WorkspaceEdit { - new() - { - CreateFile = new CreateFile - { - Options = new FileOptions - { - IgnoreIfExists = false, - Overwrite = true - }, - AnnotationId = "createFile", - Kind = "create", - Uri = "newDocument.anytext" - } - }, - new() + DocumentChanges = new List { - TextDocumentEdit = new TextDocumentEdit + new() { - TextDocument = new OptionalVersionedTextDocumentIdentifier + CreateFile = new CreateFile { - Version = 1, + Options = new FileOptions + { + IgnoreIfExists = false, + Overwrite = true + }, + AnnotationId = "createFile", + Kind = "create", Uri = "newDocument.anytext" - }, - Edits = new[] + } + }, + new() + { + TextDocumentEdit = new TextDocumentEdit { - new TextEdit(new ParsePosition(0, 0), new ParsePosition(0, 0), - new[] { "Text in new File" }) + TextDocument = new OptionalVersionedTextDocumentIdentifier + { + Version = 1, + Uri = "newDocument.anytext" + }, + Edits = new[] + { + new TextEdit(new ParsePosition(0, 0), new ParsePosition(0, 0), + new[] { "Text in new File" }) + } } } - } - }, - ChangeAnnotations = new Dictionary - { + }, + ChangeAnnotations = new Dictionary { - "createFile", new ChangeAnnotation { - Description = "description", - Label = "label", - NeedsConfirmation = true + "createFile", new ChangeAnnotation + { + Description = "description", + Label = "label", + NeedsConfirmation = true + } } } - } + }; }, - Diagnostics = new[] { "" } + Diagnostics = new[] { "" }, + } }; } @@ -101,39 +106,20 @@ public partial class GrammarRule CommandTitle = "Insert Comment Header", WorkspaceEdit = null, Diagnostics = new[] { "" }, - Command = "editor.action.addCommentHeader" - } - }; - } - - /// - /// Dictionary of Code Identifier and the Executable Action - /// - public Dictionary> ExecutableActions { get; } = new() - { - { - "editor.action.addCommentHeader", obj => - { - var documentUri = obj.DocumentUri; - var start = obj.Start; - var end = obj.End; - if (documentUri != null) + CommandIdentifier = "editor.action.addCommentHeader", + Action =obj => { - InsertCommentHeader(documentUri); - return "Comment header generated."; + var documentUri = obj.DocumentUri; + var start = obj.Start; + var end = obj.End; + if (documentUri != null) + { + InsertCommentHeader(documentUri); + } } - - return "Invalid document URI."; - } - }, - { - "codelens.runTest", obj => - { - Console.Error.WriteLine("TestRun"); - return null; } - } - }; + }; + } private static void InsertCommentHeader(string filePath) { From bdd0321cbff1ba5f31c577fc79140fec0622f642 Mon Sep 17 00:00:00 2001 From: ahert001 Date: Tue, 28 Jan 2025 08:29:15 +0100 Subject: [PATCH 6/6] Add RuleApplication to ActionArguments --- AnyText/AnyText.Core/CodeActionInfo.cs | 15 ++++------ AnyText/AnyText.Core/CodeLensInfo.cs | 16 ++++------ .../AnyText.Core/ExecuteCommandArguments.cs | 9 +++++- AnyText/AnyText.Core/Grammars/Grammar.cs | 11 +------ AnyText/AnyText.Core/Parser.CodeAction.cs | 10 +++---- AnyText/AnyText.Core/Rules/RuleApplication.cs | 4 +-- AnyText/AnyText.Lsp/LspServer.CodeAction.cs | 15 ++++++---- AnyText/AnyText.Lsp/LspServer.CodeLens.cs | 24 +++++++++++---- .../AnyText.Lsp/LspServer.ExecuteCommand.cs | 29 +++++++++---------- .../Grammars/AnyTextGrammar.manual.Actions.cs | 6 ++++ 10 files changed, 73 insertions(+), 66 deletions(-) diff --git a/AnyText/AnyText.Core/CodeActionInfo.cs b/AnyText/AnyText.Core/CodeActionInfo.cs index 7b0e2579..2caee1e2 100644 --- a/AnyText/AnyText.Core/CodeActionInfo.cs +++ b/AnyText/AnyText.Core/CodeActionInfo.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using NMF.AnyText.Rules; using NMF.AnyText.Workspace; namespace NMF.AnyText @@ -9,6 +10,10 @@ namespace NMF.AnyText /// public class CodeActionInfo { + /// + /// RuleApplication of the Action + /// + public RuleApplication RuleApplication { get; set; } /// /// The title is typically displayed in the UI to describe the action. /// @@ -62,16 +67,6 @@ public class CodeActionInfo /// public Func WorkspaceEdit { get; set; } - /// - /// Start Position of the RuleApplication of this action - /// - public ParsePosition Start { get; set; } - - /// - /// End Position of the RuleApplication of this action - /// - public ParsePosition End { get; set; } - /// /// The actual execution of this CodeAction /// diff --git a/AnyText/AnyText.Core/CodeLensInfo.cs b/AnyText/AnyText.Core/CodeLensInfo.cs index 4fe74b70..c7fbe351 100644 --- a/AnyText/AnyText.Core/CodeLensInfo.cs +++ b/AnyText/AnyText.Core/CodeLensInfo.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using NMF.AnyText.Rules; namespace NMF.AnyText { @@ -9,6 +10,10 @@ namespace NMF.AnyText /// public class CodeLensInfo { + /// + /// RuleApplication of the Lens + /// + public RuleApplication RuleApplication {get;set;} /// /// Gets or sets the title of the CodeLens item, typically a label displayed in the editor. /// @@ -24,21 +29,10 @@ public class CodeLensInfo /// public Dictionary Arguments { get; set; } - /// - /// Gets or sets the start position of the text range that this CodeLens is associated with. - /// - public ParsePosition Start { get; set; } - - /// - /// Gets or sets the end position of the text range that this CodeLens is associated with. - /// - public ParsePosition End { get; set; } - /// /// Gets or sets additional data associated with this CodeLens, which can be used for custom functionality. /// public object Data { get; set; } - /// /// The actual execution of this CodeLens /// diff --git a/AnyText/AnyText.Core/ExecuteCommandArguments.cs b/AnyText/AnyText.Core/ExecuteCommandArguments.cs index 4da6b455..8ec4f226 100644 --- a/AnyText/AnyText.Core/ExecuteCommandArguments.cs +++ b/AnyText/AnyText.Core/ExecuteCommandArguments.cs @@ -1,16 +1,23 @@ using System.Collections.Generic; +using NMF.AnyText.Rules; namespace NMF.AnyText { /// /// Represents the arguments for executing a command on a document. /// - public class ExecuteCommandArguments + public class ExecuteCommandArguments { + /// + /// RuleApplication of the Action + /// + public RuleApplication RuleApplication { get; set; } + /// /// ParseContext of the Document /// public ParseContext Context { get; set; } + /// /// URI of the document. /// diff --git a/AnyText/AnyText.Core/Grammars/Grammar.cs b/AnyText/AnyText.Core/Grammars/Grammar.cs index 5fed85c1..c7f54fc3 100644 --- a/AnyText/AnyText.Core/Grammars/Grammar.cs +++ b/AnyText/AnyText.Core/Grammars/Grammar.cs @@ -197,17 +197,8 @@ public Rule Root /// Dictionary of executable actions. /// The key is the action identifier, and the value is the action executor. /// - protected Dictionary> ExecutableActions { get; } = new (); + private Dictionary> ExecutableActions { get; } = new (); - /// - /// Adds a new code action to the dictionary. - /// - /// The identifier of the action. - /// The action executor. - protected void AddExecutableAction(string actionIdentifier, Action executor) - { - ExecutableActions.Add(actionIdentifier, executor); - } /// /// Retrieves the dictionary of executable actions as a read-only dictionary. /// diff --git a/AnyText/AnyText.Core/Parser.CodeAction.cs b/AnyText/AnyText.Core/Parser.CodeAction.cs index 56ae1d67..ff8b51bd 100644 --- a/AnyText/AnyText.Core/Parser.CodeAction.cs +++ b/AnyText/AnyText.Core/Parser.CodeAction.cs @@ -33,27 +33,27 @@ public IEnumerable GetCodeActionInfo(ParsePosition start, ParseP return codeActionInfos; } - CollectCodeActionsWithPositions(ruleApp, predicate, codeActionInfos); + CollectCodeActionsWithRuleApplication(ruleApp, predicate, codeActionInfos); var parent = ruleApp.Parent; while (parent != null && parent.Length == ruleApp.Length) { - CollectCodeActionsWithPositions(parent, predicate, codeActionInfos); + CollectCodeActionsWithRuleApplication(parent, predicate, codeActionInfos); parent = parent.Parent; + } return codeActionInfos; } - private static void CollectCodeActionsWithPositions(RuleApplication ruleApp, Predicate predicate, + private static void CollectCodeActionsWithRuleApplication(RuleApplication ruleApp, Predicate predicate, List codeActionInfos) { if (predicate.Invoke(ruleApp)) codeActionInfos.AddRange(ruleApp.Rule.SupportedCodeActions.Select(a => new CodeActionInfo { - Start = ruleApp.CurrentPosition, - End = ruleApp.CurrentPosition + ruleApp.Length, + RuleApplication = ruleApp, Action = a.Action, CommandIdentifier = a.CommandIdentifier, Arguments = a.Arguments, diff --git a/AnyText/AnyText.Core/Rules/RuleApplication.cs b/AnyText/AnyText.Core/Rules/RuleApplication.cs index c30d748c..1a5aa383 100644 --- a/AnyText/AnyText.Core/Rules/RuleApplication.cs +++ b/AnyText/AnyText.Core/Rules/RuleApplication.cs @@ -184,15 +184,13 @@ public virtual void AddCodeLenses(ICollection codeLenses, Predicat if (Rule.SupportedCodeLenses.Any() && predicate.Invoke(this)) { - var end = CurrentPosition + Length; var ruleCodeLenses = Rule.SupportedCodeLenses.Select(a => new CodeLensInfo() { Arguments = a.Arguments, CommandIdentifier = a.CommandIdentifier, Data = a.Data, Title = a.Title, - Start = CurrentPosition, - End = end, + RuleApplication = this }); foreach (var codeLens in ruleCodeLenses) { diff --git a/AnyText/AnyText.Lsp/LspServer.CodeAction.cs b/AnyText/AnyText.Lsp/LspServer.CodeAction.cs index bdde215c..a017103c 100644 --- a/AnyText/AnyText.Lsp/LspServer.CodeAction.cs +++ b/AnyText/AnyText.Lsp/LspServer.CodeAction.cs @@ -3,6 +3,7 @@ using System.Linq; using LspTypes; using Newtonsoft.Json.Linq; +using NMF.AnyText.Rules; using NMF.AnyText.Workspace; using Range = LspTypes.Range; using ChangeAnnotation = NMF.AnyText.Workspace.ChangeAnnotation; @@ -18,6 +19,7 @@ namespace NMF.AnyText { public partial class LspServer { + private readonly Dictionary _codeActionRuleApplications = new(); /// public CodeAction[] CodeAction(JToken arg) { @@ -27,6 +29,8 @@ public CodeAction[] CodeAction(JToken arg) if (!_documents.TryGetValue(request.TextDocument.Uri, out var document)) return codeActions.ToArray(); + _codeActionRuleApplications.Clear(); + var codeActionCapabilities = _clientCapabilities?.TextDocument?.CodeAction; var supportsIsPreferred = codeActionCapabilities?.IsPreferredSupport == true; @@ -43,8 +47,11 @@ public CodeAction[] CodeAction(JToken arg) foreach (var action in actions) { + var guid = Guid.NewGuid().ToString(); + _codeActionRuleApplications.TryAdd(guid, action.RuleApplication); + var diagnosticIdentifier = action.DiagnosticIdentifier; - var relevantDiagnostics = diagnostics + var relevantDiagnostics = string.IsNullOrEmpty(diagnosticIdentifier) ? new Diagnostic[] {} :diagnostics .Where(d => d.Message.Contains(diagnosticIdentifier)) .ToArray(); @@ -57,15 +64,13 @@ public CodeAction[] CodeAction(JToken arg) var workspaceEdit = action.WorkspaceEdit?.Invoke(new ExecuteCommandArguments() { + RuleApplication = action.RuleApplication, Context = document.Context, DocumentUri = documentUri, - Start = action.Start, - End = action.End, OtherOptions = action.Arguments }); - var arguments = new object[] - { documentUri, action.Start, action.End }; + var arguments = new object[] { documentUri, guid }; codeActions.Add(new CodeAction { Title = action.Title, diff --git a/AnyText/AnyText.Lsp/LspServer.CodeLens.cs b/AnyText/AnyText.Lsp/LspServer.CodeLens.cs index b2d4ea31..695abfd9 100644 --- a/AnyText/AnyText.Lsp/LspServer.CodeLens.cs +++ b/AnyText/AnyText.Lsp/LspServer.CodeLens.cs @@ -1,14 +1,22 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; using LspTypes; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using NMF.AnyText.Rules; using NMF.Utilities; +using JsonSerializer = System.Text.Json.JsonSerializer; +using Range = LspTypes.Range; namespace NMF.AnyText { public partial class LspServer { + private readonly Dictionary _codeLensRuleApplications = new(); + /// public CodeLens[] CodeLens(JToken arg) { @@ -17,16 +25,22 @@ public CodeLens[] CodeLens(JToken arg) if (!_documents.TryGetValue(request.TextDocument.Uri, out var document)) return new CodeLens[] {}; + _codeLensRuleApplications.Clear(); + var codeLensInfos = new List(); document.Context.RootRuleApplication.AddCodeLenses(codeLensInfos); + var codeLenses = codeLensInfos.Select(c => { - - var arguments = new object[] { request.TextDocument.Uri, c.Start, c.End, }; + var guid = Guid.NewGuid().ToString(); + _codeLensRuleApplications.TryAdd(guid, c.RuleApplication); + var arguments = new object[] { request.TextDocument.Uri, guid }; + var end = c.RuleApplication.CurrentPosition + c.RuleApplication.Length; return new CodeLens { Command = new Command() { + Title = c.Title, CommandIdentifier = c.CommandIdentifier, Arguments = c.Arguments != null @@ -36,8 +50,8 @@ public CodeLens[] CodeLens(JToken arg) Data = c.Data, Range = new Range() { - Start = new Position((uint)c.Start.Line, (uint)c.Start.Col), - End = new Position((uint)c.End.Line, (uint)c.End.Col) + Start = new Position((uint)c.RuleApplication.CurrentPosition.Line, (uint) c.RuleApplication.CurrentPosition.Col), + End = new Position((uint) end.Line, (uint) end.Col) } }; }); diff --git a/AnyText/AnyText.Lsp/LspServer.ExecuteCommand.cs b/AnyText/AnyText.Lsp/LspServer.ExecuteCommand.cs index 06fcfb8e..bfb2cba8 100644 --- a/AnyText/AnyText.Lsp/LspServer.ExecuteCommand.cs +++ b/AnyText/AnyText.Lsp/LspServer.ExecuteCommand.cs @@ -5,11 +5,14 @@ using LspTypes; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using NMF.AnyText.Rules; namespace NMF.AnyText { public partial class LspServer { + private Dictionary ActionRuleApplications => _codeActionRuleApplications.Concat(_codeLensRuleApplications).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + /// public void ExecuteCommand(JToken arg) { @@ -25,7 +28,6 @@ private void ExecuteCommand(string commandIdentifier, object[] args) return; } - var actions = language.GetExecutableActions(); if (!actions.TryGetValue(commandIdentifier, out var action)) @@ -42,23 +44,28 @@ private void ExecuteCommand(string commandIdentifier, object[] args) } Dictionary dict = null; - if (args.Length > 3 && args[3] != null) + if (args.Length > 2 && args[2] != null) { - var jsonObject = JsonConvert.DeserializeObject(args[3].ToString()!); + var jsonObject = JsonConvert.DeserializeObject(args[2].ToString()!); dict = new Dictionary { { jsonObject.Key.ToString(), jsonObject.Value } }; } + + if (!ActionRuleApplications.TryGetValue(args[1].ToString()!, out var actionRuleApplication)) + { + SendLogMessage(MessageType.Error, $"{commandIdentifier} no RuleApplication found for this Action"); + return; + } - + var elem = actionRuleApplication.ContextElement.GetType(); var executeCommandArguments = new ExecuteCommandArguments { + RuleApplication = actionRuleApplication, Context = document.Context, DocumentUri = uri, - Start = ParsePositionFromJson(args[1].ToString()), - End = ParsePositionFromJson(args[2].ToString()), OtherOptions = dict }; action.Invoke(executeCommandArguments); @@ -81,15 +88,5 @@ private Registration CreateExecuteCommandRegistration(string languageId) Method = Methods.WorkspaceExecuteCommandName }; } - - private static ParsePosition ParsePositionFromJson(string jsonString) - { - var jsonDocument = JsonDocument.Parse(jsonString); - - var line = jsonDocument.RootElement.GetProperty("Line").GetInt32(); - var col = jsonDocument.RootElement.GetProperty("Col").GetInt32(); - - return new ParsePosition(line, col); - } } } \ No newline at end of file diff --git a/AnyText/AnyText/Grammars/AnyTextGrammar.manual.Actions.cs b/AnyText/AnyText/Grammars/AnyTextGrammar.manual.Actions.cs index 94ab7129..ae9d381b 100644 --- a/AnyText/AnyText/Grammars/AnyTextGrammar.manual.Actions.cs +++ b/AnyText/AnyText/Grammars/AnyTextGrammar.manual.Actions.cs @@ -3,6 +3,7 @@ using System.IO; using System.Text; using System.Text.Json; +using NMF.AnyText.Metamodel; using NMF.AnyText.Workspace; using FileOptions = NMF.AnyText.Workspace.FileOptions; @@ -22,8 +23,13 @@ public partial class FragmentRuleRule { {"test","test"}, }, + Action = args => { + var ruleApplication = args.RuleApplication; + var context = args.Context; + var semanticElement = (FragmentRule) args.RuleApplication.ContextElement; + var uri = args.DocumentUri; Console.Error.WriteLine("TestRun"); }