77 *
88 * SPDX-License-Identifier: MIT
99 */
10- import { StateEffect , StateField } from "@codemirror/state" ;
10+ import { EditorState , StateEffect , StateField } from "@codemirror/state" ;
1111import {
1212 Command ,
1313 EditorView ,
@@ -16,7 +16,6 @@ import {
1616 logException ,
1717 PluginValue ,
1818 showTooltip ,
19- Tooltip ,
2019 ViewPlugin ,
2120 ViewUpdate ,
2221} from "@codemirror/view" ;
@@ -38,18 +37,32 @@ import {
3837import { nameFromSignature , removeFullyQualifiedName } from "./names" ;
3938import { offsetToPosition } from "./positions" ;
4039
41- interface SignatureChangeEffect {
42- pos : number ;
43- result : SignatureHelp | null ;
44- }
40+ export const setSignatureHelpRequestPosition = StateEffect . define < number > ( { } ) ;
4541
46- export const setSignatureHelpEffect = StateEffect . define < SignatureChangeEffect > (
42+ export const setSignatureHelpResult = StateEffect . define < SignatureHelp | null > (
4743 { }
4844) ;
4945
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+ */
5257 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+ }
5366}
5467
5568const signatureHelpToolTipBaseTheme = EditorView . baseTheme ( {
@@ -63,32 +76,42 @@ const signatureHelpToolTipBaseTheme = EditorView.baseTheme({
6376 } ,
6477} ) ;
6578
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 ;
7086 const params : SignatureHelpParams = {
7187 textDocument : { uri } ,
72- position : offsetToPosition ( view . state . doc , pos ) ,
88+ position : offsetToPosition ( state . doc , pos ) ,
7389 } ;
7490 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+ } ) ;
7598 const result = await client . connection . sendRequest (
7699 SignatureHelpRequest . type ,
77100 params
78101 ) ;
79102 view . dispatch ( {
80- effects : [ setSignatureHelpEffect . of ( { pos , result } ) ] ,
103+ effects : [ setSignatureHelpResult . of ( result ) ] ,
81104 } ) ;
82105 } catch ( e ) {
83- logException ( view . state , e , "signature-help" ) ;
106+ logException ( state , e , "signature-help" ) ;
84107 view . dispatch ( {
85- effects : [ setSignatureHelpEffect . of ( { pos , result : null } ) ] ,
108+ effects : [ setSignatureHelpResult . of ( null ) ] ,
86109 } ) ;
87110 }
88111} ;
89112
90113const openSignatureHelp : Command = ( view : EditorView ) => {
91- triggerSignatureHelpRequest ( view ) ;
114+ triggerSignatureHelpRequest ( view , view . state ) ;
92115 return true ;
93116} ;
94117
@@ -98,28 +121,57 @@ export const signatureHelp = (
98121 apiReferenceMap : ApiReferenceMap
99122) => {
100123 const signatureHelpTooltipField = StateField . define < SignatureHelpState > ( {
101- create : ( ) => ( {
102- result : null ,
103- tooltip : null ,
104- } ) ,
124+ create : ( ) => new SignatureHelpState ( - 1 , null ) ,
105125 update ( state , tr ) {
126+ let { pos, result } = state ;
106127 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+ }
109136 }
110137 }
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 ) ;
112149 } ,
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+ } ) ,
114169 } ) ;
115170
116171 const closeSignatureHelp : Command = ( view : EditorView ) => {
117- if ( view . state . field ( signatureHelpTooltipField ) . tooltip ) {
172+ if ( view . state . field ( signatureHelpTooltipField ) . pos !== - 1 ) {
118173 view . dispatch ( {
119- effects : setSignatureHelpEffect . of ( {
120- pos : - 1 ,
121- result : null ,
122- } ) ,
174+ effects : setSignatureHelpRequestPosition . of ( - 1 ) ,
123175 } ) ;
124176 return true ;
125177 }
@@ -139,64 +191,29 @@ export const signatureHelp = (
139191 constructor ( view : EditorView , private automatic : boolean ) {
140192 super ( view ) ;
141193 }
142- update ( { docChanged , selectionSet , transactions } : ViewUpdate ) {
194+ update ( update : ViewUpdate ) {
143195 if (
144- ( docChanged || selectionSet ) &&
145- this . view . state . field ( signatureHelpTooltipField ) . tooltip
196+ ( update . docChanged || update . selectionSet ) &&
197+ this . view . state . field ( signatureHelpTooltipField ) . pos !== - 1
146198 ) {
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 ] ;
150202
151203 // This needs to trigger for autocomplete adding function parens
152204 // as well as normal user input with `closebrackets` inserting
153205 // the closing bracket.
154206 if ( last . isUserEvent ( "input" ) || last . isUserEvent ( "dnd.drop.call" ) ) {
155207 last . changes . iterChanges ( ( _fromA , _toA , _fromB , _toB , inserted ) => {
156208 if ( inserted . sliceString ( 0 ) . trim ( ) . endsWith ( "()" ) ) {
157- triggerSignatureHelpRequest ( this . view ) ;
209+ triggerSignatureHelpRequest ( this . view , update . state ) ;
158210 }
159211 } ) ;
160212 }
161213 }
162214 }
163215 }
164216
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-
200217 const formatSignatureHelp = (
201218 help : SignatureHelp ,
202219 apiReferenceMap : ApiReferenceMap
0 commit comments