diff --git a/build/Packages.props b/build/Packages.props
index 1763397560..724e628df0 100644
--- a/build/Packages.props
+++ b/build/Packages.props
@@ -6,7 +6,7 @@
3.1.12
16.9.0
5.2.0
- 3.10.0-1.21125.6
+ 3.10.0-3.21222.20
2.4.1
diff --git a/src/OmniSharp.Abstractions/IsExternalInit.cs b/src/OmniSharp.Abstractions/IsExternalInit.cs
new file mode 100644
index 0000000000..4594436de5
--- /dev/null
+++ b/src/OmniSharp.Abstractions/IsExternalInit.cs
@@ -0,0 +1,4 @@
+namespace System.Runtime.CompilerServices
+{
+ internal sealed class IsExternalInit { }
+}
diff --git a/src/OmniSharp.Abstractions/Models/v1/Completion/CompletionAfterInsertRequest.cs b/src/OmniSharp.Abstractions/Models/v1/Completion/CompletionAfterInsertRequest.cs
new file mode 100644
index 0000000000..5afcb3f4d6
--- /dev/null
+++ b/src/OmniSharp.Abstractions/Models/v1/Completion/CompletionAfterInsertRequest.cs
@@ -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!;
+ }
+}
diff --git a/src/OmniSharp.Abstractions/Models/v1/Completion/CompletionAfterInsertResponse.cs b/src/OmniSharp.Abstractions/Models/v1/Completion/CompletionAfterInsertResponse.cs
new file mode 100644
index 0000000000..a8e4b65487
--- /dev/null
+++ b/src/OmniSharp.Abstractions/Models/v1/Completion/CompletionAfterInsertResponse.cs
@@ -0,0 +1,26 @@
+#nullable enable
+
+using Newtonsoft.Json;
+using System.Collections.Generic;
+
+namespace OmniSharp.Models.v1.Completion
+{
+ public class CompletionAfterInsertResponse
+ {
+ ///
+ /// Text changes to be applied to the document. These need to be applied in batch, all with reference to
+ /// the same original document.
+ ///
+ public IReadOnlyList? Changes { get; set; }
+ ///
+ /// Line to position the cursor on after applying .
+ ///
+ [JsonConverter(typeof(ZeroBasedIndexConverter))]
+ public int? Line { get; set; }
+ ///
+ /// Column to position the cursor on after applying .
+ ///
+ [JsonConverter(typeof(ZeroBasedIndexConverter))]
+ public int? Column { get; set; }
+ }
+}
diff --git a/src/OmniSharp.Abstractions/Models/v1/Completion/CompletionItem.cs b/src/OmniSharp.Abstractions/Models/v1/Completion/CompletionItem.cs
index 7e7acd64a0..4544e5f6ae 100644
--- a/src/OmniSharp.Abstractions/Models/v1/Completion/CompletionItem.cs
+++ b/src/OmniSharp.Abstractions/Models/v1/Completion/CompletionItem.cs
@@ -87,7 +87,12 @@ public class CompletionItem
///
/// Index in the completions list that this completion occurred.
///
- public int Data { get; set; }
+ public (long CacheId, int Index) Data { get; set; }
+
+ ///
+ /// True if there is a post-insert step for this completion item for asynchronous completion support.
+ ///
+ public bool HasAfterInsertStep { get; set; }
public override string ToString()
{
diff --git a/src/OmniSharp.Abstractions/OmniSharpEndpoints.cs b/src/OmniSharp.Abstractions/OmniSharpEndpoints.cs
index f8b8c98d45..fd292c4c84 100644
--- a/src/OmniSharp.Abstractions/OmniSharpEndpoints.cs
+++ b/src/OmniSharp.Abstractions/OmniSharpEndpoints.cs
@@ -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
{
diff --git a/src/OmniSharp.LanguageServerProtocol/Handlers/OmniSharpCompletionHandler.cs b/src/OmniSharp.LanguageServerProtocol/Handlers/OmniSharpCompletionHandler.cs
index ed312dec51..e2e58ea28f 100644
--- a/src/OmniSharp.LanguageServerProtocol/Handlers/OmniSharpCompletionHandler.cs
+++ b/src/OmniSharp.LanguageServerProtocol/Handlers/OmniSharpCompletionHandler.cs
@@ -28,6 +28,8 @@ namespace OmniSharp.LanguageServerProtocol.Handlers
{
class OmniSharpCompletionHandler : CompletionHandlerBase
{
+ const string AfterInsertCommandName = "csharp.completion.afterInsert";
+
public static IEnumerable Enumerate(RequestHandlers handlers)
{
foreach (var (selector, completionHandler, completionResolveHandler) in handlers
@@ -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)
@@ -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()
+ Data = completionItem.Data!.ToObject<(long, int)>()
};
}
}
diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionListBuilder.cs b/src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionListBuilder.cs
index 693fa10b5c..8fb3742769 100644
--- a/src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionListBuilder.cs
+++ b/src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionListBuilder.cs
@@ -7,6 +7,7 @@
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;
@@ -63,15 +64,16 @@ internal static partial class CompletionListBuilder
internal static async Task<(IReadOnlyList, bool)> BuildCompletionItems(
Document document,
SourceText sourceText,
+ long cacheId,
int position,
CSharpCompletionService completionService,
CSharpCompletionList completions,
TextSpan typedSpan,
bool expectingImportedItems,
- bool isSuggestionMode)
- {
- return await BuildCompletionItemsSync(document, sourceText, position, completionService, completions, typedSpan, expectingImportedItems, isSuggestionMode);
- }
+ 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)
{
@@ -85,5 +87,76 @@ internal static LinePositionSpanTextChange GetChangeForTextAndSpan(string? inser
EndColumn = changeLinePositionSpan.End.Character
};
}
+
+ private static IReadOnlyList? BuildCommitCharacters(ImmutableArray characterRules, bool isSuggestionMode, Dictionary, IReadOnlyList> commitCharacterRulesCache, HashSet 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 characters)
+ {
+ foreach (var @char in characters)
+ {
+ commitCharactersBuilder.Add(@char);
+ }
+ }
+ }
+
+ private static CompletionItemKind GetCompletionItemKind(ImmutableArray tags)
+ {
+ foreach (var tag in tags)
+ {
+ if (s_roslynTagToCompletionItemKind.TryGetValue(tag, out var itemKind))
+ {
+ return itemKind;
+ }
+ }
+
+ return CompletionItemKind.Text;
+ }
}
}
diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionListBuilder_Async.cs b/src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionListBuilder_Async.cs
new file mode 100644
index 0000000000..d761515d03
--- /dev/null
+++ b/src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionListBuilder_Async.cs
@@ -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, bool)> BuildCompletionItemsAsync(
+ Document document,
+ SourceText sourceText,
+ long cacheId,
+ int position,
+ CSharpCompletionService completionService,
+ CSharpCompletionList completions,
+ TextSpan typedSpan,
+ bool expectingImportedItems, bool isSuggestionMode)
+ {
+ var completionsBuilder = new List(completions.Items.Length);
+ var seenUnimportedCompletions = false;
+ var commitCharacterRuleCache = new Dictionary, IReadOnlyList>();
+ var commitCharacterRuleBuilder = new HashSet();
+ 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? 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);
+ }
+ }
+}
diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionListBuilder_Sync.cs b/src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionListBuilder_Sync.cs
index 80f5e41926..f4d8cbf26f 100644
--- a/src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionListBuilder_Sync.cs
+++ b/src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionListBuilder_Sync.cs
@@ -2,22 +2,19 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Completion;
-using Microsoft.CodeAnalysis.Tags;
using Microsoft.CodeAnalysis.Text;
using OmniSharp.Models;
using OmniSharp.Models.v1.Completion;
using OmniSharp.Roslyn.CSharp.Helpers;
using OmniSharp.Roslyn.CSharp.Services.Intellisense;
using OmniSharp.Utilities;
-using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
-using System.Text;
using System.Threading.Tasks;
using CompletionItem = OmniSharp.Models.v1.Completion.CompletionItem;
-using CompletionTriggerKind = OmniSharp.Models.v1.Completion.CompletionTriggerKind;
+using CSharpCompletionItem = Microsoft.CodeAnalysis.Completion.CompletionItem;
using CSharpCompletionList = Microsoft.CodeAnalysis.Completion.CompletionList;
using CSharpCompletionService = Microsoft.CodeAnalysis.Completion.CompletionService;
@@ -28,6 +25,7 @@ internal static partial class CompletionListBuilder
internal static async Task<(IReadOnlyList, bool)> BuildCompletionItemsSync(
Document document,
SourceText sourceText,
+ long cacheId,
int position,
CSharpCompletionService completionService,
CSharpCompletionList completions,
@@ -53,6 +51,7 @@ internal static partial class CompletionListBuilder
return ((Task?)arg.completionService.GetChangeAsync(arg.document, completion), providerName);
}
});
+
for (int i = 0; i < completions.Items.Length; i++)
{
TextSpan changeSpan = typedSpan;
@@ -84,94 +83,22 @@ internal static partial class CompletionListBuilder
// Except for import completion, we just resolve the change up front in the sync version. It's only expensive
// for override completion, but there's not a heck of a lot we can do about that for the sync scenario
Debug.Assert(changeTask is not null);
- var change = await changeTask!;
-
- // Roslyn will give us the position to move the cursor after the completion is entered.
- // However, this is in the _new_ document, after changes have been applied. In order to
- // snippetize the insertion text, we need to calculate the offset as we move along the
- // edits, subtracting or adding the difference for edits that do not insersect the current
- // span.
- var adjustedNewPosition = change!.NewPosition;
-
- // There must be at least one change that affects the current location, or something is seriously wrong
- Debug.Assert(change.TextChanges.Any(change => change.Span.IntersectsWith(position)));
-
- foreach (var textChange in change.TextChanges)
- {
- if (!textChange.Span.IntersectsWith(position))
- {
- additionalTextEdits ??= new();
- additionalTextEdits.Add(GetChangeForTextAndSpan(textChange.NewText!, textChange.Span, sourceText));
-
- if (adjustedNewPosition is int newPosition)
- {
- // Find the diff between the original text length and the new text length.
- var diff = (textChange.NewText?.Length ?? 0) - textChange.Span.Length;
- // If the new text is longer than the replaced text, we want to subtract that
- // length from the current new position to find the adjusted position in the old
- // document. If the new text was shorter, diff will be negative, and subtracting
- // will result in increasing the adjusted position as expected
- adjustedNewPosition = newPosition - diff;
- }
- }
- else
- {
- // Either there should be no new position, or it should be within the text that is being added
- // by this change.
- Debug.Assert(adjustedNewPosition is null ||
- (adjustedNewPosition.Value <= textChange.Span.Start + textChange.NewText!.Length) &&
- (adjustedNewPosition.Value >= textChange.Span.Start));
-
- changeSpan = textChange.Span;
- (insertText, insertTextFormat) = getPossiblySnippitizedInsertText(textChange, adjustedNewPosition);
-
- // If we're expecting there to be unimported types, put in an explicit sort text to put things already in scope first.
- // Otherwise, omit the sort text if it's the same as the label to save on space.
- sortText = expectingImportedItems
- ? '0' + completion.SortText
- : labelText == completion.SortText ? null : completion.SortText;
-
- // If the completion is replacing a bigger range than the previously-typed word, we need to have the filter
- // text compensate. Clients will use the range of the text edit to determine the thing that is being filtered
- // against. For example, override completion:
- //
- // override $$
- // |--------| Range that is being changed by the completion
- //
- // That means vscode will consider "override " when looking to see whether the item
- // still matches. To compensate, we add the start of the replacing range, up to the start of the current word,
- // to ensure the item isn't silently filtered out.
-
- if (changeSpan != typedSpan)
- {
- if (typedSpan.Start < changeSpan.Start)
- {
- // This means that some part of the currently-typed text is an exact match for the start of the
- // change, so chop off changeSpan.Start - typedSpan.Start from the filter text to get it to match
- // up with the range
- int prefixMatchElement = changeSpan.Start - typedSpan.Start;
- Debug.Assert(completion.FilterText.StartsWith(sourceText.GetSubText(new TextSpan(typedSpan.Start, prefixMatchElement)).ToString()));
- filterText = completion.FilterText.Substring(prefixMatchElement);
- }
- else
- {
- var prefix = sourceText.GetSubText(TextSpan.FromBounds(changeSpan.Start, typedSpan.Start)).ToString();
- filterText = prefix + completion.FilterText;
- }
- }
- else
- {
- filterText = labelText == completion.FilterText ? null : completion.FilterText;
- }
- }
- }
+ GetCompletionInfo(
+ sourceText,
+ position,
+ completion,
+ await changeTask!,
+ typedSpan,
+ labelText,
+ expectingImportedItems,
+ out insertText, out filterText, out sortText, out insertTextFormat, out changeSpan, out additionalTextEdits);
break;
}
}
- var commitCharacters = buildCommitCharacters(completion.Rules.CommitCharacterRules, isSuggestionMode, commitCharacterRuleCache, commitCharacterRuleBuilder);
+ var commitCharacters = BuildCommitCharacters(completion.Rules.CommitCharacterRules, isSuggestionMode, commitCharacterRuleCache, commitCharacterRuleBuilder);
completionsBuilder.Add(new CompletionItem
{
@@ -181,15 +108,117 @@ internal static partial class CompletionListBuilder
AdditionalTextEdits = additionalTextEdits,
SortText = sortText,
FilterText = filterText,
- Kind = getCompletionItemKind(completion.Tags),
+ Kind = GetCompletionItemKind(completion.Tags),
Detail = completion.InlineDescription,
- Data = i,
+ Data = (cacheId, i),
Preselect = completion.Rules.SelectionBehavior == CompletionItemSelectionBehavior.HardSelection,
CommitCharacters = commitCharacters,
});
}
return (completionsBuilder, seenUnimportedCompletions);
+ }
+
+ private static void GetCompletionInfo(
+ SourceText sourceText,
+ int position,
+ CSharpCompletionItem completion,
+ CompletionChange change,
+ TextSpan typedSpan,
+ string labelText,
+ bool expectingImportedItems,
+ out string? insertText,
+ out string? filterText,
+ out string? sortText,
+ out InsertTextFormat insertTextFormat,
+ out TextSpan changeSpan,
+ out List? additionalTextEdits)
+ {
+ insertTextFormat = InsertTextFormat.PlainText;
+ changeSpan = typedSpan;
+ insertText = null;
+ filterText = null;
+ sortText = null;
+ additionalTextEdits = null;
+
+ // Roslyn will give us the position to move the cursor after the completion is entered.
+ // However, this is in the _new_ document, after changes have been applied. In order to
+ // snippetize the insertion text, we need to calculate the offset as we move along the
+ // edits, subtracting or adding the difference for edits that do not insersect the current
+ // span.
+ var adjustedNewPosition = change!.NewPosition;
+
+ // There must be at least one change that affects the current location, or something is seriously wrong
+ Debug.Assert(change.TextChanges.Any(change => change.Span.IntersectsWith(position)));
+
+ foreach (var textChange in change.TextChanges)
+ {
+ if (!textChange.Span.IntersectsWith(position))
+ {
+ additionalTextEdits ??= new();
+ additionalTextEdits.Add(GetChangeForTextAndSpan(textChange.NewText!, textChange.Span, sourceText));
+
+ if (adjustedNewPosition is int newPosition)
+ {
+ // Find the diff between the original text length and the new text length.
+ var diff = (textChange.NewText?.Length ?? 0) - textChange.Span.Length;
+
+ // If the new text is longer than the replaced text, we want to subtract that
+ // length from the current new position to find the adjusted position in the old
+ // document. If the new text was shorter, diff will be negative, and subtracting
+ // will result in increasing the adjusted position as expected
+ adjustedNewPosition = newPosition - diff;
+ }
+ }
+ else
+ {
+ // Either there should be no new position, or it should be within the text that is being added
+ // by this change.
+ Debug.Assert(adjustedNewPosition is null ||
+ (adjustedNewPosition.Value <= textChange.Span.Start + textChange.NewText!.Length) &&
+ (adjustedNewPosition.Value >= textChange.Span.Start));
+
+ changeSpan = textChange.Span;
+ (insertText, insertTextFormat) = getPossiblySnippitizedInsertText(textChange, adjustedNewPosition);
+
+ // If we're expecting there to be unimported types, put in an explicit sort text to put things already in scope first.
+ // Otherwise, omit the sort text if it's the same as the label to save on space.
+ sortText = GetSortText(completion, labelText, expectingImportedItems);
+
+ // If the completion is replacing a bigger range than the previously-typed word, we need to have the filter
+ // text compensate. Clients will use the range of the text edit to determine the thing that is being filtered
+ // against. For example, override completion:
+ //
+ // override $$
+ // |--------| Range that is being changed by the completion
+ //
+ // That means vscode will consider "override " when looking to see whether the item
+ // still matches. To compensate, we add the start of the replacing range, up to the start of the current word,
+ // to ensure the item isn't silently filtered out.
+
+ if (changeSpan != typedSpan)
+ {
+ if (typedSpan.Start < changeSpan.Start)
+ {
+ // This means that some part of the currently-typed text is an exact match for the start of the
+ // change, so chop off changeSpan.Start - typedSpan.Start from the filter text to get it to match
+ // up with the range
+ int prefixMatchElement = changeSpan.Start - typedSpan.Start;
+ Debug.Assert(completion.FilterText!.StartsWith(sourceText.GetSubText(new TextSpan(typedSpan.Start, prefixMatchElement)).ToString()));
+ filterText = completion.FilterText.Substring(prefixMatchElement);
+ }
+ else
+ {
+ var prefix = sourceText.GetSubText(TextSpan.FromBounds(changeSpan.Start, typedSpan.Start)).ToString();
+ filterText = prefix + completion.FilterText;
+ }
+ }
+ else
+ {
+ filterText = labelText == completion.FilterText ? null : completion.FilterText;
+ }
+ }
+ }
static (string?, InsertTextFormat) getPossiblySnippitizedInsertText(TextChange change, int? adjustedNewPosition)
{
@@ -207,81 +236,13 @@ internal static partial class CompletionListBuilder
return ($"{beforeText}$0{afterText}", InsertTextFormat.Snippet);
}
+ }
- static CompletionItemKind getCompletionItemKind(ImmutableArray tags)
- {
- foreach (var tag in tags)
- {
- if (s_roslynTagToCompletionItemKind.TryGetValue(tag, out var itemKind))
- {
- return itemKind;
- }
- }
-
- return CompletionItemKind.Text;
- }
-
- static IReadOnlyList? buildCommitCharacters(
- ImmutableArray characterRules,
- bool isSuggestionMode,
- Dictionary, IReadOnlyList> commitCharacterRulesCache,
- HashSet 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 characters)
- {
- foreach (var @char in characters)
- {
- commitCharactersBuilder.Add(@char);
- }
- }
- }
+ private static string? GetSortText(CSharpCompletionItem completion, string labelText, bool expectingImportedItems)
+ {
+ return expectingImportedItems
+ ? '0' + completion.SortText
+ : labelText == completion.SortText ? null : completion.SortText;
}
}
}
diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionListCache.cs b/src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionListCache.cs
new file mode 100644
index 0000000000..a1ff6f0c7a
--- /dev/null
+++ b/src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionListCache.cs
@@ -0,0 +1,97 @@
+// Based on dotnet/roslyn:src/Features/LanguageServer/Protocol/Handler/Completion/CompletionListCache.cs
+// CommitID: 62528a6843dc5b21b1a7d399bae814868456e823
+// Original license is MIT
+
+#nullable enable
+
+using System.Collections.Generic;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Completion;
+
+namespace OmniSharp.Roslyn.CSharp.Services.Completion
+{
+ ///
+ /// Caches completion lists in between calls to CompletionHandler and
+ /// CompletionResolveHandler. Used to avoid unnecessary recomputation.
+ ///
+ internal class CompletionListCache
+ {
+ ///
+ /// Maximum number of completion lists allowed in cache. Must be >= 1.
+ ///
+ private const int MaxCacheSize = 2;
+
+ ///
+ /// Multiple cache requests or updates may be received concurrently.
+ /// We need this lock to ensure that we aren't making concurrent
+ /// modifications to _nextResultId or _resultIdToCompletionList.
+ ///
+ private readonly object _accessLock = new object();
+
+ #region protected by _accessLock
+ ///
+ /// The next resultId available to use.
+ ///
+ private long _nextResultId;
+
+ ///
+ /// Keeps track of the resultIds in the cache and their associated
+ /// completion list.
+ ///
+ private readonly List _resultIdToCompletionList = new();
+ #endregion
+
+ ///
+ /// Adds a completion list to the cache. If the cache reaches its maximum size, the oldest completion
+ /// list in the cache is removed.
+ ///
+ ///
+ /// The generated resultId associated with the passed in completion list.
+ ///
+ public long UpdateCache(Document document, int position, CompletionList completionList)
+ {
+ lock (_accessLock)
+ {
+ // If cache exceeds maximum size, remove the oldest list in the cache
+ if (_resultIdToCompletionList.Count >= MaxCacheSize)
+ {
+ _resultIdToCompletionList.RemoveAt(0);
+ }
+
+ // Getting the generated unique resultId
+ var resultId = _nextResultId++;
+
+ // Add passed in completion list to cache
+ var cacheEntry = new CacheEntry(resultId, document, position, completionList);
+ _resultIdToCompletionList.Add(cacheEntry);
+
+ // Return generated resultId so completion list can later be retrieved from cache
+ return resultId;
+ }
+ }
+
+ ///
+ /// Attempts to return the completion list in the cache associated with the given resultId.
+ /// Returns null if no match is found.
+ ///
+ public CacheEntry? GetCachedCompletionList(long resultId)
+ {
+ lock (_accessLock)
+ {
+ foreach (var cacheEntry in _resultIdToCompletionList)
+ {
+ if (cacheEntry.ResultId == resultId)
+ {
+ // We found a match - return completion list
+ return cacheEntry;
+ }
+ }
+
+ // A completion list associated with the given resultId was not found
+ return null;
+ }
+ }
+
+ public record CacheEntry(long ResultId, Document Document, int Position, CompletionList CompletionList);
+ }
+}
diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionService.cs
index 541e4c948d..cdb3807390 100644
--- a/src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionService.cs
+++ b/src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionService.cs
@@ -9,6 +9,7 @@
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Completion;
+using Microsoft.CodeAnalysis.Text;
using Microsoft.Extensions.Logging;
using OmniSharp.Extensions;
using OmniSharp.Mef;
@@ -17,9 +18,9 @@
using OmniSharp.Options;
using OmniSharp.Roslyn.CSharp.Helpers;
using OmniSharp.Roslyn.CSharp.Services.Intellisense;
+using OmniSharp.Utilities;
using CompletionItem = OmniSharp.Models.v1.Completion.CompletionItem;
using CompletionTriggerKind = OmniSharp.Models.v1.Completion.CompletionTriggerKind;
-using CSharpCompletionList = Microsoft.CodeAnalysis.Completion.CompletionList;
using CSharpCompletionService = Microsoft.CodeAnalysis.Completion.CompletionService;
namespace OmniSharp.Roslyn.CSharp.Services.Completion
@@ -27,33 +28,31 @@ namespace OmniSharp.Roslyn.CSharp.Services.Completion
[Shared]
[OmniSharpHandler(OmniSharpEndpoints.Completion, LanguageNames.CSharp)]
[OmniSharpHandler(OmniSharpEndpoints.CompletionResolve, LanguageNames.CSharp)]
+ [OmniSharpHandler(OmniSharpEndpoints.CompletionAfterInsert, LanguageNames.CSharp)]
public class CompletionService :
IRequestHandler,
- IRequestHandler
+ IRequestHandler,
+ IRequestHandler
{
-
private readonly OmniSharpWorkspace _workspace;
+ private readonly OmniSharpOptions _omniSharpOptions;
private readonly FormattingOptions _formattingOptions;
private readonly ILogger _logger;
- private readonly object _lock = new();
- private (CSharpCompletionList Completions, string FileName, int position)? _lastCompletion = null;
+ private readonly CompletionListCache _cache = new();
[ImportingConstructor]
- public CompletionService(OmniSharpWorkspace workspace, FormattingOptions formattingOptions, ILoggerFactory loggerFactory)
+ public CompletionService(OmniSharpWorkspace workspace, FormattingOptions formattingOptions, ILoggerFactory loggerFactory, OmniSharpOptions omniSharpOptions)
{
_workspace = workspace;
_formattingOptions = formattingOptions;
_logger = loggerFactory.CreateLogger();
+ _omniSharpOptions = omniSharpOptions;
}
public async Task Handle(CompletionRequest request)
{
_logger.LogTrace("Completions requested");
- lock (_lock)
- {
- _lastCompletion = null;
- }
var document = _workspace.GetDocument(request.FileName);
if (document is null)
@@ -70,7 +69,6 @@ public async Task Handle(CompletionRequest request)
CompletionTrigger trigger = request.CompletionTrigger switch
{
- CompletionTriggerKind.Invoked => CompletionTrigger.Invoke,
CompletionTriggerKind.TriggerCharacter when request.TriggerCharacter is char c => CompletionTrigger.CreateInsertionTrigger(c),
_ => CompletionTrigger.Invoke,
};
@@ -109,11 +107,7 @@ CompletionItemExtensions.PartialMethodCompletionProvider or
var typedSpan = completionService.GetDefaultCompletionListSpan(sourceText, position);
string typedText = sourceText.GetSubText(typedSpan).ToString();
_logger.LogTrace("Completions filled in");
-
- lock (_lock)
- {
- _lastCompletion = (completions, request.FileName, position);
- }
+ var cacheId = _cache.UpdateCache(document, position, completions);
// If we don't encounter any unimported types, and the completion context thinks that some would be available, then
@@ -128,7 +122,17 @@ CompletionItemExtensions.PartialMethodCompletionProvider or
var isSuggestionMode = completions.SuggestionModeItem is not null;
- var (completionsList, seenUnimportedCompletions) = await CompletionListBuilder.BuildCompletionItems(document, sourceText, position, completionService, completions, typedSpan, expectingImportedItems, isSuggestionMode);
+ var (completionsList, seenUnimportedCompletions) = await CompletionListBuilder.BuildCompletionItems(
+ document,
+ sourceText,
+ cacheId,
+ position,
+ completionService,
+ completions,
+ typedSpan,
+ expectingImportedItems,
+ isSuggestionMode,
+ _omniSharpOptions.RoslynExtensionsOptions.EnableAsyncCompletion);
return new CompletionResponse
{
@@ -139,42 +143,35 @@ CompletionItemExtensions.PartialMethodCompletionProvider or
public async Task Handle(CompletionResolveRequest request)
{
- if (_lastCompletion is null)
+ var cachedList = _cache.GetCachedCompletionList(request.Item.Data.CacheId);
+ if (cachedList is null)
{
_logger.LogError("Cannot call completion/resolve before calling completion!");
return new CompletionResolveResponse { Item = request.Item };
}
- var (completions, fileName, position) = _lastCompletion.Value;
+ var (_, document, position, completions) = cachedList;
+ var index = request.Item.Data.Index;
if (request.Item is null
- || request.Item.Data >= completions.Items.Length
- || request.Item.Data < 0)
+ || index >= completions.Items.Length
+ || index < 0)
{
_logger.LogError("Received invalid completion resolve!");
return new CompletionResolveResponse { Item = request.Item };
}
- var lastCompletionItem = completions.Items[request.Item.Data];
+ var lastCompletionItem = completions.Items[index];
if (lastCompletionItem.DisplayTextPrefix + lastCompletionItem.DisplayText + lastCompletionItem.DisplayTextSuffix != request.Item.Label)
{
_logger.LogError("Inconsistent completion data. Requested data on {0}, but found completion item {1}", request.Item.Label, lastCompletionItem.DisplayText);
return new CompletionResolveResponse { Item = request.Item };
}
-
- var document = _workspace.GetDocument(fileName);
- if (document is null)
- {
- _logger.LogInformation("Could not find document for file {0}", fileName);
- return new CompletionResolveResponse { Item = request.Item };
- }
-
var completionService = CSharpCompletionService.GetService(document);
-
var description = await completionService.GetDescriptionAsync(document, lastCompletionItem);
- StringBuilder textBuilder = new StringBuilder();
+ var textBuilder = new StringBuilder();
MarkdownHelpers.TaggedTextToMarkdown(description.TaggedParts, textBuilder, _formattingOptions, MarkdownFormat.FirstLineAsCSharp, out _);
request.Item.Documentation = textBuilder.ToString();
@@ -209,5 +206,62 @@ public async Task Handle(CompletionResolveRequest req
Item = request.Item
};
}
+
+ public async Task Handle(CompletionAfterInsertRequest request)
+ {
+ var cachedList = _cache.GetCachedCompletionList(request.Item.Data.CacheId);
+ if (cachedList is null)
+ {
+ _logger.LogError("Cannot call completion/afterInsert before calling completion!");
+ return new CompletionAfterInsertResponse();
+ }
+
+ var (_, document, _, completions) = cachedList;
+ var index = request.Item.Data.Index;
+
+ if (request.Item is null
+ || index >= completions.Items.Length
+ || index < 0
+ || request.Item.TextEdit is null)
+ {
+ _logger.LogError("Received invalid completion afterInsert!");
+ return new CompletionAfterInsertResponse();
+ }
+
+ var lastCompletionItem = completions.Items[index];
+ if (lastCompletionItem.DisplayTextPrefix + lastCompletionItem.DisplayText + lastCompletionItem.DisplayTextSuffix != request.Item.Label)
+ {
+ _logger.LogError("Inconsistent completion data. Requested data on {0}, but found completion item {1}", request.Item.Label, lastCompletionItem.DisplayText);
+ return new CompletionAfterInsertResponse();
+ }
+
+ if (lastCompletionItem.GetProviderName() is not (CompletionItemExtensions.OverrideCompletionProvider or
+ CompletionItemExtensions.PartialMethodCompletionProvider)
+ and var name)
+ {
+ _logger.LogWarning("Received unsupported afterInsert completion request for provider {0}", name);
+ return new CompletionAfterInsertResponse();
+ }
+
+ var completionService = CSharpCompletionService.GetService(document);
+
+ // Get a document with change from the completion inserted, so that we can resolve the completion and get the
+ // final full change.
+ var sourceText = await document.GetTextAsync();
+ var insertedSpan = sourceText.GetSpanFromLinePositionSpanTextChange(request.Item.TextEdit);
+ var changedText = sourceText.WithChanges(new TextChange(insertedSpan, request.Item.TextEdit.NewText));
+ var changedDocument = document.WithText(changedText);
+
+ var finalChange = await completionService.GetChangeAsync(changedDocument, lastCompletionItem, new TextSpan(insertedSpan.Start, request.Item.TextEdit.NewText.Length));
+ var finalText = changedText.WithChanges(finalChange.TextChange);
+ var finalPosition = finalText.GetPointFromPosition(finalChange.NewPosition!.Value);
+
+ return new CompletionAfterInsertResponse
+ {
+ Changes = finalChange.TextChanges.SelectAsArray(changedText, static (c, changedText) => CompletionListBuilder.GetChangeForTextAndSpan(c.NewText, c.Span, changedText)),
+ Line = finalPosition.Line,
+ Column = finalPosition.Column,
+ };
+ }
}
}
diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Intellisense/CompletionItemExtensions.cs b/src/OmniSharp.Roslyn.CSharp/Services/Intellisense/CompletionItemExtensions.cs
index 148125cd40..2980abf9bd 100644
--- a/src/OmniSharp.Roslyn.CSharp/Services/Intellisense/CompletionItemExtensions.cs
+++ b/src/OmniSharp.Roslyn.CSharp/Services/Intellisense/CompletionItemExtensions.cs
@@ -92,9 +92,11 @@ public static async Task> GetCompletionSymbolsAsync(this Co
&& properties.TryGetValue(SymbolKind, out string symbolKindValue)
&& int.Parse(symbolKindValue) is int symbolKindInt)
{
+#pragma warning disable RS1024 // Compare symbols correctly: service is deprecated, not going to change behavior now.
return recommendedSymbols
.Where(x => (int)x.Kind == symbolKindInt && x.Name.Equals(symbolNameValue, StringComparison.OrdinalIgnoreCase))
.Distinct();
+#pragma warning restore RS1024 // Compare symbols correctly
}
return Enumerable.Empty();
diff --git a/src/OmniSharp.Roslyn/Extensions/TextExtensions.cs b/src/OmniSharp.Roslyn/Extensions/TextExtensions.cs
index b6b8844747..1f22f1fffe 100644
--- a/src/OmniSharp.Roslyn/Extensions/TextExtensions.cs
+++ b/src/OmniSharp.Roslyn/Extensions/TextExtensions.cs
@@ -1,4 +1,5 @@
using Microsoft.CodeAnalysis.Text;
+using OmniSharp.Models;
using OmniSharp.Models.V2;
namespace OmniSharp.Extensions
@@ -48,5 +49,13 @@ public static TextSpan GetSpanFromRange(this SourceText text, Range range)
=> TextSpan.FromBounds(
start: text.GetPositionFromPoint(range.Start),
end: text.GetPositionFromPoint(range.End));
+
+ ///
+ /// Converts an OmniSharp to a within a .
+ ///
+ public static TextSpan GetSpanFromLinePositionSpanTextChange(this SourceText text, LinePositionSpanTextChange change)
+ => TextSpan.FromBounds(
+ start: text.GetPositionFromLineAndOffset(change.StartLine, change.StartColumn),
+ end: text.GetPositionFromLineAndOffset(change.EndLine, change.EndColumn));
}
}
diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/CompletionFacts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/CompletionFacts.cs
index a859d6e7eb..ddfebf4f1d 100644
--- a/tests/OmniSharp.Roslyn.CSharp.Tests/CompletionFacts.cs
+++ b/tests/OmniSharp.Roslyn.CSharp.Tests/CompletionFacts.cs
@@ -146,9 +146,11 @@ public Class1()
}
[Theory]
- [InlineData("dummy.cs")]
- [InlineData("dummy.csx")]
- public async Task ImportCompletionResolvesOnSubsequentQueries(string filename)
+ [InlineData("dummy.cs", true)]
+ [InlineData("dummy.cs", false)]
+ [InlineData("dummy.csx", true)]
+ [InlineData("dummy.csx", false)]
+ public async Task ImportCompletionResolvesOnSubsequentQueries(string filename, bool useAsyncCompletion)
{
const string input =
@"public class Class1 {
@@ -158,12 +160,13 @@ public Class1()
}
}";
- using var host = GetImportCompletionHost();
+ using var host = useAsyncCompletion ? GetAsyncCompletionAndImportCompletionHost() : GetImportCompletionHost();
// First completion request should kick off the task to update the completion cache.
var completions = await FindCompletionsAsync(filename, input, host);
Assert.True(completions.IsIncomplete);
Assert.DoesNotContain("Guid", completions.Items.Select(c => c.TextEdit.NewText));
+ Assert.All(completions.Items, c => Assert.False(c.HasAfterInsertStep));
// Populating the completion cache should take no more than a few ms, don't let it take too
// long
@@ -182,9 +185,11 @@ await Task.Run(async () =>
}
[Theory]
- [InlineData("dummy.cs")]
- [InlineData("dummy.csx")]
- public async Task ImportCompletion_LocalsPrioritizedOverImports(string filename)
+ [InlineData("dummy.cs", true)]
+ [InlineData("dummy.cs", false)]
+ [InlineData("dummy.csx", true)]
+ [InlineData("dummy.csx", false)]
+ public async Task ImportCompletion_LocalsPrioritizedOverImports(string filename, bool useAsyncCompletion)
{
const string input =
@@ -196,20 +201,22 @@ public Class1()
}
}";
- using var host = GetImportCompletionHost();
+ using var host = useAsyncCompletion ? GetAsyncCompletionAndImportCompletionHost() : GetImportCompletionHost();
var completions = await FindCompletionsWithImportedAsync(filename, input, host);
CompletionItem localCompletion = completions.Items.First(c => c.TextEdit.NewText == "guid");
CompletionItem typeCompletion = completions.Items.First(c => c.TextEdit.NewText == "Guid");
- Assert.True(localCompletion.Data < typeCompletion.Data);
+ Assert.True(localCompletion.Data.Index < typeCompletion.Data.Index);
Assert.StartsWith("0", localCompletion.SortText);
Assert.StartsWith("1", typeCompletion.SortText);
VerifySortOrders(completions.Items);
}
[Theory]
- [InlineData("dummy.cs")]
- [InlineData("dummy.csx")]
- public async Task ImportCompletions_IncludesExtensionMethods(string filename)
+ [InlineData("dummy.cs", true)]
+ [InlineData("dummy.cs", false)]
+ [InlineData("dummy.csx", true)]
+ [InlineData("dummy.csx", false)]
+ public async Task ImportCompletions_IncludesExtensionMethods(string filename, bool useAsyncCompletion)
{
const string input =
@"namespace N1
@@ -232,16 +239,18 @@ public static void Test(this object o)
}
}";
- using var host = GetImportCompletionHost();
+ using var host = useAsyncCompletion ? GetAsyncCompletionAndImportCompletionHost() : GetImportCompletionHost();
var completions = await FindCompletionsWithImportedAsync(filename, input, host);
Assert.Contains("Test", completions.Items.Select(c => c.TextEdit.NewText));
VerifySortOrders(completions.Items);
}
[Theory]
- [InlineData("dummy.cs")]
- [InlineData("dummy.csx")]
- public async Task ImportCompletion_ResolveAddsImportEdit(string filename)
+ [InlineData("dummy.cs", true)]
+ [InlineData("dummy.cs", false)]
+ [InlineData("dummy.csx", true)]
+ [InlineData("dummy.csx", false)]
+ public async Task ImportCompletion_ResolveAddsImportEdit(string filename, bool useAsyncCompletion)
{
const string input =
@"namespace N1
@@ -264,7 +273,7 @@ public static void Test(this object o)
}
}";
- using var host = GetImportCompletionHost();
+ using var host = useAsyncCompletion ? GetAsyncCompletionAndImportCompletionHost() : GetImportCompletionHost();
var completions = await FindCompletionsWithImportedAsync(filename, input, host);
var resolved = await ResolveCompletionAsync(completions.Items.First(c => c.TextEdit.NewText == "Test"), host);
@@ -280,13 +289,15 @@ public static void Test(this object o)
}
[Theory]
- [InlineData("dummy.cs")]
- [InlineData("dummy.csx")]
- public async Task ImportCompletion_OnLine0(string filename)
+ [InlineData("dummy.cs", true)]
+ [InlineData("dummy.cs", false)]
+ [InlineData("dummy.csx", true)]
+ [InlineData("dummy.csx", false)]
+ public async Task ImportCompletion_OnLine0(string filename, bool useAsyncCompletion)
{
const string input = @"$$";
- using var host = GetImportCompletionHost();
+ using var host = useAsyncCompletion ? GetAsyncCompletionAndImportCompletionHost() : GetImportCompletionHost();
var completions = await FindCompletionsWithImportedAsync(filename, input, host);
var resolved = await ResolveCompletionAsync(completions.Items.First(c => c.TextEdit.NewText == "Console"), host);
@@ -302,9 +313,11 @@ public async Task ImportCompletion_OnLine0(string filename)
}
[Theory]
- [InlineData("dummy.cs")]
- [InlineData("dummy.csx")]
- public async Task SelectsLastInstanceOfCompletion(string filename)
+ [InlineData("dummy.cs", true)]
+ [InlineData("dummy.cs", false)]
+ [InlineData("dummy.csx", true)]
+ [InlineData("dummy.csx", false)]
+ public async Task SelectsLastInstanceOfCompletion(string filename, bool useAsyncCompletion)
{
const string input =
@"namespace N1
@@ -327,7 +340,7 @@ public static void Test(this object o)
}
}";
- using var host = GetImportCompletionHost();
+ using var host = useAsyncCompletion ? GetAsyncCompletionAndImportCompletionHost() : GetImportCompletionHost();
var completions = await FindCompletionsWithImportedAsync(filename, input, host);
var resolved = await ResolveCompletionAsync(completions.Items.First(c => c.TextEdit.NewText == "Guid"), host);
@@ -343,9 +356,11 @@ public static void Test(this object o)
}
[Theory]
- [InlineData("dummy.cs")]
- [InlineData("dummy.csx")]
- public async Task UsingsAddedInOrder(string filename)
+ [InlineData("dummy.cs", true)]
+ [InlineData("dummy.cs", false)]
+ [InlineData("dummy.csx", true)]
+ [InlineData("dummy.csx", false)]
+ public async Task UsingsAddedInOrder(string filename, bool useAsyncCompletion)
{
const string input =
@@ -374,7 +389,7 @@ public class C3
}
}";
- using var host = GetImportCompletionHost();
+ using var host = useAsyncCompletion ? GetAsyncCompletionAndImportCompletionHost() : GetImportCompletionHost();
var completions = await FindCompletionsWithImportedAsync(filename, input, host);
var resolved = await ResolveCompletionAsync(completions.Items.First(c => c.TextEdit.NewText == "C2"), host);
@@ -603,7 +618,45 @@ MyClass m$$
[Theory]
[InlineData("dummy.cs")]
[InlineData("dummy.csx")]
- public async Task OverrideSignatures_Publics(string filename)
+ public async Task OverrideSignatures_Publics_Async(string filename)
+ {
+ const string source = @"
+class Foo
+{
+ public virtual void Test(string text) {}
+ public virtual void Test(string text, string moreText) {}
+}
+class FooChild : Foo
+{
+ override $$
+}
+";
+
+ using var host = GetAsyncCompletionAndImportCompletionHost();
+ var completions = await FindCompletionsAsync(filename, source, host);
+ Assert.Equal(new[] { "Equals(object obj)", "GetHashCode()", "Test(string text)", "Test(string text, string moreText)", "ToString()" },
+ completions.Items.Select(c => c.Label));
+ Assert.Equal(new[] { "Equals", "GetHashCode", "Test", "Test", "ToString" },
+ completions.Items.Select(c => c.TextEdit.NewText));
+
+ Assert.All(completions.Items.Select(c => c.AdditionalTextEdits), edit => Assert.Null(edit));
+ Assert.All(completions.Items, c => Assert.Equal(InsertTextFormat.PlainText, c.InsertTextFormat));
+
+ var afterInsert = await AfterInsertResponse(completions.Items[0], host);
+ var change = afterInsert.Changes.Single();
+ Assert.Equal("public override bool Equals(object obj)\n {\n return base.Equals(obj);\n }", change.NewText);
+ Assert.Equal(8, change.StartLine);
+ Assert.Equal(4, change.StartColumn);
+ Assert.Equal(8, change.EndLine);
+ Assert.Equal(19, change.EndColumn);
+ Assert.Equal(10, afterInsert.Line);
+ Assert.Equal(32, afterInsert.Column);
+ }
+
+ [Theory]
+ [InlineData("dummy.cs")]
+ [InlineData("dummy.csx")]
+ public async Task OverrideSignatures_Publics_Sync(string filename)
{
const string source = @"
class Foo
@@ -655,7 +708,54 @@ class FooChild : Foo
[Theory]
[InlineData("dummy.cs")]
[InlineData("dummy.csx")]
- public async Task OverrideSignatures_UnimportedTypesFullyQualified(string filename)
+ public async Task OverrideSignatures_UnimportedTypesFullyQualified_Async(string filename)
+ {
+ const string source = @"
+using N2;
+namespace N1
+{
+ public class CN1 {}
+}
+namespace N2
+{
+ using N1;
+ public abstract class IN2 { protected abstract CN1 GetN1(); }
+}
+namespace N3
+{
+ class CN3 : IN2
+ {
+ override $$
+ }
+}";
+
+ using var host = GetAsyncCompletionAndImportCompletionHost();
+ var completions = await FindCompletionsAsync(filename, source, host);
+ Assert.Equal(new[] { "Equals(object obj)", "GetHashCode()", "GetN1()", "ToString()" },
+ completions.Items.Select(c => c.Label));
+
+ Assert.Equal(new[] { "Equals", "GetHashCode", "GetN1", "ToString" },
+ completions.Items.Select(c => c.TextEdit.NewText));
+
+ Assert.All(completions.Items.Select(c => c.AdditionalTextEdits), edit => Assert.Null(edit));
+ Assert.All(completions.Items, c => Assert.Equal(InsertTextFormat.PlainText, c.InsertTextFormat));
+
+ var afterInsert = await AfterInsertResponse(completions.Items.First(i => i.Label == "GetN1()"), host);
+ var change = afterInsert.Changes.Single();
+ Assert.Equal("protected override N1.CN1 GetN1()\n {\n throw new System.NotImplementedException();\n }",
+ change.NewText);
+ Assert.Equal(15, change.StartLine);
+ Assert.Equal(8, change.StartColumn);
+ Assert.Equal(15, change.EndLine);
+ Assert.Equal(22, change.EndColumn);
+ Assert.Equal(17, afterInsert.Line);
+ Assert.Equal(55, afterInsert.Column);
+ }
+
+ [Theory]
+ [InlineData("dummy.cs")]
+ [InlineData("dummy.csx")]
+ public async Task OverrideSignatures_UnimportedTypesFullyQualified_Sync(string filename)
{
const string source = @"
using N2;
@@ -711,7 +811,40 @@ class CN3 : IN2
[Theory]
[InlineData("dummy.cs")]
[InlineData("dummy.csx")]
- public async Task OverrideSignatures_ModifierInFront(string filename)
+ public async Task OverrideSignatures_ModifierInFront_Async(string filename)
+ {
+ const string source = @"
+class C
+{
+ public override $$
+}";
+
+ using var host = GetAsyncCompletionAndImportCompletionHost();
+ var completions = await FindCompletionsAsync(filename, source, host);
+ Assert.Equal(new[] { "Equals(object obj)", "GetHashCode()", "ToString()" },
+ completions.Items.Select(c => c.Label));
+
+ Assert.Equal(new[] { "Equals", "GetHashCode", "ToString" },
+ completions.Items.Select(c => c.TextEdit.NewText));
+
+ Assert.All(completions.Items.Select(c => c.AdditionalTextEdits), a => Assert.Null(a));
+ Assert.All(completions.Items, c => Assert.Equal(InsertTextFormat.PlainText, c.InsertTextFormat));
+
+ var afterInsert = await AfterInsertResponse(completions.Items[0], host);
+ var change = afterInsert.Changes.Single();
+ Assert.Equal("bool Equals(object obj)\n {\n return base.Equals(obj);\n }", change.NewText);
+ Assert.Equal(3, change.StartLine);
+ Assert.Equal(20, change.StartColumn);
+ Assert.Equal(3, change.EndLine);
+ Assert.Equal(26, change.EndColumn);
+ Assert.Equal(5, afterInsert.Line);
+ Assert.Equal(32, afterInsert.Column);
+ }
+
+ [Theory]
+ [InlineData("dummy.cs")]
+ [InlineData("dummy.csx")]
+ public async Task OverrideSignatures_ModifierInFront_Sync(string filename)
{
const string source = @"
class C
@@ -742,7 +875,96 @@ public override $$
[Theory]
[InlineData("dummy.cs")]
[InlineData("dummy.csx")]
- public async Task OverrideSignatures_ModifierAndReturnTypeInFront(string filename)
+ public async Task OverrideSignatures_PartiallyTypedMethod_Async(string filename)
+ {
+ const string source = @"
+class C
+{
+ override Eq$$
+}";
+
+ using var host = GetAsyncCompletionAndImportCompletionHost();
+ var completions = await FindCompletionsAsync(filename, source, host);
+ Assert.Equal(new[] { "Equals(object obj)", "GetHashCode()", "ToString()" },
+ completions.Items.Select(c => c.Label));
+
+ Assert.Equal(new[] { "Equals", "GetHashCode", "ToString" },
+ completions.Items.Select(c => c.TextEdit.NewText));
+ Assert.All(completions.Items.Select(c => c.AdditionalTextEdits), a => Assert.Null(a));
+ Assert.All(completions.Items, c => Assert.Equal(InsertTextFormat.PlainText, c.InsertTextFormat));
+
+ var afterInsert = await AfterInsertResponse(completions.Items[0], host);
+ var change = afterInsert.Changes.Single();
+ Assert.Equal("public override bool Equals(object obj)\n {\n return base.Equals(obj);\n }", change.NewText);
+ Assert.Equal(3, change.StartLine);
+ Assert.Equal(4, change.StartColumn);
+ Assert.Equal(3, change.EndLine);
+ Assert.Equal(19, change.EndColumn);
+ Assert.Equal(5, afterInsert.Line);
+ Assert.Equal(32, afterInsert.Column);
+ }
+
+ [Theory]
+ [InlineData("dummy.cs")]
+ [InlineData("dummy.csx")]
+ public async Task OverrideSignatures_PartiallyTypedMethod_Sync(string filename)
+ {
+ const string source = @"
+class C
+{
+ override Eq$$
+}";
+
+ var completions = await FindCompletionsAsync(filename, source, SharedOmniSharpTestHost);
+ Assert.Equal(new[] { "Equals(object obj)", "GetHashCode()", "ToString()" },
+ completions.Items.Select(c => c.Label));
+
+ Assert.Equal(new[] { "public override bool Equals(object obj)\n {\n return base.Equals(obj);$0\n \\}",
+ "public override int GetHashCode()\n {\n return base.GetHashCode();$0\n \\}",
+ "public override string ToString()\n {\n return base.ToString();$0\n \\}"
+ },
+ completions.Items.Select(c => c.TextEdit.NewText));
+ Assert.All(completions.Items.Select(c => c.AdditionalTextEdits), a => Assert.Null(a));
+ Assert.All(completions.Items, c => Assert.Equal(InsertTextFormat.Snippet, c.InsertTextFormat));
+ }
+
+ [Theory]
+ [InlineData("dummy.cs")]
+ [InlineData("dummy.csx")]
+ public async Task OverrideSignatures_ModifierAndReturnTypeInFront_Async(string filename)
+ {
+ const string source = @"
+class C
+{
+ public override bool $$
+}";
+
+ using var host = GetAsyncCompletionAndImportCompletionHost();
+ var completions = await FindCompletionsAsync(filename, source, host);
+ Assert.Equal(new[] { "Equals(object obj)" },
+ completions.Items.Select(c => c.Label));
+
+ Assert.Equal(new[] { "Equals" },
+ completions.Items.Select(c => c.TextEdit.NewText));
+
+ Assert.All(completions.Items.Select(c => c.AdditionalTextEdits), edit => Assert.Null(edit));
+ Assert.All(completions.Items, c => Assert.Equal(InsertTextFormat.PlainText, c.InsertTextFormat));
+
+ var afterInsert = await AfterInsertResponse(completions.Items[0], host);
+ var change = afterInsert.Changes.Single();
+ Assert.Equal("(object obj)\n {\n return base.Equals(obj);\n }", change.NewText);
+ Assert.Equal(3, change.StartLine);
+ Assert.Equal(31, change.StartColumn);
+ Assert.Equal(3, change.EndLine);
+ Assert.Equal(31, change.EndColumn);
+ Assert.Equal(5, afterInsert.Line);
+ Assert.Equal(32, afterInsert.Column);
+ }
+
+ [Theory]
+ [InlineData("dummy.cs")]
+ [InlineData("dummy.csx")]
+ public async Task OverrideSignatures_ModifierAndReturnTypeInFront_Sync(string filename)
{
const string source = @"
class C
@@ -767,7 +989,45 @@ public override bool $$
[Theory]
[InlineData("dummy.cs")]
[InlineData("dummy.csx")]
- public async Task OverrideSignatures_TestTest(string filename)
+ public async Task OverrideSignatures_TestTest_Async(string filename)
+ {
+ const string source = @"
+class Test {}
+abstract class Base
+{
+ protected abstract Test Test();
+}
+class Derived : Base
+{
+ override $$
+}";
+
+ using var host = GetAsyncCompletionAndImportCompletionHost();
+ var completions = await FindCompletionsAsync(filename, source, host);
+ Assert.Equal(new[] { "Equals(object obj)", "GetHashCode()", "Test()", "ToString()" },
+ completions.Items.Select(c => c.Label));
+
+ Assert.Equal(new[] { "Equals", "GetHashCode", "Test", "ToString" },
+ completions.Items.Select(c => c.TextEdit.NewText));
+
+ Assert.All(completions.Items.Select(c => c.AdditionalTextEdits), edit => Assert.Null(edit));
+ Assert.All(completions.Items, c => Assert.Equal(InsertTextFormat.PlainText, c.InsertTextFormat));
+
+ var afterInsert = await AfterInsertResponse(completions.Items[0], host);
+ var change = afterInsert.Changes.Single();
+ Assert.Equal("public override bool Equals(object obj)\n {\n return base.Equals(obj);\n }", change.NewText);
+ Assert.Equal(8, change.StartLine);
+ Assert.Equal(4, change.StartColumn);
+ Assert.Equal(8, change.EndLine);
+ Assert.Equal(19, change.EndColumn);
+ Assert.Equal(10, afterInsert.Line);
+ Assert.Equal(32, afterInsert.Column);
+ }
+
+ [Theory]
+ [InlineData("dummy.cs")]
+ [InlineData("dummy.csx")]
+ public async Task OverrideSignatures_TestTest_Sync(string filename)
{
const string source = @"
class Test {}
@@ -812,7 +1072,53 @@ class Derived : Base
}
[Fact]
- public async Task OverrideCompletion_TypesNeedImport()
+ public async Task OverrideCompletion_TypesNeedImport_Async()
+ {
+ const string baseText = @"
+using System;
+public class Base
+{
+ public virtual Action GetAction(Action a) => null;
+}
+";
+
+ const string derivedText = @"
+public class Derived : Base
+{
+ override $$
+}";
+
+ using var host = GetAsyncCompletionAndImportCompletionHost();
+ var completions = await FindCompletionsAsync("derived.cs", derivedText, host, additionalFiles: new[] { new TestFile("base.cs", baseText) });
+ var item = completions.Items.Single(c => c.Label.StartsWith("GetAction"));
+ Assert.Equal("GetAction(System.Action a)", item.Label);
+ Assert.Equal("GetAction", item.TextEdit.NewText);
+
+ Assert.Null(item.AdditionalTextEdits);
+
+ var afterInsert = await AfterInsertResponse(item, host);
+ Assert.Equal(2, afterInsert.Changes.Count);
+
+ var firstChange = afterInsert.Changes[0];
+ Assert.Equal(NormalizeNewlines("using System;\n\n"), firstChange.NewText);
+ Assert.Equal(1, firstChange.StartLine);
+ Assert.Equal(0, firstChange.StartColumn);
+ Assert.Equal(1, firstChange.EndLine);
+ Assert.Equal(0, firstChange.EndColumn);
+
+ var secondChange = afterInsert.Changes[1];
+ Assert.Equal(NormalizeNewlines("public override Action GetAction(Action a)\n {\n return base.GetAction(a);\n }"), secondChange.NewText);
+ Assert.Equal(3, secondChange.StartLine);
+ Assert.Equal(4, secondChange.StartColumn);
+ Assert.Equal(3, secondChange.EndLine);
+ Assert.Equal(22, secondChange.EndColumn);
+
+ Assert.Equal(7, afterInsert.Line);
+ Assert.Equal(33, afterInsert.Column);
+ }
+
+ [Fact]
+ public async Task OverrideCompletion_TypesNeedImport_Sync()
{
const string baseText = @"
using System;
@@ -838,7 +1144,7 @@ public class Derived : Base
Assert.Equal(0, item.AdditionalTextEdits[0].StartColumn);
Assert.Equal(1, item.AdditionalTextEdits[0].EndLine);
Assert.Equal(0, item.AdditionalTextEdits[0].EndColumn);
- Assert.Equal("public override Action GetAction(Action a)\n {\n return base.GetAction(a);$0\n \\}", item.TextEdit.NewText);
+ Assert.Equal(NormalizeNewlines("public override Action GetAction(Action a)\n {\n return base.GetAction(a);$0\n \\}"), item.TextEdit.NewText);
Assert.Equal(3, item.TextEdit.StartLine);
Assert.Equal(4, item.TextEdit.StartColumn);
Assert.Equal(3, item.TextEdit.EndLine);
@@ -847,7 +1153,41 @@ public class Derived : Base
}
[Fact]
- public async Task OverrideCompletion_FromNullableToNonNullableContext()
+ public async Task OverrideCompletion_FromNullableToNonNullableContext_Async()
+ {
+ const string text = @"
+#nullable enable
+public class Base
+{
+ public virtual object? M1(object? param) => throw null;
+}
+#nullable disable
+public class Derived : Base
+{
+ override $$
+}";
+
+ using var host = GetAsyncCompletionAndImportCompletionHost();
+ var completions = await FindCompletionsAsync("derived.cs", text, host);
+ var item = completions.Items.Single(c => c.Label.StartsWith("M1"));
+ Assert.Equal("M1(object? param)", item.Label);
+ Assert.Equal("M1", item.TextEdit.NewText);
+
+ Assert.Null(item.AdditionalTextEdits);
+
+ var afterInsert = await AfterInsertResponse(completions.Items[0], host);
+ var change = afterInsert.Changes.Single();
+ Assert.Equal("public override bool Equals(object obj)\n {\n return base.Equals(obj);\n }", change.NewText);
+ Assert.Equal(9, change.StartLine);
+ Assert.Equal(4, change.StartColumn);
+ Assert.Equal(9, change.EndLine);
+ Assert.Equal(19, change.EndColumn);
+ Assert.Equal(11, afterInsert.Line);
+ Assert.Equal(32, afterInsert.Column);
+ }
+
+ [Fact]
+ public async Task OverrideCompletion_FromNullableToNonNullableContext_Sync()
{
const string text = @"
#nullable enable
@@ -877,7 +1217,7 @@ public class Derived : Base
[Theory]
[InlineData("dummy.cs")]
[InlineData("dummy.csx")]
- public async Task OverrideCompletion_PropertyGetSet(string filename)
+ public async Task OverrideCompletion_PropertyGetSet_Sync(string filename)
{
const string source = @"
using System;
@@ -907,7 +1247,43 @@ public class Derived : Base
[Theory]
[InlineData("dummy.cs")]
[InlineData("dummy.csx")]
- public async Task OverrideCompletion_PropertyGet(string filename)
+ public async Task OverrideCompletion_PropertyGetSet_Async(string filename)
+ {
+ const string source = @"
+using System;
+public class Base
+{
+ public abstract string Prop { get; set; }
+}
+public class Derived : Base
+{
+ override $$
+}
+";
+
+ using var host = GetAsyncCompletionAndImportCompletionHost();
+ var completions = await FindCompletionsAsync(filename, source, host);
+ var item = completions.Items.Single(c => c.Label.StartsWith("Prop"));
+ Assert.Equal("Prop", item.Label);
+ Assert.Equal("Prop", item.TextEdit.NewText);
+
+ Assert.Null(item.AdditionalTextEdits);
+
+ var afterInsert = await AfterInsertResponse(item, host);
+ var change = afterInsert.Changes.Single();
+ Assert.Equal("public override string Prop { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }", change.NewText);
+ Assert.Equal(8, change.StartLine);
+ Assert.Equal(4, change.StartColumn);
+ Assert.Equal(8, change.EndLine);
+ Assert.Equal(17, change.EndColumn);
+ Assert.Equal(8, afterInsert.Line);
+ Assert.Equal(76, afterInsert.Column);
+ }
+
+ [Theory]
+ [InlineData("dummy.cs")]
+ [InlineData("dummy.csx")]
+ public async Task OverrideCompletion_PropertyGet_Sync(string filename)
{
const string source = @"
using System;
@@ -938,7 +1314,45 @@ public class Derived : Base
[Theory]
[InlineData("dummy.cs")]
[InlineData("dummy.csx")]
- public async Task PartialCompletion(string filename)
+ public async Task PartialCompletion_Async(string filename)
+ {
+ const string source = @"
+partial class C
+{
+ partial void M1(string param);
+}
+partial class C
+{
+ partial $$
+}
+";
+
+ using var host = GetAsyncCompletionAndImportCompletionHost();
+ var completions = await FindCompletionsAsync(filename, source, host);
+ Assert.Equal(new[] { "M1(string param)" },
+ completions.Items.Select(c => c.Label));
+
+ Assert.Equal(new[] { "M1" },
+ completions.Items.Select(c => c.TextEdit.NewText));
+
+ Assert.All(completions.Items.Select(c => c.AdditionalTextEdits), edit => Assert.Null(edit));
+ Assert.All(completions.Items, c => Assert.Equal(InsertTextFormat.PlainText, c.InsertTextFormat));
+
+ var afterInsert = await AfterInsertResponse(completions.Items[0], host);
+ var change = afterInsert.Changes.Single();
+ Assert.Equal("void M1(string param)\n {\n throw new System.NotImplementedException();\n }", change.NewText);
+ Assert.Equal(7, change.StartLine);
+ Assert.Equal(12, change.StartColumn);
+ Assert.Equal(7, change.EndLine);
+ Assert.Equal(14, change.EndColumn);
+ Assert.Equal(9, afterInsert.Line);
+ Assert.Equal(51, afterInsert.Column);
+ }
+
+ [Theory]
+ [InlineData("dummy.cs")]
+ [InlineData("dummy.csx")]
+ public async Task PartialCompletion_Sync(string filename)
{
const string source = @"
partial class C
@@ -963,7 +1377,52 @@ partial class C
}
[Fact]
- public async Task PartialCompletion_TypesNeedImport()
+ public async Task PartialCompletion_TypesNeedImport_Async()
+ {
+ const string file1 = @"
+using System;
+public partial class C
+{
+ partial void M(Action a);
+}
+";
+
+ const string file2 = @"
+public partial class C
+{
+ partial $$
+}";
+
+ using var host = GetAsyncCompletionAndImportCompletionHost();
+ var completions = await FindCompletionsAsync("derived.cs", file2, host, additionalFiles: new[] { new TestFile("base.cs", file1) });
+ var item = completions.Items.Single(c => c.Label.StartsWith("M"));
+
+ Assert.Null(item.AdditionalTextEdits);
+ Assert.Equal("M", item.TextEdit.NewText);
+
+ var afterInsert = await AfterInsertResponse(item, host);
+ Assert.Equal(2, afterInsert.Changes.Count);
+
+ var firstChange = afterInsert.Changes[0];
+ Assert.Equal(NormalizeNewlines("using System;\n\n"), firstChange.NewText);
+ Assert.Equal(1, firstChange.StartLine);
+ Assert.Equal(0, firstChange.StartColumn);
+ Assert.Equal(1, firstChange.EndLine);
+ Assert.Equal(0, firstChange.EndColumn);
+
+ var secondChange = afterInsert.Changes[1];
+ Assert.Equal(NormalizeNewlines("void M(Action a)\n {\n throw new NotImplementedException();\n }"), secondChange.NewText);
+ Assert.Equal(3, secondChange.StartLine);
+ Assert.Equal(12, secondChange.StartColumn);
+ Assert.Equal(3, secondChange.EndLine);
+ Assert.Equal(13, secondChange.EndColumn);
+
+ Assert.Equal(7, afterInsert.Line);
+ Assert.Equal(44, afterInsert.Column);
+ }
+
+ [Fact]
+ public async Task PartialCompletion_TypesNeedImport_Sync()
{
const string file1 = @"
using System;
@@ -998,7 +1457,40 @@ public partial class C
}
[Fact]
- public async Task PartialCompletion_FromNullableToNonNullableContext()
+ public async Task PartialCompletion_FromNullableToNonNullableContext_Async()
+ {
+ const string text = @"
+#nullable enable
+public partial class C
+{
+ partial void M1(object? param);
+}
+#nullable disable
+public partial class C
+{
+ partial $$
+}";
+
+ using var host = GetAsyncCompletionAndImportCompletionHost();
+ var completions = await FindCompletionsAsync("derived.cs", text, host);
+ var item = completions.Items.Single(c => c.Label.StartsWith("M1"));
+ Assert.Equal("M1(object param)", item.Label);
+ Assert.Null(item.AdditionalTextEdits);
+ Assert.Equal("M1", item.TextEdit.NewText);
+
+ var afterInsert = await AfterInsertResponse(item, host);
+ var change = afterInsert.Changes.Single();
+ Assert.Equal("void M1(object param)\n {\n throw new System.NotImplementedException();\n }", change.NewText);
+ Assert.Equal(9, change.StartLine);
+ Assert.Equal(12, change.StartColumn);
+ Assert.Equal(9, change.EndLine);
+ Assert.Equal(14, change.EndColumn);
+ Assert.Equal(11, afterInsert.Line);
+ Assert.Equal(51, afterInsert.Column);
+ }
+
+ [Fact]
+ public async Task PartialCompletion_FromNullableToNonNullableContext_Sync()
{
const string text = @"
#nullable enable
@@ -1022,7 +1514,40 @@ public partial class C
[Theory]
[InlineData("dummy.cs")]
[InlineData("dummy.csx")]
- public async Task OverrideSignatures_PartiallyTypedIdentifier(string filename)
+ public async Task OverrideSignatures_PartiallyTypedIdentifier_Async(string filename)
+ {
+ const string source = @"
+class C
+{
+ override Ge$$
+}";
+
+ using var host = GetAsyncCompletionAndImportCompletionHost();
+ var completions = await FindCompletionsAsync(filename, source, host);
+ Assert.Equal(new[] { "Equals(object obj)", "GetHashCode()", "ToString()" },
+ completions.Items.Select(c => c.Label));
+
+ Assert.Equal(new[] { "Equals", "GetHashCode", "ToString" },
+ completions.Items.Select(c => c.TextEdit.NewText));
+
+ Assert.All(completions.Items.Select(c => c.AdditionalTextEdits), edit => Assert.Null(edit));
+ Assert.All(completions.Items, c => Assert.Equal(InsertTextFormat.PlainText, c.InsertTextFormat));
+
+ var afterInsert = await AfterInsertResponse(completions.Items[0], host);
+ var change = afterInsert.Changes.Single();
+ Assert.Equal("public override bool Equals(object obj)\n {\n return base.Equals(obj);\n }", change.NewText);
+ Assert.Equal(3, change.StartLine);
+ Assert.Equal(4, change.StartColumn);
+ Assert.Equal(3, change.EndLine);
+ Assert.Equal(19, change.EndColumn);
+ Assert.Equal(5, afterInsert.Line);
+ Assert.Equal(32, afterInsert.Column);
+ }
+
+ [Theory]
+ [InlineData("dummy.cs")]
+ [InlineData("dummy.csx")]
+ public async Task OverrideSignatures_PartiallyTypedIdentifier_Sync_Sync(string filename)
{
const string source = @"
class C
@@ -1398,6 +1923,38 @@ public async Task InternalsVisibleToCompletionSkipsMiscProject()
Assert.Equal("AssemblyNameVal", completions.Items[0].TextEdit.NewText);
}
+ [Theory]
+ [InlineData("dummy.cs")]
+ [InlineData("dummy.csx")]
+ public async Task PrefixHeaderIsFullyCorrect_Async(string filename)
+ {
+ const string input =
+@"public class Base
+{
+ protected virtual void OnEnable() {}
+}
+public class Derived : Base
+{
+ protected override void On$$
+}";
+
+ using var host = GetAsyncCompletionAndImportCompletionHost();
+ var completions = await FindCompletionsAsync(filename, input, host);
+ var onEnable = completions.Items.Single(c => c.TextEdit.NewText.Contains("OnEnable"));
+
+ Assert.Equal("OnEnable", onEnable.TextEdit.NewText);
+
+ var afterInsert = await AfterInsertResponse(onEnable, host);
+ var change = afterInsert.Changes.Single();
+ Assert.Equal("()\n {\n base.OnEnable();\n }", change.NewText);
+ Assert.Equal(6, change.StartLine);
+ Assert.Equal(36, change.StartColumn);
+ Assert.Equal(6, change.EndLine);
+ Assert.Equal(36, change.EndColumn);
+ Assert.Equal(8, afterInsert.Line);
+ Assert.Equal(24, afterInsert.Column);
+ }
+
[Theory]
[InlineData("dummy.cs")]
[InlineData("dummy.csx")]
@@ -1423,7 +1980,40 @@ protected override void On$$
[Theory]
[InlineData("dummy.cs")]
[InlineData("dummy.csx")]
- public async Task PrefixHeaderIsPartiallyCorrect_1(string filename)
+ public async Task PrefixHeaderIsPartiallyCorrect_1_Async(string filename)
+ {
+ const string input =
+@"public class Base
+{
+ protected virtual void OnEnable() {}
+}
+public class Derived : Base
+{
+ protected override void ON$$
+}";
+
+ using var host = GetAsyncCompletionAndImportCompletionHost();
+ var completions = await FindCompletionsAsync(filename, input, host);
+ var onEnable = completions.Items.Single(c => c.TextEdit.NewText.Contains("OnEnable"));
+ Assert.Equal(onEnable.TextEdit.StartLine, onEnable.TextEdit.EndLine);
+ Assert.Equal(2, onEnable.TextEdit.EndColumn - onEnable.TextEdit.StartColumn);
+ Assert.Equal("OnEnable", onEnable.TextEdit.NewText);
+
+ var afterInsert = await AfterInsertResponse(onEnable, host);
+ var change = afterInsert.Changes.Single();
+ Assert.Equal("()\n {\n base.OnEnable();\n }", change.NewText);
+ Assert.Equal(6, change.StartLine);
+ Assert.Equal(36, change.StartColumn);
+ Assert.Equal(6, change.EndLine);
+ Assert.Equal(36, change.EndColumn);
+ Assert.Equal(8, afterInsert.Line);
+ Assert.Equal(24, afterInsert.Column);
+ }
+
+ [Theory]
+ [InlineData("dummy.cs")]
+ [InlineData("dummy.csx")]
+ public async Task PrefixHeaderIsPartiallyCorrect_1_Sync(string filename)
{
const string input =
@"public class Base
@@ -1445,7 +2035,40 @@ protected override void ON$$
[Theory]
[InlineData("dummy.cs")]
[InlineData("dummy.csx")]
- public async Task PrefixHeaderIsPartiallyCorrect_2(string filename)
+ public async Task PrefixHeaderIsPartiallyCorrect_2_Async(string filename)
+ {
+ const string input =
+@"public class Base
+{
+ protected virtual void OnEnable() {}
+}
+public class Derived : Base
+{
+ protected override void on$$
+}";
+
+ using var host = GetAsyncCompletionAndImportCompletionHost();
+ var completions = await FindCompletionsAsync(filename, input, host);
+ var onEnable = completions.Items.Single(c => c.TextEdit.NewText.Contains("OnEnable"));
+ Assert.Equal(onEnable.TextEdit.StartLine, onEnable.TextEdit.EndLine);
+ Assert.Equal(2, onEnable.TextEdit.EndColumn - onEnable.TextEdit.StartColumn);
+ Assert.Equal("OnEnable", onEnable.TextEdit.NewText);
+
+ var afterInsert = await AfterInsertResponse(onEnable, host);
+ var change = afterInsert.Changes.Single();
+ Assert.Equal("()\n {\n base.OnEnable();\n }", change.NewText);
+ Assert.Equal(6, change.StartLine);
+ Assert.Equal(36, change.StartColumn);
+ Assert.Equal(6, change.EndLine);
+ Assert.Equal(36, change.EndColumn);
+ Assert.Equal(8, afterInsert.Line);
+ Assert.Equal(24, afterInsert.Column);
+ }
+
+ [Theory]
+ [InlineData("dummy.cs")]
+ [InlineData("dummy.csx")]
+ public async Task PrefixHeaderIsPartiallyCorrect_2_Sync(string filename)
{
const string input =
@"public class Base
@@ -1571,6 +2194,24 @@ private OmniSharpTestHost GetImportCompletionHost()
return testHost;
}
+ private OmniSharpTestHost GetAsyncCompletionAndImportCompletionHost()
+ {
+ var testHost = CreateOmniSharpHost(configurationData: new[] {
+ new KeyValuePair("RoslynExtensionsOptions:EnableImportCompletion", "true"),
+ new KeyValuePair("RoslynExtensionsOptions:EnableAsyncCompletion", "true"),
+ });
+ testHost.AddFilesToWorkspace();
+ return testHost;
+ }
+
+ private Task AfterInsertResponse(CompletionItem completionItem, OmniSharpTestHost testHost)
+ {
+ return GetCompletionService(testHost).Handle(new CompletionAfterInsertRequest
+ {
+ Item = completionItem
+ });
+ }
+
private static string NormalizeNewlines(string str)
=> str.Replace("\r\n", Environment.NewLine);