Skip to content

Commit 1f3d6f6

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 1f3d6f6

File tree

1 file changed

+77
-57
lines changed

1 file changed

+77
-57
lines changed

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

Lines changed: 77 additions & 57 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

@@ -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

Comments
 (0)