Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support completion of unimported types. #1896

Merged
merged 5 commits into from
Aug 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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);
}
}
103 changes: 71 additions & 32 deletions src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -114,7 +115,10 @@ public async Task<CompletionResponse> Handle(CompletionRequest request)
return new CompletionResponse { Items = ImmutableArray<CompletionItem>.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,
Expand Down Expand Up @@ -142,12 +146,19 @@ public async Task<CompletionResponse> Handle(CompletionRequest request)

lock (_lock)
{
_lastCompletion = (completions, request.FileName);
_lastCompletion = (completions, request.FileName, position);
}

var triggerCharactersBuilder = ImmutableArray.CreateBuilder<char>(completions.Rules.DefaultCommitCharacters.Length);
var completionsBuilder = ImmutableArray.CreateBuilder<CompletionItem>(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];
Expand Down Expand Up @@ -226,38 +237,23 @@ public async Task<CompletionResponse> 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;
Expand All @@ -282,7 +278,7 @@ public async Task<CompletionResponse> Handle(CompletionRequest request)

return new CompletionResponse
{
IsIncomplete = false,
IsIncomplete = isIncomplete,
Items = completionsBuilder.MoveToImmutable()
};

Expand Down Expand Up @@ -399,7 +395,7 @@ public async Task<CompletionResolveResponse> 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
Expand Down Expand Up @@ -433,12 +429,55 @@ public async Task<CompletionResolveResponse> 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<LinePositionSpanTextChange> 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -22,21 +24,29 @@ 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<bool?> ShowItemsFromUnimportedNamespaces = new PerLanguageOption<bool?>("CompletionOptions", "ShowItemsFromUnimportedNamespaces", defaultValue: null);

static CompletionItemExtensions()
{
_symbolCompletionItemType = typeof(CompletionItem).GetTypeInfo().Assembly.GetType(SymbolCompletionItem);
_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);
333fred marked this conversation as resolved.
Show resolved Hide resolved
_getChangeAsync = typeof(CompletionService).GetMethod(nameof(GetChangeAsync), BindingFlags.NonPublic | BindingFlags.Instance);
}

internal static string GetProviderName(this CompletionItem item)
Expand All @@ -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<string> 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<CompletionChange> GetChangeAsync(
this CompletionService completionService,
Document document,
CompletionItem item,
TextSpan completionListSpan,
char? commitCharacter = null,
CancellationToken cancellationToken = default)
=> (Task<CompletionChange>)_getChangeAsync.Invoke(completionService, new object[] { document, item, completionListSpan, commitCharacter, cancellationToken });

public static async Task<IEnumerable<ISymbol>> GetCompletionSymbolsAsync(this CompletionItem completionItem, IEnumerable<ISymbol> recommendedSymbols, Document document)
{
Expand Down
1 change: 1 addition & 0 deletions src/OmniSharp.Shared/Options/RoslynExtensionsOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Loading