Merge from vscode 1ec43773e37997841c5af42b33ddb180e9735bf2

This commit is contained in:
ADS Merger
2020-03-29 01:29:32 +00:00
parent 586ec50916
commit a64304602e
316 changed files with 6524 additions and 11687 deletions

View File

@@ -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>;
}

View File

@@ -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)));
}

View File

@@ -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));

View File

@@ -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> {

View File

@@ -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'));

View File

@@ -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

View File

@@ -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 {

View File

@@ -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];
};
});
}

View File

@@ -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,
});
}

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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
}
}
};

View File

@@ -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;
}

View File

@@ -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');
}
});
});

View File

@@ -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

View File

@@ -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();
});
});

View File

@@ -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();