Skip to content

Commit d5f7a77

Browse files
authored
Merge pull request #1908 from 333fred/master
Better handle completion when the display text is not in the final result
2 parents 33a747e + 1f64a7b commit d5f7a77

File tree

4 files changed

+196
-37
lines changed

4 files changed

+196
-37
lines changed

src/OmniSharp.Abstractions/Models/v1/Completion/CompletionItem.cs

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#nullable enable
22

3-
using System.Collections.Immutable;
3+
using System.Collections.Generic;
44

55
namespace OmniSharp.Models.v1.Completion
66
{
@@ -23,7 +23,7 @@ public class CompletionItem
2323
/// <summary>
2424
/// Tags for this completion item
2525
/// </summary>
26-
public ImmutableArray<CompletionItemTag>? Tags { get; set; }
26+
public IReadOnlyList<CompletionItemTag>? Tags { get; set; }
2727

2828
/// <summary>
2929
/// A human-readable string with additional information
@@ -69,7 +69,7 @@ public class CompletionItem
6969
/// An optional set of characters that when pressed while this completion is active will accept it first and
7070
/// then type that character.
7171
/// </summary>
72-
public ImmutableArray<char>? CommitCharacters { get; set; }
72+
public IReadOnlyList<char>? CommitCharacters { get; set; }
7373

7474
/// <summary>
7575
/// An optional array of additional text edits that are applied when
@@ -80,7 +80,7 @@ public class CompletionItem
8080
/// (for example adding an import statement at the top of the file if the completion item will
8181
/// insert an unqualified type).
8282
/// </summary>
83-
public ImmutableArray<LinePositionSpanTextChange>? AdditionalTextEdits { get; set; }
83+
public IReadOnlyList<LinePositionSpanTextChange>? AdditionalTextEdits { get; set; }
8484

8585
/// <summary>
8686
/// Index in the completions list that this completion occurred.

src/OmniSharp.Abstractions/Models/v1/Completion/CompletionResponse.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#nullable enable
22

3-
using System.Collections.Immutable;
3+
using System.Collections.Generic;
44

55
namespace OmniSharp.Models.v1.Completion
66
{
@@ -14,6 +14,6 @@ public class CompletionResponse
1414
/// <summary>
1515
/// The completion items.
1616
/// </summary>
17-
public ImmutableArray<CompletionItem> Items { get; set; }
17+
public IReadOnlyList<CompletionItem> Items { get; set; } = null!;
1818
}
1919
}

src/OmniSharp.Roslyn.CSharp/Services/Completion/CompletionService.cs

+57-13
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77
using System.Linq;
88
using System.Text;
99
using System.Threading.Tasks;
10-
using System.Xml.Resolvers;
1110
using Microsoft.CodeAnalysis;
1211
using Microsoft.CodeAnalysis.Completion;
12+
using Microsoft.CodeAnalysis.CSharp;
13+
using Microsoft.CodeAnalysis.CSharp.Syntax;
1314
using Microsoft.CodeAnalysis.Tags;
1415
using Microsoft.CodeAnalysis.Text;
1516
using Microsoft.Extensions.Logging;
@@ -159,17 +160,19 @@ public async Task<CompletionResponse> Handle(CompletionRequest request)
159160
// the completion as done.
160161
bool seenUnimportedCompletions = false;
161162
bool expectingImportedItems = expandedItemsAvailable && _workspace.Options.GetOption(CompletionItemExtensions.ShowItemsFromUnimportedNamespaces, LanguageNames.CSharp) == true;
163+
var syntax = await document.GetSyntaxTreeAsync();
162164

