Skip to content

Commit

Permalink
Lsp code action (#81)
Browse files Browse the repository at this point in the history
  • Loading branch information
georghinkel authored Jan 31, 2025
2 parents 9fa0f23 + c06fe0b commit 32e3c45
Show file tree
Hide file tree
Showing 28 changed files with 1,240 additions and 7 deletions.
75 changes: 75 additions & 0 deletions AnyText/AnyText.Core/CodeActionInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using NMF.AnyText.Rules;
using NMF.AnyText.Workspace;

namespace NMF.AnyText
{
/// <summary>
/// Represents the information about a code action.
/// </summary>
public class CodeActionInfo
{
/// <summary>
/// RuleApplication of the Action
/// </summary>
public RuleApplication RuleApplication { get; set; }
/// <summary>
/// The title is typically displayed in the UI to describe the action.
/// </summary>
public string Title { get; set; }

/// <summary>
/// Kind of the code action.
/// Possible values:
/// - "quickfix"
/// - "refactor"
/// - "refactor.extract"
/// - "refactor.inline"
/// - "refactor.rewrite"
/// - "source"
/// - "source.organizeImports"
/// </summary>
public string Kind { get; set; }

/// <summary>
/// This array holds diagnostics for which this action is relevant. If no diagnostics are set, the action may apply generally.
/// </summary>
public string[] Diagnostics { get; set; }

/// <summary>
/// A value of <c>true</c> indicates that the code action is preferred; otherwise, <c>false</c> or <c>null</c> if there's no preference.
/// </summary>
public bool IsPreferred { get; set; }

/// <summary>
/// This is the text that describes the command to execute, which can be shown to the user.
/// </summary>
public string CommandTitle { get; set; }

/// <summary>
/// The command is the identifier or name of the action to execute when the user selects it.
/// </summary>
public string CommandIdentifier { get; set; }

/// <summary>
/// These are the parameters passed to the command when it is executed.
/// </summary>
public Dictionary<string, object> Arguments { get; set; }

/// <summary>
/// Identifies the Diagnostic that this Action fixes
/// </summary>
public string DiagnosticIdentifier { get; set; }

/// <summary>
/// Defines the how the WorkspaceEdit Object of this CodeAction is created
/// </summary>
public Func<ExecuteCommandArguments, WorkspaceEdit> WorkspaceEdit { get; set; }

/// <summary>
/// The actual execution of this CodeAction
/// </summary>
public Action<ExecuteCommandArguments> Action { get; set; }
}
}
41 changes: 41 additions & 0 deletions AnyText/AnyText.Core/CodeLensInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using NMF.AnyText.Rules;

namespace NMF.AnyText
{
/// <summary>
/// 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.
/// </summary>
public class CodeLensInfo
{
/// <summary>
/// RuleApplication of the Lens
/// </summary>
public RuleApplication RuleApplication {get;set;}
/// <summary>
/// Gets or sets the title of the CodeLens item, typically a label displayed in the editor.
/// </summary>
public string Title { get; set; }

/// <summary>
/// Gets or sets the identifier for the command to be executed when the CodeLens is activated.
/// </summary>
public string CommandIdentifier { get; set; }

/// <summary>
/// Gets or sets the dictionary of arguments to be passed along with the command when invoked.
/// </summary>
public Dictionary<string, object> Arguments { get; set; }

/// <summary>
/// Gets or sets additional data associated with this CodeLens, which can be used for custom functionality.
/// </summary>
public object Data { get; set; }
/// <summary>
/// The actual execution of this CodeLens
/// </summary>
public Action<ExecuteCommandArguments> Action { get; set; }
}
}
41 changes: 41 additions & 0 deletions AnyText/AnyText.Core/ExecuteCommandArguments.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.Collections.Generic;
using NMF.AnyText.Rules;

namespace NMF.AnyText
{
/// <summary>
/// Represents the arguments for executing a command on a document.
/// </summary>
public class ExecuteCommandArguments
{
/// <summary>
/// RuleApplication of the Action
/// </summary>
public RuleApplication RuleApplication { get; set; }

/// <summary>
/// ParseContext of the Document
/// </summary>
public ParseContext Context { get; set; }

/// <summary>
/// URI of the document.
/// </summary>
public string DocumentUri { get; set; }

/// <summary>
/// Starting position of the Range.
/// </summary>
public ParsePosition Start { get; set; }

/// <summary>
/// Ending position of the Range.
/// </summary>
public ParsePosition End { get; set; }

/// <summary>
/// Additional options for the command execution.
/// </summary>
public Dictionary<string, object> OtherOptions { get; set; }
}
}
30 changes: 30 additions & 0 deletions AnyText/AnyText.Core/Grammars/Grammar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
}

/// <summary>
/// Gets an array of comment rules used by this grammar
/// </summary>
Expand Down Expand Up @@ -177,5 +192,20 @@ public Rule Root
/// <param name="context">a context to resolve the root rule</param>
/// <returns>the root rule for this grammar</returns>
protected abstract Rule GetRootRule(GrammarContext context);

