From ebd2189dca7d4b6a642d140de201cb562d299592 Mon Sep 17 00:00:00 2001 From: Fredric Silberberg Date: Tue, 18 Aug 2020 00:08:54 -0700 Subject: [PATCH 1/4] Support completion of unimported types. This needs to call a few internal overloads for completion, instead of the public versions, in order to determine whether any completions can be expanded (ie, imported) and to get the full change for these items, including the import. The public APIs do not provide this information currently. --- .../Models/v1/Completion/CompletionItem.cs | 2 +- .../Services/Completion/CompletionService.cs | 111 ++++++--- .../Intellisense/CompletionItemExtensions.cs | 30 ++- .../CompletionFacts.cs | 227 ++++++++++++++++++ 4 files changed, 332 insertions(+), 38 deletions(-) diff --git a/src/OmniSharp.Abstractions/Models/v1/Completion/CompletionItem.cs b/src/OmniSharp.Abstractions/Models/v1/Completion/CompletionItem.cs index 141c3a2575..faad280a6c 100644 --- a/src/OmniSharp.Abstractions/Models/v1/Completion/CompletionItem.cs +++ b/src/OmniSharp.Abstractions/Models/v1/Completion/CompletionItem.cs @@ -85,7 +85,7 @@ public class CompletionItem /// /// Index in the completions list that this completion occurred. /// - public int Data { get; set; } + public (int Index, bool IsExpanded) Data { get; set; } public override string ToString() { diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionService.cs index 7d89f6c69e..d32b7e3ed5 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionService.cs @@ -9,7 +9,9 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Completion; +using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Tags; +using Microsoft.CodeAnalysis.Text; using Microsoft.Extensions.Logging; using OmniSharp.Extensions; using OmniSharp.Mef; @@ -76,7 +78,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 +116,14 @@ public async Task Handle(CompletionRequest request) return new CompletionResponse { Items = ImmutableArray.Empty }; } - var completions = await completionService.GetCompletionsAsync(document, position, getCompletionTrigger(includeTriggerCharacter: false)); + OptionSet completionsWithImport = await document.GetOptionsAsync(); + completionsWithImport = completionsWithImport.WithChangedOption(CompletionItemExtensions.ShowItemsFromUnimportedNamespaces, LanguageNames.CSharp, true); + + var (completions, expandedItemsAvailable) = await completionService.GetCompletionsInternalAsync( + document, + position, + getCompletionTrigger(includeTriggerCharacter: false), + options: completionsWithImport); _logger.LogTrace("Found {0} completions for {1}:{2},{3}", completions?.Items.IsDefaultOrEmpty != true ? 0 : completions.Items.Length, request.FileName, @@ -142,12 +151,18 @@ 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; + for (int i = 0; i < completions.Items.Length; i++) { var completion = completions.Items[i]; @@ -155,6 +170,7 @@ public async Task Handle(CompletionRequest request) var insertTextFormat = InsertTextFormat.PlainText; ImmutableArray? additionalTextEdits = null; + var isExpanded = false; if (!completion.TryGetInsertionText(out string insertText)) { @@ -226,38 +242,21 @@ 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 + isIncomplete = false; + isExpanded = true; + goto default; + default: insertText = completion.DisplayText; break; @@ -274,7 +273,7 @@ public async Task Handle(CompletionRequest request) FilterText = completion.FilterText, Kind = getCompletionItemKind(completion.Tags), Detail = completion.InlineDescription, - Data = i, + Data = (i, isExpanded), Preselect = completion.Rules.MatchPriority == MatchPriority.Preselect || filteredItems.Contains(completion.DisplayText), CommitCharacters = commitCharacters, }); @@ -282,7 +281,7 @@ public async Task Handle(CompletionRequest request) return new CompletionResponse { - IsIncomplete = false, + IsIncomplete = isIncomplete, Items = completionsBuilder.MoveToImmutable() }; @@ -399,17 +398,17 @@ 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 - || request.Item.Data < 0) + || request.Item.Data.Index >= completions.Items.Length + || request.Item.Data.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[request.Item.Data.Index]; if (lastCompletionItem.DisplayTextPrefix + lastCompletionItem.DisplayText + lastCompletionItem.DisplayTextSuffix != request.Item.Label) { _logger.LogError($"Inconsistent completion data. Requested data on {request.Item.Label}, but found completion item {lastCompletionItem.DisplayText}"); @@ -433,12 +432,52 @@ public async Task Handle(CompletionResolveRequest req request.Item.Documentation = textBuilder.ToString(); - // TODO: Do import completion diffing here + if (request.Item.Data.IsExpanded) + { + 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); + } 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/tests/OmniSharp.Roslyn.CSharp.Tests/CompletionFacts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/CompletionFacts.cs index fc60da18ec..b6143b9c0c 100644 --- a/tests/OmniSharp.Roslyn.CSharp.Tests/CompletionFacts.cs +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/CompletionFacts.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.Extensions.Logging; @@ -103,6 +104,163 @@ public Class1() Assert.Contains("TryParse", completions.Items.Select(c => c.InsertText)); } + [Theory] + [InlineData("dummy.cs")] + [InlineData("dummy.csx")] + public async Task ImportCompletion(string filename) + { + const string input = +@"public class Class1 { + public Class1() + { + Gui$$ + } +}"; + + // First completions should kick off the task. + var completions = await FindCompletionsAsync(filename, input); + Assert.True(completions.IsIncomplete); + Assert.DoesNotContain("Guid", completions.Items.Select(c => c.InsertText)); + + // Populating the completion list should take no more than a few ms, don't let it take too + // long + CancellationTokenSource cts = new CancellationTokenSource(100); + await Task.Run(async () => + { + while (completions.IsIncomplete) + { + completions = await FindCompletionsAsync(filename, input); + 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$$ + } +}"; + + var completions = await FindCompletionsWithImportedAsync(filename, input); + Assert.True(completions.Items.First(c => c.InsertText == "guid").Data.Index < completions.Items.First(c => c.InsertText == "Guid").Data.Index); + } + + [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) + { + } + } +}"; + + var completions = await FindCompletionsWithImportedAsync(filename, input); + 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) + { + } + } +}"; + + var completions = await FindCompletionsWithImportedAsync(filename, input); + var resolved = await ResolveCompletionAsync(completions.Items.First(c => c.InsertText == "Test")); + Assert.Single(resolved.Item.AdditionalTextEdits.Value); + var additionalEdit = resolved.Item.AdditionalTextEdits.Value[0]; + Assert.Equal("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) + { + } + } +}"; + + var completions = await FindCompletionsWithImportedAsync(filename, input); + var resolved = await ResolveCompletionAsync(completions.Items.First(c => c.InsertText == "Guid")); + Assert.Single(resolved.Item.AdditionalTextEdits.Value); + var additionalEdit = resolved.Item.AdditionalTextEdits.Value[0]; + Assert.Equal("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")] @@ -500,6 +658,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); + 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")] @@ -936,6 +1139,30 @@ protected async Task FindCompletionsAsync(string filename, s return await requestHandler.Handle(request); } + private async Task FindCompletionsWithImportedAsync(string filename, string source, char? triggerChar = null) + { + var completions = await FindCompletionsAsync(filename, source); + 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(100); + await Task.Run(async () => + { + while (completions.IsIncomplete) + { + completions = await FindCompletionsAsync(filename, source); + cts.Token.ThrowIfCancellationRequested(); + } + }, cts.Token); + + Assert.False(completions.IsIncomplete); + return completions; + } + protected async Task ResolveCompletionAsync(CompletionItem completionItem) => await GetCompletionService(SharedOmniSharpTestHost).Handle(new CompletionResolveRequest { Item = completionItem }); } From 033ffb286455c34baa4c5a27b8dbe3b891540647 Mon Sep 17 00:00:00 2001 From: Fredric Silberberg Date: Tue, 18 Aug 2020 20:58:38 -0700 Subject: [PATCH 2/4] PR Feedback: * Added global option for controlling whether import completion is turned on, off by default. * Additional testing. --- .../Models/v1/Completion/CompletionItem.cs | 2 +- .../Completion/CompletionOptionsProvider.cs | 23 ++ .../Services/Completion/CompletionService.cs | 38 ++-- .../Options/RoslynExtensionsOptions.cs | 1 + .../CompletionFacts.cs | 210 ++++++++++++------ 5 files changed, 193 insertions(+), 81 deletions(-) create mode 100644 src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionOptionsProvider.cs diff --git a/src/OmniSharp.Abstractions/Models/v1/Completion/CompletionItem.cs b/src/OmniSharp.Abstractions/Models/v1/Completion/CompletionItem.cs index faad280a6c..141c3a2575 100644 --- a/src/OmniSharp.Abstractions/Models/v1/Completion/CompletionItem.cs +++ b/src/OmniSharp.Abstractions/Models/v1/Completion/CompletionItem.cs @@ -85,7 +85,7 @@ public class CompletionItem /// /// Index in the completions list that this completion occurred. /// - public (int Index, bool IsExpanded) Data { get; set; } + public int Data { get; set; } public override string ToString() { 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 d32b7e3ed5..baa925f99e 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionService.cs @@ -116,14 +116,10 @@ public async Task Handle(CompletionRequest request) return new CompletionResponse { Items = ImmutableArray.Empty }; } - OptionSet completionsWithImport = await document.GetOptionsAsync(); - completionsWithImport = completionsWithImport.WithChangedOption(CompletionItemExtensions.ShowItemsFromUnimportedNamespaces, LanguageNames.CSharp, true); - var (completions, expandedItemsAvailable) = await completionService.GetCompletionsInternalAsync( document, position, - getCompletionTrigger(includeTriggerCharacter: false), - options: completionsWithImport); + getCompletionTrigger(includeTriggerCharacter: false)); _logger.LogTrace("Found {0} completions for {1}:{2},{3}", completions?.Items.IsDefaultOrEmpty != true ? 0 : completions.Items.Length, request.FileName, @@ -161,7 +157,8 @@ public async Task Handle(CompletionRequest request) // 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; + bool isIncomplete = expandedItemsAvailable && + _workspace.Options.GetOption(CompletionItemExtensions.ShowItemsFromUnimportedNamespaces, LanguageNames.CSharp) == true; for (int i = 0; i < completions.Items.Length; i++) { @@ -170,7 +167,6 @@ public async Task Handle(CompletionRequest request) var insertTextFormat = InsertTextFormat.PlainText; ImmutableArray? additionalTextEdits = null; - var isExpanded = false; if (!completion.TryGetInsertionText(out string insertText)) { @@ -252,9 +248,11 @@ public async Task Handle(CompletionRequest request) case CompletionItemExtensions.TypeImportCompletionProvider: case CompletionItemExtensions.ExtensionMethodImportCompletionProvider: - // We did indeed find unimported types, the completion list can be considered complete + // 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; - isExpanded = true; goto default; default: @@ -273,7 +271,7 @@ public async Task Handle(CompletionRequest request) FilterText = completion.FilterText, Kind = getCompletionItemKind(completion.Tags), Detail = completion.InlineDescription, - Data = (i, isExpanded), + Data = i, Preselect = completion.Rules.MatchPriority == MatchPriority.Preselect || filteredItems.Contains(completion.DisplayText), CommitCharacters = commitCharacters, }); @@ -401,14 +399,14 @@ public async Task Handle(CompletionResolveRequest req var (completions, fileName, position) = _lastCompletion.Value; if (request.Item is null - || request.Item.Data.Index >= completions.Items.Length - || request.Item.Data.Index < 0) + || request.Item.Data >= completions.Items.Length + || request.Item.Data < 0) { _logger.LogError("Received invalid completion resolve!"); return new CompletionResolveResponse { Item = request.Item }; } - var lastCompletionItem = completions.Items[request.Item.Data.Index]; + var lastCompletionItem = completions.Items[request.Item.Data]; if (lastCompletionItem.DisplayTextPrefix + lastCompletionItem.DisplayText + lastCompletionItem.DisplayTextSuffix != request.Item.Label) { _logger.LogError($"Inconsistent completion data. Requested data on {request.Item.Label}, but found completion item {lastCompletionItem.DisplayText}"); @@ -432,12 +430,16 @@ public async Task Handle(CompletionResolveRequest req request.Item.Documentation = textBuilder.ToString(); - if (request.Item.Data.IsExpanded) + switch (lastCompletionItem.GetProviderName()) { - 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); + + 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 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 b6143b9c0c..6bfd6519a7 100644 --- a/tests/OmniSharp.Roslyn.CSharp.Tests/CompletionFacts.cs +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/CompletionFacts.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -12,7 +13,7 @@ namespace OmniSharp.Roslyn.CSharp.Tests { - public class CompletionFacts : AbstractTestFixture + public class CompletionFacts : AbstractTestFixture { private readonly ILogger _logger; @@ -38,7 +39,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)); } @@ -57,7 +58,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)); } @@ -79,11 +80,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); } @@ -100,14 +101,14 @@ 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 ImportCompletion(string filename) + public async Task ImportCompletionTurnedOff(string filename) { const string input = @"public class Class1 { @@ -117,19 +118,39 @@ public Class1() } }"; - // First completions should kick off the task. - var completions = await FindCompletionsAsync(filename, input); + 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 list should take no more than a few ms, don't let it take too + // Populating the completion cache should take no more than a few ms, don't let it take too // long - CancellationTokenSource cts = new CancellationTokenSource(100); + CancellationTokenSource cts = new CancellationTokenSource(millisecondsDelay: 100); await Task.Run(async () => { while (completions.IsIncomplete) { - completions = await FindCompletionsAsync(filename, input); + completions = await FindCompletionsAsync(filename, input, host); cts.Token.ThrowIfCancellationRequested(); } }, cts.Token); @@ -153,8 +174,9 @@ public Class1() } }"; - var completions = await FindCompletionsWithImportedAsync(filename, input); - Assert.True(completions.Items.First(c => c.InsertText == "guid").Data.Index < completions.Items.First(c => c.InsertText == "Guid").Data.Index); + 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] @@ -183,7 +205,8 @@ public static void Test(this object o) } }"; - var completions = await FindCompletionsWithImportedAsync(filename, input); + using var host = GetImportCompletionHost(); + var completions = await FindCompletionsWithImportedAsync(filename, input, host); Assert.Contains("Test", completions.Items.Select(c => c.InsertText)); } @@ -213,11 +236,14 @@ public static void Test(this object o) } }"; - var completions = await FindCompletionsWithImportedAsync(filename, input); - var resolved = await ResolveCompletionAsync(completions.Items.First(c => c.InsertText == "Test")); + 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("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(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); @@ -250,17 +276,66 @@ public static void Test(this object o) } }"; - var completions = await FindCompletionsWithImportedAsync(filename, input); - var resolved = await ResolveCompletionAsync(completions.Items.First(c => c.InsertText == "Guid")); + 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("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(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")] @@ -274,7 +349,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)); } @@ -291,7 +366,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)); } @@ -308,11 +383,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); } @@ -331,7 +406,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 => @@ -363,7 +438,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 => @@ -394,7 +469,7 @@ var x$$ } }"; - var completions = await FindCompletionsAsync(filename, source); + var completions = await FindCompletionsAsync(filename, source, SharedOmniSharpTestHost); Assert.Empty(completions.Items); } @@ -411,7 +486,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 => @@ -448,7 +523,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); @@ -474,7 +549,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); @@ -505,7 +580,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)); } @@ -528,7 +603,7 @@ class FooChild : Foo } "; - 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 \\}", @@ -582,7 +657,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)); @@ -622,7 +697,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)); @@ -647,7 +722,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)); @@ -674,7 +749,7 @@ class Derived : Base override $$ }"; - var completions = await FindCompletionsAsync(filename, source); + var completions = await FindCompletionsAsync(filename, source, SharedOmniSharpTestHost); Assert.Equal(new[] { "Equals(object obj)", "GetHashCode()", "Test()", "ToString()" }, completions.Items.Select(c => c.Label)); @@ -719,7 +794,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)); @@ -741,7 +816,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)); @@ -794,7 +869,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 => { @@ -823,7 +898,7 @@ public class MyClass1 { } "; - var completions = await FindCompletionsAsync(filename, source); + var completions = await FindCompletionsAsync(filename, source, SharedOmniSharpTestHost); Assert.Equal(new[] { "!--$0-->", "![CDATA[$0]]>", "c", @@ -844,7 +919,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"); } @@ -870,7 +945,7 @@ void M() } "; - var completions = await FindCompletionsAsync(filename, source); + var completions = await FindCompletionsAsync(filename, source, SharedOmniSharpTestHost); Assert.True(completions.Items.All(c => c.IsSuggestionMode())); } @@ -893,7 +968,7 @@ void M() } "; - var completions = await FindCompletionsAsync(filename, source); + var completions = await FindCompletionsAsync(filename, source, SharedOmniSharpTestHost); Assert.True(completions.Items.All(c => c.IsSuggestionMode())); } @@ -918,7 +993,7 @@ void M() } "; - var completions = await FindCompletionsAsync(filename, source); + var completions = await FindCompletionsAsync(filename, source, SharedOmniSharpTestHost); Assert.True(completions.Items.All(c => !c.IsSuggestionMode())); } @@ -941,7 +1016,7 @@ void M() } "; - var completions = await FindCompletionsAsync(filename, source); + var completions = await FindCompletionsAsync(filename, source, SharedOmniSharpTestHost); Assert.True(completions.Items.All(c => !c.IsSuggestionMode())); } @@ -957,7 +1032,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 => @@ -990,7 +1065,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 => { @@ -1023,7 +1098,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 => @@ -1054,7 +1129,7 @@ public M() } }"; - var completions = await FindCompletionsAsync(filename, input, triggerChar: ' '); + var completions = await FindCompletionsAsync(filename, input, SharedOmniSharpTestHost, triggerChar: ' '); Assert.NotEmpty(completions.Items); } @@ -1071,7 +1146,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)); } @@ -1088,7 +1163,7 @@ public M() } }"; - var completions = await FindCompletionsAsync(filename, input, triggerChar: ' '); + var completions = await FindCompletionsAsync(filename, input, SharedOmniSharpTestHost, triggerChar: ' '); Assert.Empty(completions.Items); } @@ -1109,7 +1184,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); @@ -1118,10 +1193,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 @@ -1134,14 +1210,14 @@ protected async Task FindCompletionsAsync(string filename, s TriggerCharacter = triggerChar }; - var requestHandler = GetCompletionService(SharedOmniSharpTestHost); + var requestHandler = GetCompletionService(testHost); return await requestHandler.Handle(request); } - private async Task FindCompletionsWithImportedAsync(string filename, string source, char? triggerChar = null) + private async Task FindCompletionsWithImportedAsync(string filename, string source, OmniSharpTestHost host) { - var completions = await FindCompletionsAsync(filename, source); + var completions = await FindCompletionsAsync(filename, source, host); if (!completions.IsIncomplete) { return completions; @@ -1149,12 +1225,12 @@ private async Task FindCompletionsWithImportedAsync(string f // Populating the completion list should take no more than a few ms, don't let it take too // long - CancellationTokenSource cts = new CancellationTokenSource(100); + CancellationTokenSource cts = new CancellationTokenSource(millisecondsDelay: 100); await Task.Run(async () => { while (completions.IsIncomplete) { - completions = await FindCompletionsAsync(filename, source); + completions = await FindCompletionsAsync(filename, source, host); cts.Token.ThrowIfCancellationRequested(); } }, cts.Token); @@ -1163,8 +1239,18 @@ await Task.Run(async () => return completions; } - protected async Task ResolveCompletionAsync(CompletionItem completionItem) - => await GetCompletionService(SharedOmniSharpTestHost).Handle(new CompletionResolveRequest { Item = completionItem }); + 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 From 97e68ea68220d208c2bed4a64d2d3ca1cac7f5c5 Mon Sep 17 00:00:00 2001 From: Fredric Silberberg Date: Tue, 18 Aug 2020 21:58:00 -0700 Subject: [PATCH 3/4] Fix formatting. --- .../Services/Completion/CompletionService.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionService.cs index baa925f99e..4fd79193c6 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionService.cs @@ -9,7 +9,6 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Completion; -using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Tags; using Microsoft.CodeAnalysis.Text; using Microsoft.Extensions.Logging; @@ -432,7 +431,6 @@ public async Task Handle(CompletionResolveRequest req switch (lastCompletionItem.GetProviderName()) { - case CompletionItemExtensions.ExtensionMethodImportCompletionProvider: case CompletionItemExtensions.TypeImportCompletionProvider: var sourceText = await document.GetTextAsync(); From 9573edc12bebf09d1b634e4f24765d1fbf434413 Mon Sep 17 00:00:00 2001 From: Fredric Silberberg Date: Wed, 19 Aug 2020 10:50:45 -0700 Subject: [PATCH 4/4] Up the timeout on import completion tests to make the tests stable on CI. --- tests/OmniSharp.Roslyn.CSharp.Tests/CompletionFacts.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/CompletionFacts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/CompletionFacts.cs index 6bfd6519a7..5f029867bb 100644 --- a/tests/OmniSharp.Roslyn.CSharp.Tests/CompletionFacts.cs +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/CompletionFacts.cs @@ -13,8 +13,9 @@ namespace OmniSharp.Roslyn.CSharp.Tests { - public class CompletionFacts : AbstractTestFixture + public class CompletionFacts : AbstractTestFixture { + private const int ImportCompletionTimeout = 1000; private readonly ILogger _logger; private string EndpointName => OmniSharpEndpoints.Completion; @@ -145,7 +146,7 @@ public Class1() // Populating the completion cache should take no more than a few ms, don't let it take too // long - CancellationTokenSource cts = new CancellationTokenSource(millisecondsDelay: 100); + CancellationTokenSource cts = new CancellationTokenSource(millisecondsDelay: ImportCompletionTimeout); await Task.Run(async () => { while (completions.IsIncomplete) @@ -597,7 +598,7 @@ public virtual void Test(string text) {} public virtual void Test(string text, string moreText) {} } -class FooChild : Foo +class FooChild : Foo { override $$ } @@ -1225,7 +1226,7 @@ private async Task FindCompletionsWithImportedAsync(string f // Populating the completion list should take no more than a few ms, don't let it take too // long - CancellationTokenSource cts = new CancellationTokenSource(millisecondsDelay: 100); + CancellationTokenSource cts = new CancellationTokenSource(millisecondsDelay: ImportCompletionTimeout); await Task.Run(async () => { while (completions.IsIncomplete)