163165
for (int i = 0; i < completions.Items.Length; i++)
164166
{
165167
var completion = completions.Items[i];
166168
var insertTextFormat = InsertTextFormat.PlainText;
167-
ImmutableArray<LinePositionSpanTextChange>? additionalTextEdits = null;
169+
IReadOnlyList<LinePositionSpanTextChange>? additionalTextEdits = null;
168170
char sortTextPrepend = '0';
169171

170172
if (!completion.TryGetInsertionText(out string insertText))
171173
{
172-
switch (completion.GetProviderName())
174+
string providerName = completion.GetProviderName();
175+
switch (providerName)
173176
{
174177
case CompletionItemExtensions.InternalsVisibleToCompletionProvider:
175178
// The IVT completer doesn't add extra things before the completion
@@ -240,7 +243,7 @@ public async Task<CompletionResponse> Handle(CompletionRequest request)
240243
}
241244

242245
int additionalEditEndOffset;
243-
(additionalTextEdits, additionalEditEndOffset) = GetAdditionalTextEdits(change, sourceText, typedSpan, completion.DisplayText, isImportCompletion: false);
246+
(additionalTextEdits, additionalEditEndOffset) = await GetAdditionalTextEdits(change, sourceText, (CSharpParseOptions)syntax!.Options, typedSpan, completion.DisplayText, providerName);
244247

245248
// Now that we have the additional edit, adjust the rest of the new text
246249
(insertText, insertTextFormat) = getAdjustedInsertTextWithPosition(change, position, additionalEditEndOffset);
@@ -435,14 +438,16 @@ public async Task<CompletionResolveResponse> Handle(CompletionResolveRequest req
435438

436439
request.Item.Documentation = textBuilder.ToString();
437440

438-
switch (lastCompletionItem.GetProviderName())
441+
string providerName = lastCompletionItem.GetProviderName();
442+
switch (providerName)
439443
{
440444
case CompletionItemExtensions.ExtensionMethodImportCompletionProvider:
441445
case CompletionItemExtensions.TypeImportCompletionProvider:
446+
var syntax = await document.GetSyntaxTreeAsync();
442447
var sourceText = await document.GetTextAsync();
443448
var typedSpan = completionService.GetDefaultCompletionListSpan(sourceText, position);
444449
var change = await completionService.GetChangeAsync(document, lastCompletionItem, typedSpan);
445-
(request.Item.AdditionalTextEdits, _) = GetAdditionalTextEdits(change, sourceText, typedSpan, lastCompletionItem.DisplayText, isImportCompletion: true);
450+
(request.Item.AdditionalTextEdits, _) = await GetAdditionalTextEdits(change, sourceText, (CSharpParseOptions)syntax!.Options, typedSpan, lastCompletionItem.DisplayText, providerName);
446451
break;
447452
}
448453

@@ -452,19 +457,20 @@ public async Task<CompletionResolveResponse> Handle(CompletionResolveRequest req
452457
};
453458
}
454459

455-
private (ImmutableArray<LinePositionSpanTextChange> edits, int endOffset) GetAdditionalTextEdits(CompletionChange change, SourceText sourceText, TextSpan typedSpan, string completionDisplayText, bool isImportCompletion)
460+
private async ValueTask<(IReadOnlyList<LinePositionSpanTextChange> edits, int endOffset)> GetAdditionalTextEdits(
461+
CompletionChange change,
462+
SourceText sourceText,
463+
CSharpParseOptions parseOptions,
464+
TextSpan typedSpan,
465+
string completionDisplayText,
466+
string providerName)
456467
{
457468
// We know the span starts before the text we're keying off of. So, break that
458469
// out into a separate edit. We need to cut out the space before the current word,
459470
// as the additional edit is not allowed to overlap with the insertion point.
460471
var additionalEditStartPosition = sourceText.Lines.GetLinePosition(change.TextChange.Span.Start);
461472
var additionalEditEndPosition = sourceText.Lines.GetLinePosition(typedSpan.Start - 1);
462-
int additionalEditEndOffset = isImportCompletion
463-
// Import completion will put the displaytext at the end of the line, override completion will
464-
// put it at the front.
465-
? change.TextChange.NewText!.LastIndexOf(completionDisplayText)
466-
: change.TextChange.NewText!.IndexOf(completionDisplayText);
467-
473+
int additionalEditEndOffset = await getAdditionalTextEditEndOffset(change, sourceText, parseOptions, typedSpan, completionDisplayText, providerName);
468474
if (additionalEditEndOffset < 1)
469475
{
470476
// The first index of this was either 0 and the edit span was wrong,
@@ -484,6 +490,44 @@ public async Task<CompletionResolveResponse> Handle(CompletionResolveRequest req
484490
EndLine = additionalEditEndPosition.Line,
485491
EndColumn = additionalEditEndPosition.Character,
486492
}), additionalEditEndOffset);
493+
494+
static async ValueTask<int> getAdditionalTextEditEndOffset(CompletionChange change, SourceText sourceText, CSharpParseOptions parseOptions, TextSpan typedSpan, string completionDisplayText, string providerName)
495+
{
496+
// For many simple cases, we can just find the first or last index of the completionDisplayText and that's good
497+
// enough
498+
int endOffset = (providerName == CompletionItemExtensions.ExtensionMethodImportCompletionProvider ||
499+
providerName == CompletionItemExtensions.TypeImportCompletionProvider)
500+
// Import completion will put the displaytext at the end of the line, override completion will
501+
// put it at the front.
502+
? change.TextChange.NewText!.LastIndexOf(completionDisplayText)
503+
: change.TextChange.NewText!.IndexOf(completionDisplayText);
504+
505+
if (endOffset > -1)
506+
{
507+
return endOffset;
508+
}
509+
510+
// The DisplayText wasn't in the final string. This can happen in a few cases:
511+
// * The override or partial method completion is involving types that need
512+
// to have a using added in the final version, and won't be fully qualified
513+
// as they were in the DisplayText
514+
// * Nullable context differences, such as if the thing you're overriding is
515+
// annotated but the final context being generated into does not have
516+
// annotations enabled.
517+
// For these cases, we currently should only be seeing override or partial
518+
// completions, as import completions don't have nullable annotations or
519+
// fully-qualified types in their DisplayTexts. If that ever changes, we'll have
520+
// to adjust the API here.
521+
//
522+
// In order to find the correct location here, we parse the change. The location
523+
// of the name of the last method is the correct location in the string.
524+
Debug.Assert(providerName == CompletionItemExtensions.OverrideCompletionProvider ||
525+
providerName == CompletionItemExtensions.PartialMethodCompletionProvider);
526+
527+
var parsedTree = CSharpSyntaxTree.ParseText(change.TextChange.NewText, parseOptions);
528+
var lastMethodDecl = (await parsedTree.GetRootAsync()).DescendantNodes().OfType<MethodDeclarationSyntax>().Last();
529+
return lastMethodDecl.Identifier.SpanStart;
530+
}
487531
}
488532
}
489533
}

0 commit comments

Comments
 (0)