/// <summary>
/// Dictionary of executable actions.
/// The key is the action identifier, and the value is the action executor.
/// </summary>
private Dictionary<string, Action<ExecuteCommandArguments>> ExecutableActions { get; } = new ();

/// <summary>
/// Retrieves the dictionary of executable actions as a read-only dictionary.
/// </summary>
/// <returns>A read-only view of the dictionary.</returns>
public IReadOnlyDictionary<string, Action<ExecuteCommandArguments>> GetExecutableActions()
{
return ExecutableActions;
}
}
}
70 changes: 70 additions & 0 deletions AnyText/AnyText.Core/Parser.CodeAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NMF.AnyText.Rules;

namespace NMF.AnyText
{
public partial class Parser
{
/// <summary>
/// Retrieves code action information within a specified range of parse positions.
/// </summary>
/// <param name="start">The starting position of the range.</param>
/// <param name="end">The ending position of the range.</param>
/// <param name="predicate">An optional predicate to filter rule applications.</param>
/// <returns>A collection of <see cref="CodeActionInfo"/> objects representing available code actions.</returns>
public IEnumerable<CodeActionInfo> GetCodeActionInfo(ParsePosition start, ParsePosition end,
Predicate<RuleApplication> predicate = null)
{
predicate ??= _ => true;
var codeActionInfos = new List<CodeActionInfo>();

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<RuleApplication> predicate,
List<CodeActionInfo> 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
}));
}
}
}
10 changes: 10 additions & 0 deletions AnyText/AnyText.Core/Rules/MultiRuleApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@ public override void Deactivate(ParseContext context)
}
base.Deactivate(context);
}

/// <inheritdoc />
public override void AddCodeLenses(ICollection<CodeLensInfo> codeLenses, Predicate<RuleApplication> predicate = null)
{
foreach (var ruleApplication in Inner)
{
ruleApplication.AddCodeLenses(codeLenses, predicate);
}
base.AddCodeLenses(codeLenses, predicate);
}

internal override RuleApplication MigrateTo(MultiRuleApplication multiRule, ParseContext context)
{
Expand Down
10 changes: 10 additions & 0 deletions AnyText/AnyText.Core/Rules/Rule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -221,5 +221,15 @@ public uint? TokenModifierIndex
get;
internal set;
}

/// <summary>
/// Gets the list of code actions for this rule.
/// </summary>
public virtual IEnumerable<CodeActionInfo> SupportedCodeActions => Enumerable.Empty<CodeActionInfo>();

/// <summary>
/// Gets the list of code lenses for this rule.
/// </summary>
public virtual IEnumerable<CodeLensInfo> SupportedCodeLenses => Enumerable.Empty<CodeLensInfo>();
}
}
28 changes: 27 additions & 1 deletion AnyText/AnyText.Core/Rules/RuleApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,33 @@ public virtual void Deactivate(ParseContext context)
/// </summary>
/// <returns>A collection of parse errors</returns>
public virtual IEnumerable<ParseError> CreateParseErrors() => Enumerable.Empty<ParseError>();


/// <summary>
/// Adds all CodeLens information of this <see cref="RuleApplication"/> to the provided collection.
/// </summary>
/// <param name="codeLenses">The collection to which the <see cref="CodeLensInfo"/> objects will be added.</param>
/// <param name="predicate">An optional predicate that filters which rule applications should have their CodeLenses added. Default is <c>true</c> for all.</param>
public virtual void AddCodeLenses(ICollection<CodeLensInfo> codeLenses, Predicate<RuleApplication> 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);
}
}
}

/// <summary>
/// Gets called when the newPosition of the given rule application changes
/// </summary>
Expand Down
9 changes: 9 additions & 0 deletions AnyText/AnyText.Core/Rules/SingleRuleApplication.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ public override void Deactivate(ParseContext context)
}
base.Deactivate(context);
}
/// <inheritdoc />
public override void AddCodeLenses(ICollection<CodeLensInfo> codeLenses, Predicate<RuleApplication> predicate = null)
{
if (Inner != null)
{
Inner.AddCodeLenses(codeLenses, predicate);
}
base.AddCodeLenses(codeLenses, predicate);
}

internal override RuleApplication MigrateTo(SingleRuleApplication singleRule, ParseContext context)
{
Expand Down
23 changes: 23 additions & 0 deletions AnyText/AnyText.Core/Workspace/ChangeAnnotation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace NMF.AnyText.Workspace
{
/// <summary>
/// Represents metadata or instructions for an annotation associated with a change.
/// </summary>
public class ChangeAnnotation
{
/// <summary>
/// A label for the annotation (e.g., "Refactor").
/// </summary>
public string Label { get; set; }

/// <summary>
/// Indicates if the change requires user confirmation.
/// </summary>
public bool? NeedsConfirmation { get; set; }

/// <summary>
/// A description or explanation of the annotation.
/// </summary>
public string Description { get; set; }
}
}
Loading

0 comments on commit 32e3c45

Please sign in to comment.