7
7
*
8
8
* SPDX-License-Identifier: MIT
9
9
*/
10
- import { StateEffect , StateField } from "@codemirror/state" ;
10
+ import { EditorState , StateEffect , StateField } from "@codemirror/state" ;
11
11
import {
12
12
Command ,
13
13
EditorView ,
@@ -16,7 +16,6 @@ import {
16
16
logException ,
17
17
PluginValue ,
18
18
showTooltip ,
19
- Tooltip ,
20
19
ViewPlugin ,
21
20
ViewUpdate ,
22
21
} from "@codemirror/view" ;
@@ -38,18 +37,32 @@ import {
38
37
import { nameFromSignature , removeFullyQualifiedName } from "./names" ;
39
38
import { offsetToPosition } from "./positions" ;
40
39
41
- interface SignatureChangeEffect {
42
- pos : number ;
43
- result : SignatureHelp | null ;
44
- }
40
+ export const setSignatureHelpRequestPosition = StateEffect . define < number > ( { } ) ;
45
41
46
- export const setSignatureHelpEffect = StateEffect . define < SignatureChangeEffect > (
42
+ export const setSignatureHelpResult = StateEffect . define < SignatureHelp | null > (
47
43
{ }
48
44
) ;
49
45
50
- interface SignatureHelpState {
51
- tooltip : Tooltip | null ;
46
+ class SignatureHelpState {
47
+ /**
48
+ * -1 for no signature help requested.
49
+ */
50
+ pos : number ;
51
+ /**
52
+ * The latest result we want to display.
53
+ *
54
+ * This may be out of date while we wait for async response from LSP
55
+ * but we display it as it's generally useful.
56
+ */
52
57
result : SignatureHelp | null ;
58
+
59
+ constructor ( pos : number , result : SignatureHelp | null ) {
60
+ if ( result && pos === - 1 ) {
61
+ throw new Error ( "Invalid state" ) ;
62
+ }
63
+ this . pos = pos ;
64
+ this . result = result ;
65
+ }
53
66
}
54
67
55
68
const signatureHelpToolTipBaseTheme = EditorView . baseTheme ( {
@@ -63,32 +76,42 @@ const signatureHelpToolTipBaseTheme = EditorView.baseTheme({
63
76
} ,
64
77
} ) ;
65
78
66
- const triggerSignatureHelpRequest = async ( view : EditorView ) : Promise < void > => {
67
- const uri = view . state . facet ( uriFacet ) ! ;
68
- const client = view . state . facet ( clientFacet ) ! ;
69
- const pos = view . state . selection . main . from ;
79
+ const triggerSignatureHelpRequest = async (
80
+ view : EditorView ,
81
+ state : EditorState
82
+ ) : Promise < void > => {
83
+ const uri = state . facet ( uriFacet ) ! ;
84
+ const client = state . facet ( clientFacet ) ! ;
85
+ const pos = state . selection . main . from ;
70
86
const params : SignatureHelpParams = {
71
87
textDocument : { uri } ,
72
- position : offsetToPosition ( view . state . doc , pos ) ,
88
+ position : offsetToPosition ( state . doc , pos ) ,
73
89
} ;
74
90
try {
91
+ // Must happen before other event handling that might dispatch more
92
+ // changes that invalidate our position.
93
+ queueMicrotask ( ( ) => {
94
+ view . dispatch ( {
95
+ effects : [ setSignatureHelpRequestPosition . of ( pos ) ] ,
96
+ } ) ;
97
+ } ) ;
75
98
const result = await client . connection . sendRequest (
76
99
SignatureHelpRequest . type ,
77
100
params
78
101
) ;
79
102
view . dispatch ( {
80
- effects : [ setSignatureHelpEffect . of ( { pos , result } ) ] ,
103
+ effects : [ setSignatureHelpResult . of ( result ) ] ,
81
104
} ) ;
82
105
} catch ( e ) {
83
- logException ( view . state , e , "signature-help" ) ;
106
+ logException ( state , e , "signature-help" ) ;
84
107
view . dispatch ( {
85
- effects : [ setSignatureHelpEffect . of ( { pos , result : null } ) ] ,
108
+ effects : [ setSignatureHelpResult . of ( null ) ] ,
86
109
} ) ;
87
110
}
88
111
} ;
89
112
90
113
const openSignatureHelp : Command = ( view : EditorView ) => {
91
- triggerSignatureHelpRequest ( view ) ;
114
+ triggerSignatureHelpRequest ( view , view . state ) ;
92
115
return true ;
93
116
} ;
94
117
@@ -98,28 +121,57 @@ export const signatureHelp = (
98
121
apiReferenceMap : ApiReferenceMap
99
122
) => {
100
123
const signatureHelpTooltipField = StateField . define < SignatureHelpState > ( {
101
- create : ( ) => ( {
102
- result : null ,
103
- tooltip : null ,
104
- } ) ,
124
+ create : ( ) => new SignatureHelpState ( - 1 , null ) ,
105
125
update ( state , tr ) {
126
+ let { pos, result } = state ;
106
127
for ( const effect of tr . effects ) {
107
- if ( effect . is ( setSignatureHelpEffect ) ) {
108
- return reduceSignatureHelpState ( state , effect . value , apiReferenceMap ) ;
128
+ if ( effect . is ( setSignatureHelpRequestPosition ) ) {
129
+ pos = effect . value ;
130
+ } else if ( effect . is ( setSignatureHelpResult ) ) {
131
+ result = effect . value ;
132
+ if ( result === null ) {
133
+ // No need to ask for more updates until triggered again.
134
+ pos = - 1 ;
135
+ }
109
136
}
110
137
}
111
- return state ;
138
+ // Even if we just got a result, if the position has been cleared we don't want it.
139
+ if ( pos === - 1 ) {
140
+ result = null ;
141
+ }
142
+
143
+ pos = pos === - 1 ? - 1 : tr . changes . mapPos ( pos ) ;
144
+ if ( state . pos === pos && state . result === result ) {
145
+ // Avoid pointless tooltip updates. If nothing else it makes e2e tests hard.
146
+ return state ;
147
+ }
148
+ return new SignatureHelpState ( pos , result ) ;
112
149
} ,
113
- provide : ( f ) => showTooltip . from ( f , ( val ) => val . tooltip ) ,
150
+ provide : ( f ) =>
151
+ showTooltip . from ( f , ( val ) => {
152
+ const { result, pos } = val ;
153
+ if ( result ) {
154
+ return {
155
+ pos,
156
+ above : true ,
157
+ // This isn't great but the impact is really bad when it conflicts with autocomplete.
158
+ // strictSide: true,
159
+ create : ( ) => {
160
+ const dom = document . createElement ( "div" ) ;
161
+ dom . className = "cm-signature-tooltip" ;
162
+ dom . appendChild ( formatSignatureHelp ( result , apiReferenceMap ) ) ;
163
+ return { dom } ;
164
+ } ,
165
+ } ;
166
+ }
167
+ return null ;
168
+ } ) ,
114
169
} ) ;
115
170
116
171
const closeSignatureHelp : Command = ( view : EditorView ) => {
117
- if ( view . state . field ( signatureHelpTooltipField ) . tooltip ) {
172
+ if ( view . state . field ( signatureHelpTooltipField ) . pos !== - 1 ) {
118
173
view . dispatch ( {
119
- effects : setSignatureHelpEffect . of ( {
120
- pos : - 1 ,
121
- result : null ,
122
- } ) ,
174
+ effects : setSignatureHelpRequestPosition . of ( - 1 ) ,
123
175
} ) ;
124
176
return true ;
125
177
}
@@ -139,64 +191,29 @@ export const signatureHelp = (
139
191
constructor ( view : EditorView , private automatic : boolean ) {
140
192
super ( view ) ;
141
193
}
142
- update ( { docChanged , selectionSet , transactions } : ViewUpdate ) {
194
+ update ( update : ViewUpdate ) {
143
195
if (
144
- ( docChanged || selectionSet ) &&
145
- this . view . state . field ( signatureHelpTooltipField ) . tooltip
196
+ ( update . docChanged || update . selectionSet ) &&
197
+ this . view . state . field ( signatureHelpTooltipField ) . pos !== - 1
146
198
) {
147
- triggerSignatureHelpRequest ( this . view ) ;
148
- } else if ( this . automatic && docChanged ) {
149
- const last = transactions [ transactions . length - 1 ] ;
199
+ triggerSignatureHelpRequest ( this . view , update . state ) ;
200
+ } else if ( this . automatic && update . docChanged ) {
201
+ const last = update . transactions [ update . transactions . length - 1 ] ;
150
202
151
203
// This needs to trigger for autocomplete adding function parens
152
204
// as well as normal user input with `closebrackets` inserting
153
205
// the closing bracket.
154
206
if ( last . isUserEvent ( "input" ) || last . isUserEvent ( "dnd.drop.call" ) ) {
155
207
last . changes . iterChanges ( ( _fromA , _toA , _fromB , _toB , inserted ) => {
156
208
if ( inserted . sliceString ( 0 ) . trim ( ) . endsWith ( "()" ) ) {
157
- triggerSignatureHelpRequest ( this . view ) ;
209
+ triggerSignatureHelpRequest ( this . view , update . state ) ;
158
210
}
159
211
} ) ;
160
212
}
161
213
}
162
214
}
163
215
}
164
216
165
- const reduceSignatureHelpState = (
166
- state : SignatureHelpState ,
167
- effect : SignatureChangeEffect ,
168
- apiReferenceMap : ApiReferenceMap
169
- ) : SignatureHelpState => {
170
- if ( state . tooltip && ! effect . result ) {
171
- return {
172
- result : null ,
173
- tooltip : null ,
174
- } ;
175
- }
176
- // It's a bit weird that we always update the position, but VS Code does this too.
177
- // I think ideally we'd have a notion of "same function call". Does the
178
- // node have a stable identity?
179
- if ( effect . result ) {
180
- const result = effect . result ;
181
- return {
182
- result,
183
- tooltip : {
184
- pos : effect . pos ,
185
- above : true ,
186
- // This isn't great but the impact is really bad when it conflicts with autocomplete.
187
- // strictSide: true,
188
- create : ( ) => {
189
- const dom = document . createElement ( "div" ) ;
190
- dom . className = "cm-signature-tooltip" ;
191
- dom . appendChild ( formatSignatureHelp ( result , apiReferenceMap ) ) ;
192
- return { dom } ;
193
- } ,
194
- } ,
195
- } ;
196
- }
197
- return state ;
198
- } ;
199
-
200
217
const formatSignatureHelp = (
201
218
help : SignatureHelp ,
202
219
apiReferenceMap : ApiReferenceMap
0 commit comments