Skip to content

Commit 9947c9a

Browse files
Signature help: restructure async work (#1198)
sendRequest isn't an async function and it can synchronously throw errors so we need something to prevent reentrant updates in the error case. Otherwise though we don't needed it if we move more work into the state. I've also calculated line/column ASAP - I'm not sure this is necessary but it feels obviously correct / straightforward which is a win in this code.
1 parent 9e9fb88 commit 9947c9a

File tree

1 file changed

+104
-58
lines changed

1 file changed

+104
-58
lines changed

src/editor/codemirror/language-server/signatureHelp.ts

Lines changed: 104 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*
88
* SPDX-License-Identifier: MIT
99
*/
10-
import { EditorState, StateEffect, StateField } from "@codemirror/state";
10+
import { Facet, StateEffect, StateField } from "@codemirror/state";
1111
import {
1212
Command,
1313
EditorView,
@@ -22,8 +22,8 @@ import {
2222
import { IntlShape } from "react-intl";
2323
import {
2424
MarkupContent,
25+
Position,
2526
SignatureHelp,
26-
SignatureHelpParams,
2727
SignatureHelpRequest,
2828
} from "vscode-languageserver-protocol";
2929
import { ApiReferenceMap } from "../../../documentation/mapping/content";
@@ -38,6 +38,10 @@ import {
3838
import { nameFromSignature, removeFullyQualifiedName } from "./names";
3939
import { offsetToPosition } from "./positions";
4040

41+
export const automaticFacet = Facet.define<boolean, boolean>({
42+
combine: (values) => values[values.length - 1] ?? true,
43+
});
44+
4145
export const setSignatureHelpRequestPosition = StateEffect.define<number>({});
4246

4347
export 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

116105
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+
});
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

Comments
 (0)