@@ -162,6 +162,9 @@ public async Task<CompletionResponse> Handle(CompletionRequest request)
162162 bool expectingImportedItems = expandedItemsAvailable && _workspace . Options . GetOption ( CompletionItemExtensions . ShowItemsFromUnimportedNamespaces , LanguageNames . CSharp ) == true ;
163163 var syntax = await document . GetSyntaxTreeAsync ( ) ;
164164
165+ var replacingSpanStartPosition = sourceText . Lines . GetLinePosition ( typedSpan . Start ) ;
166+ var replacingSpanEndPosition = sourceText . Lines . GetLinePosition ( typedSpan . End ) ;
167+
165168 for ( int i = 0 ; i < completions . Items . Length ; i ++ )
166169 {
167170 var completion = completions . Items [ i ] ;
@@ -233,25 +236,28 @@ public async Task<CompletionResponse> Handle(CompletionRequest request)
233236
234237 var change = await completionService . GetChangeAsync ( document , completion ) ;
235238
236- // If the span we're using to key the completion off is the same as the replacement
237- // span, then we don't need to do anything special, just snippitize the text and
238- // exit
239239 if ( typedSpan == change . TextChange . Span )
240240 {
241+ // If the span we're using to key the completion off is the same as the replacement
242+ // span, then we don't need to do anything special, just snippitize the text and
243+ // exit
241244 ( insertText , insertTextFormat ) = getAdjustedInsertTextWithPosition ( change , position , newOffset : 0 ) ;
242245 break ;
243246 }
244-
245- // If the span we are using is re-using part of the typed text we just need to grab the completion an prefix it
246- // with the existing text. Such as Onenabled -> OnEnabled, this will re-use On of the typed text
247- if ( typedSpan . Start < change . TextChange . Span . Start && typedSpan . Start < change . TextChange . Span . End && typedSpan . End == change . TextChange . Span . End )
247+ if ( change . TextChange . Span . Start > typedSpan . Start )
248248 {
249+ // If the span we're using to key the replacement span within the original typed span
250+ // span, we want to prepend the missing text from the original typed text to here. The
251+ // reason is that some lsp clients, such as vscode, use the range from the text edit as
252+ // the selector for what filter text to use. This can lead to odd scenarios where invoking
253+ // completion and typing `EQ` will bring up the Equals override, but then dismissing and
254+ // reinvoking completion will have a range that just replaces the Q. Vscode will then consider
255+ // that capital Q to be the start of the filter text, and filter out the Equals overload
256+ // leaving the user with no completion and no explanation
257+ Debug . Assert ( change . TextChange . Span . End == typedSpan . End ) ;
249258 var prefix = typedText . Substring ( 0 , change . TextChange . Span . Start - typedSpan . Start ) ;
250-
251- ( insertText , insertTextFormat ) = getAdjustedInsertTextWithPosition ( change , position , newOffset : 0 ) ;
252-
253- insertText = prefix + insertText ;
254259
260+ ( insertText , insertTextFormat ) = getAdjustedInsertTextWithPosition ( change , position , newOffset : 0 , prefix ) ;
255261 break ;
256262 }
257263
@@ -284,7 +290,14 @@ public async Task<CompletionResponse> Handle(CompletionRequest request)
284290 completionsBuilder . Add ( new CompletionItem
285291 {
286292 Label = completion . DisplayTextPrefix + completion . DisplayText + completion . DisplayTextSuffix ,
287- InsertText = insertText ,
293+ TextEdit = new LinePositionSpanTextChange
294+ {
295+ NewText = insertText ,
296+ StartLine = replacingSpanStartPosition . Line ,
297+ StartColumn = replacingSpanStartPosition . Character ,
298+ EndLine = replacingSpanEndPosition . Line ,
299+ EndColumn = replacingSpanEndPosition . Character
300+ } ,
288301 InsertTextFormat = insertTextFormat ,
289302 AdditionalTextEdits = additionalTextEdits ,
290303 // Ensure that unimported items are sorted after things already imported.
@@ -372,7 +385,8 @@ static ImmutableArray<char> buildCommitCharacters(CSharpCompletionList completio
372385 static ( string , InsertTextFormat ) getAdjustedInsertTextWithPosition (
373386 CompletionChange change ,
374387 int originalPosition ,
375- int newOffset )
388+ int newOffset ,
389+ string ? prependText = null )
376390 {
377391 // We often have to trim part of the given change off the front, but we
378392 // still want to turn the resulting change into a snippet and control
@@ -388,7 +402,7 @@ static ImmutableArray<char> buildCommitCharacters(CSharpCompletionList completio
388402 if ( ! ( change . NewPosition is int newPosition )
389403 || newPosition >= ( change . TextChange . Span . Start + newText . Length ) )
390404 {
391- return ( newText . Substring ( newOffset ) , InsertTextFormat . PlainText ) ;
405+ return ( prependText + newText . Substring ( newOffset ) , InsertTextFormat . PlainText ) ;
392406 }
393407
394408 if ( newPosition < ( originalPosition + newOffset ) )
@@ -402,7 +416,7 @@ static ImmutableArray<char> buildCommitCharacters(CSharpCompletionList completio
402416 // requested start to the new position, and from the new position to the end of the
403417 // string.
404418 int midpoint = newPosition - change . TextChange . Span . Start ;
405- var beforeText = LspSnippetHelpers . Escape ( newText . Substring ( newOffset , midpoint - newOffset ) ) ;
419+ var beforeText = LspSnippetHelpers . Escape ( prependText + newText . Substring ( newOffset , midpoint - newOffset ) ) ;
406420 var afterText = LspSnippetHelpers . Escape ( newText . Substring ( midpoint ) ) ;
407421
408422 return ( beforeText + "$0" + afterText , InsertTextFormat . Snippet ) ;
0 commit comments