Skip to content

Commit

Permalink
Merge pull request #1896 from 333fred/import-completion
Browse files Browse the repository at this point in the history
Support completion of unimported types.
  • Loading branch information
JoeRobich authored Aug 19, 2020
2 parents 0a1f774 + 1522f98 commit 0829793
Show file tree
Hide file tree
Showing 5 changed files with 480 additions and 75 deletions.
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);
_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

0 comments on commit 0829793

Please sign in to comment.