UNPKG

@theia/core

Version:

Theia is a cloud & desktop IDE framework implemented in TypeScript.

246 lines • 9.74 kB
"use strict"; // ***************************************************************************** // Copyright (C) 2020 Red Hat, Inc. and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** Object.defineProperty(exports, "__esModule", { value: true }); exports.DecorationsServiceImpl = exports.DecorationsService = void 0; const tslib_1 = require("tslib"); const inversify_1 = require("inversify"); const promise_util_1 = require("../common/promise-util"); const common_1 = require("../common"); const ternary_search_tree_1 = require("../common/ternary-search-tree"); const debounce = require("lodash.debounce"); exports.DecorationsService = Symbol('DecorationsService'); class DecorationDataRequest { constructor(source, thenable, /** * Last settled decoration from before this request started. It is served while * the request is in flight and used to detect removals when it resolves. */ previous) { this.source = source; this.thenable = thenable; this.previous = previous; } } /** * A decoration that a flush event marked as possibly outdated. It is served until its * resource is queried again, which triggers the re-fetch. */ class StaleDecoration { constructor(value) { this.value = value; } } /** * The decoration an entry last settled on: the resolved decoration itself, the * pre-request value of an in-flight request, or the flushed value of a stale entry. */ function lastKnownDecoration(entry) { if (entry instanceof DecorationDataRequest) { return entry.previous; } if (entry instanceof StaleDecoration) { return entry.value; } return entry ?? undefined; } class DecorationProviderWrapper { constructor(provider, /** Reports a changed decoration, or `undefined` when the decoration was removed. */ notifyDecorationChange, /** Reports that all decorations of this provider may have changed. */ notifyFlush) { this.provider = provider; this.notifyDecorationChange = notifyDecorationChange; this.notifyFlush = notifyFlush; this.data = ternary_search_tree_1.TernarySearchTree.forUris(true); this.disposable = this.provider.onDidChange(uris => { if (!uris) { this.flush(); } else { // selective changes -> re-fetch the given resources for (const uri of uris) { this.fetchData(uri); } } }); } dispose() { this.disposable.dispose(); this.data.clear(); } /** * Handles a flush event: all cached data is dropped to be re-fetched on demand. * Previously fetched decorations are kept as {@link StaleDecoration stale} values * that are served until the re-fetch of their resource settles, so that displayed * decorations do not blink out while the round trip is in flight. */ flush() { const stale = []; this.data.forEach((value, key) => { if (value instanceof DecorationDataRequest) { value.source.cancel(); } const decoration = lastKnownDecoration(value); if (decoration) { stale.push([key, new StaleDecoration(decoration)]); } }); this.data.clear(); for (const [key, value] of stale) { this.data.set(key, value); } this.notifyFlush(); } getOrRetrieve(uri, includeChildren, callback) { let item = this.data.get(uri); if (item === undefined || item instanceof StaleDecoration) { // unknown or stale -> trigger request item = this.fetchData(uri); } // the result, or the last known decoration while a request is in flight const decoration = lastKnownDecoration(item); if (decoration) { callback(decoration, false); } if (includeChildren) { // (last known decorations of) children const iter = this.data.findSuperstr(uri); if (iter) { let next = iter.next(); while (!next.done) { const value = lastKnownDecoration(next.value); if (value) { callback(value, true); } next = iter.next(); } } } } fetchData(uri) { // check for pending request and cancel it, keeping the last settled value const existing = this.data.get(uri); const previous = lastKnownDecoration(existing); if (existing instanceof DecorationDataRequest) { existing.source.cancel(); this.data.delete(uri); } const source = new common_1.CancellationTokenSource(); const dataOrThenable = this.provider.provideDecorations(uri, source.token); if (!(0, promise_util_1.isThenable)(dataOrThenable)) { // sync -> we have a result now return this.keepItem(uri, dataOrThenable, previous); } else { // async -> we have a result soon const request = new DecorationDataRequest(source, Promise.resolve(dataOrThenable).then(data => { if (this.data.get(uri) === request) { this.keepItem(uri, data, request.previous); } }).catch(err => { if (!(err instanceof Error && err.name === 'Canceled' && err.message === 'Canceled') && this.data.get(uri) === request) { this.data.delete(uri); } }), previous); this.data.set(uri, request); return request; } } keepItem(uri, data, previous) { // eslint-disable-next-line no-null/no-null const deco = data ? data : null; this.data.set(uri, deco); if (deco || previous) { // only notify when something actually changed: a decoration appeared or // changed, or a previously known decoration was removed. Resources resolving // to "no decoration" from an unknown or pending state stay silent so that // on-demand queries of undecorated resources cause no render churn. this.notifyDecorationChange(uri, deco ?? undefined); } return deco; } } let DecorationsServiceImpl = class DecorationsServiceImpl { constructor() { this.data = []; this.onDidChangeDecorationsEmitter = new common_1.Emitter(); /** * Accumulates decoration changes until the batched {@link onDidChangeDecorations} * event fires. `undefined` values mark removed decorations. */ this.pendingChanges = new Map(); this.pendingFlush = false; this.onDidChangeDecorations = this.onDidChangeDecorationsEmitter.event; /** Coalesces all changes of the same tick into a single event. */ this.fireSoon = debounce(() => this.fireChangeEvent(), 0); } dispose() { this.fireSoon.cancel(); this.onDidChangeDecorationsEmitter.dispose(); } registerDecorationsProvider(provider) { const wrapper = new DecorationProviderWrapper(provider, (uri, decoration) => this.notifyDecorationChange(uri, decoration), () => this.notifyFlush()); this.data.push(wrapper); return common_1.Disposable.create(() => { // dispose and remove the provider, then signal that // any of its previously provided decorations may be gone. this.data.splice(this.data.indexOf(wrapper), 1); wrapper.dispose(); this.notifyFlush(); }); } getDecoration(uri, includeChildren) { const data = []; let containsChildren = false; for (const wrapper of this.data) { wrapper.getOrRetrieve(uri, includeChildren, (deco, isChild) => { if (!isChild || deco.bubble) { data.push(deco); containsChildren = isChild || containsChildren; } }); } return data; } notifyDecorationChange(uri, decoration) { this.pendingChanges.set(uri.toString(), decoration); this.fireSoon(); } notifyFlush() { this.pendingFlush = true; this.fireSoon(); } fireChangeEvent() { const event = new Map(); if (!this.pendingFlush) { for (const [uri, decoration] of this.pendingChanges) { if (decoration) { event.set(uri, decoration); } } } this.pendingFlush = false; this.pendingChanges.clear(); this.onDidChangeDecorationsEmitter.fire(event); } }; exports.DecorationsServiceImpl = DecorationsServiceImpl; exports.DecorationsServiceImpl = DecorationsServiceImpl = tslib_1.__decorate([ (0, inversify_1.injectable)() ], DecorationsServiceImpl); //# sourceMappingURL=decorations-service.js.map