77 *
88 * SPDX-License-Identifier: MIT
99 */
10- import { EditorState , StateEffect , StateField } from "@codemirror/state" ;
10+ import { Facet , StateEffect , StateField } from "@codemirror/state" ;
1111import {
1212 Command ,
1313 EditorView ,
@@ -22,8 +22,8 @@ import {
2222import { IntlShape } from "react-intl" ;
2323import {
2424 MarkupContent ,
25+ Position ,
2526 SignatureHelp ,
26- SignatureHelpParams ,
2727 SignatureHelpRequest ,
2828} from "vscode-languageserver-protocol" ;
2929import { ApiReferenceMap } from "../../../documentation/mapping/content" ;
@@ -38,6 +38,10 @@ import {
3838import { nameFromSignature , removeFullyQualifiedName } from "./names" ;
3939import { offsetToPosition } from "./positions" ;
4040
41+ export const automaticFacet = Facet . define < boolean , boolean > ( {
42+ combine : ( values ) => values [ values . length - 1 ] ?? true ,
43+ } ) ;
44+
4145export const setSignatureHelpRequestPosition = StateEffect . define < number > ( { } ) ;
4246
4347export const setSignatureHelpResult = StateEffect . define < SignatureHelp | null > (
@@ -49,6 +53,12 @@ class SignatureHelpState {
4953 * -1 for no signature help requested.
5054 */
5155 pos : number ;
56+
57+ /**
58+ * The LSP position for pos.
59+ */
60+ position : Position | null ;
61+
5262 /**
5363 * The latest result we want to display.
5464 *
@@ -57,11 +67,16 @@ class SignatureHelpState {
5767 */
5868 result : SignatureHelp | null ;
5969
60- constructor ( pos : number , result : SignatureHelp | null ) {
70+ constructor (
71+ pos : number ,
72+ position : Position | null ,
73+ result : SignatureHelp | null
74+ ) {
6175 if ( result && pos === - 1 ) {
6276 throw new Error ( "Invalid state" ) ;
6377 }
6478 this . pos = pos ;
79+ this . position = position ;
6580 this . result = result ;
6681 }
6782}
@@ -77,44 +92,22 @@ const signatureHelpToolTipBaseTheme = EditorView.baseTheme({
7792 } ,
7893} ) ;
7994
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 ;
113101 }
102+ return a . character === b . character && a . line === b . line ;
114103} ;
115104
116105const openSignatureHelp : Command = ( view : EditorView ) => {
117- triggerSignatureHelpRequest ( view , view . state ) ;
106+ view . dispatch ( {
107+ effects : [
108+ setSignatureHelpRequestPosition . of ( view . state . selection . main . from ) ,
109+ ] ,
110+ } ) ;
118111 return true ;
119112} ;
120113
@@ -124,7 +117,7 @@ export const signatureHelp = (
124117 apiReferenceMap : ApiReferenceMap
125118) => {
126119 const signatureHelpTooltipField = StateField . define < SignatureHelpState > ( {
127- create : ( ) => new SignatureHelpState ( - 1 , null ) ,
120+ create : ( ) => new SignatureHelpState ( - 1 , null , null ) ,
128121 update ( state , tr ) {
129122 let { pos, result } = state ;
130123 for ( const effect of tr . effects ) {
@@ -138,17 +131,45 @@ export const signatureHelp = (
138131 }
139132 }
140133 }
134+
141135 // Even if we just got a result, if the position has been cleared we don't want it.
142136 if ( pos === - 1 ) {
143137 result = null ;
144138 }
145139
140+ // By default map the previous position forward
146141 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+ ) {
148169 // Avoid pointless tooltip updates. If nothing else it makes e2e tests hard.
149170 return state ;
150171 }
151- return new SignatureHelpState ( pos , result ) ;
172+ return new SignatureHelpState ( pos , position , result ) ;
152173 } ,
153174 provide : ( f ) =>
154175 showTooltip . from ( f , ( val ) => {
@@ -191,30 +212,54 @@ export const signatureHelp = (
191212 extends BaseLanguageServerView
192213 implements PluginValue
193214 {
194- constructor ( view : EditorView , private automatic : boolean ) {
215+ private destroyed = false ;
216+ private lastPosition : Position | null = null ;
217+
218+ constructor ( view : EditorView ) {
195219 super ( view ) ;
196220 }
197221 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+ } ) ;
213255 }
214- } ) ;
256+ } ) ( ) ;
215257 }
216258 }
217259 }
260+ destroy ( ) : void {
261+ this . destroyed = true ;
262+ }
218263 }
219264
220265 const formatSignatureHelp = (
@@ -306,10 +351,11 @@ export const signatureHelp = (
306351
307352 return [
308353 // View only handles automatic triggering.
309- ViewPlugin . define ( ( view ) => new SignatureHelpView ( view , automatic ) ) ,
354+ ViewPlugin . define ( ( view ) => new SignatureHelpView ( view ) ) ,
310355 signatureHelpTooltipField ,
311356 signatureHelpToolTipBaseTheme ,
312357 keymap . of ( signatureHelpKeymap ) ,
358+ automaticFacet . of ( automatic ) ,
313359 EditorView . domEventHandlers ( {
314360 blur ( event , view ) {
315361 // Close signature help as it interacts badly with drag and drop if
0 commit comments