RobotNet/RobotNet.WebApp/Scripts/Monaco/Languages/CompletionExtensions.cs
2025-10-15 15:15:53 +07:00

249 lines
11 KiB
C#

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Tags;
using Microsoft.CodeAnalysis.Text;
using RobotNet.WebApp.Scripts.Monaco.Editor;
using System.Collections.Immutable;
using System.Reflection;
using System.Text.RegularExpressions;
namespace RobotNet.WebApp.Scripts.Monaco.Languages;
public static class CompletionExtensions
{
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
{
1 => CompletionTrigger.Invoke,
// https://github.com/dotnet/roslyn/issues/42982: Passing a trigger character
// to GetCompletionsAsync causes a null ref currently.
2 when includeTriggerCharacter => CompletionTrigger.CreateInsertionTrigger((char)triggerCharacter!),
_ => CompletionTrigger.Invoke,
};
//private static readonly Regex EscapeRegex = new(@"([\\\$}])", RegexOptions.Compiled);
//private static string GetAdjustedInsertTextWithPosition(
// CompletionChange change,
// int originalPosition,
// int newOffset,
// string? prependText = null)
//{
// string newText = change.TextChange.NewText!;
// if (change.NewPosition is not int newPosition
// || newPosition >= change.TextChange.Span.Start + newText.Length)
// {
// return prependText + newText[newOffset..];
// }
// int midpoint = newPosition - change.TextChange.Span.Start;
// var beforeText = prependText + newText[newOffset..midpoint];
// if (beforeText != null) beforeText = EscapeRegex.Replace(beforeText, @"\$1");
// var afterText = newText[midpoint..];
// if (afterText != null) afterText = EscapeRegex.Replace(afterText, @"\$1");
// return beforeText + "$0" + afterText;
//}
private static ImmutableArray<char> BuildCommitCharacters(Microsoft.CodeAnalysis.Completion.CompletionList completions, ImmutableArray<CharacterSetModificationRule> characterRules, ImmutableArray<char>.Builder triggerCharactersBuilder)
{
if (completions is null) return [];
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 = 0; i < triggerCharactersBuilder.Count; i++)
{
if (modifiedRule.Characters.Contains(triggerCharactersBuilder[i]))
{
triggerCharactersBuilder.RemoveAt(i);
i--;
}
}
break;
case CharacterSetModificationKind.Replace:
triggerCharactersBuilder.Clear();
triggerCharactersBuilder.AddRange(modifiedRule.Characters);
break;
}
}
if (completions.SuggestionModeItem is not null)
{
triggerCharactersBuilder.Remove(' ');
}
if (triggerCharactersBuilder.Capacity == triggerCharactersBuilder.Count)
{
return triggerCharactersBuilder.MoveToImmutable();
}
else
{
var result = triggerCharactersBuilder.ToImmutable();
triggerCharactersBuilder.Clear();
return result;
}
}
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<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();
var position = sourceText.Lines.GetPosition(new LinePosition(line - 1, column - 1));
var completionService = CompletionService.GetService(document);
if (completionService == null) return [];
if (kind == 3 && !completionService.ShouldTriggerCompletion(sourceText, position, GetCompletionTrigger(kind - 1, triggerCharacter, includeTriggerCharacter: true)))
{
return [];
}
Microsoft.CodeAnalysis.Completion.CompletionList? completionList = null;
try
{
completionList = await completionService.GetCompletionsAsync(
document,
position,
GetCompletionTrigger(kind - 1, triggerCharacter, includeTriggerCharacter: false));
}
catch (Exception ex)
{
// Log the exception or handle it as needed
Console.WriteLine($"Error getting completions: position:{position} \n {ex}");
}
if (completionList is null || completionList.ItemsList.Count <= 0) return [];
var typedSpan = completionService.GetDefaultCompletionListSpan(sourceText, position);
string typedText = sourceText.GetSubText(typedSpan).ToString();
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 syntax = await document.GetSyntaxTreeAsync();
var replacingSpanStartPosition = sourceText.Lines.GetLinePosition(typedSpan.Start);
var replacingSpanEndPosition = sourceText.Lines.GetLinePosition(typedSpan.End);
var triggerCharactersBuilder = ImmutableArray.CreateBuilder<char>(completionList.Rules.DefaultCommitCharacters.Length);
var completionsBuilder = new List<CompletionItem>();
foreach (var completion in completionList.ItemsList)
{
SingleEditOperation[]? additionalTextEdits = null;
char sortTextPrepend = '0';
if (!completion.Properties.TryGetValue("InsertionText", out string? insertText))
{
if (GetCompletionItemKind(completion.Tags) == CompletionItemKind.Keyword)
{
insertText = completion.DisplayText;
}
else
{
continue;
}
}
if (string.IsNullOrEmpty(insertText)) continue;
var commitCharacters = BuildCommitCharacters(completionList, completion.Rules.CommitCharacterRules, triggerCharactersBuilder).Select(c => c.ToString());
completionsBuilder.Add(new CompletionItem
{
Label = completion.DisplayTextPrefix + completion.DisplayText + completion.DisplayTextSuffix,
Kind = GetCompletionItemKind(completion.Tags),
Documentation = "",
InsertText = insertText,
Range = new Range
{
StartLineNumber = replacingSpanStartPosition.Line + 1,
EndLineNumber = replacingSpanEndPosition.Line + 1,
StartColumn = replacingSpanStartPosition.Character + 1,
EndColumn = replacingSpanEndPosition.Character + 1,
},
AdditionalTextEdits = additionalTextEdits,
// Ensure that unimported items are sorted after things already imported.
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 = null,
Command = null,
InsertTextRules = null,
});
}
return completionsBuilder;
}
}