Skip to content

Commit 46dfdd2

Browse files
authored
perf: cancel redundant interactive requests (#746)
This PR ensures that the infoview cancels interactive goal/term goal/diagnostic/widget requests when they are no longer needed (e.g. because the user moved their cursor away). - Also cancel `infoToInteractive` (popup contents), `lazyTraceChildrenToInteractive` (trace explorer contents), and `msgToInteractive` (message contents). - Remove the `autoCancel` RPC call option. I think it was a mistake and we should just auto-cancel all requests when the infoview closes. - Cleanup `useAsyncWithTrigger` slightly.
1 parent 39b9340 commit 46dfdd2

File tree

13 files changed

+191
-102
lines changed

13 files changed

+191
-102
lines changed

lean4-infoview-api/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@leanprover/infoview-api",
3-
"version": "0.12.0",
3+
"version": "0.13.0",
44
"description": "Types and API for @leanprover/infoview.",
55
"repository": {
66
"type": "git",

lean4-infoview-api/src/infoviewApi.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,6 @@ export interface ClientRequestOptions {
6161
* Does nothing if the promise has already been resolved or rejected.
6262
*/
6363
abortSignal?: AbortSignal
64-
/**
65-
* If `true`, the request will be automatically cancelled when the infoview is closed.
66-
*/
67-
autoCancel?: boolean
6864
}
6965

7066
/** Interface that the InfoView WebView uses to talk to the hosting editor. */

lean4-infoview-api/src/rpcApi.ts

Lines changed: 53 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
import type { LocationLink, Position, Range, TextDocumentPositionParams } from 'vscode-languageserver-protocol'
9+
import { ClientRequestOptions } from './infoviewApi'
910
import { LeanDiagnostic, RpcPtr } from './lspTypes'
1011
import { RpcSessionAtPos } from './rpcSessions'
1112

@@ -85,15 +86,17 @@ export interface InteractiveGoals {
8586
export function getInteractiveGoals(
8687
rs: RpcSessionAtPos,
8788
pos: TextDocumentPositionParams,
89+
options?: ClientRequestOptions,
8890
): Promise<InteractiveGoals | undefined> {
89-
return rs.call('Lean.Widget.getInteractiveGoals', pos)
91+
return rs.call('Lean.Widget.getInteractiveGoals', pos, options)
9092
}
9193

9294
export function getInteractiveTermGoal(
9395
rs: RpcSessionAtPos,
9496
pos: TextDocumentPositionParams,
97+
options?: ClientRequestOptions,
9598
): Promise<InteractiveTermGoal | undefined> {
96-
return rs.call('Lean.Widget.getInteractiveTermGoal', pos)
99+
return rs.call('Lean.Widget.getInteractiveTermGoal', pos, options)
97100
}
98101

99102
export type Name = string
@@ -125,43 +128,59 @@ export interface LineRange {
125128
export function getInteractiveDiagnostics(
126129
rs: RpcSessionAtPos,
127130
lineRange?: LineRange,
131+
options?: ClientRequestOptions,
128132
): Promise<InteractiveDiagnostic[]> {
129-
return rs.call('Lean.Widget.getInteractiveDiagnostics', { lineRange })
133+
return rs.call('Lean.Widget.getInteractiveDiagnostics', { lineRange }, options)
130134
}
131135

132136
export function InteractiveDiagnostics_msgToInteractive(
133137
rs: RpcSessionAtPos,
134138
msg: MessageData,
135139
indent: number,
140+
options?: ClientRequestOptions,
136141
): Promise<TaggedText<MsgEmbed>> {
137142
interface MessageToInteractive {
138143
msg: MessageData
139144
indent: number
140145
}
141-
return rs.call<MessageToInteractive, TaggedText<MsgEmbed>>('Lean.Widget.InteractiveDiagnostics.msgToInteractive', {
142-
msg,
143-
indent,
144-
})
146+
return rs.call<MessageToInteractive, TaggedText<MsgEmbed>>(
147+
'Lean.Widget.InteractiveDiagnostics.msgToInteractive',
148+
{
149+
msg,
150+
indent,
151+
},
152+
options,
153+
)
145154
}
146155

147156
export function lazyTraceChildrenToInteractive(
148157
rs: RpcSessionAtPos,
149158
children: LazyTraceChildren,
159+
options?: ClientRequestOptions,
150160
): Promise<TaggedText<MsgEmbed>[]> {
151-
return rs.call('Lean.Widget.lazyTraceChildrenToInteractive', children)
161+
return rs.call('Lean.Widget.lazyTraceChildrenToInteractive', children, options)
152162
}
153163

154-
export function InteractiveDiagnostics_infoToInteractive(rs: RpcSessionAtPos, info: InfoWithCtx): Promise<InfoPopup> {
155-
return rs.call('Lean.Widget.InteractiveDiagnostics.infoToInteractive', info)
164+
export function InteractiveDiagnostics_infoToInteractive(
165+
rs: RpcSessionAtPos,
166+
info: InfoWithCtx,
167+
options?: ClientRequestOptions,
168+
): Promise<InfoPopup> {
169+
return rs.call('Lean.Widget.InteractiveDiagnostics.infoToInteractive', info, options)
156170
}
157171

158172
export type GoToKind = 'declaration' | 'definition' | 'type'
159-
export function getGoToLocation(rs: RpcSessionAtPos, kind: GoToKind, info: InfoWithCtx): Promise<LocationLink[]> {
173+
export function getGoToLocation(
174+
rs: RpcSessionAtPos,
175+
kind: GoToKind,
176+
info: InfoWithCtx,
177+
options?: ClientRequestOptions,
178+
): Promise<LocationLink[]> {
160179
interface GetGoToLocationParams {
161180
kind: GoToKind
162181
info: InfoWithCtx
163182
}
164-
return rs.call<GetGoToLocationParams, LocationLink[]>('Lean.Widget.getGoToLocation', { kind, info })
183+
return rs.call<GetGoToLocationParams, LocationLink[]>('Lean.Widget.getGoToLocation', { kind, info }, options)
165184
}
166185

167186
export interface UserWidget {
@@ -192,8 +211,12 @@ export interface UserWidgets {
192211
}
193212

194213
/** Given a position, returns all of the user-widgets on the infotree at this position. */
195-
export function Widget_getWidgets(rs: RpcSessionAtPos, pos: Position): Promise<UserWidgets> {
196-
return rs.call<Position, UserWidgets>('Lean.Widget.getWidgets', pos)
214+
export function Widget_getWidgets(
215+
rs: RpcSessionAtPos,
216+
pos: Position,
217+
options?: ClientRequestOptions,
218+
): Promise<UserWidgets> {
219+
return rs.call<Position, UserWidgets>('Lean.Widget.getWidgets', pos, options)
197220
}
198221

199222
/** Code that should be dynamically loaded by the UserWidget component. */
@@ -208,12 +231,17 @@ export interface WidgetSource {
208231
*
209232
* We make the assumption that either the code doesn't exist, or it exists and does not change for the lifetime of the widget.
210233
*/
211-
export function Widget_getWidgetSource(rs: RpcSessionAtPos, pos: Position, hash: string): Promise<WidgetSource> {
234+
export function Widget_getWidgetSource(
235+
rs: RpcSessionAtPos,
236+
pos: Position,
237+
hash: string,
238+
options?: ClientRequestOptions,
239+
): Promise<WidgetSource> {
212240
interface GetWidgetSourceParams {
213241
hash: string
214242
pos: Position
215243
}
216-
return rs.call<GetWidgetSourceParams, WidgetSource>('Lean.Widget.getWidgetSource', { pos, hash })
244+
return rs.call<GetWidgetSourceParams, WidgetSource>('Lean.Widget.getWidgetSource', { pos, hash }, options)
217245
}
218246

219247
export type HighlightedSubexprInfo = SubexprInfo | 'highlighted'
@@ -244,9 +272,14 @@ export function highlightMatches(
244272
rs: RpcSessionAtPos,
245273
query: string,
246274
msg: TaggedText<MsgEmbed>,
275+
options?: ClientRequestOptions,
247276
): Promise<TaggedText<HighlightedMsgEmbed>> {
248-
return rs.call<HighlightMatchesParams, TaggedText<HighlightedMsgEmbed>>('Lean.Widget.highlightMatches', {
249-
query,
250-
msg,
251-
})
277+
return rs.call<HighlightMatchesParams, TaggedText<HighlightedMsgEmbed>>(
278+
'Lean.Widget.highlightMatches',
279+
{
280+
query,
281+
msg,
282+
},
283+
options,
284+
)
252285
}

lean4-infoview/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@leanprover/infoview",
3-
"version": "0.12.0",
3+
"version": "0.13.0",
44
"description": "An interactive display for the Lean 4 theorem prover.",
55
"repository": {
66
"type": "git",
@@ -57,7 +57,7 @@
5757
"typescript": "^5.4.5"
5858
},
5959
"dependencies": {
60-
"@leanprover/infoview-api": "~0.12.0",
60+
"@leanprover/infoview-api": "~0.13.0",
6161
"@vscode/codicons": "^0.0.40",
6262
"@vscode-elements/react-elements": "^0.5.0",
6363
"es-module-lexer": "^1.5.4",

lean4-infoview/src/index.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ export { MessageData }
3030
/** Display the given message data as interactive, pretty-printed text. */
3131
export function InteractiveMessageData({ msg }: { msg: MessageData }) {
3232
const rs = useRpcSession()
33-
const interactive = useAsync(() => InteractiveDiagnostics_msgToInteractive(rs, msg, 0), [rs, msg])
33+
const interactive = useAsync(
34+
abortSignal => InteractiveDiagnostics_msgToInteractive(rs, msg, 0, { abortSignal }),
35+
[rs, msg],
36+
)
3437

3538
if (interactive.state === 'resolved') {
3639
return <InteractiveMessage fmt={interactive.value} />

lean4-infoview/src/infoview/info.tsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -509,12 +509,16 @@ function InfoAux(props: InfoProps) {
509509
// with e.g. a new `pos`.
510510
type InfoRequestResult = Omit<InfoDisplayProps, 'triggerUpdate'>
511511
const [state, triggerUpdateCore] = useAsyncWithTrigger(
512-
() =>
512+
abortSignal =>
513513
new Promise<InfoRequestResult>((resolve, reject) => {
514-
const goalsReq = getInteractiveGoals(rpcSess, DocumentPosition.toTdpp(pos))
515-
const termGoalReq = getInteractiveTermGoal(rpcSess, DocumentPosition.toTdpp(pos))
516-
const widgetsReq = Widget_getWidgets(rpcSess, pos).catch(discardMethodNotFound)
517-
const messagesReq = getInteractiveDiagnostics(rpcSess, { start: pos.line, end: pos.line + 1 })
514+
const goalsReq = getInteractiveGoals(rpcSess, DocumentPosition.toTdpp(pos), { abortSignal })
515+
const termGoalReq = getInteractiveTermGoal(rpcSess, DocumentPosition.toTdpp(pos), { abortSignal })
516+
const widgetsReq = Widget_getWidgets(rpcSess, pos, { abortSignal }).catch(discardMethodNotFound)
517+
const messagesReq = getInteractiveDiagnostics(
518+
rpcSess,
519+
{ start: pos.line, end: pos.line + 1 },
520+
{ abortSignal },
521+
)
518522
// fall back to non-interactive diagnostics when lake fails
519523
// (see https://github.com/leanprover/vscode-lean4/issues/90)
520524
.then(diags => (diags.length === 0 ? lspDiagsHere.map(lspDiagToInteractive) : diags))
@@ -566,6 +570,12 @@ function InfoAux(props: InfoProps) {
566570
reject('retry')
567571
return
568572
}
573+
// When `ex?.code === RpcErrorCode.RequestCancelled`,
574+
// we fall through to resolving with an error below.
575+
// - In case we were cancelled by `useAsyncWithTrigger`,
576+
// `resolve` will do nothing.
577+
// - We should not be cancelled otherwise,
578+
// and if that happens it is an error that should be displayed.
569579

570580
let errorString = ''
571581
if (typeof ex === 'string') {

lean4-infoview/src/infoview/interactiveCode.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,10 @@ function TypePopupContents({ info }: TypePopupContentsProps) {
114114
// When `err` is defined we show the error,
115115
// otherwise if `ip` is defined we show its contents,
116116
// otherwise a 'loading' message.
117-
const interactive = useAsync(() => InteractiveDiagnostics_infoToInteractive(rs, info.info), [rs, info.info])
117+
const interactive = useAsync(
118+
abortSignal => InteractiveDiagnostics_infoToInteractive(rs, info.info, { abortSignal }),
119+
[rs, info.info],
120+
)
118121

119122
// Even when subexpressions are selectable in our parent component, it doesn't make sense
120123
// to select things inside the *type* of the parent, so we clear the context.

lean4-infoview/src/infoview/traceExplorer.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,16 @@ function ChildlessTraceNode(traceEmbed: HighlightedTraceEmbed) {
5252
function CollapsibleTraceNode(traceEmbed: HighlightedTraceEmbed) {
5353
const { cls, collapsed: collapsedByDefault, children: lazyKids } = traceEmbed
5454
const rs = useRpcSession()
55-
const [children, fetchChildren] = useAsyncWithTrigger(async () => {
56-
if ('strict' in lazyKids) {
57-
return lazyKids.strict
58-
} else {
59-
return lazyTraceChildrenToInteractive(rs, lazyKids.lazy)
60-
}
61-
}, [rs, lazyKids])
55+
const [children, fetchChildren] = useAsyncWithTrigger(
56+
async abortSignal => {
57+
if ('strict' in lazyKids) {
58+
return lazyKids.strict
59+
} else {
60+
return lazyTraceChildrenToInteractive(rs, lazyKids.lazy, { abortSignal })
61+
}
62+
},
63+
[rs, lazyKids],
64+
)
6265

6366
const [open, setOpen] = React.useState(!collapsedByDefault)
6467
if (open && children.state === 'notStarted') void fetchChildren()

0 commit comments

Comments
 (0)