Initial commit
This commit is contained in:
@@ -0,0 +1,443 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user