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 ,
@@ -23,7 +23,6 @@ import { IntlShape } from "react-intl";
2323import {
2424 MarkupContent ,
2525 SignatureHelp ,
26- SignatureHelpParams ,
2726 SignatureHelpRequest ,
2827} from "vscode-languageserver-protocol" ;
2928import { ApiReferenceMap } from "../../../documentation/mapping/content" ;
@@ -38,6 +37,10 @@ import {
3837import { nameFromSignature , removeFullyQualifiedName } from "./names" ;
3938import { offsetToPosition } from "./positions" ;
4039
40+ export const automaticFacet = Facet . define < boolean , boolean > ( {
41+ combine : ( values ) => values [ values . length - 1 ] ?? true ,
42+ } ) ;
43+
4144export const setSignatureHelpRequestPosition = StateEffect . define < number > ( { } ) ;
4245
4346export const setSignatureHelpResult = StateEffect . define < SignatureHelp | null > (
@@ -49,6 +52,7 @@ class SignatureHelpState {
4952 * -1 for no signature help requested.
5053 */
5154 pos : number ;
55+
5256 /**
5357 * The latest result we want to display.
5458 *
@@ -77,44 +81,12 @@ const signatureHelpToolTipBaseTheme = EditorView.baseTheme({
7781 } ,
7882} ) ;
7983
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- } ) ;
113- }
114- } ;
115-
11684const openSignatureHelp : Command = ( view : EditorView ) => {
117- triggerSignatureHelpRequest ( view , view . state ) ;
85+ view . dispatch ( {
86+ effects : [
87+ setSignatureHelpRequestPosition . of ( view . state . selection . main . from ) ,
88+ ] ,
89+ } ) ;
11890 return true ;
11991} ;
12092
@@ -138,12 +110,35 @@ export const signatureHelp = (
138110 }
139111 }
140112 }
113+
141114 // Even if we just got a result, if the position has been cleared we don't want it.
142115 if ( pos === - 1 ) {
143116 result = null ;
144117 }
145118
119+ // By default map the previous position forward
146120 pos = pos === - 1 ? - 1 : tr . changes . mapPos ( pos ) ;
121+
122+ // Did the selection moved while open? We'll re-request but keep the old result for now.
123+ if ( pos !== - 1 && tr . selection ) {
124+ pos = tr . selection . main . from ;
125+ }
126+
127+ // Automatic triggering cases
128+ const automatic = tr . state . facet ( automaticFacet ) . valueOf ( ) ;
129+ if (
130+ automatic &&
131+ ( ( tr . docChanged && tr . isUserEvent ( "input" ) ) ||
132+ tr . isUserEvent ( "dnd.drop.call" ) )
133+ ) {
134+ tr . changes . iterChanges ( ( _fromA , _toA , _fromB , _toB , inserted ) => {
135+ if ( inserted . sliceString ( 0 ) . trim ( ) . endsWith ( "()" ) ) {
136+ // Triggered
137+ pos = tr . newSelection . main . from ;
138+ }
139+ } ) ;
140+ }
141+
147142 if ( state . pos === pos && state . result === result ) {
148143 // Avoid pointless tooltip updates. If nothing else it makes e2e tests hard.
149144 return state ;
@@ -191,30 +186,54 @@ export const signatureHelp = (
191186 extends BaseLanguageServerView
192187 implements PluginValue
193188 {
194- constructor ( view : EditorView , private automatic : boolean ) {
189+ private destroyed = false ;
190+ private lastPos = - 1 ;
191+
192+ constructor ( view : EditorView ) {
195193 super ( view ) ;
196194 }
197195 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 ) ;
196+ const { view, state } = update ;
197+ const uri = state . facet ( uriFacet ) ! ;
198+ const client = state . facet ( clientFacet ) ! ;
199+ const { pos } = update . state . field ( signatureHelpTooltipField ) ;
200+ if ( this . lastPos !== pos ) {
201+ this . lastPos = pos ;
202+ if ( this . lastPos !== - 1 ) {
203+ ( async ( ) => {
204+ try {
205+ const result = await client . connection . sendRequest (
206+ SignatureHelpRequest . type ,
207+ {
208+ textDocument : { uri } ,
209+ position : offsetToPosition ( state . doc , this . lastPos ) ,
210+ }
211+ ) ;
212+ if ( ! this . destroyed ) {
213+ view . dispatch ( {
214+ effects : [ setSignatureHelpResult . of ( result ) ] ,
215+ } ) ;
216+ }
217+ } catch ( e ) {
218+ if ( ! isErrorDueToDispose ( e ) ) {
219+ logException ( state , e , "signature-help" ) ;
220+ }
221+ // The sendRequest call can fail synchronously when disposed so we need to ensure our clean-up doesn't happen inside the CM update call.
222+ queueMicrotask ( ( ) => {
223+ if ( ! this . destroyed ) {
224+ view . dispatch ( {
225+ effects : [ setSignatureHelpResult . of ( null ) ] ,
226+ } ) ;
227+ }
228+ } ) ;
213229 }
214- } ) ;
230+ } ) ( ) ;
215231 }
216232 }
217233 }
234+ destroy ( ) : void {
235+ this . destroyed = true ;
236+ }
218237 }
219238
220239 const formatSignatureHelp = (
@@ -306,10 +325,11 @@ export const signatureHelp = (
306325
307326 return [
308327 // View only handles automatic triggering.
309- ViewPlugin . define ( ( view ) => new SignatureHelpView ( view , automatic ) ) ,
328+ ViewPlugin . define ( ( view ) => new SignatureHelpView ( view ) ) ,
310329 signatureHelpTooltipField ,
311330 signatureHelpToolTipBaseTheme ,
312331 keymap . of ( signatureHelpKeymap ) ,
332+ automaticFacet . of ( automatic ) ,
313333 EditorView . domEventHandlers ( {
314334 blur ( event , view ) {
315335 // Close signature help as it interacts badly with drag and drop if
0 commit comments