Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add async completion support #1986

Merged
merged 8 commits into from May 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion build/Packages.props
Expand Up @@ -6,7 +6,7 @@
<MicrosoftExtensionPackageVersion>3.1.12</MicrosoftExtensionPackageVersion>
<MSBuildPackageVersion>16.9.0</MSBuildPackageVersion>
<NuGetPackageVersion>5.2.0</NuGetPackageVersion>
<RoslynPackageVersion>3.10.0-1.21125.6</RoslynPackageVersion>
<RoslynPackageVersion>3.10.0-3.21222.20</RoslynPackageVersion>
<XunitPackageVersion>2.4.1</XunitPackageVersion>
</PropertyGroup>

Expand Down
7 changes: 7 additions & 0 deletions src/OmniSharp.Abstractions/IsExternalInit.cs
@@ -0,0 +1,7 @@
using System.ComponentModel;

namespace System.Runtime.CompilerServices
{
[EditorBrowsable(EditorBrowsableState.Never)]
internal sealed class IsExternalInit { }
333fred marked this conversation as resolved.
Show resolved Hide resolved
}
@@ -0,0 +1,12 @@
#nullable enable

using OmniSharp.Mef;

namespace OmniSharp.Models.v1.Completion
{
[OmniSharpEndpoint(OmniSharpEndpoints.CompletionAfterInsert, typeof(CompletionAfterInsertRequest), typeof(CompletionAfterInsertResponse))]
public class CompletionAfterInsertRequest : IRequest
{
public CompletionItem Item { get; set; } = null!;
}
}
@@ -0,0 +1,26 @@
#nullable enable

using Newtonsoft.Json;
using System.Collections.Generic;

namespace OmniSharp.Models.v1.Completion
{
public class CompletionAfterInsertResponse
{
/// <summary>
/// Text changes to be applied to the document. These need to be applied in batch, all with reference to
/// the same original document.
/// </summary>
public IReadOnlyList<LinePositionSpanTextChange>? Changes { get; set; }
/// <summary>
/// Line to position the cursor on after applying <see cref="Changes"/>.
/// </summary>
[JsonConverter(typeof(ZeroBasedIndexConverter))]
public int? Line { get; set; }
/// <summary>
/// Column to position the cursor on after applying <see cref="Changes"/>.
/// </summary>
[JsonConverter(typeof(ZeroBasedIndexConverter))]
public int? Column { get; set; }
}
}
Expand Up @@ -87,7 +87,12 @@ public class CompletionItem
/// <summary>
/// Index in the completions list that this completion occurred.
/// </summary>
public int Data { get; set; }
public (long CacheId, int Index) Data { get; set; }
333fred marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// True if there is a post-insert step for this completion item for asynchronous completion support.
/// </summary>
public bool HasAfterInsertStep { get; set; }

