diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionOptionsProvider.cs b/src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionOptionsProvider.cs new file mode 100644 index 0000000000..704a661b9d --- /dev/null +++ b/src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionOptionsProvider.cs @@ -0,0 +1,23 @@ +#nullable enable + +using System.Composition; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Options; +using OmniSharp.Options; +using OmniSharp.Roslyn.CSharp.Services.Intellisense; +using OmniSharp.Roslyn.Options; + +namespace OmniSharp.Roslyn.CSharp.Services.Completion +{ + [Export(typeof(IWorkspaceOptionsProvider)), Shared] + public class CompletionOptionsProvider : IWorkspaceOptionsProvider + { + public int Order => 0; + + public OptionSet Process(OptionSet currentOptionSet, OmniSharpOptions omniSharpOptions, IOmniSharpEnvironment omnisharpEnvironment) + => currentOptionSet.WithChangedOption( + option: CompletionItemExtensions.ShowItemsFromUnimportedNamespaces, + language: LanguageNames.CSharp, + value: omniSharpOptions.RoslynExtensionsOptions.EnableImportCompletion); + } +} diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionService.cs index b1c481f901..37f2a3de2e 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionService.cs @@ -10,6 +10,7 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Completion; using Microsoft.CodeAnalysis.Tags; +using Microsoft.CodeAnalysis.Text; using Microsoft.Extensions.Logging; using OmniSharp.Extensions; using OmniSharp.Mef; @@ -76,7 +77,7 @@ public class CompletionService : private readonly ILogger _logger; private readonly object _lock = new object(); - private (CSharpCompletionList Completions, string FileName)? _lastCompletion = null; + private (CSharpCompletionList Completions, string FileName, int position)? _lastCompletion = null; [ImportingConstructor] public CompletionService(OmniSharpWorkspace workspace, FormattingOptions formattingOptions, ILoggerFactory loggerFactory) @@ -114,7 +115,10 @@ public async Task Handle(CompletionRequest request) return new CompletionResponse { Items = ImmutableArray.Empty }; } - var completions = await completionService.GetCompletionsAsync(document, position, getCompletionTrigger(includeTriggerCharacter: false)); + var (completions, expandedItemsAvailable) = await completionService.GetCompletionsInternalAsync( + document, + position, + getCompletionTrigger(includeTriggerCharacter: false)); _logger.LogTrace("Found {0} completions for {1}:{2},{3}", completions?.Items.IsDefaultOrEmpty != true ? 0 : completions.Items.Length, request.FileName, @@ -142,12 +146,19 @@ public async Task Handle(CompletionRequest request) lock (_lock) { - _lastCompletion = (completions, request.FileName); + _lastCompletion = (completions, request.FileName, position); } var triggerCharactersBuilder = ImmutableArray.CreateBuilder(completions.Rules.DefaultCommitCharacters.Length); var completionsBuilder = ImmutableArray.CreateBuilder(completions.Items.Length); + // If we don't encounter any unimported types, and the completion context thinks that some would be available, then + // that completion provider is still creating the cache. We'll mark this completion list as not completed, and the + // editor will ask again when the user types more. By then, hopefully the cache will have populated and we can mark + // the completion as done. + bool isIncomplete = expandedItemsAvailable && + _workspace.Options.GetOption(CompletionItemExtensions.ShowItemsFromUnimportedNamespaces, LanguageNames.CSharp) == true; + for (int i = 0; i < completions.Items.Length; i++) { var completion = completions.Items[i]; @@ -226,38 +237,23 @@ public async Task Handle(CompletionRequest request) break; } - // We know the span starts before the text we're keying off of. So, break that - // out into a separate edit. We need to cut out the space before the current word, - // as the additional edit is not allowed to overlap with the insertion point. - var additionalEditStartPosition = sourceText.Lines.GetLinePosition(change.TextChange.Span.Start); - var additionalEditEndPosition = sourceText.Lines.GetLinePosition(typedSpan.Start - 1); - int additionalEditEndOffset = change.TextChange.NewText!.IndexOf(completion.DisplayText); - if (additionalEditEndOffset < 1) - { - // The first index of this was either 0 and the edit span was wrong, - // or it wasn't found at all. In this case, just do the best we can: - // send the whole string wtih no additional edits and log a warning. - _logger.LogWarning("Could not find the first index of the display text.\nDisplay text: {0}.\nCompletion Text: {1}", - completion.DisplayText, change.TextChange.NewText); - (insertText, insertTextFormat) = getAdjustedInsertTextWithPosition(change, position, newOffset: 0); - break; - } - - additionalTextEdits = ImmutableArray.Create(new LinePositionSpanTextChange - { - // Again, we cut off the space at the end of the offset - NewText = change.TextChange.NewText!.Substring(0, additionalEditEndOffset - 1), - StartLine = additionalEditStartPosition.Line, - StartColumn = additionalEditStartPosition.Character, - EndLine = additionalEditEndPosition.Line, - EndColumn = additionalEditEndPosition.Character, - }); + int additionalEditEndOffset; + (additionalTextEdits, additionalEditEndOffset) = GetAdditionalTextEdits(change, sourceText, typedSpan, completion.DisplayText, isImportCompletion: false); // Now that we have the additional edit, adjust the rest of the new text (insertText, insertTextFormat) = getAdjustedInsertTextWithPosition(change, position, additionalEditEndOffset); } break; + case CompletionItemExtensions.TypeImportCompletionProvider: + case CompletionItemExtensions.ExtensionMethodImportCompletionProvider: + // We did indeed find unimported types, the completion list can be considered complete. + // This is technically slightly incorrect: extension method completion can provide + // partial results. However, this should only affect the first completion session or + // two and isn't a big problem in practice. + isIncomplete = false; + goto default; + default: insertText = completion.DisplayText; break; @@ -282,7 +278,7 @@ public async Task Handle(CompletionRequest request) return new CompletionResponse { - IsIncomplete = false, + IsIncomplete = isIncomplete, Items = completionsBuilder.MoveToImmutable() }; @@ -399,7 +395,7 @@ public async Task Handle(CompletionResolveRequest req return new CompletionResolveResponse { Item = request.Item }; } - var (completions, fileName) = _lastCompletion.Value; + var (completions, fileName, position) = _lastCompletion.Value; if (request.Item is null || request.Item.Data >= completions.Items.Length @@ -433,12 +429,55 @@ public async Task Handle(CompletionResolveRequest req request.Item.Documentation = textBuilder.ToString(); - // TODO: Do import completion diffing here + switch (lastCompletionItem.GetProviderName()) + { + case CompletionItemExtensions.ExtensionMethodImportCompletionProvider: + case CompletionItemExtensions.TypeImportCompletionProvider: + var sourceText = await document.GetTextAsync(); + var typedSpan = completionService.GetDefaultCompletionListSpan(sourceText, position); + var change = await completionService.GetChangeAsync(document, lastCompletionItem, typedSpan); + (request.Item.AdditionalTextEdits, _) = GetAdditionalTextEdits(change, sourceText, typedSpan, lastCompletionItem.DisplayText, isImportCompletion: true); + break; + } return new CompletionResolveResponse { Item = request.Item }; } + + private (ImmutableArray edits, int endOffset) GetAdditionalTextEdits(CompletionChange change, SourceText sourceText, TextSpan typedSpan, string completionDisplayText, bool isImportCompletion) + { + // We know the span starts before the text we're keying off of. So, break that + // out into a separate edit. We need to cut out the space before the current word, + // as the additional edit is not allowed to overlap with the insertion point. + var additionalEditStartPosition = sourceText.Lines.GetLinePosition(change.TextChange.Span.Start); + var additionalEditEndPosition = sourceText.Lines.GetLinePosition(typedSpan.Start - 1); + int additionalEditEndOffset = isImportCompletion + // Import completion will put the displaytext at the end of the line, override completion will + // put it at the front. + ? change.TextChange.NewText!.LastIndexOf(completionDisplayText) + : change.TextChange.NewText!.IndexOf(completionDisplayText); + + if (additionalEditEndOffset < 1) + { + // The first index of this was either 0 and the edit span was wrong, + // or it wasn't found at all. In this case, just do the best we can: + // send the whole string wtih no additional edits and log a warning. + _logger.LogWarning("Could not find the first index of the display text.\nDisplay text: {0}.\nCompletion Text: {1}", + completionDisplayText, change.TextChange.NewText); + return default; + } + + return (ImmutableArray.Create(new LinePositionSpanTextChange + { + // Again, we cut off the space at the end of the offset + NewText = change.TextChange.NewText!.Substring(0, additionalEditEndOffset - 1), + StartLine = additionalEditStartPosition.Line, + StartColumn = additionalEditStartPosition.Character, + EndLine = additionalEditEndPosition.Line, + EndColumn = additionalEditEndPosition.Character, + }), additionalEditEndOffset); + } } } diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Intellisense/CompletionItemExtensions.cs b/src/OmniSharp.Roslyn.CSharp/Services/Intellisense/CompletionItemExtensions.cs index f9d51e8876..5e936bb300 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Intellisense/CompletionItemExtensions.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Intellisense/CompletionItemExtensions.cs @@ -7,6 +7,8 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Completion; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.Text; using OmniSharp.Models.AutoComplete; using OmniSharp.Utilities; @@ -22,14 +24,19 @@ internal static class CompletionItemExtensions internal const string PartialMethodCompletionProvider = "Microsoft.CodeAnalysis.CSharp.Completion.Providers.PartialMethodCompletionProvider"; internal const string InternalsVisibleToCompletionProvider = "Microsoft.CodeAnalysis.CSharp.Completion.Providers.InternalsVisibleToCompletionProvider"; internal const string XmlDocCommentCompletionProvider = "Microsoft.CodeAnalysis.CSharp.Completion.Providers.XmlDocCommentCompletionProvider"; + internal const string TypeImportCompletionProvider = "Microsoft.CodeAnalysis.CSharp.Completion.Providers.TypeImportCompletionProvider"; + internal const string ExtensionMethodImportCompletionProvider = "Microsoft.CodeAnalysis.CSharp.Completion.Providers.ExtensionMethodImportCompletionProvider"; private const string ProviderName = nameof(ProviderName); private const string SymbolCompletionItem = "Microsoft.CodeAnalysis.Completion.Providers.SymbolCompletionItem"; private const string SymbolKind = nameof(SymbolKind); private const string SymbolName = nameof(SymbolName); private const string Symbols = nameof(Symbols); private static readonly Type _symbolCompletionItemType; - private static MethodInfo _getSymbolsAsync; + private static readonly MethodInfo _getSymbolsAsync; private static readonly PropertyInfo _getProviderName; + private static readonly MethodInfo _getCompletionsInternalAsync; + private static readonly MethodInfo _getChangeAsync; + internal static readonly PerLanguageOption ShowItemsFromUnimportedNamespaces = new PerLanguageOption("CompletionOptions", "ShowItemsFromUnimportedNamespaces", defaultValue: null); static CompletionItemExtensions() { @@ -37,6 +44,9 @@ static CompletionItemExtensions() _getSymbolsAsync = _symbolCompletionItemType.GetMethod(GetSymbolsAsync, BindingFlags.Public | BindingFlags.Static); _getProviderName = typeof(CompletionItem).GetProperty(ProviderName, BindingFlags.NonPublic | BindingFlags.Instance); + + _getCompletionsInternalAsync = typeof(CompletionService).GetMethod(nameof(GetCompletionsInternalAsync), BindingFlags.NonPublic | BindingFlags.Instance); + _getChangeAsync = typeof(CompletionService).GetMethod(nameof(GetChangeAsync), BindingFlags.NonPublic | BindingFlags.Instance); } internal static string GetProviderName(this CompletionItem item) @@ -48,6 +58,24 @@ public static bool IsObjectCreationCompletionItem(this CompletionItem item) { return GetProviderName(item) == ObjectCreationCompletionProvider; } + public static Task<(CompletionList completionList, bool expandItemsAvailable)> GetCompletionsInternalAsync( + this CompletionService completionService, + Document document, + int caretPosition, + CompletionTrigger trigger = default, + ImmutableHashSet roles = null, + OptionSet options = null, + CancellationToken cancellationToken = default) + => (Task<(CompletionList completionList, bool expandItemsAvailable)>)_getCompletionsInternalAsync.Invoke(completionService, new object[] { document, caretPosition, trigger, roles, options, cancellationToken }); + + internal static Task GetChangeAsync( + this CompletionService completionService, + Document document, + CompletionItem item, + TextSpan completionListSpan, + char? commitCharacter = null, + CancellationToken cancellationToken = default) + => (Task)_getChangeAsync.Invoke(completionService, new object[] { document, item, completionListSpan, commitCharacter, cancellationToken }); public static async Task> GetCompletionSymbolsAsync(this CompletionItem completionItem, IEnumerable recommendedSymbols, Document document) { diff --git a/src/OmniSharp.Shared/Options/RoslynExtensionsOptions.cs b/src/OmniSharp.Shared/Options/RoslynExtensionsOptions.cs index 2c827c001a..56ca6bbb91 100644 --- a/src/OmniSharp.Shared/Options/RoslynExtensionsOptions.cs +++ b/src/OmniSharp.Shared/Options/RoslynExtensionsOptions.cs @@ -9,6 +9,7 @@ public class RoslynExtensionsOptions : OmniSharpExtensionsOptions { public bool EnableDecompilationSupport { get; set; } public bool EnableAnalyzersSupport { get; set; } + public bool EnableImportCompletion { get; set; } public int DocumentAnalysisTimeoutMs { get; set; } = 10 * 1000; } diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/CompletionFacts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/CompletionFacts.cs index fc60da18ec..5f029867bb 100644 --- a/tests/OmniSharp.Roslyn.CSharp.Tests/CompletionFacts.cs +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/CompletionFacts.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.Extensions.Logging; @@ -13,6 +15,7 @@ namespace OmniSharp.Roslyn.CSharp.Tests { public class CompletionFacts : AbstractTestFixture { + private const int ImportCompletionTimeout = 1000; private readonly ILogger _logger; private string EndpointName => OmniSharpEndpoints.Completion; @@ -37,7 +40,7 @@ public Class1() } }"; - var completions = await FindCompletionsAsync(filename, input); + var completions = await FindCompletionsAsync(filename, input, SharedOmniSharpTestHost); Assert.Contains("Foo", completions.Items.Select(c => c.Label)); Assert.Contains("Foo", completions.Items.Select(c => c.InsertText)); } @@ -56,7 +59,7 @@ public Class1() } }"; - var completions = await FindCompletionsAsync(filename, input); + var completions = await FindCompletionsAsync(filename, input, SharedOmniSharpTestHost); Assert.Contains("foo", completions.Items.Select(c => c.Label)); Assert.Contains("foo", completions.Items.Select(c => c.InsertText)); } @@ -78,11 +81,11 @@ public void Foo(int bar = 1) } }"; - var completions = await FindCompletionsAsync(filename, input); + var completions = await FindCompletionsAsync(filename, input, SharedOmniSharpTestHost); Assert.All(completions.Items, c => Assert.Null(c.Documentation)); var fooCompletion = completions.Items.Single(c => c.Label == "Foo"); - var resolvedCompletion = await ResolveCompletionAsync(fooCompletion); + var resolvedCompletion = await ResolveCompletionAsync(fooCompletion, SharedOmniSharpTestHost); Assert.Equal("```csharp\nvoid Class1.Foo([int bar = 1])\n```\n\nSome Text", resolvedCompletion.Item.Documentation); } @@ -99,10 +102,241 @@ public Class1() } }"; - var completions = await FindCompletionsAsync(filename, input); + var completions = await FindCompletionsAsync(filename, input, SharedOmniSharpTestHost); Assert.Contains("TryParse", completions.Items.Select(c => c.InsertText)); } + [Theory] + [InlineData("dummy.cs")] + [InlineData("dummy.csx")] + public async Task ImportCompletionTurnedOff(string filename) + { + const string input = +@"public class Class1 { + public Class1() + { + Gui$$ + } +}"; + + var completions = await FindCompletionsAsync(filename, input, SharedOmniSharpTestHost); + Assert.False(completions.IsIncomplete); + Assert.DoesNotContain("Guid", completions.Items.Select(c => c.InsertText)); + } + + [Theory] + [InlineData("dummy.cs")] + [InlineData("dummy.csx")] + public async Task ImportCompletionResolvesOnSubsequentQueries(string filename) + { + const string input = +@"public class Class1 { + public Class1() + { + Gui$$ + } +}"; + + using var host = 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.InsertText)); + + // Populating the completion cache should take no more than a few ms, don't let it take too + // long + CancellationTokenSource cts = new CancellationTokenSource(millisecondsDelay: ImportCompletionTimeout); + await Task.Run(async () => + { + while (completions.IsIncomplete) + { + completions = await FindCompletionsAsync(filename, input, host); + cts.Token.ThrowIfCancellationRequested(); + } + }, cts.Token); + + Assert.False(completions.IsIncomplete); + Assert.Contains("Guid", completions.Items.Select(c => c.InsertText)); + } + + [Theory] + [InlineData("dummy.cs")] + [InlineData("dummy.csx")] + public async Task ImportCompletion_LocalsPrioritizedOverImports(string filename) + { + + const string input = +@"public class Class1 { + public Class1() + { + string guid; + Gui$$ + } +}"; + + using var host = GetImportCompletionHost(); + var completions = await FindCompletionsWithImportedAsync(filename, input, host); + Assert.True(completions.Items.First(c => c.InsertText == "guid").Data < completions.Items.First(c => c.InsertText == "Guid").Data); + } + + [Theory] + [InlineData("dummy.cs")] + [InlineData("dummy.csx")] + public async Task ImportCompletions_IncludesExtensionMethods(string filename) + { + const string input = +@"namespace N1 +{ + public class C1 + { + public void M(object o) + { + o.$$ + } + } +} +namespace N2 +{ + public static class ObjectExtensions + { + public static void Test(this object o) + { + } + } +}"; + + using var host = GetImportCompletionHost(); + var completions = await FindCompletionsWithImportedAsync(filename, input, host); + Assert.Contains("Test", completions.Items.Select(c => c.InsertText)); + } + + [Theory] + [InlineData("dummy.cs")] + [InlineData("dummy.csx")] + public async Task ImportCompletion_ResolveAddsImportEdit(string filename) + { + const string input = +@"namespace N1 +{ + public class C1 + { + public void M(object o) + { + o.$$ + } + } +} +namespace N2 +{ + public static class ObjectExtensions + { + public static void Test(this object o) + { + } + } +}"; + + using var host = GetImportCompletionHost(); + var completions = await FindCompletionsWithImportedAsync(filename, input, host); + var resolved = await ResolveCompletionAsync(completions.Items.First(c => c.InsertText == "Test"), host); + + Assert.Single(resolved.Item.AdditionalTextEdits.Value); + var additionalEdit = resolved.Item.AdditionalTextEdits.Value[0]; + Assert.Equal(NormalizeNewlines("using N2;\n\nnamespace N1\r\n{\r\n public class C1\r\n {\r\n public void M(object o)\r\n {\r\n o"), + additionalEdit.NewText); + Assert.Equal(0, additionalEdit.StartLine); + Assert.Equal(0, additionalEdit.StartColumn); + Assert.Equal(6, additionalEdit.EndLine); + Assert.Equal(13, additionalEdit.EndColumn); + } + + [Theory] + [InlineData("dummy.cs")] + [InlineData("dummy.csx")] + public async Task SelectsLastInstanceOfCompletion(string filename) + { + const string input = +@"namespace N1 +{ + public class C1 + { + public void M(object o) + { + /*Guid*/$$//Guid + } + } +} +namespace N2 +{ + public static class ObjectExtensions + { + public static void Test(this object o) + { + } + } +}"; + + using var host = GetImportCompletionHost(); + var completions = await FindCompletionsWithImportedAsync(filename, input, host); + var resolved = await ResolveCompletionAsync(completions.Items.First(c => c.InsertText == "Guid"), host); + + Assert.Single(resolved.Item.AdditionalTextEdits.Value); + var additionalEdit = resolved.Item.AdditionalTextEdits.Value[0]; + Assert.Equal(NormalizeNewlines("using System;\n\nnamespace N1\r\n{\r\n public class C1\r\n {\r\n public void M(object o)\r\n {\r\n /*Guid*"), + additionalEdit.NewText); + Assert.Equal(0, additionalEdit.StartLine); + Assert.Equal(0, additionalEdit.StartColumn); + Assert.Equal(6, additionalEdit.EndLine); + Assert.Equal(19, additionalEdit.EndColumn); + } + + [Theory] + [InlineData("dummy.cs")] + [InlineData("dummy.csx")] + public async Task UsingsAddedInOrder(string filename) + { + + const string input = +@"using N1; +using N3; +namespace N1 +{ + public class C1 + { + public void M(object o) + { + $$ + } + } +} +namespace N2 +{ + public class C2 + { + } +} +namespace N3 +{ + public class C3 + { + } +}"; + + using var host = GetImportCompletionHost(); + var completions = await FindCompletionsWithImportedAsync(filename, input, host); + var resolved = await ResolveCompletionAsync(completions.Items.First(c => c.InsertText == "C2"), host); + + Assert.Single(resolved.Item.AdditionalTextEdits.Value); + var additionalEdit = resolved.Item.AdditionalTextEdits.Value[0]; + Assert.Equal(NormalizeNewlines("N2;\nusing N3;\r\nnamespace N1\r\n{\r\n public class C1\r\n {\r\n public void M(object o)\r\n {\r\n "), + additionalEdit.NewText); + Assert.Equal(1, additionalEdit.StartLine); + Assert.Equal(6, additionalEdit.StartColumn); + Assert.Equal(8, additionalEdit.EndLine); + Assert.Equal(11, additionalEdit.EndColumn); + } + [Theory] [InlineData("dummy.cs")] [InlineData("dummy.csx")] @@ -116,7 +350,7 @@ public Class1() } }"; - var completions = await FindCompletionsAsync(filename, input); + var completions = await FindCompletionsAsync(filename, input, SharedOmniSharpTestHost); Assert.Contains("NewGuid", completions.Items.Select(c => c.Label)); } @@ -133,7 +367,7 @@ public Class1() } }"; - var completions = await FindCompletionsAsync(filename, input); + var completions = await FindCompletionsAsync(filename, input, SharedOmniSharpTestHost); Assert.Contains("NewGuid", completions.Items.Select(c => c.Label)); } @@ -150,11 +384,11 @@ public Class1() } }"; - var completions = await FindCompletionsAsync(filename, input); + var completions = await FindCompletionsAsync(filename, input, SharedOmniSharpTestHost); Assert.All(completions.Items, c => Assert.Null(c.Documentation)); var fooCompletion = completions.Items.Single(c => c.Label == "NewGuid"); - var resolvedCompletion = await ResolveCompletionAsync(fooCompletion); + var resolvedCompletion = await ResolveCompletionAsync(fooCompletion, SharedOmniSharpTestHost); Assert.Equal("```csharp\nSystem.Guid System.Guid.NewGuid()\n```", resolvedCompletion.Item.Documentation); } @@ -173,7 +407,7 @@ public MyClass1() } }"; - var completions = await FindCompletionsAsync(filename, input); + var completions = await FindCompletionsAsync(filename, input, SharedOmniSharpTestHost); Assert.Contains(completions.Items, c => c.Label == "myvar"); Assert.Contains(completions.Items, c => c.Label == "MyClass1"); Assert.All(completions.Items, c => @@ -205,7 +439,7 @@ public MyClass1() } }"; - var completions = await FindCompletionsAsync(filename, input); + var completions = await FindCompletionsAsync(filename, input, SharedOmniSharpTestHost); Assert.Contains(completions.Items, c => c.Label == "myvar"); Assert.Contains(completions.Items, c => c.Label == "MyClass1"); Assert.All(completions.Items, c => @@ -236,7 +470,7 @@ var x$$ } }"; - var completions = await FindCompletionsAsync(filename, source); + var completions = await FindCompletionsAsync(filename, source, SharedOmniSharpTestHost); Assert.Empty(completions.Items); } @@ -253,7 +487,7 @@ public class BarAttribute : Attribute {} [B$$ public class Foo {}"; - var completions = await FindCompletionsAsync(filename, source); + var completions = await FindCompletionsAsync(filename, source, SharedOmniSharpTestHost); Assert.Contains(completions.Items, c => c.Label == "Bar"); Assert.Contains(completions.Items, c => c.InsertText == "Bar"); Assert.All(completions.Items, c => @@ -290,7 +524,7 @@ public MyClass2() } "; - var completions = await FindCompletionsAsync(filename, source); + var completions = await FindCompletionsAsync(filename, source, SharedOmniSharpTestHost); Assert.Single(completions.Items); Assert.Equal("Foo", completions.Items[0].Label); Assert.Equal("Foo", completions.Items[0].InsertText); @@ -316,7 +550,7 @@ public MyClass2() } "; - var completions = await FindCompletionsAsync(filename, source); + var completions = await FindCompletionsAsync(filename, source, SharedOmniSharpTestHost); var item = completions.Items.First(c => c.Label == "text:"); Assert.NotNull(item); Assert.Equal("text", item.InsertText); @@ -347,7 +581,7 @@ MyClass m$$ } "; - var completions = await FindCompletionsAsync(filename, source); + var completions = await FindCompletionsAsync(filename, source, SharedOmniSharpTestHost); Assert.Equal(new[] { "myClass", "my", "@class", "MyClass", "My", "Class", "GetMyClass", "GetMy", "GetClass" }, completions.Items.Select(c => c.Label)); } @@ -364,13 +598,13 @@ public virtual void Test(string text) {} public virtual void Test(string text, string moreText) {} } -class FooChild : Foo +class FooChild : Foo { override $$ } "; - var completions = await FindCompletionsAsync(filename, source); + var completions = await FindCompletionsAsync(filename, source, SharedOmniSharpTestHost); 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(object obj)\n {\n return base.Equals(obj);$0\n \\}", @@ -424,7 +658,7 @@ class CN3 : IN2 } }"; - var completions = await FindCompletionsAsync(filename, source); + var completions = await FindCompletionsAsync(filename, source, SharedOmniSharpTestHost); Assert.Equal(new[] { "Equals(object obj)", "GetHashCode()", "GetN1()", "ToString()" }, completions.Items.Select(c => c.Label)); @@ -464,7 +698,7 @@ class C public override $$ }"; - var completions = await FindCompletionsAsync(filename, source); + var completions = await FindCompletionsAsync(filename, source, SharedOmniSharpTestHost); Assert.Equal(new[] { "Equals(object obj)", "GetHashCode()", "ToString()" }, completions.Items.Select(c => c.Label)); @@ -489,7 +723,7 @@ class C public override bool $$ }"; - var completions = await FindCompletionsAsync(filename, source); + var completions = await FindCompletionsAsync(filename, source, SharedOmniSharpTestHost); Assert.Equal(new[] { "Equals(object obj)" }, completions.Items.Select(c => c.Label)); @@ -500,6 +734,51 @@ public override bool $$ Assert.All(completions.Items, c => Assert.Equal(InsertTextFormat.Snippet, c.InsertTextFormat)); } + [Theory] + [InlineData("dummy.cs")] + [InlineData("dummy.csx")] + public async Task OverrideSignatures_TestTest(string filename) + { + const string source = @" +class Test {} +abstract class Base +{ + protected abstract Test Test(); +} +class Derived : Base +{ + override $$ +}"; + + var completions = await FindCompletionsAsync(filename, source, SharedOmniSharpTestHost); + Assert.Equal(new[] { "Equals(object obj)", "GetHashCode()", "Test()", "ToString()" }, + completions.Items.Select(c => c.Label)); + + Assert.Equal(new[] { "Equals(object obj)\n {\n return base.Equals(obj);$0\n \\}", + "GetHashCode()\n {\n return base.GetHashCode();$0\n \\}", + "Test()\n {\n throw new System.NotImplementedException();$0\n \\}", + "ToString()\n {\n return base.ToString();$0\n \\}" + }, + completions.Items.Select(c => c.InsertText)); + + Assert.Equal(new[] { "public override bool", + "public override int", + "protected override Test", + "public override string"}, + completions.Items.Select(c => c.AdditionalTextEdits.Value.Single().NewText)); + + Assert.All(completions.Items.Select(c => c.AdditionalTextEdits.Value.Single()), + r => + { + Assert.Equal(8, r.StartLine); + Assert.Equal(4, r.StartColumn); + Assert.Equal(8, r.EndLine); + Assert.Equal(12, r.EndColumn); + }); + + Assert.All(completions.Items, c => Assert.Equal(InsertTextFormat.Snippet, c.InsertTextFormat)); + } + [Theory] [InlineData("dummy.cs")] [InlineData("dummy.csx")] @@ -516,7 +795,7 @@ partial class C } "; - var completions = await FindCompletionsAsync(filename, source); + var completions = await FindCompletionsAsync(filename, source, SharedOmniSharpTestHost); Assert.Equal(new[] { "M1(string param)" }, completions.Items.Select(c => c.Label)); @@ -538,7 +817,7 @@ class C override Ge$$ }"; - var completions = await FindCompletionsAsync(filename, source); + var completions = await FindCompletionsAsync(filename, source, SharedOmniSharpTestHost); Assert.Equal(new[] { "Equals(object obj)", "GetHashCode()", "ToString()" }, completions.Items.Select(c => c.Label)); @@ -591,7 +870,7 @@ public class MyClass1 { } "; - var completions = await FindCompletionsAsync(filename, source); + var completions = await FindCompletionsAsync(filename, source, SharedOmniSharpTestHost); Assert.Contains(completions.Items, c => c.Label == "MyClass1"); Assert.All(completions.Items, c => { @@ -620,7 +899,7 @@ public class MyClass1 { } "; - var completions = await FindCompletionsAsync(filename, source); + var completions = await FindCompletionsAsync(filename, source, SharedOmniSharpTestHost); Assert.Equal(new[] { "!--$0-->", "![CDATA[$0]]>", "c", @@ -641,7 +920,7 @@ public async Task HostObjectCompletionInScripts() const string source = "Prin$$"; - var completions = await FindCompletionsAsync("dummy.csx", source); + var completions = await FindCompletionsAsync("dummy.csx", source, SharedOmniSharpTestHost); Assert.Contains(completions.Items, c => c.Label == "Print"); Assert.Contains(completions.Items, c => c.Label == "PrintOptions"); } @@ -667,7 +946,7 @@ void M() } "; - var completions = await FindCompletionsAsync(filename, source); + var completions = await FindCompletionsAsync(filename, source, SharedOmniSharpTestHost); Assert.True(completions.Items.All(c => c.IsSuggestionMode())); } @@ -690,7 +969,7 @@ void M() } "; - var completions = await FindCompletionsAsync(filename, source); + var completions = await FindCompletionsAsync(filename, source, SharedOmniSharpTestHost); Assert.True(completions.Items.All(c => c.IsSuggestionMode())); } @@ -715,7 +994,7 @@ void M() } "; - var completions = await FindCompletionsAsync(filename, source); + var completions = await FindCompletionsAsync(filename, source, SharedOmniSharpTestHost); Assert.True(completions.Items.All(c => !c.IsSuggestionMode())); } @@ -738,7 +1017,7 @@ void M() } "; - var completions = await FindCompletionsAsync(filename, source); + var completions = await FindCompletionsAsync(filename, source, SharedOmniSharpTestHost); Assert.True(completions.Items.All(c => !c.IsSuggestionMode())); } @@ -754,7 +1033,7 @@ public async Task ScriptingIncludes7_1() tuple.n$$ "; - var completions = await FindCompletionsAsync("dummy.csx", source); + var completions = await FindCompletionsAsync("dummy.csx", source, SharedOmniSharpTestHost); Assert.Contains(completions.Items, c => c.Label == "number1"); Assert.Contains(completions.Items, c => c.Label == "number2"); Assert.All(completions.Items, c => @@ -787,7 +1066,7 @@ public Bar() } "; - var completions = await FindCompletionsAsync("dummy.csx", source); + var completions = await FindCompletionsAsync("dummy.csx", source, SharedOmniSharpTestHost); Assert.Contains(completions.Items, c => c.Label == "myValue"); Assert.All(completions.Items, c => { @@ -820,7 +1099,7 @@ public Point(int x, int y) { points[0].Po$$ "; - var completions = await FindCompletionsAsync("dummy.csx", source); + var completions = await FindCompletionsAsync("dummy.csx", source, SharedOmniSharpTestHost); Assert.Contains(completions.Items, c => c.Label == "PositionX"); Assert.Contains(completions.Items, c => c.Label == "PositionY"); Assert.All(completions.Items, c => @@ -851,7 +1130,7 @@ public M() } }"; - var completions = await FindCompletionsAsync(filename, input, triggerChar: ' '); + var completions = await FindCompletionsAsync(filename, input, SharedOmniSharpTestHost, triggerChar: ' '); Assert.NotEmpty(completions.Items); } @@ -868,7 +1147,7 @@ public M() } }"; - var completions = await FindCompletionsAsync(filename, input, triggerChar: ' '); + var completions = await FindCompletionsAsync(filename, input, SharedOmniSharpTestHost, triggerChar: ' '); Assert.NotEmpty(completions.Items.Where(completion => completion.Preselect == true)); } @@ -885,7 +1164,7 @@ public M() } }"; - var completions = await FindCompletionsAsync(filename, input, triggerChar: ' '); + var completions = await FindCompletionsAsync(filename, input, SharedOmniSharpTestHost, triggerChar: ' '); Assert.Empty(completions.Items); } @@ -906,7 +1185,7 @@ public async Task InternalsVisibleToCompletion() [assembly: System.Runtime.CompilerServices.InternalsVisibleTo(""$$"; - var completions = await FindCompletionsAsync("dummy.cs", input); + var completions = await FindCompletionsAsync("dummy.cs", input, SharedOmniSharpTestHost); Assert.Single(completions.Items); Assert.Equal("AssemblyNameVal", completions.Items[0].Label); Assert.Equal("AssemblyNameVal", completions.Items[0].InsertText); @@ -915,10 +1194,11 @@ public async Task InternalsVisibleToCompletion() private CompletionService GetCompletionService(OmniSharpTestHost host) => host.GetRequestHandler(EndpointName); - protected async Task FindCompletionsAsync(string filename, string source, char? triggerChar = null) + protected async Task FindCompletionsAsync(string filename, string source, OmniSharpTestHost testHost, char? triggerChar = null) { var testFile = new TestFile(filename, source); - SharedOmniSharpTestHost.AddFilesToWorkspace(testFile); + + testHost.AddFilesToWorkspace(testFile); var point = testFile.Content.GetPointFromPosition(); var request = new CompletionRequest @@ -931,13 +1211,47 @@ protected async Task FindCompletionsAsync(string filename, s TriggerCharacter = triggerChar }; - var requestHandler = GetCompletionService(SharedOmniSharpTestHost); + var requestHandler = GetCompletionService(testHost); return await requestHandler.Handle(request); } - protected async Task ResolveCompletionAsync(CompletionItem completionItem) - => await GetCompletionService(SharedOmniSharpTestHost).Handle(new CompletionResolveRequest { Item = completionItem }); + private async Task FindCompletionsWithImportedAsync(string filename, string source, OmniSharpTestHost host) + { + var completions = await FindCompletionsAsync(filename, source, host); + if (!completions.IsIncomplete) + { + return completions; + } + + // Populating the completion list should take no more than a few ms, don't let it take too + // long + CancellationTokenSource cts = new CancellationTokenSource(millisecondsDelay: ImportCompletionTimeout); + await Task.Run(async () => + { + while (completions.IsIncomplete) + { + completions = await FindCompletionsAsync(filename, source, host); + cts.Token.ThrowIfCancellationRequested(); + } + }, cts.Token); + + Assert.False(completions.IsIncomplete); + return completions; + } + + protected async Task ResolveCompletionAsync(CompletionItem completionItem, OmniSharpTestHost testHost) + => await GetCompletionService(testHost).Handle(new CompletionResolveRequest { Item = completionItem }); + + private OmniSharpTestHost GetImportCompletionHost() + { + var testHost = CreateOmniSharpHost(configurationData: new[] { new KeyValuePair("RoslynExtensionsOptions:EnableImportCompletion", "true") }); + testHost.AddFilesToWorkspace(); + return testHost; + } + + private static string NormalizeNewlines(string str) + => str.Replace("\r\n", Environment.NewLine); } internal static class CompletionResponseExtensions