7
7
*
8
8
* SPDX-License-Identifier: MIT
9
9
*/
10
- import { EditorState , StateEffect , StateField } from "@codemirror/state" ;
10
+ import { Facet , StateEffect , StateField } from "@codemirror/state" ;
11
11
import {
12
12
Command ,
13
13
EditorView ,
@@ -22,8 +22,8 @@ import {
22
22
import { IntlShape } from "react-intl" ;
23
23
import {
24
24
MarkupContent ,
25
+ Position ,
25
26
SignatureHelp ,
26
- SignatureHelpParams ,
27
27
SignatureHelpRequest ,
28
28
} from "vscode-languageserver-protocol" ;
29
29
import { ApiReferenceMap } from "../../../documentation/mapping/content" ;
@@ -38,6 +38,10 @@ import {
38
38
import { nameFromSignature , removeFullyQualifiedName } from "./names" ;
39
39
import { offsetToPosition } from "./positions" ;
40
40
41
+ export const automaticFacet = Facet . define < boolean , boolean > ( {
42
+ combine : ( values ) => values [ values . length - 1 ] ?? true ,
43
+ } ) ;
44
+
41
45
export const setSignatureHelpRequestPosition = StateEffect . define < number > ( { } ) ;
42
46
43
47
export const setSignatureHelpResult = StateEffect . define < SignatureHelp | null > (
@@ -49,6 +53,12 @@ class SignatureHelpState {
49
53
* -1 for no signature help requested.
50
54
*/
51
55
pos : number ;
56
+
57
+ /**
58
+ * The LSP position for pos.
59
+ */
60
+ position : Position | null ;
61
+
52
62
/**
53
63
* The latest result we want to display.
54
64
*
@@ -57,11 +67,16 @@ class SignatureHelpState {
57
67
*/
58
68
result : SignatureHelp | null ;
59
69
60
- constructor ( pos : number , result : SignatureHelp | null ) {
70
+ constructor (
71
+ pos : number ,
72
+ position : Position | null ,
73
+ result : SignatureHelp | null
74
+ ) {
61
75
if ( result && pos === - 1 ) {
62
76
throw new Error ( "Invalid state" ) ;
63
77
}
64
78
this . pos = pos ;
79
+ this . position = position ;
65
80
this . result = result ;
66
81
}
67
82
}
@@ -77,44 +92,22 @@ const signatureHelpToolTipBaseTheme = EditorView.baseTheme({
77
92
} ,
78
93
} ) ;
79
94
80
- const triggerSignatureHelpRequest = async (
81
- view : EditorView ,
82
- state : EditorState
83
- ) : Promise < void > => {
84
- const uri = state . facet ( uriFacet ) ! ;
85
- const client = state . facet ( clientFacet ) ! ;
86
- const pos = state . selection . main . from ;
87
- const params : SignatureHelpParams = {
88
- textDocument : { uri } ,
89
- position : offsetToPosition ( state . doc , pos ) ,
90
- } ;
91
- try {
92
- // Must happen before other event handling that might dispatch more
93
- // changes that invalidate our position.
94
- queueMicrotask ( ( ) => {
95
- view . dispatch ( {
96
- effects : [ setSignatureHelpRequestPosition . of ( pos ) ] ,
97
- } ) ;
98
- } ) ;
99
- const result = await client . connection . sendRequest (
100
- SignatureHelpRequest . type ,
101
- params
102
- ) ;
103
- view . dispatch ( {
104
- effects : [ setSignatureHelpResult . of ( result ) ] ,
105
- } ) ;
106
- } catch ( e ) {
107
- if ( ! isErrorDueToDispose ( e ) ) {
108
- logException ( state , e , "signature-help" ) ;
109
- }
110
- view . dispatch ( {
111
- effects : [ setSignatureHelpResult . of ( null ) ] ,
112
- } ) ;
95
+ const positionEq = ( a : Position | null , b : Position | null ) : boolean => {
96
+ if ( a === null ) {
97
+ return b === null ;
98
+ }
99
+ if ( b === null ) {
100
+ return a === null ;
113
101
}
102
+ return a . character === b . character && a . line === b . line ;
114
103
} ;
115
104
116
105
const openSignatureHelp : Command = ( view : EditorView ) => {
117
- triggerSignatureHelpRequest ( view , view . state ) ;
106
+ view . dispatch ( {
107
+ effects : [
108
+ setSignatureHelpRequestPosition . of ( view . state . selection . main . from ) ,
109
+ ] ,
110
+ } ) ;
118
111
return true ;
119
112
} ;
120
113
@@ -124,7 +117,7 @@ export const signatureHelp = (
124
117
apiReferenceMap : ApiReferenceMap
125
118
) => {
126
119
const signatureHelpTooltipField = StateField . define < SignatureHelpState > ( {
127
- create : ( ) => new SignatureHelpState ( - 1 , null ) ,
120
+ create : ( ) => new SignatureHelpState ( - 1 , null , null ) ,
128
121
update ( state , tr ) {
129
122
let { pos, result } = state ;
130
123
for ( const effect of tr . effects ) {
@@ -138,17 +131,45 @@ export const signatureHelp = (
138
131
}
139
132
}
140
133
}
134
+
141
135
// Even if we just got a result, if the position has been cleared we don't want it.
142
136
if ( pos === - 1 ) {
143
137
result = null ;
144
138
}
145
139
140
+ // By default map the previous position forward
146
141
pos = pos === - 1 ? - 1 : tr . changes . mapPos ( pos ) ;
147
- if ( state . pos === pos && state . result === result ) {
142
+
143
+ // Did the selection moved while open? We'll re-request but keep the old result for now.
144
+ if ( pos !== - 1 && tr . selection ) {
145
+ pos = tr . selection . main . from ;
146
+ }
147
+
148
+ // Automatic triggering cases
149
+ const automatic = tr . state . facet ( automaticFacet ) . valueOf ( ) ;
150
+ if (
151
+ automatic &&
152
+ ( ( tr . docChanged && tr . isUserEvent ( "input" ) ) ||
153
+ tr . isUserEvent ( "dnd.drop.call" ) )
154
+ ) {
155
+ tr . changes . iterChanges ( ( _fromA , _toA , _fromB , _toB , inserted ) => {
156
+ if ( inserted . sliceString ( 0 ) . trim ( ) . endsWith ( "()" ) ) {
157
+ // Triggered
158
+ pos = tr . newSelection . main . from ;
159
+ }
160
+ } ) ;
161
+ }
162
+
163
+ const position = pos === - 1 ? null : offsetToPosition ( tr . state . doc , pos ) ;
164
+ if (
165
+ state . pos === pos &&
166
+ state . result === result &&
167
+ positionEq ( state . position , position )
168
+ ) {
148
169
// Avoid pointless tooltip updates. If nothing else it makes e2e tests hard.
149
170
return state ;
150
171
}
151
- return new SignatureHelpState ( pos , result ) ;
172
+ return new SignatureHelpState ( pos , position , result ) ;
152
173
} ,
153
174
provide : ( f ) =>
154
175
showTooltip . from ( f , ( val ) => {
@@ -191,30 +212,54 @@ export const signatureHelp = (
191
212
extends BaseLanguageServerView
192
213
implements PluginValue
193
214
{
194
- constructor ( view : EditorView , private automatic : boolean ) {
215
+ private destroyed = false ;
216
+ private lastPosition : Position | null = null ;
217
+
218
+ constructor ( view : EditorView ) {
195
219
super ( view ) ;
196
220
}
197
221
update ( update : ViewUpdate ) {
198
- if (
199
- ( update . docChanged || update . selectionSet ) &&
200
- this . view . state . field ( signatureHelpTooltipField ) . pos !== - 1
201
- ) {
202
- triggerSignatureHelpRequest ( this . view , update . state ) ;
203
- } else if ( this . automatic && update . docChanged ) {
204
- const last = update . transactions [ update . transactions . length - 1 ] ;
205
-
206
- // This needs to trigger for autocomplete adding function parens
207
- // as well as normal user input with `closebrackets` inserting
208
- // the closing bracket.
209
- if ( last . isUserEvent ( "input" ) || last . isUserEvent ( "dnd.drop.call" ) ) {
210
- last . changes . iterChanges ( ( _fromA , _toA , _fromB , _toB , inserted ) => {
211
- if ( inserted . sliceString ( 0 ) . trim ( ) . endsWith ( "()" ) ) {
212
- triggerSignatureHelpRequest ( this . view , update . state ) ;
222
+ const { view, state } = update ;
223
+ const uri = state . facet ( uriFacet ) ! ;
224
+ const client = state . facet ( clientFacet ) ! ;
225
+ const { position } = update . state . field ( signatureHelpTooltipField ) ;
226
+ if ( ! positionEq ( this . lastPosition , position ) ) {
227
+ this . lastPosition = position ;
228
+ if ( position !== null ) {
229
+ ( async ( ) => {
230
+ try {
231
+ const result = await client . connection . sendRequest (
232
+ SignatureHelpRequest . type ,
233
+ {
234
+ textDocument : { uri } ,
235
+ position,
236
+ }
237
+ ) ;
238
+ if ( ! this . destroyed ) {
239
+ view . dispatch ( {
240
+ effects : [ setSignatureHelpResult . of ( result ) ] ,
241
+ } ) ;
242
+ }
243
+ } catch ( e ) {
244
+ if ( ! isErrorDueToDispose ( e ) ) {
245
+ logException ( state , e , "signature-help" ) ;
246
+ }
247
+ // The sendRequest call can fail synchronously when disposed so we need to ensure our clean-up doesn't happen inside the CM update call.
248
+ queueMicrotask ( ( ) => {
249
+ if ( ! this . destroyed ) {
250
+ view . dispatch ( {
251
+ effects : [ setSignatureHelpResult . of ( null ) ] ,
252
+ } ) ;
253
+ }
254
+ } ) ;
213
255
}
214
- } ) ;
256
+ } ) ( ) ;
215
257
}
216
258
}
217
259
}
260
+ destroy ( ) : void {
261
+ this . destroyed = true ;
262
+ }
218
263
}
219
264
220
265
const formatSignatureHelp = (
@@ -306,10 +351,11 @@ export const signatureHelp = (
306
351
307
352
return [
308
353
// View only handles automatic triggering.
309
- ViewPlugin . define ( ( view ) => new SignatureHelpView ( view , automatic ) ) ,
354
+ ViewPlugin . define ( ( view ) => new SignatureHelpView ( view ) ) ,
310
355
signatureHelpTooltipField ,
311
356
signatureHelpToolTipBaseTheme ,
312
357
keymap . of ( signatureHelpKeymap ) ,
358
+ automaticFacet . of ( automatic ) ,
313
359
EditorView . domEventHandlers ( {
314
360
blur ( event , view ) {
315
361
// Close signature help as it interacts badly with drag and drop if
0 commit comments