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(@"\s*(.+?)\s*", 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 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 BuildCommitCharacters( Microsoft.CodeAnalysis.Completion.CompletionList completions, ImmutableArray characterRules, ImmutableArray.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 tags) { foreach (var tag in tags) { if (s_roslynTagToCompletionItemKind.TryGetValue(tag, out var itemKind)) { return itemKind; } } return CompletionItemKind.Text; } public static async Task> 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 filteredItems = typedText != string.Empty ? [.. completionService.FilterItems(document, [.. completionList.ItemsList], typedText) .Select(i => i.DisplayText)] : []; bool expectingImportedItems = workspace.Options.GetOption( new PerLanguageOption("CompletionOptions", "ShowItemsFromUnimportedNamespaces", defaultValue: null), LanguageNames.CSharp) == true; var completionsBuilder = new List(completionList.ItemsList.Count); var commitCharactersCache = new Dictionary(); 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( 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 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 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() .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(); } }