diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Intellisense/CompletionItemExtensions.cs b/src/OmniSharp.Roslyn.CSharp/Services/Intellisense/CompletionItemExtensions.cs new file mode 100644 index 0000000000..703c3ff72a --- /dev/null +++ b/src/OmniSharp.Roslyn.CSharp/Services/Intellisense/CompletionItemExtensions.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Completion; +using OmniSharp.Utilities; + +namespace OmniSharp.Roslyn.CSharp.Services.Intellisense +{ + internal static class CompletionItemExtensions + { + private static MethodInfo _getSymbolsAsync; + + static CompletionItemExtensions() + { + var symbolCompletionItemType = typeof(CompletionItem).GetTypeInfo().Assembly.GetType("Microsoft.CodeAnalysis.Completion.Providers.SymbolCompletionItem"); + _getSymbolsAsync = symbolCompletionItemType.GetMethod("GetSymbolsAsync", BindingFlags.Public | BindingFlags.Static); + } + + public static async Task> GetCompletionSymbolsAsync(this CompletionItem completionItem, IEnumerable recommendedSymbols, Document document) + { + // for SymbolCompletionProvider, use the logic of extracting information from recommended symbols + if (completionItem.Properties.ContainsKey("Provider") && completionItem.Properties["Provider"] == "Microsoft.CodeAnalysis.CSharp.Completion.Providers.SymbolCompletionProvider") + { + return recommendedSymbols.Where(x => x.Name == completionItem.Properties["SymbolName"] && (int)x.Kind == int.Parse(completionItem.Properties["SymbolKind"])).Distinct(); + } + + // if the completion provider encoded symbols into Properties, we can return them + if (completionItem.Properties.ContainsKey("Symbols")) + { + // the API to decode symbols is not public at the moment + // http://source.roslyn.io/#Microsoft.CodeAnalysis.Features/Completion/Providers/SymbolCompletionItem.cs,93 + var decodedSymbolsTask = _getSymbolsAsync.InvokeStatic>>(new object[] { completionItem, document, default(CancellationToken) }); + if (decodedSymbolsTask != null) + { + return await decodedSymbolsTask; + } + } + + return Enumerable.Empty(); + } + } +} diff --git a/src/OmniSharp.Roslyn.CSharp/Services/Intellisense/IntellisenseService.cs b/src/OmniSharp.Roslyn.CSharp/Services/Intellisense/IntellisenseService.cs index 292bf8141e..6383d200c3 100644 --- a/src/OmniSharp.Roslyn.CSharp/Services/Intellisense/IntellisenseService.cs +++ b/src/OmniSharp.Roslyn.CSharp/Services/Intellisense/IntellisenseService.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Composition; using System.Linq; @@ -37,54 +38,60 @@ public async Task> Handle(AutoCompleteRequest { var sourceText = await document.GetTextAsync(); var position = sourceText.Lines.GetPosition(new LinePosition(request.Line, request.Column)); - var service = CompletionService.GetService(document); var completionList = await service.GetCompletionsAsync(document, position); - // Add keywords from the completion list. We'll use the recommender service to get symbols - // to create snippets from. - if (completionList != null) { + // get recommened symbols to match them up later with SymbolCompletionProvider + var semanticModel = await document.GetSemanticModelAsync(); + var recommendedSymbols = await Recommender.GetRecommendedSymbolsAtPositionAsync(semanticModel, position, _workspace); + foreach (var item in completionList.Items) { - if (item.Tags.Contains(CompletionTags.Keyword)) + var completionText = item.DisplayText; + if (completionText.IsValidCompletionFor(wordToComplete)) { - // Note: For keywords, we'll just assume that the completion text is the same - // as the display text. - var keyword = item.DisplayText; - if (keyword.IsValidCompletionFor(wordToComplete)) + var symbols = await item.GetCompletionSymbolsAsync(recommendedSymbols, document); + if (symbols.Any()) { - var response = new AutoCompleteResponse() + foreach (var symbol in symbols) { - CompletionText = item.DisplayText, - DisplayText = item.DisplayText, - Snippet = item.DisplayText, - Kind = request.WantKind ? "Keyword" : null - }; - - completions.Add(response); + if (symbol != null) + { + if (request.WantSnippet) + { + foreach (var completion in MakeSnippetedResponses(request, symbol, item.DisplayText)) + { + completions.Add(completion); + } + } + else + { + completions.Add(MakeAutoCompleteResponse(request, symbol, item.DisplayText)); + } + } + } + + // if we had any symbols from the completion, we can continue, otherwise it means + // the completion didn't have an associated symbol so we'll add it manually + continue; } - } - } - } - var model = await document.GetSemanticModelAsync(); - var symbols = await Recommender.GetRecommendedSymbolsAtPositionAsync(model, position, _workspace); + // for other completions, i.e. keywords, create a simple AutoCompleteResponse + // we'll just assume that the completion text is the same + // as the display text. + var response = new AutoCompleteResponse() + { + CompletionText = item.DisplayText, + DisplayText = item.DisplayText, + Snippet = item.DisplayText, + Kind = request.WantKind ? item.Tags.First() : null + }; - foreach (var symbol in symbols.Where(s => s.Name.IsValidCompletionFor(wordToComplete))) - { - if (request.WantSnippet) - { - foreach (var completion in MakeSnippetedResponses(request, symbol)) - { - completions.Add(completion); + completions.Add(response); } } - else - { - completions.Add(MakeAutoCompleteResponse(request, symbol)); - } } } @@ -93,10 +100,11 @@ public async Task> Handle(AutoCompleteRequest .ThenByDescending(c => c.CompletionText.IsValidCompletionStartsWithIgnoreCase(wordToComplete)) .ThenByDescending(c => c.CompletionText.IsCamelCaseMatch(wordToComplete)) .ThenByDescending(c => c.CompletionText.IsSubsequenceMatch(wordToComplete)) - .ThenBy(c => c.CompletionText); + .ThenBy(c => c.DisplayText, StringComparer.OrdinalIgnoreCase) + .ThenBy(c => c.CompletionText, StringComparer.OrdinalIgnoreCase); } - private IEnumerable MakeSnippetedResponses(AutoCompleteRequest request, ISymbol symbol) + private IEnumerable MakeSnippetedResponses(AutoCompleteRequest request, ISymbol symbol, string displayName) { var completions = new List(); @@ -105,10 +113,10 @@ private IEnumerable MakeSnippetedResponses(AutoCompleteReq { if (methodSymbol.Parameters.Any(p => p.IsOptional)) { - completions.Add(MakeAutoCompleteResponse(request, symbol, false)); + completions.Add(MakeAutoCompleteResponse(request, symbol, displayName, false)); } - completions.Add(MakeAutoCompleteResponse(request, symbol)); + completions.Add(MakeAutoCompleteResponse(request, symbol, displayName)); return completions; } @@ -116,30 +124,30 @@ private IEnumerable MakeSnippetedResponses(AutoCompleteReq var typeSymbol = symbol as INamedTypeSymbol; if (typeSymbol != null) { - completions.Add(MakeAutoCompleteResponse(request, symbol)); + completions.Add(MakeAutoCompleteResponse(request, symbol, displayName)); if (typeSymbol.TypeKind != TypeKind.Enum) { foreach (var ctor in typeSymbol.InstanceConstructors) { - completions.Add(MakeAutoCompleteResponse(request, ctor)); + completions.Add(MakeAutoCompleteResponse(request, ctor, displayName)); } } return completions; } - return new[] { MakeAutoCompleteResponse(request, symbol) }; + return new[] { MakeAutoCompleteResponse(request, symbol, displayName) }; } - private AutoCompleteResponse MakeAutoCompleteResponse(AutoCompleteRequest request, ISymbol symbol, bool includeOptionalParams = true) + private AutoCompleteResponse MakeAutoCompleteResponse(AutoCompleteRequest request, ISymbol symbol, string displayName, bool includeOptionalParams = true) { var displayNameGenerator = new SnippetGenerator(); displayNameGenerator.IncludeMarkers = false; displayNameGenerator.IncludeOptionalParameters = includeOptionalParams; var response = new AutoCompleteResponse(); - response.CompletionText = symbol.Name; + response.CompletionText = displayName; // TODO: Do something more intelligent here response.DisplayText = displayNameGenerator.Generate(symbol); diff --git a/tests/OmniSharp.Roslyn.CSharp.Tests/IntellisenseFacts.cs b/tests/OmniSharp.Roslyn.CSharp.Tests/IntellisenseFacts.cs index 0f10597f21..f224085fbe 100644 --- a/tests/OmniSharp.Roslyn.CSharp.Tests/IntellisenseFacts.cs +++ b/tests/OmniSharp.Roslyn.CSharp.Tests/IntellisenseFacts.cs @@ -202,6 +202,110 @@ var x$$ ContainsCompletions(completions.Select(c => c.CompletionText), Array.Empty()); } + [Theory] + [InlineData("dummy.cs")] + [InlineData("dummy.csx")] + public async Task Returns_attribute_without_attribute_suffix(string filename) + { + const string source = + @"using System; + + public class BarAttribute : Attribute {} + + [B$$ + public class Foo {}"; + + var completions = await FindCompletionsAsync(filename, source); + ContainsCompletions(completions.Select(c => c.CompletionText).Take(1), "Bar"); + } + + [Theory] + [InlineData("dummy.cs")] + [InlineData("dummy.csx")] + public async Task Returns_members_in_object_initializer_context(string filename) + { + const string source = + @"public class MyClass1 { + public string Foo {get; set;} + } + + public class MyClass2 { + + public MyClass2() + { + var c = new MyClass1 { + F$$ + } + } + "; + + var completions = await FindCompletionsAsync(filename, source); + ContainsCompletions(completions.Select(c => c.CompletionText), "Foo"); + } + + [Theory] + [InlineData("dummy.cs")] + [InlineData("dummy.csx")] + public async Task Returns_parameter_name_inside_a_method(string filename) + { + const string source = + @"public class MyClass1 { + public void SayHi(string text) {} + } + + public class MyClass2 { + + public MyClass2() + { + var c = new MyClass1(); + c.SayHi(te$$ + } + } + "; + + var completions = await FindCompletionsAsync(filename, source); + ContainsCompletions(completions.Select(c => c.CompletionText).Take(1), "text:"); + } + + [Theory] + [InlineData("dummy.cs")] + [InlineData("dummy.csx")] + public async Task Returns_override_signatures(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 $$ + } + "; + + var completions = await FindCompletionsAsync(filename, source); + ContainsCompletions(completions.Select(c => c.CompletionText), "Equals(object obj)", "GetHashCode()", "Test(string text)", "Test(string text, string moreText)", "ToString()"); + } + + [Theory] + [InlineData("dummy.cs")] + [InlineData("dummy.csx")] + public async Task Returns_cref_completion(string filename) + { + const string source = + @" /// + /// A comment. for more details + /// + public class MyClass1 { + } + "; + + var completions = await FindCompletionsAsync(filename, source); + ContainsCompletions(completions.Select(c => c.CompletionText).Take(1), "MyClass1"); + } + private void ContainsCompletions(IEnumerable completions, params string[] expected) { if (!completions.SequenceEqual(expected))