249 lines
11 KiB
C#
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;
|
|
}
|
|
}
|