Skip to content

Commit ccbfe24

Browse files
authored
feat: improve decoration computation performance (#604)
This PR improves the performance of the new editor decorations that were introduced in #585. Specifically, it makes the following adjustments: - The result of `workspace.getConfiguration` is cached. This VS Code API is slower than expected, so we can't call it for every single decoration. - The decoration debounce delay is increased. - The decoration debounce delay applies to 'unsolved goals' decorations as well. - The work to compute the decorations is slightly reduced.
1 parent 4c7cbdd commit ccbfe24

File tree

1 file changed

+148
-140
lines changed

1 file changed

+148
-140
lines changed

vscode-lean4/src/taskgutter.ts

Lines changed: 148 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ class LeanFileTaskGutter implements Disposable {
6161
}
6262

6363
onDidUpdateState(newDecorationStates: DecorationState[]) {
64-
this.scheduleUpdate(newDecorationStates, 20)
64+
this.scheduleUpdate(newDecorationStates, 100)
6565
}
6666

6767
clear(clearedDecorationTypes: TextEditorDecorationType[]) {
@@ -84,13 +84,11 @@ class LeanFileTaskGutter implements Disposable {
8484
this.displayDecorations('Instantaneous')
8585
}, ms)
8686
}
87-
if (this.editDelayTimeout !== undefined) {
88-
clearTimeout(this.editDelayTimeout)
89-
}
87+
clearTimeout(this.editDelayTimeout)
9088
const remainingDelayMs =
9189
this.lastEditTimestampMs !== undefined
92-
? Math.max(0, this.editDelayMs - (Date.now() - this.lastEditTimestampMs))
93-
: 0
90+
? Math.max(ms, this.editDelayMs - (Date.now() - this.lastEditTimestampMs))
91+
: ms
9492
this.editDelayTimeout = setTimeout(() => {
9593
this.editDelayTimeout = undefined
9694
this.displayDecorations('EditDelayed')
@@ -214,19 +212,7 @@ function mergeDiagnosticGutterDecos(a: DiagnosticGutterDeco, b: DiagnosticGutter
214212
}
215213

216214
function isGoalsAccomplishedDiagnostic(d: LeanDiagnostic): boolean {
217-
return (
218-
d.leanTags !== undefined &&
219-
d.leanTags.some(t => t === LeanTag.GoalsAccomplished) &&
220-
goalsAccomplishedDecorationKind() !== 'Off'
221-
)
222-
}
223-
224-
function isGutterDecoDiagnostic(d: LeanDiagnostic): boolean {
225-
return (
226-
d.severity === DiagnosticSeverity.Error ||
227-
d.severity === DiagnosticSeverity.Warning ||
228-
isGoalsAccomplishedDiagnostic(d)
229-
)
215+
return d.leanTags !== undefined && d.leanTags.some(t => t === LeanTag.GoalsAccomplished)
230216
}
231217

232218
function determineDiagStart(d: LeanDiagnostic, startLine: number, endLine: number, line: number): DiagStart {
@@ -276,29 +262,14 @@ function determineDiagnosticGutterDeco(
276262
}
277263
}
278264

279-
function computeDiagnosticGutterDecos(diagnostics: LeanDiagnostic[]): DiagnosticGutterDeco[] {
280-
const decos: Map<number, DiagnosticGutterDeco> = new Map()
281-
for (const d of diagnostics) {
282-
if (!isGutterDecoDiagnostic(d)) {
283-
continue
284-
}
285-
const range = diagRange(d)
286-
const startLine = range.start.line
287-
const endLine = inclusiveEndLine(range)
288-
for (let line = startLine; line <= endLine; line++) {
289-
const deco = determineDiagnosticGutterDeco(d, startLine, endLine, line)
290-
const oldDeco = decos.get(deco.line)
291-
if (oldDeco === undefined) {
292-
decos.set(deco.line, deco)
293-
} else {
294-
const mergedDeco = mergeDiagnosticGutterDecos(oldDeco, deco)
295-
decos.set(deco.line, mergedDeco)
296-
}
297-
}
265+
function updateDecos(decos: Map<number, DiagnosticGutterDeco>, deco: DiagnosticGutterDeco) {
266+
const oldDeco = decos.get(deco.line)
267+
if (oldDeco === undefined) {
268+
decos.set(deco.line, deco)
269+
} else {
270+
const mergedDeco = mergeDiagnosticGutterDecos(oldDeco, deco)
271+
decos.set(deco.line, mergedDeco)
298272
}
299-
const result: DiagnosticGutterDeco[] = [...decos.values()]
300-
result.sort((a, b) => a.line - b.line)
301-
return result
302273
}
303274

304275
const diagnosticGutterDecoKinds = [
@@ -333,103 +304,6 @@ const diagnosticGutterDecoKinds = [
333304
] as const
334305
type DiagnosticGutterDecoKind = (typeof diagnosticGutterDecoKinds)[number]
335306

336-
function getGoalsAccomplishedDiagnosticGutterDecoKindName(): string {
337-
const configName = goalsAccomplishedDecorationKind()
338-
if (configName === 'Double Checkmark') {
339-
return 'goals-accomplished-checkmark'
340-
}
341-
if (configName === 'Circled Checkmark') {
342-
return 'goals-accomplished-circled-checkmark'
343-
}
344-
if (configName === 'Octopus') {
345-
return 'goals-accomplished-octopus'
346-
}
347-
if (configName === 'Tada') {
348-
return 'goals-accomplished-tada'
349-
}
350-
return 'goals-accomplished-checkmark'
351-
}
352-
353-
function determineSingleLineDiagnosticGutterDecoKind(d: DiagnosticGutterDeco, name: string): DiagnosticGutterDecoKind {
354-
const c = d.isPreviousDiagContinue
355-
const e = d.isPreviousDiagEnd
356-
if (!c && !e) {
357-
return name as DiagnosticGutterDecoKind
358-
}
359-
if (!c && e) {
360-
return `${name}-l-passthrough` as DiagnosticGutterDecoKind
361-
}
362-
if (c && !e) {
363-
return `${name}-i-passthrough` as DiagnosticGutterDecoKind
364-
}
365-
if (c && e) {
366-
return `${name}-t-passthrough` as DiagnosticGutterDecoKind
367-
}
368-
assert(false)
369-
}
370-
371-
function determineDiagnosticGutterDecoKind(d: DiagnosticGutterDeco): DiagnosticGutterDecoKind | undefined {
372-
const s = d.diagStart
373-
const c = d.isPreviousDiagContinue
374-
const e = d.isPreviousDiagEnd
375-
if (s !== 'None') {
376-
const k = s.kind
377-
const r = s.range
378-
if (k === 'Error') {
379-
if (!c && !e) {
380-
if (r === 'SingleLine') {
381-
return 'error'
382-
}
383-
if (r === 'MultiLine') {
384-
return 'error-init'
385-
}
386-
r satisfies never
387-
}
388-
if (!c && e) {
389-
if (r === 'SingleLine') {
390-
return 'error-l-passthrough'
391-
}
392-
if (r === 'MultiLine') {
393-
return 'error-t-passthrough'
394-
}
395-
r satisfies never
396-
}
397-
if (c && !e) {
398-
// All designs I can think of that would distinguish `SingleLine` and `MultiLine` in this case
399-
// have too much visual complexity for the small gutter.
400-
return 'error-i-passthrough'
401-
}
402-
if (c && e) {
403-
// All designs I can think of that would distinguish `SingleLine` and `MultiLine` in this case
404-
// have too much visual complexity for the small gutter.
405-
return 'error-t-passthrough'
406-
}
407-
assert(false)
408-
}
409-
if (k === 'Warning') {
410-
return determineSingleLineDiagnosticGutterDecoKind(d, 'warning')
411-
}
412-
if (k === 'GoalsAccomplished') {
413-
return determineSingleLineDiagnosticGutterDecoKind(d, getGoalsAccomplishedDiagnosticGutterDecoKindName())
414-
}
415-
k satisfies never
416-
}
417-
assert(s === 'None')
418-
if (!c && !e) {
419-
return undefined
420-
}
421-
if (!c && e) {
422-
return 'error-l'
423-
}
424-
if (c && !e) {
425-
return 'error-i'
426-
}
427-
if (c && e) {
428-
return 'error-t'
429-
}
430-
assert(false)
431-
}
432-
433307
export class LeanTaskGutter implements Disposable {
434308
private processingDecorationType: TextEditorDecorationType
435309
private fatalErrorDecorationType: TextEditorDecorationType
@@ -439,6 +313,7 @@ export class LeanTaskGutter implements Disposable {
439313
private gutters: Map<string, LeanFileTaskGutter> = new Map()
440314
private subscriptions: Disposable[] = []
441315
private showDiagnosticGutterDecorations: boolean = true
316+
private goalsAccomplishedDecorationKind: string
442317
private showUnsolvedGoalsDecoration: boolean = true
443318

444319
constructor(
@@ -542,6 +417,7 @@ export class LeanTaskGutter implements Disposable {
542417
errorLensExtensions.isActive &&
543418
workspace.getConfiguration('errorLens').get('gutterIconsEnabled', false)
544419
this.showDiagnosticGutterDecorations = !isErrorLensGutterEnabled && showDiagnosticGutterDecorations()
420+
this.goalsAccomplishedDecorationKind = goalsAccomplishedDecorationKind()
545421
this.showUnsolvedGoalsDecoration = showUnsolvedGoalsDecoration()
546422
if (!this.showDiagnosticGutterDecorations) {
547423
for (const gutter of this.gutters.values()) {
@@ -622,9 +498,9 @@ export class LeanTaskGutter implements Disposable {
622498
decos: [],
623499
})
624500
}
625-
const decos = computeDiagnosticGutterDecos(diagnostics)
501+
const decos = this.computeDiagnosticGutterDecos(diagnostics)
626502
for (const deco of decos) {
627-
const kind = determineDiagnosticGutterDecoKind(deco)
503+
const kind = this.determineDiagnosticGutterDecoKind(deco)
628504
if (kind === undefined) {
629505
continue
630506
}
@@ -635,6 +511,138 @@ export class LeanTaskGutter implements Disposable {
635511
return [...decoStates.values()]
636512
}
637513

514+
computeDiagnosticGutterDecos(diagnostics: LeanDiagnostic[]): DiagnosticGutterDeco[] {
515+
const decos: Map<number, DiagnosticGutterDeco> = new Map()
516+
for (const d of diagnostics) {
517+
if (!this.isGutterDecoDiagnostic(d)) {
518+
continue
519+
}
520+
const range = diagRange(d)
521+
const startLine = range.start.line
522+
const endLine = inclusiveEndLine(range)
523+
const startDeco = determineDiagnosticGutterDeco(d, startLine, endLine, startLine)
524+
updateDecos(decos, startDeco)
525+
if (startDeco.diagStart !== 'None' && startDeco.diagStart.range === 'SingleLine') {
526+
continue
527+
}
528+
for (let line = startLine + 1; line <= endLine; line++) {
529+
const deco = determineDiagnosticGutterDeco(d, startLine, endLine, line)
530+
updateDecos(decos, deco)
531+
}
532+
}
533+
const result: DiagnosticGutterDeco[] = [...decos.values()]
534+
result.sort((a, b) => a.line - b.line)
535+
return result
536+
}
537+
538+
isGutterDecoDiagnostic(d: LeanDiagnostic): boolean {
539+
return (
540+
d.severity === DiagnosticSeverity.Error ||
541+
d.severity === DiagnosticSeverity.Warning ||
542+
(isGoalsAccomplishedDiagnostic(d) && this.goalsAccomplishedDecorationKind !== 'Off')
543+
)
544+
}
545+
546+
getGoalsAccomplishedDiagnosticGutterDecoKindName(): string {
547+
const configName = this.goalsAccomplishedDecorationKind
548+
if (configName === 'Double Checkmark') {
549+
return 'goals-accomplished-checkmark'
550+
}
551+
if (configName === 'Circled Checkmark') {
552+
return 'goals-accomplished-circled-checkmark'
553+
}
554+
if (configName === 'Octopus') {
555+
return 'goals-accomplished-octopus'
556+
}
557+
if (configName === 'Tada') {
558+
return 'goals-accomplished-tada'
559+
}
560+
return 'goals-accomplished-checkmark'
561+
}
562+
563+
determineSingleLineDiagnosticGutterDecoKind(d: DiagnosticGutterDeco, name: string): DiagnosticGutterDecoKind {
564+
const c = d.isPreviousDiagContinue
565+
const e = d.isPreviousDiagEnd
566+
if (!c && !e) {
567+
return name as DiagnosticGutterDecoKind
568+
}
569+
if (!c && e) {
570+
return `${name}-l-passthrough` as DiagnosticGutterDecoKind
571+
}
572+
if (c && !e) {
573+
return `${name}-i-passthrough` as DiagnosticGutterDecoKind
574+
}
575+
if (c && e) {
576+
return `${name}-t-passthrough` as DiagnosticGutterDecoKind
577+
}
578+
assert(false)
579+
}
580+
581+
determineDiagnosticGutterDecoKind(d: DiagnosticGutterDeco): DiagnosticGutterDecoKind | undefined {
582+
const s = d.diagStart
583+
const c = d.isPreviousDiagContinue
584+
const e = d.isPreviousDiagEnd
585+
if (s !== 'None') {
586+
const k = s.kind
587+
const r = s.range
588+
if (k === 'Error') {
589+
if (!c && !e) {
590+
if (r === 'SingleLine') {
591+
return 'error'
592+
}
593+
if (r === 'MultiLine') {
594+
return 'error-init'
595+
}
596+
r satisfies never
597+
}
598+
if (!c && e) {
599+
if (r === 'SingleLine') {
600+
return 'error-l-passthrough'
601+
}
602+
if (r === 'MultiLine') {
603+
return 'error-t-passthrough'
604+
}
605+
r satisfies never
606+
}
607+
if (c && !e) {
608+
// All designs I can think of that would distinguish `SingleLine` and `MultiLine` in this case
609+
// have too much visual complexity for the small gutter.
610+
return 'error-i-passthrough'
611+
}
612+
if (c && e) {
613+
// All designs I can think of that would distinguish `SingleLine` and `MultiLine` in this case
614+
// have too much visual complexity for the small gutter.
615+
return 'error-t-passthrough'
616+
}
617+
assert(false)
618+
}
619+
if (k === 'Warning') {
620+
return this.determineSingleLineDiagnosticGutterDecoKind(d, 'warning')
621+
}
622+
if (k === 'GoalsAccomplished') {
623+
return this.determineSingleLineDiagnosticGutterDecoKind(
624+
d,
625+
this.getGoalsAccomplishedDiagnosticGutterDecoKindName(),
626+
)
627+
}
628+
k satisfies never
629+
}
630+
assert(s === 'None')
631+
if (!c && !e) {
632+
return undefined
633+
}
634+
if (!c && e) {
635+
return 'error-l'
636+
}
637+
if (c && !e) {
638+
return 'error-i'
639+
}
640+
if (c && e) {
641+
return 'error-t'
642+
}
643+
assert(false)
644+
}
645+
638646
private computeUnsolvedGoalsDecoState(diagnostics: LeanDiagnostic[]): DecorationState {
639647
const unsolvedGoalsLines = diagnostics
640648
.filter(d => {

0 commit comments

Comments
 (0)