mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 18:46:40 -05:00
Merge from vscode 1ec43773e37997841c5af42b33ddb180e9735bf2
This commit is contained in:
@@ -63,9 +63,14 @@ export interface IBackupFileService {
|
||||
backup<T extends object>(resource: URI, content?: ITextSnapshot, versionId?: number, meta?: T): Promise<void>;
|
||||
|
||||
/**
|
||||
* Discards the backup associated with a resource if it exists..
|
||||
* Discards the backup associated with a resource if it exists.
|
||||
*
|
||||
* @param resource The resource whose backup is being discarded discard to back up.
|
||||
*/
|
||||
discardBackup(resource: URI): Promise<void>;
|
||||
|
||||
/**
|
||||
* Discards all backups.
|
||||
*/
|
||||
discardBackups(): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -165,6 +165,10 @@ export class BackupFileService implements IBackupFileService {
|
||||
return this.impl.discardBackup(resource);
|
||||
}
|
||||
|
||||
discardBackups(): Promise<void> {
|
||||
return this.impl.discardBackups();
|
||||
}
|
||||
|
||||
getBackups(): Promise<URI[]> {
|
||||
return this.impl.getBackups();
|
||||
}
|
||||
@@ -260,6 +264,14 @@ class BackupFileServiceImpl extends Disposable implements IBackupFileService {
|
||||
});
|
||||
}
|
||||
|
||||
async discardBackups(): Promise<void> {
|
||||
const model = await this.ready;
|
||||
|
||||
await this.deleteIgnoreFileNotFound(this.backupWorkspacePath);
|
||||
|
||||
model.clear();
|
||||
}
|
||||
|
||||
discardBackup(resource: URI): Promise<void> {
|
||||
const backupResource = this.toBackupResource(resource);
|
||||
|
||||
@@ -429,6 +441,10 @@ export class InMemoryBackupFileService implements IBackupFileService {
|
||||
this.backups.delete(this.toBackupResource(resource).toString());
|
||||
}
|
||||
|
||||
async discardBackups(): Promise<void> {
|
||||
this.backups.clear();
|
||||
}
|
||||
|
||||
toBackupResource(resource: URI): URI {
|
||||
return URI.file(join(resource.scheme, this.hashPath(resource)));
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ const barFile = URI.file(platform.isWindows ? 'c:\\Bar' : '/Bar');
|
||||
const fooBarFile = URI.file(platform.isWindows ? 'c:\\Foo Bar' : '/Foo Bar');
|
||||
const untitledFile = URI.from({ scheme: Schemas.untitled, path: 'Untitled-1' });
|
||||
const fooBackupPath = path.join(workspaceBackupPath, 'file', hashPath(fooFile));
|
||||
const barBackupPath = path.join(workspaceBackupPath, 'file', hashPath(barFile));
|
||||
const untitledBackupPath = path.join(workspaceBackupPath, 'untitled', hashPath(untitledFile));
|
||||
|
||||
class TestBackupEnvironmentService extends NativeWorkbenchEnvironmentService {
|
||||
@@ -285,6 +286,33 @@ suite('BackupFileService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
suite('discardBackups', () => {
|
||||
test('text file', async () => {
|
||||
await service.backup(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false));
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 1);
|
||||
await service.backup(barFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false));
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'file')).length, 2);
|
||||
await service.discardBackups();
|
||||
assert.equal(fs.existsSync(fooBackupPath), false);
|
||||
assert.equal(fs.existsSync(barBackupPath), false);
|
||||
assert.equal(fs.existsSync(path.join(workspaceBackupPath, 'file')), false);
|
||||
});
|
||||
|
||||
test('untitled file', async () => {
|
||||
await service.backup(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false));
|
||||
assert.equal(fs.readdirSync(path.join(workspaceBackupPath, 'untitled')).length, 1);
|
||||
await service.discardBackups();
|
||||
assert.equal(fs.existsSync(untitledBackupPath), false);
|
||||
assert.equal(fs.existsSync(path.join(workspaceBackupPath, 'untitled')), false);
|
||||
});
|
||||
|
||||
test('can backup after discarding all', async () => {
|
||||
await service.discardBackups();
|
||||
await service.backup(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false));
|
||||
assert.equal(fs.existsSync(workspaceBackupPath), true);
|
||||
});
|
||||
});
|
||||
|
||||
suite('getBackups', () => {
|
||||
test('("file") - text file', async () => {
|
||||
await service.backup(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).createSnapshot(false));
|
||||
|
||||
@@ -8,7 +8,7 @@ import * as resources from 'vs/base/common/resources';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import * as errors from 'vs/base/common/errors';
|
||||
import { Disposable, IDisposable, dispose, toDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
|
||||
import { RunOnceScheduler, runWhenIdle } from 'vs/base/common/async';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { FileChangeType, FileChangesEvent, IFileService, whenProviderRegistered, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
|
||||
import { ConfigurationModel, ConfigurationModelParser, UserSettings } from 'vs/platform/configuration/common/configurationModels';
|
||||
import { WorkspaceConfigurationModelParser, StandaloneConfigurationModelParser } from 'vs/workbench/services/configuration/common/configurationModels';
|
||||
@@ -26,13 +26,14 @@ import { hash } from 'vs/base/common/hash';
|
||||
|
||||
export class UserConfiguration extends Disposable {
|
||||
|
||||
private readonly _onDidInitializeCompleteConfiguration: Emitter<void> = this._register(new Emitter<void>());
|
||||
private readonly _onDidChangeConfiguration: Emitter<ConfigurationModel> = this._register(new Emitter<ConfigurationModel>());
|
||||
readonly onDidChangeConfiguration: Event<ConfigurationModel> = this._onDidChangeConfiguration.event;
|
||||
|
||||
private readonly userConfiguration: MutableDisposable<UserSettings | FileServiceBasedConfiguration> = this._register(new MutableDisposable<UserSettings | FileServiceBasedConfiguration>());
|
||||
private readonly reloadConfigurationScheduler: RunOnceScheduler;
|
||||
|
||||
get hasTasksLoaded(): boolean { return this.userConfiguration.value instanceof FileServiceBasedConfiguration; }
|
||||
|
||||
constructor(
|
||||
private readonly userSettingsResource: URI,
|
||||
private readonly scopes: ConfigurationScope[] | undefined,
|
||||
@@ -42,9 +43,6 @@ export class UserConfiguration extends Disposable {
|
||||
this.userConfiguration.value = new UserSettings(this.userSettingsResource, this.scopes, this.fileService);
|
||||
this._register(this.userConfiguration.value.onDidChange(() => this.reloadConfigurationScheduler.schedule()));
|
||||
this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.reload().then(configurationModel => this._onDidChangeConfiguration.fire(configurationModel)), 50));
|
||||
|
||||
runWhenIdle(() => this._onDidInitializeCompleteConfiguration.fire(), 5000);
|
||||
this._register(Event.once(this._onDidInitializeCompleteConfiguration.event)(() => this.reloadConfigurationScheduler.schedule()));
|
||||
}
|
||||
|
||||
async initialize(): Promise<ConfigurationModel> {
|
||||
@@ -52,13 +50,22 @@ export class UserConfiguration extends Disposable {
|
||||
}
|
||||
|
||||
async reload(): Promise<ConfigurationModel> {
|
||||
if (!(this.userConfiguration.value instanceof FileServiceBasedConfiguration)) {
|
||||
const folder = resources.dirname(this.userSettingsResource);
|
||||
const standAloneConfigurationResources: [string, URI][] = [TASKS_CONFIGURATION_KEY].map(name => ([name, resources.joinPath(folder, `${name}.json`)]));
|
||||
this.userConfiguration.value = new FileServiceBasedConfiguration(folder.toString(), [this.userSettingsResource], standAloneConfigurationResources, this.scopes, this.fileService);
|
||||
if (this.hasTasksLoaded) {
|
||||
return this.userConfiguration.value!.loadConfiguration();
|
||||
}
|
||||
|
||||
const folder = resources.dirname(this.userSettingsResource);
|
||||
const standAloneConfigurationResources: [string, URI][] = [TASKS_CONFIGURATION_KEY].map(name => ([name, resources.joinPath(folder, `${name}.json`)]));
|
||||
const fileServiceBasedConfiguration = new FileServiceBasedConfiguration(folder.toString(), [this.userSettingsResource], standAloneConfigurationResources, this.scopes, this.fileService);
|
||||
const configurationModel = await fileServiceBasedConfiguration.loadConfiguration();
|
||||
this.userConfiguration.value = fileServiceBasedConfiguration;
|
||||
|
||||
// Check for value because userConfiguration might have been disposed.
|
||||
if (this.userConfiguration.value) {
|
||||
this._register(this.userConfiguration.value.onDidChange(() => this.reloadConfigurationScheduler.schedule()));
|
||||
}
|
||||
return this.userConfiguration.value!.loadConfiguration();
|
||||
|
||||
return configurationModel;
|
||||
}
|
||||
|
||||
reprocess(): ConfigurationModel {
|
||||
@@ -101,11 +108,12 @@ class FileServiceBasedConfiguration extends Disposable {
|
||||
const content = await this.fileService.readFile(resource);
|
||||
return content.value.toString();
|
||||
} catch (error) {
|
||||
if ((<FileOperationError>error).fileOperationResult !== FileOperationResult.FILE_NOT_FOUND) {
|
||||
if ((<FileOperationError>error).fileOperationResult !== FileOperationResult.FILE_NOT_FOUND
|
||||
&& (<FileOperationError>error).fileOperationResult !== FileOperationResult.FILE_NOT_DIRECTORY) {
|
||||
errors.onUnexpectedError(error);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
return '{}';
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -720,6 +728,7 @@ export class FolderConfiguration extends Disposable implements IFolderConfigurat
|
||||
this.folderConfiguration = this.cachedFolderConfiguration = new CachedFolderConfiguration(workspaceFolder.uri, configFolderRelativePath, configurationCache);
|
||||
if (workspaceFolder.uri.scheme === Schemas.file) {
|
||||
this.folderConfiguration = this.createFileServiceBasedConfiguration(fileService);
|
||||
this.folderConfigurationDisposable = this._register(this.folderConfiguration.onDidChange(e => this.onDidFolderConfigurationChange()));
|
||||
} else {
|
||||
whenProviderRegistered(workspaceFolder.uri, fileService)
|
||||
.then(() => {
|
||||
@@ -730,7 +739,6 @@ export class FolderConfiguration extends Disposable implements IFolderConfigurat
|
||||
this.onDidFolderConfigurationChange();
|
||||
});
|
||||
}
|
||||
this.folderConfigurationDisposable = this._register(this.folderConfiguration.onDidChange(e => this.onDidFolderConfigurationChange()));
|
||||
}
|
||||
|
||||
loadConfiguration(): Promise<ConfigurationModel> {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { ResourceMap } from 'vs/base/common/map';
|
||||
import { equals } from 'vs/base/common/objects';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Queue, Barrier } from 'vs/base/common/async';
|
||||
import { Queue, Barrier, runWhenIdle } from 'vs/base/common/async';
|
||||
import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
|
||||
import { IWorkspaceContextService, Workspace, WorkbenchState, IWorkspaceFolder, toWorkspaceFolders, IWorkspaceFoldersChangeEvent, WorkspaceFolder, toWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
|
||||
import { ConfigurationModel, DefaultConfigurationModel, ConfigurationChangeEvent, AllKeysConfigurationChangeEvent, mergeChanges } from 'vs/platform/configuration/common/configurationModels';
|
||||
@@ -156,7 +156,7 @@ export class WorkspaceService extends Disposable implements IConfigurationServic
|
||||
return false;
|
||||
}
|
||||
|
||||
private doUpdateFolders(foldersToAdd: IWorkspaceFolderCreationData[], foldersToRemove: URI[], index?: number): Promise<void> {
|
||||
private async doUpdateFolders(foldersToAdd: IWorkspaceFolderCreationData[], foldersToRemove: URI[], index?: number): Promise<void> {
|
||||
if (this.getWorkbenchState() !== WorkbenchState.WORKSPACE) {
|
||||
return Promise.resolve(undefined); // we need a workspace to begin with
|
||||
}
|
||||
@@ -192,13 +192,19 @@ export class WorkspaceService extends Disposable implements IConfigurationServic
|
||||
|
||||
const storedFoldersToAdd: IStoredWorkspaceFolder[] = [];
|
||||
|
||||
foldersToAdd.forEach(folderToAdd => {
|
||||
await Promise.all(foldersToAdd.map(async folderToAdd => {
|
||||
const folderURI = folderToAdd.uri;
|
||||
if (this.contains(currentWorkspaceFolderUris, folderURI)) {
|
||||
return; // already existing
|
||||
}
|
||||
try {
|
||||
const result = await this.fileService.resolve(folderURI);
|
||||
if (!result.isDirectory) {
|
||||
return;
|
||||
}
|
||||
} catch (e) { /* Ignore */ }
|
||||
storedFoldersToAdd.push(getStoredWorkspaceFolder(folderURI, folderToAdd.name, workspaceConfigFolder, slashForPath));
|
||||
});
|
||||
}));
|
||||
|
||||
// Apply to array of newStoredFolders
|
||||
if (storedFoldersToAdd.length > 0) {
|
||||
@@ -380,6 +386,15 @@ export class WorkspaceService extends Disposable implements IConfigurationServic
|
||||
if (folderChanges && (folderChanges.added.length || folderChanges.removed.length || folderChanges.changed.length)) {
|
||||
this._onDidChangeWorkspaceFolders.fire(folderChanges);
|
||||
}
|
||||
|
||||
} else {
|
||||
// Not waiting on this validation to unblock start up
|
||||
this.validateWorkspaceFoldersAndReload();
|
||||
}
|
||||
|
||||
if (!this.localUserConfiguration.hasTasksLoaded) {
|
||||
// Reload local user configuration again to load user tasks
|
||||
runWhenIdle(() => this.reloadLocalUserConfiguration().then(configurationModel => this.onLocalUserConfigurationChanged(configurationModel)), 5000);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -531,11 +546,11 @@ export class WorkspaceService extends Disposable implements IConfigurationServic
|
||||
this.triggerConfigurationChange(change, previous, ConfigurationTarget.USER);
|
||||
}
|
||||
|
||||
private onWorkspaceConfigurationChanged(): Promise<void> {
|
||||
private async onWorkspaceConfigurationChanged(): Promise<void> {
|
||||
if (this.workspace && this.workspace.configuration) {
|
||||
const previous = { data: this._configuration.toData(), workspace: this.workspace };
|
||||
const change = this._configuration.compareAndUpdateWorkspaceConfiguration(this.workspaceConfiguration.getConfiguration());
|
||||
let configuredFolders = toWorkspaceFolders(this.workspaceConfiguration.getFolders(), this.workspace.configuration);
|
||||
let configuredFolders = await this.toValidWorkspaceFolders(toWorkspaceFolders(this.workspaceConfiguration.getFolders(), this.workspace.configuration));
|
||||
const changes = this.compareFolders(this.workspace.folders, configuredFolders);
|
||||
if (changes.added.length || changes.removed.length || changes.changed.length) {
|
||||
this.workspace.folders = configuredFolders;
|
||||
@@ -600,6 +615,28 @@ export class WorkspaceService extends Disposable implements IConfigurationServic
|
||||
})]);
|
||||
}
|
||||
|
||||
private async validateWorkspaceFoldersAndReload(): Promise<void> {
|
||||
const validWorkspaceFolders = await this.toValidWorkspaceFolders(this.workspace.folders);
|
||||
const { removed } = this.compareFolders(this.workspace.folders, validWorkspaceFolders);
|
||||
if (removed.length) {
|
||||
return this.onWorkspaceConfigurationChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async toValidWorkspaceFolders(workspaceFolders: WorkspaceFolder[]): Promise<WorkspaceFolder[]> {
|
||||
const validWorkspaceFolders: WorkspaceFolder[] = [];
|
||||
for (const workspaceFolder of workspaceFolders) {
|
||||
try {
|
||||
const result = await this.fileService.resolve(workspaceFolder.uri);
|
||||
if (!result.isDirectory) {
|
||||
continue;
|
||||
}
|
||||
} catch (e) { /* Ignore */ }
|
||||
validWorkspaceFolders.push(workspaceFolder);
|
||||
}
|
||||
return validWorkspaceFolders;
|
||||
}
|
||||
|
||||
private writeConfigurationValue(key: string, value: any, target: ConfigurationTarget, overrides: IConfigurationOverrides | undefined, donotNotifyError: boolean): Promise<void> {
|
||||
if (target === ConfigurationTarget.DEFAULT) {
|
||||
return Promise.reject(new Error('Invalid configuration target'));
|
||||
|
||||
@@ -30,8 +30,8 @@ import { IJSONEditingService } from 'vs/workbench/services/configuration/common/
|
||||
import { JSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditingService';
|
||||
import { createHash } from 'crypto';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { originalFSPath } from 'vs/base/common/resources';
|
||||
import { isLinux } from 'vs/base/common/platform';
|
||||
import { originalFSPath, joinPath } from 'vs/base/common/resources';
|
||||
import { isLinux, isMacintosh } from 'vs/base/common/platform';
|
||||
import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl';
|
||||
import { RemoteAuthorityResolverService } from 'vs/platform/remote/electron-browser/remoteAuthorityResolverService';
|
||||
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
|
||||
@@ -47,6 +47,8 @@ import { IKeybindingEditingService, KeybindingsEditingService } from 'vs/workben
|
||||
import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
|
||||
class TestEnvironmentService extends NativeWorkbenchEnvironmentService {
|
||||
|
||||
@@ -719,6 +721,8 @@ suite.skip('WorkspaceConfigurationService - Folder', () => { // {{SQL CARBON EDI
|
||||
|
||||
let workspaceName = `testWorkspace${uuid.generateUuid()}`, parentResource: string, workspaceDir: string, testObject: IConfigurationService, globalSettingsFile: string, globalTasksFile: string, workspaceService: WorkspaceService;
|
||||
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
|
||||
let fileService: IFileService;
|
||||
let disposableStore: DisposableStore = new DisposableStore();
|
||||
|
||||
suiteSetup(() => {
|
||||
configurationRegistry.registerConfiguration({
|
||||
@@ -767,15 +771,18 @@ suite.skip('WorkspaceConfigurationService - Folder', () => { // {{SQL CARBON EDI
|
||||
const environmentService = new TestEnvironmentService(URI.file(parentDir));
|
||||
const remoteAgentService = instantiationService.createInstance(RemoteAgentService, {});
|
||||
instantiationService.stub(IRemoteAgentService, remoteAgentService);
|
||||
const fileService = new FileService(new NullLogService());
|
||||
fileService = new FileService(new NullLogService());
|
||||
const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService());
|
||||
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
|
||||
fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, environmentService.backupHome, diskFileSystemProvider, environmentService));
|
||||
workspaceService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, remoteAgentService);
|
||||
workspaceService = disposableStore.add(new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, remoteAgentService));
|
||||
instantiationService.stub(IWorkspaceContextService, workspaceService);
|
||||
instantiationService.stub(IConfigurationService, workspaceService);
|
||||
instantiationService.stub(IEnvironmentService, environmentService);
|
||||
|
||||
// Watch workspace configuration directory
|
||||
disposableStore.add(fileService.watch(joinPath(URI.file(workspaceDir), '.vscode')));
|
||||
|
||||
return workspaceService.initialize(convertToWorkspacePayload(URI.file(folderDir))).then(() => {
|
||||
instantiationService.stub(IFileService, fileService);
|
||||
instantiationService.stub(IKeybindingEditingService, instantiationService.createInstance(KeybindingsEditingService));
|
||||
@@ -788,9 +795,7 @@ suite.skip('WorkspaceConfigurationService - Folder', () => { // {{SQL CARBON EDI
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
if (testObject) {
|
||||
(<WorkspaceService>testObject).dispose();
|
||||
}
|
||||
disposableStore.clear();
|
||||
if (parentResource) {
|
||||
return pfs.rimraf(parentResource, pfs.RimRafMode.MOVE);
|
||||
}
|
||||
@@ -1101,6 +1106,40 @@ suite.skip('WorkspaceConfigurationService - Folder', () => { // {{SQL CARBON EDI
|
||||
fs.writeFileSync(globalTasksFile, '{ "version": "1.0.0", "tasks": [{ "taskName": "myTask" }');
|
||||
return new Promise((c) => testObject.onDidChangeConfiguration(() => c()));
|
||||
});
|
||||
|
||||
test('creating workspace settings', async () => {
|
||||
fs.writeFileSync(globalSettingsFile, '{ "configurationService.folder.testSetting": "userValue" }');
|
||||
await testObject.reloadConfiguration();
|
||||
const workspaceSettingsResource = URI.file(path.join(workspaceDir, '.vscode', 'settings.json'));
|
||||
await new Promise(async (c) => {
|
||||
const disposable = testObject.onDidChangeConfiguration(e => {
|
||||
assert.ok(e.affectsConfiguration('configurationService.folder.testSetting'));
|
||||
assert.equal(testObject.getValue('configurationService.folder.testSetting'), 'workspaceValue');
|
||||
disposable.dispose();
|
||||
c();
|
||||
});
|
||||
await fileService.writeFile(workspaceSettingsResource, VSBuffer.fromString('{ "configurationService.folder.testSetting": "workspaceValue" }'));
|
||||
});
|
||||
});
|
||||
|
||||
test('deleting workspace settings', async () => {
|
||||
if (!isMacintosh) {
|
||||
return;
|
||||
}
|
||||
fs.writeFileSync(globalSettingsFile, '{ "configurationService.folder.testSetting": "userValue" }');
|
||||
const workspaceSettingsResource = URI.file(path.join(workspaceDir, '.vscode', 'settings.json'));
|
||||
await fileService.writeFile(workspaceSettingsResource, VSBuffer.fromString('{ "configurationService.folder.testSetting": "workspaceValue" }'));
|
||||
await testObject.reloadConfiguration();
|
||||
await new Promise(async (c) => {
|
||||
const disposable = testObject.onDidChangeConfiguration(e => {
|
||||
assert.ok(e.affectsConfiguration('configurationService.folder.testSetting'));
|
||||
assert.equal(testObject.getValue('configurationService.folder.testSetting'), 'userValue');
|
||||
disposable.dispose();
|
||||
c();
|
||||
});
|
||||
await fileService.del(workspaceSettingsResource);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite.skip('WorkspaceConfigurationService-Multiroot', () => { // {{SQL CARBON EDIT}} skip suite
|
||||
|
||||
@@ -584,10 +584,6 @@ class MockQuickInputService implements IQuickInputService {
|
||||
cancel(): Promise<void> {
|
||||
throw new Error('not implemented.');
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
throw new Error('not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
class MockInputsConfigurationService extends TestConfigurationService {
|
||||
|
||||
@@ -22,6 +22,7 @@ import { ExtHostExtensionService } from 'vs/workbench/api/node/extHostExtensionS
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { promisify } from 'util';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
|
||||
interface ConnectionResult {
|
||||
proxy: string;
|
||||
@@ -318,14 +319,14 @@ function createPatchedModules(configProvider: ExtHostConfigProvider, resolveProx
|
||||
override: assign({}, http, patches(http, resolveProxy, { config: 'override' }, certSetting, true)),
|
||||
onRequest: assign({}, http, patches(http, resolveProxy, proxySetting, certSetting, true)),
|
||||
default: assign(http, patches(http, resolveProxy, proxySetting, certSetting, false)) // run last
|
||||
},
|
||||
} as Record<string, typeof http>,
|
||||
https: {
|
||||
off: assign({}, https, patches(https, resolveProxy, { config: 'off' }, certSetting, true)),
|
||||
on: assign({}, https, patches(https, resolveProxy, { config: 'on' }, certSetting, true)),
|
||||
override: assign({}, https, patches(https, resolveProxy, { config: 'override' }, certSetting, true)),
|
||||
onRequest: assign({}, https, patches(https, resolveProxy, proxySetting, certSetting, true)),
|
||||
default: assign(https, patches(https, resolveProxy, proxySetting, certSetting, false)) // run last
|
||||
},
|
||||
} as Record<string, typeof https>,
|
||||
tls: assign(tls, tlsPatches(tls))
|
||||
};
|
||||
}
|
||||
@@ -411,6 +412,7 @@ function tlsPatches(originals: typeof tls) {
|
||||
}
|
||||
}
|
||||
|
||||
const modulesCache = new Map<IExtensionDescription | undefined, { http?: typeof http, https?: typeof https }>();
|
||||
function configureModuleLoading(extensionService: ExtHostExtensionService, lookup: ReturnType<typeof createPatchedModules>): Promise<void> {
|
||||
return extensionService.getExtensionPathIndex()
|
||||
.then(extensionPaths => {
|
||||
@@ -427,10 +429,18 @@ function configureModuleLoading(extensionService: ExtHostExtensionService, looku
|
||||
|
||||
const modules = lookup[request];
|
||||
const ext = extensionPaths.findSubstr(URI.file(parent.filename).fsPath);
|
||||
if (ext && ext.enableProposedApi) {
|
||||
return (modules as any)[(<any>ext).proxySupport] || modules.onRequest;
|
||||
let cache = modulesCache.get(ext);
|
||||
if (!cache) {
|
||||
modulesCache.set(ext, cache = {});
|
||||
}
|
||||
return modules.default;
|
||||
if (!cache[request]) {
|
||||
let mod = modules.default;
|
||||
if (ext && ext.enableProposedApi) {
|
||||
mod = (modules as any)[(<any>ext).proxySupport] || modules.onRequest;
|
||||
}
|
||||
cache[request] = <any>{ ...mod }; // Copy to work around #93167.
|
||||
}
|
||||
return cache[request];
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,25 +15,35 @@ import { QuickInputController } from 'vs/base/parts/quickinput/browser/quickInpu
|
||||
import { QuickInputService as BaseQuickInputService } from 'vs/platform/quickinput/browser/quickInput';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
|
||||
import { InQuickPickContextKey } from 'vs/workbench/browser/quickaccess';
|
||||
|
||||
export class QuickInputService extends BaseQuickInputService {
|
||||
|
||||
private readonly inQuickInputContext = InQuickPickContextKey.bindTo(this.contextKeyService);
|
||||
|
||||
constructor(
|
||||
@IEnvironmentService private environmentService: IEnvironmentService,
|
||||
@IConfigurationService private configurationService: IConfigurationService,
|
||||
@IEnvironmentService private readonly environmentService: IEnvironmentService,
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IKeybindingService private keybindingService: IKeybindingService,
|
||||
@IKeybindingService private readonly keybindingService: IKeybindingService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IAccessibilityService accessibilityService: IAccessibilityService,
|
||||
@ILayoutService protected layoutService: ILayoutService
|
||||
@ILayoutService protected readonly layoutService: ILayoutService,
|
||||
) {
|
||||
super(instantiationService, contextKeyService, themeService, accessibilityService, layoutService);
|
||||
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
private registerListeners(): void {
|
||||
this._register(this.onShow(() => this.inQuickInputContext.set(true)));
|
||||
this._register(this.onHide(() => this.inQuickInputContext.set(false)));
|
||||
}
|
||||
|
||||
protected createController(): QuickInputController {
|
||||
return super.createController(this.layoutService, {
|
||||
ignoreFocusOut: () => this.environmentService.args['sticky-quickopen'] || !this.configurationService.getValue('workbench.quickOpen.closeOnFocusLost'),
|
||||
ignoreFocusOut: () => this.environmentService.args['sticky-quickinput'] || !this.configurationService.getValue('workbench.quickOpen.closeOnFocusLost'),
|
||||
backKeybindingLabel: () => this.keybindingService.lookupKeybinding('workbench.action.quickInputBack')?.getLabel() || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -316,12 +316,12 @@ export interface TextSearchContext {
|
||||
export type TextSearchResult = TextSearchMatch | TextSearchContext;
|
||||
|
||||
/**
|
||||
* A FileSearchProvider provides search results for files in the given folder that match a query string. It can be invoked by quickopen or other extensions.
|
||||
* A FileSearchProvider provides search results for files in the given folder that match a query string. It can be invoked by quickaccess or other extensions.
|
||||
*
|
||||
* A FileSearchProvider is the more powerful of two ways to implement file search in VS Code. Use a FileSearchProvider if you wish to search within a folder for
|
||||
* all files that match the user's query.
|
||||
*
|
||||
* The FileSearchProvider will be invoked on every keypress in quickopen. When `workspace.findFiles` is called, it will be invoked with an empty query string,
|
||||
* The FileSearchProvider will be invoked on every keypress in quickaccess. When `workspace.findFiles` is called, it will be invoked with an empty query string,
|
||||
* and in that case, every file in the folder should be returned.
|
||||
*/
|
||||
export interface FileSearchProvider {
|
||||
|
||||
@@ -77,7 +77,7 @@ export class FileWalker {
|
||||
this.errors = [];
|
||||
|
||||
if (this.filePattern) {
|
||||
this.normalizedFilePatternLowercase = prepareQuery(this.filePattern).lowercase;
|
||||
this.normalizedFilePatternLowercase = prepareQuery(this.filePattern).valueLowercase;
|
||||
}
|
||||
|
||||
this.globalExcludePattern = config.excludePattern && glob.parse(config.excludePattern);
|
||||
|
||||
@@ -183,7 +183,7 @@ export class SearchService implements IRawSearchService {
|
||||
const sortSW = (typeof config.maxResults !== 'number' || config.maxResults > 0) && StopWatch.create(false);
|
||||
return this.sortResults(config, results, scorerCache, token)
|
||||
.then<[ISerializedSearchSuccess, IRawFileMatch[]]>(sortedResults => {
|
||||
// sortingTime: -1 indicates a "sorted" search that was not sorted, i.e. populating the cache when quickopen is opened.
|
||||
// sortingTime: -1 indicates a "sorted" search that was not sorted, i.e. populating the cache when quickaccess is opened.
|
||||
// Contrasting with findFiles which is not sorted and will have sortingTime: undefined
|
||||
const sortingTime = sortSW ? sortSW.elapsed() : -1;
|
||||
|
||||
@@ -312,7 +312,7 @@ export class SearchService implements IRawSearchService {
|
||||
|
||||
// Pattern match on results
|
||||
const results: IRawFileMatch[] = [];
|
||||
const normalizedSearchValueLowercase = prepareQuery(searchValue).lowercase;
|
||||
const normalizedSearchValueLowercase = prepareQuery(searchValue).valueLowercase;
|
||||
for (const entry of cachedEntries) {
|
||||
|
||||
// Check if this entry is a match for the search value
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Emitter } from 'vs/base/common/event';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel';
|
||||
import { dispose, IDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { ITextFileEditorModel, ITextFileEditorModelManager, ITextFileEditorModelLoadOrCreateOptions, ITextFileLoadEvent, ITextFileSaveEvent, ITextFileSaveParticipant, IResolvedTextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { ITextFileEditorModel, ITextFileEditorModelManager, ITextFileEditorModelLoadOrCreateOptions, ITextFileLoadEvent, ITextFileSaveEvent, ITextFileSaveParticipant } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ResourceMap } from 'vs/base/common/map';
|
||||
@@ -388,7 +388,7 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE
|
||||
return this.saveParticipants.addSaveParticipant(participant);
|
||||
}
|
||||
|
||||
runSaveParticipants(model: IResolvedTextFileEditorModel, context: { reason: SaveReason; }, token: CancellationToken): Promise<void> {
|
||||
runSaveParticipants(model: ITextFileEditorModel, context: { reason: SaveReason; }, token: CancellationToken): Promise<void> {
|
||||
return this.saveParticipants.participate(model, context, token);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { raceCancellation } from 'vs/base/common/async';
|
||||
import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
|
||||
import { ITextFileSaveParticipant, IResolvedTextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { ITextFileSaveParticipant, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles';
|
||||
import { SaveReason } from 'vs/workbench/common/editor';
|
||||
import { IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
@@ -29,7 +29,7 @@ export class TextFileSaveParticipant extends Disposable {
|
||||
return toDisposable(() => this.saveParticipants.splice(this.saveParticipants.indexOf(participant), 1));
|
||||
}
|
||||
|
||||
participate(model: IResolvedTextFileEditorModel, context: { reason: SaveReason; }, token: CancellationToken): Promise<void> {
|
||||
participate(model: ITextFileEditorModel, context: { reason: SaveReason; }, token: CancellationToken): Promise<void> {
|
||||
const cts = new CancellationTokenSource(token);
|
||||
|
||||
return this.progressService.withProgress({
|
||||
@@ -40,10 +40,10 @@ export class TextFileSaveParticipant extends Disposable {
|
||||
}, async progress => {
|
||||
|
||||
// undoStop before participation
|
||||
model.textEditorModel.pushStackElement();
|
||||
model.textEditorModel?.pushStackElement();
|
||||
|
||||
for (const saveParticipant of this.saveParticipants) {
|
||||
if (cts.token.isCancellationRequested) {
|
||||
if (cts.token.isCancellationRequested || !model.textEditorModel /* disposed */) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ export class TextFileSaveParticipant extends Disposable {
|
||||
}
|
||||
|
||||
// undoStop after participation
|
||||
model.textEditorModel.pushStackElement();
|
||||
model.textEditorModel?.pushStackElement();
|
||||
}, () => {
|
||||
// user cancel
|
||||
cts.dispose(true);
|
||||
|
||||
@@ -310,7 +310,7 @@ export interface ITextFileSaveParticipant {
|
||||
* before it is being saved to disk.
|
||||
*/
|
||||
participate(
|
||||
model: IResolvedTextFileEditorModel,
|
||||
model: ITextFileEditorModel,
|
||||
context: { reason: SaveReason },
|
||||
progress: IProgress<IProgressStep>,
|
||||
token: CancellationToken
|
||||
@@ -347,7 +347,10 @@ export interface ITextFileEditorModelManager {
|
||||
*/
|
||||
addSaveParticipant(participant: ITextFileSaveParticipant): IDisposable;
|
||||
|
||||
runSaveParticipants(model: IResolvedTextFileEditorModel, context: { reason: SaveReason; }, token: CancellationToken): Promise<void>
|
||||
/**
|
||||
* Runs the registered save participants on the provided model.
|
||||
*/
|
||||
runSaveParticipants(model: ITextFileEditorModel, context: { reason: SaveReason; }, token: CancellationToken): Promise<void>
|
||||
|
||||
disposeModel(model: ITextFileEditorModel): void;
|
||||
}
|
||||
|
||||
@@ -237,8 +237,7 @@ function _processIconThemeDocument(id: string, iconThemeDocumentLocation: URI, i
|
||||
qualifier = baseThemeClassName + ' ' + qualifier;
|
||||
}
|
||||
|
||||
const expanded = '.monaco-tree-row.expanded'; // workaround for #11453
|
||||
const expanded2 = '.monaco-tl-twistie.collapsible:not(.collapsed) + .monaco-tl-contents'; // new tree
|
||||
const expanded = '.monaco-tl-twistie.collapsible:not(.collapsed) + .monaco-tl-contents';
|
||||
|
||||
if (associations.folder) {
|
||||
addSelector(`${qualifier} .folder-icon::before`, associations.folder);
|
||||
@@ -247,7 +246,6 @@ function _processIconThemeDocument(id: string, iconThemeDocumentLocation: URI, i
|
||||
|
||||
if (associations.folderExpanded) {
|
||||
addSelector(`${qualifier} ${expanded} .folder-icon::before`, associations.folderExpanded);
|
||||
addSelector(`${qualifier} ${expanded2} .folder-icon::before`, associations.folderExpanded);
|
||||
result.hasFolderIcons = true;
|
||||
}
|
||||
|
||||
@@ -261,7 +259,6 @@ function _processIconThemeDocument(id: string, iconThemeDocumentLocation: URI, i
|
||||
|
||||
if (rootFolderExpanded) {
|
||||
addSelector(`${qualifier} ${expanded} .rootfolder-icon::before`, rootFolderExpanded);
|
||||
addSelector(`${qualifier} ${expanded2} .rootfolder-icon::before`, rootFolderExpanded);
|
||||
result.hasFolderIcons = true;
|
||||
}
|
||||
|
||||
@@ -281,7 +278,6 @@ function _processIconThemeDocument(id: string, iconThemeDocumentLocation: URI, i
|
||||
if (folderNamesExpanded) {
|
||||
for (let folderName in folderNamesExpanded) {
|
||||
addSelector(`${qualifier} ${expanded} .${escapeCSS(folderName.toLowerCase())}-name-folder-icon.folder-icon::before`, folderNamesExpanded[folderName]);
|
||||
addSelector(`${qualifier} ${expanded2} .${escapeCSS(folderName.toLowerCase())}-name-folder-icon.folder-icon::before`, folderNamesExpanded[folderName]);
|
||||
result.hasFolderIcons = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { convertSettings } from 'vs/workbench/services/themes/common/themeCompat
|
||||
import * as nls from 'vs/nls';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import * as objects from 'vs/base/common/objects';
|
||||
import * as arrays from 'vs/base/common/arrays';
|
||||
import * as resources from 'vs/base/common/resources';
|
||||
import { Extensions as ColorRegistryExtensions, IColorRegistry, ColorIdentifier, editorBackground, editorForeground } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { ThemeType, ITokenStyle } from 'vs/platform/theme/common/themeService';
|
||||
@@ -19,7 +20,7 @@ import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { parse as parsePList } from 'vs/workbench/services/themes/common/plistParser';
|
||||
import { startsWith } from 'vs/base/common/strings';
|
||||
import { TokenStyle, ProbeScope, TokenStylingRule, getTokenClassificationRegistry, TokenStyleValue, TokenStyleData } from 'vs/platform/theme/common/tokenClassificationRegistry';
|
||||
import { TokenStyle, ProbeScope, TokenStylingRule, getTokenClassificationRegistry, TokenStyleValue, TokenStyleData, parseClassifierString } from 'vs/platform/theme/common/tokenClassificationRegistry';
|
||||
import { MatcherWithPriority, Matcher, createMatchers } from 'vs/workbench/services/themes/common/textMateScopeMatcher';
|
||||
import { IExtensionResourceLoaderService } from 'vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader';
|
||||
import { CharCode } from 'vs/base/common/charCode';
|
||||
@@ -67,7 +68,7 @@ export class ColorThemeData implements IWorkbenchColorTheme {
|
||||
private colorMap: IColorMap = {};
|
||||
private customColorMap: IColorMap = {};
|
||||
|
||||
private tokenStylingRules: TokenStylingRule[] | undefined = undefined; // undefined if the theme has no tokenStylingRules section
|
||||
private tokenStylingRules: TokenStylingRule[] = [];
|
||||
private customTokenStylingRules: TokenStylingRule[] = [];
|
||||
|
||||
private themeTokenScopeMatchers: Matcher<ProbeScope>[] | undefined;
|
||||
@@ -137,7 +138,7 @@ export class ColorThemeData implements IWorkbenchColorTheme {
|
||||
return color;
|
||||
}
|
||||
|
||||
public getTokenStyle(type: string, modifiers: string[], useDefault = true, definitions: TokenStyleDefinitions = {}): TokenStyle | undefined {
|
||||
private getTokenStyle(type: string, modifiers: string[], language: string, useDefault = true, definitions: TokenStyleDefinitions = {}): TokenStyle | undefined {
|
||||
let result: any = {
|
||||
foreground: undefined,
|
||||
bold: undefined,
|
||||
@@ -169,36 +170,33 @@ export class ColorThemeData implements IWorkbenchColorTheme {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.tokenStylingRules === undefined) {
|
||||
for (const rule of tokenClassificationRegistry.getTokenStylingDefaultRules()) {
|
||||
const matchScore = rule.selector.match(type, modifiers);
|
||||
if (matchScore >= 0) {
|
||||
let style: TokenStyle | undefined;
|
||||
if (rule.defaults.scopesToProbe) {
|
||||
style = this.resolveScopes(rule.defaults.scopesToProbe);
|
||||
if (style) {
|
||||
_processStyle(matchScore, style, rule.defaults.scopesToProbe);
|
||||
}
|
||||
}
|
||||
if (!style && useDefault !== false) {
|
||||
const tokenStyleValue = rule.defaults[this.type];
|
||||
style = this.resolveTokenStyleValue(tokenStyleValue);
|
||||
if (style) {
|
||||
_processStyle(matchScore, style, tokenStyleValue!);
|
||||
}
|
||||
for (const rule of tokenClassificationRegistry.getTokenStylingDefaultRules()) {
|
||||
const matchScore = rule.selector.match(type, modifiers, language);
|
||||
if (matchScore >= 0) {
|
||||
let style: TokenStyle | undefined;
|
||||
if (rule.defaults.scopesToProbe) {
|
||||
style = this.resolveScopes(rule.defaults.scopesToProbe);
|
||||
if (style) {
|
||||
_processStyle(matchScore, style, rule.defaults.scopesToProbe);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const rule of this.tokenStylingRules) {
|
||||
const matchScore = rule.selector.match(type, modifiers);
|
||||
if (matchScore >= 0) {
|
||||
_processStyle(matchScore, rule.style, rule);
|
||||
if (!style && useDefault !== false) {
|
||||
const tokenStyleValue = rule.defaults[this.type];
|
||||
style = this.resolveTokenStyleValue(tokenStyleValue);
|
||||
if (style) {
|
||||
_processStyle(matchScore, style, tokenStyleValue!);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const rule of this.tokenStylingRules) {
|
||||
const matchScore = rule.selector.match(type, modifiers, language);
|
||||
if (matchScore >= 0) {
|
||||
_processStyle(matchScore, rule.style, rule);
|
||||
}
|
||||
}
|
||||
for (const rule of this.customTokenStylingRules) {
|
||||
const matchScore = rule.selector.match(type, modifiers);
|
||||
const matchScore = rule.selector.match(type, modifiers, language);
|
||||
if (matchScore >= 0) {
|
||||
_processStyle(matchScore, rule.style, rule);
|
||||
}
|
||||
@@ -210,12 +208,12 @@ export class ColorThemeData implements IWorkbenchColorTheme {
|
||||
/**
|
||||
* @param tokenStyleValue Resolve a tokenStyleValue in the context of a theme
|
||||
*/
|
||||
private resolveTokenStyleValue(tokenStyleValue: TokenStyleValue | undefined): TokenStyle | undefined {
|
||||
public resolveTokenStyleValue(tokenStyleValue: TokenStyleValue | undefined): TokenStyle | undefined {
|
||||
if (tokenStyleValue === undefined) {
|
||||
return undefined;
|
||||
} else if (typeof tokenStyleValue === 'string') {
|
||||
const [type, ...modifiers] = tokenStyleValue.split('.');
|
||||
return this.getTokenStyle(type, modifiers);
|
||||
const { type, modifiers, language } = parseClassifierString(tokenStyleValue);
|
||||
return this.getTokenStyle(type, modifiers, language || '');
|
||||
} else if (typeof tokenStyleValue === 'object') {
|
||||
return tokenStyleValue;
|
||||
}
|
||||
@@ -231,16 +229,13 @@ export class ColorThemeData implements IWorkbenchColorTheme {
|
||||
index.add(rule.settings.background);
|
||||
});
|
||||
|
||||
if (this.tokenStylingRules) {
|
||||
this.tokenStylingRules.forEach(r => index.add(r.style.foreground));
|
||||
} else {
|
||||
tokenClassificationRegistry.getTokenStylingDefaultRules().forEach(r => {
|
||||
const defaultColor = r.defaults[this.type];
|
||||
if (defaultColor && typeof defaultColor === 'object') {
|
||||
index.add(defaultColor.foreground);
|
||||
}
|
||||
});
|
||||
}
|
||||
this.tokenStylingRules.forEach(r => index.add(r.style.foreground));
|
||||
tokenClassificationRegistry.getTokenStylingDefaultRules().forEach(r => {
|
||||
const defaultColor = r.defaults[this.type];
|
||||
if (defaultColor && typeof defaultColor === 'object') {
|
||||
index.add(defaultColor.foreground);
|
||||
}
|
||||
});
|
||||
this.customTokenStylingRules.forEach(r => index.add(r.style.foreground));
|
||||
|
||||
this.tokenColorIndex = index;
|
||||
@@ -252,8 +247,9 @@ export class ColorThemeData implements IWorkbenchColorTheme {
|
||||
return this.getTokenColorIndex().asArray();
|
||||
}
|
||||
|
||||
public getTokenStyleMetadata(type: string, modifiers: string[], useDefault = true, definitions: TokenStyleDefinitions = {}): ITokenStyle | undefined {
|
||||
const style = this.getTokenStyle(type, modifiers, useDefault, definitions);
|
||||
public getTokenStyleMetadata(typeWithLanguage: string, modifiers: string[], defaultLanguage: string, useDefault = true, definitions: TokenStyleDefinitions = {}): ITokenStyle | undefined {
|
||||
const { type, language } = parseClassifierString(typeWithLanguage);
|
||||
let style = this.getTokenStyle(type, modifiers, language || defaultLanguage, useDefault, definitions);
|
||||
if (!style) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -270,7 +266,7 @@ export class ColorThemeData implements IWorkbenchColorTheme {
|
||||
if (this.customTokenStylingRules.indexOf(rule) !== -1) {
|
||||
return 'setting';
|
||||
}
|
||||
if (this.tokenStylingRules && this.tokenStylingRules.indexOf(rule) !== -1) {
|
||||
if (this.tokenStylingRules.indexOf(rule) !== -1) {
|
||||
return 'theme';
|
||||
}
|
||||
return undefined;
|
||||
@@ -442,7 +438,7 @@ export class ColorThemeData implements IWorkbenchColorTheme {
|
||||
const result = {
|
||||
colors: {},
|
||||
textMateRules: [],
|
||||
stylingRules: undefined,
|
||||
stylingRules: [],
|
||||
semanticHighlighting: false
|
||||
};
|
||||
return _loadColorTheme(extensionResourceLoaderService, this.location, result).then(_ => {
|
||||
@@ -473,6 +469,7 @@ export class ColorThemeData implements IWorkbenchColorTheme {
|
||||
settingsId: this.settingsId,
|
||||
selector: this.id.split(' ').join('.'), // to not break old clients
|
||||
themeTokenColors: this.themeTokenColors,
|
||||
tokenStylingRules: this.tokenStylingRules.map(TokenStylingRule.toJSONObject),
|
||||
extensionData: this.extensionData,
|
||||
themeSemanticHighlighting: this.themeSemanticHighlighting,
|
||||
colorMap: colorMapData,
|
||||
@@ -482,7 +479,10 @@ export class ColorThemeData implements IWorkbenchColorTheme {
|
||||
}
|
||||
|
||||
hasEqualData(other: ColorThemeData) {
|
||||
return objects.equals(this.colorMap, other.colorMap) && objects.equals(this.themeTokenColors, other.themeTokenColors) && this.themeSemanticHighlighting === other.themeSemanticHighlighting;
|
||||
return objects.equals(this.colorMap, other.colorMap)
|
||||
&& objects.equals(this.themeTokenColors, other.themeTokenColors)
|
||||
&& arrays.equals(this.tokenStylingRules, other.tokenStylingRules, TokenStylingRule.equals)
|
||||
&& this.themeSemanticHighlighting === other.themeSemanticHighlighting;
|
||||
}
|
||||
|
||||
get baseTheme(): string {
|
||||
@@ -535,6 +535,17 @@ export class ColorThemeData implements IWorkbenchColorTheme {
|
||||
case 'id': case 'label': case 'settingsId': case 'extensionData': case 'watch': case 'themeSemanticHighlighting':
|
||||
(theme as any)[key] = data[key];
|
||||
break;
|
||||
case 'tokenStylingRules':
|
||||
const rulesData = data[key];
|
||||
if (Array.isArray(rulesData)) {
|
||||
for (let d of rulesData) {
|
||||
const rule = TokenStylingRule.fromJSONObject(tokenClassificationRegistry, d);
|
||||
if (rule) {
|
||||
theme.tokenStylingRules.push(rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!theme.id || !theme.settingsId) {
|
||||
@@ -576,57 +587,60 @@ function toCSSSelector(extensionId: string, path: string) {
|
||||
return str;
|
||||
}
|
||||
|
||||
function _loadColorTheme(extensionResourceLoaderService: IExtensionResourceLoaderService, themeLocation: URI, result: { textMateRules: ITextMateThemingRule[], colors: IColorMap, stylingRules: TokenStylingRule[] | undefined, semanticHighlighting: boolean }): Promise<any> {
|
||||
async function _loadColorTheme(extensionResourceLoaderService: IExtensionResourceLoaderService, themeLocation: URI, result: { textMateRules: ITextMateThemingRule[], colors: IColorMap, stylingRules: TokenStylingRule[], semanticHighlighting: boolean }): Promise<any> {
|
||||
if (resources.extname(themeLocation) === '.json') {
|
||||
return extensionResourceLoaderService.readExtensionResource(themeLocation).then(content => {
|
||||
let errors: Json.ParseError[] = [];
|
||||
let contentValue = Json.parse(content, errors);
|
||||
if (errors.length > 0) {
|
||||
return Promise.reject(new Error(nls.localize('error.cannotparsejson', "Problems parsing JSON theme file: {0}", errors.map(e => getParseErrorMessage(e.error)).join(', '))));
|
||||
} else if (Json.getNodeType(contentValue) !== 'object') {
|
||||
return Promise.reject(new Error(nls.localize('error.invalidformat', "Invalid format for JSON theme file: Object expected.")));
|
||||
const content = await extensionResourceLoaderService.readExtensionResource(themeLocation);
|
||||
let errors: Json.ParseError[] = [];
|
||||
let contentValue = Json.parse(content, errors);
|
||||
if (errors.length > 0) {
|
||||
return Promise.reject(new Error(nls.localize('error.cannotparsejson', "Problems parsing JSON theme file: {0}", errors.map(e => getParseErrorMessage(e.error)).join(', '))));
|
||||
} else if (Json.getNodeType(contentValue) !== 'object') {
|
||||
return Promise.reject(new Error(nls.localize('error.invalidformat', "Invalid format for JSON theme file: Object expected.")));
|
||||
}
|
||||
if (contentValue.include) {
|
||||
await _loadColorTheme(extensionResourceLoaderService, resources.joinPath(resources.dirname(themeLocation), contentValue.include), result);
|
||||
}
|
||||
if (Array.isArray(contentValue.settings)) {
|
||||
convertSettings(contentValue.settings, result);
|
||||
return null;
|
||||
}
|
||||
result.semanticHighlighting = result.semanticHighlighting || contentValue.semanticHighlighting;
|
||||
let colors = contentValue.colors;
|
||||
if (colors) {
|
||||
if (typeof colors !== 'object') {
|
||||
return Promise.reject(new Error(nls.localize({ key: 'error.invalidformat.colors', comment: ['{0} will be replaced by a path. Values in quotes should not be translated.'] }, "Problem parsing color theme file: {0}. Property 'colors' is not of type 'object'.", themeLocation.toString())));
|
||||
}
|
||||
let includeCompletes: Promise<any> = Promise.resolve(null);
|
||||
if (contentValue.include) {
|
||||
includeCompletes = _loadColorTheme(extensionResourceLoaderService, resources.joinPath(resources.dirname(themeLocation), contentValue.include), result);
|
||||
// new JSON color themes format
|
||||
for (let colorId in colors) {
|
||||
let colorHex = colors[colorId];
|
||||
if (typeof colorHex === 'string') { // ignore colors tht are null
|
||||
result.colors[colorId] = Color.fromHex(colors[colorId]);
|
||||
}
|
||||
}
|
||||
return includeCompletes.then(_ => {
|
||||
if (Array.isArray(contentValue.settings)) {
|
||||
convertSettings(contentValue.settings, result);
|
||||
return null;
|
||||
}
|
||||
result.semanticHighlighting = result.semanticHighlighting || contentValue.semanticHighlighting;
|
||||
let colors = contentValue.colors;
|
||||
if (colors) {
|
||||
if (typeof colors !== 'object') {
|
||||
return Promise.reject(new Error(nls.localize({ key: 'error.invalidformat.colors', comment: ['{0} will be replaced by a path. Values in quotes should not be translated.'] }, "Problem parsing color theme file: {0}. Property 'colors' is not of type 'object'.", themeLocation.toString())));
|
||||
}
|
||||
// new JSON color themes format
|
||||
for (let colorId in colors) {
|
||||
let colorHex = colors[colorId];
|
||||
if (typeof colorHex === 'string') { // ignore colors tht are null
|
||||
result.colors[colorId] = Color.fromHex(colors[colorId]);
|
||||
}
|
||||
}
|
||||
let tokenColors = contentValue.tokenColors;
|
||||
if (tokenColors) {
|
||||
if (Array.isArray(tokenColors)) {
|
||||
result.textMateRules.push(...tokenColors);
|
||||
} else if (typeof tokenColors === 'string') {
|
||||
await _loadSyntaxTokens(extensionResourceLoaderService, resources.joinPath(resources.dirname(themeLocation), tokenColors), result);
|
||||
} else {
|
||||
return Promise.reject(new Error(nls.localize({ key: 'error.invalidformat.tokenColors', comment: ['{0} will be replaced by a path. Values in quotes should not be translated.'] }, "Problem parsing color theme file: {0}. Property 'tokenColors' should be either an array specifying colors or a path to a TextMate theme file", themeLocation.toString())));
|
||||
}
|
||||
}
|
||||
let semanticTokenColors = contentValue.semanticTokenColors;
|
||||
if (semanticTokenColors && typeof semanticTokenColors === 'object') {
|
||||
for (let key in semanticTokenColors) {
|
||||
try {
|
||||
const rule = readCustomTokenStyleRule(key, semanticTokenColors[key]);
|
||||
if (rule) {
|
||||
result.stylingRules.push(rule);
|
||||
}
|
||||
} catch (e) {
|
||||
return Promise.reject(new Error(nls.localize({ key: 'error.invalidformat.semanticTokenColors', comment: ['{0} will be replaced by a path. Values in quotes should not be translated.'] }, "Problem parsing color theme file: {0}. Property 'semanticTokenColors' conatains a invalid selector", themeLocation.toString())));
|
||||
}
|
||||
let tokenColors = contentValue.tokenColors;
|
||||
if (tokenColors) {
|
||||
if (Array.isArray(tokenColors)) {
|
||||
result.textMateRules.push(...tokenColors);
|
||||
return null;
|
||||
} else if (typeof tokenColors === 'string') {
|
||||
return _loadSyntaxTokens(extensionResourceLoaderService, resources.joinPath(resources.dirname(themeLocation), tokenColors), result);
|
||||
} else {
|
||||
return Promise.reject(new Error(nls.localize({ key: 'error.invalidformat.tokenColors', comment: ['{0} will be replaced by a path. Values in quotes should not be translated.'] }, "Problem parsing color theme file: {0}. Property 'tokenColors' should be either an array specifying colors or a path to a TextMate theme file", themeLocation.toString())));
|
||||
}
|
||||
}
|
||||
// let tokenStylingRules = contentValue.tokenStylingRules;
|
||||
// if (tokenStylingRules && typeof tokenStylingRules === 'object') {
|
||||
// result.stylingRules = readCustomTokenStyleRules(tokenStylingRules, result.stylingRules);
|
||||
// }
|
||||
return null;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return _loadSyntaxTokens(extensionResourceLoaderService, themeLocation, result);
|
||||
}
|
||||
@@ -738,22 +752,27 @@ function getScopeMatcher(rule: ITextMateThemingRule): Matcher<ProbeScope> {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function readCustomTokenStyleRule(selectorString: string, settings: ITokenColorizationSetting | string | undefined): TokenStylingRule | undefined {
|
||||
const selector = tokenClassificationRegistry.parseTokenSelector(selectorString);
|
||||
let style: TokenStyle | undefined;
|
||||
if (typeof settings === 'string') {
|
||||
style = TokenStyle.fromSettings(settings, undefined);
|
||||
} else if (isTokenColorizationSetting(settings)) {
|
||||
style = TokenStyle.fromSettings(settings.foreground, settings.fontStyle);
|
||||
}
|
||||
if (style) {
|
||||
return { selector, style };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function readCustomTokenStyleRules(tokenStylingRuleSection: IExperimentalTokenStyleCustomizations, result: TokenStylingRule[] = []) {
|
||||
for (let key in tokenStylingRuleSection) {
|
||||
if (key[0] !== '[') {
|
||||
try {
|
||||
const selector = tokenClassificationRegistry.parseTokenSelector(key);
|
||||
const settings = tokenStylingRuleSection[key];
|
||||
let style: TokenStyle | undefined;
|
||||
if (typeof settings === 'string') {
|
||||
style = TokenStyle.fromSettings(settings, undefined);
|
||||
} else if (isTokenColorizationSetting(settings)) {
|
||||
style = TokenStyle.fromSettings(settings.foreground, settings.fontStyle);
|
||||
}
|
||||
if (style) {
|
||||
result.push({ selector, style });
|
||||
const rule = readCustomTokenStyleRule(key, tokenStylingRuleSection[key]);
|
||||
if (rule) {
|
||||
result.push(rule);
|
||||
}
|
||||
} catch (e) {
|
||||
// invalid selector, ignore
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Extensions as JSONExtensions, IJSONContributionRegistry } from 'vs/plat
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
|
||||
import { workbenchColorsSchemaId } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { tokenStylingSchemaId } from 'vs/platform/theme/common/tokenClassificationRegistry';
|
||||
|
||||
let textMateScopes = [
|
||||
'comment',
|
||||
@@ -226,6 +227,11 @@ const colorThemeSchema: IJSONSchema = {
|
||||
semanticHighlighting: {
|
||||
type: 'boolean',
|
||||
description: nls.localize('schema.supportsSemanticHighlighting', 'Whether semantic highlighting should be enabled for this theme.')
|
||||
},
|
||||
semanticTokenColors: {
|
||||
type: 'object',
|
||||
description: nls.localize('schema.semanticTokenColors', 'Colors for semantic tokens'),
|
||||
$ref: tokenStylingSchemaId
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { ExtensionsRegistry, ExtensionMessageCollector } from 'vs/workbench/services/extensions/common/extensionsRegistry';
|
||||
import { getTokenClassificationRegistry, ITokenClassificationRegistry, typeAndModifierIdPattern, TokenStyleDefaults, TokenStyle, fontStylePattern } from 'vs/platform/theme/common/tokenClassificationRegistry';
|
||||
import { getTokenClassificationRegistry, ITokenClassificationRegistry, typeAndModifierIdPattern, TokenStyleDefaults, TokenStyle, fontStylePattern, selectorPattern } from 'vs/platform/theme/common/tokenClassificationRegistry';
|
||||
import { textmateColorSettingsSchemaId } from 'vs/workbench/services/themes/common/colorThemeSchema';
|
||||
|
||||
interface ITokenTypeExtensionPoint {
|
||||
@@ -36,7 +36,6 @@ interface ITokenStyleDefaultExtensionPoint {
|
||||
};
|
||||
}
|
||||
|
||||
const selectorPattern = '^([-_\\w]+|\\*)(\\.[-_\\w+]+)*$';
|
||||
const colorPattern = '^#([0-9A-Fa-f]{6})([0-9A-Fa-f]{2})?$';
|
||||
|
||||
const tokenClassificationRegistry: ITokenClassificationRegistry = getTokenClassificationRegistry();
|
||||
@@ -98,7 +97,7 @@ const tokenStyleDefaultsExtPoint = ExtensionsRegistry.registerExtensionPoint<ITo
|
||||
type: 'string',
|
||||
description: nls.localize('contributes.semanticTokenStyleDefaults.selector', 'The selector matching token types and modifiers.'),
|
||||
pattern: selectorPattern,
|
||||
patternErrorMessage: nls.localize('contributes.semanticTokenStyleDefaults.selector.format', 'Selectors should be in the form (type|*)(.modifier)*'),
|
||||
patternErrorMessage: nls.localize('contributes.semanticTokenStyleDefaults.selector.format', 'Selectors should be in the form (type|*)(.modifier)*(:language)?'),
|
||||
},
|
||||
scope: {
|
||||
type: 'array',
|
||||
@@ -226,7 +225,7 @@ export class TokenClassificationExtensionPoints {
|
||||
continue;
|
||||
}
|
||||
if (!contribution.selector.match(selectorPattern)) {
|
||||
collector.error(nls.localize('invalid.selector.format', "'configuration.semanticTokenStyleDefaults.selector' must be in the form (type|*)(.modifier)*"));
|
||||
collector.error(nls.localize('invalid.selector.format', "'configuration.semanticTokenStyleDefaults.selector' must be in the form (type|*)(.modifier)*(:language)?"));
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -47,36 +47,34 @@ function assertTokenStyle(actual: TokenStyle | undefined | null, expected: Token
|
||||
assert.equal(tokenStyleAsString(actual), tokenStyleAsString(expected), message);
|
||||
}
|
||||
|
||||
function assertTokenStyleMetaData(colorIndex: string[], actual: ITokenStyle | undefined, expected: TokenStyle | undefined | null, message?: string) {
|
||||
function assertTokenStyleMetaData(colorIndex: string[], actual: ITokenStyle | undefined, expected: TokenStyle | undefined | null, message = '') {
|
||||
if (expected === undefined || expected === null || actual === undefined) {
|
||||
assert.equal(actual, expected, message);
|
||||
return;
|
||||
}
|
||||
assert.strictEqual(actual.bold, expected.bold, 'bold');
|
||||
assert.strictEqual(actual.italic, expected.italic, 'italic');
|
||||
assert.strictEqual(actual.underline, expected.underline, 'underline');
|
||||
assert.strictEqual(actual.bold, expected.bold, 'bold ' + message);
|
||||
assert.strictEqual(actual.italic, expected.italic, 'italic ' + message);
|
||||
assert.strictEqual(actual.underline, expected.underline, 'underline ' + message);
|
||||
|
||||
const actualForegroundIndex = actual.foreground;
|
||||
if (expected.foreground) {
|
||||
assert.equal(actualForegroundIndex, colorIndex.indexOf(Color.Format.CSS.formatHexA(expected.foreground, true).toUpperCase()), 'foreground');
|
||||
if (actualForegroundIndex && expected.foreground) {
|
||||
assert.equal(colorIndex[actualForegroundIndex], Color.Format.CSS.formatHexA(expected.foreground, true).toUpperCase(), 'foreground ' + message);
|
||||
} else {
|
||||
assert.equal(actualForegroundIndex, 0, 'foreground');
|
||||
assert.equal(actualForegroundIndex, expected.foreground || 0, 'foreground ' + message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function assertTokenStyles(themeData: ColorThemeData, expected: { [qualifiedClassifier: string]: TokenStyle }) {
|
||||
function assertTokenStyles(themeData: ColorThemeData, expected: { [qualifiedClassifier: string]: TokenStyle }, language = 'typescript') {
|
||||
const colorIndex = themeData.tokenColorMap;
|
||||
|
||||
for (let qualifiedClassifier in expected) {
|
||||
const [type, ...modifiers] = qualifiedClassifier.split('.');
|
||||
|
||||
const tokenStyle = themeData.getTokenStyle(type, modifiers);
|
||||
const expectedTokenStyle = expected[qualifiedClassifier];
|
||||
assertTokenStyle(tokenStyle, expectedTokenStyle, qualifiedClassifier);
|
||||
|
||||
const tokenStyleMetaData = themeData.getTokenStyleMetadata(type, modifiers);
|
||||
assertTokenStyleMetaData(colorIndex, tokenStyleMetaData, expectedTokenStyle);
|
||||
const tokenStyleMetaData = themeData.getTokenStyleMetadata(type, modifiers, language);
|
||||
assertTokenStyleMetaData(colorIndex, tokenStyleMetaData, expectedTokenStyle, qualifiedClassifier);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,6 +322,7 @@ suite('Themes - TokenStyleResolving', () => {
|
||||
|
||||
});
|
||||
|
||||
|
||||
test('rule matching', async () => {
|
||||
const themeData = ColorThemeData.createLoadedEmptyTheme('test', 'test');
|
||||
themeData.setCustomColors({ 'editor.foreground': '#000000' });
|
||||
@@ -350,8 +349,10 @@ suite('Themes - TokenStyleResolving', () => {
|
||||
});
|
||||
|
||||
test('super type', async () => {
|
||||
getTokenClassificationRegistry().registerTokenType('myTestInterface', 'A type just for testing', 'interface');
|
||||
getTokenClassificationRegistry().registerTokenType('myTestSubInterface', 'A type just for testing', 'myTestInterface');
|
||||
const registry = getTokenClassificationRegistry();
|
||||
|
||||
registry.registerTokenType('myTestInterface', 'A type just for testing', 'interface');
|
||||
registry.registerTokenType('myTestSubInterface', 'A type just for testing', 'myTestInterface');
|
||||
|
||||
try {
|
||||
const themeData = ColorThemeData.createLoadedEmptyTheme('test', 'test');
|
||||
@@ -371,7 +372,56 @@ suite('Themes - TokenStyleResolving', () => {
|
||||
});
|
||||
assertTokenStyles(themeData, { 'myTestSubInterface': ts('#ff00ff', { italic: true }) });
|
||||
} finally {
|
||||
getTokenClassificationRegistry().deregisterTokenType('myTestInterface');
|
||||
registry.deregisterTokenType('myTestInterface');
|
||||
registry.deregisterTokenType('myTestSubInterface');
|
||||
}
|
||||
});
|
||||
|
||||
test('language', async () => {
|
||||
try {
|
||||
const themeData = ColorThemeData.createLoadedEmptyTheme('test', 'test');
|
||||
themeData.setCustomColors({ 'editor.foreground': '#000000' });
|
||||
themeData.setCustomTokenStyleRules({
|
||||
'interface': '#fff000',
|
||||
'interface:java': '#ff0000',
|
||||
'interface.static': { fontStyle: 'bold' },
|
||||
'interface.static:typescript': { fontStyle: 'italic' }
|
||||
});
|
||||
|
||||
assertTokenStyles(themeData, { 'interface': ts('#ff0000', undefined) }, 'java');
|
||||
assertTokenStyles(themeData, { 'interface': ts('#fff000', undefined) }, 'typescript');
|
||||
assertTokenStyles(themeData, { 'interface.static': ts('#ff0000', { bold: true }) }, 'java');
|
||||
assertTokenStyles(themeData, { 'interface.static': ts('#fff000', { bold: true, italic: true }) }, 'typescript');
|
||||
} finally {
|
||||
}
|
||||
});
|
||||
|
||||
test('language - scope resolving', async () => {
|
||||
const registry = getTokenClassificationRegistry();
|
||||
registry.registerTokenStyleDefault(registry.parseTokenSelector('type:typescript'), { scopesToProbe: [['entity.name.type.ts']] });
|
||||
|
||||
|
||||
try {
|
||||
const themeData = ColorThemeData.createLoadedEmptyTheme('test', 'test');
|
||||
themeData.setCustomColors({ 'editor.foreground': '#000000' });
|
||||
themeData.setCustomTokenColors({
|
||||
textMateRules: [
|
||||
{
|
||||
scope: 'entity.name.type',
|
||||
settings: { foreground: '#aa0000' }
|
||||
},
|
||||
{
|
||||
scope: 'entity.name.type.ts',
|
||||
settings: { foreground: '#bb0000' }
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
assertTokenStyles(themeData, { 'type': ts('#aa0000', undefined) }, 'javascript');
|
||||
assertTokenStyles(themeData, { 'type': ts('#bb0000', undefined) }, 'typescript');
|
||||
|
||||
} finally {
|
||||
registry.deregisterTokenType('type/typescript');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { Event, AsyncEmitter, IWaitUntil } from 'vs/base/common/event';
|
||||
import { insert } from 'vs/base/common/arrays';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IFileService, FileOperation, IFileStatWithMetadata } from 'vs/platform/files/common/files';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IWorkingCopyService, IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService';
|
||||
@@ -57,6 +58,11 @@ export interface IWorkingCopyFileOperationParticipant {
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the working copies for a given resource.
|
||||
*/
|
||||
type WorkingCopyProvider = (resourceOrFolder: URI) => IWorkingCopy[];
|
||||
|
||||
/**
|
||||
* A service that allows to perform file operations with working copy support.
|
||||
* Any operation that would leave a stale dirty working copy behind will make
|
||||
@@ -142,9 +148,15 @@ export interface IWorkingCopyFileService {
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region Path related
|
||||
|
||||
/**
|
||||
* Register a new provider for working copies based on a resource.
|
||||
*
|
||||
* @return a disposable that unregisters the provider.
|
||||
*/
|
||||
registerWorkingCopyProvider(provider: WorkingCopyProvider): IDisposable;
|
||||
|
||||
/**
|
||||
* Will return all working copies that are dirty matching the provided resource.
|
||||
* If the resource is a folder and the scheme supports file operations, a working
|
||||
@@ -180,6 +192,20 @@ export class WorkingCopyFileService extends Disposable implements IWorkingCopyFi
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService
|
||||
) {
|
||||
super();
|
||||
|
||||
// register a default working copy provider that uses the working copy service
|
||||
this.registerWorkingCopyProvider(resource => {
|
||||
return this.workingCopyService.workingCopies.filter(workingCopy => {
|
||||
if (this.fileService.canHandleResource(resource)) {
|
||||
// only check for parents if the resource can be handled
|
||||
// by the file system where we then assume a folder like
|
||||
// path structure
|
||||
return isEqualOrParent(workingCopy.resource, resource);
|
||||
}
|
||||
|
||||
return isEqual(workingCopy.resource, resource);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async move(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata> {
|
||||
@@ -275,17 +301,23 @@ export class WorkingCopyFileService extends Disposable implements IWorkingCopyFi
|
||||
|
||||
//#region Path related
|
||||
|
||||
getDirty(resource: URI): IWorkingCopy[] {
|
||||
return this.workingCopyService.dirtyWorkingCopies.filter(dirty => {
|
||||
if (this.fileService.canHandleResource(resource)) {
|
||||
// only check for parents if the resource can be handled
|
||||
// by the file system where we then assume a folder like
|
||||
// path structure
|
||||
return isEqualOrParent(dirty.resource, resource);
|
||||
}
|
||||
private readonly workingCopyProviders: WorkingCopyProvider[] = [];
|
||||
|
||||
return isEqual(dirty.resource, resource);
|
||||
});
|
||||
registerWorkingCopyProvider(provider: WorkingCopyProvider): IDisposable {
|
||||
const remove = insert(this.workingCopyProviders, provider);
|
||||
return toDisposable(remove);
|
||||
}
|
||||
|
||||
getDirty(resource: URI): IWorkingCopy[] {
|
||||
const dirtyWorkingCopies = new Set<IWorkingCopy>();
|
||||
for (const provider of this.workingCopyProviders) {
|
||||
for (const workingCopy of provider(resource)) {
|
||||
if (workingCopy.isDirty()) {
|
||||
dirtyWorkingCopies.add(workingCopy);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Array.from(dirtyWorkingCopies);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
@@ -11,6 +11,7 @@ import { toResource } from 'vs/base/test/common/utils';
|
||||
import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { FileOperation } from 'vs/platform/files/common/files';
|
||||
import { TestWorkingCopy } from 'vs/workbench/services/workingCopy/test/common/workingCopyService.test';
|
||||
|
||||
suite('WorkingCopyFileService', () => {
|
||||
|
||||
@@ -186,4 +187,29 @@ suite('WorkingCopyFileService', () => {
|
||||
model1.dispose();
|
||||
model2.dispose();
|
||||
});
|
||||
|
||||
test('registerWorkingCopyProvider', async function () {
|
||||
const model1 = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/file-1.txt'), 'utf8', undefined);
|
||||
(<TextFileEditorModelManager>accessor.textFileService.files).add(model.resource, model);
|
||||
await model1.load();
|
||||
model1.textEditorModel!.setValue('foo');
|
||||
|
||||
const testWorkingCopy = new TestWorkingCopy(toResource.call(this, '/path/file-2.txt'), true);
|
||||
const registration = accessor.workingCopyFileService.registerWorkingCopyProvider(() => {
|
||||
return [model1, testWorkingCopy];
|
||||
});
|
||||
|
||||
let dirty = accessor.workingCopyFileService.getDirty(model1.resource);
|
||||
assert.strictEqual(dirty.length, 2, 'Should return default working copy + working copy from provider');
|
||||
assert.strictEqual(dirty[0], model1);
|
||||
assert.strictEqual(dirty[1], testWorkingCopy);
|
||||
|
||||
registration.dispose();
|
||||
|
||||
dirty = accessor.workingCopyFileService.getDirty(model1.resource);
|
||||
assert.strictEqual(dirty.length, 1, 'Should have unregistered our provider');
|
||||
assert.strictEqual(dirty[0], model1);
|
||||
|
||||
model1.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,65 +12,66 @@ import { TestWorkingCopyService } from 'vs/workbench/test/common/workbenchTestSe
|
||||
import { ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor';
|
||||
import { basename } from 'vs/base/common/resources';
|
||||
|
||||
suite('WorkingCopyService', () => {
|
||||
export class TestWorkingCopy extends Disposable implements IWorkingCopy {
|
||||
|
||||
class TestWorkingCopy extends Disposable implements IWorkingCopy {
|
||||
private readonly _onDidChangeDirty = this._register(new Emitter<void>());
|
||||
readonly onDidChangeDirty = this._onDidChangeDirty.event;
|
||||
|
||||
private readonly _onDidChangeDirty = this._register(new Emitter<void>());
|
||||
readonly onDidChangeDirty = this._onDidChangeDirty.event;
|
||||
private readonly _onDidChangeContent = this._register(new Emitter<void>());
|
||||
readonly onDidChangeContent = this._onDidChangeContent.event;
|
||||
|
||||
private readonly _onDidChangeContent = this._register(new Emitter<void>());
|
||||
readonly onDidChangeContent = this._onDidChangeContent.event;
|
||||
private readonly _onDispose = this._register(new Emitter<void>());
|
||||
readonly onDispose = this._onDispose.event;
|
||||
|
||||
private readonly _onDispose = this._register(new Emitter<void>());
|
||||
readonly onDispose = this._onDispose.event;
|
||||
readonly capabilities = 0;
|
||||
|
||||
readonly capabilities = 0;
|
||||
readonly name = basename(this.resource);
|
||||
|
||||
readonly name = basename(this.resource);
|
||||
private dirty = false;
|
||||
|
||||
private dirty = false;
|
||||
constructor(public readonly resource: URI, isDirty = false) {
|
||||
super();
|
||||
|
||||
constructor(public readonly resource: URI, isDirty = false) {
|
||||
super();
|
||||
this.dirty = isDirty;
|
||||
}
|
||||
|
||||
this.dirty = isDirty;
|
||||
}
|
||||
|
||||
setDirty(dirty: boolean): void {
|
||||
if (this.dirty !== dirty) {
|
||||
this.dirty = dirty;
|
||||
this._onDidChangeDirty.fire();
|
||||
}
|
||||
}
|
||||
|
||||
setContent(content: string): void {
|
||||
this._onDidChangeContent.fire();
|
||||
}
|
||||
|
||||
isDirty(): boolean {
|
||||
return this.dirty;
|
||||
}
|
||||
|
||||
async save(options?: ISaveOptions): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
async revert(options?: IRevertOptions): Promise<void> {
|
||||
this.setDirty(false);
|
||||
}
|
||||
|
||||
async backup(): Promise<IWorkingCopyBackup> {
|
||||
return {};
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._onDispose.fire();
|
||||
|
||||
super.dispose();
|
||||
setDirty(dirty: boolean): void {
|
||||
if (this.dirty !== dirty) {
|
||||
this.dirty = dirty;
|
||||
this._onDidChangeDirty.fire();
|
||||
}
|
||||
}
|
||||
|
||||
setContent(content: string): void {
|
||||
this._onDidChangeContent.fire();
|
||||
}
|
||||
|
||||
isDirty(): boolean {
|
||||
return this.dirty;
|
||||
}
|
||||
|
||||
async save(options?: ISaveOptions): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
async revert(options?: IRevertOptions): Promise<void> {
|
||||
this.setDirty(false);
|
||||
}
|
||||
|
||||
async backup(): Promise<IWorkingCopyBackup> {
|
||||
return {};
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._onDispose.fire();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
suite('WorkingCopyService', () => {
|
||||
|
||||
|
||||
test('registry - basics', () => {
|
||||
const service = new TestWorkingCopyService();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user