Files
I150/srcs/RobotNet10/Components/RobotNet10.ScriptEditor/Helpers/AdhocWorkspaceHelper.cs
2026-07-03 16:37:12 +07:00

443 lines
18 KiB
C#

using BlazorMonaco.Languages;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.QuickInfo;
using Microsoft.CodeAnalysis.Tags;
using Microsoft.CodeAnalysis.Text;
using System.Collections.Immutable;
using System.Text;
using System.Text.RegularExpressions;
namespace RobotNet10.ScriptEditor.Helpers;
public static partial class AdhocWorkspaceHelper
{
[GeneratedRegex(@"<summary>\s*(.+?)\s*</summary>", RegexOptions.Singleline)]
private static partial Regex SummaryRegex();
[GeneratedRegex(@"\s+")]
private static partial Regex WhitespaceRegex();
private const int TriggerKind_Invoke = 1;
private const int TriggerKind_TriggerCharacter = 2;
private const int TriggerKind_TriggerForIncompleteCompletions = 3;
private static readonly Dictionary<string, CompletionItemKind> s_roslynTagToCompletionItemKind = new()
{
{ WellKnownTags.Public, CompletionItemKind.Keyword },
{ WellKnownTags.Protected, CompletionItemKind.Keyword },
{ WellKnownTags.Private, CompletionItemKind.Keyword },
{ WellKnownTags.Internal, CompletionItemKind.Keyword },
{ WellKnownTags.File, CompletionItemKind.File },
{ WellKnownTags.Project, CompletionItemKind.File },
{ WellKnownTags.Folder, CompletionItemKind.Folder },
{ WellKnownTags.Assembly, CompletionItemKind.File },
{ WellKnownTags.Class, CompletionItemKind.Class },
{ WellKnownTags.Constant, CompletionItemKind.Constant },
{ WellKnownTags.Delegate, CompletionItemKind.Function },
{ WellKnownTags.Enum, CompletionItemKind.Enum },
{ WellKnownTags.EnumMember, CompletionItemKind.EnumMember },
{ WellKnownTags.Event, CompletionItemKind.Event },
{ WellKnownTags.ExtensionMethod, CompletionItemKind.Method },
{ WellKnownTags.Field, CompletionItemKind.Field },
{ WellKnownTags.Interface, CompletionItemKind.Interface },
{ WellKnownTags.Intrinsic, CompletionItemKind.Text },
{ WellKnownTags.Keyword, CompletionItemKind.Keyword },
{ WellKnownTags.Label, CompletionItemKind.Text },
{ WellKnownTags.Local, CompletionItemKind.Variable },
{ WellKnownTags.Namespace, CompletionItemKind.Module },
{ WellKnownTags.Method, CompletionItemKind.Method },
{ WellKnownTags.Module, CompletionItemKind.Module },
{ WellKnownTags.Operator, CompletionItemKind.Operator },
{ WellKnownTags.Parameter, CompletionItemKind.Value },
{ WellKnownTags.Property, CompletionItemKind.Property },
{ WellKnownTags.RangeVariable, CompletionItemKind.Variable },
{ WellKnownTags.Reference, CompletionItemKind.Reference },
{ WellKnownTags.Structure, CompletionItemKind.Struct },
{ WellKnownTags.TypeParameter, CompletionItemKind.TypeParameter },
{ WellKnownTags.Snippet, CompletionItemKind.Snippet },
{ WellKnownTags.Error, CompletionItemKind.Text },
{ WellKnownTags.Warning, CompletionItemKind.Text },
};
private static CompletionTrigger GetCompletionTrigger(int kind, char? triggerCharacter, bool includeTriggerCharacter)
=> kind switch
{
TriggerKind_Invoke => CompletionTrigger.Invoke,
TriggerKind_TriggerCharacter when includeTriggerCharacter && triggerCharacter.HasValue
=> CompletionTrigger.CreateInsertionTrigger(triggerCharacter.Value),
_ => CompletionTrigger.Invoke,
};
private static ImmutableArray<char> BuildCommitCharacters(
Microsoft.CodeAnalysis.Completion.CompletionList completions,
ImmutableArray<CharacterSetModificationRule> characterRules,
ImmutableArray<char>.Builder triggerCharactersBuilder)
{
if (completions is null) return [];
triggerCharactersBuilder.Clear();
triggerCharactersBuilder.AddRange(completions.Rules.DefaultCommitCharacters);
foreach (var modifiedRule in characterRules)
{
switch (modifiedRule.Kind)
{
case CharacterSetModificationKind.Add:
triggerCharactersBuilder.AddRange(modifiedRule.Characters);
break;
case CharacterSetModificationKind.Remove:
for (int i = triggerCharactersBuilder.Count - 1; i >= 0; i--)
{
if (modifiedRule.Characters.Contains(triggerCharactersBuilder[i]))
{
triggerCharactersBuilder.RemoveAt(i);
}
}
break;
case CharacterSetModificationKind.Replace:
triggerCharactersBuilder.Clear();
triggerCharactersBuilder.AddRange(modifiedRule.Characters);
break;
}
}
if (completions.SuggestionModeItem is not null)
{
triggerCharactersBuilder.Remove(' ');
}
return triggerCharactersBuilder.ToImmutable();
}
private static CompletionItemKind GetCompletionItemKind(ImmutableArray<string> tags)
{
foreach (var tag in tags)
{
if (s_roslynTagToCompletionItemKind.TryGetValue(tag, out var itemKind))
{
return itemKind;
}
}
return CompletionItemKind.Text;
}
public static async Task<IEnumerable<BlazorMonaco.Languages.CompletionItem>> GetCompletionAsync(
this AdhocWorkspace workspace,
DocumentId documentId,
int line,
int column,
int kind,
char? triggerCharacter)
{
if (triggerCharacter == ' ') return [];
var document = workspace.CurrentSolution.GetDocument(documentId);
if (document is null) return [];
var sourceText = await document.GetTextAsync();
if (line < 0 || line >= sourceText.Lines.Count)
return [];
var lineObj = sourceText.Lines[line];
int maxColumn = lineObj.End - lineObj.Start;
if (column < 0)
return [];
if (column > maxColumn)
column = maxColumn;
var position = sourceText.Lines.GetPosition(new LinePosition(line, column));
if (position < 0 || position > sourceText.Length)
return [];
var completionService = CompletionService.GetService(document);
if (completionService == null) return [];
if (kind == TriggerKind_TriggerForIncompleteCompletions
&& !completionService.ShouldTriggerCompletion(
sourceText,
position,
GetCompletionTrigger(TriggerKind_TriggerCharacter, triggerCharacter, includeTriggerCharacter: true)))
{
return [];
}
Microsoft.CodeAnalysis.Completion.CompletionList? completionList = null;
try
{
completionList = await completionService.GetCompletionsAsync(
document,
position,
GetCompletionTrigger(kind - 1, triggerCharacter, includeTriggerCharacter: false));
}
catch
{
return [];
}
if (completionList is null || completionList.ItemsList.Count <= 0)
return [];
var typedSpan = completionService.GetDefaultCompletionListSpan(sourceText, position);
if (typedSpan.Start < 0 || typedSpan.End > sourceText.Length)
return [];
var typedText = sourceText.GetSubText(typedSpan).ToString();
LinePosition replacingSpanStart;
LinePosition replacingSpanEnd;
try
{
replacingSpanStart = sourceText.Lines.GetLinePosition(typedSpan.Start);
replacingSpanEnd = sourceText.Lines.GetLinePosition(typedSpan.End);
}
catch
{
return [];
}
if (!typedSpan.IsEmpty || triggerCharacter != '.')
{
if (replacingSpanStart.Line != replacingSpanEnd.Line
|| replacingSpanStart.Character > replacingSpanEnd.Character)
{
return [];
}
}
ImmutableArray<string> filteredItems = typedText != string.Empty
? [.. completionService.FilterItems(document, [.. completionList.ItemsList], typedText)
.Select(i => i.DisplayText)]
: [];
bool expectingImportedItems = workspace.Options.GetOption(
new PerLanguageOption<bool?>("CompletionOptions", "ShowItemsFromUnimportedNamespaces", defaultValue: null),
LanguageNames.CSharp) == true;
var completionsBuilder = new List<BlazorMonaco.Languages.CompletionItem>(completionList.ItemsList.Count);
var commitCharactersCache = new Dictionary<int, string[]>();
var range = new BlazorMonaco.Range
{
StartLineNumber = replacingSpanStart.Line + 1,
EndLineNumber = replacingSpanEnd.Line + 1,
StartColumn = replacingSpanStart.Character + 1,
EndColumn = replacingSpanEnd.Character + 1,
};
foreach (var completion in completionList.ItemsList)
{
string? insertText = completion.Properties.TryGetValue("InsertionText", out var propInsertText)
? propInsertText
: completion.DisplayText;
if (string.IsNullOrEmpty(insertText))
continue;
string documentation = "";
int rulesHash = GetRulesHash(completion.Rules.CommitCharacterRules);
if (!commitCharactersCache.TryGetValue(rulesHash, out var commitCharacters))
{
var localBuilder = ImmutableArray.CreateBuilder<char>(
completionList.Rules.DefaultCommitCharacters.Length);
var chars = BuildCommitCharacters(
completionList,
completion.Rules.CommitCharacterRules,
localBuilder);
commitCharacters = [.. chars.Select(c => c.ToString())];
commitCharactersCache[rulesHash] = commitCharacters;
}
char sortTextPrepend = '0';
CompletionItemInsertTextRule? insertTextRules = null;
if (completion.IsComplexTextEdit ||
(completion.Properties.ContainsKey("Provider") &&
completion.Properties["Provider"] == "SnippetCompletionProvider"))
{
insertTextRules = CompletionItemInsertTextRule.InsertAsSnippet;
}
completionsBuilder.Add(new BlazorMonaco.Languages.CompletionItem
{
LabelAsString = completion.DisplayTextPrefix + completion.DisplayText + completion.DisplayTextSuffix,
Kind = GetCompletionItemKind(completion.Tags),
DocumentationAsString = documentation,
InsertText = insertText,
RangeAsObject = range,
AdditionalTextEdits = [],
SortText = expectingImportedItems ? sortTextPrepend + completion.SortText : completion.SortText,
FilterText = completion.FilterText,
Detail = completion.InlineDescription,
Preselect = completion.Rules.MatchPriority == MatchPriority.Preselect
|| filteredItems.Contains(completion.DisplayText),
CommitCharacters = [.. commitCharacters],
Tags = [],
Command = null,
InsertTextRules = insertTextRules,
});
}
return completionsBuilder;
}
private static int GetRulesHash(ImmutableArray<CharacterSetModificationRule> rules)
{
if (rules.IsEmpty) return 0;
var hash = new HashCode();
foreach (var rule in rules)
{
hash.Add(rule.Kind);
hash.Add(rule.Characters.Length);
foreach (var c in rule.Characters)
{
hash.Add(c);
}
}
return hash.ToHashCode();
}
public static async Task<string?> GetQuickInfoAsync(
this AdhocWorkspace workspace,
DocumentId documentId,
int line,
int column)
{
var document = workspace.CurrentSolution.GetDocument(documentId);
if (document is null) return null;
var sourceText = await document.GetTextAsync();
var position = sourceText.Lines.GetPosition(new LinePosition(line, column));
var quickInfoService = QuickInfoService.GetService(document);
if (quickInfoService is null) return string.Empty;
var quickInfo = await quickInfoService.GetQuickInfoAsync(document, position);
if (quickInfo is null) return string.Empty;
var finalTextBuilder = new StringBuilder();
bool lastSectionHadLineBreak = true;
var description = quickInfo.Sections.FirstOrDefault(s => s.Kind == QuickInfoSectionKinds.Description);
if (description is not null)
{
finalTextBuilder.AppendSection(description, MarkdownFormat.AllTextAsCSharp, ref lastSectionHadLineBreak);
}
var summary = quickInfo.Sections.FirstOrDefault(s => s.Kind == QuickInfoSectionKinds.DocumentationComments);
if (summary is not null)
{
finalTextBuilder.AppendSection(summary, MarkdownFormat.Default, ref lastSectionHadLineBreak);
}
foreach (var section in quickInfo.Sections)
{
switch (section.Kind)
{
case QuickInfoSectionKinds.Description:
case QuickInfoSectionKinds.DocumentationComments:
continue;
case QuickInfoSectionKinds.TypeParameters:
finalTextBuilder.AppendSection(section, MarkdownFormat.AllTextAsCSharp, ref lastSectionHadLineBreak);
break;
case QuickInfoSectionKinds.AnonymousTypes:
finalTextBuilder.AppendSection(section, MarkdownFormat.FirstLineDefaultRestCSharp, ref lastSectionHadLineBreak);
break;
case "NullabilityAnalysis":
finalTextBuilder.AppendSection(section, MarkdownFormat.Italicize, ref lastSectionHadLineBreak);
break;
default:
finalTextBuilder.AppendSection(section, MarkdownFormat.Default, ref lastSectionHadLineBreak);
break;
}
}
try
{
var semanticModel = await document.GetSemanticModelAsync();
var syntaxTree = await document.GetSyntaxTreeAsync();
if (semanticModel is not null && syntaxTree is not null)
{
var root = await syntaxTree.GetRootAsync();
var node = root.FindToken(position).Parent;
while (node is not null)
{
var symbolInfo = semanticModel.GetSymbolInfo(node);
var symbol = symbolInfo.Symbol ?? semanticModel.GetDeclaredSymbol(node);
if (symbol is IMethodSymbol methodSymbol)
{
var containingType = methodSymbol.ContainingType;
if (containingType is not null)
{
var overloads = containingType.GetMembers(methodSymbol.Name)
.OfType<IMethodSymbol>()
.Where(m => m.MethodKind == methodSymbol.MethodKind)
.ToList();
if (overloads.Count > 1)
{
finalTextBuilder.AppendLine();
finalTextBuilder.AppendLine();
finalTextBuilder.AppendLine("---");
finalTextBuilder.AppendLine($"**Overloads ({overloads.Count}):**");
finalTextBuilder.AppendLine();
foreach (var overload in overloads)
{
finalTextBuilder.AppendLine("```csharp");
finalTextBuilder.AppendLine(overload.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat));
finalTextBuilder.AppendLine("```");
var xmlDoc = overload.GetDocumentationCommentXml();
if (!string.IsNullOrEmpty(xmlDoc))
{
var summaryMatch = SummaryRegex().Match(xmlDoc);
if (summaryMatch.Success)
{
var summaryText = summaryMatch.Groups[1].Value.Trim();
summaryText = WhitespaceRegex().Replace(summaryText, " ");
finalTextBuilder.AppendLine(summaryText);
}
}
finalTextBuilder.AppendLine();
}
}
}
break;
}
node = node.Parent;
}
}
}
catch
{
// Silently handle errors
}
return finalTextBuilder.ToString().Trim();
}
}