Skip to content

Commit

Permalink
Merge pull request #1986 from 333fred/async-completion
Browse files Browse the repository at this point in the history
Add async completion support
  • Loading branch information
filipw authored May 6, 2021
2 parents f343373 + fe1fa21 commit aa1018c
Show file tree
Hide file tree
Showing 19 changed files with 1,498 additions and 390 deletions.
2 changes: 1 addition & 1 deletion build/Packages.props
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
using System.ComponentModel;

namespace System.Runtime.CompilerServices
{
[EditorBrowsable(EditorBrowsableState.Never)]
internal sealed class IsExternalInit { }
}
Original file line number Diff line number Diff line change
@@ -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!;
}
}
Original file line number Diff line number Diff line change
@@ -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; }
}
}
Original file line number Diff line number Diff line change
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; }

/// <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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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)>()
};
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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;

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);
}
}
}
Loading

0 comments on commit aa1018c

Please sign in to comment.