@@ -95,10 +95,20 @@ export function setDependencyBuildMode(
9595export const p2cConverter = createP2CConverter ( undefined , true , true )
9696export const c2pConverter = createC2PConverter ( undefined )
9797
98+ /** Patch the given converters to support Lean-specific extensions to LSP datatypes.
99+ *
100+ * Patches need to be updated when bumping vscode-languageclient. */
98101export function patchConverters ( p2cConverter : Protocol2CodeConverter , c2pConverter : Code2ProtocolConverter ) {
102+ // Although converters are objects,
103+ // their methods refer to other methods by closure-captured local binding rather than via `this`,
104+ // so it doesn't suffice to patch a method: we must also patch any callers.
105+ // For example, we patch `asDiagnostics` to invoke our version of `asDiagnostic`.
106+
99107 // eslint-disable-next-line @typescript-eslint/unbound-method
100108 const oldP2cAsDiagnostic = p2cConverter . asDiagnostic
101- p2cConverter . asDiagnostic = function ( protDiag : LeanDiagnostic ) : code . Diagnostic {
109+ p2cConverter . asDiagnostic = function (
110+ protDiag : LeanDiagnostic ,
111+ ) : code . Diagnostic & { fullRange ?: code . Range ; isSilent ?: boolean ; leanTags ?: LeanTag [ ] } {
102112 if ( ! protDiag . message ) {
103113 // Fixes: Notification handler 'textDocument/publishDiagnostics' failed with message: message must be set
104114 protDiag . message = ' '
@@ -109,9 +119,6 @@ export function patchConverters(p2cConverter: Protocol2CodeConverter, c2pConvert
109119 diag . isSilent = protDiag . isSilent
110120 return diag
111121 }
112- // The original definition refers to `asDiagnostic` as a local function
113- // rather than as a member of `Protocol2CodeConverter`,
114- // so we need to overwrite it, too.
115122 p2cConverter . asDiagnostics = async ( diags , token ) => async . map ( diags , d => p2cConverter . asDiagnostic ( d ) , token )
116123
117124 // eslint-disable-next-line @typescript-eslint/unbound-method
@@ -126,20 +133,18 @@ export function patchConverters(p2cConverter: Protocol2CodeConverter, c2pConvert
126133 return protDiag
127134 }
128135 c2pConverter . asDiagnostics = async ( diags , token ) => async . map ( diags , d => c2pConverter . asDiagnostic ( d ) , token )
136+ c2pConverter . asDiagnosticsSync = diags => diags . map ( d => c2pConverter . asDiagnostic ( d ) )
129137
130138 // eslint-disable-next-line @typescript-eslint/unbound-method
131139 const oldC2pAsOpenTextDocumentParams = c2pConverter . asOpenTextDocumentParams
132- c2pConverter . asOpenTextDocumentParams = doc => {
140+ c2pConverter . asOpenTextDocumentParams = function ( doc ) {
133141 const params = oldC2pAsOpenTextDocumentParams . apply ( this , [ doc ] )
134142 return setDependencyBuildMode ( params , 'never' )
135143 }
136144
137145 // eslint-disable-next-line @typescript-eslint/unbound-method
138146 const oldP2CAsWorkspaceEdit = p2cConverter . asWorkspaceEdit
139- p2cConverter . asWorkspaceEdit = async function (
140- item : ls . WorkspaceEdit | null | undefined ,
141- token ?: code . CancellationToken ,
142- ) {
147+ p2cConverter . asWorkspaceEdit = async function ( item , token ) {
143148 if ( item === undefined || item === null ) return undefined
144149 if ( item . documentChanges ) {
145150 // 1. Preprocess `documentChanges` by filtering out snippet edits
@@ -151,52 +156,94 @@ export function patchConverters(p2cConverter: Protocol2CodeConverter, c2pConvert
151156 // so users cannot rely on it;
152157 // but a mix of both doesn't seem to work in VSCode anyway as of 1.84.2.
153158 const snippetChanges : [ code . Uri , code . SnippetTextEdit [ ] ] [ ] = [ ]
154- const documentChanges = await async . map (
155- item . documentChanges ,
156- change => {
157- if ( ! ls . TextDocumentEdit . is ( change ) ) return true
158- const uri = code . Uri . parse ( change . textDocument . uri )
159- const snippetEdits : code . SnippetTextEdit [ ] = [ ]
160- const edits = change . edits . filter ( edit => {
161- if ( ! SnippetTextEdit . is ( edit ) ) return true
162- const range = p2cConverter . asRange ( edit . range )
163- snippetEdits . push (
164- new code . SnippetTextEdit ( range , new code . SnippetString ( edit . leanExtSnippet . value ) ) ,
165- )
166- return false
167- } )
168- snippetChanges . push ( [ uri , snippetEdits ] )
169- return { ...change , edits }
170- } ,
171- token ,
172- )
159+ const documentChanges : ( ls . TextDocumentEdit | ls . CreateFile | ls . RenameFile | ls . DeleteFile ) [ ] =
160+ await async . map (
161+ item . documentChanges ,
162+ change => {
163+ if ( ! ls . TextDocumentEdit . is ( change ) ) return change
164+ const uri = code . Uri . parse ( change . textDocument . uri )
165+ const snippetEdits : code . SnippetTextEdit [ ] = [ ]
166+ const edits = change . edits . filter ( edit => {
167+ if ( ! SnippetTextEdit . is ( edit ) ) return true
168+ const range = p2cConverter . asRange ( edit . range )
169+ snippetEdits . push (
170+ new code . SnippetTextEdit ( range , new code . SnippetString ( edit . leanExtSnippet . value ) ) ,
171+ )
172+ return false
173+ } )
174+ snippetChanges . push ( [ uri , snippetEdits ] )
175+ return { ...change , edits }
176+ } ,
177+ token ,
178+ )
173179 const newItem = { ...item , documentChanges }
174180 const result : code . WorkspaceEdit = await oldP2CAsWorkspaceEdit . apply ( this , [ newItem , token ] )
181+ // Despite the name and docstring,
182+ // `WorkspaceEdit.set` appends `snippetEdits` rather than replacing what's already there.
175183 for ( const [ uri , snippetEdits ] of snippetChanges ) result . set ( uri , snippetEdits )
176184 return result
177185 }
178186 return oldP2CAsWorkspaceEdit . apply ( this , [ item , token ] )
179187 }
180188
189+ // Note: as of vscode-languageclient 9.0.1, there is no c2pConverter.asWorkspaceEdit.
190+ // This is possibly because code.WorkspaceEdit supports features
191+ // that cannot be encoded in ls.WorkspaceEdit.
192+
181193 // eslint-disable-next-line @typescript-eslint/unbound-method
182194 const oldP2cAsCodeAction = p2cConverter . asCodeAction
183- p2cConverter . asCodeAction = async function (
184- item : ls . CodeAction | null | undefined ,
185- token ?: code . CancellationToken ,
186- ) {
195+ p2cConverter . asCodeAction = async function ( item , token ) {
187196 if ( item === undefined || item === null ) return undefined
188- if ( item . edit || item . diagnostics ) {
189- const result : code . CodeAction = await oldP2cAsCodeAction . apply ( this , [ item , token ] )
190- if ( item . diagnostics !== undefined )
191- result . diagnostics = await p2cConverter . asDiagnostics ( item . diagnostics , token )
192- if ( item . edit ) result . edit = await p2cConverter . asWorkspaceEdit ( item . edit , token )
197+ const result : code . CodeAction = await oldP2cAsCodeAction . apply ( this , [ item , token ] )
198+ if ( item . diagnostics !== undefined )
199+ // Call our modified asDiagnostics, defined above. Upstream calls `asDiagnosticsSync`.
200+ result . diagnostics = await p2cConverter . asDiagnostics ( item . diagnostics , token )
201+ if ( item . edit !== undefined ) result . edit = await p2cConverter . asWorkspaceEdit ( item . edit , token )
202+ return result as any /* tsc incompleteness */
203+ }
204+ p2cConverter . asCodeActionResult = async ( items , token ) =>
205+ async . mapAsync (
206+ items ,
207+ async item => ( ls . Command . is ( item ) ? p2cConverter . asCommand ( item ) : p2cConverter . asCodeAction ( item , token ) ) ,
208+ token ,
209+ )
210+
211+ // eslint-disable-next-line @typescript-eslint/unbound-method
212+ const oldC2pAsCodeAction = c2pConverter . asCodeAction
213+ c2pConverter . asCodeAction = async function ( item , token ) {
214+ const result : ls . CodeAction = await oldC2pAsCodeAction . apply ( this , [ item , token ] )
215+ if ( item . diagnostics !== undefined )
216+ result . diagnostics = await c2pConverter . asDiagnostics ( item . diagnostics , token )
217+ return result
218+ }
219+
220+ // eslint-disable-next-line @typescript-eslint/unbound-method
221+ const oldC2pAsCodeActionSync = c2pConverter . asCodeActionSync
222+ c2pConverter . asCodeActionSync = function ( item ) {
223+ const result : ls . CodeAction = oldC2pAsCodeActionSync . apply ( this , [ item ] )
224+ if ( item . diagnostics !== undefined ) result . diagnostics = c2pConverter . asDiagnosticsSync ( item . diagnostics )
225+ return result
226+ }
227+
228+ // eslint-disable-next-line @typescript-eslint/unbound-method
229+ const oldC2pAsCodeActionContext = c2pConverter . asCodeActionContext
230+ c2pConverter . asCodeActionContext = async function ( context , token ) {
231+ const result = await oldC2pAsCodeActionContext . apply ( this , [ context , token ] )
232+ if ( context . diagnostics !== undefined ) {
233+ result . diagnostics = await c2pConverter . asDiagnostics ( context . diagnostics as code . Diagnostic [ ] , token )
193234 }
194- return oldP2cAsCodeAction . apply ( this , [ item , token ] )
235+ return result
195236 }
196237
197- // Note: as of 2023-12-10, there is no c2pConverter.asWorkspaceEdit.
198- // This is possibly because code.WorkspaceEdit supports features
199- // that cannot be encoded in ls.WorkspaceEdit.
238+ // eslint-disable-next-line @typescript-eslint/unbound-method
239+ const oldC2pAsCodeActionContextSync = c2pConverter . asCodeActionContextSync
240+ c2pConverter . asCodeActionContextSync = function ( context ) {
241+ const result = oldC2pAsCodeActionContextSync . apply ( this , [ context ] )
242+ if ( context . diagnostics !== undefined ) {
243+ result . diagnostics = c2pConverter . asDiagnosticsSync ( context . diagnostics as code . Diagnostic [ ] )
244+ }
245+ return result
246+ }
200247}
201248
202249patchConverters ( p2cConverter , c2pConverter )
0 commit comments