Skip to content

Commit 20b9541

Browse files
Signature help: restructure async work
sendRequest isn't an async function as 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.
1 parent 9e9fb88 commit 20b9541

File tree

1 file changed

+72
-61
lines changed

1 file changed

+72
-61
lines changed

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

Lines changed: 72 additions & 61 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,
@@ -23,7 +23,6 @@ import { IntlShape } from "react-intl";
2323
import {
2424
MarkupContent,
2525
SignatureHelp,
26-
SignatureHelpParams,
2726
SignatureHelpRequest,
2827
} from "vscode-languageserver-protocol";
2928
import { ApiReferenceMap } from "../../../documentation/mapping/content";
@@ -38,6 +37,10 @@ import {
3837
import { nameFromSignature, removeFullyQualifiedName } from "./names";
3938
import { offsetToPosition } from "./positions";
4039

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

4346
export 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-
11684
const 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

@@ -142,11 +114,25 @@ export const signatureHelp = (
142114
if (pos === -1) {
143115
result = null;
144116
}
145-
117+
// By default map the previous position forward
146118
pos = pos === -1 ? -1 : tr.changes.mapPos(pos);
147-
if (state.pos === pos && state.result === result) {
148-
// Avoid pointless tooltip updates. If nothing else it makes e2e tests hard.
149-
return state;
119+
if (pos !== -1 && tr.selection) {
120+
// Selection moved while open
121+
pos = tr.selection.main.from;
122+
}
123+
124+
const automatic = tr.state.facet(automaticFacet).valueOf();
125+
if (
126+
automatic &&
127+
((tr.docChanged && tr.isUserEvent("input")) ||
128+
tr.isUserEvent("dnd.drop.call"))
129+
) {
130+
tr.changes.iterChanges((_fromA, _toA, _fromB, _toB, inserted) => {
131+
if (inserted.sliceString(0).trim().endsWith("()")) {
132+
// Triggered
133+
pos = tr.newSelection.main.from;
134+
}
135+
});
150136
}
151137
return new SignatureHelpState(pos, result);
152138
},
@@ -191,30 +177,54 @@ export const signatureHelp = (
191177
extends BaseLanguageServerView
192178
implements PluginValue
193179
{
194-
constructor(view: EditorView, private automatic: boolean) {
180+
private destroyed = false;
181+
private lastPos = -1;
182+
183+
constructor(view: EditorView) {
195184
super(view);
196185
}
197186
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);
187+
const { view, state } = update;
188+
const uri = state.facet(uriFacet)!;
189+
const client = state.facet(clientFacet)!;
190+
const { pos } = update.state.field(signatureHelpTooltipField);
191+
if (this.lastPos !== pos) {
192+
this.lastPos = pos;
193+
if (this.lastPos !== -1) {
194+
(async () => {
195+
try {
196+
const result = await client.connection.sendRequest(
197+
SignatureHelpRequest.type,
198+
{
199+
textDocument: { uri },
200+
position: offsetToPosition(state.doc, this.lastPos),
201+
}
202+
);
203+
if (!this.destroyed) {
204+
view.dispatch({
205+
effects: [setSignatureHelpResult.of(result)],
206+
});
207+
}
208+
} catch (e) {
209+
if (!isErrorDueToDispose(e)) {
210+
logException(state, e, "signature-help");
211+
}
212+
// The sendRequest call can fail synchronously when disposed so we need to ensure our clean-up doesn't happen inside the CM update call.
213+
queueMicrotask(() => {
214+
if (!this.destroyed) {
215+
view.dispatch({
216+
effects: [setSignatureHelpResult.of(null)],
217+
});
218+
}
219+
});
213220
}
214-
});
221+
})();
215222
}
216223
}
217224
}
225+
destroy(): void {
226+
this.destroyed = true;
227+
}
218228
}
219229

220230
const formatSignatureHelp = (
@@ -306,10 +316,11 @@ export const signatureHelp = (
306316

307317
return [
308318
// View only handles automatic triggering.
309-
ViewPlugin.define((view) => new SignatureHelpView(view, automatic)),
319+
ViewPlugin.define((view) => new SignatureHelpView(view)),
310320
signatureHelpTooltipField,
311321
signatureHelpToolTipBaseTheme,
312322
keymap.of(signatureHelpKeymap),
323+
automaticFacet.of(automatic),
313324
EditorView.domEventHandlers({
314325
blur(event, view) {
315326
// Close signature help as it interacts badly with drag and drop if

0 commit comments

Comments
 (0)