diff --git a/AnyText/AnyText.Core/CodeActionInfo.cs b/AnyText/AnyText.Core/CodeActionInfo.cs new file mode 100644 index 00000000..2caee1e2 --- /dev/null +++ b/AnyText/AnyText.Core/CodeActionInfo.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using NMF.AnyText.Rules; +using NMF.AnyText.Workspace; + +namespace NMF.AnyText +{ + /// + /// Represents the information about a code action. + /// + public class CodeActionInfo + { + /// + /// RuleApplication of the Action + /// + public RuleApplication RuleApplication { get; set; } + /// + /// 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 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 how the WorkspaceEdit Object of this CodeAction is created + /// + public Func WorkspaceEdit { 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 new file mode 100644 index 00000000..c7fbe351 --- /dev/null +++ b/AnyText/AnyText.Core/CodeLensInfo.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using NMF.AnyText.Rules; + +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 + { + /// + /// 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. + /// + 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 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 new file mode 100644 index 00000000..8ec4f226 --- /dev/null +++ b/AnyText/AnyText.Core/ExecuteCommandArguments.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; +using NMF.AnyText.Rules; + +namespace NMF.AnyText +{ + /// + /// Represents the arguments for executing a command on a document. + /// + 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. + /// + 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 0755638c..c7f54fc3 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 /// @@ -177,5 +192,20 @@ public Rule Root /// a context to resolve the root rule /// the root rule for this grammar protected abstract Rule GetRootRule(GrammarContext context); + + /// + /// Dictionary of executable actions. + /// The key is the action identifier, and the value is the action executor. + /// + private Dictionary> ExecutableActions { get; } = new (); + + /// + /// Retrieves the dictionary of executable actions as a read-only dictionary. + /// + /// A read-only view of the dictionary. + 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..ff8b51bd --- /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; + } + + CollectCodeActionsWithRuleApplication(ruleApp, predicate, codeActionInfos); + + var parent = ruleApp.Parent; + while (parent != null && parent.Length == ruleApp.Length) + { + CollectCodeActionsWithRuleApplication(parent, predicate, codeActionInfos); + parent = parent.Parent; + + } + + + return codeActionInfos; + } + + private static void CollectCodeActionsWithRuleApplication(RuleApplication ruleApp, Predicate predicate, + List codeActionInfos) + { + if (predicate.Invoke(ruleApp)) + codeActionInfos.AddRange(ruleApp.Rule.SupportedCodeActions.Select(a => new CodeActionInfo + { + RuleApplication = ruleApp, + 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 e5ce2894..d68b7b13 100644 --- a/AnyText/AnyText.Core/Rules/MultiRuleApplication.cs +++ b/AnyText/AnyText.Core/Rules/MultiRuleApplication.cs @@ -60,6 +60,16 @@ public override void Deactivate(ParseContext context) } base.Deactivate(context); } + + /// + public override void AddCodeLenses(ICollection codeLenses, Predicate predicate = null) + { + foreach (var ruleApplication in Inner) + { + ruleApplication.AddCodeLenses(codeLenses, predicate); + } + 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 2d016875..b686aee3 100644 --- a/AnyText/AnyText.Core/Rules/Rule.cs +++ b/AnyText/AnyText.Core/Rules/Rule.cs @@ -221,5 +221,15 @@ public uint? TokenModifierIndex get; internal set; } + + /// + /// Gets the list of code actions for this rule. + /// + public virtual IEnumerable SupportedCodeActions => Enumerable.Empty(); + + /// + /// Gets the list of code lenses for this rule. + /// + public virtual IEnumerable SupportedCodeLenses => Enumerable.Empty(); } } diff --git a/AnyText/AnyText.Core/Rules/RuleApplication.cs b/AnyText/AnyText.Core/Rules/RuleApplication.cs index 2cb0489e..8cd7ae74 100644 --- a/AnyText/AnyText.Core/Rules/RuleApplication.cs +++ b/AnyText/AnyText.Core/Rules/RuleApplication.cs @@ -212,7 +212,33 @@ public virtual void Deactivate(ParseContext context) /// /// A collection of parse errors public virtual IEnumerable CreateParseErrors() => Enumerable.Empty(); - + + /// + /// Adds all CodeLens information of this to the provided collection. + /// + /// 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) + { + predicate ??= _ => true; + + if (Rule.SupportedCodeLenses.Any() && predicate.Invoke(this)) + { + var ruleCodeLenses = Rule.SupportedCodeLenses.Select(a => new CodeLensInfo() + { + Arguments = a.Arguments, + CommandIdentifier = a.CommandIdentifier, + Data = a.Data, + Title = a.Title, + RuleApplication = this + }); + 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 14b71a2d..8c172893 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, Predicate predicate = null) + { + if (Inner != null) + { + Inner.AddCodeLenses(codeLenses, predicate); + } + base.AddCodeLenses(codeLenses, predicate); + } internal override RuleApplication MigrateTo(SingleRuleApplication singleRule, ParseContext context) { 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.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.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/ILspServer.cs b/AnyText/AnyText.Lsp/ILspServer.cs index ab067e6f..19caab10 100644 --- a/AnyText/AnyText.Lsp/ILspServer.cs +++ b/AnyText/AnyText.Lsp/ILspServer.cs @@ -43,11 +43,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..a017103c --- /dev/null +++ b/AnyText/AnyText.Lsp/LspServer.CodeAction.cs @@ -0,0 +1,294 @@ +using System; +using System.Collections.Generic; +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; +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 +{ + public partial class LspServer + { + private readonly Dictionary _codeActionRuleApplications = new(); + /// + 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(); + + _codeActionRuleApplications.Clear(); + + var codeActionCapabilities = _clientCapabilities?.TextDocument?.CodeAction; + var supportsIsPreferred = codeActionCapabilities?.IsPreferredSupport == true; + + var documentUri = request.TextDocument.Uri; + var diagnostics = request.Context.Diagnostics; + var kindFilter = request.Context.Only; + + var startPosition = AsParsePosition(request.Range.Start); + var endPosition = AsParsePosition(request.Range.End); + + var actions = document.GetCodeActionInfo(startPosition, endPosition); + + + + foreach (var action in actions) + { + var guid = Guid.NewGuid().ToString(); + _codeActionRuleApplications.TryAdd(guid, action.RuleApplication); + + var diagnosticIdentifier = action.DiagnosticIdentifier; + var relevantDiagnostics = string.IsNullOrEmpty(diagnosticIdentifier) ? new Diagnostic[] {} :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; + + var workspaceEdit = action.WorkspaceEdit?.Invoke(new ExecuteCommandArguments() + { + RuleApplication = action.RuleApplication, + Context = document.Context, + DocumentUri = documentUri, + OtherOptions = action.Arguments + }); + + var arguments = new object[] { documentUri, guid }; + codeActions.Add(new CodeAction + { + Title = action.Title, + Kind = actionKind, + Diagnostics = relevantDiagnostics.Length == 0 ? null : relevantDiagnostics, + Edit = action.WorkspaceEdit != null ? MapWorkspaceEdit(workspaceEdit) : null, + IsPreferred = supportsIsPreferred && action.IsPreferred ? true : null, + Command = action.CommandIdentifier != null + ? new Command + { + Title = action.CommandTitle, + CommandIdentifier = action.CommandIdentifier, + Arguments = action.Arguments != null + ? arguments.Concat(action.Arguments.Cast()).ToArray() + : 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 = workspaceEdit.Changes != null ? MapChanges(workspaceEdit.Changes) : null, + DocumentChanges = MapDocumentChanges(workspaceEdit.DocumentChanges), + ChangeAnnotations = workspaceEdit.ChangeAnnotations != null + ? MapChangeAnnotations(workspaceEdit.ChangeAnnotations) + : null + }; + } + + private Dictionary MapChanges(Dictionary changes) + { + var lspChanges = new Dictionary(); + var workspaceFolder = _workspaceFolders.FirstOrDefault()?.Uri; + foreach (var entry in changes) + lspChanges.Add($"{workspaceFolder}/{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 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 OptionalVersionedTextDocumentIdentifier + { + Uri = fileUri, + Version = textDocumentEdit.TextDocument.Version + }, + Edits = edits + }; + + return new + SumType( + lspTextDocumentEdit); + } + + private SumType + MapCreateFile(CreateFile createFile) + { + var workspaceFolder = _workspaceFolders.FirstOrDefault()?.Uri; + var fileUri = $"{workspaceFolder}/{createFile.Uri}"; + var lspCreateFile = new LspTypes.CreateFile + { + Kind = createFile.Kind, + Uri = fileUri, + Options = new CreateFileOptions + { + Overwrite = createFile.Options?.Overwrite, + IgnoreIfExists = createFile.Options?.IgnoreIfExists + }, + AnnotationId = createFile.AnnotationId + }; + + return new + SumType( + lspCreateFile); + } + + private SumType + MapRenameFile(RenameFile renameFile) + { + 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, + Options = new RenameFileOptions + { + Overwrite = renameFile.Options?.Overwrite, + IgnoreIfExists = renameFile.Options?.IgnoreIfExists + }, + AnnotationId = renameFile.AnnotationId + }; + + return new + SumType( + lspRenameFile); + } + + private SumType + MapDeleteFile(DeleteFile deleteFile) + { + var workspaceFolder = _workspaceFolders.FirstOrDefault()?.Uri; + var fileUri = $"{workspaceFolder}/{deleteFile.Uri}"; + var lspDeleteFile = new LspTypes.DeleteFile + { + Kind = deleteFile.Kind, + Uri = fileUri, + Options = new 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.CodeLens.cs b/AnyText/AnyText.Lsp/LspServer.CodeLens.cs new file mode 100644 index 00000000..695abfd9 --- /dev/null +++ b/AnyText/AnyText.Lsp/LspServer.CodeLens.cs @@ -0,0 +1,71 @@ +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) + { + var request = arg.ToObject(); + + 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 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 + ? arguments.Concat(c.Arguments.Cast()).ToArray() + : arguments.ToArray() + }, + Data = c.Data, + Range = new Range() + { + Start = new Position((uint)c.RuleApplication.CurrentPosition.Line, (uint) c.RuleApplication.CurrentPosition.Col), + End = new Position((uint) end.Line, (uint) 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 new file mode 100644 index 00000000..bfb2cba8 --- /dev/null +++ b/AnyText/AnyText.Lsp/LspServer.ExecuteCommand.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +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) + { + 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"); + return; + } + + var actions = language.GetExecutableActions(); + + if (!actions.TryGetValue(commandIdentifier, out var action)) + { + SendLogMessage(MessageType.Error, $"{commandIdentifier} Command not supported"); + 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 > 2 && args[2] != null) + { + 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, + OtherOptions = dict + }; + action.Invoke(executeCommandArguments); + } + + private Registration CreateExecuteCommandRegistration(string languageId) + { + _languages.TryGetValue(languageId, out var language); + if (language == null) return null; + + + var registrationOptions = new ExecuteCommandRegistrationOptions + { + Commands = language.GetExecutableActions().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 2f011f24..f6f9bdf1 100644 --- a/AnyText/AnyText.Lsp/LspServer.cs +++ b/AnyText/AnyText.Lsp/LspServer.cs @@ -20,7 +20,8 @@ public partial class LspServer : ILspServer private readonly JsonRpc _rpc; private readonly Dictionary _documents = new Dictionary(); private readonly Dictionary _languages; - + private ClientCapabilities _clientCapabilities; + private WorkspaceFolder[] _workspaceFolders; /// /// Creates a new instance /// @@ -50,7 +51,8 @@ public InitializeResult Initialize( , WorkspaceFolder[] workspaceFolders , object InitializationOptions = null) { - + _clientCapabilities = capabilities; + _workspaceFolders = workspaceFolders; var serverCapabilities = new ServerCapabilities { TextDocumentSync = new TextDocumentSyncOptions @@ -66,6 +68,19 @@ public InitializeResult Initialize( { WorkDoneProgress = false }, + CodeActionProvider = new CodeActionOptions + { + CodeActionKinds = new[] + { + CodeActionKind.RefactorExtract, CodeActionKind.Empty, CodeActionKind.Refactor, + CodeActionKind.Source, CodeActionKind.QuickFix, CodeActionKind.RefactorInline, + CodeActionKind.RefactorRewrite, CodeActionKind.SourceOrganizeImports + } + }, + CodeLensProvider = new CodeLensOptions() + { + ResolveProvider = true + }, FoldingRangeProvider = new FoldingRangeOptions { WorkDoneProgress = false diff --git a/AnyText/AnyText/Grammars/AnyTextGrammar.manual.Actions.cs b/AnyText/AnyText/Grammars/AnyTextGrammar.manual.Actions.cs new file mode 100644 index 00000000..ae9d381b --- /dev/null +++ b/AnyText/AnyText/Grammars/AnyTextGrammar.manual.Actions.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.Json; +using NMF.AnyText.Metamodel; +using NMF.AnyText.Workspace; +using FileOptions = NMF.AnyText.Workspace.FileOptions; + +namespace NMF.AnyText.Grammars +{ + public partial class AnyTextGrammar + { + public partial class FragmentRuleRule + { + public override IEnumerable SupportedCodeLenses => new List() + { + new() + { + Title = "Run Test", + CommandIdentifier = "codelens.runTest", + Arguments = new Dictionary() + { + {"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"); + } + + } + }; + } + + public partial class ModelRuleRule + { + public override IEnumerable SupportedCodeActions => new List + { + new() + { + Title = "Copy to new File", + Kind = "quickfix", + WorkspaceEdit = (args) => + { + return 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[] { "" }, + CommandIdentifier = "editor.action.addCommentHeader", + Action =obj => + { + var documentUri = obj.DocumentUri; + var start = obj.Start; + var end = obj.End; + if (documentUri != null) + { + InsertCommentHeader(documentUri); + } + } + } + }; + } + + 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); + } + } +} \ No newline at end of file