public override string ToString()
{
Expand Down
1 change: 1 addition & 0 deletions src/OmniSharp.Abstractions/OmniSharpEndpoints.cs
Expand Up @@ -49,6 +49,7 @@ public static class OmniSharpEndpoints

public const string Completion = "/completion";
public const string CompletionResolve = "/completion/resolve";
public const string CompletionAfterInsert = "/completion/afterinsert";

public static class V2
{
Expand Down
Expand Up @@ -28,6 +28,8 @@ namespace OmniSharp.LanguageServerProtocol.Handlers
{
class OmniSharpCompletionHandler : CompletionHandlerBase
{
const string AfterInsertCommandName = "csharp.completion.afterInsert";

public static IEnumerable<IJsonRpcHandler> Enumerate(RequestHandlers handlers)
{
foreach (var (selector, completionHandler, completionResolveHandler) in handlers
Expand Down Expand Up @@ -139,7 +141,10 @@ private CompletionItem ToLSPCompletionItem(OmnisharpCompletionItem omnisharpComp
AdditionalTextEdits = omnisharpCompletionItem.AdditionalTextEdits is { } edits
? TextEditContainer.From(edits.Select(e => Helpers.ToTextEdit(e)))
: null,
Data = JToken.FromObject(omnisharpCompletionItem.Data)
Data = JToken.FromObject(omnisharpCompletionItem.Data),
Command = omnisharpCompletionItem.HasAfterInsertStep
? Command.Create(AfterInsertCommandName)
: null,
};

private OmnisharpCompletionItem ToOmnisharpCompletionItem(CompletionItem completionItem)
Expand All @@ -157,7 +162,7 @@ private OmnisharpCompletionItem ToOmnisharpCompletionItem(CompletionItem complet
TextEdit = Helpers.FromTextEdit(completionItem.TextEdit!.TextEdit),
CommitCharacters = completionItem.CommitCharacters?.Select(i => i[0]).ToList(),
AdditionalTextEdits = completionItem.AdditionalTextEdits?.Select(e => Helpers.FromTextEdit(e)).ToList(),
Data = completionItem.Data!.ToObject<int>()
Data = completionItem.Data!.ToObject<(long, int)>()
};
}
}
@@ -0,0 +1,162 @@
#nullable enable

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.Tags;
using Microsoft.CodeAnalysis.Text;
using OmniSharp.Models;
using OmniSharp.Models.v1.Completion;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using CompletionItem = OmniSharp.Models.v1.Completion.CompletionItem;
using CSharpCompletionList = Microsoft.CodeAnalysis.Completion.CompletionList;
using CSharpCompletionService = Microsoft.CodeAnalysis.Completion.CompletionService;

namespace OmniSharp.Roslyn.CSharp.Services.Completion
{
internal static partial class CompletionListBuilder
{
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.Variable },
{ 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 },
};

// VS has a more complex concept of a commit mode vs suggestion mode for intellisense.
// LSP doesn't have this, so mock it as best we can by removing space ` ` from the list
// of commit characters if we're in suggestion mode.
private static readonly IReadOnlyList<char> DefaultRulesWithoutSpace = CompletionRules.Default.DefaultCommitCharacters.Where(c => c != ' ').ToList();

internal static async Task<(IReadOnlyList<CompletionItem>, bool)> BuildCompletionItems(
Document document,
SourceText sourceText,
long cacheId,
int position,
CSharpCompletionService completionService,
CSharpCompletionList completions,
TextSpan typedSpan,
bool expectingImportedItems,
bool isSuggestionMode, bool enableAsyncCompletion)
=> enableAsyncCompletion
? await BuildCompletionItemsAsync(document, sourceText, cacheId, position, completionService, completions, typedSpan, expectingImportedItems, isSuggestionMode)
: await BuildCompletionItemsSync(document, sourceText, cacheId, position, completionService, completions, typedSpan, expectingImportedItems, isSuggestionMode);

internal static LinePositionSpanTextChange GetChangeForTextAndSpan(string? insertText, TextSpan changeSpan, SourceText sourceText)
{
var changeLinePositionSpan = sourceText.Lines.GetLinePositionSpan(changeSpan);
return new()
{
NewText = insertText ?? "",
StartLine = changeLinePositionSpan.Start.Line,
StartColumn = changeLinePositionSpan.Start.Character,
EndLine = changeLinePositionSpan.End.Line,
EndColumn = changeLinePositionSpan.End.Character
};
}

private static IReadOnlyList<char>? BuildCommitCharacters(ImmutableArray<CharacterSetModificationRule> characterRules, bool isSuggestionMode, Dictionary<ImmutableArray<CharacterSetModificationRule>, IReadOnlyList<char>> commitCharacterRulesCache, HashSet<char> commitCharactersBuilder)
{
if (characterRules.IsEmpty)
{
// Use defaults
return isSuggestionMode ? DefaultRulesWithoutSpace : CompletionRules.Default.DefaultCommitCharacters;
}

if (commitCharacterRulesCache.TryGetValue(characterRules, out var cachedRules))
{
return cachedRules;
}

addAllCharacters(CompletionRules.Default.DefaultCommitCharacters);

foreach (var modifiedRule in characterRules)
{
switch (modifiedRule.Kind)
{
case CharacterSetModificationKind.Add:
commitCharactersBuilder.UnionWith(modifiedRule.Characters);
break;

case CharacterSetModificationKind.Remove:
commitCharactersBuilder.ExceptWith(modifiedRule.Characters);
break;

case CharacterSetModificationKind.Replace:
commitCharactersBuilder.Clear();
addAllCharacters(modifiedRule.Characters);
break;
}
}

// VS has a more complex concept of a commit mode vs suggestion mode for intellisense.
// LSP doesn't have this, so mock it as best we can by removing space ` ` from the list
// of commit characters if we're in suggestion mode.
if (isSuggestionMode)
{
commitCharactersBuilder.Remove(' ');
}

var finalCharacters = commitCharactersBuilder.ToList();
commitCharactersBuilder.Clear();

commitCharacterRulesCache.Add(characterRules, finalCharacters);

return finalCharacters;

void addAllCharacters(ImmutableArray<char> characters)
{
foreach (var @char in characters)
{
commitCharactersBuilder.Add(@char);
}
}
}

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;
}
}
}
@@ -0,0 +1,114 @@
#nullable enable

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Completion;
using Microsoft.CodeAnalysis.Text;
using OmniSharp.Models;
using OmniSharp.Models.v1.Completion;
using OmniSharp.Roslyn.CSharp.Services.Intellisense;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Threading.Tasks;
using CompletionItem = OmniSharp.Models.v1.Completion.CompletionItem;
using CSharpCompletionList = Microsoft.CodeAnalysis.Completion.CompletionList;
using CSharpCompletionService = Microsoft.CodeAnalysis.Completion.CompletionService;

namespace OmniSharp.Roslyn.CSharp.Services.Completion
{
internal static partial class CompletionListBuilder
{
internal static async Task<(IReadOnlyList<CompletionItem>, bool)> BuildCompletionItemsAsync(
Document document,
SourceText sourceText,
long cacheId,
int position,
CSharpCompletionService completionService,
CSharpCompletionList completions,
TextSpan typedSpan,
bool expectingImportedItems, bool isSuggestionMode)
{
var completionsBuilder = new List<CompletionItem>(completions.Items.Length);
var seenUnimportedCompletions = false;
var commitCharacterRuleCache = new Dictionary<ImmutableArray<CharacterSetModificationRule>, IReadOnlyList<char>>();
var commitCharacterRuleBuilder = new HashSet<char>();
var isOverrideOrPartialCompletion = completions.Items.Length > 0
&& completions.Items[0].GetProviderName() is CompletionItemExtensions.OverrideCompletionProvider or CompletionItemExtensions.PartialMethodCompletionProvider;
333fred marked this conversation as resolved.
Show resolved Hide resolved

for (int i = 0; i < completions.Items.Length; i++)
{
var completion = completions.Items[i];
string labelText = completion.DisplayTextPrefix + completion.DisplayText + completion.DisplayTextSuffix;
string? insertText;
string? filterText = null;
List<LinePositionSpanTextChange>? additionalTextEdits = null;
InsertTextFormat insertTextFormat = InsertTextFormat.PlainText;
TextSpan changeSpan;
string? sortText;
bool hasAfterInsertStep = false;
if (completion.IsComplexTextEdit)
{
// The completion is somehow expensive. Currently, this one of two categories: import completion, or override/partial
// completion.
Debug.Assert(completion.GetProviderName() is CompletionItemExtensions.OverrideCompletionProvider or CompletionItemExtensions.PartialMethodCompletionProvider
or CompletionItemExtensions.TypeImportCompletionProvider or CompletionItemExtensions.ExtensionMethodImportCompletionProvider);

changeSpan = typedSpan;

if (isOverrideOrPartialCompletion)
{
// For override and partial completion, we don't want to use the DisplayText as the insert text because they contain
// characters that will affect our ability to asynchronously resolve the change later.
insertText = completion.FilterText;
sortText = GetSortText(completion, labelText, expectingImportedItems);
hasAfterInsertStep = true;
}
else
{
insertText = completion.DisplayText;
sortText = '1' + completion.SortText;
seenUnimportedCompletions = true;
}
}
else
{
// For non-complex completions, just await the text edit. It's cheap enough that it doesn't impact our ability
// to pop completions quickly

// If the completion item is the misc project name, skip it.
if (completion.DisplayText == Configuration.OmniSharpMiscProjectName) continue;

GetCompletionInfo(
sourceText,
position,
completion,
await completionService.GetChangeAsync(document, completion),
typedSpan,
labelText,
expectingImportedItems,
out insertText, out filterText, out sortText, out insertTextFormat, out changeSpan, out additionalTextEdits);
}

var commitCharacters = BuildCommitCharacters(completion.Rules.CommitCharacterRules, isSuggestionMode, commitCharacterRuleCache, commitCharacterRuleBuilder);

completionsBuilder.Add(new CompletionItem
{
Label = labelText,
TextEdit = GetChangeForTextAndSpan(insertText!, changeSpan, sourceText),
InsertTextFormat = insertTextFormat,
AdditionalTextEdits = additionalTextEdits,
SortText = sortText,
FilterText = filterText,
Kind = GetCompletionItemKind(completion.Tags),
Detail = completion.InlineDescription,
Data = (cacheId, i),
Preselect = completion.Rules.SelectionBehavior == CompletionItemSelectionBehavior.HardSelection,
CommitCharacters = commitCharacters,
HasAfterInsertStep = hasAfterInsertStep,
});
}

return (completionsBuilder, seenUnimportedCompletions);
}
}
}