import { Disposable, ExtensionContext, OverviewRulerLane, Range, TextEditorDecorationType, window } from 'vscode'; import { LeanFileProgressKind, LeanFileProgressProcessingInfo } from '@lean4/infoview-api'; import { LeanClientProvider } from './utils/clientProvider'; class LeanFileTaskGutter { private timeout?: NodeJS.Timeout constructor(private uri: string, private decorations: Map<LeanFileProgressKind, [TextEditorDecorationType, string]>, private processed: LeanFileProgressProcessingInfo[]) { this.schedule(100) this.processed = [] } setProcessed(processed: LeanFileProgressProcessingInfo[]) { if (processed === this.processed) return; const oldProcessed = this.processed; this.processed = processed; if (processed === undefined) { this.processed = [] this.clearTimeout(); this.updateDecos(); } else if (this.timeout === undefined) { this.schedule(oldProcessed === undefined ? 500 : 20) } } private schedule(ms: number) { this.timeout = setTimeout(() => { this.timeout = undefined this.updateDecos() }, ms) } private clearTimeout() { if (this.timeout !== undefined) { clearTimeout(this.timeout) this.timeout = undefined; } } private updateDecos() { for (const editor of window.visibleTextEditors) { if (editor.document.uri.toString() === this.uri) { for (const [kind, [decoration, message]] of this.decorations) { editor.setDecorations( decoration, this.processed .filter(info => (info.kind === undefined ? LeanFileProgressKind.Processing : info.kind) === kind) .map(info => ({ range: new Range(info.range.start.line, 0, info.range.end.line, 0), hoverMessage: message })) ) } } } } dispose() { this.clearTimeout(); } } export class LeanTaskGutter implements Disposable { private decorations: Map<LeanFileProgressKind, [TextEditorDecorationType, string]> = new Map<LeanFileProgressKind, [TextEditorDecorationType, string]>(); private status: { [uri: string]: LeanFileProgressProcessingInfo[] } = {}; private gutters: { [uri: string]: LeanFileTaskGutter | undefined } = {}; private subscriptions: Disposable[] = []; constructor(client: LeanClientProvider, context: ExtensionContext) { this.decorations.set(LeanFileProgressKind.Processing, [ window.createTextEditorDecorationType({ overviewRulerLane: OverviewRulerLane.Left, overviewRulerColor: 'rgba(255, 165, 0, 0.5)', dark: { gutterIconPath: context.asAbsolutePath('media/progress-dark.svg'), }, light: { gutterIconPath: context.asAbsolutePath('media/progress-light.svg'), }, gutterIconSize: 'contain', }), 'busily processing...' ]) this.decorations.set(LeanFileProgressKind.FatalError, [ window.createTextEditorDecorationType({ overviewRulerLane: OverviewRulerLane.Left, overviewRulerColor: 'rgba(255, 0, 0, 0.5)', dark: { gutterIconPath: context.asAbsolutePath('media/progress-error-dark.svg'), }, light: { gutterIconPath: context.asAbsolutePath('media/progress-error-light.svg'), }, gutterIconSize: 'contain', }), 'processing stopped' ]) this.subscriptions.push( window.onDidChangeVisibleTextEditors(() => this.updateDecos()), client.progressChanged(([uri, processing]) => { this.status[uri.toString()] = processing this.updateDecos() })); } private updateDecos() { const uris: { [uri: string]: boolean } = {} for (const editor of window.visibleTextEditors) { if (editor.document.languageId !== 'lean4' && editor.document.languageId !== 'lean') continue; const uri = editor.document.uri.toString(); uris[uri] = true const processed = uri in this.status ? this.status[uri] : [] if (this.gutters[uri]) { const gutter = this.gutters[uri]; if (gutter) gutter.setProcessed(processed) } else { this.gutters[uri] = new LeanFileTaskGutter(uri, this.decorations, processed) } } for (const uri of Object.getOwnPropertyNames(this.gutters)) { if (!uris[uri]) { this.gutters[uri]?.dispose(); this.gutters[uri] = undefined; // TODO: also clear this.status for this uri ? } } } dispose(): void { for (const [decoration, _message] of this.decorations.values()) { decoration.dispose() } for (const s of this.subscriptions) { s.dispose(); } } }