diff --git a/.vscode/searches/es6.code-search b/.vscode/searches/es6.code-search new file mode 100644 index 00000000000..09a841dc699 --- /dev/null +++ b/.vscode/searches/es6.code-search @@ -0,0 +1,74 @@ +# Query: @deprecated ES6 +# Flags: CaseSensitive WordMatch +# ContextLines: 2 + +11 results - 3 files + +src/vs/base/common/arrays.ts: + 401 + 402 /** + 403: * @deprecated ES6: use `Array.findIndex` + 404 */ + 405 export function firstIndex(array: ReadonlyArray, fn: (item: T) => boolean): number { + + 417 + 418 /** + 419: * @deprecated ES6: use `Array.find` + 420 */ + 421 export function first(array: ReadonlyArray, fn: (item: T) => boolean, notFoundValue: T): T; + + 474 + 475 /** + 476: * @deprecated ES6: use `Array.fill` + 477 */ + 478 export function fill(num: number, value: T, arr: T[] = []): T[] { + + 571 + 572 /** + 573: * @deprecated ES6: use `Array.find` + 574 */ + 575 export function find(arr: ArrayLike, predicate: (value: T, index: number, arr: ArrayLike) => any): T | undefined { + +src/vs/base/common/map.ts: + 9 + 10 /** + 11: * @deprecated ES6: use `[...SetOrMap.values()]` + 12 */ + 13 export function values(set: Set): V[]; + + 20 + 21 /** + 22: * @deprecated ES6: use `[...map.keys()]` + 23 */ + 24 export function keys(map: Map): K[] { + + 58 + 59 /** + 60: * @deprecated ES6: use `...Map.entries()` + 61 */ + 62 export function mapToSerializable(map: Map): [string, string][] { + + 71 + 72 /** + 73: * @deprecated ES6: use `new Map([[key1, value1],[key2, value2]])` + 74 */ + 75 export function serializableToMap(serializable: [string, string][]): Map { + +src/vs/base/common/strings.ts: + 16 + 17 /** + 18: * @deprecated ES6: use `String.padStart` + 19 */ + 20 export function pad(n: number, l: number, char: string = '0'): string { + + 147 + 148 /** + 149: * @deprecated ES6: use `String.startsWith` + 150 */ + 151 export function startsWith(haystack: string, needle: string): boolean { + + 168 + 169 /** + 170: * @deprecated ES6: use `String.endsWith` + 171 */ + 172 export function endsWith(haystack: string, needle: string): boolean { diff --git a/.vscode/settings.json b/.vscode/settings.json index d56f75c41d4..2a9a8093ced 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -69,5 +69,8 @@ "msjsdiag.debugger-for-chrome": "workspace" }, "gulp.autoDetect": "off", - "files.insertFinalNewline": true + "files.insertFinalNewline": true, + "[typescript]": { + "editor.defaultFormatter": "vscode.typescript-language-features" + } } diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index 795bc78556e..aae3ccb7297 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -135,6 +135,23 @@ steps: displayName: Run integration tests (Browser) condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) +- script: | + set -e + APP_ROOT=$(agent.builddirectory)/VSCode-darwin + APP_NAME="`ls $APP_ROOT | head -n 1`" + yarn smoketest --build "$APP_ROOT/$APP_NAME" + continueOnError: true + displayName: Run smoke tests (Electron) + condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) + +- script: | + set -e + VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-darwin" \ + yarn smoketest --web --headless + continueOnError: true + displayName: Run smoke tests (Browser) + condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) + - script: | set -e security create-keychain -p pwd $(agent.tempdirectory)/buildagent.keychain diff --git a/build/builtInExtensions.json b/build/builtInExtensions.json index 9d176d98076..63c41b403da 100644 --- a/build/builtInExtensions.json +++ b/build/builtInExtensions.json @@ -1,7 +1,7 @@ [ { "name": "ms-vscode.node-debug", - "version": "1.44.1", + "version": "1.44.2", "repo": "https://github.com/Microsoft/vscode-node-debug", "metadata": { "id": "b6ded8fb-a0a0-4c1c-acbd-ab2a3bc995a6", diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index ae71fd3bf63..3494753b30a 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -122,6 +122,10 @@ "name": "vs/workbench/contrib/preferences", "project": "vscode-workbench" }, + { + "name": "vs/workbench/contrib/quickaccess", + "project": "vscode-workbench" + }, { "name": "vs/workbench/contrib/quickopen", "project": "vscode-workbench" @@ -262,6 +266,10 @@ "name": "vs/workbench/services/files", "project": "vscode-workbench" }, + { + "name": "vs/workbench/services/log", + "project": "vscode-workbench" + }, { "name": "vs/workbench/services/integrity", "project": "vscode-workbench" diff --git a/build/package.json b/build/package.json index 8196dd52002..2abecaa1279 100644 --- a/build/package.json +++ b/build/package.json @@ -43,7 +43,7 @@ "minimist": "^1.2.0", "request": "^2.85.0", "terser": "4.3.8", - "typescript": "^3.9.0-dev.20200229", + "typescript": "3.9.0-dev.20200304", "vsce": "1.48.0", "vscode-telemetry-extractor": "^1.5.4", "xml2js": "^0.4.17" diff --git a/build/yarn.lock b/build/yarn.lock index fd19a1b7221..ef17978a534 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -2453,16 +2453,16 @@ typed-rest-client@^0.9.0: tunnel "0.0.4" underscore "1.8.3" +typescript@3.9.0-dev.20200304: + version "3.9.0-dev.20200304" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.0-dev.20200304.tgz#3cc35357eff29dc5604b4fa56d6597e13daf86ed" + integrity sha512-eUip/GgJmjp4qtHiJDxVhE5SDDiPzBUg7KBAFUgb7HgL/tv10JAHej7fnS1i+7xrq1eDtbkJyPaYOVnhL9db7Q== + typescript@^3.0.1: version "3.5.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.3.tgz#c830f657f93f1ea846819e929092f5fe5983e977" integrity sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g== -typescript@^3.9.0-dev.20200229: - version "3.9.0-dev.20200229" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.0-dev.20200229.tgz#45f0821d5c420a4c7d6d894c64531e1301dfa9bd" - integrity sha512-DtSLzxoiUir0qRc3+JJBxiAe6NvTEM3uDxnPxVWJU6sRDhUi8Ssx6DBjGWCZAQJlLk5A+jk2ptf3JvvZrQlLNQ== - typical@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4" diff --git a/extensions/emmet/src/util.ts b/extensions/emmet/src/util.ts index b6281272851..49cc45cd630 100644 --- a/extensions/emmet/src/util.ts +++ b/extensions/emmet/src/util.ts @@ -32,7 +32,12 @@ export function updateEmmetExtensionsPath() { let extensionsPath = vscode.workspace.getConfiguration('emmet')['extensionsPath']; if (_currentExtensionsPath !== extensionsPath) { _currentExtensionsPath = extensionsPath; - _emmetHelper.updateExtensionsPath(extensionsPath, vscode.workspace.rootPath).then(null, (err: string) => vscode.window.showErrorMessage(err)); + if (!vscode.workspace.workspaceFolders) { + return; + } else { + const rootPath = vscode.workspace.workspaceFolders[0].uri.fsPath; + _emmetHelper.updateExtensionsPath(extensionsPath, rootPath).then(null, (err: string) => vscode.window.showErrorMessage(err)); + } } } @@ -622,4 +627,4 @@ export function trimQuotes(s: string) { } return s; -} \ No newline at end of file +} diff --git a/extensions/extension-editing/package.json b/extensions/extension-editing/package.json index fe4a7b44d2a..75d77925b0d 100644 --- a/extensions/extension-editing/package.json +++ b/extensions/extension-editing/package.json @@ -35,9 +35,13 @@ "url": "vscode://schemas/language-configuration" }, { - "fileMatch": "*icon-theme.json", + "fileMatch": ["*icon-theme.json", "!*product-icon-theme.json"], "url": "vscode://schemas/icon-theme" }, + { + "fileMatch": "*product-icon-theme.json", + "url": "vscode://schemas/product-icon-theme" + }, { "fileMatch": "*color-theme.json", "url": "vscode://schemas/color-theme" diff --git a/extensions/github-authentication/build/postinstall.js b/extensions/github-authentication/build/postinstall.js index 16e51347559..e12fd05f191 100644 --- a/extensions/github-authentication/build/postinstall.js +++ b/extensions/github-authentication/build/postinstall.js @@ -20,9 +20,7 @@ function main() { } } - if (Object.keys(content).length > 0) { - fs.writeFileSync(path.join(__dirname, '../src/common/config.json'), JSON.stringify(content)); - } + fs.writeFileSync(path.join(__dirname, '../src/common/config.json'), JSON.stringify(content)); } main(); diff --git a/extensions/json-language-features/client/src/jsonMain.ts b/extensions/json-language-features/client/src/jsonMain.ts index 7b307332241..5d41458931c 100644 --- a/extensions/json-language-features/client/src/jsonMain.ts +++ b/extensions/json-language-features/client/src/jsonMain.ts @@ -40,8 +40,13 @@ export interface ISchemaAssociations { [pattern: string]: string[]; } +export interface ISchemaAssociation { + fileMatch: string[]; + uri: string; +} + namespace SchemaAssociationNotification { - export const type: NotificationType = new NotificationType('json/schemaAssociations'); + export const type: NotificationType = new NotificationType('json/schemaAssociations'); } namespace ResultLimitReachedNotification { @@ -264,10 +269,10 @@ export function activate(context: ExtensionContext) { toDispose.push(commands.registerCommand('_json.retryResolveSchema', handleRetryResolveSchemaCommand)); - client.sendNotification(SchemaAssociationNotification.type, getSchemaAssociation(context)); + client.sendNotification(SchemaAssociationNotification.type, getSchemaAssociations(context)); extensions.onDidChange(_ => { - client.sendNotification(SchemaAssociationNotification.type, getSchemaAssociation(context)); + client.sendNotification(SchemaAssociationNotification.type, getSchemaAssociations(context)); }); // manually register / deregister format provider based on the `html.format.enable` setting avoiding issues with late registration. See #71652. @@ -324,8 +329,8 @@ export function deactivate(): Promise { return telemetryReporter ? telemetryReporter.dispose() : Promise.resolve(null); } -function getSchemaAssociation(_context: ExtensionContext): ISchemaAssociations { - const associations: ISchemaAssociations = {}; +function getSchemaAssociations(_context: ExtensionContext): ISchemaAssociation[] { + const associations: ISchemaAssociation[] = []; extensions.all.forEach(extension => { const packageJSON = extension.packageJSON; if (packageJSON && packageJSON.contributes && packageJSON.contributes.jsonValidation) { @@ -333,23 +338,21 @@ function getSchemaAssociation(_context: ExtensionContext): ISchemaAssociations { if (Array.isArray(jsonValidation)) { jsonValidation.forEach(jv => { let { fileMatch, url } = jv; - if (fileMatch && url) { - if (url[0] === '.' && url[1] === '/') { - url = Uri.file(path.join(extension.extensionPath, url)).toString(); - } - if (fileMatch[0] === '%') { - fileMatch = fileMatch.replace(/%APP_SETTINGS_HOME%/, '/User'); - fileMatch = fileMatch.replace(/%MACHINE_SETTINGS_HOME%/, '/Machine'); - fileMatch = fileMatch.replace(/%APP_WORKSPACES_HOME%/, '/Workspaces'); - } else if (fileMatch.charAt(0) !== '/' && !fileMatch.match(/\w+:\/\//)) { - fileMatch = '/' + fileMatch; - } - let association = associations[fileMatch]; - if (!association) { - association = []; - associations[fileMatch] = association; - } - association.push(url); + if (typeof fileMatch === 'string') { + fileMatch = [fileMatch]; + } + if (Array.isArray(fileMatch) && url) { + fileMatch = fileMatch.map(fm => { + if (fm[0] === '%') { + fm = fm.replace(/%APP_SETTINGS_HOME%/, '/User'); + fm = fm.replace(/%MACHINE_SETTINGS_HOME%/, '/Machine'); + fm = fm.replace(/%APP_WORKSPACES_HOME%/, '/Workspaces'); + } else if (!fm.match(/^(\w+:\/\/|\/|!)/)) { + fm = '/' + fm; + } + return fm; + }); + associations.push({ fileMatch, uri: url }); } }); } diff --git a/extensions/json-language-features/server/src/jsonServerMain.ts b/extensions/json-language-features/server/src/jsonServerMain.ts index 182eef7e9ee..e339620d1e1 100644 --- a/extensions/json-language-features/server/src/jsonServerMain.ts +++ b/extensions/json-language-features/server/src/jsonServerMain.ts @@ -23,8 +23,13 @@ interface ISchemaAssociations { [pattern: string]: string[]; } +interface ISchemaAssociation { + fileMatch: string[]; + uri: string; +} + namespace SchemaAssociationNotification { - export const type: NotificationType = new NotificationType('json/schemaAssociations'); + export const type: NotificationType = new NotificationType('json/schemaAssociations'); } namespace VSCodeContentRequest { @@ -230,7 +235,7 @@ namespace LimitExceededWarnings { } let jsonConfigurationSettings: JSONSchemaSettings[] | undefined = undefined; -let schemaAssociations: ISchemaAssociations | undefined = undefined; +let schemaAssociations: ISchemaAssociations | ISchemaAssociation[] | undefined = undefined; let formatterRegistration: Thenable | null = null; // The settings have changed. Is send on server activation as well. @@ -291,12 +296,16 @@ function updateConfiguration() { schemas: new Array() }; if (schemaAssociations) { - for (const pattern in schemaAssociations) { - const association = schemaAssociations[pattern]; - if (Array.isArray(association)) { - association.forEach(uri => { - languageSettings.schemas.push({ uri, fileMatch: [pattern] }); - }); + if (Array.isArray(schemaAssociations)) { + Array.prototype.push.apply(languageSettings.schemas, schemaAssociations); + } else { + for (const pattern in schemaAssociations) { + const association = schemaAssociations[pattern]; + if (Array.isArray(association)) { + association.forEach(uri => { + languageSettings.schemas.push({ uri, fileMatch: [pattern] }); + }); + } } } } diff --git a/extensions/markdown-language-features/src/features/preview.ts b/extensions/markdown-language-features/src/features/preview.ts index ad8edc1c2ad..2b71b348f4d 100644 --- a/extensions/markdown-language-features/src/features/preview.ts +++ b/extensions/markdown-language-features/src/features/preview.ts @@ -457,7 +457,10 @@ export class DynamicMarkdownPreview extends Disposable { const folder = vscode.workspace.getWorkspaceFolder(base); if (folder) { - baseRoots.push(folder.uri); + const workspaceRoots = vscode.workspace.workspaceFolders?.map(folder => folder.uri); + if (workspaceRoots) { + baseRoots.push(...workspaceRoots); + } } else if (!base.scheme || base.scheme === 'file') { baseRoots.push(vscode.Uri.file(path.dirname(base.fsPath))); } diff --git a/extensions/typescript-language-features/src/features/directiveCommentCompletions.ts b/extensions/typescript-language-features/src/features/directiveCommentCompletions.ts index c0a222e47df..8a33a76a5a8 100644 --- a/extensions/typescript-language-features/src/features/directiveCommentCompletions.ts +++ b/extensions/typescript-language-features/src/features/directiveCommentCompletions.ts @@ -6,6 +6,7 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import { ITypeScriptServiceClient } from '../typescriptService'; +import API from '../utils/api'; const localize = nls.loadMessageBundle(); @@ -33,11 +34,26 @@ const directives: Directive[] = [ } ]; +const directives390: Directive[] = [ + ...directives, + { + value: '@ts-expect-error', + description: localize( + 'ts-expect-error', + "Suppresses @ts-check errors on the next line of a file, expecting at least one to exist.") + } +]; + class DirectiveCommentCompletionProvider implements vscode.CompletionItemProvider { + private readonly directives: Directive[]; constructor( private readonly client: ITypeScriptServiceClient, - ) { } + ) { + this.directives = client.apiVersion.gte(API.v390) + ? directives390 + : directives; + } public provideCompletionItems( document: vscode.TextDocument, @@ -53,7 +69,7 @@ class DirectiveCommentCompletionProvider implements vscode.CompletionItemProvide const prefix = line.slice(0, position.character); const match = prefix.match(/^\s*\/\/+\s?(@[a-zA-Z\-]*)?$/); if (match) { - return directives.map(directive => { + return this.directives.map(directive => { const item = new vscode.CompletionItem(directive.value, vscode.CompletionItemKind.Snippet); item.detail = directive.description; item.range = new vscode.Range(position.line, Math.max(0, position.character - (match[1] ? match[1].length : 0)), position.line, position.character); diff --git a/extensions/typescript-language-features/src/features/rename.ts b/extensions/typescript-language-features/src/features/rename.ts index 886cf420b74..db68acf38f3 100644 --- a/extensions/typescript-language-features/src/features/rename.ts +++ b/extensions/typescript-language-features/src/features/rename.ts @@ -8,7 +8,6 @@ import * as vscode from 'vscode'; import * as nls from 'vscode-nls'; import type * as Proto from '../protocol'; import { ITypeScriptServiceClient, ServerResponse } from '../typescriptService'; -import API from '../utils/api'; import * as typeConverters from '../utils/typeConverters'; import FileConfigurationManager from './fileConfigurationManager'; @@ -26,7 +25,7 @@ class TypeScriptRenameProvider implements vscode.RenameProvider { token: vscode.CancellationToken ): Promise { const response = await this.execRename(document, position, token); - if (!response || response.type !== 'response' || !response.body) { + if (response?.type !== 'response' || !response.body) { return null; } @@ -35,11 +34,9 @@ class TypeScriptRenameProvider implements vscode.RenameProvider { return Promise.reject(renameInfo.localizedErrorMessage); } - if (this.client.apiVersion.gte(API.v310)) { - const triggerSpan = renameInfo.triggerSpan; - if (triggerSpan) { - return typeConverters.Range.fromTextSpan(triggerSpan); - } + const triggerSpan = renameInfo.triggerSpan; // added in TS 3.1 + if (triggerSpan) { + return typeConverters.Range.fromTextSpan(triggerSpan); } return null; @@ -61,17 +58,15 @@ class TypeScriptRenameProvider implements vscode.RenameProvider { return Promise.reject(renameInfo.localizedErrorMessage); } - - if (this.client.apiVersion.gte(API.v310)) { - if (renameInfo.fileToRename) { - const edits = await this.renameFile(renameInfo.fileToRename, newName, token); - if (edits) { - return edits; - } else { - return Promise.reject(localize('fileRenameFail', "An error occurred while renaming file")); - } + if (renameInfo.fileToRename) { + const edits = await this.renameFile(renameInfo.fileToRename, newName, token); + if (edits) { + return edits; + } else { + return Promise.reject(localize('fileRenameFail', "An error occurred while renaming file")); } } + return this.updateLocs(response.body.locs, newName); } @@ -104,11 +99,9 @@ class TypeScriptRenameProvider implements vscode.RenameProvider { const edit = new vscode.WorkspaceEdit(); for (const spanGroup of locations) { const resource = this.client.toResource(spanGroup.file); - if (resource) { - for (const textSpan of spanGroup.locs as Proto.RenameTextSpan[]) { - edit.replace(resource, typeConverters.Range.fromTextSpan(textSpan), - (textSpan.prefixText || '') + newName + (textSpan.suffixText || '')); - } + for (const textSpan of spanGroup.locs) { + edit.replace(resource, typeConverters.Range.fromTextSpan(textSpan), + (textSpan.prefixText || '') + newName + (textSpan.suffixText || '')); } } return edit; diff --git a/extensions/typescript-language-features/src/features/workspaceSymbols.ts b/extensions/typescript-language-features/src/features/workspaceSymbols.ts index eb46f166df9..7b23d385c1f 100644 --- a/extensions/typescript-language-features/src/features/workspaceSymbols.ts +++ b/extensions/typescript-language-features/src/features/workspaceSymbols.ts @@ -44,7 +44,8 @@ class TypeScriptWorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvide const args: Proto.NavtoRequestArgs = { file: filepath, - searchValue: search + searchValue: search, + maxResultCount: 256, }; const response = await this.client.execute('navto', args, token); @@ -52,18 +53,12 @@ class TypeScriptWorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvide return []; } - const result: vscode.SymbolInformation[] = []; - for (const item of response.body) { - if (!item.containerName && item.kind === 'alias') { - continue; - } - const label = TypeScriptWorkspaceSymbolProvider.getLabel(item); - result.push(new vscode.SymbolInformation(label, getSymbolKind(item), item.containerName || '', - typeConverters.Location.fromTextSpan(this.client.toResource(item.file), item))); - } - return result; + return response.body + .filter(item => item.containerName && item.kind !== 'alias') + .map(item => this.toSymbolInformation(item)); } + private async toOpenedFiledPath(document: vscode.TextDocument) { if (document.uri.scheme === fileSchemes.git) { try { @@ -79,6 +74,15 @@ class TypeScriptWorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvide return this.client.toOpenedFilePath(document); } + private toSymbolInformation(item: Proto.NavtoItem) { + const label = TypeScriptWorkspaceSymbolProvider.getLabel(item); + return new vscode.SymbolInformation( + label, + getSymbolKind(item), + item.containerName || '', + typeConverters.Location.fromTextSpan(this.client.toResource(item.file), item)); + } + private static getLabel(item: Proto.NavtoItem) { const label = item.name; if (item.kind === 'method' || item.kind === 'function') { diff --git a/extensions/typescript-language-features/src/typescriptServiceClient.ts b/extensions/typescript-language-features/src/typescriptServiceClient.ts index 1809417fd0e..b1e55fd0d32 100644 --- a/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -872,7 +872,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType } } -function getReportIssueArgsForError(error: TypeScriptServerError): { issueTitle: string, issueBody: string } | undefined { +function getReportIssueArgsForError(error: TypeScriptServerError): { extensionId: string, issueTitle: string, issueBody: string } | undefined { if (!error.serverStack || !error.serverMessage) { return undefined; } @@ -880,6 +880,7 @@ function getReportIssueArgsForError(error: TypeScriptServerError): { issueTitle: // Note these strings are intentionally not localized // as we want users to file issues in english return { + extensionId: 'vscode.typescript-language-features', issueTitle: `TS Server fatal error: ${error.serverMessage}`, issueBody: `**TypeScript Version:** ${error.version.apiVersion?.fullVersionString} diff --git a/extensions/typescript-language-features/src/utils/api.ts b/extensions/typescript-language-features/src/utils/api.ts index 25be4b5e516..1845285caa4 100644 --- a/extensions/typescript-language-features/src/utils/api.ts +++ b/extensions/typescript-language-features/src/utils/api.ts @@ -33,6 +33,7 @@ export default class API { public static readonly v350 = API.fromSimpleString('3.5.0'); public static readonly v380 = API.fromSimpleString('3.8.0'); public static readonly v381 = API.fromSimpleString('3.8.1'); + public static readonly v390 = API.fromSimpleString('3.9.0'); public static fromVersionString(versionString: string): API { let version = semver.valid(versionString); diff --git a/extensions/vscode-colorize-tests/package.json b/extensions/vscode-colorize-tests/package.json index d99e050d302..0f3815b2b65 100644 --- a/extensions/vscode-colorize-tests/package.json +++ b/extensions/vscode-colorize-tests/package.json @@ -55,6 +55,14 @@ "fontStyle": "bold" } } + ], + "productIconThemes": [ + { + "id": "Test Product Icons", + "label": "The Test Product Icon Theme", + "path": "./producticons/test-product-icon-theme.json", + "_watch": true + } ] } } diff --git a/extensions/vscode-colorize-tests/producticons/ElegantIcons.woff b/extensions/vscode-colorize-tests/producticons/ElegantIcons.woff new file mode 100644 index 00000000000..393305253e5 Binary files /dev/null and b/extensions/vscode-colorize-tests/producticons/ElegantIcons.woff differ diff --git a/extensions/vscode-colorize-tests/producticons/index.html b/extensions/vscode-colorize-tests/producticons/index.html new file mode 100644 index 00000000000..0d34ddedb57 --- /dev/null +++ b/extensions/vscode-colorize-tests/producticons/index.html @@ -0,0 +1,3049 @@ + + + + + Your Font/Glyphs + + + + + + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+

Class Names

+
+ + +  arrow_up + + + +  arrow_down + + + +  arrow_left + + + +  arrow_right + + + +  arrow_left-up + + + +  arrow_right-up + + + +  arrow_right-down + + + +  arrow_left-down + + + +  arrow-up-down + + + +  arrow_up-down_alt + + + +  arrow_left-right_alt + + + +  arrow_left-right + + + +  arrow_expand_alt2 + + + +  arrow_expand_alt + + + +  arrow_condense + + + +  arrow_expand + + + +  arrow_move + + + +  arrow_carrot-up + + + +  arrow_carrot-down + + + +  arrow_carrot-left + + + +  arrow_carrot-right + + + +  arrow_carrot-2up + + + +  arrow_carrot-2down + + + +  arrow_carrot-2left + + + +  arrow_carrot-2right + + + +  arrow_carrot-up_alt2 + + + +  arrow_carrot-down_alt2 + + + +  arrow_carrot-left_alt2 + + + +  arrow_carrot-right_alt2 + + + +  arrow_carrot-2up_alt2 + + + +  arrow_carrot-2down_alt2 + + + +  arrow_carrot-2left_alt2 + + + +  arrow_carrot-2right_alt2 + + + +  arrow_triangle-up + + + +  arrow_triangle-down + + + +  arrow_triangle-left + + + +  arrow_triangle-right + + + +  arrow_triangle-up_alt2 + + + +  arrow_triangle-down_alt2 + + + +  arrow_triangle-left_alt2 + + + +  arrow_triangle-right_alt2 + + + +  arrow_back + + + +  icon_minus-06 + + + +  icon_plus + + + +  icon_close + + + +  icon_check + + + +  icon_minus_alt2 + + + +  icon_plus_alt2 + + + +  icon_close_alt2 + + + +  icon_check_alt2 + + + +  icon_zoom-out_alt + + + +  icon_zoom-in_alt + + + +  icon_search + + + +  icon_box-empty + + + +  icon_box-selected + + + +  icon_minus-box + + + +  icon_plus-box + + + +  icon_box-checked + + + +  icon_circle-empty + + + +  icon_circle-slelected + + + +  icon_stop_alt2 + + + +  icon_stop + + + +  icon_pause_alt2 + + + +  icon_pause + + + +  icon_menu + + + +  icon_menu-square_alt2 + + + +  icon_menu-circle_alt2 + + + +  icon_ul + + + +  icon_ol + + + +  icon_adjust-horiz + + + +  icon_adjust-vert + + + +  icon_document_alt + + + +  icon_documents_alt + + + +  icon_pencil + + + +  icon_pencil-edit_alt + + + +  icon_pencil-edit + + + +  icon_folder-alt + + + +  icon_folder-open_alt + + + +  icon_folder-add_alt + + + +  icon_info_alt + + + +  icon_error-oct_alt + + + +  icon_error-circle_alt + + + +  icon_error-triangle_alt + + + +  icon_question_alt2 + + + +  icon_question + + + +  icon_comment_alt + + + +  icon_chat_alt + + + +  icon_vol-mute_alt + + + +  icon_volume-low_alt + + + +  icon_volume-high_alt + + + +  icon_quotations + + + +  icon_quotations_alt2 + + + +  icon_clock_alt + + + +  icon_lock_alt + + + +  icon_lock-open_alt + + + +  icon_key_alt + + + +  icon_cloud_alt + + + +  icon_cloud-upload_alt + + + +  icon_cloud-download_alt + + + +  icon_image + + + +  icon_images + + + +  icon_lightbulb_alt + + + +  icon_gift_alt + + + +  icon_house_alt + + + +  icon_genius + + + +  icon_mobile + + + +  icon_tablet + + + +  icon_laptop + + + +  icon_desktop + + + +  icon_camera_alt + + + +  icon_mail_alt + + + +  icon_cone_alt + + + +  icon_ribbon_alt + + + +  icon_bag_alt + + + +  icon_creditcard + + + +  icon_cart_alt + + + +  icon_paperclip + + + +  icon_tag_alt + + + +  icon_tags_alt + + + +  icon_trash_alt + + + +  icon_cursor_alt + + + +  icon_mic_alt + + + +  icon_compass_alt + + + +  icon_pin_alt + + + +  icon_pushpin_alt + + + +  icon_map_alt + + + +  icon_drawer_alt + + + +  icon_toolbox_alt + + + +  icon_book_alt + + + +  icon_calendar + + + +  icon_film + + + +  icon_table + + + +  icon_contacts_alt + + + +  icon_headphones + + + +  icon_lifesaver + + + +  icon_piechart + + + +  icon_refresh + + + +  icon_link_alt + + + +  icon_link + + + +  icon_loading + + + +  icon_blocked + + + +  icon_archive_alt + + + +  icon_heart_alt + + +
+ + + +  icon_printer + + + +  icon_calulator + + + +  icon_building + + + +  icon_floppy + + + +  icon_drive + + + +  icon_search-2 + + + +  icon_id + + + +  icon_id-2 + + + +  icon_puzzle + + + +  icon_like + + + +  icon_dislike + + + +  icon_mug + + + +  icon_currency + + + +  icon_wallet + + + +  icon_pens + + + +  icon_easel + + + +  icon_flowchart + + + +  icon_datareport + + + +  icon_briefcase + + + +  icon_shield + + + +  icon_percent + + + +  icon_globe + + + +  icon_globe-2 + + + +  icon_target + + + +  icon_hourglass + + + +  icon_balance + + +
+ + + +  icon_star_alt + + + +  icon_star-half_alt + + + +  icon_star + + + +  icon_star-half + + + +  icon_tools + + + +  icon_tool + + + +  icon_cog + + + +  icon_cogs + + + +  arrow_up_alt + + + +  arrow_down_alt + + + +  arrow_left_alt + + + +  arrow_right_alt + + + +  arrow_left-up_alt + + + +  arrow_right-up_alt + + + +  arrow_right-down_alt + + + +  arrow_left-down_alt + + + +  arrow_condense_alt + + + +  arrow_expand_alt3 + + + +  arrow_carrot_up_alt + + + +  arrow_carrot-down_alt + + + +  arrow_carrot-left_alt + + + +  arrow_carrot-right_alt + + + +  arrow_carrot-2up_alt + + + +  arrow_carrot-2dwnn_alt + + + +  arrow_carrot-2left_alt + + + +  arrow_carrot-2right_alt + + + +  arrow_triangle-up_alt + + + +  arrow_triangle-down_alt + + + +  arrow_triangle-left_alt + + + +  arrow_triangle-right_alt + + + +  icon_minus_alt + + + +  icon_plus_alt + + + +  icon_close_alt + + + +  icon_check_alt + + + +  icon_zoom-out + + + +  icon_zoom-in + + + +  icon_stop_alt + + + +  icon_menu-square_alt + + + +  icon_menu-circle_alt + + + +  icon_document + + + +  icon_documents + + + +  icon_pencil_alt + + + +  icon_folder + + + +  icon_folder-open + + + +  icon_folder-add + + + +  icon_folder_upload + + + +  icon_folder_download + + + +  icon_info + + + +  icon_error-circle + + + +  icon_error-oct + + + +  icon_error-triangle + + + +  icon_question_alt + + + +  icon_comment + + + +  icon_chat + + + +  icon_vol-mute + + + +  icon_volume-low + + + +  icon_volume-high + + + +  icon_quotations_alt + + + +  icon_clock + + + +  icon_lock + + + +  icon_lock-open + + + +  icon_key + + + +  icon_cloud + + + +  icon_cloud-upload + + + +  icon_cloud-download + + + +  icon_lightbulb + + + +  icon_gift + + + +  icon_house + + + +  icon_camera + + + +  icon_mail + + + +  icon_cone + + + +  icon_ribbon + + + +  icon_bag + + + +  icon_cart + + + +  icon_tag + + + +  icon_tags + + + +  icon_trash + + + +  icon_cursor + + + +  icon_mic + + + +  icon_compass + + + +  icon_pin + + + +  icon_pushpin + + + +  icon_map + + + +  icon_drawer + + + +  icon_toolbox + + + +  icon_book + + + +  icon_contacts + + + +  icon_archive + + + +  icon_heart + + + +  icon_profile + + + +  icon_group + + + +  icon_grid-2x2 + + + +  icon_grid-3x3 + + + +  icon_music + + + +  icon_pause_alt + + + +  icon_phone + + + +  icon_upload + + + +  icon_download + + + +  icon_rook + + +
+ + + +  icon_printer-alt + + + +  icon_calculator_alt + + + +  icon_building_alt + + + +  icon_floppy_alt + + + +  icon_drive_alt + + + +  icon_search_alt + + + +  icon_id_alt + + + +  icon_id-2_alt + + + +  icon_puzzle_alt + + + +  icon_like_alt + + + +  icon_dislike_alt + + + +  icon_mug_alt + + + +  icon_currency_alt + + + +  icon_wallet_alt + + + +  icon_pens_alt + + + +  icon_easel_alt + + + +  icon_flowchart_alt + + + +  icon_datareport_alt + + + +  icon_briefcase_alt + + + +  icon_shield_alt + + + +  icon_percent_alt + + + +  icon_globe_alt + + + +  icon_clipboard + + +
+ + + +  social_facebook + + + +  social_twitter + + + +  social_pinterest + + + +  social_googleplus + + + +  social_tumblr + + + +  social_tumbleupon + + + +  social_wordpress + + + +  social_instagram + + + +  social_dribbble + + + +  social_vimeo + + + +  social_linkedin + + + +  social_rss + + + +  social_deviantart + + + +  social_share + + + +  social_myspace + + + +  social_skype + + + +  social_youtube + + + +  social_picassa + + + +  social_googledrive + + + +  social_flickr + + + +  social_blogger + + + +  social_spotify + + + +  social_delicious + + + +  social_facebook_circle + + + +  social_twitter_circle + + + +  social_pinterest_circle + + + +  social_googleplus_circle + + + +  social_tumblr_circle + + + +  social_stumbleupon_circle + + + +  social_wordpress_circle + + + +  social_instagram_circle + + + +  social_dribbble_circle + + + +  social_vimeo_circle + + + +  social_linkedin_circle + + + +  social_rss_circle + + + +  social_deviantart_circle + + + +  social_share_circle + + + +  social_myspace_circle + + + +  social_skype_circle + + + +  social_youtube_circle + + + +  social_picassa_circle + + + +  social_googledrive_alt2 + + + +  social_flickr_circle + + + +  social_blogger_circle + + + +  social_spotify_circle + + + +  social_delicious_circle + + + +  social_facebook_square + + + +  social_twitter_square + + + +  social_pinterest_square + + + +  social_googleplus_square + + + +  social_tumblr_square + + + +  social_stumbleupon_square + + + +  social_wordpress_square + + + +  social_instagram_square + + + +  social_dribbble_square + + + +  social_vimeo_square + + + +  social_linkedin_square + + + +  social_rss_square + + + +  social_deviantart_square + + + +  social_share_square + + + +  social_myspace_square + + + +  social_skype_square + + + +  social_youtube_square + + + +  social_picassa_square + + + +  social_googledrive_square + + + +  social_flickr_square + + + +  social_blogger_square + + + +  social_spotify_square + + + +  social_delicious_square + +
+ +
+ + + + diff --git a/extensions/vscode-colorize-tests/producticons/mit_license.txt b/extensions/vscode-colorize-tests/producticons/mit_license.txt new file mode 100644 index 00000000000..effefee5f0c --- /dev/null +++ b/extensions/vscode-colorize-tests/producticons/mit_license.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) <2013> + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/extensions/vscode-colorize-tests/producticons/test-product-icon-theme.json b/extensions/vscode-colorize-tests/producticons/test-product-icon-theme.json new file mode 100644 index 00000000000..dc076aef5f9 --- /dev/null +++ b/extensions/vscode-colorize-tests/producticons/test-product-icon-theme.json @@ -0,0 +1,43 @@ +{ + // ElegantIcons from https://www.elegantthemes.com/icons/elegant_font.zip + "fonts": [ + { + "id": "elegant", + "src": [ + { + "path": "./ElegantIcons.woff", + "format": "woff" + } + ], + "weight": "normal", + "style": "normal", + } + ], + "iconDefinitions": { + "chevron-down": { + "fontCharacter": "\\43", + }, + "chevron-right": { + "fontCharacter": "\\45" + }, + "error": { + "fontCharacter": "\\e062" + }, + "warning": { + "fontCharacter": "\\e063" + }, + "settings-gear": { + "fontCharacter": "\\e030" + }, + "files": { + "fontCharacter": "\\e056" + }, + "extensions": { + "fontCharacter": "\\e015" + }, + "debug-alt-2": { + "fontCharacter": "\\e072" + } + + } +} diff --git a/package.json b/package.json index 38b128e6fd5..ae6bcde9491 100644 --- a/package.json +++ b/package.json @@ -150,7 +150,7 @@ "source-map": "^0.4.4", "style-loader": "^1.0.0", "ts-loader": "^4.4.2", - "typescript": "^3.9.0-dev.20200229", + "typescript": "3.9.0-dev.20200304", "typescript-formatter": "7.1.0", "underscore": "^1.8.2", "vinyl": "^2.0.0", diff --git a/src/bootstrap-fork.js b/src/bootstrap-fork.js index e0c6cf34c56..7239ae52837 100644 --- a/src/bootstrap-fork.js +++ b/src/bootstrap-fork.js @@ -8,6 +8,9 @@ const bootstrap = require('./bootstrap'); +// Remove global paths from the node module lookup +bootstrap.removeGlobalNodeModuleLookupPaths(); + // Enable ASAR in our forked processes bootstrap.enableASARSupport(); diff --git a/src/bootstrap.js b/src/bootstrap.js index b035adc9f41..cc63fc39422 100644 --- a/src/bootstrap.js +++ b/src/bootstrap.js @@ -53,6 +53,29 @@ exports.injectNodeModuleLookupPath = function (injectPath) { }; //#endregion +//#region Remove global paths from the node lookup paths + +exports.removeGlobalNodeModuleLookupPaths = function() { + // @ts-ignore + const Module = require('module'); + // @ts-ignore + const globalPaths = Module.globalPaths; + + // @ts-ignore + const originalResolveLookupPaths = Module._resolveLookupPaths; + + // @ts-ignore + Module._resolveLookupPaths = function (moduleName, parent) { + const paths = originalResolveLookupPaths(moduleName, parent); + let commonSuffixLength = 0; + while (commonSuffixLength < paths.length && paths[paths.length - 1 - commonSuffixLength] === globalPaths[globalPaths.length - 1 - commonSuffixLength]) { + commonSuffixLength++; + } + return paths.slice(0, paths.length - commonSuffixLength); + }; +}; +//#endregion + //#region Add support for using node_modules.asar /** * @param {string=} nodeModulesPath diff --git a/src/vs/base/browser/ui/dialog/dialog.css b/src/vs/base/browser/ui/dialog/dialog.css index 876f2168fe3..911b5616e70 100644 --- a/src/vs/base/browser/ui/dialog/dialog.css +++ b/src/vs/base/browser/ui/dialog/dialog.css @@ -25,7 +25,7 @@ flex-direction: column-reverse; width: min-content; min-width: 500px; - max-width: 90%; + max-width: 90vw; min-height: 75px; padding: 10px; transform: translate3d(0px, 0px, 0px); diff --git a/src/vs/base/browser/ui/inputbox/inputBox.ts b/src/vs/base/browser/ui/inputbox/inputBox.ts index 96d5013909b..e5ccf0f4bf2 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.ts +++ b/src/vs/base/browser/ui/inputbox/inputBox.ts @@ -248,6 +248,10 @@ export class InputBox extends Widget { } } + public getAriaLabel(): string { + return this.ariaLabel; + } + public get mirrorElement(): HTMLElement | undefined { return this.mirror; } diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 8ab511f551b..b2ee2f104f8 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -180,16 +180,16 @@ class Trait implements ISpliceable, IDisposable { } } -class FocusTrait extends Trait { +class SelectionTrait extends Trait { - constructor(private isAriaSelected: (index: number) => boolean) { - super('focused'); + constructor() { + super('selected'); } renderIndex(index: number, container: HTMLElement): void { super.renderIndex(index, container); - if (this.contains(index) || this.isAriaSelected(index)) { + if (this.contains(index)) { container.setAttribute('aria-selected', 'true'); } else { container.setAttribute('aria-selected', 'false'); @@ -1198,8 +1198,8 @@ export class List implements ISpliceable, IDisposable { renderers: IListRenderer[], private _options: IListOptions = DefaultOptions ) { - this.selection = new Trait('selected'); - this.focus = new FocusTrait(this.selection.contains); + this.selection = new SelectionTrait(); + this.focus = new Trait('focused'); mixin(_options, defaultStyles, false); diff --git a/src/vs/base/browser/ui/splitview/paneview.ts b/src/vs/base/browser/ui/splitview/paneview.ts index 037ffdce27b..8d310aefee9 100644 --- a/src/vs/base/browser/ui/splitview/paneview.ts +++ b/src/vs/base/browser/ui/splitview/paneview.ts @@ -15,12 +15,13 @@ import { Color, RGBA } from 'vs/base/common/color'; import { SplitView, IView } from './splitview'; import { isFirefox } from 'vs/base/browser/browser'; import { DataTransfers } from 'vs/base/browser/dnd'; +import { localize } from 'vs/nls'; export interface IPaneOptions { - ariaHeaderLabel?: string; minimumBodySize?: number; maximumBodySize?: number; expanded?: boolean; + title: string; } export interface IPaneStyles { @@ -116,10 +117,10 @@ export abstract class Pane extends Disposable implements IView { width: number = 0; - constructor(options: IPaneOptions = {}) { + constructor(options: IPaneOptions) { super(); this._expanded = typeof options.expanded === 'undefined' ? true : !!options.expanded; - this.ariaHeaderLabel = options.ariaHeaderLabel || ''; + this.ariaHeaderLabel = localize('viewSection', "{0} Section", options.title); this._minimumBodySize = typeof options.minimumBodySize === 'number' ? options.minimumBodySize : 120; this._maximumBodySize = typeof options.maximumBodySize === 'number' ? options.maximumBodySize : Number.POSITIVE_INFINITY; diff --git a/src/vs/base/parts/quickopen/common/quickOpenScorer.ts b/src/vs/base/common/fuzzyScorer.ts similarity index 97% rename from src/vs/base/parts/quickopen/common/quickOpenScorer.ts rename to src/vs/base/common/fuzzyScorer.ts index 21cb08caace..c43b4f20e85 100644 --- a/src/vs/base/parts/quickopen/common/quickOpenScorer.ts +++ b/src/vs/base/common/fuzzyScorer.ts @@ -285,15 +285,15 @@ export interface IItemAccessor { /** * Just the label of the item to score on. */ - getItemLabel(item: T): string | null; + getItemLabel(item: T): string | undefined; /** - * The optional description of the item to score on. Can be null. + * The optional description of the item to score on. */ - getItemDescription(item: T): string | null; + getItemDescription(item: T): string | undefined; /** - * If the item is a file, the path of the file to score on. Can be null. + * If the item is a file, the path of the file to score on. */ getItemPath(file: T): string | undefined; } @@ -311,7 +311,7 @@ export interface IPreparedQuery { } /** - * Helper function to prepare a search value for scoring in quick open by removing unwanted characters. + * Helper function to prepare a search value for scoring by removing unwanted characters. */ export function prepareQuery(original: string): IPreparedQuery { if (!original) { @@ -364,6 +364,7 @@ function createMatches(offsets: undefined | number[]): IMatch[] { if (!offsets) { return ret; } + let last: IMatch | undefined; for (const pos of offsets) { if (last && last.end === pos) { @@ -373,10 +374,11 @@ function createMatches(offsets: undefined | number[]): IMatch[] { ret.push(last); } } + return ret; } -function doScoreItem(label: string, description: string | null, path: string | undefined, query: IPreparedQuery, fuzzy: boolean): IItemScore { +function doScoreItem(label: string, description: string | undefined, path: string | undefined, query: IPreparedQuery, fuzzy: boolean): IItemScore { // 1.) treat identity matches on full path highest if (path && (isLinux ? query.original === path : equalsIgnoreCase(query.original, path))) { @@ -589,7 +591,7 @@ function compareByMatchLength(matchesA?: IMatch[], matchesB?: IMatch[]): number return matchLengthA === matchLengthB ? 0 : matchLengthB < matchLengthA ? 1 : -1; } -export function fallbackCompare(itemA: T, itemB: T, query: IPreparedQuery, accessor: IItemAccessor): number { +function fallbackCompare(itemA: T, itemB: T, query: IPreparedQuery, accessor: IItemAccessor): number { // check for label + description length and prefer shorter const labelA = accessor.getItemLabel(itemA) || ''; diff --git a/src/vs/base/parts/quickinput/browser/quickInput.ts b/src/vs/base/parts/quickinput/browser/quickInput.ts index 212810233e4..7defd5cf056 100644 --- a/src/vs/base/parts/quickinput/browser/quickInput.ts +++ b/src/vs/base/parts/quickinput/browser/quickInput.ts @@ -142,6 +142,7 @@ class QuickInput extends Disposable implements IQuickInput { private buttonsUpdated = false; private readonly onDidTriggerButtonEmitter = this._register(new Emitter()); private readonly onDidHideEmitter = this._register(new Emitter()); + private readonly onDisposeEmitter = this._register(new Emitter()); protected readonly visibleDisposables = this._register(new DisposableStore()); @@ -235,7 +236,7 @@ class QuickInput extends Disposable implements IQuickInput { this.update(); } - onDidTriggerButton = this.onDidTriggerButtonEmitter.event; + readonly onDidTriggerButton = this.onDidTriggerButtonEmitter.event; show(): void { if (this.visible) { @@ -266,7 +267,7 @@ class QuickInput extends Disposable implements IQuickInput { this.onDidHideEmitter.fire(); } - onDidHide = this.onDidHideEmitter.event; + readonly onDidHide = this.onDidHideEmitter.event; protected update() { if (!this.visible) { @@ -298,9 +299,8 @@ class QuickInput extends Disposable implements IQuickInput { this.ui.leftActionBar.clear(); const leftButtons = this.buttons.filter(button => button === backButton); this.ui.leftActionBar.push(leftButtons.map((button, index) => { - const action = new Action(`id-${index}`, '', button.iconClass || getIconClass(button.iconPath), true, () => { + const action = new Action(`id-${index}`, '', button.iconClass || getIconClass(button.iconPath), true, async () => { this.onDidTriggerButtonEmitter.fire(button); - return Promise.resolve(null); }); action.tooltip = button.tooltip || ''; return action; @@ -308,9 +308,8 @@ class QuickInput extends Disposable implements IQuickInput { this.ui.rightActionBar.clear(); const rightButtons = this.buttons.filter(button => button !== backButton); this.ui.rightActionBar.push(rightButtons.map((button, index) => { - const action = new Action(`id-${index}`, '', button.iconClass || getIconClass(button.iconPath), true, () => { + const action = new Action(`id-${index}`, '', button.iconClass || getIconClass(button.iconPath), true, async () => { this.onDidTriggerButtonEmitter.fire(button); - return Promise.resolve(null); }); action.tooltip = button.tooltip || ''; return action; @@ -362,17 +361,22 @@ class QuickInput extends Disposable implements IQuickInput { } } + readonly onDispose = this.onDisposeEmitter.event; + public dispose(): void { this.hide(); + this.onDisposeEmitter.fire(); + super.dispose(); } } class QuickPick extends QuickInput implements IQuickPick { - private static readonly INPUT_BOX_ARIA_LABEL = localize('quickInputBox.ariaLabel', "Type to narrow down results."); + private static readonly DEFAULT_ARIA_LABEL = localize('quickInputBox.ariaLabel', "Type to narrow down results."); private _value = ''; + private _ariaLabel = QuickPick.DEFAULT_ARIA_LABEL; private _placeholder: string | undefined; private readonly onDidChangeValueEmitter = this._register(new Emitter()); private readonly onDidAcceptEmitter = this._register(new Emitter()); @@ -404,7 +408,6 @@ class QuickPick extends QuickInput implements IQuickPi quickNavigate: IQuickNavigateConfiguration | undefined; - get value() { return this._value; } @@ -414,6 +417,17 @@ class QuickPick extends QuickInput implements IQuickPi this.update(); } + filterValue = (value: string) => value; + + set ariaLabel(ariaLabel: string) { + this._ariaLabel = ariaLabel || QuickPick.DEFAULT_ARIA_LABEL; + this.update(); + } + + get ariaLabel() { + return this._ariaLabel; + } + get placeholder() { return this._placeholder; } @@ -599,7 +613,7 @@ class QuickPick extends QuickInput implements IQuickPi return; } this._value = value; - this.ui.list.filter(this.ui.inputBox.value); + this.ui.list.filter(this.filterValue(this.ui.inputBox.value)); this.trySelectFirst(); this.onDidChangeValueEmitter.fire(value); })); @@ -770,10 +784,17 @@ class QuickPick extends QuickInput implements IQuickPi if (this.ui.inputBox.placeholder !== (this.placeholder || '')) { this.ui.inputBox.placeholder = (this.placeholder || ''); } + if (this.ui.inputBox.ariaLabel !== this.ariaLabel) { + this.ui.inputBox.ariaLabel = this.ariaLabel; + } + this.ui.list.matchOnDescription = this.matchOnDescription; + this.ui.list.matchOnDetail = this.matchOnDetail; + this.ui.list.matchOnLabel = this.matchOnLabel; + this.ui.list.sortByLabel = this.sortByLabel; if (this.itemsUpdated) { this.itemsUpdated = false; this.ui.list.setElements(this.items); - this.ui.list.filter(this.ui.inputBox.value); + this.ui.list.filter(this.filterValue(this.ui.inputBox.value)); this.ui.checkAll.checked = this.ui.list.getAllVisibleChecked(); this.ui.visibleCount.setCount(this.ui.list.getVisibleCount()); this.ui.count.setCount(this.ui.list.getCheckedCount()); @@ -815,12 +836,7 @@ class QuickPick extends QuickInput implements IQuickPi } this.ui.customButton.label = this.customLabel || ''; this.ui.customButton.element.title = this.customHover || ''; - this.ui.list.matchOnDescription = this.matchOnDescription; - this.ui.list.matchOnDetail = this.matchOnDetail; - this.ui.list.matchOnLabel = this.matchOnLabel; - this.ui.list.sortByLabel = this.sortByLabel; this.ui.setComboboxAccessibility(true); - this.ui.inputBox.setAttribute('aria-label', QuickPick.INPUT_BOX_ARIA_LABEL); } } @@ -1378,7 +1394,7 @@ export class QuickInputController extends Disposable { ui.list.sortByLabel = true; ui.ignoreFocusOut = false; this.setComboboxAccessibility(false); - ui.inputBox.removeAttribute('aria-label'); + ui.inputBox.ariaLabel = ''; const backKeybindingLabel = this.options.backKeybindingLabel(); backButton.tooltip = backKeybindingLabel ? localize('quickInput.backWithKeybinding', "Back ({0})", backKeybindingLabel) : localize('quickInput.back', "Back"); @@ -1472,19 +1488,16 @@ export class QuickInputController extends Disposable { } } - accept() { + async accept() { this.onDidAcceptEmitter.fire(); - return Promise.resolve(undefined); } - back() { + async back() { this.onDidTriggerButtonEmitter.fire(this.backButton); - return Promise.resolve(undefined); } - cancel() { + async cancel() { this.hide(); - return Promise.resolve(undefined); } layout(dimension: dom.IDimension, titleBarOffset: number): void { diff --git a/src/vs/base/parts/quickinput/browser/quickInputBox.ts b/src/vs/base/parts/quickinput/browser/quickInputBox.ts index 31c4f7cb160..6ae2b873034 100644 --- a/src/vs/base/parts/quickinput/browser/quickInputBox.ts +++ b/src/vs/base/parts/quickinput/browser/quickInputBox.ts @@ -66,6 +66,14 @@ export class QuickInputBox extends Disposable { this.inputBox.setPlaceHolder(placeholder); } + get ariaLabel() { + return this.inputBox.getAriaLabel(); + } + + set ariaLabel(ariaLabel: string) { + this.inputBox.setAriaLabel(ariaLabel); + } + get password() { return this.inputBox.inputElement.type === 'password'; } diff --git a/src/vs/base/parts/quickinput/browser/quickInputList.ts b/src/vs/base/parts/quickinput/browser/quickInputList.ts index 051f7f56333..c816f564689 100644 --- a/src/vs/base/parts/quickinput/browser/quickInputList.ts +++ b/src/vs/base/parts/quickinput/browser/quickInputList.ts @@ -33,8 +33,12 @@ interface IListElement { readonly index: number; readonly item: IQuickPickItem; readonly saneLabel: string; + readonly saneAriaLabel: string; readonly saneDescription?: string; readonly saneDetail?: string; + readonly labelHighlights?: IMatch[]; + readonly descriptionHighlights?: IMatch[]; + readonly detailHighlights?: IMatch[]; readonly checked: boolean; readonly separator?: IQuickPickSeparator; readonly fireButtonTriggered: (event: IQuickPickItemButtonEvent) => void; @@ -44,6 +48,7 @@ class ListElement implements IListElement { index!: number; item!: IQuickPickItem; saneLabel!: string; + saneAriaLabel!: string; saneDescription?: string; saneDetail?: string; hidden = false; @@ -142,16 +147,14 @@ class ListElementRenderer implements IListRenderer s && parseCodicons(s).text) - .filter(s => !!s) - .join(', ')); + data.entry.setAttribute('aria-label', element.saneAriaLabel); // Separator if (element.separator && element.separator.label) { @@ -364,12 +367,24 @@ export class QuickInputList { this.elements = inputElements.reduce((result, item, index) => { if (item.type !== 'separator') { const previous = index && inputElements[index - 1]; + const saneLabel = item.label && item.label.replace(/\r?\n/g, ' '); + const saneDescription = item.description && item.description.replace(/\r?\n/g, ' '); + const saneDetail = item.detail && item.detail.replace(/\r?\n/g, ' '); + const saneAriaLabel = item.ariaLabel || [saneLabel, saneDescription, saneDetail] + .map(s => s && parseCodicons(s).text) + .filter(s => !!s) + .join(', '); + result.push(new ListElement({ index, item, - saneLabel: item.label && item.label.replace(/\r?\n/g, ' '), - saneDescription: item.description && item.description.replace(/\r?\n/g, ' '), - saneDetail: item.detail && item.detail.replace(/\r?\n/g, ' '), + saneLabel, + saneAriaLabel, + saneDescription, + saneDetail, + labelHighlights: item.highlights?.label, + descriptionHighlights: item.highlights?.description, + detailHighlights: item.highlights?.detail, checked: false, separator: previous && previous.type === 'separator' ? previous : undefined, fireButtonTriggered @@ -472,6 +487,7 @@ export class QuickInputList { filter(query: string) { if (!(this.sortByLabel || this.matchOnLabel || this.matchOnDescription || this.matchOnDetail)) { + this.list.layout(); return; } query = query.trim(); diff --git a/src/vs/base/parts/quickinput/common/quickInput.ts b/src/vs/base/parts/quickinput/common/quickInput.ts index 10c8255a444..9a844ac255f 100644 --- a/src/vs/base/parts/quickinput/common/quickInput.ts +++ b/src/vs/base/parts/quickinput/common/quickInput.ts @@ -6,14 +6,25 @@ import { ResolvedKeybinding } from 'vs/base/common/keyCodes'; import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { IMatch } from 'vs/base/common/filters'; + +export interface IQuickPickItemHighlights { + label?: IMatch[]; + description?: IMatch[]; + detail?: IMatch[]; +} export interface IQuickPickItem { type?: 'item'; id?: string; label: string; + ariaLabel?: string; description?: string; detail?: string; iconClasses?: string[]; + italic?: boolean; + highlights?: IQuickPickItemHighlights; buttons?: IQuickInputButton[]; picked?: boolean; alwaysShow?: boolean; @@ -125,7 +136,10 @@ export interface IInputOptions { validateInput?: (input: string) => Promise; } -export interface IQuickInput { +export interface IQuickInput extends IDisposable { + + readonly onDidHide: Event; + readonly onDispose: Event; title: string | undefined; @@ -146,16 +160,20 @@ export interface IQuickInput { show(): void; hide(): void; - - onDidHide: Event; - - dispose(): void; } export interface IQuickPick extends IQuickInput { value: string; + /** + * A method that allows to massage the value used + * for filtering, e.g, to remove certain parts. + */ + filterValue: (value: string) => string; + + ariaLabel: string; + placeholder: string | undefined; readonly onDidChangeValue: Event; diff --git a/src/vs/base/parts/quickopen/browser/quickOpenModel.ts b/src/vs/base/parts/quickopen/browser/quickOpenModel.ts index b846129db46..68cc687c5a3 100644 --- a/src/vs/base/parts/quickopen/browser/quickOpenModel.ts +++ b/src/vs/base/parts/quickopen/browser/quickOpenModel.ts @@ -17,7 +17,7 @@ import { IQuickOpenStyles } from 'vs/base/parts/quickopen/browser/quickOpenWidge import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; import { OS } from 'vs/base/common/platform'; import { ResolvedKeybinding } from 'vs/base/common/keyCodes'; -import { IItemAccessor } from 'vs/base/parts/quickopen/common/quickOpenScorer'; +import { IItemAccessor } from 'vs/base/common/fuzzyScorer'; import { coalesce } from 'vs/base/common/arrays'; import { IMatch } from 'vs/base/common/filters'; @@ -35,12 +35,12 @@ let IDS = 0; export class QuickOpenItemAccessorClass implements IItemAccessor { - getItemLabel(entry: QuickOpenEntry): string | null { - return types.withUndefinedAsNull(entry.getLabel()); + getItemLabel(entry: QuickOpenEntry): string | undefined { + return entry.getLabel(); } - getItemDescription(entry: QuickOpenEntry): string | null { - return types.withUndefinedAsNull(entry.getDescription()); + getItemDescription(entry: QuickOpenEntry): string | undefined { + return entry.getDescription(); } getItemPath(entry: QuickOpenEntry): string | undefined { diff --git a/src/vs/base/parts/quickopen/test/common/quickOpenScorer.test.ts b/src/vs/base/test/common/fuzzyScorer.test.ts similarity index 99% rename from src/vs/base/parts/quickopen/test/common/quickOpenScorer.test.ts rename to src/vs/base/test/common/fuzzyScorer.test.ts index a7bbe6fb2a0..43cb8756d3e 100644 --- a/src/vs/base/parts/quickopen/test/common/quickOpenScorer.test.ts +++ b/src/vs/base/test/common/fuzzyScorer.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import * as scorer from 'vs/base/parts/quickopen/common/quickOpenScorer'; +import * as scorer from 'vs/base/common/fuzzyScorer'; import { URI } from 'vs/base/common/uri'; import { basename, dirname, sep } from 'vs/base/common/path'; import { isWindows } from 'vs/base/common/platform'; @@ -49,14 +49,14 @@ function scoreItem(item: T, query: string, fuzzy: boolean, accessor: scorer.I return scorer.scoreItem(item, scorer.prepareQuery(query), fuzzy, accessor, cache); } -function compareItemsByScore(itemA: T, itemB: T, query: string, fuzzy: boolean, accessor: scorer.IItemAccessor, cache: scorer.ScorerCache, fallbackComparer = scorer.fallbackCompare): number { - return scorer.compareItemsByScore(itemA, itemB, scorer.prepareQuery(query), fuzzy, accessor, cache, fallbackComparer); +function compareItemsByScore(itemA: T, itemB: T, query: string, fuzzy: boolean, accessor: scorer.IItemAccessor, cache: scorer.ScorerCache, fallbackComparer?: (itemA: T, itemB: T, query: scorer.IPreparedQuery, accessor: scorer.IItemAccessor) => number): number { + return scorer.compareItemsByScore(itemA, itemB, scorer.prepareQuery(query), fuzzy, accessor, cache, fallbackComparer as any); } const NullAccessor = new NullAccessorClass(); let cache: scorer.ScorerCache = Object.create(null); -suite('Quick Open Scorer', () => { +suite('Fuzzy Scorer', () => { setup(() => { cache = Object.create(null); diff --git a/src/vs/code/electron-browser/issue/issueReporterMain.ts b/src/vs/code/electron-browser/issue/issueReporterMain.ts index 8ed46109857..2d32cf27fde 100644 --- a/src/vs/code/electron-browser/issue/issueReporterMain.ts +++ b/src/vs/code/electron-browser/issue/issueReporterMain.ts @@ -81,6 +81,8 @@ export class IssueReporter extends Disposable { this.initServices(configuration); const isSnap = process.platform === 'linux' && process.env.SNAP && process.env.SNAP_REVISION; + + const targetExtension = configuration.data.extensionId ? configuration.data.enabledExtensions.find(extension => extension.id === configuration.data.extensionId) : undefined; this.issueReporterModel = new IssueReporterModel({ issueType: configuration.data.issueType || IssueType.Bug, versionInfo: { @@ -88,8 +90,8 @@ export class IssueReporter extends Disposable { os: `${os.type()} ${os.arch()} ${os.release()}${isSnap ? ' snap' : ''}` }, extensionsDisabled: !!this.environmentService.disableExtensions, - fileOnExtension: configuration.data.extensionId ? true : undefined, - selectedExtension: configuration.data.extensionId ? configuration.data.enabledExtensions.filter(extension => extension.id === configuration.data.extensionId)[0] : undefined + fileOnExtension: configuration.data.extensionId ? !targetExtension?.isBuiltin : undefined, + selectedExtension: targetExtension, }); const issueReporterElement = this.getElementById('issue-reporter'); @@ -260,19 +262,20 @@ export class IssueReporter extends Disposable { } private handleExtensionData(extensions: IssueReporterExtensionData[]) { - const { nonThemes, themes } = collections.groupBy(extensions, ext => { + const installedExtensions = extensions.filter(x => !x.isBuiltin); + const { nonThemes, themes } = collections.groupBy(installedExtensions, ext => { return ext.isTheme ? 'themes' : 'nonThemes'; }); const numberOfThemeExtesions = themes && themes.length; - this.issueReporterModel.update({ numberOfThemeExtesions, enabledNonThemeExtesions: nonThemes, allExtensions: extensions }); + this.issueReporterModel.update({ numberOfThemeExtesions, enabledNonThemeExtesions: nonThemes, allExtensions: installedExtensions }); this.updateExtensionTable(nonThemes, numberOfThemeExtesions); - if (this.environmentService.disableExtensions || extensions.length === 0) { + if (this.environmentService.disableExtensions || installedExtensions.length === 0) { (this.getElementById('disableExtensions')).disabled = true; } - this.updateExtensionSelector(extensions); + this.updateExtensionSelector(installedExtensions); } private handleSettingsSearchData(data: ISettingsSearchIssueReporterData): void { @@ -748,10 +751,14 @@ export class IssueReporter extends Disposable { private setSourceOptions(): void { const sourceSelect = this.getElementById('issue-source')! as HTMLSelectElement; - const { issueType, fileOnExtension } = this.issueReporterModel.getData(); + const { issueType, fileOnExtension, selectedExtension } = this.issueReporterModel.getData(); let selected = sourceSelect.selectedIndex; - if (selected === -1 && fileOnExtension !== undefined) { - selected = fileOnExtension ? 2 : 1; + if (selected === -1) { + if (fileOnExtension !== undefined) { + selected = fileOnExtension ? 2 : 1; + } else if (selectedExtension?.isBuiltin) { + selected = 1; + } } sourceSelect.innerHTML = ''; diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index ac772deb3cf..0e6c2358199 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -49,10 +49,10 @@ import { IFileService } from 'vs/platform/files/common/files'; import { DiskFileSystemProvider } from 'vs/platform/files/electron-browser/diskFileSystemProvider'; import { Schemas } from 'vs/base/common/network'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IUserDataSyncService, IUserDataSyncStoreService, registerConfiguration, IUserDataSyncLogService, IUserDataSyncUtilService, ISettingsSyncService, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, IUserDataSyncStoreService, registerConfiguration, IUserDataSyncLogService, IUserDataSyncUtilService, ISettingsSyncService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; -import { UserDataSyncChannel, UserDataSyncUtilServiceClient, SettingsSyncChannel, UserDataAutoSyncChannel, UserDataSyncStoreServiceChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc'; +import { UserDataSyncChannel, UserDataSyncUtilServiceClient, SettingsSyncChannel, UserDataAutoSyncChannel, UserDataSyncStoreServiceChannel, UserDataSyncBackupStoreServiceChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc'; import { IElectronService } from 'vs/platform/electron/node/electron'; import { LoggerService } from 'vs/platform/log/node/loggerService'; import { UserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSyncLog'; @@ -67,6 +67,7 @@ import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagemen import { UserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSyncEnablementService'; import { IAuthenticationTokenService, AuthenticationTokenService } from 'vs/platform/authentication/common/authentication'; import { AuthenticationTokenServiceChannel } from 'vs/platform/authentication/common/authenticationIpc'; +import { UserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSyncBackupStoreService'; export interface ISharedProcessConfiguration { readonly machineId: string; @@ -194,6 +195,7 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat services.set(IUserDataSyncUtilService, new UserDataSyncUtilServiceClient(server.getChannel('userDataSyncUtil', client => client.ctx !== 'main'))); services.set(IGlobalExtensionEnablementService, new SyncDescriptor(GlobalExtensionEnablementService)); services.set(IUserDataSyncStoreService, new SyncDescriptor(UserDataSyncStoreService)); + services.set(IUserDataSyncBackupStoreService, new SyncDescriptor(UserDataSyncBackupStoreService)); services.set(IUserDataSyncEnablementService, new SyncDescriptor(UserDataSyncEnablementService)); services.set(ISettingsSyncService, new SyncDescriptor(SettingsSynchroniser)); services.set(IUserDataSyncService, new SyncDescriptor(UserDataSyncService)); @@ -223,6 +225,10 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat const userDataSyncStoreServiceChannel = new UserDataSyncStoreServiceChannel(userDataSyncStoreService); server.registerChannel('userDataSyncStoreService', userDataSyncStoreServiceChannel); + const userDataSyncBackupStoreService = accessor.get(IUserDataSyncBackupStoreService); + const userDataSyncBackupStoreServiceChannel = new UserDataSyncBackupStoreServiceChannel(userDataSyncBackupStoreService); + server.registerChannel('userDataSyncBackupStoreService', userDataSyncBackupStoreServiceChannel); + const settingsSyncService = accessor.get(ISettingsSyncService); const settingsSyncChannel = new SettingsSyncChannel(settingsSyncService); server.registerChannel('settingsSync', settingsSyncChannel); diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index 7e4ddbbe0a3..de0a7dff13a 100644 --- a/src/vs/code/electron-main/window.ts +++ b/src/vs/code/electron-main/window.ts @@ -392,6 +392,8 @@ export class CodeWindow extends Disposable implements ICodeWindow { this.setFullScreen(false); this.setFullScreen(true); } + + this.sendWhenReady('vscode:displayChanged'); }, 100)); const displayChangedListener = () => simpleFullScreenScheduler.schedule(); diff --git a/src/vs/editor/browser/controller/coreCommands.ts b/src/vs/editor/browser/controller/coreCommands.ts index ca00039a592..b2a8e0c96e2 100644 --- a/src/vs/editor/browser/controller/coreCommands.ts +++ b/src/vs/editor/browser/controller/coreCommands.ts @@ -856,18 +856,13 @@ export namespace CoreNavigationCommands { } })); - export const CursorLineStart: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand { - constructor() { - super({ - id: 'cursorLineStart', - precondition: undefined, - kbOpts: { - weight: CORE_WEIGHT, - kbExpr: EditorContextKeys.textInputFocus, - primary: 0, - mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_A } - } - }); + class LineStartCommand extends CoreEditorCommand { + + private readonly _inSelectionMode: boolean; + + constructor(opts: ICommandOptions & { inSelectionMode: boolean; }) { + super(opts); + this._inSelectionMode = opts.inSelectionMode; } public runCoreEditorCommand(cursors: ICursors, args: any): void { @@ -885,11 +880,35 @@ export namespace CoreNavigationCommands { for (let i = 0, len = cursors.length; i < len; i++) { const cursor = cursors[i]; const lineNumber = cursor.modelState.position.lineNumber; - result[i] = CursorState.fromModelState(cursor.modelState.move(false, lineNumber, 1, 0)); + result[i] = CursorState.fromModelState(cursor.modelState.move(this._inSelectionMode, lineNumber, 1, 0)); } return result; } - }); + } + + export const CursorLineStart: CoreEditorCommand = registerEditorCommand(new LineStartCommand({ + inSelectionMode: false, + id: 'cursorLineStart', + precondition: undefined, + kbOpts: { + weight: CORE_WEIGHT, + kbExpr: EditorContextKeys.textInputFocus, + primary: 0, + mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_A } + } + })); + + export const CursorLineStartSelect: CoreEditorCommand = registerEditorCommand(new LineStartCommand({ + inSelectionMode: true, + id: 'cursorLineStartSelect', + precondition: undefined, + kbOpts: { + weight: CORE_WEIGHT, + kbExpr: EditorContextKeys.textInputFocus, + primary: 0, + mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KEY_A } + } + })); class EndCommand extends CoreEditorCommand { @@ -935,18 +954,13 @@ export namespace CoreNavigationCommands { } })); - export const CursorLineEnd: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand { - constructor() { - super({ - id: 'cursorLineEnd', - precondition: undefined, - kbOpts: { - weight: CORE_WEIGHT, - kbExpr: EditorContextKeys.textInputFocus, - primary: 0, - mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_E } - } - }); + class LineEndCommand extends CoreEditorCommand { + + private readonly _inSelectionMode: boolean; + + constructor(opts: ICommandOptions & { inSelectionMode: boolean; }) { + super(opts); + this._inSelectionMode = opts.inSelectionMode; } public runCoreEditorCommand(cursors: ICursors, args: any): void { @@ -965,11 +979,35 @@ export namespace CoreNavigationCommands { const cursor = cursors[i]; const lineNumber = cursor.modelState.position.lineNumber; const maxColumn = context.model.getLineMaxColumn(lineNumber); - result[i] = CursorState.fromModelState(cursor.modelState.move(false, lineNumber, maxColumn, 0)); + result[i] = CursorState.fromModelState(cursor.modelState.move(this._inSelectionMode, lineNumber, maxColumn, 0)); } return result; } - }); + } + + export const CursorLineEnd: CoreEditorCommand = registerEditorCommand(new LineEndCommand({ + inSelectionMode: false, + id: 'cursorLineEnd', + precondition: undefined, + kbOpts: { + weight: CORE_WEIGHT, + kbExpr: EditorContextKeys.textInputFocus, + primary: 0, + mac: { primary: KeyMod.WinCtrl | KeyCode.KEY_E } + } + })); + + export const CursorLineEndSelect: CoreEditorCommand = registerEditorCommand(new LineEndCommand({ + inSelectionMode: true, + id: 'cursorLineEndSelect', + precondition: undefined, + kbOpts: { + weight: CORE_WEIGHT, + kbExpr: EditorContextKeys.textInputFocus, + primary: 0, + mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KEY_E } + } + })); class TopCommand extends CoreEditorCommand { diff --git a/src/vs/editor/browser/controller/mouseTarget.ts b/src/vs/editor/browser/controller/mouseTarget.ts index f83ff9bfd94..f0ad0cf79dc 100644 --- a/src/vs/editor/browser/controller/mouseTarget.ts +++ b/src/vs/editor/browser/controller/mouseTarget.ts @@ -925,6 +925,23 @@ export class MouseTargetFactory { } } + // For inline decorations, Gecko returns the `` of the line and the offset is the `` with the inline decoration + if (hitResult.offsetNode.nodeType === hitResult.offsetNode.ELEMENT_NODE) { + const parent1 = hitResult.offsetNode.parentNode; // expected to be the view line div + const parent1ClassName = parent1 && parent1.nodeType === parent1.ELEMENT_NODE ? (parent1).className : null; + + if (parent1ClassName === ViewLine.CLASS_NAME) { + const tokenSpan = hitResult.offsetNode.childNodes[Math.min(hitResult.offset, hitResult.offsetNode.childNodes.length - 1)]; + if (tokenSpan) { + const p = ctx.getPositionFromDOMInfo(tokenSpan, 0); + return { + position: p, + hitTarget: null + }; + } + } + } + return { position: null, hitTarget: hitResult.offsetNode diff --git a/src/vs/editor/browser/controller/textAreaHandler.ts b/src/vs/editor/browser/controller/textAreaHandler.ts index 79e69fdde44..043bd0eb018 100644 --- a/src/vs/editor/browser/controller/textAreaHandler.ts +++ b/src/vs/editor/browser/controller/textAreaHandler.ts @@ -252,9 +252,9 @@ export class TextAreaHandler extends ViewPart { this._viewController.setSelection('keyboard', modelSelection); })); - this._register(this._textAreaInput.onCompositionStart(() => { + this._register(this._textAreaInput.onCompositionStart((e) => { const lineNumber = this._selections[0].startLineNumber; - const column = this._selections[0].startColumn; + const column = this._selections[0].startColumn - (e.moveOneCharacterLeft ? 1 : 0); this._context.privateViewEventBus.emit(new viewEvents.ViewRevealRangeRequestEvent( 'keyboard', diff --git a/src/vs/editor/browser/controller/textAreaInput.ts b/src/vs/editor/browser/controller/textAreaInput.ts index ba6a141c70a..225d9e73e8a 100644 --- a/src/vs/editor/browser/controller/textAreaInput.ts +++ b/src/vs/editor/browser/controller/textAreaInput.ts @@ -95,6 +95,10 @@ class InMemoryClipboardMetadataManager { } } +export interface ICompositionStartEvent { + moveOneCharacterLeft: boolean; +} + /** * Writes screen reader content to the textarea and is able to analyze its input events to generate: * - onCut @@ -126,8 +130,8 @@ export class TextAreaInput extends Disposable { private _onType = this._register(new Emitter()); public readonly onType: Event = this._onType.event; - private _onCompositionStart = this._register(new Emitter()); - public readonly onCompositionStart: Event = this._onCompositionStart.event; + private _onCompositionStart = this._register(new Emitter()); + public readonly onCompositionStart: Event = this._onCompositionStart.event; private _onCompositionUpdate = this._register(new Emitter()); public readonly onCompositionUpdate: Event = this._onCompositionUpdate.event; @@ -165,9 +169,11 @@ export class TextAreaInput extends Disposable { this._isDoingComposition = false; this._nextCommand = ReadFromTextArea.Type; + let lastKeyDown: IKeyboardEvent | null = null; + this._register(dom.addStandardDisposableListener(textArea.domNode, 'keydown', (e: IKeyboardEvent) => { - if (this._isDoingComposition && - (e.keyCode === KeyCode.KEY_IN_COMPOSITION || e.keyCode === KeyCode.Backspace)) { + if (e.keyCode === KeyCode.KEY_IN_COMPOSITION + || (this._isDoingComposition && e.keyCode === KeyCode.Backspace)) { // Stop propagation for keyDown events if the IME is processing key input e.stopPropagation(); } @@ -177,6 +183,8 @@ export class TextAreaInput extends Disposable { // See https://msdn.microsoft.com/en-us/library/ie/ms536939(v=vs.85).aspx e.preventDefault(); } + + lastKeyDown = e; this._onKeyDown.fire(e); })); @@ -190,12 +198,35 @@ export class TextAreaInput extends Disposable { } this._isDoingComposition = true; - // In IE we cannot set .value when handling 'compositionstart' because the entire composition will get canceled. - if (!browser.isEdge) { + let moveOneCharacterLeft = false; + if ( + platform.isMacintosh + && lastKeyDown + && lastKeyDown.equals(KeyCode.KEY_IN_COMPOSITION) + && this._textAreaState.selectionStart === this._textAreaState.selectionEnd + && this._textAreaState.selectionStart > 0 + && this._textAreaState.value.substr(this._textAreaState.selectionStart - 1, 1) === e.data + ) { + // Handling long press case on macOS + arrow key => pretend the character was selected + if (lastKeyDown.code === 'ArrowRight' || lastKeyDown.code === 'ArrowLeft') { + moveOneCharacterLeft = true; + } + } + + if (moveOneCharacterLeft) { + this._textAreaState = new TextAreaState( + this._textAreaState.value, + this._textAreaState.selectionStart - 1, + this._textAreaState.selectionEnd, + this._textAreaState.selectionStartPosition ? new Position(this._textAreaState.selectionStartPosition.lineNumber, this._textAreaState.selectionStartPosition.column - 1) : null, + this._textAreaState.selectionEndPosition + ); + } else if (!browser.isEdge) { + // In IE we cannot set .value when handling 'compositionstart' because the entire composition will get canceled. this._setAndWriteTextAreaState('compositionstart', TextAreaState.EMPTY); } - this._onCompositionStart.fire(); + this._onCompositionStart.fire({ moveOneCharacterLeft }); })); /** diff --git a/src/vs/editor/browser/services/openerService.ts b/src/vs/editor/browser/services/openerService.ts index 4dfb0035522..0390927a4c4 100644 --- a/src/vs/editor/browser/services/openerService.ts +++ b/src/vs/editor/browser/services/openerService.ts @@ -11,7 +11,7 @@ import { Schemas } from 'vs/base/common/network'; import { normalizePath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; +import { ICommandService } from 'vs/platform/commands/common/commands'; import { IOpener, IOpenerService, IValidator, IExternalUriResolver, OpenOptions, ResolveExternalUriOptions, IResolvedExternalUri, IExternalOpener, matchesScheme } from 'vs/platform/opener/common/opener'; import { EditorOpenContext } from 'vs/platform/editor/common/editor'; @@ -28,9 +28,6 @@ class CommandOpener implements IOpener { if (typeof target === 'string') { target = URI.parse(target); } - if (!CommandsRegistry.getCommand(target.path)) { - throw new Error(`command '${target.path}' NOT known`); - } // execute as command let args: any = []; try { diff --git a/src/vs/editor/common/controller/cursor.ts b/src/vs/editor/common/controller/cursor.ts index a7dcdf9040c..07a39961aa6 100644 --- a/src/vs/editor/common/controller/cursor.ts +++ b/src/vs/editor/common/controller/cursor.ts @@ -779,7 +779,7 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { } private _type(source: string, text: string): void { - if (!this._isDoingComposition && source === 'keyboard') { + if (source === 'keyboard') { // If this event is coming straight from the keyboard, look for electric characters and enter const len = text.length; @@ -790,7 +790,7 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { // Here we must interpret each typed character individually const autoClosedCharacters = AutoClosedAction.getAllAutoClosedCharacters(this._autoClosedActions); - this._executeEditOperation(TypeOperations.typeWithInterceptors(this._prevEditOperationType, this.context.config, this.context.model, this.getSelections(), autoClosedCharacters, chr)); + this._executeEditOperation(TypeOperations.typeWithInterceptors(this._isDoingComposition, this._prevEditOperationType, this.context.config, this.context.model, this.getSelections(), autoClosedCharacters, chr)); offset += charLength; } diff --git a/src/vs/editor/common/controller/cursorMoveOperations.ts b/src/vs/editor/common/controller/cursorMoveOperations.ts index df45d78e9f9..0687dbb6426 100644 --- a/src/vs/editor/common/controller/cursorMoveOperations.ts +++ b/src/vs/editor/common/controller/cursorMoveOperations.ts @@ -91,9 +91,10 @@ export class MoveOperations { public static down(config: CursorConfiguration, model: ICursorSimpleModel, lineNumber: number, column: number, leftoverVisibleColumns: number, count: number, allowMoveOnLastLine: boolean): CursorPosition { const currentVisibleColumn = CursorColumns.visibleColumnFromColumn(model.getLineContent(lineNumber), column, config.tabSize) + leftoverVisibleColumns; + const lineCount = model.getLineCount(); + const wasOnLastPosition = (lineNumber === lineCount && column === model.getLineMaxColumn(lineNumber)); lineNumber = lineNumber + count; - let lineCount = model.getLineCount(); if (lineNumber > lineCount) { lineNumber = lineCount; if (allowMoveOnLastLine) { @@ -105,7 +106,11 @@ export class MoveOperations { column = CursorColumns.columnFromVisibleColumn2(config, model, lineNumber, currentVisibleColumn); } - leftoverVisibleColumns = currentVisibleColumn - CursorColumns.visibleColumnFromColumn(model.getLineContent(lineNumber), column, config.tabSize); + if (wasOnLastPosition) { + leftoverVisibleColumns = 0; + } else { + leftoverVisibleColumns = currentVisibleColumn - CursorColumns.visibleColumnFromColumn(model.getLineContent(lineNumber), column, config.tabSize); + } return new CursorPosition(lineNumber, column, leftoverVisibleColumns); } @@ -144,6 +149,7 @@ export class MoveOperations { public static up(config: CursorConfiguration, model: ICursorSimpleModel, lineNumber: number, column: number, leftoverVisibleColumns: number, count: number, allowMoveOnFirstLine: boolean): CursorPosition { const currentVisibleColumn = CursorColumns.visibleColumnFromColumn(model.getLineContent(lineNumber), column, config.tabSize) + leftoverVisibleColumns; + const wasOnFirstPosition = (lineNumber === 1 && column === 1); lineNumber = lineNumber - count; if (lineNumber < 1) { @@ -157,7 +163,11 @@ export class MoveOperations { column = CursorColumns.columnFromVisibleColumn2(config, model, lineNumber, currentVisibleColumn); } - leftoverVisibleColumns = currentVisibleColumn - CursorColumns.visibleColumnFromColumn(model.getLineContent(lineNumber), column, config.tabSize); + if (wasOnFirstPosition) { + leftoverVisibleColumns = 0; + } else { + leftoverVisibleColumns = currentVisibleColumn - CursorColumns.visibleColumnFromColumn(model.getLineContent(lineNumber), column, config.tabSize); + } return new CursorPosition(lineNumber, column, leftoverVisibleColumns); } diff --git a/src/vs/editor/common/controller/cursorTypeOperations.ts b/src/vs/editor/common/controller/cursorTypeOperations.ts index 64db6112da9..0155bf76b6e 100644 --- a/src/vs/editor/common/controller/cursorTypeOperations.ts +++ b/src/vs/editor/common/controller/cursorTypeOperations.ts @@ -269,9 +269,15 @@ export class TypeOperations { commands[i] = null; continue; } - let pos = selection.getPosition(); - let startColumn = Math.max(1, pos.column - replaceCharCnt); - let range = new Range(pos.lineNumber, startColumn, pos.lineNumber, pos.column); + const pos = selection.getPosition(); + const startColumn = Math.max(1, pos.column - replaceCharCnt); + const range = new Range(pos.lineNumber, startColumn, pos.lineNumber, pos.column); + const oldText = model.getValueInRange(range); + if (oldText === txt) { + // => ignore composition that doesn't do anything + commands[i] = null; + continue; + } commands[i] = new ReplaceCommand(range, txt); } return new EditOperationResult(EditOperationType.Typing, commands, { @@ -796,9 +802,9 @@ export class TypeOperations { return null; } - public static typeWithInterceptors(prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], autoClosedCharacters: Range[], ch: string): EditOperationResult { + public static typeWithInterceptors(isDoingComposition: boolean, prevEditOperationType: EditOperationType, config: CursorConfiguration, model: ITextModel, selections: Selection[], autoClosedCharacters: Range[], ch: string): EditOperationResult { - if (ch === '\n') { + if (!isDoingComposition && ch === '\n') { let commands: ICommand[] = []; for (let i = 0, len = selections.length; i < len; i++) { commands[i] = TypeOperations._enter(config, model, false, selections[i]); @@ -809,7 +815,7 @@ export class TypeOperations { }); } - if (this._isAutoIndentType(config, model, selections)) { + if (!isDoingComposition && this._isAutoIndentType(config, model, selections)) { let commands: Array = []; let autoIndentFails = false; for (let i = 0, len = selections.length; i < len; i++) { @@ -827,13 +833,15 @@ export class TypeOperations { } } - if (this._isAutoClosingOvertype(config, model, selections, autoClosedCharacters, ch)) { + if (!isDoingComposition && this._isAutoClosingOvertype(config, model, selections, autoClosedCharacters, ch)) { return this._runAutoClosingOvertype(prevEditOperationType, config, model, selections, ch); } - const autoClosingPairOpenCharType = this._isAutoClosingOpenCharType(config, model, selections, ch, true); - if (autoClosingPairOpenCharType) { - return this._runAutoClosingOpenCharType(prevEditOperationType, config, model, selections, ch, true, autoClosingPairOpenCharType); + if (!isDoingComposition) { + const autoClosingPairOpenCharType = this._isAutoClosingOpenCharType(config, model, selections, ch, true); + if (autoClosingPairOpenCharType) { + return this._runAutoClosingOpenCharType(prevEditOperationType, config, model, selections, ch, true, autoClosingPairOpenCharType); + } } if (this._isSurroundSelectionType(config, model, selections, ch)) { @@ -842,7 +850,7 @@ export class TypeOperations { // Electric characters make sense only when dealing with a single cursor, // as multiple cursors typing brackets for example would interfer with bracket matching - if (this._isTypeInterceptorElectricChar(config, model, selections)) { + if (!isDoingComposition && this._isTypeInterceptorElectricChar(config, model, selections)) { const r = this._typeInterceptorElectricChar(prevEditOperationType, config, model, selections[0], ch); if (r) { return r; diff --git a/src/vs/editor/common/standaloneStrings.ts b/src/vs/editor/common/standaloneStrings.ts index 130e4884908..164708bd894 100644 --- a/src/vs/editor/common/standaloneStrings.ts +++ b/src/vs/editor/common/standaloneStrings.ts @@ -45,6 +45,10 @@ export namespace GoToLineNLS { export const gotoLineActionLabel = nls.localize('gotoLineActionLabel', "Go to Line..."); } +export namespace QuickHelpNLS { + export const helpQuickAccessActionLabel = nls.localize('helpQuickAccess', "Show all Quick Access Providers"); +} + export namespace QuickCommandNLS { export const ariaLabelEntryWithKey = nls.localize('ariaLabelEntryWithKey', "{0}, {1}, commands"); export const ariaLabelEntry = nls.localize('ariaLabelEntry', "{0}, commands"); diff --git a/src/vs/editor/contrib/caretOperations/moveCaretCommand.ts b/src/vs/editor/contrib/caretOperations/moveCaretCommand.ts index 0ce4feb6174..81db0fd6217 100644 --- a/src/vs/editor/contrib/caretOperations/moveCaretCommand.ts +++ b/src/vs/editor/contrib/caretOperations/moveCaretCommand.ts @@ -13,66 +13,43 @@ export class MoveCaretCommand implements ICommand { private readonly _selection: Selection; private readonly _isMovingLeft: boolean; - private _cutStartIndex: number; - private _cutEndIndex: number; - private _moved: boolean; - - private _selectionId: string | null; - constructor(selection: Selection, isMovingLeft: boolean) { this._selection = selection; this._isMovingLeft = isMovingLeft; - this._cutStartIndex = -1; - this._cutEndIndex = -1; - this._moved = false; - this._selectionId = null; } public getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void { - let s = this._selection; - this._selectionId = builder.trackSelection(s); - if (s.startLineNumber !== s.endLineNumber) { + if (this._selection.startLineNumber !== this._selection.endLineNumber || this._selection.isEmpty()) { return; } - if (this._isMovingLeft && s.startColumn === 0) { - return; - } else if (!this._isMovingLeft && s.endColumn === model.getLineMaxColumn(s.startLineNumber)) { + const lineNumber = this._selection.startLineNumber; + const startColumn = this._selection.startColumn; + const endColumn = this._selection.endColumn; + if (this._isMovingLeft && startColumn === 1) { + return; + } + if (!this._isMovingLeft && endColumn === model.getLineMaxColumn(lineNumber)) { return; } - - let lineNumber = s.selectionStartLineNumber; - let lineContent = model.getLineContent(lineNumber); - - let left: string; - let middle: string; - let right: string; if (this._isMovingLeft) { - left = lineContent.substring(0, s.startColumn - 2); - middle = lineContent.substring(s.startColumn - 1, s.endColumn - 1); - right = lineContent.substring(s.startColumn - 2, s.startColumn - 1) + lineContent.substring(s.endColumn - 1); + const rangeBefore = new Range(lineNumber, startColumn - 1, lineNumber, startColumn); + const charBefore = model.getValueInRange(rangeBefore); + builder.addEditOperation(rangeBefore, null); + builder.addEditOperation(new Range(lineNumber, endColumn, lineNumber, endColumn), charBefore); } else { - left = lineContent.substring(0, s.startColumn - 1) + lineContent.substring(s.endColumn - 1, s.endColumn); - middle = lineContent.substring(s.startColumn - 1, s.endColumn - 1); - right = lineContent.substring(s.endColumn); + const rangeAfter = new Range(lineNumber, endColumn, lineNumber, endColumn + 1); + const charAfter = model.getValueInRange(rangeAfter); + builder.addEditOperation(rangeAfter, null); + builder.addEditOperation(new Range(lineNumber, startColumn, lineNumber, startColumn), charAfter); } - - let newLineContent = left + middle + right; - - builder.addEditOperation(new Range(lineNumber, 1, lineNumber, model.getLineMaxColumn(lineNumber)), null); - builder.addEditOperation(new Range(lineNumber, 1, lineNumber, 1), newLineContent); - - this._cutStartIndex = s.startColumn + (this._isMovingLeft ? -1 : 1); - this._cutEndIndex = this._cutStartIndex + s.endColumn - s.startColumn; - this._moved = true; } public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection { - let result = helper.getTrackedSelection(this._selectionId!); - if (this._moved) { - result = result.setStartPosition(result.startLineNumber, this._cutStartIndex); - result = result.setEndPosition(result.startLineNumber, this._cutEndIndex); + if (this._isMovingLeft) { + return new Selection(this._selection.startLineNumber, this._selection.startColumn - 1, this._selection.endLineNumber, this._selection.endColumn - 1); + } else { + return new Selection(this._selection.startLineNumber, this._selection.startColumn + 1, this._selection.endLineNumber, this._selection.endColumn + 1); } - return result; } } diff --git a/src/vs/editor/contrib/codelens/codelensController.ts b/src/vs/editor/contrib/codelens/codelensController.ts index 2b4865134c3..0a1538a6455 100644 --- a/src/vs/editor/contrib/codelens/codelensController.ts +++ b/src/vs/editor/contrib/codelens/codelensController.ts @@ -8,7 +8,7 @@ import { onUnexpectedError, onUnexpectedExternalError } from 'vs/base/common/err import { toDisposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; import { StableEditorScrollState } from 'vs/editor/browser/core/editorState'; import { ICodeEditor, MouseTargetType, IViewZoneChangeAccessor, IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { registerEditorContribution, EditorCommand, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { IModelDecorationsChangeAccessor } from 'vs/editor/common/model'; import { CodeLensProviderRegistry, CodeLens } from 'vs/editor/common/modes'; @@ -20,6 +20,7 @@ import { ICodeLensCache } from 'vs/editor/contrib/codelens/codeLensCache'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import * as dom from 'vs/base/browser/dom'; import { hash } from 'vs/base/common/hash'; +import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; export class CodeLensContribution implements IEditorContribution { @@ -402,6 +403,53 @@ export class CodeLensContribution implements IEditorContribution { } }); } + + public getLenses(): CodeLensWidget[] { + return this._lenses; + } +} + +export class ShowLensesInCurrentLineCommand extends EditorCommand { + public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void | Promise { + const quickInputService = accessor.get(IQuickInputService); + const commandService = accessor.get(ICommandService); + const notificationService = accessor.get(INotificationService); + + const lineNumber = editor.getSelection()?.positionLineNumber; + const codelensController = editor.getContribution(CodeLensContribution.ID) as CodeLensContribution; + + const activeLensesWidgets = codelensController.getLenses().filter(lens => lens.getLineNumber() === lineNumber); + + const commandArguments: Map = new Map(); + + const items: (IQuickPickItem | IQuickPickSeparator)[] = []; + + activeLensesWidgets.forEach(widget => { + widget.getItems().forEach(codelens => { + const command = codelens.symbol.command; + if (!command) { + return; + } + items.push({ id: command.id, label: command.title }); + + commandArguments.set(command.id, command.arguments); + }); + }); + + // We dont want an empty picker + if (!items.length) { + return; + } + + quickInputService.pick(items, { canPickMany: false }).then(item => { + const id = item.id!; + commandService.executeCommand(id, ...(commandArguments.get(id) || [])).catch(err => notificationService.error(err)); + }); + } + } registerEditorContribution(CodeLensContribution.ID, CodeLensContribution); + +const showLensesInCurrentLineCommand = new ShowLensesInCurrentLineCommand({ id: 'codelens.showLensesInCurrentLine', precondition: undefined }); +showLensesInCurrentLineCommand.register(); diff --git a/src/vs/editor/contrib/codelens/codelensWidget.ts b/src/vs/editor/contrib/codelens/codelensWidget.ts index 622e7785aaf..273fcfb7ea1 100644 --- a/src/vs/editor/contrib/codelens/codelensWidget.ts +++ b/src/vs/editor/contrib/codelens/codelensWidget.ts @@ -336,6 +336,10 @@ export class CodeLensWidget { } } } + + getItems(): CodeLensItem[] { + return this._data; + } } registerThemingParticipant((theme, collector) => { diff --git a/src/vs/editor/contrib/gotoSymbol/link/clickLinkGesture.ts b/src/vs/editor/contrib/gotoSymbol/link/clickLinkGesture.ts index bc3e6cad236..19f2de625f8 100644 --- a/src/vs/editor/contrib/gotoSymbol/link/clickLinkGesture.ts +++ b/src/vs/editor/contrib/gotoSymbol/link/clickLinkGesture.ts @@ -108,8 +108,9 @@ export class ClickLinkGesture extends Disposable { private readonly _editor: ICodeEditor; private _opts: ClickLinkOptions; - private lastMouseMoveEvent: ClickLinkMouseEvent | null; - private hasTriggerKeyOnMouseDown: boolean; + private _lastMouseMoveEvent: ClickLinkMouseEvent | null; + private _hasTriggerKeyOnMouseDown: boolean; + private _lineNumberOnMouseDown: number; constructor(editor: ICodeEditor) { super(); @@ -117,8 +118,9 @@ export class ClickLinkGesture extends Disposable { this._editor = editor; this._opts = createOptions(this._editor.getOption(EditorOption.multiCursorModifier)); - this.lastMouseMoveEvent = null; - this.hasTriggerKeyOnMouseDown = false; + this._lastMouseMoveEvent = null; + this._hasTriggerKeyOnMouseDown = false; + this._lineNumberOnMouseDown = 0; this._register(this._editor.onDidChangeConfiguration((e) => { if (e.hasChanged(EditorOption.multiCursorModifier)) { @@ -127,77 +129,80 @@ export class ClickLinkGesture extends Disposable { return; } this._opts = newOpts; - this.lastMouseMoveEvent = null; - this.hasTriggerKeyOnMouseDown = false; + this._lastMouseMoveEvent = null; + this._hasTriggerKeyOnMouseDown = false; + this._lineNumberOnMouseDown = 0; this._onCancel.fire(); } })); - this._register(this._editor.onMouseMove((e: IEditorMouseEvent) => this.onEditorMouseMove(new ClickLinkMouseEvent(e, this._opts)))); - this._register(this._editor.onMouseDown((e: IEditorMouseEvent) => this.onEditorMouseDown(new ClickLinkMouseEvent(e, this._opts)))); - this._register(this._editor.onMouseUp((e: IEditorMouseEvent) => this.onEditorMouseUp(new ClickLinkMouseEvent(e, this._opts)))); - this._register(this._editor.onKeyDown((e: IKeyboardEvent) => this.onEditorKeyDown(new ClickLinkKeyboardEvent(e, this._opts)))); - this._register(this._editor.onKeyUp((e: IKeyboardEvent) => this.onEditorKeyUp(new ClickLinkKeyboardEvent(e, this._opts)))); - this._register(this._editor.onMouseDrag(() => this.resetHandler())); + this._register(this._editor.onMouseMove((e: IEditorMouseEvent) => this._onEditorMouseMove(new ClickLinkMouseEvent(e, this._opts)))); + this._register(this._editor.onMouseDown((e: IEditorMouseEvent) => this._onEditorMouseDown(new ClickLinkMouseEvent(e, this._opts)))); + this._register(this._editor.onMouseUp((e: IEditorMouseEvent) => this._onEditorMouseUp(new ClickLinkMouseEvent(e, this._opts)))); + this._register(this._editor.onKeyDown((e: IKeyboardEvent) => this._onEditorKeyDown(new ClickLinkKeyboardEvent(e, this._opts)))); + this._register(this._editor.onKeyUp((e: IKeyboardEvent) => this._onEditorKeyUp(new ClickLinkKeyboardEvent(e, this._opts)))); + this._register(this._editor.onMouseDrag(() => this._resetHandler())); - this._register(this._editor.onDidChangeCursorSelection((e) => this.onDidChangeCursorSelection(e))); - this._register(this._editor.onDidChangeModel((e) => this.resetHandler())); - this._register(this._editor.onDidChangeModelContent(() => this.resetHandler())); + this._register(this._editor.onDidChangeCursorSelection((e) => this._onDidChangeCursorSelection(e))); + this._register(this._editor.onDidChangeModel((e) => this._resetHandler())); + this._register(this._editor.onDidChangeModelContent(() => this._resetHandler())); this._register(this._editor.onDidScrollChange((e) => { if (e.scrollTopChanged || e.scrollLeftChanged) { - this.resetHandler(); + this._resetHandler(); } })); } - private onDidChangeCursorSelection(e: ICursorSelectionChangedEvent): void { + private _onDidChangeCursorSelection(e: ICursorSelectionChangedEvent): void { if (e.selection && e.selection.startColumn !== e.selection.endColumn) { - this.resetHandler(); // immediately stop this feature if the user starts to select (https://github.com/Microsoft/vscode/issues/7827) + this._resetHandler(); // immediately stop this feature if the user starts to select (https://github.com/Microsoft/vscode/issues/7827) } } - private onEditorMouseMove(mouseEvent: ClickLinkMouseEvent): void { - this.lastMouseMoveEvent = mouseEvent; + private _onEditorMouseMove(mouseEvent: ClickLinkMouseEvent): void { + this._lastMouseMoveEvent = mouseEvent; this._onMouseMoveOrRelevantKeyDown.fire([mouseEvent, null]); } - private onEditorMouseDown(mouseEvent: ClickLinkMouseEvent): void { + private _onEditorMouseDown(mouseEvent: ClickLinkMouseEvent): void { // We need to record if we had the trigger key on mouse down because someone might select something in the editor // holding the mouse down and then while mouse is down start to press Ctrl/Cmd to start a copy operation and then // release the mouse button without wanting to do the navigation. // With this flag we prevent goto definition if the mouse was down before the trigger key was pressed. - this.hasTriggerKeyOnMouseDown = mouseEvent.hasTriggerModifier; + this._hasTriggerKeyOnMouseDown = mouseEvent.hasTriggerModifier; + this._lineNumberOnMouseDown = mouseEvent.target.position ? mouseEvent.target.position.lineNumber : 0; } - private onEditorMouseUp(mouseEvent: ClickLinkMouseEvent): void { - if (this.hasTriggerKeyOnMouseDown) { + private _onEditorMouseUp(mouseEvent: ClickLinkMouseEvent): void { + const currentLineNumber = mouseEvent.target.position ? mouseEvent.target.position.lineNumber : 0; + if (this._hasTriggerKeyOnMouseDown && this._lineNumberOnMouseDown && this._lineNumberOnMouseDown === currentLineNumber) { this._onExecute.fire(mouseEvent); } } - private onEditorKeyDown(e: ClickLinkKeyboardEvent): void { + private _onEditorKeyDown(e: ClickLinkKeyboardEvent): void { if ( - this.lastMouseMoveEvent + this._lastMouseMoveEvent && ( e.keyCodeIsTriggerKey // User just pressed Ctrl/Cmd (normal goto definition) || (e.keyCodeIsSideBySideKey && e.hasTriggerModifier) // User pressed Ctrl/Cmd+Alt (goto definition to the side) ) ) { - this._onMouseMoveOrRelevantKeyDown.fire([this.lastMouseMoveEvent, e]); + this._onMouseMoveOrRelevantKeyDown.fire([this._lastMouseMoveEvent, e]); } else if (e.hasTriggerModifier) { this._onCancel.fire(); // remove decorations if user holds another key with ctrl/cmd to prevent accident goto declaration } } - private onEditorKeyUp(e: ClickLinkKeyboardEvent): void { + private _onEditorKeyUp(e: ClickLinkKeyboardEvent): void { if (e.keyCodeIsTriggerKey) { this._onCancel.fire(); } } - private resetHandler(): void { - this.lastMouseMoveEvent = null; - this.hasTriggerKeyOnMouseDown = false; + private _resetHandler(): void { + this._lastMouseMoveEvent = null; + this._hasTriggerKeyOnMouseDown = false; this._onCancel.fire(); } } diff --git a/src/vs/editor/contrib/quickAccess/gotoLine.ts b/src/vs/editor/contrib/quickAccess/gotoLine.ts new file mode 100644 index 00000000000..941c3f654f9 --- /dev/null +++ b/src/vs/editor/contrib/quickAccess/gotoLine.ts @@ -0,0 +1,206 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { IQuickPick, IQuickPickItem, IKeyMods } from 'vs/platform/quickinput/common/quickInput'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { DisposableStore, toDisposable, IDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { once } from 'vs/base/common/functional'; +import { IEditor, ScrollType, IDiffEditor } from 'vs/editor/common/editorCommon'; +import { ITextModel } from 'vs/editor/common/model'; +import { isDiffEditor } from 'vs/editor/browser/editorBrowser'; +import { IRange } from 'vs/editor/common/core/range'; +import { withNullAsUndefined } from 'vs/base/common/types'; +import { AbstractEditorQuickAccessProvider } from 'vs/editor/contrib/quickAccess/quickAccess'; +import { IPosition } from 'vs/editor/common/core/position'; + +export const GOTO_LINE_PREFIX = ':'; + +interface IGotoLineQuickPickItem extends IQuickPickItem, Partial { } + +export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditorQuickAccessProvider { + + provide(picker: IQuickPick, token: CancellationToken): IDisposable { + const disposables = new DisposableStore(); + + // Disable filtering & sorting, we control the results + picker.matchOnLabel = picker.matchOnDescription = picker.matchOnDetail = picker.sortByLabel = false; + + // Provide based on current active editor + let pickerDisposable = this.doProvide(picker, token); + disposables.add(toDisposable(() => pickerDisposable.dispose())); + + // Re-create whenever the active editor changes + disposables.add(this.onDidActiveTextEditorControlChange(() => { + pickerDisposable.dispose(); + pickerDisposable = this.doProvide(picker, token); + })); + + return disposables; + } + + private doProvide(picker: IQuickPick, token: CancellationToken): IDisposable { + + // With text control + if (this.activeTextEditorControl) { + return this.doProvideWithTextEditor(this.activeTextEditorControl, picker, token); + } + + // Without text control + return this.doProvideWithoutTextEditor(picker); + } + + private doProvideWithoutTextEditor(picker: IQuickPick): IDisposable { + const label = localize('cannotRunGotoLine', "Open a text file first to go to a line."); + picker.items = [{ label }]; + picker.ariaLabel = label; + + return Disposable.None; + } + + private doProvideWithTextEditor(editor: IEditor, picker: IQuickPick, token: CancellationToken): IDisposable { + const disposables = new DisposableStore(); + + // Restore any view state if this picker was closed + // without actually going to a line + const lastKnownEditorViewState = withNullAsUndefined(editor.saveViewState()); + once(token.onCancellationRequested)(() => { + if (lastKnownEditorViewState) { + editor.restoreViewState(lastKnownEditorViewState); + } + }); + + // Goto line once picked + disposables.add(picker.onDidAccept(() => { + const [item] = picker.selectedItems; + if (item) { + if (!this.isValidLineNumber(editor, item.lineNumber)) { + return; + } + + this.gotoLine(editor, this.toRange(item.lineNumber, item.column), picker.keyMods); + + picker.hide(); + } + })); + + // React to picker changes + const updatePickerAndEditor = () => { + const position = this.parsePosition(editor, picker.value.trim().substr(GOTO_LINE_PREFIX.length)); + const label = this.getPickLabel(editor, position.lineNumber, position.column); + + // Picker + picker.items = [{ + lineNumber: position.lineNumber, + column: position.column, + label + }]; + + // ARIA Label + picker.ariaLabel = label; + + // Clear decorations for invalid range + if (!this.isValidLineNumber(editor, position.lineNumber)) { + this.clearDecorations(editor); + return; + } + + // Reveal + const range = this.toRange(position.lineNumber, position.column); + editor.revealRangeInCenter(range, ScrollType.Smooth); + + // Decorate + this.addDecorations(editor, range); + }; + updatePickerAndEditor(); + disposables.add(picker.onDidChangeValue(() => updatePickerAndEditor())); + + // Clean up decorations on dispose + disposables.add(toDisposable(() => this.clearDecorations(editor))); + + return disposables; + } + + private toRange(lineNumber = 1, column = 1): IRange { + return { + startLineNumber: lineNumber, + startColumn: column, + endLineNumber: lineNumber, + endColumn: column + }; + } + + private parsePosition(editor: IEditor, value: string): IPosition { + + // Support line-col formats of `line,col`, `line:col`, `line#col` + const numbers = value.split(/,|:|#/).map(part => parseInt(part, 10)).filter(part => !isNaN(part)); + const endLine = this.lineCount(editor) + 1; + + return { + lineNumber: numbers[0] > 0 ? numbers[0] : endLine + numbers[0], + column: numbers[1] + }; + } + + private getPickLabel(editor: IEditor, lineNumber: number, column: number | undefined): string { + + // Location valid: indicate this as picker label + if (this.isValidLineNumber(editor, lineNumber)) { + if (this.isValidColumn(editor, lineNumber, column)) { + return localize('gotoLineColumnLabel', "Go to line {0} and column {1}.", lineNumber, column); + } + + return localize('gotoLineLabel', "Go to line {0}.", lineNumber); + } + + // Location invalid: show generic label + const position = editor.getPosition() || { lineNumber: 1, column: 1 }; + const lineCount = this.lineCount(editor); + if (lineCount > 1) { + return localize('gotoLineLabelEmptyWithLimit', "Current Line: {0}, Column: {1}. Type a line number between 1 and {2} to navigate to.", position.lineNumber, position.column, lineCount); + } + + return localize('gotoLineLabelEmpty', "Current Line: {0}, Column: {1}. Type a line number to navigate to.", position.lineNumber, position.column); + } + + private isValidLineNumber(editor: IEditor, lineNumber: number | undefined): boolean { + if (!lineNumber || typeof lineNumber !== 'number') { + return false; + } + + return lineNumber > 0 && lineNumber <= this.lineCount(editor); + } + + private isValidColumn(editor: IEditor, lineNumber: number, column: number | undefined): boolean { + if (!column || typeof column !== 'number') { + return false; + } + + const model = this.getModel(editor); + if (!model) { + return false; + } + + const positionCandidate = { lineNumber, column }; + + return model.validatePosition(positionCandidate).equals(positionCandidate); + } + + private lineCount(editor: IEditor): number { + return this.getModel(editor)?.getLineCount() ?? 0; + } + + private getModel(editor: IEditor | IDiffEditor): ITextModel | undefined { + return isDiffEditor(editor) ? + editor.getModel()?.modified : + editor.getModel() as ITextModel; + } + + protected gotoLine(editor: IEditor, range: IRange, keyMods: IKeyMods): void { + editor.setSelection(range); + editor.revealRangeInCenter(range, ScrollType.Smooth); + editor.focus(); + } +} diff --git a/src/vs/editor/contrib/quickAccess/quickAccess.ts b/src/vs/editor/contrib/quickAccess/quickAccess.ts new file mode 100644 index 00000000000..3b315010d9c --- /dev/null +++ b/src/vs/editor/contrib/quickAccess/quickAccess.ts @@ -0,0 +1,104 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IQuickAccessProvider } from 'vs/platform/quickinput/common/quickAccess'; +import { IEditor } from 'vs/editor/common/editorCommon'; +import { IModelDeltaDecoration, OverviewRulerLane } from 'vs/editor/common/model'; +import { IRange } from 'vs/editor/common/core/range'; +import { themeColorFromId } from 'vs/platform/theme/common/themeService'; +import { overviewRulerRangeHighlight } from 'vs/editor/common/view/editorColorRegistry'; +import { IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { Event } from 'vs/base/common/event'; + +interface IEditorLineDecoration { + rangeHighlightId: string; + overviewRulerDecorationId: string; +} + +/** + * A reusable quick access provider for the editor with support for adding decorations. + */ +export abstract class AbstractEditorQuickAccessProvider implements IQuickAccessProvider { + + /** + * Subclasses to provide an event when the active editor control changes. + */ + abstract readonly onDidActiveTextEditorControlChange: Event; + + /** + * Subclasses to provide the current active editor control. + */ + abstract activeTextEditorControl: IEditor | undefined; + + /** + * Subclasses to implement the quick access picker. + */ + abstract provide(picker: IQuickPick, token: CancellationToken): IDisposable; + + + //#region Decorations Utils + + private rangeHighlightDecorationId: IEditorLineDecoration | undefined = undefined; + + protected addDecorations(editor: IEditor, range: IRange): void { + editor.changeDecorations(changeAccessor => { + + // Reset old decorations if any + const deleteDecorations: string[] = []; + if (this.rangeHighlightDecorationId) { + deleteDecorations.push(this.rangeHighlightDecorationId.overviewRulerDecorationId); + deleteDecorations.push(this.rangeHighlightDecorationId.rangeHighlightId); + + this.rangeHighlightDecorationId = undefined; + } + + // Add new decorations for the range + const newDecorations: IModelDeltaDecoration[] = [ + + // highlight the entire line on the range + { + range, + options: { + className: 'rangeHighlight', + isWholeLine: true + } + }, + + // also add overview ruler highlight + { + range, + options: { + overviewRuler: { + color: themeColorFromId(overviewRulerRangeHighlight), + position: OverviewRulerLane.Full + } + } + } + ]; + + const [rangeHighlightId, overviewRulerDecorationId] = changeAccessor.deltaDecorations(deleteDecorations, newDecorations); + + this.rangeHighlightDecorationId = { rangeHighlightId, overviewRulerDecorationId }; + }); + } + + protected clearDecorations(editor: IEditor): void { + const rangeHighlightDecorationId = this.rangeHighlightDecorationId; + if (rangeHighlightDecorationId) { + editor.changeDecorations(changeAccessor => { + changeAccessor.deltaDecorations([ + rangeHighlightDecorationId.overviewRulerDecorationId, + rangeHighlightDecorationId.rangeHighlightId + ], []); + }); + + this.rangeHighlightDecorationId = undefined; + } + } + + //#endregion +} diff --git a/src/vs/editor/editor.main.ts b/src/vs/editor/editor.main.ts index 2566ecac65b..875233ba7a8 100644 --- a/src/vs/editor/editor.main.ts +++ b/src/vs/editor/editor.main.ts @@ -10,6 +10,8 @@ import 'vs/editor/standalone/browser/inspectTokens/inspectTokens'; import 'vs/editor/standalone/browser/quickOpen/gotoLine'; import 'vs/editor/standalone/browser/quickOpen/quickCommand'; import 'vs/editor/standalone/browser/quickOpen/quickOutline'; +import 'vs/editor/standalone/browser/quickAccess/standaloneHelpQuickAccess'; +import 'vs/editor/standalone/browser/quickAccess/standaloneGotoLineQuickAccess'; import 'vs/editor/standalone/browser/referenceSearch/standaloneReferenceSearch'; import 'vs/editor/standalone/browser/toggleHighContrast/toggleHighContrast'; diff --git a/src/vs/editor/standalone/browser/quickAccess/standaloneGotoLineQuickAccess.ts b/src/vs/editor/standalone/browser/quickAccess/standaloneGotoLineQuickAccess.ts new file mode 100644 index 00000000000..d513431044b --- /dev/null +++ b/src/vs/editor/standalone/browser/quickAccess/standaloneGotoLineQuickAccess.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AbstractGotoLineQuickAccessProvider, GOTO_LINE_PREFIX } from 'vs/editor/contrib/quickAccess/gotoLine'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IQuickAccessRegistry, Extensions } from 'vs/platform/quickinput/common/quickAccess'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { withNullAsUndefined } from 'vs/base/common/types'; +import { GoToLineNLS } from 'vs/editor/common/standaloneStrings'; +import { Event } from 'vs/base/common/event'; + +export class StandaloneGotoLineQuickAccessProvider extends AbstractGotoLineQuickAccessProvider { + + readonly onDidActiveTextEditorControlChange = Event.None; + + constructor(@ICodeEditorService private readonly editorService: ICodeEditorService) { + super(); + } + + get activeTextEditorControl() { + return withNullAsUndefined(this.editorService.getFocusedCodeEditor()); + } +} + +Registry.as(Extensions.Quickaccess).registerQuickAccessProvider({ + ctor: StandaloneGotoLineQuickAccessProvider, + prefix: GOTO_LINE_PREFIX, + helpEntries: [{ description: GoToLineNLS.gotoLineActionLabel, needsEditor: true }] +}); diff --git a/src/vs/editor/standalone/browser/quickAccess/standaloneHelpQuickAccess.ts b/src/vs/editor/standalone/browser/quickAccess/standaloneHelpQuickAccess.ts new file mode 100644 index 00000000000..94deb2ba64e --- /dev/null +++ b/src/vs/editor/standalone/browser/quickAccess/standaloneHelpQuickAccess.ts @@ -0,0 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Registry } from 'vs/platform/registry/common/platform'; +import { IQuickAccessRegistry, Extensions } from 'vs/platform/quickinput/common/quickAccess'; +import { QuickHelpNLS } from 'vs/editor/common/standaloneStrings'; +import { HelpQuickAccessProvider } from 'vs/platform/quickinput/browser/helpQuickAccess'; + +Registry.as(Extensions.Quickaccess).defaultProvider = { + ctor: HelpQuickAccessProvider, + prefix: '', + helpEntries: [{ description: QuickHelpNLS.helpQuickAccessActionLabel, needsEditor: true }] +}; diff --git a/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputServiceImpl.ts b/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputServiceImpl.ts index 30d8373545c..20342263dc2 100644 --- a/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputServiceImpl.ts +++ b/src/vs/editor/standalone/browser/quickInput/standaloneQuickInputServiceImpl.ts @@ -10,10 +10,7 @@ import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IQuickInputService, IQuickInputButton, IQuickPickItem, IQuickPick, IInputBox, IQuickNavigateConfiguration, IPickOptions, QuickPickInput, IInputOptions } from 'vs/platform/quickinput/common/quickInput'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; @@ -21,6 +18,7 @@ import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService import { QuickInputController } from 'vs/base/parts/quickinput/browser/quickInput'; import { QuickInputService, IQuickInputControllerHost } from 'vs/platform/quickinput/browser/quickInput'; import { once } from 'vs/base/common/functional'; +import { IQuickAccessController } from 'vs/platform/quickinput/common/quickAccess'; export class EditorScopedQuickInputServiceImpl extends QuickInputService { @@ -28,15 +26,13 @@ export class EditorScopedQuickInputServiceImpl extends QuickInputService { constructor( editor: ICodeEditor, - @IConfigurationService configurationService: IConfigurationService, @IInstantiationService instantiationService: IInstantiationService, - @IKeybindingService keybindingService: IKeybindingService, @IContextKeyService contextKeyService: IContextKeyService, @IThemeService themeService: IThemeService, @IAccessibilityService accessibilityService: IAccessibilityService, @ILayoutService layoutService: ILayoutService ) { - super({ args: Object.create(null) } as IEnvironmentService, configurationService, instantiationService, keybindingService, contextKeyService, themeService, accessibilityService, layoutService); + super(instantiationService, contextKeyService, themeService, accessibilityService, layoutService); // Use the passed in code editor as host for the quick input widget const contribution = QuickInputEditorContribution.get(editor); @@ -81,6 +77,8 @@ export class StandaloneQuickInputServiceImpl implements IQuickInputService { return quickInputService; } + get quickAccess(): IQuickAccessController { return this.activeService.quickAccess; } + get backButton(): IQuickInputButton { return this.activeService.backButton; } get onShow() { return this.activeService.onShow; } diff --git a/src/vs/editor/test/browser/controller/cursor.test.ts b/src/vs/editor/test/browser/controller/cursor.test.ts index b6984401643..34902600555 100644 --- a/src/vs/editor/test/browser/controller/cursor.test.ts +++ b/src/vs/editor/test/browser/controller/cursor.test.ts @@ -397,6 +397,25 @@ suite('Editor Controller - Cursor', () => { assertCursor(thisCursor, new Position(1, LINE1.length + 1)); }); + test('issue #44465: cursor position not correct when move', () => { + thisCursor.setSelections('test', [new Selection(1, 5, 1, 5)]); + // going once up on the first line remembers the offset visual columns + moveUp(thisCursor); + assertCursor(thisCursor, new Position(1, 1)); + moveDown(thisCursor); + assertCursor(thisCursor, new Position(2, 2)); + moveUp(thisCursor); + assertCursor(thisCursor, new Position(1, 5)); + + // going twice up on the first line discards the offset visual columns + moveUp(thisCursor); + assertCursor(thisCursor, new Position(1, 1)); + moveUp(thisCursor); + assertCursor(thisCursor, new Position(1, 1)); + moveDown(thisCursor); + assertCursor(thisCursor, new Position(2, 1)); + }); + // --------- move to beginning of line test('move to beginning of line', () => { @@ -5044,6 +5063,28 @@ suite('autoClosingPairs', () => { mode.dispose(); }); + test('issue #90016: allow accents on mac US intl keyboard to surround selection', () => { + let mode = new AutoClosingMode(); + usingCursor({ + text: [ + 'test' + ], + languageIdentifier: mode.getLanguageIdentifier() + }, (model, cursor) => { + cursor.setSelections('test', [new Selection(1, 1, 1, 5)]); + + // Typing ` + e on the mac US intl kb layout + cursorCommand(cursor, H.CompositionStart, null, 'keyboard'); + cursorCommand(cursor, H.Type, { text: '\'' }, 'keyboard'); + cursorCommand(cursor, H.ReplacePreviousChar, { replaceCharCnt: 1, text: '\'' }, 'keyboard'); + cursorCommand(cursor, H.ReplacePreviousChar, { replaceCharCnt: 1, text: '\'' }, 'keyboard'); + cursorCommand(cursor, H.CompositionEnd, null, 'keyboard'); + + assert.equal(model.getValue(), '\'test\''); + }); + mode.dispose(); + }); + test('issue #53357: Over typing ignores characters after backslash', () => { let mode = new AutoClosingMode(); usingCursor({ diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 72dc61aa5af..ab37df3e9fa 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -373,7 +373,7 @@ export interface IAction2Options extends ICommandAction { /** * One or many menu items. */ - menu?: OneOrN<{ id: MenuId } & Omit>; + menu?: OneOrN<{ id: MenuId } & Omit & { command?: Partial> }>; /** * One keybinding. @@ -396,39 +396,43 @@ export function registerAction2(ctor: { new(): Action2 }): IDisposable { const disposables = new DisposableStore(); const action = new ctor(); + const { f1, menu: menus, keybinding, description, ...command } = action.desc; + // command disposables.add(CommandsRegistry.registerCommand({ - id: action.desc.id, + id: command.id, handler: (accessor, ...args) => action.run(accessor, ...args), - description: action.desc.description, + description: description, })); // menu - if (Array.isArray(action.desc.menu)) { - for (let item of action.desc.menu) { - disposables.add(MenuRegistry.appendMenuItem(item.id, { command: action.desc, ...item })); + if (Array.isArray(menus)) { + for (let item of menus) { + const { command: commandOverrides, ...menu } = item; + disposables.add(MenuRegistry.appendMenuItem(item.id, { command: { ...command, ...commandOverrides }, ...menu })); } - } else if (action.desc.menu) { - disposables.add(MenuRegistry.appendMenuItem(action.desc.menu.id, { command: action.desc, ...action.desc.menu })); + } else if (menus) { + const { command: commandOverrides, ...menu } = menus; + disposables.add(MenuRegistry.appendMenuItem(menu.id, { command: { ...command, ...commandOverrides }, ...menu })); } - if (action.desc.f1) { - disposables.add(MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: action.desc, ...action.desc })); + if (f1) { + disposables.add(MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: command })); } // keybinding - if (Array.isArray(action.desc.keybinding)) { - for (let item of action.desc.keybinding) { + if (Array.isArray(keybinding)) { + for (let item of keybinding) { KeybindingsRegistry.registerKeybindingRule({ ...item, - id: action.desc.id, - when: ContextKeyExpr.and(action.desc.precondition, item.when) + id: command.id, + when: ContextKeyExpr.and(command.precondition, item.when) }); } - } else if (action.desc.keybinding) { + } else if (keybinding) { KeybindingsRegistry.registerKeybindingRule({ - ...action.desc.keybinding, - id: action.desc.id, - when: ContextKeyExpr.and(action.desc.precondition, action.desc.keybinding.when) + ...keybinding, + id: command.id, + when: ContextKeyExpr.and(command.precondition, keybinding.when) }); } diff --git a/src/vs/platform/contextkey/browser/contextKeyService.ts b/src/vs/platform/contextkey/browser/contextKeyService.ts index 58d6e846e52..78f7843ea46 100644 --- a/src/vs/platform/contextkey/browser/contextKeyService.ts +++ b/src/vs/platform/contextkey/browser/contextKeyService.ts @@ -142,6 +142,10 @@ class ConfigAwareContextValuesContainer extends Context { case 'string': value = configValue; break; + default: + if (Array.isArray(configValue)) { + value = JSON.stringify(configValue); + } } this._values.set(key, value); diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index 87d90b14746..ac6ec196a03 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -38,7 +38,7 @@ export interface IGrammar { } export interface IJSONValidation { - fileMatch: string; + fileMatch: string | string[]; url: string; } diff --git a/src/vs/platform/issue/node/issue.ts b/src/vs/platform/issue/node/issue.ts index 75fb71f9817..08a9c5d456f 100644 --- a/src/vs/platform/issue/node/issue.ts +++ b/src/vs/platform/issue/node/issue.ts @@ -49,6 +49,7 @@ export interface IssueReporterExtensionData { version: string; id: string; isTheme: boolean; + isBuiltin: boolean; displayName: string | undefined; repositoryUrl: string | undefined; bugsUrl: string | undefined; diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index 0f084e82add..58a9d5e09ee 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -1153,7 +1153,7 @@ configurationRegistry.registerConfiguration({ [horizontalScrollingKey]: { 'type': 'boolean', 'default': false, - 'description': localize('horizontalScrolling setting', "Controls whether lists and trees support horizontal scrolling in the workbench.") + 'description': localize('horizontalScrolling setting', "Controls whether lists and trees support horizontal scrolling in the workbench. Warning: turning on this setting has a performance implication.") }, 'workbench.tree.horizontalScrolling': { 'type': 'boolean', diff --git a/src/vs/platform/quickinput/browser/helpQuickAccess.ts b/src/vs/platform/quickinput/browser/helpQuickAccess.ts new file mode 100644 index 00000000000..d4a66d3556b --- /dev/null +++ b/src/vs/platform/quickinput/browser/helpQuickAccess.ts @@ -0,0 +1,75 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IQuickPick, IQuickPickItem, IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickAccessProvider, IQuickAccessRegistry, Extensions } from 'vs/platform/quickinput/common/quickAccess'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { localize } from 'vs/nls'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; + +interface IHelpQuickAccessPickItem extends IQuickPickItem { + prefix: string; +} + +export class HelpQuickAccessProvider implements IQuickAccessProvider { + + private readonly registry = Registry.as(Extensions.Quickaccess); + + constructor(@IQuickInputService private readonly quickInputService: IQuickInputService) { } + + provide(picker: IQuickPick, token: CancellationToken): IDisposable { + const disposables = new DisposableStore(); + + // Open a picker with the selected value if picked + disposables.add(picker.onDidAccept(() => { + const [item] = picker.selectedItems; + if (item) { + this.quickInputService.quickAccess.show(item.prefix); + } + })); + + // Fill in all providers separated by editor/global scope + const { editorProviders, globalProviders } = this.getQuickAccessProviders(); + picker.items = editorProviders.length === 0 || globalProviders.length === 0 ? + + // Without groups + [ + ...(editorProviders.length === 0 ? globalProviders : editorProviders) + ] : + + // With groups + [ + { label: localize('globalCommands', "global commands"), type: 'separator' }, + ...globalProviders, + { label: localize('editorCommands', "editor commands"), type: 'separator' }, + ...editorProviders + ]; + + return disposables; + } + + private getQuickAccessProviders(): { editorProviders: IHelpQuickAccessPickItem[], globalProviders: IHelpQuickAccessPickItem[] } { + const globalProviders: IHelpQuickAccessPickItem[] = []; + const editorProviders: IHelpQuickAccessPickItem[] = []; + + for (const provider of this.registry.getQuickAccessProviders().sort((p1, p2) => p1.prefix.localeCompare(p2.prefix))) { + for (const helpEntry of provider.helpEntries) { + const prefix = helpEntry.prefix || provider.prefix; + const label = prefix || '\u2026' /* ... */; + + (helpEntry.needsEditor ? editorProviders : globalProviders).push({ + prefix, + label, + description: helpEntry.description, + ariaLabel: localize('entryAriaLabel', "{0}, picker help", label) + }); + } + } + + return { editorProviders, globalProviders }; + } +} + diff --git a/src/vs/platform/quickinput/browser/quickAccess.ts b/src/vs/platform/quickinput/browser/quickAccess.ts new file mode 100644 index 00000000000..377db89fbd3 --- /dev/null +++ b/src/vs/platform/quickinput/browser/quickAccess.ts @@ -0,0 +1,95 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IQuickInputService, IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { IQuickAccessController, IQuickAccessProvider, IQuickAccessRegistry, Extensions, IQuickAccessProviderDescriptor } from 'vs/platform/quickinput/common/quickAccess'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { once } from 'vs/base/common/functional'; + +export class QuickAccessController extends Disposable implements IQuickAccessController { + + private readonly registry = Registry.as(Extensions.Quickaccess); + private readonly mapProviderToDescriptor = new Map(); + + private lastActivePicker: IQuickPick | undefined = undefined; + + constructor( + @IQuickInputService private readonly quickInputService: IQuickInputService, + @IInstantiationService private readonly instantiationService: IInstantiationService + ) { + super(); + } + + show(value = ''): void { + const disposables = new DisposableStore(); + + // Hide any previous picker if any + this.lastActivePicker?.hide(); + + // Find provider for the value to show + const [provider, descriptor] = this.getOrInstantiateProvider(value); + + // Create a picker for the provider to use with the initial value + // and adjust the filtering to exclude the prefix from filtering + const picker = disposables.add(this.quickInputService.createQuickPick()); + picker.placeholder = descriptor.placeholder; + picker.value = value; + picker.valueSelection = [value.length, value.length]; + picker.contextKey = descriptor.contextKey; + picker.filterValue = (value: string) => value.substring(descriptor.prefix.length); + + // Remember as last active picker and clean up once picker get's disposed + this.lastActivePicker = picker; + disposables.add(toDisposable(() => { + if (picker === this.lastActivePicker) { + this.lastActivePicker = undefined; + } + })); + + // Create a cancellation token source that is valid as long as the + // picker has not been closed without picking an item + const cts = disposables.add(new CancellationTokenSource()); + once(picker.onDidHide)(() => { + if (picker.selectedItems.length === 0) { + cts.cancel(); + } + + // Start to dispose once picker hides + disposables.dispose(); + }); + + // Whenever the value changes, check if the provider has + // changed and if so - re-create the picker from the beginning + disposables.add(picker.onDidChangeValue(value => { + const [providerForValue] = this.getOrInstantiateProvider(value); + if (providerForValue !== provider) { + this.show(value); + } + })); + + // Ask provider to fill the picker as needed + disposables.add(provider.provide(picker, cts.token)); + + // Finally, show the picker. This is important because a provider + // may not call this and then our disposables would leak that rely + // on the onDidHide event. + picker.show(); + } + + private getOrInstantiateProvider(value: string): [IQuickAccessProvider, IQuickAccessProviderDescriptor] { + const providerDescriptor = this.registry.getQuickAccessProvider(value) || this.registry.defaultProvider; + + let provider = this.mapProviderToDescriptor.get(providerDescriptor); + if (!provider) { + provider = this.instantiationService.createInstance(providerDescriptor.ctor); + this.mapProviderToDescriptor.set(providerDescriptor, provider); + } + + return [provider, providerDescriptor]; + } +} diff --git a/src/vs/platform/quickinput/browser/quickInput.ts b/src/vs/platform/quickinput/browser/quickInput.ts index 1005f963acb..1e3a7b5ec0f 100644 --- a/src/vs/platform/quickinput/browser/quickInput.ts +++ b/src/vs/platform/quickinput/browser/quickInput.ts @@ -9,16 +9,15 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; import { inputBackground, inputForeground, inputBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationInfoBorder, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationWarningBorder, inputValidationErrorBackground, inputValidationErrorForeground, inputValidationErrorBorder, badgeBackground, badgeForeground, contrastBorder, buttonForeground, buttonBackground, buttonHoverBackground, progressBarBackground, widgetShadow, listFocusForeground, listFocusBackground, activeContrastBorder, pickerGroupBorder, pickerGroupForeground, quickInputForeground, quickInputBackground, quickInputTitleBackground } from 'vs/platform/theme/common/colorRegistry'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { computeStyles } from 'vs/platform/theme/common/styler'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; -import { QuickInputController, IQuickInputStyles } from 'vs/base/parts/quickinput/browser/quickInput'; +import { QuickInputController, IQuickInputStyles, IQuickInputOptions } from 'vs/base/parts/quickinput/browser/quickInput'; import { WorkbenchList } from 'vs/platform/list/browser/listService'; import { List, IListOptions } from 'vs/base/browser/ui/list/listWidget'; import { IListVirtualDelegate, IListRenderer } from 'vs/base/browser/ui/list/list'; +import { IQuickAccessController } from 'vs/platform/quickinput/common/quickAccess'; +import { QuickAccessController } from 'vs/platform/quickinput/browser/quickAccess'; export interface IQuickInputControllerHost extends ILayoutService { } @@ -40,28 +39,34 @@ export class QuickInputService extends Themable implements IQuickInputService { return this._controller; } + private _quickAccess: IQuickAccessController | undefined; + get quickAccess(): IQuickAccessController { + if (!this._quickAccess) { + this._quickAccess = this._register(this.instantiationService.createInstance(QuickAccessController)); + } + + return this._quickAccess; + } + private readonly contexts = new Map>(); constructor( - @IEnvironmentService private readonly environmentService: IEnvironmentService, - @IConfigurationService private readonly configurationService: IConfigurationService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IKeybindingService private readonly keybindingService: IKeybindingService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IThemeService themeService: IThemeService, @IAccessibilityService private readonly accessibilityService: IAccessibilityService, - @ILayoutService private readonly layoutService: ILayoutService + @ILayoutService protected readonly layoutService: ILayoutService ) { super(themeService); } - protected createController(host: IQuickInputControllerHost = this.layoutService): QuickInputController { - const controller = this._register(new QuickInputController({ + protected createController(host: IQuickInputControllerHost = this.layoutService, options?: Partial): QuickInputController { + const defaultOptions: IQuickInputOptions = { idPrefix: 'quickInput_', // Constant since there is still only one. container: host.container, - ignoreFocusOut: () => this.environmentService.args['sticky-quickopen'] || !this.configurationService.getValue('workbench.quickOpen.closeOnFocusLost'), + ignoreFocusOut: () => false, isScreenReaderOptimized: () => this.accessibilityService.isScreenReaderOptimized(), - backKeybindingLabel: () => this.keybindingService.lookupKeybinding('workbench.action.quickInputBack')?.getLabel() || undefined, + backKeybindingLabel: () => undefined, setContextKey: (id?: string) => this.setContextKey(id), returnFocus: () => host.focus(), createList: ( @@ -72,6 +77,11 @@ export class QuickInputService extends Themable implements IQuickInputService { options: IListOptions, ) => this.instantiationService.createInstance(WorkbenchList, user, container, delegate, renderers, options) as List, styles: this.computeStyles() + }; + + const controller = this._register(new QuickInputController({ + ...defaultOptions, + ...options })); controller.layout(host.dimension, host.offset?.top ?? 0); diff --git a/src/vs/platform/quickinput/common/quickAccess.ts b/src/vs/platform/quickinput/common/quickAccess.ts new file mode 100644 index 00000000000..be8fba3f311 --- /dev/null +++ b/src/vs/platform/quickinput/common/quickAccess.ts @@ -0,0 +1,142 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { first } from 'vs/base/common/arrays'; +import { startsWith } from 'vs/base/common/strings'; +import { assertIsDefined } from 'vs/base/common/types'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; + +export interface IQuickAccessController { + + /** + * Open the quick access picker with the optional value prefilled. + */ + show(value?: string): void; +} + +export interface IQuickAccessProvider { + + /** + * Called whenever a prefix was typed into quick pick that matches the provider. + * + * @param picker the picker to use for showing provider results. The picker is + * automatically shown after the method returns, no need to call `show()`. + * @param token providers have to check the cancellation token everytime after + * a long running operation or from event handlers because it could be that the + * picker has been closed or changed meanwhile. The token can be used to find out + * that the picker was closed without picking an entry (e.g. was canceled by the user). + * @return a disposable that will automatically be disposed when the picker + * closes or is replaced by another picker. + */ + provide(picker: IQuickPick, token: CancellationToken): IDisposable; +} + +export interface IQuickAccessProviderHelp { + + /** + * The prefix to show for the help entry. If not provided, + * the prefix used for registration will be taken. + */ + prefix?: string; + + /** + * A description text to help understand the intent of the provider. + */ + description: string; + + /** + * Separation between provider for editors and global ones. + */ + needsEditor: boolean; +} + +export interface IQuickAccessProviderDescriptor { + + /** + * The actual provider that will be instantiated as needed. + */ + readonly ctor: { new(...services: any /* TS BrandedService but no clue how to type this properly */[]): IQuickAccessProvider }; + + /** + * The prefix for quick access picker to use the provider for. + */ + readonly prefix: string; + + /** + * A placeholder to use for the input field when the provider is active. + * This will also be read out by screen readers and thus helps for + * accessibility. + */ + readonly placeholder?: string; + + /** + * Documentation for the provider in the quick access help. + */ + readonly helpEntries: IQuickAccessProviderHelp[]; + + /** + * A context key that will be set automatically when the + * picker for the provider is showing. + */ + readonly contextKey?: string; +} + +export const Extensions = { + Quickaccess: 'workbench.contributions.quickaccess' +}; + +export interface IQuickAccessRegistry { + + /** + * The default provider to use when no other provider matches. + */ + defaultProvider: IQuickAccessProviderDescriptor; + + /** + * Registers a quick access provider to the platform. + */ + registerQuickAccessProvider(provider: IQuickAccessProviderDescriptor): IDisposable; + + /** + * Get all registered quick access providers. + */ + getQuickAccessProviders(): IQuickAccessProviderDescriptor[]; + + /** + * Get a specific quick access provider for a given prefix. + */ + getQuickAccessProvider(prefix: string): IQuickAccessProviderDescriptor | undefined; +} + +class QuickAccessRegistry implements IQuickAccessRegistry { + private providers: IQuickAccessProviderDescriptor[] = []; + + private _defaultProvider: IQuickAccessProviderDescriptor | undefined = undefined; + get defaultProvider(): IQuickAccessProviderDescriptor { return assertIsDefined(this._defaultProvider); } + set defaultProvider(provider: IQuickAccessProviderDescriptor) { this._defaultProvider = provider; } + + registerQuickAccessProvider(provider: IQuickAccessProviderDescriptor): IDisposable { + this.providers.push(provider); + + // sort the providers by decreasing prefix length, such that longer + // prefixes take priority: 'ext' vs 'ext install' - the latter should win + this.providers.sort((providerA, providerB) => providerB.prefix.length - providerA.prefix.length); + + return toDisposable(() => this.providers.splice(this.providers.indexOf(provider), 1)); + } + + getQuickAccessProviders(): IQuickAccessProviderDescriptor[] { + return [this.defaultProvider, ...this.providers]; + } + + getQuickAccessProvider(prefix: string): IQuickAccessProviderDescriptor | undefined { + return prefix ? (first(this.providers, provider => startsWith(prefix, provider.prefix)) || undefined) : undefined; + } +} + +Registry.add(Extensions.Quickaccess, new QuickAccessRegistry()); diff --git a/src/vs/platform/quickinput/common/quickInput.ts b/src/vs/platform/quickinput/common/quickInput.ts index a21044f67e5..fdbded969de 100644 --- a/src/vs/platform/quickinput/common/quickInput.ts +++ b/src/vs/platform/quickinput/common/quickInput.ts @@ -7,8 +7,9 @@ import { Event } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IQuickPickItem, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickPick, IQuickInputButton, IInputBox, QuickPickInput } from 'vs/base/parts/quickinput/common/quickInput'; +import { IQuickAccessController } from 'vs/platform/quickinput/common/quickAccess'; -export { IQuickPickItem, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickPick, IQuickInput, IQuickInputButton, IInputBox, IQuickPickItemButtonEvent, QuickPickInput, IQuickPickSeparator, IKeyMods } from 'vs/base/parts/quickinput/common/quickInput'; +export * from 'vs/base/parts/quickinput/common/quickInput'; export const IQuickInputService = createDecorator('quickInputService'); @@ -18,20 +19,29 @@ export interface IQuickInputService { _serviceBrand: undefined; + /** + * Provides access to the back button in quick input. + */ readonly backButton: IQuickInputButton; /** - * Allows to register on the event that quick input is showing + * Provides access to the quick access providers. + */ + readonly quickAccess: IQuickAccessController; + + /** + * Allows to register on the event that quick input is showing. */ readonly onShow: Event; /** - * Allows to register on the event that quick input is hiding + * Allows to register on the event that quick input is hiding. */ readonly onHide: Event; /** - * Opens the quick input box for selecting items and returns a promise with the user selected item(s) if any. + * Opens the quick input box for selecting items and returns a promise + * with the user selected item(s) if any. */ pick(picks: Promise[]> | QuickPickInput[], options?: IPickOptions & { canPickMany: true }, token?: CancellationToken): Promise; pick(picks: Promise[]> | QuickPickInput[], options?: IPickOptions & { canPickMany: false }, token?: CancellationToken): Promise; @@ -42,20 +52,46 @@ export interface IQuickInputService { */ input(options?: IInputOptions, token?: CancellationToken): Promise; + /** + * Provides raw access to the quick pick controller. + */ createQuickPick(): IQuickPick; + + /** + * Provides raw access to the quick input controller. + */ createInputBox(): IInputBox; + /** + * Moves focus into quick input. + */ focus(): void; + /** + * Toggle the checked state of the selected item. + */ toggle(): void; + /** + * Navigate inside the opened quick input list. + */ navigate(next: boolean, quickNavigate?: IQuickNavigateConfiguration): void; - accept(): Promise; - + /** + * Navigate back in a multi-step quick input. + */ back(): Promise; + /** + * Accept the selected item. + */ + accept(): Promise; + + /** + * Cancels quick input and closes it. + */ cancel(): Promise; + // TODO@Ben remove once quick open is gone hide(focusLost?: boolean): void; } diff --git a/src/vs/platform/theme/common/iconRegistry.ts b/src/vs/platform/theme/common/iconRegistry.ts new file mode 100644 index 00000000000..42b1d589352 --- /dev/null +++ b/src/vs/platform/theme/common/iconRegistry.ts @@ -0,0 +1,543 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as platform from 'vs/platform/registry/common/platform'; +import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema'; +import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { Event, Emitter } from 'vs/base/common/event'; +import { localize } from 'vs/nls'; + +// ------ API types + + +// color registry +export const Extensions = { + IconContribution: 'base.contributions.icons' +}; + +export interface IconDefaults { + font?: string; + character: string; +} + +export interface IconContribution { + id: string; + description: string; + deprecationMessage?: string; + defaults: IconDefaults; +} + +export interface IIconRegistry { + + readonly onDidChangeSchema: Event; + + /** + * Register a icon to the registry. + * @param id The icon id + * @param defaults The default values + * @description the description + */ + registerIcon(id: string, defaults: IconDefaults, description: string): ThemeIcon; + + /** + * Register a icon to the registry. + */ + deregisterIcon(id: string): void; + + /** + * Get all icon contributions + */ + getIcons(): IconContribution[]; + + /** + * JSON schema for an object to assign icon values to one of the color contributions. + */ + getIconSchema(): IJSONSchema; + + /** + * JSON schema to for a reference to a icon contribution. + */ + getIconReferenceSchema(): IJSONSchema; + +} + +class IconRegistry implements IIconRegistry { + + private readonly _onDidChangeSchema = new Emitter(); + readonly onDidChangeSchema: Event = this._onDidChangeSchema.event; + + private iconsById: { [key: string]: IconContribution }; + private iconSchema: IJSONSchema & { properties: IJSONSchemaMap } = { type: 'object', properties: {} }; + private iconReferenceSchema: IJSONSchema & { enum: string[], enumDescriptions: string[] } = { type: 'string', enum: [], enumDescriptions: [] }; + + constructor() { + this.iconsById = {}; + } + + public registerIcon(id: string, defaults: IconDefaults, description: string, deprecationMessage?: string): ThemeIcon { + let iconContribution: IconContribution = { id, description, defaults, deprecationMessage }; + this.iconsById[id] = iconContribution; + let propertySchema: IJSONSchema = { type: 'object', description, properties: { font: { type: 'string' }, fontCharacter: { type: 'string' } }, defaultSnippets: [{ body: { fontCharacter: '\\\\e030' } }] }; + if (deprecationMessage) { + propertySchema.deprecationMessage = deprecationMessage; + } + this.iconSchema.properties[id] = propertySchema; + this.iconReferenceSchema.enum.push(id); + this.iconReferenceSchema.enumDescriptions.push(description); + + this._onDidChangeSchema.fire(); + return { id }; + } + + + public deregisterIcon(id: string): void { + delete this.iconsById[id]; + delete this.iconSchema.properties[id]; + const index = this.iconReferenceSchema.enum.indexOf(id); + if (index !== -1) { + this.iconReferenceSchema.enum.splice(index, 1); + this.iconReferenceSchema.enumDescriptions.splice(index, 1); + } + this._onDidChangeSchema.fire(); + } + + public getIcons(): IconContribution[] { + return Object.keys(this.iconsById).map(id => this.iconsById[id]); + } + + public getIconSchema(): IJSONSchema { + return this.iconSchema; + } + + public getIconReferenceSchema(): IJSONSchema { + return this.iconReferenceSchema; + } + + public toString() { + let sorter = (a: string, b: string) => { + let cat1 = a.indexOf('.') === -1 ? 0 : 1; + let cat2 = b.indexOf('.') === -1 ? 0 : 1; + if (cat1 !== cat2) { + return cat1 - cat2; + } + return a.localeCompare(b); + }; + + return Object.keys(this.iconsById).sort(sorter).map(k => `- \`${k}\`: ${this.iconsById[k].description}`).join('\n'); + } + +} + +const iconRegistry = new IconRegistry(); +platform.Registry.add(Extensions.IconContribution, iconRegistry); + +export function registerIcon(id: string, defaults: IconDefaults, description: string, deprecationMessage?: string): ThemeIcon { + return iconRegistry.registerIcon(id, defaults, description, deprecationMessage); +} + +export function getIconRegistry(): IIconRegistry { + return iconRegistry; +} + +registerIcon('add', { character: '\ea60' }, localize('add', '')); +registerIcon('plus', { character: '\ea60' }, localize('plus', '')); +registerIcon('gist-new', { character: '\ea60' }, localize('gist-new', '')); +registerIcon('repo-create', { character: '\ea60' }, localize('repo-create', '')); +registerIcon('lightbulb', { character: '\ea61' }, localize('lightbulb', '')); +registerIcon('light-bulb', { character: '\ea61' }, localize('light-bulb', '')); +registerIcon('repo', { character: '\ea62' }, localize('repo', '')); +registerIcon('repo-delete', { character: '\ea62' }, localize('repo-delete', '')); +registerIcon('gist-fork', { character: '\ea63' }, localize('gist-fork', '')); +registerIcon('repo-forked', { character: '\ea63' }, localize('repo-forked', '')); +registerIcon('git-pull-request', { character: '\ea64' }, localize('git-pull-request', '')); +registerIcon('git-pull-request-abandoned', { character: '\ea64' }, localize('git-pull-request-abandoned', '')); +registerIcon('record-keys', { character: '\ea65' }, localize('record-keys', '')); +registerIcon('keyboard', { character: '\ea65' }, localize('keyboard', '')); +registerIcon('tag', { character: '\ea66' }, localize('tag', '')); +registerIcon('tag-add', { character: '\ea66' }, localize('tag-add', '')); +registerIcon('tag-remove', { character: '\ea66' }, localize('tag-remove', '')); +registerIcon('person', { character: '\ea67' }, localize('person', '')); +registerIcon('person-add', { character: '\ea67' }, localize('person-add', '')); +registerIcon('person-follow', { character: '\ea67' }, localize('person-follow', '')); +registerIcon('person-outline', { character: '\ea67' }, localize('person-outline', '')); +registerIcon('person-filled', { character: '\ea67' }, localize('person-filled', '')); +registerIcon('git-branch', { character: '\ea68' }, localize('git-branch', '')); +registerIcon('git-branch-create', { character: '\ea68' }, localize('git-branch-create', '')); +registerIcon('git-branch-delete', { character: '\ea68' }, localize('git-branch-delete', '')); +registerIcon('source-control', { character: '\ea68' }, localize('source-control', '')); +registerIcon('mirror', { character: '\ea69' }, localize('mirror', '')); +registerIcon('mirror-public', { character: '\ea69' }, localize('mirror-public', '')); +registerIcon('star', { character: '\ea6a' }, localize('star', '')); +registerIcon('star-add', { character: '\ea6a' }, localize('star-add', '')); +registerIcon('star-delete', { character: '\ea6a' }, localize('star-delete', '')); +registerIcon('star-empty', { character: '\ea6a' }, localize('star-empty', '')); +registerIcon('comment', { character: '\ea6b' }, localize('comment', '')); +registerIcon('comment-add', { character: '\ea6b' }, localize('comment-add', '')); +registerIcon('alert', { character: '\ea6c' }, localize('alert', '')); +registerIcon('warning', { character: '\ea6c' }, localize('warning', '')); +registerIcon('search', { character: '\ea6d' }, localize('search', '')); +registerIcon('search-save', { character: '\ea6d' }, localize('search-save', '')); +registerIcon('log-out', { character: '\ea6e' }, localize('log-out', '')); +registerIcon('sign-out', { character: '\ea6e' }, localize('sign-out', '')); +registerIcon('log-in', { character: '\ea6f' }, localize('log-in', '')); +registerIcon('sign-in', { character: '\ea6f' }, localize('sign-in', '')); +registerIcon('eye', { character: '\ea70' }, localize('eye', '')); +registerIcon('eye-unwatch', { character: '\ea70' }, localize('eye-unwatch', '')); +registerIcon('eye-watch', { character: '\ea70' }, localize('eye-watch', '')); +registerIcon('circle-filled', { character: '\ea71' }, localize('circle-filled', '')); +registerIcon('primitive-dot', { character: '\ea71' }, localize('primitive-dot', '')); +registerIcon('close-dirty', { character: '\ea71' }, localize('close-dirty', '')); +registerIcon('debug-breakpoint', { character: '\ea71' }, localize('debug-breakpoint', '')); +registerIcon('debug-breakpoint-disabled', { character: '\ea71' }, localize('debug-breakpoint-disabled', '')); +registerIcon('debug-hint', { character: '\ea71' }, localize('debug-hint', '')); +registerIcon('primitive-square', { character: '\ea72' }, localize('primitive-square', '')); +registerIcon('edit', { character: '\ea73' }, localize('edit', '')); +registerIcon('pencil', { character: '\ea73' }, localize('pencil', '')); +registerIcon('info', { character: '\ea74' }, localize('info', '')); +registerIcon('issue-opened', { character: '\ea74' }, localize('issue-opened', '')); +registerIcon('gist-private', { character: '\ea75' }, localize('gist-private', '')); +registerIcon('git-fork-private', { character: '\ea75' }, localize('git-fork-private', '')); +registerIcon('lock', { character: '\ea75' }, localize('lock', '')); +registerIcon('mirror-private', { character: '\ea75' }, localize('mirror-private', '')); +registerIcon('close', { character: '\ea76' }, localize('close', '')); +registerIcon('remove-close', { character: '\ea76' }, localize('remove-close', '')); +registerIcon('x', { character: '\ea76' }, localize('x', '')); +registerIcon('repo-sync', { character: '\ea77' }, localize('repo-sync', '')); +registerIcon('sync', { character: '\ea77' }, localize('sync', '')); +registerIcon('clone', { character: '\ea78' }, localize('clone', '')); +registerIcon('desktop-download', { character: '\ea78' }, localize('desktop-download', '')); +registerIcon('beaker', { character: '\ea79' }, localize('beaker', '')); +registerIcon('microscope', { character: '\ea79' }, localize('microscope', '')); +registerIcon('vm', { character: '\ea7a' }, localize('vm', '')); +registerIcon('device-desktop', { character: '\ea7a' }, localize('device-desktop', '')); +registerIcon('file', { character: '\ea7b' }, localize('file', '')); +registerIcon('file-text', { character: '\ea7b' }, localize('file-text', '')); +registerIcon('more', { character: '\ea7c' }, localize('more', '')); +registerIcon('ellipsis', { character: '\ea7c' }, localize('ellipsis', '')); +registerIcon('kebab-horizontal', { character: '\ea7c' }, localize('kebab-horizontal', '')); +registerIcon('mail-reply', { character: '\ea7d' }, localize('mail-reply', '')); +registerIcon('reply', { character: '\ea7d' }, localize('reply', '')); +registerIcon('organization', { character: '\ea7e' }, localize('organization', '')); +registerIcon('organization-filled', { character: '\ea7e' }, localize('organization-filled', '')); +registerIcon('organization-outline', { character: '\ea7e' }, localize('organization-outline', '')); +registerIcon('new-file', { character: '\ea7f' }, localize('new-file', '')); +registerIcon('file-add', { character: '\ea7f' }, localize('file-add', '')); +registerIcon('new-folder', { character: '\ea80' }, localize('new-folder', '')); +registerIcon('file-directory-create', { character: '\ea80' }, localize('file-directory-create', '')); +registerIcon('trash', { character: '\ea81' }, localize('trash', '')); +registerIcon('trashcan', { character: '\ea81' }, localize('trashcan', '')); +registerIcon('history', { character: '\ea82' }, localize('history', '')); +registerIcon('clock', { character: '\ea82' }, localize('clock', '')); +registerIcon('folder', { character: '\ea83' }, localize('folder', '')); +registerIcon('file-directory', { character: '\ea83' }, localize('file-directory', '')); +registerIcon('symbol-folder', { character: '\ea83' }, localize('symbol-folder', '')); +registerIcon('logo-github', { character: '\ea84' }, localize('logo-github', '')); +registerIcon('mark-github', { character: '\ea84' }, localize('mark-github', '')); +registerIcon('github', { character: '\ea84' }, localize('github', '')); +registerIcon('terminal', { character: '\ea85' }, localize('terminal', '')); +registerIcon('console', { character: '\ea85' }, localize('console', '')); +registerIcon('repl', { character: '\ea85' }, localize('repl', '')); +registerIcon('zap', { character: '\ea86' }, localize('zap', '')); +registerIcon('symbol-event', { character: '\ea86' }, localize('symbol-event', '')); +registerIcon('error', { character: '\ea87' }, localize('error', '')); +registerIcon('stop', { character: '\ea87' }, localize('stop', '')); +registerIcon('variable', { character: '\ea88' }, localize('variable', '')); +registerIcon('symbol-variable', { character: '\ea88' }, localize('symbol-variable', '')); +registerIcon('array', { character: '\ea8a' }, localize('array', '')); +registerIcon('symbol-array', { character: '\ea8a' }, localize('symbol-array', '')); +registerIcon('symbol-module', { character: '\ea8b' }, localize('symbol-module', '')); +registerIcon('symbol-package', { character: '\ea8b' }, localize('symbol-package', '')); +registerIcon('symbol-namespace', { character: '\ea8b' }, localize('symbol-namespace', '')); +registerIcon('symbol-object', { character: '\ea8b' }, localize('symbol-object', '')); +registerIcon('symbol-method', { character: '\ea8c' }, localize('symbol-method', '')); +registerIcon('symbol-function', { character: '\ea8c' }, localize('symbol-function', '')); +registerIcon('symbol-constructor', { character: '\ea8c' }, localize('symbol-constructor', '')); +registerIcon('symbol-boolean', { character: '\ea8f' }, localize('symbol-boolean', '')); +registerIcon('symbol-null', { character: '\ea8f' }, localize('symbol-null', '')); +registerIcon('symbol-numeric', { character: '\ea90' }, localize('symbol-numeric', '')); +registerIcon('symbol-number', { character: '\ea90' }, localize('symbol-number', '')); +registerIcon('symbol-structure', { character: '\ea91' }, localize('symbol-structure', '')); +registerIcon('symbol-struct', { character: '\ea91' }, localize('symbol-struct', '')); +registerIcon('symbol-parameter', { character: '\ea92' }, localize('symbol-parameter', '')); +registerIcon('symbol-type-parameter', { character: '\ea92' }, localize('symbol-type-parameter', '')); +registerIcon('symbol-key', { character: '\ea93' }, localize('symbol-key', '')); +registerIcon('symbol-text', { character: '\ea93' }, localize('symbol-text', '')); +registerIcon('symbol-reference', { character: '\ea94' }, localize('symbol-reference', '')); +registerIcon('go-to-file', { character: '\ea94' }, localize('go-to-file', '')); +registerIcon('symbol-enum', { character: '\ea95' }, localize('symbol-enum', '')); +registerIcon('symbol-value', { character: '\ea95' }, localize('symbol-value', '')); +registerIcon('symbol-ruler', { character: '\ea96' }, localize('symbol-ruler', '')); +registerIcon('symbol-unit', { character: '\ea96' }, localize('symbol-unit', '')); +registerIcon('activate-breakpoints', { character: '\ea97' }, localize('activate-breakpoints', '')); +registerIcon('archive', { character: '\ea98' }, localize('archive', '')); +registerIcon('arrow-both', { character: '\ea99' }, localize('arrow-both', '')); +registerIcon('arrow-down', { character: '\ea9a' }, localize('arrow-down', '')); +registerIcon('arrow-left', { character: '\ea9b' }, localize('arrow-left', '')); +registerIcon('arrow-right', { character: '\ea9c' }, localize('arrow-right', '')); +registerIcon('arrow-small-down', { character: '\ea9d' }, localize('arrow-small-down', '')); +registerIcon('arrow-small-left', { character: '\ea9e' }, localize('arrow-small-left', '')); +registerIcon('arrow-small-right', { character: '\ea9f' }, localize('arrow-small-right', '')); +registerIcon('arrow-small-up', { character: '\eaa0' }, localize('arrow-small-up', '')); +registerIcon('arrow-up', { character: '\eaa1' }, localize('arrow-up', '')); +registerIcon('bell', { character: '\eaa2' }, localize('bell', '')); +registerIcon('bold', { character: '\eaa3' }, localize('bold', '')); +registerIcon('book', { character: '\eaa4' }, localize('book', '')); +registerIcon('bookmark', { character: '\eaa5' }, localize('bookmark', '')); +registerIcon('debug-breakpoint-conditional-unverified', { character: '\eaa6' }, localize('debug-breakpoint-conditional-unverified', '')); +registerIcon('debug-breakpoint-conditional', { character: '\eaa7' }, localize('debug-breakpoint-conditional', '')); +registerIcon('debug-breakpoint-conditional-disabled', { character: '\eaa7' }, localize('debug-breakpoint-conditional-disabled', '')); +registerIcon('debug-breakpoint-data-unverified', { character: '\eaa8' }, localize('debug-breakpoint-data-unverified', '')); +registerIcon('debug-breakpoint-data', { character: '\eaa9' }, localize('debug-breakpoint-data', '')); +registerIcon('debug-breakpoint-data-disabled', { character: '\eaa9' }, localize('debug-breakpoint-data-disabled', '')); +registerIcon('debug-breakpoint-log-unverified', { character: '\eaaa' }, localize('debug-breakpoint-log-unverified', '')); +registerIcon('debug-breakpoint-log', { character: '\eaab' }, localize('debug-breakpoint-log', '')); +registerIcon('debug-breakpoint-log-disabled', { character: '\eaab' }, localize('debug-breakpoint-log-disabled', '')); +registerIcon('briefcase', { character: '\eaac' }, localize('briefcase', '')); +registerIcon('broadcast', { character: '\eaad' }, localize('broadcast', '')); +registerIcon('browser', { character: '\eaae' }, localize('browser', '')); +registerIcon('bug', { character: '\eaaf' }, localize('bug', '')); +registerIcon('calendar', { character: '\eab0' }, localize('calendar', '')); +registerIcon('case-sensitive', { character: '\eab1' }, localize('case-sensitive', '')); +registerIcon('check', { character: '\eab2' }, localize('check', '')); +registerIcon('checklist', { character: '\eab3' }, localize('checklist', '')); +registerIcon('chevron-down', { character: '\eab4' }, localize('chevron-down', '')); +registerIcon('chevron-left', { character: '\eab5' }, localize('chevron-left', '')); +registerIcon('chevron-right', { character: '\eab6' }, localize('chevron-right', '')); +registerIcon('chevron-up', { character: '\eab7' }, localize('chevron-up', '')); +registerIcon('chrome-close', { character: '\eab8' }, localize('chrome-close', '')); +registerIcon('chrome-maximize', { character: '\eab9' }, localize('chrome-maximize', '')); +registerIcon('chrome-minimize', { character: '\eaba' }, localize('chrome-minimize', '')); +registerIcon('chrome-restore', { character: '\eabb' }, localize('chrome-restore', '')); +registerIcon('circle-outline', { character: '\eabc' }, localize('circle-outline', '')); +registerIcon('debug-breakpoint-unverified', { character: '\eabc' }, localize('debug-breakpoint-unverified', '')); +registerIcon('circle-slash', { character: '\eabd' }, localize('circle-slash', '')); +registerIcon('circuit-board', { character: '\eabe' }, localize('circuit-board', '')); +registerIcon('clear-all', { character: '\eabf' }, localize('clear-all', '')); +registerIcon('clippy', { character: '\eac0' }, localize('clippy', '')); +registerIcon('close-all', { character: '\eac1' }, localize('close-all', '')); +registerIcon('cloud-download', { character: '\eac2' }, localize('cloud-download', '')); +registerIcon('cloud-upload', { character: '\eac3' }, localize('cloud-upload', '')); +registerIcon('code', { character: '\eac4' }, localize('code', '')); +registerIcon('collapse-all', { character: '\eac5' }, localize('collapse-all', '')); +registerIcon('color-mode', { character: '\eac6' }, localize('color-mode', '')); +registerIcon('comment-discussion', { character: '\eac7' }, localize('comment-discussion', '')); +registerIcon('compare-changes', { character: '\eac8' }, localize('compare-changes', '')); +registerIcon('credit-card', { character: '\eac9' }, localize('credit-card', '')); +registerIcon('dash', { character: '\eacc' }, localize('dash', '')); +registerIcon('dashboard', { character: '\eacd' }, localize('dashboard', '')); +registerIcon('database', { character: '\eace' }, localize('database', '')); +registerIcon('debug-continue', { character: '\eacf' }, localize('debug-continue', '')); +registerIcon('debug-disconnect', { character: '\ead0' }, localize('debug-disconnect', '')); +registerIcon('debug-pause', { character: '\ead1' }, localize('debug-pause', '')); +registerIcon('debug-restart', { character: '\ead2' }, localize('debug-restart', '')); +registerIcon('debug-start', { character: '\ead3' }, localize('debug-start', '')); +registerIcon('debug-step-into', { character: '\ead4' }, localize('debug-step-into', '')); +registerIcon('debug-step-out', { character: '\ead5' }, localize('debug-step-out', '')); +registerIcon('debug-step-over', { character: '\ead6' }, localize('debug-step-over', '')); +registerIcon('debug-stop', { character: '\ead7' }, localize('debug-stop', '')); +registerIcon('debug', { character: '\ead8' }, localize('debug', '')); +registerIcon('device-camera-video', { character: '\ead9' }, localize('device-camera-video', '')); +registerIcon('device-camera', { character: '\eada' }, localize('device-camera', '')); +registerIcon('device-mobile', { character: '\eadb' }, localize('device-mobile', '')); +registerIcon('diff-added', { character: '\eadc' }, localize('diff-added', '')); +registerIcon('diff-ignored', { character: '\eadd' }, localize('diff-ignored', '')); +registerIcon('diff-modified', { character: '\eade' }, localize('diff-modified', '')); +registerIcon('diff-removed', { character: '\eadf' }, localize('diff-removed', '')); +registerIcon('diff-renamed', { character: '\eae0' }, localize('diff-renamed', '')); +registerIcon('diff', { character: '\eae1' }, localize('diff', '')); +registerIcon('discard', { character: '\eae2' }, localize('discard', '')); +registerIcon('editor-layout', { character: '\eae3' }, localize('editor-layout', '')); +registerIcon('empty-window', { character: '\eae4' }, localize('empty-window', '')); +registerIcon('exclude', { character: '\eae5' }, localize('exclude', '')); +registerIcon('extensions', { character: '\eae6' }, localize('extensions', '')); +registerIcon('eye-closed', { character: '\eae7' }, localize('eye-closed', '')); +registerIcon('file-binary', { character: '\eae8' }, localize('file-binary', '')); +registerIcon('file-code', { character: '\eae9' }, localize('file-code', '')); +registerIcon('file-media', { character: '\eaea' }, localize('file-media', '')); +registerIcon('file-pdf', { character: '\eaeb' }, localize('file-pdf', '')); +registerIcon('file-submodule', { character: '\eaec' }, localize('file-submodule', '')); +registerIcon('file-symlink-directory', { character: '\eaed' }, localize('file-symlink-directory', '')); +registerIcon('file-symlink-file', { character: '\eaee' }, localize('file-symlink-file', '')); +registerIcon('file-zip', { character: '\eaef' }, localize('file-zip', '')); +registerIcon('files', { character: '\eaf0' }, localize('files', '')); +registerIcon('filter', { character: '\eaf1' }, localize('filter', '')); +registerIcon('flame', { character: '\eaf2' }, localize('flame', '')); +registerIcon('fold-down', { character: '\eaf3' }, localize('fold-down', '')); +registerIcon('fold-up', { character: '\eaf4' }, localize('fold-up', '')); +registerIcon('fold', { character: '\eaf5' }, localize('fold', '')); +registerIcon('folder-active', { character: '\eaf6' }, localize('folder-active', '')); +registerIcon('folder-opened', { character: '\eaf7' }, localize('folder-opened', '')); +registerIcon('gear', { character: '\eaf8' }, localize('gear', '')); +registerIcon('gift', { character: '\eaf9' }, localize('gift', '')); +registerIcon('gist-secret', { character: '\eafa' }, localize('gist-secret', '')); +registerIcon('gist', { character: '\eafb' }, localize('gist', '')); +registerIcon('git-commit', { character: '\eafc' }, localize('git-commit', '')); +registerIcon('git-compare', { character: '\eafd' }, localize('git-compare', '')); +registerIcon('git-merge', { character: '\eafe' }, localize('git-merge', '')); +registerIcon('github-action', { character: '\eaff' }, localize('github-action', '')); +registerIcon('github-alt', { character: '\eb00' }, localize('github-alt', '')); +registerIcon('globe', { character: '\eb01' }, localize('globe', '')); +registerIcon('grabber', { character: '\eb02' }, localize('grabber', '')); +registerIcon('graph', { character: '\eb03' }, localize('graph', '')); +registerIcon('gripper', { character: '\eb04' }, localize('gripper', '')); +registerIcon('heart', { character: '\eb05' }, localize('heart', '')); +registerIcon('home', { character: '\eb06' }, localize('home', '')); +registerIcon('horizontal-rule', { character: '\eb07' }, localize('horizontal-rule', '')); +registerIcon('hubot', { character: '\eb08' }, localize('hubot', '')); +registerIcon('inbox', { character: '\eb09' }, localize('inbox', '')); +registerIcon('issue-closed', { character: '\eb0a' }, localize('issue-closed', '')); +registerIcon('issue-reopened', { character: '\eb0b' }, localize('issue-reopened', '')); +registerIcon('issues', { character: '\eb0c' }, localize('issues', '')); +registerIcon('italic', { character: '\eb0d' }, localize('italic', '')); +registerIcon('jersey', { character: '\eb0e' }, localize('jersey', '')); +registerIcon('json', { character: '\eb0f' }, localize('json', '')); +registerIcon('kebab-vertical', { character: '\eb10' }, localize('kebab-vertical', '')); +registerIcon('key', { character: '\eb11' }, localize('key', '')); +registerIcon('law', { character: '\eb12' }, localize('law', '')); +registerIcon('lightbulb-autofix', { character: '\eb13' }, localize('lightbulb-autofix', '')); +registerIcon('link-external', { character: '\eb14' }, localize('link-external', '')); +registerIcon('link', { character: '\eb15' }, localize('link', '')); +registerIcon('list-ordered', { character: '\eb16' }, localize('list-ordered', '')); +registerIcon('list-unordered', { character: '\eb17' }, localize('list-unordered', '')); +registerIcon('live-share', { character: '\eb18' }, localize('live-share', '')); +registerIcon('loading', { character: '\eb19' }, localize('loading', '')); +registerIcon('location', { character: '\eb1a' }, localize('location', '')); +registerIcon('mail-read', { character: '\eb1b' }, localize('mail-read', '')); +registerIcon('mail', { character: '\eb1c' }, localize('mail', '')); +registerIcon('markdown', { character: '\eb1d' }, localize('markdown', '')); +registerIcon('megaphone', { character: '\eb1e' }, localize('megaphone', '')); +registerIcon('mention', { character: '\eb1f' }, localize('mention', '')); +registerIcon('milestone', { character: '\eb20' }, localize('milestone', '')); +registerIcon('mortar-board', { character: '\eb21' }, localize('mortar-board', '')); +registerIcon('move', { character: '\eb22' }, localize('move', '')); +registerIcon('multiple-windows', { character: '\eb23' }, localize('multiple-windows', '')); +registerIcon('mute', { character: '\eb24' }, localize('mute', '')); +registerIcon('no-newline', { character: '\eb25' }, localize('no-newline', '')); +registerIcon('note', { character: '\eb26' }, localize('note', '')); +registerIcon('octoface', { character: '\eb27' }, localize('octoface', '')); +registerIcon('open-preview', { character: '\eb28' }, localize('open-preview', '')); +registerIcon('package', { character: '\eb29' }, localize('package', '')); +registerIcon('paintcan', { character: '\eb2a' }, localize('paintcan', '')); +registerIcon('pin', { character: '\eb2b' }, localize('pin', '')); +registerIcon('play', { character: '\eb2c' }, localize('play', '')); +registerIcon('run', { character: '\eb2c' }, localize('run', '')); +registerIcon('plug', { character: '\eb2d' }, localize('plug', '')); +registerIcon('preserve-case', { character: '\eb2e' }, localize('preserve-case', '')); +registerIcon('preview', { character: '\eb2f' }, localize('preview', '')); +registerIcon('project', { character: '\eb30' }, localize('project', '')); +registerIcon('pulse', { character: '\eb31' }, localize('pulse', '')); +registerIcon('question', { character: '\eb32' }, localize('question', '')); +registerIcon('quote', { character: '\eb33' }, localize('quote', '')); +registerIcon('radio-tower', { character: '\eb34' }, localize('radio-tower', '')); +registerIcon('reactions', { character: '\eb35' }, localize('reactions', '')); +registerIcon('references', { character: '\eb36' }, localize('references', '')); +registerIcon('refresh', { character: '\eb37' }, localize('refresh', '')); +registerIcon('regex', { character: '\eb38' }, localize('regex', '')); +registerIcon('remote-explorer', { character: '\eb39' }, localize('remote-explorer', '')); +registerIcon('remote', { character: '\eb3a' }, localize('remote', '')); +registerIcon('remove', { character: '\eb3b' }, localize('remove', '')); +registerIcon('replace-all', { character: '\eb3c' }, localize('replace-all', '')); +registerIcon('replace', { character: '\eb3d' }, localize('replace', '')); +registerIcon('repo-clone', { character: '\eb3e' }, localize('repo-clone', '')); +registerIcon('repo-force-push', { character: '\eb3f' }, localize('repo-force-push', '')); +registerIcon('repo-pull', { character: '\eb40' }, localize('repo-pull', '')); +registerIcon('repo-push', { character: '\eb41' }, localize('repo-push', '')); +registerIcon('report', { character: '\eb42' }, localize('report', '')); +registerIcon('request-changes', { character: '\eb43' }, localize('request-changes', '')); +registerIcon('rocket', { character: '\eb44' }, localize('rocket', '')); +registerIcon('root-folder-opened', { character: '\eb45' }, localize('root-folder-opened', '')); +registerIcon('root-folder', { character: '\eb46' }, localize('root-folder', '')); +registerIcon('rss', { character: '\eb47' }, localize('rss', '')); +registerIcon('ruby', { character: '\eb48' }, localize('ruby', '')); +registerIcon('save-all', { character: '\eb49' }, localize('save-all', '')); +registerIcon('save-as', { character: '\eb4a' }, localize('save-as', '')); +registerIcon('save', { character: '\eb4b' }, localize('save', '')); +registerIcon('screen-full', { character: '\eb4c' }, localize('screen-full', '')); +registerIcon('screen-normal', { character: '\eb4d' }, localize('screen-normal', '')); +registerIcon('search-stop', { character: '\eb4e' }, localize('search-stop', '')); +registerIcon('server', { character: '\eb50' }, localize('server', '')); +registerIcon('settings-gear', { character: '\eb51' }, localize('settings-gear', '')); +registerIcon('settings', { character: '\eb52' }, localize('settings', '')); +registerIcon('shield', { character: '\eb53' }, localize('shield', '')); +registerIcon('smiley', { character: '\eb54' }, localize('smiley', '')); +registerIcon('sort-precedence', { character: '\eb55' }, localize('sort-precedence', '')); +registerIcon('split-horizontal', { character: '\eb56' }, localize('split-horizontal', '')); +registerIcon('split-vertical', { character: '\eb57' }, localize('split-vertical', '')); +registerIcon('squirrel', { character: '\eb58' }, localize('squirrel', '')); +registerIcon('star-full', { character: '\eb59' }, localize('star-full', '')); +registerIcon('star-half', { character: '\eb5a' }, localize('star-half', '')); +registerIcon('symbol-class', { character: '\eb5b' }, localize('symbol-class', '')); +registerIcon('symbol-color', { character: '\eb5c' }, localize('symbol-color', '')); +registerIcon('symbol-constant', { character: '\eb5d' }, localize('symbol-constant', '')); +registerIcon('symbol-enum-member', { character: '\eb5e' }, localize('symbol-enum-member', '')); +registerIcon('symbol-field', { character: '\eb5f' }, localize('symbol-field', '')); +registerIcon('symbol-file', { character: '\eb60' }, localize('symbol-file', '')); +registerIcon('symbol-interface', { character: '\eb61' }, localize('symbol-interface', '')); +registerIcon('symbol-keyword', { character: '\eb62' }, localize('symbol-keyword', '')); +registerIcon('symbol-misc', { character: '\eb63' }, localize('symbol-misc', '')); +registerIcon('symbol-operator', { character: '\eb64' }, localize('symbol-operator', '')); +registerIcon('symbol-property', { character: '\eb65' }, localize('symbol-property', '')); +registerIcon('wrench', { character: '\eb65' }, localize('wrench', '')); +registerIcon('wrench-subaction', { character: '\eb65' }, localize('wrench-subaction', '')); +registerIcon('symbol-snippet', { character: '\eb66' }, localize('symbol-snippet', '')); +registerIcon('tasklist', { character: '\eb67' }, localize('tasklist', '')); +registerIcon('telescope', { character: '\eb68' }, localize('telescope', '')); +registerIcon('text-size', { character: '\eb69' }, localize('text-size', '')); +registerIcon('three-bars', { character: '\eb6a' }, localize('three-bars', '')); +registerIcon('thumbsdown', { character: '\eb6b' }, localize('thumbsdown', '')); +registerIcon('thumbsup', { character: '\eb6c' }, localize('thumbsup', '')); +registerIcon('tools', { character: '\eb6d' }, localize('tools', '')); +registerIcon('triangle-down', { character: '\eb6e' }, localize('triangle-down', '')); +registerIcon('triangle-left', { character: '\eb6f' }, localize('triangle-left', '')); +registerIcon('triangle-right', { character: '\eb70' }, localize('triangle-right', '')); +registerIcon('triangle-up', { character: '\eb71' }, localize('triangle-up', '')); +registerIcon('twitter', { character: '\eb72' }, localize('twitter', '')); +registerIcon('unfold', { character: '\eb73' }, localize('unfold', '')); +registerIcon('unlock', { character: '\eb74' }, localize('unlock', '')); +registerIcon('unmute', { character: '\eb75' }, localize('unmute', '')); +registerIcon('unverified', { character: '\eb76' }, localize('unverified', '')); +registerIcon('verified', { character: '\eb77' }, localize('verified', '')); +registerIcon('versions', { character: '\eb78' }, localize('versions', '')); +registerIcon('vm-active', { character: '\eb79' }, localize('vm-active', '')); +registerIcon('vm-outline', { character: '\eb7a' }, localize('vm-outline', '')); +registerIcon('vm-running', { character: '\eb7b' }, localize('vm-running', '')); +registerIcon('watch', { character: '\eb7c' }, localize('watch', '')); +registerIcon('whitespace', { character: '\eb7d' }, localize('whitespace', '')); +registerIcon('whole-word', { character: '\eb7e' }, localize('whole-word', '')); +registerIcon('window', { character: '\eb7f' }, localize('window', '')); +registerIcon('word-wrap', { character: '\eb80' }, localize('word-wrap', '')); +registerIcon('zoom-in', { character: '\eb81' }, localize('zoom-in', '')); +registerIcon('zoom-out', { character: '\eb82' }, localize('zoom-out', '')); +registerIcon('list-filter', { character: '\eb83' }, localize('list-filter', '')); +registerIcon('list-flat', { character: '\eb84' }, localize('list-flat', '')); +registerIcon('list-selection', { character: '\eb85' }, localize('list-selection', '')); +registerIcon('selection', { character: '\eb85' }, localize('selection', '')); +registerIcon('list-tree', { character: '\eb86' }, localize('list-tree', '')); +registerIcon('debug-breakpoint-function-unverified', { character: '\eb87' }, localize('debug-breakpoint-function-unverified', '')); +registerIcon('debug-breakpoint-function', { character: '\eb88' }, localize('debug-breakpoint-function', '')); +registerIcon('debug-breakpoint-function-disabled', { character: '\eb88' }, localize('debug-breakpoint-function-disabled', '')); +registerIcon('debug-stackframe-active', { character: '\eb89' }, localize('debug-stackframe-active', '')); +registerIcon('debug-stackframe-dot', { character: '\eb8a' }, localize('debug-stackframe-dot', '')); +registerIcon('debug-stackframe', { character: '\eb8b' }, localize('debug-stackframe', '')); +registerIcon('debug-stackframe-focused', { character: '\eb8b' }, localize('debug-stackframe-focused', '')); +registerIcon('debug-breakpoint-unsupported', { character: '\eb8c' }, localize('debug-breakpoint-unsupported', '')); +registerIcon('symbol-string', { character: '\eb8d' }, localize('symbol-string', '')); +registerIcon('debug-reverse-continue', { character: '\eb8e' }, localize('debug-reverse-continue', '')); +registerIcon('debug-step-back', { character: '\eb8f' }, localize('debug-step-back', '')); +registerIcon('debug-restart-frame', { character: '\eb90' }, localize('debug-restart-frame', '')); +registerIcon('debug-alternate', { character: '\eb91' }, localize('debug-alternate', '')); +registerIcon('call-incoming', { character: '\eb92' }, localize('call-incoming', '')); +registerIcon('call-outgoing', { character: '\eb93' }, localize('call-outgoing', '')); +registerIcon('menu', { character: '\eb94' }, localize('menu', '')); +registerIcon('expand-all', { character: '\eb95' }, localize('expand-all', '')); +registerIcon('feedback', { character: '\eb96' }, localize('feedback', '')); +registerIcon('group-by-ref-type', { character: '\eb97' }, localize('group-by-ref-type', '')); +registerIcon('ungroup-by-ref-type', { character: '\eb98' }, localize('ungroup-by-ref-type', '')); +registerIcon('bell-dot', { character: '\f101' }, localize('bell-dot', '')); +registerIcon('debug-alt-2', { character: '\f102' }, localize('debug-alt-2', '')); +registerIcon('debug-alt', { character: '\f103' }, localize('debug-alt', '')); + + +// setTimeout(_ => console.log(colorRegistry.toString()), 5000); diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index 03edf498efd..8f79415fe07 100644 --- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -7,11 +7,10 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IFileService, IFileContent, FileChangesEvent, FileSystemProviderError, FileSystemProviderErrorCode, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files'; import { VSBuffer } from 'vs/base/common/buffer'; import { URI } from 'vs/base/common/uri'; -import { SyncSource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService, ResourceKey, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; +import { SyncSource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService, ResourceKey, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { joinPath, dirname } from 'vs/base/common/resources'; -import { toLocalISOString } from 'vs/base/common/date'; -import { ThrottledDelayer, CancelablePromise } from 'vs/base/common/async'; +import { CancelablePromise } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ParseError, parse } from 'vs/base/common/json'; @@ -19,6 +18,7 @@ import { FormattingOptions } from 'vs/base/common/jsonFormatter'; import { IStringDictionary } from 'vs/base/common/collections'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { isString } from 'vs/base/common/types'; type SyncSourceClassification = { source?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; @@ -44,7 +44,6 @@ function isSyncData(thing: any): thing is ISyncData { export abstract class AbstractSynchroniser extends Disposable { protected readonly syncFolder: URI; - private cleanUpDelayer: ThrottledDelayer; private _status: SyncStatus = SyncStatus.Idle; get status(): SyncStatus { return this._status; } @@ -58,9 +57,11 @@ export abstract class AbstractSynchroniser extends Disposable { constructor( readonly source: SyncSource, + readonly resourceKey: ResourceKey, @IFileService protected readonly fileService: IFileService, @IEnvironmentService environmentService: IEnvironmentService, @IUserDataSyncStoreService protected readonly userDataSyncStoreService: IUserDataSyncStoreService, + @IUserDataSyncBackupStoreService protected readonly userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, @IUserDataSyncEnablementService protected readonly userDataSyncEnablementService: IUserDataSyncEnablementService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IUserDataSyncLogService protected readonly logService: IUserDataSyncLogService, @@ -68,9 +69,7 @@ export abstract class AbstractSynchroniser extends Disposable { ) { super(); this.syncFolder = joinPath(environmentService.userDataSyncHome, source); - this.lastSyncResource = joinPath(this.syncFolder, `lastSync${source}.json`); - this.cleanUpDelayer = new ThrottledDelayer(50); - this.cleanUpBackup(); + this.lastSyncResource = joinPath(this.syncFolder, `lastSync${this.resourceKey}.json`); } protected setStatus(status: SyncStatus): void { @@ -153,10 +152,18 @@ export abstract class AbstractSynchroniser extends Disposable { return !!lastSyncData; } - async getRemoteContent(): Promise { - const lastSyncData = await this.getLastSyncUserData(); - const { syncData } = await this.getRemoteUserData(lastSyncData); - return syncData ? syncData.content : null; + async getRemoteContentFromPreview(): Promise { + return null; + } + + async getRemoteContent(ref?: string): Promise { + const refOrLastSyncUserData: string | IRemoteUserData | null = ref || await this.getLastSyncUserData(); + const { content } = await this.getUserData(refOrLastSyncUserData); + return content; + } + + async getLocalBackupContent(ref?: string): Promise { + return this.userDataSyncBackupStoreService.resolveContent(this.resourceKey, ref); } async resetLocal(): Promise { @@ -192,76 +199,51 @@ export abstract class AbstractSynchroniser extends Disposable { } protected async getRemoteUserData(lastSyncData: IRemoteUserData | null): Promise { - const lastSyncUserData: IUserData | null = lastSyncData ? { ref: lastSyncData.ref, content: lastSyncData.syncData ? JSON.stringify(lastSyncData.syncData) : null } : null; - const { ref, content } = await this.userDataSyncStoreService.read(this.resourceKey, lastSyncUserData, this.source); + const { ref, content } = await this.getUserData(lastSyncData); let syncData: ISyncData | null = null; if (content !== null) { - try { - syncData = JSON.parse(content); - - // Migration from old content to sync data - if (!isSyncData(syncData)) { - syncData = { version: this.version, content }; - } - - } catch (e) { - this.logService.error(e); - } + syncData = this.parseSyncData(content); } return { ref, syncData }; } + protected parseSyncData(content: string): ISyncData | null { + let syncData: ISyncData | null = null; + try { + syncData = JSON.parse(content); + + // Migration from old content to sync data + if (!isSyncData(syncData)) { + syncData = { version: this.version, content }; + } + + } catch (e) { + this.logService.error(e); + } + return syncData; + } + + private async getUserData(refOrLastSyncData: string | IRemoteUserData | null): Promise { + if (isString(refOrLastSyncData)) { + const content = await this.userDataSyncStoreService.resolveContent(this.resourceKey, refOrLastSyncData); + return { ref: refOrLastSyncData, content }; + } else { + const lastSyncUserData: IUserData | null = refOrLastSyncData ? { ref: refOrLastSyncData.ref, content: refOrLastSyncData.syncData ? JSON.stringify(refOrLastSyncData.syncData) : null } : null; + return this.userDataSyncStoreService.read(this.resourceKey, lastSyncUserData, this.source); + } + } + protected async updateRemoteUserData(content: string, ref: string | null): Promise { const syncData: ISyncData = { version: this.version, content }; ref = await this.userDataSyncStoreService.write(this.resourceKey, JSON.stringify(syncData), ref, this.source); return { ref, syncData }; } - protected async backupLocal(content: VSBuffer): Promise { - const resource = joinPath(this.syncFolder, `${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}.json`); - try { - await this.fileService.writeFile(resource, content); - } catch (e) { - this.logService.error(e); - } - this.cleanUpDelayer.trigger(() => this.cleanUpBackup()); + protected async backupLocal(content: string): Promise { + const syncData: ISyncData = { version: this.version, content }; + return this.userDataSyncBackupStoreService.backup(this.resourceKey, JSON.stringify(syncData)); } - private async cleanUpBackup(): Promise { - try { - if (!(await this.fileService.exists(this.syncFolder))) { - return; - } - const stat = await this.fileService.resolve(this.syncFolder); - if (stat.children) { - const all = stat.children.filter(stat => stat.isFile && /^\d{8}T\d{6}(\.json)?$/.test(stat.name)).sort(); - const backUpMaxAge = 1000 * 60 * 60 * 24 * (this.configurationService.getValue('sync.localBackupDuration') || 30 /* Default 30 days */); - let toDelete = all.filter(stat => { - const ctime = stat.ctime || new Date( - parseInt(stat.name.substring(0, 4)), - parseInt(stat.name.substring(4, 6)) - 1, - parseInt(stat.name.substring(6, 8)), - parseInt(stat.name.substring(9, 11)), - parseInt(stat.name.substring(11, 13)), - parseInt(stat.name.substring(13, 15)) - ).getTime(); - return Date.now() - ctime > backUpMaxAge; - }); - const remaining = all.length - toDelete.length; - if (remaining < 10) { - toDelete = toDelete.slice(10 - remaining); - } - await Promise.all(toDelete.map(stat => { - this.logService.info('Deleting from backup', stat.resource.path); - this.fileService.del(stat.resource); - })); - } - } catch (e) { - this.logService.error(e); - } - } - - abstract readonly resourceKey: ResourceKey; protected abstract readonly version: number; protected abstract performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise; } @@ -283,15 +265,17 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser { constructor( protected readonly file: URI, source: SyncSource, + resourceKey: ResourceKey, @IFileService fileService: IFileService, @IEnvironmentService environmentService: IEnvironmentService, @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, + @IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, @IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService, @ITelemetryService telemetryService: ITelemetryService, @IUserDataSyncLogService logService: IUserDataSyncLogService, @IConfigurationService configurationService: IConfigurationService, ) { - super(source, fileService, environmentService, userDataSyncStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService); + super(source, resourceKey, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService); this._register(this.fileService.watch(dirname(file))); this._register(this.fileService.onDidFilesChange(e => this.onFileChanges(e))); } @@ -305,14 +289,12 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser { this.setStatus(SyncStatus.Idle); } - async getRemoteContent(preview?: boolean): Promise { - if (preview) { - if (this.syncPreviewResultPromise) { - const result = await this.syncPreviewResultPromise; - return result.remoteUserData && result.remoteUserData.syncData ? result.remoteUserData.syncData.content : null; - } + async getRemoteContentFromPreview(): Promise { + if (this.syncPreviewResultPromise) { + const result = await this.syncPreviewResultPromise; + return result.remoteUserData && result.remoteUserData.syncData ? result.remoteUserData.syncData.content : null; } - return super.getRemoteContent(); + return null; } protected async getLocalFileContent(): Promise { @@ -327,7 +309,6 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser { try { if (oldContent) { // file exists already - await this.backupLocal(oldContent.value); await this.fileService.writeFile(this.file, VSBuffer.fromString(newContent), oldContent); } else { // file does not exist @@ -382,16 +363,18 @@ export abstract class AbstractJsonFileSynchroniser extends AbstractFileSynchroni constructor( file: URI, source: SyncSource, + resourceKey: ResourceKey, @IFileService fileService: IFileService, @IEnvironmentService environmentService: IEnvironmentService, @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, + @IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, @IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService, @ITelemetryService telemetryService: ITelemetryService, @IUserDataSyncLogService logService: IUserDataSyncLogService, @IUserDataSyncUtilService protected readonly userDataSyncUtilService: IUserDataSyncUtilService, @IConfigurationService configurationService: IConfigurationService, ) { - super(file, source, fileService, environmentService, userDataSyncStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService); + super(file, source, resourceKey, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService); } protected hasErrors(content: string): boolean { diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index 1e9c81523e0..cdc3d6faab5 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SyncStatus, IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncSource, ResourceKey, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; +import { SyncStatus, IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncSource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; import { Event } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IExtensionManagementService, IExtensionGalleryService, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement'; @@ -16,7 +16,6 @@ import { merge } from 'vs/platform/userDataSync/common/extensionsMerge'; import { isNonEmptyArray } from 'vs/base/common/arrays'; import { AbstractSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { VSBuffer } from 'vs/base/common/buffer'; interface ISyncPreviewResult { readonly localExtensions: ISyncExtension[]; @@ -35,7 +34,6 @@ interface ILastSyncUserData extends IRemoteUserData { export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser { - readonly resourceKey: ResourceKey = 'extensions'; protected readonly version: number = 2; protected isEnabled(): boolean { return super.isEnabled() && this.extensionGalleryService.isEnabled(); } @@ -43,6 +41,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse @IEnvironmentService environmentService: IEnvironmentService, @IFileService fileService: IFileService, @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, + @IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IGlobalExtensionEnablementService private readonly extensionEnablementService: IGlobalExtensionEnablementService, @IUserDataSyncLogService logService: IUserDataSyncLogService, @@ -51,7 +50,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse @IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService, @ITelemetryService telemetryService: ITelemetryService, ) { - super(SyncSource.Extensions, fileService, environmentService, userDataSyncStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService); + super(SyncSource.Extensions, 'extensions', fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService); this._register( Event.debounce( Event.any( @@ -121,6 +120,33 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse async stop(): Promise { } + async getRemoteContent(ref?: string, fragment?: string): Promise { + const content = await super.getRemoteContent(ref); + if (content !== null && fragment) { + return this.getFragment(content, fragment); + } + return content; + } + + async getLocalBackupContent(ref?: string, fragment?: string): Promise { + let content = await super.getLocalBackupContent(ref); + if (content !== null && fragment) { + return this.getFragment(content, fragment); + } + return content; + } + + private getFragment(content: string, fragment: string): string | null { + const syncData = this.parseSyncData(content); + if (syncData) { + switch (fragment) { + case 'extensions': + return syncData.content; + } + } + return null; + } + accept(content: string): Promise { throw new Error('Extensions: Conflicts should not occur'); } @@ -137,10 +163,6 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse return false; } - async getRemoteContent(): Promise { - return null; - } - protected async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise { const previewResult = await this.getPreview(remoteUserData, lastSyncUserData); await this.apply(previewResult); @@ -180,7 +202,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse if (added.length || removed.length || updated.length) { // back up all disabled or market place extensions const backUpExtensions = localExtensions.filter(e => e.disabled || !!e.identifier.uuid); - await this.backupLocal(VSBuffer.fromString(JSON.stringify(backUpExtensions, null, '\t'))); + await this.backupLocal(JSON.stringify(backUpExtensions)); skippedExtensions = await this.updateLocalExtensions(added, removed, updated, skippedExtensions); } diff --git a/src/vs/platform/userDataSync/common/globalStateSync.ts b/src/vs/platform/userDataSync/common/globalStateSync.ts index ff9dc75d808..a095a09b482 100644 --- a/src/vs/platform/userDataSync/common/globalStateSync.ts +++ b/src/vs/platform/userDataSync/common/globalStateSync.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncSource, IUserDataSynchroniser, ResourceKey, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; +import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncSource, IUserDataSynchroniser, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; import { VSBuffer } from 'vs/base/common/buffer'; import { Event } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -29,19 +29,19 @@ interface ISyncPreviewResult { export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser { - readonly resourceKey: ResourceKey = 'globalState'; protected readonly version: number = 1; constructor( @IFileService fileService: IFileService, @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, + @IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, @IUserDataSyncLogService logService: IUserDataSyncLogService, @IEnvironmentService private readonly environmentService: IEnvironmentService, @IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService, @ITelemetryService telemetryService: ITelemetryService, @IConfigurationService configurationService: IConfigurationService, ) { - super(SyncSource.GlobalState, fileService, environmentService, userDataSyncStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService); + super(SyncSource.GlobalState, 'globalState', fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService); this._register(this.fileService.watch(dirname(this.environmentService.argvResource))); this._register(Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.environmentService.argvResource))(() => this._onDidChangeLocal.fire())); } @@ -104,6 +104,33 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs async stop(): Promise { } + async getRemoteContent(ref?: string, fragment?: string): Promise { + let content = await super.getRemoteContent(ref); + if (content !== null && fragment) { + return this.getFragment(content, fragment); + } + return content; + } + + async getLocalBackupContent(ref?: string, fragment?: string): Promise { + let content = await super.getLocalBackupContent(ref); + if (content !== null && fragment) { + return this.getFragment(content, fragment); + } + return content; + } + + private getFragment(content: string, fragment: string): string | null { + const syncData = this.parseSyncData(content); + if (syncData) { + switch (fragment) { + case 'globalState': + return syncData.content; + } + } + return null; + } + accept(content: string): Promise { throw new Error('UI State: Conflicts should not occur'); } @@ -120,17 +147,13 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs return false; } - async getRemoteContent(): Promise { - return null; - } - protected async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { const result = await this.getPreview(remoteUserData, lastSyncUserData); await this.apply(result); return SyncStatus.Idle; } - private async getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null,): Promise { + private async getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null; const lastSyncGlobalState = lastSyncUserData && lastSyncUserData.syncData ? JSON.parse(lastSyncUserData.syncData.content) : null; @@ -158,7 +181,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs if (local) { // update local this.logService.trace('UI State: Updating local ui state...'); - await this.backupLocal(VSBuffer.fromString(JSON.stringify(localUserData, null, '\t'))); + await this.backupLocal(JSON.stringify(localUserData)); await this.writeLocalGlobalState(local); this.logService.info('UI State: Updated local ui state'); } diff --git a/src/vs/platform/userDataSync/common/keybindingsSync.ts b/src/vs/platform/userDataSync/common/keybindingsSync.ts index e6e7e2931da..c3f02bac0da 100644 --- a/src/vs/platform/userDataSync/common/keybindingsSync.ts +++ b/src/vs/platform/userDataSync/common/keybindingsSync.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; -import { UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, SyncSource, IUserDataSynchroniser, ResourceKey, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; +import { UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, SyncSource, IUserDataSynchroniser, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge'; import { VSBuffer } from 'vs/base/common/buffer'; import { parse } from 'vs/base/common/json'; @@ -29,12 +29,12 @@ interface ISyncContent { export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implements IUserDataSynchroniser { - readonly resourceKey: ResourceKey = 'keybindings'; protected get conflictsPreviewResource(): URI { return this.environmentService.keybindingsSyncPreviewResource; } protected readonly version: number = 1; constructor( @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, + @IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, @IUserDataSyncLogService logService: IUserDataSyncLogService, @IConfigurationService configurationService: IConfigurationService, @IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService, @@ -43,7 +43,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem @IUserDataSyncUtilService userDataSyncUtilService: IUserDataSyncUtilService, @ITelemetryService telemetryService: ITelemetryService, ) { - super(environmentService.keybindingsResource, SyncSource.Keybindings, fileService, environmentService, userDataSyncStoreService, userDataSyncEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService); + super(environmentService.keybindingsResource, SyncSource.Keybindings, 'keybindings', fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService); } async pull(): Promise { @@ -156,11 +156,38 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem return false; } - async getRemoteContent(preview?: boolean): Promise { - const content = await super.getRemoteContent(preview); + async getRemoteContentFromPreview(): Promise { + const content = await super.getRemoteContentFromPreview(); return content !== null ? this.getKeybindingsContentFromSyncContent(content) : null; } + async getRemoteContent(ref?: string, fragment?: string): Promise { + const content = await super.getRemoteContent(ref); + if (content !== null && fragment) { + return this.getFragment(content, fragment); + } + return content; + } + + async getLocalBackupContent(ref?: string, fragment?: string): Promise { + let content = await super.getLocalBackupContent(ref); + if (content !== null && fragment) { + return this.getFragment(content, fragment); + } + return content; + } + + private getFragment(content: string, fragment: string): string | null { + const syncData = this.parseSyncData(content); + if (syncData) { + switch (fragment) { + case 'keybindings': + return this.getKeybindingsContentFromSyncContent(syncData.content); + } + } + return null; + } + protected async performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { try { const result = await this.getPreview(remoteUserData, lastSyncUserData); @@ -197,13 +224,14 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem if (hasLocalChanged) { this.logService.trace('Keybindings: Updating local keybindings...'); + await this.backupLocal(this.toSyncContent(content, null)); await this.updateLocalFileContent(content, fileContent); this.logService.info('Keybindings: Updated local keybindings'); } if (hasRemoteChanged) { this.logService.trace('Keybindings: Updating remote keybindings...'); - const remoteContents = this.updateSyncContent(content, remoteUserData.syncData ? remoteUserData.syncData.content : null); + const remoteContents = this.toSyncContent(content, remoteUserData.syncData ? remoteUserData.syncData.content : null); remoteUserData = await this.updateRemoteUserData(remoteContents, forcePush ? null : remoteUserData.ref); this.logService.info('Keybindings: Updated remote keybindings'); } @@ -218,7 +246,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem if (lastSyncUserData?.ref !== remoteUserData.ref && (content !== null || fileContent !== null)) { this.logService.trace('Keybindings: Updating last synchronized keybindings...'); - const lastSyncContent = this.updateSyncContent(content !== null ? content : fileContent!.value.toString(), null); + const lastSyncContent = this.toSyncContent(content !== null ? content : fileContent!.value.toString(), null); await this.updateLastSyncUserData({ ref: remoteUserData.ref, syncData: { version: remoteUserData.syncData!.version, content: lastSyncContent } }); this.logService.info('Keybindings: Updated last synchronized keybindings'); } @@ -301,7 +329,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem } } - private updateSyncContent(keybindingsContent: string, syncContent: string | null): string { + private toSyncContent(keybindingsContent: string, syncContent: string | null): string { let parsed: ISyncContent = {}; try { parsed = JSON.parse(syncContent || '{}'); diff --git a/src/vs/platform/userDataSync/common/settingsSync.ts b/src/vs/platform/userDataSync/common/settingsSync.ts index 1c75533e750..219f2c2a698 100644 --- a/src/vs/platform/userDataSync/common/settingsSync.ts +++ b/src/vs/platform/userDataSync/common/settingsSync.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; -import { UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, IConflictSetting, ISettingsSyncService, CONFIGURATION_SYNC_STORE_KEY, SyncSource, ResourceKey, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; +import { UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, IConflictSetting, ISettingsSyncService, CONFIGURATION_SYNC_STORE_KEY, SyncSource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; import { VSBuffer } from 'vs/base/common/buffer'; import { parse } from 'vs/base/common/json'; import { localize } from 'vs/nls'; @@ -37,7 +37,6 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement _serviceBrand: any; - readonly resourceKey: ResourceKey = 'settings'; protected readonly version: number = 1; protected get conflictsPreviewResource(): URI { return this.environmentService.settingsSyncPreviewResource; } @@ -50,6 +49,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement @IFileService fileService: IFileService, @IEnvironmentService private readonly environmentService: IEnvironmentService, @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, + @IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, @IUserDataSyncLogService logService: IUserDataSyncLogService, @IUserDataSyncUtilService userDataSyncUtilService: IUserDataSyncUtilService, @IConfigurationService configurationService: IConfigurationService, @@ -57,7 +57,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement @ITelemetryService telemetryService: ITelemetryService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, ) { - super(environmentService.settingsResource, SyncSource.Settings, fileService, environmentService, userDataSyncStoreService, userDataSyncEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService); + super(environmentService.settingsResource, SyncSource.Settings, 'settings', fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService); } protected setStatus(status: SyncStatus): void { @@ -187,13 +187,13 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement return false; } - async getRemoteContent(preview?: boolean): Promise { - let content = await super.getRemoteContent(preview); + async getRemoteContentFromPreview(): Promise { + let content = await super.getRemoteContentFromPreview(); if (content !== null) { const settingsSyncContent = this.parseSettingsSyncContent(content); content = settingsSyncContent ? settingsSyncContent.settings : null; } - if (preview && content !== null) { + if (content !== null) { const formatUtils = await this.getFormattingOptions(); // remove ignored settings from the remote content for preview const ignoredSettings = await this.getIgnoredSettings(); @@ -202,6 +202,36 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement return content; } + async getRemoteContent(ref?: string, fragment?: string): Promise { + let content = await super.getRemoteContent(ref); + if (content !== null && fragment) { + return this.getFragment(content, fragment); + } + return content; + } + + async getLocalBackupContent(ref?: string, fragment?: string): Promise { + let content = await super.getLocalBackupContent(ref); + if (content !== null && fragment) { + return this.getFragment(content, fragment); + } + return content; + } + + private getFragment(content: string, fragment: string): string | null { + const syncData = this.parseSyncData(content); + if (syncData) { + const settingsSyncContent = this.parseSettingsSyncContent(syncData.content); + if (settingsSyncContent) { + switch (fragment) { + case 'settings': + return settingsSyncContent.settings; + } + } + } + return null; + } + async accept(content: string): Promise { if (this.status === SyncStatus.HasConflicts) { const preview = await this.syncPreviewResultPromise!; @@ -259,6 +289,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement if (hasLocalChanged) { this.logService.trace('Settings: Updating local settings...'); + await this.backupLocal(JSON.stringify(this.toSettingsSyncContent(content))); await this.updateLocalFileContent(content, fileContent); this.logService.info('Settings: Updated local settings'); } @@ -269,7 +300,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement const ignoredSettings = await this.getIgnoredSettings(content); content = updateIgnoredSettings(content, remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : '{}', ignoredSettings, formatUtils); this.logService.trace('Settings: Updating remote settings...'); - remoteUserData = await this.updateRemoteUserData(JSON.stringify({ settings: content }), forcePush ? null : remoteUserData.ref); + remoteUserData = await this.updateRemoteUserData(JSON.stringify(this.toSettingsSyncContent(content)), forcePush ? null : remoteUserData.ref); this.logService.info('Settings: Updated remote settings'); } @@ -354,6 +385,10 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement return null; } + private toSettingsSyncContent(settings: string): ISettingsSyncContent { + return { settings }; + } + private _defaultIgnoredSettings: Promise | undefined = undefined; protected async getIgnoredSettings(content?: string): Promise { if (!this._defaultIgnoredSettings) { diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 315eeb93e3d..0744514d2f5 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -18,7 +18,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IStringDictionary } from 'vs/base/common/collections'; import { FormattingOptions } from 'vs/base/common/jsonFormatter'; import { URI } from 'vs/base/common/uri'; -import { isEqual, joinPath } from 'vs/base/common/resources'; +import { isEqual, joinPath, dirname, basename } from 'vs/base/common/resources'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IProductService } from 'vs/platform/product/common/productService'; import { distinct } from 'vs/base/common/arrays'; @@ -143,6 +143,11 @@ export interface IUserDataManifest { session: string; } +export interface IResourceRefHandle { + ref: string; + created: number; +} + export const IUserDataSyncStoreService = createDecorator('IUserDataSyncStoreService'); export interface IUserDataSyncStoreService { _serviceBrand: undefined; @@ -151,11 +156,19 @@ export interface IUserDataSyncStoreService { write(key: ResourceKey, content: string, ref: string | null, source?: SyncSource): Promise; manifest(): Promise; clear(): Promise; - getAllRefs(key: ResourceKey): Promise; + getAllRefs(key: ResourceKey): Promise; resolveContent(key: ResourceKey, ref: string): Promise; delete(key: ResourceKey): Promise; } +export const IUserDataSyncBackupStoreService = createDecorator('IUserDataSyncBackupStoreService'); +export interface IUserDataSyncBackupStoreService { + _serviceBrand: undefined; + backup(resourceKey: ResourceKey, content: string): Promise; + getAllRefs(key: ResourceKey): Promise; + resolveContent(key: ResourceKey, ref?: string): Promise; +} + //#endregion // #region User Data Sync Error @@ -248,7 +261,9 @@ export interface IUserDataSynchroniser { hasLocalData(): Promise; resetLocal(): Promise; - getRemoteContent(preivew?: boolean): Promise; + getRemoteContentFromPreview(): Promise; + getRemoteContent(ref?: string, fragment?: string): Promise; + getLocalBackupContent(ref?: string, fragment?: string): Promise; accept(content: string): Promise; } @@ -293,7 +308,7 @@ export interface IUserDataSyncService { resetLocal(): Promise; isFirstTimeSyncWithMerge(): Promise; - getRemoteContent(source: SyncSource, preview: boolean): Promise; + resolveContent(resource: URI): Promise; accept(source: SyncSource, content: string): Promise; } @@ -335,12 +350,27 @@ export const CONTEXT_SYNC_STATE = new RawContextKey('syncStatus', SyncSt export const CONTEXT_SYNC_ENABLEMENT = new RawContextKey('syncEnabled', false); export const USER_DATA_SYNC_SCHEME = 'vscode-userdata-sync'; -export function toRemoteContentResource(source: SyncSource): URI { - return URI.from({ scheme: USER_DATA_SYNC_SCHEME, path: `${source}/remoteContent` }); +export const PREVIEW_QUERY = 'preview=true'; +export function toRemoteSyncResourceFromSource(source: SyncSource, ref?: string): URI { + return toRemoteSyncResource(getResourceKeyFromSyncSource(source), ref); } -export function getSyncSourceFromRemoteContentResource(uri: URI): SyncSource | undefined { - return [SyncSource.Settings, SyncSource.Keybindings, SyncSource.Extensions, SyncSource.GlobalState].filter(source => isEqual(uri, toRemoteContentResource(source)))[0]; +export function toRemoteSyncResource(resourceKey: ResourceKey, ref?: string): URI { + return URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote', path: `/${resourceKey}/${ref ? ref : 'latest'}` }); } +export function toLocalBackupSyncResource(resourceKey: ResourceKey, ref?: string): URI { + return URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local-backup', path: `/${resourceKey}/${ref ? ref : 'latest'}` }); +} + +export function resolveSyncResource(resource: URI): { remote: boolean, resourceKey: ResourceKey, ref?: string } | null { + const remote = resource.authority === 'remote'; + const resourceKey: ResourceKey = basename(dirname(resource)) as ResourceKey; + const ref = basename(resource); + if (resourceKey && ref) { + return { remote, resourceKey, ref: ref !== 'latest' ? ref : undefined }; + } + return null; +} + export function getSyncSourceFromPreviewResource(uri: URI, environmentService: IEnvironmentService): SyncSource | undefined { if (isEqual(uri, environmentService.settingsSyncPreviewResource)) { return SyncSource.Settings; @@ -350,3 +380,21 @@ export function getSyncSourceFromPreviewResource(uri: URI, environmentService: I } return undefined; } + +export function getResourceKeyFromSyncSource(source: SyncSource): ResourceKey { + switch (source) { + case SyncSource.Settings: return 'settings'; + case SyncSource.Keybindings: return 'keybindings'; + case SyncSource.Extensions: return 'extensions'; + case SyncSource.GlobalState: return 'globalState'; + } +} + +export function getSyncSourceFromResourceKey(resourceKey: ResourceKey): SyncSource { + switch (resourceKey) { + case 'settings': return SyncSource.Settings; + case 'keybindings': return SyncSource.Keybindings; + case 'extensions': return SyncSource.Extensions; + case 'globalState': return SyncSource.GlobalState; + } +} diff --git a/src/vs/platform/userDataSync/common/userDataSyncBackupStoreService.ts b/src/vs/platform/userDataSync/common/userDataSyncBackupStoreService.ts new file mode 100644 index 00000000000..b439de0bc02 --- /dev/null +++ b/src/vs/platform/userDataSync/common/userDataSyncBackupStoreService.ts @@ -0,0 +1,109 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, } from 'vs/base/common/lifecycle'; +import { IUserDataSyncLogService, ResourceKey, ALL_RESOURCE_KEYS, IUserDataSyncBackupStoreService, IResourceRefHandle } from 'vs/platform/userDataSync/common/userDataSync'; +import { joinPath } from 'vs/base/common/resources'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IFileService, IFileStat } from 'vs/platform/files/common/files'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { toLocalISOString } from 'vs/base/common/date'; +import { VSBuffer } from 'vs/base/common/buffer'; + +export class UserDataSyncBackupStoreService extends Disposable implements IUserDataSyncBackupStoreService { + + _serviceBrand: any; + + constructor( + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IFileService private readonly fileService: IFileService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, + ) { + super(); + ALL_RESOURCE_KEYS.forEach(resourceKey => this.cleanUpBackup(resourceKey)); + } + + async getAllRefs(resourceKey: ResourceKey): Promise { + const folder = joinPath(this.environmentService.userDataSyncHome, resourceKey); + const stat = await this.fileService.resolve(folder); + if (stat.children) { + const all = stat.children.filter(stat => stat.isFile && /^\d{8}T\d{6}(\.json)?$/.test(stat.name)).sort().reverse(); + return all.map(stat => ({ + ref: stat.name, + created: this.getCreationTime(stat) + })); + } + return []; + } + + async resolveContent(resourceKey: ResourceKey, ref?: string): Promise { + if (!ref) { + const refs = await this.getAllRefs(resourceKey); + if (refs.length) { + ref = refs[refs.length - 1].ref; + } + } + if (ref) { + const file = joinPath(this.environmentService.userDataSyncHome, resourceKey, ref); + const content = await this.fileService.readFile(file); + return content.value.toString(); + } + return null; + } + + async backup(resourceKey: ResourceKey, content: string): Promise { + const folder = joinPath(this.environmentService.userDataSyncHome, resourceKey); + const resource = joinPath(folder, `${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}.json`); + try { + await this.fileService.writeFile(resource, VSBuffer.fromString(content)); + } catch (e) { + this.logService.error(e); + } + try { + this.cleanUpBackup(resourceKey); + } catch (e) { /* Ignore */ } + } + + private async cleanUpBackup(resourceKey: ResourceKey): Promise { + const folder = joinPath(this.environmentService.userDataSyncHome, resourceKey); + try { + try { + if (!(await this.fileService.exists(folder))) { + return; + } + } catch (e) { + return; + } + const stat = await this.fileService.resolve(folder); + if (stat.children) { + const all = stat.children.filter(stat => stat.isFile && /^\d{8}T\d{6}(\.json)?$/.test(stat.name)).sort(); + const backUpMaxAge = 1000 * 60 * 60 * 24 * (this.configurationService.getValue('sync.localBackupDuration') || 30 /* Default 30 days */); + let toDelete = all.filter(stat => Date.now() - this.getCreationTime(stat) > backUpMaxAge); + const remaining = all.length - toDelete.length; + if (remaining < 10) { + toDelete = toDelete.slice(10 - remaining); + } + await Promise.all(toDelete.map(stat => { + this.logService.info('Deleting from backup', stat.resource.path); + this.fileService.del(stat.resource); + })); + } + } catch (e) { + this.logService.error(e); + } + } + + private getCreationTime(stat: IFileStat) { + return stat.ctime || new Date( + parseInt(stat.name.substring(0, 4)), + parseInt(stat.name.substring(4, 6)) - 1, + parseInt(stat.name.substring(6, 8)), + parseInt(stat.name.substring(9, 11)), + parseInt(stat.name.substring(11, 13)), + parseInt(stat.name.substring(13, 15)) + ).getTime(); + } +} diff --git a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts index 1c346ac1360..de2736a2fe3 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts @@ -5,7 +5,7 @@ import { IServerChannel, IChannel } from 'vs/base/parts/ipc/common/ipc'; import { Event } from 'vs/base/common/event'; -import { IUserDataSyncService, IUserDataSyncUtilService, ISettingsSyncService, IUserDataAutoSyncService, IUserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, IUserDataSyncUtilService, ISettingsSyncService, IUserDataAutoSyncService, IUserDataSyncStoreService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; import { URI } from 'vs/base/common/uri'; import { IStringDictionary } from 'vs/base/common/collections'; import { FormattingOptions } from 'vs/base/common/jsonFormatter'; @@ -34,7 +34,7 @@ export class UserDataSyncChannel implements IServerChannel { case 'stop': this.service.stop(); return Promise.resolve(); case 'reset': return this.service.reset(); case 'resetLocal': return this.service.resetLocal(); - case 'getRemoteContent': return this.service.getRemoteContent(args[0], args[1]); + case 'resolveContent': return this.service.resolveContent(URI.revive(args[0])); case 'isFirstTimeSyncWithMerge': return this.service.isFirstTimeSyncWithMerge(); } throw new Error('Invalid call'); @@ -67,7 +67,9 @@ export class SettingsSyncChannel implements IServerChannel { case 'hasPreviouslySynced': return this.service.hasPreviouslySynced(); case 'hasLocalData': return this.service.hasLocalData(); case 'resolveSettingsConflicts': return this.service.resolveSettingsConflicts(args[0]); - case 'getRemoteContent': return this.service.getRemoteContent(args[0]); + case 'getRemoteContentFromPreview': return this.service.getRemoteContentFromPreview(); + case 'getRemoteContent': return this.service.getRemoteContent(args[0], args[1]); + case 'getLocalBackupContent': return this.service.getLocalBackupContent(args[0], args[1]); } throw new Error('Invalid call'); } @@ -148,3 +150,20 @@ export class UserDataSyncStoreServiceChannel implements IServerChannel { throw new Error('Invalid call'); } } + +export class UserDataSyncBackupStoreServiceChannel implements IServerChannel { + + constructor(private readonly service: IUserDataSyncBackupStoreService) { } + + listen(_: unknown, event: string): Event { + throw new Error(`Event not found: ${event}`); + } + + call(context: any, command: string, args?: any): Promise { + switch (command) { + case 'getAllRefs': return this.service.getAllRefs(args[0]); + case 'resolveContent': return this.service.resolveContent(args[0], args[1]); + } + throw new Error('Invalid call'); + } +} diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index f88cce3588c..4be50d8387a 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncSource, ISettingsSyncService, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncStoreError, UserDataSyncErrorCode, UserDataSyncError } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncSource, ISettingsSyncService, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncStoreError, UserDataSyncErrorCode, UserDataSyncError, resolveSyncResource, PREVIEW_QUERY } from 'vs/platform/userDataSync/common/userDataSync'; import { Disposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Emitter, Event } from 'vs/base/common/event'; @@ -15,6 +15,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { equals } from 'vs/base/common/arrays'; import { localize } from 'vs/nls'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { URI } from 'vs/base/common/uri'; type SyncErrorClassification = { source: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; @@ -176,11 +177,15 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ await synchroniser.accept(content); } - async getRemoteContent(source: SyncSource, preview: boolean): Promise { - await this.checkEnablement(); - for (const synchroniser of this.synchronisers) { - if (synchroniser.source === source) { - return synchroniser.getRemoteContent(preview); + async resolveContent(resource: URI): Promise { + const result = resolveSyncResource(resource); + if (result) { + const synchronizer = this.synchronisers.filter(s => s.resourceKey === result.resourceKey)[0]; + if (synchronizer) { + if (PREVIEW_QUERY === resource.query) { + return result.remote ? synchronizer.getRemoteContentFromPreview() : null; + } + return result.remote ? synchronizer.getRemoteContent(result.ref, resource.fragment) : synchronizer.getLocalBackupContent(result.ref, resource.fragment); } } return null; diff --git a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts index 3057e9052a5..091170c7abc 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, } from 'vs/base/common/lifecycle'; -import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, getUserDataSyncStore, SyncSource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest, ResourceKey } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, getUserDataSyncStore, SyncSource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest, ResourceKey, IResourceRefHandle } from 'vs/platform/userDataSync/common/userDataSync'; import { IRequestService, asText, isSuccess, asJson } from 'vs/platform/request/common/request'; import { joinPath, relativePath } from 'vs/base/common/resources'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -31,7 +31,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn this.userDataSyncStore = getUserDataSyncStore(productService, configurationService); } - async getAllRefs(key: ResourceKey): Promise { + async getAllRefs(key: ResourceKey): Promise { if (!this.userDataSyncStore) { throw new Error('No settings sync store url configured.'); } @@ -45,8 +45,8 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, undefined); } - const resources: string[] = await asJson(context) || []; - return resources.map(resource => relativePath(uri, URI.parse(resource))!); + const result = await asJson<{ url: string, created: number }[]>(context) || []; + return result.map(({ url, created }) => ({ ref: relativePath(uri, URI.parse(url))!, created: created })); } async resolveContent(key: ResourceKey, ref: string): Promise { diff --git a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts index 14c69ca7bc1..b2fa135d413 100644 --- a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts +++ b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts @@ -68,7 +68,7 @@ suite('TestSynchronizer', () => { teardown(() => disposableStore.clear()); test('status is syncing', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncSource.Settings); + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncSource.Settings, 'settings'); const actual: SyncStatus[] = []; disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); @@ -85,7 +85,7 @@ suite('TestSynchronizer', () => { }); test('status is set correctly when sync is finished', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncSource.Settings); + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncSource.Settings, 'settings'); testObject.syncBarrier.open(); const actual: SyncStatus[] = []; @@ -97,7 +97,7 @@ suite('TestSynchronizer', () => { }); test('status is set correctly when sync has conflicts', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncSource.Settings); + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncSource.Settings, 'settings'); testObject.syncResult = { status: SyncStatus.HasConflicts }; testObject.syncBarrier.open(); @@ -110,7 +110,7 @@ suite('TestSynchronizer', () => { }); test('status is set correctly when sync has errors', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncSource.Settings); + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncSource.Settings, 'settings'); testObject.syncResult = { error: true }; testObject.syncBarrier.open(); @@ -127,7 +127,7 @@ suite('TestSynchronizer', () => { }); test('sync should not run if syncing already', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncSource.Settings); + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncSource.Settings, 'settings'); const promise = Event.toPromise(testObject.onDoSyncCall.event); testObject.sync(); @@ -144,7 +144,7 @@ suite('TestSynchronizer', () => { }); test('sync should not run if disabled', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncSource.Settings); + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncSource.Settings, 'settings'); client.instantiationService.get(IUserDataSyncEnablementService).setResourceEnablement(testObject.resourceKey, false); const actual: SyncStatus[] = []; @@ -157,7 +157,7 @@ suite('TestSynchronizer', () => { }); test('sync should not run if there are conflicts', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncSource.Settings); + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncSource.Settings, 'settings'); testObject.syncResult = { status: SyncStatus.HasConflicts }; testObject.syncBarrier.open(); await testObject.sync(); @@ -171,7 +171,7 @@ suite('TestSynchronizer', () => { }); test('request latest data on precondition failure', async () => { - const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncSource.Settings); + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncSource.Settings, 'settings'); // Sync once testObject.syncBarrier.open(); await testObject.sync(); diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index b94d21ce4ed..181df5ae44f 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -6,7 +6,7 @@ import { IRequestService } from 'vs/platform/request/common/request'; import { IRequestOptions, IRequestContext, IHeaders } from 'vs/base/parts/request/common/request'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IUserData, ResourceKey, IUserDataManifest, ALL_RESOURCE_KEYS, IUserDataSyncLogService, IUserDataSyncStoreService, IUserDataSyncUtilService, IUserDataSyncEnablementService, ISettingsSyncService, IUserDataSyncService, getDefaultIgnoredSettings } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserData, ResourceKey, IUserDataManifest, ALL_RESOURCE_KEYS, IUserDataSyncLogService, IUserDataSyncStoreService, IUserDataSyncUtilService, IUserDataSyncEnablementService, ISettingsSyncService, IUserDataSyncService, getDefaultIgnoredSettings, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; import { generateUuid } from 'vs/base/common/uuid'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; @@ -36,6 +36,7 @@ import { Emitter } from 'vs/base/common/event'; import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication'; import product from 'vs/platform/product/common/product'; import { IProductService } from 'vs/platform/product/common/productService'; +import { UserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSyncBackupStoreService'; export class UserDataSyncClient extends Disposable { @@ -89,6 +90,7 @@ export class UserDataSyncClient extends Disposable { this.instantiationService.stub(IUserDataSyncLogService, logService); this.instantiationService.stub(ITelemetryService, NullTelemetryService); this.instantiationService.stub(IUserDataSyncStoreService, this.instantiationService.createInstance(UserDataSyncStoreService)); + this.instantiationService.stub(IUserDataSyncBackupStoreService, this.instantiationService.createInstance(UserDataSyncBackupStoreService)); this.instantiationService.stub(IUserDataSyncUtilService, new TestUserDataSyncUtilService()); this.instantiationService.stub(IUserDataSyncEnablementService, this.instantiationService.createInstance(UserDataSyncEnablementService)); diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 9e852eca701..6ba87f6803a 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -4975,10 +4975,11 @@ declare module 'vscode' { color: string | ThemeColor | undefined; /** - * The identifier of a command to run on click. The command must be - * [known](#commands.getCommands). + * [`Command`](#Command) or identifier of a command to run on click. + * + * The command must be [known](#commands.getCommands). */ - command: string | undefined; + command: string | Command | undefined; /** * Shows the entry in the status bar. diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 083c74124bd..b4c379c9227 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -35,7 +35,7 @@ declare module 'vscode' { readonly added: string[]; /** - * The ids of the [authenticationProvider](#AuthenticationProvider)s that have been removed.. + * The ids of the [authenticationProvider](#AuthenticationProvider)s that have been removed. */ readonly removed: string[]; } @@ -43,14 +43,14 @@ declare module 'vscode' { export interface AuthenticationProvider { /** * Used as an identifier for extensions trying to work with a particular - * provider: 'Microsoft', 'GitHub', etc. id must be unique, registering + * provider: 'microsoft', 'github', etc. id must be unique, registering * another provider with the same id will fail. */ readonly id: string; readonly displayName: string; /** - * A [enent](#Event) which fires when the array of sessions has changed, or data + * An [event](#Event) which fires when the array of sessions has changed, or data * within a session has changed. */ readonly onDidChangeSessions: Event; @@ -75,7 +75,31 @@ declare module 'vscode' { */ export const onDidChangeAuthenticationProviders: Event; - export const providers: ReadonlyArray; + /** + * Returns whether a provider with providerId is currently registered. + */ + export function hasProvider(providerId: string): boolean; + + /** + * Get existing authentication sessions. Rejects if a provider with providerId is not + * registered, or if the user does not consent to sharing authentication information with + * the extension. + */ + export function getSessions(providerId: string, scopes: string[]): Thenable; + + /** + * Prompt a user to login to create a new authenticaiton session. Rejects if a provider with + * providerId is not registered, or if the user does not consent to sharing authentication + * information with the extension. + */ + export function login(providerId: string, scopes: string[]): Thenable; + + /** + * An [event](#Event) which fires when the array of sessions has changed, or data + * within a session has changed for a provider. Fires with the ids of the providers + * that have had session data change. + */ + export const onDidChangeSessions: Event; } //#endregion @@ -1745,4 +1769,18 @@ declare module 'vscode' { } //#endregion + + //#region https://github.com/microsoft/vscode/issues/90517 + + export interface FileSystemError { + /** + * A code that identifies this error. + * + * Possible values are names of errors, like [`FileNotFound`](#FileSystemError.FileNotFound), + * or `undefined` for an unspecified error. + */ + readonly code?: string; + } + + ////#endregion } diff --git a/src/vs/workbench/api/browser/mainThreadStatusBar.ts b/src/vs/workbench/api/browser/mainThreadStatusBar.ts index 91a9ba1419f..fb733a38228 100644 --- a/src/vs/workbench/api/browser/mainThreadStatusBar.ts +++ b/src/vs/workbench/api/browser/mainThreadStatusBar.ts @@ -8,6 +8,7 @@ import { MainThreadStatusBarShape, MainContext, IExtHostContext } from '../commo import { ThemeColor } from 'vs/platform/theme/common/themeService'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { dispose } from 'vs/base/common/lifecycle'; +import { Command } from 'vs/editor/common/modes'; @extHostNamedCustomer(MainContext.MainThreadStatusBar) export class MainThreadStatusBar implements MainThreadStatusBarShape { @@ -24,7 +25,7 @@ export class MainThreadStatusBar implements MainThreadStatusBarShape { this.entries.clear(); } - $setEntry(id: number, statusId: string, statusName: string, text: string, tooltip: string | undefined, command: string | undefined, color: string | ThemeColor | undefined, alignment: MainThreadStatusBarAlignment, priority: number | undefined): void { + $setEntry(id: number, statusId: string, statusName: string, text: string, tooltip: string | undefined, command: Command | undefined, color: string | ThemeColor | undefined, alignment: MainThreadStatusBarAlignment, priority: number | undefined): void { const entry: IStatusbarEntry = { text, tooltip, command, color }; if (typeof priority === 'undefined') { diff --git a/src/vs/workbench/api/browser/mainThreadTask.ts b/src/vs/workbench/api/browser/mainThreadTask.ts index ede1ae5c393..30fd88fcb13 100644 --- a/src/vs/workbench/api/browser/mainThreadTask.ts +++ b/src/vs/workbench/api/browser/mainThreadTask.ts @@ -33,6 +33,7 @@ import { RunOptionsDTO } from 'vs/workbench/api/common/shared/tasks'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; +import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; namespace TaskExecutionDTO { export function from(value: TaskExecution): TaskExecutionDTO { @@ -604,7 +605,7 @@ export class MainThreadTask implements MainThreadTaskShape { return URI.parse(`${info.scheme}://${info.authority}${path}`); }, context: this._extHostContext, - resolveVariables: (workspaceFolder: IWorkspaceFolder, toResolve: ResolveSet): Promise => { + resolveVariables: (workspaceFolder: IWorkspaceFolder, toResolve: ResolveSet, target: ConfigurationTarget): Promise => { const vars: string[] = []; toResolve.variables.forEach(item => vars.push(item)); return Promise.resolve(this._proxy.$resolveVariables(workspaceFolder.uri, { process: toResolve.process, variables: vars })).then(values => { @@ -613,7 +614,7 @@ export class MainThreadTask implements MainThreadTaskShape { partiallyResolvedVars.push(entry.value); }); return new Promise((resolve, reject) => { - this._configurationResolverService.resolveWithInteraction(workspaceFolder, partiallyResolvedVars, 'tasks').then(resolvedVars => { + this._configurationResolverService.resolveWithInteraction(workspaceFolder, partiallyResolvedVars, 'tasks', undefined, target).then(resolvedVars => { const result: ResolvedVariables = { process: undefined, variables: new Map() diff --git a/src/vs/workbench/api/browser/viewsExtensionPoint.ts b/src/vs/workbench/api/browser/viewsExtensionPoint.ts index 67e90081285..8375e443d49 100644 --- a/src/vs/workbench/api/browser/viewsExtensionPoint.ts +++ b/src/vs/workbench/api/browser/viewsExtensionPoint.ts @@ -9,7 +9,7 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema'; import * as resources from 'vs/base/common/resources'; import { ExtensionMessageCollector, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { ViewContainer, IViewsRegistry, ITreeViewDescriptor, IViewContainersRegistry, Extensions as ViewContainerExtensions, TEST_VIEW_CONTAINER_ID, IViewDescriptor, ViewContainerLocation } from 'vs/workbench/common/views'; -import { CustomTreeViewPane, CustomTreeView } from 'vs/workbench/browser/parts/views/customView'; +import { TreeViewPane, CustomTreeView } from 'vs/workbench/browser/parts/views/treeView'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { coalesce, } from 'vs/base/common/arrays'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -396,7 +396,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution { const viewDescriptor = { id: item.id, name: item.name, - ctorDescriptor: new SyncDescriptor(CustomTreeViewPane), + ctorDescriptor: new SyncDescriptor(TreeViewPane), when: ContextKeyExpr.deserialize(item.when), canToggleVisibility: true, canMoveView: true, diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index c57fae94bbd..39f0534293c 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -143,7 +143,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostClipboard = new ExtHostClipboard(rpcProtocol); const extHostMessageService = new ExtHostMessageService(rpcProtocol, extHostLogService); const extHostDialogs = new ExtHostDialogs(rpcProtocol); - const extHostStatusBar = new ExtHostStatusBar(rpcProtocol); + const extHostStatusBar = new ExtHostStatusBar(rpcProtocol, extHostCommands.converter); const extHostLanguages = new ExtHostLanguages(rpcProtocol, extHostDocuments); // Register API-ish commands @@ -185,12 +185,21 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I registerAuthenticationProvider(provider: vscode.AuthenticationProvider): vscode.Disposable { return extHostAuthentication.registerAuthenticationProvider(provider); }, - get providers() { - return extHostAuthentication.providers(extension); - }, get onDidChangeAuthenticationProviders(): Event { return extHostAuthentication.onDidChangeAuthenticationProviders; - } + }, + hasProvider(providerId: string): boolean { + return extHostAuthentication.hasProvider(providerId); + }, + getSessions(providerId: string, scopes: string[]): Thenable { + return extHostAuthentication.getSessions(extension, providerId, scopes); + }, + login(providerId: string, scopes: string[]): Thenable { + return extHostAuthentication.login(extension, providerId, scopes); + }, + get onDidChangeSessions(): Event { + return extHostAuthentication.onDidChangeSessions; + }, }; // namespace: commands diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index a21449c9036..ec69c297aa0 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -533,7 +533,7 @@ export interface MainThreadQuickOpenShape extends IDisposable { } export interface MainThreadStatusBarShape extends IDisposable { - $setEntry(id: number, statusId: string, statusName: string, text: string, tooltip: string | undefined, command: string | undefined, color: string | ThemeColor | undefined, alignment: statusbar.StatusbarAlignment, priority: number | undefined): void; + $setEntry(id: number, statusId: string, statusName: string, text: string, tooltip: string | undefined, command: ICommandDto | undefined, color: string | ThemeColor | undefined, alignment: statusbar.StatusbarAlignment, priority: number | undefined): void; $dispose(id: number): void; } diff --git a/src/vs/workbench/api/common/extHostAuthentication.ts b/src/vs/workbench/api/common/extHostAuthentication.ts index b3b2d4d0f4a..baa486b5c5c 100644 --- a/src/vs/workbench/api/common/extHostAuthentication.ts +++ b/src/vs/workbench/api/common/extHostAuthentication.ts @@ -10,61 +10,6 @@ import { IMainContext, MainContext, MainThreadAuthenticationShape, ExtHostAuthen import { Disposable } from 'vs/workbench/api/common/extHostTypes'; import { IExtensionDescription, ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -export class AuthenticationProviderWrapper implements vscode.AuthenticationProvider { - readonly onDidChangeSessions: vscode.Event; - - constructor(private _requestingExtension: IExtensionDescription, - private _provider: vscode.AuthenticationProvider, - private _proxy: MainThreadAuthenticationShape) { - - this.onDidChangeSessions = this._provider.onDidChangeSessions; - } - - get id(): string { - return this._provider.id; - } - - get displayName(): string { - return this._provider.displayName; - } - - async getSessions(): Promise> { - return (await this._provider.getSessions()).map(session => { - return { - id: session.id, - accountName: session.accountName, - scopes: session.scopes, - getAccessToken: async () => { - const isAllowed = await this._proxy.$getSessionsPrompt( - this._provider.id, - this.displayName, - ExtensionIdentifier.toKey(this._requestingExtension.identifier), - this._requestingExtension.displayName || this._requestingExtension.name); - - if (!isAllowed) { - throw new Error('User did not consent to token access.'); - } - - return session.getAccessToken(); - } - }; - }); - } - - async login(scopes: string[]): Promise { - const isAllowed = await this._proxy.$loginPrompt(this._provider.id, this.displayName, ExtensionIdentifier.toKey(this._requestingExtension.identifier), this._requestingExtension.displayName || this._requestingExtension.name); - if (!isAllowed) { - throw new Error('User did not consent to login.'); - } - - return this._provider.login(scopes); - } - - logout(sessionId: string): Thenable { - return this._provider.logout(sessionId); - } -} - export class ExtHostAuthentication implements ExtHostAuthenticationShape { private _proxy: MainThreadAuthenticationShape; private _authenticationProviders: Map = new Map(); @@ -72,14 +17,60 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { private _onDidChangeAuthenticationProviders = new Emitter(); readonly onDidChangeAuthenticationProviders: Event = this._onDidChangeAuthenticationProviders.event; + private _onDidChangeSessions = new Emitter(); + readonly onDidChangeSessions: Event = this._onDidChangeSessions.event; + constructor(mainContext: IMainContext) { this._proxy = mainContext.getProxy(MainContext.MainThreadAuthentication); } - providers(requestingExtension: IExtensionDescription): vscode.AuthenticationProvider[] { - let providers: vscode.AuthenticationProvider[] = []; - this._authenticationProviders.forEach(provider => providers.push(new AuthenticationProviderWrapper(requestingExtension, provider, this._proxy))); - return providers; + hasProvider(providerId: string): boolean { + return !!this._authenticationProviders.get(providerId); + } + + async getSessions(requestingExtension: IExtensionDescription, providerId: string, scopes: string[]): Promise { + const provider = this._authenticationProviders.get(providerId); + if (!provider) { + throw new Error(`No authentication provider with id '${providerId}' is currently registered.`); + } + + const orderedScopes = scopes.sort().join(' '); + return (await provider.getSessions()) + .filter(session => session.scopes.sort().join(' ') === orderedScopes) + .map(session => { + return { + id: session.id, + accountName: session.accountName, + scopes: session.scopes, + getAccessToken: async () => { + const isAllowed = await this._proxy.$getSessionsPrompt( + provider.id, + provider.displayName, + ExtensionIdentifier.toKey(requestingExtension.identifier), + requestingExtension.displayName || requestingExtension.name); + + if (!isAllowed) { + throw new Error('User did not consent to token access.'); + } + + return session.getAccessToken(); + } + }; + }); + } + + async login(requestingExtension: IExtensionDescription, providerId: string, scopes: string[]): Promise { + const provider = this._authenticationProviders.get(providerId); + if (!provider) { + throw new Error(`No authentication provider with id '${providerId}' is currently registered.`); + } + + const isAllowed = await this._proxy.$loginPrompt(provider.id, provider.displayName, ExtensionIdentifier.toKey(requestingExtension.identifier), requestingExtension.displayName || requestingExtension.name); + if (!isAllowed) { + throw new Error('User did not consent to login.'); + } + + return provider.login(scopes); } registerAuthenticationProvider(provider: vscode.AuthenticationProvider): vscode.Disposable { @@ -91,6 +82,7 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { const listener = provider.onDidChangeSessions(_ => { this._proxy.$onDidChangeSessions(provider.id); + this._onDidChangeSessions.fire([provider.id]); }); this._proxy.$registerAuthenticationProvider(provider.id, provider.displayName); diff --git a/src/vs/workbench/api/common/extHostExtensionActivator.ts b/src/vs/workbench/api/common/extHostExtensionActivator.ts index 3a076946edd..7e86c340c1d 100644 --- a/src/vs/workbench/api/common/extHostExtensionActivator.ts +++ b/src/vs/workbench/api/common/extHostExtensionActivator.ts @@ -252,7 +252,15 @@ export class ExtensionsActivator { return; } - const currentExtension = this._registry.getExtensionDescription(currentActivation.id)!; + const currentExtension = this._registry.getExtensionDescription(currentActivation.id); + if (!currentExtension) { + // Error condition 0: unknown extension + this._host.onExtensionActivationError(currentActivation.id, new MissingDependencyError(currentActivation.id.value)); + const error = new Error(`Unknown dependency '${currentActivation.id.value}'`); + this._activatedExtensions.set(ExtensionIdentifier.toKey(currentActivation.id), new FailedExtension(error)); + return; + } + const depIds = (typeof currentExtension.extensionDependencies === 'undefined' ? [] : currentExtension.extensionDependencies); let currentExtensionGetsGreenLight = true; diff --git a/src/vs/workbench/api/common/extHostFileSystem.ts b/src/vs/workbench/api/common/extHostFileSystem.ts index a721785d28c..ca30ef0d660 100644 --- a/src/vs/workbench/api/common/extHostFileSystem.ts +++ b/src/vs/workbench/api/common/extHostFileSystem.ts @@ -148,7 +148,16 @@ class ConsumerFileSystem implements vscode.FileSystem { } // file system error - throw new FileSystemError(err.message, err.name as files.FileSystemProviderErrorCode); + switch (err.name) { + case files.FileSystemProviderErrorCode.FileExists: throw FileSystemError.FileExists(err.message); + case files.FileSystemProviderErrorCode.FileNotFound: throw FileSystemError.FileNotFound(err.message); + case files.FileSystemProviderErrorCode.FileNotADirectory: throw FileSystemError.FileNotADirectory(err.message); + case files.FileSystemProviderErrorCode.FileIsADirectory: throw FileSystemError.FileIsADirectory(err.message); + case files.FileSystemProviderErrorCode.NoPermissions: throw FileSystemError.NoPermissions(err.message); + case files.FileSystemProviderErrorCode.Unavailable: throw FileSystemError.Unavailable(err.message); + + default: throw new FileSystemError(err.message, err.name as files.FileSystemProviderErrorCode); + } } } diff --git a/src/vs/workbench/api/common/extHostStatusBar.ts b/src/vs/workbench/api/common/extHostStatusBar.ts index 02c89a6e89e..d26ee3d7c86 100644 --- a/src/vs/workbench/api/common/extHostStatusBar.ts +++ b/src/vs/workbench/api/common/extHostStatusBar.ts @@ -5,11 +5,13 @@ import { StatusbarAlignment as MainThreadStatusBarAlignment } from 'vs/workbench/services/statusbar/common/statusbar'; import { StatusBarAlignment as ExtHostStatusBarAlignment, Disposable, ThemeColor } from './extHostTypes'; -import { StatusBarItem, StatusBarAlignment } from 'vscode'; -import { MainContext, MainThreadStatusBarShape, IMainContext } from './extHost.protocol'; +import type * as vscode from 'vscode'; +import { MainContext, MainThreadStatusBarShape, IMainContext, ICommandDto } from './extHost.protocol'; import { localize } from 'vs/nls'; +import { CommandsConverter } from 'vs/workbench/api/common/extHostCommands'; +import { DisposableStore } from 'vs/base/common/lifecycle'; -export class ExtHostStatusBarEntry implements StatusBarItem { +export class ExtHostStatusBarEntry implements vscode.StatusBarItem { private static ID_GEN = 0; private _id: number; @@ -24,14 +26,20 @@ export class ExtHostStatusBarEntry implements StatusBarItem { private _text: string = ''; private _tooltip?: string; private _color?: string | ThemeColor; - private _command?: string; + private readonly _internalCommandRegistration = new DisposableStore(); + private _command?: { + readonly fromApi: string | vscode.Command, + readonly internal: ICommandDto, + }; private _timeoutHandle: any; private _proxy: MainThreadStatusBarShape; + private _commands: CommandsConverter; - constructor(proxy: MainThreadStatusBarShape, id: string, name: string, alignment: ExtHostStatusBarAlignment = ExtHostStatusBarAlignment.Left, priority?: number) { + constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, id: string, name: string, alignment: ExtHostStatusBarAlignment = ExtHostStatusBarAlignment.Left, priority?: number) { this._id = ExtHostStatusBarEntry.ID_GEN++; this._proxy = proxy; + this._commands = commands; this._statusId = id; this._statusName = name; this._alignment = alignment; @@ -42,7 +50,7 @@ export class ExtHostStatusBarEntry implements StatusBarItem { return this._id; } - public get alignment(): StatusBarAlignment { + public get alignment(): vscode.StatusBarAlignment { return this._alignment; } @@ -62,8 +70,8 @@ export class ExtHostStatusBarEntry implements StatusBarItem { return this._color; } - public get command(): string | undefined { - return this._command; + public get command(): string | vscode.Command | undefined { + return this._command?.fromApi; } public set text(text: string) { @@ -81,8 +89,25 @@ export class ExtHostStatusBarEntry implements StatusBarItem { this.update(); } - public set command(command: string | undefined) { - this._command = command; + public set command(command: string | vscode.Command | undefined) { + if (this._command?.fromApi === command) { + return; + } + + this._internalCommandRegistration.clear(); + if (typeof command === 'string') { + this._command = { + fromApi: command, + internal: this._commands.toInternal({ title: '', command }, this._internalCommandRegistration), + }; + } else if (command) { + this._command = { + fromApi: command, + internal: this._commands.toInternal(command, this._internalCommandRegistration), + }; + } else { + this._command = undefined; + } this.update(); } @@ -109,7 +134,7 @@ export class ExtHostStatusBarEntry implements StatusBarItem { this._timeoutHandle = undefined; // Set to status bar - this._proxy.$setEntry(this.id, this._statusId, this._statusName, this.text, this.tooltip, this.command, this.color, + this._proxy.$setEntry(this.id, this._statusId, this._statusName, this.text, this.tooltip, this._command?.internal, this.color, this._alignment === ExtHostStatusBarAlignment.Left ? MainThreadStatusBarAlignment.LEFT : MainThreadStatusBarAlignment.RIGHT, this._priority); }, 0); @@ -123,7 +148,7 @@ export class ExtHostStatusBarEntry implements StatusBarItem { class StatusBarMessage { - private _item: StatusBarItem; + private _item: vscode.StatusBarItem; private _messages: { message: string }[] = []; constructor(statusBar: ExtHostStatusBar) { @@ -161,16 +186,18 @@ class StatusBarMessage { export class ExtHostStatusBar { - private _proxy: MainThreadStatusBarShape; + private readonly _proxy: MainThreadStatusBarShape; + private readonly _commands: CommandsConverter; private _statusMessage: StatusBarMessage; - constructor(mainContext: IMainContext) { + constructor(mainContext: IMainContext, commands: CommandsConverter) { this._proxy = mainContext.getProxy(MainContext.MainThreadStatusBar); + this._commands = commands; this._statusMessage = new StatusBarMessage(this); } - createStatusBarEntry(id: string, name: string, alignment?: ExtHostStatusBarAlignment, priority?: number): StatusBarItem { - return new ExtHostStatusBarEntry(this._proxy, id, name, alignment, priority); + createStatusBarEntry(id: string, name: string, alignment?: ExtHostStatusBarAlignment, priority?: number): vscode.StatusBarItem { + return new ExtHostStatusBarEntry(this._proxy, this._commands, id, name, alignment, priority); } setStatusBarMessage(text: string, timeoutOrThenable?: number | Thenable): Disposable { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index e7810d7b6a3..0c62c89ad01 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2333,9 +2333,13 @@ export class FileSystemError extends Error { return new FileSystemError(messageOrUri, FileSystemProviderErrorCode.Unavailable, FileSystemError.Unavailable); } + readonly code?: string; + constructor(uriOrMessage?: string | URI, code: FileSystemProviderErrorCode = FileSystemProviderErrorCode.Unknown, terminator?: Function) { super(URI.isUri(uriOrMessage) ? uriOrMessage.toString(true) : uriOrMessage); + this.code = terminator?.name; + // mark the error as file system provider error so that // we can extract the error code on the receiving side markAsFileSystemProviderError(this, code); diff --git a/src/vs/workbench/api/common/jsonValidationExtensionPoint.ts b/src/vs/workbench/api/common/jsonValidationExtensionPoint.ts index c3f09c4cdb1..dd2ad38b467 100644 --- a/src/vs/workbench/api/common/jsonValidationExtensionPoint.ts +++ b/src/vs/workbench/api/common/jsonValidationExtensionPoint.ts @@ -7,9 +7,10 @@ import * as nls from 'vs/nls'; import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import * as strings from 'vs/base/common/strings'; import * as resources from 'vs/base/common/resources'; +import { isString } from 'vs/base/common/types'; interface IJSONValidationExtensionPoint { - fileMatch: string; + fileMatch: string | string[]; url: string; } @@ -25,8 +26,11 @@ const configurationExtPoint = ExtensionsRegistry.registerExtensionPoint { - if (typeof extension.fileMatch !== 'string') { - collector.error(nls.localize('invalid.fileMatch', "'configuration.jsonValidation.fileMatch' must be defined")); + if (!isString(extension.fileMatch) && !(Array.isArray(extension.fileMatch) && extension.fileMatch.every(isString))) { + collector.error(nls.localize('invalid.fileMatch', "'configuration.jsonValidation.fileMatch' must be defined as a string or an array of strings.")); return; } let uri = extension.url; - if (typeof extension.url !== 'string') { + if (!isString(uri)) { collector.error(nls.localize('invalid.url', "'configuration.jsonValidation.url' must be a URL or relative path")); return; } diff --git a/src/vs/workbench/browser/media/style.css b/src/vs/workbench/browser/media/style.css index 72c43f410d8..3b8797b5666 100644 --- a/src/vs/workbench/browser/media/style.css +++ b/src/vs/workbench/browser/media/style.css @@ -23,9 +23,9 @@ .linux:lang(ja) { font-family: "Ubuntu", "Droid Sans", "Source Han Sans J", "Source Han Sans JP", "Source Han Sans", sans-serif; } .linux:lang(ko) { font-family: "Ubuntu", "Droid Sans", "Source Han Sans K", "Source Han Sans JR", "Source Han Sans", "UnDotum", "FBaekmuk Gulim", sans-serif; } -.mac { --monaco-monospace-font: "SF Mono", Monaco, Menlo, Inconsolata, "Courier New", monospace; } -.windows { --monaco-monospace-font: Consolas, Inconsolata, "Courier New", monospace; } -.linux { --monaco-monospace-font: "Droid Sans Mono", Inconsolata, "Courier New", monospace, "Droid Sans Fallback"; } +.mac { --monaco-monospace-font: "SF Mono", Monaco, Menlo, Courier, monospace; } +.windows { --monaco-monospace-font: Consolas, "Courier New", monospace; } +.linux { --monaco-monospace-font: "Ubuntu Mono", "Liberation Mono", "DejaVu Sans Mono", "Courier New", monospace; } /* Global Styles */ diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index f5fff985bed..bc2a05ab0cf 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -56,6 +56,8 @@ import { IFilesConfigurationService } from 'vs/workbench/services/filesConfigura import { EditorAutoSave } from 'vs/workbench/browser/parts/editor/editorAutoSave'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; +import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from 'vs/platform/quickinput/common/quickAccess'; +import { ActiveGroupEditorsByMostRecentlyUsedQuickAccess, AllEditorsByAppearanceQuickAccess, AllEditorsByMostRecentlyUsedQuickAccess } from 'vs/workbench/browser/parts/editor/editorQuickAccess'; // Register String Editor Registry.as(EditorExtensions.Editors).registerEditor( @@ -359,6 +361,33 @@ Registry.as(QuickOpenExtensions.Quickopen).registerQuickOpen ) ); +// Register Editor Quick Access +const quickAccessRegistry = Registry.as(QuickAccessExtensions.Quickaccess); + +quickAccessRegistry.registerQuickAccessProvider({ + ctor: ActiveGroupEditorsByMostRecentlyUsedQuickAccess, + prefix: ActiveGroupEditorsByMostRecentlyUsedQuickAccess.PREFIX, + contextKey: editorPickerContextKey, + placeholder: nls.localize('editorQuickAccessPlaceholder', "Type the name of an editor to open it."), + helpEntries: [{ description: nls.localize('activeGroupEditorsByMostRecentlyUsedQuickAccess', "Show Editors in Active Group by Most Recently Used."), needsEditor: false }] +}); + +quickAccessRegistry.registerQuickAccessProvider({ + ctor: AllEditorsByAppearanceQuickAccess, + prefix: AllEditorsByAppearanceQuickAccess.PREFIX, + contextKey: editorPickerContextKey, + placeholder: nls.localize('editorQuickAccessPlaceholder', "Type the name of an editor to open it."), + helpEntries: [{ description: nls.localize('allEditorsByAppearanceQuickAccess', "Show All Opened Editors By Appearance"), needsEditor: false }] +}); + +quickAccessRegistry.registerQuickAccessProvider({ + ctor: AllEditorsByMostRecentlyUsedQuickAccess, + prefix: AllEditorsByMostRecentlyUsedQuickAccess.PREFIX, + contextKey: editorPickerContextKey, + placeholder: nls.localize('editorQuickAccessPlaceholder', "Type the name of an editor to open it."), + helpEntries: [{ description: nls.localize('allEditorsByMostRecentlyUsedQuickAccess', "Show All Opened Editors By Most Recently Used"), needsEditor: false }] +}); + // Register Editor Actions const category = nls.localize('view', "View"); registry.registerWorkbenchAction(SyncActionDescriptor.create(OpenNextEditor, OpenNextEditor.ID, OpenNextEditor.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.PageDown, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.RightArrow, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_CLOSE_SQUARE_BRACKET] } }), 'View: Open Next Editor', category); diff --git a/src/vs/workbench/browser/parts/editor/editorPicker.ts b/src/vs/workbench/browser/parts/editor/editorPicker.ts index 427a7bce430..10f2a7bc04e 100644 --- a/src/vs/workbench/browser/parts/editor/editorPicker.ts +++ b/src/vs/workbench/browser/parts/editor/editorPicker.ts @@ -16,7 +16,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IEditorGroupsService, IEditorGroup, GroupsOrder } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { toResource, SideBySideEditor, IEditorInput, EditorsOrder } from 'vs/workbench/common/editor'; -import { compareItemsByScore, scoreItem, ScorerCache, prepareQuery } from 'vs/base/parts/quickopen/common/quickOpenScorer'; +import { compareItemsByScore, scoreItem, ScorerCache, prepareQuery } from 'vs/base/common/fuzzyScorer'; import { CancellationToken } from 'vs/base/common/cancellation'; export class EditorPickerEntry extends QuickOpenEntryGroup { diff --git a/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts b/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts new file mode 100644 index 00000000000..8a8ae3937ae --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/editorQuickAccess.ts @@ -0,0 +1,205 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { IQuickPick, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { IQuickAccessProvider } from 'vs/platform/quickinput/common/quickAccess'; +import { IEditorGroupsService, GroupsOrder } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { EditorsOrder, IEditorIdentifier, toResource, SideBySideEditor } from 'vs/workbench/common/editor'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { getIconClasses } from 'vs/editor/common/services/getIconClasses'; +import { prepareQuery, IPreparedQuery, ScorerCache, scoreItem, compareItemsByScore, IItemAccessor } from 'vs/base/common/fuzzyScorer'; +import { Schemas } from 'vs/base/common/network'; + +interface IEditorQuickPickItem extends IQuickPickItem, IEditorIdentifier { } + +export abstract class BaseEditorQuickAccessProvider implements IQuickAccessProvider { + + protected abstract readonly prefix: string; + + private editorQuickPickScoringAccessor = new class implements IItemAccessor { + getItemLabel(entry: IEditorQuickPickItem): string | undefined { + return entry.label; + } + + getItemDescription(entry: IEditorQuickPickItem): string | undefined { + return entry.description; + } + + getItemPath(entry: IEditorQuickPickItem): string | undefined { + const resource = toResource(entry.editor, { supportSideBySide: SideBySideEditor.MASTER }); + if (resource?.scheme === Schemas.file) { + return resource?.fsPath; + } + + return resource?.path; + } + }; + + constructor( + @IEditorGroupsService protected readonly editorGroupService: IEditorGroupsService, + @IEditorService protected readonly editorService: IEditorService, + @IModelService private readonly modelService: IModelService, + @IModeService private readonly modeService: IModeService + ) { + } + + provide(picker: IQuickPick, token: CancellationToken): IDisposable { + const disposables = new DisposableStore(); + + // Disable filtering & sorting, we control the results + picker.matchOnLabel = picker.matchOnDescription = picker.matchOnDetail = picker.sortByLabel = false; + + // Add all view items & filter on type + const scorerCache = Object.create(null); + const updatePickerItems = () => picker.items = this.getEditorPickItems(prepareQuery(picker.value.trim().substr(this.prefix.length)), scorerCache); + disposables.add(picker.onDidChangeValue(() => updatePickerItems())); + updatePickerItems(); + + // Open the picked view on accept + disposables.add(picker.onDidAccept(() => { + const [item] = picker.selectedItems; + if (item) { + picker.hide(); + this.editorGroupService.getGroup(item.groupId)?.openEditor(item.editor); + } + })); + + return disposables; + } + + private getEditorPickItems(query: IPreparedQuery, scorerCache: ScorerCache): Array { + const filteredEditorEntries = this.doGetEditorPickItems().filter(entry => { + if (!query.value) { + return true; + } + + // Score on label and description + const itemScore = scoreItem(entry, query, true, this.editorQuickPickScoringAccessor, scorerCache); + if (!itemScore.score) { + return false; + } + + // Apply highlights + entry.highlights = { label: itemScore.labelMatch, description: itemScore.descriptionMatch }; + + return true; + }); + + // Sorting + if (query.value) { + const groups = this.editorGroupService.getGroups(GroupsOrder.GRID_APPEARANCE).map(group => group.id); + filteredEditorEntries.sort((entryA, entryB) => { + if (entryA.groupId !== entryB.groupId) { + return groups.indexOf(entryA.groupId) - groups.indexOf(entryB.groupId); // older groups first + } + + return compareItemsByScore(entryA, entryB, query, true, this.editorQuickPickScoringAccessor, scorerCache); + }); + } + + // Grouping (for more than one group) + const filteredEditorEntriesWithSeparators: Array = []; + if (this.editorGroupService.count > 1) { + let lastGroupId: number | undefined = undefined; + for (const entry of filteredEditorEntries) { + if (typeof lastGroupId !== 'number' || lastGroupId !== entry.groupId) { + const group = this.editorGroupService.getGroup(entry.groupId); + if (group) { + filteredEditorEntriesWithSeparators.push({ type: 'separator', label: group.label }); + } + lastGroupId = entry.groupId; + } + + filteredEditorEntriesWithSeparators.push(entry); + } + } else { + filteredEditorEntriesWithSeparators.push(...filteredEditorEntries); + } + + return filteredEditorEntriesWithSeparators; + } + + private doGetEditorPickItems(): Array { + return this.doGetEditors().map(({ editor, groupId }) => ({ + editor, + groupId, + label: editor.isDirty() && !editor.isSaving() ? `$(circle-filled) ${editor.getName()}` : editor.getName(), + ariaLabel: localize('entryAriaLabel', "{0}, editor picker", editor.getName()), + description: editor.getDescription(), + iconClasses: getIconClasses(this.modelService, this.modeService, toResource(editor, { supportSideBySide: SideBySideEditor.MASTER })), + italic: !this.editorGroupService.getGroup(groupId)?.isPinned(editor) + })); + } + + protected abstract doGetEditors(): IEditorIdentifier[]; +} + +//#region Active Editor Group Editors by Most Recently Used + +export class ActiveGroupEditorsByMostRecentlyUsedQuickAccess extends BaseEditorQuickAccessProvider { + + static PREFIX = 'edt active '; + + readonly prefix = ActiveGroupEditorsByMostRecentlyUsedQuickAccess.PREFIX; + + protected doGetEditors(): IEditorIdentifier[] { + const group = this.editorGroupService.activeGroup; + + return group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).map(editor => ({ editor, groupId: group.id })); + } +} + +//#endregion + + +//#region All Editors by Appearance + +export class AllEditorsByAppearanceQuickAccess extends BaseEditorQuickAccessProvider { + + static PREFIX = 'edt '; + + readonly prefix = AllEditorsByAppearanceQuickAccess.PREFIX; + + protected doGetEditors(): IEditorIdentifier[] { + const entries: IEditorIdentifier[] = []; + + for (const group of this.editorGroupService.getGroups(GroupsOrder.GRID_APPEARANCE)) { + for (const editor of group.getEditors(EditorsOrder.SEQUENTIAL)) { + entries.push({ editor, groupId: group.id }); + } + } + + return entries; + } +} + +//#endregion + + +//#region All Editors by Most Recently Used + +export class AllEditorsByMostRecentlyUsedQuickAccess extends BaseEditorQuickAccessProvider { + + static PREFIX = 'edt mru '; + + readonly prefix = AllEditorsByMostRecentlyUsedQuickAccess.PREFIX; + + protected doGetEditors(): IEditorIdentifier[] { + const entries: IEditorIdentifier[] = []; + + for (const editor of this.editorService.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)) { + entries.push(editor); + } + + return entries; + } +} + +//#endregion diff --git a/src/vs/workbench/browser/parts/editor/rangeDecorations.ts b/src/vs/workbench/browser/parts/editor/rangeDecorations.ts index 8c45a102a63..98304f2366b 100644 --- a/src/vs/workbench/browser/parts/editor/rangeDecorations.ts +++ b/src/vs/workbench/browser/parts/editor/rangeDecorations.ts @@ -10,7 +10,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IRange } from 'vs/editor/common/core/range'; import { CursorChangeReason, ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { TrackedRangeStickiness, IModelDecorationsChangeAccessor } from 'vs/editor/common/model'; export interface IRangeHighlightDecoration { @@ -41,9 +41,9 @@ export class RangeHighlightDecorations extends Disposable { this.rangeHighlightDecorationId = null; } - highlightRange(range: IRangeHighlightDecoration, editor?: ICodeEditor) { + highlightRange(range: IRangeHighlightDecoration, editor?: any) { editor = editor ? editor : this.getEditor(range); - if (editor) { + if (isCodeEditor(editor)) { this.doHighlightRange(editor, range); } } diff --git a/src/vs/workbench/browser/parts/panel/panelActions.ts b/src/vs/workbench/browser/parts/panel/panelActions.ts index 7cb981e9583..1501c87bcce 100644 --- a/src/vs/workbench/browser/parts/panel/panelActions.ts +++ b/src/vs/workbench/browser/parts/panel/panelActions.ts @@ -74,7 +74,6 @@ class FocusPanelAction extends Action { // Show panel if (!this.layoutService.isVisible(Parts.PANEL_PART)) { this.layoutService.setPanelHidden(false); - return; } // Focus into active panel diff --git a/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts b/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts index 2cb29536098..9da034ae867 100644 --- a/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts +++ b/src/vs/workbench/browser/parts/quickopen/quickOpenController.ts @@ -38,7 +38,7 @@ import { quickInputBackground, quickInputForeground } from 'vs/platform/theme/co import { attachQuickOpenStyler } from 'vs/platform/theme/common/styler'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IFileService } from 'vs/platform/files/common/files'; -import { scoreItem, ScorerCache, compareItemsByScore, prepareQuery } from 'vs/base/parts/quickopen/common/quickOpenScorer'; +import { scoreItem, ScorerCache, compareItemsByScore, prepareQuery } from 'vs/base/common/fuzzyScorer'; import { WorkbenchTree } from 'vs/platform/list/browser/listService'; import { Schemas } from 'vs/base/common/network'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -707,8 +707,8 @@ class EditorHistoryItemAccessorClass extends QuickOpenItemAccessorClass { super(); } - getItemDescription(entry: QuickOpenEntry): string | null { - return this.allowMatchOnDescription ? types.withUndefinedAsNull(entry.getDescription()) : null; + getItemDescription(entry: QuickOpenEntry): string | undefined { + return this.allowMatchOnDescription ? entry.getDescription() : undefined; } } diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index 4d140afcf29..b3a0df2c36b 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -34,6 +34,7 @@ import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { values } from 'vs/base/common/map'; import { assertIsDefined } from 'vs/base/common/types'; import { Emitter, Event } from 'vs/base/common/event'; +import { Command } from 'vs/editor/common/modes'; interface IPendingStatusbarEntry { id: string; @@ -702,7 +703,7 @@ class StatusbarEntryItem extends Disposable { const command = entry.command; if (command) { - this.commandListener.value = addDisposableListener(this.labelContainer, EventType.CLICK, () => this.executeCommand(command, entry.arguments)); + this.commandListener.value = addDisposableListener(this.labelContainer, EventType.CLICK, () => this.executeCommand(command)); removeClass(this.labelContainer, 'disabled'); } else { @@ -738,8 +739,9 @@ class StatusbarEntryItem extends Disposable { this.entry = entry; } - private async executeCommand(id: string, args?: unknown[]): Promise { - args = args || []; + private async executeCommand(command: string | Command): Promise { + const id = typeof command === 'string' ? command : command.id; + const args = typeof command === 'string' ? [] : command.arguments ?? []; // Maintain old behaviour of always focusing the editor here const activeTextEditorControl = this.editorService.activeTextEditorControl; diff --git a/src/vs/workbench/browser/parts/views/customView.ts b/src/vs/workbench/browser/parts/views/treeView.ts similarity index 84% rename from src/vs/workbench/browser/parts/views/customView.ts rename to src/vs/workbench/browser/parts/views/treeView.ts index 3e31d2a53ae..1624e9274c9 100644 --- a/src/vs/workbench/browser/parts/views/customView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -7,12 +7,12 @@ import 'vs/css!./media/views'; import { Event, Emitter } from 'vs/base/common/event'; import { IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IAction, ActionRunner, Action } from 'vs/base/common/actions'; +import { IAction, ActionRunner } from 'vs/base/common/actions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { IMenuService, MenuId, MenuItemAction, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { ContextAwareMenuEntryActionViewItem, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, ContextKeyExpr, ContextKeyEqualsExpr, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { ITreeView, ITreeItem, TreeItemCollapsibleState, ITreeViewDataProvider, TreeViewItemHandleArg, ITreeViewDescriptor, IViewsRegistry, ITreeItemLabel, Extensions, IViewDescriptorService, ViewContainer, ViewContainerLocation } from 'vs/workbench/common/views'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -44,7 +44,7 @@ import { SIDE_BAR_BACKGROUND, PANEL_BACKGROUND } from 'vs/workbench/common/theme import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -export class CustomTreeViewPane extends ViewPane { +export class TreeViewPane extends ViewPane { private treeView: ITreeView; @@ -60,13 +60,14 @@ export class CustomTreeViewPane extends ViewPane { @IThemeService themeService: IThemeService, @ITelemetryService telemetryService: ITelemetryService, ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: options.title, titleMenuId: MenuId.ViewTitle }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + super({ ...(options as IViewPaneOptions), titleMenuId: MenuId.ViewTitle }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); const { treeView } = (Registry.as(Extensions.ViewsRegistry).getView(options.id)); this.treeView = treeView; this._register(this.treeView.onDidChangeActions(() => this.updateActions(), this)); this._register(this.treeView.onDidChangeTitle((newTitle) => this.updateTitle(newTitle))); this._register(toDisposable(() => this.treeView.setVisibility(false))); this._register(this.onDidChangeBodyVisibility(() => this.updateTreeVisibility())); + this._register(this.treeView.onDidChangeWelcomeState(() => this._onDidChangeViewWelcomeState.fire())); this.updateTreeVisibility(); } @@ -78,23 +79,19 @@ export class CustomTreeViewPane extends ViewPane { renderBody(container: HTMLElement): void { super.renderBody(container); - if (this.treeView instanceof CustomTreeView) { + if (this.treeView instanceof TreeView) { this.treeView.show(container); } } + shouldShowWelcome(): boolean { + return ((this.treeView.dataProvider === undefined) || !!this.treeView.dataProvider.isTreeEmpty) && (this.treeView.message === undefined); + } + layoutBody(height: number, width: number): void { this.treeView.layout(height, width); } - getActions(): IAction[] { - return [...super.getActions(), ...this.treeView.getPrimaryActions()]; - } - - getSecondaryActions(): IAction[] { - return [...super.getSecondaryActions(), ...this.treeView.getSecondaryActions()]; - } - getOptimalWidth(): number { return this.treeView.getOptimalWidth(); } @@ -114,15 +111,18 @@ class Root implements ITreeItem { const noDataProviderMessage = localize('no-dataprovider', "There is no data provider registered that can provide view data."); -class CustomTree extends WorkbenchAsyncDataTree { } +class Tree extends WorkbenchAsyncDataTree { } -export class CustomTreeView extends Disposable implements ITreeView { +export class TreeView extends Disposable implements ITreeView { private isVisible: boolean = false; - private activated: boolean = false; private _hasIconForParentNode = false; private _hasIconForLeafNode = false; - private _showCollapseAllAction = false; + + private readonly collapseAllContextKey: RawContextKey; + private readonly collapseAllContext: IContextKey; + private readonly refreshContextKey: RawContextKey; + private readonly refreshContext: IContextKey; private focused: boolean = false; private domNode!: HTMLElement; @@ -130,7 +130,7 @@ export class CustomTreeView extends Disposable implements ITreeView { private _messageValue: string | undefined; private _canSelectMany: boolean = false; private messageElement!: HTMLDivElement; - private tree: CustomTree | undefined; + private tree: Tree | undefined; private treeLabels: ResourceLabels | undefined; private root: ITreeItem; @@ -151,27 +151,35 @@ export class CustomTreeView extends Disposable implements ITreeView { private readonly _onDidChangeActions: Emitter = this._register(new Emitter()); readonly onDidChangeActions: Event = this._onDidChangeActions.event; + private readonly _onDidChangeWelcomeState: Emitter = this._register(new Emitter()); + readonly onDidChangeWelcomeState: Event = this._onDidChangeWelcomeState.event; + private readonly _onDidChangeTitle: Emitter = this._register(new Emitter()); readonly onDidChangeTitle: Event = this._onDidChangeTitle.event; private readonly _onDidCompleteRefresh: Emitter = this._register(new Emitter()); constructor( - private id: string, + protected readonly id: string, private _title: string, - @IExtensionService private readonly extensionService: IExtensionService, @IThemeService private readonly themeService: IThemeService, @IInstantiationService private readonly instantiationService: IInstantiationService, @ICommandService private readonly commandService: ICommandService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IProgressService private readonly progressService: IProgressService, + @IProgressService protected readonly progressService: IProgressService, @IContextMenuService private readonly contextMenuService: IContextMenuService, @IKeybindingService private readonly keybindingService: IKeybindingService, @INotificationService private readonly notificationService: INotificationService, - @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService + @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, + @IContextKeyService contextKeyService: IContextKeyService ) { super(); this.root = new Root(); + this.collapseAllContextKey = new RawContextKey(`treeView.${this.id}.enableCollapseAll`, false); + this.collapseAllContext = this.collapseAllContextKey.bindTo(contextKeyService); + this.refreshContextKey = new RawContextKey(`treeView.${this.id}.enableRefresh`, false); + this.refreshContext = this.refreshContextKey.bindTo(contextKeyService); + this._register(this.themeService.onDidFileIconThemeChange(() => this.doRefresh([this.root]) /** soft refresh **/)); this._register(this.themeService.onDidColorThemeChange(() => this.doRefresh([this.root]) /** soft refresh **/)); this._register(this.configurationService.onDidChangeConfiguration(e => { @@ -184,6 +192,7 @@ export class CustomTreeView extends Disposable implements ITreeView { this.tree?.updateOptions({ overrideStyles: { listBackground: this.viewLocation === ViewContainerLocation.Sidebar ? SIDE_BAR_BACKGROUND : PANEL_BACKGROUND } }); } })); + this.registerActions(); this.create(); } @@ -208,21 +217,43 @@ export class CustomTreeView extends Disposable implements ITreeView { if (dataProvider) { this._dataProvider = new class implements ITreeViewDataProvider { + private _isEmpty: boolean = true; + private _onDidChangeEmpty: Emitter = new Emitter(); + public onDidChangeEmpty: Event = this._onDidChangeEmpty.event; + + get isTreeEmpty(): boolean { + return this._isEmpty; + } + async getChildren(node: ITreeItem): Promise { + let children: ITreeItem[]; if (node && node.children) { - return Promise.resolve(node.children); + children = node.children; + } else { + children = await (node instanceof Root ? dataProvider.getChildren() : dataProvider.getChildren(node)); + node.children = children; + } + if (node instanceof Root) { + const oldEmpty = this._isEmpty; + this._isEmpty = children.length === 0; + if (oldEmpty !== this._isEmpty) { + this._onDidChangeEmpty.fire(); + } } - const children = await (node instanceof Root ? dataProvider.getChildren() : dataProvider.getChildren(node)); - node.children = children; return children; } }; + if (this._dataProvider.onDidChangeEmpty) { + this._register(this._dataProvider.onDidChangeEmpty(() => this._onDidChangeWelcomeState.fire())); + } this.updateMessage(); this.refresh(); } else { this._dataProvider = undefined; this.updateMessage(); } + + this._onDidChangeWelcomeState.fire(); } private _message: string | undefined; @@ -233,6 +264,7 @@ export class CustomTreeView extends Disposable implements ITreeView { set message(message: string | undefined) { this._message = message; this.updateMessage(); + this._onDidChangeWelcomeState.fire(); } get title(): string { @@ -265,26 +297,61 @@ export class CustomTreeView extends Disposable implements ITreeView { } get showCollapseAllAction(): boolean { - return this._showCollapseAllAction; + return !!this.collapseAllContext.get(); } set showCollapseAllAction(showCollapseAllAction: boolean) { - if (this._showCollapseAllAction !== !!showCollapseAllAction) { - this._showCollapseAllAction = !!showCollapseAllAction; - this._onDidChangeActions.fire(); - } + this.collapseAllContext.set(showCollapseAllAction); } - getPrimaryActions(): IAction[] { - if (this.showCollapseAllAction) { - return [new Action('vs.tree.collapse', localize('collapseAll', "Collapse All"), 'monaco-tree-action codicon-collapse-all', true, () => this.tree ? new CollapseAllAction(this.tree, true).run() : Promise.resolve())]; - } else { - return []; - } + get showRefreshAction(): boolean { + return !!this.refreshContext.get(); } - getSecondaryActions(): IAction[] { - return []; + set showRefreshAction(showRefreshAction: boolean) { + this.refreshContext.set(showRefreshAction); + } + + private registerActions() { + const that = this; + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.actions.treeView.${that.id}.refresh`, + title: localize('refresh', "Refresh"), + menu: { + id: MenuId.ViewTitle, + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', that.id), that.refreshContextKey), + group: 'navigation', + order: Number.MAX_SAFE_INTEGER - 1, + }, + icon: { id: 'codicon/refresh' } + }); + } + async run(): Promise { + return that.refresh(); + } + })); + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.actions.treeView.${that.id}.collapseAll`, + title: localize('collapseAll', "Collapse All"), + menu: { + id: MenuId.ViewTitle, + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', that.id), that.collapseAllContextKey), + group: 'navigation', + order: Number.MAX_SAFE_INTEGER, + }, + icon: { id: 'codicon/collapse-all' } + }); + } + async run(): Promise { + if (that.tree) { + return new CollapseAllAction(that.tree, true).run(); + } + } + })); } setVisibility(isVisible: boolean): void { @@ -294,9 +361,6 @@ export class CustomTreeView extends Disposable implements ITreeView { } this.isVisible = isVisible; - if (this.isVisible) { - this.activate(); - } if (this.tree) { if (this.isVisible) { @@ -354,9 +418,9 @@ export class CustomTreeView extends Disposable implements ITreeView { const aligner = new Aligner(this.themeService); const renderer = this.instantiationService.createInstance(TreeRenderer, this.id, treeMenus, this.treeLabels, actionViewItemProvider, aligner); - this.tree = this._register(this.instantiationService.createInstance(CustomTree, 'CustomView', this.treeContainer, new CustomTreeDelegate(), [renderer], + this.tree = this._register(this.instantiationService.createInstance(Tree, this.id, this.treeContainer, new TreeViewDelegate(), [renderer], dataSource, { - identityProvider: new CustomViewIdentityProvider(), + identityProvider: new TreeViewIdentityProvider(), accessibilityProvider: { getAriaLabel(element: ITreeItem): string { return element.tooltip ? element.tooltip : element.label ? element.label.label : ''; @@ -398,9 +462,9 @@ export class CustomTreeView extends Disposable implements ITreeView { })); this.tree.setInput(this.root).then(() => this.updateContentAreas()); - const customTreeNavigator = ResourceNavigator.createTreeResourceNavigator(this.tree, { openOnFocus: false, openOnSelection: false }); - this._register(customTreeNavigator); - this._register(customTreeNavigator.onDidOpenResource(e => { + const treeNavigator = ResourceNavigator.createTreeResourceNavigator(this.tree, { openOnFocus: false, openOnSelection: false }); + this._register(treeNavigator); + this._register(treeNavigator.onDidOpenResource(e => { if (!e.browserEvent) { return; } @@ -451,7 +515,7 @@ export class CustomTreeView extends Disposable implements ITreeView { }); } - private updateMessage(): void { + protected updateMessage(): void { if (this._message) { this.showMessage(this._message); } else if (!this.dataProvider) { @@ -568,17 +632,6 @@ export class CustomTreeView extends Disposable implements ITreeView { return Promise.resolve(); } - private activate() { - if (!this.activated) { - this.progressService.withProgress({ location: this.viewContainer.id }, () => this.extensionService.activateByEvent(`onView:${this.id}`)) - .then(() => timeout(2000)) - .then(() => { - this.updateMessage(); - }); - this.activated = true; - } - } - private refreshing: boolean = false; private async doRefresh(elements: ITreeItem[]): Promise { const tree = this.tree; @@ -607,13 +660,13 @@ export class CustomTreeView extends Disposable implements ITreeView { } } -class CustomViewIdentityProvider implements IIdentityProvider { +class TreeViewIdentityProvider implements IIdentityProvider { getId(element: ITreeItem): { toString(): string; } { return element.handle; } } -class CustomTreeDelegate implements IListVirtualDelegate { +class TreeViewDelegate implements IListVirtualDelegate { getHeight(element: ITreeItem): number { return TreeRenderer.ITEM_HEIGHT; @@ -932,3 +985,44 @@ class TreeMenus extends Disposable implements IDisposable { return result; } } + +export class CustomTreeView extends TreeView { + + private activated: boolean = false; + + constructor( + id: string, + title: string, + @IThemeService themeService: IThemeService, + @IInstantiationService instantiationService: IInstantiationService, + @ICommandService commandService: ICommandService, + @IConfigurationService configurationService: IConfigurationService, + @IProgressService progressService: IProgressService, + @IContextMenuService contextMenuService: IContextMenuService, + @IKeybindingService keybindingService: IKeybindingService, + @INotificationService notificationService: INotificationService, + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, + @IContextKeyService contextKeyService: IContextKeyService, + @IExtensionService private readonly extensionService: IExtensionService, + ) { + super(id, title, themeService, instantiationService, commandService, configurationService, progressService, contextMenuService, keybindingService, notificationService, viewDescriptorService, contextKeyService); + } + + setVisibility(isVisible: boolean): void { + super.setVisibility(isVisible); + if (this.visible) { + this.activate(); + } + } + + private activate() { + if (!this.activated) { + this.progressService.withProgress({ location: this.viewContainer.id }, () => this.extensionService.activateByEvent(`onView:${this.id}`)) + .then(() => timeout(2000)) + .then(() => { + this.updateMessage(); + }); + this.activated = true; + } + } +} diff --git a/src/vs/workbench/browser/parts/views/viewMenuActions.ts b/src/vs/workbench/browser/parts/views/viewMenuActions.ts index f55455a925e..3c0ad25bc70 100644 --- a/src/vs/workbench/browser/parts/views/viewMenuActions.ts +++ b/src/vs/workbench/browser/parts/views/viewMenuActions.ts @@ -36,7 +36,7 @@ export class ViewMenuActions extends Disposable { const updateActions = () => { this.primaryActions = []; this.secondaryActions = []; - this.titleActionsDisposable.value = createAndFillInActionBarActions(menu, undefined, { primary: this.primaryActions, secondary: this.secondaryActions }); + this.titleActionsDisposable.value = createAndFillInActionBarActions(menu, { shouldForwardArgs: true }, { primary: this.primaryActions, secondary: this.secondaryActions }); this._onDidChangeTitle.fire(); }; this._register(menu.onDidChange(updateActions)); @@ -45,7 +45,7 @@ export class ViewMenuActions extends Disposable { const contextMenu = this._register(this.menuService.createMenu(contextMenuId, scopedContextKeyService)); const updateContextMenuActions = () => { this.contextMenuActions = []; - this.titleActionsDisposable.value = createAndFillInActionBarActions(contextMenu, undefined, { primary: [], secondary: this.contextMenuActions }); + this.titleActionsDisposable.value = createAndFillInActionBarActions(contextMenu, { shouldForwardArgs: true }, { primary: [], secondary: this.contextMenuActions }); }; this._register(contextMenu.onDidChange(updateContextMenuActions)); updateContextMenuActions(); diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index 06161af0a38..4ad5250ccf6 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -53,7 +53,6 @@ export interface IPaneColors extends IColorMapping { export interface IViewPaneOptions extends IPaneOptions { id: string; - title: string; showActionsAlways?: boolean; titleMenuId?: MenuId; } diff --git a/src/vs/workbench/browser/parts/views/views.ts b/src/vs/workbench/browser/parts/views/views.ts index c431d4e0904..4d4cb205a2d 100644 --- a/src/vs/workbench/browser/parts/views/views.ts +++ b/src/vs/workbench/browser/parts/views/views.ts @@ -486,11 +486,10 @@ export class ViewsService extends Disposable implements IViewsService { this._register(this.viewContainersRegistry.onDidRegister(({ viewContainer, viewContainerLocation }) => this.onDidRegisterViewContainer(viewContainer, viewContainerLocation))); } - registerViewPaneContainer(viewPaneContainer: ViewPaneContainer): ViewPaneContainer { + private registerViewPaneContainer(viewPaneContainer: ViewPaneContainer): void { this._register(viewPaneContainer.onDidAddViews(views => this.onViewsAdded(views))); this._register(viewPaneContainer.onDidChangeViewVisibility(view => this.onViewsVisibilityChanged(view, view.isBodyVisible()))); this._register(viewPaneContainer.onDidRemoveViews(views => this.onViewsRemoved(views))); - return viewPaneContainer; } private onViewsAdded(added: IView[]): void { @@ -520,7 +519,8 @@ export class ViewsService extends Disposable implements IViewsService { return contextKey; } - private onDidRegisterViewContainer(viewContainer: ViewContainer, location: ViewContainerLocation): void { + private onDidRegisterViewContainer(viewContainer: ViewContainer, viewContainerLocation: ViewContainerLocation): void { + this.registerViewletOrPanel(viewContainer, viewContainerLocation); const viewDescriptorCollection = this.viewDescriptorService.getViewDescriptors(viewContainer); this.onViewDescriptorsAdded(viewDescriptorCollection.allViewDescriptors, viewContainer); this._register(viewDescriptorCollection.onDidChangeViews(({ added, removed }) => { @@ -698,6 +698,77 @@ export class ViewsService extends Disposable implements IViewsService { return null; } + + private registerViewletOrPanel(viewContainer: ViewContainer, viewContainerLocation: ViewContainerLocation): void { + switch (viewContainerLocation) { + case ViewContainerLocation.Panel: + this.registerPanel(viewContainer); + break; + case ViewContainerLocation.Sidebar: + if (viewContainer.ctorDescriptor) { + this.registerViewlet(viewContainer); + } + break; + } + } + + private registerPanel(viewContainer: ViewContainer): void { + const that = this; + class PaneContainerPanel extends PaneCompositePanel { + constructor( + @ITelemetryService telemetryService: ITelemetryService, + @IStorageService storageService: IStorageService, + @IInstantiationService instantiationService: IInstantiationService, + @IThemeService themeService: IThemeService, + @IContextMenuService contextMenuService: IContextMenuService, + @IExtensionService extensionService: IExtensionService, + @IWorkspaceContextService contextService: IWorkspaceContextService, + ) { + // Use composite's instantiation service to get the editor progress service for any editors instantiated within the composite + const viewPaneContainer = (instantiationService as any).createInstance(viewContainer.ctorDescriptor!.ctor, ...(viewContainer.ctorDescriptor!.staticArguments || [])); + super(viewContainer.id, viewPaneContainer, telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService); + that.registerViewPaneContainer(this.viewPaneContainer); + } + } + Registry.as(PanelExtensions.Panels).registerPanel(PanelDescriptor.create( + PaneContainerPanel, + viewContainer.id, + viewContainer.name, + isString(viewContainer.icon) ? viewContainer.icon : undefined, + viewContainer.order, + viewContainer.focusCommand?.id, + )); + } + + private registerViewlet(viewContainer: ViewContainer): void { + const that = this; + class PaneContainerViewlet extends Viewlet { + constructor( + @IConfigurationService configurationService: IConfigurationService, + @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, + @ITelemetryService telemetryService: ITelemetryService, + @IWorkspaceContextService contextService: IWorkspaceContextService, + @IStorageService storageService: IStorageService, + @IInstantiationService instantiationService: IInstantiationService, + @IThemeService themeService: IThemeService, + @IContextMenuService contextMenuService: IContextMenuService, + @IExtensionService extensionService: IExtensionService, + ) { + // Use composite's instantiation service to get the editor progress service for any editors instantiated within the composite + const viewPaneContainer = (instantiationService as any).createInstance(viewContainer.ctorDescriptor!.ctor, ...(viewContainer.ctorDescriptor!.staticArguments || [])); + super(viewContainer.id, viewPaneContainer, telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService, layoutService, configurationService); + that.registerViewPaneContainer(this.viewPaneContainer); + } + } + Registry.as(ViewletExtensions.Viewlets).registerViewlet(ViewletDescriptor.create( + PaneContainerViewlet, + viewContainer.id, + viewContainer.name, + isString(viewContainer.icon) ? viewContainer.icon : undefined, + viewContainer.order, + viewContainer.icon instanceof URI ? viewContainer.icon : undefined + )); + } } export function createFileIconThemableTreeContainerScope(container: HTMLElement, themeService: IThemeService): IDisposable { @@ -714,79 +785,3 @@ export function createFileIconThemableTreeContainerScope(container: HTMLElement, } registerSingleton(IViewsService, ViewsService); - -// Viewlets & Panels -(function registerViewletsAndPanels(): void { - const registerPanel = (viewContainer: ViewContainer): void => { - class PaneContainerPanel extends PaneCompositePanel { - constructor( - @ITelemetryService telemetryService: ITelemetryService, - @IStorageService storageService: IStorageService, - @IInstantiationService instantiationService: IInstantiationService, - @IThemeService themeService: IThemeService, - @IContextMenuService contextMenuService: IContextMenuService, - @IExtensionService extensionService: IExtensionService, - @IWorkspaceContextService contextService: IWorkspaceContextService, - @IViewsService viewsService: ViewsService - ) { - // Use composite's instantiation service to get the editor progress service for any editors instantiated within the composite - const viewPaneContainer = viewsService.registerViewPaneContainer((instantiationService as any).createInstance(viewContainer.ctorDescriptor!.ctor, ...(viewContainer.ctorDescriptor!.staticArguments || []))); - super(viewContainer.id, viewPaneContainer, telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService); - } - } - Registry.as(PanelExtensions.Panels).registerPanel(PanelDescriptor.create( - PaneContainerPanel, - viewContainer.id, - viewContainer.name, - isString(viewContainer.icon) ? viewContainer.icon : undefined, - viewContainer.order, - viewContainer.focusCommand?.id, - )); - }; - - const registerViewlet = (viewContainer: ViewContainer): void => { - class PaneContainerViewlet extends Viewlet { - constructor( - @IConfigurationService configurationService: IConfigurationService, - @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, - @ITelemetryService telemetryService: ITelemetryService, - @IWorkspaceContextService contextService: IWorkspaceContextService, - @IStorageService storageService: IStorageService, - @IInstantiationService instantiationService: IInstantiationService, - @IThemeService themeService: IThemeService, - @IContextMenuService contextMenuService: IContextMenuService, - @IExtensionService extensionService: IExtensionService, - @IViewsService viewsService: ViewsService - ) { - // Use composite's instantiation service to get the editor progress service for any editors instantiated within the composite - const viewPaneContainer = viewsService.registerViewPaneContainer((instantiationService as any).createInstance(viewContainer.ctorDescriptor!.ctor, ...(viewContainer.ctorDescriptor!.staticArguments || []))); - super(viewContainer.id, viewPaneContainer, telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService, layoutService, configurationService); - } - } - const viewletDescriptor = ViewletDescriptor.create( - PaneContainerViewlet, - viewContainer.id, - viewContainer.name, - isString(viewContainer.icon) ? viewContainer.icon : undefined, - viewContainer.order, - viewContainer.icon instanceof URI ? viewContainer.icon : undefined - ); - - Registry.as(ViewletExtensions.Viewlets).registerViewlet(viewletDescriptor); - }; - - const viewContainerRegistry = Registry.as(ViewExtensions.ViewContainersRegistry); - viewContainerRegistry.getViewContainers(ViewContainerLocation.Panel).forEach(viewContainer => registerPanel(viewContainer)); - viewContainerRegistry.onDidRegister(({ viewContainer, viewContainerLocation }) => { - switch (viewContainerLocation) { - case ViewContainerLocation.Panel: - registerPanel(viewContainer); - return; - case ViewContainerLocation.Sidebar: - if (viewContainer.ctorDescriptor) { - registerViewlet(viewContainer); - } - return; - } - }); -})(); diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index a85e346bd98..14564a51d3b 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -488,6 +488,8 @@ export interface ITreeView extends IDisposable { readonly onDidChangeTitle: Event; + readonly onDidChangeWelcomeState: Event; + refresh(treeItems?: ITreeItem[]): Promise; setVisibility(visible: boolean): void; @@ -506,9 +508,6 @@ export interface ITreeView extends IDisposable { setFocus(item: ITreeItem): void; - getPrimaryActions(): IAction[]; - - getSecondaryActions(): IAction[]; } export interface IRevealOptions { @@ -574,7 +573,8 @@ export interface ITreeItem { } export interface ITreeViewDataProvider { - + readonly isTreeEmpty?: boolean; + onDidChangeEmpty?: Event; getChildren(element?: ITreeItem): Promise; } diff --git a/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts b/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts index eae8927c912..ddcd8ff16b4 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/codeEditor.contribution.ts @@ -9,6 +9,7 @@ import './diffEditorHelper'; import './inspectKeybindings'; import './largeFileOptimizations'; import './inspectEditorTokens/inspectEditorTokens'; +import './quickaccess/gotoLineQuickAccess'; import './saveParticipants'; import './toggleColumnSelection'; import './toggleMinimap'; diff --git a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts new file mode 100644 index 00000000000..6404881f956 --- /dev/null +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { IKeyMods } from 'vs/platform/quickinput/common/quickInput'; +import { IEditor } from 'vs/editor/common/editorCommon'; +import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { IRange } from 'vs/editor/common/core/range'; +import { AbstractGotoLineQuickAccessProvider, GOTO_LINE_PREFIX } from 'vs/editor/contrib/quickAccess/gotoLine'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IQuickAccessRegistry, Extensions } from 'vs/platform/quickinput/common/quickAccess'; + +export class GotoLineQuickAccessProvider extends AbstractGotoLineQuickAccessProvider { + + readonly onDidActiveTextEditorControlChange = this.editorService.onDidActiveEditorChange; + + constructor(@IEditorService private readonly editorService: IEditorService) { + super(); + } + + get activeTextEditorControl() { + return this.editorService.activeTextEditorControl; + } + + protected gotoLine(editor: IEditor, range: IRange, keyMods: IKeyMods): void { + + // Check for sideBySide use + if (keyMods.ctrlCmd && this.editorService.activeEditor) { + this.editorService.openEditor(this.editorService.activeEditor, { selection: range, pinned: keyMods.alt }, SIDE_GROUP); + } + + // Otherwise let parent handle it + else { + super.gotoLine(editor, range, keyMods); + } + } +} + +Registry.as(Extensions.Quickaccess).registerQuickAccessProvider({ + ctor: GotoLineQuickAccessProvider, + prefix: GOTO_LINE_PREFIX, + placeholder: localize('gotoLineQuickAccessPlaceholder', "Type the line number and optional column to go to (e.g. 42:5 for line 42 and column 5)."), + helpEntries: [{ description: localize('gotoLineQuickAccess', "Go to Line"), needsEditor: true }] +}); diff --git a/src/vs/workbench/contrib/comments/browser/commentsView.ts b/src/vs/workbench/contrib/comments/browser/commentsView.ts index 31d066a3e35..3a60d76d637 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsView.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsView.ts @@ -55,7 +55,7 @@ export class CommentsPanel extends ViewPane { @ICommentService private readonly commentService: ICommentService, @ITelemetryService telemetryService: ITelemetryService, ) { - super({ ...(options as IViewPaneOptions), id: COMMENTS_VIEW_ID, ariaHeaderLabel: COMMENTS_VIEW_TITLE }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); } public renderBody(container: HTMLElement): void { diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index 8d8aaabded6..cbecd41545e 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -28,7 +28,7 @@ import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; -import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { ILabelService } from 'vs/platform/label/common/label'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { Gesture } from 'vs/base/browser/touch'; @@ -74,7 +74,7 @@ export class BreakpointsView extends ViewPane { @IOpenerService openerService: IOpenerService, @ITelemetryService telemetryService: ITelemetryService, ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('breakpointsSection', "Breakpoints Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this.minimumBodySize = this.maximumBodySize = getExpandedBodySize(this.debugService.getModel()); this._register(this.debugService.getModel().onDidChangeBreakpoints(() => this.onBreakpointsChange())); diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index d8af8fb5a29..77086ba5fc7 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -18,7 +18,7 @@ import { IAction, Action } from 'vs/base/common/actions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { ILabelService } from 'vs/platform/label/common/label'; import { IAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; @@ -104,7 +104,7 @@ export class CallStackView extends ViewPane { @IThemeService themeService: IThemeService, @ITelemetryService telemetryService: ITelemetryService, ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('callstackSection', "Call Stack Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this.callStackItemType = CONTEXT_CALLSTACK_ITEM_TYPE.bindTo(contextKeyService); this.contributedContextMenu = menuService.createMenu(MenuId.DebugCallStackContext, contextKeyService); diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 4dcff55440e..3cc92cc0715 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -44,7 +44,7 @@ import { WatchExpressionsView } from 'vs/workbench/contrib/debug/browser/watchEx import { VariablesView } from 'vs/workbench/contrib/debug/browser/variablesView'; import { ClearReplAction, Repl } from 'vs/workbench/contrib/debug/browser/repl'; import { DebugContentProvider } from 'vs/workbench/contrib/debug/common/debugContentProvider'; -import { StartView } from 'vs/workbench/contrib/debug/browser/startView'; +import { WelcomeView } from 'vs/workbench/contrib/debug/browser/welcomeView'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { DebugViewPaneContainer, OpenDebugPanelAction } from 'vs/workbench/contrib/debug/browser/debugViewlet'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; @@ -108,7 +108,7 @@ viewsRegistry.registerViews([{ id: VARIABLES_VIEW_ID, name: nls.localize('variab viewsRegistry.registerViews([{ id: WATCH_VIEW_ID, name: nls.localize('watch', "Watch"), ctorDescriptor: new SyncDescriptor(WatchExpressionsView), order: 20, weight: 10, canToggleVisibility: true, canMoveView: true, focusCommand: { id: 'workbench.debug.action.focusWatchView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], viewContainer); viewsRegistry.registerViews([{ id: CALLSTACK_VIEW_ID, name: nls.localize('callStack', "Call Stack"), ctorDescriptor: new SyncDescriptor(CallStackView), order: 30, weight: 30, canToggleVisibility: true, canMoveView: true, focusCommand: { id: 'workbench.debug.action.focusCallStackView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], viewContainer); viewsRegistry.registerViews([{ id: BREAKPOINTS_VIEW_ID, name: nls.localize('breakpoints', "Breakpoints"), ctorDescriptor: new SyncDescriptor(BreakpointsView), order: 40, weight: 20, canToggleVisibility: true, canMoveView: true, focusCommand: { id: 'workbench.debug.action.focusBreakpointsView' }, when: ContextKeyExpr.or(CONTEXT_BREAKPOINTS_EXIST, CONTEXT_DEBUG_UX.isEqualTo('default')) }], viewContainer); -viewsRegistry.registerViews([{ id: StartView.ID, name: StartView.LABEL, ctorDescriptor: new SyncDescriptor(StartView), order: 10, weight: 40, canToggleVisibility: true, when: CONTEXT_DEBUG_UX.isEqualTo('simple') }], viewContainer); +viewsRegistry.registerViews([{ id: WelcomeView.ID, name: WelcomeView.LABEL, ctorDescriptor: new SyncDescriptor(WelcomeView), order: 10, weight: 40, canToggleVisibility: true, when: CONTEXT_DEBUG_UX.isEqualTo('simple') }], viewContainer); viewsRegistry.registerViews([{ id: LOADED_SCRIPTS_VIEW_ID, name: nls.localize('loadedScripts', "Loaded Scripts"), ctorDescriptor: new SyncDescriptor(LoadedScriptsView), order: 35, weight: 5, canToggleVisibility: true, canMoveView: true, collapsed: true, when: ContextKeyExpr.and(CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_DEBUG_UX.isEqualTo('default')) }], viewContainer); registerCommands(); diff --git a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts index 7011327d8da..2c59ca9f209 100644 --- a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts +++ b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts @@ -32,8 +32,8 @@ import { MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryAc import { INotificationService } from 'vs/platform/notification/common/notification'; import { TogglePanelAction } from 'vs/workbench/browser/panel'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; -import { StartView } from 'vs/workbench/contrib/debug/browser/startView'; import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { WelcomeView } from 'vs/workbench/contrib/debug/browser/welcomeView'; export class DebugViewPaneContainer extends ViewPaneContainer { @@ -92,7 +92,7 @@ export class DebugViewPaneContainer extends ViewPaneContainer { if (this.startDebugActionViewItem) { this.startDebugActionViewItem.focus(); } else { - this.focusView(StartView.ID); + this.focusView(WelcomeView.ID); } } diff --git a/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts b/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts index 4bfd98599aa..5df627acdb2 100644 --- a/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts +++ b/src/vs/workbench/contrib/debug/browser/loadedScriptsView.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { normalize, isAbsolute, posix } from 'vs/base/common/path'; -import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -430,7 +430,7 @@ export class LoadedScriptsView extends ViewPane { @IThemeService themeService: IThemeService, @ITelemetryService telemetryService: ITelemetryService, ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('loadedScriptsSection', "Loaded Scripts Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this.loadedScriptsItemType = CONTEXT_LOADED_SCRIPTS_ITEM_TYPE.bindTo(contextKeyService); } diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index 0f54aa901c9..56022d9f636 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -113,7 +113,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { @IOpenerService openerService: IOpenerService, @ITelemetryService telemetryService: ITelemetryService, ) { - super({ ...(options as IViewPaneOptions), id: REPL_VIEW_ID, ariaHeaderLabel: localize('debugConsole', "Debug Console") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this.history = new HistoryNavigator(JSON.parse(this.storageService.get(HISTORY_STORAGE_KEY, StorageScope.WORKSPACE, '[]')), 50); codeEditorService.registerDecorationType(DECORATION_KEY, {}); diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index 2c40b45a4e2..6011bdbd99a 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -17,7 +17,7 @@ import { IAction, Action } from 'vs/base/common/actions'; import { CopyValueAction } from 'vs/workbench/contrib/debug/browser/debugActions'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { ITreeRenderer, ITreeNode, ITreeMouseEvent, ITreeContextMenuEvent, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; @@ -61,7 +61,7 @@ export class VariablesView extends ViewPane { @IThemeService themeService: IThemeService, @ITelemetryService telemetryService: ITelemetryService, ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('variablesSection', "Variables Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); // Use scheduler to prevent unnecessary flashing this.onFocusStackFrameScheduler = new RunOnceScheduler(async () => { diff --git a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts index 1be23a91b3e..0dbd60fc37c 100644 --- a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts +++ b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts @@ -18,7 +18,7 @@ import { IAction, Action } from 'vs/base/common/actions'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { renderExpressionValue, renderViewTree, IInputBoxOptions, AbstractExpressionsRenderer, IExpressionTemplateData } from 'vs/workbench/contrib/debug/browser/baseDebugView'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; @@ -58,7 +58,7 @@ export class WatchExpressionsView extends ViewPane { @IThemeService themeService: IThemeService, @ITelemetryService telemetryService: ITelemetryService, ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('watchExpressionsSection', "Watch Expressions Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this.onWatchExpressionsUpdatedScheduler = new RunOnceScheduler(() => { this.needsRefresh = false; diff --git a/src/vs/workbench/contrib/debug/browser/startView.ts b/src/vs/workbench/contrib/debug/browser/welcomeView.ts similarity index 88% rename from src/vs/workbench/contrib/debug/browser/startView.ts rename to src/vs/workbench/contrib/debug/browser/welcomeView.ts index c69b7c228fd..333c01000bd 100644 --- a/src/vs/workbench/contrib/debug/browser/startView.ts +++ b/src/vs/workbench/contrib/debug/browser/welcomeView.ts @@ -13,7 +13,7 @@ import { localize } from 'vs/nls'; import { StartAction, ConfigureAction } from 'vs/workbench/contrib/debug/browser/debugActions'; import { IDebugService } from 'vs/workbench/contrib/debug/common/debug'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IViewPaneOptions, ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IViewDescriptorService, IViewsRegistry, Extensions } from 'vs/workbench/common/views'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -29,10 +29,10 @@ const debugStartLanguageKey = 'debugStartLanguage'; const CONTEXT_DEBUG_START_LANGUAGE = new RawContextKey(debugStartLanguageKey, undefined); const CONTEXT_DEBUGGER_INTERESTED_IN_ACTIVE_EDITOR = new RawContextKey('debuggerInterestedInActiveEditor', false); -export class StartView extends ViewPane { +export class WelcomeView extends ViewPane { - static ID = 'workbench.debug.startView'; - static LABEL = localize('start', "Start"); + static ID = 'workbench.debug.welcome'; + static LABEL = localize('run', "Run"); private debugStartLanguageContext: IContextKey; private debuggerInterestedContext: IContextKey; @@ -52,7 +52,7 @@ export class StartView extends ViewPane { @IStorageService storageSevice: IStorageService, @ITelemetryService telemetryService: ITelemetryService, ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: localize('debugStart', "Debug Start Section") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this.debugStartLanguageContext = CONTEXT_DEBUG_START_LANGUAGE.bindTo(contextKeyService); this.debuggerInterestedContext = CONTEXT_DEBUGGER_INTERESTED_IN_ACTIVE_EDITOR.bindTo(contextKeyService); @@ -87,23 +87,23 @@ export class StartView extends ViewPane { } const viewsRegistry = Registry.as(Extensions.ViewsRegistry); -viewsRegistry.registerViewWelcomeContent(StartView.ID, { +viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { content: localize('openAFileWhichCanBeDebugged', "[Open a file](command:{0}) which can be debugged or run.", isMacintosh ? OpenFileFolderAction.ID : OpenFileAction.ID), when: CONTEXT_DEBUGGER_INTERESTED_IN_ACTIVE_EDITOR.toNegated() }); let debugKeybindingLabel = ''; -viewsRegistry.registerViewWelcomeContent(StartView.ID, { +viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { content: localize('runAndDebugAction', "[Run and Debug{0}](command:{1})", debugKeybindingLabel, StartAction.ID), preconditions: [CONTEXT_DEBUGGER_INTERESTED_IN_ACTIVE_EDITOR] }); -viewsRegistry.registerViewWelcomeContent(StartView.ID, { +viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { content: localize('customizeRunAndDebug', "To customize Run and Debug [create a launch.json file](command:{0}).", ConfigureAction.ID), when: WorkbenchStateContext.notEqualsTo('empty') }); -viewsRegistry.registerViewWelcomeContent(StartView.ID, { +viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { content: localize('customizeRunAndDebugOpenFolder', "To customize Run and Debug, [open a folder](command:{0}) and create a launch.json file.", isMacintosh ? OpenFileFolderAction.ID : OpenFolderAction.ID), when: WorkbenchStateContext.isEqualTo('empty') }); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index 733bff5f29a..b68c1e206e8 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -1208,7 +1208,7 @@ export class ExtensionEditor extends BaseEditor { $('th', undefined, localize('schema', "Schema")) ), ...contrib.map(v => $('tr', undefined, - $('td', undefined, $('code', undefined, v.fileMatch)), + $('td', undefined, $('code', undefined, Array.isArray(v.fileMatch) ? v.fileMatch.join(', ') : v.fileMatch)), $('td', undefined, v.url) )))); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 856cb6f48c5..9b53fb17a6f 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -114,7 +114,7 @@ export class ExtensionsListView extends ViewPane { @IMenuService private readonly menuService: IMenuService, @IOpenerService openerService: IOpenerService, ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: options.title, showActionsAlways: true }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + super({ ...(options as IViewPaneOptions), showActionsAlways: true }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this.server = options.server; } diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts index b7d2136893e..b455f29a5c0 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts @@ -27,7 +27,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { TestContextService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; import { TestSharedProcessService } from 'vs/workbench/test/electron-browser/workbenchTestServices'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsTipsService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsTipsService.test.ts index 5242e47633c..d8a87bcbf64 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsTipsService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsTipsService.test.ts @@ -22,7 +22,8 @@ import { Emitter } from 'vs/base/common/event'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { TestContextService, TestLifecycleService, productService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestLifecycleService, productService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; import { TestSharedProcessService } from 'vs/workbench/test/electron-browser/workbenchTestServices'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts index 7334f11625f..8347fcae927 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts @@ -27,7 +27,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { TestContextService, TestMenuService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestMenuService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestSharedProcessService } from 'vs/workbench/test/electron-browser/workbenchTestServices'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; @@ -46,6 +46,7 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { IMenuService } from 'vs/platform/actions/common/actions'; +import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; suite('ExtensionsListView Tests', () => { diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts index 6d22875244d..f04352be125 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts @@ -27,7 +27,6 @@ import { IPager } from 'vs/base/common/paging'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { TestContextService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestSharedProcessService } from 'vs/workbench/test/electron-browser/workbenchTestServices'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; @@ -41,6 +40,7 @@ import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browser/remoteAgentServiceImpl'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; +import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; suite('ExtensionsWorkbenchServiceTest', () => { diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index f7107199951..f92fbd96463 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -305,7 +305,7 @@ configurationRegistry.registerConfiguration({ }, 'files.watcherExclude': { 'type': 'object', - 'default': platform.isWindows /* https://github.com/Microsoft/vscode/issues/23954 */ ? { '**/.git/objects/**': true, '**/.git/subtree-cache/**': true, '**/node_modules/*/**': true } : { '**/.git/objects/**': true, '**/.git/subtree-cache/**': true, '**/node_modules/**': true }, + 'default': platform.isWindows /* https://github.com/Microsoft/vscode/issues/23954 */ ? { '**/.git/objects/**': true, '**/.git/subtree-cache/**': true, '**/node_modules/*/**': true, '**/.hg/store/**': true } : { '**/.git/objects/**': true, '**/.git/subtree-cache/**': true, '**/node_modules/**': true, '**/.hg/store/**': true }, 'description': nls.localize('watcherExclude', "Configure glob patterns of file paths to exclude from file watching. Patterns must match on absolute paths (i.e. prefix with ** or the full path to match properly). Changing this setting requires a restart. When you experience Code consuming lots of cpu time on startup, you can exclude large folders to reduce the initial load."), 'scope': ConfigurationScope.RESOURCE }, diff --git a/src/vs/workbench/contrib/files/browser/views/emptyView.ts b/src/vs/workbench/contrib/files/browser/views/emptyView.ts index f5de88ae4eb..042b938fef1 100644 --- a/src/vs/workbench/contrib/files/browser/views/emptyView.ts +++ b/src/vs/workbench/contrib/files/browser/views/emptyView.ts @@ -11,7 +11,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { ResourcesDropHandler, DragAndDropObserver } from 'vs/workbench/browser/dnd'; import { listDropBackground } from 'vs/platform/theme/common/colorRegistry'; import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; @@ -40,7 +40,7 @@ export class EmptyView extends ViewPane { @IOpenerService openerService: IOpenerService, @ITelemetryService telemetryService: ITelemetryService, ) { - super({ ...(options as IViewPaneOptions), ariaHeaderLabel: nls.localize('explorerSection', "Explorer Section: No Folder Opened") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this._register(this.contextService.onDidChangeWorkbenchState(() => this.refreshTitle())); this._register(this.labelService.onDidChangeFormatters(() => this.refreshTitle())); diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index 2a72ae6c655..a349a0c5c76 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -172,7 +172,7 @@ export class ExplorerView extends ViewPane { @IFileService private readonly fileService: IFileService, @IOpenerService openerService: IOpenerService, ) { - super({ ...(options as IViewPaneOptions), id: ExplorerView.ID, ariaHeaderLabel: nls.localize('explorerSection', "Explorer Section: {0}", labelService.getWorkspaceLabel(contextService.getWorkspace())) }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this.resourceContext = instantiationService.createInstance(ResourceContextKey); this._register(this.resourceContext); @@ -354,6 +354,7 @@ export class ExplorerView extends ViewPane { private createTree(container: HTMLElement): void { this.filter = this.instantiationService.createInstance(FilesFilter); this._register(this.filter); + this._register(this.filter.onDidChange(() => this.refresh(true))); const explorerLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this.onDidChangeBodyVisibility }); this._register(explorerLabels); @@ -458,18 +459,7 @@ export class ExplorerView extends ViewPane { this.autoReveal = configuration?.explorer?.autoReveal; // Push down config updates to components of viewer - let needsRefresh = false; - if (this.filter) { - needsRefresh = this.filter.updateConfiguration(); - } - - if (event && !needsRefresh) { - needsRefresh = event.affectsConfiguration('explorer.decorations.colors') - || event.affectsConfiguration('explorer.decorations.badges'); - } - - // Refresh viewer as needed if this originates from a config event - if (event && needsRefresh) { + if (event && (event.affectsConfiguration('explorer.decorations.colors') || event.affectsConfiguration('explorer.decorations.badges'))) { this.refresh(true); } } @@ -672,19 +662,23 @@ export class ExplorerView extends ViewPane { } if (item && item.parent) { - if (reveal) { - if (item.isDisposed) { - return this.onSelectResource(resource, reveal, retry + 1); + try { + if (reveal) { + if (item.isDisposed) { + return this.onSelectResource(resource, reveal, retry + 1); + } + + // Don't scroll to the item if it's already visible + if (this.tree.getRelativeTop(item) === null) { + this.tree.reveal(item, 0.5); + } } - // Don't scroll to the item if it's already visible - if (this.tree.getRelativeTop(item) === null) { - this.tree.reveal(item, 0.5); - } + this.tree.setFocus([item]); + this.tree.setSelection([item]); + } catch (e) { + // Element might not be in the tree, silently fail } - - this.tree.setFocus([item]); - this.tree.setSelection([item]); } } diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 9ae1935e2cd..107f5f1da8b 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -55,6 +55,7 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { isNumber } from 'vs/base/common/types'; import { domEvent } from 'vs/base/browser/event'; import { IEditableData } from 'vs/workbench/common/views'; +import { IEditorInput } from 'vs/workbench/common/editor'; export class ExplorerDelegate implements IListVirtualDelegate { @@ -472,28 +473,70 @@ interface CachedParsedExpression { parsed: glob.ParsedExpression; } +/** + * Respectes files.exclude setting in filtering out content from the explorer. + * Makes sure that visible editors are always shown in the explorer even if they are filtered out by settings. + */ export class FilesFilter implements ITreeFilter { private hiddenExpressionPerRoot: Map; - private workspaceFolderChangeListener: IDisposable; + private hiddenUris = new Set(); + private editorsAffectingFilter = new Set(); + private _onDidChange = new Emitter(); + private toDispose: IDisposable[] = []; constructor( @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IExplorerService private readonly explorerService: IExplorerService + @IExplorerService private readonly explorerService: IExplorerService, + @IEditorService private readonly editorService: IEditorService, ) { this.hiddenExpressionPerRoot = new Map(); - this.workspaceFolderChangeListener = this.contextService.onDidChangeWorkspaceFolders(() => this.updateConfiguration()); + this.toDispose.push(this.contextService.onDidChangeWorkspaceFolders(() => this.updateConfiguration())); + this.toDispose.push(this.configurationService.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration('files.exclude')) { + this.updateConfiguration(); + } + })); + this.toDispose.push(this.editorService.onDidVisibleEditorsChange(() => { + const editors = this.editorService.visibleEditors; + let shouldFire = false; + this.hiddenUris.forEach(u => { + editors.forEach(e => { + if (e.resource && isEqualOrParent(e.resource, u)) { + // A filtered resource suddenly became visible since user opened an editor + shouldFire = true; + } + }); + }); + + this.editorsAffectingFilter.forEach(e => { + if (editors.indexOf(e) === -1) { + // Editor that was affecting filtering is no longer visible + shouldFire = true; + } + }); + if (shouldFire) { + this.editorsAffectingFilter.clear(); + this.hiddenUris.clear(); + this._onDidChange.fire(); + } + })); + this.updateConfiguration(); } - updateConfiguration(): boolean { - let needsRefresh = false; + get onDidChange(): Event { + return this._onDidChange.event; + } + + private updateConfiguration(): void { + let shouldFire = false; this.contextService.getWorkspace().folders.forEach(folder => { const configuration = this.configurationService.getValue({ resource: folder.uri }); const excludesConfig: glob.IExpression = configuration?.files?.exclude || Object.create(null); - if (!needsRefresh) { + if (!shouldFire) { const cached = this.hiddenExpressionPerRoot.get(folder.uri.toString()); - needsRefresh = !cached || !equals(cached.original, excludesConfig); + shouldFire = !cached || !equals(cached.original, excludesConfig); } const excludesConfigCopy = deepClone(excludesConfig); // do not keep the config, as it gets mutated under our hoods @@ -501,10 +544,25 @@ export class FilesFilter implements ITreeFilter { this.hiddenExpressionPerRoot.set(folder.uri.toString(), { original: excludesConfigCopy, parsed: glob.parse(excludesConfigCopy) }); }); - return needsRefresh; + if (shouldFire) { + this.editorsAffectingFilter.clear(); + this.hiddenUris.clear(); + this._onDidChange.fire(); + } } filter(stat: ExplorerItem, parentVisibility: TreeVisibility): TreeFilterResult { + const isVisible = this.isVisible(stat, parentVisibility); + if (isVisible) { + this.hiddenUris.delete(stat.resource); + } else { + this.hiddenUris.add(stat.resource); + } + + return isVisible; + } + + private isVisible(stat: ExplorerItem, parentVisibility: TreeVisibility): boolean { if (parentVisibility === TreeVisibility.Hidden) { return false; } @@ -515,14 +573,21 @@ export class FilesFilter implements ITreeFilter { // Hide those that match Hidden Patterns const cached = this.hiddenExpressionPerRoot.get(stat.root.resource.toString()); if (cached && cached.parsed(path.relative(stat.root.resource.path, stat.resource.path), stat.name, name => !!(stat.parent && stat.parent.getChild(name)))) { + const editors = this.editorService.visibleEditors; + const editor = editors.filter(e => e.resource && isEqualOrParent(e.resource, stat.resource)).pop(); + if (editor) { + this.editorsAffectingFilter.add(editor); + return true; // Show all opened files and their parents + } + return false; // hidden through pattern } return true; } - public dispose(): void { - dispose(this.workspaceFolderChangeListener); + dispose(): void { + dispose(this.toDispose); } } diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index ff9ec14a6af..1c22f7201cd 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -34,7 +34,7 @@ import { IMenuService, MenuId, IMenu } from 'vs/platform/actions/common/actions' import { DirtyEditorContext, OpenEditorsGroupContext, ReadonlyEditorContext } from 'vs/workbench/contrib/files/browser/fileCommands'; import { ResourceContextKey } from 'vs/workbench/common/resources'; import { ResourcesDropHandler, fillResourceDataTransfers, CodeDataTransfers, containsDragType } from 'vs/workbench/browser/dnd'; -import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IDragAndDropData, DataTransfers } from 'vs/base/browser/dnd'; import { memoize } from 'vs/base/common/decorators'; @@ -84,10 +84,7 @@ export class OpenEditorsView extends ViewPane { @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, @IOpenerService openerService: IOpenerService, ) { - super({ - ...(options as IViewPaneOptions), - ariaHeaderLabel: nls.localize({ key: 'openEditosrSection', comment: ['Open is an adjective'] }, "Open Editors Section"), - }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this.structuralRefreshDelay = 0; this.listRefreshScheduler = new RunOnceScheduler(() => { diff --git a/src/vs/workbench/contrib/issue/electron-browser/issueService.ts b/src/vs/workbench/contrib/issue/electron-browser/issueService.ts index b25543f78c8..3efe946f1ee 100644 --- a/src/vs/workbench/contrib/issue/electron-browser/issueService.ts +++ b/src/vs/workbench/contrib/issue/electron-browser/issueService.ts @@ -12,9 +12,9 @@ import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/exte import { webFrame } from 'electron'; import { assign } from 'vs/base/common/objects'; import { IWorkbenchIssueService } from 'vs/workbench/contrib/issue/electron-browser/issue'; -import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; +import { ExtensionType } from 'vs/platform/extensions/common/extensions'; export class WorkbenchIssueService implements IWorkbenchIssueService { _serviceBrand: undefined; @@ -28,12 +28,13 @@ export class WorkbenchIssueService implements IWorkbenchIssueService { ) { } async openReporter(dataOverrides: Partial = {}): Promise { - const extensions = await this.extensionManagementService.getInstalled(ExtensionType.User); + const extensions = await this.extensionManagementService.getInstalled(); const enabledExtensions = extensions.filter(extension => this.extensionEnablementService.isEnabled(extension)); - const extensionData: IssueReporterExtensionData[] = enabledExtensions.map(extension => { + const extensionData = enabledExtensions.map((extension): IssueReporterExtensionData => { const { manifest } = extension; const manifestKeys = manifest.contributes ? Object.keys(manifest.contributes) : []; const isTheme = !manifest.activationEvents && manifestKeys.length === 1 && manifestKeys[0] === 'themes'; + const isBuiltin = extension.type === ExtensionType.System; return { name: manifest.name, publisher: manifest.publisher, @@ -42,14 +43,15 @@ export class WorkbenchIssueService implements IWorkbenchIssueService { bugsUrl: manifest.bugs && manifest.bugs.url, displayName: manifest.displayName, id: extension.identifier.id, - isTheme: isTheme + isTheme, + isBuiltin, }; }); const theme = this.themeService.getColorTheme(); const issueReporterData: IssueReporterData = assign({ styles: getIssueReporterStyles(theme), zoomLevel: webFrame.getZoomLevel(), - enabledExtensions: extensionData + enabledExtensions: extensionData, }, dataOverrides); return this.issueService.openReporter(issueReporterData); } diff --git a/src/vs/workbench/contrib/markers/browser/constants.ts b/src/vs/workbench/contrib/markers/browser/constants.ts index 6a06908d88a..6714a443377 100644 --- a/src/vs/workbench/contrib/markers/browser/constants.ts +++ b/src/vs/workbench/contrib/markers/browser/constants.ts @@ -14,6 +14,7 @@ export default { RELATED_INFORMATION_COPY_MESSAGE_ACTION_ID: 'problems.action.copyRelatedInformationMessage', FOCUS_PROBLEMS_FROM_FILTER: 'problems.action.focusProblemsFromFilter', MARKERS_VIEW_FOCUS_FILTER: 'problems.action.focusFilter', + MARKERS_VIEW_CLEAR_FILTER_TEXT: 'problems.action.clearFilterText', MARKERS_VIEW_SHOW_MULTILINE_MESSAGE: 'problems.action.showMultilineMessage', MARKERS_VIEW_SHOW_SINGLELINE_MESSAGE: 'problems.action.showSinglelineMessage', MARKER_OPEN_SIDE_ACTION_ID: 'problems.action.openToSide', @@ -21,7 +22,7 @@ export default { MARKER_SHOW_QUICK_FIX: 'problems.action.showQuickFixes', TOGGLE_MARKERS_VIEW_ACTION_ID: 'workbench.actions.view.toggleProblems', - MarkerViewFocusContextKey: new RawContextKey('problemsViewFocus', false), + MarkersViewSmallLayoutContextKey: new RawContextKey(`problemsView.smallLayout`, false), MarkerFocusContextKey: new RawContextKey('problemFocus', false), MarkerViewFilterFocusContextKey: new RawContextKey('problemsFilterFocus', false), RelatedInformationFocusContextKey: new RawContextKey('relatedInformationFocus', false) diff --git a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts index 30e05092dfe..2620461b323 100644 --- a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts +++ b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts @@ -28,7 +28,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment, IStatusbarEntry } from 'vs/workbench/services/statusbar/common/statusbar'; import { IMarkerService, MarkerStatistics } from 'vs/platform/markers/common/markers'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { ViewContainer, IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainerLocation, IViewsRegistry, IViewsService, getVisbileViewContextKey } from 'vs/workbench/common/views'; +import { ViewContainer, IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainerLocation, IViewsRegistry, IViewsService, getVisbileViewContextKey, FocusedViewContext } from 'vs/workbench/common/views'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; @@ -211,7 +211,7 @@ registerAction2(class extends Action2 { id: Constants.MARKERS_VIEW_FOCUS_FILTER, title: localize('focusProblemsFilter', "Focus problems filter"), keybinding: { - when: Constants.MarkerViewFocusContextKey, + when: FocusedViewContext.isEqualTo(Constants.MARKERS_VIEW_ID), weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.CtrlCmd | KeyCode.KEY_F } @@ -259,6 +259,25 @@ registerAction2(class extends Action2 { } } }); +registerAction2(class extends Action2 { + constructor() { + super({ + id: Constants.MARKERS_VIEW_CLEAR_FILTER_TEXT, + title: localize('clearFiltersText', "Clear filters text"), + category: localize('problems', "Problems"), + keybinding: { + when: Constants.MarkerViewFilterFocusContextKey, + weight: KeybindingWeight.WorkbenchContrib, + } + }); + } + run(accessor: ServicesAccessor) { + const markersView = accessor.get(IViewsService).getActiveViewWithId(Constants.MARKERS_VIEW_ID); + if (markersView) { + markersView.clearFilterText(); + } + } +}); async function copyMarker(viewsService: IViewsService, clipboardService: IClipboardService) { const markersView = viewsService.getActiveViewWithId(Constants.MARKERS_VIEW_ID); diff --git a/src/vs/workbench/contrib/markers/browser/markersView.ts b/src/vs/workbench/contrib/markers/browser/markersView.ts index aa7d6318e91..d19f41ae6f6 100644 --- a/src/vs/workbench/contrib/markers/browser/markersView.ts +++ b/src/vs/workbench/contrib/markers/browser/markersView.ts @@ -13,7 +13,7 @@ import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/ import Constants from 'vs/workbench/contrib/markers/browser/constants'; import { Marker, ResourceMarkers, RelatedInformation, MarkerChangesEvent } from 'vs/workbench/contrib/markers/browser/markersModel'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { MarkersFilterActionViewItem, MarkersFilterAction, IMarkersFilterActionChangeEvent, IMarkerFilterController } from 'vs/workbench/contrib/markers/browser/markersViewActions'; +import { MarkersFilterActionViewItem, MarkersFilters, IMarkersFiltersChangeEvent, IMarkerFilterController } from 'vs/workbench/contrib/markers/browser/markersViewActions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import Messages from 'vs/workbench/contrib/markers/browser/messages'; import { RangeHighlightDecorations } from 'vs/workbench/browser/parts/editor/rangeDecorations'; @@ -22,7 +22,7 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IMarkersWorkbenchService } from 'vs/workbench/contrib/markers/browser/markers'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { localize } from 'vs/nls'; -import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKey, IContextKeyService, ContextKeyEqualsExpr, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { Iterator } from 'vs/base/common/iterator'; import { ITreeElement, ITreeNode, ITreeContextMenuEvent, ITreeRenderer } from 'vs/base/browser/ui/tree/tree'; import { Relay, Event, Emitter } from 'vs/base/common/event'; @@ -34,7 +34,7 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { FilterData, Filter, VirtualDelegate, ResourceMarkersRenderer, MarkerRenderer, RelatedInformationRenderer, TreeElement, MarkersTreeAccessibilityProvider, MarkersViewModel, ResourceDragAndDrop } from 'vs/workbench/contrib/markers/browser/markersTreeViewer'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { Separator, ActionViewItem, ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; +import { IMenuService, MenuId, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { StandardKeyboardEvent, IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { domEvent } from 'vs/base/browser/event'; @@ -75,22 +75,28 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { private filterActionBar: ActionBar | undefined; private messageBoxContainer: HTMLElement | undefined; private ariaLabelElement: HTMLElement | undefined; - private readonly collapseAllAction: IAction; - private readonly filterAction: MarkersFilterAction; + readonly filters: MarkersFilters; private readonly panelState: MementoObject; - private panelFoucusContextKey: IContextKey; - private _onDidFilter = this._register(new Emitter()); - readonly onDidFilter: Event = this._onDidFilter.event; + private _onDidChangeFilterStats = this._register(new Emitter<{ total: number, filtered: number }>()); + readonly onDidChangeFilterStats: Event<{ total: number, filtered: number }> = this._onDidChangeFilterStats.event; private cachedFilterStats: { total: number; filtered: number; } | undefined = undefined; private currentResourceGotAddedToMarkersData: boolean = false; readonly markersViewModel: MarkersViewModel; - private isSmallLayout: boolean = false; + private readonly smallLayoutContextKey: IContextKey; + private get smallLayout(): boolean { return !!this.smallLayoutContextKey.get(); } + private set smallLayout(smallLayout: boolean) { this.smallLayoutContextKey.set(smallLayout); } readonly onDidChangeVisibility = this.onDidChangeBodyVisibility; + private readonly _onDidFocusFilter: Emitter = this._register(new Emitter()); + readonly onDidFocusFilter: Event = this._onDidFocusFilter.event; + + private readonly _onDidClearFilterText: Emitter = this._register(new Emitter()); + readonly onDidClearFilterText: Event = this._onDidClearFilterText.event; + constructor( options: IViewPaneOptions, @IInstantiationService instantiationService: IInstantiationService, @@ -108,8 +114,8 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, ) { - super({ ...(options as IViewPaneOptions), id: Constants.MARKERS_VIEW_ID, ariaHeaderLabel: Messages.MARKERS_PANEL_TITLE_PROBLEMS }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); - this.panelFoucusContextKey = Constants.MarkerViewFocusContextKey.bindTo(contextKeyService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + this.smallLayoutContextKey = Constants.MarkersViewSmallLayoutContextKey.bindTo(this.contextKeyService); this.panelState = new Memento(Constants.MARKERS_VIEW_STORAGE_ID, storageService).getMemento(StorageScope.WORKSPACE); this.markersViewModel = this._register(instantiationService.createInstance(MarkersViewModel, this.panelState['multiline'])); this._register(this.markersViewModel.onDidChange(marker => this.onDidChangeViewState(marker))); @@ -119,15 +125,16 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { this.rangeHighlightDecorations = this._register(this.instantiationService.createInstance(RangeHighlightDecorations)); // actions - this.collapseAllAction = this._register(new Action('vs.tree.collapse', localize('collapseAll', "Collapse All"), 'monaco-tree-action codicon-collapse-all', true, async () => this.collapseAll())); - this.filterAction = this._register(this.instantiationService.createInstance(MarkersFilterAction, { + this.regiserActions(); + this.filters = this._register(new MarkersFilters({ filterText: this.panelState['filter'] || '', filterHistory: this.panelState['filterHistory'] || [], showErrors: this.panelState['showErrors'] !== false, showWarnings: this.panelState['showWarnings'] !== false, showInfos: this.panelState['showInfos'] !== false, excludedFiles: !!this.panelState['useFilesExclude'], - activeFile: !!this.panelState['activeFile'] + activeFile: !!this.panelState['activeFile'], + layout: new dom.Dimension(0, 0) })); } @@ -146,9 +153,6 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { this.updateFilter(); - this._register(this.onDidFocus(() => this.panelFoucusContextKey.set(true))); - this._register(this.onDidBlur(() => this.panelFoucusContextKey.set(false))); - this._register(this.onDidChangeVisibility(visible => { if (visible) { this.refreshPanel(); @@ -157,7 +161,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { } })); - this.filterActionBar!.push(this.filterAction); + this.filterActionBar!.push(new Action(`workbench.actions.treeView.${this.id}.filter`)); this.renderContent(); } @@ -166,22 +170,21 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { } public layoutBody(height: number, width: number): void { - const wasSmallLayout = this.isSmallLayout; - this.isSmallLayout = width < 600 && height > 100; - if (this.isSmallLayout !== wasSmallLayout) { - this.updateActions(); + const wasSmallLayout = this.smallLayout; + this.smallLayout = width < 600 && height > 100; + if (this.smallLayout !== wasSmallLayout) { if (this.filterActionBar) { - dom.toggleClass(this.filterActionBar.getContainer(), 'hide', !this.isSmallLayout); + dom.toggleClass(this.filterActionBar.getContainer(), 'hide', !this.smallLayout); } } - const contentHeight = this.isSmallLayout ? height - 44 : height; + const contentHeight = this.smallLayout ? height - 44 : height; if (this.tree) { this.tree.layout(contentHeight, width); } if (this.messageBoxContainer) { this.messageBoxContainer.style.height = `${contentHeight}px`; } - this.filterAction.layout(this.isSmallLayout ? width : width - 200); + this.filters.layout = new dom.Dimension(this.smallLayout ? width : width - 200, height); } public focus(): void { @@ -197,14 +200,48 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { } public focusFilter(): void { - this.filterAction.focus(); + this._onDidFocusFilter.fire(); } - public getActions(): IAction[] { - if (this.isSmallLayout) { - return [this.collapseAllAction]; - } - return [this.filterAction, this.collapseAllAction]; + public clearFilterText(): void { + this._onDidClearFilterText.fire(); + } + + private regiserActions(): void { + const that = this; + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.actions.treeView.${that.id}.collapseAll`, + title: localize('collapseAll', "Collapse All"), + menu: { + id: MenuId.ViewTitle, + when: ContextKeyEqualsExpr.create('view', that.id), + group: 'navigation', + order: Number.MAX_SAFE_INTEGER, + }, + icon: { id: 'codicon/collapse-all' } + }); + } + async run(): Promise { + return that.collapseAll(); + } + })); + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.actions.treeView.${that.id}.filter`, + title: localize('filter', "Filter"), + menu: { + id: MenuId.ViewTitle, + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', that.id), Constants.MarkersViewSmallLayoutContextKey.negate()), + group: 'navigation', + order: 1, + }, + }); + } + async run(): Promise { } + })); } public showQuickFixes(marker: Marker): void { @@ -279,7 +316,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { const { total, filtered } = this.getFilterStats(); this.tree.toggleVisibility(total === 0 || filtered === 0); this.renderMessage(); - this._onDidFilter.fire(); + this._onDidChangeFilterStats.fire(this.getFilterStats()); } } @@ -292,7 +329,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { return; } let resourceMarkers: ResourceMarkers[] = []; - if (this.filterAction.activeFile) { + if (this.filters.activeFile) { if (this.currentActiveResource) { const activeResourceMarkers = this.markersWorkbenchService.markersModel.getResourceMarkers(this.currentActiveResource); if (activeResourceMarkers) { @@ -307,11 +344,11 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { private updateFilter() { this.cachedFilterStats = undefined; - this.filter.options = new FilterOptions(this.filterAction.filterText, this.getFilesExcludeExpressions(), this.filterAction.showWarnings, this.filterAction.showErrors, this.filterAction.showInfos); + this.filter.options = new FilterOptions(this.filters.filterText, this.getFilesExcludeExpressions(), this.filters.showWarnings, this.filters.showErrors, this.filters.showInfos); if (this.tree) { this.tree.refilter(); } - this._onDidFilter.fire(); + this._onDidChangeFilterStats.fire(this.getFilterStats()); const { total, filtered } = this.getFilterStats(); if (this.tree) { @@ -321,7 +358,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { } private getFilesExcludeExpressions(): { root: URI, expression: IExpression }[] | IExpression { - if (!this.filterAction.excludedFiles) { + if (!this.filters.excludedFiles) { return []; } @@ -338,7 +375,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { private createFilterActionBar(parent: HTMLElement): void { this.filterActionBar = this._register(new ActionBar(parent, { actionViewItemProvider: action => this.getActionViewItem(action) })); dom.addClass(this.filterActionBar.getContainer(), 'markers-panel-filter-container'); - dom.toggleClass(this.filterActionBar.getContainer(), 'hide', !this.isSmallLayout); + dom.toggleClass(this.filterActionBar.getContainer(), 'hide', !this.smallLayout); } private createMessageBox(parent: HTMLElement): void { @@ -416,7 +453,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { this._register(this.tree.onContextMenu(this.onContextMenu, this)); this._register(this.configurationService.onDidChangeConfiguration(e => { - if (this.filterAction.excludedFiles && e.affectsConfiguration('files.exclude')) { + if (this.filters.excludedFiles && e.affectsConfiguration('files.exclude')) { this.updateFilter(); } })); @@ -424,7 +461,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { // move focus to input, whenever a key is pressed in the panel container this._register(domEvent(parent, 'keydown')(e => { if (this.keybindingService.mightProducePrintableCharacter(new StandardKeyboardEvent(e))) { - this.filterAction.focus(); + this.focusFilter(); } })); @@ -462,7 +499,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { if (this.tree) { this._register(this.tree.onDidChangeSelection(() => this.onSelected())); } - this._register(this.filterAction.onDidChange((event: IMarkersFilterActionChangeEvent) => { + this._register(this.filters.onDidChange((event: IMarkersFiltersChangeEvent) => { this.reportFilteringUsed(); if (event.activeFile) { this.refreshPanel(); @@ -508,7 +545,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { private onActiveEditorChanged(): void { this.setCurrentActiveEditor(); - if (this.filterAction.activeFile) { + if (this.filters.activeFile) { this.refreshPanel(); } this.autoReveal(); @@ -552,7 +589,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { if (filtered === 0) { this.messageBoxContainer.style.display = 'block'; this.messageBoxContainer.setAttribute('tabIndex', '0'); - if (this.filterAction.activeFile) { + if (this.filters.activeFile) { this.renderFilterMessageForActiveFile(this.messageBoxContainer); } else { if (total > 0) { @@ -611,16 +648,16 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { } private clearFilters(): void { - this.filterAction.filterText = ''; - this.filterAction.excludedFiles = false; - this.filterAction.showErrors = true; - this.filterAction.showWarnings = true; - this.filterAction.showInfos = true; + this.filters.filterText = ''; + this.filters.excludedFiles = false; + this.filters.showErrors = true; + this.filters.showWarnings = true; + this.filters.showInfos = true; } private autoReveal(focus: boolean = false): void { // No need to auto reveal if active file filter is on - if (this.filterAction.activeFile || !this.tree) { + if (this.filters.activeFile || !this.tree) { return; } let autoReveal = this.configurationService.getValue('problems.autoReveal'); @@ -749,16 +786,12 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { } public getActionViewItem(action: IAction): IActionViewItem | undefined { - if (action.id === MarkersFilterAction.ID) { - return this.instantiationService.createInstance(MarkersFilterActionViewItem, this.filterAction, this); + if (action.id === `workbench.actions.treeView.${this.id}.filter`) { + return this.instantiationService.createInstance(MarkersFilterActionViewItem, action, this); } return super.getActionViewItem(action); } - getFilterOptions(): FilterOptions { - return this.filter.options; - } - getFilterStats(): { total: number; filtered: number; } { if (!this.cachedFilterStats) { this.cachedFilterStats = this.computeFilterStats(); @@ -790,11 +823,11 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { private reportFilteringUsed(): void { const data = { - errors: this.filterAction.showErrors, - warnings: this.filterAction.showWarnings, - infos: this.filterAction.showInfos, - activeFile: this.filterAction.activeFile, - excludedFiles: this.filterAction.excludedFiles, + errors: this.filters.showErrors, + warnings: this.filters.showWarnings, + infos: this.filters.showInfos, + activeFile: this.filters.activeFile, + excludedFiles: this.filters.excludedFiles, }; /* __GDPR__ "problems.filter" : { @@ -809,13 +842,13 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { } saveState(): void { - this.panelState['filter'] = this.filterAction.filterText; - this.panelState['filterHistory'] = this.filterAction.filterHistory; - this.panelState['showErrors'] = this.filterAction.showErrors; - this.panelState['showWarnings'] = this.filterAction.showWarnings; - this.panelState['showInfos'] = this.filterAction.showInfos; - this.panelState['useFilesExclude'] = this.filterAction.excludedFiles; - this.panelState['activeFile'] = this.filterAction.activeFile; + this.panelState['filter'] = this.filters.filterText; + this.panelState['filterHistory'] = this.filters.filterHistory; + this.panelState['showErrors'] = this.filters.showErrors; + this.panelState['showWarnings'] = this.filters.showWarnings; + this.panelState['showInfos'] = this.filters.showInfos; + this.panelState['useFilesExclude'] = this.filters.excludedFiles; + this.panelState['activeFile'] = this.filters.activeFile; this.panelState['multiline'] = this.markersViewModel.multiline; super.saveState(); diff --git a/src/vs/workbench/contrib/markers/browser/markersViewActions.ts b/src/vs/workbench/contrib/markers/browser/markersViewActions.ts index 839661e78e9..0f424cd9e16 100644 --- a/src/vs/workbench/contrib/markers/browser/markersViewActions.ts +++ b/src/vs/workbench/contrib/markers/browser/markersViewActions.ts @@ -5,7 +5,7 @@ import { Delayer } from 'vs/base/common/async'; import * as DOM from 'vs/base/browser/dom'; -import { Action, IActionChangeEvent, IAction, IActionRunner } from 'vs/base/common/actions'; +import { Action, IAction, IActionRunner } from 'vs/base/common/actions'; import { HistoryInputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { KeyCode } from 'vs/base/common/keyCodes'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; @@ -14,7 +14,7 @@ import Messages from 'vs/workbench/contrib/markers/browser/messages'; import Constants from 'vs/workbench/contrib/markers/browser/constants'; import { IThemeService, registerThemingParticipant, ICssStyleCollector, IColorTheme } from 'vs/platform/theme/common/themeService'; import { attachInputBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; -import { toDisposable } from 'vs/base/common/lifecycle'; +import { toDisposable, Disposable } from 'vs/base/common/lifecycle'; import { BaseActionViewItem, ActionViewItem, ActionBar, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import { badgeBackground, badgeForeground, contrastBorder, inputActiveOptionBorder, inputActiveOptionBackground } from 'vs/platform/theme/common/colorRegistry'; import { localize } from 'vs/nls'; @@ -23,7 +23,6 @@ import { ContextScopedHistoryInputBox } from 'vs/platform/browser/contextScopedH import { Marker } from 'vs/workbench/contrib/markers/browser/markersModel'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { Event, Emitter } from 'vs/base/common/event'; -import { FilterOptions } from 'vs/workbench/contrib/markers/browser/markersFilterOptions'; import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdown'; import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; import { IViewsService } from 'vs/workbench/common/views'; @@ -44,16 +43,17 @@ export class ShowProblemsPanelAction extends Action { } } -export interface IMarkersFilterActionChangeEvent extends IActionChangeEvent { +export interface IMarkersFiltersChangeEvent { filterText?: boolean; excludedFiles?: boolean; showWarnings?: boolean; showErrors?: boolean; showInfos?: boolean; activeFile?: boolean; + layout?: boolean; } -export interface IMarkersFilterActionOptions { +export interface IMarkersFiltersOptions { filterText: string; filterHistory: string[]; showErrors: boolean; @@ -61,17 +61,16 @@ export interface IMarkersFilterActionOptions { showInfos: boolean; excludedFiles: boolean; activeFile: boolean; + layout: DOM.Dimension; } -export class MarkersFilterAction extends Action { +export class MarkersFilters extends Disposable { - public static readonly ID: string = 'workbench.actions.problems.filter'; + private readonly _onDidChange: Emitter = this._register(new Emitter()); + readonly onDidChange: Event = this._onDidChange.event; - private readonly _onFocus: Emitter = this._register(new Emitter()); - readonly onFocus: Event = this._onFocus.event; - - constructor(options: IMarkersFilterActionOptions) { - super(MarkersFilterAction.ID, Messages.MARKERS_PANEL_ACTION_TOOLTIP_FILTER, 'markers-panel-action-filter', true); + constructor(options: IMarkersFiltersOptions) { + super(); this._filterText = options.filterText; this._showErrors = options.showErrors; this._showWarnings = options.showWarnings; @@ -79,6 +78,7 @@ export class MarkersFilterAction extends Action { this._excludedFiles = options.excludedFiles; this._activeFile = options.activeFile; this.filterHistory = options.filterHistory; + this._layout = options.layout; } private _filterText: string; @@ -88,7 +88,7 @@ export class MarkersFilterAction extends Action { set filterText(filterText: string) { if (this._filterText !== filterText) { this._filterText = filterText; - this._onDidChange.fire({ filterText: true }); + this._onDidChange.fire({ filterText: true }); } } @@ -101,7 +101,7 @@ export class MarkersFilterAction extends Action { set excludedFiles(filesExclude: boolean) { if (this._excludedFiles !== filesExclude) { this._excludedFiles = filesExclude; - this._onDidChange.fire({ excludedFiles: true }); + this._onDidChange.fire({ excludedFiles: true }); } } @@ -112,7 +112,7 @@ export class MarkersFilterAction extends Action { set activeFile(activeFile: boolean) { if (this._activeFile !== activeFile) { this._activeFile = activeFile; - this._onDidChange.fire({ activeFile: true }); + this._onDidChange.fire({ activeFile: true }); } } @@ -123,7 +123,7 @@ export class MarkersFilterAction extends Action { set showWarnings(showWarnings: boolean) { if (this._showWarnings !== showWarnings) { this._showWarnings = showWarnings; - this._onDidChange.fire({ showWarnings: true }); + this._onDidChange.fire({ showWarnings: true }); } } @@ -134,7 +134,7 @@ export class MarkersFilterAction extends Action { set showErrors(showErrors: boolean) { if (this._showErrors !== showErrors) { this._showErrors = showErrors; - this._onDidChange.fire({ showErrors: true }); + this._onDidChange.fire({ showErrors: true }); } } @@ -145,35 +145,34 @@ export class MarkersFilterAction extends Action { set showInfos(showInfos: boolean) { if (this._showInfos !== showInfos) { this._showInfos = showInfos; - this._onDidChange.fire({ showInfos: true }); + this._onDidChange.fire({ showInfos: true }); } } - focus(): void { - this._onFocus.fire(); + private _layout: DOM.Dimension = new DOM.Dimension(0, 0); + get layout(): DOM.Dimension { + return this._layout; } - - layout(width: number): void { - if (width > 600) { - this.class = 'markers-panel-action-filter grow'; - } else if (width < 400) { - this.class = 'markers-panel-action-filter small'; - } else { - this.class = 'markers-panel-action-filter'; + set layout(layout: DOM.Dimension) { + if (this._layout.width !== layout.width || this._layout.height !== layout.height) { + this._layout = layout; + this._onDidChange.fire({ layout: true }); } } } export interface IMarkerFilterController { - onDidFilter: Event; - getFilterOptions(): FilterOptions; + readonly onDidFocusFilter: Event; + readonly onDidClearFilterText: Event; + readonly filters: MarkersFilters; + readonly onDidChangeFilterStats: Event<{ total: number, filtered: number }>; getFilterStats(): { total: number, filtered: number }; } class FiltersDropdownMenuActionViewItem extends DropdownMenuActionViewItem { constructor( - action: IAction, private filterAction: MarkersFilterAction, actionRunner: IActionRunner, + action: IAction, private filters: MarkersFilters, actionRunner: IActionRunner, @IContextMenuService contextMenuService: IContextMenuService ) { super(action, @@ -194,53 +193,53 @@ class FiltersDropdownMenuActionViewItem extends DropdownMenuActionViewItem { private getActions(): IAction[] { return [ { - checked: this.filterAction.showErrors, + checked: this.filters.showErrors, class: undefined, enabled: true, id: 'showErrors', label: Messages.MARKERS_PANEL_FILTER_LABEL_SHOW_ERRORS, - run: async () => this.filterAction.showErrors = !this.filterAction.showErrors, + run: async () => this.filters.showErrors = !this.filters.showErrors, tooltip: '', dispose: () => null }, { - checked: this.filterAction.showWarnings, + checked: this.filters.showWarnings, class: undefined, enabled: true, id: 'showWarnings', label: Messages.MARKERS_PANEL_FILTER_LABEL_SHOW_WARNINGS, - run: async () => this.filterAction.showWarnings = !this.filterAction.showWarnings, + run: async () => this.filters.showWarnings = !this.filters.showWarnings, tooltip: '', dispose: () => null }, { - checked: this.filterAction.showInfos, + checked: this.filters.showInfos, class: undefined, enabled: true, id: 'showInfos', label: Messages.MARKERS_PANEL_FILTER_LABEL_SHOW_INFOS, - run: async () => this.filterAction.showInfos = !this.filterAction.showInfos, + run: async () => this.filters.showInfos = !this.filters.showInfos, tooltip: '', dispose: () => null }, new Separator(), { - checked: this.filterAction.activeFile, + checked: this.filters.activeFile, class: undefined, enabled: true, id: 'activeFile', label: Messages.MARKERS_PANEL_FILTER_LABEL_ACTIVE_FILE, - run: async () => this.filterAction.activeFile = !this.filterAction.activeFile, + run: async () => this.filters.activeFile = !this.filters.activeFile, tooltip: '', dispose: () => null }, { - checked: this.filterAction.excludedFiles, + checked: this.filters.excludedFiles, class: undefined, enabled: true, id: 'useFilesExclude', label: Messages.MARKERS_PANEL_FILTER_LABEL_EXCLUDED_FILES, - run: async () => this.filterAction.excludedFiles = !this.filterAction.excludedFiles, + run: async () => this.filters.excludedFiles = !this.filters.excludedFiles, tooltip: '', dispose: () => null }, @@ -263,7 +262,7 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { private readonly filtersAction: IAction; constructor( - readonly action: MarkersFilterAction, + action: IAction, private filterController: IMarkerFilterController, @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextViewService private readonly contextViewService: IContextViewService, @@ -274,10 +273,11 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { this.focusContextKey = Constants.MarkerViewFilterFocusContextKey.bindTo(contextKeyService); this.delayedFilterUpdate = new Delayer(200); this._register(toDisposable(() => this.delayedFilterUpdate.cancel())); - this._register(action.onFocus(() => this.focus())); + this._register(filterController.onDidFocusFilter(() => this.focus())); + this._register(filterController.onDidClearFilterText(() => this.clearFilterText())); this.filtersAction = new Action('markersFiltersAction', Messages.MARKERS_PANEL_ACTION_TOOLTIP_MORE_FILTERS, 'markers-filters codicon-filter'); this.filtersAction.checked = this.hasFiltersChanged(); - this._register(action.onDidChange(() => this.filtersAction.checked = this.hasFiltersChanged())); + this._register(filterController.filters.onDidChange(e => this.onDidFiltersChange(e))); } render(container: HTMLElement): void { @@ -285,7 +285,7 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { DOM.addClass(this.container, 'markers-panel-action-filter-container'); this.element = DOM.append(this.container, DOM.$('')); - this.element.className = this.action.class || ''; + this.element.className = this.class; this.createInput(this.element); this.createControls(this.element); this.updateClass(); @@ -299,23 +299,36 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { } } + private clearFilterText(): void { + if (this.filterInputBox) { + this.filterInputBox.value = ''; + } + } + + private onDidFiltersChange(e: IMarkersFiltersChangeEvent): void { + this.filtersAction.checked = this.hasFiltersChanged(); + if (e.layout) { + this.updateClass(); + } + } + private hasFiltersChanged(): boolean { - return !this.action.showErrors || !this.action.showWarnings || !this.action.showInfos || this.action.excludedFiles || this.action.activeFile; + return !this.filterController.filters.showErrors || !this.filterController.filters.showWarnings || !this.filterController.filters.showInfos || this.filterController.filters.excludedFiles || this.filterController.filters.activeFile; } private createInput(container: HTMLElement): void { this.filterInputBox = this._register(this.instantiationService.createInstance(ContextScopedHistoryInputBox, container, this.contextViewService, { placeholder: Messages.MARKERS_PANEL_FILTER_PLACEHOLDER, ariaLabel: Messages.MARKERS_PANEL_FILTER_ARIA_LABEL, - history: this.action.filterHistory + history: this.filterController.filters.filterHistory })); this.filterInputBox.inputElement.setAttribute('aria-labelledby', 'markers-panel-arialabel'); this._register(attachInputBoxStyler(this.filterInputBox, this.themeService)); - this.filterInputBox.value = this.action.filterText; + this.filterInputBox.value = this.filterController.filters.filterText; this._register(this.filterInputBox.onDidChange(filter => this.delayedFilterUpdate.trigger(() => this.onDidInputChange(this.filterInputBox!)))); - this._register(this.action.onDidChange((event: IMarkersFilterActionChangeEvent) => { + this._register(this.filterController.filters.onDidChange((event: IMarkersFiltersChangeEvent) => { if (event.filterText) { - this.filterInputBox!.value = this.action.filterText; + this.filterInputBox!.value = this.filterController.filters.filterText; } })); this._register(DOM.addStandardDisposableListener(this.filterInputBox.inputElement, DOM.EventType.KEY_DOWN, (e: any) => this.onInputKeyDown(e, this.filterInputBox!))); @@ -349,14 +362,14 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { filterBadge.style.color = foreground; })); this.updateBadge(); - this._register(this.filterController.onDidFilter(() => this.updateBadge())); + this._register(this.filterController.onDidChangeFilterStats(() => this.updateBadge())); } private createFilters(container: HTMLElement): void { const actionbar = this._register(new ActionBar(container, { actionViewItemProvider: action => { if (action.id === this.filtersAction.id) { - return this.instantiationService.createInstance(FiltersDropdownMenuActionViewItem, action, this.action, this.actionRunner); + return this.instantiationService.createInstance(FiltersDropdownMenuActionViewItem, action, this.filterController.filters, this.actionRunner); } return undefined; } @@ -366,8 +379,8 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { private onDidInputChange(inputbox: HistoryInputBox) { inputbox.addToHistory(); - this.action.filterText = inputbox.value; - this.action.filterHistory = inputbox.getHistory(); + this.filterController.filters.filterText = inputbox.value; + this.filterController.filters.filterHistory = inputbox.getHistory(); } private updateBadge(): void { @@ -399,7 +412,7 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { private onInputKeyDown(event: StandardKeyboardEvent, filterInputBox: HistoryInputBox) { let handled = false; if (event.equals(KeyCode.Escape)) { - filterInputBox.value = ''; + this.clearFilterText(); handled = true; } if (handled) { @@ -410,11 +423,21 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { protected updateClass(): void { if (this.element && this.container) { - this.element.className = this.action.class || ''; + this.element.className = this.class; DOM.toggleClass(this.container, 'grow', DOM.hasClass(this.element, 'grow')); this.adjustInputBox(); } } + + protected get class(): string { + if (this.filterController.filters.layout.width > 600) { + return 'markers-panel-action-filter grow'; + } else if (this.filterController.filters.layout.width < 400) { + return 'markers-panel-action-filter small'; + } else { + return 'markers-panel-action-filter'; + } + } } export class QuickFixAction extends Action { diff --git a/src/vs/workbench/contrib/output/browser/output.contribution.ts b/src/vs/workbench/contrib/output/browser/output.contribution.ts index fc1a2c61224..ea14e0d998b 100644 --- a/src/vs/workbench/contrib/output/browser/output.contribution.ts +++ b/src/vs/workbench/contrib/output/browser/output.contribution.ts @@ -4,15 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; +import * as aria from 'vs/base/browser/ui/aria/aria'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; -import { MenuId, MenuRegistry, SyncActionDescriptor, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; +import { MenuId, MenuRegistry, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { OutputService, LogContentProvider } from 'vs/workbench/contrib/output/browser/outputServices'; -import { ToggleOutputAction, ClearOutputAction, OpenLogOutputFile, ShowLogsOutputChannelAction, OpenOutputLogFileAction } from 'vs/workbench/contrib/output/browser/outputActions'; -import { OUTPUT_MODE_ID, OUTPUT_MIME, OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, LOG_SCHEME, LOG_MODE_ID, LOG_MIME, CONTEXT_ACTIVE_LOG_OUTPUT } from 'vs/workbench/contrib/output/common/output'; +import { OUTPUT_MODE_ID, OUTPUT_MIME, OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, LOG_SCHEME, LOG_MODE_ID, LOG_MIME, CONTEXT_ACTIVE_LOG_OUTPUT, CONTEXT_OUTPUT_SCROLL_LOCK } from 'vs/workbench/contrib/output/common/output'; import { OutputViewPane } from 'vs/workbench/contrib/output/browser/outputView'; import { IEditorRegistry, Extensions as EditorExtensions, EditorDescriptor } from 'vs/workbench/browser/editor'; import { LogViewer, LogViewerInput } from 'vs/workbench/contrib/output/browser/logViewer'; @@ -21,9 +20,18 @@ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWo import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { ViewContainer, IViewContainersRegistry, ViewContainerLocation, Extensions as ViewContainerExtensions, IViewsRegistry } from 'vs/workbench/common/views'; +import { ViewContainer, IViewContainersRegistry, ViewContainerLocation, Extensions as ViewContainerExtensions, IViewsRegistry, IViewsService } from 'vs/workbench/common/views'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { IQuickPickItem, IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { IOutputChannelDescriptor, IFileOutputChannelDescriptor } from 'vs/workbench/services/output/common/output'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { assertIsDefined } from 'vs/base/common/types'; +import { TogglePanelAction } from 'vs/workbench/browser/panel'; +import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; +import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; +import { ContextKeyEqualsExpr, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; // Register Service registerSingleton(IOutputService, OutputService); @@ -43,18 +51,18 @@ ModesRegistry.registerLanguage({ }); // register output container +const toggleOutputAcitonId = 'workbench.action.output.toggleOutput'; +const toggleOutputActionKeybindings = { + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_U, + linux: { + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_H) // On Ubuntu Ctrl+Shift+U is taken by some global OS command + } +}; const VIEW_CONTAINER: ViewContainer = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ id: OUTPUT_VIEW_ID, name: nls.localize('output', "Output"), ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [OUTPUT_VIEW_ID, OUTPUT_VIEW_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), - focusCommand: { - id: ToggleOutputAction.ID, keybindings: { - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_U, - linux: { - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_H) // On Ubuntu Ctrl+Shift+U is taken by some global OS command - } - } - } + focusCommand: { id: toggleOutputAcitonId, keybindings: toggleOutputActionKeybindings } }, ViewContainerLocation.Panel); Registry.as(ViewContainerExtensions.ViewsRegistry).registerViews([{ @@ -86,62 +94,220 @@ class OutputContribution implements IWorkbenchContribution { Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(OutputContribution, LifecyclePhase.Restored); -// register toggle output action globally -const actionRegistry = Registry.as(ActionExtensions.WorkbenchActions); -actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(ToggleOutputAction, ToggleOutputAction.ID, ToggleOutputAction.LABEL, { - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_U, - linux: { - primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_H) // On Ubuntu Ctrl+Shift+U is taken by some global OS command - } -}), 'View: Toggle Output', nls.localize('viewCategory', "View")); - -actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(ClearOutputAction, ClearOutputAction.ID, ClearOutputAction.LABEL), - 'View: Clear Output', nls.localize('viewCategory', "View")); - -const devCategory = nls.localize('developer', "Developer"); -actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(ShowLogsOutputChannelAction, ShowLogsOutputChannelAction.ID, ShowLogsOutputChannelAction.LABEL), 'Developer: Show Logs...', devCategory); -actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(OpenOutputLogFileAction, OpenOutputLogFileAction.ID, OpenOutputLogFileAction.LABEL), 'Developer: Open Log File...', devCategory); - -// Define clear command, contribute to editor context menu registerAction2(class extends Action2 { constructor() { super({ - id: 'editor.action.clearoutput', - title: { value: nls.localize('clearOutput.label', "Clear Output"), original: 'Clear Output' }, + id: `workbench.output.action.switchBetweenOutputs`, + title: nls.localize('switchToOutput.label', "Switch to Output"), menu: { - id: MenuId.EditorContext, - when: CONTEXT_IN_OUTPUT + id: MenuId.ViewTitle, + when: ContextKeyEqualsExpr.create('view', OUTPUT_VIEW_ID), + group: 'navigation', + order: 1 }, }); } - run(accessor: ServicesAccessor) { - const activeChannel = accessor.get(IOutputService).getActiveChannel(); + async run(accessor: ServicesAccessor, channelId: string): Promise { + accessor.get(IOutputService).showChannel(channelId); + } +}); +registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.output.action.clearOutput`, + title: { value: nls.localize('clearOutput.label', "Clear Output"), original: 'Clear Output' }, + category: nls.localize('viewCategory', "View"), + menu: [{ + id: MenuId.ViewTitle, + when: ContextKeyEqualsExpr.create('view', OUTPUT_VIEW_ID), + group: 'navigation', + order: 2 + }, { + id: MenuId.CommandPalette + }, { + id: MenuId.EditorContext, + when: CONTEXT_IN_OUTPUT + }], + icon: { id: 'codicon/clear-all' } + }); + } + async run(accessor: ServicesAccessor): Promise { + const outputService = accessor.get(IOutputService); + const activeChannel = outputService.getActiveChannel(); if (activeChannel) { activeChannel.clear(); + aria.status(nls.localize('outputCleared', "Output was cleared")); + } + } +}); +registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.output.action.turnOffAutoScroll`, + title: nls.localize('outputScrollOff', "Turn Auto Scrolling Off"), + menu: { + id: MenuId.ViewTitle, + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', OUTPUT_VIEW_ID), CONTEXT_OUTPUT_SCROLL_LOCK.negate()), + group: 'navigation', + order: 3, + }, + icon: { id: 'codicon/unlock' } + }); + } + async run(accessor: ServicesAccessor): Promise { + const outputView = accessor.get(IViewsService).getActiveViewWithId(OUTPUT_VIEW_ID)!; + outputView.scrollLock = true; + } +}); +registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.output.action.turnOnAutoScroll`, + title: nls.localize('outputScrollOn', "Turn Auto Scrolling On"), + menu: { + id: MenuId.ViewTitle, + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', OUTPUT_VIEW_ID), CONTEXT_OUTPUT_SCROLL_LOCK), + group: 'navigation', + order: 3, + }, + icon: { id: 'codicon/lock' }, + }); + } + async run(accessor: ServicesAccessor): Promise { + const outputView = accessor.get(IViewsService).getActiveViewWithId(OUTPUT_VIEW_ID)!; + outputView.scrollLock = false; + } +}); +registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.action.openActiveLogOutputFile`, + title: { value: nls.localize('openActiveLogOutputFile', "Open Log Output File"), original: 'Open Log Output File' }, + menu: [{ + id: MenuId.ViewTitle, + when: ContextKeyEqualsExpr.create('view', OUTPUT_VIEW_ID), + group: 'navigation', + order: 4 + }, { + id: MenuId.CommandPalette, + when: CONTEXT_ACTIVE_LOG_OUTPUT, + }], + icon: { id: 'codicon/go-to-file' }, + precondition: CONTEXT_ACTIVE_LOG_OUTPUT + }); + } + async run(accessor: ServicesAccessor): Promise { + const outputService = accessor.get(IOutputService); + const editorService = accessor.get(IEditorService); + const instantiationService = accessor.get(IInstantiationService); + const logFileOutputChannelDescriptor = this.getLogFileOutputChannelDescriptor(outputService); + if (logFileOutputChannelDescriptor) { + await editorService.openEditor(instantiationService.createInstance(LogViewerInput, logFileOutputChannelDescriptor)); + } + } + private getLogFileOutputChannelDescriptor(outputService: IOutputService): IFileOutputChannelDescriptor | null { + const channel = outputService.getActiveChannel(); + if (channel) { + const descriptor = outputService.getChannelDescriptors().filter(c => c.id === channel.id)[0]; + if (descriptor && descriptor.file && descriptor.log) { + return descriptor; + } + } + return null; + } +}); + +// register toggle output action globally +registerAction2(class extends Action2 { + constructor() { + super({ + id: toggleOutputAcitonId, + title: { value: nls.localize('toggleOutput', "Toggle Output"), original: 'Toggle Output' }, + category: { value: nls.localize('viewCategory', "View"), original: 'View' }, + menu: { + id: MenuId.CommandPalette, + }, + keybinding: { + ...toggleOutputActionKeybindings, + ...{ + weight: KeybindingWeight.WorkbenchContrib, + when: undefined + } + }, + }); + } + async run(accessor: ServicesAccessor): Promise { + const panelService = accessor.get(IPanelService); + const layoutService = accessor.get(IWorkbenchLayoutService); + return new class ToggleOutputAction extends TogglePanelAction { + constructor() { + super(toggleOutputAcitonId, 'Toggle Output', OUTPUT_VIEW_ID, panelService, layoutService); + } + }().run(); + } +}); + +const devCategory = { value: nls.localize('developer', "Developer"), original: 'Developer' }; +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'workbench.action.showLogs', + title: { value: nls.localize('showLogs', "Show Logs..."), original: 'Show Logs...' }, + category: devCategory, + menu: { + id: MenuId.CommandPalette, + }, + }); + } + async run(accessor: ServicesAccessor): Promise { + const outputService = accessor.get(IOutputService); + const quickInputService = accessor.get(IQuickInputService); + const entries: { id: string, label: string }[] = outputService.getChannelDescriptors().filter(c => c.file && c.log) + .map(({ id, label }) => ({ id, label })); + + const entry = await quickInputService.pick(entries, { placeHolder: nls.localize('selectlog', "Select Log") }); + if (entry) { + return outputService.showChannel(entry.id); } } }); +interface IOutputChannelQuickPickItem extends IQuickPickItem { + channel: IOutputChannelDescriptor; +} + registerAction2(class extends Action2 { constructor() { super({ - id: 'workbench.action.openActiveLogOutputFile', - title: { value: nls.localize('openActiveLogOutputFile', "Open Active Log Output File"), original: 'Open Active Log Output File' }, + id: 'workbench.action.openLogFile', + title: { value: nls.localize('openLogFile', "Open Log File..."), original: 'Open Log File...' }, + category: devCategory, menu: { id: MenuId.CommandPalette, - when: CONTEXT_ACTIVE_LOG_OUTPUT }, }); } - run(accessor: ServicesAccessor) { - accessor.get(IInstantiationService).createInstance(OpenLogOutputFile).run(); + async run(accessor: ServicesAccessor): Promise { + const outputService = accessor.get(IOutputService); + const quickInputService = accessor.get(IQuickInputService); + const instantiationService = accessor.get(IInstantiationService); + const editorService = accessor.get(IEditorService); + + const entries: IOutputChannelQuickPickItem[] = outputService.getChannelDescriptors().filter(c => c.file && c.log) + .map(channel => ({ id: channel.id, label: channel.label, channel })); + + const entry = await quickInputService.pick(entries, { placeHolder: nls.localize('selectlogFile', "Select Log file") }); + if (entry) { + assertIsDefined(entry.channel.file); + await editorService.openEditor(instantiationService.createInstance(LogViewerInput, (entry.channel as IFileOutputChannelDescriptor))); + } } }); MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, { group: '4_panels', command: { - id: ToggleOutputAction.ID, + id: toggleOutputAcitonId, title: nls.localize({ key: 'miToggleOutput', comment: ['&& denotes a mnemonic'] }, "&&Output") }, order: 1 diff --git a/src/vs/workbench/contrib/output/browser/outputActions.ts b/src/vs/workbench/contrib/output/browser/outputActions.ts deleted file mode 100644 index db610d418d1..00000000000 --- a/src/vs/workbench/contrib/output/browser/outputActions.ts +++ /dev/null @@ -1,269 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as nls from 'vs/nls'; -import * as aria from 'vs/base/browser/ui/aria/aria'; -import { IAction, Action } from 'vs/base/common/actions'; -import { IOutputChannelRegistry, Extensions as OutputExt, IOutputChannelDescriptor, IFileOutputChannelDescriptor } from 'vs/workbench/services/output/common/output'; -import { IOutputService, OUTPUT_VIEW_ID } from 'vs/workbench/contrib/output/common/output'; -import { SelectActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; -import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; -import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; -import { TogglePanelAction } from 'vs/workbench/browser/panel'; -import { attachSelectBoxStyler } from 'vs/platform/theme/common/styler'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { groupBy } from 'vs/base/common/arrays'; -import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { LogViewerInput } from 'vs/workbench/contrib/output/browser/logViewer'; -import { ISelectOptionItem } from 'vs/base/browser/ui/selectBox/selectBox'; -import { assertIsDefined } from 'vs/base/common/types'; - -export class ToggleOutputAction extends TogglePanelAction { - - static readonly ID = 'workbench.action.output.toggleOutput'; - static readonly LABEL = nls.localize('toggleOutput', "Toggle Output"); - - constructor( - id: string, label: string, - @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, - @IPanelService panelService: IPanelService, - ) { - super(id, label, OUTPUT_VIEW_ID, panelService, layoutService); - } -} - -export class ClearOutputAction extends Action { - - static readonly ID = 'workbench.output.action.clearOutput'; - static readonly LABEL = nls.localize('clearOutput', "Clear Output"); - - constructor( - id: string, label: string, - @IOutputService private readonly outputService: IOutputService - ) { - super(id, label, 'output-action codicon-clear-all'); - } - - run(): Promise { - const activeChannel = this.outputService.getActiveChannel(); - if (activeChannel) { - activeChannel.clear(); - aria.status(nls.localize('outputCleared', "Output was cleared")); - } - return Promise.resolve(true); - } -} - -// this action can be triggered in two ways: -// 1. user clicks the action icon, In which case the action toggles the lock state -// 2. user clicks inside the output view, which sets the lock, Or unsets it if they click the last line. -export class ToggleOrSetOutputScrollLockAction extends Action { - - static readonly ID = 'workbench.output.action.toggleOutputScrollLock'; - static readonly LABEL = nls.localize({ key: 'toggleOutputScrollLock', comment: ['Turn on / off automatic output scrolling'] }, "Toggle Output Scroll Lock"); - - constructor(id: string, label: string, @IOutputService private readonly outputService: IOutputService) { - super(id, label, 'output-action codicon-unlock'); - this._register(this.outputService.onActiveOutputChannel(channel => { - const activeChannel = this.outputService.getActiveChannel(); - if (activeChannel) { - this.setClassAndLabel(activeChannel.scrollLock); - } - })); - } - - run(newLockState?: boolean): Promise { - - const activeChannel = this.outputService.getActiveChannel(); - if (activeChannel) { - if (typeof (newLockState) === 'boolean') { - activeChannel.scrollLock = newLockState; - } - else { - activeChannel.scrollLock = !activeChannel.scrollLock; - } - this.setClassAndLabel(activeChannel.scrollLock); - } - - return Promise.resolve(true); - } - - private setClassAndLabel(locked: boolean) { - if (locked) { - this.class = 'output-action codicon-lock'; - this.label = nls.localize('outputScrollOn', "Turn Auto Scrolling On"); - } else { - this.class = 'output-action codicon-unlock'; - this.label = nls.localize('outputScrollOff', "Turn Auto Scrolling Off"); - } - } -} - -export class SwitchOutputAction extends Action { - - static readonly ID = 'workbench.output.action.switchBetweenOutputs'; - - constructor(@IOutputService private readonly outputService: IOutputService) { - super(SwitchOutputAction.ID, nls.localize('switchToOutput.label', "Switch to Output")); - - this.class = 'output-action switch-to-output'; - } - - run(channelId: string): Promise { - return this.outputService.showChannel(channelId); - } -} - -export class SwitchOutputActionViewItem extends SelectActionViewItem { - - private static readonly SEPARATOR = '─────────'; - - private outputChannels: IOutputChannelDescriptor[] = []; - private logChannels: IOutputChannelDescriptor[] = []; - - constructor( - action: IAction, - @IOutputService private readonly outputService: IOutputService, - @IThemeService themeService: IThemeService, - @IContextViewService contextViewService: IContextViewService - ) { - super(null, action, [], 0, contextViewService, { ariaLabel: nls.localize('outputChannels', 'Output Channels.') }); - - let outputChannelRegistry = Registry.as(OutputExt.OutputChannels); - this._register(outputChannelRegistry.onDidRegisterChannel(() => this.updateOtions())); - this._register(outputChannelRegistry.onDidRemoveChannel(() => this.updateOtions())); - this._register(this.outputService.onActiveOutputChannel(() => this.updateOtions())); - this._register(attachSelectBoxStyler(this.selectBox, themeService)); - - this.updateOtions(); - } - - protected getActionContext(option: string, index: number): string { - const channel = index < this.outputChannels.length ? this.outputChannels[index] : this.logChannels[index - this.outputChannels.length - 1]; - return channel ? channel.id : option; - } - - private updateOtions(): void { - const groups = groupBy(this.outputService.getChannelDescriptors(), (c1: IOutputChannelDescriptor, c2: IOutputChannelDescriptor) => { - if (!c1.log && c2.log) { - return -1; - } - if (c1.log && !c2.log) { - return 1; - } - return 0; - }); - this.outputChannels = groups[0] || []; - this.logChannels = groups[1] || []; - const showSeparator = this.outputChannels.length && this.logChannels.length; - const separatorIndex = showSeparator ? this.outputChannels.length : -1; - const options: string[] = [...this.outputChannels.map(c => c.label), ...(showSeparator ? [SwitchOutputActionViewItem.SEPARATOR] : []), ...this.logChannels.map(c => nls.localize('logChannel', "Log ({0})", c.label))]; - let selected = 0; - const activeChannel = this.outputService.getActiveChannel(); - if (activeChannel) { - selected = this.outputChannels.map(c => c.id).indexOf(activeChannel.id); - if (selected === -1) { - const logChannelIndex = this.logChannels.map(c => c.id).indexOf(activeChannel.id); - selected = logChannelIndex !== -1 ? separatorIndex + 1 + logChannelIndex : 0; - } - } - this.setOptions(options.map((label, index) => { text: label, isDisabled: (index === separatorIndex ? true : false) }), Math.max(0, selected)); - } -} - -export class OpenLogOutputFile extends Action { - - static readonly ID = 'workbench.output.action.openLogOutputFile'; - static readonly LABEL = nls.localize('openInLogViewer', "Open Log File"); - - constructor( - @IOutputService private readonly outputService: IOutputService, - @IEditorService private readonly editorService: IEditorService, - @IInstantiationService private readonly instantiationService: IInstantiationService - ) { - super(OpenLogOutputFile.ID, OpenLogOutputFile.LABEL, 'output-action codicon-go-to-file'); - this._register(this.outputService.onActiveOutputChannel(this.update, this)); - this.update(); - } - - private update(): void { - this.enabled = !!this.getLogFileOutputChannelDescriptor(); - } - - async run(): Promise { - const logFileOutputChannelDescriptor = this.getLogFileOutputChannelDescriptor(); - if (logFileOutputChannelDescriptor) { - await this.editorService.openEditor(this.instantiationService.createInstance(LogViewerInput, logFileOutputChannelDescriptor)); - } - } - - private getLogFileOutputChannelDescriptor(): IFileOutputChannelDescriptor | null { - const channel = this.outputService.getActiveChannel(); - if (channel) { - const descriptor = this.outputService.getChannelDescriptors().filter(c => c.id === channel.id)[0]; - if (descriptor && descriptor.file && descriptor.log) { - return descriptor; - } - } - return null; - } -} - -export class ShowLogsOutputChannelAction extends Action { - - static readonly ID = 'workbench.action.showLogs'; - static readonly LABEL = nls.localize('showLogs', "Show Logs..."); - - constructor(id: string, label: string, - @IQuickInputService private readonly quickInputService: IQuickInputService, - @IOutputService private readonly outputService: IOutputService - ) { - super(id, label); - } - - async run(): Promise { - const entries: { id: string, label: string }[] = this.outputService.getChannelDescriptors().filter(c => c.file && c.log) - .map(({ id, label }) => ({ id, label })); - - const entry = await this.quickInputService.pick(entries, { placeHolder: nls.localize('selectlog', "Select Log") }); - if (entry) { - return this.outputService.showChannel(entry.id); - } - } -} - -interface IOutputChannelQuickPickItem extends IQuickPickItem { - channel: IOutputChannelDescriptor; -} - -export class OpenOutputLogFileAction extends Action { - - static readonly ID = 'workbench.action.openLogFile'; - static readonly LABEL = nls.localize('openLogFile', "Open Log File..."); - - constructor(id: string, label: string, - @IQuickInputService private readonly quickInputService: IQuickInputService, - @IOutputService private readonly outputService: IOutputService, - @IEditorService private readonly editorService: IEditorService, - @IInstantiationService private readonly instantiationService: IInstantiationService - ) { - super(id, label); - } - - async run(): Promise { - const entries: IOutputChannelQuickPickItem[] = this.outputService.getChannelDescriptors().filter(c => c.file && c.log) - .map(channel => ({ id: channel.id, label: channel.label, channel })); - - const entry = await this.quickInputService.pick(entries, { placeHolder: nls.localize('selectlogFile', "Select Log file") }); - if (entry) { - assertIsDefined(entry.channel.file); - await this.editorService.openEditor(this.instantiationService.createInstance(LogViewerInput, (entry.channel as IFileOutputChannelDescriptor))); - } - } -} diff --git a/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index bcc70dcadcf..c7fbe8223f5 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; import { IAction } from 'vs/base/common/actions'; -import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IActionViewItem, SelectActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -13,11 +13,10 @@ import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { EditorInput, EditorOptions } from 'vs/workbench/common/editor'; import { AbstractTextResourceEditor } from 'vs/workbench/browser/parts/editor/textResourceEditor'; -import { OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, IOutputChannel, CONTEXT_ACTIVE_LOG_OUTPUT } from 'vs/workbench/contrib/output/common/output'; -import { SwitchOutputAction, SwitchOutputActionViewItem, ClearOutputAction, ToggleOrSetOutputScrollLockAction, OpenLogOutputFile } from 'vs/workbench/contrib/output/browser/outputActions'; +import { OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, IOutputChannel, CONTEXT_ACTIVE_LOG_OUTPUT, CONTEXT_OUTPUT_SCROLL_LOCK } from 'vs/workbench/contrib/output/common/output'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -26,10 +25,15 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { CursorChangeReason } from 'vs/editor/common/controller/cursorEvents'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IOutputChannelDescriptor, IOutputChannelRegistry, Extensions } from 'vs/workbench/services/output/common/output'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { attachSelectBoxStyler } from 'vs/platform/theme/common/styler'; +import { ISelectOptionItem } from 'vs/base/browser/ui/selectBox/selectBox'; +import { groupBy } from 'vs/base/common/arrays'; export class OutputViewPane extends ViewPane { @@ -38,6 +42,10 @@ export class OutputViewPane extends ViewPane { private editorPromise: Promise | null = null; private actions: IAction[] | undefined; + private readonly scrollLockContextKey: IContextKey; + get scrollLock(): boolean { return !!this.scrollLockContextKey.get(); } + set scrollLock(scrollLock: boolean) { this.scrollLockContextKey.set(scrollLock); } + constructor( options: IViewPaneOptions, @IKeybindingService keybindingService: IKeybindingService, @@ -52,6 +60,7 @@ export class OutputViewPane extends ViewPane { @ITelemetryService telemetryService: ITelemetryService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + this.scrollLockContextKey = CONTEXT_OUTPUT_SCROLL_LOCK.bindTo(this.contextKeyService); this.editor = instantiationService.createInstance(OutputEditor); this._register(this.editor.onTitleAreaUpdate(() => { this.updateTitle(this.editor.getTitle()); @@ -81,7 +90,7 @@ export class OutputViewPane extends ViewPane { const codeEditor = this.editor.getControl(); this._register(codeEditor.onDidChangeModelContent(() => { const activeChannel = this.outputService.getActiveChannel(); - if (activeChannel && !activeChannel.scrollLock) { + if (activeChannel && !this.scrollLock) { this.editor.revealLastLine(); } })); @@ -98,9 +107,7 @@ export class OutputViewPane extends ViewPane { if (model && this.actions) { const newPositionLine = e.position.lineNumber; const lastLine = model.getLineCount(); - const newLockState = lastLine !== newPositionLine; - const lockAction = this.actions.filter((action) => action.id === ToggleOrSetOutputScrollLockAction.ID)[0]; - lockAction.run(newLockState); + this.scrollLock = lastLine !== newPositionLine; } })); } @@ -112,10 +119,10 @@ export class OutputViewPane extends ViewPane { getActions(): IAction[] { if (!this.actions) { this.actions = [ - this._register(this.instantiationService.createInstance(SwitchOutputAction)), - this._register(this.instantiationService.createInstance(ClearOutputAction, ClearOutputAction.ID, ClearOutputAction.LABEL)), - this._register(this.instantiationService.createInstance(ToggleOrSetOutputScrollLockAction, ToggleOrSetOutputScrollLockAction.ID, ToggleOrSetOutputScrollLockAction.LABEL)), - this._register(this.instantiationService.createInstance(OpenLogOutputFile)) + // this._register(this.instantiationService.createInstance(SwitchOutputAction)), + // this._register(this.instantiationService.createInstance(ClearOutputAction, ClearOutputAction.ID, ClearOutputAction.LABEL)), + // this._register(this.instantiationService.createInstance(ToggleOrSetOutputScrollLockAction, ToggleOrSetOutputScrollLockAction.ID, ToggleOrSetOutputScrollLockAction.LABEL)), + // this._register(this.instantiationService.createInstance(OpenLogOutputFile)) ]; } return [...super.getActions(), ...this.actions]; @@ -126,13 +133,12 @@ export class OutputViewPane extends ViewPane { } getActionViewItem(action: IAction): IActionViewItem | undefined { - if (action.id === SwitchOutputAction.ID) { + if (action.id === 'workbench.output.action.switchBetweenOutputs') { return this.instantiationService.createInstance(SwitchOutputActionViewItem, action); } return super.getActionViewItem(action); } - private onDidChangeVisibility(visible: boolean): void { this.editor.setVisible(visible); let channel: IOutputChannel | undefined = undefined; @@ -268,3 +274,61 @@ export class OutputEditor extends AbstractTextResourceEditor { CONTEXT_IN_OUTPUT.bindTo(scopedContextKeyService).set(true); } } + +class SwitchOutputActionViewItem extends SelectActionViewItem { + + private static readonly SEPARATOR = '─────────'; + + private outputChannels: IOutputChannelDescriptor[] = []; + private logChannels: IOutputChannelDescriptor[] = []; + + constructor( + action: IAction, + @IOutputService private readonly outputService: IOutputService, + @IThemeService themeService: IThemeService, + @IContextViewService contextViewService: IContextViewService + ) { + super(null, action, [], 0, contextViewService, { ariaLabel: nls.localize('outputChannels', 'Output Channels.') }); + + let outputChannelRegistry = Registry.as(Extensions.OutputChannels); + this._register(outputChannelRegistry.onDidRegisterChannel(() => this.updateOtions())); + this._register(outputChannelRegistry.onDidRemoveChannel(() => this.updateOtions())); + this._register(this.outputService.onActiveOutputChannel(() => this.updateOtions())); + this._register(attachSelectBoxStyler(this.selectBox, themeService)); + + this.updateOtions(); + } + + protected getActionContext(option: string, index: number): string { + const channel = index < this.outputChannels.length ? this.outputChannels[index] : this.logChannels[index - this.outputChannels.length - 1]; + return channel ? channel.id : option; + } + + private updateOtions(): void { + const groups = groupBy(this.outputService.getChannelDescriptors(), (c1: IOutputChannelDescriptor, c2: IOutputChannelDescriptor) => { + if (!c1.log && c2.log) { + return -1; + } + if (c1.log && !c2.log) { + return 1; + } + return 0; + }); + this.outputChannels = groups[0] || []; + this.logChannels = groups[1] || []; + const showSeparator = this.outputChannels.length && this.logChannels.length; + const separatorIndex = showSeparator ? this.outputChannels.length : -1; + const options: string[] = [...this.outputChannels.map(c => c.label), ...(showSeparator ? [SwitchOutputActionViewItem.SEPARATOR] : []), ...this.logChannels.map(c => nls.localize('logChannel', "Log ({0})", c.label))]; + let selected = 0; + const activeChannel = this.outputService.getActiveChannel(); + if (activeChannel) { + selected = this.outputChannels.map(c => c.id).indexOf(activeChannel.id); + if (selected === -1) { + const logChannelIndex = this.logChannels.map(c => c.id).indexOf(activeChannel.id); + selected = logChannelIndex !== -1 ? separatorIndex + 1 + logChannelIndex : 0; + } + } + this.setOptions(options.map((label, index) => { text: label, isDisabled: (index === separatorIndex ? true : false) }), Math.max(0, selected)); + } +} + diff --git a/src/vs/workbench/contrib/output/common/output.ts b/src/vs/workbench/contrib/output/common/output.ts index 238e2b7758f..483be2132b5 100644 --- a/src/vs/workbench/contrib/output/common/output.ts +++ b/src/vs/workbench/contrib/output/common/output.ts @@ -52,6 +52,8 @@ export const CONTEXT_IN_OUTPUT = new RawContextKey('inOutput', false); export const CONTEXT_ACTIVE_LOG_OUTPUT = new RawContextKey('activeLogOutput', false); +export const CONTEXT_OUTPUT_SCROLL_LOCK = new RawContextKey(`outputView.scrollLock`, false); + export const IOutputService = createDecorator(OUTPUT_SERVICE_ID); /** @@ -105,11 +107,6 @@ export interface IOutputChannel { */ label: string; - /** - * Returns the value indicating whether the channel has scroll locked. - */ - scrollLock: boolean; - /** * URI of the output channel. */ diff --git a/src/vs/workbench/contrib/output/test/browser/outputLinkProvider.test.ts b/src/vs/workbench/contrib/output/test/browser/outputLinkProvider.test.ts index ce131747b3c..a53b8c389e7 100644 --- a/src/vs/workbench/contrib/output/test/browser/outputLinkProvider.test.ts +++ b/src/vs/workbench/contrib/output/test/browser/outputLinkProvider.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { isMacintosh, isLinux, isWindows } from 'vs/base/common/platform'; import { OutputLinkComputer } from 'vs/workbench/contrib/output/common/outputLinkComputer'; -import { TestContextService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; function toOSPath(p: string): string { if (isMacintosh || isLinux) { diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts index bdd7e5e3b26..17bc4b85e9c 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts @@ -953,7 +953,7 @@ class CommandColumn extends Column { } private getAriaLabel(keybindingItemEntry: IKeybindingItemEntry): string { - return localize('commandAriaLabel', "Command is {0}.", keybindingItemEntry.keybindingItem.commandLabel ? keybindingItemEntry.keybindingItem.commandLabel : keybindingItemEntry.keybindingItem.command); + return keybindingItemEntry.keybindingItem.commandLabel ? keybindingItemEntry.keybindingItem.commandLabel : keybindingItemEntry.keybindingItem.command; } } diff --git a/src/vs/workbench/contrib/quickaccess/browser/quickAccess.contribution.ts b/src/vs/workbench/contrib/quickaccess/browser/quickAccess.contribution.ts new file mode 100644 index 00000000000..8a03f8bd360 --- /dev/null +++ b/src/vs/workbench/contrib/quickaccess/browser/quickAccess.contribution.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { IQuickAccessRegistry, Extensions } from 'vs/platform/quickinput/common/quickAccess'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { HelpQuickAccessProvider } from 'vs/platform/quickinput/browser/helpQuickAccess'; +import { ViewQuickAccessProvider, VIEW_QUICK_ACCESS_PREFIX } from 'vs/workbench/contrib/quickaccess/browser/viewQuickAccess'; +import { QUICK_ACCESS_COMMAND_ID } from 'vs/workbench/contrib/quickaccess/browser/quickAccessCommands'; +import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; + +const registry = Registry.as(Extensions.Quickaccess); + +registry.defaultProvider = { + ctor: HelpQuickAccessProvider, + prefix: '', + placeholder: localize('helpQuickAccessPlaceholder', "Type '?' to get help on the actions you can take from here."), + helpEntries: [{ description: localize('gotoFileQuickAccess', "Go to File"), needsEditor: false }] +}; + +registry.registerQuickAccessProvider({ + ctor: HelpQuickAccessProvider, + prefix: '?', + placeholder: localize('helpQuickAccessPlaceholder', "Type '?' to get help on the actions you can take from here."), + helpEntries: [{ description: localize('helpQuickAccess', "Show all Quick Access Providers"), needsEditor: false }] +}); + +registry.registerQuickAccessProvider({ + ctor: ViewQuickAccessProvider, + prefix: VIEW_QUICK_ACCESS_PREFIX, + placeholder: localize('viewQuickAccessPlaceholder', "Type the name of a view, output channel or terminal to open."), + helpEntries: [{ description: localize('viewQuickAccess', "Open View"), needsEditor: false }] +}); + +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: QUICK_ACCESS_COMMAND_ID, title: { + value: localize('openQuickAccess', "Open Quick Access"), original: 'Open Quick Access' + }, + category: localize('quickAccess', "Quick Access") + } +}); diff --git a/src/vs/workbench/contrib/quickaccess/browser/quickAccessCommands.ts b/src/vs/workbench/contrib/quickaccess/browser/quickAccessCommands.ts new file mode 100644 index 00000000000..23b2521b043 --- /dev/null +++ b/src/vs/workbench/contrib/quickaccess/browser/quickAccessCommands.ts @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; + +export const QUICK_ACCESS_COMMAND_ID = 'workbench.action.openQuickAccess'; + +CommandsRegistry.registerCommand({ + id: QUICK_ACCESS_COMMAND_ID, + handler: async function (accessor: ServicesAccessor, prefix: string | null = null) { + const quickInputService = accessor.get(IQuickInputService); + + quickInputService.quickAccess.show(typeof prefix === 'string' ? prefix : undefined); + }, + description: { + description: `Quick access`, + args: [{ + name: 'prefix', + schema: { + 'type': 'string' + } + }] + } +}); diff --git a/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts b/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts new file mode 100644 index 00000000000..d19c973f300 --- /dev/null +++ b/src/vs/workbench/contrib/quickaccess/browser/viewQuickAccess.ts @@ -0,0 +1,201 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IQuickPick, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { IQuickAccessProvider } from 'vs/platform/quickinput/common/quickAccess'; +import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; +import { IViewDescriptorService, IViewsService, ViewContainer, IViewsRegistry, Extensions as ViewExtensions, IViewContainersRegistry } from 'vs/workbench/common/views'; +import { IOutputService } from 'vs/workbench/contrib/output/common/output'; +import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ViewletDescriptor } from 'vs/workbench/browser/viewlet'; +import { matchesFuzzy } from 'vs/base/common/filters'; +import { fuzzyContains } from 'vs/base/common/strings'; +import { withNullAsUndefined } from 'vs/base/common/types'; + +export const VIEW_QUICK_ACCESS_PREFIX = 'view '; + +interface IViewQuickPickItem extends IQuickPickItem { + containerLabel: string; + run: () => Promise; +} + +export class ViewQuickAccessProvider implements IQuickAccessProvider { + + constructor( + @IViewletService private readonly viewletService: IViewletService, + @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, + @IViewsService private readonly viewsService: IViewsService, + @IOutputService private readonly outputService: IOutputService, + @ITerminalService private readonly terminalService: ITerminalService, + @IPanelService private readonly panelService: IPanelService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + ) { + } + + provide(picker: IQuickPick, token: CancellationToken): IDisposable { + const disposables = new DisposableStore(); + + // Disable filtering & sorting, we control the results + picker.matchOnLabel = picker.matchOnDescription = picker.matchOnDetail = picker.sortByLabel = false; + + // Add all view items & filter on type + const updatePickerItems = () => picker.items = this.getViewPickItems(picker.value.trim().substr(VIEW_QUICK_ACCESS_PREFIX.length)); + disposables.add(picker.onDidChangeValue(() => updatePickerItems())); + updatePickerItems(); + + // Open the picked view on accept + disposables.add(picker.onDidAccept(() => { + const [item] = picker.selectedItems; + if (item) { + picker.hide(); + item.run(); + } + })); + + return disposables; + } + + private getViewPickItems(filter: string): Array { + const filteredViewEntries = this.doGetViewPickItems().filter(entry => { + if (!filter) { + return true; + } + + // Match fuzzy on label + entry.highlights = { label: withNullAsUndefined(matchesFuzzy(filter, entry.label, true)) }; + + // Return if we have a match on label or container + return entry.highlights.label || fuzzyContains(entry.containerLabel, filter); + }); + + // Map entries to container labels + const mapEntryToContainer = new Map(); + for (const entry of filteredViewEntries) { + if (!mapEntryToContainer.has(entry.label)) { + mapEntryToContainer.set(entry.label, entry.containerLabel); + } + } + + // Add separators for containers + const filteredViewEntriesWithSeparators: Array = []; + let lastContainer: string | undefined = undefined; + for (const entry of filteredViewEntries) { + if (lastContainer !== entry.containerLabel) { + lastContainer = entry.containerLabel; + + // When the entry container has a parent container, set container + // label as Parent / Child. For example, `Views / Explorer`. + let separatorLabel: string; + if (mapEntryToContainer.has(lastContainer)) { + separatorLabel = `${mapEntryToContainer.get(lastContainer)} / ${lastContainer}`; + } else { + separatorLabel = lastContainer; + } + + filteredViewEntriesWithSeparators.push({ type: 'separator', label: separatorLabel }); + + } + + filteredViewEntriesWithSeparators.push(entry); + } + + return filteredViewEntriesWithSeparators; + } + + private doGetViewPickItems(): Array { + const viewEntries: Array = []; + + const getViewEntriesForViewlet = (viewlet: ViewletDescriptor, viewContainer: ViewContainer): IViewQuickPickItem[] => { + const views = Registry.as(ViewExtensions.ViewsRegistry).getViews(viewContainer); + const result: IViewQuickPickItem[] = []; + for (const view of views) { + if (this.contextKeyService.contextMatchesRules(view.when)) { + result.push({ + label: view.name, + containerLabel: viewlet.name, + run: () => this.viewsService.openView(view.id, true) + }); + } + } + + return result; + }; + + // Viewlets + const viewlets = this.viewletService.getViewlets(); + for (const viewlet of viewlets) { + if (this.includeViewlet(viewlet)) { + viewEntries.push({ + label: viewlet.name, + containerLabel: localize('views', "Side Bar"), + run: () => this.viewletService.openViewlet(viewlet.id, true) + }); + } + } + + // Panels + const panels = this.panelService.getPanels(); + for (const panel of panels) { + viewEntries.push({ + label: panel.name, + containerLabel: localize('panels', "Panel"), + run: () => this.panelService.openPanel(panel.id, true) + }); + } + + // Viewlet Views + for (const viewlet of viewlets) { + const viewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).get(viewlet.id); + if (viewContainer) { + viewEntries.push(...getViewEntriesForViewlet(viewlet, viewContainer)); + } + } + + // Terminals + this.terminalService.terminalTabs.forEach((tab, tabIndex) => { + tab.terminalInstances.forEach((terminal, terminalIndex) => { + viewEntries.push({ + label: localize('terminalTitle', "{0}: {1}", `${tabIndex + 1}.${terminalIndex + 1}`, terminal.title), + containerLabel: localize('terminals', "Terminal"), + run: async () => { + await this.terminalService.showPanel(true); + + return this.terminalService.setActiveInstance(terminal); + } + }); + }); + }); + + // Output Channels + const channels = this.outputService.getChannelDescriptors(); + for (const channel of channels) { + viewEntries.push({ + label: channel.log ? localize('logChannel', "Log ({0})", channel.label) : channel.label, + containerLabel: localize('channels', "Output"), + run: () => this.outputService.showChannel(channel.id) + }); + } + + // Add generic ARIA label + viewEntries.forEach(entry => entry.ariaLabel = localize('entryAriaLabel', "{0}, view picker", entry.label)); + + return viewEntries; + } + + private includeViewlet(viewlet: ViewletDescriptor): boolean { + const viewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).get(viewlet.id); + if (viewContainer?.hideIfEmpty) { + return this.viewDescriptorService.getViewDescriptors(viewContainer).activeViewDescriptors.length > 0; + } + + return true; + } +} diff --git a/src/vs/workbench/contrib/remote/browser/remote.ts b/src/vs/workbench/contrib/remote/browser/remote.ts index 7d69b78bc83..9767a932bfa 100644 --- a/src/vs/workbench/contrib/remote/browser/remote.ts +++ b/src/vs/workbench/contrib/remote/browser/remote.ts @@ -584,26 +584,100 @@ Registry.as(WorkbenchActionExtensions.WorkbenchActions nls.localize('view', "View") ); +class VisibleProgress { -class ProgressReporter { - private _currentProgress: IProgress | null = null; - private lastReport: string | null = null; + private _isDisposed: boolean; + private _lastReport: string | null; + private _currentProgressPromiseResolve: (() => void) | null; + private _currentProgress: IProgress | null; + private _currentTimer: ReconnectionTimer2 | null; - constructor(currentProgress: IProgress | null) { - this._currentProgress = currentProgress; + public get lastReport(): string | null { + return this._lastReport; } - set currentProgress(progress: IProgress) { - this._currentProgress = progress; + constructor(progressService: IProgressService, location: ProgressLocation, initialReport: string | null, buttons: string[], onDidCancel: (choice: number | undefined, lastReport: string | null) => void) { + this._isDisposed = false; + this._lastReport = initialReport; + this._currentProgressPromiseResolve = null; + this._currentProgress = null; + this._currentTimer = null; + + const promise = new Promise((resolve) => this._currentProgressPromiseResolve = resolve); + + progressService.withProgress( + { location: location, buttons: buttons }, + (progress) => { if (!this._isDisposed) { this._currentProgress = progress; } return promise; }, + (choice) => onDidCancel(choice, this._lastReport) + ); + + if (this._lastReport) { + this.report(); + } } - report(message?: string) { + public dispose(): void { + this._isDisposed = true; + if (this._currentProgressPromiseResolve) { + this._currentProgressPromiseResolve(); + this._currentProgressPromiseResolve = null; + } + this._currentProgress = null; + if (this._currentTimer) { + this._currentTimer.dispose(); + this._currentTimer = null; + } + } + + public report(message?: string) { if (message) { - this.lastReport = message; + this._lastReport = message; } - if (this.lastReport && this._currentProgress) { - this._currentProgress.report({ message: this.lastReport }); + if (this._lastReport && this._currentProgress) { + this._currentProgress.report({ message: this._lastReport }); + } + } + + public startTimer(completionTime: number): void { + this.stopTimer(); + this._currentTimer = new ReconnectionTimer2(this, completionTime); + } + + public stopTimer(): void { + if (this._currentTimer) { + this._currentTimer.dispose(); + this._currentTimer = null; + } + } +} + +class ReconnectionTimer2 implements IDisposable { + private readonly _parent: VisibleProgress; + private readonly _completionTime: number; + private readonly _token: any; + + constructor(parent: VisibleProgress, completionTime: number) { + this._parent = parent; + this._completionTime = completionTime; + this._token = setInterval(() => this._render(), 1000); + this._render(); + } + + public dispose(): void { + clearInterval(this._token); + } + + private _render() { + const remainingTimeMs = this._completionTime - Date.now(); + if (remainingTimeMs < 0) { + return; + } + const remainingTime = Math.ceil(remainingTimeMs / 1000); + if (remainingTime === 1) { + this._parent.report(nls.localize('reconnectionWaitOne', "Attempting to reconnect in {0} second...", remainingTime)); + } else { + this._parent.report(nls.localize('reconnectionWaitMany', "Attempting to reconnect in {0} seconds...", remainingTime)); } } } @@ -618,58 +692,41 @@ class RemoteAgentConnectionStatusListener implements IWorkbenchContribution { ) { const connection = remoteAgentService.getConnection(); if (connection) { - let currentProgressPromiseResolve: (() => void) | null = null; - let progressReporter: ProgressReporter | null = null; - let lastLocation: ProgressLocation | null = null; - let currentTimer: ReconnectionTimer | null = null; + let visibleProgress: VisibleProgress | null = null; + let lastLocation: ProgressLocation.Dialog | ProgressLocation.Notification | null = null; let reconnectWaitEvent: ReconnectionWaitEvent | null = null; let disposableListener: IDisposable | null = null; - function showProgress(location: ProgressLocation, buttons: { label: string, callback: () => void }[]) { - if (currentProgressPromiseResolve) { - currentProgressPromiseResolve(); + function showProgress(location: ProgressLocation.Dialog | ProgressLocation.Notification, buttons: { label: string, callback: () => void }[], initialReport: string | null = null): VisibleProgress { + if (visibleProgress) { + visibleProgress.dispose(); + visibleProgress = null; } - const promise = new Promise((resolve) => currentProgressPromiseResolve = resolve); lastLocation = location; - if (location === ProgressLocation.Dialog) { - // Show dialog - progressService!.withProgress( - { location: ProgressLocation.Dialog, buttons: buttons.map(button => button.label) }, - (progress) => { if (progressReporter) { progressReporter.currentProgress = progress; } return promise; }, - (choice?) => { - // Handle choice from dialog - if (typeof choice !== 'undefined' && buttons[choice]) { - buttons[choice].callback(); - } else { - showProgress(ProgressLocation.Notification, buttons); - } - - progressReporter!.report(); - }); - } else { - // Show notification - progressService!.withProgress( - { location: ProgressLocation.Notification, buttons: buttons.map(button => button.label) }, - (progress) => { if (progressReporter) { progressReporter.currentProgress = progress; } return promise; }, - (choice?) => { - // Handle choice from dialog - if (typeof choice !== 'undefined' && buttons[choice]) { - buttons[choice].callback(); + return new VisibleProgress( + progressService, location, initialReport, buttons.map(button => button.label), + (choice, lastReport) => { + // Handle choice from dialog + if (typeof choice !== 'undefined' && buttons[choice]) { + buttons[choice].callback(); + } else { + if (location === ProgressLocation.Dialog) { + visibleProgress = showProgress(ProgressLocation.Notification, buttons, lastReport); } else { hideProgress(); } - }); - } + } + } + ); } function hideProgress() { - if (currentProgressPromiseResolve) { - currentProgressPromiseResolve(); + if (visibleProgress) { + visibleProgress.dispose(); + visibleProgress = null; } - - currentProgressPromiseResolve = null; } const reconnectButton = { @@ -689,9 +746,8 @@ class RemoteAgentConnectionStatusListener implements IWorkbenchContribution { }; connection.onDidStateChange((e) => { - if (currentTimer) { - currentTimer.dispose(); - currentTimer = null; + if (visibleProgress) { + visibleProgress.stopTimer(); } if (disposableListener) { @@ -700,33 +756,27 @@ class RemoteAgentConnectionStatusListener implements IWorkbenchContribution { } switch (e.type) { case PersistentConnectionEventType.ConnectionLost: - if (!currentProgressPromiseResolve) { - progressReporter = new ProgressReporter(null); - showProgress(ProgressLocation.Dialog, [reconnectButton, reloadButton]); + if (!visibleProgress) { + visibleProgress = showProgress(ProgressLocation.Dialog, [reconnectButton, reloadButton]); } - - progressReporter!.report(nls.localize('connectionLost', "Connection Lost")); + visibleProgress.report(nls.localize('connectionLost', "Connection Lost")); break; case PersistentConnectionEventType.ReconnectionWait: - hideProgress(); reconnectWaitEvent = e; - showProgress(lastLocation || ProgressLocation.Notification, [reconnectButton, reloadButton]); - currentTimer = new ReconnectionTimer(progressReporter!, Date.now() + 1000 * e.durationSeconds); + visibleProgress = showProgress(lastLocation || ProgressLocation.Notification, [reconnectButton, reloadButton]); + visibleProgress.startTimer(Date.now() + 1000 * e.durationSeconds); break; case PersistentConnectionEventType.ReconnectionRunning: - hideProgress(); - showProgress(lastLocation || ProgressLocation.Notification, [reloadButton]); - progressReporter!.report(nls.localize('reconnectionRunning', "Attempting to reconnect...")); + visibleProgress = showProgress(lastLocation || ProgressLocation.Notification, [reloadButton]); + visibleProgress.report(nls.localize('reconnectionRunning', "Attempting to reconnect...")); // Register to listen for quick input is opened disposableListener = contextKeyService.onDidChangeContext((contextKeyChangeEvent) => { const reconnectInteraction = new Set(['inQuickOpen']); if (contextKeyChangeEvent.affectsSome(reconnectInteraction)) { // Need to move from dialog if being shown and user needs to type in a prompt - if (lastLocation === ProgressLocation.Dialog && progressReporter !== null) { - hideProgress(); - showProgress(ProgressLocation.Notification, [reloadButton]); - progressReporter.report(); + if (lastLocation === ProgressLocation.Dialog && visibleProgress !== null) { + visibleProgress = showProgress(ProgressLocation.Notification, [reloadButton], visibleProgress.lastReport); } } }); @@ -734,7 +784,6 @@ class RemoteAgentConnectionStatusListener implements IWorkbenchContribution { break; case PersistentConnectionEventType.ReconnectionPermanentFailure: hideProgress(); - progressReporter = null; dialogService.show(Severity.Error, nls.localize('reconnectionPermanentFailure', "Cannot reconnect. Please reload the window."), [nls.localize('reloadWindow', "Reload Window"), nls.localize('cancel', "Cancel")], { cancelId: 1 }).then(result => { // Reload the window @@ -745,7 +794,6 @@ class RemoteAgentConnectionStatusListener implements IWorkbenchContribution { break; case PersistentConnectionEventType.ConnectionGain: hideProgress(); - progressReporter = null; break; } }); @@ -753,35 +801,5 @@ class RemoteAgentConnectionStatusListener implements IWorkbenchContribution { } } -class ReconnectionTimer implements IDisposable { - private readonly _progressReporter: ProgressReporter; - private readonly _completionTime: number; - private readonly _token: any; - - constructor(progressReporter: ProgressReporter, completionTime: number) { - this._progressReporter = progressReporter; - this._completionTime = completionTime; - this._token = setInterval(() => this._render(), 1000); - this._render(); - } - - public dispose(): void { - clearInterval(this._token); - } - - private _render() { - const remainingTimeMs = this._completionTime - Date.now(); - if (remainingTimeMs < 0) { - return; - } - const remainingTime = Math.ceil(remainingTimeMs / 1000); - if (remainingTime === 1) { - this._progressReporter.report(nls.localize('reconnectionWaitOne', "Attempting to reconnect in {0} second...", remainingTime)); - } else { - this._progressReporter.report(nls.localize('reconnectionWaitMany', "Attempting to reconnect in {0} seconds...", remainingTime)); - } - } -} - const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchContributionsRegistry.registerWorkbenchContribution(RemoteAgentConnectionStatusListener, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/scm/browser/activity.ts b/src/vs/workbench/contrib/scm/browser/activity.ts index 150e3b562e7..13ed604bd7f 100644 --- a/src/vs/workbench/contrib/scm/browser/activity.ts +++ b/src/vs/workbench/contrib/scm/browser/activity.ts @@ -159,8 +159,7 @@ export class SCMStatusController implements IWorkbenchContribution { disposables.add(this.statusbarService.addEntry({ text: c.title, tooltip: `${label} - ${c.tooltip}`, - command: c.id, - arguments: c.arguments + command: c }, 'status.scm', localize('status.scm', "Source Control"), MainThreadStatusBarAlignment.LEFT, 10000)); } diff --git a/src/vs/workbench/contrib/search/browser/openAnythingHandler.ts b/src/vs/workbench/contrib/search/browser/openAnythingHandler.ts index 7b3f04acbd6..a91deb9962b 100644 --- a/src/vs/workbench/contrib/search/browser/openAnythingHandler.ts +++ b/src/vs/workbench/contrib/search/browser/openAnythingHandler.ts @@ -16,7 +16,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchSearchConfiguration } from 'vs/workbench/contrib/search/common/search'; import { IRange } from 'vs/editor/common/core/range'; -import { compareItemsByScore, scoreItem, ScorerCache, prepareQuery } from 'vs/base/parts/quickopen/common/quickOpenScorer'; +import { compareItemsByScore, scoreItem, ScorerCache, prepareQuery } from 'vs/base/common/fuzzyScorer'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { isPromiseCanceledError } from 'vs/base/common/errors'; import { CancellationToken } from 'vs/base/common/cancellation'; diff --git a/src/vs/workbench/contrib/search/browser/openFileHandler.ts b/src/vs/workbench/contrib/search/browser/openFileHandler.ts index 3c88120a110..5329b1f91ae 100644 --- a/src/vs/workbench/contrib/search/browser/openFileHandler.ts +++ b/src/vs/workbench/contrib/search/browser/openFileHandler.ts @@ -13,7 +13,7 @@ import { basename, dirname, toLocalResource } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { QuickOpenEntry, QuickOpenModel } from 'vs/base/parts/quickopen/browser/quickOpenModel'; import { IAutoFocus } from 'vs/base/parts/quickopen/common/quickOpen'; -import { IPreparedQuery, prepareQuery } from 'vs/base/parts/quickopen/common/quickOpenScorer'; +import { IPreparedQuery, prepareQuery } from 'vs/base/common/fuzzyScorer'; import { IRange } from 'vs/editor/common/core/range'; import { getIconClasses } from 'vs/editor/common/services/getIconClasses'; import { IModelService } from 'vs/editor/common/services/modelService'; diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index c7a28d962bb..3d9deddf982 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -52,9 +52,9 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ExplorerViewPaneContainer } from 'vs/workbench/contrib/files/browser/explorerViewlet'; import { assertType, assertIsDefined } from 'vs/base/common/types'; -import { SearchViewPaneContainer } from 'vs/workbench/contrib/search/browser/searchViewlet'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditor'; +import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; registerSingleton(ISearchWorkbenchService, SearchWorkbenchService, true); registerSingleton(ISearchHistoryService, SearchHistoryService, true); @@ -80,6 +80,19 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'workbench.action.searchEditor.deleteResultBlock', + weight: KeybindingWeight.WorkbenchContrib, + when: SearchEditorConstants.InSearchEditor, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Backspace, + handler: accessor => { + const contextService = accessor.get(IContextKeyService).getContext(document.activeElement); + if (contextService.getValue(SearchEditorConstants.InSearchEditor.serialize())) { + (accessor.get(IEditorService).activeEditorPane as SearchEditor).deleteResultBlock(); + } + } +}); + KeybindingsRegistry.registerCommandAndKeybindingRule({ id: Constants.FocusSearchFromResults, weight: KeybindingWeight.WorkbenchContrib, @@ -486,7 +499,7 @@ class ShowAllSymbolsAction extends Action { const viewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ id: VIEWLET_ID, name: nls.localize('name', "Search"), - ctorDescriptor: new SyncDescriptor(SearchViewPaneContainer), + ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [VIEWLET_ID, `${VIEWLET_ID}.state`, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), hideIfEmpty: true, icon: 'codicon-search', order: 1 diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index e00d561e775..c82dcac7c67 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -34,7 +34,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ResourceNavigator, WorkbenchObjectTree, getSelectionKeyboardEvent } from 'vs/platform/list/browser/listService'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IProgressService, IProgressStep, IProgress } from 'vs/platform/progress/common/progress'; -import { IPatternInfo, ISearchComplete, ISearchConfiguration, ISearchConfigurationProperties, ITextQuery, VIEW_ID, SearchSortOrder, SearchCompletionExitCode } from 'vs/workbench/services/search/common/search'; +import { IPatternInfo, ISearchComplete, ISearchConfiguration, ISearchConfigurationProperties, ITextQuery, SearchSortOrder, SearchCompletionExitCode } from 'vs/workbench/services/search/common/search'; import { ISearchHistoryService, ISearchHistoryValues } from 'vs/workbench/contrib/search/common/searchHistoryService'; import { diffInserted, diffInsertedOutline, diffRemoved, diffRemovedOutline, editorFindMatchHighlight, editorFindMatchHighlightBorder, listActiveSelectionForeground, foreground } from 'vs/platform/theme/common/colorRegistry'; import { ICssStyleCollector, IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; @@ -179,7 +179,7 @@ export class SearchView extends ViewPane { @ITelemetryService telemetryService: ITelemetryService, ) { - super({ ...options, id: VIEW_ID, ariaHeaderLabel: nls.localize('searchView', "Search") }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this.container = dom.$('.search-view'); diff --git a/src/vs/workbench/contrib/search/browser/searchViewlet.ts b/src/vs/workbench/contrib/search/browser/searchViewlet.ts deleted file mode 100644 index 2754b71d7b8..00000000000 --- a/src/vs/workbench/contrib/search/browser/searchViewlet.ts +++ /dev/null @@ -1,42 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IStorageService } from 'vs/platform/storage/common/storage'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { VIEWLET_ID, VIEW_ID } from 'vs/workbench/services/search/common/search'; -import { SearchView } from 'vs/workbench/contrib/search/browser/searchView'; -import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; -import { IViewDescriptorService } from 'vs/workbench/common/views'; - - -export class SearchViewPaneContainer extends ViewPaneContainer { - - constructor( - @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, - @ITelemetryService telemetryService: ITelemetryService, - @IWorkspaceContextService protected contextService: IWorkspaceContextService, - @IStorageService protected storageService: IStorageService, - @IConfigurationService configurationService: IConfigurationService, - @IInstantiationService protected instantiationService: IInstantiationService, - @IThemeService themeService: IThemeService, - @IContextMenuService contextMenuService: IContextMenuService, - @IExtensionService extensionService: IExtensionService, - @IViewDescriptorService viewDescriptorService: IViewDescriptorService - ) { - super(VIEWLET_ID, `${VIEWLET_ID}.state`, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService, viewDescriptorService); - } - - getSearchView(): SearchView | undefined { - const view = super.getView(VIEW_ID); - return view ? view as SearchView : undefined; - } -} diff --git a/src/vs/workbench/contrib/search/browser/searchWidget.ts b/src/vs/workbench/contrib/search/browser/searchWidget.ts index 428becff1e7..706c368d91a 100644 --- a/src/vs/workbench/contrib/search/browser/searchWidget.ts +++ b/src/vs/workbench/contrib/search/browser/searchWidget.ts @@ -634,10 +634,17 @@ export class SearchWidget extends Widget { this._onSearchSubmit.fire({ triggeredOnType, delay }); } - contextLines() { + getContextLines() { return this.showContextCheckbox.checked ? +this.contextLinesInput.value : 0; } + modifyContextLines(increase: boolean) { + const current = +this.contextLinesInput.value; + const modified = current + (increase ? 1 : -1); + this.showContextCheckbox.checked = modified !== 0; + this.contextLinesInput.value = '' + modified; + } + toggleContextLines() { this.showContextCheckbox.checked = !this.showContextCheckbox.checked; this.onContextLinesChanged(); diff --git a/src/vs/workbench/contrib/search/test/browser/queryBuilder.test.ts b/src/vs/workbench/contrib/search/test/browser/queryBuilder.test.ts index 621cf435ac4..09b44564799 100644 --- a/src/vs/workbench/contrib/search/test/browser/queryBuilder.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/queryBuilder.test.ts @@ -13,8 +13,9 @@ import { TestInstantiationService } from 'vs/platform/instantiation/test/common/ import { IFolderQuery, IPatternInfo, QueryType, ITextQuery, IFileQuery } from 'vs/workbench/services/search/common/search'; import { IWorkspaceContextService, toWorkspaceFolder, Workspace, toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; import { ISearchPathsInfo, QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder'; -import { TestContextService, TestEnvironmentService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestEnvironmentService } from 'vs/workbench/test/browser/workbenchTestServices'; import { isWindows } from 'vs/base/common/platform'; +import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; const DEFAULT_EDITOR_CONFIG = {}; const DEFAULT_USER_CONFIG = { useRipgrep: true, useIgnoreFiles: true, useGlobalIgnoreFiles: true }; diff --git a/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts b/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts index a1c96e21aa2..c8f8bbbccac 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchViewlet.test.ts @@ -13,8 +13,8 @@ import { IFileMatch, ITextSearchMatch, OneLineRange, QueryType, SearchSortOrder import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { TestWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; import { FileMatch, Match, searchMatchComparer, SearchResult } from 'vs/workbench/contrib/search/common/searchModel'; -import { TestContextService } from 'vs/workbench/test/browser/workbenchTestServices'; import { isWindows } from 'vs/base/common/platform'; +import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; suite('Search - Viewlet', () => { let instantiation: TestInstantiationService; diff --git a/src/vs/workbench/contrib/search/test/electron-browser/queryBuilder.test.ts b/src/vs/workbench/contrib/search/test/electron-browser/queryBuilder.test.ts index 5222422e2b4..6f1e394203f 100644 --- a/src/vs/workbench/contrib/search/test/electron-browser/queryBuilder.test.ts +++ b/src/vs/workbench/contrib/search/test/electron-browser/queryBuilder.test.ts @@ -8,9 +8,9 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IWorkspaceContextService, toWorkspaceFolder, Workspace } from 'vs/platform/workspace/common/workspace'; import { ISearchPathsInfo, QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder'; -import { TestContextService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestEnvironmentService } from 'vs/workbench/test/electron-browser/workbenchTestServices'; import { assertEqualSearchPathResults, getUri, patternsToIExpression, globalGlob, fixPath } from 'vs/workbench/contrib/search/test/browser/queryBuilder.test'; +import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; const DEFAULT_EDITOR_CONFIG = {}; const DEFAULT_USER_CONFIG = { useRipgrep: true, useIgnoreFiles: true, useGlobalIgnoreFiles: true }; diff --git a/src/vs/workbench/contrib/searchEditor/browser/constants.ts b/src/vs/workbench/contrib/searchEditor/browser/constants.ts index 7de8978e745..39c264c57c9 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/constants.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/constants.ts @@ -7,11 +7,15 @@ import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; export const OpenInEditorCommandId = 'search.action.openInEditor'; export const OpenNewEditorCommandId = 'search.action.openNewEditor'; +export const OpenNewEditorToSideCommandId = 'search.action.openNewEditorToSide'; export const ToggleSearchEditorCaseSensitiveCommandId = 'toggleSearchEditorCaseSensitive'; export const ToggleSearchEditorWholeWordCommandId = 'toggleSearchEditorWholeWord'; export const ToggleSearchEditorRegexCommandId = 'toggleSearchEditorRegex'; export const ToggleSearchEditorContextLinesCommandId = 'toggleSearchEditorContextLines'; +export const IncreaseSearchEditorContextLinesCommandId = 'increaseSearchEditorContextLines'; +export const DecreaseSearchEditorContextLinesCommandId = 'decreaseSearchEditorContextLines'; + export const RerunSearchEditorSearchCommandId = 'rerunSearchEditorSearch'; export const CleanSearchEditorStateCommandId = 'cleanSearchEditorState'; export const SelectAllSearchEditorMatchesCommandId = 'selectAllSearchEditorMatches'; diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts index 63948369b3a..e74e02bdbab 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts @@ -24,7 +24,7 @@ import { Extensions as EditorInputExtensions, IEditorInputFactory, IEditorInputF import * as SearchConstants from 'vs/workbench/contrib/search/common/constants'; import * as SearchEditorConstants from 'vs/workbench/contrib/searchEditor/browser/constants'; import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditor'; -import { OpenResultsInEditorAction, OpenSearchEditorAction, toggleSearchEditorCaseSensitiveCommand, toggleSearchEditorContextLinesCommand, toggleSearchEditorRegexCommand, toggleSearchEditorWholeWordCommand, selectAllSearchEditorMatchesCommand, RerunSearchEditorSearchAction } from 'vs/workbench/contrib/searchEditor/browser/searchEditorActions'; +import { OpenResultsInEditorAction, OpenSearchEditorAction, toggleSearchEditorCaseSensitiveCommand, toggleSearchEditorContextLinesCommand, toggleSearchEditorRegexCommand, toggleSearchEditorWholeWordCommand, selectAllSearchEditorMatchesCommand, RerunSearchEditorSearchAction, OpenSearchEditorToSideAction, modifySearchEditorContextLinesCommand } from 'vs/workbench/contrib/searchEditor/browser/searchEditorActions'; import { getOrMakeSearchEditorInput, SearchEditorInput } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; @@ -151,6 +151,24 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_L } }); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: SearchEditorConstants.IncreaseSearchEditorContextLinesCommandId, + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(SearchEditorConstants.InSearchEditor), + handler: (accessor: ServicesAccessor) => modifySearchEditorContextLinesCommand(accessor, true), + primary: KeyMod.Alt | KeyCode.KEY_L, + mac: { primary: KeyMod.Alt | KeyCode.US_EQUAL } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: SearchEditorConstants.DecreaseSearchEditorContextLinesCommandId, + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(SearchEditorConstants.InSearchEditor), + handler: (accessor: ServicesAccessor) => modifySearchEditorContextLinesCommand(accessor, false), + primary: KeyMod.Alt | KeyCode.KEY_L, + mac: { primary: KeyMod.Alt | KeyCode.US_MINUS } +}); + KeybindingsRegistry.registerCommandAndKeybindingRule({ id: SearchEditorConstants.SelectAllSearchEditorMatchesCommandId, weight: KeybindingWeight.WorkbenchContrib, @@ -183,6 +201,10 @@ registry.registerWorkbenchAction( SyncActionDescriptor.create(OpenSearchEditorAction, OpenSearchEditorAction.ID, OpenSearchEditorAction.LABEL), 'Search Editor: Open New Search Editor', category); +registry.registerWorkbenchAction( + SyncActionDescriptor.create(OpenSearchEditorToSideAction, OpenSearchEditorToSideAction.ID, OpenSearchEditorToSideAction.LABEL), + 'Search Editor: Open New Search Editor to Side', category); + registry.registerWorkbenchAction(SyncActionDescriptor.create(RerunSearchEditorSearchAction, RerunSearchEditorSearchAction.ID, RerunSearchEditorSearchAction.LABEL, { mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_R } }, ContextKeyExpr.and(SearchEditorConstants.InSearchEditor)), 'Search Editor: Rerun', category); diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts index f8e1edbb330..382284e12ee 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts @@ -295,10 +295,65 @@ export class SearchEditor extends BaseTextEditor { this.queryEditorWidget.toggleContextLines(); } + modifyContextLines(increase: boolean) { + this.queryEditorWidget.modifyContextLines(increase); + } + toggleQueryDetails() { this.toggleIncludesExcludes(); } + deleteResultBlock() { + const linesToDelete = new Set(); + + const selections = this.searchResultEditor.getSelections(); + const model = this.searchResultEditor.getModel(); + if (!(selections && model)) { return; } + + const maxLine = model.getLineCount(); + const minLine = 1; + + const deleteUp = (start: number) => { + for (let cursor = start; cursor >= minLine; cursor--) { + const line = model.getLineContent(cursor); + linesToDelete.add(cursor); + if (line[0] !== undefined && line[0] !== ' ') { + break; + } + } + }; + + const deleteDown = (start: number): number | undefined => { + linesToDelete.add(start); + for (let cursor = start + 1; cursor <= maxLine; cursor++) { + const line = model.getLineContent(cursor); + if (line[0] !== undefined && line[0] !== ' ') { + return cursor; + } + linesToDelete.add(cursor); + } + return; + }; + + const endingCursorLines: Array = []; + for (const selection of selections) { + const lineNumber = selection.startLineNumber; + endingCursorLines.push(deleteDown(lineNumber)); + deleteUp(lineNumber); + for (let inner = selection.startLineNumber; inner <= selection.endLineNumber; inner++) { + linesToDelete.add(inner); + } + } + + if (endingCursorLines.length === 0) { endingCursorLines.push(1); } + + const isDefined = (x: T | undefined): x is T => x !== undefined; + + model.pushEditOperations(this.searchResultEditor.getSelections(), + [...linesToDelete].map(line => ({ range: new Range(line, 1, line + 1, 1), text: '' })), + () => endingCursorLines.filter(isDefined).map(line => new Selection(line, 1, line, 1))); + } + cleanState() { this.getInput()?.setDirty(false); } @@ -370,7 +425,7 @@ export class SearchEditor extends BaseTextEditor { private readConfigFromWidget() { return { caseSensitive: this.queryEditorWidget.searchInput.getCaseSensitive(), - contextLines: this.queryEditorWidget.contextLines(), + contextLines: this.queryEditorWidget.getContextLines(), excludes: this.inputPatternExcludes.getValue(), includes: this.inputPatternIncludes.getValue(), query: this.queryEditorWidget.searchInput.getValue(), diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts index cc291cefbbd..29d36746319 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts @@ -19,7 +19,7 @@ import * as Constants from 'vs/workbench/contrib/searchEditor/browser/constants' import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditor'; import { getOrMakeSearchEditorInput, SearchEditorInput } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; import { serializeSearchResultForEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditorSerialization'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search'; export const toggleSearchEditorCaseSensitiveCommand = (accessor: ServicesAccessor) => { @@ -54,6 +54,14 @@ export const toggleSearchEditorContextLinesCommand = (accessor: ServicesAccessor } }; +export const modifySearchEditorContextLinesCommand = (accessor: ServicesAccessor, increase: boolean) => { + const editorService = accessor.get(IEditorService); + const input = editorService.activeEditor; + if (input instanceof SearchEditorInput) { + (editorService.activeEditorPane as SearchEditor).modifyContextLines(increase); + } +}; + export const selectAllSearchEditorMatchesCommand = (accessor: ServicesAccessor) => { const editorService = accessor.get(IEditorService); const input = editorService.activeEditor; @@ -87,6 +95,22 @@ export class OpenSearchEditorAction extends Action { } } +export class OpenSearchEditorToSideAction extends Action { + + static readonly ID: string = Constants.OpenNewEditorToSideCommandId; + static readonly LABEL = localize('search.openNewEditorToSide', "Open New Search Editor to Side"); + + constructor(id: string, label: string, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(id, label, 'codicon-new-file'); + } + + async run() { + await this.instantiationService.invokeFunction(openNewSearchEditor, true); + } +} + export class OpenResultsInEditorAction extends Action { static readonly ID: string = Constants.OpenInEditorCommandId; @@ -135,7 +159,7 @@ export class RerunSearchEditorSearchAction extends Action { } const openNewSearchEditor = - async (accessor: ServicesAccessor) => { + async (accessor: ServicesAccessor, toSide = false) => { const editorService = accessor.get(IEditorService); const telemetryService = accessor.get(ITelemetryService); const instantiationService = accessor.get(IInstantiationService); @@ -166,7 +190,7 @@ const openNewSearchEditor = telemetryService.publicLog2('searchEditor/openNewSearchEditor'); const input = instantiationService.invokeFunction(getOrMakeSearchEditorInput, { config: { query: selected } }); - const editor = await editorService.openEditor(input, { pinned: true }) as SearchEditor; + const editor = await editorService.openEditor(input, { pinned: true }, toSide ? SIDE_GROUP : ACTIVE_GROUP) as SearchEditor; if (selected && configurationService.getValue('search').searchOnType) { editor.triggerSearch(); diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts index 698bb4c8f97..4b351522614 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts @@ -249,6 +249,7 @@ export class SearchEditorInput extends EditorInput { public getMatchRanges(): Range[] { return (this._cachedContentsModel?.getAllDecorations() ?? []) .filter(decoration => decoration.options.className === SearchEditorFindMatchClass) + .filter(({ range }) => !(range.startColumn === 1 && range.endColumn === 1)) .map(({ range }) => range); } diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index bf6617e3b90..99c961b924a 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -33,7 +33,7 @@ import { IOutputService } from 'vs/workbench/contrib/output/common/output'; import { StartStopProblemCollector, WatchingProblemCollector, ProblemCollectorEventKind, ProblemHandlingStrategy } from 'vs/workbench/contrib/tasks/common/problemCollectors'; import { Task, CustomTask, ContributedTask, RevealKind, CommandOptions, ShellConfiguration, RuntimeType, PanelKind, - TaskEvent, TaskEventKind, ShellQuotingOptions, ShellQuoting, CommandString, CommandConfiguration, ExtensionTaskSource, TaskScope, RevealProblemKind, DependsOrder + TaskEvent, TaskEventKind, ShellQuotingOptions, ShellQuoting, CommandString, CommandConfiguration, ExtensionTaskSource, TaskScope, RevealProblemKind, DependsOrder, TaskSourceKind } from 'vs/workbench/contrib/tasks/common/tasks'; import { ITaskSystem, ITaskSummary, ITaskExecuteResult, TaskExecuteKind, TaskError, TaskErrors, ITaskResolver, @@ -545,7 +545,7 @@ export class TerminalTaskSystem implements ITaskSystem { resolveSet.process.path = envPath; } } - resolvedVariables = taskSystemInfo.resolveVariables(workspaceFolder, resolveSet).then(async (resolved) => { + resolvedVariables = taskSystemInfo.resolveVariables(workspaceFolder, resolveSet, TaskSourceKind.toConfigurationTarget(task._source.kind)).then(async (resolved) => { this.mergeMaps(alreadyResolved, resolved.variables); resolved.variables = new Map(alreadyResolved); if (isProcess) { @@ -563,7 +563,7 @@ export class TerminalTaskSystem implements ITaskSystem { unresolved.forEach(variable => variablesArray.push(variable)); return new Promise((resolve, reject) => { - this.configurationResolverService.resolveWithInteraction(workspaceFolder, variablesArray, 'tasks').then(async (resolvedVariablesMap: Map | undefined) => { + this.configurationResolverService.resolveWithInteraction(workspaceFolder, variablesArray, 'tasks', undefined, TaskSourceKind.toConfigurationTarget(task._source.kind)).then(async (resolvedVariablesMap: Map | undefined) => { if (resolvedVariablesMap) { this.mergeMaps(alreadyResolved, resolvedVariablesMap); resolvedVariablesMap = new Map(alreadyResolved); diff --git a/src/vs/workbench/contrib/tasks/common/taskSystem.ts b/src/vs/workbench/contrib/tasks/common/taskSystem.ts index 0ac0d5799a6..8784de163b4 100644 --- a/src/vs/workbench/contrib/tasks/common/taskSystem.ts +++ b/src/vs/workbench/contrib/tasks/common/taskSystem.ts @@ -10,6 +10,7 @@ import { Event } from 'vs/base/common/event'; import { Platform } from 'vs/base/common/platform'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { Task, TaskEvent, KeyedTaskIdentifier } from './tasks'; +import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; export const enum TaskErrors { NotConfigured, @@ -118,7 +119,7 @@ export interface TaskSystemInfo { platform: Platform; context: any; uriProvider: (this: void, path: string) => URI; - resolveVariables(workspaceFolder: IWorkspaceFolder, toResolve: ResolveSet): Promise; + resolveVariables(workspaceFolder: IWorkspaceFolder, toResolve: ResolveSet, target: ConfigurationTarget): Promise; getDefaultShellAndArgs(): Promise<{ shell: string, args: string[] | string | undefined }>; } diff --git a/src/vs/workbench/contrib/tasks/common/tasks.ts b/src/vs/workbench/contrib/tasks/common/tasks.ts index 2fcd3f90195..f8eff3b1d4b 100644 --- a/src/vs/workbench/contrib/tasks/common/tasks.ts +++ b/src/vs/workbench/contrib/tasks/common/tasks.ts @@ -15,6 +15,7 @@ import { IWorkspaceFolder, IWorkspace } from 'vs/platform/workspace/common/works import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { TaskDefinitionRegistry } from 'vs/workbench/contrib/tasks/common/taskDefinitionRegistry'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; export const TASK_RUNNING_STATE = new RawContextKey('taskRunning', false); @@ -377,6 +378,14 @@ export namespace TaskSourceKind { export const InMemory: 'inMemory' = 'inMemory'; export const WorkspaceFile: 'workspaceFile' = 'workspaceFile'; export const User: 'user' = 'user'; + + export function toConfigurationTarget(kind: string): ConfigurationTarget { + switch (kind) { + case TaskSourceKind.User: return ConfigurationTarget.USER; + case TaskSourceKind.WorkspaceFile: return ConfigurationTarget.WORKSPACE; + default: return ConfigurationTarget.WORKSPACE_FOLDER; + } + } } export interface TaskSourceConfigElement { diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index 9c5881e6e64..b96337ce6f5 100644 --- a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts +++ b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts @@ -18,13 +18,13 @@ import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/vie import { ResourceNavigator, WorkbenchObjectTree } from 'vs/platform/list/browser/listService'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IContextKeyService, IContextKey, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ITimelineService, TimelineChangeEvent, TimelineItem, TimelineOptions, TimelineProvidersChangeEvent, TimelineRequest, Timeline, TimelinePaneId } from 'vs/workbench/contrib/timeline/common/timeline'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { SideBySideEditor, toResource } from 'vs/workbench/common/editor'; -import { ICommandService, CommandsRegistry, ICommandHandler } from 'vs/platform/commands/common/commands'; +import { ICommandService } from 'vs/platform/commands/common/commands'; import { IThemeService, LIGHT, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { basename } from 'vs/base/common/path'; @@ -34,9 +34,10 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IActionViewItemProvider, ActionBar, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { IAction, ActionRunner } from 'vs/base/common/actions'; import { ContextAwareMenuEntryActionViewItem, createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { MenuItemAction, IMenuService, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { MenuItemAction, IMenuService, MenuId, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { fromNow } from 'vs/base/common/date'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { escapeRegExpCharacters } from 'vs/base/common/strings'; const InitialPageSize = 20; const SubsequentPageSize = 40; @@ -88,7 +89,7 @@ interface TimelineCursors { export const TimelineFollowActiveEditorContext = new RawContextKey('timelineFollowActiveEditor', true); export class TimelinePane extends ViewPane { - static readonly TITLE = localize('timeline', 'Timeline'); + static readonly TITLE = localize('timeline', "Timeline"); private _$container!: HTMLElement; private _$message!: HTMLDivElement; @@ -96,7 +97,7 @@ export class TimelinePane extends ViewPane { private _$tree!: HTMLDivElement; private _tree!: WorkbenchObjectTree; private _treeRenderer: TimelineTreeRenderer | undefined; - private _menus: TimelinePaneMenus; + private commands: TimelinePaneCommands; private _visibilityDisposables: DisposableStore | undefined; private _followActiveEditorContext: IContextKey; @@ -125,8 +126,7 @@ export class TimelinePane extends ViewPane { ) { super({ ...options, titleMenuId: MenuId.TimelineTitle }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); - this._menus = this._register(this.instantiationService.createInstance(TimelinePaneMenus, this.id)); - this._register(this.instantiationService.createInstance(TimelinePaneCommands, this)); + this.commands = this._register(this.instantiationService.createInstance(TimelinePaneCommands, this)); const scopedContextKeyService = this._register(this.contextKeyService.createScoped()); scopedContextKeyService.createKey('view', TimelinePaneId); @@ -170,6 +170,7 @@ export class TimelinePane extends ViewPane { } this._uri = uri; + this.titleDescription = uri ? basename(uri.fsPath) : ''; this._treeRenderer?.setUri(uri); this.loadTimeline(true); } @@ -305,7 +306,7 @@ export class TimelinePane extends ViewPane { return; } - const filteredSources = (sources ?? this.timelineService.getSources()).filter(s => !this._excludedSources.has(s)); + const filteredSources = (sources ?? this.timelineService.getSources().map(s => s.id)).filter(s => !this._excludedSources.has(s)); if (filteredSources.length === 0) { if (reset) { this.refresh(); @@ -636,6 +637,8 @@ export class TimelinePane extends ViewPane { } protected renderBody(container: HTMLElement): void { + super.renderBody(container); + this._$container = container; DOM.addClasses(container, 'tree-explorer-viewlet-tree-view', 'timeline-tree-view'); @@ -649,7 +652,7 @@ export class TimelinePane extends ViewPane { // DOM.addClass(this._treeElement, 'show-file-icons'); container.appendChild(this._$tree); - this._treeRenderer = this.instantiationService.createInstance(TimelineTreeRenderer, this._menus); + this._treeRenderer = this.instantiationService.createInstance(TimelineTreeRenderer, this.commands); this._tree = >this.instantiationService.createInstance(WorkbenchObjectTree, 'TimelinePane', this._$tree, new TimelineListVirtualDelegate(), [this._treeRenderer], { identityProvider: new TimelineIdentityProvider(), @@ -662,7 +665,7 @@ export class TimelinePane extends ViewPane { const customTreeNavigator = ResourceNavigator.createTreeResourceNavigator(this._tree, { openOnFocus: false, openOnSelection: false }); this._register(customTreeNavigator); - this._register(this._tree.onContextMenu(e => this.onContextMenu(this._menus, e))); + this._register(this._tree.onContextMenu(e => this.onContextMenu(this.commands, e))); this._register(this._tree.onDidChangeSelection(e => this.ensureValidItems())); this._register( customTreeNavigator.onDidOpenResource(e => { @@ -712,7 +715,7 @@ export class TimelinePane extends ViewPane { this.message = file ? localize('timeline.loading', 'Loading timeline for {0}...', file) : ''; } - private onContextMenu(menus: TimelinePaneMenus, treeEvent: ITreeContextMenuEvent): void { + private onContextMenu(commands: TimelinePaneCommands, treeEvent: ITreeContextMenuEvent): void { const item = treeEvent.element; if (item === null) { return; @@ -727,7 +730,7 @@ export class TimelinePane extends ViewPane { } this._tree.setFocus([item]); - const actions = menus.getResourceContextActions(item); + const actions = commands.getItemContextActions(item); if (!actions.length) { return; } @@ -836,7 +839,7 @@ class TimelineTreeRenderer implements ITreeRenderer this._pane.reset(); - } - - toggleFollowActiveEditorCommand(): ICommandHandler { - return (accessor, arg) => this._pane.followActiveEditor = !this._pane.followActiveEditor; - } -} - -class TimelinePaneMenus extends Disposable { + private sourceDisposables: DisposableStore; constructor( - private id: string, + private readonly pane: TimelinePane, + @ITimelineService private readonly timelineService: ITimelineService, + @IConfigurationService private readonly configurationService: IConfigurationService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IMenuService private readonly menuService: IMenuService, @IContextMenuService private readonly contextMenuService: IContextMenuService ) { super(); + + this._register(this.sourceDisposables = new DisposableStore()); + + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: 'timeline.refresh', + title: { value: localize('refresh', "Refresh"), original: 'Refresh' }, + icon: { id: 'codicon/refresh' }, + category: { value: localize('timeline', "Timeline"), original: 'Timeline' }, + menu: { + id: MenuId.TimelineTitle, + group: 'navigation', + order: 99, + } + }); + } + run(accessor: ServicesAccessor, ...args: any[]) { + pane.reset(); + } + })); + + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: 'timeline.toggleFollowActiveEditor', + title: { value: localize('timeline.toggleFollowActiveEditorCommand', "Toggle Active Editor Following"), original: 'Toggle Active Editor Following' }, + category: { value: localize('timeline', "Timeline"), original: 'Timeline' }, + menu: [{ + id: MenuId.TimelineTitle, + command: { + // title: localize(`timeline.toggleFollowActiveEditorCommand.stop`, "Stop following the Active Editor"), + icon: { id: 'codicon/eye' } + }, + group: 'navigation', + order: 98, + when: TimelineFollowActiveEditorContext + }, + { + id: MenuId.TimelineTitle, + command: { + // title: localize(`ToggleFollowActiveEditorCommand.follow`, "Follow the Active Editor"), + icon: { id: 'codicon/eye-closed' } + }, + group: 'navigation', + order: 98, + when: TimelineFollowActiveEditorContext.toNegated() + }] + }); + } + run(accessor: ServicesAccessor, ...args: any[]) { + pane.followActiveEditor = !pane.followActiveEditor; + } + })); + + this._register(timelineService.onDidChangeProviders(() => this.updateTimelineSourceFilters())); + this.updateTimelineSourceFilters(); } - getResourceActions(element: TreeElement): IAction[] { + getItemActions(element: TreeElement): IAction[] { return this.getActions(MenuId.TimelineItemContext, { key: 'timelineItem', value: element.contextValue }).primary; } - getResourceContextActions(element: TreeElement): IAction[] { + getItemContextActions(element: TreeElement): IAction[] { return this.getActions(MenuId.TimelineItemContext, { key: 'timelineItem', value: element.contextValue }).secondary; } private getActions(menuId: MenuId, context: { key: string, value?: string }): { primary: IAction[]; secondary: IAction[]; } { const contextKeyService = this.contextKeyService.createScoped(); - contextKeyService.createKey('view', this.id); + contextKeyService.createKey('view', this.pane.id); contextKeyService.createKey(context.key, context.value); const menu = this.menuService.createMenu(menuId, contextKeyService); @@ -981,4 +993,37 @@ class TimelinePaneMenus extends Disposable { return result; } + + private updateTimelineSourceFilters() { + this.sourceDisposables.clear(); + + const excluded = new Set(this.configurationService.getValue('timeline.excludeSources') ?? []); + + for (const source of this.timelineService.getSources()) { + this.sourceDisposables.add(registerAction2(class extends Action2 { + constructor() { + super({ + id: `timeline.toggleExcludeSource:${source.id}`, + title: { value: localize('timeline.filterSource', "Include: {0}", source.label), original: `Include: ${source.label}` }, + category: { value: localize('timeline', "Timeline"), original: 'Timeline' }, + menu: { + id: MenuId.TimelineTitle, + group: '2_sources', + }, + toggled: ContextKeyExpr.regex(`config.timeline.excludeSources`, new RegExp(`\\b${escapeRegExpCharacters(source.id)}\\b`)).negate() + }); + } + run(accessor: ServicesAccessor, ...args: any[]) { + if (excluded.has(source.id)) { + excluded.delete(source.id); + } else { + excluded.add(source.id); + } + + const configurationService = accessor.get(IConfigurationService); + configurationService.updateValue('timeline.excludeSources', [...excluded.keys()]); + } + })); + } + } } diff --git a/src/vs/workbench/contrib/timeline/common/timeline.ts b/src/vs/workbench/contrib/timeline/common/timeline.ts index d9e4720eb00..6555bc8deb6 100644 --- a/src/vs/workbench/contrib/timeline/common/timeline.ts +++ b/src/vs/workbench/contrib/timeline/common/timeline.ts @@ -69,6 +69,11 @@ export interface TimelineProvider extends TimelineProviderDescriptor, IDisposabl provideTimeline(uri: URI, options: TimelineOptions, token: CancellationToken, internalOptions?: InternalTimelineOptions): Promise; } +export interface TimelineSource { + id: string; + label: string; +} + export interface TimelineProviderDescriptor { id: string; label: string; @@ -98,7 +103,7 @@ export interface ITimelineService { registerTimelineProvider(provider: TimelineProvider): IDisposable; unregisterTimelineProvider(id: string): void; - getSources(): string[]; + getSources(): TimelineSource[]; getTimeline(id: string, uri: URI, options: TimelineOptions, tokenSource: CancellationTokenSource, internalOptions?: InternalTimelineOptions): TimelineRequest | undefined; diff --git a/src/vs/workbench/contrib/timeline/common/timelineService.ts b/src/vs/workbench/contrib/timeline/common/timelineService.ts index fc996a3d8c5..5d2b7242c50 100644 --- a/src/vs/workbench/contrib/timeline/common/timelineService.ts +++ b/src/vs/workbench/contrib/timeline/common/timelineService.ts @@ -98,7 +98,7 @@ export class TimelineService implements ITimelineService { } getSources() { - return [...this._providers.keys()]; + return [...this._providers.values()].map(p => ({ id: p.id, label: p.label })); } getTimeline(id: string, uri: URI, options: TimelineOptions, tokenSource: CancellationTokenSource, internalOptions?: InternalTimelineOptions) { diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts index 367dd0b6fb3..d215668d3f0 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts @@ -7,6 +7,8 @@ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } fr import { Registry } from 'vs/platform/registry/common/platform'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { UserDataSyncWorkbenchContribution } from 'vs/workbench/contrib/userDataSync/browser/userDataSync'; +import { UserDataSyncViewContribution } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncView'; const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(UserDataSyncWorkbenchContribution, LifecyclePhase.Ready); +workbenchRegistry.registerWorkbenchContribution(UserDataSyncViewContribution, LifecyclePhase.Ready); diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 05c609878ac..d7f3ea4d80d 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -30,7 +30,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { CONTEXT_SYNC_STATE, getSyncSourceFromRemoteContentResource, getUserDataSyncStore, ISyncConfiguration, IUserDataAutoSyncService, IUserDataSyncService, IUserDataSyncStore, registerConfiguration, SyncSource, SyncStatus, toRemoteContentResource, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_SCHEME, IUserDataSyncEnablementService, ResourceKey, getSyncSourceFromPreviewResource, CONTEXT_SYNC_ENABLEMENT } from 'vs/platform/userDataSync/common/userDataSync'; +import { CONTEXT_SYNC_STATE, getUserDataSyncStore, ISyncConfiguration, IUserDataAutoSyncService, IUserDataSyncService, IUserDataSyncStore, registerConfiguration, SyncSource, SyncStatus, UserDataSyncError, UserDataSyncErrorCode, USER_DATA_SYNC_SCHEME, IUserDataSyncEnablementService, ResourceKey, getSyncSourceFromPreviewResource, CONTEXT_SYNC_ENABLEMENT, toRemoteSyncResourceFromSource, PREVIEW_QUERY, resolveSyncResource, getSyncSourceFromResourceKey } from 'vs/platform/userDataSync/common/userDataSync'; import { FloatingClickWidget } from 'vs/workbench/browser/parts/editor/editorWidgets'; import { GLOBAL_ACTIVITY_ID } from 'vs/workbench/common/activity'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -354,7 +354,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo private async acceptRemote(syncSource: SyncSource) { try { - const contents = await this.userDataSyncService.getRemoteContent(syncSource, false); + const contents = await this.userDataSyncService.resolveContent(toRemoteSyncResourceFromSource(syncSource).with({ query: PREVIEW_QUERY })); if (contents) { await this.userDataSyncService.accept(syncSource, contents); } @@ -754,7 +754,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo label = localize('keybindings conflicts preview', "Keybindings Conflicts (Remote ↔ Local)"); } if (previewResource) { - const remoteContentResource = toRemoteContentResource(source); + const remoteContentResource = toRemoteSyncResourceFromSource(source).with({ query: PREVIEW_QUERY }); await this.editorService.openEditor({ leftResource: remoteContentResource, rightResource: previewResource, @@ -1056,15 +1056,8 @@ class UserDataRemoteContentProvider implements ITextModelContentProvider { } provideTextContent(uri: URI): Promise | null { - let promise: Promise | undefined; - if (isEqual(uri, toRemoteContentResource(SyncSource.Settings))) { - promise = this.userDataSyncService.getRemoteContent(SyncSource.Settings, true); - } - if (isEqual(uri, toRemoteContentResource(SyncSource.Keybindings))) { - promise = this.userDataSyncService.getRemoteContent(SyncSource.Keybindings, true); - } - if (promise) { - return promise.then(content => this.modelService.createModel(content || '', this.modeService.create('jsonc'), uri)); + if (uri.scheme === USER_DATA_SYNC_SCHEME) { + return this.userDataSyncService.resolveContent(uri).then(content => this.modelService.createModel(content || '', this.modeService.create('jsonc'), uri)); } return null; } @@ -1120,7 +1113,7 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio return true; } - if (getSyncSourceFromRemoteContentResource(model.uri) !== undefined) { + if (resolveSyncResource(model.uri) !== null && model.uri.query === PREVIEW_QUERY) { return this.configurationService.getValue('diffEditor.renderSideBySide'); } @@ -1130,14 +1123,14 @@ class AcceptChangesContribution extends Disposable implements IEditorContributio private createAcceptChangesWidgetRenderer(): void { if (!this.acceptChangesButton) { - const isRemote = getSyncSourceFromRemoteContentResource(this.editor.getModel()!.uri) !== undefined; + const isRemote = resolveSyncResource(this.editor.getModel()!.uri) !== null; const acceptRemoteLabel = localize('accept remote', "Accept Remote"); const acceptLocalLabel = localize('accept local', "Accept Local"); this.acceptChangesButton = this.instantiationService.createInstance(FloatingClickWidget, this.editor, isRemote ? acceptRemoteLabel : acceptLocalLabel, null); this._register(this.acceptChangesButton.onClick(async () => { const model = this.editor.getModel(); if (model) { - const conflictsSource = (getSyncSourceFromPreviewResource(model.uri, this.environmentService) || getSyncSourceFromRemoteContentResource(model.uri))!; + const conflictsSource = (getSyncSourceFromPreviewResource(model.uri, this.environmentService) || getSyncSourceFromResourceKey(resolveSyncResource(model.uri)!.resourceKey))!; this.telemetryService.publicLog2<{ source: string, action: string }, SyncConflictsClassification>('sync/handleConflicts', { source: conflictsSource, action: isRemote ? 'acceptRemote' : 'acceptLocal' }); const syncAreaLabel = getSyncAreaLabel(conflictsSource); const result = await this.dialogService.confirm({ diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts new file mode 100644 index 00000000000..b189b51863d --- /dev/null +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncView.ts @@ -0,0 +1,223 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IViewsRegistry, Extensions, ITreeViewDescriptor, ITreeViewDataProvider, ITreeItem, TreeItemCollapsibleState, IViewsService, TreeViewItemHandleArg, IViewContainersRegistry, ViewContainerLocation, ViewContainer } from 'vs/workbench/common/views'; +import { localize } from 'vs/nls'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { TreeViewPane, TreeView } from 'vs/workbench/browser/parts/views/treeView'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { ALL_RESOURCE_KEYS, CONTEXT_SYNC_ENABLEMENT, IUserDataSyncStoreService, toRemoteSyncResource, resolveSyncResource, IUserDataSyncBackupStoreService, IResourceRefHandle, ResourceKey, toLocalBackupSyncResource } from 'vs/platform/userDataSync/common/userDataSync'; +import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/actions'; +import { IContextKeyService, RawContextKey, ContextKeyExpr, ContextKeyEqualsExpr } from 'vs/platform/contextkey/common/contextkey'; +import { URI } from 'vs/base/common/uri'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { FolderThemeIcon, FileThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { fromNow } from 'vs/base/common/date'; +import { pad } from 'vs/base/common/strings'; +import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; + +export class UserDataSyncViewContribution implements IWorkbenchContribution { + + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService, + @IUserDataSyncBackupStoreService private readonly userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, + ) { + const container = this.registerSyncViewContainer(); + // Disable remote backup view until server is upgraded. + // this.registerBackupView(container, true); + this.registerBackupView(container, false); + } + + private registerSyncViewContainer(): ViewContainer { + return Registry.as(Extensions.ViewContainersRegistry).registerViewContainer( + { + id: 'workbench.view.sync', + name: localize('sync', "Sync"), + ctorDescriptor: new SyncDescriptor( + ViewPaneContainer, + ['workbench.view.sync', `workbench.view.sync.state`, { mergeViewWithContainerWhenSingleView: true }] + ), + icon: 'codicon-sync', + hideIfEmpty: true, + }, ViewContainerLocation.Sidebar); + } + + private registerBackupView(container: ViewContainer, remote: boolean): void { + const id = `workbench.views.sync.${remote ? 'remote' : 'local'}BackupView`; + const name = remote ? localize('remote title', "Remote Backup") : localize('local title', "Local Backup"); + const contextKey = new RawContextKey(`showUserDataSync${remote ? 'Remote' : 'Local'}BackupView`, false); + const viewEnablementContext = contextKey.bindTo(this.contextKeyService); + const treeView = this.instantiationService.createInstance(TreeView, id, name); + treeView.showCollapseAllAction = true; + treeView.showRefreshAction = true; + const disposable = treeView.onDidChangeVisibility(visible => { + if (visible && !treeView.dataProvider) { + disposable.dispose(); + treeView.dataProvider = this.instantiationService.createInstance(UserDataSyncHistoryViewDataProvider, id, + (resourceKey: ResourceKey) => remote ? this.userDataSyncStoreService.getAllRefs(resourceKey) : this.userDataSyncBackupStoreService.getAllRefs(resourceKey), + (resourceKey: ResourceKey, ref: string) => remote ? toRemoteSyncResource(resourceKey, ref) : toLocalBackupSyncResource(resourceKey, ref)); + } + }); + const viewsRegistry = Registry.as(Extensions.ViewsRegistry); + viewsRegistry.registerViews([{ + id, + name, + ctorDescriptor: new SyncDescriptor(TreeViewPane), + when: ContextKeyExpr.and(CONTEXT_SYNC_ENABLEMENT, contextKey), + canToggleVisibility: true, + canMoveView: true, + treeView, + collapsed: false, + order: 100, + }], container); + + registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.actions.showSync${remote ? 'Remote' : 'Local'}BackupView`, + title: remote ? + { value: localize('workbench.action.showSyncRemoteBackup', "Show Remote Backup"), original: `Show Remote Backup` } + : { value: localize('workbench.action.showSyncLocalBackup', "Show Local Backup"), original: `Show Local Backup` }, + category: { value: localize('sync', "Sync"), original: `Sync` }, + menu: { + id: MenuId.CommandPalette, + when: CONTEXT_SYNC_ENABLEMENT + }, + }); + } + async run(accessor: ServicesAccessor): Promise { + viewEnablementContext.set(true); + accessor.get(IViewsService).openView(id, true); + } + }); + + this.registerActions(id); + } + + private registerActions(viewId: string) { + registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.actions.sync.${viewId}.resolveResourceRef`, + title: localize('workbench.actions.sync.resolveResourceRef', "Resolve Resource Ref"), + }); + } + async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise { + const editorService = accessor.get(IEditorService); + let resource = URI.parse(handle.$treeItemHandle); + const result = resolveSyncResource(resource); + if (result) { + resource = resource.with({ fragment: result.resourceKey }); + await editorService.openEditor({ resource }); + } + } + }); + registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.actions.sync.${viewId}.resolveResourceRefCompletely`, + title: localize('workbench.actions.sync.resolveResourceRefCompletely', "Show full content"), + menu: { + id: MenuId.ViewItemContext, + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', viewId), ContextKeyExpr.regex('viewItem', /syncref-.*/i)) + }, + }); + } + async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise { + const editorService = accessor.get(IEditorService); + await editorService.openEditor({ resource: URI.parse(handle.$treeItemHandle) }); + } + }); + registerAction2(class extends Action2 { + constructor() { + super({ + id: `workbench.actions.${viewId}.commpareWithLocal`, + title: localize('workbench.action.deleteRef', "Open Changes"), + menu: { + id: MenuId.ViewItemContext, + when: ContextKeyExpr.and(ContextKeyEqualsExpr.create('view', viewId), ContextKeyExpr.regex('viewItem', /syncref-(settings|keybindings).*/i)) + }, + }); + } + async run(accessor: ServicesAccessor, handle: TreeViewItemHandleArg): Promise { + const editorService = accessor.get(IEditorService); + const environmentService = accessor.get(IEnvironmentService); + const resource = URI.parse(handle.$treeItemHandle); + const result = resolveSyncResource(resource); + if (result) { + const leftResource: URI = resource.with({ fragment: result.resourceKey }); + const rightResource: URI = result.resourceKey === 'settings' ? environmentService.settingsResource : environmentService.keybindingsResource; + await editorService.openEditor({ + leftResource, + rightResource, + options: { + preserveFocus: false, + pinned: true, + revealIfVisible: true, + }, + }); + } + } + }); + } + +} + +class UserDataSyncHistoryViewDataProvider implements ITreeViewDataProvider { + + constructor( + private readonly viewId: string, + private getAllRefs: (resourceKey: ResourceKey) => Promise, + private toResource: (resourceKey: ResourceKey, ref: string) => URI + ) { + } + + async getChildren(element?: ITreeItem): Promise { + if (element) { + return this.getResources(element.handle); + } + return ALL_RESOURCE_KEYS.map(resourceKey => ({ + handle: resourceKey, + collapsibleState: TreeItemCollapsibleState.Collapsed, + label: { label: resourceKey }, + themeIcon: FolderThemeIcon, + contextValue: `sync-${resourceKey}` + })); + } + + private async getResources(handle: string): Promise { + const resourceKey = ALL_RESOURCE_KEYS.filter(key => key === handle)[0]; + if (resourceKey) { + const refHandles = await this.getAllRefs(resourceKey); + return refHandles.map(({ ref, created }) => { + const handle = this.toResource(resourceKey, ref).toString(); + return { + handle, + collapsibleState: TreeItemCollapsibleState.None, + label: { label: label(new Date(created)) }, + description: fromNow(created, true), + command: { id: `workbench.actions.sync.${this.viewId}.resolveResourceRef`, title: '', arguments: [{ $treeItemHandle: handle, $treeViewId: '' }] }, + themeIcon: FileThemeIcon, + contextValue: `syncref-${resourceKey}` + }; + }); + } + return []; + } + +} + +function label(date: Date): string { + return date.toLocaleDateString() + + ' ' + pad(date.getHours(), 2) + + ':' + pad(date.getMinutes(), 2) + + ':' + pad(date.getSeconds(), 2); +} + diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts index e2a484ad05e..c840aa894be 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts @@ -133,8 +133,12 @@ class WebviewProtocolProvider extends Disposable { }); this._register(handle.onFirstLoad(contents => { - registerFileProtocol(contents, WebviewResourceScheme, fileService, getExtensionLocation(), getLocalResourceRoots) - .then(this._resolve, this._reject); + try { + registerFileProtocol(contents, WebviewResourceScheme, fileService, getExtensionLocation(), getLocalResourceRoots); + this._resolve(); + } catch { + this._reject(); + } })); } } diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewProtocols.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewProtocols.ts index 39e4349cb40..e84ce9bf6fb 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewProtocols.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewProtocols.ts @@ -14,27 +14,24 @@ export function registerFileProtocol( extensionLocation: URI | undefined, getRoots: () => ReadonlyArray ) { - return new Promise((resolve, reject) => - contents.session.protocol.registerBufferProtocol(protocol, async (request, callback: any) => { - try { - const result = await loadLocalResource(URI.parse(request.url), fileService, extensionLocation, getRoots); - if (result.type === WebviewResourceResponse.Type.Success) { - return callback({ - data: Buffer.from(result.data.buffer), - mimeType: result.mimeType - }); - } - if (result.type === WebviewResourceResponse.Type.AccessDenied) { - console.error('Webview: Cannot load resource outside of protocol root'); - return callback({ error: -10 /* ACCESS_DENIED: https://cs.chromium.org/chromium/src/net/base/net_error_list.h */ }); - } - } catch { - // noop + contents.session.protocol.registerBufferProtocol(protocol, async (request, callback: any) => { + try { + const result = await loadLocalResource(URI.parse(request.url), fileService, extensionLocation, getRoots); + if (result.type === WebviewResourceResponse.Type.Success) { + return callback({ + data: Buffer.from(result.data.buffer), + mimeType: result.mimeType + }); } + if (result.type === WebviewResourceResponse.Type.AccessDenied) { + console.error('Webview: Cannot load resource outside of protocol root'); + return callback({ error: -10 /* ACCESS_DENIED: https://cs.chromium.org/chromium/src/net/base/net_error_list.h */ }); + } + } catch { + // noop + } - return callback({ error: -2 /* FAILED: https://cs.chromium.org/chromium/src/net/base/net_error_list.h */ }); - }, (err) => { - err ? reject(err) : resolve(); - })); + return callback({ error: -2 /* FAILED: https://cs.chromium.org/chromium/src/net/base/net_error_list.h */ }); + }); } diff --git a/src/vs/workbench/contrib/welcome/common/viewsWelcomeExtensionPoint.ts b/src/vs/workbench/contrib/welcome/common/viewsWelcomeExtensionPoint.ts index 9a1fdbe078e..513eba9642f 100644 --- a/src/vs/workbench/contrib/welcome/common/viewsWelcomeExtensionPoint.ts +++ b/src/vs/workbench/contrib/welcome/common/viewsWelcomeExtensionPoint.ts @@ -22,7 +22,7 @@ export type ViewsWelcomeExtensionPoint = ViewWelcome[]; export const ViewIdentifierMap: { [key: string]: string } = { 'explorer': 'workbench.explorer.emptyView', - 'debug': 'workbench.debug.startView', + 'debug': 'workbench.debug.welcome', 'scm': 'workbench.scm', }; @@ -40,7 +40,6 @@ const viewsWelcomeExtensionPointSchema = Object.freeze**Tip:** You can also enable the checks workspace or application wide by adding |"javascript.implicitProjectConfig.checkJs": true| to your workspace or user settings and explicitly ignoring files or lines using |// @ts-nocheck| and |// @ts-ignore|. Check out the docs on [JavaScript in VS Code](https://code.visualstudio.com/docs/languages/javascript) to learn more. +>**Tip:** You can also enable the checks workspace or application wide by adding |"javascript.implicitProjectConfig.checkJs": true| to your workspace or user settings and explicitly ignoring files or lines using |// @ts-nocheck| and |// @ts-expect-error|. Check out the docs on [JavaScript in VS Code](https://code.visualstudio.com/docs/languages/javascript) to learn more. ## Thanks! diff --git a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts index 1803459b953..7cc9d291293 100644 --- a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts +++ b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts @@ -426,7 +426,7 @@ export class WalkThroughPart extends BaseEditor { alwaysConsumeMouseWheel: false }, overviewRulerLanes: 3, - fixedOverflowWidgets: true, + fixedOverflowWidgets: false, lineNumbersMinChars: 1, minimap: { enabled: false }, }; diff --git a/src/vs/workbench/electron-browser/window.ts b/src/vs/workbench/electron-browser/window.ts index 55ee4b0f1cb..6f7117b9068 100644 --- a/src/vs/workbench/electron-browser/window.ts +++ b/src/vs/workbench/electron-browser/window.ts @@ -62,6 +62,7 @@ import { IWorkingCopyService, WorkingCopyCapabilities } from 'vs/workbench/servi import { AutoSaveMode, IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { Event } from 'vs/base/common/event'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; +import { clearAllFontInfos } from 'vs/editor/browser/config/configuration'; export class NativeWindow extends Disposable { @@ -179,6 +180,10 @@ export class NativeWindow extends Disposable { this.notificationService.info(message); }); + ipc.on('vscode:displayChanged', (event: IpcEvent) => { + clearAllFontInfos(); + }); + // Fullscreen Events ipc.on('vscode:enterFullScreen', async () => { await this.lifecycleService.when(LifecyclePhase.Ready); diff --git a/src/vs/workbench/services/configuration/common/configuration.ts b/src/vs/workbench/services/configuration/common/configuration.ts index 888f442d2fe..630086370ba 100644 --- a/src/vs/workbench/services/configuration/common/configuration.ts +++ b/src/vs/workbench/services/configuration/common/configuration.ts @@ -40,3 +40,5 @@ export interface IConfigurationCache { remove(key: ConfigurationKey): Promise; } + +export const TASKS_DEFAULT = '{\n\t\"version\": \"2.0.0\",\n\t\"tasks\": []\n}'; diff --git a/src/vs/workbench/services/configuration/common/configurationEditingService.ts b/src/vs/workbench/services/configuration/common/configurationEditingService.ts index 6b15032cd5c..573b4f0f546 100644 --- a/src/vs/workbench/services/configuration/common/configurationEditingService.ts +++ b/src/vs/workbench/services/configuration/common/configurationEditingService.ts @@ -20,7 +20,7 @@ import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/ import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IConfigurationService, IConfigurationOverrides, keyFromOverrideIdentifier } from 'vs/platform/configuration/common/configuration'; -import { FOLDER_SETTINGS_PATH, WORKSPACE_STANDALONE_CONFIGURATIONS, TASKS_CONFIGURATION_KEY, LAUNCH_CONFIGURATION_KEY, USER_STANDALONE_CONFIGURATIONS } from 'vs/workbench/services/configuration/common/configuration'; +import { FOLDER_SETTINGS_PATH, WORKSPACE_STANDALONE_CONFIGURATIONS, TASKS_CONFIGURATION_KEY, LAUNCH_CONFIGURATION_KEY, USER_STANDALONE_CONFIGURATIONS, TASKS_DEFAULT } from 'vs/workbench/services/configuration/common/configuration'; import { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; import { OVERRIDE_PROPERTY_PATTERN, IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; @@ -434,10 +434,19 @@ export class ConfigurationEditingService { return setProperty(model.getValue(), jsonPath, value, { tabSize, insertSpaces, eol }); } + private defaultResourceValue(resource: URI): string { + const basename: string = resources.basename(resource); + const configurationValue: string = basename.substr(0, basename.length - resources.extname(resource).length); + switch (configurationValue) { + case TASKS_CONFIGURATION_KEY: return TASKS_DEFAULT; + default: return '{}'; + } + } + private async resolveModelReference(resource: URI): Promise> { const exists = await this.fileService.exists(resource); if (!exists) { - await this.textFileService.write(resource, '{}', { encoding: 'utf8' }); + await this.textFileService.write(resource, this.defaultResourceValue(resource), { encoding: 'utf8' }); } return this.textModelResolverService.createModelReference(resource); } diff --git a/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts b/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts index 9402493cd86..290c68e8bde 100644 --- a/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts +++ b/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts @@ -11,7 +11,7 @@ import { Schemas } from 'vs/base/common/network'; import { toResource } from 'vs/workbench/common/editor'; import { IStringDictionary, forEach, fromMap } from 'vs/base/common/collections'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IWorkspaceFolder, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -86,12 +86,12 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR }, envVariables); } - public async resolveWithInteractionReplace(folder: IWorkspaceFolder | undefined, config: any, section?: string, variables?: IStringDictionary): Promise { + public async resolveWithInteractionReplace(folder: IWorkspaceFolder | undefined, config: any, section?: string, variables?: IStringDictionary, target?: ConfigurationTarget): Promise { // resolve any non-interactive variables and any contributed variables config = this.resolveAny(folder, config); // resolve input variables in the order in which they are encountered - return this.resolveWithInteraction(folder, config, section, variables).then(mapping => { + return this.resolveWithInteraction(folder, config, section, variables, target).then(mapping => { // finally substitute evaluated command variables (if there are any) if (!mapping) { return null; @@ -103,14 +103,14 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR }); } - public async resolveWithInteraction(folder: IWorkspaceFolder | undefined, config: any, section?: string, variables?: IStringDictionary): Promise | undefined> { + public async resolveWithInteraction(folder: IWorkspaceFolder | undefined, config: any, section?: string, variables?: IStringDictionary, target?: ConfigurationTarget): Promise | undefined> { // resolve any non-interactive variables and any contributed variables const resolved = await this.resolveAnyMap(folder, config); config = resolved.newConfig; const allVariableMapping: Map = resolved.resolvedVariables; // resolve input and command variables in the order in which they are encountered - return this.resolveWithInputAndCommands(folder, config, variables, section).then(inputOrCommandMapping => { + return this.resolveWithInputAndCommands(folder, config, variables, section, target).then(inputOrCommandMapping => { if (this.updateMapping(inputOrCommandMapping, allVariableMapping)) { return allVariableMapping; } @@ -139,7 +139,7 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR * * @param variableToCommandMap Aliases for commands */ - private async resolveWithInputAndCommands(folder: IWorkspaceFolder | undefined, configuration: any, variableToCommandMap?: IStringDictionary, section?: string): Promise | undefined> { + private async resolveWithInputAndCommands(folder: IWorkspaceFolder | undefined, configuration: any, variableToCommandMap?: IStringDictionary, section?: string, target?: ConfigurationTarget): Promise | undefined> { if (!configuration) { return Promise.resolve(undefined); @@ -148,9 +148,18 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR // get all "inputs" let inputs: ConfiguredInput[] = []; if (folder && this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY && section) { - let result = this.configurationService.getValue(section, { resource: folder.uri }); - if (result) { - inputs = result.inputs; + let result = this.configurationService.inspect(section, { resource: folder.uri }); + if (result && (result.userValue || result.workspaceValue || result.workspaceFolderValue)) { + switch (target) { + case ConfigurationTarget.USER: inputs = (result.userValue)?.inputs; break; + case ConfigurationTarget.WORKSPACE: inputs = (result.workspaceValue)?.inputs; break; + default: inputs = (result.workspaceFolderValue)?.inputs; + } + } else { + const valueResult = this.configurationService.getValue(section, { resource: folder.uri }); + if (valueResult) { + inputs = valueResult.inputs; + } } } diff --git a/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts b/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts index 8c3a5694be0..33a6c5d05da 100644 --- a/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts +++ b/src/vs/workbench/services/configurationResolver/common/configurationResolver.ts @@ -6,6 +6,7 @@ import { IStringDictionary } from 'vs/base/common/collections'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; export const IConfigurationResolverService = createDecorator('configurationResolverService'); @@ -29,13 +30,13 @@ export interface IConfigurationResolverService { * @param section For example, 'tasks' or 'debug'. Used for resolving inputs. * @param variables Aliases for commands. */ - resolveWithInteractionReplace(folder: IWorkspaceFolder | undefined, config: any, section?: string, variables?: IStringDictionary): Promise; + resolveWithInteractionReplace(folder: IWorkspaceFolder | undefined, config: any, section?: string, variables?: IStringDictionary, target?: ConfigurationTarget): Promise; /** * Similar to resolveWithInteractionReplace, except without the replace. Returns a map of variables and their resolution. * Keys in the map will be of the format input:variableName or command:variableName. */ - resolveWithInteraction(folder: IWorkspaceFolder | undefined, config: any, section?: string, variables?: IStringDictionary): Promise | undefined>; + resolveWithInteraction(folder: IWorkspaceFolder | undefined, config: any, section?: string, variables?: IStringDictionary, target?: ConfigurationTarget): Promise | undefined>; /** * Contributes a variable that can be resolved later. Consumers that use resolveAny, resolveWithInteraction, diff --git a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts index d8bda093b41..91e52e14ed3 100644 --- a/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts +++ b/src/vs/workbench/services/configurationResolver/test/electron-browser/configurationResolverService.test.ts @@ -12,7 +12,7 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { BaseConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/configurationResolverService'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { TestEditorService, TestContextService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestEditorService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestWindowConfiguration } from 'vs/workbench/test/electron-browser/workbenchTestServices'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -22,6 +22,7 @@ import * as Types from 'vs/base/common/types'; import { EditorType } from 'vs/editor/common/editorCommon'; import { Selection } from 'vs/editor/common/core/selection'; import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; +import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; const mockLineNumber = 10; class TestEditorServiceWithActiveEditor extends TestEditorService { @@ -534,6 +535,8 @@ class MockQuickInputService implements IQuickInputService { readonly onShow = Event.None; readonly onHide = Event.None; + readonly quickAccess = undefined!; + public pick(picks: Promise[]> | QuickPickInput[], options?: IPickOptions & { canPickMany: true }, token?: CancellationToken): Promise; public pick(picks: Promise[]> | QuickPickInput[], options?: IPickOptions & { canPickMany: false }, token?: CancellationToken): Promise; public pick(picks: Promise[]> | QuickPickInput[], options?: Omit, 'canPickMany'>, token?: CancellationToken): Promise { diff --git a/src/vs/workbench/services/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts index fec64ea2b21..90f3c5fee04 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -9,7 +9,7 @@ import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { EditorInput, EditorsOrder, SideBySideEditorInput } from 'vs/workbench/common/editor'; -import { workbenchInstantiationService, TestStorageService, TestServiceAccessor, registerTestEditor, TestFileEditorInput } from 'vs/workbench/test/browser/workbenchTestServices'; +import { workbenchInstantiationService, TestServiceAccessor, registerTestEditor, TestFileEditorInput } from 'vs/workbench/test/browser/workbenchTestServices'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { EditorService, DelegatingEditorService } from 'vs/workbench/services/editor/browser/editorService'; @@ -27,6 +27,7 @@ import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry'; import { UntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; import { NullFileSystemProvider } from 'vs/platform/files/test/common/nullFileSystemProvider'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; +import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; const TEST_EDITOR_ID = 'MyTestEditorForEditorService'; const TEST_EDITOR_INPUT_ID = 'testEditorInputForEditorService'; diff --git a/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts b/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts index c0e0d8a1921..c0d0d905221 100644 --- a/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { EditorOptions, IEditorInputFactoryRegistry, Extensions as EditorExtensions } from 'vs/workbench/common/editor'; import { URI } from 'vs/base/common/uri'; -import { workbenchInstantiationService, TestStorageService, TestFileEditorInput, registerTestEditor, TestEditorPart } from 'vs/workbench/test/browser/workbenchTestServices'; +import { workbenchInstantiationService, TestFileEditorInput, registerTestEditor, TestEditorPart } from 'vs/workbench/test/browser/workbenchTestServices'; import { Registry } from 'vs/platform/registry/common/platform'; import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; @@ -16,6 +16,7 @@ import { WillSaveStateReason } from 'vs/platform/storage/common/storage'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { EditorsObserver } from 'vs/workbench/browser/parts/editor/editorsObserver'; import { timeout } from 'vs/base/common/async'; +import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; const TEST_EDITOR_ID = 'MyTestEditorForEditorsObserver'; const TEST_EDITOR_INPUT_ID = 'testEditorInputForEditorsObserver'; diff --git a/src/vs/workbench/services/keybinding/test/electron-browser/browserKeyboardMapper.test.ts b/src/vs/workbench/services/keybinding/test/electron-browser/browserKeyboardMapper.test.ts index 93aec7997a5..0405c4f0556 100644 --- a/src/vs/workbench/services/keybinding/test/electron-browser/browserKeyboardMapper.test.ts +++ b/src/vs/workbench/services/keybinding/test/electron-browser/browserKeyboardMapper.test.ts @@ -12,8 +12,8 @@ import { TestInstantiationService } from 'vs/platform/instantiation/test/common/ import { INotificationService } from 'vs/platform/notification/common/notification'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { TestStorageService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; +import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; class TestKeyboardMapperFactory extends BrowserKeyboardMapperFactoryBase { constructor(notificationService: INotificationService, storageService: IStorageService, commandService: ICommandService) { diff --git a/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts b/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts index 822ef2faced..fc22f855180 100644 --- a/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts +++ b/src/vs/workbench/services/keybinding/test/electron-browser/keybindingEditing.test.ts @@ -38,7 +38,7 @@ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editor import { KeybindingsEditingService } from 'vs/workbench/services/keybinding/common/keybindingEditing'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { TextModelResolverService } from 'vs/workbench/services/textmodelResolver/common/textModelResolverService'; -import { TestBackupFileService, TestContextService, TestEditorGroupsService, TestEditorService, TestLifecycleService, TestTextResourcePropertiesService, TestWorkingCopyService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestBackupFileService, TestEditorGroupsService, TestEditorService, TestLifecycleService } from 'vs/workbench/test/browser/workbenchTestServices'; import { FileService } from 'vs/platform/files/common/fileService'; import { Schemas } from 'vs/base/common/network'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; @@ -54,6 +54,7 @@ import { IFilesConfigurationService, FilesConfigurationService } from 'vs/workbe import { WorkingCopyFileService, IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; +import { TestTextResourcePropertiesService, TestContextService, TestWorkingCopyService } from 'vs/workbench/test/common/workbenchTestServices'; class TestEnvironmentService extends NativeWorkbenchEnvironmentService { diff --git a/src/vs/workbench/services/label/test/browser/label.test.ts b/src/vs/workbench/services/label/test/browser/label.test.ts index b9afd8221e9..897da308f9a 100644 --- a/src/vs/workbench/services/label/test/browser/label.test.ts +++ b/src/vs/workbench/services/label/test/browser/label.test.ts @@ -4,12 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { TestEnvironmentService, TestContextService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestEnvironmentService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; import { URI } from 'vs/base/common/uri'; import { sep } from 'vs/base/common/path'; import { isWindows } from 'vs/base/common/platform'; import { LabelService } from 'vs/workbench/services/label/common/labelService'; +import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; suite('URI Label', () => { diff --git a/src/vs/workbench/services/log/common/keyValueLogProvider.ts b/src/vs/workbench/services/log/common/keyValueLogProvider.ts index 0db6b00da32..da0fdb3f167 100644 --- a/src/vs/workbench/services/log/common/keyValueLogProvider.ts +++ b/src/vs/workbench/services/log/common/keyValueLogProvider.ts @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { URI } from 'vs/base/common/uri'; -import { IFileSystemProviderWithFileReadWriteCapability, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileOverwriteOptions, FileType, FileDeleteOptions, FileWriteOptions, FileChangeType, FileSystemProviderErrorCode } from 'vs/platform/files/common/files'; +import { IFileSystemProviderWithFileReadWriteCapability, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileOverwriteOptions, FileType, FileDeleteOptions, FileWriteOptions, FileChangeType, createFileSystemProviderError, FileSystemProviderErrorCode } from 'vs/platform/files/common/files'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import { VSBuffer } from 'vs/base/common/buffer'; -import { FileSystemError } from 'vs/workbench/api/common/extHostTypes'; import { isEqualOrParent, joinPath, relativePath } from 'vs/base/common/resources'; import { values } from 'vs/base/common/map'; +import { localize } from 'vs/nls'; export abstract class KeyValueLogProvider extends Disposable implements IFileSystemProviderWithFileReadWriteCapability { @@ -53,13 +53,13 @@ export abstract class KeyValueLogProvider extends Disposable implements IFileSys size: 0 }; } - return Promise.reject(new FileSystemError(resource, FileSystemProviderErrorCode.FileNotFound)); + throw createFileSystemProviderError(localize('fileNotExists', "File does not exist"), FileSystemProviderErrorCode.FileNotFound); } async readdir(resource: URI): Promise<[string, FileType][]> { const hasKey = await this.hasKey(resource.path); if (hasKey) { - return Promise.reject(new FileSystemError(resource, FileSystemProviderErrorCode.FileNotADirectory)); + throw createFileSystemProviderError(localize('fileNotDirectory', "File is not a directory"), FileSystemProviderErrorCode.FileNotADirectory); } const keys = await this.getAllKeys(); const files: Map = new Map(); @@ -79,7 +79,7 @@ export abstract class KeyValueLogProvider extends Disposable implements IFileSys async readFile(resource: URI): Promise { const hasKey = await this.hasKey(resource.path); if (!hasKey) { - return Promise.reject(new FileSystemError(resource, FileSystemProviderErrorCode.FileNotFound)); + throw createFileSystemProviderError(localize('fileNotFound', "File not found"), FileSystemProviderErrorCode.FileNotFound); } const value = await this.getValue(resource.path); return VSBuffer.fromString(value).buffer; @@ -90,7 +90,7 @@ export abstract class KeyValueLogProvider extends Disposable implements IFileSys if (!hasKey) { const files = await this.readdir(resource); if (files.length) { - return Promise.reject(new FileSystemError(resource, FileSystemProviderErrorCode.FileIsADirectory)); + throw createFileSystemProviderError(localize('fileIsDirectory', "File is Directory"), FileSystemProviderErrorCode.FileIsADirectory); } } await this.setValue(resource.path, VSBuffer.wrap(content).toString()); diff --git a/src/vs/workbench/services/quickinput/browser/quickInputService.ts b/src/vs/workbench/services/quickinput/browser/quickInputService.ts new file mode 100644 index 00000000000..94d8ad01f04 --- /dev/null +++ b/src/vs/workbench/services/quickinput/browser/quickInputService.ts @@ -0,0 +1,42 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { QuickInputController } from 'vs/base/parts/quickinput/browser/quickInput'; +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'; + +export class QuickInputService extends BaseQuickInputService { + + constructor( + @IEnvironmentService private environmentService: IEnvironmentService, + @IConfigurationService private configurationService: IConfigurationService, + @IInstantiationService instantiationService: IInstantiationService, + @IKeybindingService private keybindingService: IKeybindingService, + @IContextKeyService contextKeyService: IContextKeyService, + @IThemeService themeService: IThemeService, + @IAccessibilityService accessibilityService: IAccessibilityService, + @ILayoutService protected layoutService: ILayoutService + ) { + super(instantiationService, contextKeyService, themeService, accessibilityService, layoutService); + } + + protected createController(): QuickInputController { + return super.createController(this.layoutService, { + ignoreFocusOut: () => this.environmentService.args['sticky-quickopen'] || !this.configurationService.getValue('workbench.quickOpen.closeOnFocusLost'), + backKeybindingLabel: () => this.keybindingService.lookupKeybinding('workbench.action.quickInputBack')?.getLabel() || undefined, + }); + } +} + +registerSingleton(IQuickInputService, QuickInputService, true); diff --git a/src/vs/workbench/services/search/node/fileSearch.ts b/src/vs/workbench/services/search/node/fileSearch.ts index e0465326652..563a66a3efc 100644 --- a/src/vs/workbench/services/search/node/fileSearch.ts +++ b/src/vs/workbench/services/search/node/fileSearch.ts @@ -22,7 +22,7 @@ import { URI } from 'vs/base/common/uri'; import { readdir } from 'vs/base/node/pfs'; import { IFileQuery, IFolderQuery, IProgressMessage, ISearchEngineStats, IRawFileMatch, ISearchEngine, ISearchEngineSuccess, isFilePatternMatch } from 'vs/workbench/services/search/common/search'; import { spawnRipgrepCmd } from './ripgrepFileSearch'; -import { prepareQuery } from 'vs/base/parts/quickopen/common/quickOpenScorer'; +import { prepareQuery } from 'vs/base/common/fuzzyScorer'; interface IDirectoryEntry { base: string; diff --git a/src/vs/workbench/services/search/node/rawSearchService.ts b/src/vs/workbench/services/search/node/rawSearchService.ts index 51f5440de19..5055db9ed39 100644 --- a/src/vs/workbench/services/search/node/rawSearchService.ts +++ b/src/vs/workbench/services/search/node/rawSearchService.ts @@ -15,7 +15,7 @@ import * as objects from 'vs/base/common/objects'; import { StopWatch } from 'vs/base/common/stopwatch'; import * as strings from 'vs/base/common/strings'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { compareItemsByScore, IItemAccessor, prepareQuery, ScorerCache } from 'vs/base/parts/quickopen/common/quickOpenScorer'; +import { compareItemsByScore, IItemAccessor, prepareQuery, ScorerCache } from 'vs/base/common/fuzzyScorer'; import { MAX_FILE_SIZE } from 'vs/base/node/pfs'; import { ICachedSearchStats, IFileQuery, IFileSearchStats, IFolderQuery, IProgressMessage, IRawFileQuery, IRawQuery, IRawTextQuery, ITextQuery, IFileSearchProgressItem, IRawFileMatch, IRawSearchService, ISearchEngine, ISearchEngineSuccess, ISerializedFileMatch, ISerializedSearchComplete, ISerializedSearchProgressItem, ISerializedSearchSuccess, isFilePatternMatch } from 'vs/workbench/services/search/common/search'; import { Engine as FileSearchEngine } from 'vs/workbench/services/search/node/fileSearch'; diff --git a/src/vs/workbench/services/statusbar/common/statusbar.ts b/src/vs/workbench/services/statusbar/common/statusbar.ts index 31f46524c9c..130101a530b 100644 --- a/src/vs/workbench/services/statusbar/common/statusbar.ts +++ b/src/vs/workbench/services/statusbar/common/statusbar.ts @@ -7,6 +7,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { IDisposable } from 'vs/base/common/lifecycle'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; import { Event } from 'vs/base/common/event'; +import { Command } from 'vs/editor/common/modes'; export const IStatusbarService = createDecorator('statusbarService'); @@ -45,12 +46,7 @@ export interface IStatusbarEntry { /** * An optional id of a command that is known to the workbench to execute on click */ - readonly command?: string; - - /** - * Optional arguments for the command. - */ - readonly arguments?: unknown[]; + readonly command?: string | Command; /** * Whether to show a beak above the status bar entry. diff --git a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts index 0c028fe722f..1044c382e9b 100644 --- a/src/vs/workbench/services/themes/browser/fileIconThemeData.ts +++ b/src/vs/workbench/services/themes/browser/fileIconThemeData.ts @@ -12,6 +12,9 @@ import { ExtensionData, IThemeExtensionPoint, IWorkbenchFileIconTheme } from 'vs import { IFileService } from 'vs/platform/files/common/files'; import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages'; import { asCSSUrl } from 'vs/base/browser/dom'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; + +const PERSISTED_FILE_ICON_THEME_STORAGE_KEY = 'iconThemeData'; export class FileIconThemeData implements IWorkbenchFileIconTheme { id: string; @@ -78,7 +81,7 @@ export class FileIconThemeData implements IWorkbenchFileIconTheme { private static _noIconTheme: FileIconThemeData | null = null; - static noIconTheme(): FileIconThemeData { + static get noIconTheme(): FileIconThemeData { let themeData = FileIconThemeData._noIconTheme; if (!themeData) { themeData = FileIconThemeData._noIconTheme = new FileIconThemeData('', '', null); @@ -103,7 +106,12 @@ export class FileIconThemeData implements IWorkbenchFileIconTheme { return themeData; } - static fromStorageData(input: string): FileIconThemeData | null { + + static fromStorageData(storageService: IStorageService): FileIconThemeData | undefined { + const input = storageService.get(PERSISTED_FILE_ICON_THEME_STORAGE_KEY, StorageScope.GLOBAL); + if (!input) { + return undefined; + } try { let data = JSON.parse(input); const theme = new FileIconThemeData('', '', null); @@ -128,12 +136,12 @@ export class FileIconThemeData implements IWorkbenchFileIconTheme { } return theme; } catch (e) { - return null; + return undefined; } } - toStorageData() { - return JSON.stringify({ + toStorage(storageService: IStorageService) { + const data = JSON.stringify({ id: this.id, label: this.label, description: this.description, @@ -145,6 +153,7 @@ export class FileIconThemeData implements IWorkbenchFileIconTheme { hidesExplorerArrows: this.hidesExplorerArrows, watch: this.watch }); + storageService.store(PERSISTED_FILE_ICON_THEME_STORAGE_KEY, data, StorageScope.GLOBAL); } } diff --git a/src/vs/workbench/services/themes/browser/productIconThemeData.ts b/src/vs/workbench/services/themes/browser/productIconThemeData.ts new file mode 100644 index 00000000000..a189eb6823f --- /dev/null +++ b/src/vs/workbench/services/themes/browser/productIconThemeData.ts @@ -0,0 +1,205 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from 'vs/base/common/uri'; +import * as nls from 'vs/nls'; +import * as Paths from 'vs/base/common/path'; +import * as resources from 'vs/base/common/resources'; +import * as Json from 'vs/base/common/json'; +import { ExtensionData, IThemeExtensionPoint, IWorkbenchProductIconTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import { IFileService } from 'vs/platform/files/common/files'; +import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages'; +import { asCSSUrl } from 'vs/base/browser/dom'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { DEFAULT_PRODUCT_ICON_THEME_SETTING_VALUE } from 'vs/workbench/services/themes/common/themeConfiguration'; + +const PERSISTED_PRODUCT_ICON_THEME_STORAGE_KEY = 'productIconThemeData'; + +export const DEFAULT_PRODUCT_ICON_THEME_ID = ''; // TODO + +export class ProductIconThemeData implements IWorkbenchProductIconTheme { + id: string; + label: string; + settingsId: string; + description?: string; + isLoaded: boolean; + location?: URI; + extensionData?: ExtensionData; + watch?: boolean; + + styleSheetContent?: string; + + private constructor(id: string, label: string, settingsId: string) { + this.id = id; + this.label = label; + this.settingsId = settingsId; + this.isLoaded = false; + } + + public ensureLoaded(fileService: IFileService): Promise { + return !this.isLoaded ? this.load(fileService) : Promise.resolve(this.styleSheetContent); + } + + public reload(fileService: IFileService): Promise { + return this.load(fileService); + } + + private load(fileService: IFileService): Promise { + if (!this.location) { + return Promise.resolve(this.styleSheetContent); + } + return _loadProductIconThemeDocument(fileService, this.location).then(iconThemeDocument => { + const result = _processIconThemeDocument(this.id, this.location!, iconThemeDocument); + this.styleSheetContent = result.content; + this.isLoaded = true; + return this.styleSheetContent; + }); + } + + static fromExtensionTheme(iconTheme: IThemeExtensionPoint, iconThemeLocation: URI, extensionData: ExtensionData): ProductIconThemeData { + const id = extensionData.extensionId + '-' + iconTheme.id; + const label = iconTheme.label || Paths.basename(iconTheme.path); + const settingsId = iconTheme.id; + + const themeData = new ProductIconThemeData(id, label, settingsId); + + themeData.description = iconTheme.description; + themeData.location = iconThemeLocation; + themeData.extensionData = extensionData; + themeData.watch = iconTheme._watch; + themeData.isLoaded = false; + return themeData; + } + + static createUnloadedTheme(id: string): ProductIconThemeData { + const themeData = new ProductIconThemeData(id, '', '__' + id); + themeData.isLoaded = false; + themeData.extensionData = undefined; + themeData.watch = false; + return themeData; + } + + private static _defaultProductIconTheme: ProductIconThemeData | null = null; + + static get defaultTheme(): ProductIconThemeData { + let themeData = ProductIconThemeData._defaultProductIconTheme; + if (!themeData) { + themeData = ProductIconThemeData._defaultProductIconTheme = new ProductIconThemeData(DEFAULT_PRODUCT_ICON_THEME_ID, nls.localize('defaultTheme', 'Default theme'), DEFAULT_PRODUCT_ICON_THEME_SETTING_VALUE); + themeData.isLoaded = true; + themeData.extensionData = undefined; + themeData.watch = false; + } + return themeData; + } + + static fromStorageData(storageService: IStorageService): ProductIconThemeData | undefined { + const input = storageService.get(PERSISTED_PRODUCT_ICON_THEME_STORAGE_KEY, StorageScope.GLOBAL); + if (!input) { + return undefined; + } + try { + let data = JSON.parse(input); + const theme = new ProductIconThemeData('', '', ''); + for (let key in data) { + switch (key) { + case 'id': + case 'label': + case 'description': + case 'settingsId': + case 'extensionData': + case 'styleSheetContent': + case 'watch': + (theme as any)[key] = data[key]; + break; + case 'location': + theme.location = URI.revive(data.location); + break; + } + } + return theme; + } catch (e) { + return undefined; + } + } + + toStorage(storageService: IStorageService) { + const data = JSON.stringify({ + id: this.id, + label: this.label, + description: this.description, + settingsId: this.settingsId, + location: this.location, + styleSheetContent: this.styleSheetContent, + watch: this.watch + }); + storageService.store(PERSISTED_PRODUCT_ICON_THEME_STORAGE_KEY, data, StorageScope.GLOBAL); + } +} + +interface IconDefinition { + fontCharacter: string; + fontId: string; +} + +interface FontDefinition { + id: string; + weight: string; + style: string; + size: string; + src: { path: string; format: string; }[]; +} + +interface ProductIconThemeDocument { + iconDefinitions: { [key: string]: IconDefinition }; + fonts: FontDefinition[]; +} + +function _loadProductIconThemeDocument(fileService: IFileService, location: URI): Promise { + return fileService.readFile(location).then((content) => { + let errors: Json.ParseError[] = []; + let contentValue = Json.parse(content.value.toString(), errors); + if (errors.length > 0) { + return Promise.reject(new Error(nls.localize('error.cannotparseicontheme', "Problems parsing product icons 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 product icons theme file: Object expected."))); + } else if (!contentValue.iconDefinitions || !Array.isArray(contentValue.fonts) || !contentValue.fonts.length) { + return Promise.reject(new Error(nls.localize('error.missingProperties', "Invalid format for product icons theme file: Must contain iconDefinitions and fonts."))); + } + return Promise.resolve(contentValue); + }); +} + +function _processIconThemeDocument(id: string, iconThemeDocumentLocation: URI, iconThemeDocument: ProductIconThemeDocument): { content: string; } { + + const result = { content: '' }; + + if (!iconThemeDocument.iconDefinitions || !Array.isArray(iconThemeDocument.fonts) || !iconThemeDocument.fonts.length) { + return result; + } + + const iconThemeDocumentLocationDirname = resources.dirname(iconThemeDocumentLocation); + function resolvePath(path: string) { + return resources.joinPath(iconThemeDocumentLocationDirname, path); + } + + let cssRules: string[] = []; + + let fonts = iconThemeDocument.fonts; + for (const font of fonts) { + const src = font.src.map(l => `${asCSSUrl(resolvePath(l.path))} format('${l.format}')`).join(', '); + cssRules.push(`@font-face { src: ${src}; font-family: '${font.id}'; font-weight: ${font.weight}; font-style: ${font.style}; }`); + } + + let primaryFontId = fonts[0].id; + let iconDefinitions = iconThemeDocument.iconDefinitions; + for (const iconId in iconThemeDocument.iconDefinitions) { + const definition = iconDefinitions[iconId]; + if (definition && definition.fontCharacter) { + cssRules.push(`.codicon-${iconId}:before { content: '${definition.fontCharacter}' !important; font-family: ${definition.fontId || primaryFontId} !important; }`); + } + } + result.content = cssRules.join('\n'); + return result; +} diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index 31bd9419b2a..47a3e6a33be 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import * as types from 'vs/base/common/types'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IWorkbenchThemeService, IWorkbenchColorTheme, ITokenColorCustomizations, IWorkbenchFileIconTheme, ExtensionData, VS_LIGHT_THEME, VS_DARK_THEME, VS_HC_THEME, ThemeSettings, IColorCustomizations, IExperimentalTokenStyleCustomizations } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import { IWorkbenchThemeService, IWorkbenchColorTheme, IWorkbenchFileIconTheme, ExtensionData, VS_LIGHT_THEME, VS_DARK_THEME, VS_HC_THEME, ThemeSettings, IWorkbenchProductIconTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -28,27 +28,26 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { IExtensionResourceLoaderService } from 'vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader'; -import { ThemeRegistry, registerColorThemeExtensionPoint, registerFileIconThemeExtensionPoint } from 'vs/workbench/services/themes/common/themeExtensionPoints'; -import { updateColorThemeConfigurationSchemas, updateFileIconThemeConfigurationSchemas } from 'vs/workbench/services/themes/common/themeConfiguration'; - +import { ThemeRegistry, registerColorThemeExtensionPoint, registerFileIconThemeExtensionPoint, registerProductIconThemeExtensionPoint } from 'vs/workbench/services/themes/common/themeExtensionPoints'; +import { updateColorThemeConfigurationSchemas, updateFileIconThemeConfigurationSchemas, ThemeConfiguration, updateProductIconThemeConfigurationSchemas } from 'vs/workbench/services/themes/common/themeConfiguration'; +import { ProductIconThemeData, DEFAULT_PRODUCT_ICON_THEME_ID } from 'vs/workbench/services/themes/browser/productIconThemeData'; +import { registerProductIconThemeSchemas } from 'vs/workbench/services/themes/common/productIconThemeSchema'; // implementation -const DEFAULT_THEME_ID = 'vs-dark vscode-theme-defaults-themes-dark_plus-json'; +const DEFAULT_COLOR_THEME_ID = 'vs-dark vscode-theme-defaults-themes-dark_plus-json'; - -const PERSISTED_THEME_STORAGE_KEY = 'colorThemeData'; -const PERSISTED_ICON_THEME_STORAGE_KEY = 'iconThemeData'; const PERSISTED_OS_COLOR_SCHEME = 'osColorScheme'; const defaultThemeExtensionId = 'vscode-theme-defaults'; const oldDefaultThemeExtensionId = 'vscode-theme-colorful-defaults'; -const DEFAULT_ICON_THEME_ID = 'vscode.vscode-theme-seti-vs-seti'; +const DEFAULT_FILE_ICON_THEME_ID = 'vscode.vscode-theme-seti-vs-seti'; const fileIconsEnabledClass = 'file-icons-enabled'; const colorThemeRulesClassName = 'contributedColorTheme'; -const iconThemeRulesClassName = 'contributedIconTheme'; +const fileIconThemeRulesClassName = 'contributedFileIconTheme'; +const productIconThemeRulesClassName = 'contributedProductIconTheme'; const themingRegistry = Registry.as(ThemingExtensions.ThemingContribution); @@ -66,36 +65,29 @@ function validateThemeId(theme: string): string { const colorThemesExtPoint = registerColorThemeExtensionPoint(); const fileIconThemesExtPoint = registerFileIconThemeExtensionPoint(); +const productIconThemesExtPoint = registerProductIconThemeExtensionPoint(); export class WorkbenchThemeService implements IWorkbenchThemeService { _serviceBrand: undefined; - private colorThemeRegistry: ThemeRegistry; - private currentColorTheme: ColorThemeData; - private container: HTMLElement; - private readonly onColorThemeChange: Emitter; - private watchedColorThemeLocation: URI | undefined; - private watchedColorThemeDisposable: IDisposable | undefined; + private readonly container: HTMLElement; + private settings: ThemeConfiguration; - private iconThemeRegistry: ThemeRegistry; + private readonly colorThemeRegistry: ThemeRegistry; + private currentColorTheme: ColorThemeData; + private readonly onColorThemeChange: Emitter; + private readonly colorThemeWatcher: ThemeFileWatcher; + private colorThemingParticipantChangeListener: IDisposable | undefined; + + private readonly fileIconThemeRegistry: ThemeRegistry; private currentFileIconTheme: FileIconThemeData; private readonly onFileIconThemeChange: Emitter; - private watchedIconThemeLocation: URI | undefined; - private watchedIconThemeDisposable: IDisposable | undefined; + private readonly fileIconThemeWatcher: ThemeFileWatcher; - private themingParticipantChangeListener: IDisposable | undefined; - - private get colorCustomizations(): IColorCustomizations { - return this.configurationService.getValue(ThemeSettings.COLOR_CUSTOMIZATIONS) || {}; - } - - private get tokenColorCustomizations(): ITokenColorCustomizations { - return this.configurationService.getValue(ThemeSettings.TOKEN_COLOR_CUSTOMIZATIONS) || {}; - } - - private get tokenStylesCustomizations(): IExperimentalTokenStyleCustomizations { - return this.configurationService.getValue(ThemeSettings.TOKEN_COLOR_CUSTOMIZATIONS_EXPERIMENTAL) || {}; - } + private readonly productIconThemeRegistry: ThemeRegistry; + private currentProductIconTheme: ProductIconThemeData; + private readonly onProductIconThemeChange: Emitter; + private readonly productIconThemeWatcher: ThemeFileWatcher; constructor( @IExtensionService extensionService: IExtensionService, @@ -107,43 +99,43 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { @IExtensionResourceLoaderService private readonly extensionResourceLoaderService: IExtensionResourceLoaderService, @IWorkbenchLayoutService readonly layoutService: IWorkbenchLayoutService ) { - this.container = layoutService.getWorkbenchContainer(); - this.colorThemeRegistry = new ThemeRegistry(extensionService, colorThemesExtPoint, ColorThemeData.fromExtensionTheme); - this.onFileIconThemeChange = new Emitter(); - this.iconThemeRegistry = new ThemeRegistry(extensionService, fileIconThemesExtPoint, FileIconThemeData.fromExtensionTheme, true); - this.onColorThemeChange = new Emitter({ leakWarningThreshold: 400 }); + this.settings = new ThemeConfiguration(configurationService); + this.colorThemeRegistry = new ThemeRegistry(extensionService, colorThemesExtPoint, ColorThemeData.fromExtensionTheme); + this.colorThemeWatcher = new ThemeFileWatcher(fileService, environmentService, this.reloadCurrentColorTheme.bind(this)); + this.onColorThemeChange = new Emitter({ leakWarningThreshold: 400 }); this.currentColorTheme = ColorThemeData.createUnloadedTheme(''); + + this.fileIconThemeWatcher = new ThemeFileWatcher(fileService, environmentService, this.reloadCurrentFileIconTheme.bind(this)); + this.fileIconThemeRegistry = new ThemeRegistry(extensionService, fileIconThemesExtPoint, FileIconThemeData.fromExtensionTheme, true, FileIconThemeData.noIconTheme); + this.onFileIconThemeChange = new Emitter(); this.currentFileIconTheme = FileIconThemeData.createUnloadedTheme(''); + this.productIconThemeWatcher = new ThemeFileWatcher(fileService, environmentService, this.reloadCurrentProductIconTheme.bind(this)); + this.productIconThemeRegistry = new ThemeRegistry(extensionService, productIconThemesExtPoint, ProductIconThemeData.fromExtensionTheme, true, ProductIconThemeData.defaultTheme); + this.onProductIconThemeChange = new Emitter(); + this.currentProductIconTheme = ProductIconThemeData.createUnloadedTheme(''); + // In order to avoid paint flashing for tokens, because // themes are loaded asynchronously, we need to initialize // a color theme document with good defaults until the theme is loaded - let themeData: ColorThemeData | undefined = undefined; - let persistedThemeData = this.storageService.get(PERSISTED_THEME_STORAGE_KEY, StorageScope.GLOBAL); - if (persistedThemeData) { - themeData = ColorThemeData.fromStorageData(persistedThemeData); - } - let containerBaseTheme = this.getBaseThemeFromContainer(); + let themeData: ColorThemeData | undefined = ColorThemeData.fromStorageData(this.storageService); + const containerBaseTheme = this.getBaseThemeFromContainer(); if (!themeData || themeData.baseTheme !== containerBaseTheme) { themeData = ColorThemeData.createUnloadedTheme(containerBaseTheme); } - themeData.setCustomColors(this.colorCustomizations); - themeData.setCustomTokenColors(this.tokenColorCustomizations); - themeData.setCustomTokenStyleRules(this.tokenStylesCustomizations); - this.updateDynamicCSSRules(themeData); + themeData.setCustomizations(this.settings); this.applyTheme(themeData, undefined, true); - let persistedIconThemeData = this.storageService.get(PERSISTED_ICON_THEME_STORAGE_KEY, StorageScope.GLOBAL); - if (persistedIconThemeData) { - const iconData = FileIconThemeData.fromStorageData(persistedIconThemeData); - if (iconData) { - _applyIconTheme(iconData, () => { - this.doSetFileIconTheme(iconData); - return Promise.resolve(iconData); - }); - } + const fileIconData = FileIconThemeData.fromStorageData(this.storageService); + if (fileIconData) { + this.applyAndSetFileIconTheme(fileIconData); + } + + const productIconData = ProductIconThemeData.fromStorageData(this.storageService); + if (productIconData) { + this.applyAndSetProductIconTheme(productIconData); } this.initialize().then(undefined, errors.onUnexpectedError).then(_ => { @@ -157,7 +149,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { this.colorThemeRegistry.onDidChange(async event => { updateColorThemeConfigurationSchemas(event.themes); - let colorThemeSetting = this.configurationService.getValue(ThemeSettings.COLOR_THEME); + const colorThemeSetting = this.settings.colorTheme; if (colorThemeSetting !== this.currentColorTheme.settingsId) { const theme = await this.colorThemeRegistry.findThemeBySettingsId(colorThemeSetting, undefined); if (theme) { @@ -171,10 +163,10 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { if (!themeData) { // current theme is no longer available prevColorId = this.currentColorTheme.id; - this.setColorTheme(DEFAULT_THEME_ID, 'auto'); + this.setColorTheme(DEFAULT_COLOR_THEME_ID, 'auto'); } else { - if (this.currentColorTheme.id === DEFAULT_THEME_ID && !types.isUndefined(prevColorId) && await this.colorThemeRegistry.findThemeById(prevColorId)) { - // restore color + if (this.currentColorTheme.id === DEFAULT_COLOR_THEME_ID && !types.isUndefined(prevColorId) && await this.colorThemeRegistry.findThemeById(prevColorId)) { + // restore theme this.setColorTheme(prevColorId, 'auto'); prevColorId = undefined; } else { @@ -185,42 +177,40 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { }); let prevFileIconId: string | undefined = undefined; - this.iconThemeRegistry.onDidChange(async event => { + this.fileIconThemeRegistry.onDidChange(async event => { updateFileIconThemeConfigurationSchemas(event.themes); - let iconThemeSetting = this.configurationService.getValue(ThemeSettings.ICON_THEME); - if (iconThemeSetting !== this.currentFileIconTheme.settingsId) { - const theme = await this.findFileIconThemeBySettingId(iconThemeSetting); - if (theme) { - this.setFileIconTheme(theme.id, undefined); - return; - } - } - - if (this.currentFileIconTheme.isLoaded) { - const theme = await this.findFileIconThemeById(this.currentFileIconTheme.id); - if (!theme) { - // current theme is no longer available - prevFileIconId = this.currentFileIconTheme.id; - this.setFileIconTheme(DEFAULT_ICON_THEME_ID, 'auto'); + if (!await this.restoreFileIconTheme()) { // checks if theme from settings exists and is set + // restore theme + if (this.currentFileIconTheme.id === DEFAULT_FILE_ICON_THEME_ID && !types.isUndefined(prevFileIconId) && await this.fileIconThemeRegistry.findThemeById(prevFileIconId)) { + this.setFileIconTheme(prevFileIconId, 'auto'); + prevFileIconId = undefined; } else { - // restore color - if (this.currentFileIconTheme.id === DEFAULT_ICON_THEME_ID && !types.isUndefined(prevFileIconId) && await this.findFileIconThemeById(prevFileIconId)) { - this.setFileIconTheme(prevFileIconId, 'auto'); - prevFileIconId = undefined; - } else { - this.reloadCurrentFileIconTheme(); - } + this.reloadCurrentFileIconTheme(); } + } else { + // current theme is no longer available + prevFileIconId = this.currentFileIconTheme.id; + this.setFileIconTheme(DEFAULT_FILE_ICON_THEME_ID, 'auto'); } }); - this.fileService.onDidFilesChange(async e => { - if (this.watchedColorThemeLocation && this.currentColorTheme && e.contains(this.watchedColorThemeLocation, FileChangeType.UPDATED)) { - this.reloadCurrentColorTheme(); - } - if (this.watchedIconThemeLocation && this.currentFileIconTheme && e.contains(this.watchedIconThemeLocation, FileChangeType.UPDATED)) { - this.reloadCurrentFileIconTheme(); + let prevProductIconId: string | undefined = undefined; + this.productIconThemeRegistry.onDidChange(async event => { + updateProductIconThemeConfigurationSchemas(event.themes); + + if (await this.restoreProductIconTheme()) { // checks if theme from settings exists and is set + // restore theme + if (this.currentProductIconTheme.id === DEFAULT_PRODUCT_ICON_THEME_ID && !types.isUndefined(prevProductIconId) && await this.productIconThemeRegistry.findThemeById(prevProductIconId)) { + this.setProductIconTheme(prevProductIconId, 'auto'); + prevProductIconId = undefined; + } else { + this.reloadCurrentProductIconTheme(); + } + } else { + // current theme is no longer available + prevProductIconId = this.currentProductIconTheme.id; + this.setProductIconTheme(DEFAULT_PRODUCT_ICON_THEME_ID, 'auto'); } }); } @@ -229,24 +219,16 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { return this.onColorThemeChange.event; } - public get onDidFileIconThemeChange(): Event { - return this.onFileIconThemeChange.event; - } - - private initialize(): Promise<[IWorkbenchColorTheme | null, IWorkbenchFileIconTheme | null]> { - const colorThemeSetting = this.configurationService.getValue(ThemeSettings.COLOR_THEME); - const iconThemeSetting = this.configurationService.getValue(ThemeSettings.ICON_THEME); - + private initialize(): Promise<[IWorkbenchColorTheme | null, IWorkbenchFileIconTheme | null, IWorkbenchProductIconTheme | null]> { const extDevLocs = this.environmentService.extensionDevelopmentLocationURI; + const extDevLoc = extDevLocs && extDevLocs.length === 1 ? extDevLocs[0] : undefined; // in dev mode, switch to a theme provided by the extension under dev. const initializeColorTheme = async () => { - if (extDevLocs && extDevLocs.length === 1) { // in dev mode, switch to a theme provided by the extension under dev. - const devThemes = await this.colorThemeRegistry.findThemeByExtensionLocation(extDevLocs[0]); - if (devThemes.length) { - return this.setColorTheme(devThemes[0].id, ConfigurationTarget.MEMORY); - } + const devThemes = await this.colorThemeRegistry.findThemeByExtensionLocation(extDevLoc); + if (devThemes.length) { + return this.setColorTheme(devThemes[0].id, ConfigurationTarget.MEMORY); } - let theme = await this.colorThemeRegistry.findThemeBySettingsId(colorThemeSetting, DEFAULT_THEME_ID); + const theme = await this.colorThemeRegistry.findThemeBySettingsId(this.settings.colorTheme, DEFAULT_COLOR_THEME_ID); const persistedColorScheme = this.storageService.get(PERSISTED_OS_COLOR_SCHEME, StorageScope.GLOBAL); const preferredColorScheme = this.getPreferredColorScheme(); @@ -256,31 +238,31 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { return this.setColorTheme(theme && theme.id, undefined); }; - const initializeIconTheme = async () => { - if (extDevLocs && extDevLocs.length === 1) { // in dev mode, switch to a theme provided by the extension under dev. - const devThemes = await this.iconThemeRegistry.findThemeByExtensionLocation(extDevLocs[0]); - if (devThemes.length) { - return this.setFileIconTheme(devThemes[0].id, ConfigurationTarget.MEMORY); - } + const initializeFileIconTheme = async () => { + const devThemes = await this.fileIconThemeRegistry.findThemeByExtensionLocation(extDevLoc); + if (devThemes.length) { + return this.setFileIconTheme(devThemes[0].id, ConfigurationTarget.MEMORY); } - const theme = await this.findFileIconThemeBySettingId(iconThemeSetting); - return this.setFileIconTheme(theme ? theme.id : DEFAULT_ICON_THEME_ID, undefined); + const theme = await this.fileIconThemeRegistry.findThemeBySettingsId(this.settings.fileIconTheme); + return this.setFileIconTheme(theme ? theme.id : DEFAULT_FILE_ICON_THEME_ID, undefined); }; - return Promise.all([initializeColorTheme(), initializeIconTheme()]); + const initializeProductIconTheme = async () => { + const devThemes = await this.productIconThemeRegistry.findThemeByExtensionLocation(extDevLoc); + if (devThemes.length) { + return this.setProductIconTheme(devThemes[0].id, ConfigurationTarget.MEMORY); + } + const theme = await this.productIconThemeRegistry.findThemeBySettingsId(this.settings.productIconTheme); + return this.setProductIconTheme(theme ? theme.id : DEFAULT_PRODUCT_ICON_THEME_ID, undefined); + }; + + return Promise.all([initializeColorTheme(), initializeFileIconTheme(), initializeProductIconTheme()]); } private installConfigurationListener() { this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(ThemeSettings.COLOR_THEME)) { - let colorThemeSetting = this.configurationService.getValue(ThemeSettings.COLOR_THEME); - if (colorThemeSetting !== this.currentColorTheme.settingsId) { - this.colorThemeRegistry.findThemeBySettingsId(colorThemeSetting, undefined).then(theme => { - if (theme) { - this.setColorTheme(theme.id, undefined); - } - }); - } + this.restoreColorTheme(); } if (e.affectsConfiguration(ThemeSettings.DETECT_COLOR_SCHEME)) { this.handlePreferredSchemeUpdated(); @@ -295,25 +277,23 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { this.applyPreferredColorTheme(HIGH_CONTRAST); } if (e.affectsConfiguration(ThemeSettings.ICON_THEME)) { - let iconThemeSetting = this.configurationService.getValue(ThemeSettings.ICON_THEME); - if (iconThemeSetting !== this.currentFileIconTheme.settingsId) { - this.findFileIconThemeBySettingId(iconThemeSetting).then(theme => { - this.setFileIconTheme(theme ? theme.id : DEFAULT_ICON_THEME_ID, undefined); - }); - } + this.restoreFileIconTheme(); + } + if (e.affectsConfiguration(ThemeSettings.PRODUCT_ICON_THEME)) { + this.restoreProductIconTheme(); } if (this.currentColorTheme) { let hasColorChanges = false; if (e.affectsConfiguration(ThemeSettings.COLOR_CUSTOMIZATIONS)) { - this.currentColorTheme.setCustomColors(this.colorCustomizations); + this.currentColorTheme.setCustomColors(this.settings.colorCustomizations); hasColorChanges = true; } if (e.affectsConfiguration(ThemeSettings.TOKEN_COLOR_CUSTOMIZATIONS)) { - this.currentColorTheme.setCustomTokenColors(this.tokenColorCustomizations); + this.currentColorTheme.setCustomTokenColors(this.settings.tokenColorCustomizations); hasColorChanges = true; } if (e.affectsConfiguration(ThemeSettings.TOKEN_COLOR_CUSTOMIZATIONS_EXPERIMENTAL)) { - this.currentColorTheme.setCustomTokenStyleRules(this.tokenStylesCustomizations); + this.currentColorTheme.setCustomTokenStyleRules(this.settings.tokenStylesCustomizations); hasColorChanges = true; } if (hasColorChanges) { @@ -340,7 +320,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { } private getPreferredColorScheme(): ThemeType | undefined { - let detectHCThemeSetting = this.configurationService.getValue(ThemeSettings.DETECT_HC); + const detectHCThemeSetting = this.configurationService.getValue(ThemeSettings.DETECT_HC); if (this.environmentService.configuration.highContrast && detectHCThemeSetting) { return HIGH_CONTRAST; } @@ -379,12 +359,12 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { return Promise.resolve(null); } if (themeId === this.currentColorTheme.id && this.currentColorTheme.isLoaded) { - return this.writeColorThemeConfiguration(settingsTarget); + return this.settings.setColorTheme(this.currentColorTheme, settingsTarget); } themeId = validateThemeId(themeId); // migrate theme ids - return this.colorThemeRegistry.findThemeById(themeId, DEFAULT_THEME_ID).then(themeData => { + return this.colorThemeRegistry.findThemeById(themeId, DEFAULT_COLOR_THEME_ID).then(themeData => { if (!themeData) { return null; } @@ -393,15 +373,10 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { this.currentColorTheme.clearCaches(); // the loaded theme is identical to the perisisted theme. Don't need to send an event. this.currentColorTheme = themeData; - themeData.setCustomColors(this.colorCustomizations); - themeData.setCustomTokenColors(this.tokenColorCustomizations); - themeData.setCustomTokenStyleRules(this.tokenStylesCustomizations); + themeData.setCustomizations(this.settings); return Promise.resolve(themeData); } - themeData.setCustomColors(this.colorCustomizations); - themeData.setCustomTokenColors(this.tokenColorCustomizations); - themeData.setCustomTokenStyleRules(this.tokenStylesCustomizations); - this.updateDynamicCSSRules(themeData); + themeData.setCustomizations(this.settings); return this.applyTheme(themeData, settingsTarget); }, error => { return Promise.reject(new Error(nls.localize('error.cannotloadtheme', "Unable to load {0}: {1}", themeData.location!.toString(), error.message))); @@ -411,15 +386,12 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { private async reloadCurrentColorTheme() { await this.currentColorTheme.reload(this.extensionResourceLoaderService); - this.currentColorTheme.setCustomColors(this.colorCustomizations); - this.currentColorTheme.setCustomTokenColors(this.tokenColorCustomizations); - this.currentColorTheme.setCustomTokenStyleRules(this.tokenStylesCustomizations); - this.updateDynamicCSSRules(this.currentColorTheme); + this.currentColorTheme.setCustomizations(this.settings); this.applyTheme(this.currentColorTheme, undefined, false); } public restoreColorTheme() { - let colorThemeSetting = this.configurationService.getValue(ThemeSettings.COLOR_THEME); + const colorThemeSetting = this.settings.colorTheme; if (colorThemeSetting !== this.currentColorTheme.settingsId) { this.colorThemeRegistry.findThemeBySettingsId(colorThemeSetting, undefined).then(theme => { if (theme) { @@ -443,6 +415,8 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { } private applyTheme(newTheme: ColorThemeData, settingsTarget: ConfigurationTarget | undefined | 'auto', silent = false): Promise { + this.updateDynamicCSSRules(newTheme); + if (this.currentColorTheme.id) { removeClasses(this.container, this.currentColorTheme.id); } else { @@ -452,19 +426,11 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { this.currentColorTheme.clearCaches(); this.currentColorTheme = newTheme; - if (!this.themingParticipantChangeListener) { - this.themingParticipantChangeListener = themingRegistry.onThemingParticipantAdded(_ => this.updateDynamicCSSRules(this.currentColorTheme)); + if (!this.colorThemingParticipantChangeListener) { + this.colorThemingParticipantChangeListener = themingRegistry.onThemingParticipantAdded(_ => this.updateDynamicCSSRules(this.currentColorTheme)); } - if (this.fileService && !resources.isEqual(newTheme.location, this.watchedColorThemeLocation)) { - dispose(this.watchedColorThemeDisposable); - this.watchedColorThemeLocation = undefined; - - if (newTheme.location && (newTheme.watch || !!this.environmentService.extensionDevelopmentLocationURI)) { - this.watchedColorThemeLocation = newTheme.location; - this.watchedColorThemeDisposable = this.fileService.watch(newTheme.location); - } - } + this.colorThemeWatcher.update(newTheme); this.sendTelemetry(newTheme.id, newTheme.extensionData, 'color'); @@ -476,23 +442,17 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { // remember theme data for a quick restore if (newTheme.isLoaded) { - this.storageService.store(PERSISTED_THEME_STORAGE_KEY, newTheme.toStorageData(), StorageScope.GLOBAL); + newTheme.toStorage(this.storageService); } - return this.writeColorThemeConfiguration(settingsTarget); + return this.settings.setColorTheme(this.currentColorTheme, settingsTarget); } - private writeColorThemeConfiguration(settingsTarget: ConfigurationTarget | undefined | 'auto'): Promise { - if (!types.isUndefinedOrNull(settingsTarget)) { - return this.writeConfiguration(ThemeSettings.COLOR_THEME, this.currentColorTheme.settingsId, settingsTarget).then(_ => this.currentColorTheme); - } - return Promise.resolve(this.currentColorTheme); - } private themeExtensionsActivated = new Map(); private sendTelemetry(themeId: string, themeData: ExtensionData | undefined, themeType: string) { if (themeData) { - let key = themeType + themeData.extensionId; + const key = themeType + themeData.extensionId; if (!this.themeExtensionsActivated.get(key)) { type ActivatePluginClassification = { id: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' }; @@ -521,82 +481,68 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { } public getFileIconThemes(): Promise { - return this.iconThemeRegistry.getThemes(); + return this.fileIconThemeRegistry.getThemes(); } public getFileIconTheme() { return this.currentFileIconTheme; } - public setFileIconTheme(iconTheme: string | undefined, settingsTarget: ConfigurationTarget | undefined | 'auto'): Promise { + public get onDidFileIconThemeChange(): Event { + return this.onFileIconThemeChange.event; + } + + + public async setFileIconTheme(iconTheme: string | undefined, settingsTarget: ConfigurationTarget | undefined | 'auto'): Promise { iconTheme = iconTheme || ''; if (iconTheme === this.currentFileIconTheme.id && this.currentFileIconTheme.isLoaded) { - return this.writeFileIconConfiguration(settingsTarget); + await this.settings.setFileIconTheme(this.currentFileIconTheme, settingsTarget); + return this.currentFileIconTheme; } - const onApply = (newIconTheme: FileIconThemeData) => { - this.doSetFileIconTheme(newIconTheme); - // remember theme data for a quick restore - if (newIconTheme.isLoaded && (!newIconTheme.location || !getRemoteAuthority(newIconTheme.location))) { - this.storageService.store(PERSISTED_ICON_THEME_STORAGE_KEY, newIconTheme.toStorageData(), StorageScope.GLOBAL); - } + const newThemeData = (await this.fileIconThemeRegistry.findThemeById(iconTheme)) || FileIconThemeData.noIconTheme; + await newThemeData.ensureLoaded(this.fileService); - return this.writeFileIconConfiguration(settingsTarget); - }; + this.applyAndSetFileIconTheme(newThemeData); - return this.findFileIconThemeById(iconTheme).then(data => { - const iconThemeData = data || FileIconThemeData.noIconTheme(); - return iconThemeData.ensureLoaded(this.fileService).then(_ => { - return _applyIconTheme(iconThemeData, onApply); - }); - }); - } + // remember theme data for a quick restore + if (newThemeData.isLoaded && (!newThemeData.location || !getRemoteAuthority(newThemeData.location))) { + newThemeData.toStorage(this.storageService); + } + await this.settings.setFileIconTheme(this.currentFileIconTheme, settingsTarget); - private async findFileIconThemeById(id: string): Promise { - return id.length === 0 ? FileIconThemeData.noIconTheme() : this.iconThemeRegistry.findThemeById(id); - } - - private async findFileIconThemeBySettingId(settingsId: string | null): Promise { - return !settingsId ? FileIconThemeData.noIconTheme() : this.iconThemeRegistry.findThemeBySettingsId(settingsId); + return newThemeData; } private async reloadCurrentFileIconTheme() { await this.currentFileIconTheme.reload(this.fileService); - _applyIconTheme(this.currentFileIconTheme, () => { - this.doSetFileIconTheme(this.currentFileIconTheme); - return Promise.resolve(this.currentFileIconTheme); - }); + this.applyAndSetFileIconTheme(this.currentFileIconTheme); } - public restoreFileIconTheme() { - let fileIconThemeSetting = this.configurationService.getValue(ThemeSettings.ICON_THEME); - if (fileIconThemeSetting !== this.currentFileIconTheme.settingsId) { - this.iconThemeRegistry.findThemeBySettingsId(fileIconThemeSetting).then(theme => { - if (theme) { - this.setFileIconTheme(theme.id, undefined); - } - }); + public async restoreFileIconTheme(): Promise { + const settingId = this.settings.fileIconTheme; + const theme = await this.fileIconThemeRegistry.findThemeBySettingsId(settingId); + if (theme) { + if (settingId !== this.currentFileIconTheme.settingsId) { + await this.setFileIconTheme(theme.id, undefined); + } + return true; } + return false; } - private doSetFileIconTheme(iconThemeData: FileIconThemeData): void { + private applyAndSetFileIconTheme(iconThemeData: FileIconThemeData): void { this.currentFileIconTheme = iconThemeData; + _applyRules(iconThemeData.styleSheetContent!, fileIconThemeRulesClassName); + if (iconThemeData.id) { addClasses(this.container, fileIconsEnabledClass); } else { removeClasses(this.container, fileIconsEnabledClass); } - if (this.fileService && !resources.isEqual(iconThemeData.location, this.watchedIconThemeLocation)) { - dispose(this.watchedIconThemeDisposable); - this.watchedIconThemeLocation = undefined; - - if (iconThemeData.location && (iconThemeData.watch || !!this.environmentService.extensionDevelopmentLocationURI)) { - this.watchedIconThemeLocation = iconThemeData.location; - this.watchedIconThemeDisposable = this.fileService.watch(iconThemeData.location); - } - } + this.fileIconThemeWatcher.update(iconThemeData); if (iconThemeData.id) { this.sendTelemetry(iconThemeData.id, iconThemeData.extensionData, 'fileIcon'); @@ -605,40 +551,69 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { } - private writeFileIconConfiguration(settingsTarget: ConfigurationTarget | undefined | 'auto'): Promise { - if (!types.isUndefinedOrNull(settingsTarget)) { - return this.writeConfiguration(ThemeSettings.ICON_THEME, this.currentFileIconTheme.settingsId, settingsTarget).then(_ => this.currentFileIconTheme); - } - return Promise.resolve(this.currentFileIconTheme); + public getProductIconThemes(): Promise { + return this.productIconThemeRegistry.getThemes(); } - public writeConfiguration(key: string, value: any, settingsTarget: ConfigurationTarget | 'auto'): Promise { - let settings = this.configurationService.inspect(key); - if (settingsTarget === 'auto') { - if (!types.isUndefined(settings.workspaceFolderValue)) { - settingsTarget = ConfigurationTarget.WORKSPACE_FOLDER; - } else if (!types.isUndefined(settings.workspaceValue)) { - settingsTarget = ConfigurationTarget.WORKSPACE; - } else { - settingsTarget = ConfigurationTarget.USER; - } + public getProductIconTheme() { + return this.currentProductIconTheme; + } + + public get onDidProductIconThemeChange(): Event { + return this.onProductIconThemeChange.event; + } + + public async setProductIconTheme(iconTheme: string | undefined, settingsTarget: ConfigurationTarget | undefined | 'auto'): Promise { + iconTheme = iconTheme || ''; + if (iconTheme === this.currentProductIconTheme.id && this.currentProductIconTheme.isLoaded) { + await this.settings.setProductIconTheme(this.currentProductIconTheme, settingsTarget); + return this.currentProductIconTheme; } - if (settingsTarget === ConfigurationTarget.USER) { - if (value === settings.userValue) { - return Promise.resolve(undefined); // nothing to do - } else if (value === settings.defaultValue) { - if (types.isUndefined(settings.userValue)) { - return Promise.resolve(undefined); // nothing to do - } - value = undefined; // remove configuration from user settings - } - } else if (settingsTarget === ConfigurationTarget.WORKSPACE || settingsTarget === ConfigurationTarget.WORKSPACE_FOLDER) { - if (value === settings.value) { - return Promise.resolve(undefined); // nothing to do - } + const newThemeData = await this.productIconThemeRegistry.findThemeById(iconTheme) || ProductIconThemeData.defaultTheme; + await newThemeData.ensureLoaded(this.fileService); + + this.applyAndSetProductIconTheme(newThemeData); + + // remember theme data for a quick restore + if (newThemeData.isLoaded && (!newThemeData.location || !getRemoteAuthority(newThemeData.location))) { + newThemeData.toStorage(this.storageService); } - return this.configurationService.updateValue(key, value, settingsTarget); + await this.settings.setProductIconTheme(this.currentProductIconTheme, settingsTarget); + + return newThemeData; + } + + private async reloadCurrentProductIconTheme() { + await this.currentProductIconTheme.reload(this.fileService); + this.applyAndSetProductIconTheme(this.currentProductIconTheme); + } + + public async restoreProductIconTheme(): Promise { + const settingId = this.settings.productIconTheme; + const theme = await this.productIconThemeRegistry.findThemeBySettingsId(settingId); + if (theme) { + if (settingId !== this.currentProductIconTheme.settingsId) { + await this.setProductIconTheme(theme.id, undefined); + } + return true; + } + return false; + } + + private applyAndSetProductIconTheme(iconThemeData: ProductIconThemeData): void { + + this.currentProductIconTheme = iconThemeData; + + _applyRules(iconThemeData.styleSheetContent!, productIconThemeRulesClassName); + + this.productIconThemeWatcher.update(iconThemeData); + + if (iconThemeData.id) { + this.sendTelemetry(iconThemeData.id, iconThemeData.extensionData, 'productIcon'); + } + this.onProductIconThemeChange.fire(this.currentProductIconTheme); + } private getBaseThemeFromContainer() { @@ -652,15 +627,43 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { } } -function _applyIconTheme(data: FileIconThemeData, onApply: (theme: FileIconThemeData) => Promise): Promise { - _applyRules(data.styleSheetContent!, iconThemeRulesClassName); - return onApply(data); +class ThemeFileWatcher { + + private inExtensionDevelopment: boolean; + private watchedLocation: URI | undefined; + private watcherDisposable: IDisposable | undefined; + private fileChangeListener: IDisposable | undefined; + + constructor(private fileService: IFileService, environmentService: IWorkbenchEnvironmentService, private onUpdate: () => void) { + this.inExtensionDevelopment = !!environmentService.extensionDevelopmentLocationURI; + } + + update(theme: { location?: URI, watch?: boolean; }) { + if (!resources.isEqual(theme.location, this.watchedLocation)) { + this.dispose(); + if (theme.location && (theme.watch || this.inExtensionDevelopment)) { + this.watchedLocation = theme.location; + this.watcherDisposable = this.fileService.watch(theme.location); + this.fileService.onDidFilesChange(e => { + if (this.watchedLocation && e.contains(this.watchedLocation, FileChangeType.UPDATED)) { + this.onUpdate(); + } + }); + } + } + } + + dispose() { + this.watcherDisposable = dispose(this.watcherDisposable); + this.fileChangeListener = dispose(this.fileChangeListener); + this.watchedLocation = undefined; + } } function _applyRules(styleSheetContent: string, rulesClassName: string) { - let themeStyles = document.head.getElementsByClassName(rulesClassName); + const themeStyles = document.head.getElementsByClassName(rulesClassName); if (themeStyles.length === 0) { - let elStyle = document.createElement('style'); + const elStyle = document.createElement('style'); elStyle.type = 'text/css'; elStyle.className = rulesClassName; elStyle.innerHTML = styleSheetContent; @@ -672,6 +675,6 @@ function _applyRules(styleSheetContent: string, rulesClassName: string) { registerColorThemeSchemas(); registerFileIconThemeSchemas(); - +registerProductIconThemeSchemas(); registerSingleton(IWorkbenchThemeService, WorkbenchThemeService); diff --git a/src/vs/workbench/services/themes/common/colorThemeData.ts b/src/vs/workbench/services/themes/common/colorThemeData.ts index 18bddfe27ba..ca5fb5e9e3d 100644 --- a/src/vs/workbench/services/themes/common/colorThemeData.ts +++ b/src/vs/workbench/services/themes/common/colorThemeData.ts @@ -23,6 +23,8 @@ import { TokenStyle, ProbeScope, TokenStylingRule, getTokenClassificationRegistr 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'; +import { StorageScope, IStorageService } from 'vs/platform/storage/common/storage'; +import { ThemeConfiguration } from 'vs/workbench/services/themes/common/themeConfiguration'; let colorRegistry = Registry.as(ColorRegistryExtensions.ColorContribution); @@ -42,6 +44,8 @@ const tokenGroupToScopesMap = { export type TokenStyleDefinition = TokenStylingRule | ProbeScope[] | TokenStyleValue; export type TokenStyleDefinitions = { [P in keyof TokenStyleData]?: TokenStyleDefinition | undefined }; +const PERSISTED_THEME_STORAGE_KEY = 'colorThemeData'; + export class ColorThemeData implements IWorkbenchColorTheme { id: string; @@ -309,6 +313,12 @@ export class ColorThemeData implements IWorkbenchColorTheme { return this.customColorMap.hasOwnProperty(colorId) || this.colorMap.hasOwnProperty(colorId); } + public setCustomizations(settings: ThemeConfiguration) { + this.setCustomColors(settings.colorCustomizations); + this.setCustomTokenColors(settings.tokenColorCustomizations); + this.setCustomTokenStyleRules(settings.tokenStylesCustomizations); + } + public setCustomColors(colors: IColorCustomizations) { this.customColorMap = {}; this.overwriteCustomColors(colors); @@ -422,13 +432,13 @@ export class ColorThemeData implements IWorkbenchColorTheme { this.customTokenScopeMatchers = undefined; } - toStorageData() { + toStorage(storageService: IStorageService) { let colorMapData: { [key: string]: string } = {}; for (let key in this.colorMap) { colorMapData[key] = Color.Format.CSS.formatHexA(this.colorMap[key], true); } // no need to persist custom colors, they will be taken from the settings - return JSON.stringify({ + const value = JSON.stringify({ id: this.id, label: this.label, settingsId: this.settingsId, @@ -438,6 +448,7 @@ export class ColorThemeData implements IWorkbenchColorTheme { colorMap: colorMapData, watch: this.watch }); + storageService.store(PERSISTED_THEME_STORAGE_KEY, value, StorageScope.GLOBAL); } hasEqualData(other: ColorThemeData) { @@ -474,7 +485,11 @@ export class ColorThemeData implements IWorkbenchColorTheme { return themeData; } - static fromStorageData(input: string): ColorThemeData | undefined { + static fromStorageData(storageService: IStorageService): ColorThemeData | undefined { + const input = storageService.get(PERSISTED_THEME_STORAGE_KEY, StorageScope.GLOBAL); + if (!input) { + return undefined; + } try { let data = JSON.parse(input); let theme = new ColorThemeData('', '', ''); diff --git a/src/vs/workbench/services/themes/common/productIconThemeSchema.ts b/src/vs/workbench/services/themes/common/productIconThemeSchema.ts new file mode 100644 index 00000000000..69d1b32d8f5 --- /dev/null +++ b/src/vs/workbench/services/themes/common/productIconThemeSchema.ts @@ -0,0 +1,81 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as nls from 'vs/nls'; + +import { Registry } from 'vs/platform/registry/common/platform'; +import { Extensions as JSONExtensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; +import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { getIconRegistry } from 'vs/platform/theme/common/iconRegistry'; + + +const schemaId = 'vscode://schemas/product-icon-theme'; +const schema: IJSONSchema = { + type: 'object', + allowComments: true, + allowTrailingCommas: true, + properties: { + fonts: { + type: 'array', + description: nls.localize('schema.fonts', 'Fonts that are used in the icon definitions.'), + items: { + type: 'object', + properties: { + id: { + type: 'string', + description: nls.localize('schema.id', 'The ID of the font.') + }, + src: { + type: 'array', + description: nls.localize('schema.src', 'The location of the font.'), + items: { + type: 'object', + properties: { + path: { + type: 'string', + description: nls.localize('schema.font-path', 'The font path, relative to the current workbench icon theme file.'), + }, + format: { + type: 'string', + description: nls.localize('schema.font-format', 'The format of the font.') + } + }, + required: [ + 'path', + 'format' + ] + } + }, + weight: { + type: 'string', + description: nls.localize('schema.font-weight', 'The weight of the font.') + }, + style: { + type: 'string', + description: nls.localize('schema.font-sstyle', 'The style of the font.') + }, + size: { + type: 'string', + description: nls.localize('schema.font-size', 'The default size of the font.') + } + }, + required: [ + 'id', + 'src' + ] + } + }, + iconDefinitions: { + type: 'object', + description: nls.localize('schema.iconDefinitions', 'Assocation of icon name to a font character.'), + properties: getIconRegistry().getIconSchema().properties, + additionalProperties: false + } + } +}; + +export function registerProductIconThemeSchemas() { + let schemaRegistry = Registry.as(JSONExtensions.JSONContribution); + schemaRegistry.registerSchema(schemaId, schema); +} diff --git a/src/vs/workbench/services/themes/common/themeConfiguration.ts b/src/vs/workbench/services/themes/common/themeConfiguration.ts index 0ae3f23ad5e..b45ebe90434 100644 --- a/src/vs/workbench/services/themes/common/themeConfiguration.ts +++ b/src/vs/workbench/services/themes/common/themeConfiguration.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; +import * as types from 'vs/base/common/types'; import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, IConfigurationPropertySchema, IConfigurationNode } from 'vs/platform/configuration/common/configurationRegistry'; @@ -11,14 +12,17 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { textmateColorsSchemaId, textmateColorGroupSchemaId } from 'vs/workbench/services/themes/common/colorThemeSchema'; import { workbenchColorsSchemaId } from 'vs/platform/theme/common/colorRegistry'; import { tokenStylingSchemaId } from 'vs/platform/theme/common/tokenClassificationRegistry'; -import { ThemeSettings, IWorkbenchColorTheme, IWorkbenchFileIconTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import { ThemeSettings, IWorkbenchColorTheme, IWorkbenchFileIconTheme, IColorCustomizations, ITokenColorCustomizations, IExperimentalTokenStyleCustomizations, IWorkbenchProductIconTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; const DEFAULT_THEME_SETTING_VALUE = 'Default Dark+'; const DEFAULT_THEME_DARK_SETTING_VALUE = 'Default Dark+'; const DEFAULT_THEME_LIGHT_SETTING_VALUE = 'Default Light+'; const DEFAULT_THEME_HC_SETTING_VALUE = 'Default High Contrast'; -const DEFAULT_ICON_THEME_SETTING_VALUE = 'vs-seti'; +const DEFAULT_FILE_ICON_THEME_SETTING_VALUE = 'vs-seti'; + +export const DEFAULT_PRODUCT_ICON_THEME_SETTING_VALUE = 'Default'; // Configuration: Themes const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -64,14 +68,6 @@ const detectColorSchemeSettingSchema: IConfigurationPropertySchema = { default: false }; -const iconThemeSettingSchema: IConfigurationPropertySchema = { - type: ['string', 'null'], - default: DEFAULT_ICON_THEME_SETTING_VALUE, - description: nls.localize('iconTheme', "Specifies the icon theme used in the workbench or 'null' to not show any file icons."), - enum: [null], - enumDescriptions: [nls.localize('noIconThemeDesc', 'No file icons')], - errorMessage: nls.localize('iconThemeError', "File icon theme is unknown or not installed.") -}; const colorCustomizationsSchema: IConfigurationPropertySchema = { type: 'object', description: nls.localize('workbenchColors', "Overrides colors from the currently selected color theme."), @@ -83,6 +79,23 @@ const colorCustomizationsSchema: IConfigurationPropertySchema = { }] }; +const fileIconThemeSettingSchema: IConfigurationPropertySchema = { + type: ['string', 'null'], + default: DEFAULT_FILE_ICON_THEME_SETTING_VALUE, + description: nls.localize('iconTheme', "Specifies the icon theme used in the workbench or 'null' to not show any file icons."), + enum: [null], + enumDescriptions: [nls.localize('noIconThemeDesc', 'No file icons')], + errorMessage: nls.localize('iconThemeError', "File icon theme is unknown or not installed.") +}; +const productIconThemeSettingSchema: IConfigurationPropertySchema = { + type: ['string', 'null'], + default: DEFAULT_PRODUCT_ICON_THEME_SETTING_VALUE, + description: nls.localize('workbenchIconTheme', "Specifies the workbench icon theme used."), + enum: [DEFAULT_PRODUCT_ICON_THEME_SETTING_VALUE], + enumDescriptions: [nls.localize('defaultWorkbenchIconThemeDesc', 'Default')], + errorMessage: nls.localize('workbenchIconThemeError', "Workbench icon theme is unknown or not installed.") +}; + const themeSettingsConfiguration: IConfigurationNode = { id: 'workbench', order: 7.1, @@ -93,8 +106,9 @@ const themeSettingsConfiguration: IConfigurationNode = { [ThemeSettings.PREFERRED_LIGHT_THEME]: preferredLightThemeSettingSchema, [ThemeSettings.PREFERRED_HC_THEME]: preferredHCThemeSettingSchema, [ThemeSettings.DETECT_COLOR_SCHEME]: detectColorSchemeSettingSchema, - [ThemeSettings.ICON_THEME]: iconThemeSettingSchema, - [ThemeSettings.COLOR_CUSTOMIZATIONS]: colorCustomizationsSchema + [ThemeSettings.ICON_THEME]: fileIconThemeSettingSchema, + [ThemeSettings.COLOR_CUSTOMIZATIONS]: colorCustomizationsSchema, + [ThemeSettings.PRODUCT_ICON_THEME]: productIconThemeSettingSchema } }; configurationRegistry.registerConfiguration(themeSettingsConfiguration); @@ -170,8 +184,94 @@ export function updateColorThemeConfigurationSchemas(themes: IWorkbenchColorThem } export function updateFileIconThemeConfigurationSchemas(themes: IWorkbenchFileIconTheme[]) { - iconThemeSettingSchema.enum = [null, ...themes.map(t => t.settingsId)]; - iconThemeSettingSchema.enumDescriptions = [iconThemeSettingSchema.enumDescriptions![0], ...themes.map(t => t.description || '')]; + fileIconThemeSettingSchema.enum!.splice(1, Number.MAX_VALUE, ...themes.map(t => t.settingsId)); + fileIconThemeSettingSchema.enumDescriptions!.splice(1, Number.MAX_VALUE, ...themes.map(t => t.description || '')); configurationRegistry.notifyConfigurationSchemaUpdated(themeSettingsConfiguration); } + +export function updateProductIconThemeConfigurationSchemas(themes: IWorkbenchProductIconTheme[]) { + productIconThemeSettingSchema.enum!.splice(1, Number.MAX_VALUE, ...themes.map(t => t.settingsId)); + productIconThemeSettingSchema.enumDescriptions!.splice(1, Number.MAX_VALUE, ...themes.map(t => t.description || '')); + + configurationRegistry.notifyConfigurationSchemaUpdated(themeSettingsConfiguration); +} + + +export class ThemeConfiguration { + constructor(private configurationService: IConfigurationService) { + } + + public get colorTheme(): string { + return this.configurationService.getValue(ThemeSettings.COLOR_THEME); + } + + public get fileIconTheme(): string | null { + return this.configurationService.getValue(ThemeSettings.ICON_THEME); + } + + public get productIconTheme(): string { + return this.configurationService.getValue(ThemeSettings.PRODUCT_ICON_THEME); + } + + public get colorCustomizations(): IColorCustomizations { + return this.configurationService.getValue(ThemeSettings.COLOR_CUSTOMIZATIONS) || {}; + } + + public get tokenColorCustomizations(): ITokenColorCustomizations { + return this.configurationService.getValue(ThemeSettings.TOKEN_COLOR_CUSTOMIZATIONS) || {}; + } + + public get tokenStylesCustomizations(): IExperimentalTokenStyleCustomizations { + return this.configurationService.getValue(ThemeSettings.TOKEN_COLOR_CUSTOMIZATIONS_EXPERIMENTAL) || {}; + } + + public async setColorTheme(theme: IWorkbenchColorTheme, settingsTarget: ConfigurationTarget | undefined | 'auto'): Promise { + await this.writeConfiguration(ThemeSettings.COLOR_THEME, theme.settingsId, settingsTarget); + return theme; + } + + public async setFileIconTheme(theme: IWorkbenchFileIconTheme, settingsTarget: ConfigurationTarget | undefined | 'auto'): Promise { + await this.writeConfiguration(ThemeSettings.ICON_THEME, theme.settingsId, settingsTarget); + return theme; + } + + public async setProductIconTheme(theme: IWorkbenchProductIconTheme, settingsTarget: ConfigurationTarget | undefined | 'auto'): Promise { + await this.writeConfiguration(ThemeSettings.PRODUCT_ICON_THEME, theme.settingsId, settingsTarget); + return theme; + } + + private async writeConfiguration(key: string, value: any, settingsTarget: ConfigurationTarget | 'auto' | undefined): Promise { + if (settingsTarget === undefined) { + return; + } + + let settings = this.configurationService.inspect(key); + if (settingsTarget === 'auto') { + if (!types.isUndefined(settings.workspaceFolderValue)) { + settingsTarget = ConfigurationTarget.WORKSPACE_FOLDER; + } else if (!types.isUndefined(settings.workspaceValue)) { + settingsTarget = ConfigurationTarget.WORKSPACE; + } else { + settingsTarget = ConfigurationTarget.USER; + } + } + + if (settingsTarget === ConfigurationTarget.USER) { + if (value === settings.userValue) { + return Promise.resolve(undefined); // nothing to do + } else if (value === settings.defaultValue) { + if (types.isUndefined(settings.userValue)) { + return Promise.resolve(undefined); // nothing to do + } + value = undefined; // remove configuration from user settings + } + } else if (settingsTarget === ConfigurationTarget.WORKSPACE || settingsTarget === ConfigurationTarget.WORKSPACE_FOLDER) { + if (value === settings.value) { + return Promise.resolve(undefined); // nothing to do + } + } + return this.configurationService.updateValue(key, value, settingsTarget); + } + +} diff --git a/src/vs/workbench/services/themes/common/themeExtensionPoints.ts b/src/vs/workbench/services/themes/common/themeExtensionPoints.ts index 18b0f092045..2d01c6b95ea 100644 --- a/src/vs/workbench/services/themes/common/themeExtensionPoints.ts +++ b/src/vs/workbench/services/themes/common/themeExtensionPoints.ts @@ -65,7 +65,36 @@ export function registerFileIconThemeExtensionPoint() { type: 'string' }, path: { - description: nls.localize('vscode.extension.contributes.iconThemes.path', 'Path of the file icon theme definition file. The path is relative to the extension folder and is typically \'./iconthemes/awesome-icon-theme.json\'.'), + description: nls.localize('vscode.extension.contributes.iconThemes.path', 'Path of the file icon theme definition file. The path is relative to the extension folder and is typically \'./fileicons/awesome-icon-theme.json\'.'), + type: 'string' + } + }, + required: ['path', 'id'] + } + } + }); +} + +export function registerProductIconThemeExtensionPoint() { + return ExtensionsRegistry.registerExtensionPoint({ + extensionPoint: 'productIconThemes', + jsonSchema: { + description: nls.localize('vscode.extension.contributes.productIconThemes', 'Contributes product icon themes.'), + type: 'array', + items: { + type: 'object', + defaultSnippets: [{ body: { id: '${1:id}', label: '${2:label}', path: './producticons/${3:id}-product-icon-theme.json' } }], + properties: { + id: { + description: nls.localize('vscode.extension.contributes.productIconThemes.id', 'Id of the product icon theme as used in the user settings.'), + type: 'string' + }, + label: { + description: nls.localize('vscode.extension.contributes.productIconThemes.label', 'Label of the product icon theme as shown in the UI.'), + type: 'string' + }, + path: { + description: nls.localize('vscode.extension.contributes.productIconThemes.path', 'Path of the product icon theme definition file. The path is relative to the extension folder and is typically \'./producticons/awesome-product-icon-theme.json\'.'), type: 'string' } }, @@ -97,7 +126,8 @@ export class ThemeRegistry { @IExtensionService private readonly extensionService: IExtensionService, private readonly themesExtPoint: IExtensionPoint, private create: (theme: IThemeExtensionPoint, themeLocation: URI, extensionData: ExtensionData) => T, - private idRequired = false + private idRequired = false, + private builtInTheme: T | undefined = undefined ) { this.extensionThemes = []; this.initialize(); @@ -169,34 +199,38 @@ export class ThemeRegistry { }); } - public findThemeById(themeId: string, defaultId?: string): Promise { - return this.getThemes().then(allThemes => { - let defaultTheme: T | undefined = undefined; - for (let t of allThemes) { - if (t.id === themeId) { - return t; - } - if (t.id === defaultId) { - defaultTheme = t; - } + public async findThemeById(themeId: string, defaultId?: string): Promise { + if (this.builtInTheme && this.builtInTheme.id === themeId) { + return this.builtInTheme; + } + const allThemes = await this.getThemes(); + let defaultTheme: T | undefined = undefined; + for (let t of allThemes) { + if (t.id === themeId) { + return t; } - return defaultTheme; - }); + if (t.id === defaultId) { + defaultTheme = t; + } + } + return defaultTheme; } - public findThemeBySettingsId(settingsId: string | null, defaultId?: string): Promise { - return this.getThemes().then(allThemes => { - let defaultTheme: T | undefined = undefined; - for (let t of allThemes) { - if (t.settingsId === settingsId) { - return t; - } - if (t.id === defaultId) { - defaultTheme = t; - } + public async findThemeBySettingsId(settingsId: string | null, defaultId?: string): Promise { + if (this.builtInTheme && this.builtInTheme.settingsId === settingsId) { + return this.builtInTheme; + } + const allThemes = await this.getThemes(); + let defaultTheme: T | undefined = undefined; + for (let t of allThemes) { + if (t.settingsId === settingsId) { + return t; } - return defaultTheme; - }); + if (t.id === defaultId) { + defaultTheme = t; + } + } + return defaultTheme; } public findThemeByExtensionLocation(extLocation: URI | undefined): Promise { diff --git a/src/vs/workbench/services/themes/common/workbenchThemeService.ts b/src/vs/workbench/services/themes/common/workbenchThemeService.ts index 1d4d91837fc..3e897cd371b 100644 --- a/src/vs/workbench/services/themes/common/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/common/workbenchThemeService.ts @@ -29,7 +29,9 @@ export enum ThemeSettings { PREFERRED_LIGHT_THEME = 'workbench.preferredLightColorTheme', PREFERRED_HC_THEME = 'workbench.preferredHighContrastColorTheme', DETECT_COLOR_SCHEME = 'window.autoDetectColorScheme', - DETECT_HC = 'window.autoDetectHighContrast' + DETECT_HC = 'window.autoDetectHighContrast', + + PRODUCT_ICON_THEME = 'workbench.productIconTheme' } export interface IWorkbenchColorTheme extends IColorTheme { @@ -59,6 +61,16 @@ export interface IWorkbenchFileIconTheme extends IFileIconTheme { readonly hidesExplorerArrows: boolean; } +export interface IWorkbenchProductIconTheme { + readonly id: string; + readonly label: string; + readonly settingsId: string; + readonly description?: string; + readonly extensionData?: ExtensionData; + + readonly isLoaded: boolean; +} + export interface IWorkbenchThemeService extends IThemeService { _serviceBrand: undefined; @@ -72,6 +84,12 @@ export interface IWorkbenchThemeService extends IThemeService { getFileIconTheme(): IWorkbenchFileIconTheme; getFileIconThemes(): Promise; onDidFileIconThemeChange: Event; + + setProductIconTheme(iconThemeId: string | undefined, settingsTarget: ConfigurationTarget | undefined): Promise; + getProductIconTheme(): IWorkbenchProductIconTheme; + getProductIconThemes(): Promise; + onDidProductIconThemeChange: Event; + } export interface IColorCustomizations { diff --git a/src/vs/workbench/services/userDataSync/electron-browser/settingsSyncService.ts b/src/vs/workbench/services/userDataSync/electron-browser/settingsSyncService.ts index c02286f7208..7d48e03e84c 100644 --- a/src/vs/workbench/services/userDataSync/electron-browser/settingsSyncService.ts +++ b/src/vs/workbench/services/userDataSync/electron-browser/settingsSyncService.ts @@ -84,8 +84,16 @@ export class SettingsSyncService extends Disposable implements ISettingsSyncServ return this.channel.call('resolveConflicts', [conflicts]); } - getRemoteContent(preview?: boolean): Promise { - return this.channel.call('getRemoteContent', [!!preview]); + getRemoteContent(ref?: string, fragment?: string): Promise { + return this.channel.call('getRemoteContent', [ref, fragment]); + } + + getLocalBackupContent(ref?: string, fragment?: string): Promise { + return this.channel.call('getLocalBackupContent', [ref, fragment]); + } + + getRemoteContentFromPreview(): Promise { + return this.channel.call('getRemoteContentFromPreview', []); } private async updateStatus(status: SyncStatus): Promise { diff --git a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncBackupStoreService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncBackupStoreService.ts new file mode 100644 index 00000000000..3a395b1c5b4 --- /dev/null +++ b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncBackupStoreService.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ResourceKey, IResourceRefHandle, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; +import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; +import { IChannel } from 'vs/base/parts/ipc/common/ipc'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; + +export class UserDataSyncBackupStoreService implements IUserDataSyncBackupStoreService { + + _serviceBrand: undefined; + private readonly channel: IChannel; + + constructor( + @ISharedProcessService sharedProcessService: ISharedProcessService, + ) { + this.channel = sharedProcessService.getChannel('userDataSyncBackupStoreService'); + } + + backup(key: ResourceKey, content: string): Promise { + return this.channel.call('backup', [key, content]); + } + + + getAllRefs(key: ResourceKey): Promise { + return this.channel.call('getAllRefs', [key]); + } + + resolveContent(key: ResourceKey, ref: string): Promise { + return this.channel.call('resolveContent', [key, ref]); + } + +} + +registerSingleton(IUserDataSyncBackupStoreService, UserDataSyncBackupStoreService); diff --git a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts index b3ec8ffeb95..686866cfc79 100644 --- a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts +++ b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncService.ts @@ -10,6 +10,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { URI } from 'vs/base/common/uri'; export class UserDataSyncService extends Disposable implements IUserDataSyncService { @@ -88,8 +89,8 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ return this.channel.call('stop'); } - getRemoteContent(source: SyncSource, preview: boolean): Promise { - return this.channel.call('getRemoteContent', [source, preview]); + resolveContent(resource: URI): Promise { + return this.channel.call('resolveContent', [resource]); } isFirstTimeSyncWithMerge(): Promise { diff --git a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncStoreService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncStoreService.ts index a79674273a5..dd3273ab5f0 100644 --- a/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncStoreService.ts +++ b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncStoreService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SyncSource, IUserDataSyncStoreService, IUserDataSyncStore, getUserDataSyncStore, ResourceKey, IUserData, IUserDataManifest } from 'vs/platform/userDataSync/common/userDataSync'; +import { SyncSource, IUserDataSyncStoreService, IUserDataSyncStore, getUserDataSyncStore, ResourceKey, IUserData, IUserDataManifest, IResourceRefHandle } from 'vs/platform/userDataSync/common/userDataSync'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -41,7 +41,7 @@ export class UserDataSyncStoreService implements IUserDataSyncStoreService { throw new Error('Not Supported'); } - getAllRefs(key: ResourceKey): Promise { + getAllRefs(key: ResourceKey): Promise { return this.channel.call('getAllRefs', [key]); } diff --git a/src/vs/workbench/test/browser/api/mainThreadDocumentsAndEditors.test.ts b/src/vs/workbench/test/browser/api/mainThreadDocumentsAndEditors.test.ts index 4101a887f69..09a1e6a56a9 100644 --- a/src/vs/workbench/test/browser/api/mainThreadDocumentsAndEditors.test.ts +++ b/src/vs/workbench/test/browser/api/mainThreadDocumentsAndEditors.test.ts @@ -13,7 +13,7 @@ import { ITextFileService } from 'vs/workbench/services/textfile/common/textfile import { ExtHostDocumentsAndEditorsShape, IDocumentsAndEditorsDelta } from 'vs/workbench/api/common/extHost.protocol'; import { createTestCodeEditor, TestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { mock } from 'vs/workbench/test/browser/api/mock'; -import { TestEditorService, TestEditorGroupsService, TestTextResourcePropertiesService, TestEnvironmentService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestEditorService, TestEditorGroupsService, TestEnvironmentService } from 'vs/workbench/test/browser/workbenchTestServices'; import { Event } from 'vs/base/common/event'; import { ITextModel } from 'vs/editor/common/model'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; @@ -25,6 +25,7 @@ import { NullLogService } from 'vs/platform/log/common/log'; import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; +import { TestTextResourcePropertiesService } from 'vs/workbench/test/common/workbenchTestServices'; suite('MainThreadDocumentsAndEditors', () => { diff --git a/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts b/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts index 0d96189e4c9..2dc9e4ca630 100644 --- a/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts +++ b/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts @@ -19,7 +19,7 @@ import { Range } from 'vs/editor/common/core/range'; import { Position } from 'vs/editor/common/core/position'; import { IModelService } from 'vs/editor/common/services/modelService'; import { EditOperation } from 'vs/editor/common/core/editOperation'; -import { TestFileService, TestEditorService, TestEditorGroupsService, TestEnvironmentService, TestContextService, TestTextResourcePropertiesService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestFileService, TestEditorService, TestEditorGroupsService, TestEnvironmentService } from 'vs/workbench/test/browser/workbenchTestServices'; import { BulkEditService } from 'vs/workbench/services/bulkEdit/browser/bulkEditService'; import { NullLogService, ILogService } from 'vs/platform/log/common/log'; import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; @@ -47,6 +47,7 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { TestTextResourcePropertiesService, TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; suite('MainThreadEditors', () => { diff --git a/src/vs/workbench/test/browser/part.test.ts b/src/vs/workbench/test/browser/part.test.ts index 63946bb4257..a2bd94ea0ec 100644 --- a/src/vs/workbench/test/browser/part.test.ts +++ b/src/vs/workbench/test/browser/part.test.ts @@ -8,8 +8,9 @@ import { Part } from 'vs/workbench/browser/part'; import * as Types from 'vs/base/common/types'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { append, $, hide } from 'vs/base/browser/dom'; -import { TestStorageService, TestLayoutService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestLayoutService } from 'vs/workbench/test/browser/workbenchTestServices'; import { StorageScope } from 'vs/platform/storage/common/storage'; +import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; class SimplePart extends Part { diff --git a/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts b/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts index d5d30e62aae..635c4c865fe 100644 --- a/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts @@ -11,7 +11,7 @@ import * as Platform from 'vs/platform/registry/common/platform'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { workbenchInstantiationService, TestEditorGroupView, TestEditorGroupsService, TestStorageService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { workbenchInstantiationService, TestEditorGroupView, TestEditorGroupsService } from 'vs/workbench/test/browser/workbenchTestServices'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { URI } from 'vs/base/common/uri'; @@ -19,6 +19,7 @@ import { IEditorRegistry, Extensions, EditorDescriptor } from 'vs/workbench/brow import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorModel } from 'vs/platform/editor/common/editor'; import { dispose } from 'vs/base/common/lifecycle'; +import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; const NullThemeService = new TestThemeService(); diff --git a/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts b/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts index d74c28ef6c4..067919112c6 100644 --- a/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/breadcrumbModel.test.ts @@ -7,9 +7,9 @@ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { Workspace, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { EditorBreadcrumbsModel, FileElement } from 'vs/workbench/browser/parts/editor/breadcrumbsModel'; -import { TestContextService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { FileKind } from 'vs/platform/files/common/files'; +import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; suite('Breadcrumb Model', function () { diff --git a/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts b/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts index 2eb866b7a4a..f8e3f5e90a5 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { EditorGroup, ISerializedEditorGroup, EditorCloseEvent } from 'vs/workbench/common/editor/editorGroup'; import { Extensions as EditorExtensions, IEditorInputFactoryRegistry, EditorInput, IFileEditorInput, IEditorInputFactory, CloseDirection, EditorsOrder } from 'vs/workbench/common/editor'; import { URI } from 'vs/base/common/uri'; -import { TestLifecycleService, TestContextService, TestStorageService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestLifecycleService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -21,6 +21,7 @@ import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtil import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { TestContextService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; function inst(): IInstantiationService { let inst = new TestInstantiationService(); diff --git a/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts b/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts index d0c2abb5eea..e74010b7c6e 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts @@ -17,13 +17,13 @@ import { ITextBufferFactory } from 'vs/editor/common/model'; import { URI } from 'vs/base/common/uri'; import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; -import { TestTextResourcePropertiesService } from 'vs/workbench/test/browser/workbenchTestServices'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { TestTextResourcePropertiesService } from 'vs/workbench/test/common/workbenchTestServices'; class MyEditorModel extends EditorModel { } class MyTextEditorModel extends BaseTextEditorModel { diff --git a/src/vs/workbench/test/browser/quickAccess.test.ts b/src/vs/workbench/test/browser/quickAccess.test.ts new file mode 100644 index 00000000000..bd9990491e4 --- /dev/null +++ b/src/vs/workbench/test/browser/quickAccess.test.ts @@ -0,0 +1,186 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IQuickAccessRegistry, Extensions, IQuickAccessProvider } from 'vs/platform/quickinput/common/quickAccess'; +import { IQuickPick, IQuickPickItem, IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { DisposableStore, toDisposable, IDisposable } from 'vs/base/common/lifecycle'; +import { timeout } from 'vs/base/common/async'; + +suite('QuickAccess', () => { + + let instantiationService: IInstantiationService; + let accessor: TestServiceAccessor; + + let provider1Called = false; + let provider1Canceled = false; + let provider1Disposed = false; + + let provider2Called = false; + let provider2Canceled = false; + let provider2Disposed = false; + + let provider3Called = false; + let provider3Canceled = false; + let provider3Disposed = false; + + let provider4Called = false; + let provider4Canceled = false; + let provider4Disposed = false; + + class TestProvider1 implements IQuickAccessProvider { + provide(picker: IQuickPick, token: CancellationToken): IDisposable { + assert.ok(picker); + provider1Called = true; + token.onCancellationRequested(() => provider1Canceled = true); + + return toDisposable(() => provider1Disposed = true); + } + } + + class TestProvider2 implements IQuickAccessProvider { + provide(picker: IQuickPick, token: CancellationToken): IDisposable { + assert.ok(picker); + provider2Called = true; + token.onCancellationRequested(() => provider2Canceled = true); + + return toDisposable(() => provider2Disposed = true); + } + } + + class TestProvider3 implements IQuickAccessProvider { + + constructor(@IQuickInputService private readonly quickInputService: IQuickInputService, disposables: DisposableStore) { } + + provide(picker: IQuickPick, token: CancellationToken): IDisposable { + assert.ok(picker); + provider3Called = true; + token.onCancellationRequested(() => provider3Canceled = true); + + // bring up provider #4 + setTimeout(() => this.quickInputService.quickAccess.show(providerDescriptor4.prefix)); + + return toDisposable(() => provider3Disposed = true); + } + } + + class TestProvider4 implements IQuickAccessProvider { + provide(picker: IQuickPick, token: CancellationToken): IDisposable { + assert.ok(picker); + provider4Called = true; + token.onCancellationRequested(() => provider4Canceled = true); + + // hide without picking + setTimeout(() => picker.hide()); + + return toDisposable(() => provider4Disposed = true); + } + } + + const defaultProviderDescriptor = { ctor: TestProvider1, prefix: '', helpEntries: [] }; + const providerDescriptor1 = { ctor: TestProvider1, prefix: 'test', helpEntries: [] }; + const providerDescriptor2 = { ctor: TestProvider2, prefix: 'test something', helpEntries: [] }; + const providerDescriptor3 = { ctor: TestProvider3, prefix: 'default', helpEntries: [] }; + const providerDescriptor4 = { ctor: TestProvider4, prefix: 'changed', helpEntries: [] }; + + setup(() => { + instantiationService = workbenchInstantiationService(); + accessor = instantiationService.createInstance(TestServiceAccessor); + }); + + test('registry', () => { + const registry = (Registry.as(Extensions.Quickaccess)); + registry.defaultProvider = defaultProviderDescriptor; + + const initialSize = registry.getQuickAccessProviders().length; + + const disposable = registry.registerQuickAccessProvider(providerDescriptor1); + + assert(registry.getQuickAccessProvider('test') === providerDescriptor1); + + const providers = registry.getQuickAccessProviders(); + assert(providers.some(provider => provider.prefix === 'test')); + + disposable.dispose(); + assert.ok(!registry.getQuickAccessProvider('test')); + assert.equal(registry.getQuickAccessProviders().length - initialSize, 0); + }); + + test('provider', async () => { + const registry = (Registry.as(Extensions.Quickaccess)); + const defaultProvider = registry.defaultProvider; + + const disposables = new DisposableStore(); + + disposables.add(registry.registerQuickAccessProvider(providerDescriptor1)); + disposables.add(registry.registerQuickAccessProvider(providerDescriptor2)); + disposables.add(registry.registerQuickAccessProvider(providerDescriptor4)); + registry.defaultProvider = providerDescriptor3; + + accessor.quickInputService.quickAccess.show('test'); + assert.equal(provider1Called, true); + assert.equal(provider2Called, false); + assert.equal(provider3Called, false); + assert.equal(provider4Called, false); + assert.equal(provider1Canceled, false); + assert.equal(provider2Canceled, false); + assert.equal(provider3Canceled, false); + assert.equal(provider4Canceled, false); + assert.equal(provider1Disposed, false); + assert.equal(provider2Disposed, false); + assert.equal(provider3Disposed, false); + assert.equal(provider4Disposed, false); + provider1Called = false; + + accessor.quickInputService.quickAccess.show('test something'); + assert.equal(provider1Called, false); + assert.equal(provider2Called, true); + assert.equal(provider3Called, false); + assert.equal(provider4Called, false); + assert.equal(provider1Canceled, true); + assert.equal(provider2Canceled, false); + assert.equal(provider3Canceled, false); + assert.equal(provider4Canceled, false); + assert.equal(provider1Disposed, true); + assert.equal(provider2Disposed, false); + assert.equal(provider3Disposed, false); + assert.equal(provider4Disposed, false); + provider2Called = false; + provider1Canceled = false; + provider1Disposed = false; + + accessor.quickInputService.quickAccess.show('usedefault'); + assert.equal(provider1Called, false); + assert.equal(provider2Called, false); + assert.equal(provider3Called, true); + assert.equal(provider4Called, false); + assert.equal(provider1Canceled, false); + assert.equal(provider2Canceled, true); + assert.equal(provider3Canceled, false); + assert.equal(provider4Canceled, false); + assert.equal(provider1Disposed, false); + assert.equal(provider2Disposed, true); + assert.equal(provider3Disposed, false); + assert.equal(provider4Disposed, false); + + await timeout(1); + + assert.equal(provider3Canceled, true); + assert.equal(provider3Disposed, true); + assert.equal(provider4Called, true); + + await timeout(1); + + assert.equal(provider4Canceled, true); + assert.equal(provider4Disposed, true); + + disposables.dispose(); + registry.defaultProvider = defaultProvider; + }); +}); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 7672be5ade0..2e064554f57 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -64,7 +64,7 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { ViewletDescriptor, Viewlet } from 'vs/workbench/browser/viewlet'; import { IViewlet } from 'vs/workbench/common/viewlet'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { isLinux } from 'vs/base/common/platform'; +import { isLinux, isWindows } from 'vs/base/common/platform'; import { LabelService } from 'vs/workbench/services/label/common/labelService'; import { Part } from 'vs/workbench/browser/part'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; @@ -81,7 +81,6 @@ import { IFilesConfigurationService, FilesConfigurationService } from 'vs/workbe import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; import { BrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; import { BrowserTextFileService } from 'vs/workbench/services/textfile/browser/browserTextFileService'; -import * as CommonWorkbenchTestServices from 'vs/workbench/test/common/workbenchTestServices'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel'; import { IRemotePathService } from 'vs/workbench/services/path/common/remotePathService'; @@ -98,13 +97,13 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; import { CodeEditorService } from 'vs/workbench/services/editor/browser/codeEditorService'; import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; - -export import TestTextResourcePropertiesService = CommonWorkbenchTestServices.TestTextResourcePropertiesService; -export import TestContextService = CommonWorkbenchTestServices.TestContextService; -export import TestStorageService = CommonWorkbenchTestServices.TestStorageService; -export import TestWorkingCopyService = CommonWorkbenchTestServices.TestWorkingCopyService; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IDiffEditor } from 'vs/editor/common/editorCommon'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { QuickInputService } from 'vs/workbench/services/quickinput/browser/quickInputService'; +import { IListService } from 'vs/platform/list/browser/listService'; +import { win32, posix } from 'vs/base/common/path'; +import { TestWorkingCopyService, TestContextService, TestStorageService, TestTextResourcePropertiesService } from 'vs/workbench/test/common/workbenchTestServices'; export function createFileEditorInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { return instantiationService.createInstance(FileEditorInput, resource, undefined, undefined); @@ -130,9 +129,12 @@ export function workbenchInstantiationService(overrides?: { textFileService?: (i instantiationService.stub(ITextResourceConfigurationService, new TestTextResourceConfigurationService(configService)); instantiationService.stub(IUntitledTextEditorService, instantiationService.createInstance(UntitledTextEditorService)); instantiationService.stub(IStorageService, new TestStorageService()); - instantiationService.stub(IWorkbenchLayoutService, new TestLayoutService()); + instantiationService.stub(IRemotePathService, new TestRemotePathService(TestEnvironmentService)); + const layoutService = new TestLayoutService(); + instantiationService.stub(IWorkbenchLayoutService, layoutService); instantiationService.stub(IDialogService, new TestDialogService()); - instantiationService.stub(IAccessibilityService, new TestAccessibilityService()); + const accessibilityService = new TestAccessibilityService(); + instantiationService.stub(IAccessibilityService, accessibilityService); instantiationService.stub(IFileDialogService, new TestFileDialogService()); instantiationService.stub(IModeService, instantiationService.createInstance(ModeServiceImpl)); instantiationService.stub(IHistoryService, new TestHistoryService()); @@ -145,7 +147,8 @@ export function workbenchInstantiationService(overrides?: { textFileService?: (i instantiationService.stub(INotificationService, new TestNotificationService()); instantiationService.stub(IUntitledTextEditorService, instantiationService.createInstance(UntitledTextEditorService)); instantiationService.stub(IMenuService, new TestMenuService()); - instantiationService.stub(IKeybindingService, new MockKeybindingService()); + const keybindingService = new MockKeybindingService(); + instantiationService.stub(IKeybindingService, keybindingService); instantiationService.stub(IDecorationsService, new TestDecorationsService()); instantiationService.stub(IExtensionService, new TestExtensionService()); instantiationService.stub(IWorkingCopyFileService, instantiationService.createInstance(WorkingCopyFileService)); @@ -162,6 +165,8 @@ export function workbenchInstantiationService(overrides?: { textFileService?: (i instantiationService.stub(IEditorService, editorService); instantiationService.stub(ICodeEditorService, new CodeEditorService(editorService, themeService)); instantiationService.stub(IViewletService, new TestViewletService()); + instantiationService.stub(IListService, new TestListService()); + instantiationService.stub(IQuickInputService, new QuickInputService(TestEnvironmentService, configService, instantiationService, keybindingService, contextKeyService, themeService, accessibilityService, layoutService)); return instantiationService; } @@ -184,7 +189,8 @@ export class TestServiceAccessor { @IUntitledTextEditorService public untitledTextEditorService: UntitledTextEditorService, @IConfigurationService public testConfigurationService: TestConfigurationService, @IBackupFileService public backupFileService: TestBackupFileService, - @IHostService public hostService: TestHostService + @IHostService public hostService: TestHostService, + @IQuickInputService public quickInputService: IQuickInputService ) { } } @@ -1065,3 +1071,28 @@ export class TestEditorPart extends EditorPart { } } } + +export class TestListService implements IListService { + _serviceBrand: undefined; + + lastFocusedList: any | undefined = undefined; + + register(): IDisposable { + return Disposable.None; + } +} + +export class TestRemotePathService implements IRemotePathService { + + _serviceBrand: undefined; + + constructor(@IWorkbenchEnvironmentService private readonly environmentService: IEnvironmentService) { } + + get path() { return Promise.resolve(isWindows ? win32 : posix); } + + get userHome() { return Promise.resolve(URI.file(this.environmentService.userHome)); } + + async fileURI(path: string): Promise { + return URI.file(path); + } +} diff --git a/src/vs/workbench/test/electron-browser/quickopen.perf.integrationTest.ts b/src/vs/workbench/test/electron-browser/quickopen.perf.integrationTest.ts index 4af6351e22c..da6aef2e37e 100644 --- a/src/vs/workbench/test/electron-browser/quickopen.perf.integrationTest.ts +++ b/src/vs/workbench/test/electron-browser/quickopen.perf.integrationTest.ts @@ -28,7 +28,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { LocalSearchService } from 'vs/workbench/services/search/node/searchService'; import { IUntitledTextEditorService, UntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; -import { TestContextService, TestEditorGroupsService, TestEditorService, TestTextResourcePropertiesService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestEditorGroupsService, TestEditorService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestEnvironmentService } from 'vs/workbench/test/electron-browser/workbenchTestServices'; import { ClassifiedEvent, StrictPropertyCheck, GDPRClassification } from 'vs/platform/telemetry/common/gdprTypings'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; @@ -39,6 +39,7 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { TestContextService, TestTextResourcePropertiesService } from 'vs/workbench/test/common/workbenchTestServices'; namespace Timer { export interface ITimerEvent { diff --git a/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts b/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts index be6ef9e5492..91558016763 100644 --- a/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts +++ b/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts @@ -17,7 +17,7 @@ import * as minimist from 'vscode-minimist'; import * as path from 'vs/base/common/path'; import { LocalSearchService } from 'vs/workbench/services/search/node/searchService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { TestContextService, TestEditorService, TestEditorGroupsService, TestTextResourcePropertiesService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestEditorService, TestEditorGroupsService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestEnvironmentService } from 'vs/workbench/test/electron-browser/workbenchTestServices'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { URI } from 'vs/base/common/uri'; @@ -42,6 +42,7 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { TestTextResourcePropertiesService, TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; // declare var __dirname: string; diff --git a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts index 233a0930623..8ee0f138984 100644 --- a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { workbenchInstantiationService as browserWorkbenchInstantiationService, ITestInstantiationService, TestLifecycleService, TestFilesConfigurationService, TestContextService, TestFileService, TestFileDialogService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { workbenchInstantiationService as browserWorkbenchInstantiationService, ITestInstantiationService, TestLifecycleService, TestFilesConfigurationService, TestFileService, TestFileDialogService } from 'vs/workbench/test/browser/workbenchTestServices'; import { Event } from 'vs/base/common/event'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { NativeWorkbenchEnvironmentService, INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; @@ -38,6 +38,7 @@ import { NodeTestBackupFileService } from 'vs/workbench/services/backup/test/ele import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { INativeWindowConfiguration } from 'vs/platform/windows/node/window'; +import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; export const TestWindowConfiguration: INativeWindowConfiguration = { windowId: 0, diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 0aac4046cb5..ec284cc0e58 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -84,6 +84,7 @@ import 'vs/workbench/services/workingCopy/common/workingCopyService'; import 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import 'vs/workbench/services/views/browser/viewDescriptorService'; +import 'vs/workbench/services/quickinput/browser/quickInputService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; @@ -112,8 +113,6 @@ import { OpenerService } from 'vs/editor/browser/services/openerService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSyncEnablementService'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; -import { QuickInputService } from 'vs/platform/quickinput/browser/quickInput'; registerSingleton(IUserDataSyncEnablementService, UserDataSyncEnablementService); registerSingleton(IGlobalExtensionEnablementService, GlobalExtensionEnablementService); @@ -129,7 +128,6 @@ registerSingleton(ITextResourceConfigurationService, TextResourceConfigurationSe registerSingleton(IMenuService, MenuService, true); registerSingleton(IDownloadService, DownloadService, true); registerSingleton(IOpenerService, OpenerService, true); -registerSingleton(IQuickInputService, QuickInputService, true); //#endregion @@ -150,6 +148,9 @@ import 'vs/workbench/contrib/logs/common/logs.contribution'; // Quick Open Handlers import 'vs/workbench/contrib/quickopen/browser/quickopen.contribution'; +// Quick Access Providers +import 'vs/workbench/contrib/quickaccess/browser/quickAccess.contribution'; + // Explorer import 'vs/workbench/contrib/files/browser/explorerViewlet'; import 'vs/workbench/contrib/files/browser/fileActions.contribution'; diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index 33ba47dadd6..a8f926566d4 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -53,6 +53,7 @@ import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncService' import 'vs/workbench/services/userDataSync/electron-browser/settingsSyncService'; import 'vs/workbench/services/userDataSync/electron-browser/userDataAutoSyncService'; import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncStoreService'; +import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncBackupStoreService'; import 'vs/workbench/services/authentication/electron-browser/authenticationTokenService'; import 'vs/workbench/services/authentication/browser/authenticationService'; import 'vs/workbench/services/host/electron-browser/desktopHostService'; diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index da7a13e906d..df598774a44 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -62,10 +62,11 @@ import { ITunnelService } from 'vs/platform/remote/common/tunnel'; import { TunnelService } from 'vs/workbench/services/remote/common/tunnelService'; import { ILoggerService } from 'vs/platform/log/common/log'; import { FileLoggerService } from 'vs/platform/log/common/fileLogService'; -import { IUserDataSyncStoreService, IUserDataSyncService, IUserDataSyncLogService, ISettingsSyncService, IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncStoreService, IUserDataSyncService, IUserDataSyncLogService, ISettingsSyncService, IUserDataAutoSyncService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; import { AuthenticationService, IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; import { UserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSyncLog'; import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; +import { UserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSyncBackupStoreService'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync'; import { IAuthenticationTokenService, AuthenticationTokenService } from 'vs/platform/authentication/common/authentication'; @@ -83,6 +84,7 @@ registerSingleton(ILoggerService, FileLoggerService); registerSingleton(IAuthenticationService, AuthenticationService); registerSingleton(IUserDataSyncLogService, UserDataSyncLogService); registerSingleton(IUserDataSyncStoreService, UserDataSyncStoreService); +registerSingleton(IUserDataSyncBackupStoreService, UserDataSyncBackupStoreService); registerSingleton(IAuthenticationTokenService, AuthenticationTokenService); registerSingleton(IUserDataAutoSyncService, UserDataAutoSyncService); registerSingleton(ISettingsSyncService, SettingsSynchroniser); diff --git a/yarn.lock b/yarn.lock index a3f8ad9006b..c2f11789a2a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9220,16 +9220,16 @@ typescript-formatter@7.1.0: commandpost "^1.0.0" editorconfig "^0.15.0" +typescript@3.9.0-dev.20200304: + version "3.9.0-dev.20200304" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.0-dev.20200304.tgz#3cc35357eff29dc5604b4fa56d6597e13daf86ed" + integrity sha512-eUip/GgJmjp4qtHiJDxVhE5SDDiPzBUg7KBAFUgb7HgL/tv10JAHej7fnS1i+7xrq1eDtbkJyPaYOVnhL9db7Q== + typescript@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.2.tgz#3c5b6fd7f6de0914269027f03c0946758f7673a4" integrity sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q= -typescript@^3.9.0-dev.20200229: - version "3.9.0-dev.20200229" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.0-dev.20200229.tgz#45f0821d5c420a4c7d6d894c64531e1301dfa9bd" - integrity sha512-DtSLzxoiUir0qRc3+JJBxiAe6NvTEM3uDxnPxVWJU6sRDhUi8Ssx6DBjGWCZAQJlLk5A+jk2ptf3JvvZrQlLNQ== - uc.micro@^1.0.1, uc.micro@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.3.tgz#7ed50d5e0f9a9fb0a573379259f2a77458d50192"