44 *--------------------------------------------------------------------------------------------*/
55
66import * as vscode from 'vscode' ;
7- import { UriConverter } from './uriConverter' ;
87
9- import { FormattingOptions , TextDocumentIdentifier } from 'vscode-languageclient/node' ;
8+ import { FormattingOptions , LanguageClient , TextDocumentIdentifier } from 'vscode-languageclient/node' ;
109import * as RoslynProtocol from './roslynProtocol' ;
1110import { RoslynLanguageServer } from './roslynLanguageServer' ;
1211
13- export function registerOnAutoInsert ( languageServer : RoslynLanguageServer ) {
12+ export function registerOnAutoInsert ( languageServer : RoslynLanguageServer , languageClient : LanguageClient ) {
1413 let source = new vscode . CancellationTokenSource ( ) ;
15- vscode . workspace . onDidChangeTextDocument ( async ( e ) => {
14+
15+ // We explicitly register against the server's didChange notification (instead of the VSCode workspace API)
16+ // as we want to ensure that the server has processed the change before we request edits from auto insert.
17+ // The VSCode workspace API will sometimes call this before the LSP client can send the change, leading to auto insert not working.
18+ languageClient . getFeature ( 'textDocument/didChange' ) . onNotificationSent ( async ( event ) => {
19+ const e = event . params ;
1620 if ( e . contentChanges . length > 1 || e . contentChanges . length === 0 ) {
1721 return ;
1822 }
1923
2024 const change = e . contentChanges [ 0 ] ;
25+ // TextDocumentContentChangeEvent is a union type that does not return a range if the change event is for a full document.
26+ // Full document changes are not supported for onautoinsert.
27+ if ( ! ( 'range' in change ) ) {
28+ return ;
29+ }
30+
31+ // Convert to a VSCode range for ease of handling.
32+ const vscodeRange = languageClient . protocol2CodeConverter . asRange ( change . range ) ;
2133
22- if ( ! change . range . isEmpty ) {
34+ // Empty or multiline changes are not supported for onautoinsert.
35+ if ( ! vscodeRange . isEmpty || ! vscodeRange . isSingleLine ) {
2336 return ;
2437 }
2538
39+ // We need to convert to a vscode TextDocument to apply the correct capabilities.
40+ const uri = languageClient . protocol2CodeConverter . asUri ( e . textDocument . uri ) ;
41+ // This is a no-op because the document is already open (in order to be edited).
42+ const document = await vscode . workspace . openTextDocument ( uri ) ;
43+
2644 const onAutoInsertFeature = languageServer . getOnAutoInsertFeature ( ) ;
27- const onAutoInsertOptions = onAutoInsertFeature ?. getOptions ( e . document ) ;
45+ const onAutoInsertOptions = onAutoInsertFeature ?. getOptions ( document ) ;
2846 const vsTriggerCharacters = onAutoInsertOptions ?. _vs_triggerCharacters ;
2947
3048 if ( vsTriggerCharacters === undefined ) {
@@ -38,10 +56,17 @@ export function registerOnAutoInsert(languageServer: RoslynLanguageServer) {
3856 return ;
3957 }
4058
59+ // We have a single line range so we can compute the length by comparing the start and end character positions.
60+ const rangeLength = vscodeRange . end . character - vscodeRange . start . character ;
61+
62+ // The server expects the request position to represent the caret position in the text after the change has already been applied.
63+ // We need to calculate what that position would be after the change is applied and send that to the server.
64+ const position = vscodeRange . start . translate ( 0 , change . text . length - rangeLength ) ;
65+
4166 source . cancel ( ) ;
4267 source = new vscode . CancellationTokenSource ( ) ;
4368 try {
44- await applyAutoInsertEdit ( e , changeTrimmed , languageServer , source . token ) ;
69+ await applyAutoInsertEdit ( position , changeTrimmed , e . textDocument , uri , languageServer , source . token ) ;
4570 } catch ( e ) {
4671 if ( e instanceof vscode . CancellationError ) {
4772 return ;
@@ -53,25 +78,18 @@ export function registerOnAutoInsert(languageServer: RoslynLanguageServer) {
5378}
5479
5580async function applyAutoInsertEdit (
56- e : vscode . TextDocumentChangeEvent ,
57- changeTrimmed : string ,
81+ position : vscode . Position ,
82+ changeTextTrimmed : string ,
83+ textDocumentIdentifier : TextDocumentIdentifier ,
84+ uri : vscode . Uri ,
5885 languageServer : RoslynLanguageServer ,
5986 token : vscode . CancellationToken
6087) {
61- const change = e . contentChanges [ 0 ] ;
62- // The server expects the request position to represent the caret position in the text after the change has already been applied.
63- // We need to calculate what that position would be after the change is applied and send that to the server.
64- const position = new vscode . Position (
65- change . range . start . line ,
66- change . range . start . character + ( change . text . length - change . rangeLength )
67- ) ;
68- const uri = UriConverter . serialize ( e . document . uri ) ;
69- const textDocument = TextDocumentIdentifier . create ( uri ) ;
7088 const formattingOptions = getFormattingOptions ( ) ;
7189 const request : RoslynProtocol . OnAutoInsertParams = {
72- _vs_textDocument : textDocument ,
90+ _vs_textDocument : textDocumentIdentifier ,
7391 _vs_position : position ,
74- _vs_ch : changeTrimmed ,
92+ _vs_ch : changeTextTrimmed ,
7593 _vs_options : formattingOptions ,
7694 } ;
7795
@@ -84,7 +102,7 @@ async function applyAutoInsertEdit(
84102 const code : any = vscode ;
85103 const textEdits = [ new code . SnippetTextEdit ( new vscode . Range ( startPosition , endPosition ) , docComment ) ] ;
86104 const edit = new vscode . WorkspaceEdit ( ) ;
87- edit . set ( e . document . uri , textEdits ) ;
105+ edit . set ( uri , textEdits ) ;
88106
89107 const applied = vscode . workspace . applyEdit ( edit ) ;
90108 if ( ! applied ) {
0 commit comments