diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index 81a9950b5e4..ef4d4407aa9 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -834,6 +834,10 @@ class StandaloneUriLabelService implements ILabelService { throw new Error('Not implemented'); } + public registerCachedFormatter(formatter: ResourceLabelFormatter): IDisposable { + return this.registerFormatter(formatter); + } + public getHostLabel(): string { return ''; } diff --git a/src/vs/platform/label/common/label.ts b/src/vs/platform/label/common/label.ts index a89fb9d0c48..e5b6b8a1045 100644 --- a/src/vs/platform/label/common/label.ts +++ b/src/vs/platform/label/common/label.ts @@ -30,6 +30,13 @@ export interface ILabelService { registerFormatter(formatter: ResourceLabelFormatter): IDisposable; onDidChangeFormatters: Event; + + /** + * Registers a formatter that's cached for the machine beyond the lifecycle + * of the current window. Disposing the formatter _will not_ remove it from + * the cache. + */ + registerCachedFormatter(formatter: ResourceLabelFormatter): IDisposable; } export interface IFormatterChangeEvent { diff --git a/src/vs/workbench/api/browser/mainThreadLabelService.ts b/src/vs/workbench/api/browser/mainThreadLabelService.ts index 4c3f813a56e..b284ef951c4 100644 --- a/src/vs/workbench/api/browser/mainThreadLabelService.ts +++ b/src/vs/workbench/api/browser/mainThreadLabelService.ts @@ -21,7 +21,7 @@ export class MainThreadLabelService implements MainThreadLabelServiceShape { $registerResourceLabelFormatter(handle: number, formatter: ResourceLabelFormatter): void { // Dynamicily registered formatters should have priority over those contributed via package.json formatter.priority = true; - const disposable = this._labelService.registerFormatter(formatter); + const disposable = this._labelService.registerCachedFormatter(formatter); this._resourceLabelFormatters.set(handle, disposable); } diff --git a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts index 330be4494f0..e486565ab36 100644 --- a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts +++ b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts @@ -700,6 +700,9 @@ class MockLabelService implements ILabelService { registerFormatter(formatter: ResourceLabelFormatter): IDisposable { throw new Error('Method not implemented.'); } + registerCachedFormatter(formatter: ResourceLabelFormatter): IDisposable { + throw new Error('Method not implemented.'); + } onDidChangeFormatters: Event = new Emitter().event; } diff --git a/src/vs/workbench/services/label/common/labelService.ts b/src/vs/workbench/services/label/common/labelService.ts index b8583dadaf2..c16d82ee53a 100644 --- a/src/vs/workbench/services/label/common/labelService.ts +++ b/src/vs/workbench/services/label/common/labelService.ts @@ -17,13 +17,15 @@ import { tildify, getPathLabel } from 'vs/base/common/labels'; import { ILabelService, ResourceLabelFormatter, ResourceLabelFormatting, IFormatterChangeEvent } from 'vs/platform/label/common/label'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { match } from 'vs/base/common/glob'; -import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { OperatingSystem, OS } from 'vs/base/common/platform'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { Schemas } from 'vs/base/common/network'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { Memento } from 'vs/workbench/common/memento'; const resourceLabelFormattersExtPoint = ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'resourceLabelFormatters', @@ -102,15 +104,24 @@ class ResourceLabelFormattersHandler implements IWorkbenchContribution { } Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ResourceLabelFormattersHandler, LifecyclePhase.Restored); +const FORMATTER_CACHE_SIZE = 50; + +interface IStoredFormatters { + formatters?: ResourceLabelFormatter[]; + i?: number; +} + export class LabelService extends Disposable implements ILabelService { declare readonly _serviceBrand: undefined; - private formatters: ResourceLabelFormatter[] = []; + private formatters: ResourceLabelFormatter[]; private readonly _onDidChangeFormatters = this._register(new Emitter({ leakWarningThreshold: 400 })); readonly onDidChangeFormatters = this._onDidChangeFormatters.event; + private readonly storedFormattersMemento: Memento; + private readonly storedFormatters: IStoredFormatters; private os: OperatingSystem; private userHome: URI | undefined; @@ -118,7 +129,9 @@ export class LabelService extends Disposable implements ILabelService { @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IPathService private readonly pathService: IPathService, - @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService + @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, + @IStorageService storageService: IStorageService, + @ILifecycleService lifecycleService: ILifecycleService, ) { super(); @@ -129,6 +142,10 @@ export class LabelService extends Disposable implements ILabelService { this.os = OS; this.userHome = pathService.defaultUriScheme === Schemas.file ? this.pathService.userHome({ preferLocal: true }) : undefined; + const memento = this.storedFormattersMemento = new Memento('cachedResourceFormatters', storageService); + this.storedFormatters = memento.getMemento(StorageScope.GLOBAL, StorageTarget.USER); + this.formatters = this.storedFormatters?.formatters || []; + // Remote environment is potentially long running this.resolveRemoteEnvironment(); } @@ -334,6 +351,28 @@ export class LabelService extends Disposable implements ILabelService { return formatter?.workspaceTooltip; } + registerCachedFormatter(formatter: ResourceLabelFormatter): IDisposable { + const list = this.storedFormatters.formatters ??= []; + + let replace = list.findIndex(f => f.scheme === formatter.scheme && f.authority === formatter.authority); + if (replace === -1 && list.length >= FORMATTER_CACHE_SIZE) { + replace = FORMATTER_CACHE_SIZE - 1; // at max capacity, replace the last element + } + + if (replace === -1) { + list.unshift(formatter); + } else { + for (let i = replace; i > 0; i--) { + list[i] = list[i - 1]; + } + list[0] = formatter; + } + + this.storedFormattersMemento.saveMemento(); + + return this.registerFormatter(formatter); + } + registerFormatter(formatter: ResourceLabelFormatter): IDisposable { this.formatters.push(formatter); this._onDidChangeFormatters.fire({ scheme: formatter.scheme }); diff --git a/src/vs/workbench/services/label/test/browser/label.test.ts b/src/vs/workbench/services/label/test/browser/label.test.ts index ab94051a7e6..31252fd11ef 100644 --- a/src/vs/workbench/services/label/test/browser/label.test.ts +++ b/src/vs/workbench/services/label/test/browser/label.test.ts @@ -5,19 +5,24 @@ import * as resources from 'vs/base/common/resources'; import * as assert from 'assert'; -import { TestEnvironmentService, TestPathService, TestRemoteAgentService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestEnvironmentService, TestLifecycleService, TestPathService, TestRemoteAgentService } from 'vs/workbench/test/browser/workbenchTestServices'; import { URI } from 'vs/base/common/uri'; import { LabelService } from 'vs/workbench/services/label/common/labelService'; -import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; +import { TestContextService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; import { WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { Workspace } from 'vs/platform/workspace/test/common/testWorkspace'; import { isWindows } from 'vs/base/common/platform'; +import { StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { Memento } from 'vs/workbench/common/memento'; +import { ResourceLabelFormatter } from 'vs/platform/label/common/label'; suite('URI Label', () => { let labelService: LabelService; + let storageService: TestStorageService; setup(() => { - labelService = new LabelService(TestEnvironmentService, new TestContextService(), new TestPathService(), new TestRemoteAgentService()); + storageService = new TestStorageService(); + labelService = new LabelService(TestEnvironmentService, new TestContextService(), new TestPathService(), new TestRemoteAgentService(), storageService, new TestLifecycleService()); }); test('custom scheme', function () { @@ -158,6 +163,41 @@ suite('URI Label', () => { const uri1 = URI.parse('vscode://microsoft.com/1/2/3/4/5'); assert.strictEqual(labelService.getUriLabel(uri1, { relative: false }), 'LABEL: /END'); }); + + + test('label caching', () => { + const m = new Memento('cachedResourceFormatters', storageService).getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); + const makeFormatter = (scheme: string): ResourceLabelFormatter => ({ formatting: { label: `\${path} (${scheme})`, separator: '/' }, scheme }); + assert.deepStrictEqual(m, {}); + + // registers a new formatter: + labelService.registerCachedFormatter(makeFormatter('a')); + assert.deepStrictEqual(m, { formatters: [makeFormatter('a')] }); + + // registers a 2nd formatter: + labelService.registerCachedFormatter(makeFormatter('b')); + assert.deepStrictEqual(m, { formatters: [makeFormatter('b'), makeFormatter('a')] }); + + // promotes a formatter on re-register: + labelService.registerCachedFormatter(makeFormatter('a')); + assert.deepStrictEqual(m, { formatters: [makeFormatter('a'), makeFormatter('b')] }); + + // no-ops if already in first place: + labelService.registerCachedFormatter(makeFormatter('a')); + assert.deepStrictEqual(m, { formatters: [makeFormatter('a'), makeFormatter('b')] }); + + // limits the cache: + for (let i = 0; i < 100; i++) { + labelService.registerCachedFormatter(makeFormatter(`i${i}`)); + } + let expected: ResourceLabelFormatter[] = []; + for (let i = 50; i < 100; i++) { + expected.unshift(makeFormatter(`i${i}`)); + } + assert.deepStrictEqual(m, { formatters: expected }); + + delete (m as any).formatters; + }); }); @@ -178,7 +218,9 @@ suite('multi-root workspace', () => { new WorkspaceFolder({ uri: other, index: 2, name: resources.basename(other) }), ])), new TestPathService(), - new TestRemoteAgentService() + new TestRemoteAgentService(), + new TestStorageService(), + new TestLifecycleService() ); }); @@ -263,7 +305,9 @@ suite('multi-root workspace', () => { new WorkspaceFolder({ uri: rootFolder, index: 0, name: 'FSProotFolder' }), ])), new TestPathService(undefined, rootFolder.scheme), - new TestRemoteAgentService() + new TestRemoteAgentService(), + new TestStorageService(), + new TestLifecycleService() ); const generated = labelService.getUriLabel(URI.parse('myscheme://myauthority/some/folder/test.txt'), { relative: true }); @@ -288,7 +332,9 @@ suite('workspace at FSP root', () => { new WorkspaceFolder({ uri: rootFolder, index: 0, name: 'FSProotFolder' }), ])), new TestPathService(), - new TestRemoteAgentService() + new TestRemoteAgentService(), + new TestStorageService(), + new TestLifecycleService() ); labelService.registerFormatter({ scheme: 'myscheme', diff --git a/src/vs/workbench/services/label/test/electron-browser/label.test.ts b/src/vs/workbench/services/label/test/electron-browser/label.test.ts index ad2976d6bb7..c22bb5c721c 100644 --- a/src/vs/workbench/services/label/test/electron-browser/label.test.ts +++ b/src/vs/workbench/services/label/test/electron-browser/label.test.ts @@ -9,16 +9,16 @@ import { URI } from 'vs/base/common/uri'; import { sep } from 'vs/base/common/path'; import { isWindows } from 'vs/base/common/platform'; import { LabelService } from 'vs/workbench/services/label/common/labelService'; -import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; +import { TestContextService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; import { TestNativePathService, TestEnvironmentService } from 'vs/workbench/test/electron-browser/workbenchTestServices'; -import { TestRemoteAgentService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestLifecycleService, TestRemoteAgentService } from 'vs/workbench/test/browser/workbenchTestServices'; suite('URI Label', () => { let labelService: LabelService; setup(() => { - labelService = new LabelService(TestEnvironmentService, new TestContextService(), new TestNativePathService(), new TestRemoteAgentService()); + labelService = new LabelService(TestEnvironmentService, new TestContextService(), new TestNativePathService(), new TestRemoteAgentService(), new TestStorageService(), new TestLifecycleService()); }); test('file scheme', function () { diff --git a/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyHistoryService.test.ts b/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyHistoryService.test.ts index 596255d0466..5068eccd657 100644 --- a/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyHistoryService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyHistoryService.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; import { TestNativePathService, TestNativeWindowConfiguration } from 'vs/workbench/test/electron-browser/workbenchTestServices'; -import { TestContextService, TestProductService, TestWorkingCopy } from 'vs/workbench/test/common/workbenchTestServices'; +import { TestContextService, TestProductService, TestStorageService, TestWorkingCopy } from 'vs/workbench/test/common/workbenchTestServices'; import { NullLogService } from 'vs/platform/log/common/log'; import { FileService } from 'vs/platform/files/common/fileService'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; @@ -57,7 +57,7 @@ export class TestWorkingCopyHistoryService extends NativeWorkingCopyHistoryServi const uriIdentityService = new UriIdentityService(fileService); - const labelService = new LabelService(environmentService, new TestContextService(), new TestNativePathService(), new TestRemoteAgentService()); + const labelService = new LabelService(environmentService, new TestContextService(), new TestNativePathService(), new TestRemoteAgentService(), new TestStorageService(), new TestLifecycleService()); const lifecycleService = new TestLifecycleService();