diff --git a/.eslintrc.json b/.eslintrc.json index 090e59c5d5..03d0f427bf 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -682,6 +682,20 @@ "**/{vs,sql}/workbench/services/**/{common,browser}/**" ] }, + { + "target": "**/{vs,sql}/workbench/contrib/notebook/common/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/**/{common,worker}/**", + "**/vs/platform/**/common/**", + "**/vs/editor/**", + "**/vs/workbench/common/**", + "**/vs/workbench/api/common/**", + "**/vs/workbench/services/**/common/**", + "**/vs/workbench/contrib/**/common/**" + ] + }, { "target": "**/{vs,sql}/workbench/contrib/**/common/**", "restrictions": [ diff --git a/.vscode/launch.json b/.vscode/launch.json index c5baadbecd..e6dc7eca9f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -19,7 +19,8 @@ "timeout": 30000, "port": 5870, "outFiles": [ - "${workspaceFolder}/out/**/*.js" + "${workspaceFolder}/out/**/*.js", + "${workspaceFolder}/extensions/*/out/**/*.js" ] }, { diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 5f6b23f91e..1d83e2301d 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -139,7 +139,7 @@ "label": "Kill Build Web Extensions", "group": "build", "presentation": { - "reveal": "never", + "reveal": "never" }, "problemMatcher": "$tsc" }, @@ -209,5 +209,19 @@ "reveal": "silent" } }, + { + "type": "npm", + "script": "tsec-compile-check", + "problemMatcher": [ + { + "base": "$tsc", + "applyTo": "allDocuments", + "owner": "tsec" + }, + ], + "group": "build", + "label": "npm: tsec-compile-check", + "detail": "node_modules/tsec/bin/tsec -p src/tsconfig.json --noEmit" + } ] } diff --git a/.yarnrc b/.yarnrc index 68cb12c128..3c6eccfb10 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,3 +1,3 @@ disturl "https://atom.io/download/electron" -target "9.2.0" +target "9.2.1" runtime "electron" diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index edda3bc7ab..d41730de73 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -42,6 +42,7 @@ const vscodeEntryPoints = _.flatten([ buildfile.entrypoint('vs/workbench/workbench.desktop.main'), buildfile.base, buildfile.workerExtensionHost, + buildfile.workerNotebook, buildfile.workbenchDesktop, buildfile.code ]); diff --git a/build/lib/asar.js b/build/lib/asar.js index 9ede8d48de..96420fea80 100644 --- a/build/lib/asar.js +++ b/build/lib/asar.js @@ -53,7 +53,9 @@ function createAsar(folderPath, unpackGlobs, destFilename) { const insertFile = (relativePath, stat, shouldUnpack) => { insertDirectoryForFile(relativePath); pendingInserts++; - filesystem.insertFile(relativePath, shouldUnpack, { stat: stat }, {}, onFileInserted); + // Do not pass `onFileInserted` directly because it gets overwritten below. + // Create a closure capturing `onFileInserted`. + filesystem.insertFile(relativePath, shouldUnpack, { stat: stat }, {}).then(() => onFileInserted(), () => onFileInserted()); }; return es.through(function (file) { if (file.stat.isDirectory()) { diff --git a/build/lib/asar.ts b/build/lib/asar.ts index 16dc232e57..bc85ecce2f 100644 --- a/build/lib/asar.ts +++ b/build/lib/asar.ts @@ -8,10 +8,17 @@ import * as path from 'path'; import * as es from 'event-stream'; const pickleĀ = require('chromium-pickle-js'); -const Filesystem = require('asar/lib/filesystem'); +const Filesystem = require('asar/lib/filesystem'); import * as VinylFile from 'vinyl'; import * as minimatch from 'minimatch'; +declare class AsarFilesystem { + readonly header: unknown; + constructor(src: string); + insertDirectory(path: string, shouldUnpack?: boolean): unknown; + insertFile(path: string, shouldUnpack: boolean, file: { stat: { size: number; mode: number; }; }, options: {}): Promise; +} + export function createAsar(folderPath: string, unpackGlobs: string[], destFilename: string): NodeJS.ReadWriteStream { const shouldUnpackFile = (file: VinylFile): boolean => { @@ -61,7 +68,9 @@ export function createAsar(folderPath: string, unpackGlobs: string[], destFilena const insertFile = (relativePath: string, stat: { size: number; mode: number; }, shouldUnpack: boolean) => { insertDirectoryForFile(relativePath); pendingInserts++; - filesystem.insertFile(relativePath, shouldUnpack, { stat: stat }, {}, onFileInserted); + // Do not pass `onFileInserted` directly because it gets overwritten below. + // Create a closure capturing `onFileInserted`. + filesystem.insertFile(relativePath, shouldUnpack, { stat: stat }, {}).then(() => onFileInserted(), () => onFileInserted()); }; return es.through(function (file) { diff --git a/build/package.json b/build/package.json index b8bb4e899e..57929be41e 100644 --- a/build/package.json +++ b/build/package.json @@ -50,7 +50,7 @@ "rollup-plugin-commonjs": "^10.1.0", "rollup-plugin-node-resolve": "^5.2.0", "terser": "4.3.8", - "typescript": "^4.0.1-rc", + "typescript": "^4.1.0-dev.20200824", "vsce": "1.48.0", "vscode-telemetry-extractor": "^1.6.0", "xml2js": "^0.4.17" diff --git a/build/yarn.lock b/build/yarn.lock index 20945bf969..7cc072a7eb 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -3544,10 +3544,10 @@ typescript@^3.0.1: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.3.tgz#c830f657f93f1ea846819e929092f5fe5983e977" integrity sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g== -typescript@^4.0.1-rc: - version "4.0.1-rc" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.1-rc.tgz#8adc78223eae56fe71d906a5fa90c3543b07a677" - integrity sha512-TCkspT3dSKOykbzS3/WSK7pqU2h1d/lEO6i45Afm5Y3XNAEAo8YXTG3kHOQk/wFq/5uPyO1+X8rb/Q+g7UsxJw== +typescript@^4.1.0-dev.20200824: + version "4.1.0-dev.20200824" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.0-dev.20200824.tgz#34c92d9b6e5124600658c0d4e9b8c125beaf577d" + integrity sha512-hTJfocmebnMKoqRw/xs3bL61z87XXtvOUwYtM7zaCX9mAvnfdo1x1bzQlLZAsvdzRIgAHPJQYbqYHKygWkDw6g== typical@^4.0.0: version "4.0.0" diff --git a/cgmanifest.json b/cgmanifest.json index 576724e75a..e6e8ce8a5c 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -60,12 +60,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "0c2cb59b6283fe8d6bb4b14f8a832e2166aeaa0c" + "commitHash": "03c7a54dc534ce1867d4393b9b1a6989d4a7e005" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "9.2.0" + "version": "9.2.1" }, { "component": { diff --git a/extensions/configuration-editing/package.json b/extensions/configuration-editing/package.json index 3cadb80fd3..477d530a0e 100644 --- a/extensions/configuration-editing/package.json +++ b/extensions/configuration-editing/package.json @@ -54,7 +54,7 @@ "url": "vscode://schemas/keybindings" }, { - "fileMatch": "vscode://defaultsettings/defaultSettings.json", + "fileMatch": "vscode://defaultsettings/*/*.json", "url": "vscode://schemas/settings/default" }, { diff --git a/extensions/docker/package.json b/extensions/docker/package.json index 3af7727a6b..a1cc782d21 100644 --- a/extensions/docker/package.json +++ b/extensions/docker/package.json @@ -15,7 +15,7 @@ "extensions": [ ".dockerfile", ".containerfile" ], "filenames": [ "Dockerfile", "Containerfile" ], "filenamePatterns": [ "Dockerfile.*", "Containerfile.*" ], - "aliases": [ "Dockerfile", "Containerfile" ], + "aliases": [ "Docker", "Dockerfile", "Containerfile" ], "configuration": "./language-configuration.json" }], "grammars": [{ diff --git a/extensions/git/package.json b/extensions/git/package.json index 748868778d..1e577006c1 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -754,13 +754,23 @@ "when": "scmProvider == git" }, { - "command": "git.checkout", - "group": "1_header", + "command": "git.pull", + "group": "1_header@1", + "when": "scmProvider == git" + }, + { + "command": "git.push", + "group": "1_header@2", "when": "scmProvider == git" }, { "command": "git.clone", - "group": "1_header", + "group": "1_header@3", + "when": "scmProvider == git" + }, + { + "command": "git.checkout", + "group": "1_header@4", "when": "scmProvider == git" }, { @@ -1915,7 +1925,11 @@ "[git-commit]": { "editor.rulers": [ 72 - ] + ], + "workbench.editor.restoreViewState": false + }, + "[git-rebase]": { + "workbench.editor.restoreViewState": false } }, "viewsWelcome": [ diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 21364140df..c55db8a2fd 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -107,7 +107,7 @@ "config.smartCommitChanges.all": "Automatically stage all changes.", "config.smartCommitChanges.tracked": "Automatically stage tracked changes only.", "config.suggestSmartCommit": "Suggests to enable smart commit (commit all changes when there are no staged changes).", - "config.enableCommitSigning": "Enables commit signing with GPG.", + "config.enableCommitSigning": "Enables commit signing with GPG or X.509.", "config.discardAllScope": "Controls what changes are discarded by the `Discard all changes` command. `all` discards all changes. `tracked` discards only tracked files. `prompt` shows a prompt dialog every time the action is run.", "config.decorations.enabled": "Controls whether Git contributes colors and badges to the explorer and the open editors view.", "config.enableStatusBarSync": "Controls whether the Git Sync command appears in the status bar.", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 8343bb9b38..b431f970a8 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -460,7 +460,7 @@ export class CommandCenter { @command('git.clone') async clone(url?: string, parentPath?: string): Promise { - if (!url) { + if (!url || typeof url !== 'string') { url = await pickRemoteSource(this.model, { providerLabel: provider => localize('clonefrom', "Clone from {0}", provider.name), urlLabel: localize('repourl', "Clone from URL") diff --git a/extensions/git/src/timelineProvider.ts b/extensions/git/src/timelineProvider.ts index 4c7b80e071..4c239f9ace 100644 --- a/extensions/git/src/timelineProvider.ts +++ b/extensions/git/src/timelineProvider.ts @@ -232,7 +232,7 @@ export class GitTimelineProvider implements TimelineProvider { private onRepositoryStatusChanged(_repo: Repository) { // console.log(`GitTimelineProvider.onRepositoryStatusChanged`); - // This is crappy, but for now just save the last time a status was run and use that as the timestamp for staged items + // This is less than ideal, but for now just save the last time a status was run and use that as the timestamp for staged items this.repoStatusDate = new Date(); this.fireChanged(); diff --git a/extensions/github-authentication/package.json b/extensions/github-authentication/package.json index 8136ebd04c..b74a91843a 100644 --- a/extensions/github-authentication/package.json +++ b/extensions/github-authentication/package.json @@ -11,15 +11,14 @@ "categories": [ "Other" ], - "activationEvents": [ - "*", - "onAuthenticationRequest:github" - ], "extensionKind": [ "ui", "workspace", "web" ], + "activationEvents": [ + "onAuthenticationRequest:github" + ], "contributes": { "commands": [ { @@ -34,7 +33,13 @@ "when": "false" } ] - } + }, + "authentication": [ + { + "label": "GitHub", + "id": "github" + } + ] }, "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", "main": "./out/extension.js", diff --git a/extensions/github-authentication/src/extension.ts b/extensions/github-authentication/src/extension.ts index 9d4fa10b10..335dbc411e 100644 --- a/extensions/github-authentication/src/extension.ts +++ b/extensions/github-authentication/src/extension.ts @@ -22,7 +22,7 @@ export async function activate(context: vscode.ExtensionContext) { return loginService.manuallyProvideToken(); })); - vscode.authentication.registerAuthenticationProvider({ + context.subscriptions.push(vscode.authentication.registerAuthenticationProvider({ id: 'github', label: 'GitHub', supportsMultipleAccounts: false, @@ -70,7 +70,7 @@ export async function activate(context: vscode.ExtensionContext) { throw e; } } - }); + })); return; } diff --git a/extensions/json-language-features/client/src/browser/jsonClientMain.ts b/extensions/json-language-features/client/src/browser/jsonClientMain.ts index c985f4dd22..4311138a68 100644 --- a/extensions/json-language-features/client/src/browser/jsonClientMain.ts +++ b/extensions/json-language-features/client/src/browser/jsonClientMain.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ExtensionContext } from 'vscode'; +import { ExtensionContext, Uri } from 'vscode'; import { LanguageClientOptions } from 'vscode-languageclient'; import { startClient, LanguageClientConstructor } from '../jsonClient'; import { LanguageClient } from 'vscode-languageclient/browser'; @@ -17,9 +17,9 @@ declare function fetch(uri: string, options: any): any; // this method is called when vs code is activated export function activate(context: ExtensionContext) { - const serverMain = context.asAbsolutePath('server/dist/browser/jsonServerMain.js'); + const serverMain = Uri.joinPath(context.extensionUri, 'server/dist/browser/jsonServerMain.js'); try { - const worker = new Worker(serverMain); + const worker = new Worker(serverMain.toString()); const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => { return new LanguageClient(id, name, clientOptions, worker); }; diff --git a/extensions/json-language-features/server/package.json b/extensions/json-language-features/server/package.json index e3e507c7b6..ddf75c333e 100644 --- a/extensions/json-language-features/server/package.json +++ b/extensions/json-language-features/server/package.json @@ -14,7 +14,7 @@ "dependencies": { "jsonc-parser": "^2.2.1", "request-light": "^0.3.0", - "vscode-json-languageservice": "^3.8.0", + "vscode-json-languageservice": "^3.8.3", "vscode-languageserver": "7.0.0-next.3", "vscode-uri": "^2.1.2" }, diff --git a/extensions/json-language-features/server/yarn.lock b/extensions/json-language-features/server/yarn.lock index 281f8276eb..ac7354a958 100644 --- a/extensions/json-language-features/server/yarn.lock +++ b/extensions/json-language-features/server/yarn.lock @@ -80,10 +80,10 @@ request-light@^0.3.0: https-proxy-agent "^2.2.4" vscode-nls "^4.1.1" -vscode-json-languageservice@^3.8.0: - version "3.8.0" - resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-3.8.0.tgz#c7e7283f993e3db39fa5501407b023ada6fd3ae3" - integrity sha512-sYz5JElJMIlPoqhrRfG3VKnDjnPinLdblIiEVsJgTz1kj2hWD2q5BSbo+evH/5/jKDXDLfA8kb0lHC4vd5g5zg== +vscode-json-languageservice@^3.8.3: + version "3.8.3" + resolved "https://registry.yarnpkg.com/vscode-json-languageservice/-/vscode-json-languageservice-3.8.3.tgz#fae5e7bdda2b6ec4f64588c571df40b6bfcb09b5" + integrity sha512-8yPag/NQHCuTthahyaTtzK0DHT0FKM/xBU0mFBQ8nMo8C1i2P+FCyIVqICoNoHkRI2BTGlXKomPUpsqjSz0TnQ== dependencies: jsonc-parser "^2.2.1" vscode-languageserver-textdocument "^1.0.1" diff --git a/extensions/microsoft-authentication/package.json b/extensions/microsoft-authentication/package.json index 81ed5c32e4..64becb5046 100644 --- a/extensions/microsoft-authentication/package.json +++ b/extensions/microsoft-authentication/package.json @@ -12,7 +12,6 @@ ], "enableProposedApi": true, "activationEvents": [ - "*", "onAuthenticationRequest:microsoft" ], "extensionKind": [ @@ -20,6 +19,14 @@ "workspace", "web" ], + "contributes": { + "authentication": [ + { + "label": "Microsoft", + "id": "microsoft" + } + ] + }, "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", "main": "./out/extension.js", "browser": "./dist/browser/extension.js", diff --git a/extensions/microsoft-authentication/src/AADHelper.ts b/extensions/microsoft-authentication/src/AADHelper.ts index 7e67df7dd4..a633ee18e0 100644 --- a/extensions/microsoft-authentication/src/AADHelper.ts +++ b/extensions/microsoft-authentication/src/AADHelper.ts @@ -5,6 +5,7 @@ import * as randomBytes from 'randombytes'; import * as querystring from 'querystring'; +import { Buffer } from 'buffer'; import * as vscode from 'vscode'; import { createServer, startServer } from './authServer'; @@ -528,7 +529,7 @@ export class AzureActiveDirectoryService { } } catch (e) { Logger.error('Refreshing token failed'); - throw e; + throw new Error(REFRESH_NETWORK_FAILURE); } } diff --git a/extensions/resource-deployment/src/services/resourceTypeService.ts b/extensions/resource-deployment/src/services/resourceTypeService.ts index 7c4ed9f374..699f9d2a3e 100644 --- a/extensions/resource-deployment/src/services/resourceTypeService.ts +++ b/extensions/resource-deployment/src/services/resourceTypeService.ts @@ -42,7 +42,7 @@ export class ResourceTypeService implements IResourceTypeService { vscode.extensions.all.forEach((extension) => { const extensionResourceTypes = extension.packageJSON.contributes && extension.packageJSON.contributes.resourceDeploymentTypes as ResourceType[]; if (extensionResourceTypes) { - extensionResourceTypes.forEach((resourceType) => { + extensionResourceTypes.forEach((resourceType: ResourceType) => { this.updatePathProperties(resourceType, extension.extensionPath); resourceType.getProvider = (selectedOptions) => { return this.getProvider(resourceType, selectedOptions); }; this._resourceTypes.push(resourceType); diff --git a/extensions/theme-abyss/themes/abyss-color-theme.json b/extensions/theme-abyss/themes/abyss-color-theme.json index 39f93305b8..7afe3bd963 100644 --- a/extensions/theme-abyss/themes/abyss-color-theme.json +++ b/extensions/theme-abyss/themes/abyss-color-theme.json @@ -233,6 +233,20 @@ "foreground": "#22aa44" } }, + { + "name": "Markup: Strong", + "scope": "markup.bold", + "settings": { + "fontStyle": "bold" + } + }, + { + "name": "Markup: Emphasis", + "scope": "markup.italic", + "settings": { + "fontStyle": "italic" + } + }, { "name": "Markup Inline", "scope": "markup.inline.raw", @@ -242,11 +256,14 @@ } }, { - "name": "Markup Setext Header", - "scope": "markup.heading.setext", + "name": "Markup Headings", + "scope": [ + "markup.heading", + "markup.heading.setext" + ], "settings": { - "fontStyle": "", - "foreground": "#ddbb88" + "fontStyle": "bold", + "foreground": "#6688cc" } } ], diff --git a/extensions/theme-defaults/themes/hc_black_defaults.json b/extensions/theme-defaults/themes/hc_black_defaults.json index 1a03010abf..495a15238d 100644 --- a/extensions/theme-defaults/themes/hc_black_defaults.json +++ b/extensions/theme-defaults/themes/hc_black_defaults.json @@ -136,6 +136,7 @@ { "scope": "markup.heading", "settings": { + "fontStyle": "bold", "foreground": "#6796e6" } }, diff --git a/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json b/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json index cdd2230711..38c8fe0996 100644 --- a/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json +++ b/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json @@ -260,7 +260,7 @@ "entity.name.section" ], "settings": { - "fontStyle": "", + "fontStyle": "bold", "foreground": "#8ab1b0" } }, diff --git a/extensions/theme-red/themes/Red-color-theme.json b/extensions/theme-red/themes/Red-color-theme.json index 8964f40a09..dbe8011320 100644 --- a/extensions/theme-red/themes/Red-color-theme.json +++ b/extensions/theme-red/themes/Red-color-theme.json @@ -350,6 +350,20 @@ "foreground": "#fb9a4bff" } }, + { + "name": "Markup: Strong", + "scope": "markup.bold", + "settings": { + "fontStyle": "bold" + } + }, + { + "name": "Markup: Emphasis", + "scope": "markup.italic", + "settings": { + "fontStyle": "italic" + } + }, { "name": "Markup Inline", "scope": "markup.inline.raw", @@ -359,17 +373,15 @@ } }, { - "name": "Markup Headings", - "scope": "markup.heading", + "name": "Headings", + "scope": [ + "markup.heading", + "markup.heading.setext", + "punctuation.definition.heading", + "entity.name.section" + ], "settings": { - "foreground": "#fec758ff" - } - }, - { - "name": "Markup Setext Header", - "scope": "markup.heading.setext", - "settings": { - "fontStyle": "", + "fontStyle": "bold", "foreground": "#fec758ff" } }, diff --git a/extensions/theme-seti/build/update-icon-theme.js b/extensions/theme-seti/build/update-icon-theme.js index 67e33fb9a6..66221a72e1 100644 --- a/extensions/theme-seti/build/update-icon-theme.js +++ b/extensions/theme-seti/build/update-icon-theme.js @@ -305,7 +305,7 @@ exports.update = function () { } return download(fileAssociationFile).then(function (content) { - let regex2 = /\.icon-(?:set|partial)\(['"]([\w-\.]+)['"],\s*['"]([\w-]+)['"],\s*(@[\w-]+)\)/g; + let regex2 = /\.icon-(?:set|partial)\(['"]([\w-\.+]+)['"],\s*['"]([\w-]+)['"],\s*(@[\w-]+)\)/g; while ((match = regex2.exec(content)) !== null) { let pattern = match[1]; let def = '_' + match[2]; diff --git a/extensions/theme-seti/cgmanifest.json b/extensions/theme-seti/cgmanifest.json index b3bb41d3eb..8899dae032 100644 --- a/extensions/theme-seti/cgmanifest.json +++ b/extensions/theme-seti/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "seti-ui", "repositoryUrl": "https://github.com/jesseweed/seti-ui", - "commitHash": "f3b2775662b0075aab56e5f0c03269f21f3f0f30" + "commitHash": "719e5d384e878b0e190abc80247a8726f083a393" } }, "version": "0.1.0" diff --git a/extensions/theme-seti/icons/seti.woff b/extensions/theme-seti/icons/seti.woff index 5dc3bb8d9b..e1a5a63449 100644 Binary files a/extensions/theme-seti/icons/seti.woff and b/extensions/theme-seti/icons/seti.woff differ diff --git a/extensions/theme-seti/icons/vs-seti-icon-theme.json b/extensions/theme-seti/icons/vs-seti-icon-theme.json index 157faca436..26a81f3045 100644 --- a/extensions/theme-seti/icons/vs-seti-icon-theme.json +++ b/extensions/theme-seti/icons/vs-seti-icon-theme.json @@ -254,1119 +254,1143 @@ "fontCharacter": "\\E01C", "fontColor": "#8dc149" }, - "_d_light": { + "_cu_light": { "fontCharacter": "\\E01D", + "fontColor": "#7fae42" + }, + "_cu": { + "fontCharacter": "\\E01D", + "fontColor": "#8dc149" + }, + "_cu_1_light": { + "fontCharacter": "\\E01D", + "fontColor": "#9068b0" + }, + "_cu_1": { + "fontCharacter": "\\E01D", + "fontColor": "#a074c4" + }, + "_d_light": { + "fontCharacter": "\\E01E", "fontColor": "#b8383d" }, "_d": { - "fontCharacter": "\\E01D", + "fontCharacter": "\\E01E", "fontColor": "#cc3e44" }, "_dart_light": { - "fontCharacter": "\\E01E", + "fontCharacter": "\\E01F", "fontColor": "#498ba7" }, "_dart": { - "fontCharacter": "\\E01E", + "fontCharacter": "\\E01F", "fontColor": "#519aba" }, "_db_light": { - "fontCharacter": "\\E01F", + "fontCharacter": "\\E020", "fontColor": "#dd4b78" }, "_db": { - "fontCharacter": "\\E01F", + "fontCharacter": "\\E020", "fontColor": "#f55385" }, "_default_light": { - "fontCharacter": "\\E020", + "fontCharacter": "\\E021", "fontColor": "#bfc2c1" }, "_default": { - "fontCharacter": "\\E020", + "fontCharacter": "\\E021", "fontColor": "#d4d7d6" }, "_docker_light": { - "fontCharacter": "\\E022", + "fontCharacter": "\\E023", "fontColor": "#498ba7" }, "_docker": { - "fontCharacter": "\\E022", + "fontCharacter": "\\E023", "fontColor": "#519aba" }, "_docker_1_light": { - "fontCharacter": "\\E022", + "fontCharacter": "\\E023", "fontColor": "#455155" }, "_docker_1": { - "fontCharacter": "\\E022", + "fontCharacter": "\\E023", "fontColor": "#4d5a5e" }, "_docker_2_light": { - "fontCharacter": "\\E022", + "fontCharacter": "\\E023", "fontColor": "#7fae42" }, "_docker_2": { - "fontCharacter": "\\E022", + "fontCharacter": "\\E023", "fontColor": "#8dc149" }, "_docker_3_light": { - "fontCharacter": "\\E022", + "fontCharacter": "\\E023", "fontColor": "#dd4b78" }, "_docker_3": { - "fontCharacter": "\\E022", + "fontCharacter": "\\E023", "fontColor": "#f55385" }, "_ejs_light": { - "fontCharacter": "\\E024", + "fontCharacter": "\\E025", "fontColor": "#b7b73b" }, "_ejs": { - "fontCharacter": "\\E024", + "fontCharacter": "\\E025", "fontColor": "#cbcb41" }, "_elixir_light": { - "fontCharacter": "\\E025", + "fontCharacter": "\\E026", "fontColor": "#9068b0" }, "_elixir": { - "fontCharacter": "\\E025", + "fontCharacter": "\\E026", "fontColor": "#a074c4" }, "_elixir_script_light": { - "fontCharacter": "\\E026", + "fontCharacter": "\\E027", "fontColor": "#9068b0" }, "_elixir_script": { - "fontCharacter": "\\E026", + "fontCharacter": "\\E027", "fontColor": "#a074c4" }, "_elm_light": { - "fontCharacter": "\\E027", + "fontCharacter": "\\E028", "fontColor": "#498ba7" }, "_elm": { - "fontCharacter": "\\E027", + "fontCharacter": "\\E028", "fontColor": "#519aba" }, "_eslint_light": { - "fontCharacter": "\\E029", + "fontCharacter": "\\E02A", "fontColor": "#9068b0" }, "_eslint": { - "fontCharacter": "\\E029", + "fontCharacter": "\\E02A", "fontColor": "#a074c4" }, "_eslint_1_light": { - "fontCharacter": "\\E029", + "fontCharacter": "\\E02A", "fontColor": "#455155" }, "_eslint_1": { - "fontCharacter": "\\E029", + "fontCharacter": "\\E02A", "fontColor": "#4d5a5e" }, "_ethereum_light": { - "fontCharacter": "\\E02A", + "fontCharacter": "\\E02B", "fontColor": "#498ba7" }, "_ethereum": { - "fontCharacter": "\\E02A", + "fontCharacter": "\\E02B", "fontColor": "#519aba" }, "_f-sharp_light": { - "fontCharacter": "\\E02B", + "fontCharacter": "\\E02C", "fontColor": "#498ba7" }, "_f-sharp": { - "fontCharacter": "\\E02B", + "fontCharacter": "\\E02C", "fontColor": "#519aba" }, "_favicon_light": { - "fontCharacter": "\\E02C", + "fontCharacter": "\\E02D", "fontColor": "#b7b73b" }, "_favicon": { - "fontCharacter": "\\E02C", + "fontCharacter": "\\E02D", "fontColor": "#cbcb41" }, "_firebase_light": { - "fontCharacter": "\\E02D", + "fontCharacter": "\\E02E", "fontColor": "#cc6d2e" }, "_firebase": { - "fontCharacter": "\\E02D", + "fontCharacter": "\\E02E", "fontColor": "#e37933" }, "_firefox_light": { - "fontCharacter": "\\E02E", + "fontCharacter": "\\E02F", "fontColor": "#cc6d2e" }, "_firefox": { - "fontCharacter": "\\E02E", + "fontCharacter": "\\E02F", "fontColor": "#e37933" }, "_font_light": { - "fontCharacter": "\\E030", + "fontCharacter": "\\E031", "fontColor": "#b8383d" }, "_font": { - "fontCharacter": "\\E030", + "fontCharacter": "\\E031", "fontColor": "#cc3e44" }, "_git_light": { - "fontCharacter": "\\E031", + "fontCharacter": "\\E032", "fontColor": "#3b4b52" }, "_git": { - "fontCharacter": "\\E031", + "fontCharacter": "\\E032", "fontColor": "#41535b" }, "_go_light": { - "fontCharacter": "\\E035", + "fontCharacter": "\\E036", "fontColor": "#498ba7" }, "_go": { - "fontCharacter": "\\E035", + "fontCharacter": "\\E036", "fontColor": "#519aba" }, "_go2_light": { - "fontCharacter": "\\E036", + "fontCharacter": "\\E037", "fontColor": "#498ba7" }, "_go2": { - "fontCharacter": "\\E036", + "fontCharacter": "\\E037", "fontColor": "#519aba" }, "_gradle_light": { - "fontCharacter": "\\E037", - "fontColor": "#7fae42" + "fontCharacter": "\\E038", + "fontColor": "#498ba7" }, "_gradle": { - "fontCharacter": "\\E037", - "fontColor": "#8dc149" + "fontCharacter": "\\E038", + "fontColor": "#519aba" }, "_grails_light": { - "fontCharacter": "\\E038", + "fontCharacter": "\\E039", "fontColor": "#7fae42" }, "_grails": { - "fontCharacter": "\\E038", + "fontCharacter": "\\E039", "fontColor": "#8dc149" }, "_graphql_light": { - "fontCharacter": "\\E039", + "fontCharacter": "\\E03A", "fontColor": "#dd4b78" }, "_graphql": { - "fontCharacter": "\\E039", + "fontCharacter": "\\E03A", "fontColor": "#f55385" }, "_grunt_light": { - "fontCharacter": "\\E03A", + "fontCharacter": "\\E03B", "fontColor": "#cc6d2e" }, "_grunt": { - "fontCharacter": "\\E03A", + "fontCharacter": "\\E03B", "fontColor": "#e37933" }, "_gulp_light": { - "fontCharacter": "\\E03B", + "fontCharacter": "\\E03C", "fontColor": "#b8383d" }, "_gulp": { - "fontCharacter": "\\E03B", + "fontCharacter": "\\E03C", "fontColor": "#cc3e44" }, "_haml_light": { - "fontCharacter": "\\E03D", + "fontCharacter": "\\E03E", "fontColor": "#b8383d" }, "_haml": { - "fontCharacter": "\\E03D", + "fontCharacter": "\\E03E", "fontColor": "#cc3e44" }, "_happenings_light": { - "fontCharacter": "\\E03E", + "fontCharacter": "\\E03F", "fontColor": "#498ba7" }, "_happenings": { - "fontCharacter": "\\E03E", + "fontCharacter": "\\E03F", "fontColor": "#519aba" }, "_haskell_light": { - "fontCharacter": "\\E03F", + "fontCharacter": "\\E040", "fontColor": "#9068b0" }, "_haskell": { - "fontCharacter": "\\E03F", + "fontCharacter": "\\E040", "fontColor": "#a074c4" }, "_haxe_light": { - "fontCharacter": "\\E040", + "fontCharacter": "\\E041", "fontColor": "#cc6d2e" }, "_haxe": { - "fontCharacter": "\\E040", + "fontCharacter": "\\E041", "fontColor": "#e37933" }, "_haxe_1_light": { - "fontCharacter": "\\E040", + "fontCharacter": "\\E041", "fontColor": "#b7b73b" }, "_haxe_1": { - "fontCharacter": "\\E040", + "fontCharacter": "\\E041", "fontColor": "#cbcb41" }, "_haxe_2_light": { - "fontCharacter": "\\E040", + "fontCharacter": "\\E041", "fontColor": "#498ba7" }, "_haxe_2": { - "fontCharacter": "\\E040", + "fontCharacter": "\\E041", "fontColor": "#519aba" }, "_haxe_3_light": { - "fontCharacter": "\\E040", + "fontCharacter": "\\E041", "fontColor": "#9068b0" }, "_haxe_3": { - "fontCharacter": "\\E040", + "fontCharacter": "\\E041", "fontColor": "#a074c4" }, "_heroku_light": { - "fontCharacter": "\\E041", + "fontCharacter": "\\E042", "fontColor": "#9068b0" }, "_heroku": { - "fontCharacter": "\\E041", + "fontCharacter": "\\E042", "fontColor": "#a074c4" }, "_hex_light": { - "fontCharacter": "\\E042", + "fontCharacter": "\\E043", "fontColor": "#b8383d" }, "_hex": { - "fontCharacter": "\\E042", + "fontCharacter": "\\E043", "fontColor": "#cc3e44" }, "_html_light": { - "fontCharacter": "\\E043", + "fontCharacter": "\\E044", "fontColor": "#498ba7" }, "_html": { - "fontCharacter": "\\E043", + "fontCharacter": "\\E044", "fontColor": "#519aba" }, "_html_1_light": { - "fontCharacter": "\\E043", + "fontCharacter": "\\E044", "fontColor": "#7fae42" }, "_html_1": { - "fontCharacter": "\\E043", + "fontCharacter": "\\E044", "fontColor": "#8dc149" }, "_html_2_light": { - "fontCharacter": "\\E043", + "fontCharacter": "\\E044", "fontColor": "#b7b73b" }, "_html_2": { - "fontCharacter": "\\E043", + "fontCharacter": "\\E044", "fontColor": "#cbcb41" }, "_html_3_light": { - "fontCharacter": "\\E043", + "fontCharacter": "\\E044", "fontColor": "#cc6d2e" }, "_html_3": { - "fontCharacter": "\\E043", + "fontCharacter": "\\E044", "fontColor": "#e37933" }, "_html_erb_light": { - "fontCharacter": "\\E044", + "fontCharacter": "\\E045", "fontColor": "#b8383d" }, "_html_erb": { - "fontCharacter": "\\E044", + "fontCharacter": "\\E045", "fontColor": "#cc3e44" }, "_ignored_light": { - "fontCharacter": "\\E045", + "fontCharacter": "\\E046", "fontColor": "#3b4b52" }, "_ignored": { - "fontCharacter": "\\E045", + "fontCharacter": "\\E046", "fontColor": "#41535b" }, "_illustrator_light": { - "fontCharacter": "\\E046", + "fontCharacter": "\\E047", "fontColor": "#b7b73b" }, "_illustrator": { - "fontCharacter": "\\E046", + "fontCharacter": "\\E047", "fontColor": "#cbcb41" }, "_image_light": { - "fontCharacter": "\\E047", + "fontCharacter": "\\E048", "fontColor": "#9068b0" }, "_image": { - "fontCharacter": "\\E047", + "fontCharacter": "\\E048", "fontColor": "#a074c4" }, "_info_light": { - "fontCharacter": "\\E048", + "fontCharacter": "\\E049", "fontColor": "#498ba7" }, "_info": { - "fontCharacter": "\\E048", + "fontCharacter": "\\E049", "fontColor": "#519aba" }, "_ionic_light": { - "fontCharacter": "\\E049", + "fontCharacter": "\\E04A", "fontColor": "#498ba7" }, "_ionic": { - "fontCharacter": "\\E049", + "fontCharacter": "\\E04A", "fontColor": "#519aba" }, "_jade_light": { - "fontCharacter": "\\E04A", + "fontCharacter": "\\E04B", "fontColor": "#b8383d" }, "_jade": { - "fontCharacter": "\\E04A", + "fontCharacter": "\\E04B", "fontColor": "#cc3e44" }, "_java_light": { - "fontCharacter": "\\E04B", + "fontCharacter": "\\E04C", "fontColor": "#b8383d" }, "_java": { - "fontCharacter": "\\E04B", + "fontCharacter": "\\E04C", "fontColor": "#cc3e44" }, "_javascript_light": { - "fontCharacter": "\\E04C", + "fontCharacter": "\\E04D", "fontColor": "#b7b73b" }, "_javascript": { - "fontCharacter": "\\E04C", + "fontCharacter": "\\E04D", "fontColor": "#cbcb41" }, "_javascript_1_light": { - "fontCharacter": "\\E04C", + "fontCharacter": "\\E04D", "fontColor": "#cc6d2e" }, "_javascript_1": { - "fontCharacter": "\\E04C", + "fontCharacter": "\\E04D", "fontColor": "#e37933" }, "_javascript_2_light": { - "fontCharacter": "\\E04C", + "fontCharacter": "\\E04D", "fontColor": "#498ba7" }, "_javascript_2": { - "fontCharacter": "\\E04C", + "fontCharacter": "\\E04D", "fontColor": "#519aba" }, "_jenkins_light": { - "fontCharacter": "\\E04D", + "fontCharacter": "\\E04E", "fontColor": "#b8383d" }, "_jenkins": { - "fontCharacter": "\\E04D", + "fontCharacter": "\\E04E", "fontColor": "#cc3e44" }, "_jinja_light": { - "fontCharacter": "\\E04E", + "fontCharacter": "\\E04F", "fontColor": "#b8383d" }, "_jinja": { - "fontCharacter": "\\E04E", + "fontCharacter": "\\E04F", "fontColor": "#cc3e44" }, "_json_light": { - "fontCharacter": "\\E050", + "fontCharacter": "\\E051", "fontColor": "#b7b73b" }, "_json": { - "fontCharacter": "\\E050", + "fontCharacter": "\\E051", "fontColor": "#cbcb41" }, "_json_1_light": { - "fontCharacter": "\\E050", + "fontCharacter": "\\E051", "fontColor": "#7fae42" }, "_json_1": { - "fontCharacter": "\\E050", + "fontCharacter": "\\E051", "fontColor": "#8dc149" }, "_julia_light": { - "fontCharacter": "\\E051", + "fontCharacter": "\\E052", "fontColor": "#9068b0" }, "_julia": { - "fontCharacter": "\\E051", + "fontCharacter": "\\E052", "fontColor": "#a074c4" }, "_karma_light": { - "fontCharacter": "\\E052", + "fontCharacter": "\\E053", "fontColor": "#7fae42" }, "_karma": { - "fontCharacter": "\\E052", + "fontCharacter": "\\E053", "fontColor": "#8dc149" }, "_kotlin_light": { - "fontCharacter": "\\E053", + "fontCharacter": "\\E054", "fontColor": "#cc6d2e" }, "_kotlin": { - "fontCharacter": "\\E053", + "fontCharacter": "\\E054", "fontColor": "#e37933" }, "_less_light": { - "fontCharacter": "\\E054", + "fontCharacter": "\\E055", "fontColor": "#498ba7" }, "_less": { - "fontCharacter": "\\E054", + "fontCharacter": "\\E055", "fontColor": "#519aba" }, "_license_light": { - "fontCharacter": "\\E055", + "fontCharacter": "\\E056", "fontColor": "#b7b73b" }, "_license": { - "fontCharacter": "\\E055", + "fontCharacter": "\\E056", "fontColor": "#cbcb41" }, "_license_1_light": { - "fontCharacter": "\\E055", + "fontCharacter": "\\E056", "fontColor": "#cc6d2e" }, "_license_1": { - "fontCharacter": "\\E055", + "fontCharacter": "\\E056", "fontColor": "#e37933" }, "_license_2_light": { - "fontCharacter": "\\E055", + "fontCharacter": "\\E056", "fontColor": "#b8383d" }, "_license_2": { - "fontCharacter": "\\E055", + "fontCharacter": "\\E056", "fontColor": "#cc3e44" }, "_liquid_light": { - "fontCharacter": "\\E056", + "fontCharacter": "\\E057", "fontColor": "#7fae42" }, "_liquid": { - "fontCharacter": "\\E056", + "fontCharacter": "\\E057", "fontColor": "#8dc149" }, "_livescript_light": { - "fontCharacter": "\\E057", + "fontCharacter": "\\E058", "fontColor": "#498ba7" }, "_livescript": { - "fontCharacter": "\\E057", + "fontCharacter": "\\E058", "fontColor": "#519aba" }, "_lock_light": { - "fontCharacter": "\\E058", + "fontCharacter": "\\E059", "fontColor": "#7fae42" }, "_lock": { - "fontCharacter": "\\E058", + "fontCharacter": "\\E059", "fontColor": "#8dc149" }, "_lua_light": { - "fontCharacter": "\\E059", + "fontCharacter": "\\E05A", "fontColor": "#498ba7" }, "_lua": { - "fontCharacter": "\\E059", + "fontCharacter": "\\E05A", "fontColor": "#519aba" }, "_makefile_light": { - "fontCharacter": "\\E05A", + "fontCharacter": "\\E05B", "fontColor": "#cc6d2e" }, "_makefile": { - "fontCharacter": "\\E05A", + "fontCharacter": "\\E05B", "fontColor": "#e37933" }, "_makefile_1_light": { - "fontCharacter": "\\E05A", + "fontCharacter": "\\E05B", "fontColor": "#9068b0" }, "_makefile_1": { - "fontCharacter": "\\E05A", + "fontCharacter": "\\E05B", "fontColor": "#a074c4" }, "_makefile_2_light": { - "fontCharacter": "\\E05A", + "fontCharacter": "\\E05B", "fontColor": "#627379" }, "_makefile_2": { - "fontCharacter": "\\E05A", + "fontCharacter": "\\E05B", "fontColor": "#6d8086" }, "_makefile_3_light": { - "fontCharacter": "\\E05A", + "fontCharacter": "\\E05B", "fontColor": "#498ba7" }, "_makefile_3": { - "fontCharacter": "\\E05A", + "fontCharacter": "\\E05B", "fontColor": "#519aba" }, "_markdown_light": { - "fontCharacter": "\\E05B", + "fontCharacter": "\\E05C", "fontColor": "#498ba7" }, "_markdown": { - "fontCharacter": "\\E05B", + "fontCharacter": "\\E05C", "fontColor": "#519aba" }, "_maven_light": { - "fontCharacter": "\\E05C", + "fontCharacter": "\\E05D", "fontColor": "#b8383d" }, "_maven": { - "fontCharacter": "\\E05C", + "fontCharacter": "\\E05D", "fontColor": "#cc3e44" }, "_mdo_light": { - "fontCharacter": "\\E05D", + "fontCharacter": "\\E05E", "fontColor": "#b8383d" }, "_mdo": { - "fontCharacter": "\\E05D", + "fontCharacter": "\\E05E", "fontColor": "#cc3e44" }, "_mustache_light": { - "fontCharacter": "\\E05E", + "fontCharacter": "\\E05F", "fontColor": "#cc6d2e" }, "_mustache": { - "fontCharacter": "\\E05E", + "fontCharacter": "\\E05F", "fontColor": "#e37933" }, + "_nim_light": { + "fontCharacter": "\\E061", + "fontColor": "#b7b73b" + }, + "_nim": { + "fontCharacter": "\\E061", + "fontColor": "#cbcb41" + }, "_npm_light": { - "fontCharacter": "\\E060", + "fontCharacter": "\\E062", "fontColor": "#3b4b52" }, "_npm": { - "fontCharacter": "\\E060", + "fontCharacter": "\\E062", "fontColor": "#41535b" }, "_npm_1_light": { - "fontCharacter": "\\E060", + "fontCharacter": "\\E062", "fontColor": "#b8383d" }, "_npm_1": { - "fontCharacter": "\\E060", + "fontCharacter": "\\E062", "fontColor": "#cc3e44" }, "_npm_ignored_light": { - "fontCharacter": "\\E061", + "fontCharacter": "\\E063", "fontColor": "#3b4b52" }, "_npm_ignored": { - "fontCharacter": "\\E061", + "fontCharacter": "\\E063", "fontColor": "#41535b" }, "_nunjucks_light": { - "fontCharacter": "\\E062", + "fontCharacter": "\\E064", "fontColor": "#7fae42" }, "_nunjucks": { - "fontCharacter": "\\E062", + "fontCharacter": "\\E064", "fontColor": "#8dc149" }, "_ocaml_light": { - "fontCharacter": "\\E063", + "fontCharacter": "\\E065", "fontColor": "#cc6d2e" }, "_ocaml": { - "fontCharacter": "\\E063", + "fontCharacter": "\\E065", "fontColor": "#e37933" }, "_odata_light": { - "fontCharacter": "\\E064", + "fontCharacter": "\\E066", "fontColor": "#cc6d2e" }, "_odata": { - "fontCharacter": "\\E064", + "fontCharacter": "\\E066", "fontColor": "#e37933" }, "_pddl_light": { - "fontCharacter": "\\E065", + "fontCharacter": "\\E067", "fontColor": "#9068b0" }, "_pddl": { - "fontCharacter": "\\E065", + "fontCharacter": "\\E067", "fontColor": "#a074c4" }, "_pdf_light": { - "fontCharacter": "\\E066", + "fontCharacter": "\\E068", "fontColor": "#b8383d" }, "_pdf": { - "fontCharacter": "\\E066", + "fontCharacter": "\\E068", "fontColor": "#cc3e44" }, "_perl_light": { - "fontCharacter": "\\E067", + "fontCharacter": "\\E069", "fontColor": "#498ba7" }, "_perl": { - "fontCharacter": "\\E067", + "fontCharacter": "\\E069", "fontColor": "#519aba" }, "_photoshop_light": { - "fontCharacter": "\\E068", + "fontCharacter": "\\E06A", "fontColor": "#498ba7" }, "_photoshop": { - "fontCharacter": "\\E068", + "fontCharacter": "\\E06A", "fontColor": "#519aba" }, "_php_light": { - "fontCharacter": "\\E069", + "fontCharacter": "\\E06B", "fontColor": "#9068b0" }, "_php": { - "fontCharacter": "\\E069", + "fontCharacter": "\\E06B", "fontColor": "#a074c4" }, "_plan_light": { - "fontCharacter": "\\E06A", + "fontCharacter": "\\E06C", "fontColor": "#7fae42" }, "_plan": { - "fontCharacter": "\\E06A", + "fontCharacter": "\\E06C", "fontColor": "#8dc149" }, "_platformio_light": { - "fontCharacter": "\\E06B", + "fontCharacter": "\\E06D", "fontColor": "#cc6d2e" }, "_platformio": { - "fontCharacter": "\\E06B", + "fontCharacter": "\\E06D", "fontColor": "#e37933" }, "_powershell_light": { - "fontCharacter": "\\E06C", + "fontCharacter": "\\E06E", "fontColor": "#498ba7" }, "_powershell": { - "fontCharacter": "\\E06C", + "fontCharacter": "\\E06E", "fontColor": "#519aba" }, "_prolog_light": { - "fontCharacter": "\\E06E", + "fontCharacter": "\\E070", "fontColor": "#cc6d2e" }, "_prolog": { - "fontCharacter": "\\E06E", + "fontCharacter": "\\E070", "fontColor": "#e37933" }, "_pug_light": { - "fontCharacter": "\\E06F", + "fontCharacter": "\\E071", "fontColor": "#b8383d" }, "_pug": { - "fontCharacter": "\\E06F", + "fontCharacter": "\\E071", "fontColor": "#cc3e44" }, "_puppet_light": { - "fontCharacter": "\\E070", + "fontCharacter": "\\E072", "fontColor": "#b7b73b" }, "_puppet": { - "fontCharacter": "\\E070", + "fontCharacter": "\\E072", "fontColor": "#cbcb41" }, "_python_light": { - "fontCharacter": "\\E071", + "fontCharacter": "\\E073", "fontColor": "#498ba7" }, "_python": { - "fontCharacter": "\\E071", + "fontCharacter": "\\E073", "fontColor": "#519aba" }, "_react_light": { - "fontCharacter": "\\E073", + "fontCharacter": "\\E075", "fontColor": "#498ba7" }, "_react": { - "fontCharacter": "\\E073", + "fontCharacter": "\\E075", "fontColor": "#519aba" }, "_react_1_light": { - "fontCharacter": "\\E073", + "fontCharacter": "\\E075", "fontColor": "#cc6d2e" }, "_react_1": { - "fontCharacter": "\\E073", + "fontCharacter": "\\E075", "fontColor": "#e37933" }, "_react_2_light": { - "fontCharacter": "\\E073", + "fontCharacter": "\\E075", "fontColor": "#b7b73b" }, "_react_2": { - "fontCharacter": "\\E073", + "fontCharacter": "\\E075", "fontColor": "#cbcb41" }, "_reasonml_light": { - "fontCharacter": "\\E074", + "fontCharacter": "\\E076", "fontColor": "#b8383d" }, "_reasonml": { - "fontCharacter": "\\E074", + "fontCharacter": "\\E076", "fontColor": "#cc3e44" }, "_rollup_light": { - "fontCharacter": "\\E075", + "fontCharacter": "\\E077", "fontColor": "#b8383d" }, "_rollup": { - "fontCharacter": "\\E075", + "fontCharacter": "\\E077", "fontColor": "#cc3e44" }, "_ruby_light": { - "fontCharacter": "\\E076", + "fontCharacter": "\\E078", "fontColor": "#b8383d" }, "_ruby": { - "fontCharacter": "\\E076", + "fontCharacter": "\\E078", "fontColor": "#cc3e44" }, "_rust_light": { - "fontCharacter": "\\E077", + "fontCharacter": "\\E079", "fontColor": "#627379" }, "_rust": { - "fontCharacter": "\\E077", + "fontCharacter": "\\E079", "fontColor": "#6d8086" }, "_salesforce_light": { - "fontCharacter": "\\E078", + "fontCharacter": "\\E07A", "fontColor": "#498ba7" }, "_salesforce": { - "fontCharacter": "\\E078", + "fontCharacter": "\\E07A", "fontColor": "#519aba" }, "_sass_light": { - "fontCharacter": "\\E079", + "fontCharacter": "\\E07B", "fontColor": "#dd4b78" }, "_sass": { - "fontCharacter": "\\E079", + "fontCharacter": "\\E07B", "fontColor": "#f55385" }, "_sbt_light": { - "fontCharacter": "\\E07A", + "fontCharacter": "\\E07C", "fontColor": "#498ba7" }, "_sbt": { - "fontCharacter": "\\E07A", + "fontCharacter": "\\E07C", "fontColor": "#519aba" }, "_scala_light": { - "fontCharacter": "\\E07B", + "fontCharacter": "\\E07D", "fontColor": "#b8383d" }, "_scala": { - "fontCharacter": "\\E07B", + "fontCharacter": "\\E07D", "fontColor": "#cc3e44" }, "_shell_light": { - "fontCharacter": "\\E07E", + "fontCharacter": "\\E080", "fontColor": "#455155" }, "_shell": { - "fontCharacter": "\\E07E", + "fontCharacter": "\\E080", "fontColor": "#4d5a5e" }, "_slim_light": { - "fontCharacter": "\\E07F", + "fontCharacter": "\\E081", "fontColor": "#cc6d2e" }, "_slim": { - "fontCharacter": "\\E07F", + "fontCharacter": "\\E081", "fontColor": "#e37933" }, "_smarty_light": { - "fontCharacter": "\\E080", + "fontCharacter": "\\E082", "fontColor": "#b7b73b" }, "_smarty": { - "fontCharacter": "\\E080", + "fontCharacter": "\\E082", "fontColor": "#cbcb41" }, "_spring_light": { - "fontCharacter": "\\E081", + "fontCharacter": "\\E083", "fontColor": "#7fae42" }, "_spring": { - "fontCharacter": "\\E081", + "fontCharacter": "\\E083", "fontColor": "#8dc149" }, "_stylelint_light": { - "fontCharacter": "\\E082", + "fontCharacter": "\\E084", "fontColor": "#bfc2c1" }, "_stylelint": { - "fontCharacter": "\\E082", + "fontCharacter": "\\E084", "fontColor": "#d4d7d6" }, "_stylelint_1_light": { - "fontCharacter": "\\E082", + "fontCharacter": "\\E084", "fontColor": "#455155" }, "_stylelint_1": { - "fontCharacter": "\\E082", + "fontCharacter": "\\E084", "fontColor": "#4d5a5e" }, "_stylus_light": { - "fontCharacter": "\\E083", + "fontCharacter": "\\E085", "fontColor": "#7fae42" }, "_stylus": { - "fontCharacter": "\\E083", + "fontCharacter": "\\E085", "fontColor": "#8dc149" }, "_sublime_light": { - "fontCharacter": "\\E084", + "fontCharacter": "\\E086", "fontColor": "#cc6d2e" }, "_sublime": { - "fontCharacter": "\\E084", + "fontCharacter": "\\E086", "fontColor": "#e37933" }, "_svg_light": { - "fontCharacter": "\\E085", + "fontCharacter": "\\E087", "fontColor": "#9068b0" }, "_svg": { - "fontCharacter": "\\E085", + "fontCharacter": "\\E087", "fontColor": "#a074c4" }, "_svg_1_light": { - "fontCharacter": "\\E085", + "fontCharacter": "\\E087", "fontColor": "#498ba7" }, "_svg_1": { - "fontCharacter": "\\E085", + "fontCharacter": "\\E087", "fontColor": "#519aba" }, "_swift_light": { - "fontCharacter": "\\E086", + "fontCharacter": "\\E088", "fontColor": "#cc6d2e" }, "_swift": { - "fontCharacter": "\\E086", + "fontCharacter": "\\E088", "fontColor": "#e37933" }, "_terraform_light": { - "fontCharacter": "\\E087", + "fontCharacter": "\\E089", "fontColor": "#9068b0" }, "_terraform": { - "fontCharacter": "\\E087", + "fontCharacter": "\\E089", "fontColor": "#a074c4" }, "_tex_light": { - "fontCharacter": "\\E088", + "fontCharacter": "\\E08A", "fontColor": "#498ba7" }, "_tex": { - "fontCharacter": "\\E088", + "fontCharacter": "\\E08A", "fontColor": "#519aba" }, "_tex_1_light": { - "fontCharacter": "\\E088", + "fontCharacter": "\\E08A", "fontColor": "#b7b73b" }, "_tex_1": { - "fontCharacter": "\\E088", + "fontCharacter": "\\E08A", "fontColor": "#cbcb41" }, "_tex_2_light": { - "fontCharacter": "\\E088", + "fontCharacter": "\\E08A", "fontColor": "#cc6d2e" }, "_tex_2": { - "fontCharacter": "\\E088", + "fontCharacter": "\\E08A", "fontColor": "#e37933" }, "_tex_3_light": { - "fontCharacter": "\\E088", + "fontCharacter": "\\E08A", "fontColor": "#bfc2c1" }, "_tex_3": { - "fontCharacter": "\\E088", + "fontCharacter": "\\E08A", "fontColor": "#d4d7d6" }, "_todo": { - "fontCharacter": "\\E08A" + "fontCharacter": "\\E08C" }, "_tsconfig_light": { - "fontCharacter": "\\E08B", + "fontCharacter": "\\E08D", "fontColor": "#498ba7" }, "_tsconfig": { - "fontCharacter": "\\E08B", + "fontCharacter": "\\E08D", "fontColor": "#519aba" }, "_twig_light": { - "fontCharacter": "\\E08C", + "fontCharacter": "\\E08E", "fontColor": "#7fae42" }, "_twig": { - "fontCharacter": "\\E08C", + "fontCharacter": "\\E08E", "fontColor": "#8dc149" }, "_typescript_light": { - "fontCharacter": "\\E08D", + "fontCharacter": "\\E08F", "fontColor": "#498ba7" }, "_typescript": { - "fontCharacter": "\\E08D", + "fontCharacter": "\\E08F", "fontColor": "#519aba" }, "_typescript_1_light": { - "fontCharacter": "\\E08D", + "fontCharacter": "\\E08F", "fontColor": "#b7b73b" }, "_typescript_1": { - "fontCharacter": "\\E08D", + "fontCharacter": "\\E08F", "fontColor": "#cbcb41" }, "_vala_light": { - "fontCharacter": "\\E08E", + "fontCharacter": "\\E090", "fontColor": "#627379" }, "_vala": { - "fontCharacter": "\\E08E", + "fontCharacter": "\\E090", "fontColor": "#6d8086" }, "_video_light": { - "fontCharacter": "\\E08F", + "fontCharacter": "\\E091", "fontColor": "#dd4b78" }, "_video": { - "fontCharacter": "\\E08F", + "fontCharacter": "\\E091", "fontColor": "#f55385" }, "_vue_light": { - "fontCharacter": "\\E090", + "fontCharacter": "\\E092", "fontColor": "#7fae42" }, "_vue": { - "fontCharacter": "\\E090", + "fontCharacter": "\\E092", "fontColor": "#8dc149" }, "_wasm_light": { - "fontCharacter": "\\E091", + "fontCharacter": "\\E093", "fontColor": "#9068b0" }, "_wasm": { - "fontCharacter": "\\E091", + "fontCharacter": "\\E093", "fontColor": "#a074c4" }, "_wat_light": { - "fontCharacter": "\\E092", + "fontCharacter": "\\E094", "fontColor": "#9068b0" }, "_wat": { - "fontCharacter": "\\E092", + "fontCharacter": "\\E094", "fontColor": "#a074c4" }, "_webpack_light": { - "fontCharacter": "\\E093", + "fontCharacter": "\\E095", "fontColor": "#498ba7" }, "_webpack": { - "fontCharacter": "\\E093", + "fontCharacter": "\\E095", "fontColor": "#519aba" }, "_wgt_light": { - "fontCharacter": "\\E094", + "fontCharacter": "\\E096", "fontColor": "#498ba7" }, "_wgt": { - "fontCharacter": "\\E094", + "fontCharacter": "\\E096", "fontColor": "#519aba" }, "_windows_light": { - "fontCharacter": "\\E095", + "fontCharacter": "\\E097", "fontColor": "#498ba7" }, "_windows": { - "fontCharacter": "\\E095", + "fontCharacter": "\\E097", "fontColor": "#519aba" }, "_word_light": { - "fontCharacter": "\\E096", + "fontCharacter": "\\E098", "fontColor": "#498ba7" }, "_word": { - "fontCharacter": "\\E096", + "fontCharacter": "\\E098", "fontColor": "#519aba" }, "_xls_light": { - "fontCharacter": "\\E097", + "fontCharacter": "\\E099", "fontColor": "#7fae42" }, "_xls": { - "fontCharacter": "\\E097", + "fontCharacter": "\\E099", "fontColor": "#8dc149" }, "_xml_light": { - "fontCharacter": "\\E098", + "fontCharacter": "\\E09A", "fontColor": "#cc6d2e" }, "_xml": { - "fontCharacter": "\\E098", + "fontCharacter": "\\E09A", "fontColor": "#e37933" }, "_yarn_light": { - "fontCharacter": "\\E099", + "fontCharacter": "\\E09B", "fontColor": "#498ba7" }, "_yarn": { - "fontCharacter": "\\E099", + "fontCharacter": "\\E09B", "fontColor": "#519aba" }, "_yml_light": { - "fontCharacter": "\\E09A", + "fontCharacter": "\\E09C", "fontColor": "#9068b0" }, "_yml": { - "fontCharacter": "\\E09A", + "fontCharacter": "\\E09C", "fontColor": "#a074c4" }, "_zip_light": { - "fontCharacter": "\\E09B", + "fontCharacter": "\\E09D", "fontColor": "#b8383d" }, "_zip": { - "fontCharacter": "\\E09B", + "fontCharacter": "\\E09D", "fontColor": "#cc3e44" }, "_zip_1_light": { - "fontCharacter": "\\E09B", + "fontCharacter": "\\E09D", "fontColor": "#627379" }, "_zip_1": { - "fontCharacter": "\\E09B", + "fontCharacter": "\\E09D", "fontColor": "#6d8086" }, // {{SQL CARBON EDIT}} @@ -1397,6 +1421,7 @@ "hh": "_cpp_1", "hpp": "_cpp_1", "hxx": "_cpp_1", + "h++": "_cpp_1", "edn": "_clojure_1", "cfc": "_coldfusion", "cfm": "_coldfusion", @@ -1413,6 +1438,9 @@ "csv": "_csv", "xls": "_xls", "xlsx": "_xls", + "cu": "_cu", + "cuh": "_cu_1", + "hu": "_cu_1", "cake": "_cake", "ctp": "_cake_php", "d": "_d", @@ -1441,6 +1469,7 @@ "hxs": "_haxe_1", "hxp": "_haxe_2", "hxml": "_haxe_3", + "jade": "_jade", "class": "_java", "classpath": "_java", "properties": "_java", @@ -1462,6 +1491,8 @@ "ad": "_argdown", "mustache": "_mustache", "stache": "_mustache", + "nim": "_nim", + "nims": "_nim", "njk": "_nunjucks", "nunjucks": "_nunjucks", "nunjs": "_nunjucks", @@ -1469,8 +1500,6 @@ "njs": "_nunjucks", "nj": "_nunjucks", "npm-debug.log": "_npm", - "npmignore": "_npm_1", - "npmrc": "_npm_1", "ml": "_ocaml", "mli": "_ocaml", "cmx": "_ocaml", @@ -1480,7 +1509,6 @@ "pddl": "_pddl", "plan": "_plan", "happenings": "_happenings", - "pug": "_pug", "pp": "_puppet", "epp": "_puppet", "spec.jsx": "_react_1", @@ -1498,6 +1526,7 @@ "springbeans": "_spring", "slim": "_slim", "smarty.tpl": "_smarty", + "tpl": "_smarty", "sbt": "_sbt", "scala": "_scala", "sol": "_ethereum", @@ -1536,6 +1565,8 @@ "pxm": "_image", "svg": "_svg", "svgx": "_image", + "tiff": "_image", + "webp": "_image", "sublime-project": "_sublime", "sublime-workspace": "_sublime", "component": "_salesforce", @@ -1661,11 +1692,13 @@ "csharp": "_c-sharp", "css": "_css", "dockerfile": "_docker", + "ignore": "_npm_1", "fsharp": "_f-sharp", "go": "_go2", "groovy": "_grails", "handlebars": "_mustache", "html": "_html_3", + "properties": "_npm_1", "java": "_java", "javascriptreact": "_react", "javascript": "_javascript", @@ -1679,7 +1712,7 @@ "perl": "_perl", "php": "_php", "powershell": "_powershell", - "jade": "_jade", + "jade": "_pug", "python": "_python", "r": "_R", "razor": "_html", @@ -1734,6 +1767,7 @@ "hh": "_cpp_1_light", "hpp": "_cpp_1_light", "hxx": "_cpp_1_light", + "h++": "_cpp_1_light", "edn": "_clojure_1_light", "cfc": "_coldfusion_light", "cfm": "_coldfusion_light", @@ -1750,6 +1784,9 @@ "csv": "_csv_light", "xls": "_xls_light", "xlsx": "_xls_light", + "cu": "_cu_light", + "cuh": "_cu_1_light", + "hu": "_cu_1_light", "cake": "_cake_light", "ctp": "_cake_php_light", "d": "_d_light", @@ -1778,6 +1815,7 @@ "hxs": "_haxe_1_light", "hxp": "_haxe_2_light", "hxml": "_haxe_3_light", + "jade": "_jade_light", "class": "_java_light", "classpath": "_java_light", "properties": "_java_light", @@ -1799,6 +1837,8 @@ "ad": "_argdown_light", "mustache": "_mustache_light", "stache": "_mustache_light", + "nim": "_nim_light", + "nims": "_nim_light", "njk": "_nunjucks_light", "nunjucks": "_nunjucks_light", "nunjs": "_nunjucks_light", @@ -1806,8 +1846,6 @@ "njs": "_nunjucks_light", "nj": "_nunjucks_light", "npm-debug.log": "_npm_light", - "npmignore": "_npm_1_light", - "npmrc": "_npm_1_light", "ml": "_ocaml_light", "mli": "_ocaml_light", "cmx": "_ocaml_light", @@ -1817,7 +1855,6 @@ "pddl": "_pddl_light", "plan": "_plan_light", "happenings": "_happenings_light", - "pug": "_pug_light", "pp": "_puppet_light", "epp": "_puppet_light", "spec.jsx": "_react_1_light", @@ -1835,6 +1872,7 @@ "springbeans": "_spring_light", "slim": "_slim_light", "smarty.tpl": "_smarty_light", + "tpl": "_smarty_light", "sbt": "_sbt_light", "scala": "_scala_light", "sol": "_ethereum_light", @@ -1873,6 +1911,8 @@ "pxm": "_image_light", "svg": "_svg_light", "svgx": "_image_light", + "tiff": "_image_light", + "webp": "_image_light", "sublime-project": "_sublime_light", "sublime-workspace": "_sublime_light", "component": "_salesforce_light", @@ -1938,11 +1978,13 @@ "csharp": "_c-sharp_light", "css": "_css_light", "dockerfile": "_docker_light", + "ignore": "_npm_1_light", "fsharp": "_f-sharp_light", "go": "_go2_light", "groovy": "_grails_light", "handlebars": "_mustache_light", "html": "_html_3_light", + "properties": "_npm_1_light", "java": "_java_light", "javascriptreact": "_react_light", "javascript": "_javascript_light", @@ -1956,7 +1998,7 @@ "perl": "_perl_light", "php": "_php_light", "powershell": "_powershell_light", - "jade": "_jade_light", + "jade": "_pug_light", "python": "_python_light", "r": "_R_light", "razor": "_html_light", @@ -2055,5 +2097,5 @@ "Schema Compare": "scmp" } }, - "version": "https://github.com/jesseweed/seti-ui/commit/f3b2775662b0075aab56e5f0c03269f21f3f0f30" + "version": "https://github.com/jesseweed/seti-ui/commit/719e5d384e878b0e190abc80247a8726f083a393" } \ No newline at end of file diff --git a/extensions/theme-solarized-dark/themes/solarized-dark-color-theme.json b/extensions/theme-solarized-dark/themes/solarized-dark-color-theme.json index b23ff8bb85..eaf90258d3 100644 --- a/extensions/theme-solarized-dark/themes/solarized-dark-color-theme.json +++ b/extensions/theme-solarized-dark/themes/solarized-dark-color-theme.json @@ -270,6 +270,20 @@ "foreground": "#D33682" } }, + { + "name": "Markup: Strong", + "scope": "markup.bold", + "settings": { + "fontStyle": "bold" + } + }, + { + "name": "Markup: Emphasis", + "scope": "markup.italic", + "settings": { + "fontStyle": "italic" + } + }, { "name": "Markup Inline", "scope": "markup.inline.raw", @@ -282,6 +296,7 @@ "name": "Markup Headings", "scope": "markup.heading", "settings": { + "fontStyle": "bold", "foreground": "#268BD2" } }, diff --git a/extensions/theme-solarized-light/themes/solarized-light-color-theme.json b/extensions/theme-solarized-light/themes/solarized-light-color-theme.json index 21f530d00a..77aa0f2907 100644 --- a/extensions/theme-solarized-light/themes/solarized-light-color-theme.json +++ b/extensions/theme-solarized-light/themes/solarized-light-color-theme.json @@ -273,6 +273,20 @@ "foreground": "#D33682" } }, + { + "name": "Markup: Strong", + "scope": "markup.bold", + "settings": { + "fontStyle": "bold" + } + }, + { + "name": "Markup: Emphasis", + "scope": "markup.italic", + "settings": { + "fontStyle": "italic" + } + }, { "name": "Markup Inline", "scope": "markup.inline.raw", @@ -285,6 +299,7 @@ "name": "Markup Headings", "scope": "markup.heading", "settings": { + "fontStyle": "bold", "foreground": "#268BD2" } }, diff --git a/extensions/theme-tomorrow-night-blue/themes/tomorrow-night-blue-theme.json b/extensions/theme-tomorrow-night-blue/themes/tomorrow-night-blue-theme.json index 0baee6822e..bdccdb49d9 100644 --- a/extensions/theme-tomorrow-night-blue/themes/tomorrow-night-blue-theme.json +++ b/extensions/theme-tomorrow-night-blue/themes/tomorrow-night-blue-theme.json @@ -223,6 +223,20 @@ "foreground": "#FFC58F" } }, + { + "name": "Markup: Strong", + "scope": "markup.bold", + "settings": { + "fontStyle": "bold" + } + }, + { + "name": "Markup: Emphasis", + "scope": "markup.italic", + "settings": { + "fontStyle": "italic" + } + }, { "name": "Markup Inline", "scope": "markup.inline.raw", @@ -231,6 +245,13 @@ "foreground": "#FF9DA4" } }, + { + "name": "Markup Headings", + "scope": "markup.heading", + "settings": { + "fontStyle": "bold" + } + }, { "scope": "token.info-token", "settings": { diff --git a/package.json b/package.json index 83c09b302c..5b7bf2a7e9 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "strict-vscode": "node --max_old_space_size=4095 node_modules/typescript/bin/tsc -p src/tsconfig.vscode.json", "strict-vscode-watch": "node --max_old_space_size=4095 node_modules/typescript/bin/tsc -p src/tsconfig.vscode.json --watch", "strict-initialization-watch": "tsc --watch -p src/tsconfig.json --noEmit --strictPropertyInitialization", + "tsec-compile-check": "node_modules/tsec/bin/tsec -p src/tsconfig.json --noEmit", "valid-layers-check": "node build/lib/layersChecker.js", "strict-function-types-watch": "tsc --watch -p src/tsconfig.json --noEmit --strictFunctionTypes", "update-distro": "node build/npm/update-distro.js", @@ -73,7 +74,7 @@ "keytar": "^5.5.0", "minimist": "^1.2.5", "native-is-elevated": "0.4.1", - "native-keymap": "2.1.2", + "native-keymap": "2.2.0", "native-watchdog": "1.3.0", "ng2-charts": "^1.6.0", "node-pty": "0.10.0-beta8", @@ -128,7 +129,7 @@ "@typescript-eslint/eslint-plugin": "3.2.0", "@typescript-eslint/parser": "^3.3.0", "ansi-colors": "^3.2.3", - "asar": "^0.14.0", + "asar": "^3.0.3", "chromium-pickle-js": "^0.2.0", "concurrently": "^5.2.0", "copy-webpack-plugin": "^4.5.2", @@ -136,7 +137,7 @@ "css-loader": "^3.2.0", "debounce": "^1.0.0", "deemon": "^1.4.0", - "electron": "9.2.0", + "electron": "9.2.1", "eslint": "6.8.0", "eslint-plugin-jsdoc": "^19.1.0", "event-stream": "3.3.4", @@ -193,7 +194,8 @@ "temp-write": "^3.4.0", "ts-loader": "^4.4.2", "typemoq": "^0.3.2", - "typescript": "^4.0.1-rc", + "tsec": "googleinterns/tsec", + "typescript": "^4.1.0-dev.20200824", "typescript-formatter": "7.1.0", "underscore": "^1.8.2", "vinyl": "^2.0.0", diff --git a/resources/web/code-web.js b/resources/web/code-web.js index 74dd39e8cb..b6d19a65f1 100644 --- a/resources/web/code-web.js +++ b/resources/web/code-web.js @@ -18,6 +18,7 @@ const fancyLog = require('fancy-log'); const ansiColors = require('ansi-colors'); const remote = require('gulp-remote-retry-src'); const vfs = require('vinyl-fs'); +const uuid = require('uuid'); const extensions = require('../../build/lib/extensions'); @@ -27,21 +28,23 @@ const BUILTIN_MARKETPLACE_EXTENSIONS_ROOT = path.join(APP_ROOT, '.build', 'built const WEB_DEV_EXTENSIONS_ROOT = path.join(APP_ROOT, '.build', 'builtInWebDevExtensions'); const WEB_MAIN = path.join(APP_ROOT, 'src', 'vs', 'code', 'browser', 'workbench', 'workbench-dev.html'); -const WEB_PLAYGROUND_VERSION = '0.0.2'; +const WEB_PLAYGROUND_VERSION = '0.0.8'; const args = minimist(process.argv, { boolean: [ 'no-launch', 'help', 'verbose', - 'wrap-iframe' + 'wrap-iframe', + 'enable-sync' ], string: [ 'scheme', 'host', 'port', 'local_port', - 'extension' + 'extension', + 'github-auth' ], }); @@ -50,11 +53,13 @@ if (args.help) { 'yarn web [options]\n' + ' --no-launch Do not open VSCode web in the browser\n' + ' --wrap-iframe Wrap the Web Worker Extension Host in an iframe\n' + + ' --enable-sync Enable sync by default\n' + ' --scheme Protocol (https or http)\n' + ' --host Remote host\n' + ' --port Remote/Local port\n' + ' --local_port Local port override\n' + ' --extension Path of an extension to include\n' + + ' --github-auth Github authentication token\n' + ' --verbose Print out more information\n' + ' --help\n' + '[Example]\n' + @@ -356,14 +361,38 @@ async function handleRoot(req, res) { const webConfigJSON = { folderUri: folderUri, staticExtensions, + enableSyncByDefault: args['enable-sync'], }; if (args['wrap-iframe']) { webConfigJSON._wrapWebWorkerExtHostInIframe = true; } + const credentials = []; + if (args['github-auth']) { + const sessionId = uuid.v4(); + credentials.push({ + service: 'code-oss.login', + account: 'account', + password: JSON.stringify({ + id: sessionId, + providerId: 'github', + accessToken: args['github-auth'] + }) + }, { + service: 'code-oss-github.login', + account: 'account', + password: JSON.stringify([{ + id: sessionId, + scopes: ['user:email'], + accessToken: args['github-auth'] + }]) + }); + } + const data = (await readFile(WEB_MAIN)).toString() .replace('{{WORKBENCH_WEB_CONFIGURATION}}', () => escapeAttribute(JSON.stringify(webConfigJSON))) // use a replace function to avoid that regexp replace patterns ($&, $0, ...) are applied .replace('{{WORKBENCH_BUILTIN_EXTENSIONS}}', () => escapeAttribute(JSON.stringify(dedupedBuiltInExtensions))) + .replace('{{WORKBENCH_CREDENTIALS}}', () => escapeAttribute(JSON.stringify(credentials))) .replace('{{WEBVIEW_ENDPOINT}}', '') .replace('{{REMOTE_USER_DATA_URI}}', ''); diff --git a/src/buildfile.js b/src/buildfile.js index 06d0d59b83..86fadf7901 100644 --- a/src/buildfile.js +++ b/src/buildfile.js @@ -16,6 +16,7 @@ exports.base = [{ }]; exports.workerExtensionHost = [entrypoint('vs/workbench/services/extensions/worker/extensionHostWorker')]; +exports.workerNotebook = [entrypoint('vs/workbench/contrib/notebook/common/services/notebookSimpleWorker')]; exports.workbenchDesktop = require('./vs/workbench/buildfile.desktop').collectModules(); exports.workbenchWeb = require('./vs/workbench/buildfile.web').collectModules(); diff --git a/src/sql/workbench/browser/modelComponents/diffeditor.component.ts b/src/sql/workbench/browser/modelComponents/diffeditor.component.ts index 859485faa1..4b0340230d 100644 --- a/src/sql/workbench/browser/modelComponents/diffeditor.component.ts +++ b/src/sql/workbench/browser/modelComponents/diffeditor.component.ts @@ -92,7 +92,7 @@ export default class DiffEditorComponent extends ComponentBase implements ICompo let editorinput1 = this._instantiationService.createInstance(ResourceEditorInput, uri1, 'source', undefined, undefined); let editorinput2 = this._instantiationService.createInstance(ResourceEditorInput, uri2, 'target', undefined, undefined); this._editorInput = new DiffEditorInput('DiffEditor', undefined, editorinput1, editorinput2, true); - this._editor.setInput(this._editorInput, undefined, cancellationTokenSource.token); + this._editor.setInput(this._editorInput, undefined, undefined, cancellationTokenSource.token); this._editorInput.resolve().then(model => { diff --git a/src/sql/workbench/browser/modelComponents/editor.component.ts b/src/sql/workbench/browser/modelComponents/editor.component.ts index 9537198164..f36811bf4a 100644 --- a/src/sql/workbench/browser/modelComponents/editor.component.ts +++ b/src/sql/workbench/browser/modelComponents/editor.component.ts @@ -70,7 +70,7 @@ export default class EditorComponent extends ComponentBase implements IComponent this._editor.setVisible(true); let uri = this.createUri(); this._editorInput = this.editorService.createEditorInput({ forceUntitled: true, resource: uri, mode: 'plaintext' }) as UntitledTextEditorInput; - await this._editor.setInput(this._editorInput, undefined); + await this._editor.setInput(this._editorInput, undefined, undefined); const model = await this._editorInput.resolve(); this._editorModel = model.textEditorModel; this.fireEvent({ diff --git a/src/sql/workbench/browser/modelComponents/modelViewEditor.ts b/src/sql/workbench/browser/modelComponents/modelViewEditor.ts index 33c4b220ff..406eaa7013 100644 --- a/src/sql/workbench/browser/modelComponents/modelViewEditor.ts +++ b/src/sql/workbench/browser/modelComponents/modelViewEditor.ts @@ -6,15 +6,15 @@ import 'vs/css!./media/modelViewEditor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { EditorOptions } from 'vs/workbench/common/editor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; +import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; import * as DOM from 'vs/base/browser/dom'; import { ModelViewInput } from 'sql/workbench/browser/modelComponents/modelViewInput'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IStorageService } from 'vs/platform/storage/common/storage'; -export class ModelViewEditor extends BaseEditor { +export class ModelViewEditor extends EditorPane { public static ID: string = 'workbench.editor.modelViewEditor'; @@ -62,7 +62,7 @@ export class ModelViewEditor extends BaseEditor { } } - async setInput(input: ModelViewInput, options?: EditorOptions): Promise { + async setInput(input: ModelViewInput, options?: EditorOptions, context?: IEditorOpenContext): Promise { if (this.input && this.input.matches(input)) { return Promise.resolve(undefined); } @@ -73,7 +73,7 @@ export class ModelViewEditor extends BaseEditor { input.container.style.visibility = 'visible'; this._content.setAttribute('aria-flowto', input.container.id); - await super.setInput(input, options, CancellationToken.None); + await super.setInput(input, options, context, CancellationToken.None); this.doUpdateContainer(); } diff --git a/src/sql/workbench/browser/modelComponents/queryTextEditor.ts b/src/sql/workbench/browser/modelComponents/queryTextEditor.ts index 08f30a3623..3fdbbc401d 100644 --- a/src/sql/workbench/browser/modelComponents/queryTextEditor.ts +++ b/src/sql/workbench/browser/modelComponents/queryTextEditor.ts @@ -15,7 +15,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; -import { EditorOptions } from 'vs/workbench/common/editor'; +import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -85,8 +85,8 @@ export class QueryTextEditor extends BaseTextEditor { return options; } - setInput(input: UntitledTextEditorInput, options: EditorOptions): Promise { - return super.setInput(input, options, CancellationToken.None) + setInput(input: UntitledTextEditorInput, options: EditorOptions, context: IEditorOpenContext): Promise { + return super.setInput(input, options, context, CancellationToken.None) .then(() => this.input.resolve() .then(editorModel => editorModel.load()) .then(editorModel => this.getControl().setModel((editorModel).textEditorModel))); diff --git a/src/sql/workbench/contrib/dashboard/browser/dashboardEditor.ts b/src/sql/workbench/contrib/dashboard/browser/dashboardEditor.ts index 7a48f3ce0c..d81a1e5b3d 100644 --- a/src/sql/workbench/contrib/dashboard/browser/dashboardEditor.ts +++ b/src/sql/workbench/contrib/dashboard/browser/dashboardEditor.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; -import { EditorOptions } from 'vs/workbench/common/editor'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -25,7 +25,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IQueryManagementService } from 'sql/workbench/services/query/common/queryManagement'; -export class DashboardEditor extends BaseEditor { +export class DashboardEditor extends EditorPane { public static ID: string = 'workbench.editor.connectiondashboard'; private _dashboardContainer: HTMLElement; @@ -77,14 +77,14 @@ export class DashboardEditor extends BaseEditor { this._dashboardService.layout(dimension); } - public async setInput(input: DashboardInput, options: EditorOptions): Promise { + public async setInput(input: DashboardInput, options: EditorOptions, context: IEditorOpenContext): Promise { if (this.input && this.input.matches(input)) { return Promise.resolve(undefined); } const parentElement = this.getContainer(); - super.setInput(input, options, CancellationToken.None); + super.setInput(input, options, context, CancellationToken.None); DOM.clearNode(parentElement); diff --git a/src/sql/workbench/contrib/editData/browser/editDataEditor.ts b/src/sql/workbench/contrib/editData/browser/editDataEditor.ts index d96f177b68..b34ba4824e 100644 --- a/src/sql/workbench/contrib/editData/browser/editDataEditor.ts +++ b/src/sql/workbench/contrib/editData/browser/editDataEditor.ts @@ -7,8 +7,8 @@ import * as strings from 'vs/base/common/strings'; import * as DOM from 'vs/base/browser/dom'; import * as nls from 'vs/nls'; -import { EditorOptions, EditorInput, IEditorControl, IEditorPane } from 'vs/workbench/common/editor'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorOptions, EditorInput, IEditorControl, IEditorPane, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -41,7 +41,7 @@ import { onUnexpectedError } from 'vs/base/common/errors'; /** * Editor that hosts an action bar and a resultSetInput for an edit data session */ -export class EditDataEditor extends BaseEditor { +export class EditDataEditor extends EditorPane { public static ID: string = 'workbench.editor.editDataEditor'; @@ -213,7 +213,7 @@ export class EditDataEditor extends BaseEditor { /** * Sets the input data for this editor. */ - public setInput(newInput: EditDataInput, options?: EditorOptions): Promise { + public setInput(newInput: EditDataInput, options?: EditorOptions, context?: IEditorOpenContext): Promise { let oldInput = this.input; if (!newInput.setup) { this._initialized = false; @@ -224,7 +224,7 @@ export class EditDataEditor extends BaseEditor { newInput.setupComplete(); } - return super.setInput(newInput, options, CancellationToken.None) + return super.setInput(newInput, options, context, CancellationToken.None) .then(() => this._updateInput(oldInput, newInput, options)); } @@ -251,7 +251,7 @@ export class EditDataEditor extends BaseEditor { } // PRIVATE METHODS //////////////////////////////////////////////////////////// - private _createEditor(editorInput: EditorInput, container: HTMLElement): Promise { + private _createEditor(editorInput: EditorInput, container: HTMLElement): Promise { const descriptor = this._editorDescriptorService.getEditor(editorInput); if (!descriptor) { return Promise.reject(new Error(strings.format('Can not find a registered editor for the input {0}', editorInput))); @@ -493,7 +493,7 @@ export class EditDataEditor extends BaseEditor { */ private _onResultsEditorCreated(resultsEditor: EditDataResultsEditor, resultsInput: EditDataResultsInput, options: EditorOptions): Promise { this._resultsEditor = resultsEditor; - return this._resultsEditor.setInput(resultsInput, options); + return this._resultsEditor.setInput(resultsInput, options, undefined); } /** @@ -501,7 +501,7 @@ export class EditDataEditor extends BaseEditor { */ private _onSqlEditorCreated(sqlEditor: TextResourceEditor, sqlInput: UntitledTextEditorInput, options: EditorOptions): Thenable { this._sqlEditor = sqlEditor; - return this._sqlEditor.setInput(sqlInput, options, CancellationToken.None); + return this._sqlEditor.setInput(sqlInput, options, undefined, CancellationToken.None); } private _resizeGridContents(): void { diff --git a/src/sql/workbench/contrib/editData/browser/editDataResultsEditor.ts b/src/sql/workbench/contrib/editData/browser/editDataResultsEditor.ts index 9fc79c7822..ccf083c6df 100644 --- a/src/sql/workbench/contrib/editData/browser/editDataResultsEditor.ts +++ b/src/sql/workbench/contrib/editData/browser/editDataResultsEditor.ts @@ -5,13 +5,13 @@ import * as DOM from 'vs/base/browser/dom'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { EditorOptions } from 'vs/workbench/common/editor'; +import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; import { getZoomLevel } from 'vs/base/browser/browser'; import { Configuration } from 'vs/editor/browser/config/configuration'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import * as types from 'vs/base/common/types'; import { IQueryModelService } from 'sql/workbench/services/query/common/queryModel'; @@ -21,7 +21,7 @@ import { EditDataResultsInput } from 'sql/workbench/browser/editData/editDataRes import { CancellationToken } from 'vs/base/common/cancellation'; import { IStorageService } from 'vs/platform/storage/common/storage'; -export class EditDataResultsEditor extends BaseEditor { +export class EditDataResultsEditor extends EditorPane { public static ID: string = 'workbench.editor.editDataResultsEditor'; protected _input: EditDataResultsInput; @@ -63,8 +63,8 @@ export class EditDataResultsEditor extends BaseEditor { public layout(dimension: DOM.Dimension): void { } - public setInput(input: EditDataResultsInput, options: EditorOptions): Promise { - super.setInput(input, options, CancellationToken.None); + public setInput(input: EditDataResultsInput, options: EditorOptions, context: IEditorOpenContext): Promise { + super.setInput(input, options, context, CancellationToken.None); this._applySettings(); if (!input.hasBootstrapped) { this.createGridPanel(); diff --git a/src/sql/workbench/contrib/extensions/browser/scenarioRecommendations.ts b/src/sql/workbench/contrib/extensions/browser/scenarioRecommendations.ts index 2f617567e6..41f6a3fb89 100644 --- a/src/sql/workbench/contrib/extensions/browser/scenarioRecommendations.ts +++ b/src/sql/workbench/contrib/extensions/browser/scenarioRecommendations.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; +import { ExtensionRecommendations, ExtensionRecommendation, PromptedExtensionRecommendations } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; import { IProductService } from 'vs/platform/product/common/productService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -19,6 +19,7 @@ import { IAdsTelemetryService } from 'sql/platform/telemetry/common/telemetry'; import * as TelemetryKeys from 'sql/platform/telemetry/common/telemetryKeys'; import { InstallRecommendedExtensionsByScenarioAction, ShowRecommendedExtensionsByScenarioAction } from 'sql/workbench/contrib/extensions/browser/extensionsActions'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; +import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; const choiceNever = localize('neverShowAgain', "Don't Show Again"); @@ -28,18 +29,20 @@ export class ScenarioRecommendations extends ExtensionRecommendations { get recommendations(): ReadonlyArray { return this._recommendations; } constructor( - isExtensionAllowedToBeRecommended: (extensionId: string) => boolean, + promptedExtensionRecommendations: PromptedExtensionRecommendations, @IProductService private readonly productService: IProductService, - @IInstantiationService instantiationService: IInstantiationService, + @IInstantiationService private readonly instantiationService: IInstantiationService, @IConfigurationService configurationService: IConfigurationService, - @INotificationService notificationService: INotificationService, + @INotificationService private readonly notificationService: INotificationService, @ITelemetryService telemetryService: ITelemetryService, - @IStorageService storageService: IStorageService, - @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @IStorageService private readonly storageService: IStorageService, + @IExtensionManagementService protected readonly extensionManagementService: IExtensionManagementService, @IAdsTelemetryService private readonly adsTelemetryService: IAdsTelemetryService, + @IExtensionsWorkbenchService protected readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService + ) { - super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService); + super(promptedExtensionRecommendations); // this._recommendations = productService.recommendedExtensionsByScenario.map(r => ({ extensionId: r, reason: { reasonId: ExtensionRecommendationReason.Application, reasonText: localize('defaultRecommendations', "This extension is recommended by Azure Data Studio.") }, source: 'application' })); } @@ -123,12 +126,11 @@ export class ScenarioRecommendations extends ExtensionRecommendations { }); } - getRecommendedExtensionsByScenario(scenarioType: string): Promise { + async getRecommendedExtensionsByScenario(scenarioType: string): Promise { if (!scenarioType) { return Promise.reject(new Error(localize('scenarioTypeUndefined', 'The scenario type for extension recommendations must be provided.'))); } - return Promise.resolve((this.productService.recommendedExtensionsByScenario[scenarioType] || []) - .filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId)) - .map(extensionId => ({ extensionId, sources: ['application'] }))); + return this.promptedExtensionRecommendations.filterIgnoredOrNotAllowed(this.productService.recommendedExtensionsByScenario[scenarioType] || []) + .map(extensionId => ({ extensionId, sources: ['application'] })); } } diff --git a/src/sql/workbench/contrib/extensions/browser/staticRecommendations.ts b/src/sql/workbench/contrib/extensions/browser/staticRecommendations.ts index 632003c766..76623915bc 100644 --- a/src/sql/workbench/contrib/extensions/browser/staticRecommendations.ts +++ b/src/sql/workbench/contrib/extensions/browser/staticRecommendations.ts @@ -3,16 +3,10 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; +import { ExtensionRecommendations, ExtensionRecommendation, PromptedExtensionRecommendations } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IStorageService } from 'vs/platform/storage/common/storage'; import { localize } from 'vs/nls'; import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; export class StaticRecommendations extends ExtensionRecommendations { @@ -20,16 +14,10 @@ export class StaticRecommendations extends ExtensionRecommendations { get recommendations(): ReadonlyArray { return this._recommendations; } constructor( - isExtensionAllowedToBeRecommended: (extensionId: string) => boolean, - @IProductService productService: IProductService, - @IInstantiationService instantiationService: IInstantiationService, - @IConfigurationService configurationService: IConfigurationService, - @INotificationService notificationService: INotificationService, - @ITelemetryService telemetryService: ITelemetryService, - @IStorageService storageService: IStorageService, - @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService + promptedExtensionRecommendations: PromptedExtensionRecommendations, + @IProductService productService: IProductService ) { - super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService); + super(promptedExtensionRecommendations); this._recommendations = productService.recommendedExtensions.map(r => ({ extensionId: r, reason: { reasonId: ExtensionRecommendationReason.Application, reasonText: localize('defaultRecommendations', "This extension is recommended by Azure Data Studio.") }, source: 'application' })); } diff --git a/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.ts b/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.ts index 32a63b1762..ebda21b6b1 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.ts @@ -203,7 +203,7 @@ export class CodeComponent extends CellView implements OnInit, OnChanges { cellModelSource = Array.isArray(this.cellModel.source) ? this.cellModel.source.join('') : this.cellModel.source; const model = this._instantiationService.createInstance(UntitledTextEditorModel, uri, false, cellModelSource, this.cellModel.language, undefined); this._editorInput = this._instantiationService.createInstance(UntitledTextEditorInput, model); - await this._editor.setInput(this._editorInput, undefined); + await this._editor.setInput(this._editorInput, undefined, undefined); this.setFocusAndScroll(); let untitledEditorModel = await this._editorInput.resolve() as UntitledTextEditorModel; diff --git a/src/sql/workbench/contrib/notebook/browser/notebookEditor.ts b/src/sql/workbench/contrib/notebook/browser/notebookEditor.ts index 9f303e03c2..9ba5c18f84 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebookEditor.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { EditorOptions } from 'vs/workbench/common/editor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; +import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; import * as DOM from 'vs/base/browser/dom'; import { bootstrapAngular } from 'sql/workbench/services/bootstrap/browser/bootstrapService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -35,7 +35,7 @@ import { TimeoutTimer } from 'vs/base/common/async'; import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; import { onUnexpectedError } from 'vs/base/common/errors'; -export class NotebookEditor extends BaseEditor implements IFindNotebookController { +export class NotebookEditor extends EditorPane implements IFindNotebookController { public static ID: string = 'workbench.editor.notebookEditor'; private _notebookContainer: HTMLElement; @@ -187,14 +187,14 @@ export class NotebookEditor extends BaseEditor implements IFindNotebookControlle } } - public async setInput(input: NotebookInput, options: EditorOptions): Promise { + public async setInput(input: NotebookInput, options: EditorOptions, context: IEditorOpenContext): Promise { if (this.input && this.input.matches(input)) { return Promise.resolve(undefined); } const parentElement = this.getContainer(); - await super.setInput(input, options, CancellationToken.None); + await super.setInput(input, options, context, CancellationToken.None); DOM.clearNode(parentElement); await this.setFindInput(parentElement); if (!input.hasBootstrapped) { diff --git a/src/sql/workbench/contrib/notebook/test/browser/notebookEditor.test.ts b/src/sql/workbench/contrib/notebook/test/browser/notebookEditor.test.ts index 6d79f354e0..fc275fdf5b 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/notebookEditor.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/notebookEditor.test.ts @@ -126,7 +126,7 @@ suite.skip('Test class NotebookEditor:', () => { const testNotebookEditor = new NotebookEditorStub({ cellGuid: cellTextEditorGuid, editor: queryTextEditor, model: notebookModel, notebookParams: { notebookUri: untitledNotebookInput.notebookUri } }); notebookService.addNotebookEditor(testNotebookEditor); notebookEditor.clearInput(); - await notebookEditor.setInput(untitledNotebookInput, EditorOptions.create({ pinned: true })); + await notebookEditor.setInput(untitledNotebookInput, EditorOptions.create({ pinned: true }), undefined); untitledNotebookInput.notebookFindModel.notebookModel = undefined; // clear preexisting notebookModel const result = await notebookEditor.getNotebookModel(); assert.strictEqual(result, notebookModel, `getNotebookModel() should return the model set in the INotebookEditor object`); @@ -214,7 +214,7 @@ suite.skip('Test class NotebookEditor:', () => { untitledNotebookInput /* set to a known input */, untitledNotebookInput /* tries to set the same input that was previously set */ ]) { - await notebookEditor.setInput(input, editorOptions); + await notebookEditor.setInput(input, editorOptions, undefined); assert.strictEqual(notebookEditor.input, input, `notebookEditor.input should be the one that we set`); } }); @@ -225,7 +225,7 @@ suite.skip('Test class NotebookEditor:', () => { for (const isRevealed of [true, false]) { notebookEditor['_findState']['_isRevealed'] = isRevealed; notebookEditor.clearInput(); - await notebookEditor.setInput(untitledNotebookInput, editorOptions); + await notebookEditor.setInput(untitledNotebookInput, editorOptions, undefined); assert.strictEqual(notebookEditor.input, untitledNotebookInput, `notebookEditor.input should be the one that we set`); } }); @@ -744,7 +744,7 @@ async function setupNotebookEditor(notebookEditor: NotebookEditor, untitledNoteb async function setInputDocument(notebookEditor: NotebookEditor, untitledNotebookInput: UntitledNotebookInput): Promise { const editorOptions = EditorOptions.create({ pinned: true }); - await notebookEditor.setInput(untitledNotebookInput, editorOptions); + await notebookEditor.setInput(untitledNotebookInput, editorOptions, undefined); assert.strictEqual(notebookEditor.options, editorOptions, 'NotebookEditor options must be the ones that we set'); } diff --git a/src/sql/workbench/contrib/profiler/browser/profilerEditor.ts b/src/sql/workbench/contrib/profiler/browser/profilerEditor.ts index 24df69040a..bb3c5fd771 100644 --- a/src/sql/workbench/contrib/profiler/browser/profilerEditor.ts +++ b/src/sql/workbench/contrib/profiler/browser/profilerEditor.ts @@ -38,7 +38,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IView, SplitView, Sizing } from 'vs/base/browser/ui/splitview/splitview'; import * as DOM from 'vs/base/browser/dom'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { EditorOptions } from 'vs/workbench/common/editor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkbenchThemeService, VS_DARK_THEME, VS_HC_THEME } from 'vs/workbench/services/themes/common/workbenchThemeService'; @@ -117,7 +117,7 @@ export interface IDetailData { value: string; } -export class ProfilerEditor extends BaseEditor { +export class ProfilerEditor extends EditorPane { public static readonly ID: string = 'workbench.editor.profiler'; private _untitledTextEditorModel: UntitledTextEditorModel; @@ -440,7 +440,7 @@ export class ProfilerEditor extends BaseEditor { this._editor.setVisible(true); this._untitledTextEditorModel = this._instantiationService.createInstance(UntitledTextEditorModel, URI.from({ scheme: Schemas.untitled }), false, undefined, 'sql', undefined); this._editorInput = this._instantiationService.createInstance(UntitledTextEditorInput, this._untitledTextEditorModel); - this._editor.setInput(this._editorInput, undefined); + this._editor.setInput(this._editorInput, undefined, undefined); this._editorInput.resolve().then(model => this._editorModel = model.textEditorModel); return editorContainer; } @@ -460,7 +460,7 @@ export class ProfilerEditor extends BaseEditor { return Promise.resolve(null); } - return super.setInput(input, options, CancellationToken.None).then(() => { + return super.setInput(input, options, undefined, CancellationToken.None).then(() => { this._profilerTableEditor.setInput(input); if (input.viewTemplate) { diff --git a/src/sql/workbench/contrib/profiler/browser/profilerResourceEditor.ts b/src/sql/workbench/contrib/profiler/browser/profilerResourceEditor.ts index 6693c54447..b3d790153d 100644 --- a/src/sql/workbench/contrib/profiler/browser/profilerResourceEditor.ts +++ b/src/sql/workbench/contrib/profiler/browser/profilerResourceEditor.ts @@ -15,7 +15,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; -import { EditorOptions } from 'vs/workbench/common/editor'; +import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -75,8 +75,8 @@ export class ProfilerResourceEditor extends BaseTextEditor { return options; } - setInput(input: UntitledTextEditorInput, options: EditorOptions): Promise { - return super.setInput(input, options, CancellationToken.None) + setInput(input: UntitledTextEditorInput, options: EditorOptions, context: IEditorOpenContext): Promise { + return super.setInput(input, options, context, CancellationToken.None) .then(() => this.input.resolve() .then(editorModel => editorModel.load()) .then(editorModel => this.getControl().setModel((editorModel).textEditorModel))); diff --git a/src/sql/workbench/contrib/profiler/browser/profilerTableEditor.ts b/src/sql/workbench/contrib/profiler/browser/profilerTableEditor.ts index d82f5554e7..a58bc9b9f1 100644 --- a/src/sql/workbench/contrib/profiler/browser/profilerTableEditor.ts +++ b/src/sql/workbench/contrib/profiler/browser/profilerTableEditor.ts @@ -20,7 +20,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IEditorAction } from 'vs/editor/common/editorCommon'; import { IOverlayWidget } from 'vs/editor/browser/editorBrowser'; import { FindReplaceState, FindReplaceStateChangedEvent } from 'vs/editor/contrib/find/findState'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { Event, Emitter } from 'vs/base/common/event'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -39,7 +39,7 @@ export interface ProfilerTableViewState { scrollLeft: number; } -export class ProfilerTableEditor extends BaseEditor implements IProfilerController, ITableController { +export class ProfilerTableEditor extends EditorPane implements IProfilerController, ITableController { public static ID: string = 'workbench.editor.profiler.table'; protected _input: ProfilerInput; diff --git a/src/sql/workbench/contrib/query/browser/queryEditor.ts b/src/sql/workbench/contrib/query/browser/queryEditor.ts index d2b5c547eb..81de149428 100644 --- a/src/sql/workbench/contrib/query/browser/queryEditor.ts +++ b/src/sql/workbench/contrib/query/browser/queryEditor.ts @@ -7,8 +7,8 @@ import 'vs/css!./media/queryEditor'; import * as DOM from 'vs/base/browser/dom'; import * as path from 'vs/base/common/path'; -import { EditorOptions, IEditorControl, IEditorMemento } from 'vs/workbench/common/editor'; -import { BaseEditor, EditorMemento } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorOptions, IEditorControl, IEditorMemento, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { EditorPane, EditorMemento } from 'vs/workbench/browser/parts/editor/editorPane'; import { Orientation } from 'vs/base/browser/ui/sash/sash'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -50,7 +50,7 @@ interface IQueryEditorViewState { * Editor that hosts 2 sub-editors: A TextResourceEditor for SQL file editing, and a QueryResultsEditor * for viewing and editing query results. This editor is based off SideBySideEditor. */ -export class QueryEditor extends BaseEditor { +export class QueryEditor extends EditorPane { public static ID: string = 'workbench.editor.queryEditor'; @@ -320,7 +320,7 @@ export class QueryEditor extends BaseEditor { this.taskbar.setContent(content); } - public async setInput(newInput: QueryEditorInput, options: EditorOptions, token: CancellationToken): Promise { + public async setInput(newInput: QueryEditorInput, options: EditorOptions, context: IEditorOpenContext, token: CancellationToken): Promise { const oldInput = this.input; if (newInput.matches(oldInput)) { @@ -350,9 +350,9 @@ export class QueryEditor extends BaseEditor { } await Promise.all([ - super.setInput(newInput, options, token), - this.currentTextEditor.setInput(newInput.text, options, token), - this.resultsEditor.setInput(newInput.results, options) + super.setInput(newInput, options, context, token), + this.currentTextEditor.setInput(newInput.text, options, context, token), + this.resultsEditor.setInput(newInput.results, options, context) ]); this.inputDisposables.clear(); diff --git a/src/sql/workbench/contrib/query/browser/queryResultsEditor.ts b/src/sql/workbench/contrib/query/browser/queryResultsEditor.ts index 448eadb32b..8f49103a06 100644 --- a/src/sql/workbench/contrib/query/browser/queryResultsEditor.ts +++ b/src/sql/workbench/contrib/query/browser/queryResultsEditor.ts @@ -3,10 +3,10 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EditorOptions } from 'vs/workbench/common/editor'; +import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; import { getZoomLevel } from 'vs/base/browser/browser'; @@ -71,7 +71,7 @@ export function getBareResultsGridInfoStyles(info: BareResultsGridInfo): string /** * Editor associated with viewing and editing the data of a query results grid. */ -export class QueryResultsEditor extends BaseEditor { +export class QueryResultsEditor extends EditorPane { public static ID: string = 'workbench.editor.queryResultsEditor'; protected _rawOptions: BareResultsGridInfo; @@ -131,8 +131,8 @@ export class QueryResultsEditor extends BaseEditor { this.resultsView.layout(dimension); } - setInput(input: QueryResultsInput, options: EditorOptions): Promise { - super.setInput(input, options, CancellationToken.None); + setInput(input: QueryResultsInput, options: EditorOptions, context: IEditorOpenContext): Promise { + super.setInput(input, options, context, CancellationToken.None); this.resultsView.input = input; return Promise.resolve(null); } diff --git a/src/sql/workbench/contrib/query/test/browser/queryEditor.test.ts b/src/sql/workbench/contrib/query/test/browser/queryEditor.test.ts index 3e10a94728..4a565a7691 100644 --- a/src/sql/workbench/contrib/query/test/browser/queryEditor.test.ts +++ b/src/sql/workbench/contrib/query/test/browser/queryEditor.test.ts @@ -16,7 +16,7 @@ import { EditorDescriptorService } from 'sql/workbench/services/queryEditor/brow import * as TypeMoq from 'typemoq'; import * as assert from 'assert'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; @@ -76,7 +76,7 @@ suite('SQL QueryEditor Tests', () => { getId: function (): string { return 'id'; }, getName: function (): string { return 'name'; }, describes: function (obj: any): boolean { return true; }, - instantiate(instantiationService: IInstantiationService): BaseEditor { return undefined; } + instantiate(instantiationService: IInstantiationService): EditorPane { return undefined; } }; editorDescriptorService = TypeMoq.Mock.ofType(EditorDescriptorService, TypeMoq.MockBehavior.Loose); editorDescriptorService.setup(x => x.getEditor(TypeMoq.It.isAny())).returns(() => descriptor); diff --git a/src/sql/workbench/contrib/queryPlan/browser/queryPlanEditor.ts b/src/sql/workbench/contrib/queryPlan/browser/queryPlanEditor.ts index 6a8c6dfd25..9720a282b1 100644 --- a/src/sql/workbench/contrib/queryPlan/browser/queryPlanEditor.ts +++ b/src/sql/workbench/contrib/queryPlan/browser/queryPlanEditor.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; -import { EditorOptions } from 'vs/workbench/common/editor'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { QueryPlanInput } from 'sql/workbench/contrib/queryPlan/common/queryPlanInput'; @@ -13,7 +13,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { QueryPlanView } from 'sql/workbench/contrib/queryPlan/browser/queryPlan'; -export class QueryPlanEditor extends BaseEditor { +export class QueryPlanEditor extends EditorPane { public static ID: string = 'workbench.editor.queryplan'; @@ -60,13 +60,13 @@ export class QueryPlanEditor extends BaseEditor { this.view.layout(dimension); } - public async setInput(input: QueryPlanInput, options: EditorOptions): Promise { + public async setInput(input: QueryPlanInput, options: EditorOptions, context: IEditorOpenContext): Promise { if (this.input instanceof QueryPlanInput && this.input.matches(input)) { return Promise.resolve(undefined); } await input.resolve(); - await super.setInput(input, options, CancellationToken.None); + await super.setInput(input, options, context, CancellationToken.None); this.view.showPlan(input.planXml!); } diff --git a/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerEditor.ts b/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerEditor.ts index 49b0c27704..f727f67c83 100644 --- a/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerEditor.ts +++ b/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerEditor.ts @@ -8,16 +8,16 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IStorageService } from 'vs/platform/storage/common/storage'; import * as DOM from 'vs/base/browser/dom'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { EditorOptions } from 'vs/workbench/common/editor'; +import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IResourceViewerStateChangedEvent } from 'sql/workbench/common/editor/resourceViewer/resourceViewerState'; import { ResourceViewerInput } from 'sql/workbench/browser/editor/resourceViewer/resourceViewerInput'; import { ResourceViewerTable } from 'sql/workbench/contrib/resourceViewer/browser/resourceViewerTable'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; -export class ResourceViewerEditor extends BaseEditor { +export class ResourceViewerEditor extends EditorPane { public static readonly ID: string = 'workbench.editor.resource-viewer'; private _container!: HTMLElement; @@ -72,8 +72,8 @@ export class ResourceViewerEditor extends BaseEditor { return this._input as ResourceViewerInput; } - public async setInput(input: ResourceViewerInput, options?: EditorOptions): Promise { - await super.setInput(input, options, CancellationToken.None); + async setInput(input: ResourceViewerInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + await super.setInput(input, options, context, token); this._inputDisposables.clear(); diff --git a/src/tsconfig.base.json b/src/tsconfig.base.json index 2b958153c9..e12531e69b 100644 --- a/src/tsconfig.base.json +++ b/src/tsconfig.base.json @@ -25,6 +25,7 @@ }, "lib": [ "ES2015", + "ES2016.Array.Include", "ES2017.String", "ES2018.Promise", "DOM", diff --git a/src/tsconfig.monaco.json b/src/tsconfig.monaco.json index 825a83761f..86b2926a1a 100644 --- a/src/tsconfig.monaco.json +++ b/src/tsconfig.monaco.json @@ -15,7 +15,6 @@ "include": [ "typings/require.d.ts", "typings/thenable.d.ts", - "typings/lib.array-ext.d.ts", "vs/css.d.ts", "vs/monaco.d.ts", "vs/nls.d.ts", diff --git a/src/vs/base/browser/codicons.ts b/src/vs/base/browser/codicons.ts new file mode 100644 index 0000000000..a418e52b19 --- /dev/null +++ b/src/vs/base/browser/codicons.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; +import { renderCodiconsRegex } from 'vs/base/common/codicons'; + +export function renderCodiconsAsElement(text: string): Array { + const elements = new Array(); + let match: RegExpMatchArray | null; + + let textStart = 0, textStop = 0; + while ((match = renderCodiconsRegex.exec(text)) !== null) { + textStop = match.index || 0; + elements.push(text.substring(textStart, textStop)); + textStart = (match.index || 0) + match[0].length; + + const [, escaped, codicon, name, animation] = match; + elements.push(escaped ? `$(${codicon})` : dom.$(`span.codicon.codicon-${name}${animation ? `.codicon-animation-${animation}` : ''}`)); + } + + if (textStart < text.length) { + elements.push(text.substring(textStart)); + } + return elements; +} \ No newline at end of file diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 07051b552a..7ae3eb6350 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -997,6 +997,11 @@ export function trackFocus(element: HTMLElement | Window): IFocusTracker { return new FocusTracker(element); } +export function after(sibling: HTMLElement, child: T): T { + sibling.after(child); + return child; +} + export function append(parent: HTMLElement, ...children: T[]): T { children.forEach(child => parent.appendChild(child)); return children[children.length - 1]; @@ -1009,6 +1014,18 @@ export function prepend(parent: HTMLElement, child: T): T { const SELECTOR_REGEX = /([\w\-]+)?(#([\w\-]+))?((\.([\w\-]+))*)/; +export function reset(parent: HTMLElement, ...children: Array) { + parent.innerText = ''; + coalesce(children) + .forEach(child => { + if (child instanceof Node) { + parent.appendChild(child); + } else { + parent.appendChild(document.createTextNode(child as string)); + } + }); +} + export enum Namespace { HTML = 'http://www.w3.org/1999/xhtml', SVG = 'http://www.w3.org/2000/svg' diff --git a/src/vs/base/browser/keyboardEvent.ts b/src/vs/base/browser/keyboardEvent.ts index 1bb5f6f0df..c68a969149 100644 --- a/src/vs/base/browser/keyboardEvent.ts +++ b/src/vs/base/browser/keyboardEvent.ts @@ -205,6 +205,40 @@ const altKeyMod = KeyMod.Alt; const shiftKeyMod = KeyMod.Shift; const metaKeyMod = (platform.isMacintosh ? KeyMod.CtrlCmd : KeyMod.WinCtrl); +export function printKeyboardEvent(e: KeyboardEvent): string { + let modifiers: string[] = []; + if (e.ctrlKey) { + modifiers.push(`ctrl`); + } + if (e.shiftKey) { + modifiers.push(`shift`); + } + if (e.altKey) { + modifiers.push(`alt`); + } + if (e.metaKey) { + modifiers.push(`meta`); + } + return `modifiers: [${modifiers.join(',')}], code: ${e.code}, keyCode: ${e.keyCode}, key: ${e.key}`; +} + +export function printStandardKeyboardEvent(e: StandardKeyboardEvent): string { + let modifiers: string[] = []; + if (e.ctrlKey) { + modifiers.push(`ctrl`); + } + if (e.shiftKey) { + modifiers.push(`shift`); + } + if (e.altKey) { + modifiers.push(`alt`); + } + if (e.metaKey) { + modifiers.push(`meta`); + } + return `modifiers: [${modifiers.join(',')}], code: ${e.code}, keyCode: ${e.keyCode} ('${KeyCodeUtils.toString(e.keyCode)}')`; +} + export class StandardKeyboardEvent implements IKeyboardEvent { readonly _standardKeyboardEventBrand = true; diff --git a/src/vs/base/browser/ui/button/button.ts b/src/vs/base/browser/ui/button/button.ts index 73839e5639..2976a3c145 100644 --- a/src/vs/base/browser/ui/button/button.ts +++ b/src/vs/base/browser/ui/button/button.ts @@ -12,8 +12,7 @@ import { mixin } from 'vs/base/common/objects'; import { Event as BaseEvent, Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { Gesture, EventType } from 'vs/base/browser/touch'; -import { renderCodicons } from 'vs/base/common/codicons'; -import { escape } from 'vs/base/common/strings'; +import { renderCodiconsAsElement } from 'vs/base/browser/codicons'; export interface IButtonOptions extends IButtonStyles { readonly title?: boolean | string; @@ -181,7 +180,7 @@ export class Button extends Disposable { DOM.addClass(this._element, 'monaco-text-button'); } if (this.options.supportCodicons) { - this._element.innerHTML = renderCodicons(escape(value)); + DOM.reset(this._element, ...renderCodiconsAsElement(value)); } else { this._element.textContent = value; } diff --git a/src/vs/base/browser/ui/codicons/codiconLabel.ts b/src/vs/base/browser/ui/codicons/codiconLabel.ts index d851fe33e3..3da4558e5d 100644 --- a/src/vs/base/browser/ui/codicons/codiconLabel.ts +++ b/src/vs/base/browser/ui/codicons/codiconLabel.ts @@ -3,8 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { escape } from 'vs/base/common/strings'; -import { renderCodicons } from 'vs/base/common/codicons'; +import { reset } from 'vs/base/browser/dom'; +import { renderCodiconsAsElement } from 'vs/base/browser/codicons'; export class CodiconLabel { @@ -13,7 +13,7 @@ export class CodiconLabel { ) { } set text(text: string) { - this._container.innerHTML = renderCodicons(escape(text ?? '')); + reset(this._container, ...renderCodiconsAsElement(text ?? '')); } set title(title: string) { diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index 9065f843b5..e61b8b8816 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -121,7 +121,7 @@ export class ExternalElementsDragAndDropData implements IDragAndDropData { } } -export class DesktopDragAndDropData implements IDragAndDropData { +export class NativeDragAndDropData implements IDragAndDropData { readonly types: any[]; readonly files: any[]; @@ -976,7 +976,7 @@ export class ListView implements ISpliceable, IDisposable { return false; } - this.currentDragData = new DesktopDragAndDropData(); + this.currentDragData = new NativeDragAndDropData(); } } diff --git a/src/vs/base/browser/ui/splitview/paneview.ts b/src/vs/base/browser/ui/splitview/paneview.ts index 2c353f3b5f..4df8c5bcd1 100644 --- a/src/vs/base/browser/ui/splitview/paneview.ts +++ b/src/vs/base/browser/ui/splitview/paneview.ts @@ -24,6 +24,7 @@ export interface IPaneOptions { expanded?: boolean; orientation?: Orientation; title: string; + titleDescription?: string; } export interface IPaneStyles { diff --git a/src/vs/base/common/arrays.ts b/src/vs/base/common/arrays.ts index 6c0d1351b4..b987fec219 100644 --- a/src/vs/base/common/arrays.ts +++ b/src/vs/base/common/arrays.ts @@ -590,17 +590,6 @@ export function asArray(x: T | T[]): T[] { return Array.isArray(x) ? x : [x]; } -/** - * @deprecated Use `Array.from` or `[...iter]` - */ -export function toArray(iterable: IterableIterator): T[] { - const result: T[] = []; - for (let element of iterable) { - result.push(element); - } - return result; -} - export function getRandomElement(arr: T[]): T | undefined { return arr[Math.floor(Math.random() * arr.length)]; } diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index 5543500af1..96e8cf8844 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -170,6 +170,25 @@ export class Sequencer { } } +export class SequencerByKey { + + private promiseMap = new Map>(); + + queue(key: TKey, promiseTask: ITask>): Promise { + const runningPromise = this.promiseMap.get(key) ?? Promise.resolve(); + const newPromise = runningPromise + .catch(() => { }) + .then(promiseTask) + .finally(() => { + if (this.promiseMap.get(key) === newPromise) { + this.promiseMap.delete(key); + } + }); + this.promiseMap.set(key, newPromise); + return newPromise; + } +} + /** * A helper to delay execution of a task that is being requested often. * diff --git a/src/vs/base/common/codicons.ts b/src/vs/base/common/codicons.ts index 88e1ac0676..1c793daa9b 100644 --- a/src/vs/base/common/codicons.ts +++ b/src/vs/base/common/codicons.ts @@ -499,7 +499,11 @@ export function markdownUnescapeCodicons(text: string): string { return text.replace(markdownUnescapeCodiconsRegex, (match, escaped, codicon) => escaped ? match : `$(${codicon})`); } -const renderCodiconsRegex = /(\\)?\$\((([a-z0-9\-]+?)(?:~([a-z0-9\-]*?))?)\)/gi; +export const renderCodiconsRegex = /(\\)?\$\((([a-z0-9\-]+?)(?:~([a-z0-9\-]*?))?)\)/gi; + +/** + * @deprecated Use `renderCodiconsAsElement` instead + */ export function renderCodicons(text: string): string { return text.replace(renderCodiconsRegex, (_, escaped, codicon, name, animation) => { // If the class for codicons is changed, it should also be updated in src\vs\base\browser\markdownRenderer.ts diff --git a/src/vs/base/common/extpath.ts b/src/vs/base/common/extpath.ts index 7eefd052fb..336ab052fe 100644 --- a/src/vs/base/common/extpath.ts +++ b/src/vs/base/common/extpath.ts @@ -142,7 +142,7 @@ export function isUNC(path: string): boolean { // Reference: https://en.wikipedia.org/wiki/Filename const WINDOWS_INVALID_FILE_CHARS = /[\\/:\*\?"<>\|]/g; const UNIX_INVALID_FILE_CHARS = /[\\/]/g; -const WINDOWS_FORBIDDEN_NAMES = /^(con|prn|aux|clock\$|nul|lpt[0-9]|com[0-9])$/i; +const WINDOWS_FORBIDDEN_NAMES = /^(con|prn|aux|clock\$|nul|lpt[0-9]|com[0-9])(\.(.*?))?$/i; export function isValidBasename(name: string | null | undefined, isWindowsOS: boolean = isWindows): boolean { const invalidFileChars = isWindowsOS ? WINDOWS_INVALID_FILE_CHARS : UNIX_INVALID_FILE_CHARS; diff --git a/src/vs/base/common/fuzzyScorer.ts b/src/vs/base/common/fuzzyScorer.ts index e4895ac6b7..685944f9f7 100644 --- a/src/vs/base/common/fuzzyScorer.ts +++ b/src/vs/base/common/fuzzyScorer.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { compareAnything } from 'vs/base/common/comparers'; -import { matchesPrefix, IMatch, isUpper, fuzzyScore, createMatches as createFuzzyMatches, matchesStrictPrefix } from 'vs/base/common/filters'; +import { matchesPrefix, IMatch, isUpper, fuzzyScore, createMatches as createFuzzyMatches } from 'vs/base/common/filters'; import { sep } from 'vs/base/common/path'; import { isWindows, isLinux } from 'vs/base/common/platform'; import { stripWildcards, equalsIgnoreCase } from 'vs/base/common/strings'; @@ -369,9 +369,8 @@ export interface IItemAccessor { } const PATH_IDENTITY_SCORE = 1 << 18; -const LABEL_PREFIX_SCORE_MATCHCASE = 1 << 17; -const LABEL_PREFIX_SCORE_IGNORECASE = 1 << 16; -const LABEL_SCORE_THRESHOLD = 1 << 15; +const LABEL_PREFIX_SCORE_THRESHOLD = 1 << 17; +const LABEL_SCORE_THRESHOLD = 1 << 16; export function scoreItemFuzzy(item: T, query: IPreparedQuery, fuzzy: boolean, accessor: IItemAccessor, cache: FuzzyScorerCache): IItemScore { if (!item || !query.normalized) { @@ -460,20 +459,33 @@ function doScoreItemFuzzyMultiple(label: string, description: string | undefined function doScoreItemFuzzySingle(label: string, description: string | undefined, path: string | undefined, query: IPreparedQueryPiece, preferLabelMatches: boolean, fuzzy: boolean): IItemScore { - // Prefer label matches if told so - if (preferLabelMatches) { - - // Treat prefix matches on the label highest - const prefixLabelMatchIgnoreCase = matchesPrefix(query.normalized, label); - if (prefixLabelMatchIgnoreCase) { - const prefixLabelMatchStrictCase = matchesStrictPrefix(query.normalized, label); - return { score: prefixLabelMatchStrictCase ? LABEL_PREFIX_SCORE_MATCHCASE : LABEL_PREFIX_SCORE_IGNORECASE, labelMatch: prefixLabelMatchStrictCase || prefixLabelMatchIgnoreCase }; - } - - // Second, score fuzzy + // Prefer label matches if told so or we have no description + if (preferLabelMatches || !description) { const [labelScore, labelPositions] = scoreFuzzy(label, query.normalized, query.normalizedLowercase, fuzzy); if (labelScore) { - return { score: labelScore + LABEL_SCORE_THRESHOLD, labelMatch: createMatches(labelPositions) }; + + // If we have a prefix match on the label, we give a much + // higher baseScore to elevate these matches over others + // This ensures that typing a file name wins over results + // that are present somewhere in the label, but not the + // beginning. + const labelPrefixMatch = matchesPrefix(query.normalized, label); + let baseScore: number; + if (labelPrefixMatch) { + baseScore = LABEL_PREFIX_SCORE_THRESHOLD; + + // We give another boost to labels that are short, e.g. given + // files "window.ts" and "windowActions.ts" and a query of + // "window", we want "window.ts" to receive a higher score. + // As such we compute the percentage the query has within the + // label and add that to the baseScore. + const prefixLengthBoost = Math.round((query.normalized.length / label.length) * 100); + baseScore += prefixLengthBoost; + } else { + baseScore = LABEL_SCORE_THRESHOLD; + } + + return { score: baseScore + labelScore, labelMatch: labelPrefixMatch || createMatches(labelPositions) }; } } @@ -600,46 +612,19 @@ export function compareItemsByFuzzyScore(itemA: T, itemB: T, query: IPrepared } } - // 2.) prefer label prefix matches (match case) - if (scoreA === LABEL_PREFIX_SCORE_MATCHCASE || scoreB === LABEL_PREFIX_SCORE_MATCHCASE) { - if (scoreA !== scoreB) { - return scoreA === LABEL_PREFIX_SCORE_MATCHCASE ? -1 : 1; - } - - const labelA = accessor.getItemLabel(itemA) || ''; - const labelB = accessor.getItemLabel(itemB) || ''; - - // prefer shorter names when both match on label prefix - if (labelA.length !== labelB.length) { - return labelA.length - labelB.length; - } - } - - // 3.) prefer label prefix matches (ignore case) - if (scoreA === LABEL_PREFIX_SCORE_IGNORECASE || scoreB === LABEL_PREFIX_SCORE_IGNORECASE) { - if (scoreA !== scoreB) { - return scoreA === LABEL_PREFIX_SCORE_IGNORECASE ? -1 : 1; - } - - const labelA = accessor.getItemLabel(itemA) || ''; - const labelB = accessor.getItemLabel(itemB) || ''; - - // prefer shorter names when both match on label prefix - if (labelA.length !== labelB.length) { - return labelA.length - labelB.length; - } - } - - // 4.) matches on label are considered higher compared to label+description matches + // 2.) matches on label are considered higher compared to label+description matches if (scoreA > LABEL_SCORE_THRESHOLD || scoreB > LABEL_SCORE_THRESHOLD) { if (scoreA !== scoreB) { return scoreA > scoreB ? -1 : 1; } - // prefer more compact matches over longer in label - const comparedByMatchLength = compareByMatchLength(itemScoreA.labelMatch, itemScoreB.labelMatch); - if (comparedByMatchLength !== 0) { - return comparedByMatchLength; + // prefer more compact matches over longer in label (unless this is a prefix match where + // longer prefix matches are actually preferred) + if (scoreA < LABEL_PREFIX_SCORE_THRESHOLD && scoreB < LABEL_PREFIX_SCORE_THRESHOLD) { + const comparedByMatchLength = compareByMatchLength(itemScoreA.labelMatch, itemScoreB.labelMatch); + if (comparedByMatchLength !== 0) { + return comparedByMatchLength; + } } // prefer shorter labels over longer labels @@ -650,12 +635,12 @@ export function compareItemsByFuzzyScore(itemA: T, itemB: T, query: IPrepared } } - // 5.) compare by score in label+description + // 3.) compare by score in label+description if (scoreA !== scoreB) { return scoreA > scoreB ? -1 : 1; } - // 6.) scores are identical: prefer matches in label over non-label matches + // 4.) scores are identical: prefer matches in label over non-label matches const itemAHasLabelMatches = Array.isArray(itemScoreA.labelMatch) && itemScoreA.labelMatch.length > 0; const itemBHasLabelMatches = Array.isArray(itemScoreB.labelMatch) && itemScoreB.labelMatch.length > 0; if (itemAHasLabelMatches && !itemBHasLabelMatches) { @@ -664,14 +649,14 @@ export function compareItemsByFuzzyScore(itemA: T, itemB: T, query: IPrepared return 1; } - // 7.) scores are identical: prefer more compact matches (label and description) + // 5.) scores are identical: prefer more compact matches (label and description) const itemAMatchDistance = computeLabelAndDescriptionMatchDistance(itemA, itemScoreA, accessor); const itemBMatchDistance = computeLabelAndDescriptionMatchDistance(itemB, itemScoreB, accessor); if (itemAMatchDistance && itemBMatchDistance && itemAMatchDistance !== itemBMatchDistance) { return itemBMatchDistance > itemAMatchDistance ? -1 : 1; } - // 8.) scores are identical: start to use the fallback compare + // 6.) scores are identical: start to use the fallback compare return fallbackCompare(itemA, itemB, query, accessor); } diff --git a/src/vs/base/common/htmlContent.ts b/src/vs/base/common/htmlContent.ts index 5bc4fc6bf4..c3ba696958 100644 --- a/src/vs/base/common/htmlContent.ts +++ b/src/vs/base/common/htmlContent.ts @@ -45,7 +45,7 @@ export class MarkdownString implements IMarkdownString { // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash this._value += (this._supportThemeIcons ? escapeCodicons(value) : value) .replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&') - .replace('\n', '\n\n'); + .replace(/\n/g, '\n\n'); return this; } diff --git a/src/vs/base/common/marshalling.ts b/src/vs/base/common/marshalling.ts index d5267bff98..13fc1746b1 100644 --- a/src/vs/base/common/marshalling.ts +++ b/src/vs/base/common/marshalling.ts @@ -5,7 +5,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { regExpFlags } from 'vs/base/common/strings'; -import { URI } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; export function stringify(obj: any): string { return JSON.stringify(obj, replacer); @@ -33,7 +33,15 @@ function replacer(key: string, value: any): any { return value; } -export function revive(obj: any, depth = 0): any { + +type Deserialize = T extends UriComponents ? URI + : T extends object + ? Revived + : T; + +export type Revived = { [K in keyof T]: Deserialize }; + +export function revive(obj: any, depth = 0): Revived { if (!obj || depth > 200) { return obj; } @@ -41,15 +49,15 @@ export function revive(obj: any, depth = 0): any { if (typeof obj === 'object') { switch ((obj).$mid) { - case 1: return URI.revive(obj); - case 2: return new RegExp(obj.source, obj.flags); + case 1: return URI.revive(obj); + case 2: return new RegExp(obj.source, obj.flags); } if ( obj instanceof VSBuffer || obj instanceof Uint8Array ) { - return obj; + return obj; } if (Array.isArray(obj)) { diff --git a/src/vs/base/common/stream.ts b/src/vs/base/common/stream.ts index d9a90ef5d9..27ac18101a 100644 --- a/src/vs/base/common/stream.ts +++ b/src/vs/base/common/stream.ts @@ -33,7 +33,7 @@ export interface ReadableStreamEvents { /** * A interface that emulates the API shape of a node.js readable - * stream for use in desktop and web environments. + * stream for use in native and web environments. */ export interface ReadableStream extends ReadableStreamEvents { @@ -60,7 +60,7 @@ export interface ReadableStream extends ReadableStreamEvents { /** * A interface that emulates the API shape of a node.js readable - * for use in desktop and web environments. + * for use in native and web environments. */ export interface Readable { @@ -73,7 +73,7 @@ export interface Readable { /** * A interface that emulates the API shape of a node.js writeable - * stream for use in desktop and web environments. + * stream for use in native and web environments. */ export interface WriteableStream extends ReadableStream { diff --git a/src/vs/base/common/types.ts b/src/vs/base/common/types.ts index acd21d9eea..ecb2dfd54b 100644 --- a/src/vs/base/common/types.ts +++ b/src/vs/base/common/types.ts @@ -258,14 +258,12 @@ export type UriDto = { [K in keyof T]: T[K] extends URI /** * Mapped-type that replaces all occurrences of URI with UriComponents and * drops all functions. - * todo@joh use toJSON-results */ -export type Dto = { [K in keyof T]: T[K] extends URI - ? UriComponents - : T[K] extends Function - ? never - : UriDto }; - +export type Dto = T extends { toJSON(): infer U } + ? U + : T extends object + ? { [k in keyof T]: Dto; } + : T; export function NotImplementedProxy(name: string): { new(): T } { return class { diff --git a/src/vs/base/parts/sandbox/electron-browser/preload.js b/src/vs/base/parts/sandbox/electron-browser/preload.js index 1e24797783..e9a88ca2fe 100644 --- a/src/vs/base/parts/sandbox/electron-browser/preload.js +++ b/src/vs/base/parts/sandbox/electron-browser/preload.js @@ -93,6 +93,14 @@ process: { platform: process.platform, env: process.env, + _whenEnvResolved: undefined, + get whenEnvResolved() { + if (!this._whenEnvResolved) { + this._whenEnvResolved = resolveEnv(); + } + + return this._whenEnvResolved; + }, on: /** * @param {string} type @@ -157,5 +165,33 @@ return true; } + /** + * If VSCode is not run from a terminal, we should resolve additional + * shell specific environment from the OS shell to ensure we are seeing + * all development related environment variables. We do this from the + * main process because it may involve spawning a shell. + */ + function resolveEnv() { + return new Promise(function (resolve) { + const handle = setTimeout(function () { + console.warn('Preload: Unable to resolve shell environment in a reasonable time'); + + // It took too long to fetch the shell environment, return + resolve(); + }, 3000); + + ipcRenderer.once('vscode:acceptShellEnv', function (event, shellEnv) { + clearTimeout(handle); + + // Assign all keys of the shell environment to our process environment + Object.assign(process.env, shellEnv); + + resolve(); + }); + + ipcRenderer.send('vscode:fetchShellEnv'); + }); + } + //#endregion }()); diff --git a/src/vs/base/parts/sandbox/electron-sandbox/globals.ts b/src/vs/base/parts/sandbox/electron-sandbox/globals.ts index e3ccd99dc5..5577f7d37d 100644 --- a/src/vs/base/parts/sandbox/electron-sandbox/globals.ts +++ b/src/vs/base/parts/sandbox/electron-sandbox/globals.ts @@ -84,6 +84,12 @@ export const process = (window as any).vscode.process as { */ env: { [key: string]: string | undefined }; + /** + * Allows to await resolving the full process environment by checking for the shell environment + * of the OS in certain cases (e.g. when the app is started from the Dock on macOS). + */ + whenEnvResolved: Promise; + /** * A listener on the process. Only a small subset of listener types are allowed. */ diff --git a/src/vs/base/parts/storage/node/storage.ts b/src/vs/base/parts/storage/node/storage.ts index 02c76ec63a..423915ce26 100644 --- a/src/vs/base/parts/storage/node/storage.ts +++ b/src/vs/base/parts/storage/node/storage.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Database, Statement } from 'vscode-sqlite3'; +import type { Database, Statement } from 'vscode-sqlite3'; import { Event } from 'vs/base/common/event'; import { timeout } from 'vs/base/common/async'; import { mapToString, setToString } from 'vs/base/common/map'; diff --git a/src/vs/base/test/browser/codicons.test.ts b/src/vs/base/test/browser/codicons.test.ts new file mode 100644 index 0000000000..6ef429afd8 --- /dev/null +++ b/src/vs/base/test/browser/codicons.test.ts @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { renderCodiconsAsElement } from 'vs/base/browser/codicons'; +import * as assert from 'assert'; + +suite('renderCodicons', () => { + + test('no codicons', () => { + const result = renderCodiconsAsElement(' hello World .'); + + assert.equal(elementsToString(result), ' hello World .'); + }); + + test('codicon only', () => { + const result = renderCodiconsAsElement('$(alert)'); + + assert.equal(elementsToString(result), ''); + }); + + test('codicon and non-codicon strings', () => { + const result = renderCodiconsAsElement(` $(alert) Unresponsive`); + + assert.equal(elementsToString(result), ' Unresponsive'); + }); + + test('multiple codicons', () => { + const result = renderCodiconsAsElement('$(check)$(error)'); + + assert.equal(elementsToString(result), ''); + }); + + test('escaped codicon', () => { + const result = renderCodiconsAsElement('\\$(escaped)'); + + assert.equal(elementsToString(result), '$(escaped)'); + }); + + test('codicon with animation', () => { + const result = renderCodiconsAsElement('$(zip~anim)'); + + assert.equal(elementsToString(result), ''); + }); + + const elementsToString = (elements: Array): string => { + return elements + .map(elem => elem instanceof HTMLElement ? elem.outerHTML : elem) + .reduce((a, b) => a + b, ''); + }; +}); diff --git a/src/vs/base/test/common/async.test.ts b/src/vs/base/test/common/async.test.ts index 72f017f264..5c4879cc5a 100644 --- a/src/vs/base/test/common/async.test.ts +++ b/src/vs/base/test/common/async.test.ts @@ -688,4 +688,22 @@ suite('Async', () => { assert.ok(Date.now() - now < 100); assert.equal(timedout, false); }); + + test('SequencerByKey', async () => { + const s = new async.SequencerByKey(); + + const r1 = await s.queue('key1', () => Promise.resolve('hello')); + assert.equal(r1, 'hello'); + + await s.queue('key2', () => Promise.reject(new Error('failed'))).then(() => { + throw new Error('should not be resolved'); + }, err => { + // Expected error + assert.equal(err.message, 'failed'); + }); + + // Still works after a queued promise is rejected + const r3 = await s.queue('key2', () => Promise.resolve('hello')); + assert.equal(r3, 'hello'); + }); }); diff --git a/src/vs/base/test/common/extpath.test.ts b/src/vs/base/test/common/extpath.test.ts index 385e421e37..c295eebce0 100644 --- a/src/vs/base/test/common/extpath.test.ts +++ b/src/vs/base/test/common/extpath.test.ts @@ -57,6 +57,13 @@ suite('Paths', () => { assert.ok(!extpath.isValidBasename('aux')); assert.ok(!extpath.isValidBasename('Aux')); assert.ok(!extpath.isValidBasename('LPT0')); + assert.ok(!extpath.isValidBasename('aux.txt')); + assert.ok(!extpath.isValidBasename('com0.abc')); + assert.ok(extpath.isValidBasename('LPT00')); + assert.ok(extpath.isValidBasename('aux1')); + assert.ok(extpath.isValidBasename('aux1.txt')); + assert.ok(extpath.isValidBasename('aux1.aux.txt')); + assert.ok(!extpath.isValidBasename('test.txt.')); assert.ok(!extpath.isValidBasename('test.txt..')); assert.ok(!extpath.isValidBasename('test.txt ')); diff --git a/src/vs/base/test/common/fuzzyScorer.test.ts b/src/vs/base/test/common/fuzzyScorer.test.ts index fb6e4a9356..e9dffd3edd 100644 --- a/src/vs/base/test/common/fuzzyScorer.test.ts +++ b/src/vs/base/test/common/fuzzyScorer.test.ts @@ -1025,6 +1025,51 @@ suite('Fuzzy Scorer', () => { assert.equal(res[1], resourceB); }); + test('compareFilesByScore - boost better prefix match if multiple queries are used', function () { + const resourceA = URI.file('src/vs/workbench/services/host/browser/browserHostService.ts'); + const resourceB = URI.file('src/vs/workbench/browser/workbench.ts'); + + for (const query of ['workbench.ts browser', 'browser workbench.ts', 'browser workbench', 'workbench browser']) { + let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceB); + assert.equal(res[1], resourceA); + + res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceB); + assert.equal(res[1], resourceA); + } + }); + + test('compareFilesByScore - boost shorter prefix match if multiple queries are used', function () { + const resourceA = URI.file('src/vs/workbench/browser/actions/windowActions.ts'); + const resourceB = URI.file('src/vs/workbench/electron-browser/window.ts'); + + for (const query of ['window browser', 'window.ts browser']) { + let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceB); + assert.equal(res[1], resourceA); + + res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceB); + assert.equal(res[1], resourceA); + } + }); + + test('compareFilesByScore - boost shorter prefix match if multiple queries are used (#99171)', function () { + const resourceA = URI.file('mesh_editor_lifetime_job.h'); + const resourceB = URI.file('lifetime_job.h'); + + for (const query of ['m life, life m']) { + let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceB); + assert.equal(res[1], resourceA); + + res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceB); + assert.equal(res[1], resourceA); + } + }); + test('prepareQuery', () => { assert.equal(scorer.prepareQuery(' f*a ').normalized, 'fa'); assert.equal(scorer.prepareQuery('model Tester.ts').original, 'model Tester.ts'); diff --git a/src/vs/code/browser/workbench/workbench-dev.html b/src/vs/code/browser/workbench/workbench-dev.html index 21fc8bc7dd..62d519eaee 100644 --- a/src/vs/code/browser/workbench/workbench-dev.html +++ b/src/vs/code/browser/workbench/workbench-dev.html @@ -17,6 +17,9 @@ + + + diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts index 74a0723359..4b97402366 100644 --- a/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts @@ -3,8 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IWorkbenchConstructionOptions, create, ICredentialsProvider, IURLCallbackProvider, IWorkspaceProvider, IWorkspace, IWindowIndicator, ICommand, IHomeIndicator, IProductQualityChangeHandler } from 'vs/workbench/workbench.web.api'; -import product from 'vs/platform/product/common/product'; +import { IWorkbenchConstructionOptions, create, ICredentialsProvider, IURLCallbackProvider, IWorkspaceProvider, IWorkspace, IWindowIndicator, IHomeIndicator, IProductQualityChangeHandler } from 'vs/workbench/workbench.web.api'; import { URI, UriComponents } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { generateUuid } from 'vs/base/common/uuid'; @@ -16,6 +15,7 @@ import { isFolderToOpen, isWorkspaceToOpen } from 'vs/platform/windows/common/wi import { isEqual } from 'vs/base/common/resources'; import { isStandalone } from 'vs/base/browser/browser'; import { localize } from 'vs/nls'; +import { Schemas } from 'vs/base/common/network'; interface ICredential { service: string; @@ -27,6 +27,13 @@ class LocalStorageCredentialsProvider implements ICredentialsProvider { static readonly CREDENTIALS_OPENED_KEY = 'credentials.provider'; + constructor(credentials: ICredential[]) { + this._credentials = credentials; + for (const { service, account, password } of this._credentials) { + this.setPassword(service, account, password); + } + } + private _credentials: ICredential[] | undefined; private get credentials(): ICredential[] { if (!this._credentials) { @@ -277,6 +284,20 @@ class WorkspaceProvider implements IWorkspaceProvider { return false; } + + hasRemote(): boolean { + if (this.workspace) { + if (isFolderToOpen(this.workspace)) { + return this.workspace.folderUri.scheme === Schemas.vscodeRemote; + } + + if (isWorkspaceToOpen(this.workspace)) { + return this.workspace.workspaceUri.scheme === Schemas.vscodeRemote; + } + } + + return true; + } } class WindowIndicator implements IWindowIndicator { @@ -287,8 +308,6 @@ class WindowIndicator implements IWindowIndicator { readonly tooltip: string; readonly command: string | undefined; - readonly commandImpl: ICommand | undefined = undefined; - constructor(workspace: IWorkspace) { let repositoryOwner: string | undefined = undefined; let repositoryName: string | undefined = undefined; @@ -306,20 +325,16 @@ class WindowIndicator implements IWindowIndicator { } } + // Repo if (repositoryName && repositoryOwner) { - this.label = localize('openInDesktopLabel', "$(remote) Open in Desktop"); - this.tooltip = localize('openInDesktopTooltip', "Open in Desktop"); - this.command = '_web.openInDesktop'; - this.commandImpl = { - id: this.command, - handler: () => { - const protocol = product.quality === 'stable' ? 'vscode' : 'vscode-insiders'; - window.open(`${protocol}://vscode.git/clone?url=${encodeURIComponent(`https://github.com/${repositoryOwner}/${repositoryName}.git`)}`); - } - }; - } else { - this.label = localize('playgroundLabel', "Web Playground"); - this.tooltip = this.label; + this.label = localize('playgroundLabelRepository', "$(remote) VS Code Web Playground: {0}/{1}", repositoryOwner, repositoryName); + this.tooltip = localize('playgroundRepositoryTooltip', "VS Code Web Playground: {0}/{1}", repositoryOwner, repositoryName); + } + + // No Repo + else { + this.label = localize('playgroundLabel', "$(remote) VS Code Web Playground"); + this.tooltip = localize('playgroundTooltip', "VS Code Web Playground"); } } } @@ -391,6 +406,9 @@ class WindowIndicator implements IWindowIndicator { } } + // Workspace Provider + const workspaceProvider = new WorkspaceProvider(workspace, payload); + // Home Indicator const homeIndicator: IHomeIndicator = { href: 'https://github.com/Microsoft/vscode', @@ -398,13 +416,10 @@ class WindowIndicator implements IWindowIndicator { title: localize('home', "Home") }; - // Commands - const commands: ICommand[] = []; - - // Window indicator - const windowIndicator = new WindowIndicator(workspace); - if (windowIndicator.commandImpl) { - commands.push(windowIndicator.commandImpl); + // Window indicator (unless connected to a remote) + let windowIndicator: WindowIndicator | undefined = undefined; + if (!workspaceProvider.hasRemote()) { + windowIndicator = new WindowIndicator(workspace); } // Product Quality Change Handler @@ -422,15 +437,19 @@ class WindowIndicator implements IWindowIndicator { window.location.href = `${window.location.origin}?${queryString}`; }; + // Find credentials from DOM + const credentialsElement = document.getElementById('vscode-workbench-credentials'); + const credentialsElementAttribute = credentialsElement ? credentialsElement.getAttribute('data-settings') : undefined; + const credentialsProvider = new LocalStorageCredentialsProvider(credentialsElementAttribute ? JSON.parse(credentialsElementAttribute) : []); + // Finally create workbench create(document.body, { ...config, homeIndicator, - commands, windowIndicator, productQualityChangeHandler, - workspaceProvider: new WorkspaceProvider(workspace, payload), + workspaceProvider, urlCallbackProvider: new PollingURLCallbackProvider(), - credentialsProvider: new LocalStorageCredentialsProvider() + credentialsProvider }); })(); diff --git a/src/vs/code/electron-browser/workbench/workbench.js b/src/vs/code/electron-browser/workbench/workbench.js index 1a190b1046..0da0faef2e 100644 --- a/src/vs/code/electron-browser/workbench/workbench.js +++ b/src/vs/code/electron-browser/workbench/workbench.js @@ -33,8 +33,8 @@ const bootstrapWindow = (() => { return window.MonacoBootstrapWindow; })(); -// Setup shell environment -process['lazyEnv'] = getLazyEnv(); +// Load environment in parallel to workbench loading to avoid waterfall +const whenEnvResolved = bootstrapWindow.globals().process.whenEnvResolved; // Load workbench main JS, CSS and NLS all in parallel. This is an // optimization to prevent a waterfall of loading to happen, because @@ -46,18 +46,19 @@ bootstrapWindow.load([ 'vs/nls!vs/workbench/workbench.desktop.main', 'vs/css!vs/workbench/workbench.desktop.main' ], - function (workbench, configuration) { + async function (workbench, configuration) { // Mark start of workbench perf.mark('didLoadWorkbenchMain'); performance.mark('workbench-start'); - return process['lazyEnv'].then(function () { - perf.mark('main/startup'); + // Wait for process environment being fully resolved + await whenEnvResolved; - // @ts-ignore - return require('vs/workbench/electron-browser/desktop.main').main(configuration); - }); + perf.mark('main/startup'); + + // @ts-ignore + return require('vs/workbench/electron-browser/desktop.main').main(configuration); }, { removeDeveloperKeybindingsAfterLoad: true, @@ -77,7 +78,7 @@ bootstrapWindow.load([ * @param {{ * partsSplashPath?: string, * highContrast?: boolean, - * defaultThemeType?: string, + * autoDetectHighContrast?: boolean, * extensionDevelopmentPath?: string[], * folderUri?: object, * workspace?: object @@ -96,7 +97,8 @@ function showPartsSplash(configuration) { } // high contrast mode has been turned on from the outside, e.g. OS -> ignore stored colors and layouts - if (data && configuration.highContrast && data.baseTheme !== 'hc-black') { + const isHighContrast = configuration.highContrast && configuration.autoDetectHighContrast; + if (data && isHighContrast && data.baseTheme !== 'hc-black') { data = undefined; } @@ -111,14 +113,10 @@ function showPartsSplash(configuration) { baseTheme = data.baseTheme; shellBackground = data.colorInfo.editorBackground; shellForeground = data.colorInfo.foreground; - } else if (configuration.highContrast || configuration.defaultThemeType === 'hc') { + } else if (isHighContrast) { baseTheme = 'hc-black'; shellBackground = '#000000'; shellForeground = '#FFFFFF'; - } else if (configuration.defaultThemeType === 'vs') { - baseTheme = 'vs'; - shellBackground = '#FFFFFF'; - shellForeground = '#000000'; } else { baseTheme = 'vs-dark'; shellBackground = '#1E1E1E'; @@ -172,26 +170,3 @@ function showPartsSplash(configuration) { perf.mark('didShowPartsSplash'); } - -/** - * @returns {Promise} - */ -function getLazyEnv() { - const ipcRenderer = bootstrapWindow.globals().ipcRenderer; - - return new Promise(function (resolve) { - const handle = setTimeout(function () { - resolve(); - console.warn('renderer did not receive lazyEnv in time'); - }, 10000); - - ipcRenderer.once('vscode:acceptShellEnv', function (event, shellEnv) { - clearTimeout(handle); - Object.assign(process.env, shellEnv); - // @ts-ignore - resolve(process.env); - }); - - ipcRenderer.send('vscode:fetchShellEnv'); - }); -} diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index bff190cb36..bc9b2563ee 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -50,7 +50,7 @@ import { setUnexpectedErrorHandler, onUnexpectedError } from 'vs/base/common/err import { ElectronURLListener } from 'vs/platform/url/electron-main/electronUrlListener'; import { serve as serveDriver } from 'vs/platform/driver/electron-main/driver'; import { IMenubarMainService, MenubarMainService } from 'vs/platform/menubar/electron-main/menubarMainService'; -import { RunOnceScheduler } from 'vs/base/common/async'; +import { RunOnceScheduler, timeout } from 'vs/base/common/async'; import { registerContextMenuListener } from 'vs/base/parts/contextmenu/electron-main/contextmenu'; import { homedir } from 'os'; import { join, sep, posix } from 'vs/base/common/path'; @@ -68,11 +68,11 @@ import { statSync } from 'fs'; import { DiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsIpc'; import { IDiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsService'; import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc'; +import { ElectronExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/electron-main/extensionHostDebugIpc'; import { IElectronMainService, ElectronMainService } from 'vs/platform/electron/electron-main/electronMainService'; import { ISharedProcessMainService, SharedProcessMainService } from 'vs/platform/ipc/electron-main/sharedProcessMainService'; import { IDialogMainService, DialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; import { withNullAsUndefined } from 'vs/base/common/types'; -import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv'; import { coalesce } from 'vs/base/common/arrays'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { StorageKeysSyncRegistryChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc'; @@ -80,8 +80,6 @@ import { INativeEnvironmentService } from 'vs/platform/environment/node/environm import { mnemonicButtonLabel, getPathLabel } from 'vs/base/common/labels'; import { WebviewMainService } from 'vs/platform/webview/electron-main/webviewMainService'; import { IWebviewManagerService } from 'vs/platform/webview/common/webviewManagerService'; -import { createServer, AddressInfo } from 'net'; -import { IOpenExtensionWindowResult } from 'vs/platform/debug/common/extensionHostDebug'; import { IFileService } from 'vs/platform/files/common/files'; import { stripComments } from 'vs/base/common/json'; import { generateUuid } from 'vs/base/common/uuid'; @@ -272,6 +270,12 @@ export class CodeApplication extends Disposable { try { const shellEnv = await getShellEnvironment(this.logService, this.environmentService); + + // TODO@sandbox workaround for https://github.com/electron/electron/issues/25119 + if (this.environmentService.sandbox) { + await timeout(100); + } + if (!webContents.isDestroyed()) { webContents.send('vscode:acceptShellEnv', shellEnv); } @@ -297,7 +301,7 @@ export class CodeApplication extends Disposable { const nativeKeymap = await import('native-keymap'); nativeKeymap.onDidChangeKeyboardLayout(() => { if (this.windowsMainService) { - this.windowsMainService.sendToAll('vscode:keyboardLayoutChanged', false); + this.windowsMainService.sendToAll('vscode:keyboardLayoutChanged'); } }); })(); @@ -364,17 +368,17 @@ export class CodeApplication extends Disposable { const sharedProcess = this.instantiationService.createInstance(SharedProcess, machineId, this.userEnv); const sharedProcessClient = sharedProcess.whenIpcReady().then(() => { this.logService.trace('Shared process: IPC ready'); + return connect(this.environmentService.sharedIPCHandle, 'main'); }); const sharedProcessReady = sharedProcess.whenReady().then(() => { this.logService.trace('Shared process: init ready'); + return sharedProcessClient; }); this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen).then(() => { this._register(new RunOnceScheduler(async () => { - const userEnv = await getShellEnvironment(this.logService, this.environmentService); - - sharedProcess.spawn(userEnv); + sharedProcess.spawn(await getShellEnvironment(this.logService, this.environmentService)); }, 3000)).schedule(); }); @@ -847,6 +851,9 @@ export class CodeApplication extends Disposable { } catch (error) { this.logService.error(error); } + + // Start to fetch shell environment after window has opened + getShellEnvironment(this.logService, this.environmentService); } private handleRemoteAuthorities(): void { @@ -858,100 +865,3 @@ export class CodeApplication extends Disposable { }); } } - -class ElectronExtensionHostDebugBroadcastChannel extends ExtensionHostDebugBroadcastChannel { - - constructor(private windowsMainService: IWindowsMainService) { - super(); - } - - call(ctx: TContext, command: string, arg?: any): Promise { - if (command === 'openExtensionDevelopmentHostWindow') { - return this.openExtensionDevelopmentHostWindow(arg[0], arg[1], arg[2]); - } else { - return super.call(ctx, command, arg); - } - } - - private async openExtensionDevelopmentHostWindow(args: string[], env: IProcessEnvironment, debugRenderer: boolean): Promise { - const pargs = parseArgs(args, OPTIONS); - const extDevPaths = pargs.extensionDevelopmentPath; - if (!extDevPaths) { - return {}; - } - - const [codeWindow] = this.windowsMainService.openExtensionDevelopmentHostWindow(extDevPaths, { - context: OpenContext.API, - cli: pargs, - userEnv: Object.keys(env).length > 0 ? env : undefined - }); - - if (!debugRenderer) { - return {}; - } - - const debug = codeWindow.win.webContents.debugger; - - let listeners = debug.isAttached() ? Infinity : 0; - const server = createServer(listener => { - if (listeners++ === 0) { - debug.attach(); - } - - let closed = false; - const writeMessage = (message: object) => { - if (!closed) { // in case sendCommand promises settle after closed - listener.write(JSON.stringify(message) + '\0'); // null-delimited, CDP-compatible - } - }; - - const onMessage = (_event: Event, method: string, params: unknown, sessionId?: string) => - writeMessage(({ method, params, sessionId })); - - codeWindow.win.on('close', () => { - debug.removeListener('message', onMessage); - listener.end(); - closed = true; - }); - - debug.addListener('message', onMessage); - - let buf = Buffer.alloc(0); - listener.on('data', data => { - buf = Buffer.concat([buf, data]); - for (let delimiter = buf.indexOf(0); delimiter !== -1; delimiter = buf.indexOf(0)) { - let data: { id: number; sessionId: string; params: {} }; - try { - const contents = buf.slice(0, delimiter).toString('utf8'); - buf = buf.slice(delimiter + 1); - data = JSON.parse(contents); - } catch (e) { - console.error('error reading cdp line', e); - } - - // depends on a new API for which electron.d.ts has not been updated: - // @ts-ignore - debug.sendCommand(data.method, data.params, data.sessionId) - .then((result: object) => writeMessage({ id: data.id, sessionId: data.sessionId, result })) - .catch((error: Error) => writeMessage({ id: data.id, sessionId: data.sessionId, error: { code: 0, message: error.message } })); - } - }); - - listener.on('error', err => { - console.error('error on cdp pipe:', err); - }); - - listener.on('close', () => { - closed = true; - if (--listeners === 0) { - debug.detach(); - } - }); - }); - - await new Promise(r => server.listen(0, r)); - codeWindow.win.on('close', () => server.close()); - - return { rendererDebugPort: (server.address() as AddressInfo).port }; - } -} diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index 48996b2012..68a5cf6b76 100644 --- a/src/vs/code/electron-main/window.ts +++ b/src/vs/code/electron-main/window.ts @@ -8,7 +8,7 @@ import * as objects from 'vs/base/common/objects'; import * as nls from 'vs/nls'; import { Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; -import { screen, BrowserWindow, systemPreferences, app, TouchBar, nativeImage, Rectangle, Display, TouchBarSegmentedControl, NativeImage, BrowserWindowConstructorOptions, SegmentedControlSegment, nativeTheme, Event } from 'electron'; +import { screen, BrowserWindow, systemPreferences, app, TouchBar, nativeImage, Rectangle, Display, TouchBarSegmentedControl, NativeImage, BrowserWindowConstructorOptions, SegmentedControlSegment, nativeTheme, Event, Details } from 'electron'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { INativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; import { ILogService } from 'vs/platform/log/common/log'; @@ -167,12 +167,23 @@ export class CodeWindow extends Disposable implements ICodeWindow { title: product.nameLong, webPreferences: { preload: URI.parse(this.doGetPreloadUrl()).fsPath, - nodeIntegration: true, enableWebSQL: false, enableRemoteModule: false, nativeWindowOpen: true, webviewTag: true, - zoomFactor: zoomLevelToZoomFactor(windowConfig?.zoomLevel) + zoomFactor: zoomLevelToZoomFactor(windowConfig?.zoomLevel), + ...this.environmentService.sandbox ? + + // Sandbox + { + sandbox: true, + contextIsolation: true + } : + + // No Sandbox + { + nodeIntegration: true + } } }; @@ -325,7 +336,18 @@ export class CodeWindow extends Disposable implements ICodeWindow { return !!this.documentEdited; } - focus(): void { + focus(options?: { force: boolean }): void { + // macOS: Electron >6.x changed its behaviour to not + // bring the application to the foreground when a window + // is focused programmatically. Only via `app.focus` and + // the option `steal: true` can you get the previous + // behaviour back. The only reason to use this option is + // when a window is getting focused while the application + // is not in the foreground. + if (isMacintosh && options?.force) { + app.focus({ steal: true }); + } + if (!this._win) { return; } @@ -392,7 +414,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { private registerListeners(): void { // Crashes & Unrsponsive - this._win.webContents.on('crashed', () => this.onWindowError(WindowError.CRASHED)); + this._win.webContents.on('render-process-gone', (event, details) => this.onWindowError(WindowError.CRASHED, details)); this._win.on('unresponsive', () => this.onWindowError(WindowError.UNRESPONSIVE)); // Window close @@ -524,8 +546,10 @@ export class CodeWindow extends Disposable implements ICodeWindow { this.marketplaceHeadersPromise.then(headers => cb({ cancel: false, requestHeaders: Object.assign(details.requestHeaders, headers) }))); } - private onWindowError(error: WindowError): void { - this.logService.error(error === WindowError.CRASHED ? '[VS Code]: renderer process crashed!' : '[VS Code]: detected unresponsive'); + private onWindowError(error: WindowError.UNRESPONSIVE): void; + private onWindowError(error: WindowError.CRASHED, details: Details): void; + private onWindowError(error: WindowError, details?: Details): void { + this.logService.error(error === WindowError.CRASHED ? `[VS Code]: renderer process crashed (detail: ${details?.reason})` : '[VS Code]: detected unresponsive'); // If we run extension tests from CLI, showing a dialog is not // very helpful in this case. Rather, we bring down the test run @@ -579,11 +603,18 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Crashed else { + let message: string; + if (details && details.reason !== 'crashed') { + message = nls.localize('appCrashedDetails', "The window has crashed (reason: '{0}')", details?.reason); + } else { + message = nls.localize('appCrashed', "The window has crashed", details?.reason); + } + this.dialogMainService.showMessageBox({ title: product.nameLong, type: 'warning', buttons: [mnemonicButtonLabel(nls.localize({ key: 'reopen', comment: ['&& denotes a mnemonic'] }, "&&Reopen")), mnemonicButtonLabel(nls.localize({ key: 'close', comment: ['&& denotes a mnemonic'] }, "&&Close"))], - message: nls.localize('appCrashed', "The window has crashed"), + message, detail: nls.localize('appCrashedDetail', "We are sorry for the inconvenience! You can reopen the window to continue where you left off."), noLink: true }, this._win).then(result => { @@ -699,7 +730,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { this.showTimeoutHandle = setTimeout(() => { if (this._win && !this._win.isVisible() && !this._win.isMinimized()) { this._win.show(); - this._win.focus(); + this.focus({ force: true }); this._win.webContents.openDevTools(); } }, 10000); @@ -754,11 +785,8 @@ export class CodeWindow extends Disposable implements ICodeWindow { windowConfiguration.fullscreen = this.isFullScreen; // Set Accessibility Config - let autoDetectHighContrast = true; - if (windowConfig?.autoDetectHighContrast === false) { - autoDetectHighContrast = false; - } - windowConfiguration.highContrast = isWindows && autoDetectHighContrast && nativeTheme.shouldUseInvertedColorScheme; + windowConfiguration.highContrast = nativeTheme.shouldUseInvertedColorScheme || nativeTheme.shouldUseHighContrastColors; + windowConfiguration.autoDetectHighContrast = windowConfig?.autoDetectHighContrast ?? true; windowConfiguration.accessibilitySupport = app.accessibilitySupportEnabled; // Title style related @@ -799,7 +827,14 @@ export class CodeWindow extends Disposable implements ICodeWindow { } private doGetUrl(config: object): string { - return `${require.toUrl('vs/code/electron-browser/workbench/workbench.html')}?config=${encodeURIComponent(JSON.stringify(config))}`; + let workbench: string; + if (this.environmentService.sandbox) { + workbench = 'vs/code/electron-sandbox/workbench/workbench.html'; + } else { + workbench = 'vs/code/electron-browser/workbench/workbench.html'; + } + + return `${require.toUrl(workbench)}?config=${encodeURIComponent(JSON.stringify(config))}`; } private doGetPreloadUrl(): string { diff --git a/src/vs/code/electron-sandbox/issue/issueReporterMain.ts b/src/vs/code/electron-sandbox/issue/issueReporterMain.ts index b792de7379..0044a57b48 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporterMain.ts +++ b/src/vs/code/electron-sandbox/issue/issueReporterMain.ts @@ -8,7 +8,7 @@ import 'vs/base/browser/ui/codicons/codiconStyles'; // make sure codicon css is import { ElectronService, IElectronService } from 'vs/platform/electron/electron-sandbox/electron'; import { ipcRenderer, process } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { applyZoom, zoomIn, zoomOut } from 'vs/platform/windows/electron-sandbox/window'; -import { $, windowOpenNoOpener, addClass } from 'vs/base/browser/dom'; +import { $, reset, windowOpenNoOpener, addClass } from 'vs/base/browser/dom'; import { Button } from 'vs/base/browser/ui/button/button'; import { CodiconLabel } from 'vs/base/browser/ui/codicons/codiconLabel'; import * as collections from 'vs/base/common/collections'; @@ -277,33 +277,25 @@ export class IssueReporter extends Disposable { } private updateSettingsSearchDetails(data: ISettingsSearchIssueReporterData): void { - const target = document.querySelector('.block-settingsSearchResults .block-info'); + const target = document.querySelector('.block-settingsSearchResults .block-info'); if (target) { - const details = ` -
-
Query: "${data.query}"
-
Literal match count: ${data.filterResultCount}
-
- `; + const queryDiv = $('div', undefined, `Query: "${data.query}"` as string); + const countDiv = $('div', undefined, `Literal match count: ${data.filterResultCount}` as string); + const detailsDiv = $('.block-settingsSearchResults-details', undefined, queryDiv, countDiv); - let table = ` - - Setting - Extension - Score - `; - - data.actualSearchResults - .forEach(setting => { - table += ` - - ${setting.key} - ${setting.extensionId} - ${String(setting.score).slice(0, 5)} - `; - }); - - target.innerHTML = `${details}${table}
`; + const table = $('table', undefined, + $('tr', undefined, + $('th', undefined, 'Setting'), + $('th', undefined, 'Extension'), + $('th', undefined, 'Score'), + ), + ...data.actualSearchResults.map(setting => $('tr', undefined, + $('td', undefined, setting.key), + $('td', undefined, setting.extensionId), + $('td', undefined, String(setting.score).slice(0, 5)), + )) + ); + reset(target, detailsDiv, table); } } @@ -654,9 +646,9 @@ export class IssueReporter extends Disposable { issueState.appendChild(issueIcon); issueState.appendChild(issueStateLabel); - item = $('div.issue', {}, issueState, link); + item = $('div.issue', undefined, issueState, link); } else { - item = $('div.issue', {}, link); + item = $('div.issue', undefined, link); } issues.appendChild(item); @@ -672,19 +664,19 @@ export class IssueReporter extends Disposable { } private setUpTypes(): void { - const makeOption = (issueType: IssueType, description: string) => ``; + const makeOption = (issueType: IssueType, description: string) => $('option', { 'value': issueType.valueOf() }, escape(description)); const typeSelect = this.getElementById('issue-type')! as HTMLSelectElement; const { issueType } = this.issueReporterModel.getData(); if (issueType === IssueType.SettingsSearchIssue) { - typeSelect.innerHTML = makeOption(IssueType.SettingsSearchIssue, localize('settingsSearchIssue', "Settings Search Issue")); + reset(typeSelect, makeOption(IssueType.SettingsSearchIssue, localize('settingsSearchIssue', "Settings Search Issue"))); typeSelect.disabled = true; } else { - typeSelect.innerHTML = [ + reset(typeSelect, makeOption(IssueType.Bug, localize('bugReporter', "Bug Report")), makeOption(IssueType.FeatureRequest, localize('featureRequest', "Feature Request")), makeOption(IssueType.PerformanceIssue, localize('performanceIssue', "Performance Issue")) - ].join('\n'); + ); } typeSelect.value = issueType.toString(); @@ -774,9 +766,8 @@ export class IssueReporter extends Disposable { } else { show(extensionsBlock); } - - descriptionTitle.innerHTML = `${localize('stepsToReproduce', "Steps to Reproduce")} *`; - descriptionSubtitle.innerHTML = localize('bugDescription', "Share the steps needed to reliably reproduce the problem. Please include actual and expected results. We support GitHub-flavored Markdown. You will be able to edit your issue and add screenshots when we preview it on GitHub."); + reset(descriptionTitle, localize('stepsToReproduce', "Steps to Reproduce"), $('span.required-input', undefined, '*')); + reset(descriptionSubtitle, localize('bugDescription', "Share the steps needed to reliably reproduce the problem. Please include actual and expected results. We support GitHub-flavored Markdown. You will be able to edit your issue and add screenshots when we preview it on GitHub.")); } else if (issueType === IssueType.PerformanceIssue) { show(blockContainer); show(systemBlock); @@ -790,11 +781,11 @@ export class IssueReporter extends Disposable { show(extensionsBlock); } - descriptionTitle.innerHTML = `${localize('stepsToReproduce', "Steps to Reproduce")} *`; - descriptionSubtitle.innerHTML = localize('performanceIssueDesciption', "When did this performance issue happen? Does it occur on startup or after a specific series of actions? We support GitHub-flavored Markdown. You will be able to edit your issue and add screenshots when we preview it on GitHub."); + reset(descriptionTitle, localize('stepsToReproduce', "Steps to Reproduce"), $('span.required-input', undefined, '*')); + reset(descriptionSubtitle, localize('performanceIssueDesciption', "When did this performance issue happen? Does it occur on startup or after a specific series of actions? We support GitHub-flavored Markdown. You will be able to edit your issue and add screenshots when we preview it on GitHub.")); } else if (issueType === IssueType.FeatureRequest) { - descriptionTitle.innerHTML = `${localize('description', "Description")} *`; - descriptionSubtitle.innerHTML = localize('featureRequestDescription', "Please describe the feature you would like to see. We support GitHub-flavored Markdown. You will be able to edit your issue and add screenshots when we preview it on GitHub."); + reset(descriptionTitle, localize('description', "Description"), $('span.required-input', undefined, '*')); + reset(descriptionSubtitle, localize('featureRequestDescription', "Please describe the feature you would like to see. We support GitHub-flavored Markdown. You will be able to edit your issue and add screenshots when we preview it on GitHub.")); show(problemSource); if (fileOnExtension) { @@ -805,8 +796,8 @@ export class IssueReporter extends Disposable { show(searchedExtensionsBlock); show(settingsSearchResultsBlock); - descriptionTitle.innerHTML = `${localize('expectedResults', "Expected Results")} *`; - descriptionSubtitle.innerHTML = localize('settingsSearchResultsDescription', "Please list the results that you were expecting to see when you searched with this query. We support GitHub-flavored Markdown. You will be able to edit your issue and add screenshots when we preview it on GitHub."); + reset(descriptionTitle, localize('expectedResults', "Expected Results"), $('span.required-input', undefined, '*')); + reset(descriptionSubtitle, localize('settingsSearchResultsDescription', "Please list the results that you were expecting to see when you searched with this query. We support GitHub-flavored Markdown. You will be able to edit your issue and add screenshots when we preview it on GitHub.")); } } @@ -928,42 +919,82 @@ export class IssueReporter extends Disposable { } private updateSystemInfo(state: IssueReporterModelData) { - const target = document.querySelector('.block-system .block-info'); + const target = document.querySelector('.block-system .block-info'); + if (target) { const systemInfo = state.systemInfo!; - let renderedData = ` - - - - - - - - -
CPUs${systemInfo.cpus}
GPU Status${Object.keys(systemInfo.gpuStatus).map(key => `${key}: ${systemInfo.gpuStatus[key]}`).join('
')}
Load (avg)${systemInfo.load}
Memory (System)${systemInfo.memory}
Process Argv${systemInfo.processArgs}
Screen Reader${systemInfo.screenReader}
VM${systemInfo.vmHint}
`; + const renderedDataTable = $('table', undefined, + $('tr', undefined, + $('td', undefined, 'CPUs'), + $('td', undefined, systemInfo.cpus || ''), + ), + $('tr', undefined, + $('td', undefined, 'GPU Status' as string), + $('td', undefined, Object.keys(systemInfo.gpuStatus).map(key => `${key}: ${systemInfo.gpuStatus[key]}`).join('\n')), + ), + $('tr', undefined, + $('td', undefined, 'Load (avg)' as string), + $('td', undefined, systemInfo.load || ''), + ), + $('tr', undefined, + $('td', undefined, 'Memory (System)' as string), + $('td', undefined, systemInfo.memory), + ), + $('tr', undefined, + $('td', undefined, 'Process Argv' as string), + $('td', undefined, systemInfo.processArgs), + ), + $('tr', undefined, + $('td', undefined, 'Screen Reader' as string), + $('td', undefined, systemInfo.screenReader), + ), + $('tr', undefined, + $('td', undefined, 'VM'), + $('td', undefined, systemInfo.vmHint), + ), + ); + reset(target, renderedDataTable); systemInfo.remoteData.forEach(remote => { + target.appendChild($('hr')); if (isRemoteDiagnosticError(remote)) { - renderedData += ` -
- - - -
Remote${remote.hostName}
${remote.errorMessage}
`; + const remoteDataTable = $('table', undefined, + $('tr', undefined, + $('td', undefined, 'Remote'), + $('td', undefined, remote.hostName) + ), + $('tr', undefined, + $('td', undefined, ''), + $('td', undefined, remote.errorMessage) + ) + ); + target.appendChild(remoteDataTable); } else { - renderedData += ` -
- - - - - - -
Remote${remote.hostName}
OS${remote.machineInfo.os}
CPUs${remote.machineInfo.cpus}
Memory (System)${remote.machineInfo.memory}
VM${remote.machineInfo.vmHint}
`; + const remoteDataTable = $('table', undefined, + $('tr', undefined, + $('td', undefined, 'Remote'), + $('td', undefined, remote.hostName) + ), + $('tr', undefined, + $('td', undefined, 'OS'), + $('td', undefined, remote.machineInfo.os) + ), + $('tr', undefined, + $('td', undefined, 'CPUs'), + $('td', undefined, remote.machineInfo.cpus || '') + ), + $('tr', undefined, + $('td', undefined, 'Memory (System)' as string), + $('td', undefined, remote.machineInfo.memory) + ), + $('tr', undefined, + $('td', undefined, 'VM'), + $('td', undefined, remote.machineInfo.vmHint) + ), + ); + target.appendChild(remoteDataTable); } }); - - target.innerHTML = renderedData; } } @@ -995,15 +1026,18 @@ export class IssueReporter extends Disposable { return 0; }); - const makeOption = (extension: IOption, selectedExtension?: IssueReporterExtensionData) => { + const makeOption = (extension: IOption, selectedExtension?: IssueReporterExtensionData): HTMLOptionElement => { const selected = selectedExtension && extension.id === selectedExtension.id; - return ``; + return $('option', { + 'value': extension.id, + 'selected': selected || '' + }, extension.name); }; const extensionsSelector = this.getElementById('extension-selector'); if (extensionsSelector) { const { selectedExtension } = this.issueReporterModel.getData(); - extensionsSelector.innerHTML = '' + extensionOptions.map(extension => makeOption(extension, selectedExtension)).join('\n'); + reset(extensionsSelector, $('option'), ...extensionOptions.map(extension => makeOption(extension, selectedExtension))); this.addEventListener('extension-selector', 'change', (e: Event) => { const selectedExtensionId = (e.target).value; @@ -1071,9 +1105,9 @@ export class IssueReporter extends Disposable { } private updateProcessInfo(state: IssueReporterModelData) { - const target = document.querySelector('.block-process .block-info'); + const target = document.querySelector('.block-process .block-info') as HTMLElement; if (target) { - target.innerHTML = `${state.processInfo}`; + reset(target, $('code', undefined, state.processInfo)); } } @@ -1085,7 +1119,7 @@ export class IssueReporter extends Disposable { const target = document.querySelector('.block-extensions .block-info'); if (target) { if (this.configuration.disableExtensions) { - target.innerHTML = localize('disabledExtensions', "Extensions are disabled"); + reset(target, localize('disabledExtensions', "Extensions are disabled")); return; } @@ -1097,8 +1131,7 @@ export class IssueReporter extends Disposable { return; } - const table = this.getExtensionTableHtml(extensions); - target.innerHTML = `${table}
${themeExclusionStr}`; + reset(target, this.getExtensionTableHtml(extensions), document.createTextNode(themeExclusionStr)); } } @@ -1111,28 +1144,24 @@ export class IssueReporter extends Disposable { } const table = this.getExtensionTableHtml(extensions); - target.innerHTML = `${table}
`; + target.innerText = ''; + target.appendChild(table); } } - private getExtensionTableHtml(extensions: IssueReporterExtensionData[]): string { - let table = ` - - Extension - Author (truncated) - Version - `; - - table += extensions.map(extension => { - return ` - - ${extension.name} - ${extension.publisher.substr(0, 3)} - ${extension.version} - `; - }).join(''); - - return table; + private getExtensionTableHtml(extensions: IssueReporterExtensionData[]): HTMLTableElement { + return $('table', undefined, + $('tr', undefined, + $('th', undefined, 'Extension'), + $('th', undefined, 'Author (truncated)' as string), + $('th', undefined, 'Version'), + ), + ...extensions.map(extension => $('tr', undefined, + $('td', undefined, extension.name), + $('td', undefined, extension.publisher.substr(0, 3)), + $('td', undefined, extension.version), + )) + ); } private openLink(event: MouseEvent): void { diff --git a/src/vs/code/electron-sandbox/processExplorer/media/collapsed.svg b/src/vs/code/electron-sandbox/processExplorer/media/collapsed.svg deleted file mode 100644 index 3a63808c35..0000000000 --- a/src/vs/code/electron-sandbox/processExplorer/media/collapsed.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/code/electron-sandbox/processExplorer/media/expanded.svg b/src/vs/code/electron-sandbox/processExplorer/media/expanded.svg deleted file mode 100644 index 75f73adbb0..0000000000 --- a/src/vs/code/electron-sandbox/processExplorer/media/expanded.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/vs/code/electron-sandbox/processExplorer/media/processExplorer.css b/src/vs/code/electron-sandbox/processExplorer/media/processExplorer.css index 2859c35b17..3ac421f113 100644 --- a/src/vs/code/electron-sandbox/processExplorer/media/processExplorer.css +++ b/src/vs/code/electron-sandbox/processExplorer/media/processExplorer.css @@ -58,6 +58,7 @@ table { width: 100%; table-layout: fixed; } + th[scope='col'] { vertical-align: bottom; border-bottom: 1px solid #cccccc; @@ -65,6 +66,7 @@ th[scope='col'] { border-top: 1px solid #cccccc; cursor: default; } + td { padding: .25rem; vertical-align: top; @@ -100,7 +102,6 @@ tbody > tr:hover { display: none; } -img { - width: 16px; - margin-right: 4px; +.header { + display: flex; } diff --git a/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts b/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts index 17d125fa93..99c51222a9 100644 --- a/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts +++ b/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/processExplorer'; +import 'vs/base/browser/ui/codicons/codiconStyles'; // make sure codicon css is loaded import { ElectronService, IElectronService } from 'vs/platform/electron/electron-sandbox/electron'; import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { localize } from 'vs/nls'; @@ -16,6 +17,7 @@ import { addDisposableListener, addClass } from 'vs/base/browser/dom'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { isRemoteDiagnosticError, IRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics'; import { MainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; +import { CodiconLabel } from 'vs/base/browser/ui/codicons/codiconLabel'; const DEBUG_FLAGS_PATTERN = /\s--(inspect|debug)(-brk|port)?=(\d+)?/; const DEBUG_PORT_PATTERN = /\s--(inspect|debug)-port=(\d+)/; @@ -156,15 +158,15 @@ class ProcessExplorer { return maxProcessId; } - private updateSectionCollapsedState(shouldExpand: boolean, body: HTMLElement, twistie: HTMLImageElement, sectionName: string) { + private updateSectionCollapsedState(shouldExpand: boolean, body: HTMLElement, twistie: CodiconLabel, sectionName: string) { if (shouldExpand) { body.classList.remove('hidden'); this.collapsedStateCache.set(sectionName, false); - twistie.src = './media/expanded.svg'; + twistie.text = '$(chevron-down)'; } else { body.classList.add('hidden'); this.collapsedStateCache.set(sectionName, true); - twistie.src = './media/collapsed.svg'; + twistie.text = '$(chevron-right)'; } } @@ -191,18 +193,27 @@ class ProcessExplorer { private renderProcessGroupHeader(sectionName: string, body: HTMLElement, container: HTMLElement) { const headerRow = document.createElement('tr'); - const data = document.createElement('td'); - data.textContent = sectionName; - data.colSpan = 4; - headerRow.appendChild(data); - const twistie = document.createElement('img'); - this.updateSectionCollapsedState(!this.collapsedStateCache.get(sectionName), body, twistie, sectionName); - data.prepend(twistie); + const headerData = document.createElement('td'); + headerData.colSpan = 4; + headerRow.appendChild(headerData); - this.listeners.add(addDisposableListener(data, 'click', (e) => { + const headerContainer = document.createElement('div'); + headerContainer.className = 'header'; + headerData.appendChild(headerContainer); + + const twistieContainer = document.createElement('div'); + const twistieCodicon = new CodiconLabel(twistieContainer); + this.updateSectionCollapsedState(!this.collapsedStateCache.get(sectionName), body, twistieCodicon, sectionName); + headerContainer.appendChild(twistieContainer); + + const headerLabel = document.createElement('span'); + headerLabel.textContent = sectionName; + headerContainer.appendChild(headerLabel); + + this.listeners.add(addDisposableListener(headerData, 'click', (e) => { const isHidden = body.classList.contains('hidden'); - this.updateSectionCollapsedState(isHidden, body, twistie, sectionName); + this.updateSectionCollapsedState(isHidden, body, twistieCodicon, sectionName); })); container.appendChild(headerRow); diff --git a/src/vs/code/electron-sandbox/workbench/workbench.html b/src/vs/code/electron-sandbox/workbench/workbench.html new file mode 100644 index 0000000000..40737461d2 --- /dev/null +++ b/src/vs/code/electron-sandbox/workbench/workbench.html @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/vs/code/electron-sandbox/workbench/workbench.js b/src/vs/code/electron-sandbox/workbench/workbench.js new file mode 100644 index 0000000000..b08b0c1c06 --- /dev/null +++ b/src/vs/code/electron-sandbox/workbench/workbench.js @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/// + +//@ts-check +'use strict'; + +const perf = (function () { + globalThis.MonacoPerformanceMarks = globalThis.MonacoPerformanceMarks || []; + return { + /** + * @param {string} name + */ + mark(name) { + globalThis.MonacoPerformanceMarks.push(name, Date.now()); + } + }; +})(); + +perf.mark('renderer/started'); + +/** + * @type {{ + * load: (modules: string[], resultCallback: (result, configuration: object) => any, options: object) => unknown, + * globals: () => typeof import('../../../base/parts/sandbox/electron-sandbox/globals') + * }} + */ +const bootstrapWindow = (() => { + // @ts-ignore (defined in bootstrap-window.js) + return window.MonacoBootstrapWindow; +})(); + +// Load environment in parallel to workbench loading to avoid waterfall +const whenEnvResolved = bootstrapWindow.globals().process.whenEnvResolved; + +// Load workbench main JS, CSS and NLS all in parallel. This is an +// optimization to prevent a waterfall of loading to happen, because +// we know for a fact that workbench.desktop.sandbox.main will depend on +// the related CSS and NLS counterparts. +bootstrapWindow.load([ + 'vs/workbench/workbench.desktop.sandbox.main', + 'vs/nls!vs/workbench/workbench.desktop.main', + 'vs/css!vs/workbench/workbench.desktop.main' +], + async function (workbench, configuration) { + + // Mark start of workbench + perf.mark('didLoadWorkbenchMain'); + performance.mark('workbench-start'); + + // Wait for process environment being fully resolved + await whenEnvResolved; + + perf.mark('main/startup'); + + // @ts-ignore + return require('vs/workbench/electron-sandbox/desktop.main').main(configuration); + }, + { + removeDeveloperKeybindingsAfterLoad: true, + canModifyDOM: function (windowConfig) { + // TODO@sandbox part-splash is non-sandboxed only + }, + beforeLoaderConfig: function (windowConfig, loaderConfig) { + loaderConfig.recordStats = true; + }, + beforeRequire: function () { + perf.mark('willLoadWorkbenchMain'); + } + } +); diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index 86a9398925..9921f2346a 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -14,7 +14,7 @@ import * as paths from 'vs/base/common/path'; import { whenDeleted, writeFileSync } from 'vs/base/node/pfs'; import { findFreePort, randomPort } from 'vs/base/node/ports'; import { isWindows, isLinux } from 'vs/base/common/platform'; -import { ProfilingSession, Target } from 'v8-inspect-profiler'; +import type { ProfilingSession, Target } from 'v8-inspect-profiler'; import { isString } from 'vs/base/common/types'; import { hasStdinWithoutTty, stdinDataListener, getStdinFilePath, readFromStdin } from 'vs/platform/environment/node/stdin'; diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index 8907917c5c..3e5664b282 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -7,7 +7,6 @@ import { localize } from 'vs/nls'; import { raceTimeout } from 'vs/base/common/async'; import product from 'vs/platform/product/common/product'; import * as path from 'vs/base/common/path'; -import * as semver from 'semver-umd'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -214,6 +213,8 @@ export class Main { throw new Error('Invalid vsix'); } + const semver = await import('semver-umd'); + const extensionIdentifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) }; const installedExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User); const newer = installedExtensions.find(local => areSameExtensions(extensionIdentifier, local.identifier) && semver.gt(local.manifest.version, manifest.version)); diff --git a/src/vs/code/node/shellEnv.ts b/src/vs/code/node/shellEnv.ts index f5b2144084..545f591b11 100644 --- a/src/vs/code/node/shellEnv.ts +++ b/src/vs/code/node/shellEnv.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as cp from 'child_process'; +import { spawn } from 'child_process'; import { generateUuid } from 'vs/base/common/uuid'; import { isWindows } from 'vs/base/common/platform'; import { ILogService } from 'vs/platform/log/common/log'; @@ -30,7 +30,7 @@ function getUnixShellEnvironment(logService: ILogService): Promise ({})); } - -let _shellEnv: Promise; +let shellEnvPromise: Promise | undefined = undefined; /** * We need to get the environment from a user's shell. @@ -91,21 +90,21 @@ let _shellEnv: Promise; * from within a shell. */ export function getShellEnvironment(logService: ILogService, environmentService: INativeEnvironmentService): Promise { - if (_shellEnv === undefined) { + if (!shellEnvPromise) { if (environmentService.args['disable-user-env-probe']) { logService.trace('getShellEnvironment: disable-user-env-probe set, skipping'); - _shellEnv = Promise.resolve({}); + shellEnvPromise = Promise.resolve({}); } else if (isWindows) { logService.trace('getShellEnvironment: running on Windows, skipping'); - _shellEnv = Promise.resolve({}); + shellEnvPromise = Promise.resolve({}); } else if (process.env['VSCODE_CLI'] === '1' && process.env['VSCODE_FORCE_USER_ENV'] !== '1') { logService.trace('getShellEnvironment: running on CLI, skipping'); - _shellEnv = Promise.resolve({}); + shellEnvPromise = Promise.resolve({}); } else { logService.trace('getShellEnvironment: running on Unix'); - _shellEnv = getUnixShellEnvironment(logService); + shellEnvPromise = getUnixShellEnvironment(logService); } } - return _shellEnv; + return shellEnvPromise; } diff --git a/src/vs/code/test/electron-main/nativeHelpers.test.ts b/src/vs/code/test/electron-main/nativeHelpers.test.ts index d35f0bb76b..38231546d7 100644 --- a/src/vs/code/test/electron-main/nativeHelpers.test.ts +++ b/src/vs/code/test/electron-main/nativeHelpers.test.ts @@ -28,9 +28,8 @@ suite('Windows Native Helpers', () => { }); test('vscode-windows-ca-certs', async () => { - const windowsCerts = await new Promise((resolve, reject) => { - require(['vscode-windows-ca-certs'], resolve, reject); - }); + // @ts-ignore Windows only + const windowsCerts = await import('vscode-windows-ca-certs'); assert.ok(windowsCerts, 'Unable to load vscode-windows-ca-certs dependency.'); }); diff --git a/src/vs/editor/browser/controller/textAreaInput.ts b/src/vs/editor/browser/controller/textAreaInput.ts index aded2667fb..bbbc09b654 100644 --- a/src/vs/editor/browser/controller/textAreaInput.ts +++ b/src/vs/editor/browser/controller/textAreaInput.ts @@ -72,7 +72,7 @@ interface InMemoryClipboardMetadata { * Every time we read from the cipboard, if the text matches our last written text, * we can fetch the previous metadata. */ -class InMemoryClipboardMetadataManager { +export class InMemoryClipboardMetadataManager { public static readonly INSTANCE = new InMemoryClipboardMetadataManager(); private _lastState: InMemoryClipboardMetadata | null; diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index ebfa357468..2688761af6 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -348,6 +348,14 @@ export interface IEditorConstructionOptions extends IEditorOptions { overflowWidgetsDomNode?: HTMLElement; } +export interface IDiffEditorConstructionOptions extends IDiffEditorOptions { + /** + * Place overflow widgets inside an external DOM node. + * Defaults to an internal DOM node. + */ + overflowWidgetsDomNode?: HTMLElement; +} + /** * A rich code editor. */ diff --git a/src/vs/editor/browser/services/bulkEditService.ts b/src/vs/editor/browser/services/bulkEditService.ts index 1c4b57699f..3c19a5f3f3 100644 --- a/src/vs/editor/browser/services/bulkEditService.ts +++ b/src/vs/editor/browser/services/bulkEditService.ts @@ -4,13 +4,64 @@ *--------------------------------------------------------------------------------------------*/ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { WorkspaceEdit } from 'vs/editor/common/modes'; +import { TextEdit, WorkspaceEdit, WorkspaceEditMetadata, WorkspaceFileEdit, WorkspaceFileEditOptions, WorkspaceTextEdit } from 'vs/editor/common/modes'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IProgress, IProgressStep } from 'vs/platform/progress/common/progress'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { isObject } from 'vs/base/common/types'; export const IBulkEditService = createDecorator('IWorkspaceEditService'); +function isWorkspaceFileEdit(thing: any): thing is WorkspaceFileEdit { + return isObject(thing) && (Boolean((thing).newUri) || Boolean((thing).oldUri)); +} + +function isWorkspaceTextEdit(thing: any): thing is WorkspaceTextEdit { + return isObject(thing) && URI.isUri((thing).resource) && isObject((thing).edit); +} + +export class ResourceEdit { + + protected constructor(readonly metadata?: WorkspaceEditMetadata) { } + + static convert(edit: WorkspaceEdit): ResourceEdit[] { + + + return edit.edits.map(edit => { + if (isWorkspaceTextEdit(edit)) { + return new ResourceTextEdit(edit.resource, edit.edit, edit.modelVersionId, edit.metadata); + } + if (isWorkspaceFileEdit(edit)) { + return new ResourceFileEdit(edit.oldUri, edit.newUri, edit.options, edit.metadata); + } + throw new Error('Unsupported edit'); + }); + } +} + +export class ResourceTextEdit extends ResourceEdit { + constructor( + readonly resource: URI, + readonly textEdit: TextEdit, + readonly versionId?: number, + readonly metadata?: WorkspaceEditMetadata + ) { + super(metadata); + } +} + +export class ResourceFileEdit extends ResourceEdit { + constructor( + readonly oldResource: URI | undefined, + readonly newResource: URI | undefined, + readonly options?: WorkspaceFileEditOptions, + readonly metadata?: WorkspaceEditMetadata + ) { + super(metadata); + } +} + export interface IBulkEditOptions { editor?: ICodeEditor; progress?: IProgress; @@ -23,7 +74,7 @@ export interface IBulkEditResult { ariaSummary: string; } -export type IBulkEditPreviewHandler = (edit: WorkspaceEdit, options?: IBulkEditOptions) => Promise; +export type IBulkEditPreviewHandler = (edits: ResourceEdit[], options?: IBulkEditOptions) => Promise; export interface IBulkEditService { readonly _serviceBrand: undefined; @@ -32,6 +83,5 @@ export interface IBulkEditService { setPreviewHandler(handler: IBulkEditPreviewHandler): IDisposable; - apply(edit: WorkspaceEdit, options?: IBulkEditOptions): Promise; + apply(edit: ResourceEdit[], options?: IBulkEditOptions): Promise; } - diff --git a/src/vs/editor/browser/viewParts/lines/viewLine.ts b/src/vs/editor/browser/viewParts/lines/viewLine.ts index e9bd6a3675..a7f68fed03 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLine.ts @@ -72,7 +72,7 @@ export class DomReadingContext { export class ViewLineOptions { public readonly themeType: ThemeType; - public readonly renderWhitespace: 'none' | 'boundary' | 'selection' | 'all'; + public readonly renderWhitespace: 'none' | 'boundary' | 'selection' | 'trailing' | 'all'; public readonly renderControlCharacters: boolean; public readonly spaceWidth: number; public readonly middotWidth: number; diff --git a/src/vs/editor/browser/viewParts/selections/selections.ts b/src/vs/editor/browser/viewParts/selections/selections.ts index 435c7ae805..1049b07071 100644 --- a/src/vs/editor/browser/viewParts/selections/selections.ts +++ b/src/vs/editor/browser/viewParts/selections/selections.ts @@ -217,7 +217,7 @@ export class SelectionsOverlay extends DynamicViewOverlay { endStyle.top = CornerStyle.INTERN; } } else if (previousFrameTop) { - // Accept some hick-ups near the viewport edges to save on repaints + // Accept some hiccups near the viewport edges to save on repaints startStyle.top = previousFrameTop.startStyle!.top; endStyle.top = previousFrameTop.endStyle!.top; } @@ -239,7 +239,7 @@ export class SelectionsOverlay extends DynamicViewOverlay { endStyle.bottom = CornerStyle.INTERN; } } else if (previousFrameBottom) { - // Accept some hick-ups near the viewport edges to save on repaints + // Accept some hiccups near the viewport edges to save on repaints startStyle.bottom = previousFrameBottom.startStyle!.bottom; endStyle.bottom = previousFrameBottom.endStyle!.bottom; } diff --git a/src/vs/editor/browser/widget/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditorWidget.ts index 64eff6a7c3..aea5b2028b 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget.ts @@ -71,7 +71,7 @@ interface IEditorsZones { modified: IMyViewZone[]; } -interface IDiffEditorWidgetStyle { +export interface IDiffEditorWidgetStyle { // {{SQL CARBON EDIT}} getEditorsDiffDecorations(lineChanges: editorCommon.ILineChange[], ignoreTrimWhitespace: boolean, renderIndicators: boolean, originalWhitespaces: IEditorWhitespace[], modifiedWhitespaces: IEditorWhitespace[], originalEditor: editorBrowser.ICodeEditor, modifiedEditor: editorBrowser.ICodeEditor, reverse?: boolean): IEditorsDiffDecorationsWithZones; setEnableSplitViewResizing(enableSplitViewResizing: boolean): void; @@ -177,6 +177,9 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE private readonly _onDidUpdateDiff: Emitter = this._register(new Emitter()); public readonly onDidUpdateDiff: Event = this._onDidUpdateDiff.event; + private readonly _onDidContentSizeChange: Emitter = this._register(new Emitter()); + public readonly onDidContentSizeChange: Event = this._onDidContentSizeChange.event; + private readonly id: number; private _state: editorBrowser.DiffEditorState; private _updatingDiffProgress: IProgressRunner | null; @@ -230,7 +233,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE constructor( domElement: HTMLElement, - options: IDiffEditorOptions, + options: editorBrowser.IDiffEditorConstructionOptions, @IClipboardService clipboardService: IClipboardService, @IEditorWorkerService editorWorkerService: IEditorWorkerService, @IContextKeyService contextKeyService: IContextKeyService, @@ -353,21 +356,19 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._diffComputationResult = null; const leftContextKeyService = this._contextKeyService.createScoped(); - leftContextKeyService.createKey('isInDiffLeftEditor', true); const leftServices = new ServiceCollection(); leftServices.set(IContextKeyService, leftContextKeyService); const leftScopedInstantiationService = instantiationService.createChild(leftServices); const rightContextKeyService = this._contextKeyService.createScoped(); - rightContextKeyService.createKey('isInDiffRightEditor', true); const rightServices = new ServiceCollection(); rightServices.set(IContextKeyService, rightContextKeyService); const rightScopedInstantiationService = instantiationService.createChild(rightServices); - this.originalEditor = this._createLeftHandSideEditor(options, leftScopedInstantiationService); - this.modifiedEditor = this._createRightHandSideEditor(options, rightScopedInstantiationService); + this.originalEditor = this._createLeftHandSideEditor(options, leftScopedInstantiationService, leftContextKeyService); + this.modifiedEditor = this._createRightHandSideEditor(options, rightScopedInstantiationService, rightContextKeyService); this._originalOverviewRuler = null; this._modifiedOverviewRuler = null; @@ -377,8 +378,6 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._containerDomElement.appendChild(this._reviewPane.shadow.domNode); this._containerDomElement.appendChild(this._reviewPane.actionBarContainer.domNode); - - // enableSplitViewResizing this._enableSplitViewResizing = true; if (typeof options.enableSplitViewResizing !== 'undefined') { @@ -426,6 +425,10 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE return this._renderIndicators; } + public getContentHeight(): number { + return this.modifiedEditor.getContentHeight(); + } + private _setState(newState: editorBrowser.DiffEditorState): void { if (this._state === newState) { return; @@ -485,7 +488,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._layoutOverviewRulers(); } - private _createLeftHandSideEditor(options: IDiffEditorOptions, instantiationService: IInstantiationService): CodeEditorWidget { + private _createLeftHandSideEditor(options: editorBrowser.IDiffEditorConstructionOptions, instantiationService: IInstantiationService, contextKeyService: IContextKeyService): CodeEditorWidget { const editor = this._createInnerEditor(instantiationService, this._originalDomNode, this._adjustOptionsForLeftHandSide(options, this._originalIsEditable, this._originalCodeLens)); this._register(editor.onDidScrollChange((e) => { @@ -515,10 +518,26 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE } })); + const isInDiffLeftEditorKey = contextKeyService.createKey('isInDiffLeftEditor', undefined); + this._register(editor.onDidFocusEditorWidget(() => isInDiffLeftEditorKey.set(true))); + this._register(editor.onDidBlurEditorWidget(() => isInDiffLeftEditorKey.set(false))); + + this._register(editor.onDidContentSizeChange(e => { + const width = this.originalEditor.getContentWidth() + this.modifiedEditor.getContentWidth() + DiffEditorWidget.ONE_OVERVIEW_WIDTH; + const height = Math.max(this.modifiedEditor.getContentHeight(), this.originalEditor.getContentHeight()); + + this._onDidContentSizeChange.fire({ + contentHeight: height, + contentWidth: width, + contentHeightChanged: e.contentHeightChanged, + contentWidthChanged: e.contentWidthChanged + }); + })); + return editor; } - private _createRightHandSideEditor(options: IDiffEditorOptions, instantiationService: IInstantiationService): CodeEditorWidget { + private _createRightHandSideEditor(options: editorBrowser.IDiffEditorConstructionOptions, instantiationService: IInstantiationService, contextKeyService: IContextKeyService): CodeEditorWidget { const editor = this._createInnerEditor(instantiationService, this._modifiedDomNode, this._adjustOptionsForRightHandSide(options, this._modifiedCodeLens)); this._register(editor.onDidScrollChange((e) => { @@ -560,6 +579,22 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE } })); + const isInDiffRightEditorKey = contextKeyService.createKey('isInDiffRightEditor', undefined); + this._register(editor.onDidFocusEditorWidget(() => isInDiffRightEditorKey.set(true))); + this._register(editor.onDidBlurEditorWidget(() => isInDiffRightEditorKey.set(false))); + + this._register(editor.onDidContentSizeChange(e => { + const width = this.originalEditor.getContentWidth() + this.modifiedEditor.getContentWidth() + DiffEditorWidget.ONE_OVERVIEW_WIDTH; + const height = Math.max(this.modifiedEditor.getContentHeight(), this.originalEditor.getContentHeight()); + + this._onDidContentSizeChange.fire({ + contentHeight: height, + contentWidth: width, + contentHeightChanged: e.contentHeightChanged, + contentWidthChanged: e.contentWidthChanged + }); + })); + return editor; } @@ -1064,8 +1099,8 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE } } - private _adjustOptionsForSubEditor(options: IDiffEditorOptions): IDiffEditorOptions { - let clonedOptions: IDiffEditorOptions = objects.deepClone(options || {}); + private _adjustOptionsForSubEditor(options: editorBrowser.IDiffEditorConstructionOptions): editorBrowser.IDiffEditorConstructionOptions { + let clonedOptions: editorBrowser.IDiffEditorConstructionOptions = objects.deepClone(options || {}); clonedOptions.inDiffEditor = true; clonedOptions.wordWrap = 'off'; clonedOptions.wordWrapMinified = false; @@ -1075,6 +1110,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE clonedOptions.folding = false; clonedOptions.codeLens = false; clonedOptions.fixedOverflowWidgets = true; + clonedOptions.overflowWidgetsDomNode = options.overflowWidgetsDomNode; // clonedOptions.lineDecorationsWidth = '2ch'; if (!clonedOptions.minimap) { clonedOptions.minimap = {}; @@ -1083,7 +1119,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE return clonedOptions; } - private _adjustOptionsForLeftHandSide(options: IDiffEditorOptions, isEditable: boolean, isCodeLensEnabled: boolean): IEditorOptions { + private _adjustOptionsForLeftHandSide(options: editorBrowser.IDiffEditorConstructionOptions, isEditable: boolean, isCodeLensEnabled: boolean): editorBrowser.IEditorConstructionOptions { let result = this._adjustOptionsForSubEditor(options); if (isCodeLensEnabled) { result.codeLens = true; @@ -1093,7 +1129,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE return result; } - private _adjustOptionsForRightHandSide(options: IDiffEditorOptions, isCodeLensEnabled: boolean): IEditorOptions { + private _adjustOptionsForRightHandSide(options: editorBrowser.IDiffEditorConstructionOptions, isCodeLensEnabled: boolean): editorBrowser.IEditorConstructionOptions { let result = this._adjustOptionsForSubEditor(options); if (isCodeLensEnabled) { result.codeLens = true; @@ -1628,14 +1664,14 @@ abstract class ViewZonesComputer { protected abstract _produceModifiedFromDiff(lineChange: editorCommon.ILineChange, lineChangeOriginalLength: number, lineChangeModifiedLength: number): IMyViewZone | null; } -function createDecoration(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, options: ModelDecorationOptions) { +export function createDecoration(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, options: ModelDecorationOptions) { return { range: new Range(startLineNumber, startColumn, endLineNumber, endColumn), options: options }; } -const DECORATIONS = { +export const DECORATIONS = { charDelete: ModelDecorationOptions.register({ className: 'char-delete' @@ -1683,7 +1719,7 @@ const DECORATIONS = { }; -class DiffEditorWidgetSideBySide extends DiffEditorWidgetStyle implements IDiffEditorWidgetStyle, IVerticalSashLayoutProvider { +export class DiffEditorWidgetSideBySide extends DiffEditorWidgetStyle implements IDiffEditorWidgetStyle, IVerticalSashLayoutProvider { static readonly MINIMUM_EDITOR_WIDTH = 100; @@ -2211,11 +2247,11 @@ class InlineViewZonesComputer extends ViewZonesComputer { } } -function isChangeOrInsert(lineChange: editorCommon.IChange): boolean { +export function isChangeOrInsert(lineChange: editorCommon.IChange): boolean { return lineChange.modifiedEndLineNumber > 0; } -function isChangeOrDelete(lineChange: editorCommon.IChange): boolean { +export function isChangeOrDelete(lineChange: editorCommon.IChange): boolean { return lineChange.originalEndLineNumber > 0; } diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 1e7b4d21d8..a493e97813 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -529,7 +529,7 @@ export interface IEditorOptions { * Enable rendering of whitespace. * Defaults to none. */ - renderWhitespace?: 'none' | 'boundary' | 'selection' | 'all'; + renderWhitespace?: 'none' | 'boundary' | 'selection' | 'trailing' | 'all'; /** * Enable rendering of control characters. * Defaults to false. @@ -856,15 +856,13 @@ class EditorBooleanOption extends SimpleEditorOption extends SimpleEditorOption { - public static clampedInt(value: any, defaultValue: number, minimum: number, maximum: number): number { - let r: number; + public static clampedInt(value: any, defaultValue: T, minimum: number, maximum: number): number | T { if (typeof value === 'undefined') { - r = defaultValue; - } else { - r = parseInt(value, 10); - if (isNaN(r)) { - r = defaultValue; - } + return defaultValue; + } + let r = parseInt(value, 10); + if (isNaN(r)) { + return defaultValue; } r = Math.max(minimum, r); r = Math.min(maximum, r); @@ -1348,7 +1346,7 @@ class EditorFind extends BaseEditorOption nls.localize('editor.find.autoFindInSelection.always', 'Always turn on Find in selection automatically'), nls.localize('editor.find.autoFindInSelection.multiline', 'Turn on Find in selection automatically when multiple lines of content are selected.') ], - description: nls.localize('find.autoFindInSelection', "Controls whether the find operation is carried out on selected text or the entire file in the editor.") + description: nls.localize('find.autoFindInSelection', "Controls the condition for turning on find in selection automatically.") }, 'editor.find.globalFindClipboard': { type: 'boolean', @@ -1491,6 +1489,48 @@ class EditorFontSize extends SimpleEditorOption { //#endregion +//#region fontWeight + +class EditorFontWeight extends BaseEditorOption { + private static SUGGESTION_VALUES = ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900']; + private static MINIMUM_VALUE = 1; + private static MAXIMUM_VALUE = 1000; + + constructor() { + super( + EditorOption.fontWeight, 'fontWeight', EDITOR_FONT_DEFAULTS.fontWeight, + { + anyOf: [ + { + type: 'number', + minimum: EditorFontWeight.MINIMUM_VALUE, + maximum: EditorFontWeight.MAXIMUM_VALUE, + errorMessage: nls.localize('fontWeightErrorMessage', "Only \"normal\" and \"bold\" keywords or numbers between 1 and 1000 are allowed.") + }, + { + type: 'string', + pattern: '^(normal|bold|1000|[1-9][0-9]{0,2})$' + }, + { + enum: EditorFontWeight.SUGGESTION_VALUES + } + ], + default: EDITOR_FONT_DEFAULTS.fontWeight, + description: nls.localize('fontWeight', "Controls the font weight. Accepts \"normal\" and \"bold\" keywords or numbers between 1 and 1000.") + } + ); + } + + public validate(input: any): string { + if (input === 'normal' || input === 'bold') { + return input; + } + return String(EditorIntOption.clampedInt(input, EDITOR_FONT_DEFAULTS.fontWeight, EditorFontWeight.MINIMUM_VALUE, EditorFontWeight.MAXIMUM_VALUE)); + } +} + +//#endregion + //#region gotoLocation export type GoToLocationValues = 'peek' | 'gotoAndPeek' | 'goto'; @@ -1824,7 +1864,7 @@ export interface EditorLayoutInfoComputerEnv { readonly memory: ComputeOptionsMemory | null; readonly outerWidth: number; readonly outerHeight: number; - readonly isDominatedByLongLines: boolean + readonly isDominatedByLongLines: boolean; readonly lineHeight: number; readonly viewLineCount: number; readonly lineNumbersDigitCount: number; @@ -1839,7 +1879,7 @@ export interface EditorLayoutInfoComputerEnv { export interface IEditorLayoutComputerInput { readonly outerWidth: number; readonly outerHeight: number; - readonly isDominatedByLongLines: boolean + readonly isDominatedByLongLines: boolean; readonly lineHeight: number; readonly lineNumbersDigitCount: number; readonly typicalHalfwidthCharacterWidth: number; @@ -3102,7 +3142,7 @@ export interface ISuggestOptions { * Controls the visibility of the status bar at the bottom of the suggest widget. */ visible?: boolean; - } + }; } export type InternalSuggestOptions = Readonly>; @@ -3869,13 +3909,7 @@ export const EditorOptions = { fontInfo: register(new EditorFontInfo()), fontLigatures2: register(new EditorFontLigatures()), fontSize: register(new EditorFontSize()), - fontWeight: register(new EditorStringOption( - EditorOption.fontWeight, 'fontWeight', EDITOR_FONT_DEFAULTS.fontWeight, - { - enum: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], - description: nls.localize('fontWeight', "Controls the font weight.") - } - )), + fontWeight: register(new EditorFontWeight()), formatOnPaste: register(new EditorBooleanOption( EditorOption.formatOnPaste, 'formatOnPaste', false, { description: nls.localize('formatOnPaste', "Controls whether the editor should automatically format the pasted content. A formatter must be available and the formatter should be able to format a range in a document.") } @@ -4054,13 +4088,14 @@ export const EditorOptions = { )), renderWhitespace: register(new EditorStringEnumOption( EditorOption.renderWhitespace, 'renderWhitespace', - 'selection' as 'selection' | 'none' | 'boundary' | 'all', - ['none', 'boundary', 'selection', 'all'] as const, + 'selection' as 'selection' | 'none' | 'boundary' | 'trailing' | 'all', + ['none', 'boundary', 'selection', 'trailing', 'all'] as const, { enumDescriptions: [ '', nls.localize('renderWhitespace.boundary', "Render whitespace characters except for single spaces between words."), nls.localize('renderWhitespace.selection', "Render whitespace characters only on selected text."), + nls.localize('renderWhitespace.trailing', "Render only trailing whitespace characters"), '' ], description: nls.localize('renderWhitespace', "Controls how the editor should render whitespace characters.") diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 33ba6f749e..38ab3cc965 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -1312,6 +1312,7 @@ export interface IReadonlyTextBuffer { getLinesContent(): string[]; getLineContent(lineNumber: number): string; getLineCharCode(lineNumber: number, index: number): number; + getCharCode(offset: number): number; getLineLength(lineNumber: number): number; getLineFirstNonWhitespaceColumn(lineNumber: number): number; getLineLastNonWhitespaceColumn(lineNumber: number): number; diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts index 74277788c8..1f972fdf76 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts @@ -626,8 +626,7 @@ export class PieceTreeBase { return this._lastVisitedLine.value; } - public getLineCharCode(lineNumber: number, index: number): number { - let nodePos = this.nodeAt2(lineNumber, index + 1); + private _getCharCode(nodePos: NodePosition): number { if (nodePos.remainder === nodePos.node.piece.length) { // the char we want to fetch is at the head of next node. let matchingNode = nodePos.node.next(); @@ -647,6 +646,11 @@ export class PieceTreeBase { } } + public getLineCharCode(lineNumber: number, index: number): number { + let nodePos = this.nodeAt2(lineNumber, index + 1); + return this._getCharCode(nodePos); + } + public getLineLength(lineNumber: number): number { if (lineNumber === this.getLineCount()) { let startOffset = this.getOffsetAt(lineNumber, 1); @@ -655,6 +659,11 @@ export class PieceTreeBase { return this.getOffsetAt(lineNumber + 1, 1) - this.getOffsetAt(lineNumber, 1) - this._EOLLength; } + public getCharCode(offset: number): number { + let nodePos = this.nodeAt(offset); + return this._getCharCode(nodePos); + } + public findMatchesInNode(node: TreeNode, searcher: Searcher, startLineNumber: number, startColumn: number, startCursor: BufferCursor, endCursor: BufferCursor, searchData: SearchData, captureMatches: boolean, limitResultCount: number, resultLen: number, result: FindMatch[]) { let buffer = this._buffers[node.piece.bufferIndex]; let startOffsetInBuffer = this.offsetInBuffer(node.piece.bufferIndex, node.piece.start); diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts index d606636c96..6a1cdd4262 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts @@ -178,6 +178,10 @@ export class PieceTreeTextBuffer implements ITextBuffer, IDisposable { return this._pieceTree.getLineCharCode(lineNumber, index); } + public getCharCode(offset: number): number { + return this._pieceTree.getCharCode(offset); + } + public getLineLength(lineNumber: number): number { return this._pieceTree.getLineLength(lineNumber); } diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 5af7bd1d3c..0e0eafbe70 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -8,7 +8,6 @@ import { Color } from 'vs/base/common/color'; import { Event } from 'vs/base/common/event'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { isObject } from 'vs/base/common/types'; import { URI, UriComponents } from 'vs/base/common/uri'; import { Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; @@ -1337,29 +1336,6 @@ export class FoldingRangeKind { } } -/** - * @internal - */ -export namespace WorkspaceFileEdit { - /** - * @internal - */ - export function is(thing: any): thing is WorkspaceFileEdit { - return isObject(thing) && (Boolean((thing).newUri) || Boolean((thing).oldUri)); - } -} - -/** - * @internal - */ -export namespace WorkspaceTextEdit { - /** - * @internal - */ - export function is(thing: any): thing is WorkspaceTextEdit { - return isObject(thing) && URI.isUri((thing).resource) && isObject((thing).edit); - } -} export interface WorkspaceEditMetadata { needsConfirmation: boolean; diff --git a/src/vs/editor/common/modes/supports/tokenization.ts b/src/vs/editor/common/modes/supports/tokenization.ts index 24e129bc88..7108d36ae9 100644 --- a/src/vs/editor/common/modes/supports/tokenization.ts +++ b/src/vs/editor/common/modes/supports/tokenization.ts @@ -399,10 +399,10 @@ export function generateTokensCSSForColorMap(colorMap: Color[]): string { let rules: string[] = []; for (let i = 1, len = colorMap.length; i < len; i++) { let color = colorMap[i]; - rules[i] = `.mtk${i} { color: ${color}; }`; + rules[i] = `.monaco-editor .mtk${i} { color: ${color}; }`; } - rules.push('.mtki { font-style: italic; }'); - rules.push('.mtkb { font-weight: bold; }'); - rules.push('.mtku { text-decoration: underline; text-underline-position: under; }'); + rules.push('.monaco-editor .mtki { font-style: italic; }'); + rules.push('.monaco-editor .mtkb { font-weight: bold; }'); + rules.push('.monaco-editor .mtku { text-decoration: underline; text-underline-position: under; }'); return rules.join('\n'); } diff --git a/src/vs/editor/common/modes/tokenizationRegistry.ts b/src/vs/editor/common/modes/tokenizationRegistry.ts index 261ccfec3c..799199022b 100644 --- a/src/vs/editor/common/modes/tokenizationRegistry.ts +++ b/src/vs/editor/common/modes/tokenizationRegistry.ts @@ -7,7 +7,6 @@ import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ColorId, ITokenizationRegistry, ITokenizationSupport, ITokenizationSupportChangedEvent } from 'vs/editor/common/modes'; -import { toArray } from 'vs/base/common/arrays'; export class TokenizationRegistryImpl implements ITokenizationRegistry { @@ -82,7 +81,7 @@ export class TokenizationRegistryImpl implements ITokenizationRegistry { public setColorMap(colorMap: Color[]): void { this._colorMap = colorMap; this._onDidChange.fire({ - changedLanguages: toArray(this._map.keys()), + changedLanguages: Array.from(this._map.keys()), changedColorMap: true }); } diff --git a/src/vs/editor/common/services/getIconClasses.ts b/src/vs/editor/common/services/getIconClasses.ts index 6dbde97acb..b80e14f598 100644 --- a/src/vs/editor/common/services/getIconClasses.ts +++ b/src/vs/editor/common/services/getIconClasses.ts @@ -54,6 +54,11 @@ export function getIconClasses(modelService: IModelService, modeService: IModeSe return classes; } + +export function getIconClassesForModeId(modeId: string): string[] { + return ['file-icon', `${cssEscape(modeId)}-lang-file-icon`]; +} + export function detectModeId(modelService: IModelService, modeService: IModeService, resource: uri): string | null { if (!resource) { return null; // we need a resource at least diff --git a/src/vs/editor/common/viewLayout/viewLineRenderer.ts b/src/vs/editor/common/viewLayout/viewLineRenderer.ts index a7402fb3bd..17abaefef8 100644 --- a/src/vs/editor/common/viewLayout/viewLineRenderer.ts +++ b/src/vs/editor/common/viewLayout/viewLineRenderer.ts @@ -14,7 +14,8 @@ export const enum RenderWhitespace { None = 0, Boundary = 1, Selection = 2, - All = 3 + Trailing = 3, + All = 4 } export const enum LinePartMetadata { @@ -113,7 +114,7 @@ export class RenderLineInput { middotWidth: number, wsmiddotWidth: number, stopRenderingLineAfter: number, - renderWhitespace: 'none' | 'boundary' | 'selection' | 'all', + renderWhitespace: 'none' | 'boundary' | 'selection' | 'trailing' | 'all', renderControlCharacters: boolean, fontLigatures: boolean, selectionsOnLine: LineRange[] | null @@ -138,7 +139,9 @@ export class RenderLineInput { ? RenderWhitespace.Boundary : renderWhitespace === 'selection' ? RenderWhitespace.Selection - : RenderWhitespace.None + : renderWhitespace === 'trailing' + ? RenderWhitespace.Trailing + : RenderWhitespace.None ); this.renderControlCharacters = renderControlCharacters; this.fontLigatures = fontLigatures; @@ -435,7 +438,11 @@ function resolveRenderLineInput(input: RenderLineInput): ResolvedRenderLineInput } let tokens = transformAndRemoveOverflowing(input.lineTokens, input.fauxIndentLength, len); - if (input.renderWhitespace === RenderWhitespace.All || input.renderWhitespace === RenderWhitespace.Boundary || (input.renderWhitespace === RenderWhitespace.Selection && !!input.selectionsOnLine)) { + if (input.renderWhitespace === RenderWhitespace.All || + input.renderWhitespace === RenderWhitespace.Boundary || + (input.renderWhitespace === RenderWhitespace.Selection && !!input.selectionsOnLine) || + input.renderWhitespace === RenderWhitespace.Trailing) { + tokens = _applyRenderWhitespace(input, lineContent, len, tokens); } let containsForeignElements = ForeignElementType.None; @@ -592,6 +599,7 @@ function _applyRenderWhitespace(input: RenderLineInput, lineContent: string, len const useMonospaceOptimizations = input.useMonospaceOptimizations; const selections = input.selectionsOnLine; const onlyBoundary = (input.renderWhitespace === RenderWhitespace.Boundary); + const onlyTrailing = (input.renderWhitespace === RenderWhitespace.Trailing); const generateLinePartForEachWhitespace = (input.renderSpaceWidth !== input.spaceWidth); let result: LinePart[] = [], resultLen = 0; @@ -600,10 +608,11 @@ function _applyRenderWhitespace(input: RenderLineInput, lineContent: string, len let tokenEndIndex = tokens[tokenIndex].endIndex; const tokensLength = tokens.length; + let lineIsEmptyOrWhitespace = false; let firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineContent); let lastNonWhitespaceIndex: number; if (firstNonWhitespaceIndex === -1) { - // The entire line is whitespace + lineIsEmptyOrWhitespace = true; firstNonWhitespaceIndex = len; lastNonWhitespaceIndex = len; } else { @@ -651,6 +660,11 @@ function _applyRenderWhitespace(input: RenderLineInput, lineContent: string, len isInWhitespace = !!currentSelection && currentSelection.startOffset <= charIndex && currentSelection.endOffset > charIndex; } + // If rendering only trailing whitespace, check that the charIndex points to trailing whitespace. + if (isInWhitespace && onlyTrailing) { + isInWhitespace = lineIsEmptyOrWhitespace || charIndex > lastNonWhitespaceIndex; + } + if (wasInWhitespace) { // was in whitespace token if (!isInWhitespace || (!useMonospaceOptimizations && tmpIndent >= tabSize)) { diff --git a/src/vs/editor/contrib/clipboard/clipboard.ts b/src/vs/editor/contrib/clipboard/clipboard.ts index 7b4435fd61..c81befdd08 100644 --- a/src/vs/editor/contrib/clipboard/clipboard.ts +++ b/src/vs/editor/contrib/clipboard/clipboard.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import * as browser from 'vs/base/browser/browser'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import * as platform from 'vs/base/common/platform'; -import { CopyOptions } from 'vs/editor/browser/controller/textAreaInput'; +import { CopyOptions, InMemoryClipboardMetadataManager } from 'vs/editor/browser/controller/textAreaInput'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, registerEditorAction, Command, MultiCommand } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; @@ -16,6 +16,8 @@ import { MenuId } from 'vs/platform/actions/common/actions'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { Handler } from 'vs/editor/common/editorCommon'; const CLIPBOARD_CONTEXT_MENU_GROUP = '9_cutcopypaste'; @@ -23,10 +25,9 @@ const supportsCut = (platform.isNative || document.queryCommandSupported('cut')) const supportsCopy = (platform.isNative || document.queryCommandSupported('copy')); // IE and Edge have trouble with setting html content in clipboard const supportsCopyWithSyntaxHighlighting = (supportsCopy && !browser.isEdge); -// Chrome incorrectly returns true for document.queryCommandSupported('paste') -// when the paste feature is available but the calling script has insufficient -// privileges to actually perform the action -const supportsPaste = (platform.isNative || (!browser.isChrome && document.queryCommandSupported('paste'))); +// Firefox only supports navigator.clipboard.readText() in browser extensions. +// See https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/readText#Browser_compatibility +const supportsPaste = (browser.isFirefox ? document.queryCommandSupported('paste') : true); function registerCommand(command: T): T { command.register(); @@ -160,7 +161,7 @@ class ExecCommandCopyWithSyntaxHighlightingAction extends EditorAction { } } -function registerExecCommandImpl(target: MultiCommand | undefined, browserCommand: 'cut' | 'copy' | 'paste'): void { +function registerExecCommandImpl(target: MultiCommand | undefined, browserCommand: 'cut' | 'copy'): void { if (!target) { return; } @@ -170,13 +171,11 @@ function registerExecCommandImpl(target: MultiCommand | undefined, browserComman // Only if editor text focus (i.e. not if editor has widget focus). const focusedEditor = accessor.get(ICodeEditorService).getFocusedCodeEditor(); if (focusedEditor && focusedEditor.hasTextFocus()) { - if (browserCommand === 'cut' || browserCommand === 'copy') { - // Do not execute if there is no selection and empty selection clipboard is off - const emptySelectionClipboard = focusedEditor.getOption(EditorOption.emptySelectionClipboard); - const selection = focusedEditor.getSelection(); - if (selection && selection.isEmpty() && !emptySelectionClipboard) { - return true; - } + // Do not execute if there is no selection and empty selection clipboard is off + const emptySelectionClipboard = focusedEditor.getOption(EditorOption.emptySelectionClipboard); + const selection = focusedEditor.getSelection(); + if (selection && selection.isEmpty() && !emptySelectionClipboard) { + return true; } document.execCommand(browserCommand); return true; @@ -186,7 +185,6 @@ function registerExecCommandImpl(target: MultiCommand | undefined, browserComman // 2. (default) handle case when focus is somewhere else. target.addImplementation(0, (accessor: ServicesAccessor, args: any) => { - // Only if editor text focus (i.e. not if editor has widget focus). document.execCommand(browserCommand); return true; }); @@ -194,7 +192,52 @@ function registerExecCommandImpl(target: MultiCommand | undefined, browserComman registerExecCommandImpl(CutAction, 'cut'); registerExecCommandImpl(CopyAction, 'copy'); -registerExecCommandImpl(PasteAction, 'paste'); + +if (PasteAction) { + // 1. Paste: handle case when focus is in editor. + PasteAction.addImplementation(10000, (accessor: ServicesAccessor, args: any) => { + const codeEditorService = accessor.get(ICodeEditorService); + const clipboardService = accessor.get(IClipboardService); + + // Only if editor text focus (i.e. not if editor has widget focus). + const focusedEditor = codeEditorService.getFocusedCodeEditor(); + if (focusedEditor && focusedEditor.hasTextFocus()) { + const result = document.execCommand('paste'); + // Use the clipboard service if document.execCommand('paste') was not successful + if (!result && platform.isWeb) { + (async () => { + const clipboardText = await clipboardService.readText(); + if (clipboardText !== '') { + const metadata = InMemoryClipboardMetadataManager.INSTANCE.get(clipboardText); + let pasteOnNewLine = false; + let multicursorText: string[] | null = null; + let mode: string | null = null; + if (metadata) { + pasteOnNewLine = (focusedEditor.getOption(EditorOption.emptySelectionClipboard) && !!metadata.isFromEmptySelection); + multicursorText = (typeof metadata.multicursorText !== 'undefined' ? metadata.multicursorText : null); + mode = metadata.mode; + } + focusedEditor.trigger('keyboard', Handler.Paste, { + text: clipboardText, + pasteOnNewLine, + multicursorText, + mode + }); + } + })(); + return true; + } + return true; + } + return false; + }); + + // 2. Paste: (default) handle case when focus is somewhere else. + PasteAction.addImplementation(0, (accessor: ServicesAccessor, args: any) => { + document.execCommand('paste'); + return true; + }); +} if (supportsCopyWithSyntaxHighlighting) { registerEditorAction(ExecCommandCopyWithSyntaxHighlightingAction); diff --git a/src/vs/editor/contrib/codeAction/codeActionCommands.ts b/src/vs/editor/contrib/codeAction/codeActionCommands.ts index f684c39bb9..e17e307db5 100644 --- a/src/vs/editor/contrib/codeAction/codeActionCommands.ts +++ b/src/vs/editor/contrib/codeAction/codeActionCommands.ts @@ -11,7 +11,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, EditorCommand, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; -import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { IBulkEditService, ResourceEdit } from 'vs/editor/browser/services/bulkEditService'; import { IPosition } from 'vs/editor/common/core/position'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; @@ -163,7 +163,7 @@ export async function applyCodeAction( }); if (action.edit) { - await bulkEditService.apply(action.edit, { editor, label: action.title }); + await bulkEditService.apply(ResourceEdit.convert(action.edit), { editor, label: action.title }); } if (action.command) { diff --git a/src/vs/editor/contrib/codeAction/test/codeActionKeybindingResolver.test.ts b/src/vs/editor/contrib/codeAction/test/codeActionKeybindingResolver.test.ts index 34f3313611..e1ea490989 100644 --- a/src/vs/editor/contrib/codeAction/test/codeActionKeybindingResolver.test.ts +++ b/src/vs/editor/contrib/codeAction/test/codeActionKeybindingResolver.test.ts @@ -89,6 +89,7 @@ function createCodeActionKeybinding(keycode: KeyCode, command: string, commandAr command, commandArgs, undefined, - false); + false, + null); } diff --git a/src/vs/editor/contrib/find/findController.ts b/src/vs/editor/contrib/find/findController.ts index b45e713dd4..3e6d2d8230 100644 --- a/src/vs/editor/contrib/find/findController.ts +++ b/src/vs/editor/contrib/find/findController.ts @@ -20,7 +20,6 @@ import { MenuId } from 'vs/platform/actions/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IContextKey, IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { optional } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; @@ -371,7 +370,6 @@ export class CommonFindController extends Disposable implements IEditorContribut public async getGlobalBufferTerm(): Promise { if (this._editor.getOption(EditorOption.find).globalFindClipboard - && this._clipboardService && this._editor.hasModel() && !this._editor.getModel().isTooLargeForSyncing() ) { @@ -382,7 +380,6 @@ export class CommonFindController extends Disposable implements IEditorContribut public setGlobalBufferTerm(text: string): void { if (this._editor.getOption(EditorOption.find).globalFindClipboard - && this._clipboardService && this._editor.hasModel() && !this._editor.getModel().isTooLargeForSyncing() ) { @@ -406,7 +403,7 @@ export class FindController extends CommonFindController implements IFindControl @INotificationService private readonly _notificationService: INotificationService, @IStorageService _storageService: IStorageService, @IStorageKeysSyncRegistryService private readonly _storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, - @optional(IClipboardService) clipboardService: IClipboardService, + @IClipboardService clipboardService: IClipboardService, ) { super(editor, _contextKeyService, _storageService, clipboardService); this._widget = null; diff --git a/src/vs/editor/contrib/find/findWidget.css b/src/vs/editor/contrib/find/findWidget.css index adfec7b508..df4c4d985c 100644 --- a/src/vs/editor/contrib/find/findWidget.css +++ b/src/vs/editor/contrib/find/findWidget.css @@ -6,7 +6,7 @@ /* Find widget */ .monaco-editor .find-widget { position: absolute; - z-index: 20; + z-index: 50; height: 33px; overflow: hidden; line-height: 19px; diff --git a/src/vs/editor/contrib/format/formatActions.ts b/src/vs/editor/contrib/format/formatActions.ts index 038ee518ba..ab18a652b7 100644 --- a/src/vs/editor/contrib/format/formatActions.ts +++ b/src/vs/editor/contrib/format/formatActions.ts @@ -267,14 +267,16 @@ class FormatSelectionAction extends EditorAction { } const instaService = accessor.get(IInstantiationService); const model = editor.getModel(); - let range: Range = editor.getSelection(); - if (range.isEmpty()) { - range = new Range(range.startLineNumber, 1, range.startLineNumber, model.getLineMaxColumn(range.startLineNumber)); - } + + const ranges = editor.getSelections().map(range => { + return range.isEmpty() + ? new Range(range.startLineNumber, 1, range.startLineNumber, model.getLineMaxColumn(range.startLineNumber)) + : range; + }); const progressService = accessor.get(IEditorProgressService); await progressService.showWhile( - instaService.invokeFunction(formatDocumentRangesWithSelectedProvider, editor, range, FormattingMode.Explicit, Progress.None, CancellationToken.None), + instaService.invokeFunction(formatDocumentRangesWithSelectedProvider, editor, ranges, FormattingMode.Explicit, Progress.None, CancellationToken.None), 250 ); } diff --git a/src/vs/editor/contrib/multicursor/multicursor.ts b/src/vs/editor/contrib/multicursor/multicursor.ts index e63c4440c6..98578dd90a 100644 --- a/src/vs/editor/contrib/multicursor/multicursor.ts +++ b/src/vs/editor/contrib/multicursor/multicursor.ts @@ -971,7 +971,7 @@ export class SelectionHighlighter extends Disposable implements IEditorContribut return; } - const hasFindOccurrences = DocumentHighlightProviderRegistry.has(model); + const hasFindOccurrences = DocumentHighlightProviderRegistry.has(model) && this.editor.getOption(EditorOption.occurrencesHighlight); let allMatches = model.findMatches(this.state.searchText, true, false, this.state.matchCase, this.state.wordSeparators, false).map(m => m.range); allMatches.sort(Range.compareRangesUsingStarts); diff --git a/src/vs/editor/contrib/rename/onTypeRename.ts b/src/vs/editor/contrib/rename/onTypeRename.ts index 9c67926e4b..6fb97b3ada 100644 --- a/src/vs/editor/contrib/rename/onTypeRename.ts +++ b/src/vs/editor/contrib/rename/onTypeRename.ts @@ -46,6 +46,8 @@ export class OnTypeRenameContribution extends Disposable implements IEditorContr return editor.getContribution(OnTypeRenameContribution.ID); } + private _debounceDuration = 200; + private readonly _editor: ICodeEditor; private _enabled: boolean; @@ -121,15 +123,15 @@ export class OnTypeRenameContribution extends Disposable implements IEditorContr this._languageWordPattern = LanguageConfigurationRegistry.getWordDefinition(model.getLanguageIdentifier().id); })); - const rangeUpdateScheduler = new Delayer(200); + const rangeUpdateScheduler = new Delayer(this._debounceDuration); const triggerRangeUpdate = () => { - this._rangeUpdateTriggerPromise = rangeUpdateScheduler.trigger(() => this.updateRanges()); + this._rangeUpdateTriggerPromise = rangeUpdateScheduler.trigger(() => this.updateRanges(), this._debounceDuration); }; const rangeSyncScheduler = new Delayer(0); const triggerRangeSync = (decorations: string[]) => { this._rangeSyncTriggerPromise = rangeSyncScheduler.trigger(() => this._syncRanges(decorations)); }; - this._localToDispose.add(this._editor.onDidChangeCursorPosition((e) => { + this._localToDispose.add(this._editor.onDidChangeCursorPosition(() => { triggerRangeUpdate(); })); this._localToDispose.add(this._editor.onDidChangeModelContent((e) => { @@ -332,6 +334,11 @@ export class OnTypeRenameContribution extends Disposable implements IEditorContr return request; } + // for testing + public setDebounceDuration(timeInMS: number) { + this._debounceDuration = timeInMS; + } + // private printDecorators(model: ITextModel) { // return this._currentDecorations.map(d => { // const range = model.getDecorationRange(d); diff --git a/src/vs/editor/contrib/rename/rename.ts b/src/vs/editor/contrib/rename/rename.ts index aab5e208b1..5f951a1696 100644 --- a/src/vs/editor/contrib/rename/rename.ts +++ b/src/vs/editor/contrib/rename/rename.ts @@ -22,7 +22,7 @@ import { MessageController } from 'vs/editor/contrib/message/messageController'; import { CodeEditorStateFlag, EditorStateCancellationTokenSource } from 'vs/editor/browser/core/editorState'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { IBulkEditService, ResourceEdit } from 'vs/editor/browser/services/bulkEditService'; import { URI } from 'vs/base/common/uri'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; @@ -226,7 +226,7 @@ class RenameController implements IEditorContribution { return; } - this._bulkEditService.apply(renameResult, { + this._bulkEditService.apply(ResourceEdit.convert(renameResult), { editor: this.editor, showPreview: inputFieldResult.wantsPreview, label: nls.localize('label', "Renaming '{0}'", loc?.text), diff --git a/src/vs/editor/contrib/rename/test/onTypeRename.test.ts b/src/vs/editor/contrib/rename/test/onTypeRename.test.ts index 86aa3e1cd8..8d23f679e9 100644 --- a/src/vs/editor/contrib/rename/test/onTypeRename.test.ts +++ b/src/vs/editor/contrib/rename/test/onTypeRename.test.ts @@ -79,6 +79,7 @@ suite('On type rename', () => { OnTypeRenameContribution.ID, OnTypeRenameContribution ); + ontypeRenameContribution.setDebounceDuration(0); const testEditor: TestEditor = { setPosition(pos: Position) { diff --git a/src/vs/editor/contrib/snippet/snippetController2.ts b/src/vs/editor/contrib/snippet/snippetController2.ts index 1be10181c4..e9eb1ae732 100644 --- a/src/vs/editor/contrib/snippet/snippetController2.ts +++ b/src/vs/editor/contrib/snippet/snippetController2.ts @@ -18,6 +18,7 @@ import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from ' import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ILogService } from 'vs/platform/log/common/log'; import { SnippetSession } from './snippetSession'; +import { OvertypingCapturer } from 'vs/editor/contrib/suggest/suggestOvertypingCapturer'; export interface ISnippetInsertOptions { overwriteBefore: number; @@ -26,6 +27,7 @@ export interface ISnippetInsertOptions { undoStopBefore: boolean; undoStopAfter: boolean; clipboardText: string | undefined; + overtypingCapturer: OvertypingCapturer | undefined; } const _defaultOptions: ISnippetInsertOptions = { @@ -34,7 +36,8 @@ const _defaultOptions: ISnippetInsertOptions = { undoStopBefore: true, undoStopAfter: true, adjustWhitespace: true, - clipboardText: undefined + clipboardText: undefined, + overtypingCapturer: undefined }; export class SnippetController2 implements IEditorContribution { diff --git a/src/vs/editor/contrib/snippet/snippetSession.ts b/src/vs/editor/contrib/snippet/snippetSession.ts index 1ab83a6a73..26681ebaf0 100644 --- a/src/vs/editor/contrib/snippet/snippetSession.ts +++ b/src/vs/editor/contrib/snippet/snippetSession.ts @@ -23,6 +23,7 @@ import * as colors from 'vs/platform/theme/common/colorRegistry'; import { withNullAsUndefined } from 'vs/base/common/types'; import { ILabelService } from 'vs/platform/label/common/label'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { OvertypingCapturer } from 'vs/editor/contrib/suggest/suggestOvertypingCapturer'; registerThemingParticipant((theme, collector) => { @@ -319,13 +320,15 @@ export interface ISnippetSessionInsertOptions { overwriteAfter: number; adjustWhitespace: boolean; clipboardText: string | undefined; + overtypingCapturer: OvertypingCapturer | undefined; } const _defaultOptions: ISnippetSessionInsertOptions = { overwriteBefore: 0, overwriteAfter: 0, adjustWhitespace: true, - clipboardText: undefined + clipboardText: undefined, + overtypingCapturer: undefined }; export class SnippetSession { @@ -382,7 +385,7 @@ export class SnippetSession { return selection; } - static createEditsAndSnippets(editor: IActiveCodeEditor, template: string, overwriteBefore: number, overwriteAfter: number, enforceFinalTabstop: boolean, adjustWhitespace: boolean, clipboardText: string | undefined): { edits: IIdentifiedSingleEditOperation[], snippets: OneSnippet[] } { + static createEditsAndSnippets(editor: IActiveCodeEditor, template: string, overwriteBefore: number, overwriteAfter: number, enforceFinalTabstop: boolean, adjustWhitespace: boolean, clipboardText: string | undefined, overtypingCapturer: OvertypingCapturer | undefined): { edits: IIdentifiedSingleEditOperation[], snippets: OneSnippet[] } { const edits: IIdentifiedSingleEditOperation[] = []; const snippets: OneSnippet[] = []; @@ -449,7 +452,7 @@ export class SnippetSession { snippet.resolveVariables(new CompositeSnippetVariableResolver([ modelBasedVariableResolver, new ClipboardBasedVariableResolver(readClipboardText, idx, indexedSelections.length, editor.getOption(EditorOption.multiCursorPaste) === 'spread'), - new SelectionBasedVariableResolver(model, selection), + new SelectionBasedVariableResolver(model, selection, idx, overtypingCapturer), new CommentBasedVariableResolver(model, selection), new TimeBasedVariableResolver, new WorkspaceBasedVariableResolver(workspaceService), @@ -496,7 +499,7 @@ export class SnippetSession { } // make insert edit and start with first selections - const { edits, snippets } = SnippetSession.createEditsAndSnippets(this._editor, this._template, this._options.overwriteBefore, this._options.overwriteAfter, false, this._options.adjustWhitespace, this._options.clipboardText); + const { edits, snippets } = SnippetSession.createEditsAndSnippets(this._editor, this._template, this._options.overwriteBefore, this._options.overwriteAfter, false, this._options.adjustWhitespace, this._options.clipboardText, this._options.overtypingCapturer); this._snippets = snippets; this._editor.executeEdits('snippet', edits, undoEdits => { @@ -516,7 +519,7 @@ export class SnippetSession { return; } this._templateMerges.push([this._snippets[0]._nestingLevel, this._snippets[0]._placeholderGroupsIdx, template]); - const { edits, snippets } = SnippetSession.createEditsAndSnippets(this._editor, template, options.overwriteBefore, options.overwriteAfter, true, options.adjustWhitespace, options.clipboardText); + const { edits, snippets } = SnippetSession.createEditsAndSnippets(this._editor, template, options.overwriteBefore, options.overwriteAfter, true, options.adjustWhitespace, options.clipboardText, options.overtypingCapturer); this._editor.executeEdits('snippet', edits, undoEdits => { for (const snippet of this._snippets) { diff --git a/src/vs/editor/contrib/snippet/snippetVariables.ts b/src/vs/editor/contrib/snippet/snippetVariables.ts index bc9bc4dc80..42dff20925 100644 --- a/src/vs/editor/contrib/snippet/snippetVariables.ts +++ b/src/vs/editor/contrib/snippet/snippetVariables.ts @@ -16,6 +16,7 @@ import { isSingleFolderWorkspaceIdentifier, toWorkspaceIdentifier, WORKSPACE_EXT import { ILabelService } from 'vs/platform/label/common/label'; import { normalizeDriveLetter } from 'vs/base/common/labels'; import { URI } from 'vs/base/common/uri'; +import { OvertypingCapturer } from 'vs/editor/contrib/suggest/suggestOvertypingCapturer'; export const KnownSnippetVariableNames: { [key: string]: true } = Object.freeze({ 'CURRENT_YEAR': true, @@ -71,7 +72,9 @@ export class SelectionBasedVariableResolver implements VariableResolver { constructor( private readonly _model: ITextModel, - private readonly _selection: Selection + private readonly _selection: Selection, + private readonly _selectionIdx: number, + private readonly _overtypingCapturer: OvertypingCapturer | undefined ) { // } @@ -82,7 +85,18 @@ export class SelectionBasedVariableResolver implements VariableResolver { if (name === 'SELECTION' || name === 'TM_SELECTED_TEXT') { let value = this._model.getValueInRange(this._selection) || undefined; - if (value && this._selection.startLineNumber !== this._selection.endLineNumber && variable.snippet) { + let isMultiline = this._selection.startLineNumber !== this._selection.endLineNumber; + + // If there was no selected text, try to get last overtyped text + if (!value && this._overtypingCapturer) { + const info = this._overtypingCapturer.getLastOvertypedInfo(this._selectionIdx); + if (info) { + value = info.value; + isMultiline = info.multiline; + } + } + + if (value && isMultiline && variable.snippet) { // Selection is a multiline string which we indentation we now // need to adjust. We compare the indentation of this variable // with the indentation at the editor position and add potential diff --git a/src/vs/editor/contrib/snippet/test/snippetSession.test.ts b/src/vs/editor/contrib/snippet/test/snippetSession.test.ts index e21ec394e7..b94b58a870 100644 --- a/src/vs/editor/contrib/snippet/test/snippetSession.test.ts +++ b/src/vs/editor/contrib/snippet/test/snippetSession.test.ts @@ -127,7 +127,7 @@ suite('SnippetSession', function () { test('snippets, newline NO whitespace adjust', () => { editor.setSelection(new Selection(2, 5, 2, 5)); - const session = new SnippetSession(editor, 'abc\n foo\n bar\n$0', { overwriteBefore: 0, overwriteAfter: 0, adjustWhitespace: false, clipboardText: undefined }); + const session = new SnippetSession(editor, 'abc\n foo\n bar\n$0', { overwriteBefore: 0, overwriteAfter: 0, adjustWhitespace: false, clipboardText: undefined, overtypingCapturer: undefined }); session.insert(); assert.equal(editor.getModel()!.getValue(), 'function foo() {\n abc\n foo\n bar\nconsole.log(a);\n}'); }); @@ -649,7 +649,7 @@ suite('SnippetSession', function () { assert.ok(actual.equalsSelection(new Selection(1, 9, 1, 12))); editor.setSelections([new Selection(1, 9, 1, 12)]); - new SnippetSession(editor, 'far', { overwriteBefore: 3, overwriteAfter: 0, adjustWhitespace: true, clipboardText: undefined }).insert(); + new SnippetSession(editor, 'far', { overwriteBefore: 3, overwriteAfter: 0, adjustWhitespace: true, clipboardText: undefined, overtypingCapturer: undefined }).insert(); assert.equal(model.getValue(), 'console.far'); }); }); diff --git a/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts b/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts index 595080fb9f..2cc92edbb2 100644 --- a/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts +++ b/src/vs/editor/contrib/snippet/test/snippetVariables.test.ts @@ -34,7 +34,7 @@ suite('Snippet Variables Resolver', function () { resolver = new CompositeSnippetVariableResolver([ new ModelBasedVariableResolver(labelService, model), - new SelectionBasedVariableResolver(model, new Selection(1, 1, 1, 1)), + new SelectionBasedVariableResolver(model, new Selection(1, 1, 1, 1), 0, undefined), ]); }); @@ -102,24 +102,24 @@ suite('Snippet Variables Resolver', function () { test('editor variables, selection', function () { - resolver = new SelectionBasedVariableResolver(model, new Selection(1, 2, 2, 3)); + resolver = new SelectionBasedVariableResolver(model, new Selection(1, 2, 2, 3), 0, undefined); assertVariableResolve(resolver, 'TM_SELECTED_TEXT', 'his is line one\nth'); assertVariableResolve(resolver, 'TM_CURRENT_LINE', 'this is line two'); assertVariableResolve(resolver, 'TM_LINE_INDEX', '1'); assertVariableResolve(resolver, 'TM_LINE_NUMBER', '2'); - resolver = new SelectionBasedVariableResolver(model, new Selection(2, 3, 1, 2)); + resolver = new SelectionBasedVariableResolver(model, new Selection(2, 3, 1, 2), 0, undefined); assertVariableResolve(resolver, 'TM_SELECTED_TEXT', 'his is line one\nth'); assertVariableResolve(resolver, 'TM_CURRENT_LINE', 'this is line one'); assertVariableResolve(resolver, 'TM_LINE_INDEX', '0'); assertVariableResolve(resolver, 'TM_LINE_NUMBER', '1'); - resolver = new SelectionBasedVariableResolver(model, new Selection(1, 2, 1, 2)); + resolver = new SelectionBasedVariableResolver(model, new Selection(1, 2, 1, 2), 0, undefined); assertVariableResolve(resolver, 'TM_SELECTED_TEXT', undefined); assertVariableResolve(resolver, 'TM_CURRENT_WORD', 'this'); - resolver = new SelectionBasedVariableResolver(model, new Selection(3, 1, 3, 1)); + resolver = new SelectionBasedVariableResolver(model, new Selection(3, 1, 3, 1), 0, undefined); assertVariableResolve(resolver, 'TM_CURRENT_WORD', undefined); }); diff --git a/src/vs/editor/contrib/suggest/suggestController.ts b/src/vs/editor/contrib/suggest/suggestController.ts index 6565976c01..3ec18c4ba0 100644 --- a/src/vs/editor/contrib/suggest/suggestController.ts +++ b/src/vs/editor/contrib/suggest/suggestController.ts @@ -34,6 +34,7 @@ import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerServ import { IdleValue } from 'vs/base/common/async'; import { isObject, assertType } from 'vs/base/common/types'; import { CommitCharacterController } from './suggestCommitCharacters'; +import { OvertypingCapturer } from './suggestOvertypingCapturer'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { TrackedRangeStickiness, ITextModel } from 'vs/editor/common/model'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; @@ -112,6 +113,7 @@ export class SuggestController implements IEditorContribution { private readonly _alternatives: IdleValue; private readonly _lineSuffix = new MutableDisposable(); private readonly _toDispose = new DisposableStore(); + private readonly _overtypingCapturer: IdleValue; constructor( editor: ICodeEditor, @@ -203,6 +205,11 @@ export class SuggestController implements IEditorContribution { return widget; })); + // Wire up text overtyping capture + this._overtypingCapturer = this._toDispose.add(new IdleValue(() => { + return this._toDispose.add(new OvertypingCapturer(this.editor, this.model)); + })); + this._alternatives = this._toDispose.add(new IdleValue(() => { return this._toDispose.add(new SuggestAlternatives(this.editor, this._contextKeyService)); })); @@ -361,7 +368,8 @@ export class SuggestController implements IEditorContribution { undoStopBefore: false, undoStopAfter: false, adjustWhitespace: !(item.completion.insertTextRules! & CompletionItemInsertTextRule.KeepWhitespace), - clipboardText: event.model.clipboardText + clipboardText: event.model.clipboardText, + overtypingCapturer: this._overtypingCapturer.value }); if (!(flags & InsertFlags.NoAfterUndoStop)) { diff --git a/src/vs/editor/contrib/suggest/suggestOvertypingCapturer.ts b/src/vs/editor/contrib/suggest/suggestOvertypingCapturer.ts new file mode 100644 index 0000000000..c6dcbb894a --- /dev/null +++ b/src/vs/editor/contrib/suggest/suggestOvertypingCapturer.ts @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { SuggestModel } from 'vs/editor/contrib/suggest/suggestModel'; + +export class OvertypingCapturer implements IDisposable { + + private static readonly _maxSelectionLength = 51200; + private readonly _disposables = new DisposableStore(); + + private _lastOvertyped: { value: string; multiline: boolean }[] = []; + private _empty: boolean = true; + + constructor(editor: ICodeEditor, suggestModel: SuggestModel) { + + this._disposables.add(editor.onWillType(() => { + if (!this._empty) { + return; + } + if (!editor.hasModel()) { + return; + } + + const selections = editor.getSelections(); + const selectionsLength = selections.length; + + // Check if it will overtype any selections + let willOvertype = false; + for (let i = 0; i < selectionsLength; i++) { + if (!selections[i].isEmpty()) { + willOvertype = true; + break; + } + } + if (!willOvertype) { + return; + } + + this._lastOvertyped = []; + const model = editor.getModel(); + for (let i = 0; i < selectionsLength; i++) { + const selection = selections[i]; + // Check for overtyping capturer restrictions + if (model.getValueLengthInRange(selection) > OvertypingCapturer._maxSelectionLength) { + return; + } + this._lastOvertyped[i] = { value: model.getValueInRange(selection), multiline: selection.startLineNumber !== selection.endLineNumber }; + } + this._empty = false; + })); + + this._disposables.add(suggestModel.onDidCancel(e => { + if (!this._empty) { + this._empty = true; + } + })); + } + + getLastOvertypedInfo(idx: number): { value: string; multiline: boolean } | undefined { + if (!this._empty && idx >= 0 && idx < this._lastOvertyped.length) { + return this._lastOvertyped[idx]; + } + return undefined; + } + + dispose() { + this._disposables.dispose(); + } +} diff --git a/src/vs/editor/contrib/wordOperations/wordOperations.ts b/src/vs/editor/contrib/wordOperations/wordOperations.ts index 963309342a..156473a507 100644 --- a/src/vs/editor/contrib/wordOperations/wordOperations.ts +++ b/src/vs/editor/contrib/wordOperations/wordOperations.ts @@ -101,13 +101,7 @@ export class CursorWordStartLeft extends WordLeftCommand { inSelectionMode: false, wordNavigationType: WordNavigationType.WordStart, id: 'cursorWordStartLeft', - precondition: undefined, - kbOpts: { - kbExpr: EditorContextKeys.textInputFocus, - primary: KeyMod.CtrlCmd | KeyCode.LeftArrow, - mac: { primary: KeyMod.Alt | KeyCode.LeftArrow }, - weight: KeybindingWeight.EditorContrib - } + precondition: undefined }); } } @@ -129,7 +123,13 @@ export class CursorWordLeft extends WordLeftCommand { inSelectionMode: false, wordNavigationType: WordNavigationType.WordStartFast, id: 'cursorWordLeft', - precondition: undefined + precondition: undefined, + kbOpts: { + kbExpr: EditorContextKeys.textInputFocus, + primary: KeyMod.CtrlCmd | KeyCode.LeftArrow, + mac: { primary: KeyMod.Alt | KeyCode.LeftArrow }, + weight: KeybindingWeight.EditorContrib + } }); } } @@ -140,13 +140,7 @@ export class CursorWordStartLeftSelect extends WordLeftCommand { inSelectionMode: true, wordNavigationType: WordNavigationType.WordStart, id: 'cursorWordStartLeftSelect', - precondition: undefined, - kbOpts: { - kbExpr: EditorContextKeys.textInputFocus, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.LeftArrow, - mac: { primary: KeyMod.Alt | KeyMod.Shift | KeyCode.LeftArrow }, - weight: KeybindingWeight.EditorContrib - } + precondition: undefined }); } } @@ -168,7 +162,13 @@ export class CursorWordLeftSelect extends WordLeftCommand { inSelectionMode: true, wordNavigationType: WordNavigationType.WordStartFast, id: 'cursorWordLeftSelect', - precondition: undefined + precondition: undefined, + kbOpts: { + kbExpr: EditorContextKeys.textInputFocus, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.LeftArrow, + mac: { primary: KeyMod.Alt | KeyMod.Shift | KeyCode.LeftArrow }, + weight: KeybindingWeight.EditorContrib + } }); } } diff --git a/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts b/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts index 056cbec768..1151c65d98 100644 --- a/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts +++ b/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./inspectTokens'; +import { $, append, reset } from 'vs/base/browser/dom'; import { CharCode } from 'vs/base/common/charCode'; import { Color } from 'vs/base/common/color'; import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable } from 'vs/base/common/lifecycle'; -import { escape } from 'vs/base/common/strings'; import { ContentWidgetPositionPreference, IActiveCodeEditor, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { Position } from 'vs/editor/common/core/position'; @@ -115,23 +115,11 @@ function renderTokenText(tokenText: string): string { let charCode = tokenText.charCodeAt(charIndex); switch (charCode) { case CharCode.Tab: - result += '→'; + result += '\u2192'; // → break; case CharCode.Space: - result += '·'; - break; - - case CharCode.LessThan: - result += '<'; - break; - - case CharCode.GreaterThan: - result += '>'; - break; - - case CharCode.Ampersand: - result += '&'; + result += '\u00B7'; // · break; default: @@ -211,8 +199,6 @@ class InspectTokensWidget extends Disposable implements IContentWidget { } } - let result = ''; - let lineContent = this._model.getLineContent(position.lineNumber); let tokenText = ''; if (token1Index < data.tokens1.length) { @@ -220,26 +206,43 @@ class InspectTokensWidget extends Disposable implements IContentWidget { let tokenEndIndex = token1Index + 1 < data.tokens1.length ? data.tokens1[token1Index + 1].offset : lineContent.length; tokenText = lineContent.substring(tokenStartIndex, tokenEndIndex); } - result += `

${renderTokenText(tokenText)}(${tokenText.length} ${tokenText.length === 1 ? 'char' : 'chars'})

`; + reset(this._domNode, + $('h2.tm-token', undefined, renderTokenText(tokenText), + $('span.tm-token-length', undefined, `${tokenText.length} ${tokenText.length === 1 ? 'char' : 'chars'}`))); - result += `
`; + append(this._domNode, $('hr.tokens-inspect-separator', { 'style': 'clear:both' })); - let metadata = (token2Index << 1) + 1 < data.tokens2.length ? this._decodeMetadata(data.tokens2[(token2Index << 1) + 1]) : null; - result += ``; - result += ``; - result += ``; - result += ``; - result += ``; - result += ``; - result += ``; - - result += `
`; + const metadata = (token2Index << 1) + 1 < data.tokens2.length ? this._decodeMetadata(data.tokens2[(token2Index << 1) + 1]) : null; + append(this._domNode, $('table.tm-metadata-table', undefined, + $('tbody', undefined, + $('tr', undefined, + $('td.tm-metadata-key', undefined, 'language'), + $('td.tm-metadata-value', undefined, `${metadata ? metadata.languageIdentifier.language : '-?-'}`) + ), + $('tr', undefined, + $('td.tm-metadata-key', undefined, 'token type' as string), + $('td.tm-metadata-value', undefined, `${metadata ? this._tokenTypeToString(metadata.tokenType) : '-?-'}`) + ), + $('tr', undefined, + $('td.tm-metadata-key', undefined, 'font style' as string), + $('td.tm-metadata-value', undefined, `${metadata ? this._fontStyleToString(metadata.fontStyle) : '-?-'}`) + ), + $('tr', undefined, + $('td.tm-metadata-key', undefined, 'foreground'), + $('td.tm-metadata-value', undefined, `${metadata ? Color.Format.CSS.formatHex(metadata.foreground) : '-?-'}`) + ), + $('tr', undefined, + $('td.tm-metadata-key', undefined, 'background'), + $('td.tm-metadata-value', undefined, `${metadata ? Color.Format.CSS.formatHex(metadata.background) : '-?-'}`) + ) + ) + )); + append(this._domNode, $('hr.tokens-inspect-separator')); if (token1Index < data.tokens1.length) { - result += `${escape(data.tokens1[token1Index].type)}`; + append(this._domNode, $('span.tm-token-type', undefined, data.tokens1[token1Index].type)); } - this._domNode.innerHTML = result; this._editor.layoutContentWidget(this); } diff --git a/src/vs/editor/standalone/browser/simpleServices.ts b/src/vs/editor/standalone/browser/simpleServices.ts index 562ddc90da..6c06365813 100644 --- a/src/vs/editor/standalone/browser/simpleServices.ts +++ b/src/vs/editor/standalone/browser/simpleServices.ts @@ -13,14 +13,13 @@ import { OS, isLinux, isMacintosh } from 'vs/base/common/platform'; import Severity from 'vs/base/common/severity'; import { URI } from 'vs/base/common/uri'; import { ICodeEditor, IDiffEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IBulkEditOptions, IBulkEditResult, IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { IBulkEditOptions, IBulkEditResult, IBulkEditService, ResourceEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { isDiffEditorConfigurationKey, isEditorConfigurationKey } from 'vs/editor/common/config/commonEditorConfig'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { IPosition, Position as Pos } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { IEditor } from 'vs/editor/common/editorCommon'; -import { ITextModel, ITextSnapshot } from 'vs/editor/common/model'; -import { TextEdit, WorkspaceEdit, WorkspaceTextEdit } from 'vs/editor/common/modes'; +import { IIdentifiedSingleEditOperation, ITextModel, ITextSnapshot } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IResolvedTextEditorModel, ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService'; import { ITextResourceConfigurationService, ITextResourcePropertiesService, ITextResourceConfigurationChangeEvent } from 'vs/editor/common/services/textResourceConfigurationService'; @@ -47,6 +46,7 @@ import { SimpleServicesNLS } from 'vs/editor/common/standaloneStrings'; import { ClassifiedEvent, StrictPropertyCheck, GDPRClassification } from 'vs/platform/telemetry/common/gdprTypings'; import { basename } from 'vs/base/common/resources'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { NullLogService } from 'vs/platform/log/common/log'; export class SimpleModel implements IResolvedTextEditorModel { @@ -78,10 +78,17 @@ export class SimpleModel implements IResolvedTextEditorModel { return false; } + private disposed = false; public dispose(): void { + this.disposed = true; + this._onDispose.fire(); } + public isDisposed(): boolean { + return this.disposed; + } + public isResolved(): boolean { return true; } @@ -293,7 +300,7 @@ export class StandaloneKeybindingService extends AbstractKeybindingService { notificationService: INotificationService, domNode: HTMLElement ) { - super(contextKeyService, commandService, telemetryService, notificationService); + super(contextKeyService, commandService, telemetryService, notificationService, new NullLogService()); this._cachedResolver = null; this._dynamicKeybindings = []; @@ -319,7 +326,8 @@ export class StandaloneKeybindingService extends AbstractKeybindingService { command: commandId, when: when, weight1: 1000, - weight2: 0 + weight2: 0, + extensionId: null }); toDispose.add(toDisposable(() => { @@ -350,7 +358,7 @@ export class StandaloneKeybindingService extends AbstractKeybindingService { if (!this._cachedResolver) { const defaults = this._toNormalizedKeybindingItems(KeybindingsRegistry.getDefaultKeybindings(), true); const overrides = this._toNormalizedKeybindingItems(this._dynamicKeybindings, false); - this._cachedResolver = new KeybindingResolver(defaults, overrides); + this._cachedResolver = new KeybindingResolver(defaults, overrides, (str) => this._log(str)); } return this._cachedResolver; } @@ -367,11 +375,11 @@ export class StandaloneKeybindingService extends AbstractKeybindingService { if (!keybinding) { // This might be a removal keybinding item in user settings => accept it - result[resultLen++] = new ResolvedKeybindingItem(undefined, item.command, item.commandArgs, when, isDefault); + result[resultLen++] = new ResolvedKeybindingItem(undefined, item.command, item.commandArgs, when, isDefault, null); } else { const resolvedKeybindings = this.resolveKeybinding(keybinding); for (const resolvedKeybinding of resolvedKeybindings) { - result[resultLen++] = new ResolvedKeybindingItem(resolvedKeybinding, item.command, item.commandArgs, when, isDefault); + result[resultLen++] = new ResolvedKeybindingItem(resolvedKeybinding, item.command, item.commandArgs, when, isDefault, null); } } } @@ -665,42 +673,43 @@ export class SimpleBulkEditService implements IBulkEditService { return Disposable.None; } - apply(workspaceEdit: WorkspaceEdit, options?: IBulkEditOptions): Promise { + async apply(edits: ResourceEdit[], _options?: IBulkEditOptions): Promise { - let edits = new Map(); + const textEdits = new Map(); - if (workspaceEdit.edits) { - for (let edit of workspaceEdit.edits) { - if (!WorkspaceTextEdit.is(edit)) { - return Promise.reject(new Error('bad edit - only text edits are supported')); - } - let model = this._modelService.getModel(edit.resource); - if (!model) { - return Promise.reject(new Error('bad edit - model not found')); - } - let array = edits.get(model); - if (!array) { - array = []; - edits.set(model, array); - } - array.push(edit.edit); + for (let edit of edits) { + if (!(edit instanceof ResourceTextEdit)) { + throw new Error('bad edit - only text edits are supported'); } + const model = this._modelService.getModel(edit.resource); + if (!model) { + throw new Error('bad edit - model not found'); + } + if (typeof edit.versionId === 'number' && model.getVersionId() !== edit.versionId) { + throw new Error('bad state - model changed in the meantime'); + } + let array = textEdits.get(model); + if (!array) { + array = []; + textEdits.set(model, array); + } + array.push(EditOperation.replaceMove(Range.lift(edit.textEdit.range), edit.textEdit.text)); } + let totalEdits = 0; let totalFiles = 0; - edits.forEach((edits, model) => { + for (const [model, edits] of textEdits) { model.pushStackElement(); - model.pushEditOperations([], edits.map((e) => EditOperation.replaceMove(Range.lift(e.range), e.text)), () => []); + model.pushEditOperations([], edits, () => []); model.pushStackElement(); totalFiles += 1; totalEdits += edits.length; - }); + } - return Promise.resolve({ - selection: undefined, + return { ariaSummary: strings.format(SimpleServicesNLS.bulkEditServiceSummary, totalEdits, totalFiles) - }); + }; } } diff --git a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts index f464ca8cec..9384fc649a 100644 --- a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts +++ b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts @@ -869,7 +869,7 @@ suite('viewLineRenderer.renderLine', () => { suite('viewLineRenderer.renderLine 2', () => { - function testCreateLineParts(fontIsMonospace: boolean, lineContent: string, tokens: ViewLineToken[], fauxIndentLength: number, renderWhitespace: 'none' | 'boundary' | 'selection' | 'all', selections: LineRange[] | null, expected: string): void { + function testCreateLineParts(fontIsMonospace: boolean, lineContent: string, tokens: ViewLineToken[], fauxIndentLength: number, renderWhitespace: 'none' | 'boundary' | 'selection' | 'trailing' | 'all', selections: LineRange[] | null, expected: string): void { let actual = renderViewLine(new RenderLineInput( fontIsMonospace, true, @@ -1355,6 +1355,95 @@ suite('viewLineRenderer.renderLine 2', () => { ); }); + test('createLineParts render whitespace for trailing with leading, inner, and without trailing whitespace', () => { + testCreateLineParts( + false, + ' Hello world!', + [ + createPart(4, 0), + createPart(6, 1), + createPart(14, 2) + ], + 0, + 'trailing', + null, + [ + '', + '\u00a0Hel', + 'lo', + '\u00a0world!', + '', + ].join('') + ); + }); + + test('createLineParts render whitespace for trailing with leading, inner, and trailing whitespace', () => { + testCreateLineParts( + false, + ' Hello world! \t', + [ + createPart(4, 0), + createPart(6, 1), + createPart(15, 2) + ], + 0, + 'trailing', + null, + [ + '', + '\u00a0Hel', + 'lo', + '\u00a0world!', + '\u00b7\u2192\u00a0', + '', + ].join('') + ); + }); + + test('createLineParts render whitespace for trailing with 8 leading and 8 trailing whitespaces', () => { + testCreateLineParts( + false, + ' Hello world! ', + [ + createPart(8, 1), + createPart(10, 2), + createPart(28, 3) + ], + 0, + 'trailing', + null, + [ + '', + '\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0', + 'He', + 'llo\u00a0world!', + '\u00b7\u00b7\u00b7\u00b7', + '\u00b7\u00b7\u00b7\u00b7', + '', + ].join('') + ); + }); + + test('createLineParts render whitespace for trailing with line containing only whitespaces', () => { + testCreateLineParts( + false, + ' \t ', + [ + createPart(2, 0), + createPart(3, 1), + ], + 0, + 'trailing', + null, + [ + '', + '\u00b7\u2192\u00a0\u00a0', + '\u00b7', + '', + ].join('') + ); + }); + test('createLineParts can handle unsorted inline decorations', () => { let actual = renderViewLine(new RenderLineInput( false, diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 5aeba3e474..86f3abf4d4 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -3068,7 +3068,7 @@ declare namespace monaco.editor { * Enable rendering of whitespace. * Defaults to none. */ - renderWhitespace?: 'none' | 'boundary' | 'selection' | 'all'; + renderWhitespace?: 'none' | 'boundary' | 'selection' | 'trailing' | 'all'; /** * Enable rendering of control characters. * Defaults to false. @@ -4056,7 +4056,7 @@ declare namespace monaco.editor { renderLineHighlight: IEditorOption; renderLineHighlightOnlyWhenFocus: IEditorOption; renderValidationDecorations: IEditorOption; - renderWhitespace: IEditorOption; + renderWhitespace: IEditorOption; revealHorizontalRightPadding: IEditorOption; roundedSelection: IEditorOption; rulers: IEditorOption; @@ -4418,6 +4418,14 @@ declare namespace monaco.editor { overflowWidgetsDomNode?: HTMLElement; } + export interface IDiffEditorConstructionOptions extends IDiffEditorOptions { + /** + * Place overflow widgets inside an external DOM node. + * Defaults to an internal DOM node. + */ + overflowWidgetsDomNode?: HTMLElement; + } + /** * A rich code editor. */ diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts index 4240854846..f3b8239fee 100644 --- a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts +++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts @@ -111,11 +111,11 @@ export function fillInActions(groups: ReadonlyArray<[string, ReadonlyArray(target) ? target : target.primary; + const to = Array.isArray(target) ? target : target.primary; to.unshift(...actions); } else { - const to = Array.isArray(target) ? target : target.secondary; + const to = Array.isArray(target) ? target : target.secondary; if (to.length > 0) { to.push(new Separator()); diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index f3dd7a35d8..31f52eea16 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -123,6 +123,8 @@ export class MenuId { static readonly NotebookCellInsert = new MenuId('NotebookCellInsert'); static readonly NotebookCellBetween = new MenuId('NotebookCellBetween'); static readonly NotebookCellListTop = new MenuId('NotebookCellTop'); + static readonly NotebookDiffCellMetadataTitle = new MenuId('NotebookDiffCellMetadataTitle'); + static readonly NotebookDiffCellOutputsTitle = new MenuId('NotebookDiffCellOutputsTitle'); static readonly BulkEditTitle = new MenuId('BulkEditTitle'); static readonly BulkEditContext = new MenuId('BulkEditContext'); static readonly ObjectExplorerItemContext = new MenuId('ObjectExplorerItemContext'); // {{SQL CARBON EDIT}} diff --git a/src/vs/platform/contextkey/browser/contextKeyService.ts b/src/vs/platform/contextkey/browser/contextKeyService.ts index f6ab855c5b..a13929e54c 100644 --- a/src/vs/platform/contextkey/browser/contextKeyService.ts +++ b/src/vs/platform/contextkey/browser/contextKeyService.ts @@ -9,7 +9,6 @@ import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContext, IContextKey, IContextKeyChangeEvent, IContextKeyService, IContextKeyServiceTarget, IReadableSet, SET_CONTEXT_COMMAND_ID, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingResolver } from 'vs/platform/keybinding/common/keybindingResolver'; -import { toArray } from 'vs/base/common/arrays'; const KEYBINDING_CONTEXT_ATTR = 'data-keybinding-context'; @@ -102,7 +101,7 @@ class ConfigAwareContextValuesContainer extends Context { this._listener = this._configurationService.onDidChangeConfiguration(event => { if (event.source === ConfigurationTarget.DEFAULT) { // new setting, reset everything - const allKeys = toArray(this._values.keys()); + const allKeys = Array.from(this._values.keys()); this._values.clear(); emitter.fire(new ArrayContextKeyChangeEvent(allKeys)); } else { diff --git a/src/vs/platform/contextkey/common/contextkey.ts b/src/vs/platform/contextkey/common/contextkey.ts index c1b3fd4c9b..a44abdd702 100644 --- a/src/vs/platform/contextkey/common/contextkey.ts +++ b/src/vs/platform/contextkey/common/contextkey.ts @@ -17,6 +17,8 @@ STATIC_VALUES.set('isWindows', isWindows); STATIC_VALUES.set('isWeb', isWeb); STATIC_VALUES.set('isMacNative', isMacintosh && !isWeb); +const hasOwnProperty = Object.prototype.hasOwnProperty; + export const enum ContextKeyExprType { False = 0, True = 1, @@ -28,8 +30,10 @@ export const enum ContextKeyExprType { Regex = 7, NotRegex = 8, Or = 9, - GreaterThanEquals = 10, // {{SQL CARBON EDIT}} add value - LessThanEquals = 11 // {{SQL CARBON EDIT}} add value + In = 10, + NotIn = 11, + GreaterThanEquals = 12, // {{SQL CARBON EDIT}} add value + LessThanEquals = 13 // {{SQL CARBON EDIT}} add value } export interface IContextKeyExprMapper { @@ -38,6 +42,7 @@ export interface IContextKeyExprMapper { mapEquals(key: string, value: any): ContextKeyExpression; mapNotEquals(key: string, value: any): ContextKeyExpression; mapRegex(key: string, regexp: RegExp | null): ContextKeyRegexExpr; + mapIn(key: string, valueKey: string): ContextKeyInExpr; } export interface IContextKeyExpression { @@ -54,7 +59,7 @@ export interface IContextKeyExpression { export type ContextKeyExpression = ( ContextKeyFalseExpr | ContextKeyTrueExpr | ContextKeyDefinedExpr | ContextKeyNotExpr | ContextKeyEqualsExpr | ContextKeyNotEqualsExpr | ContextKeyRegexExpr - | ContextKeyNotRegexExpr | ContextKeyAndExpr | ContextKeyOrExpr | ContextKeyGreaterThanEqualsExpr | ContextKeyLessThanEqualsExpr + | ContextKeyNotRegexExpr | ContextKeyAndExpr | ContextKeyOrExpr | ContextKeyInExpr | ContextKeyNotInExpr | ContextKeyGreaterThanEqualsExpr | ContextKeyLessThanEqualsExpr // {{ SQL CARBON EDIT }} ); export abstract class ContextKeyExpr { @@ -83,6 +88,10 @@ export abstract class ContextKeyExpr { return ContextKeyRegexExpr.create(key, value); } + public static in(key: string, value: string): ContextKeyExpression { + return ContextKeyInExpr.create(key, value); + } + public static not(key: string): ContextKeyExpression { return ContextKeyNotExpr.create(key); } @@ -151,6 +160,10 @@ export abstract class ContextKeyExpr { return ContextKeyLessThanEqualsExpr.create(pieces[0].trim(), this._deserializeValue(pieces[1], strict)); } // + if (serializedOne.indexOf(' in ') >= 0) { + let pieces = serializedOne.split(' in '); + return ContextKeyInExpr.create(pieces[0].trim(), pieces[1].trim()); + } if (/^\!\s*/.test(serializedOne)) { return ContextKeyNotExpr.create(serializedOne.substr(1).trim()); @@ -416,6 +429,122 @@ export class ContextKeyEqualsExpr implements IContextKeyExpression { } } +export class ContextKeyInExpr implements IContextKeyExpression { + + public static create(key: string, valueKey: string): ContextKeyInExpr { + return new ContextKeyInExpr(key, valueKey); + } + + public readonly type = ContextKeyExprType.In; + + private constructor(private readonly key: string, private readonly valueKey: string) { + } + + public cmp(other: ContextKeyExpression): number { + if (other.type !== this.type) { + return this.type - other.type; + } + if (this.key < other.key) { + return -1; + } + if (this.key > other.key) { + return 1; + } + if (this.valueKey < other.valueKey) { + return -1; + } + if (this.valueKey > other.valueKey) { + return 1; + } + return 0; + } + + public equals(other: ContextKeyExpression): boolean { + if (other.type === this.type) { + return (this.key === other.key && this.valueKey === other.valueKey); + } + return false; + } + + public evaluate(context: IContext): boolean { + const source = context.getValue(this.valueKey); + + const item = context.getValue(this.key); + + if (Array.isArray(source)) { + return (source.indexOf(item) >= 0); + } + + if (typeof item === 'string' && typeof source === 'object' && source !== null) { + return hasOwnProperty.call(source, item); + } + return false; + } + + public serialize(): string { + return this.key + ' in \'' + this.valueKey + '\''; + } + + public keys(): string[] { + return [this.key, this.valueKey]; + } + + public map(mapFnc: IContextKeyExprMapper): ContextKeyInExpr { + return mapFnc.mapIn(this.key, this.valueKey); + } + + public negate(): ContextKeyExpression { + return ContextKeyNotInExpr.create(this); + } +} + +export class ContextKeyNotInExpr implements IContextKeyExpression { + + public static create(actual: ContextKeyInExpr): ContextKeyNotInExpr { + return new ContextKeyNotInExpr(actual); + } + + public readonly type = ContextKeyExprType.NotIn; + + private constructor(private readonly _actual: ContextKeyInExpr) { + // + } + + public cmp(other: ContextKeyExpression): number { + if (other.type !== this.type) { + return this.type - other.type; + } + return this._actual.cmp(other._actual); + } + + public equals(other: ContextKeyExpression): boolean { + if (other.type === this.type) { + return this._actual.equals(other._actual); + } + return false; + } + + public evaluate(context: IContext): boolean { + return !this._actual.evaluate(context); + } + + public serialize(): string { + throw new Error('Method not implemented.'); + } + + public keys(): string[] { + return this._actual.keys(); + } + + public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression { + return new ContextKeyNotInExpr(this._actual.map(mapFnc)); + } + + public negate(): ContextKeyExpression { + return this._actual; + } +} + export class ContextKeyNotEqualsExpr implements IContextKeyExpression { public static create(key: string, value: any): ContextKeyExpression { diff --git a/src/vs/platform/contextkey/test/common/contextkey.test.ts b/src/vs/platform/contextkey/test/common/contextkey.test.ts index 19baf98d18..507a14dc35 100644 --- a/src/vs/platform/contextkey/test/common/contextkey.test.ts +++ b/src/vs/platform/contextkey/test/common/contextkey.test.ts @@ -162,4 +162,19 @@ suite('ContextKeyExpr', () => { t('a || b', 'c && d', 'a && c && d || b && c && d'); t('a || b', 'c && d || e', 'a && e || b && e || a && c && d || b && c && d'); }); + + test('ContextKeyInExpr', () => { + const ainb = ContextKeyExpr.deserialize('a in b')!; + assert.equal(ainb.evaluate(createContext({ 'a': 3, 'b': [3, 2, 1] })), true); + assert.equal(ainb.evaluate(createContext({ 'a': 3, 'b': [1, 2, 3] })), true); + assert.equal(ainb.evaluate(createContext({ 'a': 3, 'b': [1, 2] })), false); + assert.equal(ainb.evaluate(createContext({ 'a': 3 })), false); + assert.equal(ainb.evaluate(createContext({ 'a': 3, 'b': null })), false); + assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': ['x'] })), true); + assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': ['y'] })), false); + assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': {} })), false); + assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': { 'x': false } })), true); + assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': { 'x': true } })), true); + assert.equal(ainb.evaluate(createContext({ 'a': 'prototype', 'b': {} })), false); + }); }); diff --git a/src/vs/platform/credentials/node/credentialsService.ts b/src/vs/platform/credentials/node/credentialsService.ts index bf994e37e3..1a00e1b764 100644 --- a/src/vs/platform/credentials/node/credentialsService.ts +++ b/src/vs/platform/credentials/node/credentialsService.ts @@ -3,15 +3,15 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import type * as keytar from 'keytar'; import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; import { IdleValue } from 'vs/base/common/async'; -type KeytarModule = typeof import('keytar'); export class KeytarCredentialsService implements ICredentialsService { declare readonly _serviceBrand: undefined; - private readonly _keytar = new IdleValue>(() => import('keytar')); + private readonly _keytar = new IdleValue>(() => import('keytar')); async getPassword(service: string, account: string): Promise { const keytar = await this._keytar.value; diff --git a/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts b/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts new file mode 100644 index 0000000000..8725455e5d --- /dev/null +++ b/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts @@ -0,0 +1,109 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IOpenExtensionWindowResult } from 'vs/platform/debug/common/extensionHostDebug'; +import { IProcessEnvironment } from 'vs/base/common/platform'; +import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv'; +import { createServer, AddressInfo } from 'net'; +import { ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc'; +import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; +import { OpenContext } from 'vs/platform/windows/node/window'; + +export class ElectronExtensionHostDebugBroadcastChannel extends ExtensionHostDebugBroadcastChannel { + + constructor(private windowsMainService: IWindowsMainService) { + super(); + } + + call(ctx: TContext, command: string, arg?: any): Promise { + if (command === 'openExtensionDevelopmentHostWindow') { + return this.openExtensionDevelopmentHostWindow(arg[0], arg[1], arg[2]); + } else { + return super.call(ctx, command, arg); + } + } + + private async openExtensionDevelopmentHostWindow(args: string[], env: IProcessEnvironment, debugRenderer: boolean): Promise { + const pargs = parseArgs(args, OPTIONS); + const extDevPaths = pargs.extensionDevelopmentPath; + if (!extDevPaths) { + return {}; + } + + const [codeWindow] = this.windowsMainService.openExtensionDevelopmentHostWindow(extDevPaths, { + context: OpenContext.API, + cli: pargs, + userEnv: Object.keys(env).length > 0 ? env : undefined + }); + + if (!debugRenderer) { + return {}; + } + + const debug = codeWindow.win.webContents.debugger; + + let listeners = debug.isAttached() ? Infinity : 0; + const server = createServer(listener => { + if (listeners++ === 0) { + debug.attach(); + } + + let closed = false; + const writeMessage = (message: object) => { + if (!closed) { // in case sendCommand promises settle after closed + listener.write(JSON.stringify(message) + '\0'); // null-delimited, CDP-compatible + } + }; + + const onMessage = (_event: Event, method: string, params: unknown, sessionId?: string) => + writeMessage(({ method, params, sessionId })); + + codeWindow.win.on('close', () => { + debug.removeListener('message', onMessage); + listener.end(); + closed = true; + }); + + debug.addListener('message', onMessage); + + let buf = Buffer.alloc(0); + listener.on('data', data => { + buf = Buffer.concat([buf, data]); + for (let delimiter = buf.indexOf(0); delimiter !== -1; delimiter = buf.indexOf(0)) { + let data: { id: number; sessionId: string; params: {} }; + try { + const contents = buf.slice(0, delimiter).toString('utf8'); + buf = buf.slice(delimiter + 1); + data = JSON.parse(contents); + } catch (e) { + console.error('error reading cdp line', e); + } + + // depends on a new API for which electron.d.ts has not been updated: + // @ts-ignore + debug.sendCommand(data.method, data.params, data.sessionId) + .then((result: object) => writeMessage({ id: data.id, sessionId: data.sessionId, result })) + .catch((error: Error) => writeMessage({ id: data.id, sessionId: data.sessionId, error: { code: 0, message: error.message } })); + } + }); + + listener.on('error', err => { + console.error('error on cdp pipe:', err); + }); + + listener.on('close', () => { + closed = true; + if (--listeners === 0) { + debug.detach(); + } + }); + }); + + await new Promise(r => server.listen(0, r)); + codeWindow.win.on('close', () => server.close()); + + return { rendererDebugPort: (server.address() as AddressInfo).port }; + } +} diff --git a/src/vs/platform/editor/common/editor.ts b/src/vs/platform/editor/common/editor.ts index 6f9679ff90..de5a8e102b 100644 --- a/src/vs/platform/editor/common/editor.ts +++ b/src/vs/platform/editor/common/editor.ts @@ -18,6 +18,11 @@ export interface IEditorModel { */ load(): Promise; + /** + * Find out if this model has been disposed. + */ + isDisposed(): boolean; + /** * Dispose associated resources */ diff --git a/src/vs/platform/electron/common/electron.ts b/src/vs/platform/electron/common/electron.ts index 7fdd75d4e0..11859bd6c1 100644 --- a/src/vs/platform/electron/common/electron.ts +++ b/src/vs/platform/electron/common/electron.ts @@ -44,7 +44,15 @@ export interface ICommonElectronService { unmaximizeWindow(): Promise; minimizeWindow(): Promise; - focusWindow(options?: { windowId?: number }): Promise; + /** + * Make the window focused. + * + * @param options Pass `force: true` if you want to make the window take + * focus even if the application does not have focus currently. This option + * should only be used if it is necessary to steal focus from the current + * focused application which may not be VSCode. + */ + focusWindow(options?: { windowId?: number, force?: boolean }): Promise; // Dialogs showMessageBox(options: MessageBoxOptions): Promise; diff --git a/src/vs/platform/electron/electron-main/electronMainService.ts b/src/vs/platform/electron/electron-main/electronMainService.ts index ea197fd5c6..571a1f87d5 100644 --- a/src/vs/platform/electron/electron-main/electronMainService.ts +++ b/src/vs/platform/electron/electron-main/electronMainService.ts @@ -172,14 +172,14 @@ export class ElectronMainService implements IElectronMainService { } } - async focusWindow(windowId: number | undefined, options?: { windowId?: number; }): Promise { + async focusWindow(windowId: number | undefined, options?: { windowId?: number; force?: boolean; }): Promise { if (options && typeof options.windowId === 'number') { windowId = options.windowId; } const window = this.windowById(windowId); if (window) { - window.focus(); + window.focus({ force: options?.force ?? false }); } } diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index 4ccd502ab6..504d826912 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -23,7 +23,7 @@ export interface IEnvironmentService { // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // NOTE: DO NOT ADD ANY OTHER PROPERTY INTO THE COLLECTION HERE - // UNLESS THIS PROPERTY IS SUPPORTED BOTH IN WEB AND DESKTOP!!!! + // UNLESS THIS PROPERTY IS SUPPORTED BOTH IN WEB AND NATIVE!!!! // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! readonly _serviceBrand: undefined; @@ -70,6 +70,6 @@ export interface IEnvironmentService { // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // NOTE: DO NOT ADD ANY OTHER PROPERTY INTO THE COLLECTION HERE - // UNLESS THIS PROPERTY IS SUPPORTED BOTH IN WEB AND DESKTOP!!!! + // UNLESS THIS PROPERTY IS SUPPORTED BOTH IN WEB AND NATIVE!!!! // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! } diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 31f0e7e3c4..6027e5816b 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -24,6 +24,7 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag import { find } from 'vs/base/common/arrays'; import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId'; import { optional } from 'vs/platform/instantiation/common/instantiation'; +import { joinPath } from 'vs/base/common/resources'; interface IRawGalleryExtensionFile { assetType: string; @@ -121,7 +122,8 @@ const PropertyType = { Engine: 'Microsoft.VisualStudio.Code.Engine', // {{SQL CARBON EDIT}} AzDataEngine: 'Microsoft.AzDataEngine', - LocalizedLanguages: 'Microsoft.VisualStudio.Code.LocalizedLanguages' + LocalizedLanguages: 'Microsoft.VisualStudio.Code.LocalizedLanguages', + WebExtension: 'Microsoft.VisualStudio.Code.WebExtension' }; interface ICriterium { @@ -308,6 +310,17 @@ function getIsPreview(flags: string): boolean { return flags.indexOf('preview') !== -1; } +function getIsWebExtension(version: IRawGalleryExtensionVersion): boolean { + const webExtensionProperty = version.properties ? version.properties.find(p => p.key === PropertyType.WebExtension) : undefined; + return !!webExtensionProperty && webExtensionProperty.value === 'true'; +} + +function getWebResource(version: IRawGalleryExtensionVersion): URI | undefined { + return version.files.some(f => f.assetType.startsWith('Microsoft.VisualStudio.Code.WebResources')) + ? joinPath(URI.parse(version.assetUri), 'Microsoft.VisualStudio.Code.WebResources', 'extension') + : undefined; +} + function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGalleryExtensionVersion, index: number, query: Query, querySource?: string): IGalleryExtension { const assets = { manifest: getVersionAsset(version, AssetType.Manifest), @@ -339,6 +352,7 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller rating: getStatistic(galleryExtension.statistics, 'averagerating'), ratingCount: getStatistic(galleryExtension.statistics, 'ratingcount'), assetUri: URI.parse(version.assetUri), + webResource: getWebResource(version), assetTypes: version.files.map(({ assetType }) => assetType), assets, properties: { @@ -347,7 +361,8 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller engine: getEngine(version), // {{SQL CARBON EDIT}} azDataEngine: getAzureDataStudioEngine(version), - localizedLanguages: getLocalizedLanguages(version) + localizedLanguages: getLocalizedLanguages(version), + webExtension: getIsWebExtension(version) }, /* __GDPR__FRAGMENT__ "GalleryExtensionTelemetryData2" : { @@ -404,7 +419,17 @@ export class ExtensionGalleryService implements IExtensionGalleryService { return !!this.extensionsGalleryUrl; } - getCompatibleExtension(arg1: IExtensionIdentifier | IGalleryExtension, version?: string): Promise { + async getCompatibleExtension(arg1: IExtensionIdentifier | IGalleryExtension, version?: string): Promise { + const extension = await this.getCompatibleExtensionByEngine(arg1, version); + + if (extension?.properties.webExtension) { + return extension.webResource ? extension : null; + } else { + return extension; + } + } + + private getCompatibleExtensionByEngine(arg1: IExtensionIdentifier | IGalleryExtension, version?: string): Promise { const extension: IGalleryExtension | null = isIExtensionIdentifier(arg1) ? null : arg1; // {{SQL CARBON EDIT}} // Change to original version: removed the extension version validation diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 22ce48bed6..5f62c289bd 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -21,6 +21,7 @@ export interface IGalleryExtensionProperties { // {{SQL CARBON EDIT}} azDataEngine?: string; localizedLanguages?: string[]; + webExtension?: boolean; } export interface IGalleryExtensionAsset { @@ -87,6 +88,7 @@ export interface IGalleryExtension { properties: IGalleryExtensionProperties; telemetryData: any; preview: boolean; + webResource?: URI; } export interface IGalleryMetadata { @@ -208,6 +210,7 @@ export interface IExtensionManagementService { unzip(zipLocation: URI): Promise; getManifest(vsix: URI): Promise; install(vsix: URI, isMachineScoped?: boolean): Promise; + canInstall(extension: IGalleryExtension): Promise; installFromGallery(extension: IGalleryExtension, isMachineScoped?: boolean): Promise; uninstall(extension: ILocalExtension, force?: boolean): Promise; reinstallFromGallery(extension: ILocalExtension): Promise; @@ -243,6 +246,7 @@ export type IExecutableBasedExtensionTip = { readonly extensionId: string, readonly extensionName: string, readonly isExtensionPack: boolean, + readonly exeName: string, readonly exeFriendlyName: string, readonly windowsPath?: string, }; diff --git a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts index 8eab687c35..34df2a9945 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts @@ -63,6 +63,7 @@ export class ExtensionManagementChannel implements IServerChannel { case 'unzip': return this.service.unzip(transformIncomingURI(args[0], uriTransformer)); case 'install': return this.service.install(transformIncomingURI(args[0], uriTransformer)); case 'getManifest': return this.service.getManifest(transformIncomingURI(args[0], uriTransformer)); + case 'canInstall': return this.service.canInstall(args[0]); case 'installFromGallery': return this.service.installFromGallery(args[0]); case 'uninstall': return this.service.uninstall(transformIncomingExtension(args[0], uriTransformer), args[1]); case 'reinstallFromGallery': return this.service.reinstallFromGallery(transformIncomingExtension(args[0], uriTransformer)); @@ -104,6 +105,10 @@ export class ExtensionManagementChannelClient implements IExtensionManagementSer return Promise.resolve(this.channel.call('getManifest', [vsix])); } + async canInstall(extension: IGalleryExtension): Promise { + return true; + } + installFromGallery(extension: IGalleryExtension): Promise { return Promise.resolve(this.channel.call('installFromGallery', [extension])).then(local => transformIncomingExtension(local, null)); } diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 979a441420..b495d77449 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -253,6 +253,10 @@ export class ExtensionManagementService extends Disposable implements IExtension )); }*/ + async canInstall(extension: IGalleryExtension): Promise { + return true; + } + async installFromGallery(extension: IGalleryExtension, isMachineScoped?: boolean): Promise { if (!this.galleryService.isEnabled()) { return Promise.reject(new Error(nls.localize('MarketPlaceDisabled', "Marketplace is not enabled"))); diff --git a/src/vs/platform/extensionManagement/node/extensionTipsService.ts b/src/vs/platform/extensionManagement/node/extensionTipsService.ts index ca72dfa347..a9fad7e688 100644 --- a/src/vs/platform/extensionManagement/node/extensionTipsService.ts +++ b/src/vs/platform/extensionManagement/node/extensionTipsService.ts @@ -105,6 +105,7 @@ export class ExtensionTipsService extends BaseExtensionTipsService { extensionId, extensionName, isExtensionPack, + exeName, exeFriendlyName: extensionTip.exeFriendlyName, windowsPath: extensionTip.windowsPath, }); diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index 9aeb043559..a3f71f80f2 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -115,6 +115,11 @@ export interface ICodeActionContribution { readonly actions: readonly ICodeActionContributionAction[]; } +export interface IAuthenticationContribution { + readonly id: string; + readonly label: string; +} + export interface IExtensionContributions { commands?: ICommand[]; configuration?: IConfiguration | IConfiguration[]; @@ -133,6 +138,7 @@ export interface IExtensionContributions { localizations?: ILocalization[]; readonly customEditors?: readonly IWebviewEditor[]; readonly codeActions?: readonly ICodeActionContribution[]; + authentication?: IAuthenticationContribution[]; } export type ExtensionKind = 'ui' | 'workspace' | 'web'; @@ -277,6 +283,10 @@ export function isLanguagePackExtension(manifest: IExtensionManifest): boolean { return manifest.contributes && manifest.contributes.localizations ? manifest.contributes.localizations.length > 0 : false; } +export function isAuthenticaionProviderExtension(manifest: IExtensionManifest): boolean { + return manifest.contributes && manifest.contributes.authentication ? manifest.contributes.authentication.length > 0 : false; +} + export interface IScannedExtension { readonly identifier: IExtensionIdentifier; readonly location: URI; diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index f5f4dd9be4..0c5cbba541 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -867,7 +867,7 @@ export function whenProviderRegistered(file: URI, fileService: IFileService): Pr } /** - * Desktop only: limits for memory sizes + * Native only: limits for memory sizes */ export const MIN_MAX_MEMORY_SIZE_MB = 2048; export const FALLBACK_MAX_MEMORY_SIZE_MB = 4096; diff --git a/src/vs/platform/keybinding/common/abstractKeybindingService.ts b/src/vs/platform/keybinding/common/abstractKeybindingService.ts index c74ceb4c35..059e275bf5 100644 --- a/src/vs/platform/keybinding/common/abstractKeybindingService.ts +++ b/src/vs/platform/keybinding/common/abstractKeybindingService.ts @@ -17,6 +17,7 @@ import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKe import { INotificationService } from 'vs/platform/notification/common/notification'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions'; +import { ILogService } from 'vs/platform/log/common/log'; interface CurrentChord { keypress: string; @@ -34,6 +35,7 @@ export abstract class AbstractKeybindingService extends Disposable implements IK private _currentChord: CurrentChord | null; private _currentChordChecker: IntervalTimer; private _currentChordStatusMessage: IDisposable | null; + protected _logging: boolean; public get inChordMode(): boolean { return !!this._currentChord; @@ -44,12 +46,14 @@ export abstract class AbstractKeybindingService extends Disposable implements IK protected _commandService: ICommandService, protected _telemetryService: ITelemetryService, private _notificationService: INotificationService, + protected _logService: ILogService, ) { super(); this._currentChord = null; this._currentChordChecker = new IntervalTimer(); this._currentChordStatusMessage = null; + this._logging = false; } public dispose(): void { @@ -69,6 +73,19 @@ export abstract class AbstractKeybindingService extends Disposable implements IK return ''; } + public toggleLogging(): boolean { + this._logging = !this._logging; + return this._logging; + } + + protected _log(str: string): void { + if (this._logging) { + this._logService.info(`[KeybindingService]: ${str}`); + } else { + this._logService.trace(`[KeybindingService]: ${str}`); + } + } + public getDefaultKeybindings(): readonly ResolvedKeybindingItem[] { return this._getResolver().getDefaultKeybindings(); } @@ -168,6 +185,7 @@ export abstract class AbstractKeybindingService extends Disposable implements IK } const [firstPart,] = keybinding.getDispatchParts(); if (firstPart === null) { + this._log(`\\ Keyboard event cannot be dispatched.`); // cannot be dispatched, probably only modifier keys return shouldPreventDefault; } @@ -177,6 +195,8 @@ export abstract class AbstractKeybindingService extends Disposable implements IK const keypressLabel = keybinding.getLabel(); const resolveResult = this._getResolver().resolve(contextValue, currentChord, firstPart); + this._logService.trace('KeybindingService#dispatch', keypressLabel, resolveResult?.commandId); + if (resolveResult && resolveResult.enterChord) { shouldPreventDefault = true; this._enterChordMode(firstPart, keypressLabel); diff --git a/src/vs/platform/keybinding/common/keybinding.ts b/src/vs/platform/keybinding/common/keybinding.ts index 876d804532..b086e6e83d 100644 --- a/src/vs/platform/keybinding/common/keybinding.ts +++ b/src/vs/platform/keybinding/common/keybinding.ts @@ -103,6 +103,8 @@ export interface IKeybindingService { registerSchemaContribution(contribution: KeybindingsSchemaContribution): void; + toggleLogging(): boolean; + _dumpDebugInfo(): string; _dumpDebugInfoJSON(): string; } diff --git a/src/vs/platform/keybinding/common/keybindingResolver.ts b/src/vs/platform/keybinding/common/keybindingResolver.ts index f5a39b939a..ff38c6e1ec 100644 --- a/src/vs/platform/keybinding/common/keybindingResolver.ts +++ b/src/vs/platform/keybinding/common/keybindingResolver.ts @@ -20,13 +20,19 @@ export interface IResolveResult { } export class KeybindingResolver { + private readonly _log: (str: string) => void; private readonly _defaultKeybindings: ResolvedKeybindingItem[]; private readonly _keybindings: ResolvedKeybindingItem[]; private readonly _defaultBoundCommands: Map; private readonly _map: Map; private readonly _lookupMap: Map; - constructor(defaultKeybindings: ResolvedKeybindingItem[], overrides: ResolvedKeybindingItem[]) { + constructor( + defaultKeybindings: ResolvedKeybindingItem[], + overrides: ResolvedKeybindingItem[], + log: (str: string) => void + ) { + this._log = log; this._defaultKeybindings = defaultKeybindings; this._defaultBoundCommands = new Map(); @@ -254,6 +260,7 @@ export class KeybindingResolver { } public resolve(context: IContext, currentChord: string | null, keypress: string): IResolveResult | null { + this._log(`| Resolving ${keypress}${currentChord ? ` chorded from ${currentChord}` : ``}`); let lookupMap: ResolvedKeybindingItem[] | null = null; if (currentChord !== null) { @@ -262,6 +269,7 @@ export class KeybindingResolver { const candidates = this._map.get(currentChord); if (typeof candidates === 'undefined') { // No chords starting with `currentChord` + this._log(`\\ No keybinding entries.`); return null; } @@ -277,6 +285,7 @@ export class KeybindingResolver { const candidates = this._map.get(keypress); if (typeof candidates === 'undefined') { // No bindings with `keypress` + this._log(`\\ No keybinding entries.`); return null; } @@ -285,11 +294,13 @@ export class KeybindingResolver { let result = this._findCommand(context, lookupMap); if (!result) { + this._log(`\\ From ${lookupMap.length} keybinding entries, no when clauses matched the context.`); return null; } // TODO@chords if (currentChord === null && result.keypressParts.length > 1 && result.keypressParts[1] !== null) { + this._log(`\\ From ${lookupMap.length} keybinding entries, matched chord, when: ${printWhenExplanation(result.when)}, source: ${printSourceExplanation(result)}.`); return { enterChord: true, leaveChord: false, @@ -299,6 +310,7 @@ export class KeybindingResolver { }; } + this._log(`\\ From ${lookupMap.length} keybinding entries, matched ${result.command}, when: ${printWhenExplanation(result.when)}, source: ${printSourceExplanation(result)}.`); return { enterChord: false, leaveChord: result.keypressParts.length > 1, @@ -362,3 +374,23 @@ export class KeybindingResolver { return unboundCommands; } } + +function printWhenExplanation(when: ContextKeyExpression | undefined): string { + if (!when) { + return `no when condition`; + } + return `${when.serialize()}`; +} + +function printSourceExplanation(kb: ResolvedKeybindingItem): string { + if (kb.isDefault) { + if (kb.extensionId) { + return `built-in extension ${kb.extensionId}`; + } + return `built-in`; + } + if (kb.extensionId) { + return `user extension ${kb.extensionId}`; + } + return `user`; +} diff --git a/src/vs/platform/keybinding/common/keybindingsRegistry.ts b/src/vs/platform/keybinding/common/keybindingsRegistry.ts index ec9eb0fcec..0b9617c34c 100644 --- a/src/vs/platform/keybinding/common/keybindingsRegistry.ts +++ b/src/vs/platform/keybinding/common/keybindingsRegistry.ts @@ -16,6 +16,7 @@ export interface IKeybindingItem { when: ContextKeyExpression | null | undefined; weight1: number; weight2: number; + extensionId: string | null; } export interface IKeybindings { @@ -51,6 +52,7 @@ export interface IKeybindingRule2 { args?: any; weight: number; when: ContextKeyExpression | undefined; + extensionId?: string; } export const enum KeybindingWeight { @@ -161,7 +163,8 @@ class KeybindingsRegistryImpl implements IKeybindingsRegistry { commandArgs: rule.args, when: rule.when, weight1: rule.weight, - weight2: 0 + weight2: 0, + extensionId: rule.extensionId || null }; } } @@ -219,7 +222,8 @@ class KeybindingsRegistryImpl implements IKeybindingsRegistry { commandArgs: commandArgs, when: when, weight1: weight1, - weight2: weight2 + weight2: weight2, + extensionId: null }); this._cachedMergedKeybindings = null; } diff --git a/src/vs/platform/keybinding/common/resolvedKeybindingItem.ts b/src/vs/platform/keybinding/common/resolvedKeybindingItem.ts index 0eea9c04b3..e80343deb5 100644 --- a/src/vs/platform/keybinding/common/resolvedKeybindingItem.ts +++ b/src/vs/platform/keybinding/common/resolvedKeybindingItem.ts @@ -17,8 +17,9 @@ export class ResolvedKeybindingItem { public readonly commandArgs: any; public readonly when: ContextKeyExpression | undefined; public readonly isDefault: boolean; + public readonly extensionId: string | null; - constructor(resolvedKeybinding: ResolvedKeybinding | undefined, command: string | null, commandArgs: any, when: ContextKeyExpression | undefined, isDefault: boolean) { + constructor(resolvedKeybinding: ResolvedKeybinding | undefined, command: string | null, commandArgs: any, when: ContextKeyExpression | undefined, isDefault: boolean, extensionId: string | null) { this.resolvedKeybinding = resolvedKeybinding; this.keypressParts = resolvedKeybinding ? removeElementsAfterNulls(resolvedKeybinding.getDispatchParts()) : []; this.bubble = (command ? command.charCodeAt(0) === CharCode.Caret : false); @@ -26,6 +27,7 @@ export class ResolvedKeybindingItem { this.commandArgs = commandArgs; this.when = when; this.isDefault = isDefault; + this.extensionId = extensionId; } } diff --git a/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts b/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts index e7972286d5..9d64be861e 100644 --- a/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts +++ b/src/vs/platform/keybinding/test/common/abstractKeybindingService.test.ts @@ -16,6 +16,7 @@ import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayo import { INotification, INotificationService, IPromptChoice, IPromptOptions, NoOpNotification, IStatusMessageOptions } from 'vs/platform/notification/common/notification'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { Disposable } from 'vs/base/common/lifecycle'; +import { NullLogService } from 'vs/platform/log/common/log'; function createContext(ctx: any) { return { @@ -36,7 +37,7 @@ suite('AbstractKeybindingService', () => { commandService: ICommandService, notificationService: INotificationService ) { - super(contextKeyService, commandService, NullTelemetryService, notificationService); + super(contextKeyService, commandService, NullTelemetryService, notificationService, new NullLogService()); this._resolver = resolver; } @@ -167,7 +168,7 @@ suite('AbstractKeybindingService', () => { setFilter() { } }; - let resolver = new KeybindingResolver(items, []); + let resolver = new KeybindingResolver(items, [], () => { }); return new TestKeybindingService(resolver, contextKeyService, commandService, notificationService); }; @@ -189,7 +190,8 @@ suite('AbstractKeybindingService', () => { command, null, when, - true + true, + null ); } diff --git a/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts b/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts index 03fbbf8fe3..fe0c22a9ed 100644 --- a/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts +++ b/src/vs/platform/keybinding/test/common/keybindingResolver.test.ts @@ -27,7 +27,8 @@ suite('KeybindingResolver', () => { command, commandArgs, when, - isDefault + isDefault, + null ); } @@ -44,7 +45,7 @@ suite('KeybindingResolver', () => { assert.equal(KeybindingResolver.contextMatchesRules(createContext({ bar: 'baz' }), contextRules), true); assert.equal(KeybindingResolver.contextMatchesRules(createContext({ bar: 'bz' }), contextRules), false); - let resolver = new KeybindingResolver([keybindingItem], []); + let resolver = new KeybindingResolver([keybindingItem], [], () => { }); assert.equal(resolver.resolve(createContext({ bar: 'baz' }), null, getDispatchStr(runtimeKeybinding))!.commandId, 'yes'); assert.equal(resolver.resolve(createContext({ bar: 'bz' }), null, getDispatchStr(runtimeKeybinding)), null); }); @@ -56,7 +57,7 @@ suite('KeybindingResolver', () => { let contextRules = ContextKeyExpr.equals('bar', 'baz'); let keybindingItem = kbItem(keybinding, 'yes', commandArgs, contextRules, true); - let resolver = new KeybindingResolver([keybindingItem], []); + let resolver = new KeybindingResolver([keybindingItem], [], () => { }); assert.equal(resolver.resolve(createContext({ bar: 'baz' }), null, getDispatchStr(runtimeKeybinding))!.commandArgs, commandArgs); }); @@ -307,7 +308,7 @@ suite('KeybindingResolver', () => { ) ]; - let resolver = new KeybindingResolver(items, []); + let resolver = new KeybindingResolver(items, [], () => { }); let testKey = (commandId: string, expectedKeys: number[]) => { // Test lookup diff --git a/src/vs/platform/keybinding/test/common/mockKeybindingService.ts b/src/vs/platform/keybinding/test/common/mockKeybindingService.ts index 4e579ee84c..6def07f24f 100644 --- a/src/vs/platform/keybinding/test/common/mockKeybindingService.ts +++ b/src/vs/platform/keybinding/test/common/mockKeybindingService.ts @@ -136,6 +136,10 @@ export class MockKeybindingService implements IKeybindingService { return false; } + public toggleLogging(): boolean { + return false; + } + public _dumpDebugInfo(): string { return ''; } diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index aeff2e83ca..1dd823e728 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -452,7 +452,6 @@ abstract class ResourceNavigator extends Disposable { super(); this.openOnFocus = options?.openOnFocus ?? false; - this.openOnSingleClick = options?.openOnSingleClick ?? true; this._register(Event.filter(this.widget.onDidChangeSelection, e => e.browserEvent instanceof KeyboardEvent)(e => this.onSelectionFromKeyboard(e))); this._register(this.widget.onPointer((e: { browserEvent: MouseEvent }) => this.onPointer(e.browserEvent))); @@ -463,9 +462,12 @@ abstract class ResourceNavigator extends Disposable { } if (typeof options?.openOnSingleClick !== 'boolean' && options?.configurationService) { + this.openOnSingleClick = options?.configurationService!.getValue(openModeSettingKey) !== 'doubleClick'; this._register(options?.configurationService.onDidChangeConfiguration(() => { this.openOnSingleClick = options?.configurationService!.getValue(openModeSettingKey) !== 'doubleClick'; })); + } else { + this.openOnSingleClick = options?.openOnSingleClick ?? true; } } @@ -493,15 +495,19 @@ abstract class ResourceNavigator extends Disposable { } private onPointer(browserEvent: MouseEvent): void { + if (!this.openOnSingleClick) { + return; + } + const isDoubleClick = browserEvent.detail === 2; - if (!this.openOnSingleClick && !isDoubleClick) { + if (isDoubleClick) { return; } const isMiddleClick = browserEvent.button === 1; - const preserveFocus = !isDoubleClick; - const pinned = isDoubleClick || isMiddleClick; + const preserveFocus = true; + const pinned = isMiddleClick; const sideBySide = browserEvent.ctrlKey || browserEvent.metaKey || browserEvent.altKey; this._open(preserveFocus, pinned, sideBySide, browserEvent); diff --git a/src/vs/platform/menubar/electron-main/menubar.ts b/src/vs/platform/menubar/electron-main/menubar.ts index 42a350171e..ef5ffa7ffb 100644 --- a/src/vs/platform/menubar/electron-main/menubar.ts +++ b/src/vs/platform/menubar/electron-main/menubar.ts @@ -7,15 +7,15 @@ import * as nls from 'vs/nls'; import { isMacintosh, language } from 'vs/base/common/platform'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { app, shell, Menu, MenuItem, BrowserWindow, MenuItemConstructorOptions, WebContents, Event, KeyboardEvent } from 'electron'; -import { getTitleBarStyle, IWindowOpenable } from 'vs/platform/windows/common/windows'; -import { OpenContext, IRunActionInWindowRequest, IRunKeybindingInWindowRequest } from 'vs/platform/windows/node/window'; +import { getTitleBarStyle, INativeRunActionInWindowRequest, INativeRunKeybindingInWindowRequest, IWindowOpenable } from 'vs/platform/windows/common/windows'; +import { OpenContext } from 'vs/platform/windows/node/window'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IUpdateService, StateType } from 'vs/platform/update/common/update'; import product from 'vs/platform/product/common/product'; import { RunOnceScheduler } from 'vs/base/common/async'; import { ILogService } from 'vs/platform/log/common/log'; -import { mnemonicMenuLabel as baseMnemonicLabel } from 'vs/base/common/labels'; +import { mnemonicMenuLabel } from 'vs/base/common/labels'; import { IWindowsMainService, IWindowsCountChangedEvent } from 'vs/platform/windows/electron-main/windows'; import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; import { IMenubarData, IMenubarKeybinding, MenubarMenuItem, isMenubarMenuItemSeparator, isMenubarMenuItemSubmenu, isMenubarMenuItemAction, IMenubarMenu, isMenubarMenuItemUriAction } from 'vs/platform/menubar/common/menubar'; @@ -755,9 +755,11 @@ export class Menubar { } if (invocation.type === 'commandId') { - activeWindow.sendWhenReady('vscode:runAction', { id: invocation.commandId, from: 'menu' } as IRunActionInWindowRequest); + const runActionPayload: INativeRunActionInWindowRequest = { id: invocation.commandId, from: 'menu' }; + activeWindow.sendWhenReady('vscode:runAction', runActionPayload); } else { - activeWindow.sendWhenReady('vscode:runKeybinding', { userSettingsLabel: invocation.userSettingsLabel } as IRunKeybindingInWindowRequest); + const runKeybindingPayload: INativeRunKeybindingInWindowRequest = { userSettingsLabel: invocation.userSettingsLabel }; + activeWindow.sendWhenReady('vscode:runKeybinding', runKeybindingPayload); } } else { this.logService.trace('menubar#runActionInRenderer: no active window found', invocation); @@ -822,7 +824,7 @@ export class Menubar { } private mnemonicLabel(label: string): string { - return baseMnemonicLabel(label, !this.currentEnableMenuBarMnemonics); + return mnemonicMenuLabel(label, !this.currentEnableMenuBarMnemonics); } } diff --git a/src/vs/platform/product/common/productService.ts b/src/vs/platform/product/common/productService.ts index d0c51c7fd4..1edb37520b 100644 --- a/src/vs/platform/product/common/productService.ts +++ b/src/vs/platform/product/common/productService.ts @@ -71,7 +71,7 @@ export interface IProductConfiguration { }; readonly extensionTips?: { [id: string]: string; }; - readonly extensionImportantTips?: { [id: string]: { name: string; pattern: string; isExtensionPack?: boolean }; }; + readonly extensionImportantTips?: IStringDictionary; readonly configBasedExtensionTips?: { [id: string]: IConfigBasedExtensionTip; }; readonly exeBasedExtensionTips?: { [id: string]: IExeBasedExtensionTip; }; readonly remoteExtensionTips?: { [remoteName: string]: IRemoteExtensionTip; }; @@ -133,6 +133,8 @@ export interface IProductConfiguration { readonly 'configurationSync.store'?: ConfigurationSyncStore; } +export type ImportantExtensionTip = { name: string; languages?: string[]; pattern?: string; isExtensionPack?: boolean }; + export interface IAppCenterConfiguration { readonly 'win32-ia32': string; readonly 'win32-x64': string; diff --git a/src/vs/platform/remote/common/remoteAgentConnection.ts b/src/vs/platform/remote/common/remoteAgentConnection.ts index cdcd3ad326..545b525aa8 100644 --- a/src/vs/platform/remote/common/remoteAgentConnection.ts +++ b/src/vs/platform/remote/common/remoteAgentConnection.ts @@ -16,6 +16,9 @@ import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async import { ILogService } from 'vs/platform/log/common/log'; import { IIPCLogger } from 'vs/base/parts/ipc/common/ipc'; +const INITIAL_CONNECT_TIMEOUT = 120 * 1000 /* 120s */; +const RECONNECT_TIMEOUT = 30 * 1000 /* 30s */; + export const enum ConnectionType { Management = 1, ExtensionHost = 2, @@ -277,7 +280,7 @@ export async function connectRemoteAgentManagement(options: IConnectionOptions, try { const reconnectionToken = generateUuid(); const simpleOptions = await resolveConnectionOptions(options, reconnectionToken, null); - const { protocol } = await connectWithTimeLimit(simpleOptions.logService, doConnectRemoteAgentManagement(simpleOptions), 30 * 1000 /*30s*/); + const { protocol } = await connectWithTimeLimit(simpleOptions.logService, doConnectRemoteAgentManagement(simpleOptions), INITIAL_CONNECT_TIMEOUT); return new ManagementPersistentConnection(options, remoteAuthority, clientId, reconnectionToken, protocol); } catch (err) { options.logService.error(`[remote-connection] An error occurred in the very first connect attempt, it will be treated as a permanent error! Error:`); @@ -291,7 +294,7 @@ export async function connectRemoteAgentExtensionHost(options: IConnectionOption try { const reconnectionToken = generateUuid(); const simpleOptions = await resolveConnectionOptions(options, reconnectionToken, null); - const { protocol, debugPort } = await connectWithTimeLimit(simpleOptions.logService, doConnectRemoteAgentExtensionHost(simpleOptions, startArguments), 30 * 1000 /*30s*/); + const { protocol, debugPort } = await connectWithTimeLimit(simpleOptions.logService, doConnectRemoteAgentExtensionHost(simpleOptions, startArguments), INITIAL_CONNECT_TIMEOUT); return new ExtensionHostPersistentConnection(options, startArguments, reconnectionToken, protocol, debugPort); } catch (err) { options.logService.error(`[remote-connection] An error occurred in the very first connect attempt, it will be treated as a permanent error! Error:`); @@ -303,7 +306,7 @@ export async function connectRemoteAgentExtensionHost(options: IConnectionOption export async function connectRemoteAgentTunnel(options: IConnectionOptions, tunnelRemotePort: number): Promise { const simpleOptions = await resolveConnectionOptions(options, generateUuid(), null); - const protocol = await connectWithTimeLimit(simpleOptions.logService, doConnectRemoteAgentTunnel(simpleOptions, { port: tunnelRemotePort }), 30 * 1000 /*30s*/); + const protocol = await connectWithTimeLimit(simpleOptions.logService, doConnectRemoteAgentTunnel(simpleOptions, { port: tunnelRemotePort }), INITIAL_CONNECT_TIMEOUT); return protocol; } @@ -434,7 +437,7 @@ abstract class PersistentConnection extends Disposable { this._options.logService.info(`${logPrefix} resolving connection...`); const simpleOptions = await resolveConnectionOptions(this._options, this.reconnectionToken, this.protocol); this._options.logService.info(`${logPrefix} connecting to ${simpleOptions.host}:${simpleOptions.port}...`); - await connectWithTimeLimit(simpleOptions.logService, this._reconnect(simpleOptions), 30 * 1000 /*30s*/); + await connectWithTimeLimit(simpleOptions.logService, this._reconnect(simpleOptions), RECONNECT_TIMEOUT); this._options.logService.info(`${logPrefix} reconnected!`); this._onDidStateChange.fire(new ConnectionGainEvent()); @@ -453,24 +456,24 @@ abstract class PersistentConnection extends Disposable { break; } if (RemoteAuthorityResolverError.isTemporarilyNotAvailable(err)) { - this._options.logService.info(`${logPrefix} A temporarily not available error occured while trying to reconnect, will try again...`); + this._options.logService.info(`${logPrefix} A temporarily not available error occurred while trying to reconnect, will try again...`); this._options.logService.trace(err); // try again! continue; } if ((err.code === 'ETIMEDOUT' || err.code === 'ENETUNREACH' || err.code === 'ECONNREFUSED' || err.code === 'ECONNRESET') && err.syscall === 'connect') { - this._options.logService.info(`${logPrefix} A network error occured while trying to reconnect, will try again...`); + this._options.logService.info(`${logPrefix} A network error occurred while trying to reconnect, will try again...`); this._options.logService.trace(err); // try again! continue; } if (isPromiseCanceledError(err)) { - this._options.logService.info(`${logPrefix} A promise cancelation error occured while trying to reconnect, will try again...`); + this._options.logService.info(`${logPrefix} A promise cancelation error occurred while trying to reconnect, will try again...`); this._options.logService.trace(err); // try again! continue; } - this._options.logService.error(`${logPrefix} An unknown error occured while trying to reconnect, since this is an unknown case, it will be treated as a permanent error! Will give up now! Error:`); + this._options.logService.error(`${logPrefix} An unknown error occurred while trying to reconnect, since this is an unknown case, it will be treated as a permanent error! Will give up now! Error:`); this._options.logService.error(err); PersistentConnection.triggerPermanentFailure(); break; diff --git a/src/vs/platform/storage/common/storage.ts b/src/vs/platform/storage/common/storage.ts index a5f0312641..20fbd11622 100644 --- a/src/vs/platform/storage/common/storage.ts +++ b/src/vs/platform/storage/common/storage.ts @@ -240,6 +240,8 @@ export class InMemoryStorageService extends Disposable implements IStorageServic isNew(): boolean { return true; // always new when in-memory } + + async close(): Promise { } } export async function logStorage(global: Map, workspace: Map, globalPath: string, workspacePath: string): Promise { diff --git a/src/vs/platform/telemetry/common/telemetryService.ts b/src/vs/platform/telemetry/common/telemetryService.ts index 3f23651387..c2edc7567f 100644 --- a/src/vs/platform/telemetry/common/telemetryService.ts +++ b/src/vs/platform/telemetry/common/telemetryService.ts @@ -89,7 +89,7 @@ export class TelemetryService implements ITelemetryService { } private _updateUserOptIn(): void { - const config = this._configurationService.getValue(TELEMETRY_SECTION_ID); + const config = this._configurationService?.getValue(TELEMETRY_SECTION_ID); this._userOptIn = config ? config.enableTelemetry : this._userOptIn; } diff --git a/src/vs/platform/theme/electron-main/themeMainService.ts b/src/vs/platform/theme/electron-main/themeMainService.ts index 83fad91610..b295e57456 100644 --- a/src/vs/platform/theme/electron-main/themeMainService.ts +++ b/src/vs/platform/theme/electron-main/themeMainService.ts @@ -42,14 +42,14 @@ export class ThemeMainService implements IThemeMainService { } getBackgroundColor(): string { - if (isWindows && nativeTheme.shouldUseInvertedColorScheme) { + if ((isWindows || isMacintosh) && nativeTheme.shouldUseInvertedColorScheme) { return DEFAULT_BG_HC_BLACK; } let background = this.stateService.getItem(THEME_BG_STORAGE_KEY, null); if (!background) { let baseTheme: string; - if (isWindows && nativeTheme.shouldUseInvertedColorScheme) { + if ((isWindows || isMacintosh) && nativeTheme.shouldUseInvertedColorScheme) { baseTheme = 'hc-black'; } else { baseTheme = this.stateService.getItem(THEME_STORAGE_KEY, 'vs-dark').split(' ')[0]; diff --git a/src/vs/platform/userDataSync/common/keybindingsSync.ts b/src/vs/platform/userDataSync/common/keybindingsSync.ts index 54b9de375b..b6e122bd1e 100644 --- a/src/vs/platform/userDataSync/common/keybindingsSync.ts +++ b/src/vs/platform/userDataSync/common/keybindingsSync.ts @@ -38,7 +38,7 @@ interface IKeybindingsResourcePreview extends IFileResourcePreview { export function getKeybindingsContentFromSyncContent(syncContent: string, platformSpecific: boolean): string | null { const parsed = JSON.parse(syncContent); - if (platformSpecific) { + if (!platformSpecific) { return isUndefined(parsed.all) ? null : parsed.all; } switch (OS) { diff --git a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts index 40a582d2ec..a74e6467c6 100644 --- a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts @@ -131,8 +131,8 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i this._register(userDataSyncAccountService.onDidChangeAccount(() => this.updateAutoSync())); this._register(userDataSyncStoreService.onDidChangeDonotMakeRequestsUntil(() => this.updateAutoSync())); - this._register(Event.debounce(userDataSyncService.onDidChangeLocal, (last, source) => last ? [...last, source] : [source], 1000)(sources => this.triggerSync(sources, false))); - this._register(Event.filter(this.userDataSyncResourceEnablementService.onDidChangeResourceEnablement, ([, enabled]) => enabled)(() => this.triggerSync(['resourceEnablement'], false))); + this._register(Event.debounce(userDataSyncService.onDidChangeLocal, (last, source) => last ? [...last, source] : [source], 1000)(sources => this.triggerSync(sources, false, false))); + this._register(Event.filter(this.userDataSyncResourceEnablementService.onDidChangeResourceEnablement, ([, enabled]) => enabled)(() => this.triggerSync(['resourceEnablement'], false, false))); } } @@ -320,7 +320,7 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i } private sources: string[] = []; - async triggerSync(sources: string[], skipIfSyncedRecently: boolean): Promise { + async triggerSync(sources: string[], skipIfSyncedRecently: boolean, disableCache: boolean): Promise { if (this.autoSync.value === undefined) { return this.syncTriggerDelayer.cancel(); } @@ -337,7 +337,7 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i this.telemetryService.publicLog2<{ sources: string[] }, AutoSyncClassification>('sync/triggered', { sources: this.sources }); this.sources = []; if (this.autoSync.value) { - await this.autoSync.value.sync('Activity'); + await this.autoSync.value.sync('Activity', disableCache); } }, this.successiveFailures ? this.getSyncTriggerDelayTime() * 1 * Math.min(Math.pow(2, this.successiveFailures), 60) /* Delay exponentially until max 1 minute */ @@ -393,14 +393,14 @@ class AutoSync extends Disposable { this.logService.info('Auto Sync: Stopped'); })); this.logService.info('Auto Sync: Started'); - this.sync(AutoSync.INTERVAL_SYNCING); + this.sync(AutoSync.INTERVAL_SYNCING, false); } private waitUntilNextIntervalAndSync(): void { - this.intervalHandler.value = disposableTimeout(() => this.sync(AutoSync.INTERVAL_SYNCING), this.interval); + this.intervalHandler.value = disposableTimeout(() => this.sync(AutoSync.INTERVAL_SYNCING, false), this.interval); } - sync(reason: string): Promise { + sync(reason: string, disableCache: boolean): Promise { const syncPromise = createCancelablePromise(async token => { if (this.syncPromise) { try { @@ -414,7 +414,7 @@ class AutoSync extends Disposable { } } } - return this.doSync(reason, token); + return this.doSync(reason, disableCache, token); }); this.syncPromise = syncPromise; this.syncPromise.finally(() => this.syncPromise = undefined); @@ -435,12 +435,12 @@ class AutoSync extends Disposable { !isEqual(current.stableUrl, previous.stableUrl)); } - private async doSync(reason: string, token: CancellationToken): Promise { + private async doSync(reason: string, disableCache: boolean, token: CancellationToken): Promise { this.logService.info(`Auto Sync: Triggered by ${reason}`); this._onDidStartSync.fire(); let error: Error | undefined; try { - this.syncTask = await this.userDataSyncService.createSyncTask(); + this.syncTask = await this.userDataSyncService.createSyncTask(disableCache); if (token.isCancellationRequested) { return; } diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index cbbc1fcae9..b3c7a37214 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -38,7 +38,6 @@ export function getDefaultIgnoredSettings(): string[] { export function registerConfiguration(): IDisposable { const ignoredSettingsSchemaId = 'vscode://schemas/ignoredSettings'; - const ignoredExtensionsSchemaId = 'vscode://schemas/ignoredExtensions'; const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); configurationRegistry.registerConfiguration({ id: 'settingsSync', @@ -60,7 +59,11 @@ export function registerConfiguration(): IDisposable { 'settingsSync.ignoredExtensions': { 'type': 'array', markdownDescription: localize('settingsSync.ignoredExtensions', "List of extensions to be ignored while synchronizing. The identifier of an extension is always `${publisher}.${name}`. For example: `vscode.csharp`."), - $ref: ignoredExtensionsSchemaId, + items: [{ + type: 'string', + pattern: EXTENSION_IDENTIFIER_PATTERN, + errorMessage: localize('app.extension.identifier.errorMessage', "Expected format '${publisher}.${name}'. Example: 'vscode.csharp'.") + }], 'default': [], 'scope': ConfigurationScope.APPLICATION, uniqueItems: true, @@ -102,11 +105,6 @@ export function registerConfiguration(): IDisposable { }; jsonRegistry.registerSchema(ignoredSettingsSchemaId, ignoredSettingsSchema); }; - jsonRegistry.registerSchema(ignoredExtensionsSchemaId, { - type: 'string', - pattern: EXTENSION_IDENTIFIER_PATTERN, - errorMessage: localize('app.extension.identifier.errorMessage', "Expected format '${publisher}.${name}'. Example: 'vscode.csharp'.") - }); return configurationRegistry.onDidUpdateConfiguration(() => registerIgnoredSettingsSchema()); } @@ -437,7 +435,7 @@ export interface IUserDataSyncService { readonly onDidResetRemote: Event; readonly onDidResetLocal: Event; - createSyncTask(): Promise; + createSyncTask(disableCache?: boolean): Promise; createManualSyncTask(): Promise; replace(uri: URI): Promise; @@ -465,7 +463,7 @@ export interface IUserDataAutoSyncService { canToggleEnablement(): boolean; turnOn(): Promise; turnOff(everywhere: boolean): Promise; - triggerSync(sources: string[], hasToLimitSync: boolean): Promise; + triggerSync(sources: string[], hasToLimitSync: boolean, disableCache: boolean): Promise; } export const IUserDataSyncUtilService = createDecorator('IUserDataSyncUtilService'); diff --git a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts index 9d7ffadfe8..358839002d 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts @@ -128,7 +128,7 @@ export class UserDataAutoSyncChannel implements IServerChannel { call(context: any, command: string, args?: any): Promise { switch (command) { - case 'triggerSync': return this.service.triggerSync(args[0], args[1]); + case 'triggerSync': return this.service.triggerSync(args[0], args[1], args[2]); case 'turnOn': return this.service.turnOn(); case 'turnOff': return this.service.turnOff(args[0]); } diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index b00b912a03..db532b8837 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -106,13 +106,17 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ this.onDidChangeLocal = Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeLocal, () => s.resource))); } - async createSyncTask(): Promise { + async createSyncTask(disableCache?: boolean): Promise { await this.checkEnablement(); const executionId = generateUuid(); let manifest: IUserDataManifest | null; try { - manifest = await this.userDataSyncStoreService.manifest(createSyncHeaders(executionId)); + const syncHeaders = createSyncHeaders(executionId); + if (disableCache) { + syncHeaders['Cache-Control'] = 'no-cache'; + } + manifest = await this.userDataSyncStoreService.manifest(syncHeaders); } catch (error) { error = UserDataSyncError.toUserDataSyncError(error); this.telemetryService.publicLog2<{ code: string, service: string, resource?: string, executionId?: string }, SyncErrorClassification>('sync/error', { code: error.code, resource: error.resource, executionId, service: this.userDataSyncStoreManagementService.userDataSyncStore!.url.toString() }); diff --git a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts index c10a1b8e9f..302c37cafb 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts @@ -394,7 +394,7 @@ export class UserDataSyncStoreClient extends Disposable implements IUserDataSync this._onTokenSucceed.fire(); if (context.res.statusCode === 409) { - throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of Conflict (409). There is new data exists for this resource. Make the request again with latest data.`, UserDataSyncErrorCode.Conflict, operationId); + throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of Conflict (409). There is new data for this resource. Make the request again with latest data.`, UserDataSyncErrorCode.Conflict, operationId); } if (context.res.statusCode === 410) { @@ -402,7 +402,7 @@ export class UserDataSyncStoreClient extends Disposable implements IUserDataSync } if (context.res.statusCode === 412) { - throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of Precondition Failed (412). There is new data exists for this resource. Make the request again with latest data.`, UserDataSyncErrorCode.PreconditionFailed, operationId); + throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of Precondition Failed (412). There is new data for this resource. Make the request again with latest data.`, UserDataSyncErrorCode.PreconditionFailed, operationId); } if (context.res.statusCode === 413) { @@ -482,7 +482,7 @@ export class RequestsSession { if (this.requests.length >= this.limit) { this.logService.info('Too many requests', ...this.requests); - throw new UserDataSyncStoreError(`Too many requests. Allowed only ${this.limit} requests in ${this.interval / (1000 * 60)} minutes.`, UserDataSyncErrorCode.LocalTooManyRequests, undefined); + throw new UserDataSyncStoreError(`Too many requests. Only ${this.limit} requests allowed in ${this.interval / (1000 * 60)} minutes.`, UserDataSyncErrorCode.LocalTooManyRequests, undefined); } this.startTime = this.startTime || new Date(); diff --git a/src/vs/platform/userDataSync/electron-browser/userDataAutoSyncService.ts b/src/vs/platform/userDataSync/electron-browser/userDataAutoSyncService.ts index bb6925de55..6c26436895 100644 --- a/src/vs/platform/userDataSync/electron-browser/userDataAutoSyncService.ts +++ b/src/vs/platform/userDataSync/electron-browser/userDataAutoSyncService.ts @@ -33,7 +33,7 @@ export class UserDataAutoSyncService extends BaseUserDataAutoSyncService { this._register(Event.debounce(Event.any( Event.map(electronService.onWindowFocus, () => 'windowFocus'), Event.map(electronService.onWindowOpen, () => 'windowOpen'), - ), (last, source) => last ? [...last, source] : [source], 1000)(sources => this.triggerSync(sources, true))); + ), (last, source) => last ? [...last, source] : [source], 1000)(sources => this.triggerSync(sources, true, false))); } } diff --git a/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts index 6125b6d77e..e553dbd761 100644 --- a/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts @@ -20,7 +20,7 @@ class TestUserDataAutoSyncService extends UserDataAutoSyncService { protected getSyncTriggerDelayTime(): number { return 50; } sync(): Promise { - return this.triggerSync(['sync'], false); + return this.triggerSync(['sync'], false, false); } } @@ -43,7 +43,7 @@ suite('UserDataAutoSyncService', () => { const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); // Trigger auto sync with settings change - await testObject.triggerSync([SyncResource.Settings], false); + await testObject.triggerSync([SyncResource.Settings], false, false); // Filter out machine requests const actual = target.requests.filter(request => !request.url.startsWith(`${target.url}/v1/resource/machines`)); @@ -66,7 +66,7 @@ suite('UserDataAutoSyncService', () => { // Trigger auto sync with settings change multiple times for (let counter = 0; counter < 2; counter++) { - await testObject.triggerSync([SyncResource.Settings], false); + await testObject.triggerSync([SyncResource.Settings], false, false); } // Filter out machine requests @@ -91,7 +91,7 @@ suite('UserDataAutoSyncService', () => { const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); // Trigger auto sync with window focus once - await testObject.triggerSync(['windowFocus'], true); + await testObject.triggerSync(['windowFocus'], true, false); // Filter out machine requests const actual = target.requests.filter(request => !request.url.startsWith(`${target.url}/v1/resource/machines`)); @@ -114,7 +114,7 @@ suite('UserDataAutoSyncService', () => { // Trigger auto sync with window focus multiple times for (let counter = 0; counter < 2; counter++) { - await testObject.triggerSync(['windowFocus'], true); + await testObject.triggerSync(['windowFocus'], true, false); } // Filter out machine requests @@ -401,4 +401,28 @@ suite('UserDataAutoSyncService', () => { assert.deepEqual(target.requests, []); }); + test('test cache control header with no cache is sent when triggered with disable cache option', async () => { + const target = new UserDataSyncTestServer(5, 1); + + // Set up and sync from the test client + const testClient = disposableStore.add(new UserDataSyncClient(target)); + await testClient.setUp(); + const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService); + + await testObject.triggerSync(['some reason'], true, true); + assert.equal(target.requestsWithAllHeaders[0].headers!['Cache-Control'], 'no-cache'); + }); + + test('test cache control header is not sent when triggered without disable cache option', async () => { + const target = new UserDataSyncTestServer(5, 1); + + // Set up and sync from the test client + const testClient = disposableStore.add(new UserDataSyncClient(target)); + await testClient.setUp(); + const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService); + + await testObject.triggerSync(['some reason'], true, false); + assert.equal(target.requestsWithAllHeaders[0].headers!['Cache-Control'], undefined); + }); + }); diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index 1b812296e4..a1c0ba79a2 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, IUserDataManifest, ALL_SYNC_RESOURCES, IUserDataSyncLogService, IUserDataSyncStoreService, IUserDataSyncUtilService, IUserDataSyncResourceEnablementService, IUserDataSyncService, getDefaultIgnoredSettings, IUserDataSyncBackupStoreService, SyncResource, ServerResource, IUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserData, IUserDataManifest, ALL_SYNC_RESOURCES, IUserDataSyncLogService, IUserDataSyncStoreService, IUserDataSyncUtilService, IUserDataSyncResourceEnablementService, IUserDataSyncService, getDefaultIgnoredSettings, IUserDataSyncBackupStoreService, SyncResource, ServerResource, IUserDataSyncStoreManagementService, registerConfiguration } 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'; @@ -49,6 +49,7 @@ export class UserDataSyncClient extends Disposable { } async setUp(empty: boolean = false): Promise { + registerConfiguration(); const userRoamingDataHome = URI.file('userdata').with({ scheme: Schemas.inMemory }); const userDataSyncHome = joinPath(userRoamingDataHome, '.sync'); const environmentService = this.instantiationService.stub(IEnvironmentService, >{ diff --git a/src/vs/platform/webview/common/webviewManagerService.ts b/src/vs/platform/webview/common/webviewManagerService.ts index 4fab9c5d66..5e15e7abce 100644 --- a/src/vs/platform/webview/common/webviewManagerService.ts +++ b/src/vs/platform/webview/common/webviewManagerService.ts @@ -14,7 +14,7 @@ export const IWebviewManagerService = createDecorator('w export interface IWebviewManagerService { _serviceBrand: unknown; - registerWebview(id: string, webContentsId: number | undefined, windowId: number, metadata: RegisterWebviewMetadata): Promise; + registerWebview(id: string, windowId: number, metadata: RegisterWebviewMetadata): Promise; unregisterWebview(id: string): Promise; updateWebviewMetadata(id: string, metadataDelta: Partial): Promise; diff --git a/src/vs/platform/webview/electron-main/webviewMainService.ts b/src/vs/platform/webview/electron-main/webviewMainService.ts index e80c5a0a3c..8558aeeca8 100644 --- a/src/vs/platform/webview/electron-main/webviewMainService.ts +++ b/src/vs/platform/webview/electron-main/webviewMainService.ts @@ -33,7 +33,7 @@ export class WebviewMainService extends Disposable implements IWebviewManagerSer this.portMappingProvider = this._register(new WebviewPortMappingProvider(tunnelService)); } - public async registerWebview(id: string, webContentsId: number | undefined, windowId: number, metadata: RegisterWebviewMetadata): Promise { + public async registerWebview(id: string, windowId: number, metadata: RegisterWebviewMetadata): Promise { const extensionLocation = metadata.extensionLocation ? URI.from(metadata.extensionLocation) : undefined; this.protocolProvider.registerWebview(id, { @@ -43,7 +43,7 @@ export class WebviewMainService extends Disposable implements IWebviewManagerSer localResourceRoots: metadata.localResourceRoots.map(x => URI.from(x)) }); - this.portMappingProvider.registerWebview(id, webContentsId, { + this.portMappingProvider.registerWebview(id, { extensionLocation, mappings: metadata.portMappings, resolvedAuthority: metadata.remoteConnectionData, diff --git a/src/vs/platform/webview/electron-main/webviewPortMappingProvider.ts b/src/vs/platform/webview/electron-main/webviewPortMappingProvider.ts index 21659ef2b6..2c2f898563 100644 --- a/src/vs/platform/webview/electron-main/webviewPortMappingProvider.ts +++ b/src/vs/platform/webview/electron-main/webviewPortMappingProvider.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { session } from 'electron'; +import { OnBeforeRequestListenerDetails, session } from 'electron'; import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { IAddress } from 'vs/platform/remote/common/remoteAgentConnection'; @@ -11,6 +11,10 @@ import { ITunnelService } from 'vs/platform/remote/common/tunnel'; import { webviewPartitionId } from 'vs/platform/webview/common/resourceLoader'; import { IWebviewPortMapping, WebviewPortMappingManager } from 'vs/platform/webview/common/webviewPortMapping'; +interface OnBeforeRequestListenerDetails_Extended extends OnBeforeRequestListenerDetails { + readonly lastCommittedOrigin?: string; +} + interface PortMappingData { readonly extensionLocation: URI | undefined; readonly mappings: readonly IWebviewPortMapping[]; @@ -20,13 +24,10 @@ interface PortMappingData { export class WebviewPortMappingProvider extends Disposable { private readonly _webviewData = new Map(); - private _webContentsIdsToWebviewIds = new Map(); - constructor( @ITunnelService private readonly _tunnelService: ITunnelService, ) { @@ -40,12 +41,15 @@ export class WebviewPortMappingProvider extends Disposable { '*://127.0.0.1:*/*', '*://0.0.0.0:*/*', ] - }, async (details, callback) => { - const webviewId = details.webContentsId && this._webContentsIdsToWebviewIds.get(details.webContentsId); - if (!webviewId) { + }, async (details: OnBeforeRequestListenerDetails_Extended, callback) => { + let origin: URI; + try { + origin = URI.parse(details.lastCommittedOrigin!); + } catch { return callback({}); } + const webviewId = origin.authority; const entry = this._webviewData.get(webviewId); if (!entry) { return callback({}); @@ -56,16 +60,13 @@ export class WebviewPortMappingProvider extends Disposable { }); } - public async registerWebview(id: string, webContentsId: number | undefined, metadata: PortMappingData): Promise { + public async registerWebview(id: string, metadata: PortMappingData): Promise { const manager = new WebviewPortMappingManager( () => this._webviewData.get(id)?.metadata.extensionLocation, () => this._webviewData.get(id)?.metadata.mappings || [], this._tunnelService); - this._webviewData.set(id, { webContentsId, metadata, manager }); - if (typeof webContentsId === 'number') { - this._webContentsIdsToWebviewIds.set(webContentsId, id); - } + this._webviewData.set(id, { metadata, manager }); } public unregisterWebview(id: string): void { @@ -73,9 +74,6 @@ export class WebviewPortMappingProvider extends Disposable { if (existing) { existing.manager.dispose(); this._webviewData.delete(id); - if (typeof existing.webContentsId === 'number') { - this._webContentsIdsToWebviewIds.delete(existing.webContentsId); - } } } diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index ae32768d36..35bff58a5a 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -7,7 +7,6 @@ import { isMacintosh, isLinux, isWeb } from 'vs/base/common/platform'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ThemeType } from 'vs/platform/theme/common/themeService'; import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; export interface IBaseOpenWindowsOptions { @@ -171,18 +170,46 @@ export interface IPathData { overrideId?: string; } +export interface IPathsToWaitFor extends IPathsToWaitForData { + paths: IPath[]; + waitMarkerFileUri: URI; +} + +interface IPathsToWaitForData { + paths: IPathData[]; + waitMarkerFileUri: UriComponents; +} + export interface IOpenFileRequest { filesToOpenOrCreate?: IPathData[]; filesToDiff?: IPathData[]; } +/** + * Additional context for the request on native only. + */ +export interface INativeOpenFileRequest extends IOpenFileRequest { + termProgram?: string; + filesToWait?: IPathsToWaitForData; +} + +export interface INativeRunActionInWindowRequest { + id: string; + from: 'menu' | 'touchbar' | 'mouse'; + args?: any[]; +} + +export interface INativeRunKeybindingInWindowRequest { + userSettingsLabel: string; +} + export interface IWindowConfiguration { sessionId: string; remoteAuthority?: string; highContrast?: boolean; - defaultThemeType?: ThemeType; + autoDetectHighContrast?: boolean; filesToOpenOrCreate?: IPath[]; filesToDiff?: IPath[]; diff --git a/src/vs/platform/windows/electron-main/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index c38c079edf..13c7709fa2 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -61,7 +61,7 @@ export interface ICodeWindow extends IDisposable { load(config: INativeWindowConfiguration, isReload?: boolean): void; reload(configuration?: INativeWindowConfiguration, cli?: ParsedArgs): void; - focus(): void; + focus(options?: { force: boolean }): void; close(): void; getBounds(): Rectangle; @@ -106,7 +106,7 @@ export interface IWindowsMainService { openExtensionDevelopmentHostWindow(extensionDevelopmentPath: string[], openConfig: IOpenConfiguration): ICodeWindow[]; sendToFocused(channel: string, ...args: any[]): void; - sendToAll(channel: string, payload: any, windowIdsToIgnore?: number[]): void; + sendToAll(channel: string, payload?: any, windowIdsToIgnore?: number[]): void; getLastActiveWindow(): ICodeWindow | undefined; diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index 1c0a9af4a7..d93443a4c8 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -19,8 +19,8 @@ import { screen, BrowserWindow, MessageBoxOptions, Display, app, nativeTheme } f import { ILifecycleMainService, UnloadReason, LifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; -import { IWindowSettings, IPath, isFileToOpen, isWorkspaceToOpen, isFolderToOpen, IWindowOpenable, IOpenEmptyWindowOptions, IAddFoldersRequest } from 'vs/platform/windows/common/windows'; -import { getLastActiveWindow, findBestWindowOrFolderForFile, findWindowOnWorkspace, findWindowOnExtensionDevelopmentPath, findWindowOnWorkspaceOrFolderUri, INativeWindowConfiguration, OpenContext, IPathsToWaitFor } from 'vs/platform/windows/node/window'; +import { IWindowSettings, IPath, isFileToOpen, isWorkspaceToOpen, isFolderToOpen, IWindowOpenable, IOpenEmptyWindowOptions, IAddFoldersRequest, IPathsToWaitFor } from 'vs/platform/windows/common/windows'; +import { getLastActiveWindow, findBestWindowOrFolderForFile, findWindowOnWorkspace, findWindowOnExtensionDevelopmentPath, findWindowOnWorkspaceOrFolderUri, INativeWindowConfiguration, OpenContext } from 'vs/platform/windows/node/window'; import { Emitter } from 'vs/base/common/event'; import product from 'vs/platform/product/common/product'; import { IWindowsMainService, IOpenConfiguration, IWindowsCountChangedEvent, ICodeWindow, IWindowState as ISingleWindowState, WindowMode, IOpenEmptyConfiguration } from 'vs/platform/windows/electron-main/windows'; @@ -40,6 +40,7 @@ import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; import { withNullAsUndefined } from 'vs/base/common/types'; import { isWindowsDriveLetter, toSlashes, parseLineAndColumnAware } from 'vs/base/common/extpath'; import { CharCode } from 'vs/base/common/charCode'; +import { getPathLabel } from 'vs/base/common/labels'; export interface IWindowState { workspace?: IWorkspaceIdentifier; @@ -166,9 +167,6 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic private readonly _onWindowReady = this._register(new Emitter()); readonly onWindowReady = this._onWindowReady.event; - private readonly _onWindowClose = this._register(new Emitter()); - readonly onWindowClose = this._onWindowClose.event; - private readonly _onWindowsCountChanged = this._register(new Emitter()); readonly onWindowsCountChanged = this._onWindowsCountChanged.event; @@ -212,8 +210,8 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic private registerListeners(): void { - // React to HC color scheme changes (Windows) - if (isWindows) { + // React to HC color scheme changes (Windows, macOS) + if (isWindows || isMacintosh) { nativeTheme.on('updated', () => { if (nativeTheme.shouldUseInvertedColorScheme || nativeTheme.shouldUseHighContrastColors) { this.sendToAll('vscode:enterHighContrast'); @@ -880,11 +878,12 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic let message, detail; if (uri.scheme === Schemas.file) { message = localize('pathNotExistTitle', "Path does not exist"); - detail = localize('pathNotExistDetail', "The path '{0}' does not seem to exist anymore on disk.", uri.fsPath); + detail = localize('pathNotExistDetail', "The path '{0}' does not seem to exist anymore on disk.", getPathLabel(uri.fsPath, this.environmentService)); } else { message = localize('uriInvalidTitle', "URI can not be opened"); detail = localize('uriInvalidDetail', "The URI '{0}' is not valid and can not be opened.", uri.toString()); } + const options: MessageBoxOptions = { title: product.nameLong, type: 'info', @@ -1624,18 +1623,6 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return state; } - focusLastActive(cli: ParsedArgs, context: OpenContext): ICodeWindow { - const lastActive = this.getLastActiveWindow(); - if (lastActive) { - lastActive.focus(); - - return lastActive; - } - - // No window - open new empty one - return this.open({ context, cli, forceEmpty: true })[0]; - } - getLastActiveWindow(): ICodeWindow | undefined { return getLastActiveWindow(WindowsMainService.WINDOWS); } @@ -1693,6 +1680,5 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Emit this._onWindowsCountChanged.fire({ oldCount: WindowsMainService.WINDOWS.length + 1, newCount: WindowsMainService.WINDOWS.length }); - this._onWindowClose.fire(win.id); } } diff --git a/src/vs/platform/windows/node/window.ts b/src/vs/platform/windows/node/window.ts index ab367312b0..b53e2aaa45 100644 --- a/src/vs/platform/windows/node/window.ts +++ b/src/vs/platform/windows/node/window.ts @@ -3,8 +3,8 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IWindowConfiguration, IPath, IOpenFileRequest, IPathData } from 'vs/platform/windows/common/windows'; -import { URI, UriComponents } from 'vs/base/common/uri'; +import { IWindowConfiguration, IPathsToWaitFor } from 'vs/platform/windows/common/windows'; +import { URI } from 'vs/base/common/uri'; import * as platform from 'vs/base/common/platform'; import * as extpath from 'vs/base/common/extpath'; import { IWorkspaceIdentifier, IResolvedWorkspace, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; @@ -34,16 +34,6 @@ export const enum OpenContext { API } -export interface IRunActionInWindowRequest { - id: string; - from: 'menu' | 'touchbar' | 'mouse'; - args?: any[]; -} - -export interface IRunKeybindingInWindowRequest { - userSettingsLabel: string; -} - export interface INativeWindowConfiguration extends IWindowConfiguration, ParsedArgs { mainPid: number; @@ -72,21 +62,6 @@ export interface INativeWindowConfiguration extends IWindowConfiguration, Parsed filesToWait?: IPathsToWaitFor; } -export interface INativeOpenFileRequest extends IOpenFileRequest { - termProgram?: string; - filesToWait?: IPathsToWaitForData; -} - -export interface IPathsToWaitFor extends IPathsToWaitForData { - paths: IPath[]; - waitMarkerFileUri: URI; -} - -export interface IPathsToWaitForData { - paths: IPathData[]; - waitMarkerFileUri: UriComponents; -} - export interface IWindowContext { openedWorkspace?: IWorkspaceIdentifier; openedFolderUri?: URI; diff --git a/src/vs/platform/workspace/common/workspace.ts b/src/vs/platform/workspace/common/workspace.ts index cd1562255e..37f1f49154 100644 --- a/src/vs/platform/workspace/common/workspace.ts +++ b/src/vs/platform/workspace/common/workspace.ts @@ -82,9 +82,9 @@ export interface IWorkspaceFoldersChangeEvent { export namespace IWorkspace { export function isIWorkspace(thing: unknown): thing is IWorkspace { - return thing && typeof thing === 'object' + return !!(thing && typeof thing === 'object' && typeof (thing as IWorkspace).id === 'string' - && Array.isArray((thing as IWorkspace).folders); + && Array.isArray((thing as IWorkspace).folders)); } } @@ -127,10 +127,10 @@ export interface IWorkspaceFolderData { export namespace IWorkspaceFolder { export function isIWorkspaceFolder(thing: unknown): thing is IWorkspaceFolder { - return thing && typeof thing === 'object' + return !!(thing && typeof thing === 'object' && URI.isUri((thing as IWorkspaceFolder).uri) && typeof (thing as IWorkspaceFolder).name === 'string' - && typeof (thing as IWorkspaceFolder).toResource === 'function'; + && typeof (thing as IWorkspaceFolder).toResource === 'function'); } } diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index ea100b576e..989ce0e673 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -3086,6 +3086,8 @@ declare module 'vscode' { * @param uri Uri of the new file.. * @param options Defines if an existing file should be overwritten or be * ignored. When overwrite and ignoreIfExists are both set overwrite wins. + * When both are unset and when the file already exists then the edit cannot + * be applied successfully. * @param metadata Optional metadata for the entry. */ createFile(uri: Uri, options?: { overwrite?: boolean, ignoreIfExists?: boolean }, metadata?: WorkspaceEditEntryMetadata): void; @@ -5624,6 +5626,9 @@ declare module 'vscode' { /** * Get the absolute path of a resource contained in the extension. * + * *Note* that an absolute uri can be constructed via [`Uri.joinPath`](#Uri.joinPath) and + * [`extensionUri`](#ExtensionContent.extensionUri), e.g. `vscode.Uri.joinPath(context.extensionUri, relativePath);` + * * @param relativePath A relative path to a resource contained in the extension. * @return The absolute path of the resource. */ @@ -6203,6 +6208,13 @@ declare module 'vscode' { */ name: string; + /** + * A human-readable string which is rendered less prominently on a separate line in places + * where the task's name is displayed. Supports rendering of [theme icons](#ThemeIcon) + * via the `$()`-syntax. + */ + detail?: string; + /** * The task's execution engine */ @@ -10697,6 +10709,27 @@ declare module 'vscode' { export function createSourceControl(id: string, label: string, rootUri?: Uri): SourceControl; } + /** + * A DebugProtocolMessage is an opaque stand-in type for the [ProtocolMessage](https://microsoft.github.io/debug-adapter-protocol/specification#Base_Protocol_ProtocolMessage) type defined in the Debug Adapter Protocol. + */ + export interface DebugProtocolMessage { + // Properties: see details [here](https://microsoft.github.io/debug-adapter-protocol/specification#Base_Protocol_ProtocolMessage). + } + + /** + * A DebugProtocolSource is an opaque stand-in type for the [Source](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Source) type defined in the Debug Adapter Protocol. + */ + export interface DebugProtocolSource { + // Properties: see details [here](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Source). + } + + /** + * A DebugProtocolBreakpoint is an opaque stand-in type for the [Breakpoint](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Breakpoint) type defined in the Debug Adapter Protocol. + */ + export interface DebugProtocolBreakpoint { + // Properties: see details [here](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Breakpoint). + } + /** * Configuration for a debug session. */ @@ -10760,6 +10793,15 @@ declare module 'vscode' { * Send a custom request to the debug adapter. */ customRequest(command: string, args?: any): Thenable; + + /** + * Maps a VS Code breakpoint to the corresponding Debug Adapter Protocol (DAP) breakpoint that is managed by the debug adapter of the debug session. + * If no DAP breakpoint exists (either because the VS Code breakpoint was not yet registered or because the debug adapter is not interested in the breakpoint), the value `undefined` is returned. + * + * @param breakpoint A VS Code [breakpoint](#Breakpoint). + * @return A promise that resolves to the Debug Adapter Protocol breakpoint or `undefined`. + */ + getDebugProtocolBreakpoint(breakpoint: Breakpoint): Thenable; } /** @@ -10935,13 +10977,6 @@ declare module 'vscode' { handleMessage(message: DebugProtocolMessage): void; } - /** - * A DebugProtocolMessage is an opaque stand-in type for the [ProtocolMessage](https://microsoft.github.io/debug-adapter-protocol/specification#Base_Protocol_ProtocolMessage) type defined in the Debug Adapter Protocol. - */ - export interface DebugProtocolMessage { - // Properties: see details [here](https://microsoft.github.io/debug-adapter-protocol/specification#Base_Protocol_ProtocolMessage). - } - /** * A debug adapter descriptor for an inline implementation. */ @@ -11163,13 +11198,6 @@ declare module 'vscode' { compact?: boolean; } - /** - * A DebugProtocolSource is an opaque stand-in type for the [Source](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Source) type defined in the Debug Adapter Protocol. - */ - export interface DebugProtocolSource { - // Properties: see details [here](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Source). - } - /** * A DebugConfigurationProviderTriggerKind specifies when the `provideDebugConfigurations` method of a `DebugConfigurationProvider` is triggered. * Currently there are two situations: to provide the initial debug configurations for a newly created launch.json or diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index b2c54e0bc8..a2b91feab3 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -112,6 +112,7 @@ declare module 'vscode' { export function registerAuthenticationProvider(provider: AuthenticationProvider): Disposable; /** + * @deprecated - getSession should now trigger extension activation. * Fires with the provider id that was registered or unregistered. */ export const onDidChangeAuthenticationProviders: Event; @@ -738,21 +739,19 @@ declare module 'vscode' { //#region debug /** - * A DebugProtocolBreakpoint is an opaque stand-in type for the [Breakpoint](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Breakpoint) type defined in the Debug Adapter Protocol. + * A DebugProtocolVariableContainer is an opaque stand-in type for the intersection of the Scope and Variable types defined in the Debug Adapter Protocol. + * See https://microsoft.github.io/debug-adapter-protocol/specification#Types_Scope and https://microsoft.github.io/debug-adapter-protocol/specification#Types_Variable. */ - export interface DebugProtocolBreakpoint { - // Properties: see details [here](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Breakpoint). + export interface DebugProtocolVariableContainer { + // Properties: the intersection of DAP's Scope and Variable types. } - export interface DebugSession { - /** - * Maps a VS Code breakpoint to the corresponding Debug Adapter Protocol (DAP) breakpoint that is managed by the debug adapter of the debug session. - * If no DAP breakpoint exists (either because the VS Code breakpoint was not yet registered or because the debug adapter is not interested in the breakpoint), the value `undefined` is returned. - * - * @param breakpoint A VS Code [breakpoint](#Breakpoint). - * @return A promise that resolves to the Debug Adapter Protocol breakpoint or `undefined`. - */ - getDebugProtocolBreakpoint(breakpoint: Breakpoint): Thenable; + /** + * A DebugProtocolVariable is an opaque stand-in type for the Variable type defined in the Debug Adapter Protocol. + * See https://microsoft.github.io/debug-adapter-protocol/specification#Types_Variable. + */ + export interface DebugProtocolVariable { + // Properties: see details [here](https://microsoft.github.io/debug-adapter-protocol/specification#Base_Protocol_Variable). } // deprecated debug API @@ -981,13 +980,6 @@ declare module 'vscode' { } //#endregion - /** - * A task to execute - */ - export class Task2 extends Task { - detail?: string; - } - //#region Task presentation group: https://github.com/microsoft/vscode/issues/47265 export interface TaskPresentationOptions { /** @@ -1204,7 +1196,7 @@ declare module 'vscode' { export interface NotebookCellMetadata { /** - * Controls if the content of a cell is editable or not. + * Controls whether a cell's editor is editable/readonly. */ editable?: boolean; @@ -1323,6 +1315,7 @@ declare module 'vscode' { export interface NotebookDocument { readonly uri: Uri; + readonly version: number; readonly fileName: string; readonly viewType: string; readonly isDirty: boolean; @@ -1352,11 +1345,45 @@ declare module 'vscode' { contains(uri: Uri): boolean } + export interface WorkspaceEdit { + replaceCells(uri: Uri, start: number, end: number, cells: NotebookCellData[], metadata?: WorkspaceEditEntryMetadata): void; + replaceCellOutput(uri: Uri, index: number, outputs: CellOutput[], metadata?: WorkspaceEditEntryMetadata): void; + replaceCellMetadata(uri: Uri, index: number, cellMetadata: NotebookCellMetadata, metadata?: WorkspaceEditEntryMetadata): void; + } + export interface NotebookEditorCellEdit { + + replaceCells(start: number, end: number, cells: NotebookCellData[]): void; + replaceOutput(index: number, outputs: CellOutput[]): void; + replaceMetadata(index: number, metadata: NotebookCellMetadata): void; + + /** @deprecated */ insert(index: number, content: string | string[], language: string, type: CellKind, outputs: CellOutput[], metadata: NotebookCellMetadata | undefined): void; + /** @deprecated */ delete(index: number): void; } + export interface NotebookCellRange { + readonly start: number; + readonly end: number; + } + + export enum NotebookEditorRevealType { + /** + * The range will be revealed with as little scrolling as possible. + */ + Default = 0, + /** + * The range will always be revealed in the center of the viewport. + */ + InCenter = 1, + /** + * If the range is outside the viewport, it will be revealed in the center of the viewport. + * Otherwise, it will be revealed with as little scrolling as possible. + */ + InCenterIfOutsideViewport = 2, + } + export interface NotebookEditor { /** * The document associated with this notebook editor. @@ -1368,6 +1395,12 @@ declare module 'vscode' { */ readonly selection?: NotebookCell; + + /** + * The current visible ranges in the editor (vertically). + */ + readonly visibleRanges: NotebookCellRange[]; + /** * The column in which this editor shows. */ @@ -1412,6 +1445,8 @@ declare module 'vscode' { asWebviewUri(localResource: Uri): Uri; edit(callback: (editBuilder: NotebookEditorCellEdit) => void): Thenable; + + revealRange(range: NotebookCellRange, revealType?: NotebookEditorRevealType): void; } export interface NotebookOutputSelector { @@ -1474,12 +1509,22 @@ declare module 'vscode' { readonly cell: NotebookCell; } + export interface NotebookEditorSelectionChangeEvent { + readonly notebookEditor: NotebookEditor; + readonly selection?: NotebookCell; + } + + export interface NotebookEditorVisibleRangesChangeEvent { + readonly notebookEditor: NotebookEditor; + readonly visibleRanges: ReadonlyArray; + } + export interface NotebookCellData { readonly cellKind: CellKind; readonly source: string; - language: string; - outputs: CellOutput[]; - metadata: NotebookCellMetadata; + readonly language: string; + readonly outputs: CellOutput[]; + readonly metadata: NotebookCellMetadata | undefined; } export interface NotebookData { @@ -1597,14 +1642,13 @@ declare module 'vscode' { saveNotebookAs(targetResource: Uri, document: NotebookDocument, cancellation: CancellationToken): Promise; readonly onDidChangeNotebook: Event; backupNotebook(document: NotebookDocument, context: NotebookDocumentBackupContext, cancellation: CancellationToken): Promise; - - kernel?: NotebookKernel; } export interface NotebookKernel { readonly id?: string; label: string; description?: string; + detail?: string; isPreferred?: boolean; preloads?: Uri[]; executeCell(document: NotebookDocument, cell: NotebookCell): void; @@ -1614,21 +1658,61 @@ declare module 'vscode' { } export interface NotebookDocumentFilter { - viewType?: string; - filenamePattern?: GlobPattern; - excludeFileNamePattern?: GlobPattern; + viewType?: string | string[]; + filenamePattern?: GlobPattern | { include: GlobPattern; exclude: GlobPattern }; } export interface NotebookKernelProvider { - onDidChangeKernels?: Event; + onDidChangeKernels?: Event; provideKernels(document: NotebookDocument, token: CancellationToken): ProviderResult; resolveKernel?(kernel: T, document: NotebookDocument, webview: NotebookCommunication, token: CancellationToken): ProviderResult; } + /** + * Represents the alignment of status bar items. + */ + export enum NotebookCellStatusBarAlignment { + + /** + * Aligned to the left side. + */ + Left = 1, + + /** + * Aligned to the right side. + */ + Right = 2 + } + + export interface NotebookCellStatusBarItem { + readonly cell: NotebookCell; + readonly alignment: NotebookCellStatusBarAlignment; + readonly priority?: number; + text: string; + tooltip: string | undefined; + command: string | Command | undefined; + accessibilityInformation?: AccessibilityInformation; + show(): void; + hide(): void; + dispose(): void; + } + export namespace notebook { export function registerNotebookContentProvider( notebookType: string, - provider: NotebookContentProvider + provider: NotebookContentProvider, + options?: { + /** + * Controls if outputs change will trigger notebook document content change and if it will be used in the diff editor + * Default to false. If the content provider doesn't persisit the outputs in the file document, this should be set to true. + */ + transientOutputs: boolean; + /** + * Controls if a meetadata property change will trigger notebook document content change and if it will be used in the diff editor + * Default to false. If the content provider doesn't persisit a metadata property in the file document, it should be set to true. + */ + transientMetadata: { [K in keyof NotebookCellMetadata]?: boolean } + } ): Disposable; export function registerNotebookKernelProvider( @@ -1636,12 +1720,6 @@ declare module 'vscode' { provider: NotebookKernelProvider ): Disposable; - export function registerNotebookKernel( - id: string, - selectors: GlobPattern[], - kernel: NotebookKernel - ): Disposable; - export const onDidOpenNotebookDocument: Event; export const onDidCloseNotebookDocument: Event; export const onDidSaveNotebookDocument: Event; @@ -1651,11 +1729,13 @@ declare module 'vscode' { */ export const notebookDocuments: ReadonlyArray; - export let visibleNotebookEditors: NotebookEditor[]; + export const visibleNotebookEditors: NotebookEditor[]; export const onDidChangeVisibleNotebookEditors: Event; - export let activeNotebookEditor: NotebookEditor | undefined; + export const activeNotebookEditor: NotebookEditor | undefined; export const onDidChangeActiveNotebookEditor: Event; + export const onDidChangeNotebookEditorSelection: Event; + export const onDidChangeNotebookEditorVisibleRanges: Event; export const onDidChangeNotebookCells: Event; export const onDidChangeCellOutputs: Event; export const onDidChangeCellLanguage: Event; @@ -1670,6 +1750,17 @@ declare module 'vscode' { export function createConcatTextDocument(notebook: NotebookDocument, selector?: DocumentSelector): NotebookConcatTextDocument; export const onDidChangeActiveNotebookKernel: Event<{ document: NotebookDocument, kernel: NotebookKernel | undefined }>; + + /** + * Creates a notebook cell status bar [item](#NotebookCellStatusBarItem). + * It will be disposed automatically when the notebook document is closed or the cell is deleted. + * + * @param cell The cell on which this item should be shown. + * @param alignment The alignment of the item. + * @param priority The priority of the item. Higher values mean the item should be shown more to the left. + * @return A new status bar item. + */ + export function createCellStatusBarItem(cell: NotebookCell, alignment?: NotebookCellStatusBarAlignment, priority?: number): NotebookCellStatusBarItem; } //#endregion @@ -1970,10 +2061,8 @@ declare module 'vscode' { /** * Event fired when the view is disposed. * - * Views are disposed of in a few cases: - * - * - When a view is collapsed and `retainContextWhenHidden` has not been set. - * - When a view is hidden by a user. + * Views are disposed when they are explicitly hidden by a user (this happens when a user + * right clicks in a view and unchecks the webview view). * * Trying to use the view after it has been disposed throws an exception. */ @@ -1987,7 +2076,14 @@ declare module 'vscode' { readonly visible: boolean; /** - * Event fired when the visibility of the view changes + * Event fired when the visibility of the view changes. + * + * Actions that trigger a visibility change: + * + * - The view is collapsed or expanded. + * - The user switches to a different view group in the sidebar or panel. + * + * Note that hiding a view using the context menu instead disposes of the view and fires `onDidDispose`. */ readonly onDidChangeVisibility: Event; } @@ -1996,8 +2092,10 @@ declare module 'vscode' { /** * Persisted state from the webview content. * - * To save resources, VS Code normally deallocates webview views that are not visible. For example, if the user - * collapse a view or switching to another top level activity, the underlying webview document is deallocates. + * To save resources, VS Code normally deallocates webview documents (the iframe content) that are not visible. + * For example, when the user collapse a view or switches to another top level activity in the sidebar, the + * `WebviewView` itself is kept alive but the webview's underlying document is deallocated. It is recreated when + * the view becomes visible again. * * You can prevent this behavior by setting `retainContextWhenHidden` in the `WebviewOptions`. However this * increases resource usage and should be avoided wherever possible. Instead, you can use persisted state to @@ -2033,7 +2131,7 @@ declare module 'vscode' { * `resolveWebviewView` is called when a view first becomes visible. This may happen when the view is * first loaded or when the user hides and then shows a view again. * - * @param webviewView Webview panel to restore. The serializer should take ownership of this panel. The + * @param webviewView Webview view to restore. The serializer should take ownership of this view. The * provider must set the webview's `.html` and hook up all webview events it is interested in. * @param context Additional metadata about the view being resolved. * @param token Cancellation token indicating that the view being provided is no longer needed. @@ -2059,20 +2157,20 @@ declare module 'vscode' { */ readonly webviewOptions?: { /** - * Controls if the webview panel's content (iframe) is kept around even when the panel + * Controls if the webview element itself (iframe) is kept around even when the view * is no longer visible. * - * Normally the webview's html context is created when the panel becomes visible + * Normally the webview's html context is created when the view becomes visible * and destroyed when it is hidden. Extensions that have complex state * or UI can set the `retainContextWhenHidden` to make VS Code keep the webview * context around, even when the webview moves to a background tab. When a webview using * `retainContextWhenHidden` becomes hidden, its scripts and other dynamic content are suspended. - * When the panel becomes visible again, the context is automatically restored + * When the view becomes visible again, the context is automatically restored * in the exact same state it was in originally. You cannot send messages to a * hidden webview, even with `retainContextWhenHidden` enabled. * * `retainContextWhenHidden` has a high memory overhead and should only be used if - * your panel's context cannot be quickly saved and restored. + * your view's context cannot be quickly saved and restored. */ readonly retainContextWhenHidden?: boolean; }; diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index 7459bd585f..dd43907ea2 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -54,7 +54,7 @@ import './mainThreadTreeViews'; import './mainThreadDownloadService'; import './mainThreadUrls'; import './mainThreadWindow'; -import './mainThreadWebview'; +import './mainThreadWebviewManager'; import './mainThreadWorkspace'; import './mainThreadComments'; import './mainThreadNotebook'; diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index ed44be4aca..d7d50d2c18 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -17,7 +17,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { fromNow } from 'vs/base/common/date'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { ActivationKind, IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { Platform, platform } from 'vs/base/common/platform'; const VSO_ALLOWED_EXTENSIONS = ['github.vscode-pull-request-github', 'github.vscode-pull-request-github-insiders', 'vscode.git', 'ms-vsonline.vsonline', 'vscode.github-browser']; @@ -132,8 +132,12 @@ export class MainThreadAuthenticationProvider extends Disposable { } private async registerCommandsAndContextMenuItems(): Promise { - const sessions = await this._proxy.$getSessions(this.id); - sessions.forEach(session => this.registerSession(session)); + try { + const sessions = await this._proxy.$getSessions(this.id); + sessions.forEach(session => this.registerSession(session)); + } catch (_) { + // Ignore + } } private registerSession(session: modes.AuthenticationSession) { @@ -232,6 +236,12 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu this._register(this.authenticationService.onDidUnregisterAuthenticationProvider(info => { this._proxy.$onDidChangeAuthenticationProviders([], [info]); })); + + this._proxy.$setProviders(this.authenticationService.declaredProviders); + + this._register(this.authenticationService.onDidChangeDeclaredProviders(e => { + this._proxy.$setProviders(e); + })); } $getProviderIds(): Promise { @@ -249,7 +259,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu } $ensureProvider(id: string): Promise { - return this.extensionService.activateByEvent(getAuthenticationProviderActivationEvent(id)); + return this.extensionService.activateByEvent(getAuthenticationProviderActivationEvent(id), ActivationKind.Immediate); } $sendDidChangeSessions(id: string, event: modes.AuthenticationSessionsChangeEvent): void { diff --git a/src/vs/workbench/api/browser/mainThreadCustomEditors.ts b/src/vs/workbench/api/browser/mainThreadCustomEditors.ts index 8ff08792d7..aa79137986 100644 --- a/src/vs/workbench/api/browser/mainThreadCustomEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadCustomEditors.ts @@ -19,7 +19,8 @@ import { IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; import { IUndoRedoService, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo'; -import type { MainThreadWebviews } from 'vs/workbench/api/browser/mainThreadWebview'; +import { MainThreadWebviewPanels } from 'vs/workbench/api/browser/mainThreadWebviewPanels'; +import { MainThreadWebviews, reviveWebviewExtension } from 'vs/workbench/api/browser/mainThreadWebviews'; import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol'; import { editorGroupToViewColumn } from 'vs/workbench/api/common/shared/editor'; import { IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor'; @@ -28,27 +29,31 @@ import { CustomDocumentBackupData } from 'vs/workbench/contrib/customEditor/brow import { ICustomEditorModel, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { CustomTextEditorModel } from 'vs/workbench/contrib/customEditor/common/customTextEditorModel'; import { WebviewExtensionDescription } from 'vs/workbench/contrib/webview/browser/webview'; +import { WebviewInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput'; import { IWebviewWorkbenchService } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { IWorkingCopy, IWorkingCopyBackup, IWorkingCopyService, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -export const enum CustomEditorModelType { +const enum CustomEditorModelType { Custom, Text, } -export class MainThreadCustomEditors extends Disposable { +export class MainThreadCustomEditors extends Disposable implements extHostProtocol.MainThreadCustomEditorsShape { private readonly _proxyCustomEditors: extHostProtocol.ExtHostCustomEditorsShape; private readonly _editorProviders = new Map(); constructor( - private readonly mainThreadWebviews: MainThreadWebviews, context: extHostProtocol.IExtHostContext, + private readonly mainThreadWebview: MainThreadWebviews, + private readonly mainThreadWebviewPanels: MainThreadWebviewPanels, + @IExtensionService extensionService: IExtensionService, @IWorkingCopyService workingCopyService: IWorkingCopyService, @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService, @ICustomEditorService private readonly _customEditorService: ICustomEditorService, @@ -61,7 +66,7 @@ export class MainThreadCustomEditors extends Disposable { this._proxyCustomEditors = context.getProxy(extHostProtocol.ExtHostContext.ExtHostCustomEditors); - workingCopyFileService.registerWorkingCopyProvider((editorResource) => { + this._register(workingCopyFileService.registerWorkingCopyProvider((editorResource) => { const matchedWorkingCopies: IWorkingCopy[] = []; for (const workingCopy of workingCopyService.workingCopies) { @@ -72,7 +77,18 @@ export class MainThreadCustomEditors extends Disposable { } } return matchedWorkingCopies; - }); + })); + + // This reviver's only job is to activate custom editor extensions. + this._register(_webviewWorkbenchService.registerResolver({ + canResolve: (webview: WebviewInput) => { + if (webview instanceof CustomEditorInput) { + extensionService.activateByEvent(`onCustomEditor:${webview.viewType}`); + } + return false; + }, + resolveWebview: () => { throw new Error('not implemented'); } + })); } dispose() { @@ -85,7 +101,15 @@ export class MainThreadCustomEditors extends Disposable { this._editorProviders.clear(); } - public registerEditorProvider( + public $registerTextEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions, capabilities: extHostProtocol.CustomTextEditorCapabilities): void { + this.registerEditorProvider(CustomEditorModelType.Text, reviveWebviewExtension(extensionData), viewType, options, capabilities, true); + } + + public $registerCustomEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions, supportsMultipleEditorsPerDocument: boolean): void { + this.registerEditorProvider(CustomEditorModelType.Custom, reviveWebviewExtension(extensionData), viewType, options, {}, supportsMultipleEditorsPerDocument); + } + + private registerEditorProvider( modelType: CustomEditorModelType, extension: WebviewExtensionDescription, viewType: string, @@ -111,7 +135,7 @@ export class MainThreadCustomEditors extends Disposable { const handle = webviewInput.id; const resource = webviewInput.resource; - this.mainThreadWebviews.addWebviewInput(handle, webviewInput); + this.mainThreadWebviewPanels.addWebviewInput(handle, webviewInput); webviewInput.webview.options = options; webviewInput.webview.extension = extension; @@ -120,7 +144,7 @@ export class MainThreadCustomEditors extends Disposable { modelRef = await this.getOrCreateCustomEditorModel(modelType, resource, viewType, { backupId: webviewInput.backupId }, cancellation); } catch (error) { onUnexpectedError(error); - webviewInput.webview.html = this.mainThreadWebviews.getWebviewResolvedFailedContent(viewType); + webviewInput.webview.html = this.mainThreadWebview.getWebviewResolvedFailedContent(viewType); return; } @@ -157,7 +181,7 @@ export class MainThreadCustomEditors extends Disposable { await this._proxyCustomEditors.$resolveWebviewEditor(resource, handle, viewType, webviewInput.getTitle(), editorGroupToViewColumn(this._editorGroupService, webviewInput.group || 0), webviewInput.webview.options, cancellation); } catch (error) { onUnexpectedError(error); - webviewInput.webview.html = this.mainThreadWebviews.getWebviewResolvedFailedContent(viewType); + webviewInput.webview.html = this.mainThreadWebview.getWebviewResolvedFailedContent(viewType); modelRef.dispose(); return; } @@ -200,7 +224,7 @@ export class MainThreadCustomEditors extends Disposable { case CustomEditorModelType.Custom: { const model = MainThreadCustomEditorModel.create(this._instantiationService, this._proxyCustomEditors, viewType, resource, options, () => { - return Array.from(this.mainThreadWebviews.webviewInputs) + return Array.from(this.mainThreadWebviewPanels.webviewInputs) .filter(editor => editor instanceof CustomEditorInput && isEqual(editor.resource, resource)) as CustomEditorInput[]; }, cancellation, this._backupService); return this._customEditorService.models.add(resource, viewType, model); @@ -333,7 +357,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod } public get capabilities(): WorkingCopyCapabilities { - return 0; + return WorkingCopyCapabilities.None; } public isDirty(): boolean { diff --git a/src/vs/workbench/api/browser/mainThreadEditors.ts b/src/vs/workbench/api/browser/mainThreadEditors.ts index 5fd5a170c5..943bb4e0b4 100644 --- a/src/vs/workbench/api/browser/mainThreadEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadEditors.ts @@ -8,7 +8,7 @@ import { disposed } from 'vs/base/common/errors'; import { IDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle'; import { equals as objectEquals } from 'vs/base/common/objects'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { IBulkEditService, ResourceEdit, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IRange } from 'vs/editor/common/core/range'; import { ISelection } from 'vs/editor/common/core/selection'; @@ -20,7 +20,7 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { IOpenerService } from 'vs/platform/opener/common/opener'; import { MainThreadDocumentsAndEditors } from 'vs/workbench/api/browser/mainThreadDocumentsAndEditors'; import { MainThreadTextEditor } from 'vs/workbench/api/browser/mainThreadEditor'; -import { ExtHostContext, ExtHostEditorsShape, IApplyEditsOptions, IExtHostContext, ITextDocumentShowOptions, ITextEditorConfigurationUpdate, ITextEditorPositionData, IUndoStopOptions, MainThreadTextEditorsShape, TextEditorRevealType, IWorkspaceEditDto, reviveWorkspaceEditDto } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostContext, ExtHostEditorsShape, IApplyEditsOptions, IExtHostContext, ITextDocumentShowOptions, ITextEditorConfigurationUpdate, ITextEditorPositionData, IUndoStopOptions, MainThreadTextEditorsShape, TextEditorRevealType, IWorkspaceEditDto, WorkspaceEditType } from 'vs/workbench/api/common/extHost.protocol'; import { EditorViewColumn, editorGroupToViewColumn, viewColumnToEditorGroup } from 'vs/workbench/api/common/shared/editor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -29,6 +29,26 @@ import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { openEditorWith } from 'vs/workbench/services/editor/common/editorOpenWith'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { revive } from 'vs/base/common/marshalling'; +import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; + +function reviveWorkspaceEditDto2(data: IWorkspaceEditDto | undefined): ResourceEdit[] { + if (!data?.edits) { + return []; + } + + const result: ResourceEdit[] = []; + for (let edit of revive(data).edits) { + if (edit._type === WorkspaceEditType.File) { + result.push(new ResourceFileEdit(edit.oldUri, edit.newUri, edit.options, edit.metadata)); + } else if (edit._type === WorkspaceEditType.Text) { + result.push(new ResourceTextEdit(edit.resource, edit.edit, edit.modelVersionId, edit.metadata)); + } else if (edit._type === WorkspaceEditType.Cell) { + result.push(new ResourceNotebookCellEdit(edit.resource, edit.edit, edit.modelVersionId, edit.metadata)); + } + } + return result; +} export class MainThreadTextEditors implements MainThreadTextEditorsShape { @@ -222,8 +242,8 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { } $tryApplyWorkspaceEdit(dto: IWorkspaceEditDto): Promise { - const { edits } = reviveWorkspaceEditDto(dto)!; - return this._bulkEditService.apply({ edits }).then(() => true, _err => false); + const edits = reviveWorkspaceEditDto2(dto); + return this._bulkEditService.apply(edits).then(() => true, _err => false); } $tryInsertSnippet(id: string, template: string, ranges: readonly IRange[], opts: IUndoStopOptions): Promise { diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 0d745ea879..46ce3d3bb8 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -127,10 +127,13 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha } private static _reviveCodeActionDto(data: ReadonlyArray): modes.CodeAction[] { - if (data) { + let dataCast = data as unknown; // {{ SQL CARBON EDIT }} + let returnval = dataCast as modes.CodeAction[]; // {{ SQL CARBON EDIT }} + if (returnval) { data.forEach(code => reviveWorkspaceEditDto(code.edit)); } - return data; + + return returnval; // {{ SQL CARBON EDIT }} } private static _reviveLinkDTO(data: ILinkDto): modes.ILink { diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index a71cbc5dbb..f0364693a3 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -4,61 +4,21 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; -import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; -import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IExtHostContext, ExtHostNotebookShape, ExtHostContext, INotebookDocumentsAndEditorsDelta } from '../common/extHost.protocol'; -import { Disposable, IDisposable, combinedDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { URI, UriComponents } from 'vs/base/common/uri'; -import { INotebookService, IMainNotebookController } from 'vs/workbench/contrib/notebook/common/notebookService'; -import { NOTEBOOK_DISPLAY_ORDER, NotebookCellOutputsSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, CellEditType, CellKind, INotebookKernelInfo, INotebookKernelInfoDto, IEditor, INotebookDocumentFilter, DisplayOrderKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; -import { IRelativePattern } from 'vs/base/common/glob'; -import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { Emitter } from 'vs/base/common/event'; +import { combinedDisposable, Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; - -export class MainThreadNotebookDocument extends Disposable { - private _textModel: NotebookTextModel; - - get textModel() { - return this._textModel; - } - - constructor( - private readonly _proxy: ExtHostNotebookShape, - public handle: number, - public viewType: string, - public supportBackup: boolean, - public uri: URI, - @INotebookService readonly notebookService: INotebookService, - @IUndoRedoService readonly undoRedoService: IUndoRedoService, - @ITextModelService modelService: ITextModelService - - ) { - super(); - - this._textModel = new NotebookTextModel(handle, viewType, supportBackup, uri, undoRedoService, modelService); - this._register(this._textModel.onDidModelChangeProxy(e => { - this._proxy.$acceptModelChanged(this.uri, e); - this._proxy.$acceptEditorPropertiesChanged(uri, { selections: { selections: this._textModel.selections }, metadata: null }); - })); - this._register(this._textModel.onDidSelectionChange(e => { - const selectionsChange = e ? { selections: e } : null; - this._proxy.$acceptEditorPropertiesChanged(uri, { selections: selectionsChange, metadata: null }); - })); - } - - dispose() { - // this._textModel.dispose(); - super.dispose(); - } -} +import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; +import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; +import { ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, CellEditType, CellKind, DisplayOrderKey, ICellEditOperation, ICellRange, IEditor, INotebookDocumentFilter, NotebookCellMetadata, NotebookCellOutputsSplice, NotebookDocumentMetadata, NOTEBOOK_DISPLAY_ORDER, TransientMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IMainNotebookController, INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ExtHostContext, ExtHostNotebookShape, IExtHostContext, INotebookCellStatusBarEntryDto, INotebookDocumentsAndEditorsDelta, MainContext, MainThreadNotebookShape, NotebookEditorRevealType, NotebookExtensionDescription } from '../common/extHost.protocol'; class DocumentAndEditorState { static ofSets(before: Set, after: Set): { removed: T[], added: T[] } { @@ -98,7 +58,7 @@ class DocumentAndEditorState { const apiEditors = []; for (let id in after.textEditors) { const editor = after.textEditors.get(id)!; - apiEditors.push({ id, documentUri: editor.uri!, selections: editor!.textModel!.selections }); + apiEditors.push({ id, documentUri: editor.uri!, selections: editor!.textModel!.selections, visibleRanges: editor.visibleRanges }); } return { @@ -112,7 +72,8 @@ class DocumentAndEditorState { const addedAPIEditors = editorDelta.added.map(add => ({ id: add.getId(), documentUri: add.uri!, - selections: add.textModel!.selections || [] + selections: add.textModel!.selections || [], + visibleRanges: add.visibleRanges })); const removedAPIEditors = editorDelta.removed.map(removed => removed.getId()); @@ -169,12 +130,13 @@ class DocumentAndEditorState { @extHostNamedCustomer(MainContext.MainThreadNotebook) export class MainThreadNotebooks extends Disposable implements MainThreadNotebookShape { private readonly _notebookProviders = new Map(); - private readonly _notebookKernels = new Map(); - private readonly _notebookKernelProviders = new Map, provider: IDisposable }>(); + private readonly _notebookKernelProviders = new Map, provider: IDisposable }>(); private readonly _proxy: ExtHostNotebookShape; private _toDisposeOnEditorRemove = new Map(); private _currentState?: DocumentAndEditorState; private _editorEventListenersMapping: Map = new Map(); + private _documentEventListenersMapping: Map = new Map(); + private readonly _cellStatusBarEntries: Map = new Map(); constructor( extHostContext: IExtHostContext, @@ -182,8 +144,8 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo @IConfigurationService private readonly configurationService: IConfigurationService, @IEditorService private readonly editorService: IEditorService, @IAccessibilityService private readonly accessibilityService: IAccessibilityService, - @ILogService private readonly logService: ILogService - + @ILogService private readonly logService: ILogService, + @INotebookCellStatusBarService private readonly cellStatusBarService: INotebookCellStatusBarService ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostNotebook); @@ -194,7 +156,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); if (textModel) { this._notebookService.transformEditsOutputs(textModel, edits); - return textModel.$applyEdit(modelVersionId, edits, true); + return textModel.applyEdit(modelVersionId, edits, true); } return false; @@ -203,9 +165,9 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo async removeNotebookTextModel(uri: URI): Promise { // TODO@rebornix, remove cell should use emitDelta as well to ensure document/editor events are sent together this._proxy.$acceptDocumentAndEditorsDelta({ removedDocuments: [uri] }); - let textModelDisposableStore = this._editorEventListenersMapping.get(uri.toString()); + let textModelDisposableStore = this._documentEventListenersMapping.get(uri.toString()); textModelDisposableStore?.dispose(); - this._editorEventListenersMapping.delete(URI.from(uri).toString()); + this._documentEventListenersMapping.delete(URI.from(uri).toString()); } private _isDeltaEmpty(delta: INotebookDocumentsAndEditorsDelta) { @@ -265,38 +227,67 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo } })); + const notebookEditorAddedHandler = (editor: IEditor) => { + if (!this._editorEventListenersMapping.has(editor.getId())) { + const disposableStore = new DisposableStore(); + disposableStore.add(editor.onDidChangeVisibleRanges(() => { + this._proxy.$acceptEditorPropertiesChanged(editor.getId(), { visibleRanges: { ranges: editor.visibleRanges } }); + })); + + this._editorEventListenersMapping.set(editor.getId(), disposableStore); + } + }; + this._register(this._notebookService.onNotebookEditorAdd(editor => { + notebookEditorAddedHandler(editor); this._addNotebookEditor(editor); })); this._register(this._notebookService.onNotebookEditorsRemove(editors => { this._removeNotebookEditor(editors); + + editors.forEach(editor => { + this._editorEventListenersMapping.get(editor.getId())?.dispose(); + this._editorEventListenersMapping.delete(editor.getId()); + }); })); + this._notebookService.listNotebookEditors().forEach(editor => { + notebookEditorAddedHandler(editor); + }); + + const notebookDocumentAddedHandler = (doc: URI) => { + if (!this._editorEventListenersMapping.has(doc.toString())) { + const disposableStore = new DisposableStore(); + const textModel = this._notebookService.getNotebookTextModel(doc); + disposableStore.add(textModel!.onDidModelChangeProxy(e => { + this._proxy.$acceptModelChanged(textModel!.uri, e, textModel!.isDirty); + this._proxy.$acceptDocumentPropertiesChanged(doc, { selections: { selections: textModel!.selections }, metadata: null }); + })); + disposableStore.add(textModel!.onDidSelectionChange(e => { + const selectionsChange = e ? { selections: e } : null; + this._proxy.$acceptDocumentPropertiesChanged(doc, { selections: selectionsChange, metadata: null }); + })); + + this._editorEventListenersMapping.set(textModel!.uri.toString(), disposableStore); + } + }; + this._register(this._notebookService.onNotebookDocumentAdd((documents) => { documents.forEach(doc => { - if (!this._editorEventListenersMapping.has(doc.toString())) { - const disposableStore = new DisposableStore(); - const textModel = this._notebookService.getNotebookTextModel(doc); - disposableStore.add(textModel!.onDidModelChangeProxy(e => { - this._proxy.$acceptModelChanged(textModel!.uri, e); - this._proxy.$acceptEditorPropertiesChanged(doc, { selections: { selections: textModel!.selections }, metadata: null }); - })); - disposableStore.add(textModel!.onDidSelectionChange(e => { - const selectionsChange = e ? { selections: e } : null; - this._proxy.$acceptEditorPropertiesChanged(doc, { selections: selectionsChange, metadata: null }); - })); - - this._editorEventListenersMapping.set(textModel!.uri.toString(), disposableStore); - } + notebookDocumentAddedHandler(doc); }); this._updateState(); })); + this._notebookService.listNotebookDocuments().forEach((doc) => { + notebookDocumentAddedHandler(doc.uri); + }); + this._register(this._notebookService.onNotebookDocumentRemove((documents) => { documents.forEach(doc => { - this._editorEventListenersMapping.get(doc.toString())?.dispose(); - this._editorEventListenersMapping.delete(doc.toString()); + this._documentEventListenersMapping.get(doc.toString())?.dispose(); + this._documentEventListenersMapping.delete(doc.toString()); }); this._updateState(); @@ -413,28 +404,28 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo // } } - async $registerNotebookProvider(_extension: NotebookExtensionDescription, _viewType: string, _supportBackup: boolean, _kernel: INotebookKernelInfoDto | undefined): Promise { + async $registerNotebookProvider(_extension: NotebookExtensionDescription, _viewType: string, _supportBackup: boolean, options: { transientOutputs: boolean; transientMetadata: TransientMetadata }): Promise { const controller: IMainNotebookController = { - kernel: _kernel, supportBackup: _supportBackup, + options: options, reloadNotebook: async (mainthreadTextModel: NotebookTextModel) => { const data = await this._proxy.$resolveNotebookData(_viewType, mainthreadTextModel.uri); if (!data) { return; } - mainthreadTextModel.languages = data.languages; + mainthreadTextModel.updateLanguages(data.languages); mainthreadTextModel.metadata = data.metadata; + mainthreadTextModel.transientOptions = options; const edits: ICellEditOperation[] = [ - { editType: CellEditType.Delete, count: mainthreadTextModel.cells.length, index: 0 }, - { editType: CellEditType.Insert, index: 0, cells: data.cells } + { editType: CellEditType.Replace, index: 0, count: mainthreadTextModel.cells.length, cells: data.cells } ]; this._notebookService.transformEditsOutputs(mainthreadTextModel, edits); await new Promise(resolve => { DOM.scheduleAtNextAnimationFrame(() => { - const ret = mainthreadTextModel!.$applyEdit(mainthreadTextModel!.versionId, edits, true); + const ret = mainthreadTextModel!.applyEdit(mainthreadTextModel!.versionId, edits, true); resolve(ret); }); }); @@ -446,40 +437,29 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo return; } - textModel.languages = data.languages; + textModel.updateLanguages(data.languages); textModel.metadata = data.metadata; + textModel.transientOptions = options; if (data.cells.length) { textModel.initialize(data!.cells); } else { - const mainCell = textModel.createCellTextModel([''], textModel.languages.length ? textModel.languages[0] : '', CellKind.Code, [], undefined); + const mainCell = textModel.createCellTextModel('', textModel.resolvedLanguages.length ? textModel.resolvedLanguages[0] : '', CellKind.Code, [], undefined); textModel.insertTemplateCell(mainCell); } - this._proxy.$acceptEditorPropertiesChanged(textModel.uri, { selections: null, metadata: textModel.metadata }); + this._proxy.$acceptDocumentPropertiesChanged(textModel.uri, { selections: null, metadata: textModel.metadata }); return; }, resolveNotebookEditor: async (viewType: string, uri: URI, editorId: string) => { await this._proxy.$resolveNotebookEditor(viewType, uri, editorId); }, - executeNotebookByAttachedKernel: async (viewType: string, uri: URI) => { - return this.executeNotebookByAttachedKernel(viewType, uri, undefined); - }, - cancelNotebookByAttachedKernel: async (viewType: string, uri: URI) => { - return this.cancelNotebookByAttachedKernel(viewType, uri, undefined); - }, onDidReceiveMessage: (editorId: string, rendererType: string | undefined, message: unknown) => { this._proxy.$onDidReceiveMessage(editorId, rendererType, message); }, removeNotebookDocument: async (uri: URI) => { return this.removeNotebookTextModel(uri); }, - executeNotebookCell: async (uri: URI, handle: number) => { - return this.executeNotebookByAttachedKernel(_viewType, uri, handle); - }, - cancelNotebookCell: async (uri: URI, handle: number) => { - return this.cancelNotebookByAttachedKernel(_viewType, uri, handle); - }, save: async (uri: URI, token: CancellationToken) => { return this._proxy.$saveNotebook(_viewType, uri, token); }, @@ -507,21 +487,8 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo return; } - async $registerNotebookKernel(extension: NotebookExtensionDescription, id: string, label: string, selectors: (string | IRelativePattern)[], preloads: UriComponents[]): Promise { - const kernel = new MainThreadNotebookKernel(this._proxy, id, label, selectors, extension.id, URI.revive(extension.location), preloads.map(preload => URI.revive(preload)), this.logService); - this._notebookKernels.set(id, kernel); - this._notebookService.registerNotebookKernel(kernel); - return; - } - - async $unregisterNotebookKernel(id: string): Promise { - this._notebookKernels.delete(id); - this._notebookService.unregisterNotebookKernel(id); - return; - } - async $registerNotebookKernelProvider(extension: NotebookExtensionDescription, handle: number, documentFilter: INotebookDocumentFilter): Promise { - const emitter = new Emitter(); + const emitter = new Emitter(); const that = this; const provider = this._notebookService.registerNotebookKernelProvider({ providerExtensionId: extension.id.value, @@ -568,10 +535,10 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo } } - $onNotebookKernelChange(handle: number): void { + $onNotebookKernelChange(handle: number, uriComponents: UriComponents): void { const entry = this._notebookKernelProviders.get(handle); - entry?.emitter.fire(); + entry?.emitter.fire(uriComponents ? URI.revive(uriComponents) : undefined); } async $updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise { @@ -589,7 +556,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo async $updateNotebookCellMetadata(viewType: string, resource: UriComponents, handle: number, metadata: NotebookCellMetadata): Promise { this.logService.debug('MainThreadNotebooks#updateNotebookCellMetadata', resource.path, handle, metadata); const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); - textModel?.updateNotebookCellMetadata(handle, metadata); + textModel?.changeCellMetadata(handle, metadata, true); } async $spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[]): Promise { @@ -598,20 +565,10 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo if (textModel) { this._notebookService.transformSpliceOutputs(textModel, splices); - textModel.$spliceNotebookCellOutputs(cellHandle, splices); + textModel.spliceNotebookCellOutputs(cellHandle, splices); } } - async executeNotebookByAttachedKernel(viewType: string, uri: URI, handle: number | undefined): Promise { - this.logService.debug('MainthreadNotebooks#executeNotebookByAttachedKernel', uri.path, handle); - return this._proxy.$executeNotebookByAttachedKernel(viewType, uri, handle); - } - - async cancelNotebookByAttachedKernel(viewType: string, uri: URI, handle: number | undefined): Promise { - this.logService.debug('MainthreadNotebooks#cancelNotebookByAttachedKernel', uri.path, handle); - return this._proxy.$cancelNotebookByAttachedKernel(viewType, uri, handle); - } - async $postMessage(editorId: string, forRendererId: string | undefined, value: any): Promise { const editor = this._notebookService.getNotebookEditor(editorId) as INotebookEditor | undefined; if (editor?.isNotebookEditor) { @@ -626,7 +583,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); if (textModel) { - textModel.$handleEdit(label, () => { + textModel.handleEdit(label, () => { return this._proxy.$undoNotebook(textModel.viewType, textModel.uri, editId, textModel.isDirty); }, () => { return this._proxy.$redoNotebook(textModel.viewType, textModel.uri, editId, textModel.isDirty); @@ -638,23 +595,49 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); textModel?.handleUnknownChange(); } -} -export class MainThreadNotebookKernel implements INotebookKernelInfo { - constructor( - private readonly _proxy: ExtHostNotebookShape, - readonly id: string, - readonly label: string, - readonly selectors: (string | IRelativePattern)[], - readonly extension: ExtensionIdentifier, - readonly extensionLocation: URI, - readonly preloads: URI[], - readonly logService: ILogService - ) { + async $tryRevealRange(id: string, range: ICellRange, revealType: NotebookEditorRevealType) { + const editor = this._notebookService.listNotebookEditors().find(editor => editor.getId() === id); + if (editor && editor.isNotebookEditor) { + const notebookEditor = editor as INotebookEditor; + const viewModel = notebookEditor.viewModel; + const cell = viewModel?.viewCells[range.start]; + if (!cell) { + return; + } + + switch (revealType) { + case NotebookEditorRevealType.Default: + notebookEditor.revealInView(cell); + break; + case NotebookEditorRevealType.InCenter: + notebookEditor.revealInCenter(cell); + break; + case NotebookEditorRevealType.InCenterIfOutsideViewport: + notebookEditor.revealInCenterIfOutsideViewport(cell); + break; + default: + break; + } + } } - async executeNotebook(viewType: string, uri: URI, handle: number | undefined): Promise { - this.logService.debug('MainThreadNotebookKernel#executeNotebook', uri.path, handle); - return this._proxy.$executeNotebook2(this.id, viewType, uri, handle); + async $setStatusBarEntry(id: number, rawStatusBarEntry: INotebookCellStatusBarEntryDto): Promise { + const statusBarEntry = { + ...rawStatusBarEntry, + ...{ cellResource: URI.revive(rawStatusBarEntry.cellResource) } + }; + + const existingEntry = this._cellStatusBarEntries.get(id); + if (existingEntry) { + existingEntry.dispose(); + } + + if (statusBarEntry.visible) { + this._cellStatusBarEntries.set( + id, + this.cellStatusBarService.addEntry(statusBarEntry)); + } } } + diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index 7e7143094c..6e1b3a6720 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -178,6 +178,10 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._linkProvider = undefined; } + public $registerProcessSupport(isSupported: boolean): void { + this._terminalService.registerProcessSupport(isSupported); + } + private _onActiveTerminalChanged(terminalId: number | null): void { this._proxy.$acceptActiveTerminalChanged(terminalId); } diff --git a/src/vs/workbench/api/browser/mainThreadWebviewManager.ts b/src/vs/workbench/api/browser/mainThreadWebviewManager.ts new file mode 100644 index 0000000000..cab4df5969 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadWebviewManager.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { MainThreadCustomEditors } from 'vs/workbench/api/browser/mainThreadCustomEditors'; +import { MainThreadWebviewPanels } from 'vs/workbench/api/browser/mainThreadWebviewPanels'; +import { MainThreadWebviews } from 'vs/workbench/api/browser/mainThreadWebviews'; +import { MainThreadWebviewsViews } from 'vs/workbench/api/browser/mainThreadWebviewViews'; +import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol'; +import { extHostCustomer } from '../common/extHostCustomers'; + +@extHostCustomer +export class MainThreadWebviewManager extends Disposable { + constructor( + context: extHostProtocol.IExtHostContext, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + + const webviews = this._register(instantiationService.createInstance(MainThreadWebviews, context)); + context.set(extHostProtocol.MainContext.MainThreadWebviews, webviews); + + const webviewPanels = this._register(instantiationService.createInstance(MainThreadWebviewPanels, context, webviews)); + context.set(extHostProtocol.MainContext.MainThreadWebviewPanels, webviewPanels); + + const customEditors = this._register(instantiationService.createInstance(MainThreadCustomEditors, context, webviews, webviewPanels)); + context.set(extHostProtocol.MainContext.MainThreadCustomEditors, customEditors); + + const webviewViews = this._register(instantiationService.createInstance(MainThreadWebviewsViews, context, webviews)); + context.set(extHostProtocol.MainContext.MainThreadWebviewViews, webviewViews); + } +} diff --git a/src/vs/workbench/api/browser/mainThreadWebview.ts b/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts similarity index 51% rename from src/vs/workbench/api/browser/mainThreadWebview.ts rename to src/vs/workbench/api/browser/mainThreadWebviewPanels.ts index 3a71c5e39a..e17b2c194a 100644 --- a/src/vs/workbench/api/browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts @@ -3,31 +3,21 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { onUnexpectedError } from 'vs/base/common/errors'; import { Disposable, DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { Schemas } from 'vs/base/common/network'; -import { isWeb } from 'vs/base/common/platform'; -import { escape } from 'vs/base/common/strings'; import { URI, UriComponents } from 'vs/base/common/uri'; -import * as modes from 'vs/editor/common/modes'; -import { localize } from 'vs/nls'; -import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { IProductService } from 'vs/platform/product/common/productService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { CustomEditorModelType, MainThreadCustomEditors } from 'vs/workbench/api/browser/mainThreadCustomEditors'; -import { MainThreadWebviewSerializers } from 'vs/workbench/api/browser/mainThreadWebviewSerializer'; -import { MainThreadWebviewsViews } from 'vs/workbench/api/browser/mainThreadWebviewViews'; +import { MainThreadWebviews, reviveWebviewExtension, reviveWebviewOptions } from 'vs/workbench/api/browser/mainThreadWebviews'; import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol'; import { editorGroupToViewColumn, EditorViewColumn, viewColumnToEditorGroup } from 'vs/workbench/api/common/shared/editor'; import { IEditorInput } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; -import { Webview, WebviewExtensionDescription, WebviewIcons, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview'; +import { WebviewIcons } from 'vs/workbench/contrib/webview/browser/webview'; import { WebviewInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput'; import { ICreateWebViewShowOptions, IWebviewWorkbenchService, WebviewInputOptions } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { extHostNamedCustomer } from '../common/extHostCustomers'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; /** * Bi-directional map between webview handles and inputs. @@ -82,48 +72,31 @@ class WebviewViewTypeTransformer { } } -@extHostNamedCustomer(extHostProtocol.MainContext.MainThreadWebviews) -export class MainThreadWebviews extends Disposable implements extHostProtocol.MainThreadWebviewsShape { +export class MainThreadWebviewPanels extends Disposable implements extHostProtocol.MainThreadWebviewPanelsShape { - private static readonly standardSupportedLinkSchemes = new Set([ - Schemas.http, - Schemas.https, - Schemas.mailto, - Schemas.vscode, - 'vscode-insider', - ]); + private readonly webviewPanelViewType = new WebviewViewTypeTransformer('mainThreadWebview-'); - public readonly webviewPanelViewType = new WebviewViewTypeTransformer('mainThreadWebview-'); + private readonly _proxy: extHostProtocol.ExtHostWebviewPanelsShape; - private readonly _proxy: extHostProtocol.ExtHostWebviewsShape; - - private readonly _webviews = new Map(); private readonly _webviewInputs = new WebviewInputStore(); private readonly _editorProviders = new Map(); private readonly _webviewFromDiffEditorHandles = new Set(); - private readonly serializers: MainThreadWebviewSerializers; - private readonly customEditors: MainThreadCustomEditors; - private readonly webviewViews: MainThreadWebviewsViews; + private readonly _revivers = new Map(); constructor( context: extHostProtocol.IExtHostContext, + private readonly _mainThreadWebviews: MainThreadWebviews, + @IExtensionService extensionService: IExtensionService, @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, @IEditorService private readonly _editorService: IEditorService, - @IOpenerService private readonly _openerService: IOpenerService, - @IProductService private readonly _productService: IProductService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @IWebviewWorkbenchService private readonly _webviewWorkbenchService: IWebviewWorkbenchService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { super(); - this._proxy = context.getProxy(extHostProtocol.ExtHostContext.ExtHostWebviews); - - this.serializers = this._instantiationService.createInstance(MainThreadWebviewSerializers, this, context); - this.customEditors = this._instantiationService.createInstance(MainThreadCustomEditors, this, context); - this.webviewViews = this._instantiationService.createInstance(MainThreadWebviewsViews, this, context); + this._proxy = context.getProxy(extHostProtocol.ExtHostContext.ExtHostWebviewPanels); this._register(_editorService.onDidActiveEditorChange(() => { const activeInput = this._editorService.activeEditor; @@ -137,6 +110,19 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma this._register(_editorService.onDidVisibleEditorsChange(() => { this.updateWebviewViewStates(this._editorService.activeEditor); })); + + // This reviver's only job is to activate extensions. + // This should trigger the real reviver to be registered from the extension host side. + this._register(_webviewWorkbenchService.registerResolver({ + canResolve: (webview: WebviewInput) => { + const viewType = this.webviewPanelViewType.toExternal(webview.viewType); + if (typeof viewType === 'string') { + extensionService.activateByEvent(`onWebviewPanel:${viewType}`); + } + return false; + }, + resolveWebview: () => { throw new Error('not implemented'); } + })); } dispose() { @@ -150,19 +136,20 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma public get webviewInputs(): Iterable { return this._webviewInputs; } - public addWebviewInput(handle: extHostProtocol.WebviewPanelHandle, input: WebviewInput): void { + public addWebviewInput(handle: extHostProtocol.WebviewHandle, input: WebviewInput): void { this._webviewInputs.add(handle, input); - this.addWebview(handle, input.webview); - } + this._mainThreadWebviews.addWebview(handle, input.webview); - public addWebview(handle: extHostProtocol.WebviewPanelHandle, webview: WebviewOverlay): void { - this._webviews.set(handle, webview); - this.hookupWebviewEventDelegate(handle, webview); + input.webview.onDispose(() => { + this._proxy.$onDidDisposeWebviewPanel(handle).finally(() => { + this._webviewInputs.delete(handle); + }); + }); } public $createWebviewPanel( extensionData: extHostProtocol.WebviewExtensionDescription, - handle: extHostProtocol.WebviewPanelHandle, + handle: extHostProtocol.WebviewHandle, viewType: string, title: string, showOptions: { viewColumn?: EditorViewColumn, preserveFocus?: boolean; }, @@ -175,10 +162,9 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma } const extension = reviveWebviewExtension(extensionData); - const webview = this._webviewWorkbenchService.createWebview(handle, this.webviewPanelViewType.fromExternal(viewType), title, mainThreadShowOptions, reviveWebviewOptions(options), extension); - this.hookupWebviewEventDelegate(handle, webview.webview); - this._webviewInputs.add(handle, webview); + const webview = this._webviewWorkbenchService.createWebview(handle, this.webviewPanelViewType.fromExternal(viewType), title, mainThreadShowOptions, reviveWebviewOptions(options), extension); + this.addWebviewInput(handle, webview); /* __GDPR__ "webviews:createWebviewPanel" : { @@ -188,36 +174,23 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma this._telemetryService.publicLog('webviews:createWebviewPanel', { extensionId: extension.id.value }); } - public $disposeWebview(handle: extHostProtocol.WebviewPanelHandle): void { + public $disposeWebview(handle: extHostProtocol.WebviewHandle): void { const webview = this.getWebviewInput(handle); webview.dispose(); } - public $setTitle(handle: extHostProtocol.WebviewPanelHandle, value: string): void { + public $setTitle(handle: extHostProtocol.WebviewHandle, value: string): void { const webview = this.getWebviewInput(handle); webview.setName(value); } - public $setWebviewViewTitle(handle: extHostProtocol.WebviewPanelHandle, value: string | undefined): void { - this.webviewViews.$setWebviewViewTitle(handle, value); - } - public $setIconPath(handle: extHostProtocol.WebviewPanelHandle, value: { light: UriComponents, dark: UriComponents; } | undefined): void { + public $setIconPath(handle: extHostProtocol.WebviewHandle, value: { light: UriComponents, dark: UriComponents; } | undefined): void { const webview = this.getWebviewInput(handle); webview.iconPath = reviveWebviewIcon(value); } - public $setHtml(handle: extHostProtocol.WebviewPanelHandle, value: string): void { - const webview = this.getWebview(handle); - webview.html = value; - } - - public $setOptions(handle: extHostProtocol.WebviewPanelHandle, options: modes.IWebviewOptions): void { - const webview = this.getWebview(handle); - webview.contentOptions = reviveWebviewOptions(options); - } - - public $reveal(handle: extHostProtocol.WebviewPanelHandle, showOptions: extHostProtocol.WebviewPanelShowOptions): void { + public $reveal(handle: extHostProtocol.WebviewHandle, showOptions: extHostProtocol.WebviewPanelShowOptions): void { const webview = this.getWebviewInput(handle); if (webview.isDisposed()) { return; @@ -229,63 +202,55 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma } } - public async $postMessage(handle: extHostProtocol.WebviewPanelHandle, message: any): Promise { - const webview = this.getWebview(handle); - webview.postMessage(message); - return true; - } + public $registerSerializer(viewType: string) + : void { + if (this._revivers.has(viewType)) { + throw new Error(`Reviver for ${viewType} already registered`); + } - public $registerSerializer(viewType: string): void { - this.serializers.$registerSerializer(viewType); + this._revivers.set(viewType, this._webviewWorkbenchService.registerResolver({ + canResolve: (webviewInput) => { + return webviewInput.viewType === this.webviewPanelViewType.fromExternal(viewType); + }, + resolveWebview: async (webviewInput): Promise => { + const viewType = this.webviewPanelViewType.toExternal(webviewInput.viewType); + if (!viewType) { + webviewInput.webview.html = this._mainThreadWebviews.getWebviewResolvedFailedContent(webviewInput.viewType); + return; + } + + + const handle = webviewInput.id; + + this.addWebviewInput(handle, webviewInput); + + let state = undefined; + if (webviewInput.webview.state) { + try { + state = JSON.parse(webviewInput.webview.state); + } catch (e) { + console.error('Could not load webview state', e, webviewInput.webview.state); + } + } + + try { + await this._proxy.$deserializeWebviewPanel(handle, viewType, webviewInput.getTitle(), state, editorGroupToViewColumn(this._editorGroupService, webviewInput.group || 0), webviewInput.webview.options); + } catch (error) { + onUnexpectedError(error); + webviewInput.webview.html = this._mainThreadWebviews.getWebviewResolvedFailedContent(viewType); + } + } + })); } public $unregisterSerializer(viewType: string): void { - this.serializers.$unregisterSerializer(viewType); - } + const reviver = this._revivers.get(viewType); + if (!reviver) { + throw new Error(`No reviver for ${viewType} registered`); + } - public $registerWebviewViewProvider(viewType: string, options?: { retainContextWhenHidden?: boolean }): void { - this.webviewViews.$registerWebviewViewProvider(viewType, options); - } - - public $unregisterWebviewViewProvider(viewType: string): void { - this.webviewViews.$unregisterWebviewViewProvider(viewType); - } - - public $registerTextEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions, capabilities: extHostProtocol.CustomTextEditorCapabilities): void { - this.customEditors.registerEditorProvider(CustomEditorModelType.Text, reviveWebviewExtension(extensionData), viewType, options, capabilities, true); - } - - public $registerCustomEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions, supportsMultipleEditorsPerDocument: boolean): void { - this.customEditors.registerEditorProvider(CustomEditorModelType.Custom, reviveWebviewExtension(extensionData), viewType, options, {}, supportsMultipleEditorsPerDocument); - } - - public $unregisterEditorProvider(viewType: string): void { - this.customEditors.$unregisterEditorProvider(viewType); - } - - public async $onDidEdit(resourceComponents: UriComponents, viewType: string, editId: number, label: string | undefined): Promise { - this.customEditors.$onDidEdit(resourceComponents, viewType, editId, label); - } - - public async $onContentChange(resourceComponents: UriComponents, viewType: string): Promise { - this.customEditors.$onContentChange(resourceComponents, viewType); - } - - public hookupWebviewEventDelegate(handle: extHostProtocol.WebviewPanelHandle, webview: WebviewOverlay) { - const disposables = new DisposableStore(); - - disposables.add(webview.onDidClickLink((uri) => this.onDidClickLink(handle, uri))); - disposables.add(webview.onMessage((message: any) => { this._proxy.$onMessage(handle, message); })); - disposables.add(webview.onMissingCsp((extension: ExtensionIdentifier) => this._proxy.$onMissingCsp(handle, extension.value))); - - disposables.add(webview.onDispose(() => { - disposables.dispose(); - - this._proxy.$onDidDisposeWebviewPanel(handle).finally(() => { - this._webviews.delete(handle); - this._webviewInputs.delete(handle); - }); - })); + reviver.dispose(); + this._revivers.delete(viewType); } private registerWebviewFromDiffEditorListeners(diffEditorInput: DiffEditorInput): void { @@ -349,32 +314,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma } } - private onDidClickLink(handle: extHostProtocol.WebviewPanelHandle, link: string): void { - const webview = this.getWebviewInput(handle); - if (this.isSupportedLink(webview, URI.parse(link))) { - this._openerService.open(link, { fromUserGesture: true }); - } - } - - private isSupportedLink(webview: WebviewInput, link: URI): boolean { - if (MainThreadWebviews.standardSupportedLinkSchemes.has(link.scheme)) { - return true; - } - if (!isWeb && this._productService.urlProtocol === link.scheme) { - return true; - } - return !!webview.webview.contentOptions.enableCommandUris && link.scheme === Schemas.command; - } - - private getWebview(handle: extHostProtocol.WebviewPanelHandle): Webview { - const webview = this._webviews.get(handle); - if (!webview) { - throw new Error(`Unknown webview handle:${handle}`); - } - return webview; - } - - private getWebviewInput(handle: extHostProtocol.WebviewPanelHandle): WebviewInput { + private getWebviewInput(handle: extHostProtocol.WebviewHandle): WebviewInput { const webview = this.tryGetWebviewInput(handle); if (!webview) { throw new Error(`Unknown webview handle:${handle}`); @@ -382,33 +322,11 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma return webview; } - private tryGetWebviewInput(handle: extHostProtocol.WebviewPanelHandle): WebviewInput | undefined { + private tryGetWebviewInput(handle: extHostProtocol.WebviewHandle): WebviewInput | undefined { return this._webviewInputs.getInputForHandle(handle); } - - public getWebviewResolvedFailedContent(viewType: string) { - return ` - - - - - - ${localize('errorMessage', "An error occurred while loading view: {0}", escape(viewType))} - `; - } } -function reviveWebviewExtension(extensionData: extHostProtocol.WebviewExtensionDescription): WebviewExtensionDescription { - return { id: extensionData.id, location: URI.revive(extensionData.location) }; -} - -function reviveWebviewOptions(options: modes.IWebviewOptions): WebviewInputOptions { - return { - ...options, - allowScripts: options.enableScripts, - localResourceRoots: Array.isArray(options.localResourceRoots) ? options.localResourceRoots.map(r => URI.revive(r)) : undefined, - }; -} function reviveWebviewIcon( value: { light: UriComponents, dark: UriComponents; } | undefined diff --git a/src/vs/workbench/api/browser/mainThreadWebviewSerializer.ts b/src/vs/workbench/api/browser/mainThreadWebviewSerializer.ts deleted file mode 100644 index 01247c9e86..0000000000 --- a/src/vs/workbench/api/browser/mainThreadWebviewSerializer.ts +++ /dev/null @@ -1,102 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { onUnexpectedError } from 'vs/base/common/errors'; -import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import type { MainThreadWebviews } from 'vs/workbench/api/browser/mainThreadWebview'; -import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol'; -import { editorGroupToViewColumn } from 'vs/workbench/api/common/shared/editor'; -import { CustomEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput'; -import { WebviewInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput'; -import { IWebviewWorkbenchService } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService'; -import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; - -export class MainThreadWebviewSerializers extends Disposable { - - private readonly _proxy: extHostProtocol.ExtHostWebviewSerializerShape; - - private readonly _revivers = new Map(); - - constructor( - private readonly mainThreadWebviews: MainThreadWebviews, - context: extHostProtocol.IExtHostContext, - @IExtensionService extensionService: IExtensionService, - @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, - @IWebviewWorkbenchService private readonly _webviewWorkbenchService: IWebviewWorkbenchService, - ) { - super(); - - this._proxy = context.getProxy(extHostProtocol.ExtHostContext.ExtHostWebviewSerializer); - - // This reviver's only job is to activate extensions. - // This should trigger the real reviver to be registered from the extension host side. - this._register(_webviewWorkbenchService.registerResolver({ - canResolve: (webview: WebviewInput) => { - if (webview instanceof CustomEditorInput) { - extensionService.activateByEvent(`onCustomEditor:${webview.viewType}`); - return false; - } - - const viewType = this.mainThreadWebviews.webviewPanelViewType.toExternal(webview.viewType); - if (typeof viewType === 'string') { - extensionService.activateByEvent(`onWebviewPanel:${viewType}`); - } - return false; - }, - resolveWebview: () => { throw new Error('not implemented'); } - })); - } - - public $registerSerializer(viewType: string): void { - if (this._revivers.has(viewType)) { - throw new Error(`Reviver for ${viewType} already registered`); - } - - this._revivers.set(viewType, this._webviewWorkbenchService.registerResolver({ - canResolve: (webviewInput) => { - return webviewInput.viewType === this.mainThreadWebviews.webviewPanelViewType.fromExternal(viewType); - }, - resolveWebview: async (webviewInput): Promise => { - const viewType = this.mainThreadWebviews.webviewPanelViewType.toExternal(webviewInput.viewType); - if (!viewType) { - webviewInput.webview.html = this.mainThreadWebviews.getWebviewResolvedFailedContent(webviewInput.viewType); - return; - } - - - const handle = webviewInput.id; - - this.mainThreadWebviews.addWebviewInput(handle, webviewInput); - - let state = undefined; - if (webviewInput.webview.state) { - try { - state = JSON.parse(webviewInput.webview.state); - } catch (e) { - console.error('Could not load webview state', e, webviewInput.webview.state); - } - } - - try { - await this._proxy.$deserializeWebviewPanel(handle, viewType, webviewInput.getTitle(), state, editorGroupToViewColumn(this._editorGroupService, webviewInput.group || 0), webviewInput.webview.options); - } catch (error) { - onUnexpectedError(error); - webviewInput.webview.html = this.mainThreadWebviews.getWebviewResolvedFailedContent(viewType); - } - } - })); - } - - public $unregisterSerializer(viewType: string): void { - const reviver = this._revivers.get(viewType); - if (!reviver) { - throw new Error(`No reviver for ${viewType} registered`); - } - - reviver.dispose(); - this._revivers.delete(viewType); - } -} diff --git a/src/vs/workbench/api/browser/mainThreadWebviewViews.ts b/src/vs/workbench/api/browser/mainThreadWebviewViews.ts index 9a44982fff..29e507a070 100644 --- a/src/vs/workbench/api/browser/mainThreadWebviewViews.ts +++ b/src/vs/workbench/api/browser/mainThreadWebviewViews.ts @@ -6,29 +6,29 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import type { MainThreadWebviews } from 'vs/workbench/api/browser/mainThreadWebview'; +import { MainThreadWebviews } from 'vs/workbench/api/browser/mainThreadWebviews'; import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol'; import { IWebviewViewService, WebviewView } from 'vs/workbench/contrib/webviewView/browser/webviewViewService'; -export class MainThreadWebviewsViews extends Disposable { +export class MainThreadWebviewsViews extends Disposable implements extHostProtocol.MainThreadWebviewViewsShape { - private readonly _proxyViews: extHostProtocol.ExtHostWebviewViewsShape; + private readonly _proxy: extHostProtocol.ExtHostWebviewViewsShape; private readonly _webviewViews = new Map(); private readonly _webviewViewProviders = new Map(); constructor( - private readonly mainThreadWebviews: MainThreadWebviews, context: extHostProtocol.IExtHostContext, + private readonly mainThreadWebviews: MainThreadWebviews, @IWebviewViewService private readonly _webviewViewService: IWebviewViewService, ) { super(); - this._proxyViews = context.getProxy(extHostProtocol.ExtHostContext.ExtHostWebviewViews); + this._proxy = context.getProxy(extHostProtocol.ExtHostContext.ExtHostWebviewViews); } - public $setWebviewViewTitle(handle: extHostProtocol.WebviewPanelHandle, value: string | undefined): void { + public $setWebviewViewTitle(handle: extHostProtocol.WebviewHandle, value: string | undefined): void { const webviewView = this._webviewViews.get(handle); if (!webviewView) { throw new Error('unknown webview view'); @@ -43,9 +43,9 @@ export class MainThreadWebviewsViews extends Disposable { this._webviewViewService.register(viewType, { resolve: async (webviewView: WebviewView, cancellation: CancellationToken) => { - this._webviewViews.set(viewType, webviewView); - const handle = webviewView.webview.id; + + this._webviewViews.set(handle, webviewView); this.mainThreadWebviews.addWebview(handle, webviewView.webview); let state = undefined; @@ -62,15 +62,16 @@ export class MainThreadWebviewsViews extends Disposable { } webviewView.onDidChangeVisibility(visible => { - this._proxyViews.$onDidChangeWebviewViewVisibility(handle, visible); + this._proxy.$onDidChangeWebviewViewVisibility(handle, visible); }); webviewView.onDispose(() => { - this._proxyViews.$disposeWebviewView(handle); + this._proxy.$disposeWebviewView(handle); + this._webviewViews.delete(handle); }); try { - await this._proxyViews.$resolveWebviewView(handle, viewType, state, cancellation); + await this._proxy.$resolveWebviewView(handle, viewType, state, cancellation); } catch (error) { onUnexpectedError(error); webviewView.webview.html = this.mainThreadWebviews.getWebviewResolvedFailedContent(viewType); diff --git a/src/vs/workbench/api/browser/mainThreadWebviews.ts b/src/vs/workbench/api/browser/mainThreadWebviews.ts new file mode 100644 index 0000000000..0dc4fa8566 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadWebviews.ts @@ -0,0 +1,125 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; +import { isWeb } from 'vs/base/common/platform'; +import { escape } from 'vs/base/common/strings'; +import { URI } from 'vs/base/common/uri'; +import { IWebviewOptions } from 'vs/editor/common/modes'; +import { localize } from 'vs/nls'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IProductService } from 'vs/platform/product/common/productService'; +import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol'; +import { Webview, WebviewExtensionDescription, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview'; +import { WebviewInputOptions } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService'; + +export class MainThreadWebviews extends Disposable implements extHostProtocol.MainThreadWebviewsShape { + + private static readonly standardSupportedLinkSchemes = new Set([ + Schemas.http, + Schemas.https, + Schemas.mailto, + Schemas.vscode, + 'vscode-insider', + ]); + + private readonly _proxy: extHostProtocol.ExtHostWebviewsShape; + + private readonly _webviews = new Map(); + + constructor( + context: extHostProtocol.IExtHostContext, + @IOpenerService private readonly _openerService: IOpenerService, + @IProductService private readonly _productService: IProductService, + ) { + super(); + + this._proxy = context.getProxy(extHostProtocol.ExtHostContext.ExtHostWebviews); + } + + public addWebview(handle: extHostProtocol.WebviewHandle, webview: WebviewOverlay): void { + this._webviews.set(handle, webview); + this.hookupWebviewEventDelegate(handle, webview); + } + + public $setHtml(handle: extHostProtocol.WebviewHandle, value: string): void { + const webview = this.getWebview(handle); + webview.html = value; + } + + public $setOptions(handle: extHostProtocol.WebviewHandle, options: IWebviewOptions): void { + const webview = this.getWebview(handle); + webview.contentOptions = reviveWebviewOptions(options); + } + + public async $postMessage(handle: extHostProtocol.WebviewHandle, message: any): Promise { + const webview = this.getWebview(handle); + webview.postMessage(message); + return true; + } + + private hookupWebviewEventDelegate(handle: extHostProtocol.WebviewHandle, webview: WebviewOverlay) { + const disposables = new DisposableStore(); + + disposables.add(webview.onDidClickLink((uri) => this.onDidClickLink(handle, uri))); + disposables.add(webview.onMessage((message: any) => { this._proxy.$onMessage(handle, message); })); + disposables.add(webview.onMissingCsp((extension: ExtensionIdentifier) => this._proxy.$onMissingCsp(handle, extension.value))); + + disposables.add(webview.onDispose(() => { + disposables.dispose(); + this._webviews.delete(handle); + })); + } + + private onDidClickLink(handle: extHostProtocol.WebviewHandle, link: string): void { + const webview = this.getWebview(handle); + if (this.isSupportedLink(webview, URI.parse(link))) { + this._openerService.open(link, { fromUserGesture: true }); + } + } + + private isSupportedLink(webview: Webview, link: URI): boolean { + if (MainThreadWebviews.standardSupportedLinkSchemes.has(link.scheme)) { + return true; + } + if (!isWeb && this._productService.urlProtocol === link.scheme) { + return true; + } + return !!webview.contentOptions.enableCommandUris && link.scheme === Schemas.command; + } + + private getWebview(handle: extHostProtocol.WebviewHandle): Webview { + const webview = this._webviews.get(handle); + if (!webview) { + throw new Error(`Unknown webview handle:${handle}`); + } + return webview; + } + + public getWebviewResolvedFailedContent(viewType: string) { + return ` + + + + + + ${localize('errorMessage', "An error occurred while loading view: {0}", escape(viewType))} + `; + } +} + +export function reviveWebviewExtension(extensionData: extHostProtocol.WebviewExtensionDescription): WebviewExtensionDescription { + return { id: extensionData.id, location: URI.revive(extensionData.location) }; +} + +export function reviveWebviewOptions(options: IWebviewOptions): WebviewInputOptions { + return { + ...options, + allowScripts: options.enableScripts, + localResourceRoots: Array.isArray(options.localResourceRoots) ? options.localResourceRoots.map(r => URI.revive(r)) : undefined, + }; +} diff --git a/src/vs/workbench/api/browser/viewsExtensionPoint.ts b/src/vs/workbench/api/browser/viewsExtensionPoint.ts index 0eea849191..e5c3ca3502 100644 --- a/src/vs/workbench/api/browser/viewsExtensionPoint.ts +++ b/src/vs/workbench/api/browser/viewsExtensionPoint.ts @@ -473,6 +473,11 @@ class ViewsExtensionHandler implements IWorkbenchContribution { return null; } + if (type === ViewType.Webview && !extension.description.enableProposedApi) { + collector.error(localize('webviewViewsRequireProposed', "Webview views are proposed api and are only supported when running out of dev or with the following command line switch: --enable-proposed-api")); + return null; + } + const viewDescriptor = { type: type, ctorDescriptor: type === ViewType.Tree ? new SyncDescriptor(TreeViewPane) : new SyncDescriptor(WebviewViewPane), diff --git a/src/vs/workbench/api/common/apiCommands.ts b/src/vs/workbench/api/common/apiCommands.ts index c4468df392..f02ec7025f 100644 --- a/src/vs/workbench/api/common/apiCommands.ts +++ b/src/vs/workbench/api/common/apiCommands.ts @@ -16,6 +16,7 @@ import { IWorkspacesService, hasWorkspaceFileExtension, IRecent } from 'vs/platf import { Schemas } from 'vs/base/common/network'; import { ILogService } from 'vs/platform/log/common/log'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IViewDescriptorService, IViewsService } from 'vs/workbench/common/views'; // ----------------------------------------------------------------- // The following commands are registered on both sides separately. @@ -264,3 +265,42 @@ CommandsRegistry.registerCommand('_extensionTests.getLogLevel', function (access return logService.getLevel(); }); + + +CommandsRegistry.registerCommand('_workbench.action.moveViews', async function (accessor: ServicesAccessor, options: { viewIds: string[], destinationId: string }) { + const viewDescriptorService = accessor.get(IViewDescriptorService); + + const destination = viewDescriptorService.getViewContainerById(options.destinationId); + if (!destination) { + return; + } + + // FYI, don't use `moveViewsToContainer` in 1 shot, because it expects all views to have the same current location + for (const viewId of options.viewIds) { + const viewDescriptor = viewDescriptorService.getViewDescriptorById(viewId); + if (viewDescriptor?.canMoveView) { + viewDescriptorService.moveViewsToContainer([viewDescriptor], destination); + } + } + + await accessor.get(IViewsService).openViewContainer(destination.id, true); +}); + +export class MoveViewsAPICommand { + public static readonly ID = 'vscode.moveViews'; + public static execute(executor: ICommandsExecutor, options: { viewIds: string[], destinationId: string }): Promise { + if (!Array.isArray(options?.viewIds) || typeof options?.destinationId !== 'string') { + return Promise.reject('Invalid arguments'); + } + + return executor.executeCommand('_workbench.action.moveViews', options); + } +} +CommandsRegistry.registerCommand({ + id: MoveViewsAPICommand.ID, + handler: adjustHandler(MoveViewsAPICommand.execute), + description: { + description: 'Move Views', + args: [] + } +}); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index fe97630c74..cde8d5a432 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -79,7 +79,7 @@ import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePa import { IExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileSystemConsumer'; import { ExtHostWebviewViews } from 'vs/workbench/api/common/extHostWebviewView'; import { ExtHostCustomEditors } from 'vs/workbench/api/common/extHostCustomEditors'; -import { ExtHostWebviewSerializer } from 'vs/workbench/api/common/extHostWebviewSerializer'; +import { ExtHostWebviewPanels } from 'vs/workbench/api/common/extHostWebviewPanels'; export interface IExtensionApiFactory { (extension: IExtensionDescription, registry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): typeof vscode; @@ -129,7 +129,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostDocuments = rpcProtocol.set(ExtHostContext.ExtHostDocuments, new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors)); const extHostDocumentContentProviders = rpcProtocol.set(ExtHostContext.ExtHostDocumentContentProviders, new ExtHostDocumentContentProvider(rpcProtocol, extHostDocumentsAndEditors, extHostLogService)); const extHostDocumentSaveParticipant = rpcProtocol.set(ExtHostContext.ExtHostDocumentSaveParticipant, new ExtHostDocumentSaveParticipant(extHostLogService, extHostDocuments, rpcProtocol.getProxy(MainContext.MainThreadTextEditors))); - const extHostEditors = rpcProtocol.set(ExtHostContext.ExtHostEditors, new ExtHostEditors(rpcProtocol, extHostDocumentsAndEditors)); + const extHostNotebook = rpcProtocol.set(ExtHostContext.ExtHostNotebook, new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, initData.environment, extHostLogService, extensionStoragePaths)); + const extHostEditors = rpcProtocol.set(ExtHostContext.ExtHostEditors, new ExtHostEditors(rpcProtocol, extHostDocumentsAndEditors, extHostNotebook)); const extHostTreeViews = rpcProtocol.set(ExtHostContext.ExtHostTreeViews, new ExtHostTreeViews(rpcProtocol.getProxy(MainContext.MainThreadTreeViews), extHostCommands, extHostLogService)); const extHostEditorInsets = rpcProtocol.set(ExtHostContext.ExtHostEditorInsets, new ExtHostEditorInsets(rpcProtocol.getProxy(MainContext.MainThreadEditorInsets), extHostEditors, initData.environment)); const extHostDiagnostics = rpcProtocol.set(ExtHostContext.ExtHostDiagnostics, new ExtHostDiagnostics(rpcProtocol, extHostLogService)); @@ -141,13 +142,12 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostComment = rpcProtocol.set(ExtHostContext.ExtHostComments, new ExtHostComments(rpcProtocol, extHostCommands, extHostDocuments)); const extHostProgress = rpcProtocol.set(ExtHostContext.ExtHostProgress, new ExtHostProgress(rpcProtocol.getProxy(MainContext.MainThreadProgress))); const extHostLabelService = rpcProtocol.set(ExtHostContext.ExtHosLabelService, new ExtHostLabelService(rpcProtocol)); - const extHostNotebook = rpcProtocol.set(ExtHostContext.ExtHostNotebook, new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, initData.environment, extHostLogService, extensionStoragePaths)); const extHostTheming = rpcProtocol.set(ExtHostContext.ExtHostTheming, new ExtHostTheming(rpcProtocol)); const extHostAuthentication = rpcProtocol.set(ExtHostContext.ExtHostAuthentication, new ExtHostAuthentication(rpcProtocol)); const extHostTimeline = rpcProtocol.set(ExtHostContext.ExtHostTimeline, new ExtHostTimeline(rpcProtocol, extHostCommands)); const extHostWebviews = rpcProtocol.set(ExtHostContext.ExtHostWebviews, new ExtHostWebviews(rpcProtocol, initData.environment, extHostWorkspace, extHostLogService, extHostApiDeprecation)); - const extHostWebviewSerializers = rpcProtocol.set(ExtHostContext.ExtHostWebviewSerializer, new ExtHostWebviewSerializer(rpcProtocol, extHostWebviews)); - const extHostCustomEditors = rpcProtocol.set(ExtHostContext.ExtHostCustomEditors, new ExtHostCustomEditors(rpcProtocol, extHostDocuments, extensionStoragePaths, extHostWebviews)); + const extHostWebviewPanels = rpcProtocol.set(ExtHostContext.ExtHostWebviewPanels, new ExtHostWebviewPanels(rpcProtocol, extHostWebviews, extHostWorkspace)); + const extHostCustomEditors = rpcProtocol.set(ExtHostContext.ExtHostCustomEditors, new ExtHostCustomEditors(rpcProtocol, extHostDocuments, extensionStoragePaths, extHostWebviews, extHostWebviewPanels)); const extHostWebviewViews = rpcProtocol.set(ExtHostContext.ExtHostWebviewViews, new ExtHostWebviewViews(rpcProtocol, extHostWebviews)); // Check that no named customers are missing @@ -573,7 +573,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostOutputService.createOutputChannel(name); }, createWebviewPanel(viewType: string, title: string, showOptions: vscode.ViewColumn | { viewColumn: vscode.ViewColumn, preserveFocus?: boolean }, options?: vscode.WebviewPanelOptions & vscode.WebviewOptions): vscode.WebviewPanel { - return extHostWebviews.createWebviewPanel(extension, viewType, title, showOptions, options); + return extHostWebviewPanels.createWebviewPanel(extension, viewType, title, showOptions, options); }, createWebviewTextEditorInset(editor: vscode.TextEditor, line: number, height: number, options?: vscode.WebviewOptions): vscode.WebviewEditorInset { checkProposedApiEnabled(extension); @@ -598,7 +598,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostTreeViews.createTreeView(viewId, options, extension); }, registerWebviewPanelSerializer: (viewType: string, serializer: vscode.WebviewPanelSerializer) => { - return extHostWebviewSerializers.registerWebviewPanelSerializer(extension, viewType, serializer); + return extHostWebviewPanels.registerWebviewPanelSerializer(extension, viewType, serializer); }, registerCustomEditorProvider: (viewType: string, provider: vscode.CustomTextEditorProvider | vscode.CustomReadonlyEditorProvider, options: { webviewOptions?: vscode.WebviewPanelOptions, supportsMultipleEditorsPerDocument?: boolean } = {}) => { return extHostCustomEditors.registerCustomEditorProvider(extension, viewType, provider, options); @@ -958,7 +958,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }, get notebookDocuments(): vscode.NotebookDocument[] { checkProposedApiEnabled(extension); - return extHostNotebook.notebookDocuments; + return extHostNotebook.notebookDocuments.map(d => d.notebookDocument); }, get visibleNotebookEditors() { checkProposedApiEnabled(extension); @@ -972,13 +972,12 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension); return extHostNotebook.onDidChangeActiveNotebookKernel; }, - registerNotebookContentProvider: (viewType: string, provider: vscode.NotebookContentProvider) => { + registerNotebookContentProvider: (viewType: string, provider: vscode.NotebookContentProvider, options?: { + transientOutputs: boolean; + transientMetadata: { [K in keyof vscode.NotebookCellMetadata]?: boolean } + }) => { checkProposedApiEnabled(extension); - return extHostNotebook.registerNotebookContentProvider(extension, viewType, provider); - }, - registerNotebookKernel: (id: string, selector: vscode.GlobPattern[], kernel: vscode.NotebookKernel) => { - checkProposedApiEnabled(extension); - return extHostNotebook.registerNotebookKernel(extension, id, selector, kernel); + return extHostNotebook.registerNotebookContentProvider(extension, viewType, provider, options); }, registerNotebookKernelProvider: (selector: vscode.NotebookDocumentFilter, provider: vscode.NotebookKernelProvider) => { checkProposedApiEnabled(extension); @@ -996,6 +995,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension); return extHostNotebook.onDidChangeNotebookCells(listener, thisArgs, disposables); }, + onDidChangeNotebookEditorSelection(listener, thisArgs?, disposables?) { + checkProposedApiEnabled(extension); + return extHostNotebook.onDidChangeNotebookEditorSelection(listener, thisArgs, disposables); + }, + onDidChangeNotebookEditorVisibleRanges(listener, thisArgs?, disposables?) { + checkProposedApiEnabled(extension); + return extHostNotebook.onDidChangeNotebookEditorVisibleRanges(listener, thisArgs, disposables); + }, onDidChangeCellOutputs(listener, thisArgs?, disposables?) { checkProposedApiEnabled(extension); return extHostNotebook.onDidChangeCellOutputs(listener, thisArgs, disposables); @@ -1011,6 +1018,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I createConcatTextDocument(notebook, selector) { checkProposedApiEnabled(extension); return new ExtHostNotebookConcatDocument(extHostNotebook, extHostDocuments, notebook, selector); + }, + createCellStatusBarItem(cell: vscode.NotebookCell, alignment?: vscode.NotebookCellStatusBarAlignment, priority?: number): vscode.NotebookCellStatusBarItem { + checkProposedApiEnabled(extension); + return extHostNotebook.createNotebookCellStatusBarItemInternal(cell, alignment, priority); } }; @@ -1114,7 +1125,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I SymbolKind: extHostTypes.SymbolKind, SymbolTag: extHostTypes.SymbolTag, Task: extHostTypes.Task, - Task2: extHostTypes.Task, TaskGroup: extHostTypes.TaskGroup, TaskPanelKind: extHostTypes.TaskPanelKind, TaskRevealKind: extHostTypes.TaskRevealKind, @@ -1146,7 +1156,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I CellKind: extHostTypes.CellKind, CellOutputKind: extHostTypes.CellOutputKind, NotebookCellRunState: extHostTypes.NotebookCellRunState, - NotebookRunState: extHostTypes.NotebookRunState + NotebookRunState: extHostTypes.NotebookRunState, + NotebookCellStatusBarAlignment: extHostTypes.NotebookCellStatusBarAlignment, + NotebookEditorRevealType: extHostTypes.NotebookEditorRevealType }; }; } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index da50040c7c..5ca5c95b07 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -42,7 +42,7 @@ import { IRevealOptions, ITreeItem } from 'vs/workbench/common/views'; import { IAdapterDescriptor, IConfig, IDebugSessionReplMode } from 'vs/workbench/contrib/debug/common/debug'; import { ITextQueryBuilderOptions } from 'vs/workbench/contrib/search/common/queryBuilder'; import { ITerminalDimensions, IShellLaunchConfig, ITerminalLaunchError } from 'vs/workbench/contrib/terminal/common/terminal'; -import { ExtensionActivationError } from 'vs/workbench/services/extensions/common/extensions'; +import { ActivationKind, ExtensionActivationError } from 'vs/workbench/services/extensions/common/extensions'; import { createExtHostContextProxyIdentifier as createExtId, createMainContextProxyIdentifier as createMainId, IRPCProtocol } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import * as search from 'vs/workbench/services/search/common/search'; import { SaveReason } from 'vs/workbench/common/editor'; @@ -51,7 +51,7 @@ import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; import { TunnelOptions } from 'vs/platform/remote/common/tunnel'; import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor, InternalTimelineOptions } from 'vs/workbench/contrib/timeline/common/timeline'; import { revive } from 'vs/base/common/marshalling'; -import { IProcessedOutput, INotebookDisplayOrder, NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEvent, NotebookDataDto, INotebookKernelInfoDto, IMainCellDto, INotebookDocumentFilter, INotebookKernelInfoDto2 } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IProcessedOutput, INotebookDisplayOrder, NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEvent, NotebookDataDto, IMainCellDto, INotebookDocumentFilter, INotebookKernelInfoDto2, TransientMetadata, INotebookCellStatusBarEntry, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { Dto } from 'vs/base/common/types'; import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; @@ -454,6 +454,7 @@ export interface MainThreadTerminalServiceShape extends IDisposable { $stopSendingDataEvents(): void; $startLinkProvider(): void; $stopLinkProvider(): void; + $registerProcessSupport(isSupported: boolean): void; $setEnvironmentVariableCollection(extensionIdentifier: string, persistent: boolean, collection: ISerializableEnvironmentVariableCollection | undefined): void; // Process @@ -585,7 +586,7 @@ export interface ExtHostEditorInsetsShape { $onDidReceiveMessage(handle: number, message: any): void; } -export type WebviewPanelHandle = string; +export type WebviewHandle = string; export interface WebviewPanelShowOptions { readonly viewColumn?: EditorViewColumn; @@ -613,31 +614,36 @@ export interface CustomTextEditorCapabilities { } export interface MainThreadWebviewsShape extends IDisposable { - $createWebviewPanel(extension: WebviewExtensionDescription, handle: WebviewPanelHandle, viewType: string, title: string, showOptions: WebviewPanelShowOptions, options: modes.IWebviewPanelOptions & modes.IWebviewOptions): void; - $disposeWebview(handle: WebviewPanelHandle): void; - $reveal(handle: WebviewPanelHandle, showOptions: WebviewPanelShowOptions): void; - $setTitle(handle: WebviewPanelHandle, value: string): void; - $setIconPath(handle: WebviewPanelHandle, value: { light: UriComponents, dark: UriComponents; } | undefined): void; + $setHtml(handle: WebviewHandle, value: string): void; + $setOptions(handle: WebviewHandle, options: modes.IWebviewOptions): void; + $postMessage(handle: WebviewHandle, value: any): Promise +} - $setHtml(handle: WebviewPanelHandle, value: string): void; - $setOptions(handle: WebviewPanelHandle, options: modes.IWebviewOptions): void; - - $postMessage(handle: WebviewPanelHandle, value: any): Promise; +export interface MainThreadWebviewPanelsShape extends IDisposable { + $createWebviewPanel(extension: WebviewExtensionDescription, handle: WebviewHandle, viewType: string, title: string, showOptions: WebviewPanelShowOptions, options: modes.IWebviewPanelOptions & modes.IWebviewOptions): void; + $disposeWebview(handle: WebviewHandle): void; + $reveal(handle: WebviewHandle, showOptions: WebviewPanelShowOptions): void; + $setTitle(handle: WebviewHandle, value: string): void; + $setIconPath(handle: WebviewHandle, value: { light: UriComponents, dark: UriComponents; } | undefined): void; $registerSerializer(viewType: string): void; $unregisterSerializer(viewType: string): void; +} +export interface MainThreadCustomEditorsShape extends IDisposable { $registerTextEditorProvider(extension: WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions, capabilities: CustomTextEditorCapabilities): void; $registerCustomEditorProvider(extension: WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions, supportsMultipleEditorsPerDocument: boolean): void; $unregisterEditorProvider(viewType: string): void; $onDidEdit(resource: UriComponents, viewType: string, editId: number, label: string | undefined): void; $onContentChange(resource: UriComponents, viewType: string): void; +} +export interface MainThreadWebviewViewsShape extends IDisposable { $registerWebviewViewProvider(viewType: string, options?: { retainContextWhenHidden?: boolean }): void; $unregisterWebviewViewProvider(viewType: string): void; - $setWebviewViewTitle(handle: WebviewPanelHandle, value: string | undefined): void; + $setWebviewViewTitle(handle: WebviewHandle, value: string | undefined): void; } export interface WebviewPanelViewStateData { @@ -649,18 +655,18 @@ export interface WebviewPanelViewStateData { } export interface ExtHostWebviewsShape { - $onMessage(handle: WebviewPanelHandle, message: any): void; - $onMissingCsp(handle: WebviewPanelHandle, extensionId: string): void; - $onDidChangeWebviewPanelViewStates(newState: WebviewPanelViewStateData): void; - $onDidDisposeWebviewPanel(handle: WebviewPanelHandle): Promise; + $onMessage(handle: WebviewHandle, message: any): void; + $onMissingCsp(handle: WebviewHandle, extensionId: string): void; } -export interface ExtHostWebviewSerializerShape { - $deserializeWebviewPanel(newWebviewHandle: WebviewPanelHandle, viewType: string, title: string, state: any, position: EditorViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions): Promise; +export interface ExtHostWebviewPanelsShape { + $onDidChangeWebviewPanelViewStates(newState: WebviewPanelViewStateData): void; + $onDidDisposeWebviewPanel(handle: WebviewHandle): Promise; + $deserializeWebviewPanel(newWebviewHandle: WebviewHandle, viewType: string, title: string, state: any, position: EditorViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions): Promise; } export interface ExtHostCustomEditorsShape { - $resolveWebviewEditor(resource: UriComponents, newWebviewHandle: WebviewPanelHandle, viewType: string, title: string, position: EditorViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions, cancellation: CancellationToken): Promise; + $resolveWebviewEditor(resource: UriComponents, newWebviewHandle: WebviewHandle, viewType: string, title: string, position: EditorViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions, cancellation: CancellationToken): Promise; $createCustomDocument(resource: UriComponents, viewType: string, backupId: string | undefined, cancellation: CancellationToken): Promise<{ editable: boolean }>; $disposeCustomDocument(resource: UriComponents, viewType: string): Promise; @@ -674,15 +680,15 @@ export interface ExtHostCustomEditorsShape { $backup(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise; - $onMoveCustomEditor(handle: WebviewPanelHandle, newResource: UriComponents, viewType: string): Promise; + $onMoveCustomEditor(handle: WebviewHandle, newResource: UriComponents, viewType: string): Promise; } export interface ExtHostWebviewViewsShape { - $resolveWebviewView(webviewHandle: WebviewPanelHandle, viewType: string, state: any, cancellation: CancellationToken): Promise; + $resolveWebviewView(webviewHandle: WebviewHandle, viewType: string, state: any, cancellation: CancellationToken): Promise; - $onDidChangeWebviewViewVisibility(webviewHandle: WebviewPanelHandle, visible: boolean): void; + $onDidChangeWebviewViewVisibility(webviewHandle: WebviewHandle, visible: boolean): void; - $disposeWebviewView(webviewHandle: WebviewPanelHandle): void; + $disposeWebviewView(webviewHandle: WebviewHandle): void; } export enum CellKind { @@ -718,22 +724,29 @@ export type NotebookCellOutputsSplice = [ IProcessedOutput[] ]; +export enum NotebookEditorRevealType { + Default = 0, + InCenter = 1, + InCenterIfOutsideViewport = 2, +} + +export type INotebookCellStatusBarEntryDto = Dto; + export interface MainThreadNotebookShape extends IDisposable { - $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string, supportBackup: boolean, kernelInfoDto: INotebookKernelInfoDto | undefined): Promise; + $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string, supportBackup: boolean, options: { transientOutputs: boolean; transientMetadata: TransientMetadata }): Promise; $onNotebookChange(viewType: string, resource: UriComponents): Promise; $unregisterNotebookProvider(viewType: string): Promise; - $registerNotebookKernel(extension: NotebookExtensionDescription, id: string, label: string, selectors: (string | IRelativePattern)[], preloads: UriComponents[]): Promise; $registerNotebookKernelProvider(extension: NotebookExtensionDescription, handle: number, documentFilter: INotebookDocumentFilter): Promise; $unregisterNotebookKernelProvider(handle: number): Promise; - $onNotebookKernelChange(handle: number): void; - $unregisterNotebookKernel(id: string): Promise; - $tryApplyEdits(viewType: string, resource: UriComponents, modelVersionId: number, edits: ICellEditOperation[], renderers: number[]): Promise; + $onNotebookKernelChange(handle: number, uri: UriComponents | undefined): void; + $tryApplyEdits(viewType: string, resource: UriComponents, modelVersionId: number, edits: ICellEditOperation[]): Promise; $updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise; $updateNotebookMetadata(viewType: string, resource: UriComponents, metadata: NotebookDocumentMetadata): Promise; $updateNotebookCellMetadata(viewType: string, resource: UriComponents, handle: number, metadata: NotebookCellMetadata | undefined): Promise; $spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[]): Promise; $postMessage(editorId: string, forRendererId: string | undefined, value: any): Promise; - + $setStatusBarEntry(id: number, statusBarEntry: INotebookCellStatusBarEntryDto): Promise; + $tryRevealRange(id: string, range: ICellRange, revealType: NotebookEditorRevealType): Promise; $onDidEdit(resource: UriComponents, viewType: string, editId: number, label: string | undefined): void; $onContentChange(resource: UriComponents, viewType: string): void; } @@ -1052,6 +1065,7 @@ export interface ExtHostAuthenticationShape { $logout(id: string, sessionId: string): Promise; $onDidChangeAuthenticationSessions(id: string, label: string, event: modes.AuthenticationSessionsChangeEvent): Promise; $onDidChangeAuthenticationProviders(added: modes.AuthenticationProviderInformation[], removed: modes.AuthenticationProviderInformation[]): Promise; + $setProviders(providers: modes.AuthenticationProviderInformation[]): Promise; } export interface ExtHostSearchShape { @@ -1079,7 +1093,7 @@ export type IResolveAuthorityResult = IResolveAuthorityErrorResult | IResolveAut export interface ExtHostExtensionServiceShape { $resolveAuthority(remoteAuthority: string, resolveAttempt: number): Promise; $startExtensionHost(enabledExtensionIds: ExtensionIdentifier[]): Promise; - $activateByEvent(activationEvent: string): Promise; + $activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise; $activate(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise; $setRemoteEnvironment(env: { [key: string]: string | null; }): Promise; $updateRemoteConnectionData(connectionData: IRemoteConnectionData): Promise; @@ -1237,7 +1251,14 @@ export interface IWorkspaceEditEntryMetadataDto { iconPath?: { id: string } | UriComponents | { light: UriComponents, dark: UriComponents }; } +export const enum WorkspaceEditType { + File = 1, + Text = 2, + Cell = 3, +} + export interface IWorkspaceFileEditDto { + _type: WorkspaceEditType.File; oldUri?: UriComponents; newUri?: UriComponents; options?: modes.WorkspaceFileEditOptions @@ -1245,14 +1266,23 @@ export interface IWorkspaceFileEditDto { } export interface IWorkspaceTextEditDto { + _type: WorkspaceEditType.Text; resource: UriComponents; edit: modes.TextEdit; modelVersionId?: number; metadata?: IWorkspaceEditEntryMetadataDto; } +export interface IWorkspaceCellEditDto { + _type: WorkspaceEditType.Cell; + resource: UriComponents; + edit: ICellEditOperation; + modelVersionId?: number; + metadata?: IWorkspaceEditEntryMetadataDto; +} + export interface IWorkspaceEditDto { - edits: Array; + edits: Array; // todo@joh reject should go into rename rejectReason?: string; @@ -1602,9 +1632,22 @@ export interface INotebookSelectionChangeEvent { selections: number[]; } +export interface INotebookCellVisibleRange { + start: number; + end: number; +} + +export interface INotebookVisibleRangesEvent { + ranges: INotebookCellVisibleRange[]; +} + export interface INotebookEditorPropertiesChangeData { - selections: INotebookSelectionChangeEvent | null; + visibleRanges: INotebookVisibleRangesEvent | null; +} + +export interface INotebookDocumentPropertiesChangeData { metadata: NotebookDocumentMetadata | null; + selections: INotebookSelectionChangeEvent | null; } export interface INotebookModelAddedData { @@ -1614,13 +1657,14 @@ export interface INotebookModelAddedData { cells: IMainCellDto[], viewType: string; metadata?: NotebookDocumentMetadata; - attachedEditor?: { id: string; selections: number[]; } + attachedEditor?: { id: string; selections: number[]; visibleRanges: ICellRange[] } } export interface INotebookEditorAddData { id: string; documentUri: UriComponents; selections: number[]; + visibleRanges: ICellRange[]; } export interface INotebookDocumentsAndEditorsDelta { @@ -1637,8 +1681,6 @@ export interface ExtHostNotebookShape { $resolveNotebookEditor(viewType: string, uri: UriComponents, editorId: string): Promise; $provideNotebookKernels(handle: number, uri: UriComponents, token: CancellationToken): Promise; $resolveNotebookKernel(handle: number, editorId: string, uri: UriComponents, kernelId: string, token: CancellationToken): Promise; - $executeNotebookByAttachedKernel(viewType: string, uri: UriComponents, cellHandle: number | undefined): Promise; - $cancelNotebookByAttachedKernel(viewType: string, uri: UriComponents, cellHandle: number | undefined): Promise; $executeNotebookKernelFromProvider(handle: number, uri: UriComponents, kernelId: string, cellHandle: number | undefined): Promise; $cancelNotebookKernelFromProvider(handle: number, uri: UriComponents, kernelId: string, cellHandle: number | undefined): Promise; $executeNotebook2(kernelId: string, viewType: string, uri: UriComponents, cellHandle: number | undefined): Promise; @@ -1648,9 +1690,10 @@ export interface ExtHostNotebookShape { $acceptDisplayOrder(displayOrder: INotebookDisplayOrder): void; $acceptNotebookActiveKernelChange(event: { uri: UriComponents, providerHandle: number | undefined, kernelId: string | undefined }): void; $onDidReceiveMessage(editorId: string, rendererId: string | undefined, message: unknown): void; - $acceptModelChanged(uriComponents: UriComponents, event: NotebookCellsChangedEvent): void; + $acceptModelChanged(uriComponents: UriComponents, event: NotebookCellsChangedEvent, isDirty: boolean): void; $acceptModelSaved(uriComponents: UriComponents): void; - $acceptEditorPropertiesChanged(uriComponents: UriComponents, data: INotebookEditorPropertiesChangeData): void; + $acceptEditorPropertiesChanged(id: string, data: INotebookEditorPropertiesChangeData): void; + $acceptDocumentPropertiesChanged(uriComponents: UriComponents, data: INotebookDocumentPropertiesChangeData): void; $acceptDocumentAndEditorsDelta(delta: INotebookDocumentsAndEditorsDelta): void; $undoNotebook(viewType: string, uri: UriComponents, editId: number, isDirty: boolean): Promise; $redoNotebook(viewType: string, uri: UriComponents, editId: number, isDirty: boolean): Promise; @@ -1713,6 +1756,9 @@ export const MainContext = { MainThreadTelemetry: createMainId('MainThreadTelemetry'), MainThreadTerminalService: createMainId('MainThreadTerminalService'), MainThreadWebviews: createMainId('MainThreadWebviews'), + MainThreadWebviewPanels: createMainId('MainThreadWebviewPanels'), + MainThreadWebviewViews: createMainId('MainThreadWebviewViews'), + MainThreadCustomEditors: createMainId('MainThreadCustomEditors'), MainThreadUrls: createMainId('MainThreadUrls'), MainThreadWorkspace: createMainId('MainThreadWorkspace'), MainThreadFileSystem: createMainId('MainThreadFileSystem'), @@ -1753,7 +1799,7 @@ export const ExtHostContext = { ExtHostWorkspace: createExtId('ExtHostWorkspace'), ExtHostWindow: createExtId('ExtHostWindow'), ExtHostWebviews: createExtId('ExtHostWebviews'), - ExtHostWebviewSerializer: createExtId('ExtHostWebviewSerializer'), + ExtHostWebviewPanels: createExtId('ExtHostWebviewPanels'), ExtHostCustomEditors: createExtId('ExtHostCustomEditors'), ExtHostWebviewViews: createExtId('ExtHostWebviewViews'), ExtHostEditorInsets: createExtId('ExtHostEditorInsets'), diff --git a/src/vs/workbench/api/common/extHostAuthentication.ts b/src/vs/workbench/api/common/extHostAuthentication.ts index ce6fe21b44..727a10eb6a 100644 --- a/src/vs/workbench/api/common/extHostAuthentication.ts +++ b/src/vs/workbench/api/common/extHostAuthentication.ts @@ -28,6 +28,11 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { this._proxy = mainContext.getProxy(MainContext.MainThreadAuthentication); } + $setProviders(providers: vscode.AuthenticationProviderInformation[]): Promise { + this._providers = providers; + return Promise.resolve(); + } + getProviderIds(): Promise> { return this._proxy.$getProviderIds(); } @@ -182,9 +187,9 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { } $onDidChangeAuthenticationProviders(added: modes.AuthenticationProviderInformation[], removed: modes.AuthenticationProviderInformation[]) { - added.forEach(id => { - if (!this._providers.includes(id)) { - this._providers.push(id); + added.forEach(provider => { + if (!this._providers.some(p => p.id === provider.id)) { + this._providers.push(provider); } }); diff --git a/src/vs/workbench/api/common/extHostCommands.ts b/src/vs/workbench/api/common/extHostCommands.ts index bbeb106b3c..1a96624d0e 100644 --- a/src/vs/workbench/api/common/extHostCommands.ts +++ b/src/vs/workbench/api/common/extHostCommands.ts @@ -141,7 +141,7 @@ export class ExtHostCommands implements ExtHostCommandsShape { try { const result = await this._proxy.$executeCommand(id, toArgs, retry); - return revive(result); + return revive(result); } catch (e) { // Rerun the command when it wasn't known, had arguments, and when retry // is enabled. We do this because the command might be registered inside diff --git a/src/vs/workbench/api/common/extHostCustomEditors.ts b/src/vs/workbench/api/common/extHostCustomEditors.ts index 7f72c25fda..0d0cebe567 100644 --- a/src/vs/workbench/api/common/extHostCustomEditors.ts +++ b/src/vs/workbench/api/common/extHostCustomEditors.ts @@ -14,6 +14,7 @@ import { IExtensionDescription } from 'vs/platform/extensions/common/extensions' import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; import { ExtHostWebviews, toExtensionData } from 'vs/workbench/api/common/extHostWebview'; +import { ExtHostWebviewPanels } from 'vs/workbench/api/common/extHostWebviewPanels'; import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor'; import type * as vscode from 'vscode'; import { Cache } from './cache'; @@ -154,7 +155,7 @@ class EditorProviderStore { export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditorsShape { - private readonly _proxy: extHostProtocol.MainThreadWebviewsShape; + private readonly _proxy: extHostProtocol.MainThreadCustomEditorsShape; private readonly _editorProviders = new EditorProviderStore(); @@ -165,8 +166,9 @@ export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditor private readonly _extHostDocuments: ExtHostDocuments, private readonly _extensionStoragePaths: IExtensionStoragePaths | undefined, private readonly _extHostWebview: ExtHostWebviews, + private readonly _extHostWebviewPanels: ExtHostWebviewPanels, ) { - this._proxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadWebviews); + this._proxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadCustomEditors); } public registerCustomEditorProvider( @@ -247,7 +249,7 @@ export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditor async $resolveWebviewEditor( resource: UriComponents, - handle: extHostProtocol.WebviewPanelHandle, + handle: extHostProtocol.WebviewHandle, viewType: string, title: string, position: EditorViewColumn, @@ -260,7 +262,7 @@ export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditor } const webview = this._extHostWebview.createNewWebview(handle, options, entry.extension); - const panel = this._extHostWebview.createNewWebviewPanel(handle, viewType, title, position, options, webview); + const panel = this._extHostWebviewPanels.createNewWebviewPanel(handle, viewType, title, position, options, webview); const revivedResource = URI.revive(resource); @@ -297,7 +299,7 @@ export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditor throw new Error(`Provider does not implement move '${viewType}'`); } - const webview = this._extHostWebview.getWebviewPanel(handle); + const webview = this._extHostWebviewPanels.getWebviewPanel(handle); if (!webview) { throw new Error(`No webview found`); } diff --git a/src/vs/workbench/api/common/extHostDecorations.ts b/src/vs/workbench/api/common/extHostDecorations.ts index 828ab9c756..661ec45f13 100644 --- a/src/vs/workbench/api/common/extHostDecorations.ts +++ b/src/vs/workbench/api/common/extHostDecorations.ts @@ -19,7 +19,7 @@ interface ProviderData { extensionId: ExtensionIdentifier; } -export class ExtHostDecorations implements IExtHostDecorations { +export class ExtHostDecorations implements ExtHostDecorationsShape { private static _handlePool = 0; @@ -85,4 +85,4 @@ export class ExtHostDecorations implements IExtHostDecorations { } export const IExtHostDecorations = createDecorator('IExtHostDecorations'); -export interface IExtHostDecorations extends ExtHostDecorations, ExtHostDecorationsShape { } +export interface IExtHostDecorations extends ExtHostDecorations { } diff --git a/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts b/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts index c62ada5235..1d92976fc3 100644 --- a/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts +++ b/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts @@ -6,7 +6,7 @@ import { Event } from 'vs/base/common/event'; import { URI, UriComponents } from 'vs/base/common/uri'; import { illegalState } from 'vs/base/common/errors'; -import { ExtHostDocumentSaveParticipantShape, MainThreadTextEditorsShape, IWorkspaceEditDto } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostDocumentSaveParticipantShape, MainThreadTextEditorsShape, IWorkspaceEditDto, WorkspaceEditType } from 'vs/workbench/api/common/extHost.protocol'; import { TextEdit } from 'vs/workbench/api/common/extHostTypes'; import { Range, TextDocumentSaveReason, EndOfLine } from 'vs/workbench/api/common/extHostTypeConverters'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; @@ -146,6 +146,7 @@ export class ExtHostDocumentSaveParticipant implements ExtHostDocumentSavePartic if (Array.isArray(value) && (value).every(e => e instanceof TextEdit)) { for (const { newText, newEol, range } of value) { dto.edits.push({ + _type: WorkspaceEditType.Text, resource: document.uri, edit: { range: range && Range.from(range), diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index 5435909fb6..55e35709f0 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -5,7 +5,6 @@ import * as nls from 'vs/nls'; import * as path from 'vs/base/common/path'; -import * as platform from 'vs/base/common/platform'; import { originalFSPath, joinPath } from 'vs/base/common/resources'; import { Barrier, timeout } from 'vs/base/common/async'; import { dispose, toDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; @@ -17,7 +16,7 @@ import { ExtHostConfiguration, IExtHostConfiguration } from 'vs/workbench/api/co import { ActivatedExtension, EmptyExtension, ExtensionActivationReason, ExtensionActivationTimes, ExtensionActivationTimesBuilder, ExtensionsActivator, IExtensionAPI, IExtensionModule, HostExtension, ExtensionActivationTimesFragment } from 'vs/workbench/api/common/extHostExtensionActivator'; import { ExtHostStorage, IExtHostStorage } from 'vs/workbench/api/common/extHostStorage'; import { ExtHostWorkspace, IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; -import { ExtensionActivationError, checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; +import { ExtensionActivationError, checkProposedApiEnabled, ActivationKind } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import * as errors from 'vs/base/common/errors'; import type * as vscode from 'vscode'; @@ -385,14 +384,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme subscriptions: [], get extensionUri() { return extensionDescription.extensionLocation; }, get extensionPath() { return extensionDescription.extensionLocation.fsPath; }, - asAbsolutePath(relativePath: string) { - if (platform.isWeb) { - // web worker - return URI.joinPath(extensionDescription.extensionLocation, relativePath).toString(); - } else { - return path.join(extensionDescription.extensionLocation.fsPath, relativePath); - } - }, + asAbsolutePath(relativePath: string) { return path.join(extensionDescription.extensionLocation.fsPath, relativePath); }, get storagePath() { return that._storagePath.workspaceValue(extensionDescription)?.fsPath; }, get globalStoragePath() { return that._storagePath.globalValue(extensionDescription).fsPath; }, get logPath() { return path.join(that._initData.logsLocation.fsPath, extensionDescription.identifier.value); }, @@ -686,7 +678,11 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme return this._startExtensionHost(); } - public $activateByEvent(activationEvent: string): Promise { + public $activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise { + if (activationKind === ActivationKind.Immediate) { + return this._activateByEvent(activationEvent, false); + } + return ( this._readyToRunExtensions.wait() .then(_ => this._activateByEvent(activationEvent, false)) diff --git a/src/vs/workbench/api/common/extHostFileSystemEventService.ts b/src/vs/workbench/api/common/extHostFileSystemEventService.ts index cb52390c3e..189d6afb80 100644 --- a/src/vs/workbench/api/common/extHostFileSystemEventService.ts +++ b/src/vs/workbench/api/common/extHostFileSystemEventService.ts @@ -8,12 +8,11 @@ import { IRelativePattern, parse } from 'vs/base/common/glob'; import { URI } from 'vs/base/common/uri'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import type * as vscode from 'vscode'; -import { ExtHostFileSystemEventServiceShape, FileSystemEvents, IMainContext, MainContext, MainThreadTextEditorsShape, IWorkspaceFileEditDto, IWorkspaceTextEditDto, SourceTargetPair } from './extHost.protocol'; +import { ExtHostFileSystemEventServiceShape, FileSystemEvents, IMainContext, MainContext, MainThreadTextEditorsShape, SourceTargetPair, IWorkspaceEditDto } from './extHost.protocol'; import * as typeConverter from './extHostTypeConverters'; import { Disposable, WorkspaceEdit } from './extHostTypes'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { FileOperation } from 'vs/platform/files/common/files'; -import { flatten } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ILogService } from 'vs/platform/log/common/log'; @@ -217,14 +216,13 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ } if (edits.length > 0) { - // flatten all WorkspaceEdits collected via waitUntil-call - // and apply them in one go. - const allEdits = new Array>(); + // concat all WorkspaceEdits collected via waitUntil-call and apply them in one go. + const dto: IWorkspaceEditDto = { edits: [] }; for (let edit of edits) { let { edits } = typeConverter.WorkspaceEdit.from(edit, this._extHostDocumentsAndEditors); - allEdits.push(edits); + dto.edits = dto.edits.concat(edits); } - return this._mainThreadTextEditors.$tryApplyWorkspaceEdit({ edits: flatten(allEdits) }); + return this._mainThreadTextEditors.$tryApplyWorkspaceEdit(dto); } } } diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 62591d6859..afec4f351d 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -1120,7 +1120,7 @@ class ColorProviderAdapter { provideColors(resource: URI, token: CancellationToken): Promise { const doc = this._documents.getDocument(resource); return asPromise(() => this._provider.provideDocumentColors(doc, token)).then(colors => { - if (!Array.isArray(colors)) { + if (!Array.isArray(colors)) { return []; } diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 5e831f7052..276279cb7e 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -14,15 +14,15 @@ import { ISplice } from 'vs/base/common/sequence'; import { URI, UriComponents } from 'vs/base/common/uri'; import * as UUID from 'vs/base/common/uuid'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { CellKind, ExtHostNotebookShape, IMainContext, IModelAddedData, INotebookDocumentsAndEditorsDelta, INotebookEditorPropertiesChangeData, MainContext, MainThreadNotebookShape, NotebookCellOutputsSplice } from 'vs/workbench/api/common/extHost.protocol'; +import { CellKind, ExtHostNotebookShape, ICommandDto, IMainContext, IModelAddedData, INotebookDocumentPropertiesChangeData, INotebookDocumentsAndEditorsDelta, INotebookEditorPropertiesChangeData, MainContext, MainThreadNotebookShape, NotebookCellOutputsSplice } from 'vs/workbench/api/common/extHost.protocol'; import { ILogService } from 'vs/platform/log/common/log'; -import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; +import { CommandsConverter, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { ExtHostDocumentsAndEditors, IExtHostModelAddedData } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; import { asWebviewUri, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; -import { CellEditType, CellOutputKind, diff, ICellDeleteEdit, ICellEditOperation, ICellInsertEdit, IMainCellDto, INotebookDisplayOrder, INotebookEditData, INotebookKernelInfoDto2, IProcessedOutput, IRawOutput, NotebookCellMetadata, NotebookCellsChangedEvent, NotebookCellsChangeType, NotebookCellsSplice2, NotebookDataDto, notebookDocumentMetadataDefaults } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { addIdToOutput, CellEditType, CellOutputKind, CellStatusbarAlignment, CellUri, diff, ICellEditOperation, ICellReplaceEdit, IMainCellDto, INotebookCellStatusBarEntry, INotebookDisplayOrder, INotebookEditData, INotebookKernelInfoDto2, IProcessedOutput, NotebookCellMetadata, NotebookCellsChangedEvent, NotebookCellsChangeType, NotebookCellsSplice2, NotebookDataDto, notebookDocumentMetadataDefaults } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import * as vscode from 'vscode'; import { Cache } from './cache'; import { ResourceMap } from 'vs/base/common/map'; @@ -55,12 +55,9 @@ interface INotebookEventEmitter { emitCellMetadataChange(event: vscode.NotebookCellMetadataChangeEvent): void; } -const addIdToOutput = (output: IRawOutput, id = UUID.generateUuid()): IProcessedOutput => output.outputKind === CellOutputKind.Rich - ? ({ ...output, outputId: id }) : output; - export class ExtHostCell extends Disposable { - public static asModelAddData(notebook: ExtHostNotebookDocument, cell: IMainCellDto): IExtHostModelAddedData { + public static asModelAddData(notebook: vscode.NotebookDocument, cell: IMainCellDto): IExtHostModelAddedData { return { EOL: cell.eol, lines: cell.source, @@ -72,6 +69,9 @@ export class ExtHostCell extends Disposable { }; } + private _onDidDispose = new Emitter(); + readonly onDidDispose: Event = this._onDidDispose.event; + private _onDidChangeOutputs = new Emitter[]>(); readonly onDidChangeOutputs: Event[]> = this._onDidChangeOutputs.event; @@ -117,7 +117,7 @@ export class ExtHostCell extends Disposable { const that = this; const document = this._extHostDocument.getDocument(this.uri)!.document; this._cell = Object.freeze({ - notebook: that._notebook, + notebook: that._notebook.notebookDocument, uri: that.uri, cellKind: this._cellData.cellKind, document, @@ -134,6 +134,15 @@ export class ExtHostCell extends Disposable { return this._cell; } + dispose() { + super.dispose(); + this._onDidDispose.fire(); + } + + setOutputs(newOutputs: vscode.CellOutput[]): void { + this._outputs = newOutputs; + } + private _updateOutputs(newOutputs: vscode.CellOutput[]) { const rawDiffs = diff(this._outputs || [], newOutputs || [], (a) => { return this._outputMapping.has(a); @@ -175,7 +184,7 @@ export class ExtHostCell extends Disposable { } private _updateMetadata(): Promise { - return this._proxy.$updateNotebookCellMetadata(this._notebook.viewType, this._notebook.uri, this.handle, this._metadata); + return this._proxy.$updateNotebookCellMetadata(this._notebook.notebookDocument.viewType, this._notebook.uri, this.handle, this._metadata); } } @@ -193,7 +202,8 @@ class RawContentChangeEvent { } } -export class ExtHostNotebookDocument extends Disposable implements vscode.NotebookDocument { +export class ExtHostNotebookDocument extends Disposable { + private static _handlePool: number = 0; readonly handle = ExtHostNotebookDocument._handlePool++; @@ -201,33 +211,45 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo private _cellDisposableMapping = new Map(); - get cells(): ReadonlyArray { - return this._cells.map(cellData => cellData.cell); - } - - private _languages: string[] = []; - - get languages() { - return this._languages = []; - } - - set languages(newLanguages: string[]) { - this._languages = newLanguages; - this._proxy.$updateNotebookLanguages(this.viewType, this.uri, this._languages); - } - - get isUntitled() { - return this.uri.scheme === Schemas.untitled; - } + private _notebook: vscode.NotebookDocument | undefined; private _metadata: Required = notebookDocumentMetadataDefaults; private _metadataChangeListener: IDisposable; + private _displayOrder: string[] = []; + private _versionId = 0; + private _isDirty: boolean = false; + private _backupCounter = 1; + private _backup?: vscode.NotebookDocumentBackup; + private _disposed = false; + private _languages: string[] = []; - get metadata() { - return this._metadata; + private readonly _edits = new Cache('notebook documents'); + + constructor( + private readonly _proxy: MainThreadNotebookShape, + private readonly _documentsAndEditors: ExtHostDocumentsAndEditors, + private readonly _emitter: INotebookEventEmitter, + private readonly _viewType: string, + public readonly uri: URI, + public readonly renderingHandler: ExtHostNotebookOutputRenderingHandler, + private readonly _storagePath: URI | undefined + ) { + super(); + + const observableMetadata = getObservable(notebookDocumentMetadataDefaults); + this._metadata = observableMetadata.proxy; + this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { + this._tryUpdateMetadata(); + })); } - set metadata(newMetadata: Required) { + dispose() { + this._disposed = true; + super.dispose(); + dispose(this._cellDisposableMapping.values()); + } + + private _updateMetadata(newMetadata: Required) { this._metadataChangeListener.dispose(); newMetadata = { ...notebookDocumentMetadataDefaults, @@ -240,34 +262,220 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo const observableMetadata = getObservable(newMetadata); this._metadata = observableMetadata.proxy; this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { - this.updateMetadata(); + this._tryUpdateMetadata(); })); - this.updateMetadata(); + this._tryUpdateMetadata(); } - private _displayOrder: string[] = []; - - get displayOrder() { - return this._displayOrder; + private _tryUpdateMetadata() { + this._proxy.$updateNotebookMetadata(this._viewType, this.uri, this._metadata); + } + get notebookDocument(): vscode.NotebookDocument { + if (!this._notebook) { + const that = this; + this._notebook = Object.freeze({ + get uri() { return that.uri; }, + get version() { return that._versionId; }, + get fileName() { return that.uri.fsPath; }, + get viewType() { return that._viewType; }, + get isDirty() { return that._isDirty; }, + get isUntitled() { return that.uri.scheme === Schemas.untitled; }, + get cells(): ReadonlyArray { return that._cells.map(cell => cell.cell); }, + get languages() { return that._languages; }, + set languages(value: string[]) { that._trySetLanguages(value); }, + get displayOrder() { return that._displayOrder; }, + set displayOrder(value: string[]) { that._displayOrder = value; }, + get metadata() { return that._metadata; }, + set metadata(value: Required) { that._updateMetadata(value); }, + }); + } + return this._notebook; } - set displayOrder(newOrder: string[]) { - this._displayOrder = newOrder; + private _trySetLanguages(newLanguages: string[]) { + this._languages = newLanguages; + this._proxy.$updateNotebookLanguages(this._viewType, this.uri, this._languages); } - private _versionId = 0; - - get versionId() { - return this._versionId; + getNewBackupUri(): URI { + if (!this._storagePath) { + throw new Error('Backup requires a valid storage path'); + } + const fileName = hashPath(this.uri) + (this._backupCounter++); + return joinPath(this._storagePath, fileName); } - private _backupCounter = 1; + updateBackup(backup: vscode.NotebookDocumentBackup): void { + this._backup?.delete(); + this._backup = backup; + } - private _backup?: vscode.NotebookDocumentBackup; + disposeBackup(): void { + this._backup?.delete(); + this._backup = undefined; + } + acceptModelChanged(event: NotebookCellsChangedEvent, isDirty: boolean): void { + this._versionId = event.versionId; + this._isDirty = isDirty; + if (event.kind === NotebookCellsChangeType.Initialize) { + this._spliceNotebookCells(event.changes, true); + } if (event.kind === NotebookCellsChangeType.ModelChange) { + this._spliceNotebookCells(event.changes, false); + } else if (event.kind === NotebookCellsChangeType.Move) { + this._moveCell(event.index, event.newIdx); + } else if (event.kind === NotebookCellsChangeType.Output) { + this._setCellOutputs(event.index, event.outputs); + } else if (event.kind === NotebookCellsChangeType.CellClearOutput) { + this._clearCellOutputs(event.index); + } else if (event.kind === NotebookCellsChangeType.CellsClearOutput) { + this._clearAllCellOutputs(); + } else if (event.kind === NotebookCellsChangeType.ChangeLanguage) { + this._changeCellLanguage(event.index, event.language); + } else if (event.kind === NotebookCellsChangeType.ChangeMetadata) { + this._changeCellMetadata(event.index, event.metadata); + } + } - private readonly _edits = new Cache('notebook documents'); + private _spliceNotebookCells(splices: NotebookCellsSplice2[], initialization: boolean): void { + if (this._disposed) { + return; + } + + const contentChangeEvents: RawContentChangeEvent[] = []; + const addedCellDocuments: IExtHostModelAddedData[] = []; + const removedCellDocuments: URI[] = []; + + splices.reverse().forEach(splice => { + const cellDtos = splice[2]; + const newCells = cellDtos.map(cell => { + + const extCell = new ExtHostCell(this._proxy, this, this._documentsAndEditors, cell); + + if (!initialization) { + addedCellDocuments.push(ExtHostCell.asModelAddData(this.notebookDocument, cell)); + } + + if (!this._cellDisposableMapping.has(extCell.handle)) { + const store = new DisposableStore(); + store.add(extCell); + this._cellDisposableMapping.set(extCell.handle, store); + } + + const store = this._cellDisposableMapping.get(extCell.handle)!; + + store.add(extCell.onDidChangeOutputs((diffs) => { + this.eventuallyUpdateCellOutputs(extCell, diffs); + })); + + return extCell; + }); + + for (let j = splice[0]; j < splice[0] + splice[1]; j++) { + this._cellDisposableMapping.get(this._cells[j].handle)?.dispose(); + this._cellDisposableMapping.delete(this._cells[j].handle); + } + + const deletedItems = this._cells.splice(splice[0], splice[1], ...newCells); + for (let cell of deletedItems) { + removedCellDocuments.push(cell.uri); + } + + contentChangeEvents.push(new RawContentChangeEvent(splice[0], splice[1], deletedItems, newCells)); + }); + + this._documentsAndEditors.acceptDocumentsAndEditorsDelta({ + addedDocuments: addedCellDocuments, + removedDocuments: removedCellDocuments + }); + + if (!initialization) { + this._emitter.emitModelChange({ + document: this.notebookDocument, + changes: contentChangeEvents.map(RawContentChangeEvent.asApiEvent) + }); + } + } + + private _moveCell(index: number, newIdx: number): void { + const cells = this._cells.splice(index, 1); + this._cells.splice(newIdx, 0, ...cells); + const changes: vscode.NotebookCellsChangeData[] = [{ + start: index, + deletedCount: 1, + deletedItems: cells.map(data => data.cell), + items: [] + }, { + start: newIdx, + deletedCount: 0, + deletedItems: [], + items: cells.map(data => data.cell) + }]; + this._emitter.emitModelChange({ + document: this.notebookDocument, + changes + }); + } + + private _setCellOutputs(index: number, outputs: IProcessedOutput[]): void { + const cell = this._cells[index]; + cell.setOutputs(outputs); + this._emitter.emitCellOutputsChange({ document: this.notebookDocument, cells: [cell.cell] }); + } + + private _clearCellOutputs(index: number): void { + const cell = this._cells[index].cell; + cell.outputs = []; + const event: vscode.NotebookCellOutputsChangeEvent = { document: this.notebookDocument, cells: [cell] }; + this._emitter.emitCellOutputsChange(event); + } + + private _clearAllCellOutputs(): void { + const modifedCells: vscode.NotebookCell[] = []; + this._cells.forEach(({ cell }) => { + if (cell.outputs.length !== 0) { + cell.outputs = []; + modifedCells.push(cell); + } + }); + const event: vscode.NotebookCellOutputsChangeEvent = { document: this.notebookDocument, cells: modifedCells }; + this._emitter.emitCellOutputsChange(event); + } + + private _changeCellLanguage(index: number, language: string): void { + const cell = this._cells[index]; + const event: vscode.NotebookCellLanguageChangeEvent = { document: this.notebookDocument, cell: cell.cell, language }; + this._emitter.emitCellLanguageChange(event); + } + + private _changeCellMetadata(index: number, newMetadata: NotebookCellMetadata | undefined): void { + const cell = this._cells[index]; + cell.setMetadata(newMetadata || {}); + const event: vscode.NotebookCellMetadataChangeEvent = { document: this.notebookDocument, cell: cell.cell }; + this._emitter.emitCellMetadataChange(event); + } + + async eventuallyUpdateCellOutputs(cell: ExtHostCell, diffs: ISplice[]) { + const outputDtos: NotebookCellOutputsSplice[] = diffs.map(diff => { + const outputs = diff.toInsert; + return [diff.start, diff.deleteCount, outputs]; + }); + + if (!outputDtos.length) { + return; + } + + await this._proxy.$spliceNotebookCellOutputs(this._viewType, this.uri, cell.handle, outputDtos); + this._emitter.emitCellOutputsChange({ + document: this.notebookDocument, + cells: [cell.cell] + }); + } + + getCell(cellHandle: number): ExtHostCell | undefined { + return this._cells.find(cell => cell.handle === cellHandle); + } addEdit(item: vscode.NotebookDocumentEditEvent): number { @@ -302,220 +510,16 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo this._edits.delete(id); } } - - private _disposed = false; - - constructor( - private readonly _proxy: MainThreadNotebookShape, - private _documentsAndEditors: ExtHostDocumentsAndEditors, - private _emitter: INotebookEventEmitter, - public viewType: string, - public uri: URI, - public renderingHandler: ExtHostNotebookOutputRenderingHandler, - private readonly _storagePath: URI | undefined - ) { - super(); - - const observableMetadata = getObservable(notebookDocumentMetadataDefaults); - this._metadata = observableMetadata.proxy; - this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { - this.updateMetadata(); - })); - } - - private updateMetadata() { - this._proxy.$updateNotebookMetadata(this.viewType, this.uri, this._metadata); - } - - getNewBackupUri(): URI { - if (!this._storagePath) { - throw new Error('Backup requires a valid storage path'); - } - const fileName = hashPath(this.uri) + (this._backupCounter++); - return joinPath(this._storagePath, fileName); - } - - updateBackup(backup: vscode.NotebookDocumentBackup): void { - this._backup?.delete(); - this._backup = backup; - } - - disposeBackup(): void { - this._backup?.delete(); - this._backup = undefined; - } - - dispose() { - this._disposed = true; - super.dispose(); - dispose(this._cellDisposableMapping.values()); - } - - get fileName() { return this.uri.fsPath; } - - get isDirty() { return false; } - - acceptModelChanged(event: NotebookCellsChangedEvent): void { - this._versionId = event.versionId; - if (event.kind === NotebookCellsChangeType.Initialize) { - this._spliceNotebookCells(event.changes, true); - } if (event.kind === NotebookCellsChangeType.ModelChange) { - this._spliceNotebookCells(event.changes, false); - } else if (event.kind === NotebookCellsChangeType.Move) { - this._moveCell(event.index, event.newIdx); - } else if (event.kind === NotebookCellsChangeType.CellClearOutput) { - this._clearCellOutputs(event.index); - } else if (event.kind === NotebookCellsChangeType.CellsClearOutput) { - this._clearAllCellOutputs(); - } else if (event.kind === NotebookCellsChangeType.ChangeLanguage) { - this._changeCellLanguage(event.index, event.language); - } else if (event.kind === NotebookCellsChangeType.ChangeMetadata) { - this._changeCellMetadata(event.index, event.metadata); - } - } - - private _spliceNotebookCells(splices: NotebookCellsSplice2[], initialization: boolean): void { - if (this._disposed) { - return; - } - - const contentChangeEvents: RawContentChangeEvent[] = []; - const addedCellDocuments: IExtHostModelAddedData[] = []; - const removedCellDocuments: URI[] = []; - - splices.reverse().forEach(splice => { - const cellDtos = splice[2]; - const newCells = cellDtos.map(cell => { - - const extCell = new ExtHostCell(this._proxy, this, this._documentsAndEditors, cell); - - if (!initialization) { - addedCellDocuments.push(ExtHostCell.asModelAddData(this, cell)); - } - - if (!this._cellDisposableMapping.has(extCell.handle)) { - this._cellDisposableMapping.set(extCell.handle, new DisposableStore()); - } - - const store = this._cellDisposableMapping.get(extCell.handle)!; - - store.add(extCell.onDidChangeOutputs((diffs) => { - this.eventuallyUpdateCellOutputs(extCell, diffs); - })); - - return extCell; - }); - - for (let j = splice[0]; j < splice[0] + splice[1]; j++) { - this._cellDisposableMapping.get(this._cells[j].handle)?.dispose(); - this._cellDisposableMapping.delete(this._cells[j].handle); - } - - const deletedItems = this._cells.splice(splice[0], splice[1], ...newCells); - for (let cell of deletedItems) { - removedCellDocuments.push(cell.uri); - } - - contentChangeEvents.push(new RawContentChangeEvent(splice[0], splice[1], deletedItems, newCells)); - }); - - this._documentsAndEditors.acceptDocumentsAndEditorsDelta({ - addedDocuments: addedCellDocuments, - removedDocuments: removedCellDocuments - }); - - if (!initialization) { - this._emitter.emitModelChange({ - document: this, - changes: contentChangeEvents.map(RawContentChangeEvent.asApiEvent) - }); - } - } - - private _moveCell(index: number, newIdx: number): void { - const cells = this._cells.splice(index, 1); - this._cells.splice(newIdx, 0, ...cells); - const changes: vscode.NotebookCellsChangeData[] = [{ - start: index, - deletedCount: 1, - deletedItems: cells.map(data => data.cell), - items: [] - }, { - start: newIdx, - deletedCount: 0, - deletedItems: [], - items: cells.map(data => data.cell) - }]; - this._emitter.emitModelChange({ - document: this, - changes - }); - } - - private _clearCellOutputs(index: number): void { - const cell = this.cells[index]; - cell.outputs = []; - const event: vscode.NotebookCellOutputsChangeEvent = { document: this, cells: [cell] }; - this._emitter.emitCellOutputsChange(event); - } - - private _clearAllCellOutputs(): void { - const modifedCells: vscode.NotebookCell[] = []; - this.cells.forEach(cell => { - if (cell.outputs.length !== 0) { - cell.outputs = []; - modifedCells.push(cell); - } - }); - const event: vscode.NotebookCellOutputsChangeEvent = { document: this, cells: modifedCells }; - this._emitter.emitCellOutputsChange(event); - } - - private _changeCellLanguage(index: number, language: string): void { - const cell = this._cells[index]; - const event: vscode.NotebookCellLanguageChangeEvent = { document: this, cell: cell.cell, language }; - this._emitter.emitCellLanguageChange(event); - } - - private _changeCellMetadata(index: number, newMetadata: NotebookCellMetadata): void { - const cell = this._cells[index]; - cell.setMetadata(newMetadata); - const event: vscode.NotebookCellMetadataChangeEvent = { document: this, cell: cell.cell }; - this._emitter.emitCellMetadataChange(event); - } - - async eventuallyUpdateCellOutputs(cell: ExtHostCell, diffs: ISplice[]) { - const outputDtos: NotebookCellOutputsSplice[] = diffs.map(diff => { - const outputs = diff.toInsert; - return [diff.start, diff.deleteCount, outputs]; - }); - - await this._proxy.$spliceNotebookCellOutputs(this.viewType, this.uri, cell.handle, outputDtos); - this._emitter.emitCellOutputsChange({ - document: this, - cells: [cell.cell] - }); - } - - getCell(cellHandle: number): ExtHostCell | undefined { - return this._cells.find(cell => cell.handle === cellHandle); - } - - getCell2(cellUri: UriComponents): ExtHostCell | undefined { - return this._cells.find(cell => cell.uri.fragment === cellUri.fragment); - } } export class NotebookEditorCellEditBuilder implements vscode.NotebookEditorCellEdit { - private _finalized: boolean = false; - private readonly _documentVersionId: number; - private _collectedEdits: ICellEditOperation[] = []; - private _renderers = new Set(); - constructor( - readonly editor: ExtHostNotebookEditor - ) { - this._documentVersionId = editor.document.versionId; + private readonly _documentVersionId: number; + private readonly _collectedEdits: ICellEditOperation[] = []; + private _finalized: boolean = false; + + constructor(documentVersionId: number) { + this._documentVersionId = documentVersionId; } finalize(): INotebookEditData { @@ -523,7 +527,6 @@ export class NotebookEditorCellEditBuilder implements vscode.NotebookEditorCellE return { documentVersionId: this._documentVersionId, edits: this._collectedEdits, - renderers: Array.from(this._renderers) }; } @@ -533,33 +536,54 @@ export class NotebookEditorCellEditBuilder implements vscode.NotebookEditorCellE } } - insert(index: number, content: string | string[], language: string, type: CellKind, outputs: vscode.CellOutput[], metadata: vscode.NotebookCellMetadata | undefined): void { + replaceMetadata(index: number, metadata: vscode.NotebookCellMetadata): void { + this._throwIfFinalized(); + this._collectedEdits.push({ + editType: CellEditType.Metadata, + index, + metadata + }); + } + + replaceOutput(index: number, outputs: vscode.CellOutput[]): void { + this._throwIfFinalized(); + this._collectedEdits.push({ + editType: CellEditType.Output, + index, + outputs: outputs.map(output => addIdToOutput(output)) + }); + } + + replaceCells(from: number, to: number, cells: vscode.NotebookCellData[]): void { this._throwIfFinalized(); - const sourceArr = Array.isArray(content) ? content : content.split(/\r|\n|\r\n/g); - const cell = { - source: sourceArr, - language, - cellKind: type, - outputs: outputs.map(o => addIdToOutput(o)), - metadata, - }; - this._collectedEdits.push({ - editType: CellEditType.Insert, - index, - cells: [cell] + editType: CellEditType.Replace, + index: from, + count: to - from, + cells: cells.map(data => { + return { + ...data, + outputs: data.outputs.map(output => addIdToOutput(output)), + }; + }) }); } + insert(index: number, content: string | string[], language: string, type: CellKind, outputs: vscode.CellOutput[], metadata: vscode.NotebookCellMetadata | undefined): void { + this._throwIfFinalized(); + this.replaceCells(index, index, [{ + language, + outputs, + metadata, + cellKind: type, + source: Array.isArray(content) ? content.join('\n') : content, + }]); + } + delete(index: number): void { this._throwIfFinalized(); - - this._collectedEdits.push({ - editType: CellEditType.Delete, - index, - count: 1 - }); + this.replaceCells(index, 1, []); } } @@ -613,6 +637,20 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook selection?: vscode.NotebookCell; + private _visibleRanges: vscode.NotebookCellRange[] = []; + + get visibleRanges() { + return this._visibleRanges; + } + + set visibleRanges(_range: vscode.NotebookCellRange[]) { + throw readonly('visibleRanges'); + } + + _acceptVisibleRanges(value: vscode.NotebookCellRange[]): void { + this._visibleRanges = value; + } + private _active: boolean = false; get active(): boolean { return this._active; @@ -660,7 +698,7 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook public uri: URI, private _proxy: MainThreadNotebookShape, private _webComm: vscode.NotebookCommunication, - public document: ExtHostNotebookDocument, + public readonly notebookData: ExtHostNotebookDocument, ) { super(); this._register(this._webComm.onDidReceiveMessage(e => { @@ -668,14 +706,17 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook })); } - edit(callback: (editBuilder: NotebookEditorCellEditBuilder) => void): Thenable { - const edit = new NotebookEditorCellEditBuilder(this); - callback(edit); - return this._applyEdit(edit); + get document(): vscode.NotebookDocument { + return this.notebookData.notebookDocument; } - private _applyEdit(editBuilder: NotebookEditorCellEditBuilder): Promise { - const editData = editBuilder.finalize(); + edit(callback: (editBuilder: NotebookEditorCellEditBuilder) => void): Thenable { + const edit = new NotebookEditorCellEditBuilder(this.document.version); + callback(edit); + return this._applyEdit(edit.finalize()); + } + + private _applyEdit(editData: INotebookEditData): Promise { // return when there is nothing to do if (editData.edits.length === 0) { @@ -695,16 +736,10 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook const prevIndex = compressedEditsIndex; const prev = compressedEdits[prevIndex]; - if (prev.editType === CellEditType.Insert && editData.edits[i].editType === CellEditType.Insert) { + if (prev.editType === CellEditType.Replace && editData.edits[i].editType === CellEditType.Replace) { if (prev.index === editData.edits[i].index) { - prev.cells.push(...(editData.edits[i] as ICellInsertEdit).cells); - continue; - } - } - - if (prev.editType === CellEditType.Delete && editData.edits[i].editType === CellEditType.Delete) { - if (prev.index === editData.edits[i].index) { - prev.count += (editData.edits[i] as ICellDeleteEdit).count; + prev.cells.push(...(editData.edits[i] as ICellReplaceEdit).cells); + prev.count += (editData.edits[i] as ICellReplaceEdit).count; continue; } } @@ -713,7 +748,11 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook compressedEditsIndex++; } - return this._proxy.$tryApplyEdits(this.viewType, this.uri, editData.documentVersionId, compressedEdits, editData.renderers); + return this._proxy.$tryApplyEdits(this.viewType, this.uri, editData.documentVersionId, compressedEdits); + } + + revealRange(range: vscode.NotebookCellRange, revealType?: extHostTypes.NotebookEditorRevealType) { + this._proxy.$tryRevealRange(this.id, range, revealType || extHostTypes.NotebookEditorRevealType.Default); } get viewColumn(): vscode.ViewColumn | undefined { @@ -756,14 +795,15 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable { super(); if (this._provider.onDidChangeKernels) { - this._register(this._provider.onDidChangeKernels(() => { - this._proxy.$onNotebookKernelChange(this._handle); + this._register(this._provider.onDidChangeKernels((e: vscode.NotebookDocument | undefined) => { + const uri = e?.uri; + this._proxy.$onNotebookKernelChange(this._handle, uri); })); } } async provideKernels(document: ExtHostNotebookDocument, token: vscode.CancellationToken): Promise { - const data = await this._provider.provideKernels(document, token) || []; + const data = await this._provider.provideKernels(document.notebookDocument, token) || []; const newMap = new Map(); let kernel_unique_pool = 0; @@ -789,6 +829,7 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable { extension: this._extension.identifier, extensionLocation: this._extension.extensionLocation, description: kernel.description, + detail: kernel.detail, isPreferred: kernel.isPreferred, preloads: kernel.preloads }; @@ -812,7 +853,7 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable { const kernel = this._idToKernel.get(kernelId); if (kernel && this._provider.resolveKernel) { - return this._provider.resolveKernel(kernel, document, webview, token); + return this._provider.resolveKernel(kernel, document.notebookDocument, webview, token); } } @@ -824,9 +865,9 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable { } if (cell) { - return withToken(token => (kernel.executeCell as any)(document, cell.cell, token)); + return withToken(token => (kernel.executeCell as any)(document.notebookDocument, cell.cell, token)); } else { - return withToken(token => (kernel.executeAllCells as any)(document, token)); + return withToken(token => (kernel.executeAllCells as any)(document.notebookDocument, token)); } } @@ -838,9 +879,9 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable { } if (cell) { - return kernel.cancelCellExecution(document, cell.cell); + return kernel.cancelCellExecution(document.notebookDocument, cell.cell); } else { - return kernel.cancelAllCellsExecution(document); + return kernel.cancelAllCellsExecution(document.notebookDocument); } } } @@ -866,6 +907,11 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN private readonly _unInitializedDocuments = new ResourceMap(); private readonly _editors = new Map(); private readonly _webviewComm = new Map(); + private readonly _commandsConverter: CommandsConverter; + private readonly _onDidChangeNotebookEditorSelection = new Emitter(); + readonly onDidChangeNotebookEditorSelection = this._onDidChangeNotebookEditorSelection.event; + private readonly _onDidChangeNotebookEditorVisibleRanges = new Emitter(); + readonly onDidChangeNotebookEditorVisibleRanges = this._onDidChangeNotebookEditorVisibleRanges.event; private readonly _onDidChangeNotebookCells = new Emitter(); readonly onDidChangeNotebookCells = this._onDidChangeNotebookCells.event; private readonly _onDidChangeCellOutputs = new Emitter(); @@ -889,10 +935,6 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN return this._activeNotebookEditor; } - get notebookDocuments() { - return [...this._documents.values()]; - } - private _onDidOpenNotebookDocument = new Emitter(); onDidOpenNotebookDocument: Event = this._onDidOpenNotebookDocument.event; private _onDidCloseNotebookDocument = new Emitter(); @@ -900,7 +942,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN private _onDidSaveNotebookDocument = new Emitter(); onDidSaveNotebookDocument: Event = this._onDidCloseNotebookDocument.event; visibleNotebookEditors: ExtHostNotebookEditor[] = []; - private _onDidChangeActiveNotebookKernel = new Emitter<{ document: ExtHostNotebookDocument, kernel: vscode.NotebookKernel | undefined; }>(); + private _onDidChangeActiveNotebookKernel = new Emitter<{ document: vscode.NotebookDocument, kernel: vscode.NotebookKernel | undefined; }>(); onDidChangeActiveNotebookKernel = this._onDidChangeActiveNotebookKernel.event; private _onDidChangeVisibleNotebookEditors = new Emitter(); onDidChangeVisibleNotebookEditors = this._onDidChangeVisibleNotebookEditors.event; @@ -914,18 +956,20 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN private readonly _extensionStoragePaths: IExtensionStoragePaths, ) { this._proxy = mainContext.getProxy(MainContext.MainThreadNotebook); + this._commandsConverter = commands.converter; commands.registerArgumentProcessor({ - processArgument: arg => { + // Serialized INotebookCellActionContext + processArgument: (arg) => { if (arg && arg.$mid === 12) { const documentHandle = arg.notebookEditor?.notebookHandle; const cellHandle = arg.cell.handle; for (const value of this._editors) { - if (value[1].editor.document.handle === documentHandle) { - const cell = value[1].editor.document.getCell(cellHandle); + if (value[1].editor.notebookData.handle === documentHandle) { + const cell = value[1].editor.notebookData.getCell(cellHandle); if (cell) { - return cell; + return cell.cell; } } } @@ -935,20 +979,28 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN }); } + get notebookDocuments() { + return [...this._documents.values()]; + } + + lookupNotebookDocument(uri: URI): ExtHostNotebookDocument | undefined { + return this._documents.get(uri); + } + registerNotebookContentProvider( extension: IExtensionDescription, viewType: string, provider: vscode.NotebookContentProvider, + options?: { + transientOutputs: boolean; + transientMetadata: { [K in keyof NotebookCellMetadata]?: boolean }; + } ): vscode.Disposable { if (this._notebookContentProviders.has(viewType)) { throw new Error(`Notebook provider for '${viewType}' already registered`); } - // if ((provider).executeCell) { - // throw new Error('NotebookContentKernel.executeCell is removed, please use vscode.notebook.registerNotebookKernel instead.'); - // } - this._notebookContentProviders.set(viewType, { extension, provider }); const listener = provider.onDidChangeNotebook @@ -970,7 +1022,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN const supportBackup = !!provider.backupNotebook; - this._proxy.$registerNotebookProvider({ id: extension.identifier, location: extension.extensionLocation, description: extension.description }, viewType, supportBackup, provider.kernel ? { id: viewType, label: provider.kernel.label, extensionLocation: extension.extensionLocation, preloads: provider.kernel.preloads } : undefined); + this._proxy.$registerNotebookProvider({ id: extension.identifier, location: extension.extensionLocation, description: extension.description }, viewType, supportBackup, { transientOutputs: options?.transientOutputs || false, transientMetadata: options?.transientMetadata || {} }); return new extHostTypes.Disposable(() => { listener.dispose(); @@ -985,8 +1037,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN this._notebookKernelProviders.set(handle, adapter); this._proxy.$registerNotebookKernelProvider({ id: extension.identifier, location: extension.extensionLocation, description: extension.description }, handle, { viewType: selector.viewType, - filenamePattern: selector.filenamePattern ? typeConverters.GlobPattern.from(selector.filenamePattern) : undefined, - excludeFileNamePattern: selector.excludeFileNamePattern ? typeConverters.GlobPattern.from(selector.excludeFileNamePattern) : undefined, + filenamePattern: selector.filenamePattern ? typeConverters.NotebookExclusiveDocumentPattern.from(selector.filenamePattern) : undefined }); return new extHostTypes.Disposable(() => { @@ -1028,21 +1079,6 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN }); } - registerNotebookKernel(extension: IExtensionDescription, id: string, selectors: vscode.GlobPattern[], kernel: vscode.NotebookKernel): vscode.Disposable { - if (this._notebookKernels.has(id)) { - throw new Error(`Notebook kernel for '${id}' already registered`); - } - - this._notebookKernels.set(id, { kernel, extension }); - const transformedSelectors = selectors.map(selector => typeConverters.GlobPattern.from(selector)); - - this._proxy.$registerNotebookKernel({ id: extension.identifier, location: extension.extensionLocation, description: extension.description }, id, kernel.label, transformedSelectors, kernel.preloads || []); - return new extHostTypes.Disposable(() => { - this._notebookKernels.delete(id); - this._proxy.$unregisterNotebookKernel(id); - }); - } - async $resolveNotebookData(viewType: string, uri: UriComponents, backupId?: string): Promise { const provider = this._notebookContentProviders.get(viewType); const revivedUri = URI.revive(uri); @@ -1106,49 +1142,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN return; } - await provider.provider.resolveNotebook(document, webComm.contentProviderComm); - } - - async $executeNotebookByAttachedKernel(viewType: string, uri: UriComponents, cellHandle: number | undefined): Promise { - const document = this._documents.get(URI.revive(uri)); - - if (!document) { - return; - } - - if (this._notebookContentProviders.has(viewType)) { - const cell = cellHandle !== undefined ? document.getCell(cellHandle) : undefined; - const provider = this._notebookContentProviders.get(viewType)!.provider; - - if (provider.kernel) { - if (cell) { - return withToken(token => (provider.kernel!.executeCell as any)(document, cell, token)); - } else { - return withToken(token => (provider.kernel!.executeAllCells as any)(document, token)); - } - } - } - } - - async $cancelNotebookByAttachedKernel(viewType: string, uri: UriComponents, cellHandle: number | undefined): Promise { - const document = this._documents.get(URI.revive(uri)); - - if (!document) { - return; - } - - if (this._notebookContentProviders.has(viewType)) { - const cell = cellHandle !== undefined ? document.getCell(cellHandle) : undefined; - const provider = this._notebookContentProviders.get(viewType)!.provider; - - if (provider.kernel) { - if (cell) { - return provider.kernel.cancelCellExecution(document, cell.cell); - } else { - return provider.kernel.cancelAllCellsExecution(document); - } - } - } + await provider.provider.resolveNotebook(document.notebookDocument, webComm.contentProviderComm); } async $executeNotebookKernelFromProvider(handle: number, uri: UriComponents, kernelId: string, cellHandle: number | undefined): Promise { @@ -1170,7 +1164,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN async $executeNotebook2(kernelId: string, viewType: string, uri: UriComponents, cellHandle: number | undefined): Promise { const document = this._documents.get(URI.revive(uri)); - if (!document || document.viewType !== viewType) { + if (!document || document.notebookDocument.viewType !== viewType) { return; } @@ -1183,9 +1177,9 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN const cell = cellHandle !== undefined ? document.getCell(cellHandle) : undefined; if (cell) { - return withToken(token => (kernelInfo!.kernel.executeCell as any)(document, cell.cell, token)); + return withToken(token => (kernelInfo!.kernel.executeCell as any)(document.notebookDocument, cell.cell, token)); } else { - return withToken(token => (kernelInfo!.kernel.executeAllCells as any)(document, token)); + return withToken(token => (kernelInfo!.kernel.executeAllCells as any)(document.notebookDocument, token)); } } @@ -1196,7 +1190,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } if (this._notebookContentProviders.has(viewType)) { - await this._notebookContentProviders.get(viewType)!.provider.saveNotebook(document, token); + await this._notebookContentProviders.get(viewType)!.provider.saveNotebook(document.notebookDocument, token); return true; } @@ -1210,7 +1204,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } if (this._notebookContentProviders.has(viewType)) { - await this._notebookContentProviders.get(viewType)!.provider.saveNotebookAs(URI.revive(target), document, token); + await this._notebookContentProviders.get(viewType)!.provider.saveNotebookAs(URI.revive(target), document.notebookDocument, token); return true; } @@ -1242,7 +1236,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN const provider = this._notebookContentProviders.get(viewType); if (document && provider && provider.provider.backupNotebook) { - const backup = await provider.provider.backupNotebook(document, { destination: document.getNewBackupUri() }, cancellation); + const backup = await provider.provider.backupNotebook(document.notebookDocument, { destination: document.getNewBackupUri() }, cancellation); document.updateBackup(backup); return backup.id; } @@ -1259,11 +1253,11 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN this._withAdapter(event.providerHandle, event.uri, async (adapter, document) => { const kernel = event.kernelId ? adapter.getKernel(event.kernelId) : undefined; this._editors.forEach(editor => { - if (editor.editor.document === document) { + if (editor.editor.notebookData === document) { editor.editor.updateActiveKernel(kernel); } }); - this._onDidChangeActiveNotebookKernel.fire({ document, kernel }); + this._onDidChangeActiveNotebookKernel.fire({ document: document.notebookDocument, kernel }); }); } } @@ -1285,12 +1279,10 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN this._webviewComm.get(editorId)?.onDidReceiveMessage(forRendererType, message); } - $acceptModelChanged(uriComponents: UriComponents, event: NotebookCellsChangedEvent): void { - + $acceptModelChanged(uriComponents: UriComponents, event: NotebookCellsChangedEvent, isDirty: boolean): void { const document = this._documents.get(URI.revive(uriComponents)); - if (document) { - document.acceptModelChanged(event); + document.acceptModelChanged(event, isDirty); } } @@ -1298,12 +1290,35 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN const document = this._documents.get(URI.revive(uriComponents)); if (document) { // this.$acceptDirtyStateChanged(uriComponents, false); - this._onDidSaveNotebookDocument.fire(document); + this._onDidSaveNotebookDocument.fire(document.notebookDocument); } } - $acceptEditorPropertiesChanged(uriComponents: UriComponents, data: INotebookEditorPropertiesChangeData): void { - this.logService.debug('ExtHostNotebook#$acceptEditorPropertiesChanged', uriComponents.path, data); + $acceptEditorPropertiesChanged(id: string, data: INotebookEditorPropertiesChangeData): void { + this.logService.debug('ExtHostNotebook#$acceptEditorPropertiesChanged', id, data); + + let editor: { editor: ExtHostNotebookEditor; } | undefined; + this._editors.forEach(e => { + if (e.editor.id === id) { + editor = e; + } + }); + + if (!editor) { + return; + } + + if (data.visibleRanges) { + editor.editor._acceptVisibleRanges(data.visibleRanges.ranges); + this._onDidChangeNotebookEditorVisibleRanges.fire({ + notebookEditor: editor.editor, + visibleRanges: editor.editor.visibleRanges + }); + } + } + + $acceptDocumentPropertiesChanged(uriComponents: UriComponents, data: INotebookDocumentPropertiesChangeData): void { + this.logService.debug('ExtHostNotebook#$acceptDocumentPropertiesChanged', uriComponents.path, data); const editor = this._getEditorFromURI(uriComponents); if (!editor) { @@ -1313,21 +1328,27 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN if (data.selections) { if (data.selections.selections.length) { const firstCell = data.selections.selections[0]; - editor.editor.selection = editor.editor.document.getCell(firstCell)?.cell; + editor.editor.selection = editor.editor.notebookData.getCell(firstCell)?.cell; } else { editor.editor.selection = undefined; } + + this._onDidChangeNotebookEditorSelection.fire({ + notebookEditor: editor.editor, + selection: editor.editor.selection + }); } + if (data.metadata) { - editor.editor.document.metadata = { + editor.editor.notebookData.notebookDocument.metadata = { ...notebookDocumentMetadataDefaults, ...data.metadata }; } } - private _createExtHostEditor(document: ExtHostNotebookDocument, editorId: string, selections: number[]) { + private _createExtHostEditor(document: ExtHostNotebookDocument, editorId: string, selections: number[], visibleRanges: vscode.NotebookCellRange[]) { const revivedUri = document.uri; let webComm = this._webviewComm.get(editorId); @@ -1337,7 +1358,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } const editor = new ExtHostNotebookEditor( - document.viewType, + document.notebookDocument.viewType, editorId, revivedUri, this._proxy, @@ -1347,11 +1368,13 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN if (selections.length) { const firstCell = selections[0]; - editor.selection = editor.document.getCell(firstCell)?.cell; + editor.selection = editor.notebookData.getCell(firstCell)?.cell; } else { editor.selection = undefined; } + editor._acceptVisibleRanges(visibleRanges); + this._editors.get(editorId)?.editor.dispose(); this._editors.set(editorId, { editor }); } @@ -1367,8 +1390,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN if (document) { document.dispose(); this._documents.delete(revivedUri); - this._documentsAndEditors.$acceptDocumentsAndEditorsDelta({ removedDocuments: document.cells.map(cell => cell.uri) }); - this._onDidCloseNotebookDocument.fire(document); + this._documentsAndEditors.$acceptDocumentsAndEditorsDelta({ removedDocuments: document.notebookDocument.cells.map(cell => cell.uri) }); + this._onDidCloseNotebookDocument.fire(document.notebookDocument); } for (const e of this._editors.values()) { @@ -1412,7 +1435,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN this._unInitializedDocuments.delete(revivedUri); if (modelData.metadata) { - document.metadata = { + document.notebookDocument.metadata = { ...notebookDocumentMetadataDefaults, ...modelData.metadata }; @@ -1426,17 +1449,17 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN 0, modelData.cells ]] - }); + }, false); // add cell document as vscode.TextDocument - addedCellDocuments.push(...modelData.cells.map(cell => ExtHostCell.asModelAddData(document, cell))); + addedCellDocuments.push(...modelData.cells.map(cell => ExtHostCell.asModelAddData(document.notebookDocument, cell))); this._documents.get(revivedUri)?.dispose(); this._documents.set(revivedUri, document); // create editor if populated if (modelData.attachedEditor) { - this._createExtHostEditor(document, modelData.attachedEditor.id, modelData.attachedEditor.selections); + this._createExtHostEditor(document, modelData.attachedEditor.id, modelData.attachedEditor.selections, modelData.attachedEditor.visibleRanges); editorChanged = true; } } @@ -1444,7 +1467,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN this._documentsAndEditors.$acceptDocumentsAndEditorsDelta({ addedDocuments: addedCellDocuments }); const document = this._documents.get(revivedUri)!; - this._onDidOpenNotebookDocument.fire(document); + this._onDidOpenNotebookDocument.fire(document.notebookDocument); } } @@ -1458,7 +1481,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN const document = this._documents.get(revivedUri); if (document) { - this._createExtHostEditor(document, editorModelData.id, editorModelData.selections); + this._createExtHostEditor(document, editorModelData.id, editorModelData.selections, editorModelData.visibleRanges); editorChanged = true; } } @@ -1523,6 +1546,24 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN this._onDidChangeActiveNotebookEditor.fire(this._activeNotebookEditor); } } + + createNotebookCellStatusBarItemInternal(cell: vscode.NotebookCell, alignment: extHostTypes.NotebookCellStatusBarAlignment | undefined, priority: number | undefined) { + const statusBarItem = new NotebookCellStatusBarItemInternal(this._proxy, this._commandsConverter, cell, alignment, priority); + + // Look up the ExtHostCell for this NotebookCell URI, bind to its disposable lifecycle + const parsedUri = CellUri.parse(cell.uri); + if (parsedUri) { + const document = this._documents.get(parsedUri.notebook); + if (document) { + const cell = document.getCell(parsedUri.handle); + if (cell) { + Event.once(cell.onDidDispose)(() => statusBarItem.dispose()); + } + } + } + + return statusBarItem; + } } function hashPath(resource: URI): string { @@ -1534,3 +1575,178 @@ function isEditEvent(e: vscode.NotebookDocumentEditEvent | vscode.NotebookDocume return typeof (e as vscode.NotebookDocumentEditEvent).undo === 'function' && typeof (e as vscode.NotebookDocumentEditEvent).redo === 'function'; } + +export class NotebookCellStatusBarItemInternal extends Disposable { + private static NEXT_ID = 0; + + private readonly _id = NotebookCellStatusBarItemInternal.NEXT_ID++; + private readonly _internalCommandRegistration: DisposableStore; + + private _isDisposed = false; + private _alignment: extHostTypes.NotebookCellStatusBarAlignment; + + constructor( + private readonly _proxy: MainThreadNotebookShape, + private readonly _commands: CommandsConverter, + private readonly _cell: vscode.NotebookCell, + alignment: extHostTypes.NotebookCellStatusBarAlignment | undefined, + private _priority: number | undefined) { + super(); + this._internalCommandRegistration = this._register(new DisposableStore()); + this._alignment = alignment ?? extHostTypes.NotebookCellStatusBarAlignment.Left; + } + + private _apiItem: vscode.NotebookCellStatusBarItem | undefined; + get apiItem(): vscode.NotebookCellStatusBarItem { + if (!this._apiItem) { + this._apiItem = createNotebookCellStatusBarApiItem(this); + } + + return this._apiItem; + } + + get cell(): vscode.NotebookCell { + return this._cell; + } + + get alignment(): extHostTypes.NotebookCellStatusBarAlignment { + return this._alignment; + } + + set alignment(v: extHostTypes.NotebookCellStatusBarAlignment) { + this._alignment = v; + this.update(); + } + + get priority(): number | undefined { + return this._priority; + } + + set priority(v: number | undefined) { + this._priority = v; + this.update(); + } + + private _text: string = ''; + get text(): string { + return this._text; + } + + set text(v: string) { + this._text = v; + this.update(); + } + + private _tooltip: string | undefined; + get tooltip(): string | undefined { + return this._tooltip; + } + + set tooltip(v: string | undefined) { + this._tooltip = v; + this.update(); + } + + private _command?: { + readonly fromApi: string | vscode.Command, + readonly internal: ICommandDto, + }; + get command(): string | vscode.Command | undefined { + return this._command?.fromApi; + } + + 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(); + } + + private _accessibilityInformation: vscode.AccessibilityInformation | undefined; + get accessibilityInformation(): vscode.AccessibilityInformation | undefined { + return this._accessibilityInformation; + } + + set accessibilityInformation(v: vscode.AccessibilityInformation | undefined) { + this._accessibilityInformation = v; + this.update(); + } + + private _visible: boolean = false; + show(): void { + this._visible = true; + this.update(); + } + + hide(): void { + this._visible = false; + this.update(); + } + + dispose(): void { + this.hide(); + this._isDisposed = true; + this._internalCommandRegistration.dispose(); + } + + private update(): void { + if (this._isDisposed) { + return; + } + + const entry: INotebookCellStatusBarEntry = { + alignment: this.alignment === extHostTypes.NotebookCellStatusBarAlignment.Left ? CellStatusbarAlignment.LEFT : CellStatusbarAlignment.RIGHT, + cellResource: this.cell.uri, + command: this._command?.internal, + text: this.text, + tooltip: this.tooltip, + accessibilityInformation: this.accessibilityInformation, + priority: this.priority, + visible: this._visible + }; + + this._proxy.$setStatusBarEntry(this._id, entry); + } +} + +function createNotebookCellStatusBarApiItem(internalItem: NotebookCellStatusBarItemInternal): vscode.NotebookCellStatusBarItem { + return Object.freeze({ + cell: internalItem.cell, + get alignment() { return internalItem.alignment; }, + set alignment(v: NotebookCellStatusBarItemInternal['alignment']) { internalItem.alignment = v; }, + + get priority() { return internalItem.priority; }, + set priority(v: NotebookCellStatusBarItemInternal['priority']) { internalItem.priority = v; }, + + get text() { return internalItem.text; }, + set text(v: NotebookCellStatusBarItemInternal['text']) { internalItem.text = v; }, + + get tooltip() { return internalItem.tooltip; }, + set tooltip(v: NotebookCellStatusBarItemInternal['tooltip']) { internalItem.tooltip = v; }, + + get command() { return internalItem.command; }, + set command(v: NotebookCellStatusBarItemInternal['command']) { internalItem.command = v; }, + + get accessibilityInformation() { return internalItem.accessibilityInformation; }, + set accessibilityInformation(v: NotebookCellStatusBarItemInternal['accessibilityInformation']) { internalItem.accessibilityInformation = v; }, + + show() { internalItem.show(); }, + hide() { internalItem.hide(); }, + dispose() { internalItem.dispose(); } + }); +} diff --git a/src/vs/workbench/api/common/extHostSCM.ts b/src/vs/workbench/api/common/extHostSCM.ts index c0e41a9615..48899af114 100644 --- a/src/vs/workbench/api/common/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -516,6 +516,7 @@ class ExtHostSourceControl implements vscode.SourceControl { } this._proxy.$registerGroups(this.handle, groups, splices); + this.createdResourceGroups.clear(); } @debounce(100) diff --git a/src/vs/workbench/api/common/extHostTask.ts b/src/vs/workbench/api/common/extHostTask.ts index c35023d408..3251137a22 100644 --- a/src/vs/workbench/api/common/extHostTask.ts +++ b/src/vs/workbench/api/common/extHostTask.ts @@ -269,8 +269,8 @@ export namespace TaskDTO { presentationOptions: TaskPresentationOptionsDTO.from(value.presentationOptions), problemMatchers: value.problemMatchers, hasDefinedMatchers: (value as types.Task).hasDefinedMatchers, - runOptions: (value).runOptions ? (value).runOptions : { reevaluateOnRerun: true }, - detail: (value).detail + runOptions: value.runOptions ? value.runOptions : { reevaluateOnRerun: true }, + detail: value.detail }; return result; } diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index 53b587f0c1..3ecf46e591 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -339,6 +339,7 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ public get onDidWriteTerminalData(): Event { return this._onDidWriteTerminalData && this._onDidWriteTerminalData.event; } constructor( + supportsProcesses: boolean, @IExtHostRpcService extHostRpc: IExtHostRpcService ) { this._proxy = extHostRpc.getProxy(MainContext.MainThreadTerminalService); @@ -347,6 +348,7 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ onFirstListenerAdd: () => this._proxy.$startSendingDataEvents(), onLastListenerRemove: () => this._proxy.$stopSendingDataEvents() }); + this._proxy.$registerProcessSupport(supportsProcesses); } public abstract createTerminal(name?: string, shellPath?: string, shellArgs?: string[] | string): vscode.Terminal; @@ -805,6 +807,12 @@ export class EnvironmentVariableCollection implements vscode.EnvironmentVariable } export class WorkerExtHostTerminalService extends BaseExtHostTerminalService { + constructor( + @IExtHostRpcService extHostRpc: IExtHostRpcService + ) { + super(false, extHostRpc); + } + public createTerminal(name?: string, shellPath?: string, shellArgs?: string[] | string): vscode.Terminal { throw new NotSupportedError(); } diff --git a/src/vs/workbench/api/common/extHostTextEditors.ts b/src/vs/workbench/api/common/extHostTextEditors.ts index 12377afa59..c0379f40bf 100644 --- a/src/vs/workbench/api/common/extHostTextEditors.ts +++ b/src/vs/workbench/api/common/extHostTextEditors.ts @@ -7,6 +7,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import * as arrays from 'vs/base/common/arrays'; import { ExtHostEditorsShape, IEditorPropertiesChangeData, IMainContext, ITextDocumentShowOptions, ITextEditorPositionData, MainContext, MainThreadTextEditorsShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; +import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; import { ExtHostTextEditor, TextEditorDecorationType } from 'vs/workbench/api/common/extHostTextEditor'; import * as TypeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { TextEditorSelectionChangeKind } from 'vs/workbench/api/common/extHostTypes'; @@ -28,16 +29,15 @@ export class ExtHostEditors implements ExtHostEditorsShape { readonly onDidChangeActiveTextEditor: Event = this._onDidChangeActiveTextEditor.event; readonly onDidChangeVisibleTextEditors: Event = this._onDidChangeVisibleTextEditors.event; - - private _proxy: MainThreadTextEditorsShape; - private _extHostDocumentsAndEditors: ExtHostDocumentsAndEditors; + private readonly _proxy: MainThreadTextEditorsShape; constructor( mainContext: IMainContext, - extHostDocumentsAndEditors: ExtHostDocumentsAndEditors, + private readonly _extHostDocumentsAndEditors: ExtHostDocumentsAndEditors, + private readonly _extHostNotebooks: ExtHostNotebookController, ) { this._proxy = mainContext.getProxy(MainContext.MainThreadTextEditors); - this._extHostDocumentsAndEditors = extHostDocumentsAndEditors; + this._extHostDocumentsAndEditors.onDidChangeVisibleTextEditors(e => this._onDidChangeVisibleTextEditors.fire(e)); this._extHostDocumentsAndEditors.onDidChangeActiveTextEditor(e => this._onDidChangeActiveTextEditor.fire(e)); @@ -93,7 +93,7 @@ export class ExtHostEditors implements ExtHostEditorsShape { } applyWorkspaceEdit(edit: vscode.WorkspaceEdit): Promise { - const dto = TypeConverters.WorkspaceEdit.from(edit, this._extHostDocumentsAndEditors); + const dto = TypeConverters.WorkspaceEdit.from(edit, this._extHostDocumentsAndEditors, this._extHostNotebooks); return this._proxy.$tryApplyWorkspaceEdit(dto); } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index ca47bd599d..046ffba3ce 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -31,6 +31,7 @@ import { LogLevel as _MainLogLevel } from 'vs/platform/log/common/log'; import { coalesce, isNonEmptyArray } from 'vs/base/common/arrays'; import { RenderLineNumbersType } from 'vs/editor/common/config/editorOptions'; import { CommandsConverter } from 'vs/workbench/api/common/extHostCommands'; +import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; export interface PositionLike { line: number; @@ -505,32 +506,42 @@ export namespace TextEdit { } export namespace WorkspaceEdit { - export function from(value: vscode.WorkspaceEdit, documents?: ExtHostDocumentsAndEditors): extHostProtocol.IWorkspaceEditDto { + export function from(value: vscode.WorkspaceEdit, documents?: ExtHostDocumentsAndEditors, notebooks?: ExtHostNotebookController): extHostProtocol.IWorkspaceEditDto { const result: extHostProtocol.IWorkspaceEditDto = { edits: [] }; if (value instanceof types.WorkspaceEdit) { - for (let entry of value.allEntries()) { + for (let entry of value._allEntries()) { - if (entry._type === 1) { + if (entry._type === types.FileEditType.File) { // file operation result.edits.push({ + _type: extHostProtocol.WorkspaceEditType.File, oldUri: entry.from, newUri: entry.to, options: entry.options, metadata: entry.metadata }); - } else { + } else if (entry._type === types.FileEditType.Text) { // text edits const doc = documents?.getDocument(entry.uri); result.edits.push({ + _type: extHostProtocol.WorkspaceEditType.Text, resource: entry.uri, edit: TextEdit.from(entry.edit), modelVersionId: doc?.version, metadata: entry.metadata }); + } else if (entry._type === types.FileEditType.Cell) { + result.edits.push({ + _type: extHostProtocol.WorkspaceEditType.Cell, + resource: entry.uri, + edit: entry.edit, + metadata: entry.metadata, + modelVersionId: notebooks?.lookupNotebookDocument(entry.uri)?.notebookDocument.version + }); } } } @@ -1281,3 +1292,58 @@ export namespace LogLevel { } } } + +export namespace NotebookExclusiveDocumentPattern { + export function from(pattern: { include: vscode.GlobPattern | undefined, exclude: vscode.GlobPattern | undefined }): { include: string | types.RelativePattern | undefined, exclude: string | types.RelativePattern | undefined }; + export function from(pattern: vscode.GlobPattern): string | types.RelativePattern; + export function from(pattern: undefined): undefined; + export function from(pattern: { include: vscode.GlobPattern | undefined | null, exclude: vscode.GlobPattern | undefined } | vscode.GlobPattern | undefined): string | types.RelativePattern | { include: string | types.RelativePattern | undefined, exclude: string | types.RelativePattern | undefined } | undefined; + export function from(pattern: { include: vscode.GlobPattern | undefined | null, exclude: vscode.GlobPattern | undefined } | vscode.GlobPattern | undefined): string | types.RelativePattern | { include: string | types.RelativePattern | undefined, exclude: string | types.RelativePattern | undefined } | undefined { + if (pattern === null || pattern === undefined) { + return undefined; + } + + if (pattern instanceof types.RelativePattern) { + return pattern; + } + + if (typeof pattern === 'string') { + return pattern; + } + + + if (isRelativePattern(pattern)) { + return new types.RelativePattern(pattern.base, pattern.pattern); + } + + if (isExclusivePattern(pattern)) { + return { + include: GlobPattern.from(pattern.include) || undefined, + exclude: GlobPattern.from(pattern.exclude) || undefined + }; + } + + return undefined; // preserve `undefined` + + } + + function isExclusivePattern(obj: any): obj is { include: types.RelativePattern | undefined | null, exclude: types.RelativePattern | undefined | null } { + const ep = obj as { include: vscode.GlobPattern, exclude: vscode.GlobPattern }; + const include = GlobPattern.from(ep.include); + if (!(include && include instanceof types.RelativePattern || typeof include === 'string')) { + return false; + } + + const exclude = GlobPattern.from(ep.exclude); + if (!(exclude && exclude instanceof types.RelativePattern || typeof exclude === 'string')) { + return false; + } + + return true; + } + + function isRelativePattern(obj: any): obj is vscode.RelativePattern { + const rp = obj as vscode.RelativePattern; + return rp && typeof rp.base === 'string' && typeof rp.pattern === 'string'; + } +} diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 902a50f717..3049fd0883 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3,17 +3,19 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { coalesce, equals } from 'vs/base/common/arrays'; +import { coalesceInPlace, equals } from 'vs/base/common/arrays'; import { escapeCodicons } from 'vs/base/common/codicons'; import { illegalArgument } from 'vs/base/common/errors'; import { IRelativePattern } from 'vs/base/common/glob'; import { isMarkdownString } from 'vs/base/common/htmlContent'; +import { ResourceMap } from 'vs/base/common/map'; import { startsWith } from 'vs/base/common/strings'; import { isStringArray } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { FileSystemProviderErrorCode, markAsFileSystemProviderError } from 'vs/platform/files/common/files'; import { RemoteAuthorityResolverErrorCode } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { addIdToOutput, CellEditType, ICellEditOperation } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import type * as vscode from 'vscode'; function es5ClassCompat(target: Function): any { @@ -575,8 +577,14 @@ export interface IFileOperationOptions { recursive?: boolean; } +export const enum FileEditType { + File = 1, + Text = 2, + Cell = 3 +} + export interface IFileOperation { - _type: 1; + _type: FileEditType.File; from?: URI; to?: URI; options?: IFileOperationOptions; @@ -584,31 +592,61 @@ export interface IFileOperation { } export interface IFileTextEdit { - _type: 2; + _type: FileEditType.Text; uri: URI; edit: TextEdit; metadata?: vscode.WorkspaceEditEntryMetadata; } +export interface IFileCellEdit { + _type: FileEditType.Cell; + uri: URI; + edit: ICellEditOperation; + metadata?: vscode.WorkspaceEditEntryMetadata; +} + @es5ClassCompat export class WorkspaceEdit implements vscode.WorkspaceEdit { - private _edits = new Array(); + private readonly _edits = new Array(); + + + _allEntries(): ReadonlyArray { + return this._edits; + } + + // --- file renameFile(from: vscode.Uri, to: vscode.Uri, options?: { overwrite?: boolean, ignoreIfExists?: boolean; }, metadata?: vscode.WorkspaceEditEntryMetadata): void { - this._edits.push({ _type: 1, from, to, options, metadata }); + this._edits.push({ _type: FileEditType.File, from, to, options, metadata }); } createFile(uri: vscode.Uri, options?: { overwrite?: boolean, ignoreIfExists?: boolean; }, metadata?: vscode.WorkspaceEditEntryMetadata): void { - this._edits.push({ _type: 1, from: undefined, to: uri, options, metadata }); + this._edits.push({ _type: FileEditType.File, from: undefined, to: uri, options, metadata }); } deleteFile(uri: vscode.Uri, options?: { recursive?: boolean, ignoreIfNotExists?: boolean; }, metadata?: vscode.WorkspaceEditEntryMetadata): void { - this._edits.push({ _type: 1, from: uri, to: undefined, options, metadata }); + this._edits.push({ _type: FileEditType.File, from: uri, to: undefined, options, metadata }); } + // --- cell + + replaceCells(uri: URI, start: number, end: number, cells: vscode.NotebookCellData[], metadata?: vscode.WorkspaceEditEntryMetadata): void { + this._edits.push({ _type: FileEditType.Cell, metadata, uri, edit: { editType: CellEditType.Replace, index: start, count: end - start, cells: cells.map(cell => ({ ...cell, outputs: cell.outputs.map(output => addIdToOutput(output)) })) } }); + } + + replaceCellOutput(uri: URI, index: number, outputs: vscode.CellOutput[], metadata?: vscode.WorkspaceEditEntryMetadata): void { + this._edits.push({ _type: FileEditType.Cell, metadata, uri, edit: { editType: CellEditType.Output, index, outputs: outputs.map(output => addIdToOutput(output)) } }); + } + + replaceCellMetadata(uri: URI, index: number, cellMetadata: vscode.NotebookCellMetadata, metadata?: vscode.WorkspaceEditEntryMetadata): void { + this._edits.push({ _type: FileEditType.Cell, metadata, uri, edit: { editType: CellEditType.Metadata, index, metadata: cellMetadata } }); + } + + // --- text + replace(uri: URI, range: Range, newText: string, metadata?: vscode.WorkspaceEditEntryMetadata): void { - this._edits.push({ _type: 2, uri, edit: new TextEdit(range, newText), metadata }); + this._edits.push({ _type: FileEditType.Text, uri, edit: new TextEdit(range, newText), metadata }); } insert(resource: URI, position: Position, newText: string, metadata?: vscode.WorkspaceEditEntryMetadata): void { @@ -619,8 +657,10 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { this.replace(resource, range, '', metadata); } + // --- text (Maplike) + has(uri: URI): boolean { - return this._edits.some(edit => edit._type === 2 && edit.uri.toString() === uri.toString()); + return this._edits.some(edit => edit._type === FileEditType.Text && edit.uri.toString() === uri.toString()); } set(uri: URI, edits: TextEdit[]): void { @@ -628,16 +668,16 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { // remove all text edits for `uri` for (let i = 0; i < this._edits.length; i++) { const element = this._edits[i]; - if (element._type === 2 && element.uri.toString() === uri.toString()) { + if (element._type === FileEditType.Text && element.uri.toString() === uri.toString()) { this._edits[i] = undefined!; // will be coalesced down below } } - this._edits = coalesce(this._edits); + coalesceInPlace(this._edits); } else { // append edit to the end for (const edit of edits) { if (edit) { - this._edits.push({ _type: 2, uri, edit }); + this._edits.push({ _type: FileEditType.Text, uri, edit }); } } } @@ -646,7 +686,7 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { get(uri: URI): TextEdit[] { const res: TextEdit[] = []; for (let candidate of this._edits) { - if (candidate._type === 2 && candidate.uri.toString() === uri.toString()) { + if (candidate._type === FileEditType.Text && candidate.uri.toString() === uri.toString()) { res.push(candidate.edit); } } @@ -654,13 +694,13 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { } entries(): [URI, TextEdit[]][] { - const textEdits = new Map(); + const textEdits = new ResourceMap<[URI, TextEdit[]]>(); for (let candidate of this._edits) { - if (candidate._type === 2) { - let textEdit = textEdits.get(candidate.uri.toString()); + if (candidate._type === FileEditType.Text) { + let textEdit = textEdits.get(candidate.uri); if (!textEdit) { textEdit = [candidate.uri, []]; - textEdits.set(candidate.uri.toString(), textEdit); + textEdits.set(candidate.uri, textEdit); } textEdit[1].push(candidate.edit); } @@ -668,22 +708,6 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { return [...textEdits.values()]; } - allEntries(): ReadonlyArray { - return this._edits; - } - - // _allEntries(): ([URI, TextEdit] | [URI?, URI?, IFileOperationOptions?])[] { - // const res: ([URI, TextEdit] | [URI?, URI?, IFileOperationOptions?])[] = []; - // for (let edit of this._edits) { - // if (edit._type === 1) { - // res.push([edit.from, edit.to, edit.options]); - // } else { - // res.push([edit.uri, edit.edit]); - // } - // } - // return res; - // } - get size(): number { return this.entries().length; } @@ -1850,7 +1874,7 @@ export class CustomExecution implements vscode.CustomExecution { } @es5ClassCompat -export class Task implements vscode.Task2 { +export class Task implements vscode.Task { private static ExtensionCallbackType: string = 'customExecution'; private static ProcessType: string = 'process'; @@ -2756,6 +2780,18 @@ export enum NotebookRunState { Idle = 2 } +export enum NotebookCellStatusBarAlignment { + Left = 1, + Right = 2 +} + +export enum NotebookEditorRevealType { + Default = 0, + InCenter = 1, + InCenterIfOutsideViewport = 2 +} + + //#endregion //#region Timeline diff --git a/src/vs/workbench/api/common/extHostWebview.ts b/src/vs/workbench/api/common/extHostWebview.ts index f517e26f05..b004c16173 100644 --- a/src/vs/workbench/api/common/extHostWebview.ts +++ b/src/vs/workbench/api/common/extHostWebview.ts @@ -4,14 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; -import { generateUuid } from 'vs/base/common/uuid'; import * as modes from 'vs/editor/common/modes'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; -import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import { asWebviewUri, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; import type * as vscode from 'vscode'; @@ -19,7 +16,7 @@ import * as extHostProtocol from './extHost.protocol'; export class ExtHostWebview implements vscode.Webview { - readonly #handle: extHostProtocol.WebviewPanelHandle; + readonly #handle: extHostProtocol.WebviewHandle; readonly #proxy: extHostProtocol.MainThreadWebviewsShape; readonly #deprecationService: IExtHostApiDeprecationService; @@ -33,7 +30,7 @@ export class ExtHostWebview implements vscode.Webview { #hasCalledAsWebviewUri = false; constructor( - handle: extHostProtocol.WebviewPanelHandle, + handle: extHostProtocol.WebviewHandle, proxy: extHostProtocol.MainThreadWebviewsShape, options: vscode.WebviewOptions, initData: WebviewInitData, @@ -114,169 +111,11 @@ export class ExtHostWebview implements vscode.Webview { } } -type IconPath = URI | { light: URI, dark: URI }; - - -class ExtHostWebviewPanel extends Disposable implements vscode.WebviewPanel { - - readonly #handle: extHostProtocol.WebviewPanelHandle; - readonly #proxy: extHostProtocol.MainThreadWebviewsShape; - readonly #viewType: string; - - readonly #webview: ExtHostWebview; - readonly #options: vscode.WebviewPanelOptions; - - #title: string; - #iconPath?: IconPath; - #viewColumn: vscode.ViewColumn | undefined = undefined; - #visible: boolean = true; - #active: boolean = true; - #isDisposed: boolean = false; - - readonly #onDidDispose = this._register(new Emitter()); - public readonly onDidDispose = this.#onDidDispose.event; - - readonly #onDidChangeViewState = this._register(new Emitter()); - public readonly onDidChangeViewState = this.#onDidChangeViewState.event; - - constructor( - handle: extHostProtocol.WebviewPanelHandle, - proxy: extHostProtocol.MainThreadWebviewsShape, - viewType: string, - title: string, - viewColumn: vscode.ViewColumn | undefined, - editorOptions: vscode.WebviewPanelOptions, - webview: ExtHostWebview - ) { - super(); - this.#handle = handle; - this.#proxy = proxy; - this.#viewType = viewType; - this.#options = editorOptions; - this.#viewColumn = viewColumn; - this.#title = title; - this.#webview = webview; - } - - public dispose() { - if (this.#isDisposed) { - return; - } - - this.#isDisposed = true; - this.#onDidDispose.fire(); - - this.#proxy.$disposeWebview(this.#handle); - this.#webview.dispose(); - - super.dispose(); - } - - get webview() { - this.assertNotDisposed(); - return this.#webview; - } - - get viewType(): string { - this.assertNotDisposed(); - return this.#viewType; - } - - get title(): string { - this.assertNotDisposed(); - return this.#title; - } - - set title(value: string) { - this.assertNotDisposed(); - if (this.#title !== value) { - this.#title = value; - this.#proxy.$setTitle(this.#handle, value); - } - } - - get iconPath(): IconPath | undefined { - this.assertNotDisposed(); - return this.#iconPath; - } - - set iconPath(value: IconPath | undefined) { - this.assertNotDisposed(); - if (this.#iconPath !== value) { - this.#iconPath = value; - - this.#proxy.$setIconPath(this.#handle, URI.isUri(value) ? { light: value, dark: value } : value); - } - } - - get options() { - return this.#options; - } - - get viewColumn(): vscode.ViewColumn | undefined { - this.assertNotDisposed(); - if (typeof this.#viewColumn === 'number' && this.#viewColumn < 0) { - // We are using a symbolic view column - // Return undefined instead to indicate that the real view column is currently unknown but will be resolved. - return undefined; - } - return this.#viewColumn; - } - - public get active(): boolean { - this.assertNotDisposed(); - return this.#active; - } - - public get visible(): boolean { - this.assertNotDisposed(); - return this.#visible; - } - - _updateViewState(newState: { active: boolean; visible: boolean; viewColumn: vscode.ViewColumn; }) { - if (this.#isDisposed) { - return; - } - - if (this.active !== newState.active || this.visible !== newState.visible || this.viewColumn !== newState.viewColumn) { - this.#active = newState.active; - this.#visible = newState.visible; - this.#viewColumn = newState.viewColumn; - this.#onDidChangeViewState.fire({ webviewPanel: this }); - } - } - - public postMessage(message: any): Promise { - this.assertNotDisposed(); - return this.#proxy.$postMessage(this.#handle, message); - } - - public reveal(viewColumn?: vscode.ViewColumn, preserveFocus?: boolean): void { - this.assertNotDisposed(); - this.#proxy.$reveal(this.#handle, { - viewColumn: viewColumn ? typeConverters.ViewColumn.from(viewColumn) : undefined, - preserveFocus: !!preserveFocus - }); - } - - private assertNotDisposed() { - if (this.#isDisposed) { - throw new Error('Webview is disposed'); - } - } -} - export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape { - private static newHandle(): extHostProtocol.WebviewPanelHandle { - return generateUuid(); - } - - private readonly _proxy: extHostProtocol.MainThreadWebviewsShape; - - private readonly _webviews = new Map(); - private readonly _webviewPanels = new Map(); + private readonly _webviewProxy: extHostProtocol.MainThreadWebviewsShape; + private readonly _webviews = new Map(); constructor( mainContext: extHostProtocol.IMainContext, @@ -285,33 +124,11 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape { private readonly _logService: ILogService, private readonly _deprecationService: IExtHostApiDeprecationService, ) { - this._proxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadWebviews); - } - - public createWebviewPanel( - extension: IExtensionDescription, - viewType: string, - title: string, - showOptions: vscode.ViewColumn | { viewColumn: vscode.ViewColumn, preserveFocus?: boolean }, - options: (vscode.WebviewPanelOptions & vscode.WebviewOptions) = {}, - ): vscode.WebviewPanel { - const viewColumn = typeof showOptions === 'object' ? showOptions.viewColumn : showOptions; - const webviewShowOptions = { - viewColumn: typeConverters.ViewColumn.from(viewColumn), - preserveFocus: typeof showOptions === 'object' && !!showOptions.preserveFocus - }; - - const handle = ExtHostWebviews.newHandle(); - this._proxy.$createWebviewPanel(toExtensionData(extension), handle, viewType, title, webviewShowOptions, convertWebviewOptions(extension, this.workspace, options)); - - const webview = this.createNewWebview(handle, options, extension); - const panel = this.createNewWebviewPanel(handle, viewType, title, viewColumn, options, webview); - - return panel; + this._webviewProxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadWebviews); } public $onMessage( - handle: extHostProtocol.WebviewPanelHandle, + handle: extHostProtocol.WebviewHandle, message: any ): void { const webview = this.getWebview(handle); @@ -321,61 +138,14 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape { } public $onMissingCsp( - _handle: extHostProtocol.WebviewPanelHandle, + _handle: extHostProtocol.WebviewHandle, extensionId: string ): void { this._logService.warn(`${extensionId} created a webview without a content security policy: https://aka.ms/vscode-webview-missing-csp`); } - public $onDidChangeWebviewPanelViewStates(newStates: extHostProtocol.WebviewPanelViewStateData): void { - const handles = Object.keys(newStates); - // Notify webviews of state changes in the following order: - // - Non-visible - // - Visible - // - Active - handles.sort((a, b) => { - const stateA = newStates[a]; - const stateB = newStates[b]; - if (stateA.active) { - return 1; - } - if (stateB.active) { - return -1; - } - return (+stateA.visible) - (+stateB.visible); - }); - - for (const handle of handles) { - const panel = this.getWebviewPanel(handle); - if (!panel) { - continue; - } - - const newState = newStates[handle]; - panel._updateViewState({ - active: newState.active, - visible: newState.visible, - viewColumn: typeConverters.ViewColumn.to(newState.position), - }); - } - } - - async $onDidDisposeWebviewPanel(handle: extHostProtocol.WebviewPanelHandle): Promise { - const panel = this.getWebviewPanel(handle); - panel?.dispose(); - - this._webviewPanels.delete(handle); - this._webviews.delete(handle); - } - - public createNewWebviewPanel(webviewHandle: string, viewType: string, title: string, position: number, options: modes.IWebviewOptions & modes.IWebviewPanelOptions, webview: ExtHostWebview) { - const panel = new ExtHostWebviewPanel(webviewHandle, this._proxy, viewType, title, typeof position === 'number' && position >= 0 ? typeConverters.ViewColumn.to(position) : undefined, options, webview); - this._webviewPanels.set(webviewHandle, panel); - return panel; - } - public createNewWebview(handle: string, options: modes.IWebviewOptions & modes.IWebviewPanelOptions, extension: IExtensionDescription): ExtHostWebview { - const webview = new ExtHostWebview(handle, this._proxy, reviveOptions(options), this.initData, this.workspace, extension, this._deprecationService); + const webview = new ExtHostWebview(handle, this._webviewProxy, reviveOptions(options), this.initData, this.workspace, extension, this._deprecationService); this._webviews.set(handle, webview); webview._onDidDispose(() => { this._webviews.delete(handle); }); @@ -383,12 +153,12 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape { return webview; } - private getWebview(handle: extHostProtocol.WebviewPanelHandle): ExtHostWebview | undefined { - return this._webviews.get(handle); + public deleteWebview(handle: string) { + this._webviews.delete(handle); } - public getWebviewPanel(handle: extHostProtocol.WebviewPanelHandle): ExtHostWebviewPanel | undefined { - return this._webviewPanels.get(handle); + private getWebview(handle: extHostProtocol.WebviewHandle): ExtHostWebview | undefined { + return this._webviews.get(handle); } } @@ -396,7 +166,7 @@ export function toExtensionData(extension: IExtensionDescription): extHostProtoc return { id: extension.identifier, location: extension.extensionLocation }; } -function convertWebviewOptions( +export function convertWebviewOptions( extension: IExtensionDescription, workspace: IExtHostWorkspace | undefined, options: vscode.WebviewPanelOptions & vscode.WebviewOptions, diff --git a/src/vs/workbench/api/common/extHostWebviewPanels.ts b/src/vs/workbench/api/common/extHostWebviewPanels.ts new file mode 100644 index 0000000000..a9973f82de --- /dev/null +++ b/src/vs/workbench/api/common/extHostWebviewPanels.ts @@ -0,0 +1,299 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { generateUuid } from 'vs/base/common/uuid'; +import * as modes from 'vs/editor/common/modes'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; +import { convertWebviewOptions, ExtHostWebview, ExtHostWebviews, toExtensionData } from 'vs/workbench/api/common/extHostWebview'; +import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; +import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor'; +import type * as vscode from 'vscode'; +import * as extHostProtocol from './extHost.protocol'; +import * as extHostTypes from './extHostTypes'; + + +type IconPath = URI | { light: URI, dark: URI }; + +class ExtHostWebviewPanel extends Disposable implements vscode.WebviewPanel { + + readonly #handle: extHostProtocol.WebviewHandle; + readonly #proxy: extHostProtocol.MainThreadWebviewPanelsShape; + readonly #viewType: string; + + readonly #webview: ExtHostWebview; + readonly #options: vscode.WebviewPanelOptions; + + #title: string; + #iconPath?: IconPath; + #viewColumn: vscode.ViewColumn | undefined = undefined; + #visible: boolean = true; + #active: boolean = true; + #isDisposed: boolean = false; + + readonly #onDidDispose = this._register(new Emitter()); + public readonly onDidDispose = this.#onDidDispose.event; + + readonly #onDidChangeViewState = this._register(new Emitter()); + public readonly onDidChangeViewState = this.#onDidChangeViewState.event; + + constructor( + handle: extHostProtocol.WebviewHandle, + proxy: extHostProtocol.MainThreadWebviewPanelsShape, + viewType: string, + title: string, + viewColumn: vscode.ViewColumn | undefined, + editorOptions: vscode.WebviewPanelOptions, + webview: ExtHostWebview + ) { + super(); + this.#handle = handle; + this.#proxy = proxy; + this.#viewType = viewType; + this.#options = editorOptions; + this.#viewColumn = viewColumn; + this.#title = title; + this.#webview = webview; + } + + public dispose() { + if (this.#isDisposed) { + return; + } + + this.#isDisposed = true; + this.#onDidDispose.fire(); + + this.#proxy.$disposeWebview(this.#handle); + this.#webview.dispose(); + + super.dispose(); + } + + get webview() { + this.assertNotDisposed(); + return this.#webview; + } + + get viewType(): string { + this.assertNotDisposed(); + return this.#viewType; + } + + get title(): string { + this.assertNotDisposed(); + return this.#title; + } + + set title(value: string) { + this.assertNotDisposed(); + if (this.#title !== value) { + this.#title = value; + this.#proxy.$setTitle(this.#handle, value); + } + } + + get iconPath(): IconPath | undefined { + this.assertNotDisposed(); + return this.#iconPath; + } + + set iconPath(value: IconPath | undefined) { + this.assertNotDisposed(); + if (this.#iconPath !== value) { + this.#iconPath = value; + + this.#proxy.$setIconPath(this.#handle, URI.isUri(value) ? { light: value, dark: value } : value); + } + } + + get options() { + return this.#options; + } + + get viewColumn(): vscode.ViewColumn | undefined { + this.assertNotDisposed(); + if (typeof this.#viewColumn === 'number' && this.#viewColumn < 0) { + // We are using a symbolic view column + // Return undefined instead to indicate that the real view column is currently unknown but will be resolved. + return undefined; + } + return this.#viewColumn; + } + + public get active(): boolean { + this.assertNotDisposed(); + return this.#active; + } + + public get visible(): boolean { + this.assertNotDisposed(); + return this.#visible; + } + + _updateViewState(newState: { active: boolean; visible: boolean; viewColumn: vscode.ViewColumn; }) { + if (this.#isDisposed) { + return; + } + + if (this.active !== newState.active || this.visible !== newState.visible || this.viewColumn !== newState.viewColumn) { + this.#active = newState.active; + this.#visible = newState.visible; + this.#viewColumn = newState.viewColumn; + this.#onDidChangeViewState.fire({ webviewPanel: this }); + } + } + + public reveal(viewColumn?: vscode.ViewColumn, preserveFocus?: boolean): void { + this.assertNotDisposed(); + this.#proxy.$reveal(this.#handle, { + viewColumn: viewColumn ? typeConverters.ViewColumn.from(viewColumn) : undefined, + preserveFocus: !!preserveFocus + }); + } + + private assertNotDisposed() { + if (this.#isDisposed) { + throw new Error('Webview is disposed'); + } + } +} + +export class ExtHostWebviewPanels implements extHostProtocol.ExtHostWebviewPanelsShape { + + private static newHandle(): extHostProtocol.WebviewHandle { + return generateUuid(); + } + + private readonly _proxy: extHostProtocol.MainThreadWebviewPanelsShape; + + private readonly _webviewPanels = new Map(); + + private readonly _serializers = new Map(); + + constructor( + mainContext: extHostProtocol.IMainContext, + private readonly webviews: ExtHostWebviews, + private readonly workspace: IExtHostWorkspace | undefined, + ) { + this._proxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadWebviewPanels); + } + + public createWebviewPanel( + extension: IExtensionDescription, + viewType: string, + title: string, + showOptions: vscode.ViewColumn | { viewColumn: vscode.ViewColumn, preserveFocus?: boolean }, + options: (vscode.WebviewPanelOptions & vscode.WebviewOptions) = {}, + ): vscode.WebviewPanel { + const viewColumn = typeof showOptions === 'object' ? showOptions.viewColumn : showOptions; + const webviewShowOptions = { + viewColumn: typeConverters.ViewColumn.from(viewColumn), + preserveFocus: typeof showOptions === 'object' && !!showOptions.preserveFocus + }; + + const handle = ExtHostWebviewPanels.newHandle(); + this._proxy.$createWebviewPanel(toExtensionData(extension), handle, viewType, title, webviewShowOptions, convertWebviewOptions(extension, this.workspace, options)); + + const webview = this.webviews.createNewWebview(handle, options, extension); + const panel = this.createNewWebviewPanel(handle, viewType, title, viewColumn, options, webview); + + return panel; + } + + public $onDidChangeWebviewPanelViewStates(newStates: extHostProtocol.WebviewPanelViewStateData): void { + const handles = Object.keys(newStates); + // Notify webviews of state changes in the following order: + // - Non-visible + // - Visible + // - Active + handles.sort((a, b) => { + const stateA = newStates[a]; + const stateB = newStates[b]; + if (stateA.active) { + return 1; + } + if (stateB.active) { + return -1; + } + return (+stateA.visible) - (+stateB.visible); + }); + + for (const handle of handles) { + const panel = this.getWebviewPanel(handle); + if (!panel) { + continue; + } + + const newState = newStates[handle]; + panel._updateViewState({ + active: newState.active, + visible: newState.visible, + viewColumn: typeConverters.ViewColumn.to(newState.position), + }); + } + } + + async $onDidDisposeWebviewPanel(handle: extHostProtocol.WebviewHandle): Promise { + const panel = this.getWebviewPanel(handle); + panel?.dispose(); + + this._webviewPanels.delete(handle); + this.webviews.deleteWebview(handle); + } + + public registerWebviewPanelSerializer( + extension: IExtensionDescription, + viewType: string, + serializer: vscode.WebviewPanelSerializer + ): vscode.Disposable { + if (this._serializers.has(viewType)) { + throw new Error(`Serializer for '${viewType}' already registered`); + } + + this._serializers.set(viewType, { serializer, extension }); + this._proxy.$registerSerializer(viewType); + + return new extHostTypes.Disposable(() => { + this._serializers.delete(viewType); + this._proxy.$unregisterSerializer(viewType); + }); + } + + async $deserializeWebviewPanel( + webviewHandle: extHostProtocol.WebviewHandle, + viewType: string, + title: string, + state: any, + position: EditorViewColumn, + options: modes.IWebviewOptions & modes.IWebviewPanelOptions + ): Promise { + const entry = this._serializers.get(viewType); + if (!entry) { + throw new Error(`No serializer found for '${viewType}'`); + } + const { serializer, extension } = entry; + + const webview = this.webviews.createNewWebview(webviewHandle, options, extension); + const revivedPanel = this.createNewWebviewPanel(webviewHandle, viewType, title, position, options, webview); + await serializer.deserializeWebviewPanel(revivedPanel, state); + } + + public createNewWebviewPanel(webviewHandle: string, viewType: string, title: string, position: number, options: modes.IWebviewOptions & modes.IWebviewPanelOptions, webview: ExtHostWebview) { + const panel = new ExtHostWebviewPanel(webviewHandle, this._proxy, viewType, title, typeof position === 'number' && position >= 0 ? typeConverters.ViewColumn.to(position) : undefined, options, webview); + this._webviewPanels.set(webviewHandle, panel); + return panel; + } + + public getWebviewPanel(handle: extHostProtocol.WebviewHandle): ExtHostWebviewPanel | undefined { + return this._webviewPanels.get(handle); + } +} diff --git a/src/vs/workbench/api/common/extHostWebviewSerializer.ts b/src/vs/workbench/api/common/extHostWebviewSerializer.ts deleted file mode 100644 index 27d5d6fa1c..0000000000 --- a/src/vs/workbench/api/common/extHostWebviewSerializer.ts +++ /dev/null @@ -1,66 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as modes from 'vs/editor/common/modes'; -import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { ExtHostWebviews } from 'vs/workbench/api/common/extHostWebview'; -import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor'; -import type * as vscode from 'vscode'; -import * as extHostProtocol from './extHost.protocol'; -import * as extHostTypes from './extHostTypes'; - -export class ExtHostWebviewSerializer implements extHostProtocol.ExtHostWebviewSerializerShape { - - private readonly _proxy: extHostProtocol.MainThreadWebviewsShape; - - private readonly _serializers = new Map(); - - constructor( - mainContext: extHostProtocol.IMainContext, - private readonly _webviewService: ExtHostWebviews, - ) { - this._proxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadWebviews); - } - - public registerWebviewPanelSerializer( - extension: IExtensionDescription, - viewType: string, - serializer: vscode.WebviewPanelSerializer - ): vscode.Disposable { - if (this._serializers.has(viewType)) { - throw new Error(`Serializer for '${viewType}' already registered`); - } - - this._serializers.set(viewType, { serializer, extension }); - this._proxy.$registerSerializer(viewType); - - return new extHostTypes.Disposable(() => { - this._serializers.delete(viewType); - this._proxy.$unregisterSerializer(viewType); - }); - } - - async $deserializeWebviewPanel( - webviewHandle: extHostProtocol.WebviewPanelHandle, - viewType: string, - title: string, - state: any, - position: EditorViewColumn, - options: modes.IWebviewOptions & modes.IWebviewPanelOptions - ): Promise { - const entry = this._serializers.get(viewType); - if (!entry) { - throw new Error(`No serializer found for '${viewType}'`); - } - const { serializer, extension } = entry; - - const webview = this._webviewService.createNewWebview(webviewHandle, options, extension); - const revivedPanel = this._webviewService.createNewWebviewPanel(webviewHandle, viewType, title, position, options, webview); - await serializer.deserializeWebviewPanel(revivedPanel, state); - } -} diff --git a/src/vs/workbench/api/common/extHostWebviewView.ts b/src/vs/workbench/api/common/extHostWebviewView.ts index 75002a755a..b167784c0a 100644 --- a/src/vs/workbench/api/common/extHostWebviewView.ts +++ b/src/vs/workbench/api/common/extHostWebviewView.ts @@ -14,8 +14,8 @@ import * as extHostTypes from './extHostTypes'; class ExtHostWebviewView extends Disposable implements vscode.WebviewView { - readonly #handle: extHostProtocol.WebviewPanelHandle; - readonly #proxy: extHostProtocol.MainThreadWebviewsShape; + readonly #handle: extHostProtocol.WebviewHandle; + readonly #proxy: extHostProtocol.MainThreadWebviewViewsShape; readonly #viewType: string; readonly #webview: ExtHostWebview; @@ -25,8 +25,8 @@ class ExtHostWebviewView extends Disposable implements vscode.WebviewView { #title: string | undefined; constructor( - handle: extHostProtocol.WebviewPanelHandle, - proxy: extHostProtocol.MainThreadWebviewsShape, + handle: extHostProtocol.WebviewHandle, + proxy: extHostProtocol.MainThreadWebviewViewsShape, viewType: string, webview: ExtHostWebview, isVisible: boolean, @@ -94,20 +94,20 @@ class ExtHostWebviewView extends Disposable implements vscode.WebviewView { export class ExtHostWebviewViews implements extHostProtocol.ExtHostWebviewViewsShape { - private readonly _proxy: extHostProtocol.MainThreadWebviewsShape; + private readonly _proxy: extHostProtocol.MainThreadWebviewViewsShape; private readonly _viewProviders = new Map(); - private readonly _webviewViews = new Map(); + private readonly _webviewViews = new Map(); constructor( mainContext: extHostProtocol.IMainContext, private readonly _extHostWebview: ExtHostWebviews, ) { - this._proxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadWebviews); + this._proxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadWebviewViews); } public registerWebviewViewProvider( diff --git a/src/vs/workbench/api/common/extHostWindow.ts b/src/vs/workbench/api/common/extHostWindow.ts index 4d84dd83e1..10dfdcf98e 100644 --- a/src/vs/workbench/api/common/extHostWindow.ts +++ b/src/vs/workbench/api/common/extHostWindow.ts @@ -12,7 +12,7 @@ import { isFalsyOrWhitespace } from 'vs/base/common/strings'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; -export class ExtHostWindow implements IExtHostWindow { +export class ExtHostWindow implements ExtHostWindowShape { private static InitialState: WindowState = { focused: true diff --git a/src/vs/workbench/api/common/menusExtensionPoint.ts b/src/vs/workbench/api/common/menusExtensionPoint.ts index d26e7dded5..35df550543 100644 --- a/src/vs/workbench/api/common/menusExtensionPoint.ts +++ b/src/vs/workbench/api/common/menusExtensionPoint.ts @@ -61,7 +61,12 @@ const apiMenus: IAPIMenu[] = [ { key: 'debug/callstack/context', id: MenuId.DebugCallStackContext, - description: localize('menus.debugCallstackContext', "The debug callstack context menu") + description: localize('menus.debugCallstackContext', "The debug callstack view context menu") + }, + { + key: 'debug/variables/context', + id: MenuId.DebugVariablesContext, + description: localize('menus.debugVariablesContext', "The debug variables view context menu") }, { key: 'debug/toolBar', diff --git a/src/vs/workbench/api/node/extHostTask.ts b/src/vs/workbench/api/node/extHostTask.ts index 615089cb69..18098699dd 100644 --- a/src/vs/workbench/api/node/extHostTask.ts +++ b/src/vs/workbench/api/node/extHostTask.ts @@ -97,7 +97,7 @@ export class ExtHostTask extends ExtHostTaskBase { // The ID is calculated on the main thread task side, so, let's call into it here. // We need the task id's pre-computed for custom task executions because when OnDidStartTask // is invoked, we have to be able to map it back to our data. - taskIdPromises.push(this.addCustomExecution(taskDTO, task, true)); + taskIdPromises.push(this.addCustomExecution(taskDTO, task, true)); } } } diff --git a/src/vs/workbench/api/node/extHostTerminalService.ts b/src/vs/workbench/api/node/extHostTerminalService.ts index f2ec469c75..39ed9ebbd0 100644 --- a/src/vs/workbench/api/node/extHostTerminalService.ts +++ b/src/vs/workbench/api/node/extHostTerminalService.ts @@ -39,7 +39,7 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { @IExtHostDocumentsAndEditors private _extHostDocumentsAndEditors: ExtHostDocumentsAndEditors, @ILogService private _logService: ILogService ) { - super(extHostRpc); + super(true, extHostRpc); this._updateLastActiveWorkspace(); this._updateVariableResolver(); this._registerListeners(); diff --git a/src/vs/workbench/browser/actions/developerActions.ts b/src/vs/workbench/browser/actions/developerActions.ts index baf6f7d1fc..7f08978583 100644 --- a/src/vs/workbench/browser/actions/developerActions.ts +++ b/src/vs/workbench/browser/actions/developerActions.ts @@ -8,6 +8,7 @@ import 'vs/css!./media/actions'; import * as nls from 'vs/nls'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { domEvent } from 'vs/base/browser/event'; +import { Color } from 'vs/base/common/color'; import { Event } from 'vs/base/common/event'; import { IDisposable, toDisposable, dispose, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { getDomNodePagePosition, createStyleSheet, createCSSRule, append, $ } from 'vs/base/browser/dom'; @@ -123,14 +124,29 @@ class ToggleScreencastModeAction extends Action2 { const onMouseUp = domEvent(container, 'mouseup', true); const onMouseMove = domEvent(container, 'mousemove', true); + const updateMouseIndicatorColor = () => { + mouseMarker.style.borderColor = Color.fromHex(configurationService.getValue('screencastMode.mouseIndicatorColor')).toString(); + }; + + let mouseIndicatorSize: number; + const updateMouseIndicatorSize = () => { + mouseIndicatorSize = clamp(configurationService.getValue('screencastMode.mouseIndicatorSize') || 20, 20, 100); + + mouseMarker.style.height = `${mouseIndicatorSize}px`; + mouseMarker.style.width = `${mouseIndicatorSize}px`; + }; + + updateMouseIndicatorColor(); + updateMouseIndicatorSize(); + disposables.add(onMouseDown(e => { - mouseMarker.style.top = `${e.clientY - 10}px`; - mouseMarker.style.left = `${e.clientX - 10}px`; + mouseMarker.style.top = `${e.clientY - mouseIndicatorSize / 2}px`; + mouseMarker.style.left = `${e.clientX - mouseIndicatorSize / 2}px`; mouseMarker.style.display = 'block'; const mouseMoveListener = onMouseMove(e => { - mouseMarker.style.top = `${e.clientY - 10}px`; - mouseMarker.style.left = `${e.clientX - 10}px`; + mouseMarker.style.top = `${e.clientY - mouseIndicatorSize / 2}px`; + mouseMarker.style.left = `${e.clientX - mouseIndicatorSize / 2}px`; }); Event.once(onMouseUp)(() => { @@ -150,8 +166,14 @@ class ToggleScreencastModeAction extends Action2 { keyboardMarker.style.bottom = `${clamp(configurationService.getValue('screencastMode.verticalOffset') || 0, 0, 90)}%`; }; + let keyboardMarkerTimeout: number; + const updateKeyboardMarkerTimeout = () => { + keyboardMarkerTimeout = clamp(configurationService.getValue('screencastMode.keyboardOverlayTimeout') || 800, 500, 5000); + }; + updateKeyboardFontSize(); updateKeyboardMarker(); + updateKeyboardMarkerTimeout(); disposables.add(configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('screencastMode.verticalOffset')) { @@ -161,6 +183,18 @@ class ToggleScreencastModeAction extends Action2 { if (e.affectsConfiguration('screencastMode.fontSize')) { updateKeyboardFontSize(); } + + if (e.affectsConfiguration('screencastMode.keyboardOverlayTimeout')) { + updateKeyboardMarkerTimeout(); + } + + if (e.affectsConfiguration('screencastMode.mouseIndicatorColor')) { + updateMouseIndicatorColor(); + } + + if (e.affectsConfiguration('screencastMode.mouseIndicatorSize')) { + updateMouseIndicatorSize(); + } })); const onKeyDown = domEvent(window, 'keydown', true); @@ -190,7 +224,7 @@ class ToggleScreencastModeAction extends Action2 { append(keyboardMarker, key); } - const promise = timeout(800); + const promise = timeout(keyboardMarkerTimeout); keyboardTimeout = toDisposable(() => promise.cancel()); promise.then(() => { @@ -276,8 +310,30 @@ configurationRegistry.registerConfiguration({ }, 'screencastMode.onlyKeyboardShortcuts': { type: 'boolean', - description: nls.localize('screencastMode.onlyKeyboardShortcuts', "Only show keyboard shortcuts in Screencast Mode."), + description: nls.localize('screencastMode.onlyKeyboardShortcuts', "Only show keyboard shortcuts in screencast mode."), default: false - } + }, + 'screencastMode.keyboardOverlayTimeout': { + type: 'number', + default: 800, + minimum: 500, + maximum: 5000, + description: nls.localize('screencastMode.keyboardOverlayTimeout', "Controls how long (in milliseconds) the keyboard overlay is shown in screencast mode.") + }, + 'screencastMode.mouseIndicatorColor': { + type: 'string', + format: 'color-hex', + default: '#FF0000', + minLength: 4, + maxLength: 9, + description: nls.localize('screencastMode.mouseIndicatorColor', "Controls the color in hex (#RGB, #RGBA, #RRGGBB or #RRGGBBAA) of the mouse indicator in screencast mode.") + }, + 'screencastMode.mouseIndicatorSize': { + type: 'number', + default: 20, + minimum: 20, + maximum: 100, + description: nls.localize('screencastMode.mouseIndicatorSize', "Controls the size (in pixels) of the mouse indicator in screencast mode.") + }, } }); diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index c87a9903c1..86b6ade822 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -763,7 +763,6 @@ export class MoveFocusedViewAction extends Action { registry.registerWorkbenchAction(SyncActionDescriptor.from(MoveFocusedViewAction), 'View: Move Focused View', viewCategory.value, FocusedViewContext.notEqualsTo('')); - // --- Reset View Location with Command export class ResetFocusedViewLocationAction extends Action { static readonly ID = 'workbench.action.resetFocusedViewLocation'; diff --git a/src/vs/workbench/browser/actions/media/actions.css b/src/vs/workbench/browser/actions/media/actions.css index ba24088cc3..b502ad162b 100644 --- a/src/vs/workbench/browser/actions/media/actions.css +++ b/src/vs/workbench/browser/actions/media/actions.css @@ -9,12 +9,9 @@ .monaco-workbench .screencast-mouse { position: absolute; - border: 2px solid red; - border-radius: 20px; - width: 20px; - height: 20px; - top: 0; - left: 0; + border-width: 2px; + border-style: solid; + border-radius: 50%; z-index: 100000; content: ' '; pointer-events: none; diff --git a/src/vs/workbench/browser/editor.ts b/src/vs/workbench/browser/editor.ts index 3cea564a0b..f81d705287 100644 --- a/src/vs/workbench/browser/editor.ts +++ b/src/vs/workbench/browser/editor.ts @@ -6,17 +6,18 @@ import { EditorInput } from 'vs/workbench/common/editor'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { Registry } from 'vs/platform/registry/common/platform'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { IConstructorSignature0, IInstantiationService, BrandedService } from 'vs/platform/instantiation/common/instantiation'; import { insert } from 'vs/base/common/arrays'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; export interface IEditorDescriptor { - instantiate(instantiationService: IInstantiationService): BaseEditor; getId(): string; getName(): string; + instantiate(instantiationService: IInstantiationService): EditorPane; + describes(obj: unknown): boolean; } @@ -56,20 +57,20 @@ export interface IEditorRegistry { export class EditorDescriptor implements IEditorDescriptor { static create( - ctor: { new(...services: Services): BaseEditor }, + ctor: { new(...services: Services): EditorPane }, id: string, name: string ): EditorDescriptor { - return new EditorDescriptor(ctor as IConstructorSignature0, id, name); + return new EditorDescriptor(ctor as IConstructorSignature0, id, name); } constructor( - private readonly ctor: IConstructorSignature0, + private readonly ctor: IConstructorSignature0, private readonly id: string, private readonly name: string ) { } - instantiate(instantiationService: IInstantiationService): BaseEditor { + instantiate(instantiationService: IInstantiationService): EditorPane { return instantiationService.createInstance(this.ctor); } @@ -82,7 +83,7 @@ export class EditorDescriptor implements IEditorDescriptor { } describes(obj: unknown): boolean { - return obj instanceof BaseEditor && obj.getId() === this.id; + return obj instanceof EditorPane && obj.getId() === this.id; } } diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index c01424cf2b..8fcfe9aca3 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -1543,6 +1543,11 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi setPanelHidden(hidden: boolean, skipLayout?: boolean): void { this.state.panel.hidden = hidden; + // Return if not initialized fully #105480 + if (!this.workbenchGrid) { + return; + } + // Adjust CSS if (hidden) { addClass(this.container, Classes.PANEL_HIDDEN); @@ -1615,6 +1620,10 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi return this.state.windowBorder; } + getWindowBorderWidth(): number { + return this.state.windowBorder ? 2 : 0; + } + getWindowBorderRadius(): string | undefined { return this.state.windowBorder && isMacintosh ? '5px' : undefined; } @@ -1636,7 +1645,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.state.menuBar.visibility = visibility; // Layout - if (!skipLayout) { + if (!skipLayout && this.workbenchGrid) { this.workbenchGrid.setViewVisible(this.titleBarPartView, this.isVisible(Parts.TITLEBAR_PART)); } } diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts index 23202f79c2..9d047a0f96 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts @@ -161,21 +161,27 @@ export class AccountsActionViewItem extends ActivityActionViewItem { const otherCommands = accountsMenu.getActions(); const providers = this.authenticationService.getProviderIds(); const allSessions = providers.map(async id => { - const sessions = await this.authenticationService.getSessions(id); + try { + const sessions = await this.authenticationService.getSessions(id); - const groupedSessions: { [label: string]: AuthenticationSession[] } = {}; - sessions.forEach(session => { - if (groupedSessions[session.account.label]) { - groupedSessions[session.account.label].push(session); - } else { - groupedSessions[session.account.label] = [session]; - } - }); + const groupedSessions: { [label: string]: AuthenticationSession[] } = {}; + sessions.forEach(session => { + if (groupedSessions[session.account.label]) { + groupedSessions[session.account.label].push(session); + } else { + groupedSessions[session.account.label] = [session]; + } + }); - return { - providerId: id, - sessions: groupedSessions - }; + return { + providerId: id, + sessions: groupedSessions + }; + } catch { + return { + providerId: id + }; + } }); const result = await Promise.all(allSessions); @@ -183,20 +189,26 @@ export class AccountsActionViewItem extends ActivityActionViewItem { const authenticationSession = this.environmentService.options?.credentialsProvider ? await getCurrentAuthenticationSessionInfo(this.environmentService, this.productService) : undefined; result.forEach(sessionInfo => { const providerDisplayName = this.authenticationService.getLabel(sessionInfo.providerId); - Object.keys(sessionInfo.sessions).forEach(accountName => { - const hasEmbedderAccountSession = sessionInfo.sessions[accountName].some(session => session.id === (authenticationSession?.id || this.environmentService.options?.authenticationSessionId)); - const manageExtensionsAction = new Action(`configureSessions${accountName}`, nls.localize('manageTrustedExtensions', "Manage Trusted Extensions"), '', true, _ => { - return this.authenticationService.manageTrustedExtensionsForAccount(sessionInfo.providerId, accountName); - }); - const signOutAction = new Action('signOut', nls.localize('signOut', "Sign Out"), '', true, _ => { - return this.authenticationService.signOutOfAccount(sessionInfo.providerId, accountName); - }); - const actions = hasEmbedderAccountSession ? [manageExtensionsAction] : [manageExtensionsAction, signOutAction]; + if (sessionInfo.sessions) { + Object.keys(sessionInfo.sessions).forEach(accountName => { + const hasEmbedderAccountSession = sessionInfo.sessions[accountName].some(session => session.id === (authenticationSession?.id || this.environmentService.options?.authenticationSessionId)); + const manageExtensionsAction = new Action(`configureSessions${accountName}`, nls.localize('manageTrustedExtensions', "Manage Trusted Extensions"), '', true, _ => { + return this.authenticationService.manageTrustedExtensionsForAccount(sessionInfo.providerId, accountName); + }); + const signOutAction = new Action('signOut', nls.localize('signOut', "Sign Out"), '', true, _ => { + return this.authenticationService.signOutOfAccount(sessionInfo.providerId, accountName); + }); - const menu = new SubmenuAction('activitybar.submenu', `${accountName} (${providerDisplayName})`, actions); + const actions = hasEmbedderAccountSession ? [manageExtensionsAction] : [manageExtensionsAction, signOutAction]; + + const menu = new SubmenuAction('activitybar.submenu', `${accountName} (${providerDisplayName})`, actions); + menus.push(menu); + }); + } else { + const menu = new Action('providerUnavailable', nls.localize('authProviderUnavailable', '{0} is currently unavailable', providerDisplayName)); menus.push(menu); - }); + } }); if (menus.length && otherCommands.length) { diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 68ab3a0042..10d41a7923 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -131,6 +131,8 @@ export class ActivitybarPart extends Part implements IActivityBarService { storageKeysSyncRegistryService.registerStorageKey({ key: ActivitybarPart.PINNED_VIEW_CONTAINERS, version: 1 }); storageKeysSyncRegistryService.registerStorageKey({ key: ActivitybarPart.HOME_BAR_VISIBILITY_PREFERENCE, version: 1 }); + storageKeysSyncRegistryService.registerStorageKey({ key: ACCOUNTS_VISIBILITY_PREFERENCE_KEY, version: 1 }); + this.migrateFromOldCachedViewContainersValue(); for (const cachedViewContainer of this.cachedViewContainers) { diff --git a/src/vs/workbench/browser/parts/editor/binaryEditor.ts b/src/vs/workbench/browser/parts/editor/binaryEditor.ts index 9d39bf3b10..d59f423958 100644 --- a/src/vs/workbench/browser/parts/editor/binaryEditor.ts +++ b/src/vs/workbench/browser/parts/editor/binaryEditor.ts @@ -6,8 +6,8 @@ import 'vs/css!./media/binaryeditor'; import * as nls from 'vs/nls'; import { Emitter } from 'vs/base/common/event'; -import { EditorInput, EditorOptions } from 'vs/workbench/common/editor'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorInput, EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; @@ -30,7 +30,7 @@ export interface IOpenCallbacks { /* * This class is only intended to be subclassed and not instantiated. */ -export abstract class BaseBinaryResourceEditor extends BaseEditor { +export abstract class BaseBinaryResourceEditor extends EditorPane { private readonly _onMetadataChanged = this._register(new Emitter()); readonly onMetadataChanged = this._onMetadataChanged.event; @@ -74,8 +74,8 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor { parent.appendChild(this.scrollbar.getDomNode()); } - async setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { - await super.setInput(input, options, token); + async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + await super.setInput(input, options, context, token); const model = await input.resolve(); // Check for cancellation diff --git a/src/vs/workbench/browser/parts/editor/editorControl.ts b/src/vs/workbench/browser/parts/editor/editorControl.ts index a1e0ec755b..25348f1044 100644 --- a/src/vs/workbench/browser/parts/editor/editorControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorControl.ts @@ -4,12 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { EditorInput, EditorOptions, IVisibleEditorPane } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, IEditorOpenContext, IVisibleEditorPane } from 'vs/workbench/common/editor'; import { Dimension, show, hide, addClass } from 'vs/base/browser/dom'; import { Registry } from 'vs/platform/registry/common/platform'; import { IEditorRegistry, Extensions as EditorExtensions, IEditorDescriptor } from 'vs/workbench/browser/editor'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IEditorProgressService, LongRunningOperation } from 'vs/platform/progress/common/progress'; import { IEditorGroupView, DEFAULT_EDITOR_MIN_DIMENSIONS, DEFAULT_EDITOR_MAX_DIMENSIONS } from 'vs/workbench/browser/parts/editor/editor'; @@ -17,7 +17,7 @@ import { Emitter } from 'vs/base/common/event'; import { assertIsDefined } from 'vs/base/common/types'; export interface IOpenEditorResult { - readonly editorPane: BaseEditor; + readonly editorPane: EditorPane; readonly editorChanged: boolean; } @@ -34,10 +34,10 @@ export class EditorControl extends Disposable { private _onDidSizeConstraintsChange = this._register(new Emitter<{ width: number; height: number; } | undefined>()); readonly onDidSizeConstraintsChange = this._onDidSizeConstraintsChange.event; - private _activeEditorPane: BaseEditor | null = null; + private _activeEditorPane: EditorPane | null = null; get activeEditorPane(): IVisibleEditorPane | null { return this._activeEditorPane as IVisibleEditorPane | null; } - private readonly editorPanes: BaseEditor[] = []; + private readonly editorPanes: EditorPane[] = []; private readonly activeEditorPaneDisposables = this._register(new DisposableStore()); private dimension: Dimension | undefined; @@ -53,7 +53,7 @@ export class EditorControl extends Disposable { super(); } - async openEditor(editor: EditorInput, options?: EditorOptions): Promise { + async openEditor(editor: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext): Promise { // Editor pane const descriptor = Registry.as(EditorExtensions.Editors).getEditor(editor); @@ -63,11 +63,11 @@ export class EditorControl extends Disposable { const editorPane = this.doShowEditorPane(descriptor); // Set input - const editorChanged = await this.doSetInput(editorPane, editor, options); + const editorChanged = await this.doSetInput(editorPane, editor, options, context); return { editorPane, editorChanged }; } - private doShowEditorPane(descriptor: IEditorDescriptor): BaseEditor { + private doShowEditorPane(descriptor: IEditorDescriptor): EditorPane { // Return early if the currently active editor pane can handle the input if (this._activeEditorPane && descriptor.describes(this._activeEditorPane)) { @@ -99,7 +99,7 @@ export class EditorControl extends Disposable { return editorPane; } - private doCreateEditorPane(descriptor: IEditorDescriptor): BaseEditor { + private doCreateEditorPane(descriptor: IEditorDescriptor): EditorPane { // Instantiate editor const editorPane = this.doInstantiateEditorPane(descriptor); @@ -116,7 +116,7 @@ export class EditorControl extends Disposable { return editorPane; } - private doInstantiateEditorPane(descriptor: IEditorDescriptor): BaseEditor { + private doInstantiateEditorPane(descriptor: IEditorDescriptor): EditorPane { // Return early if already instantiated const existingEditorPane = this.editorPanes.find(editorPane => descriptor.describes(editorPane)); @@ -131,7 +131,7 @@ export class EditorControl extends Disposable { return editorPane; } - private doSetActiveEditorPane(editorPane: BaseEditor | null) { + private doSetActiveEditorPane(editorPane: EditorPane | null) { this._activeEditorPane = editorPane; // Clear out previous active editor pane listeners @@ -147,7 +147,7 @@ export class EditorControl extends Disposable { this._onDidSizeConstraintsChange.fire(undefined); } - private async doSetInput(editorPane: BaseEditor, editor: EditorInput, options: EditorOptions | undefined): Promise { + private async doSetInput(editorPane: EditorPane, editor: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext): Promise { // If the input did not change, return early and only apply the options // unless the options instruct us to force open it even if it is the same @@ -174,7 +174,7 @@ export class EditorControl extends Disposable { // Call into editor pane const editorWillChange = !inputMatches; try { - await editorPane.setInput(editor, options, operation.token); + await editorPane.setInput(editor, options, context, operation.token); // Focus (unless prevented or another operation is running) if (operation.isCurrent()) { diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 37f8a065c5..9fcbde9ba5 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -459,7 +459,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { const activeElement = document.activeElement; // Show active editor - await this.doShowEditor(activeEditor, true, options); + await this.doShowEditor(activeEditor, { active: true, isNew: false /* restored */ }, options); // Set focused now if this is the active group and focus has // not changed meanwhile. This prevents focus from being @@ -954,10 +954,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Update model and make sure to continue to use the editor we get from // the model. It is possible that the editor was already opened and we // want to ensure that we use the existing instance in that case. - const openedEditor = this._group.openEditor(editor, openEditorOptions); + const { editor: openedEditor, isNew } = this._group.openEditor(editor, openEditorOptions); // Show editor - const showEditorResult = this.doShowEditor(openedEditor, !!openEditorOptions.active, options); + const showEditorResult = this.doShowEditor(openedEditor, { active: !!openEditorOptions.active, isNew }, options); // Finally make sure the group is active or restored as instructed if (activateGroup) { @@ -969,14 +969,14 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return showEditorResult; } - private async doShowEditor(editor: EditorInput, active: boolean, options?: EditorOptions): Promise { + private async doShowEditor(editor: EditorInput, context: { active: boolean, isNew: boolean }, options?: EditorOptions): Promise { // Show in editor control if the active editor changed let openEditorPromise: Promise | undefined; - if (active) { + if (context.active) { openEditorPromise = (async () => { try { - const result = await this.editorControl.openEditor(editor, options); + const result = await this.editorControl.openEditor(editor, options, { newInGroup: context.isNew }); // Editor change event if (result.editorChanged) { diff --git a/src/vs/workbench/browser/parts/editor/baseEditor.ts b/src/vs/workbench/browser/parts/editor/editorPane.ts similarity index 96% rename from src/vs/workbench/browser/parts/editor/baseEditor.ts rename to src/vs/workbench/browser/parts/editor/editorPane.ts index 5aad958dd7..8c231c28e3 100644 --- a/src/vs/workbench/browser/parts/editor/baseEditor.ts +++ b/src/vs/workbench/browser/parts/editor/editorPane.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Composite } from 'vs/workbench/browser/composite'; -import { EditorInput, EditorOptions, IEditorPane, GroupIdentifier, IEditorMemento } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, IEditorPane, GroupIdentifier, IEditorMemento, IEditorOpenContext } from 'vs/workbench/common/editor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -41,7 +41,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; * * This class is only intended to be subclassed and not instantiated. */ -export abstract class BaseEditor extends Composite implements IEditorPane { +export abstract class EditorPane extends Composite implements IEditorPane { private static readonly EDITOR_MEMENTOS = new Map>(); @@ -91,10 +91,12 @@ export abstract class BaseEditor extends Composite implements IEditorPane { * to be different from the previous input that was set using the `input.matches()` * method. * + * The provided context gives more information around how the editor was opened. + * * The provided cancellation token should be used to test if the operation * was cancelled. */ - async setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { this._input = input; this._options = options; } @@ -146,10 +148,10 @@ export abstract class BaseEditor extends Composite implements IEditorPane { protected getEditorMemento(editorGroupService: IEditorGroupsService, key: string, limit: number = 10): IEditorMemento { const mementoKey = `${this.getId()}${key}`; - let editorMemento = BaseEditor.EDITOR_MEMENTOS.get(mementoKey); + let editorMemento = EditorPane.EDITOR_MEMENTOS.get(mementoKey); if (!editorMemento) { editorMemento = new EditorMemento(this.getId(), key, this.getMemento(StorageScope.WORKSPACE), limit, editorGroupService); - BaseEditor.EDITOR_MEMENTOS.set(mementoKey, editorMemento); + EditorPane.EDITOR_MEMENTOS.set(mementoKey, editorMemento); } return editorMemento; @@ -158,7 +160,7 @@ export abstract class BaseEditor extends Composite implements IEditorPane { protected saveState(): void { // Save all editor memento for this editor type - BaseEditor.EDITOR_MEMENTOS.forEach(editorMemento => { + EditorPane.EDITOR_MEMENTOS.forEach(editorMemento => { if (editorMemento.id === this.getId()) { editorMemento.saveState(); } diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index dfe7a795ca..58abf003b2 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -26,7 +26,6 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IFileService, FILES_ASSOCIATIONS_CONFIG } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IModeService, ILanguageSelection } from 'vs/editor/common/services/modeService'; -import { IModelService } from 'vs/editor/common/services/modelService'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { TabFocus } from 'vs/editor/common/config/commonEditorConfig'; @@ -43,7 +42,7 @@ import { ICodeEditor, getCodeEditor } from 'vs/editor/browser/editorBrowser'; import { Schemas } from 'vs/base/common/network'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; -import { getIconClasses } from 'vs/editor/common/services/getIconClasses'; +import { getIconClassesForModeId } from 'vs/editor/common/services/getIconClasses'; import { timeout } from 'vs/base/common/async'; import { INotificationHandle, INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { Event } from 'vs/base/common/event'; @@ -1045,7 +1044,6 @@ export class ChangeModeAction extends Action { actionId: string, actionLabel: string, @IModeService private readonly modeService: IModeService, - @IModelService private readonly modelService: IModelService, @IEditorService private readonly editorService: IEditorService, @IConfigurationService private readonly configurationService: IConfigurationService, @IQuickInputService private readonly quickInputService: IQuickInputService, @@ -1072,26 +1070,27 @@ export class ChangeModeAction extends Action { } // Compute mode + let currentLanguageId: string | undefined; let currentModeId: string | undefined; - let modeId: string | undefined; if (textModel) { - modeId = textModel.getLanguageIdentifier().language; - currentModeId = withNullAsUndefined(this.modeService.getLanguageName(modeId)); + currentModeId = textModel.getLanguageIdentifier().language; + currentLanguageId = withNullAsUndefined(this.modeService.getLanguageName(currentModeId)); } // All languages are valid picks const languages = this.modeService.getRegisteredLanguageNames(); const picks: QuickPickInput[] = languages.sort().map((lang, index) => { + const modeId = this.modeService.getModeIdForLanguageName(lang.toLowerCase()) || 'unknown'; let description: string; - if (currentModeId === lang) { - description = nls.localize('languageDescription', "({0}) - Configured Language", this.modeService.getModeIdForLanguageName(lang.toLowerCase())); + if (currentLanguageId === lang) { + description = nls.localize('languageDescription', "({0}) - Configured Language", modeId); } else { - description = nls.localize('languageDescriptionConfigured', "({0})", this.modeService.getModeIdForLanguageName(lang.toLowerCase())); + description = nls.localize('languageDescriptionConfigured', "({0})", modeId); } return { label: lang, - iconClasses: getIconClasses(this.modelService, this.modeService, this.getFakeResource(lang)), + iconClasses: getIconClassesForModeId(modeId), description }; }); @@ -1112,7 +1111,7 @@ export class ChangeModeAction extends Action { picks.unshift(galleryAction); } - configureModeSettings = { label: nls.localize('configureModeSettings', "Configure '{0}' language based settings...", currentModeId) }; + configureModeSettings = { label: nls.localize('configureModeSettings', "Configure '{0}' language based settings...", currentLanguageId) }; picks.unshift(configureModeSettings); configureModeAssociations = { label: nls.localize('configureAssociationsExt', "Configure File Association for '{0}'...", ext) }; picks.unshift(configureModeAssociations); @@ -1147,7 +1146,7 @@ export class ChangeModeAction extends Action { // User decided to configure settings for current language if (pick === configureModeSettings) { - this.preferencesService.openGlobalSettings(true, { editSetting: `[${withUndefinedAsNull(modeId)}]` }); + this.preferencesService.openGlobalSettings(true, { editSetting: `[${withUndefinedAsNull(currentModeId)}]` }); return; } @@ -1185,12 +1184,12 @@ export class ChangeModeAction extends Action { const languages = this.modeService.getRegisteredLanguageNames(); const picks: IQuickPickItem[] = languages.sort().map((lang, index) => { - const id = withNullAsUndefined(this.modeService.getModeIdForLanguageName(lang.toLowerCase())); + const id = withNullAsUndefined(this.modeService.getModeIdForLanguageName(lang.toLowerCase())) || 'unknown'; return { id, label: lang, - iconClasses: getIconClasses(this.modelService, this.modeService, this.getFakeResource(lang)), + iconClasses: getIconClassesForModeId(id), description: (id === currentAssociation) ? nls.localize('currentAssociation', "Current Association") : undefined }; }); @@ -1221,22 +1220,6 @@ export class ChangeModeAction extends Action { } }, 50 /* quick input is sensitive to being opened so soon after another */); } - - private getFakeResource(lang: string): URI | undefined { - let fakeResource: URI | undefined; - - const extensions = this.modeService.getExtensions(lang); - if (extensions?.length) { - fakeResource = URI.file(extensions[0]); - } else { - const filenames = this.modeService.getFilenames(lang); - if (filenames?.length) { - fakeResource = URI.file(filenames[0]); - } - } - - return fakeResource; - } } export interface IChangeEOLEntry extends IQuickPickItem { diff --git a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts index 5a74c1a900..5ba8c7b422 100644 --- a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts +++ b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts @@ -5,8 +5,8 @@ import * as DOM from 'vs/base/browser/dom'; import { Registry } from 'vs/platform/registry/common/platform'; -import { EditorInput, EditorOptions, SideBySideEditorInput, IEditorControl, IEditorPane } from 'vs/workbench/common/editor'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorInput, EditorOptions, SideBySideEditorInput, IEditorControl, IEditorPane, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -19,7 +19,7 @@ import { Event, Relay, Emitter } from 'vs/base/common/event'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { assertIsDefined } from 'vs/base/common/types'; -export class SideBySideEditor extends BaseEditor { +export class SideBySideEditor extends EditorPane { static readonly ID: string = 'workbench.editor.sidebysideEditor'; @@ -33,7 +33,7 @@ export class SideBySideEditor extends BaseEditor { private get minimumSecondaryHeight() { return this.secondaryEditorPane ? this.secondaryEditorPane.minimumHeight : 0; } private get maximumSecondaryHeight() { return this.secondaryEditorPane ? this.secondaryEditorPane.maximumHeight : Number.POSITIVE_INFINITY; } - // these setters need to exist because this extends from BaseEditor + // these setters need to exist because this extends from EditorPane set minimumWidth(value: number) { /* noop */ } set maximumWidth(value: number) { /* noop */ } set minimumHeight(value: number) { /* noop */ } @@ -44,8 +44,8 @@ export class SideBySideEditor extends BaseEditor { get minimumHeight() { return this.minimumPrimaryHeight + this.minimumSecondaryHeight; } get maximumHeight() { return this.maximumPrimaryHeight + this.maximumSecondaryHeight; } - protected primaryEditorPane?: BaseEditor; - protected secondaryEditorPane?: BaseEditor; + protected primaryEditorPane?: EditorPane; + protected secondaryEditorPane?: EditorPane; private primaryEditorContainer: HTMLElement | undefined; private secondaryEditorContainer: HTMLElement | undefined; @@ -94,11 +94,11 @@ export class SideBySideEditor extends BaseEditor { this.updateStyles(); } - async setInput(newInput: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + async setInput(newInput: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { const oldInput = this.input as SideBySideEditorInput; - await super.setInput(newInput, options, token); + await super.setInput(newInput, options, context, token); - return this.updateInput(oldInput, (newInput as SideBySideEditorInput), options, token); + return this.updateInput(oldInput, (newInput as SideBySideEditorInput), options, context, token); } setOptions(options: EditorOptions | undefined): void { @@ -162,13 +162,13 @@ export class SideBySideEditor extends BaseEditor { return this.secondaryEditorPane; } - private async updateInput(oldInput: SideBySideEditorInput, newInput: SideBySideEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + private async updateInput(oldInput: SideBySideEditorInput, newInput: SideBySideEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { if (!newInput.matches(oldInput)) { if (oldInput) { this.disposeEditors(); } - return this.setNewInput(newInput, options, token); + return this.setNewInput(newInput, options, context, token); } if (!this.secondaryEditorPane || !this.primaryEditorPane) { @@ -176,19 +176,19 @@ export class SideBySideEditor extends BaseEditor { } await Promise.all([ - this.secondaryEditorPane.setInput(newInput.secondary, undefined, token), - this.primaryEditorPane.setInput(newInput.primary, options, token) + this.secondaryEditorPane.setInput(newInput.secondary, undefined, context, token), + this.primaryEditorPane.setInput(newInput.primary, options, context, token) ]); } - private setNewInput(newInput: SideBySideEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + private setNewInput(newInput: SideBySideEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { const secondaryEditor = this.doCreateEditor(newInput.secondary, assertIsDefined(this.secondaryEditorContainer)); const primaryEditor = this.doCreateEditor(newInput.primary, assertIsDefined(this.primaryEditorContainer)); - return this.onEditorsCreated(secondaryEditor, primaryEditor, newInput.secondary, newInput.primary, options, token); + return this.onEditorsCreated(secondaryEditor, primaryEditor, newInput.secondary, newInput.primary, options, context, token); } - private doCreateEditor(editorInput: EditorInput, container: HTMLElement): BaseEditor { + private doCreateEditor(editorInput: EditorInput, container: HTMLElement): EditorPane { const descriptor = Registry.as(EditorExtensions.Editors).getEditor(editorInput); if (!descriptor) { throw new Error('No descriptor for editor found'); @@ -201,7 +201,7 @@ export class SideBySideEditor extends BaseEditor { return editor; } - private async onEditorsCreated(secondary: BaseEditor, primary: BaseEditor, secondaryInput: EditorInput, primaryInput: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + private async onEditorsCreated(secondary: EditorPane, primary: EditorPane, secondaryInput: EditorInput, primaryInput: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { this.secondaryEditorPane = secondary; this.primaryEditorPane = primary; @@ -213,8 +213,8 @@ export class SideBySideEditor extends BaseEditor { this.onDidCreateEditors.fire(undefined); await Promise.all([ - this.secondaryEditorPane.setInput(secondaryInput, undefined, token), - this.primaryEditorPane.setInput(primaryInput, options, token)] + this.secondaryEditorPane.setInput(secondaryInput, undefined, context, token), + this.primaryEditorPane.setInput(primaryInput, options, context, token)] ); } diff --git a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts index 69a45950c2..a2352ffdc2 100644 --- a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts @@ -9,7 +9,7 @@ import { isFunction, isObject, isArray, assertIsDefined } from 'vs/base/common/t import { IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IDiffEditorOptions, IEditorOptions as ICodeEditorOptions } from 'vs/editor/common/config/editorOptions'; import { BaseTextEditor, IEditorConfiguration } from 'vs/workbench/browser/parts/editor/textEditor'; -import { TextEditorOptions, EditorInput, EditorOptions, TEXT_DIFF_EDITOR_ID, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions, ITextDiffEditorPane, IEditorInput } from 'vs/workbench/common/editor'; +import { TextEditorOptions, EditorInput, EditorOptions, TEXT_DIFF_EDITOR_ID, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions, ITextDiffEditorPane, IEditorInput, IEditorOpenContext } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { DiffNavigator } from 'vs/editor/browser/widget/diffNavigator'; import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; @@ -80,7 +80,7 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan return this.instantiationService.createInstance(DiffEditorWidget, parent, configuration); } - async setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { // Dispose previous diff navigator this.diffNavigatorDisposables.clear(); @@ -89,7 +89,7 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan this.doSaveOrClearTextDiffEditorViewState(this.input); // Set input and resolve - await super.setInput(input, options, token); + await super.setInput(input, options, context, token); try { const resolvedModel = await input.resolve(); @@ -115,9 +115,9 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan optionsGotApplied = (options).apply(diffEditor, ScrollType.Immediate); } - // Otherwise restore View State + // Otherwise restore View State unless disabled via settings let hasPreviousViewState = false; - if (!optionsGotApplied) { + if (!optionsGotApplied && this.shouldRestoreTextEditorViewState(input, context)) { hasPreviousViewState = this.restoreTextDiffEditorViewState(input, diffEditor); } @@ -296,7 +296,7 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan } // Clear view state if input is disposed or we are configured to not storing any state - if (input.isDisposed() || (!this.shouldRestoreViewState && (!this.group || !this.group.isOpened(input)))) { + if (input.isDisposed() || (!this.shouldRestoreTextEditorViewState(input) && (!this.group || !this.group.isOpened(input)))) { super.clearTextEditorViewState([resource], this.group); } diff --git a/src/vs/workbench/browser/parts/editor/textEditor.ts b/src/vs/workbench/browser/parts/editor/textEditor.ts index fb4af4681b..f32031363c 100644 --- a/src/vs/workbench/browser/parts/editor/textEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textEditor.ts @@ -10,8 +10,8 @@ import { Event } from 'vs/base/common/event'; import { isObject, assertIsDefined, withNullAsUndefined, isFunction } from 'vs/base/common/types'; import { Dimension } from 'vs/base/browser/dom'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; -import { EditorInput, EditorOptions, IEditorMemento, ITextEditorPane, TextEditorOptions, IEditorCloseEvent, IEditorInput, computeEditorAriaLabel } from 'vs/workbench/common/editor'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorInput, EditorOptions, IEditorMemento, ITextEditorPane, TextEditorOptions, IEditorCloseEvent, IEditorInput, computeEditorAriaLabel, IEditorOpenContext, toResource, SideBySideEditor } from 'vs/workbench/common/editor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { IEditorViewState, IEditor, ScrollType } from 'vs/editor/common/editorCommon'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -35,7 +35,7 @@ export interface IEditorConfiguration { * The base class of editors that leverage the text editor for the editing experience. This class is only intended to * be subclassed and not instantiated. */ -export abstract class BaseTextEditor extends BaseEditor implements ITextEditorPane { +export abstract class BaseTextEditor extends EditorPane implements ITextEditorPane { static readonly TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'textEditorViewState'; @@ -47,9 +47,6 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditorPa private readonly groupListener = this._register(new MutableDisposable()); - private _shouldRestoreViewState: boolean | undefined; - protected get shouldRestoreViewState(): boolean | undefined { return this._shouldRestoreViewState; } - private _instantiationService: IInstantiationService; protected get instantiationService(): IInstantiationService { return this._instantiationService; } protected set instantiationService(value: IInstantiationService) { this._instantiationService = value; } @@ -69,7 +66,7 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditorPa this.editorMemento = this.getEditorMemento(editorGroupService, BaseTextEditor.TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY, 100); - this._register(this.textResourceConfigurationService.onDidChangeConfiguration(e => { + this._register(this.textResourceConfigurationService.onDidChangeConfiguration(() => { const resource = this.getActiveResource(); const value = resource ? this.textResourceConfigurationService.getValue(resource) : undefined; @@ -84,13 +81,9 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditorPa this.editorContainer?.setAttribute('aria-label', ariaLabel); this.editorControl?.updateOptions({ ariaLabel }); })); - - this.updateRestoreViewStateConfiguration(); } protected handleConfigurationChangeEvent(configuration?: IEditorConfiguration): void { - this.updateRestoreViewStateConfiguration(); - if (this.isVisible()) { this.updateEditorConfiguration(configuration); } else { @@ -98,10 +91,6 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditorPa } } - private updateRestoreViewStateConfiguration(): void { - this._shouldRestoreViewState = this.textResourceConfigurationService.getValue(undefined, 'workbench.editor.restoreViewState') ?? true /* default */; - } - private consumePendingConfigurationChangeEvent(): void { if (this.hasPendingConfigurationChange) { this.updateEditorConfiguration(); @@ -163,8 +152,8 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditorPa return this.instantiationService.createInstance(CodeEditorWidget, parent, configuration, {}); } - async setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { - await super.setInput(input, options, token); + async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + await super.setInput(input, options, context, token); // Update editor options after having set the input. We do this because there can be // editor input specific options (e.g. an ARIA label depending on the input showing) @@ -238,6 +227,17 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditorPa this.editorMemento.saveEditorState(this.group, resource, editorViewState); } + protected shouldRestoreTextEditorViewState(editor: IEditorInput, context?: IEditorOpenContext): boolean { + + // new editor: check with workbench.editor.restoreViewState setting + if (context?.newInGroup) { + return this.textResourceConfigurationService.getValue(toResource(editor, { supportSideBySide: SideBySideEditor.PRIMARY }), 'workbench.editor.restoreViewState') === false ? false : true /* restore by default */; + } + + // existing editor: always restore viewstate + return true; + } + getViewState(): IEditorViewState | undefined { const resource = this.input?.resource; if (resource) { diff --git a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts index 22c2934069..c75507ad11 100644 --- a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { assertIsDefined, isFunction, withNullAsUndefined } from 'vs/base/common/types'; import { ICodeEditor, getCodeEditor, IPasteEvent } from 'vs/editor/browser/editorBrowser'; -import { TextEditorOptions, EditorInput, EditorOptions } from 'vs/workbench/common/editor'; +import { TextEditorOptions, EditorInput, EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; @@ -54,13 +54,13 @@ export class AbstractTextResourceEditor extends BaseTextEditor { return nls.localize('textEditor', "Text Editor"); } - async setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { // Remember view settings if input changes this.saveTextResourceEditorViewState(this.input); // Set input and resolve - await super.setInput(input, options, token); + await super.setInput(input, options, context, token); const resolvedModel = await input.resolve(); // Check for cancellation @@ -85,8 +85,8 @@ export class AbstractTextResourceEditor extends BaseTextEditor { optionsGotApplied = textOptions.apply(textEditor, ScrollType.Immediate); } - // Otherwise restore View State - if (!optionsGotApplied) { + // Otherwise restore View State unless disabled via settings + if (!optionsGotApplied && this.shouldRestoreTextEditorViewState(input, context)) { this.restoreTextResourceEditorViewState(input, textEditor); } diff --git a/src/vs/workbench/browser/parts/editor/titleControl.ts b/src/vs/workbench/browser/parts/editor/titleControl.ts index 45ba6c8dfa..bb6653f143 100644 --- a/src/vs/workbench/browser/parts/editor/titleControl.ts +++ b/src/vs/workbench/browser/parts/editor/titleControl.ts @@ -28,7 +28,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { listActiveSelectionBackground, listActiveSelectionForeground } from 'vs/platform/theme/common/colorRegistry'; import { ICssStyleCollector, IColorTheme, IThemeService, registerThemingParticipant, Themable } from 'vs/platform/theme/common/themeService'; import { DraggedEditorGroupIdentifier, DraggedEditorIdentifier, fillResourceDataTransfers, LocalSelectionTransfer } from 'vs/workbench/browser/dnd'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { BreadcrumbsConfig } from 'vs/workbench/browser/parts/editor/breadcrumbs'; import { BreadcrumbsControl, IBreadcrumbsControlOptions } from 'vs/workbench/browser/parts/editor/breadcrumbsControl'; import { IEditorGroupsAccessor, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; @@ -164,7 +164,7 @@ export abstract class TitleControl extends Themable { const activeEditorPane = this.group.activeEditorPane; // Check Active Editor - if (activeEditorPane instanceof BaseEditor) { + if (activeEditorPane instanceof EditorPane) { const result = activeEditorPane.getActionViewItem(action); if (result) { @@ -237,7 +237,7 @@ export abstract class TitleControl extends Themable { // Editor actions require the editor control to be there, so we retrieve it via service const activeEditorPane = this.group.activeEditorPane; - if (activeEditorPane instanceof BaseEditor) { + if (activeEditorPane instanceof EditorPane) { const codeEditor = getCodeEditor(activeEditorPane.getControl()); const scopedContextKeyService = codeEditor?.invokeWithinContext(accessor => accessor.get(IContextKeyService)) || this.contextKeyService; const titleBarMenu = this.menuService.createMenu(MenuId.EditorTitle, scopedContextKeyService); diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts index 82c2d07616..769b9f64f6 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts @@ -221,7 +221,7 @@ export function registerNotificationCommands(center: INotificationsCenterControl // Commands for Command Palette const category = { value: localize('notifications', "Notifications"), original: 'Notifications' }; - MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: SHOW_NOTIFICATIONS_CENTER, title: { value: localize('showNotifications', "Show Notifications"), original: 'Show Notifications' }, category }, when: NotificationsCenterVisibleContext.toNegated() }); + MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: SHOW_NOTIFICATIONS_CENTER, title: { value: localize('showNotifications', "Show Notifications"), original: 'Show Notifications' }, category } }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: HIDE_NOTIFICATIONS_CENTER, title: { value: localize('hideNotifications', "Hide Notifications"), original: 'Hide Notifications' }, category }, when: NotificationsCenterVisibleContext }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: CLEAR_ALL_NOTIFICATIONS, title: { value: localize('clearAllNotifications', "Clear All Notifications"), original: 'Clear All Notifications' }, category } }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: FOCUS_NOTIFICATION_TOAST, title: { value: localize('focusNotificationToasts', "Focus Notification Toast"), original: 'Focus Notification Toast' }, category }, when: NotificationsToastsVisibleContext }); diff --git a/src/vs/workbench/browser/parts/views/media/paneviewlet.css b/src/vs/workbench/browser/parts/views/media/paneviewlet.css index 21d6925fd5..0bc6aff978 100644 --- a/src/vs/workbench/browser/parts/views/media/paneviewlet.css +++ b/src/vs/workbench/browser/parts/views/media/paneviewlet.css @@ -38,6 +38,26 @@ -webkit-margin-after: 0; } +.monaco-pane-view .pane > .pane-header .description { + display: block; + font-weight: normal; + margin-left: 10px; + opacity: 0.6; + overflow: hidden; + text-overflow: ellipsis; + text-transform: none; + white-space: nowrap; +} + +.monaco-pane-view .pane > .pane-header .description .codicon { + font-size: 9px; + margin-left: 2px; +} + +.monaco-pane-view .pane > .pane-header:not(.expanded) .description { + display: none; +} + .monaco-pane-view .pane.horizontal:not(.expanded) > .pane-header h3.title, .monaco-pane-view .pane.horizontal:not(.expanded) > .pane-header .description { display: none; diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index ef2e0d84fd..9d3d41b23f 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -9,7 +9,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { ColorIdentifier, activeContrastBorder, foreground } from 'vs/platform/theme/common/colorRegistry'; import { attachStyler, IColorMapping, attachButtonStyler, attachLinkStyler, attachProgressBarStyler } from 'vs/platform/theme/common/styler'; import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND, SIDE_BAR_SECTION_HEADER_FOREGROUND, SIDE_BAR_SECTION_HEADER_BACKGROUND, SIDE_BAR_SECTION_HEADER_BORDER, PANEL_BACKGROUND, SIDE_BAR_BACKGROUND, PANEL_SECTION_HEADER_FOREGROUND, PANEL_SECTION_HEADER_BACKGROUND, PANEL_SECTION_HEADER_BORDER, PANEL_SECTION_DRAG_AND_DROP_BACKGROUND, PANEL_SECTION_BORDER } from 'vs/workbench/common/theme'; -import { append, $, trackFocus, toggleClass, EventType, isAncestor, Dimension, addDisposableListener, removeClass, addClass, createCSSRule, asCSSUrl, addClasses } from 'vs/base/browser/dom'; +import { after, append, $, trackFocus, toggleClass, EventType, isAncestor, Dimension, addDisposableListener, removeClass, addClass, createCSSRule, asCSSUrl, addClasses } from 'vs/base/browser/dom'; import { IDisposable, combinedDisposable, dispose, toDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { firstIndex } from 'vs/base/common/arrays'; import { IAction, Separator, IActionViewItem } from 'vs/base/common/actions'; @@ -184,6 +184,11 @@ export abstract class ViewPane extends Pane implements IView { return this._title; } + private _titleDescription: string | undefined; + public get titleDescription(): string | undefined { + return this._titleDescription; + } + private readonly menuActions: ViewMenuActions; private progressBar!: ProgressBar; private progressIndicator!: IProgressIndicator; @@ -192,6 +197,7 @@ export abstract class ViewPane extends Pane implements IView { private readonly showActionsAlways: boolean = false; private headerContainer?: HTMLElement; private titleContainer?: HTMLElement; + private titleDescriptionContainer?: HTMLElement; private iconContainer?: HTMLElement; protected twistiesContainer?: HTMLElement; @@ -216,6 +222,7 @@ export abstract class ViewPane extends Pane implements IView { this.id = options.id; this._title = options.title; + this._titleDescription = options.titleDescription; this.showActionsAlways = !!options.showActionsAlways; this.focusedViewContextKey = FocusedViewContext.bindTo(contextKeyService); @@ -360,6 +367,11 @@ export abstract class ViewPane extends Pane implements IView { const calculatedTitle = this.calculateTitle(title); this.titleContainer = append(container, $('h3.title', undefined, calculatedTitle)); + + if (this._titleDescription) { + this.setTitleDescription(this._titleDescription); + } + this.iconContainer.title = calculatedTitle; this.iconContainer.setAttribute('aria-label', calculatedTitle); } @@ -379,6 +391,22 @@ export abstract class ViewPane extends Pane implements IView { this._onDidChangeTitleArea.fire(); } + private setTitleDescription(description: string | undefined) { + if (this.titleDescriptionContainer) { + this.titleDescriptionContainer.textContent = description ?? ''; + } + else if (description && this.titleContainer) { + this.titleDescriptionContainer = after(this.titleContainer, $('span.description', undefined, description)); + } + } + + protected updateTitleDescription(description?: string | undefined): void { + this.setTitleDescription(description); + + this._titleDescription = description; + this._onDidChangeTitleArea.fire(); + } + private calculateTitle(title: string): string { const viewContainer = this.viewDescriptorService.getViewContainerByViewId(this.id)!; const model = this.viewDescriptorService.getViewContainerModel(viewContainer); diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 4f77b66c50..ee61d479c8 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -208,10 +208,17 @@ class BrowserMain extends Disposable { const requestService = new BrowserRequestService(remoteAgentService, configurationService, logService); serviceCollection.set(IRequestService, requestService); - // initialize user data + // Userdata Initialize Service const userDataInitializationService = new UserDataInitializationService(environmentService, fileService, storageService, productService, requestService, logService); serviceCollection.set(IUserDataInitializationService, userDataInitializationService); - await userDataInitializationService.initializeRequiredResources(); + + if (await userDataInitializationService.requiresInitialization()) { + // Initialize required resources - settings & global state + await userDataInitializationService.initializeRequiredResources(); + + // Reload configuration after initializing + await configurationService.reloadConfiguration(); + } return { serviceCollection, logService, storageService }; } diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 12ad6208d3..fe64f60c57 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -150,8 +150,9 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio }, 'workbench.editor.restoreViewState': { 'type': 'boolean', - 'description': nls.localize('restoreViewState', "Restores the last view state (e.g. scroll position) when re-opening files after they have been closed."), + 'description': nls.localize('restoreViewState', "Restores the last view state (e.g. scroll position) when re-opening textual editors after they have been closed."), 'default': true, + 'scope': ConfigurationScope.LANGUAGE_OVERRIDABLE }, 'workbench.editor.centeredLayoutAutoResize': { 'type': 'boolean', diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 21769343dc..22e60ecebe 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -819,6 +819,8 @@ export class EditorModel extends Disposable implements IEditorModel { private readonly _onDispose = this._register(new Emitter()); readonly onDispose = this._onDispose.event; + private disposed = false; + /** * Causes this model to load returning a promise when loading is completed. */ @@ -833,10 +835,18 @@ export class EditorModel extends Disposable implements IEditorModel { return true; } + /** + * Find out if this model has been disposed. + */ + isDisposed(): boolean { + return this.disposed; + } + /** * Subclasses should implement to free resources that have been claimed through loading. */ dispose(): void { + this.disposed = true; this._onDispose.fire(); super.dispose(); @@ -1137,6 +1147,22 @@ export class TextEditorOptions extends EditorOptions implements ITextEditorOptio } } +/** + * Context passed into `EditorPane#setInput` to give additional + * context information around why the editor was opened. + */ +export interface IEditorOpenContext { + + /** + * An indicator if the editor input is new for the group the editor is in. + * An editor is new for a group if it was not part of the group before and + * otherwise was already opened in the group and just became the active editor. + * + * This hint can e.g. be used to decide wether to restore view state or not. + */ + newInGroup?: boolean; +} + export interface IEditorIdentifier { groupId: GroupIdentifier; editor: IEditorInput; diff --git a/src/vs/workbench/common/editor/editorGroup.ts b/src/vs/workbench/common/editor/editorGroup.ts index aebe30b865..ee2325194a 100644 --- a/src/vs/workbench/common/editor/editorGroup.ts +++ b/src/vs/workbench/common/editor/editorGroup.ts @@ -20,38 +20,43 @@ const EditorOpenPositioning = { }; export interface EditorCloseEvent extends IEditorCloseEvent { - editor: EditorInput; + readonly editor: EditorInput; } export interface EditorIdentifier extends IEditorIdentifier { - groupId: GroupIdentifier; - editor: EditorInput; + readonly groupId: GroupIdentifier; + readonly editor: EditorInput; } export interface IEditorOpenOptions { - pinned?: boolean; + readonly pinned?: boolean; sticky?: boolean; active?: boolean; - index?: number; + readonly index?: number; +} + +export interface IEditorOpenResult { + readonly editor: EditorInput; + readonly isNew: boolean; } export interface ISerializedEditorInput { - id: string; - value: string; + readonly id: string; + readonly value: string; } export interface ISerializedEditorGroup { - id: number; - editors: ISerializedEditorInput[]; - mru: number[]; - preview?: number; + readonly id: number; + readonly editors: ISerializedEditorInput[]; + readonly mru: number[]; + readonly preview?: number; sticky?: number; } export function isSerializedEditorGroup(obj?: unknown): obj is ISerializedEditorGroup { const group = obj as ISerializedEditorGroup; - return obj && typeof obj === 'object' && Array.isArray(group.editors) && Array.isArray(group.mru); + return !!(obj && typeof obj === 'object' && Array.isArray(group.editors) && Array.isArray(group.mru)); } export class EditorGroup extends Disposable { @@ -175,7 +180,7 @@ export class EditorGroup extends Disposable { return this.preview; } - openEditor(candidate: EditorInput, options?: IEditorOpenOptions): EditorInput { + openEditor(candidate: EditorInput, options?: IEditorOpenOptions): IEditorOpenResult { const makeSticky = options?.sticky || (typeof options?.index === 'number' && this.isSticky(options.index)); const makePinned = options?.pinned || options?.sticky; const makeActive = options?.active || !this.activeEditor || (!makePinned && this.matches(this.preview, this.activeEditor)); @@ -275,7 +280,10 @@ export class EditorGroup extends Disposable { this.doSetActive(newEditor); } - return newEditor; + return { + editor: newEditor, + isNew: true + }; } // Existing editor @@ -303,7 +311,10 @@ export class EditorGroup extends Disposable { this.doStick(existingEditor, this.indexOf(existingEditor)); } - return existingEditor; + return { + editor: existingEditor, + isNew: false + }; } } diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts new file mode 100644 index 0000000000..8b9071e855 --- /dev/null +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts @@ -0,0 +1,60 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { groupBy } from 'vs/base/common/arrays'; +import { compare } from 'vs/base/common/strings'; +import { URI } from 'vs/base/common/uri'; +import { ResourceEdit } from 'vs/editor/browser/services/bulkEditService'; +import { WorkspaceEditMetadata } from 'vs/editor/common/modes'; +import { IProgress } from 'vs/platform/progress/common/progress'; +import { ICellEditOperation } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; + +export class ResourceNotebookCellEdit extends ResourceEdit { + + constructor( + readonly resource: URI, + readonly cellEdit: ICellEditOperation, + readonly versionId?: number, + readonly metadata?: WorkspaceEditMetadata + ) { + super(metadata); + } +} + +export class BulkCellEdits { + + constructor( + private readonly _progress: IProgress, + private readonly _edits: ResourceNotebookCellEdit[], + @INotebookService private readonly _notebookService: INotebookService, + @INotebookEditorModelResolverService private readonly _notebookModelService: INotebookEditorModelResolverService, + ) { } + + async apply(): Promise { + + const editsByNotebook = groupBy(this._edits, (a, b) => compare(a.resource.toString(), b.resource.toString())); + + for (let group of editsByNotebook) { + const [first] = group; + const ref = await this._notebookModelService.resolve(first.resource); + + // check state + // if (typeof first.versionId === 'number' && ref.object.notebook.versionId !== first.versionId) { + // ref.dispose(); + // throw new Error(`Notebook '${first.resource}' has changed in the meantime`); + // } + + // apply edits + const cellEdits = group.map(edit => edit.cellEdit); + this._notebookService.transformEditsOutputs(ref.object.notebook, cellEdits); + ref.object.notebook.applyEdit(ref.object.notebook.versionId, cellEdits, true); + ref.dispose(); + + this._progress.report(undefined); + } + } +} diff --git a/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts similarity index 59% rename from src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts rename to src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts index af111d237a..7c4be8ab16 100644 --- a/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts @@ -6,39 +6,28 @@ import { localize } from 'vs/nls'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IBulkEditOptions, IBulkEditResult, IBulkEditService, IBulkEditPreviewHandler } from 'vs/editor/browser/services/bulkEditService'; -import { WorkspaceFileEdit, WorkspaceTextEdit, WorkspaceEdit } from 'vs/editor/common/modes'; +import { IBulkEditOptions, IBulkEditResult, IBulkEditService, IBulkEditPreviewHandler, ResourceEdit, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; import { IProgress, IProgressStep, Progress } from 'vs/platform/progress/common/progress'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { BulkTextEdits } from 'vs/workbench/services/bulkEdit/browser/bulkTextEdits'; -import { BulkFileEdits } from 'vs/workbench/services/bulkEdit/browser/bulkFileEdits'; -import { ResourceMap } from 'vs/base/common/map'; - -type Edit = WorkspaceFileEdit | WorkspaceTextEdit; +import { BulkTextEdits } from 'vs/workbench/contrib/bulkEdit/browser/bulkTextEdits'; +import { BulkFileEdits } from 'vs/workbench/contrib/bulkEdit/browser/bulkFileEdits'; +import { BulkCellEdits, ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; class BulkEdit { - private readonly _label: string | undefined; - private readonly _edits: Edit[] = []; - private readonly _editor: ICodeEditor | undefined; - private readonly _progress: IProgress; - constructor( - label: string | undefined, - editor: ICodeEditor | undefined, - progress: IProgress | undefined, - edits: Edit[], + private readonly _label: string | undefined, + private readonly _editor: ICodeEditor | undefined, + private readonly _progress: IProgress, + private readonly _edits: ResourceEdit[], @IInstantiationService private readonly _instaService: IInstantiationService, @ILogService private readonly _logService: ILogService, ) { - this._label = label; - this._editor = editor; - this._progress = progress || Progress.None; - this._edits = edits; + } ariaMessage(): string { @@ -55,56 +44,56 @@ class BulkEdit { async perform(): Promise { - let seen = new ResourceMap(); - let total = 0; + if (this._edits.length === 0) { + return; + } - const groups: Edit[][] = []; - let group: Edit[] | undefined; - for (const edit of this._edits) { - if (!group - || (WorkspaceFileEdit.is(group[0]) && !WorkspaceFileEdit.is(edit)) - || (WorkspaceTextEdit.is(group[0]) && !WorkspaceTextEdit.is(edit)) - ) { - group = []; - groups.push(group); - } - group.push(edit); - - if (WorkspaceFileEdit.is(edit)) { - total += 1; - } else if (!seen.has(edit.resource)) { - seen.set(edit.resource, true); - total += 2; + const ranges: number[] = [1]; + for (let i = 1; i < this._edits.length; i++) { + if (Object.getPrototypeOf(this._edits[i - 1]) === Object.getPrototypeOf(this._edits[i])) { + ranges[ranges.length - 1]++; + } else { + ranges.push(1); } } - // define total work and progress callback - // for child operations - this._progress.report({ total }); - + this._progress.report({ total: this._edits.length }); const progress: IProgress = { report: _ => this._progress.report({ increment: 1 }) }; - // do it. - for (const group of groups) { - if (WorkspaceFileEdit.is(group[0])) { - await this._performFileEdits(group, progress); + + let index = 0; + for (let range of ranges) { + const group = this._edits.slice(index, index + range); + if (group[0] instanceof ResourceFileEdit) { + await this._performFileEdits(group, progress); + } else if (group[0] instanceof ResourceTextEdit) { + await this._performTextEdits(group, progress); + } else if (group[0] instanceof ResourceNotebookCellEdit) { + await this._performCellEdits(group, progress); } else { - await this._performTextEdits(group, progress); + console.log('UNKNOWN EDIT'); } + index = index + range; } } - private async _performFileEdits(edits: WorkspaceFileEdit[], progress: IProgress) { + private async _performFileEdits(edits: ResourceFileEdit[], progress: IProgress) { this._logService.debug('_performFileEdits', JSON.stringify(edits)); const model = this._instaService.createInstance(BulkFileEdits, this._label || localize('workspaceEdit', "Workspace Edit"), progress, edits); await model.apply(); } - private async _performTextEdits(edits: WorkspaceTextEdit[], progress: IProgress): Promise { + private async _performTextEdits(edits: ResourceTextEdit[], progress: IProgress): Promise { this._logService.debug('_performTextEdits', JSON.stringify(edits)); const model = this._instaService.createInstance(BulkTextEdits, this._label || localize('workspaceEdit', "Workspace Edit"), this._editor, progress, edits); await model.apply(); } + + private async _performCellEdits(edits: ResourceNotebookCellEdit[], progress: IProgress): Promise { + this._logService.debug('_performCellEdits', JSON.stringify(edits)); + const model = this._instaService.createInstance(BulkCellEdits, progress, edits); + await model.apply(); + } } export class BulkEditService implements IBulkEditService { @@ -132,17 +121,16 @@ export class BulkEditService implements IBulkEditService { return Boolean(this._previewHandler); } - async apply(edit: WorkspaceEdit, options?: IBulkEditOptions): Promise { + async apply(edits: ResourceEdit[], options?: IBulkEditOptions): Promise { - if (edit.edits.length === 0) { + if (edits.length === 0) { return { ariaSummary: localize('nothing', "Made no edits") }; } - if (this._previewHandler && (options?.showPreview || edit.edits.some(value => value.metadata?.needsConfirmation))) { - edit = await this._previewHandler(edit, options); + if (this._previewHandler && (options?.showPreview || edits.some(value => value.metadata?.needsConfirmation))) { + edits = await this._previewHandler(edits, options); } - const { edits } = edit; let codeEditor = options?.editor; // try to find code editor if (!codeEditor) { @@ -156,15 +144,23 @@ export class BulkEditService implements IBulkEditService { // If the code editor is readonly still allow bulk edits to be applied #68549 codeEditor = undefined; } - const bulkEdit = this._instaService.createInstance(BulkEdit, options?.quotableLabel || options?.label, codeEditor, options?.progress, edits); - return bulkEdit.perform().then(() => { + + const bulkEdit = this._instaService.createInstance( + BulkEdit, + options?.quotableLabel || options?.label, + codeEditor, options?.progress ?? Progress.None, + edits + ); + + try { + await bulkEdit.perform(); return { ariaSummary: bulkEdit.ariaMessage() }; - }).catch(err => { + } catch (err) { // console.log('apply FAILED'); // console.log(err); this._logService.error(err); throw err; - }); + } } } diff --git a/src/vs/workbench/services/bulkEdit/browser/bulkFileEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts similarity index 93% rename from src/vs/workbench/services/bulkEdit/browser/bulkFileEdits.ts rename to src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts index 151044baf8..a7ea1964a8 100644 --- a/src/vs/workbench/services/bulkEdit/browser/bulkFileEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ -import { WorkspaceFileEdit, WorkspaceFileEditOptions } from 'vs/editor/common/modes'; +import { WorkspaceFileEditOptions } from 'vs/editor/common/modes'; import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { IProgress } from 'vs/platform/progress/common/progress'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -14,6 +14,7 @@ import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { VSBuffer } from 'vs/base/common/buffer'; +import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService'; interface IFileOperation { uris: URI[]; @@ -147,7 +148,7 @@ export class BulkFileEdits { constructor( private readonly _label: string, private readonly _progress: IProgress, - private readonly _edits: WorkspaceFileEdit[], + private readonly _edits: ResourceFileEdit[], @IInstantiationService private readonly _instaService: IInstantiationService, @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, ) { } @@ -159,15 +160,15 @@ export class BulkFileEdits { const options = edit.options || {}; let op: IFileOperation | undefined; - if (edit.newUri && edit.oldUri) { + if (edit.newResource && edit.oldResource) { // rename - op = this._instaService.createInstance(RenameOperation, edit.newUri, edit.oldUri, options); - } else if (!edit.newUri && edit.oldUri) { + op = this._instaService.createInstance(RenameOperation, edit.newResource, edit.oldResource, options); + } else if (!edit.newResource && edit.oldResource) { // delete file - op = this._instaService.createInstance(DeleteOperation, edit.oldUri, options); - } else if (edit.newUri && !edit.oldUri) { + op = this._instaService.createInstance(DeleteOperation, edit.oldResource, options); + } else if (edit.newResource && !edit.oldResource) { // create file - op = this._instaService.createInstance(CreateOperation, edit.newUri, options, undefined); + op = this._instaService.createInstance(CreateOperation, edit.newResource, options, undefined); } if (op) { const undoOp = await op.perform(); diff --git a/src/vs/workbench/services/bulkEdit/browser/bulkTextEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts similarity index 88% rename from src/vs/workbench/services/bulkEdit/browser/bulkTextEdits.ts rename to src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts index 7bb376903d..29e9e8a58b 100644 --- a/src/vs/workbench/services/bulkEdit/browser/bulkTextEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts @@ -11,7 +11,6 @@ import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { EndOfLineSequence, IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/model'; -import { WorkspaceTextEdit } from 'vs/editor/common/modes'; import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; import { IProgress } from 'vs/platform/progress/common/progress'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; @@ -19,6 +18,7 @@ import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { SingleModelEditStackElement, MultiModelEditStackElement } from 'vs/editor/common/model/editStack'; import { ResourceMap } from 'vs/base/common/map'; import { IModelService } from 'vs/editor/common/services/modelService'; +import { ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; type ValidationResult = { canApply: true } | { canApply: false, reason: URI }; @@ -39,31 +39,31 @@ class ModelEditTask implements IDisposable { this._modelReference.dispose(); } - addEdit(resourceEdit: WorkspaceTextEdit): void { - this._expectedModelVersionId = resourceEdit.modelVersionId; - const { edit } = resourceEdit; + addEdit(resourceEdit: ResourceTextEdit): void { + this._expectedModelVersionId = resourceEdit.versionId; + const { textEdit } = resourceEdit; - if (typeof edit.eol === 'number') { + if (typeof textEdit.eol === 'number') { // honor eol-change - this._newEol = edit.eol; + this._newEol = textEdit.eol; } - if (!edit.range && !edit.text) { + if (!textEdit.range && !textEdit.text) { // lacks both a range and the text return; } - if (Range.isEmpty(edit.range) && !edit.text) { + if (Range.isEmpty(textEdit.range) && !textEdit.text) { // no-op edit (replace empty range with empty text) return; } // create edit operation let range: Range; - if (!edit.range) { + if (!textEdit.range) { range = this.model.getFullModelRange(); } else { - range = Range.lift(edit.range); + range = Range.lift(textEdit.range); } - this._edits.push(EditOperation.replaceMove(range, edit.text)); + this._edits.push(EditOperation.replaceMove(range, textEdit.text)); } validate(): ValidationResult { @@ -116,13 +116,13 @@ class EditorEditTask extends ModelEditTask { export class BulkTextEdits { - private readonly _edits = new ResourceMap(); + private readonly _edits = new ResourceMap(); constructor( private readonly _label: string, private readonly _editor: ICodeEditor | undefined, private readonly _progress: IProgress, - edits: WorkspaceTextEdit[], + edits: ResourceTextEdit[], @IEditorWorkerService private readonly _editorWorker: IEditorWorkerService, @IModelService private readonly _modelService: IModelService, @ITextModelService private readonly _textModelResolverService: ITextModelService, @@ -143,9 +143,9 @@ export class BulkTextEdits { // First check if loaded models were not changed in the meantime for (const array of this._edits.values()) { for (let edit of array) { - if (typeof edit.modelVersionId === 'number') { + if (typeof edit.versionId === 'number') { let model = this._modelService.getModel(edit.resource); - if (model && model.getVersionId() !== edit.modelVersionId) { + if (model && model.getVersionId() !== edit.versionId) { // model changed in the meantime throw new Error(`${model.uri.toString()} has changed in the meantime`); } @@ -172,12 +172,12 @@ export class BulkTextEdits { for (const edit of value) { if (makeMinimal) { - const newEdits = await this._editorWorker.computeMoreMinimalEdits(edit.resource, [edit.edit]); + const newEdits = await this._editorWorker.computeMoreMinimalEdits(edit.resource, [edit.textEdit]); if (!newEdits) { task.addEdit(edit); } else { for (let moreMinialEdit of newEdits) { - task.addEdit({ ...edit, edit: moreMinialEdit }); + task.addEdit(new ResourceTextEdit(edit.resource, moreMinialEdit, edit.versionId, edit.metadata)); } } } else { @@ -186,7 +186,6 @@ export class BulkTextEdits { } tasks.push(task); - this._progress.report(undefined); }); promises.push(promise); } diff --git a/src/vs/workbench/services/bulkEdit/browser/conflicts.ts b/src/vs/workbench/contrib/bulkEdit/browser/conflicts.ts similarity index 80% rename from src/vs/workbench/services/bulkEdit/browser/conflicts.ts rename to src/vs/workbench/contrib/bulkEdit/browser/conflicts.ts index 98cd381d16..b0b40f8c74 100644 --- a/src/vs/workbench/services/bulkEdit/browser/conflicts.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/conflicts.ts @@ -5,12 +5,12 @@ import { IFileService } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; -import { WorkspaceEdit, WorkspaceTextEdit } from 'vs/editor/common/modes'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ResourceMap } from 'vs/base/common/map'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { Emitter, Event } from 'vs/base/common/event'; import { ITextModel } from 'vs/editor/common/model'; +import { ResourceEdit, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; export class ConflictDetector { @@ -21,31 +21,35 @@ export class ConflictDetector { readonly onDidConflict: Event = this._onDidConflict.event; constructor( - workspaceEdit: WorkspaceEdit, + edits: ResourceEdit[], @IFileService fileService: IFileService, @IModelService modelService: IModelService, ) { const _workspaceEditResources = new ResourceMap(); - for (let edit of workspaceEdit.edits) { - if (WorkspaceTextEdit.is(edit)) { - + for (let edit of edits) { + if (edit instanceof ResourceTextEdit) { _workspaceEditResources.set(edit.resource, true); - - if (typeof edit.modelVersionId === 'number') { + if (typeof edit.versionId === 'number') { const model = modelService.getModel(edit.resource); - if (model && model.getVersionId() !== edit.modelVersionId) { + if (model && model.getVersionId() !== edit.versionId) { this._conflicts.set(edit.resource, true); this._onDidConflict.fire(this); } } - } else if (edit.newUri) { - _workspaceEditResources.set(edit.newUri, true); + } else if (edit instanceof ResourceFileEdit) { + if (edit.newResource) { + _workspaceEditResources.set(edit.newResource, true); - } else if (edit.oldUri) { - _workspaceEditResources.set(edit.oldUri, true); + } else if (edit.oldResource) { + _workspaceEditResources.set(edit.oldResource, true); + } + + } else { + //todo@jrieken + console.log('UNKNOWN EDIT TYPE'); } } diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.contribution.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.contribution.ts similarity index 95% rename from src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.contribution.ts rename to src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.contribution.ts index 6b489c666f..86d4daefd1 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.contribution.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.contribution.ts @@ -7,16 +7,15 @@ import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; -import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; -import { WorkspaceEdit } from 'vs/editor/common/modes'; -import { BulkEditPane } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditPane'; +import { IBulkEditService, ResourceEdit } from 'vs/editor/browser/services/bulkEditService'; +import { BulkEditPane } from 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane'; import { IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainerLocation, IViewsRegistry, FocusedViewContext, IViewsService } from 'vs/workbench/common/views'; import { localize } from 'vs/nls'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { RawContextKey, IContextKeyService, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; -import { BulkEditPreviewProvider } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditPreview'; +import { BulkEditPreviewProvider } from 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { WorkbenchListFocusContextKey } from 'vs/platform/list/browser/listService'; @@ -105,18 +104,18 @@ class BulkEditPreviewContribution { @IBulkEditService bulkEditService: IBulkEditService, @IContextKeyService contextKeyService: IContextKeyService, ) { - bulkEditService.setPreviewHandler((edit) => this._previewEdit(edit)); + bulkEditService.setPreviewHandler(edits => this._previewEdit(edits)); this._ctxEnabled = BulkEditPreviewContribution.ctxEnabled.bindTo(contextKeyService); } - private async _previewEdit(edit: WorkspaceEdit) { + private async _previewEdit(edits: ResourceEdit[]): Promise { this._ctxEnabled.set(true); const uxState = this._activeSession?.uxState ?? new UXState(this._panelService, this._editorGroupsService); const view = await getBulkEditPane(this._viewsService); if (!view) { this._ctxEnabled.set(false); - return edit; + return edits; } // check for active preview session and let the user decide @@ -130,7 +129,7 @@ class BulkEditPreviewContribution { if (choice.choice === 0) { // this refactoring is being cancelled - return { edits: [] }; + return []; } } @@ -147,12 +146,7 @@ class BulkEditPreviewContribution { // the actual work... try { - const newEditOrUndefined = await view.setInput(edit, session.cts.token); - if (!newEditOrUndefined) { - return { edits: [] }; - } - - return newEditOrUndefined; + return await view.setInput(edits, session.cts.token); } finally { // restore UX state @@ -366,4 +360,3 @@ Registry.as(ViewContainerExtensions.ViewsRegistry).registerViews ctorDescriptor: new SyncDescriptor(BulkEditPane), containerIcon: Codicon.lightbulb.classNames, }], container); - diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.css b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.css similarity index 100% rename from src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.css rename to src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.css diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPane.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts similarity index 96% rename from src/vs/workbench/contrib/bulkEdit/browser/bulkEditPane.ts rename to src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts index 321cee7fa1..d93a5dfbc5 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPane.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts @@ -5,8 +5,7 @@ import 'vs/css!./bulkEdit'; import { WorkbenchAsyncDataTree, IOpenEvent } from 'vs/platform/list/browser/listService'; -import { WorkspaceEdit } from 'vs/editor/common/modes'; -import { BulkEditElement, BulkEditDelegate, TextEditElementRenderer, FileElementRenderer, BulkEditDataSource, BulkEditIdentityProvider, FileElement, TextEditElement, BulkEditAccessibilityProvider, CategoryElementRenderer, BulkEditNaviLabelProvider, CategoryElement, BulkEditSorter } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditTree'; +import { BulkEditElement, BulkEditDelegate, TextEditElementRenderer, FileElementRenderer, BulkEditDataSource, BulkEditIdentityProvider, FileElement, TextEditElement, BulkEditAccessibilityProvider, CategoryElementRenderer, BulkEditNaviLabelProvider, CategoryElement, BulkEditSorter } from 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree'; import { FuzzyScore } from 'vs/base/common/filters'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { registerThemingParticipant, IColorTheme, ICssStyleCollector, IThemeService } from 'vs/platform/theme/common/themeService'; @@ -14,7 +13,7 @@ import { diffInserted, diffRemoved } from 'vs/platform/theme/common/colorRegistr import { localize } from 'vs/nls'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { BulkEditPreviewProvider, BulkFileOperations, BulkFileOperationType } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditPreview'; +import { BulkEditPreviewProvider, BulkFileOperations, BulkFileOperationType } from 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview'; import { ILabelService } from 'vs/platform/label/common/label'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { URI } from 'vs/base/common/uri'; @@ -39,6 +38,7 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { ResourceEdit } from 'vs/editor/browser/services/bulkEditService'; const enum State { Data = 'data', @@ -66,7 +66,7 @@ export class BulkEditPane extends ViewPane { private readonly _disposables = new DisposableStore(); private readonly _sessionDisposables = new DisposableStore(); - private _currentResolve?: (edit?: WorkspaceEdit) => void; + private _currentResolve?: (edit?: ResourceEdit[]) => void; private _currentInput?: BulkFileOperations; @@ -163,7 +163,7 @@ export class BulkEditPane extends ViewPane { this.element.dataset['state'] = state; } - async setInput(edit: WorkspaceEdit, token: CancellationToken): Promise { + async setInput(edit: ResourceEdit[], token: CancellationToken): Promise { this._setState(State.Data); this._sessionDisposables.clear(); this._treeViewStates.clear(); @@ -307,11 +307,11 @@ export class BulkEditPane extends ViewPane { let fileElement: FileElement; if (e.element instanceof TextEditElement) { fileElement = e.element.parent; - options.selection = e.element.edit.textEdit.edit.range; + options.selection = e.element.edit.textEdit.textEdit.range; } else if (e.element instanceof FileElement) { fileElement = e.element; - options.selection = e.element.edit.textEdits[0]?.textEdit.edit.range; + options.selection = e.element.edit.textEdits[0]?.textEdit.textEdit.range; } else { // invalid event diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPreview.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview.ts similarity index 81% rename from src/vs/workbench/contrib/bulkEdit/browser/bulkEditPreview.ts rename to src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview.ts index 00756da47d..c3b6a26f3d 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPreview.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview.ts @@ -8,7 +8,7 @@ import { URI } from 'vs/base/common/uri'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; -import { WorkspaceEdit, WorkspaceTextEdit, WorkspaceFileEdit, WorkspaceEditMetadata } from 'vs/editor/common/modes'; +import { WorkspaceEditMetadata } from 'vs/editor/common/modes'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { mergeSort, coalesceInPlace } from 'vs/base/common/arrays'; import { Range } from 'vs/editor/common/core/range'; @@ -17,10 +17,11 @@ import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiati import { IFileService } from 'vs/platform/files/common/files'; import { Emitter, Event } from 'vs/base/common/event'; import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; -import { ConflictDetector } from 'vs/workbench/services/bulkEdit/browser/conflicts'; +import { ConflictDetector } from 'vs/workbench/contrib/bulkEdit/browser/conflicts'; import { ResourceMap } from 'vs/base/common/map'; import { localize } from 'vs/nls'; import { extUri } from 'vs/base/common/resources'; +import { ResourceEdit, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; export class CheckedStates { @@ -67,7 +68,7 @@ export class BulkTextEdit { constructor( readonly parent: BulkFileOperation, - readonly textEdit: WorkspaceTextEdit + readonly textEdit: ResourceTextEdit ) { } } @@ -82,7 +83,7 @@ export class BulkFileOperation { type: BulkFileOperationType = 0; textEdits: BulkTextEdit[] = []; - originalEdits = new Map(); + originalEdits = new Map(); newUri?: URI; constructor( @@ -90,14 +91,14 @@ export class BulkFileOperation { readonly parent: BulkFileOperations ) { } - addEdit(index: number, type: BulkFileOperationType, edit: WorkspaceTextEdit | WorkspaceFileEdit) { + addEdit(index: number, type: BulkFileOperationType, edit: ResourceTextEdit | ResourceFileEdit) { this.type |= type; this.originalEdits.set(index, edit); - if (WorkspaceTextEdit.is(edit)) { + if (edit instanceof ResourceTextEdit) { this.textEdits.push(new BulkTextEdit(this, edit)); } else if (type === BulkFileOperationType.Rename) { - this.newUri = edit.newUri; + this.newUri = edit.newResource; } } @@ -134,19 +135,19 @@ export class BulkCategory { export class BulkFileOperations { - static async create(accessor: ServicesAccessor, bulkEdit: WorkspaceEdit): Promise { + static async create(accessor: ServicesAccessor, bulkEdit: ResourceEdit[]): Promise { const result = accessor.get(IInstantiationService).createInstance(BulkFileOperations, bulkEdit); return await result._init(); } - readonly checked = new CheckedStates(); + readonly checked = new CheckedStates(); readonly fileOperations: BulkFileOperation[] = []; readonly categories: BulkCategory[] = []; readonly conflicts: ConflictDetector; constructor( - private readonly _bulkEdit: WorkspaceEdit, + private readonly _bulkEdit: ResourceEdit[], @IFileService private readonly _fileService: IFileService, @IInstantiationService instaService: IInstantiationService, ) { @@ -164,8 +165,8 @@ export class BulkFileOperations { const newToOldUri = new ResourceMap(); - for (let idx = 0; idx < this._bulkEdit.edits.length; idx++) { - const edit = this._bulkEdit.edits[idx]; + for (let idx = 0; idx < this._bulkEdit.length; idx++) { + const edit = this._bulkEdit[idx]; let uri: URI; let type: BulkFileOperationType; @@ -173,39 +174,45 @@ export class BulkFileOperations { // store inital checked state this.checked.updateChecked(edit, !edit.metadata?.needsConfirmation); - if (WorkspaceTextEdit.is(edit)) { + if (edit instanceof ResourceTextEdit) { type = BulkFileOperationType.TextEdit; uri = edit.resource; - } else if (edit.newUri && edit.oldUri) { - type = BulkFileOperationType.Rename; - uri = edit.oldUri; - if (edit.options?.overwrite === undefined && edit.options?.ignoreIfExists && await this._fileService.exists(uri)) { - // noop -> "soft" rename to something that already exists - continue; - } - // map newUri onto oldUri so that text-edit appear for - // the same file element - newToOldUri.set(edit.newUri, uri); + } else if (edit instanceof ResourceFileEdit) { + if (edit.newResource && edit.oldResource) { + type = BulkFileOperationType.Rename; + uri = edit.oldResource; + if (edit.options?.overwrite === undefined && edit.options?.ignoreIfExists && await this._fileService.exists(uri)) { + // noop -> "soft" rename to something that already exists + continue; + } + // map newResource onto oldResource so that text-edit appear for + // the same file element + newToOldUri.set(edit.newResource, uri); - } else if (edit.oldUri) { - type = BulkFileOperationType.Delete; - uri = edit.oldUri; - if (edit.options?.ignoreIfNotExists && !await this._fileService.exists(uri)) { - // noop -> "soft" delete something that doesn't exist - continue; - } + } else if (edit.oldResource) { + type = BulkFileOperationType.Delete; + uri = edit.oldResource; + if (edit.options?.ignoreIfNotExists && !await this._fileService.exists(uri)) { + // noop -> "soft" delete something that doesn't exist + continue; + } - } else if (edit.newUri) { - type = BulkFileOperationType.Create; - uri = edit.newUri; - if (edit.options?.overwrite === undefined && edit.options?.ignoreIfExists && await this._fileService.exists(uri)) { - // noop -> "soft" create something that already exists + } else if (edit.newResource) { + type = BulkFileOperationType.Create; + uri = edit.newResource; + if (edit.options?.overwrite === undefined && edit.options?.ignoreIfExists && await this._fileService.exists(uri)) { + // noop -> "soft" create something that already exists + continue; + } + + } else { + // invalid edit -> skip continue; } } else { - // invalid edit -> skip + // unsupported edit continue; } @@ -249,7 +256,7 @@ export class BulkFileOperations { if (file.type !== BulkFileOperationType.TextEdit) { let checked = true; for (const edit of file.originalEdits.values()) { - if (WorkspaceFileEdit.is(edit)) { + if (edit instanceof ResourceFileEdit) { checked = checked && this.checked.isChecked(edit); } } @@ -275,14 +282,14 @@ export class BulkFileOperations { return this; } - getWorkspaceEdit(): WorkspaceEdit { - const result: WorkspaceEdit = { edits: [] }; + getWorkspaceEdit(): ResourceEdit[] { + const result: ResourceEdit[] = []; let allAccepted = true; - for (let i = 0; i < this._bulkEdit.edits.length; i++) { - const edit = this._bulkEdit.edits[i]; + for (let i = 0; i < this._bulkEdit.length; i++) { + const edit = this._bulkEdit[i]; if (this.checked.isChecked(edit)) { - result.edits[i] = edit; + result[i] = edit; continue; } allAccepted = false; @@ -293,7 +300,7 @@ export class BulkFileOperations { } // not all edits have been accepted - coalesceInPlace(result.edits); + coalesceInPlace(result); return result; } @@ -306,9 +313,9 @@ export class BulkFileOperations { let ignoreAll = false; for (const edit of file.originalEdits.values()) { - if (WorkspaceTextEdit.is(edit)) { + if (edit instanceof ResourceTextEdit) { if (this.checked.isChecked(edit)) { - result.push(EditOperation.replaceMove(Range.lift(edit.edit.range), edit.edit.text)); + result.push(EditOperation.replaceMove(Range.lift(edit.textEdit.range), edit.textEdit.text)); } } else if (!this.checked.isChecked(edit)) { @@ -330,7 +337,7 @@ export class BulkFileOperations { return []; } - getUriOfEdit(edit: WorkspaceFileEdit | WorkspaceTextEdit): URI { + getUriOfEdit(edit: ResourceEdit): URI { for (let file of this.fileOperations) { for (const value of file.originalEdits.values()) { if (value === edit) { diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditTree.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts similarity index 96% rename from src/vs/workbench/contrib/bulkEdit/browser/bulkEditTree.ts rename to src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts index 42dc8ba66b..bacaba5a78 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditTree.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts @@ -14,7 +14,7 @@ import * as dom from 'vs/base/browser/dom'; import { ITextModel } from 'vs/editor/common/model'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { TextModel } from 'vs/editor/common/model/textModel'; -import { BulkFileOperations, BulkFileOperation, BulkFileOperationType, BulkTextEdit, BulkCategory } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditPreview'; +import { BulkFileOperations, BulkFileOperation, BulkFileOperationType, BulkTextEdit, BulkCategory } from 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview'; import { FileKind } from 'vs/platform/files/common/files'; import { localize } from 'vs/nls'; import { ILabelService } from 'vs/platform/label/common/label'; @@ -22,11 +22,11 @@ import type { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWid import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { basename } from 'vs/base/common/resources'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { WorkspaceFileEdit } from 'vs/editor/common/modes'; import { compare } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { Iterable } from 'vs/base/common/iterator'; +import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService'; // --- VIEW MODEL @@ -62,7 +62,7 @@ export class FileElement implements ICheckable { // multiple file edits -> reflect single state for (let edit of this.edit.originalEdits.values()) { - if (WorkspaceFileEdit.is(edit)) { + if (edit instanceof ResourceFileEdit) { checked = checked && model.checked.isChecked(edit); } } @@ -73,7 +73,7 @@ export class FileElement implements ICheckable { for (let file of category.fileOperations) { if (file.uri.toString() === this.edit.uri.toString()) { for (const edit of file.originalEdits.values()) { - if (WorkspaceFileEdit.is(edit)) { + if (edit instanceof ResourceFileEdit) { checked = checked && model.checked.isChecked(edit); } } @@ -113,7 +113,7 @@ export class FileElement implements ICheckable { for (let file of category.fileOperations) { if (file.uri.toString() === this.edit.uri.toString()) { for (const edit of file.originalEdits.values()) { - if (WorkspaceFileEdit.is(edit)) { + if (edit instanceof ResourceFileEdit) { checked = checked && model.checked.isChecked(edit); } } @@ -155,7 +155,7 @@ export class TextEditElement implements ICheckable { // make sure parent is checked when this element is checked... if (value) { for (const edit of this.parent.edit.originalEdits.values()) { - if (WorkspaceFileEdit.is(edit)) { + if (edit instanceof ResourceFileEdit) { (model).checked.updateChecked(edit, value); } } @@ -219,7 +219,7 @@ export class BulkEditDataSource implements IAsyncDataSource { - const range = Range.lift(edit.textEdit.edit.range); + const range = Range.lift(edit.textEdit.textEdit.range); //prefix-math let startTokens = textModel.getLineTokens(range.startLineNumber); @@ -241,7 +241,7 @@ export class BulkEditDataSource implements IAsyncDataSource { } if (a instanceof TextEditElement && b instanceof TextEditElement) { - return Range.compareRangesUsingStarts(a.edit.textEdit.edit.range, b.edit.textEdit.edit.range); + return Range.compareRangesUsingStarts(a.edit.textEdit.textEdit.range, b.edit.textEdit.textEdit.range); } return 0; @@ -336,13 +336,13 @@ export class BulkEditAccessibilityProvider implements IListAccessibilityProvider if (element instanceof TextEditElement) { if (element.selecting.length > 0 && element.inserting.length > 0) { // edit: replace - return localize('aria.replace', "line {0}, replacing {1} with {2}", element.edit.textEdit.edit.range.startLineNumber, element.selecting, element.inserting); + return localize('aria.replace', "line {0}, replacing {1} with {2}", element.edit.textEdit.textEdit.range.startLineNumber, element.selecting, element.inserting); } else if (element.selecting.length > 0 && element.inserting.length === 0) { // edit: delete - return localize('aria.del', "line {0}, removing {1}", element.edit.textEdit.edit.range.startLineNumber, element.selecting); + return localize('aria.del', "line {0}, removing {1}", element.edit.textEdit.textEdit.range.startLineNumber, element.selecting); } else if (element.selecting.length === 0 && element.inserting.length > 0) { // edit: insert - return localize('aria.insert', "line {0}, inserting {1}", element.edit.textEdit.edit.range.startLineNumber, element.selecting); + return localize('aria.insert', "line {0}, inserting {1}", element.edit.textEdit.textEdit.range.startLineNumber, element.selecting); } } diff --git a/src/vs/workbench/contrib/bulkEdit/test/browser/bulkEditPreview.test.ts b/src/vs/workbench/contrib/bulkEdit/test/browser/bulkEditPreview.test.ts index 5b2b782fcf..f950121212 100644 --- a/src/vs/workbench/contrib/bulkEdit/test/browser/bulkEditPreview.test.ts +++ b/src/vs/workbench/contrib/bulkEdit/test/browser/bulkEditPreview.test.ts @@ -11,10 +11,10 @@ import { InstantiationService } from 'vs/platform/instantiation/common/instantia import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IModelService } from 'vs/editor/common/services/modelService'; -import type { WorkspaceEdit } from 'vs/editor/common/modes'; import { URI } from 'vs/base/common/uri'; -import { BulkFileOperations } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditPreview'; +import { BulkFileOperations } from 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview'; import { Range } from 'vs/editor/common/core/range'; +import { ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; suite('BulkEditPreview', function () { @@ -47,28 +47,25 @@ suite('BulkEditPreview', function () { test('one needsConfirmation unchecks all of file', async function () { - const edit: WorkspaceEdit = { - edits: [ - { newUri: URI.parse('some:///uri1'), metadata: { label: 'cat1', needsConfirmation: true } }, - { oldUri: URI.parse('some:///uri1'), newUri: URI.parse('some:///uri2'), metadata: { label: 'cat2', needsConfirmation: false } }, - ] - }; + const edits = [ + new ResourceFileEdit(undefined, URI.parse('some:///uri1'), undefined, { label: 'cat1', needsConfirmation: true }), + new ResourceFileEdit(URI.parse('some:///uri1'), URI.parse('some:///uri2'), undefined, { label: 'cat2', needsConfirmation: false }), + ]; - const ops = await instaService.invokeFunction(BulkFileOperations.create, edit); + const ops = await instaService.invokeFunction(BulkFileOperations.create, edits); assert.equal(ops.fileOperations.length, 1); - assert.equal(ops.checked.isChecked(edit.edits[0]), false); + assert.equal(ops.checked.isChecked(edits[0]), false); }); test('has categories', async function () { - const edit: WorkspaceEdit = { - edits: [ - { newUri: URI.parse('some:///uri1'), metadata: { label: 'uri1', needsConfirmation: true } }, - { newUri: URI.parse('some:///uri2'), metadata: { label: 'uri2', needsConfirmation: false } } - ] - }; + const edits = [ + new ResourceFileEdit(undefined, URI.parse('some:///uri1'), undefined, { label: 'uri1', needsConfirmation: true }), + new ResourceFileEdit(undefined, URI.parse('some:///uri2'), undefined, { label: 'uri2', needsConfirmation: false }), + ]; - const ops = await instaService.invokeFunction(BulkFileOperations.create, edit); + + const ops = await instaService.invokeFunction(BulkFileOperations.create, edits); assert.equal(ops.categories.length, 2); assert.equal(ops.categories[0].metadata.label, 'uri1'); // unconfirmed! assert.equal(ops.categories[1].metadata.label, 'uri2'); @@ -76,14 +73,12 @@ suite('BulkEditPreview', function () { test('has not categories', async function () { - const edit: WorkspaceEdit = { - edits: [ - { newUri: URI.parse('some:///uri1'), metadata: { label: 'uri1', needsConfirmation: true } }, - { newUri: URI.parse('some:///uri2'), metadata: { label: 'uri1', needsConfirmation: false } } - ] - }; + const edits = [ + new ResourceFileEdit(undefined, URI.parse('some:///uri1'), undefined, { label: 'uri1', needsConfirmation: true }), + new ResourceFileEdit(undefined, URI.parse('some:///uri2'), undefined, { label: 'uri1', needsConfirmation: false }), + ]; - const ops = await instaService.invokeFunction(BulkFileOperations.create, edit); + const ops = await instaService.invokeFunction(BulkFileOperations.create, edits); assert.equal(ops.categories.length, 1); assert.equal(ops.categories[0].metadata.label, 'uri1'); // unconfirmed! assert.equal(ops.categories[0].metadata.label, 'uri1'); @@ -91,43 +86,41 @@ suite('BulkEditPreview', function () { test('category selection', async function () { - const edit: WorkspaceEdit = { - edits: [ - { newUri: URI.parse('some:///uri1'), metadata: { label: 'C1', needsConfirmation: false } }, - { resource: URI.parse('some:///uri2'), edit: { text: 'foo', range: new Range(1, 1, 1, 1) }, metadata: { label: 'C2', needsConfirmation: false } } - ] - }; + const edits = [ + new ResourceFileEdit(undefined, URI.parse('some:///uri1'), undefined, { label: 'C1', needsConfirmation: false }), + new ResourceTextEdit(URI.parse('some:///uri2'), { text: 'foo', range: new Range(1, 1, 1, 1) }, undefined, { label: 'C2', needsConfirmation: false }), + ]; - const ops = await instaService.invokeFunction(BulkFileOperations.create, edit); - assert.equal(ops.checked.isChecked(edit.edits[0]), true); - assert.equal(ops.checked.isChecked(edit.edits[1]), true); + const ops = await instaService.invokeFunction(BulkFileOperations.create, edits); - assert.ok(edit === ops.getWorkspaceEdit()); + assert.equal(ops.checked.isChecked(edits[0]), true); + assert.equal(ops.checked.isChecked(edits[1]), true); + + assert.ok(edits === ops.getWorkspaceEdit()); // NOT taking to create, but the invalid text edit will // go through - ops.checked.updateChecked(edit.edits[0], false); - const newEdit = ops.getWorkspaceEdit(); - assert.ok(edit !== newEdit); + ops.checked.updateChecked(edits[0], false); + const newEdits = ops.getWorkspaceEdit(); + assert.ok(edits !== newEdits); - assert.equal(edit.edits.length, 2); - assert.equal(newEdit.edits.length, 1); + assert.equal(edits.length, 2); + assert.equal(newEdits.length, 1); }); test('fix bad metadata', async function () { // bogous edit that wants creation to be confirmed, but not it's textedit-child... - const edit: WorkspaceEdit = { - edits: [ - { newUri: URI.parse('some:///uri1'), metadata: { label: 'C1', needsConfirmation: true } }, - { resource: URI.parse('some:///uri1'), edit: { text: 'foo', range: new Range(1, 1, 1, 1) }, metadata: { label: 'C2', needsConfirmation: false } } - ] - }; - const ops = await instaService.invokeFunction(BulkFileOperations.create, edit); + const edits = [ + new ResourceFileEdit(undefined, URI.parse('some:///uri1'), undefined, { label: 'C1', needsConfirmation: true }), + new ResourceTextEdit(URI.parse('some:///uri1'), { text: 'foo', range: new Range(1, 1, 1, 1) }, undefined, { label: 'C2', needsConfirmation: false }) + ]; - assert.equal(ops.checked.isChecked(edit.edits[0]), false); - assert.equal(ops.checked.isChecked(edit.edits[1]), false); + const ops = await instaService.invokeFunction(BulkFileOperations.create, edits); + + assert.equal(ops.checked.isChecked(edits[0]), false); + assert.equal(ops.checked.isChecked(edits[1]), false); }); }); diff --git a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts index 0d4dc667bc..678169a506 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts @@ -10,7 +10,6 @@ import { CharCode } from 'vs/base/common/charCode'; import { Color } from 'vs/base/common/color'; import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable } from 'vs/base/common/lifecycle'; -import { escape } from 'vs/base/common/strings'; import { ContentWidgetPositionPreference, IActiveCodeEditor, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { Position } from 'vs/editor/common/core/position'; @@ -31,6 +30,8 @@ import { SemanticTokenRule, TokenStyleData, TokenStyle } from 'vs/platform/theme import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { SEMANTIC_HIGHLIGHTING_SETTING_ID, IEditorSemanticHighlightingOptions } from 'vs/editor/common/services/modelServiceImpl'; +const $ = dom.$; + class InspectEditorTokensController extends Disposable implements IEditorContribution { public static readonly ID = 'editor.contrib.inspectEditorTokens'; @@ -151,23 +152,11 @@ function renderTokenText(tokenText: string): string { let charCode = tokenText.charCodeAt(charIndex); switch (charCode) { case CharCode.Tab: - result += '→'; + result += '\u2192'; // → break; case CharCode.Space: - result += '·'; - break; - - case CharCode.LessThan: - result += '<'; - break; - - case CharCode.GreaterThan: - result += '>'; - break; - - case CharCode.Ampersand: - result += '&'; + result += '\u00B7'; // · break; default: @@ -246,8 +235,7 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { if (this._isDisposed) { return; } - let text = this._compute(grammar, semanticTokens, position); - this._domNode.innerHTML = text; + this._compute(grammar, semanticTokens, position); this._domNode.style.maxWidth = `${Math.max(this._editor.getLayoutInfo().width * 0.66, 500)}px`; this._editor.layoutContentWidget(this); }, (err) => { @@ -268,11 +256,12 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { return this._themeService.getColorTheme().semanticHighlighting; } - private _compute(grammar: IGrammar | null, semanticTokens: SemanticTokensResult | null, position: Position): string { + private _compute(grammar: IGrammar | null, semanticTokens: SemanticTokensResult | null, position: Position) { const textMateTokenInfo = grammar && this._getTokensAtPosition(grammar, position); const semanticTokenInfo = semanticTokens && this._getSemanticTokenAtPosition(semanticTokens, position); if (!textMateTokenInfo && !semanticTokenInfo) { - return 'No grammar or semantic tokens available.'; + dom.reset(this._domNode, 'No grammar or semantic tokens available.'); + return; } let tmMetadata = textMateTokenInfo?.metadata; @@ -283,91 +272,125 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { const tokenText = semTokenText || tmTokenText || ''; - let result = ''; - result += `

${tokenText}(${tokenText.length} ${tokenText.length === 1 ? 'char' : 'chars'})

`; - result += ``; - - result += ``; - result += ``; - result += ``; - - result += this._formatMetadata(semMetadata, tmMetadata); - result += ``; + dom.reset(this._domNode, + $('h2.tiw-token', undefined, + tokenText, + $('span.tiw-token-length', undefined, `${tokenText.length} ${tokenText.length === 1 ? 'char' : 'chars'}`))); + dom.append(this._domNode, $('hr.tiw-metadata-separator', { 'style': 'clear:both' })); + dom.append(this._domNode, $('table.tiw-metadata-table', undefined, + $('tbody', undefined, + $('tr', undefined, + $('td.tiw-metadata-key', undefined, 'language'), + $('td.tiw-metadata-value', undefined, tmMetadata?.languageIdentifier.language || '') + ), + $('tr', undefined, + $('td.tiw-metadata-key', undefined, 'standard token type' as string), + $('td.tiw-metadata-value', undefined, this._tokenTypeToString(tmMetadata?.tokenType || StandardTokenType.Other)) + ), + ...this._formatMetadata(semMetadata, tmMetadata) + ) + )); if (semanticTokenInfo) { - result += ``; - result += ``; - result += ``; + dom.append(this._domNode, $('hr.tiw-metadata-separator')); + const table = dom.append(this._domNode, $('table.tiw-metadata-table', undefined)); + const tbody = dom.append(table, $('tbody', undefined, + $('tr', undefined, + $('td.tiw-metadata-key', undefined, 'semantic token type' as string), + $('td.tiw-metadata-value', undefined, semanticTokenInfo.type) + ) + )); if (semanticTokenInfo.modifiers.length) { - result += ``; + dom.append(tbody, $('tr', undefined, + $('td.tiw-metadata-key', undefined, 'modifiers'), + $('td.tiw-metadata-value', undefined, semanticTokenInfo.modifiers.join(' ')), + )); } if (semanticTokenInfo.metadata) { const properties: (keyof TokenStyleData)[] = ['foreground', 'bold', 'italic', 'underline']; const propertiesByDefValue: { [rule: string]: string[] } = {}; - const allDefValues = []; // remember the order + const allDefValues = new Array<[Array, string]>(); // remember the order // first collect to detect when the same rule is used for multiple properties for (let property of properties) { if (semanticTokenInfo.metadata[property] !== undefined) { const definition = semanticTokenInfo.definitions[property]; const defValue = this._renderTokenStyleDefinition(definition, property); - let properties = propertiesByDefValue[defValue]; + const defValueStr = defValue.map(el => el instanceof HTMLElement ? el.outerHTML : el).join(); + let properties = propertiesByDefValue[defValueStr]; if (!properties) { - propertiesByDefValue[defValue] = properties = []; - allDefValues.push(defValue); + propertiesByDefValue[defValueStr] = properties = []; + allDefValues.push([defValue, defValueStr]); } properties.push(property); } } - for (let defValue of allDefValues) { - result += ``; + for (const [defValue, defValueStr] of allDefValues) { + dom.append(tbody, $('tr', undefined, + $('td.tiw-metadata-key', undefined, propertiesByDefValue[defValueStr].join(', ')), + $('td.tiw-metadata-value', undefined, ...defValue) + )); } } - result += ``; } if (textMateTokenInfo) { let theme = this._themeService.getColorTheme(); - result += ``; - result += ``; + dom.append(this._domNode, $('hr.tiw-metadata-separator')); + const table = dom.append(this._domNode, $('table.tiw-metadata-table')); + const tbody = dom.append(table, $('tbody')); + if (tmTokenText && tmTokenText !== tokenText) { - result += ``; + dom.append(tbody, $('tr', undefined, + $('td.tiw-metadata-key', undefined, 'textmate token' as string), + $('td.tiw-metadata-value', undefined, `${tmTokenText} (${tmTokenText.length})`) + )); } - let scopes = ''; + const scopes = new Array(); for (let i = textMateTokenInfo.token.scopes.length - 1; i >= 0; i--) { - scopes += escape(textMateTokenInfo.token.scopes[i]); + scopes.push(textMateTokenInfo.token.scopes[i]); if (i > 0) { - scopes += '
'; + scopes.push($('br')); } } - result += `
`; + dom.append(tbody, $('tr', undefined, + $('td.tiw-metadata-key', undefined, 'textmate scopes' as string), + $('td.tiw-metadata-value.tiw-metadata-scopes', undefined, ...scopes), + )); let matchingRule = findMatchingThemeRule(theme, textMateTokenInfo.token.scopes, false); const semForeground = semanticTokenInfo?.metadata?.foreground; if (matchingRule) { - let defValue = `${matchingRule.rawSelector}\n${JSON.stringify(matchingRule.settings, null, '\t')}`; if (semForeground !== textMateTokenInfo.metadata.foreground) { + let defValue = $('code.tiw-theme-selector', undefined, + matchingRule.rawSelector, $('br'), JSON.stringify(matchingRule.settings, null, '\t')); if (semForeground) { - defValue = `${defValue}`; + defValue = $('s', undefined, defValue); } - result += ``; + dom.append(tbody, $('tr', undefined, + $('td.tiw-metadata-key', undefined, 'foreground'), + $('td.tiw-metadata-value', undefined, defValue), + )); } } else if (!semForeground) { - result += ``; + dom.append(tbody, $('tr', undefined, + $('td.tiw-metadata-key', undefined, 'foreground'), + $('td.tiw-metadata-value', undefined, 'No theme selector' as string), + )); } - result += ``; } - return result; } - private _formatMetadata(semantic?: IDecodedMetadata, tm?: IDecodedMetadata) { - let result = ''; + private _formatMetadata(semantic?: IDecodedMetadata, tm?: IDecodedMetadata): Array { + const elements = new Array(); function render(property: 'foreground' | 'background') { let value = semantic?.[property] || tm?.[property]; if (value !== undefined) { const semanticStyle = semantic?.[property] ? 'tiw-metadata-semantic' : ''; - result += `${property}${value}`; - + elements.push($('tr', undefined, + $('td.tiw-metadata-key', undefined, property), + $(`td.tiw-metadata-value.${semanticStyle}`, undefined, value) + )); } return value; } @@ -377,17 +400,23 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { if (foreground && background) { const backgroundColor = Color.fromHex(background), foregroundColor = Color.fromHex(foreground); if (backgroundColor.isOpaque()) { - result += `contrast ratio${backgroundColor.getContrastRatio(foregroundColor.makeOpaque(backgroundColor)).toFixed(2)}`; + elements.push($('tr', undefined, + $('td.tiw-metadata-key', undefined, 'contrast ratio' as string), + $('td.tiw-metadata-value', undefined, backgroundColor.getContrastRatio(foregroundColor.makeOpaque(backgroundColor)).toFixed(2)) + )); } else { - result += 'Contrast ratio cannot be precise for background colors that use transparency'; + elements.push($('tr', undefined, + $('td.tiw-metadata-key', undefined, 'Contrast ratio cannot be precise for background colors that use transparency' as string), + $('td.tiw-metadata-value') + )); } } - let fontStyleLabels: string[] = []; + const fontStyleLabels = new Array(); function addStyle(key: 'bold' | 'italic' | 'underline') { if (semantic && semantic[key]) { - fontStyleLabels.push(``); + fontStyleLabels.push($('span.tiw-metadata-semantic', undefined, key)); } else if (tm && tm[key]) { fontStyleLabels.push(key); } @@ -396,9 +425,12 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { addStyle('italic'); addStyle('underline'); if (fontStyleLabels.length) { - result += `font style${fontStyleLabels.join(' ')}`; + elements.push($('tr', undefined, + $('td.tiw-metadata-key', undefined, 'font style' as string), + $('td.tiw-metadata-value', undefined, fontStyleLabels.join(' ')) + )); } - return result; + return elements; } private _decodeMetadata(metadata: number): IDecodedMetadata { @@ -549,9 +581,10 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { return null; } - private _renderTokenStyleDefinition(definition: TokenStyleDefinition | undefined, property: keyof TokenStyleData): string { + private _renderTokenStyleDefinition(definition: TokenStyleDefinition | undefined, property: keyof TokenStyleData): Array { + const elements = new Array(); if (definition === undefined) { - return ''; + return elements; } const theme = this._themeService.getColorTheme() as ColorThemeData; @@ -561,20 +594,27 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { const matchingRule = scopesDefinition[property]; if (matchingRule && scopesDefinition.scope) { const strScopes = Array.isArray(matchingRule.scope) ? matchingRule.scope.join(', ') : String(matchingRule.scope); - return `${escape(scopesDefinition.scope.join(' '))}
${strScopes}\n${JSON.stringify(matchingRule.settings, null, '\t')}`; + elements.push( + scopesDefinition.scope.join(' '), + $('br'), + $('code.tiw-theme-selector', undefined, strScopes, $('br'), JSON.stringify(matchingRule.settings, null, '\t'))); + return elements; } - return ''; + return elements; } else if (SemanticTokenRule.is(definition)) { const scope = theme.getTokenStylingRuleScope(definition); if (scope === 'setting') { - return `User settings: ${definition.selector.id} - ${this._renderStyleProperty(definition.style, property)}`; + elements.push(`User settings: ${definition.selector.id} - ${this._renderStyleProperty(definition.style, property)}`); + return elements; } else if (scope === 'theme') { - return `Color theme: ${definition.selector.id} - ${this._renderStyleProperty(definition.style, property)}`; + elements.push(`Color theme: ${definition.selector.id} - ${this._renderStyleProperty(definition.style, property)}`); + return elements; } - return ''; + return elements; } else { const style = theme.resolveTokenStyleValue(definition); - return `Default: ${style ? this._renderStyleProperty(style, property) : ''}`; + elements.push(`Default: ${style ? this._renderStyleProperty(style, property) : ''}`); + return elements; } } diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index 472a63dd47..93a645ceba 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -535,11 +535,11 @@ function fillInActions(groups: [string, Array(target) ? target : target.primary; + const to = Array.isArray(target) ? target : target.primary; to.unshift(...actions); } else { - const to = Array.isArray(target) ? target : target.secondary; + const to = Array.isArray(target) ? target : target.secondary; if (to.length > 0) { to.push(new Separator()); diff --git a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index f8e2f2a285..6c31c54d61 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -35,7 +35,6 @@ import { isSafari } from 'vs/base/browser/browser'; import { registerThemingParticipant, themeColorFromId } from 'vs/platform/theme/common/themeService'; import { registerColor } from 'vs/platform/theme/common/colorRegistry'; import { ILabelService } from 'vs/platform/label/common/label'; -import { debugAdapterRegisteredEmitter } from 'vs/workbench/contrib/debug/browser/debugConfigurationManager'; const $ = dom.$; @@ -169,20 +168,16 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi ) { this.breakpointWidgetVisible = CONTEXT_BREAKPOINT_WIDGET_VISIBLE.bindTo(contextKeyService); this.setDecorationsScheduler = new RunOnceScheduler(() => this.setDecorations(), 30); - const manager = this.debugService.getConfigurationManager(); - if (manager.hasDebuggers()) { - this.registerListeners(); - this.setDecorationsScheduler.schedule(); - } else { - this.toDispose.push(debugAdapterRegisteredEmitter.event(() => { - this.registerListeners(); - this.setDecorationsScheduler.schedule(); - })); - } + this.registerListeners(); + this.setDecorationsScheduler.schedule(); } private registerListeners(): void { this.toDispose.push(this.editor.onMouseDown(async (e: IEditorMouseEvent) => { + if (!this.debugService.getConfigurationManager().hasDebuggers()) { + return; + } + const data = e.target.detail as IMarginData; const model = this.editor.getModel(); if (!e.target.position || !model || e.target.type !== MouseTargetType.GUTTER_GLYPH_MARGIN || data.isAfterLines || !this.marginFreeFromNonDebugDecorations(e.target.position.lineNumber)) { @@ -257,6 +252,10 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi * 2. When users click on line numbers, the breakpoint hint displays immediately, however it doesn't create the breakpoint unless users click on the left gutter. On a touch screen, it's hard to click on that small area. */ this.toDispose.push(this.editor.onMouseMove((e: IEditorMouseEvent) => { + if (!this.debugService.getConfigurationManager().hasDebuggers()) { + return; + } + let showBreakpointHintAtLineNumber = -1; const model = this.editor.getModel(); if (model && e.target.position && (e.target.type === MouseTargetType.GUTTER_GLYPH_MARGIN || e.target.type === MouseTargetType.GUTTER_LINE_NUMBERS) && this.debugService.getConfigurationManager().canSetBreakpointsIn(model) && diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 355a58afaa..5f994a915f 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -17,7 +17,7 @@ import { CallStackView } from 'vs/workbench/contrib/debug/browser/callStackView' import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { IDebugService, VIEWLET_ID, DEBUG_PANEL_ID, CONTEXT_IN_DEBUG_MODE, INTERNAL_CONSOLE_OPTIONS_SCHEMA, - CONTEXT_DEBUG_STATE, VARIABLES_VIEW_ID, CALLSTACK_VIEW_ID, WATCH_VIEW_ID, BREAKPOINTS_VIEW_ID, LOADED_SCRIPTS_VIEW_ID, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_DEBUG_UX, BREAKPOINT_EDITOR_CONTRIBUTION_ID, REPL_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, EDITOR_CONTRIBUTION_ID, + CONTEXT_DEBUG_STATE, VARIABLES_VIEW_ID, CALLSTACK_VIEW_ID, WATCH_VIEW_ID, BREAKPOINTS_VIEW_ID, LOADED_SCRIPTS_VIEW_ID, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_DEBUG_UX, BREAKPOINT_EDITOR_CONTRIBUTION_ID, REPL_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, EDITOR_CONTRIBUTION_ID, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_SET_VARIABLE_SUPPORTED, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, } from 'vs/workbench/contrib/debug/common/debug'; import { StartAction, AddFunctionBreakpointAction, ConfigureAction, DisableAllBreakpointsAction, EnableAllBreakpointsAction, RemoveAllBreakpointsAction, RunAction, ReapplyBreakpointsAction, SelectAndStartAction } from 'vs/workbench/contrib/debug/browser/debugActions'; import { DebugToolBar } from 'vs/workbench/contrib/debug/browser/debugToolBar'; @@ -34,7 +34,7 @@ import { launchSchemaId } from 'vs/workbench/services/configuration/common/confi import { LoadedScriptsView } from 'vs/workbench/contrib/debug/browser/loadedScriptsView'; import { ADD_LOG_POINT_ID, TOGGLE_CONDITIONAL_BREAKPOINT_ID, TOGGLE_BREAKPOINT_ID, RunToCursorAction, registerEditorActions } from 'vs/workbench/contrib/debug/browser/debugEditorActions'; import { WatchExpressionsView } from 'vs/workbench/contrib/debug/browser/watchExpressionsView'; -import { VariablesView } from 'vs/workbench/contrib/debug/browser/variablesView'; +import { VariablesView, SET_VARIABLE_ID, COPY_VALUE_ID, BREAK_WHEN_VALUE_CHANGES_ID, COPY_EVALUATE_PATH_ID, ADD_TO_WATCH_ID } 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 { WelcomeView } from 'vs/workbench/contrib/debug/browser/welcomeView'; @@ -51,25 +51,20 @@ import { DebugProgressContribution } from 'vs/workbench/contrib/debug/browser/de import { DebugTitleContribution } from 'vs/workbench/contrib/debug/browser/debugTitle'; import { Codicon } from 'vs/base/common/codicons'; import { registerColors } from 'vs/workbench/contrib/debug/browser/debugColors'; -import { debugAdapterRegisteredEmitter } from 'vs/workbench/contrib/debug/browser/debugConfigurationManager'; import { DebugEditorContribution } from 'vs/workbench/contrib/debug/browser/debugEditorContribution'; const registry = Registry.as(WorkbenchActionRegistryExtensions.WorkbenchActions); -// register service -debugAdapterRegisteredEmitter.event(() => { - // Register these contributions lazily only once a debug adapter extension has been registered - registerWorkbenchContributions(); - registerColors(); - registerCommandsAndActions(); - registerDebugMenu(); -}); +const debugCategory = nls.localize('debugCategory', "Debug"); +const runCategroy = nls.localize('runCategory', "Run"); +registerWorkbenchContributions(); +registerColors(); +registerCommandsAndActions(); +registerDebugMenu(); registerEditorActions(); registerCommands(); registerDebugPanel(); -const debugCategory = nls.localize('debugCategory', "Debug"); -const runCategroy = nls.localize('runCategory', "Run"); -registry.registerWorkbenchAction(SyncActionDescriptor.from(StartAction, { primary: KeyCode.F5 }, CONTEXT_IN_DEBUG_MODE.toNegated()), 'Debug: Start Debugging', debugCategory); -registry.registerWorkbenchAction(SyncActionDescriptor.from(RunAction, { primary: KeyMod.CtrlCmd | KeyCode.F5, mac: { primary: KeyMod.WinCtrl | KeyCode.F5 } }), 'Run: Start Without Debugging', runCategroy); +registry.registerWorkbenchAction(SyncActionDescriptor.from(StartAction, { primary: KeyCode.F5 }, CONTEXT_IN_DEBUG_MODE.toNegated()), 'Debug: Start Debugging', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); +registry.registerWorkbenchAction(SyncActionDescriptor.from(RunAction, { primary: KeyMod.CtrlCmd | KeyCode.F5, mac: { primary: KeyMod.WinCtrl | KeyCode.F5 } }), 'Run: Start Without Debugging', runCategroy, CONTEXT_DEBUGGERS_AVAILABLE); registerSingleton(IDebugService, DebugService, true); registerDebugView(); @@ -106,18 +101,18 @@ function regsiterEditorContributions(): void { function registerCommandsAndActions(): void { - registry.registerWorkbenchAction(SyncActionDescriptor.from(ConfigureAction), 'Debug: Open launch.json', debugCategory); - registry.registerWorkbenchAction(SyncActionDescriptor.from(AddFunctionBreakpointAction), 'Debug: Add Function Breakpoint', debugCategory); - registry.registerWorkbenchAction(SyncActionDescriptor.from(ReapplyBreakpointsAction), 'Debug: Reapply All Breakpoints', debugCategory); - registry.registerWorkbenchAction(SyncActionDescriptor.from(RemoveAllBreakpointsAction), 'Debug: Remove All Breakpoints', debugCategory); - registry.registerWorkbenchAction(SyncActionDescriptor.from(EnableAllBreakpointsAction), 'Debug: Enable All Breakpoints', debugCategory); - registry.registerWorkbenchAction(SyncActionDescriptor.from(DisableAllBreakpointsAction), 'Debug: Disable All Breakpoints', debugCategory); - registry.registerWorkbenchAction(SyncActionDescriptor.from(SelectAndStartAction), 'Debug: Select and Start Debugging', debugCategory); - registry.registerWorkbenchAction(SyncActionDescriptor.from(ClearReplAction), 'Debug: Clear Console', debugCategory); + registry.registerWorkbenchAction(SyncActionDescriptor.from(ConfigureAction), 'Debug: Open launch.json', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); + registry.registerWorkbenchAction(SyncActionDescriptor.from(AddFunctionBreakpointAction), 'Debug: Add Function Breakpoint', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); + registry.registerWorkbenchAction(SyncActionDescriptor.from(ReapplyBreakpointsAction), 'Debug: Reapply All Breakpoints', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); + registry.registerWorkbenchAction(SyncActionDescriptor.from(RemoveAllBreakpointsAction), 'Debug: Remove All Breakpoints', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); + registry.registerWorkbenchAction(SyncActionDescriptor.from(EnableAllBreakpointsAction), 'Debug: Enable All Breakpoints', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); + registry.registerWorkbenchAction(SyncActionDescriptor.from(DisableAllBreakpointsAction), 'Debug: Disable All Breakpoints', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); + registry.registerWorkbenchAction(SyncActionDescriptor.from(SelectAndStartAction), 'Debug: Select and Start Debugging', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); + registry.registerWorkbenchAction(SyncActionDescriptor.from(ClearReplAction), 'Debug: Clear Console', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); const registerDebugCommandPaletteItem = (id: string, title: string, when?: ContextKeyExpression, precondition?: ContextKeyExpression) => { MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - when, + when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, when), command: { id, title: `Debug: ${title}`, @@ -169,8 +164,8 @@ function registerCommandsAndActions(): void { registerDebugToolBarItem(REVERSE_CONTINUE_ID, nls.localize('reverseContinue', "Reverse"), 60, { id: 'codicon/debug-reverse-continue' }, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); // Debug callstack context menu - const registerDebugCallstackItem = (id: string, title: string, order: number, when?: ContextKeyExpression, precondition?: ContextKeyExpression, group = 'navigation') => { - MenuRegistry.appendMenuItem(MenuId.DebugCallStackContext, { + const registerDebugViewMenuItem = (menuId: MenuId, id: string, title: string, order: number, when?: ContextKeyExpression, precondition?: ContextKeyExpression, group = 'navigation') => { + MenuRegistry.appendMenuItem(menuId, { group, when, order, @@ -181,16 +176,22 @@ function registerCommandsAndActions(): void { } }); }; - registerDebugCallstackItem(RESTART_SESSION_ID, RESTART_LABEL, 10, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('session')); - registerDebugCallstackItem(STOP_ID, STOP_LABEL, 20, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('session')); - registerDebugCallstackItem(PAUSE_ID, PAUSE_LABEL, 10, ContextKeyExpr.and(CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), CONTEXT_DEBUG_STATE.isEqualTo('running'))); - registerDebugCallstackItem(CONTINUE_ID, CONTINUE_LABEL, 10, ContextKeyExpr.and(CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), CONTEXT_DEBUG_STATE.isEqualTo('stopped'))); - registerDebugCallstackItem(STEP_OVER_ID, STEP_OVER_LABEL, 20, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), CONTEXT_DEBUG_STATE.isEqualTo('stopped')); - registerDebugCallstackItem(STEP_INTO_ID, STEP_INTO_LABEL, 30, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), CONTEXT_DEBUG_STATE.isEqualTo('stopped')); - registerDebugCallstackItem(STEP_OUT_ID, STEP_OUT_LABEL, 40, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), CONTEXT_DEBUG_STATE.isEqualTo('stopped')); - registerDebugCallstackItem(TERMINATE_THREAD_ID, nls.localize('terminateThread', "Terminate Thread"), 10, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), undefined, 'termination'); - registerDebugCallstackItem(RESTART_FRAME_ID, nls.localize('restartFrame', "Restart Frame"), 10, ContextKeyExpr.and(CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('stackFrame'), CONTEXT_RESTART_FRAME_SUPPORTED)); - registerDebugCallstackItem(COPY_STACK_TRACE_ID, nls.localize('copyStackTrace', "Copy Call Stack"), 20, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('stackFrame')); + registerDebugViewMenuItem(MenuId.DebugCallStackContext, RESTART_SESSION_ID, RESTART_LABEL, 10, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('session')); + registerDebugViewMenuItem(MenuId.DebugCallStackContext, STOP_ID, STOP_LABEL, 20, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('session')); + registerDebugViewMenuItem(MenuId.DebugCallStackContext, PAUSE_ID, PAUSE_LABEL, 10, ContextKeyExpr.and(CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), CONTEXT_DEBUG_STATE.isEqualTo('running'))); + registerDebugViewMenuItem(MenuId.DebugCallStackContext, CONTINUE_ID, CONTINUE_LABEL, 10, ContextKeyExpr.and(CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), CONTEXT_DEBUG_STATE.isEqualTo('stopped'))); + registerDebugViewMenuItem(MenuId.DebugCallStackContext, STEP_OVER_ID, STEP_OVER_LABEL, 20, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), CONTEXT_DEBUG_STATE.isEqualTo('stopped')); + registerDebugViewMenuItem(MenuId.DebugCallStackContext, STEP_INTO_ID, STEP_INTO_LABEL, 30, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), CONTEXT_DEBUG_STATE.isEqualTo('stopped')); + registerDebugViewMenuItem(MenuId.DebugCallStackContext, STEP_OUT_ID, STEP_OUT_LABEL, 40, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), CONTEXT_DEBUG_STATE.isEqualTo('stopped')); + registerDebugViewMenuItem(MenuId.DebugCallStackContext, TERMINATE_THREAD_ID, nls.localize('terminateThread', "Terminate Thread"), 10, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('thread'), undefined, 'termination'); + registerDebugViewMenuItem(MenuId.DebugCallStackContext, RESTART_FRAME_ID, nls.localize('restartFrame', "Restart Frame"), 10, ContextKeyExpr.and(CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('stackFrame'), CONTEXT_RESTART_FRAME_SUPPORTED)); + registerDebugViewMenuItem(MenuId.DebugCallStackContext, COPY_STACK_TRACE_ID, nls.localize('copyStackTrace', "Copy Call Stack"), 20, CONTEXT_CALLSTACK_ITEM_TYPE.isEqualTo('stackFrame')); + + registerDebugViewMenuItem(MenuId.DebugVariablesContext, SET_VARIABLE_ID, nls.localize('setValue', "Set Value"), 10, CONTEXT_SET_VARIABLE_SUPPORTED); + registerDebugViewMenuItem(MenuId.DebugVariablesContext, COPY_VALUE_ID, nls.localize('copyValue', "Copy Value"), 20); + registerDebugViewMenuItem(MenuId.DebugVariablesContext, COPY_EVALUATE_PATH_ID, nls.localize('copyAsExpression', "Copy as Expression"), 30, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT); + registerDebugViewMenuItem(MenuId.DebugVariablesContext, ADD_TO_WATCH_ID, nls.localize('addToWatchExpressions', "Add to Watch"), 10, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT, undefined, '3_watch'); + registerDebugViewMenuItem(MenuId.DebugVariablesContext, BREAK_WHEN_VALUE_CHANGES_ID, nls.localize('breakWhenValueChanges', "Break When Value Changes"), 20, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, undefined, '5_breakpoint'); // Touch Bar if (isMacintosh) { @@ -202,7 +203,7 @@ function registerCommandsAndActions(): void { title, icon: { dark: iconUri } }, - when, + when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, when), group: '9_debug', order }); @@ -249,7 +250,8 @@ function registerDebugMenu(): void { id: StartAction.ID, title: nls.localize({ key: 'miStartDebugging', comment: ['&& denotes a mnemonic'] }, "&&Start Debugging") }, - order: 1 + order: 1, + when: CONTEXT_DEBUGGERS_AVAILABLE }); MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { @@ -258,7 +260,8 @@ function registerDebugMenu(): void { id: RunAction.ID, title: nls.localize({ key: 'miRun', comment: ['&& denotes a mnemonic'] }, "Run &&Without Debugging") }, - order: 2 + order: 2, + when: CONTEXT_DEBUGGERS_AVAILABLE }); MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { @@ -268,7 +271,8 @@ function registerDebugMenu(): void { title: nls.localize({ key: 'miStopDebugging', comment: ['&& denotes a mnemonic'] }, "&&Stop Debugging"), precondition: CONTEXT_IN_DEBUG_MODE }, - order: 3 + order: 3, + when: CONTEXT_DEBUGGERS_AVAILABLE }); MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { @@ -278,7 +282,8 @@ function registerDebugMenu(): void { title: nls.localize({ key: 'miRestart Debugging', comment: ['&& denotes a mnemonic'] }, "&&Restart Debugging"), precondition: CONTEXT_IN_DEBUG_MODE }, - order: 4 + order: 4, + when: CONTEXT_DEBUGGERS_AVAILABLE }); // Configuration @@ -288,7 +293,8 @@ function registerDebugMenu(): void { id: ConfigureAction.ID, title: nls.localize({ key: 'miOpenConfigurations', comment: ['&& denotes a mnemonic'] }, "Open &&Configurations") }, - order: 1 + order: 1, + when: CONTEXT_DEBUGGERS_AVAILABLE }); MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { @@ -297,7 +303,8 @@ function registerDebugMenu(): void { id: ADD_CONFIGURATION_ID, title: nls.localize({ key: 'miAddConfiguration', comment: ['&& denotes a mnemonic'] }, "A&&dd Configuration...") }, - order: 2 + order: 2, + when: CONTEXT_DEBUGGERS_AVAILABLE }); // Step Commands @@ -308,7 +315,8 @@ function registerDebugMenu(): void { title: nls.localize({ key: 'miStepOver', comment: ['&& denotes a mnemonic'] }, "Step &&Over"), precondition: CONTEXT_DEBUG_STATE.isEqualTo('stopped') }, - order: 1 + order: 1, + when: CONTEXT_DEBUGGERS_AVAILABLE }); MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { @@ -318,7 +326,8 @@ function registerDebugMenu(): void { title: nls.localize({ key: 'miStepInto', comment: ['&& denotes a mnemonic'] }, "Step &&Into"), precondition: CONTEXT_DEBUG_STATE.isEqualTo('stopped') }, - order: 2 + order: 2, + when: CONTEXT_DEBUGGERS_AVAILABLE }); MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { @@ -328,7 +337,8 @@ function registerDebugMenu(): void { title: nls.localize({ key: 'miStepOut', comment: ['&& denotes a mnemonic'] }, "Step O&&ut"), precondition: CONTEXT_DEBUG_STATE.isEqualTo('stopped') }, - order: 3 + order: 3, + when: CONTEXT_DEBUGGERS_AVAILABLE }); MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { @@ -338,7 +348,8 @@ function registerDebugMenu(): void { title: nls.localize({ key: 'miContinue', comment: ['&& denotes a mnemonic'] }, "&&Continue"), precondition: CONTEXT_DEBUG_STATE.isEqualTo('stopped') }, - order: 4 + order: 4, + when: CONTEXT_DEBUGGERS_AVAILABLE }); // New Breakpoints @@ -348,7 +359,8 @@ function registerDebugMenu(): void { id: TOGGLE_BREAKPOINT_ID, title: nls.localize({ key: 'miToggleBreakpoint', comment: ['&& denotes a mnemonic'] }, "Toggle &&Breakpoint") }, - order: 1 + order: 1, + when: CONTEXT_DEBUGGERS_AVAILABLE }); MenuRegistry.appendMenuItem(MenuId.MenubarNewBreakpointMenu, { @@ -357,7 +369,8 @@ function registerDebugMenu(): void { id: TOGGLE_CONDITIONAL_BREAKPOINT_ID, title: nls.localize({ key: 'miConditionalBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&Conditional Breakpoint...") }, - order: 1 + order: 1, + when: CONTEXT_DEBUGGERS_AVAILABLE }); MenuRegistry.appendMenuItem(MenuId.MenubarNewBreakpointMenu, { @@ -366,7 +379,8 @@ function registerDebugMenu(): void { id: TOGGLE_INLINE_BREAKPOINT_ID, title: nls.localize({ key: 'miInlineBreakpoint', comment: ['&& denotes a mnemonic'] }, "Inline Breakp&&oint") }, - order: 2 + order: 2, + when: CONTEXT_DEBUGGERS_AVAILABLE }); MenuRegistry.appendMenuItem(MenuId.MenubarNewBreakpointMenu, { @@ -375,7 +389,8 @@ function registerDebugMenu(): void { id: AddFunctionBreakpointAction.ID, title: nls.localize({ key: 'miFunctionBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&Function Breakpoint...") }, - order: 3 + order: 3, + when: CONTEXT_DEBUGGERS_AVAILABLE }); MenuRegistry.appendMenuItem(MenuId.MenubarNewBreakpointMenu, { @@ -384,14 +399,16 @@ function registerDebugMenu(): void { id: ADD_LOG_POINT_ID, title: nls.localize({ key: 'miLogPoint', comment: ['&& denotes a mnemonic'] }, "&&Logpoint...") }, - order: 4 + order: 4, + when: CONTEXT_DEBUGGERS_AVAILABLE }); MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { group: '4_new_breakpoint', title: nls.localize({ key: 'miNewBreakpoint', comment: ['&& denotes a mnemonic'] }, "&&New Breakpoint"), submenu: MenuId.MenubarNewBreakpointMenu, - order: 2 + order: 2, + when: CONTEXT_DEBUGGERS_AVAILABLE }); // Modify Breakpoints @@ -401,7 +418,8 @@ function registerDebugMenu(): void { id: EnableAllBreakpointsAction.ID, title: nls.localize({ key: 'miEnableAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "&&Enable All Breakpoints") }, - order: 1 + order: 1, + when: CONTEXT_DEBUGGERS_AVAILABLE }); MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { @@ -410,7 +428,8 @@ function registerDebugMenu(): void { id: DisableAllBreakpointsAction.ID, title: nls.localize({ key: 'miDisableAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "Disable A&&ll Breakpoints") }, - order: 2 + order: 2, + when: CONTEXT_DEBUGGERS_AVAILABLE }); MenuRegistry.appendMenuItem(MenuId.MenubarDebugMenu, { @@ -419,7 +438,8 @@ function registerDebugMenu(): void { id: RemoveAllBreakpointsAction.ID, title: nls.localize({ key: 'miRemoveAllBreakpoints', comment: ['&& denotes a mnemonic'] }, "Remove &&All Breakpoints") }, - order: 3 + order: 3, + when: CONTEXT_DEBUGGERS_AVAILABLE }); // Install Debuggers @@ -453,10 +473,11 @@ function registerDebugPanel(): void { containerIcon: Codicon.debugConsole.classNames, canToggleVisibility: false, canMoveView: true, + when: CONTEXT_DEBUGGERS_AVAILABLE, ctorDescriptor: new SyncDescriptor(Repl), }], VIEW_CONTAINER); - registry.registerWorkbenchAction(SyncActionDescriptor.from(OpenDebugConsoleAction, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Y }), 'View: Debug Console', nls.localize('view', "View")); + registry.registerWorkbenchAction(SyncActionDescriptor.from(OpenDebugConsoleAction, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Y }), 'View: Debug Console', nls.localize('view', "View"), CONTEXT_DEBUGGERS_AVAILABLE); } function registerDebugView(): void { diff --git a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts index a2fd4edcb7..1bafe60ee2 100644 --- a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts @@ -21,7 +21,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IDebugConfigurationProvider, ICompound, IDebugConfiguration, IConfig, IGlobalConfig, IConfigurationManager, ILaunch, IDebugAdapterDescriptorFactory, IDebugAdapter, IDebugSession, IAdapterDescriptor, CONTEXT_DEBUG_CONFIGURATION_TYPE, IDebugAdapterFactory, IConfigPresentation } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugConfigurationProvider, ICompound, IDebugConfiguration, IConfig, IGlobalConfig, IConfigurationManager, ILaunch, IDebugAdapterDescriptorFactory, IDebugAdapter, IDebugSession, IAdapterDescriptor, CONTEXT_DEBUG_CONFIGURATION_TYPE, IDebugAdapterFactory, IConfigPresentation, CONTEXT_DEBUGGERS_AVAILABLE } from 'vs/workbench/contrib/debug/common/debug'; import { Debugger } from 'vs/workbench/contrib/debug/common/debugger'; import { IEditorService, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -49,8 +49,6 @@ const DEBUG_SELECTED_ROOT = 'debug.selectedroot'; interface IDynamicPickItem { label: string, launch: ILaunch, config: IConfig } -export const debugAdapterRegisteredEmitter = new Emitter(); - export class ConfigurationManager implements IConfigurationManager { private debuggers: Debugger[]; private breakpointModeIdsSet = new Set(); @@ -64,6 +62,7 @@ export class ConfigurationManager implements IConfigurationManager { private adapterDescriptorFactories: IDebugAdapterDescriptorFactory[]; private debugAdapterFactories = new Map(); private debugConfigurationTypeContext: IContextKey; + private debuggersAvailable: IContextKey; private readonly _onDidRegisterDebugger = new Emitter(); constructor( @@ -87,6 +86,7 @@ export class ConfigurationManager implements IConfigurationManager { const previousSelectedRoot = this.storageService.get(DEBUG_SELECTED_ROOT, StorageScope.WORKSPACE); const previousSelectedLaunch = this.launches.find(l => l.uri.toString() === previousSelectedRoot); this.debugConfigurationTypeContext = CONTEXT_DEBUG_CONFIGURATION_TYPE.bindTo(contextKeyService); + this.debuggersAvailable = CONTEXT_DEBUGGERS_AVAILABLE.bindTo(contextKeyService); if (previousSelectedLaunch && previousSelectedLaunch.getConfigurationNames().length) { this.selectConfiguration(previousSelectedLaunch, this.storageService.get(DEBUG_SELECTED_CONFIG_NAME_KEY, StorageScope.WORKSPACE)); } else if (this.launches.length > 0) { @@ -97,11 +97,9 @@ export class ConfigurationManager implements IConfigurationManager { // debuggers registerDebugAdapterFactory(debugTypes: string[], debugAdapterLauncher: IDebugAdapterFactory): IDisposable { - const firstTimeRegistration = debugTypes.length && this.debugAdapterFactories.size === 0; debugTypes.forEach(debugType => this.debugAdapterFactories.set(debugType, debugAdapterLauncher)); - if (firstTimeRegistration) { - debugAdapterRegisteredEmitter.fire(); - } + this.debuggersAvailable.set(this.debugAdapterFactories.size > 0); + this._onDidRegisterDebugger.fire(); return { dispose: () => { @@ -428,7 +426,6 @@ export class ConfigurationManager implements IConfigurationManager { }); this.setCompoundSchemaValues(); - this._onDidRegisterDebugger.fire(); }); breakpointsExtPoint.setHandler((extensions, delta) => { diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts index 58911fcb63..a01c96c2fd 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts @@ -9,7 +9,7 @@ import { Range } from 'vs/editor/common/core/range'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ServicesAccessor, registerEditorAction, EditorAction, IActionOptions } from 'vs/editor/browser/editorExtensions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { IDebugService, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE, State, IDebugEditorContribution, EDITOR_CONTRIBUTION_ID, BreakpointWidgetContext, IBreakpoint, BREAKPOINT_EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution, REPL_VIEW_ID, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, WATCH_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE, State, IDebugEditorContribution, EDITOR_CONTRIBUTION_ID, BreakpointWidgetContext, IBreakpoint, BREAKPOINT_EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution, REPL_VIEW_ID, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, WATCH_VIEW_ID, CONTEXT_DEBUGGERS_AVAILABLE } from 'vs/workbench/contrib/debug/common/debug'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { openBreakpointSource } from 'vs/workbench/contrib/debug/browser/breakpointsView'; @@ -27,7 +27,7 @@ class ToggleBreakpointAction extends EditorAction { id: TOGGLE_BREAKPOINT_ID, label: nls.localize('toggleBreakpointAction', "Debug: Toggle Breakpoint"), alias: 'Debug: Toggle Breakpoint', - precondition: undefined, + precondition: CONTEXT_DEBUGGERS_AVAILABLE, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyCode.F9, @@ -66,7 +66,7 @@ class ConditionalBreakpointAction extends EditorAction { id: TOGGLE_CONDITIONAL_BREAKPOINT_ID, label: nls.localize('conditionalBreakpointEditorAction', "Debug: Add Conditional Breakpoint..."), alias: 'Debug: Add Conditional Breakpoint...', - precondition: undefined + precondition: CONTEXT_DEBUGGERS_AVAILABLE }); } @@ -88,7 +88,7 @@ class LogPointAction extends EditorAction { id: ADD_LOG_POINT_ID, label: nls.localize('logPointEditorAction', "Debug: Add Logpoint..."), alias: 'Debug: Add Logpoint...', - precondition: undefined + precondition: CONTEXT_DEBUGGERS_AVAILABLE }); } @@ -339,7 +339,7 @@ class GoToNextBreakpointAction extends GoToBreakpointAction { id: 'editor.debug.action.goToNextBreakpoint', label: nls.localize('goToNextBreakpoint', "Debug: Go To Next Breakpoint"), alias: 'Debug: Go To Next Breakpoint', - precondition: undefined + precondition: CONTEXT_DEBUGGERS_AVAILABLE }); } } @@ -350,7 +350,7 @@ class GoToPreviousBreakpointAction extends GoToBreakpointAction { id: 'editor.debug.action.goToPreviousBreakpoint', label: nls.localize('goToPreviousBreakpoint', "Debug: Go To Previous Breakpoint"), alias: 'Debug: Go To Previous Breakpoint', - precondition: undefined + precondition: CONTEXT_DEBUGGERS_AVAILABLE }); } } diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 8d37cca7a2..b06ba110ac 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -111,7 +111,7 @@ export class DebugService implements IDebugService { this.debugState = CONTEXT_DEBUG_STATE.bindTo(contextKeyService); this.inDebugMode = CONTEXT_IN_DEBUG_MODE.bindTo(contextKeyService); this.debugUx = CONTEXT_DEBUG_UX.bindTo(contextKeyService); - this.debugUx.set(!!this.configurationManager.selectedConfiguration.name ? 'default' : 'simple'); + this.debugUx.set((this.configurationManager.hasDebuggers() && !!this.configurationManager.selectedConfiguration.name) ? 'default' : 'simple'); this.breakpointsExist = CONTEXT_BREAKPOINTS_EXIST.bindTo(contextKeyService); }); @@ -160,8 +160,8 @@ export class DebugService implements IDebugService { this.toDispose.push(this.viewModel.onDidFocusSession(() => { this.onStateChange(); })); - this.toDispose.push(this.configurationManager.onDidSelectConfiguration(() => { - this.debugUx.set(!!(this.state !== State.Inactive || this.configurationManager.selectedConfiguration.name) ? 'default' : 'simple'); + this.toDispose.push(Event.any(this.configurationManager.onDidRegisterDebugger, this.configurationManager.onDidSelectConfiguration)(() => { + this.debugUx.set(!!(this.state !== State.Inactive || (this.configurationManager.selectedConfiguration.name && this.configurationManager.hasDebuggers())) ? 'default' : 'simple'); })); this.toDispose.push(this.model.onDidChangeCallStack(() => { const numberOfSessions = this.model.getSessions().filter(s => !s.parentSession).length; @@ -243,7 +243,7 @@ export class DebugService implements IDebugService { this.debugState.set(getStateLabel(state)); this.inDebugMode.set(state !== State.Inactive); // Only show the simple ux if debug is not yet started and if no launch.json exists - this.debugUx.set(((state !== State.Inactive && state !== State.Initializing) || this.configurationManager.selectedConfiguration.name) ? 'default' : 'simple'); + this.debugUx.set(((state !== State.Inactive && state !== State.Initializing) || (this.configurationManager.hasDebuggers() && this.configurationManager.selectedConfiguration.name)) ? 'default' : 'simple'); }); this.previousState = state; this._onDidChangeState.fire(state); diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index a33a449342..b6700ca8ed 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -264,7 +264,7 @@ export class DebugSession implements IDebugSession { */ async launchOrAttach(config: IConfig): Promise { if (!this.raw) { - throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'launch or attach')); + throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'launch or attach')); } if (this.parentSession && this.parentSession.state === State.Inactive) { throw canceled(); @@ -327,7 +327,7 @@ export class DebugSession implements IDebugSession { */ async restart(): Promise { if (!this.raw) { - throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'restart')); + throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'restart')); } this.cancelAllRequests(); @@ -336,7 +336,7 @@ export class DebugSession implements IDebugSession { async sendBreakpoints(modelUri: URI, breakpointsToSend: IBreakpoint[], sourceModified: boolean): Promise { if (!this.raw) { - throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'breakpoints')); + throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'breakpoints')); } if (!this.raw.readyForBreakpoints) { @@ -370,7 +370,7 @@ export class DebugSession implements IDebugSession { async sendFunctionBreakpoints(fbpts: IFunctionBreakpoint[]): Promise { if (!this.raw) { - throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'function breakpoints')); + throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'function breakpoints')); } if (this.raw.readyForBreakpoints) { @@ -387,7 +387,7 @@ export class DebugSession implements IDebugSession { async sendExceptionBreakpoints(exbpts: IExceptionBreakpoint[]): Promise { if (!this.raw) { - throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'exception breakpoints')); + throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'exception breakpoints')); } if (this.raw.readyForBreakpoints) { @@ -397,7 +397,7 @@ export class DebugSession implements IDebugSession { async dataBreakpointInfo(name: string, variablesReference?: number): Promise<{ dataId: string | null, description: string, canPersist?: boolean } | undefined> { if (!this.raw) { - throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'data breakpoints info')); + throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'data breakpoints info')); } if (!this.raw.readyForBreakpoints) { throw new Error(localize('sessionNotReadyForBreakpoints', "Session is not ready for breakpoints")); @@ -409,7 +409,7 @@ export class DebugSession implements IDebugSession { async sendDataBreakpoints(dataBreakpoints: IDataBreakpoint[]): Promise { if (!this.raw) { - throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'data breakpoints')); + throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'data breakpoints')); } if (this.raw.readyForBreakpoints) { @@ -426,7 +426,7 @@ export class DebugSession implements IDebugSession { async breakpointsLocations(uri: URI, lineNumber: number): Promise { if (!this.raw) { - throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'breakpoints locations')); + throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'breakpoints locations')); } const source = this.getRawSource(uri); @@ -446,7 +446,7 @@ export class DebugSession implements IDebugSession { customRequest(request: string, args: any): Promise { if (!this.raw) { - throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", request)); + throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", request)); } return this.raw.custom(request, args); @@ -454,7 +454,7 @@ export class DebugSession implements IDebugSession { stackTrace(threadId: number, startFrame: number, levels: number, token: CancellationToken): Promise { if (!this.raw) { - throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'stackTrace')); + throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'stackTrace')); } const sessionToken = this.getNewCancellationToken(threadId, token); @@ -463,7 +463,7 @@ export class DebugSession implements IDebugSession { async exceptionInfo(threadId: number): Promise { if (!this.raw) { - throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'exceptionInfo')); + throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'exceptionInfo')); } const response = await this.raw.exceptionInfo({ threadId }); @@ -481,7 +481,7 @@ export class DebugSession implements IDebugSession { scopes(frameId: number, threadId: number): Promise { if (!this.raw) { - throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'scopes')); + throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'scopes')); } const token = this.getNewCancellationToken(threadId); @@ -490,7 +490,7 @@ export class DebugSession implements IDebugSession { variables(variablesReference: number, threadId: number | undefined, filter: 'indexed' | 'named' | undefined, start: number | undefined, count: number | undefined): Promise { if (!this.raw) { - throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'variables')); + throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'variables')); } const token = threadId ? this.getNewCancellationToken(threadId) : undefined; @@ -499,7 +499,7 @@ export class DebugSession implements IDebugSession { evaluate(expression: string, frameId: number, context?: string): Promise { if (!this.raw) { - throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'evaluate')); + throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'evaluate')); } return this.raw.evaluate({ expression, frameId, context }); @@ -507,7 +507,7 @@ export class DebugSession implements IDebugSession { async restartFrame(frameId: number, threadId: number): Promise { if (!this.raw) { - throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'restartFrame')); + throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'restartFrame')); } await this.raw.restartFrame({ frameId }, threadId); @@ -515,7 +515,7 @@ export class DebugSession implements IDebugSession { async next(threadId: number): Promise { if (!this.raw) { - throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'next')); + throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'next')); } await this.raw.next({ threadId }); @@ -523,7 +523,7 @@ export class DebugSession implements IDebugSession { async stepIn(threadId: number, targetId?: number): Promise { if (!this.raw) { - throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'stepIn')); + throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'stepIn')); } await this.raw.stepIn({ threadId, targetId }); @@ -531,7 +531,7 @@ export class DebugSession implements IDebugSession { async stepOut(threadId: number): Promise { if (!this.raw) { - throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'stepOut')); + throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'stepOut')); } await this.raw.stepOut({ threadId }); @@ -539,7 +539,7 @@ export class DebugSession implements IDebugSession { async stepBack(threadId: number): Promise { if (!this.raw) { - throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'stepBack')); + throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'stepBack')); } await this.raw.stepBack({ threadId }); @@ -547,7 +547,7 @@ export class DebugSession implements IDebugSession { async continue(threadId: number): Promise { if (!this.raw) { - throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'continue')); + throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'continue')); } await this.raw.continue({ threadId }); @@ -555,7 +555,7 @@ export class DebugSession implements IDebugSession { async reverseContinue(threadId: number): Promise { if (!this.raw) { - throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'reverse continue')); + throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'reverse continue')); } await this.raw.reverseContinue({ threadId }); @@ -563,7 +563,7 @@ export class DebugSession implements IDebugSession { async pause(threadId: number): Promise { if (!this.raw) { - throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'pause')); + throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'pause')); } await this.raw.pause({ threadId }); @@ -571,7 +571,7 @@ export class DebugSession implements IDebugSession { async terminateThreads(threadIds?: number[]): Promise { if (!this.raw) { - throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'terminateThreads')); + throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'terminateThreads')); } await this.raw.terminateThreads({ threadIds }); @@ -579,7 +579,7 @@ export class DebugSession implements IDebugSession { setVariable(variablesReference: number, name: string, value: string): Promise { if (!this.raw) { - throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'setVariable')); + throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'setVariable')); } return this.raw.setVariable({ variablesReference, name, value }); @@ -587,7 +587,7 @@ export class DebugSession implements IDebugSession { gotoTargets(source: DebugProtocol.Source, line: number, column?: number): Promise { if (!this.raw) { - throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'gotoTargets')); + throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'gotoTargets')); } return this.raw.gotoTargets({ source, line, column }); @@ -595,7 +595,7 @@ export class DebugSession implements IDebugSession { goto(threadId: number, targetId: number): Promise { if (!this.raw) { - throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'goto')); + throw new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'goto')); } return this.raw.goto({ threadId, targetId }); @@ -603,7 +603,7 @@ export class DebugSession implements IDebugSession { loadSource(resource: URI): Promise { if (!this.raw) { - return Promise.reject(new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'loadSource'))); + return Promise.reject(new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'loadSource'))); } const source = this.getSourceForUri(resource); @@ -621,7 +621,7 @@ export class DebugSession implements IDebugSession { async getLoadedSources(): Promise { if (!this.raw) { - return Promise.reject(new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'getLoadedSources'))); + return Promise.reject(new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'getLoadedSources'))); } const response = await this.raw.loadedSources({}); @@ -634,7 +634,7 @@ export class DebugSession implements IDebugSession { async completions(frameId: number | undefined, threadId: number, text: string, position: Position, overwriteBefore: number, token: CancellationToken): Promise { if (!this.raw) { - return Promise.reject(new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'completions'))); + return Promise.reject(new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'completions'))); } const sessionCancelationToken = this.getNewCancellationToken(threadId, token); @@ -648,7 +648,7 @@ export class DebugSession implements IDebugSession { async stepInTargets(frameId: number): Promise<{ id: number, label: string }[]> { if (!this.raw) { - return Promise.reject(new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'stepInTargets'))); + return Promise.reject(new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'stepInTargets'))); } const response = await this.raw.stepInTargets({ frameId }); @@ -657,7 +657,7 @@ export class DebugSession implements IDebugSession { async cancel(progressId: string): Promise { if (!this.raw) { - return Promise.reject(new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'cancel'))); + return Promise.reject(new Error(localize('noDebugAdapter', "No debugger available, can not send '{0}'", 'cancel'))); } return this.raw.cancel({ progressId }); @@ -813,7 +813,7 @@ export class DebugSession implements IDebugSession { } if (this.configurationService.getValue('debug').focusWindowOnBreak) { - this.hostService.focus(); + this.hostService.focus({ force: true /* Application may not be active */ }); } } } diff --git a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts index dcf12b968a..559eb7a433 100644 --- a/src/vs/workbench/contrib/debug/browser/debugToolBar.ts +++ b/src/vs/workbench/contrib/debug/browser/debugToolBar.ts @@ -21,7 +21,6 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { registerThemingParticipant, IThemeService, Themable } from 'vs/platform/theme/common/themeService'; import { registerColor, contrastBorder, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; import { localize } from 'vs/nls'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { RunOnceScheduler } from 'vs/base/common/async'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -58,7 +57,6 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution { @IThemeService themeService: IThemeService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IMenuService menuService: IMenuService, - @IContextMenuService contextMenuService: IContextMenuService, @IContextKeyService contextKeyService: IContextKeyService ) { super(themeService); diff --git a/src/vs/workbench/contrib/debug/browser/media/repl.css b/src/vs/workbench/contrib/debug/browser/media/repl.css index 9bd326213e..a57e67b51c 100644 --- a/src/vs/workbench/contrib/debug/browser/media/repl.css +++ b/src/vs/workbench/contrib/debug/browser/media/repl.css @@ -88,7 +88,7 @@ .monaco-workbench .repl .repl-tree .output.expression .code-italic { font-style: italic; } .monaco-workbench .repl .repl-tree .output.expression .code-underline { text-decoration: underline; } -.monaco-action-bar .action-item.panel-action-tree-filter-container { +.monaco-action-bar .action-item.repl-panel-filter-container { cursor: default; display: flex; } @@ -114,13 +114,7 @@ height: 25px; } -.panel > .title .monaco-action-bar .action-item.panel-action-tree-filter-container { - max-width: 400px; - min-width: 300px; +.panel > .title .monaco-action-bar .action-item.repl-panel-filter-container { + min-width: 200px; margin-right: 10px; } - -.monaco-action-bar .action-item.panel-action-tree-filter-container, -.panel > .title .monaco-action-bar .action-item.panel-action-tree-filter-container.grow { - flex: 1; -} diff --git a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts index 0d3090d416..df7c3d6de2 100644 --- a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts @@ -626,7 +626,7 @@ export class RawDebugSession implements IDisposable { // We are in shutdown silently complete completeDispatch(); } else { - errorDispatch(new Error(nls.localize('noDebugAdapter', "No debug adapter found. Can not send '{0}'.", command))); + errorDispatch(new Error(nls.localize('noDebugAdapter', "No debugger available found. Can not send '{0}'.", command))); } return; } diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index 5ec5ae92f5..6bec8b7308 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -59,7 +59,7 @@ import { ReplGroup } from 'vs/workbench/contrib/debug/common/replModel'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { EDITOR_FONT_DEFAULTS, EditorOption } from 'vs/editor/common/config/editorOptions'; import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from 'vs/base/browser/ui/mouseCursor/mouseCursor'; -import { ReplFilter, TreeFilterState, TreeFilterPanelActionViewItem } from 'vs/workbench/contrib/debug/browser/replFilter'; +import { ReplFilter, ReplFilterState, ReplFilterActionViewItem } from 'vs/workbench/contrib/debug/browser/replFilter'; const $ = dom.$; @@ -95,7 +95,8 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { private completionItemProvider: IDisposable | undefined; private modelChangeListener: IDisposable = Disposable.None; private filter: ReplFilter; - private filterState: TreeFilterState; + private filterState: ReplFilterState; + private filterActionViewItem: ReplFilterActionViewItem | undefined; constructor( options: IViewPaneOptions, @@ -120,11 +121,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { this.history = new HistoryNavigator(JSON.parse(this.storageService.get(HISTORY_STORAGE_KEY, StorageScope.WORKSPACE, '[]')), 50); this.filter = new ReplFilter(); - this.filterState = this._register(new TreeFilterState({ - filterText: '', - filterHistory: [], - layout: new dom.Dimension(0, 0), - })); + this.filterState = new ReplFilterState(); codeEditorService.registerDecorationType(DECORATION_KEY, {}); this.registerListeners(); @@ -248,13 +245,10 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { this.setMode(); })); - this._register(this.filterState.onDidChange((e) => { - if (e.filterText) { - this.filter.filterQuery = this.filterState.filterText; - if (this.tree) { - this.tree.refilter(); - } - } + this._register(this.filterState.onDidChange(() => { + this.filter.filterQuery = this.filterState.filterText; + this.tree.refilter(); + revealLastElement(this.tree); })); } @@ -276,8 +270,8 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { this.navigateHistory(false); } - focusRepl(): void { - this.tree.domFocus(); + focusFilter(): void { + this.filterActionViewItem?.focus(); } private setMode(): void { @@ -447,7 +441,6 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { this.replInputContainer.style.height = `${replInputHeight}px`; this.replInput.layout({ width: width - 30, height: replInputHeight }); - this.filterState.layout = new dom.Dimension(width, height); } focus(): void { @@ -458,7 +451,8 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { if (action.id === SelectReplAction.ID) { return this.instantiationService.createInstance(SelectReplActionViewItem, this.selectReplAction); } else if (action.id === FILTER_ACTION_ID) { - return this.instantiationService.createInstance(TreeFilterPanelActionViewItem, action, localize('workbench.debug.filter.placeholder', "Filter. E.g.: text, !exclude"), this.filterState); + this.filterActionViewItem = this.instantiationService.createInstance(ReplFilterActionViewItem, action, localize('workbench.debug.filter.placeholder', "Filter (e.g. text, !exclude)"), this.filterState); + return this.filterActionViewItem; } return super.getActionViewItem(action); @@ -764,7 +758,7 @@ class FilterReplAction extends EditorAction { run(accessor: ServicesAccessor, editor: ICodeEditor): void | Promise { SuggestController.get(editor).acceptSelectedSuggestion(false, true); const repl = getReplView(accessor.get(IViewsService)); - repl?.focusRepl(); + repl?.focusFilter(); } } diff --git a/src/vs/workbench/contrib/debug/browser/replFilter.ts b/src/vs/workbench/contrib/debug/browser/replFilter.ts index ebcd97c007..8e5313b233 100644 --- a/src/vs/workbench/contrib/debug/browser/replFilter.ts +++ b/src/vs/workbench/contrib/debug/browser/replFilter.ts @@ -15,7 +15,7 @@ import { IAction } from 'vs/base/common/actions'; import { HistoryInputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { toDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { toDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; @@ -51,10 +51,6 @@ export class ReplFilter implements ITreeFilter { } filter(element: IReplElement, parentVisibility: TreeVisibility): TreeFilterResult { - if (this._parsedQueries.length === 0) { - return parentVisibility; - } - let includeQueryPresent = false; let includeQueryMatched = false; @@ -72,68 +68,41 @@ export class ReplFilter implements ITreeFilter { } } - return includeQueryPresent ? includeQueryMatched : parentVisibility; + return includeQueryPresent ? includeQueryMatched : (typeof parentVisibility !== 'undefined' ? parentVisibility : TreeVisibility.Visible); } } -export interface IReplFiltersChangeEvent { - filterText?: boolean; - layout?: boolean; -} +export class ReplFilterState { -export interface IReplFiltersOptions { - filterText: string; - filterHistory: string[]; - layout: DOM.Dimension; -} - -export class TreeFilterState extends Disposable { - - private readonly _onDidChange: Emitter = this._register(new Emitter()); - readonly onDidChange: Event = this._onDidChange.event; - - constructor(options: IReplFiltersOptions) { - super(); - this._filterText = options.filterText; - this.filterHistory = options.filterHistory; - this._layout = options.layout; + private readonly _onDidChange: Emitter = new Emitter(); + get onDidChange(): Event { + return this._onDidChange.event; } - private _filterText: string; + private _filterText = ''; + get filterText(): string { return this._filterText; } + set filterText(filterText: string) { if (this._filterText !== filterText) { this._filterText = filterText; - this._onDidChange.fire({ filterText: true }); - } - } - - filterHistory: string[]; - - private _layout: DOM.Dimension = new DOM.Dimension(0, 0); - get layout(): DOM.Dimension { - return this._layout; - } - set layout(layout: DOM.Dimension) { - if (this._layout.width !== layout.width || this._layout.height !== layout.height) { - this._layout = layout; - this._onDidChange.fire({ layout: true }); + this._onDidChange.fire(); } } } -export class TreeFilterPanelActionViewItem extends BaseActionViewItem { +export class ReplFilterActionViewItem extends BaseActionViewItem { private delayedFilterUpdate: Delayer; - private container: HTMLElement | undefined; - private filterInputBox: HistoryInputBox | undefined; + private container!: HTMLElement; + private filterInputBox!: HistoryInputBox; constructor( action: IAction, private placeholder: string, - private filters: TreeFilterState, + private filters: ReplFilterState, @IInstantiationService private readonly instantiationService: IInstantiationService, @IThemeService private readonly themeService: IThemeService, @IContextViewService private readonly contextViewService: IContextViewService) { @@ -144,40 +113,33 @@ export class TreeFilterPanelActionViewItem extends BaseActionViewItem { render(container: HTMLElement): void { this.container = container; - DOM.addClass(this.container, 'panel-action-tree-filter-container'); + DOM.addClass(this.container, 'repl-panel-filter-container'); this.element = DOM.append(this.container, DOM.$('')); this.element.className = this.class; this.createInput(this.element); this.updateClass(); - - this.adjustInputBox(); } focus(): void { - if (this.filterInputBox) { - this.filterInputBox.focus(); - } + this.filterInputBox.focus(); } private clearFilterText(): void { - if (this.filterInputBox) { - this.filterInputBox.value = ''; - } + this.filterInputBox.value = ''; } private createInput(container: HTMLElement): void { this.filterInputBox = this._register(this.instantiationService.createInstance(ContextScopedHistoryInputBox, container, this.contextViewService, { placeholder: this.placeholder, - history: this.filters.filterHistory + history: [] })); this._register(attachInputBoxStyler(this.filterInputBox, this.themeService)); this.filterInputBox.value = this.filters.filterText; + this._register(this.filterInputBox.onDidChange(() => this.delayedFilterUpdate.trigger(() => this.onDidInputChange(this.filterInputBox!)))); - this._register(this.filters.onDidChange((event: IReplFiltersChangeEvent) => { - if (event.filterText) { - this.filterInputBox!.value = this.filters.filterText; - } + this._register(this.filters.onDidChange(() => { + this.filterInputBox.value = this.filters.filterText; })); this._register(DOM.addStandardDisposableListener(this.filterInputBox.inputElement, DOM.EventType.KEY_DOWN, (e: any) => this.onInputKeyDown(e))); this._register(DOM.addStandardDisposableListener(container, DOM.EventType.KEY_DOWN, this.handleKeyboardEvent)); @@ -186,19 +148,11 @@ export class TreeFilterPanelActionViewItem extends BaseActionViewItem { e.stopPropagation(); e.preventDefault(); })); - this._register(this.filters.onDidChange(e => this.onDidFiltersChange(e))); - } - - private onDidFiltersChange(e: IReplFiltersChangeEvent): void { - if (e.layout) { - this.updateClass(); - } } private onDidInputChange(inputbox: HistoryInputBox) { inputbox.addToHistory(); this.filters.filterText = inputbox.value; - this.filters.filterHistory = inputbox.getHistory(); } // Action toolbar is swallowing some keys for action items which should not be for an input box @@ -220,27 +174,7 @@ export class TreeFilterPanelActionViewItem extends BaseActionViewItem { } } - private adjustInputBox(): void { - if (this.element && this.filterInputBox) { - this.filterInputBox.inputElement.style.paddingRight = DOM.hasClass(this.element, 'small') ? '25px' : '150px'; - } - } - - protected updateClass(): void { - if (this.element && this.container) { - this.element.className = this.class; - DOM.toggleClass(this.container, 'grow', DOM.hasClass(this.element, 'grow')); - this.adjustInputBox(); - } - } - protected get class(): string { - if (this.filters.layout.width > 800) { - return 'panel-action-tree-filter grow'; - } else if (this.filters.layout.width < 600) { - return 'panel-action-tree-filter small'; - } else { - return 'panel-action-tree-filter'; - } + return 'panel-action-tree-filter'; } } diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index e23686febe..74148ee13c 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -8,37 +8,47 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import * as dom from 'vs/base/browser/dom'; import { CollapseAction } from 'vs/workbench/browser/viewlet'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; -import { IDebugService, IExpression, IScope, CONTEXT_VARIABLES_FOCUSED, IStackFrame } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, IExpression, IScope, CONTEXT_VARIABLES_FOCUSED, IStackFrame, CONTEXT_DEBUG_PROTOCOL_VARIABLE_MENU_CONTEXT, IDataBreakpointInfoResponse, CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED, CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT } from 'vs/workbench/contrib/debug/common/debug'; import { Variable, Scope, ErrorScope, StackFrame } from 'vs/workbench/contrib/debug/common/debugModel'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { renderViewTree, renderVariable, IInputBoxOptions, AbstractExpressionsRenderer, IExpressionTemplateData } from 'vs/workbench/contrib/debug/browser/baseDebugView'; -import { IAction, Action, Separator } from 'vs/base/common/actions'; +import { IAction } from 'vs/base/common/actions'; import { CopyValueAction } from 'vs/workbench/contrib/debug/browser/debugActions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IListAccessibilityProvider } 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'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { Emitter } from 'vs/base/common/event'; import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; import { IAsyncDataTreeViewState } from 'vs/base/browser/ui/tree/asyncDataTree'; import { FuzzyScore, createMatches } from 'vs/base/common/filters'; import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { dispose } from 'vs/base/common/lifecycle'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { withUndefinedAsNull } from 'vs/base/common/types'; +import { IMenuService, IMenu, MenuId } from 'vs/platform/actions/common/actions'; +import { createAndFillInContextMenuActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; const $ = dom.$; let forgetScopes = true; export const variableSetEmitter = new Emitter(); +let variableInternalContext: Variable | undefined; +let dataBreakpointInfoResponse: IDataBreakpointInfoResponse | undefined; + +interface IVariablesContext { + container: DebugProtocol.Variable | DebugProtocol.Scope; + variable: DebugProtocol.Variable; +} export class VariablesView extends ViewPane { @@ -47,6 +57,10 @@ export class VariablesView extends ViewPane { private tree!: WorkbenchAsyncDataTree; private savedViewState = new Map(); private autoExpandedScopes = new Set(); + private menu: IMenu; + private debugProtocolVariableMenuContext: IContextKey; + private breakWhenValueChangesSupported: IContextKey; + private variableEvaluateName: IContextKey; constructor( options: IViewletViewOptions, @@ -56,14 +70,20 @@ export class VariablesView extends ViewPane { @IConfigurationService configurationService: IConfigurationService, @IInstantiationService instantiationService: IInstantiationService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, - @IClipboardService private readonly clipboardService: IClipboardService, @IContextKeyService contextKeyService: IContextKeyService, @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, @ITelemetryService telemetryService: ITelemetryService, + @IMenuService menuService: IMenuService ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + this.menu = menuService.createMenu(MenuId.DebugVariablesContext, contextKeyService); + this._register(this.menu); + this.debugProtocolVariableMenuContext = CONTEXT_DEBUG_PROTOCOL_VARIABLE_MENU_CONTEXT.bindTo(contextKeyService); + this.breakWhenValueChangesSupported = CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED.bindTo(contextKeyService); + this.variableEvaluateName = CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT.bindTo(contextKeyService); + // Use scheduler to prevent unnecessary flashing this.onFocusStackFrameScheduler = new RunOnceScheduler(async () => { const stackFrame = this.debugService.getViewModel().focusedStackFrame; @@ -183,41 +203,27 @@ export class VariablesView extends ViewPane { private async onContextMenu(e: ITreeContextMenuEvent): Promise { const variable = e.element; if (variable instanceof Variable && !!variable.value) { - const actions: IAction[] = []; + this.debugProtocolVariableMenuContext.set(variable.variableMenuContext || ''); + variableInternalContext = variable; const session = this.debugService.getViewModel().focusedSession; - if (session && session.capabilities.supportsSetVariable) { - actions.push(new Action('workbench.setValue', nls.localize('setValue', "Set Value"), undefined, true, () => { - this.debugService.getViewModel().setSelectedExpression(variable); - return Promise.resolve(); - })); - } - actions.push(this.instantiationService.createInstance(CopyValueAction, CopyValueAction.ID, CopyValueAction.LABEL, variable, 'variables')); - if (variable.evaluateName) { - actions.push(new Action('debug.copyEvaluatePath', nls.localize('copyAsExpression', "Copy as Expression"), undefined, true, () => { - return this.clipboardService.writeText(variable.evaluateName!); - })); - actions.push(new Separator()); - actions.push(new Action('debug.addToWatchExpressions', nls.localize('addToWatchExpressions', "Add to Watch"), undefined, true, () => { - this.debugService.addWatchExpression(variable.evaluateName); - return Promise.resolve(undefined); - })); - } + this.variableEvaluateName.set(!!variable.evaluateName); + this.breakWhenValueChangesSupported.reset(); if (session && session.capabilities.supportsDataBreakpoints) { const response = await session.dataBreakpointInfo(variable.name, variable.parent.reference); - const dataid = response?.dataId; - if (response && dataid) { - actions.push(new Separator()); - actions.push(new Action('debug.breakWhenValueChanges', nls.localize('breakWhenValueChanges', "Break When Value Changes"), undefined, true, () => { - return this.debugService.addDataBreakpoint(response.description, dataid, !!response.canPersist, response.accessTypes); - })); - } + const dataBreakpointId = response?.dataId; + this.breakWhenValueChangesSupported.set(!!dataBreakpointId); } + const context: IVariablesContext = { + container: (variable.parent as (Variable | Scope)).toDebugProtocolObject(), + variable: variable.toDebugProtocolObject() + }; + const actions: IAction[] = []; + const actionsDisposable = createAndFillInContextMenuActions(this.menu, { arg: context, shouldForwardArgs: false }, actions, this.contextMenuService); this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, getActions: () => actions, - getActionsContext: () => variable, - onHide: () => dispose(actions) + onHide: () => dispose(actionsDisposable) }); } } @@ -377,3 +383,54 @@ class VariablesAccessibilityProvider implements IListAccessibilityProvider { + const debugService = accessor.get(IDebugService); + debugService.getViewModel().setSelectedExpression(variableInternalContext); + } +}); + +export const COPY_VALUE_ID = 'debug.copyValue'; +CommandsRegistry.registerCommand({ + id: COPY_VALUE_ID, + handler: async (accessor: ServicesAccessor) => { + const instantiationService = accessor.get(IInstantiationService); + if (variableInternalContext) { + const action = instantiationService.createInstance(CopyValueAction, CopyValueAction.ID, CopyValueAction.LABEL, variableInternalContext, 'variables'); + await action.run(); + } + } +}); + +export const BREAK_WHEN_VALUE_CHANGES_ID = 'debug.breakWhenValueChanges'; +CommandsRegistry.registerCommand({ + id: BREAK_WHEN_VALUE_CHANGES_ID, + handler: async (accessor: ServicesAccessor) => { + const debugService = accessor.get(IDebugService); + if (dataBreakpointInfoResponse) { + await debugService.addDataBreakpoint(dataBreakpointInfoResponse.description, dataBreakpointInfoResponse.dataId!, !!dataBreakpointInfoResponse.canPersist, dataBreakpointInfoResponse.accessTypes); + } + } +}); + +export const COPY_EVALUATE_PATH_ID = 'debug.copyEvaluatePath'; +CommandsRegistry.registerCommand({ + id: COPY_EVALUATE_PATH_ID, + handler: async (accessor: ServicesAccessor, context: IVariablesContext) => { + const clipboardService = accessor.get(IClipboardService); + await clipboardService.writeText(context.variable.evaluateName!); + } +}); + +export const ADD_TO_WATCH_ID = 'debug.addToWatchExpressions'; +CommandsRegistry.registerCommand({ + id: ADD_TO_WATCH_ID, + handler: async (accessor: ServicesAccessor, context: IVariablesContext) => { + const debugService = accessor.get(IDebugService); + debugService.addWatchExpression(context.variable.evaluateName); + } +}); + diff --git a/src/vs/workbench/contrib/debug/browser/welcomeView.ts b/src/vs/workbench/contrib/debug/browser/welcomeView.ts index fc3b181ef3..e73718d726 100644 --- a/src/vs/workbench/contrib/debug/browser/welcomeView.ts +++ b/src/vs/workbench/contrib/debug/browser/welcomeView.ts @@ -8,10 +8,10 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, RawContextKey, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { localize } from 'vs/nls'; import { StartAction, ConfigureAction, SelectAndStartAction } from 'vs/workbench/contrib/debug/browser/debugActions'; -import { IDebugService } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, CONTEXT_DEBUGGERS_AVAILABLE } from 'vs/workbench/contrib/debug/common/debug'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -109,30 +109,32 @@ const viewsRegistry = Registry.as(Extensions.ViewsRegistry); viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { content: localize({ key: 'openAFileWhichCanBeDebugged', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, "[Open a file](command:{0}) which can be debugged or run.", isMacintosh ? OpenFileFolderAction.ID : OpenFileAction.ID), - when: CONTEXT_DEBUGGER_INTERESTED_IN_ACTIVE_EDITOR.toNegated() + when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUGGER_INTERESTED_IN_ACTIVE_EDITOR.toNegated()) }); let debugKeybindingLabel = ''; viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { content: localize({ key: 'runAndDebugAction', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, "[Run and Debug{0}](command:{1})", debugKeybindingLabel, StartAction.ID), - preconditions: [CONTEXT_DEBUGGER_INTERESTED_IN_ACTIVE_EDITOR] + preconditions: [CONTEXT_DEBUGGER_INTERESTED_IN_ACTIVE_EDITOR], + when: CONTEXT_DEBUGGERS_AVAILABLE }); viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { content: localize({ key: 'detectThenRunAndDebug', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, "[Show](command:{0}) all automatic debug configurations.", SelectAndStartAction.ID), - priority: ViewContentPriority.Lowest + priority: ViewContentPriority.Lowest, + when: CONTEXT_DEBUGGERS_AVAILABLE }); viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { content: localize({ key: 'customizeRunAndDebug', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, "To customize Run and Debug [create a launch.json file](command:{0}).", ConfigureAction.ID), - when: WorkbenchStateContext.notEqualsTo('empty') + when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, WorkbenchStateContext.notEqualsTo('empty')) }); viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { content: localize({ key: 'customizeRunAndDebugOpenFolder', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, "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') + when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, WorkbenchStateContext.isEqualTo('empty')) }); diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index d2f9c2ae69..bcb95cb592 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -59,6 +59,11 @@ export const CONTEXT_RESTART_FRAME_SUPPORTED = new RawContextKey('resta export const CONTEXT_JUMP_TO_CURSOR_SUPPORTED = new RawContextKey('jumpToCursorSupported', false); export const CONTEXT_STEP_INTO_TARGETS_SUPPORTED = new RawContextKey('stepIntoTargetsSupported', false); export const CONTEXT_BREAKPOINTS_EXIST = new RawContextKey('breakpointsExist', false); +export const CONTEXT_DEBUGGERS_AVAILABLE = new RawContextKey('debuggersAvailable', false); +export const CONTEXT_DEBUG_PROTOCOL_VARIABLE_MENU_CONTEXT = new RawContextKey('debugProtocolVariableMenuContext', undefined); +export const CONTEXT_SET_VARIABLE_SUPPORTED = new RawContextKey('debugSetVariableSupported', false); +export const CONTEXT_BREAK_WHEN_VALUE_CHANGES_SUPPORTED = new RawContextKey('breakWhenValueChangesSupported', false); +export const CONTEXT_VARIABLE_EVALUATE_NAME_PRESENT = new RawContextKey('variableEvaluateNamePresent', false); export const EDITOR_CONTRIBUTION_ID = 'editor.contrib.debug'; export const BREAKPOINT_EDITOR_CONTRIBUTION_ID = 'editor.contrib.breakpoint'; @@ -160,6 +165,13 @@ export interface IDebugSessionOptions { compact?: boolean; } +export interface IDataBreakpointInfoResponse { + dataId: string | null; + description: string; + canPersist?: boolean, + accessTypes?: DebugProtocol.DataBreakpointAccessType[]; +} + export interface IDebugSession extends ITreeElement { readonly configuration: IConfig; @@ -220,7 +232,7 @@ export interface IDebugSession extends ITreeElement { sendBreakpoints(modelUri: uri, bpts: IBreakpoint[], sourceModified: boolean): Promise; sendFunctionBreakpoints(fbps: IFunctionBreakpoint[]): Promise; - dataBreakpointInfo(name: string, variablesReference?: number): Promise<{ dataId: string | null, description: string, canPersist?: boolean, accessTypes?: DebugProtocol.DataBreakpointAccessType[] } | undefined>; + dataBreakpointInfo(name: string, variablesReference?: number): Promise; sendDataBreakpoints(dbps: IDataBreakpoint[]): Promise; sendExceptionBreakpoints(exbpts: IExceptionBreakpoint[]): Promise; breakpointsLocations(uri: uri, lineNumber: number): Promise; diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index eba078274a..641b8d309b 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -24,6 +24,10 @@ import { mixin } from 'vs/base/common/objects'; import { DebugStorage } from 'vs/workbench/contrib/debug/common/debugStorage'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; +interface IDebugProtocolVariableWithContext extends DebugProtocol.Variable { + __vscodeVariableMenuContext?: string; +} + export class ExpressionContainer implements IExpressionContainer { public static readonly allValues = new Map(); @@ -86,7 +90,7 @@ export class ExpressionContainer implements IExpressionContainer { for (let i = 0; i < numberOfChunks; i++) { const start = (this.startOfVariables || 0) + i * chunkSize; const count = Math.min(chunkSize, this.indexedVariables - i * chunkSize); - children.push(new Variable(this.session, this.threadId, this, this.reference, `[${start}..${start + count - 1}]`, '', '', undefined, count, { kind: 'virtual' }, undefined, true, start)); + children.push(new Variable(this.session, this.threadId, this, this.reference, `[${start}..${start + count - 1}]`, '', '', undefined, count, { kind: 'virtual' }, undefined, undefined, true, start)); } return children; @@ -117,14 +121,14 @@ export class ExpressionContainer implements IExpressionContainer { try { const response = await this.session!.variables(this.reference || 0, this.threadId, filter, start, count); return response && response.body && response.body.variables - ? distinct(response.body.variables.filter(v => !!v), v => v.name).map(v => { + ? distinct(response.body.variables.filter(v => !!v), v => v.name).map((v: IDebugProtocolVariableWithContext) => { if (isString(v.value) && isString(v.name) && typeof v.variablesReference === 'number') { - return new Variable(this.session, this.threadId, this, v.variablesReference, v.name, v.evaluateName, v.value, v.namedVariables, v.indexedVariables, v.presentationHint, v.type); + return new Variable(this.session, this.threadId, this, v.variablesReference, v.name, v.evaluateName, v.value, v.namedVariables, v.indexedVariables, v.presentationHint, v.type, v.__vscodeVariableMenuContext); } - return new Variable(this.session, this.threadId, this, 0, '', undefined, nls.localize('invalidVariableAttributes', "Invalid variable attributes"), 0, 0, { kind: 'virtual' }, undefined, false); + return new Variable(this.session, this.threadId, this, 0, '', undefined, nls.localize('invalidVariableAttributes', "Invalid variable attributes"), 0, 0, { kind: 'virtual' }, undefined, undefined, false); }) : []; } catch (e) { - return [new Variable(this.session, this.threadId, this, 0, '', undefined, e.message, 0, 0, { kind: 'virtual' }, undefined, false)]; + return [new Variable(this.session, this.threadId, this, 0, '', undefined, e.message, 0, 0, { kind: 'virtual' }, undefined, undefined, false)]; } } @@ -218,6 +222,7 @@ export class Variable extends ExpressionContainer implements IExpression { indexedVariables: number | undefined, public presentationHint: DebugProtocol.VariablePresentationHint | undefined, public type: string | undefined = undefined, + public variableMenuContext: string | undefined = undefined, public available = true, startOfVariables = 0 ) { @@ -247,6 +252,15 @@ export class Variable extends ExpressionContainer implements IExpression { toString(): string { return `${this.name}: ${this.value}`; } + + toDebugProtocolObject(): DebugProtocol.Variable { + return { + name: this.name, + variablesReference: this.reference || 0, + value: this.value, + evaluateName: this.evaluateName + }; + } } export class Scope extends ExpressionContainer implements IScope { @@ -267,6 +281,14 @@ export class Scope extends ExpressionContainer implements IScope { toString(): string { return this.name; } + + toDebugProtocolObject(): DebugProtocol.Scope { + return { + name: this.name, + variablesReference: this.reference || 0, + expensive: this.expensive + }; + } } export class ErrorScope extends Scope { diff --git a/src/vs/workbench/contrib/debug/common/debugViewModel.ts b/src/vs/workbench/contrib/debug/common/debugViewModel.ts index 8f7f33ad6f..a1b0c2cfcc 100644 --- a/src/vs/workbench/contrib/debug/common/debugViewModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugViewModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event, Emitter } from 'vs/base/common/event'; -import { CONTEXT_EXPRESSION_SELECTED, IViewModel, IStackFrame, IDebugSession, IThread, IExpression, IFunctionBreakpoint, CONTEXT_BREAKPOINT_SELECTED, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_STEP_INTO_TARGETS_SUPPORTED } from 'vs/workbench/contrib/debug/common/debug'; +import { CONTEXT_EXPRESSION_SELECTED, IViewModel, IStackFrame, IDebugSession, IThread, IExpression, IFunctionBreakpoint, CONTEXT_BREAKPOINT_SELECTED, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, CONTEXT_SET_VARIABLE_SUPPORTED } from 'vs/workbench/contrib/debug/common/debug'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { isSessionAttach } from 'vs/workbench/contrib/debug/common/debugUtils'; @@ -29,6 +29,7 @@ export class ViewModel implements IViewModel { private restartFrameSupportedContextKey!: IContextKey; private stepIntoTargetsSupported!: IContextKey; private jumpToCursorSupported!: IContextKey; + private setVariableSupported!: IContextKey; constructor(private contextKeyService: IContextKeyService) { this.multiSessionView = false; @@ -41,6 +42,7 @@ export class ViewModel implements IViewModel { this.restartFrameSupportedContextKey = CONTEXT_RESTART_FRAME_SUPPORTED.bindTo(contextKeyService); this.stepIntoTargetsSupported = CONTEXT_STEP_INTO_TARGETS_SUPPORTED.bindTo(contextKeyService); this.jumpToCursorSupported = CONTEXT_JUMP_TO_CURSOR_SUPPORTED.bindTo(contextKeyService); + this.setVariableSupported = CONTEXT_SET_VARIABLE_SUPPORTED.bindTo(contextKeyService); }); } @@ -74,6 +76,7 @@ export class ViewModel implements IViewModel { this.restartFrameSupportedContextKey.set(session ? !!session.capabilities.supportsRestartFrame : false); this.stepIntoTargetsSupported.set(session ? !!session.capabilities.supportsStepInTargetsRequest : false); this.jumpToCursorSupported.set(session ? !!session.capabilities.supportsGotoTargetsRequest : false); + this.setVariableSupported.set(session ? !!session.capabilities.supportsSetVariable : false); const attach = !!session && isSessionAttach(session); this.focusedSessionIsAttach.set(attach); }); diff --git a/src/vs/workbench/contrib/debug/common/replModel.ts b/src/vs/workbench/contrib/debug/common/replModel.ts index fcdfc5b60b..eba05910d1 100644 --- a/src/vs/workbench/contrib/debug/common/replModel.ts +++ b/src/vs/workbench/contrib/debug/common/replModel.ts @@ -27,7 +27,8 @@ export class SimpleReplElement implements IReplElement { ) { } toString(): string { - return this.value; + const sourceStr = this.sourceData ? ` ${this.sourceData.source.name}:${this.sourceData.lineNumber}` : ''; + return this.value + sourceStr; } getId(): string { @@ -144,7 +145,8 @@ export class ReplGroup implements IReplElement { } toString(): string { - return this.name; + const sourceStr = this.sourceData ? ` ${this.sourceData.source.name}:${this.sourceData.lineNumber}` : ''; + return this.name + sourceStr; } addChild(child: IReplElement): void { @@ -174,18 +176,13 @@ export class ReplGroup implements IReplElement { } } -type FilterFunc = ((element: IReplElement) => void); - export class ReplModel { private replElements: IReplElement[] = []; private readonly _onDidChangeElements = new Emitter(); readonly onDidChangeElements = this._onDidChangeElements.event; - private filterFunc: FilterFunc | undefined; getReplElements(): IReplElement[] { - return this.replElements.filter(element => - this.filterFunc ? this.filterFunc(element) : true - ); + return this.replElements; } async addReplExpression(session: IDebugSession, stackFrame: IStackFrame | undefined, name: string): Promise { @@ -320,10 +317,6 @@ export class ReplModel { } } - setFilter(filterFunc: FilterFunc): void { - this.filterFunc = filterFunc; - } - removeReplExpressions(): void { if (this.replElements.length > 0) { this.replElements = []; diff --git a/src/vs/workbench/contrib/debug/node/terminals.ts b/src/vs/workbench/contrib/debug/node/terminals.ts index 0684ddbba1..d77087e604 100644 --- a/src/vs/workbench/contrib/debug/node/terminals.ts +++ b/src/vs/workbench/contrib/debug/node/terminals.ts @@ -177,7 +177,7 @@ export function prepareCommand(shell: string, args: string[], cwd?: string, env? command += `cd ${quote(cwd)} ; `; } if (env) { - command += 'env'; + command += '/usr/bin/env'; for (let key in env) { const value = env[key]; if (value === null) { diff --git a/src/vs/workbench/contrib/debug/test/browser/repl.test.ts b/src/vs/workbench/contrib/debug/test/browser/repl.test.ts index 32920ff6b2..317998bc78 100644 --- a/src/vs/workbench/contrib/debug/test/browser/repl.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/repl.test.ts @@ -197,10 +197,13 @@ suite('Debug - REPL', () => { const repl = new ReplModel(); const replFilter = new ReplFilter(); - repl.setFilter((element) => { - const filterResult = replFilter.filter(element, TreeVisibility.Visible); - return filterResult === true || filterResult === TreeVisibility.Visible; - }); + const getFilteredElements = () => { + const elements = repl.getReplElements(); + return elements.filter(e => { + const filterResult = replFilter.filter(e, TreeVisibility.Visible); + return filterResult === true || filterResult === TreeVisibility.Visible; + }); + }; repl.appendToRepl(session, 'first line\n', severity.Info); repl.appendToRepl(session, 'second line\n', severity.Info); @@ -208,19 +211,19 @@ suite('Debug - REPL', () => { repl.appendToRepl(session, 'fourth line\n', severity.Info); replFilter.filterQuery = 'first'; - let r1 = repl.getReplElements(); + let r1 = getFilteredElements(); assert.equal(r1.length, 1); assert.equal(r1[0].value, 'first line\n'); replFilter.filterQuery = '!first'; - let r2 = repl.getReplElements(); + let r2 = getFilteredElements(); assert.equal(r1.length, 1); assert.equal(r2[0].value, 'second line\n'); assert.equal(r2[1].value, 'third line\n'); assert.equal(r2[2].value, 'fourth line\n'); replFilter.filterQuery = 'first, line'; - let r3 = repl.getReplElements(); + let r3 = getFilteredElements(); assert.equal(r3.length, 4); assert.equal(r3[0].value, 'first line\n'); assert.equal(r3[1].value, 'second line\n'); @@ -228,22 +231,22 @@ suite('Debug - REPL', () => { assert.equal(r3[3].value, 'fourth line\n'); replFilter.filterQuery = 'line, !second'; - let r4 = repl.getReplElements(); + let r4 = getFilteredElements(); assert.equal(r4.length, 3); assert.equal(r4[0].value, 'first line\n'); assert.equal(r4[1].value, 'third line\n'); assert.equal(r4[2].value, 'fourth line\n'); replFilter.filterQuery = '!second, line'; - let r4_same = repl.getReplElements(); + let r4_same = getFilteredElements(); assert.equal(r4.length, r4_same.length); replFilter.filterQuery = '!line'; - let r5 = repl.getReplElements(); + let r5 = getFilteredElements(); assert.equal(r5.length, 0); replFilter.filterQuery = 'smth'; - let r6 = repl.getReplElements(); + let r6 = getFilteredElements(); assert.equal(r6.length, 0); }); }); diff --git a/src/vs/workbench/contrib/extensions/browser/configBasedRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/configBasedRecommendations.ts index 211935be2f..75a3f04049 100644 --- a/src/vs/workbench/contrib/extensions/browser/configBasedRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/configBasedRecommendations.ts @@ -3,24 +3,21 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IExtensionTipsService, IExtensionManagementService, ILocalExtension, IConfigBasedExtensionTip } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; +import { IExtensionTipsService, IConfigBasedExtensionTip } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionRecommendations, ExtensionRecommendation, PromptedExtensionRecommendations } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; import { localize } from 'vs/nls'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { INotificationService } from 'vs/platform/notification/common/notification'; import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IStorageService } from 'vs/platform/storage/common/storage'; -import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { IWorkspaceContextService, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; -import { distinct } from 'vs/base/common/arrays'; +import { Emitter } from 'vs/base/common/event'; export class ConfigBasedRecommendations extends ExtensionRecommendations { private importantTips: IConfigBasedExtensionTip[] = []; private otherTips: IConfigBasedExtensionTip[] = []; + private _onDidChangeRecommendations = this._register(new Emitter()); + readonly onDidChangeRecommendations = this._onDidChangeRecommendations.event; + private _otherRecommendations: ExtensionRecommendation[] = []; get otherRecommendations(): ReadonlyArray { return this._otherRecommendations; } @@ -30,24 +27,16 @@ export class ConfigBasedRecommendations extends ExtensionRecommendations { get recommendations(): ReadonlyArray { return [...this.importantRecommendations, ...this.otherRecommendations]; } constructor( - isExtensionAllowedToBeRecommended: (extensionId: string) => boolean, + promptedExtensionRecommendations: PromptedExtensionRecommendations, @IExtensionTipsService private readonly extensionTipsService: IExtensionTipsService, - @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @IInstantiationService instantiationService: IInstantiationService, - @IConfigurationService configurationService: IConfigurationService, - @INotificationService notificationService: INotificationService, - @ITelemetryService telemetryService: ITelemetryService, - @IStorageService storageService: IStorageService, - @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, ) { - super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService); + super(promptedExtensionRecommendations); } protected async doActivate(): Promise { await this.fetch(); this._register(this.workspaceContextService.onDidChangeWorkspaceFolders(e => this.onWorkspaceFoldersChanged(e))); - this.promptWorkspaceRecommendations(); } private async fetch(): Promise { @@ -70,54 +59,13 @@ export class ConfigBasedRecommendations extends ExtensionRecommendations { this._importantRecommendations = this.importantTips.map(tip => this.toExtensionRecommendation(tip)); } - private async promptWorkspaceRecommendations(): Promise { - if (this.hasToIgnoreRecommendationNotifications()) { - return; - } - - if (this.importantTips.length === 0) { - return; - } - - const local = await this.extensionManagementService.getInstalled(); - const { uninstalled } = this.groupByInstalled(distinct(this.importantTips.map(({ extensionId }) => extensionId)), local); - if (uninstalled.length === 0) { - return; - } - - const importantExtensions = this.filterIgnoredOrNotAllowed(uninstalled); - if (importantExtensions.length === 0) { - return; - } - - for (const extension of importantExtensions) { - const tip = this.importantTips.filter(tip => tip.extensionId === extension)[0]; - const message = tip.isExtensionPack ? localize('extensionPackRecommended', "The '{0}' extension pack is recommended for this workspace.", tip.extensionName) - : localize('extensionRecommended', "The '{0}' extension is recommended for this workspace.", tip.extensionName); - this.promptImportantExtensionsInstallNotification([extension], message); - } - } - - private groupByInstalled(recommendationsToSuggest: string[], local: ILocalExtension[]): { installed: string[], uninstalled: string[] } { - const installed: string[] = [], uninstalled: string[] = []; - const installedExtensionsIds = local.reduce((result, i) => { result.add(i.identifier.id.toLowerCase()); return result; }, new Set()); - recommendationsToSuggest.forEach(id => { - if (installedExtensionsIds.has(id.toLowerCase())) { - installed.push(id); - } else { - uninstalled.push(id); - } - }); - return { installed, uninstalled }; - } - private async onWorkspaceFoldersChanged(event: IWorkspaceFoldersChangeEvent): Promise { if (event.added.length) { const oldImportantRecommended = this.importantTips; await this.fetch(); // Suggest only if at least one of the newly added recommendations was not suggested before if (this.importantTips.some(current => oldImportantRecommended.every(old => current.extensionId !== old.extensionId))) { - return this.promptWorkspaceRecommendations(); + this._onDidChangeRecommendations.fire(); } } } diff --git a/src/vs/workbench/contrib/extensions/browser/dynamicWorkspaceRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/dynamicWorkspaceRecommendations.ts index e3ebf0422e..e88c2282cb 100644 --- a/src/vs/workbench/contrib/extensions/browser/dynamicWorkspaceRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/dynamicWorkspaceRecommendations.ts @@ -11,13 +11,9 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { isNonEmptyArray } from 'vs/base/common/arrays'; import { IWorkspaceTagsService } from 'vs/workbench/contrib/tags/common/workspaceTags'; import { isNumber } from 'vs/base/common/types'; -import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { INotificationService } from 'vs/platform/notification/common/notification'; +import { ExtensionRecommendations, ExtensionRecommendation, PromptedExtensionRecommendations } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { localize } from 'vs/nls'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; type DynamicWorkspaceRecommendationsClassification = { count: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; @@ -34,19 +30,15 @@ export class DynamicWorkspaceRecommendations extends ExtensionRecommendations { get recommendations(): ReadonlyArray { return this._recommendations; } constructor( - isExtensionAllowedToBeRecommended: (extensionId: string) => boolean, + promptedExtensionRecommendations: PromptedExtensionRecommendations, @IExtensionTipsService private readonly extensionTipsService: IExtensionTipsService, @IWorkspaceTagsService private readonly workspaceTagsService: IWorkspaceTagsService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IFileService private readonly fileService: IFileService, - @IInstantiationService instantiationService: IInstantiationService, - @IConfigurationService configurationService: IConfigurationService, - @INotificationService notificationService: INotificationService, - @ITelemetryService telemetryService: ITelemetryService, - @IStorageService storageService: IStorageService, - @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IStorageService private readonly storageService: IStorageService, ) { - super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService); + super(promptedExtensionRecommendations); } protected async doActivate(): Promise { diff --git a/src/vs/workbench/contrib/extensions/browser/exeBasedRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/exeBasedRecommendations.ts index beabcded64..7303bf6afc 100644 --- a/src/vs/workbench/contrib/extensions/browser/exeBasedRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/exeBasedRecommendations.ts @@ -5,17 +5,12 @@ import { IExtensionTipsService, IExecutableBasedExtensionTip, IExtensionManagementService, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; +import { ExtensionRecommendations, ExtensionRecommendation, PromptedExtensionRecommendations } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; import { timeout } from 'vs/base/common/async'; import { localize } from 'vs/nls'; -import { IStringDictionary } from 'vs/base/common/collections'; -import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation'; -import { INotificationService } from 'vs/platform/notification/common/notification'; +import { optional } from 'vs/platform/instantiation/common/instantiation'; import { basename } from 'vs/base/common/path'; import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IStorageService } from 'vs/platform/storage/common/storage'; -import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; type ExeExtensionRecommendationsClassification = { @@ -25,30 +20,24 @@ type ExeExtensionRecommendationsClassification = { export class ExeBasedRecommendations extends ExtensionRecommendations { + private _otherTips: IExecutableBasedExtensionTip[] = []; + private _importantTips: IExecutableBasedExtensionTip[] = []; - private readonly _otherRecommendations: ExtensionRecommendation[] = []; - get otherRecommendations(): ReadonlyArray { return this._otherRecommendations; } - - private readonly _importantRecommendations: ExtensionRecommendation[] = []; - get importantRecommendations(): ReadonlyArray { return this._importantRecommendations; } + get otherRecommendations(): ReadonlyArray { return this._otherTips.map(tip => this.toExtensionRecommendation(tip)); } + get importantRecommendations(): ReadonlyArray { return this._importantTips.map(tip => this.toExtensionRecommendation(tip)); } get recommendations(): ReadonlyArray { return [...this.importantRecommendations, ...this.otherRecommendations]; } private readonly tasExperimentService: ITASExperimentService | undefined; constructor( - isExtensionAllowedToBeRecommended: (extensionId: string) => boolean, + promptedExtensionRecommendations: PromptedExtensionRecommendations, @IExtensionTipsService private readonly extensionTipsService: IExtensionTipsService, - @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @optional(ITASExperimentService) tasExperimentService: ITASExperimentService, - @IInstantiationService instantiationService: IInstantiationService, - @IConfigurationService configurationService: IConfigurationService, - @INotificationService notificationService: INotificationService, - @ITelemetryService telemetryService: ITelemetryService, - @IStorageService storageService: IStorageService, - @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, ) { - super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService); + super(promptedExtensionRecommendations); this.tasExperimentService = tasExperimentService; /* @@ -58,27 +47,35 @@ export class ExeBasedRecommendations extends ExtensionRecommendations { timeout(3000).then(() => this.fetchAndPromptImportantExeBasedRecommendations()); } + getRecommendations(exe: string): { important: ExtensionRecommendation[], others: ExtensionRecommendation[] } { + const important = this._importantTips + .filter(tip => tip.exeName.toLowerCase() === exe.toLowerCase()) + .map(tip => this.toExtensionRecommendation(tip)); + + const others = this._otherTips + .filter(tip => tip.exeName.toLowerCase() === exe.toLowerCase()) + .map(tip => this.toExtensionRecommendation(tip)); + + return { important, others }; + } + protected async doActivate(): Promise { - const otherExectuableBasedTips = await this.extensionTipsService.getOtherExecutableBasedTips(); - otherExectuableBasedTips.forEach(tip => this._otherRecommendations.push(this.toExtensionRecommendation(tip))); + this._otherTips = await this.extensionTipsService.getOtherExecutableBasedTips(); await this.fetchImportantExeBasedRecommendations(); } - private _importantExeBasedRecommendations: Promise> | undefined; - private async fetchImportantExeBasedRecommendations(): Promise> { + private _importantExeBasedRecommendations: Promise> | undefined; + private async fetchImportantExeBasedRecommendations(): Promise> { if (!this._importantExeBasedRecommendations) { this._importantExeBasedRecommendations = this.doFetchImportantExeBasedRecommendations(); } return this._importantExeBasedRecommendations; } - private async doFetchImportantExeBasedRecommendations(): Promise> { - const importantExeBasedRecommendations: IStringDictionary = {}; - const importantExectuableBasedTips = await this.extensionTipsService.getImportantExecutableBasedTips(); - importantExectuableBasedTips.forEach(tip => { - this._importantRecommendations.push(this.toExtensionRecommendation(tip)); - importantExeBasedRecommendations[tip.extensionId.toLowerCase()] = tip; - }); + private async doFetchImportantExeBasedRecommendations(): Promise> { + const importantExeBasedRecommendations = new Map(); + this._importantTips = await this.extensionTipsService.getImportantExecutableBasedTips(); + this._importantTips.forEach(tip => importantExeBasedRecommendations.set(tip.extensionId.toLowerCase(), tip)); return importantExeBasedRecommendations; } @@ -86,39 +83,45 @@ export class ExeBasedRecommendations extends ExtensionRecommendations { const importantExeBasedRecommendations = await this.fetchImportantExeBasedRecommendations(); const local = await this.extensionManagementService.getInstalled(); - const { installed, uninstalled } = this.groupByInstalled(Object.keys(importantExeBasedRecommendations), local); + const { installed, uninstalled } = this.groupByInstalled([...importantExeBasedRecommendations.keys()], local); /* Log installed and uninstalled exe based recommendations */ for (const extensionId of installed) { - const tip = importantExeBasedRecommendations[extensionId]; - this.telemetryService.publicLog2<{ exeName: string, extensionId: string }, ExeExtensionRecommendationsClassification>('exeExtensionRecommendations:alreadyInstalled', { extensionId, exeName: basename(tip.windowsPath!) }); + const tip = importantExeBasedRecommendations.get(extensionId); + if (tip) { + this.telemetryService.publicLog2<{ exeName: string, extensionId: string }, ExeExtensionRecommendationsClassification>('exeExtensionRecommendations:alreadyInstalled', { extensionId, exeName: basename(tip.windowsPath!) }); + } } for (const extensionId of uninstalled) { - const tip = importantExeBasedRecommendations[extensionId]; - this.telemetryService.publicLog2<{ exeName: string, extensionId: string }, ExeExtensionRecommendationsClassification>('exeExtensionRecommendations:notInstalled', { extensionId, exeName: basename(tip.windowsPath!) }); + const tip = importantExeBasedRecommendations.get(extensionId); + if (tip) { + this.telemetryService.publicLog2<{ exeName: string, extensionId: string }, ExeExtensionRecommendationsClassification>('exeExtensionRecommendations:notInstalled', { extensionId, exeName: basename(tip.windowsPath!) }); + } } this.promptImportantExeBasedRecommendations(uninstalled, importantExeBasedRecommendations); } - private async promptImportantExeBasedRecommendations(recommendations: string[], importantExeBasedRecommendations: IStringDictionary): Promise { - if (this.hasToIgnoreRecommendationNotifications()) { + private async promptImportantExeBasedRecommendations(recommendations: string[], importantExeBasedRecommendations: Map): Promise { + if (this.promptedExtensionRecommendations.hasToIgnoreRecommendationNotifications()) { return; } - recommendations = this.filterIgnoredOrNotAllowed(recommendations); + recommendations = this.promptedExtensionRecommendations.filterIgnoredOrNotAllowed(recommendations); if (recommendations.length === 0) { return; } const recommendationsByExe = new Map(); for (const extensionId of recommendations) { - const tip = importantExeBasedRecommendations[extensionId]; - let tips = recommendationsByExe.get(tip.exeFriendlyName); - if (!tips) { - tips = []; - recommendationsByExe.set(tip.exeFriendlyName, tips); + const tip = importantExeBasedRecommendations.get(extensionId); + if (tip) { + let tips = recommendationsByExe.get(tip.exeFriendlyName); + if (!tips) { + tips = []; + recommendationsByExe.set(tip.exeFriendlyName, tips); + } + tips.push(tip); } - tips.push(tip); } for (const [, tips] of recommendationsByExe) { @@ -127,22 +130,8 @@ export class ExeBasedRecommendations extends ExtensionRecommendations { await this.tasExperimentService.getTreatment('wslpopupaa'); } - if (tips.length === 1) { - const tip = tips[0]; - const message = tip.isExtensionPack ? localize('extensionPackRecommended', "The '{0}' extension pack is recommended as you have {1} installed on your system.", tip.extensionName, tip.exeFriendlyName || basename(tip.windowsPath!)) - : localize('exeRecommended', "The '{0}' extension is recommended as you have {1} installed on your system.", tip.extensionName, tip.exeFriendlyName || basename(tip.windowsPath!)); - this.promptImportantExtensionsInstallNotification(extensionIds, message); - } - - else if (tips.length === 2) { - const message = localize('two extensions recommended', "The '{0}' and '{1}' extensions are recommended as you have {2} installed on your system.", tips[0].extensionName, tips[1].extensionName, tips[0].exeFriendlyName || basename(tips[0].windowsPath!)); - this.promptImportantExtensionsInstallNotification(extensionIds, message); - } - - else if (tips.length > 2) { - const message = localize('more than two extensions recommended', "The '{0}', '{1}' and other extensions are recommended as you have {2} installed on your system.", tips[0].extensionName, tips[1].extensionName, tips[0].exeFriendlyName || basename(tips[0].windowsPath!)); - this.promptImportantExtensionsInstallNotification(extensionIds, message); - } + const message = localize('exeRecommended', "You have {0} installed on your system. Do you want to install the recommended extensions for it?", tips[0].exeFriendlyName); + this.promptedExtensionRecommendations.promptImportantExtensionsInstallNotification(extensionIds, message, `@exe:"${tips[0].exeName}"`); } } diff --git a/src/vs/workbench/contrib/extensions/browser/experimentalRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/experimentalRecommendations.ts index 85c3fb606e..351167edad 100644 --- a/src/vs/workbench/contrib/extensions/browser/experimentalRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/experimentalRecommendations.ts @@ -3,16 +3,10 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { isNonEmptyArray } from 'vs/base/common/arrays'; -import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { INotificationService } from 'vs/platform/notification/common/notification'; +import { ExtensionRecommendations, ExtensionRecommendation, PromptedExtensionRecommendations } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IExperimentService, ExperimentActionType, ExperimentState } from 'vs/workbench/contrib/experiments/common/experimentService'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IStorageService } from 'vs/platform/storage/common/storage'; -import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; export class ExperimentalRecommendations extends ExtensionRecommendations { @@ -20,16 +14,10 @@ export class ExperimentalRecommendations extends ExtensionRecommendations { get recommendations(): ReadonlyArray { return this._recommendations; } constructor( - isExtensionAllowedToBeRecommended: (extensionId: string) => boolean, + promptedExtensionRecommendations: PromptedExtensionRecommendations, @IExperimentService private readonly experimentService: IExperimentService, - @IConfigurationService configurationService: IConfigurationService, - @IInstantiationService instantiationService: IInstantiationService, - @INotificationService notificationService: INotificationService, - @ITelemetryService telemetryService: ITelemetryService, - @IStorageService storageService: IStorageService, - @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, ) { - super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService); + super(promptedExtensionRecommendations); } /** diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index 7aff5709e1..5a5f5146df 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -15,7 +15,7 @@ import { isPromiseCanceledError } from 'vs/base/common/errors'; import { dispose, toDisposable, Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { domEvent } from 'vs/base/browser/event'; import { append, $, addClass, removeClass, finalHandler, join, toggleClass, hide, show, addDisposableListener, EventType } from 'vs/base/browser/dom'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -25,7 +25,7 @@ import { ResolvedKeybinding, KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, IExtension, ExtensionContainers } from 'vs/workbench/contrib/extensions/common/extensions'; import { /*RatingsWidget, InstallCountWidget, */RemoteBadgeWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets'; -import { EditorOptions } from 'vs/workbench/common/editor'; +import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { CombinedInstallAction, UpdateAction, ExtensionEditorDropDownAction, ReloadAction, MaliciousStatusLabelAction, IgnoreExtensionRecommendationAction, UndoIgnoreExtensionRecommendationAction, EnableDropDownAction, DisableDropDownAction, StatusLabelAction, SetFileIconThemeAction, SetColorThemeAction, RemoteInstallAction, ExtensionToolTipAction, SystemDisabledWarningAction, LocalInstallAction, SyncIgnoredIconAction, SetProductIconThemeAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -165,7 +165,7 @@ interface IExtensionEditorTemplate { header: HTMLElement; } -export class ExtensionEditor extends BaseEditor { +export class ExtensionEditor extends EditorPane { static readonly ID: string = 'workbench.editor.extension'; @@ -315,8 +315,8 @@ export class ExtensionEditor extends BaseEditor { return disposables; } - async setInput(input: ExtensionsInput, options: EditorOptions | undefined, token: CancellationToken): Promise { - await super.setInput(input, options, token); + async setInput(input: ExtensionsInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + await super.setInput(input, options, context, token); if (this.template) { await this.updateTemplate(input, this.template, !!options?.preserveFocus); } @@ -927,6 +927,7 @@ export class ExtensionEditor extends BaseEditor { this.renderLocalizations(content, manifest, layout), renderDashboardContributions(content, manifest, layout), // {{SQL CARBON EDIT}} this.renderCustomEditors(content, manifest, layout), + this.renderAuthentication(content, manifest, layout), ]; scrollableContent.scanDomNode(); @@ -1174,6 +1175,32 @@ export class ExtensionEditor extends BaseEditor { return true; } + private renderAuthentication(container: HTMLElement, manifest: IExtensionManifest, onDetailsToggle: Function): boolean { + const authentication = manifest.contributes?.authentication || []; + if (!authentication.length) { + return false; + } + + const details = $('details', { open: true, ontoggle: onDetailsToggle }, + $('summary', { tabindex: '0' }, localize('authentication', "Authentication ({0})", authentication.length)), + $('table', undefined, + $('tr', undefined, + $('th', undefined, localize('authentication.label', "Label")), + $('th', undefined, localize('authentication.id', "Id")) + ), + ...authentication.map(action => + $('tr', undefined, + $('td', undefined, action.label), + $('td', undefined, action.id) + ) + ) + ) + ); + + append(container, details); + return true; + } + private renderColorThemes(container: HTMLElement, manifest: IExtensionManifest, onDetailsToggle: Function): boolean { const contrib = manifest.contributes?.themes || []; if (!contrib.length) { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendations.ts index 48194e95cd..fbc6deb915 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendations.ts @@ -8,19 +8,27 @@ import { INotificationService, Severity } from 'vs/platform/notification/common/ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { localize } from 'vs/nls'; -import { InstallRecommendedExtensionAction, ShowRecommendedExtensionAction, ShowRecommendedExtensionsAction, InstallRecommendedExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; -import { ExtensionRecommendationSource, IExtensionRecommendationReson } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { IExtensionsConfiguration, ConfigurationKey } from 'vs/workbench/contrib/extensions/common/extensions'; +import { SearchExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { EnablementState, ExtensionRecommendationSource, IExtensionRecommendationReson, IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IExtensionsConfiguration, ConfigurationKey, IExtension, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { IAction } from 'vs/base/common/actions'; +import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; type ExtensionRecommendationsNotificationClassification = { userReaction: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; - extensionId: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' }; + extensionId?: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' }; }; +type ExtensionWorkspaceRecommendationsNotificationClassification = { + userReaction: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; +}; + +const ignoreWorkspaceRecommendationsStorageKey = 'extensionsAssistant/workspaceRecommendationsIgnore'; const ignoreImportantExtensionRecommendation = 'extensionsAssistant/importantRecommendationsIgnore'; const choiceNever = localize('neverShowAgain', "Don't Show Again"); @@ -36,16 +44,9 @@ export abstract class ExtensionRecommendations extends Disposable { protected abstract doActivate(): Promise; constructor( - protected readonly isExtensionAllowedToBeRecommended: (extensionId: string) => boolean, - @IInstantiationService protected readonly instantiationService: IInstantiationService, - @IConfigurationService protected readonly configurationService: IConfigurationService, - @INotificationService protected readonly notificationService: INotificationService, - @ITelemetryService protected readonly telemetryService: ITelemetryService, - @IStorageService protected readonly storageService: IStorageService, - @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, + protected readonly promptedExtensionRecommendations: PromptedExtensionRecommendations, ) { super(); - storageKeysSyncRegistryService.registerStorageKey({ key: ignoreImportantExtensionRecommendation, version: 1 }); } private _activationPromise: Promise | null = null; @@ -57,47 +58,63 @@ export abstract class ExtensionRecommendations extends Disposable { return this._activationPromise; } - private runAction(action: IAction) { - try { - action.run(); - } finally { - action.dispose(); - } +} + +export class PromptedExtensionRecommendations extends Disposable { + + constructor( + private readonly isExtensionAllowedToBeRecommended: (extensionId: string) => boolean, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @INotificationService private readonly notificationService: INotificationService, + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IStorageService private readonly storageService: IStorageService, + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, + @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, + @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, + ) { + super(); + storageKeysSyncRegistryService.registerStorageKey({ key: ignoreImportantExtensionRecommendation, version: 1 }); } - protected promptImportantExtensionsInstallNotification(extensionIds: string[], message: string): void { + async promptImportantExtensionsInstallNotification(extensionIds: string[], message: string, searchValue: string): Promise { + if (this.hasToIgnoreRecommendationNotifications()) { + return; + } + + const extensions = await this.getInstallableExtensions(extensionIds); + if (!extensions.length) { + return; + } + this.notificationService.prompt(Severity.Info, message, [{ - label: extensionIds.length === 1 ? localize('install', 'Install') : localize('installAll', "Install All"), + label: localize('install', "Install"), run: async () => { - for (const extensionId of extensionIds) { - this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'install', extensionId }); - } - if (extensionIds.length === 1) { - this.runAction(this.instantiationService.createInstance(InstallRecommendedExtensionAction, extensionIds[0])); - } else { - this.runAction(this.instantiationService.createInstance(InstallRecommendedExtensionsAction, InstallRecommendedExtensionsAction.ID, InstallRecommendedExtensionsAction.LABEL, extensionIds, 'install-recommendations')); - } + this.runAction(this.instantiationService.createInstance(SearchExtensionsAction, searchValue)); + await Promise.all(extensions.map(async extension => { + this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'install', extensionId: extension.identifier.id }); + this.extensionsWorkbenchService.open(extension, { pinned: true }); + await this.extensionManagementService.installFromGallery(extension.gallery!); + })); } }, { - label: extensionIds.length === 1 ? localize('moreInformation', "More Information") : localize('showRecommendations', "Show Recommendations"), - run: () => { - for (const extensionId of extensionIds) { - this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'show', extensionId }); - } - if (extensionIds.length === 1) { - this.runAction(this.instantiationService.createInstance(ShowRecommendedExtensionAction, extensionIds[0])); - } else { - this.runAction(this.instantiationService.createInstance(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, ShowRecommendedExtensionsAction.LABEL)); + label: localize('show recommendations', "Show Recommendations"), + run: async () => { + for (const extension of extensions) { + this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'show', extensionId: extension.identifier.id }); + this.extensionsWorkbenchService.open(extension, { pinned: true }); } + this.runAction(this.instantiationService.createInstance(SearchExtensionsAction, searchValue)); } }, { label: choiceNever, isSecondary: true, run: () => { - for (const extensionId of extensionIds) { - this.addToImportantRecommendationsIgnore(extensionId); - this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'neverShowAgain', extensionId }); + for (const extension of extensions) { + this.addToImportantRecommendationsIgnore(extension.identifier.id); + this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'neverShowAgain', extensionId: extension.identifier.id }); } this.notificationService.prompt( Severity.Info, @@ -115,20 +132,78 @@ export abstract class ExtensionRecommendations extends Disposable { { sticky: true, onCancel: () => { - for (const extensionId of extensionIds) { - this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'cancelled', extensionId }); + for (const extension of extensions) { + this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'cancelled', extensionId: extension.identifier.id }); } } } ); } - protected hasToIgnoreRecommendationNotifications(): boolean { + async promptWorkspaceRecommendations(recommendations: string[]): Promise { + if (this.hasToIgnoreWorkspaceRecommendationNotifications()) { + return; + } + + let installed = await this.extensionManagementService.getInstalled(); + installed = installed.filter(l => this.extensionEnablementService.getEnablementState(l) !== EnablementState.DisabledByExtensionKind); // Filter extensions disabled by kind + recommendations = recommendations.filter(extensionId => installed.every(local => !areSameExtensions({ id: extensionId }, local.identifier))); + + if (!recommendations.length) { + return; + } + + const extensions = await this.getInstallableExtensions(recommendations); + if (!extensions.length) { + return; + } + + const searchValue = '@recommended '; + this.notificationService.prompt( + Severity.Info, + localize('workspaceRecommended', "Do you want to install the recommended extensions for this repository?"), + [{ + label: localize('install', "Install"), + run: async () => { + this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'install' }); + await Promise.all(extensions.map(async extension => { + this.extensionsWorkbenchService.open(extension, { pinned: true }); + await this.extensionManagementService.installFromGallery(extension.gallery!); + })); + } + }, { + label: localize('showRecommendations', "Show Recommendations"), + run: async () => { + this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'show' }); + this.runAction(this.instantiationService.createInstance(SearchExtensionsAction, searchValue)); + } + }, { + label: localize('neverShowAgain', "Don't Show Again"), + isSecondary: true, + run: () => { + this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'neverShowAgain' }); + this.storageService.store(ignoreWorkspaceRecommendationsStorageKey, true, StorageScope.WORKSPACE); + } + }], + { + sticky: true, + onCancel: () => { + this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'cancelled' }); + } + } + ); + } + + hasToIgnoreRecommendationNotifications(): boolean { const config = this.configurationService.getValue(ConfigurationKey); return config.ignoreRecommendations || config.showRecommendationsOnlyOnDemand; } - protected filterIgnoredOrNotAllowed(recommendationsToSuggest: string[]): string[] { + hasToIgnoreWorkspaceRecommendationNotifications(): boolean { + return this.hasToIgnoreRecommendationNotifications() || this.storageService.getBoolean(ignoreWorkspaceRecommendationsStorageKey, StorageScope.WORKSPACE, false); + } + + filterIgnoredOrNotAllowed(recommendationsToSuggest: string[]): string[] { const importantRecommendationsIgnoreList = (JSON.parse(this.storageService.get(ignoreImportantExtensionRecommendation, StorageScope.GLOBAL, '[]'))).map(e => e.toLowerCase()); return recommendationsToSuggest.filter(id => { if (importantRecommendationsIgnoreList.indexOf(id) !== -1) { @@ -141,6 +216,27 @@ export abstract class ExtensionRecommendations extends Disposable { }); } + private async getInstallableExtensions(extensionIds: string[]): Promise { + const extensions: IExtension[] = []; + if (extensionIds.length) { + const pager = await this.extensionsWorkbenchService.queryGallery({ names: extensionIds, pageSize: extensionIds.length, source: 'install-recommendations' }, CancellationToken.None); + for (const extension of pager.firstPage) { + if (extension.gallery && (await this.extensionManagementService.canInstall(extension.gallery))) { + extensions.push(extension); + } + } + } + return extensions; + } + + private async runAction(action: IAction): Promise { + try { + await action.run(); + } finally { + action.dispose(); + } + } + private addToImportantRecommendationsIgnore(id: string) { const importantRecommendationsIgnoreList = JSON.parse(this.storageService.get(ignoreImportantExtensionRecommendation, StorageScope.GLOBAL, '[]')); importantRecommendationsIgnoreList.push(id.toLowerCase()); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts index 74dd72c0e9..750d0cb5d8 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts @@ -22,12 +22,12 @@ import { ExperimentalRecommendations } from 'vs/workbench/contrib/extensions/bro import { WorkspaceRecommendations } from 'vs/workbench/contrib/extensions/browser/workspaceRecommendations'; import { FileBasedRecommendations } from 'vs/workbench/contrib/extensions/browser/fileBasedRecommendations'; import { KeymapRecommendations } from 'vs/workbench/contrib/extensions/browser/keymapRecommendations'; -import { ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; -import { ExtensionsPolicyKey, ExtensionsPolicy } from 'vs/platform/extensions/common/extensions'; -import { StaticRecommendations } from 'sql/workbench/contrib/extensions/browser/staticRecommendations'; -import { ScenarioRecommendations } from 'sql/workbench/contrib/extensions/browser/scenarioRecommendations'; +import { ExtensionRecommendation, PromptedExtensionRecommendations } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { ConfigBasedRecommendations } from 'vs/workbench/contrib/extensions/browser/configBasedRecommendations'; +import { StaticRecommendations } from 'sql/workbench/contrib/extensions/browser/staticRecommendations'; +import { ScenarioRecommendations } from 'sql/workbench/contrib/extensions/browser/scenarioRecommendations'; +import { ExtensionsPolicyKey, ExtensionsPolicy } from 'vs/platform/extensions/common/extensions'; type IgnoreRecommendationClassification = { recommendationReason: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; @@ -40,6 +40,8 @@ export class ExtensionRecommendationsService extends Disposable implements IExte declare readonly _serviceBrand: undefined; + private readonly promptedExtensionRecommendations: PromptedExtensionRecommendations; + // Recommendations private readonly fileBasedRecommendations: FileBasedRecommendations; private readonly workspaceRecommendations: WorkspaceRecommendations; @@ -54,7 +56,7 @@ export class ExtensionRecommendationsService extends Disposable implements IExte // Ignored Recommendations private globallyIgnoredRecommendations: string[] = []; - public loadWorkspaceConfigPromise: Promise; + public readonly activationPromise: Promise; private sessionSeed: number; private readonly _onRecommendationChange = this._register(new Emitter()); @@ -62,7 +64,7 @@ export class ExtensionRecommendationsService extends Disposable implements IExte constructor( @IInstantiationService instantiationService: IInstantiationService, - @ILifecycleService lifecycleService: ILifecycleService, + @ILifecycleService private readonly lifecycleService: ILifecycleService, @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @IStorageService private readonly storageService: IStorageService, @@ -76,19 +78,20 @@ export class ExtensionRecommendationsService extends Disposable implements IExte storageKeysSyncRegistryService.registerStorageKey({ key: ignoredRecommendationsStorageKey, version: 1 }); const isExtensionAllowedToBeRecommended = (extensionId: string) => this.isExtensionAllowedToBeRecommended(extensionId); - this.workspaceRecommendations = instantiationService.createInstance(WorkspaceRecommendations, isExtensionAllowedToBeRecommended); - this.fileBasedRecommendations = instantiationService.createInstance(FileBasedRecommendations, isExtensionAllowedToBeRecommended); - this.experimentalRecommendations = instantiationService.createInstance(ExperimentalRecommendations, isExtensionAllowedToBeRecommended); - this.configBasedRecommendations = instantiationService.createInstance(ConfigBasedRecommendations, isExtensionAllowedToBeRecommended); - this.exeBasedRecommendations = instantiationService.createInstance(ExeBasedRecommendations, isExtensionAllowedToBeRecommended); - this.dynamicWorkspaceRecommendations = instantiationService.createInstance(DynamicWorkspaceRecommendations, isExtensionAllowedToBeRecommended); - this.keymapRecommendations = instantiationService.createInstance(KeymapRecommendations, isExtensionAllowedToBeRecommended); - this.staticRecommendations = instantiationService.createInstance(StaticRecommendations, isExtensionAllowedToBeRecommended); // {{SQL CARBON EDIT}} add ours - this.scenarioRecommendations = instantiationService.createInstance(ScenarioRecommendations, isExtensionAllowedToBeRecommended); // {{SQL CARBON EDIT}} add ours + this.promptedExtensionRecommendations = instantiationService.createInstance(PromptedExtensionRecommendations, isExtensionAllowedToBeRecommended); + this.workspaceRecommendations = instantiationService.createInstance(WorkspaceRecommendations, this.promptedExtensionRecommendations); + this.fileBasedRecommendations = instantiationService.createInstance(FileBasedRecommendations, this.promptedExtensionRecommendations); + this.experimentalRecommendations = instantiationService.createInstance(ExperimentalRecommendations, this.promptedExtensionRecommendations); + this.configBasedRecommendations = instantiationService.createInstance(ConfigBasedRecommendations, this.promptedExtensionRecommendations); + this.exeBasedRecommendations = instantiationService.createInstance(ExeBasedRecommendations, this.promptedExtensionRecommendations); + this.dynamicWorkspaceRecommendations = instantiationService.createInstance(DynamicWorkspaceRecommendations, this.promptedExtensionRecommendations); + this.keymapRecommendations = instantiationService.createInstance(KeymapRecommendations, this.promptedExtensionRecommendations); + this.staticRecommendations = instantiationService.createInstance(StaticRecommendations, this.promptedExtensionRecommendations); // {{SQL CARBON EDIT}} add ours + this.scenarioRecommendations = instantiationService.createInstance(ScenarioRecommendations, this.promptedExtensionRecommendations); // {{SQL CARBON EDIT}} add ours if (!this.isEnabled()) { this.sessionSeed = 0; - this.loadWorkspaceConfigPromise = Promise.resolve(); + this.activationPromise = Promise.resolve(); return; } @@ -96,19 +99,35 @@ export class ExtensionRecommendationsService extends Disposable implements IExte this.globallyIgnoredRecommendations = this.getCachedIgnoredRecommendations(); // Activation - this.loadWorkspaceConfigPromise = this.workspaceRecommendations.activate().then(() => this.fileBasedRecommendations.activate()); - this.experimentalRecommendations.activate(); - this.keymapRecommendations.activate(); - this.staticRecommendations.activate(); // {{SQL CARBON EDIT}} add ours - this.scenarioRecommendations.activate(); // {{SQL CARBON EDIT}} add ours - if (!this.configurationService.getValue(ShowRecommendationsOnlyOnDemandKey)) { - lifecycleService.when(LifecyclePhase.Eventually).then(() => this.activateProactiveRecommendations()); - } + this.activationPromise = this.activate(); this._register(this.extensionManagementService.onDidInstallExtension(e => this.onDidInstallExtension(e))); this._register(this.storageService.onDidChangeStorage(e => this.onDidStorageChange(e))); } + private async activate(): Promise { + await this.lifecycleService.when(LifecyclePhase.Restored); + + // activate all recommendations + await Promise.all([ + this.workspaceRecommendations.activate(), + this.fileBasedRecommendations.activate(), + this.experimentalRecommendations.activate(), + this.keymapRecommendations.activate(), + this.staticRecommendations.activate(), // {{SQL CARBON EDIT}} add ours + this.scenarioRecommendations.activate(), // {{SQL CARBON EDIT}} add ours + this.lifecycleService.when(LifecyclePhase.Eventually) + .then(() => { + if (!this.configurationService.getValue(ShowRecommendationsOnlyOnDemandKey)) { + this.activateProactiveRecommendations(); + } + }) + ]); + + await this.promptWorkspaceRecommendations(); + this._register(Event.any(this.workspaceRecommendations.onDidChangeRecommendations, this.configBasedRecommendations.onDidChangeRecommendations)(() => this.promptWorkspaceRecommendations())); + } + private isEnabled(): boolean { return this.galleryService.isEnabled() && !this.environmentService.extensionDevelopmentLocationURI && this.configurationService.getValue(ExtensionsPolicyKey) !== ExtensionsPolicy.allowNone; } @@ -143,9 +162,12 @@ export class ExtensionRecommendationsService extends Disposable implements IExte return output; } - async getConfigBasedRecommendations(): Promise { + async getConfigBasedRecommendations(): Promise<{ important: IExtensionRecommendation[], others: IExtensionRecommendation[] }> { await this.configBasedRecommendations.activate(); - return this.toExtensionRecommendations(this.configBasedRecommendations.recommendations); + return { + important: this.toExtensionRecommendations(this.configBasedRecommendations.importantRecommendations), + others: this.toExtensionRecommendations(this.configBasedRecommendations.otherRecommendations) + }; } async getOtherRecommendations(): Promise { @@ -202,6 +224,13 @@ export class ExtensionRecommendationsService extends Disposable implements IExte return this.toExtensionRecommendations(this.workspaceRecommendations.recommendations); } + async getExeBasedRecommendations(exe?: string): Promise<{ important: IExtensionRecommendation[], others: IExtensionRecommendation[] }> { + await this.exeBasedRecommendations.activate(); + const { important, others } = exe ? this.exeBasedRecommendations.getRecommendations(exe) + : { important: this.exeBasedRecommendations.importantRecommendations, others: this.exeBasedRecommendations.otherRecommendations }; + return { important: this.toExtensionRecommendations(important), others: this.toExtensionRecommendations(others) }; + } + getFileBasedRecommendations(): IExtensionRecommendation[] { return this.toExtensionRecommendations(this.fileBasedRecommendations.recommendations); } @@ -265,6 +294,16 @@ export class ExtensionRecommendationsService extends Disposable implements IExte return allIgnoredRecommendations.indexOf(id.toLowerCase()) === -1; } + private async promptWorkspaceRecommendations(): Promise { + const allowedRecommendations = [...this.workspaceRecommendations.recommendations, ...this.configBasedRecommendations.importantRecommendations] + .map(({ extensionId }) => extensionId) + .filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId)); + + if (allowedRecommendations.length) { + await this.promptedExtensionRecommendations.promptWorkspaceRecommendations(allowedRecommendations); + } + } + private onDidStorageChange(e: IWorkspaceStorageChangeEvent): void { if (e.key === ignoredRecommendationsStorageKey && e.scope === StorageScope.GLOBAL && this.ignoredRecommendationsValue !== this.getStoredIgnoredRecommendationsValue() /* This checks if current window changed the value or not */) { diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index f303eeb103..a8a9f123e7 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -15,11 +15,10 @@ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWo import { IOutputChannelRegistry, Extensions as OutputExtensions } from 'vs/workbench/services/output/common/output'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID } from 'vs/workbench/contrib/extensions/common/extensions'; -import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService'; import { OpenExtensionsViewletAction, InstallExtensionsAction, ShowOutdatedExtensionsAction, ShowRecommendedExtensionsAction, ShowRecommendedKeymapExtensionsAction, ShowPopularExtensionsAction, ShowEnabledExtensionsAction, ShowInstalledExtensionsAction, ShowDisabledExtensionsAction, ShowBuiltInExtensionsAction, UpdateAllAction, - EnableAllAction, EnableAllWorkspaceAction, DisableAllAction, DisableAllWorkspaceAction, CheckForUpdatesAction, ShowLanguageExtensionsAction, ShowAzureExtensionsAction, EnableAutoUpdateAction, DisableAutoUpdateAction, ConfigureRecommendedExtensionsCommandsContributor, InstallVSIXAction, ReinstallAction, InstallSpecificVersionOfExtensionAction, ClearExtensionsSearchResultsAction + EnableAllAction, EnableAllWorkspaceAction, DisableAllAction, DisableAllWorkspaceAction, CheckForUpdatesAction, ShowLanguageExtensionsAction, EnableAutoUpdateAction, DisableAutoUpdateAction, ConfigureRecommendedExtensionsCommandsContributor, InstallVSIXAction, ReinstallAction, InstallSpecificVersionOfExtensionAction, ClearExtensionsSearchResultsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; import { ExtensionEditor } from 'vs/workbench/contrib/extensions/browser/extensionEditor'; @@ -55,7 +54,7 @@ import { MultiCommand } from 'vs/editor/browser/editorExtensions'; import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; // Singletons -registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); +// registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); // TODO@sandbox TODO@ben uncomment when 'semver-umd' can be loaded registerSingleton(IExtensionRecommendationsService, ExtensionRecommendationsService); Registry.as(OutputExtensions.OutputChannels) @@ -113,9 +112,6 @@ actionRegistry.registerWorkbenchAction(keymapRecommendationsActionDescriptor, 'P const languageExtensionsActionDescriptor = SyncActionDescriptor.from(ShowLanguageExtensionsAction); actionRegistry.registerWorkbenchAction(languageExtensionsActionDescriptor, 'Preferences: Language Extensions', PreferencesLabel); -const azureExtensionsActionDescriptor = SyncActionDescriptor.from(ShowAzureExtensionsAction); -actionRegistry.registerWorkbenchAction(azureExtensionsActionDescriptor, 'Preferences: Azure Extensions', PreferencesLabel); - const popularActionDescriptor = SyncActionDescriptor.from(ShowPopularExtensionsAction); actionRegistry.registerWorkbenchAction(popularActionDescriptor, 'Extensions: Show Popular Extensions', ExtensionsLabel); diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.web.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.web.contribution.ts new file mode 100644 index 0000000000..a602b0ec75 --- /dev/null +++ b/src/vs/workbench/contrib/extensions/browser/extensions.web.contribution.ts @@ -0,0 +1,11 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; +import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService'; + +// TODO@sandbox TODO@ben move back into common/extensions.contribution.ts when 'semver-umd' can be loaded +registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 224ac29c90..d687751eb5 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -35,7 +35,6 @@ import { Color } from 'vs/base/common/color'; import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing'; import { ITextEditorSelection } from 'vs/platform/editor/common/editor'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { PagedModel } from 'vs/base/common/paging'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { MenuRegistry, MenuId, IMenuService } from 'vs/platform/actions/common/actions'; @@ -928,7 +927,7 @@ export class EnableForWorkspaceAction extends ExtensionAction { if (this.extension && this.extension.local) { this.enabled = this.extension.state === ExtensionState.Installed && !this.extensionEnablementService.isEnabled(this.extension.local) - && this.extensionEnablementService.canChangeEnablement(this.extension.local); + && this.extensionEnablementService.canChangeWorkspaceEnablement(this.extension.local); } } @@ -989,7 +988,7 @@ export class DisableForWorkspaceAction extends ExtensionAction { if (this.extension && this.extension.local && this.runningExtensions.some(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, this.extension!.identifier) && this.workspaceContextService.getWorkbenchState() !== WorkbenchState.EMPTY)) { this.enabled = this.extension.state === ExtensionState.Installed && (this.extension.enablementState === EnablementState.EnabledGlobally || this.extension.enablementState === EnablementState.EnabledWorkspace) - && this.extensionEnablementService.canChangeEnablement(this.extension.local); + && this.extensionEnablementService.canChangeWorkspaceEnablement(this.extension.local); } } @@ -1850,86 +1849,6 @@ export class ShowRecommendedExtensionsAction extends Action { } } -export class InstallRecommendedExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.installRecommendedExtensions'; - static readonly LABEL = localize('installRecommendedExtensions', "Install Recommended Extensions"); - - private _recommendations: string[] = []; - get recommendations(): string[] { return this._recommendations; } - set recommendations(recommendations: string[]) { this._recommendations = recommendations; this.enabled = this._recommendations.length > 0; } - - constructor( - id: string, - label: string, - recommendations: string[], - private readonly source: string, - @IViewletService private readonly viewletService: IViewletService, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IExtensionsWorkbenchService private readonly extensionWorkbenchService: IExtensionsWorkbenchService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, - @IProductService private readonly productService: IProductService, - ) { - super(id, label, 'extension-action'); - this.recommendations = recommendations; - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@recommended '); - viewlet.focus(); - const names = this.recommendations; - return this.extensionWorkbenchService.queryGallery({ names, source: this.source }, CancellationToken.None).then(pager => { - let installPromises: Promise[] = []; - let model = new PagedModel(pager); - for (let i = 0; i < pager.total; i++) { - installPromises.push(model.resolve(i, CancellationToken.None).then(e => this.installExtension(e))); - } - return Promise.all(installPromises); - }); - }); - } - - private async installExtension(extension: IExtension): Promise { - try { - if (extension.local && extension.gallery) { - if (prefersExecuteOnUI(extension.local.manifest, this.productService, this.configurationService)) { - if (this.extensionManagementServerService.localExtensionManagementServer) { - await this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.installFromGallery(extension.gallery); - return; - } - } else if (this.extensionManagementServerService.remoteExtensionManagementServer) { - await this.extensionManagementServerService.remoteExtensionManagementServer.extensionManagementService.installFromGallery(extension.gallery); - return; - } - } - await this.extensionWorkbenchService.install(extension); - } catch (err) { - console.error(err); - return promptDownloadManually(extension.gallery, localize('failedToInstall', "Failed to install \'{0}\'.", extension.identifier.id), err, this.instantiationService); - } - } -} - -export class InstallWorkspaceRecommendedExtensionsAction extends InstallRecommendedExtensionsAction { - - constructor( - recommendations: string[], - @IViewletService viewletService: IViewletService, - @IInstantiationService instantiationService: IInstantiationService, - @IExtensionsWorkbenchService extensionWorkbenchService: IExtensionsWorkbenchService, - @IConfigurationService configurationService: IConfigurationService, - @IExtensionManagementServerService extensionManagementServerService: IExtensionManagementServerService, - @IProductService productService: IProductService, - ) { - super('workbench.extensions.action.installWorkspaceRecommendedExtensions', localize('installWorkspaceRecommendedExtensions', "Install Workspace Recommended Extensions"), recommendations, 'install-all-workspace-recommendations', - viewletService, instantiationService, extensionWorkbenchService, configurationService, extensionManagementServerService, productService); - } -} - export class ShowRecommendedExtensionAction extends Action { static readonly ID = 'workbench.extensions.action.showRecommendedExtension'; @@ -1942,7 +1861,7 @@ export class ShowRecommendedExtensionAction extends Action { @IViewletService private readonly viewletService: IViewletService, @IExtensionsWorkbenchService private readonly extensionWorkbenchService: IExtensionsWorkbenchService, ) { - super(InstallRecommendedExtensionAction.ID, InstallRecommendedExtensionAction.LABEL, undefined, false); + super(ShowRecommendedExtensionAction.ID, ShowRecommendedExtensionAction.LABEL, undefined, false); this.extensionId = extensionId; } @@ -2096,29 +2015,6 @@ export class ShowLanguageExtensionsAction extends Action { } } -export class ShowAzureExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.showAzureExtensions'; - static readonly LABEL = localize('showAzureExtensionsShort', "Azure Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@sort:installs azure '); - viewlet.focus(); - }); - } -} - export class SearchCategoryAction extends Action { constructor( @@ -2131,12 +2027,23 @@ export class SearchCategoryAction extends Action { } run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search(`@category:"${this.category.toLowerCase()}"`); - viewlet.focus(); - }); + return new SearchExtensionsAction(`@category:"${this.category.toLowerCase()}"`, this.viewletService).run(); + } +} + +export class SearchExtensionsAction extends Action { + + constructor( + private readonly searchValue: string, + @IViewletService private readonly viewletService: IViewletService + ) { + super('extensions.searchExtensions', localize('search recommendations', "Search Extensions"), undefined, true); + } + + async run(): Promise { + const viewPaneContainer = (await this.viewletService.openViewlet(VIEWLET_ID, true))?.getViewPaneContainer() as IExtensionsViewPaneContainer; + viewPaneContainer.search(this.searchValue); + viewPaneContainer.focus(); } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index dd122ce10c..c26e58b33b 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -60,6 +60,7 @@ import { IPreferencesService } from 'vs/workbench/services/preferences/common/pr import { DragAndDropObserver } from 'vs/workbench/browser/dnd'; import { URI } from 'vs/base/common/uri'; import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; const NonEmptyWorkspaceContext = new RawContextKey('nonEmptyWorkspace', false); const DefaultViewsContext = new RawContextKey('defaultExtensionViews', true); @@ -128,14 +129,18 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio if (this.extensionManagementServerService.localExtensionManagementServer) { servers.push(this.extensionManagementServerService.localExtensionManagementServer); } + if (this.extensionManagementServerService.webExtensionManagementServer) { + servers.push(this.extensionManagementServerService.webExtensionManagementServer); + } if (this.extensionManagementServerService.remoteExtensionManagementServer) { servers.push(this.extensionManagementServerService.remoteExtensionManagementServer); } - if (servers.length === 0 && this.extensionManagementServerService.webExtensionManagementServer) { - servers.push(this.extensionManagementServerService.webExtensionManagementServer); - } const getViewName = (viewTitle: string, server: IExtensionManagementServer): string => { - return servers.length > 1 ? `${server.label} - ${viewTitle}` : viewTitle; + if (servers.length) { + const serverLabel = server === this.extensionManagementServerService.webExtensionManagementServer && !this.extensionManagementServerService.localExtensionManagementServer ? localize('local', "Local") : server.label; + return servers.length > 1 ? `${serverLabel} - ${viewTitle}` : viewTitle; + } + return viewTitle; }; for (const server of servers) { const getInstalledViewName = (): string => getViewName(localize('installed', "Installed"), server); @@ -350,6 +355,8 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE @IInstantiationService instantiationService: IInstantiationService, @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, @INotificationService private readonly notificationService: INotificationService, @IViewletService private readonly viewletService: IViewletService, @@ -520,14 +527,19 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE ]); if (this.extensionGalleryService.isEnabled()) { - filterActions.splice(0, 0, ...[ + const galleryFilterActions = [ // this.instantiationService.createInstance(PredefinedExtensionFilterAction, 'extensions.filter.featured', localize('featured filter', "Featured"), '@featured'), // {{SQL CARBON EDIT}} // this.instantiationService.createInstance(PredefinedExtensionFilterAction, 'extensions.filter.popular', localize('most popular filter', "Most Popular"), '@popular'), // {{SQL CARBON EDIT}} this.instantiationService.createInstance(PredefinedExtensionFilterAction, 'extensions.filter.recommended', localize('most popular recommended', "Recommended"), '@recommended'), // this.instantiationService.createInstance(RecentlyPublishedExtensionsAction, RecentlyPublishedExtensionsAction.ID, localize('recently published filter', "Recently Published")), // {{SQL CARBON EDIT}} + new Separator(), new SubmenuAction('workbench.extensions.action.filterExtensionsByCategory', localize('filter by category', "Category"), EXTENSION_CATEGORIES.map(category => this.instantiationService.createInstance(SearchCategoryAction, `extensions.actions.searchByCategory.${category}`, category, category))), new Separator(), - ]); + ]; + if (this.extensionManagementServerService.webExtensionManagementServer || !this.environmentService.isBuilt) { + galleryFilterActions.splice(4, 0, this.instantiationService.createInstance(PredefinedExtensionFilterAction, 'extensions.filter.web', localize('web filter', "Web"), '@web')); + } + filterActions.splice(0, 0, ...galleryFilterActions); filterActions.push(...[ new Separator(), new SubmenuAction('workbench.extensions.action.sortBy', localize('sorty by', "Sort By"), this.sortActions), @@ -582,6 +594,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE .replace(/@tag:/g, 'tag:') .replace(/@ext:/g, 'ext:') .replace(/@featured/g, 'featured') + .replace(/@web/g, 'tag:"__web_extension"') .replace(/@popular/g, '@sort:installs') : ''; } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index b773241186..60db2f72ad 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -9,7 +9,7 @@ import { assign } from 'vs/base/common/objects'; import { Event, Emitter } from 'vs/base/common/event'; import { isPromiseCanceledError, getErrorMessage } from 'vs/base/common/errors'; import { PagedModel, IPagedModel, IPager, DelayedPagedModel } from 'vs/base/common/paging'; -import { SortBy, SortOrder, IQueryOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { SortBy, SortOrder, IQueryOptions, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionManagementServer, IExtensionManagementServerService, IExtensionRecommendationsService, IExtensionRecommendation, EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -17,7 +17,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { append, $, toggleClass, addClass } from 'vs/base/browser/dom'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Delegate, Renderer, IExtensionsViewState } from 'vs/workbench/contrib/extensions/browser/extensionsList'; -import { IExtension, IExtensionsWorkbenchService, ExtensionState } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IExtension, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; import { Query } from 'vs/workbench/contrib/extensions/common/extensionQuery'; import { IExtensionService, toExtension } from 'vs/workbench/services/extensions/common/extensions'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -25,13 +25,13 @@ import { attachBadgeStyler } from 'vs/platform/theme/common/styler'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; -import { InstallWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, ManageExtensionAction, InstallLocalExtensionsInRemoteAction, getContextMenuActions, ExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { ConfigureWorkspaceFolderRecommendedExtensionsAction, ManageExtensionAction, InstallLocalExtensionsInRemoteAction, getContextMenuActions, ExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { WorkbenchPagedList, ListResourceNavigator } from 'vs/platform/list/browser/listService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { distinct, coalesce, firstIndex } from 'vs/base/common/arrays'; +import { coalesce, distinct, flatten, firstIndex } from 'vs/base/common/arrays'; // {{ SQL CARBON EDIT }} import { IExperimentService, IExperiment, ExperimentActionType } from 'vs/workbench/contrib/experiments/common/experimentService'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { IListContextMenuEvent } from 'vs/base/browser/ui/list/list'; @@ -53,6 +53,10 @@ import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; // Extensions that are automatically classified as Programming Language extensions, but should be Feature extensions const FORCE_FEATURE_EXTENSIONS = ['vscode.git', 'vscode.search-result']; +type WorkspaceRecommendationsClassification = { + count: { classification: 'SystemMetaData', purpose: 'FeatureInsight', 'isMeasurement': true }; +}; + class ExtensionsViewState extends Disposable implements IExtensionsViewState { private readonly _onFocus: Emitter = this._register(new Emitter()); @@ -98,12 +102,13 @@ export class ExtensionsListView extends ViewPane { @IThemeService themeService: IThemeService, @IExtensionService private readonly extensionService: IExtensionService, @IExtensionsWorkbenchService protected extensionsWorkbenchService: IExtensionsWorkbenchService, - @IExtensionRecommendationsService protected tipsService: IExtensionRecommendationsService, + @IExtensionRecommendationsService protected extensionRecommendationsService: IExtensionRecommendationsService, @ITelemetryService telemetryService: ITelemetryService, @IConfigurationService configurationService: IConfigurationService, @IWorkspaceContextService protected contextService: IWorkspaceContextService, @IExperimentService private readonly experimentService: IExperimentService, @IExtensionManagementServerService protected readonly extensionManagementServerService: IExtensionManagementServerService, + @IExtensionManagementService protected readonly extensionManagementService: IExtensionManagementService, @IProductService protected readonly productService: IProductService, @IContextKeyService contextKeyService: IContextKeyService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @@ -471,14 +476,9 @@ export class ExtensionsListView extends ViewPane { } // {{SQL CARBON EDIT}} - End - if (ExtensionsListView.isWorkspaceRecommendedExtensionsQuery(query.value)) { - return this.getWorkspaceRecommendationsModel(query, options, token); - } else if (ExtensionsListView.isKeymapsRecommendedExtensionsQuery(query.value)) { - return this.getKeymapRecommendationsModel(query, options, token); - } else if (/@recommended:all/i.test(query.value) || ExtensionsListView.isSearchRecommendedExtensionsQuery(query.value)) { - return this.getAllRecommendationsModel(query, options, token); - } else if (ExtensionsListView.isRecommendedExtensionsQuery(query.value)) { - return this.getRecommendationsModel(query, options, token); + + if (this.isRecommendationsQuery(query)) { + return this.queryRecommendations(query, options, token); } else if (ExtensionsListView.isAllMarketplaceExtensionsQuery(query.value)) { // {{SQL CARBON EDIT}} add if return this.getAllMarketplaceModel(query, options, token); } @@ -557,51 +557,6 @@ export class ExtensionsListView extends ViewPane { return extensions; } - // Get All types of recommendations, trimmed to show a max of 8 at any given time - private getAllRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { - const value = query.value.replace(/@recommended:all/g, '').replace(/@recommended/g, '').trim().toLowerCase(); - - return this.extensionsWorkbenchService.queryLocal(this.server) - .then(result => result.filter(e => e.type === ExtensionType.User)) - .then(local => { - const fileBasedRecommendations = this.tipsService.getFileBasedRecommendations(); - const configBasedRecommendationsPromise = this.tipsService.getConfigBasedRecommendations(); - const othersPromise = this.tipsService.getOtherRecommendations(); - const workspacePromise = this.tipsService.getWorkspaceRecommendations(); - const importantRecommendationsPromise = this.tipsService.getImportantRecommendations(); - - return Promise.all([othersPromise, workspacePromise, configBasedRecommendationsPromise, importantRecommendationsPromise]) - .then(([others, workspaceRecommendations, configBasedRecommendations, importantRecommendations]) => { - const names = this.getTrimmedRecommendations(local, value, importantRecommendations, fileBasedRecommendations, configBasedRecommendations, others, workspaceRecommendations); - const recommendationsWithReason = this.tipsService.getAllRecommendationsWithReason(); - /* __GDPR__ - "extensionAllRecommendations:open" : { - "count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "recommendations": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetryService.publicLog('extensionAllRecommendations:open', { - count: names.length, - recommendations: names.map(id => { - return { - id, - recommendationReason: recommendationsWithReason[id.toLowerCase()].reasonId - }; - }) - }); - if (!names.length) { - return Promise.resolve(new PagedModel([])); - } - options.source = 'recommendations-all'; - return this.extensionsWorkbenchService.queryGallery(assign(options, { names, pageSize: names.length }), token) - .then(pager => { - this.sortFirstPage(pager, names); - return this.getPagedModel(pager || []); - }); - }); - }); - } - private async getCuratedModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { const value = query.value.replace(/curated:/g, '').trim(); const names = await this.experimentService.getCuratedExtensionsList(value); @@ -614,55 +569,13 @@ export class ExtensionsListView extends ViewPane { return new PagedModel([]); } - // Get All types of recommendations other than Workspace recommendations, trimmed to show a max of 8 at any given time - private getRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { - const value = query.value.replace(/@recommended/g, '').trim().toLowerCase(); - - return this.extensionsWorkbenchService.queryLocal(this.server) - .then(result => result.filter(e => e.type === ExtensionType.User)) - .then(local => { - let fileBasedRecommendations = this.tipsService.getFileBasedRecommendations(); - const configBasedRecommendationsPromise = this.tipsService.getConfigBasedRecommendations(); - const othersPromise = this.tipsService.getOtherRecommendations(); - const workspacePromise = this.tipsService.getWorkspaceRecommendations(); - const importantRecommendationsPromise = this.tipsService.getImportantRecommendations(); - - return Promise.all([othersPromise, workspacePromise, configBasedRecommendationsPromise, importantRecommendationsPromise]) - .then(([others, workspaceRecommendations, configBasedRecommendations, importantRecommendations]) => { - configBasedRecommendations = configBasedRecommendations.filter(x => workspaceRecommendations.every(({ extensionId }) => x.extensionId !== extensionId)); - fileBasedRecommendations = fileBasedRecommendations.filter(x => workspaceRecommendations.every(({ extensionId }) => x.extensionId !== extensionId)); - others = others.filter(x => workspaceRecommendations.every(({ extensionId }) => x.extensionId !== extensionId)); - - const names = this.getTrimmedRecommendations(local, value, importantRecommendations, fileBasedRecommendations, configBasedRecommendations, others, []); - const recommendationsWithReason = this.tipsService.getAllRecommendationsWithReason(); - - /* __GDPR__ - "extensionRecommendations:open" : { - "count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "recommendations": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetryService.publicLog('extensionRecommendations:open', { - count: names.length, - recommendations: names.map(id => { - return { - id, - recommendationReason: recommendationsWithReason[id.toLowerCase()].reasonId - }; - }) - }); - - if (!names.length) { - return Promise.resolve(new PagedModel([])); - } - options.source = 'recommendations'; - return this.extensionsWorkbenchService.queryGallery(assign(options, { names, pageSize: names.length }), token) - .then(pager => { - this.sortFirstPage(pager, names); - return this.getPagedModel(pager || []); - }); - }); - }); + private isRecommendationsQuery(query: Query): boolean { + return ExtensionsListView.isWorkspaceRecommendedExtensionsQuery(query.value) + || ExtensionsListView.isKeymapsRecommendedExtensionsQuery(query.value) + || ExtensionsListView.isExeRecommendedExtensionsQuery(query.value) + || /@recommended:all/i.test(query.value) + || ExtensionsListView.isSearchRecommendedExtensionsQuery(query.value) + || ExtensionsListView.isRecommendedExtensionsQuery(query.value); } // {{SQL CARBON EDIT}} @@ -671,7 +584,7 @@ export class ExtensionsListView extends ViewPane { return this.extensionsWorkbenchService.queryLocal() .then(result => result.filter(e => e.type === ExtensionType.User)) .then(local => { - return this.tipsService.getOtherRecommendations().then((recommmended) => { + return this.extensionRecommendationsService.getOtherRecommendations().then((recommmended) => { const installedExtensions = local.map(x => `${x.publisher}.${x.name}`); options = assign(options, { text: value, source: 'searchText' }); return this.extensionsWorkbenchService.queryGallery(options, token).then((pager) => { @@ -709,7 +622,7 @@ export class ExtensionsListView extends ViewPane { return this.extensionsWorkbenchService.queryLocal() .then(result => result.filter(e => e.type === ExtensionType.User)) .then(local => { - return this.tipsService.getRecommendedExtensionsByScenario(scenarioType).then((recommmended) => { + return this.extensionRecommendationsService.getRecommendedExtensionsByScenario(scenarioType).then((recommmended) => { const installedExtensions = local.map(x => `${x.publisher}.${x.name}`); return this.extensionsWorkbenchService.queryGallery(token).then((pager) => { // filter out installed extensions and the extensions not in the recommended list @@ -726,88 +639,129 @@ export class ExtensionsListView extends ViewPane { } // {{SQL CARBON EDIT}} - End - // Given all recommendations, trims and returns recommendations in the relevant order after filtering out installed extensions - private getTrimmedRecommendations(installedExtensions: IExtension[], value: string, importantRecommendations: IExtensionRecommendation[], fileBasedRecommendations: IExtensionRecommendation[], configBasedRecommendations: IExtensionRecommendation[], otherRecommendations: IExtensionRecommendation[], workspaceRecommendations: IExtensionRecommendation[]): string[] { - const totalCount = 10; - workspaceRecommendations = workspaceRecommendations - .filter(recommendation => { - return !this.isRecommendationInstalled(recommendation, installedExtensions) - && recommendation.extensionId.toLowerCase().indexOf(value) > -1; - }); - importantRecommendations = importantRecommendations - .filter(recommendation => { - return !this.isRecommendationInstalled(recommendation, installedExtensions) - && workspaceRecommendations.every(workspaceRecommendation => workspaceRecommendation.extensionId !== recommendation.extensionId) - && recommendation.extensionId.toLowerCase().indexOf(value) > -1; - }); - configBasedRecommendations = configBasedRecommendations - .filter(recommendation => { - return !this.isRecommendationInstalled(recommendation, installedExtensions) - && workspaceRecommendations.every(workspaceRecommendation => workspaceRecommendation.extensionId !== recommendation.extensionId) - && importantRecommendations.every(importantRecommendation => importantRecommendation.extensionId !== recommendation.extensionId) - && recommendation.extensionId.toLowerCase().indexOf(value) > -1; - }); - fileBasedRecommendations = fileBasedRecommendations.filter(recommendation => { - return !this.isRecommendationInstalled(recommendation, installedExtensions) - && workspaceRecommendations.every(workspaceRecommendation => workspaceRecommendation.extensionId !== recommendation.extensionId) - && importantRecommendations.every(importantRecommendation => importantRecommendation.extensionId !== recommendation.extensionId) - && configBasedRecommendations.every(configBasedRecommendation => configBasedRecommendation.extensionId !== recommendation.extensionId) - && recommendation.extensionId.toLowerCase().indexOf(value) > -1; - }); - otherRecommendations = otherRecommendations.filter(recommendation => { - return !this.isRecommendationInstalled(recommendation, installedExtensions) - && fileBasedRecommendations.every(fileBasedRecommendation => fileBasedRecommendation.extensionId !== recommendation.extensionId) - && workspaceRecommendations.every(workspaceRecommendation => workspaceRecommendation.extensionId !== recommendation.extensionId) - && importantRecommendations.every(importantRecommendation => importantRecommendation.extensionId !== recommendation.extensionId) - && configBasedRecommendations.every(configBasedRecommendation => configBasedRecommendation.extensionId !== recommendation.extensionId) - && recommendation.extensionId.toLowerCase().indexOf(value) > -1; - }); - - const otherCount = Math.min(2, otherRecommendations.length); - const fileBasedCount = Math.min(fileBasedRecommendations.length, totalCount - workspaceRecommendations.length - importantRecommendations.length - configBasedRecommendations.length - otherCount); - const recommendations = [...workspaceRecommendations, ...importantRecommendations, ...configBasedRecommendations]; - recommendations.push(...fileBasedRecommendations.splice(0, fileBasedCount)); - recommendations.push(...otherRecommendations.splice(0, otherCount)); - - return distinct(recommendations.map(({ extensionId }) => extensionId)); - } - - private isRecommendationInstalled(recommendation: IExtensionRecommendation, installed: IExtension[]): boolean { - return installed.some(i => areSameExtensions(i.identifier, { id: recommendation.extensionId })); - } - - private getWorkspaceRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { - const value = query.value.replace(/@recommended:workspace/g, '').trim().toLowerCase(); - return this.tipsService.getWorkspaceRecommendations() - .then(recommendations => { - const names = recommendations.map(({ extensionId }) => extensionId).filter(name => name.toLowerCase().indexOf(value) > -1); - /* __GDPR__ - "extensionWorkspaceRecommendations:open" : { - "count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } - } - */ - this.telemetryService.publicLog('extensionWorkspaceRecommendations:open', { count: names.length }); - - if (!names.length) { - return Promise.resolve(new PagedModel([])); - } - options.source = 'recommendations-workspace'; - return this.extensionsWorkbenchService.queryGallery(assign(options, { names, pageSize: names.length }), token) - .then(pager => this.getPagedModel(pager || [])); - }); - } - - private getKeymapRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { - const value = query.value.replace(/@recommended:keymaps/g, '').trim().toLowerCase(); - const names: string[] = this.tipsService.getKeymapRecommendations().map(({ extensionId }) => extensionId) - .filter(extensionId => extensionId.toLowerCase().indexOf(value) > -1); - - if (!names.length) { - return Promise.resolve(new PagedModel([])); + private async queryRecommendations(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { + // Workspace recommendations + if (ExtensionsListView.isWorkspaceRecommendedExtensionsQuery(query.value)) { + return this.getWorkspaceRecommendationsModel(query, options, token); } - options.source = 'recommendations-keymaps'; - return this.extensionsWorkbenchService.queryGallery(assign(options, { names, pageSize: names.length }), token) - .then(result => this.getPagedModel(result)); + + // Keymap recommendations + if (ExtensionsListView.isKeymapsRecommendedExtensionsQuery(query.value)) { + return this.getKeymapRecommendationsModel(query, options, token); + } + + // Exe recommendations + if (ExtensionsListView.isExeRecommendedExtensionsQuery(query.value)) { + return this.getExeRecommendationsModel(query, options, token); + } + + // All recommendations + if (/@recommended:all/i.test(query.value) || ExtensionsListView.isSearchRecommendedExtensionsQuery(query.value)) { + return this.getAllRecommendationsModel(query, options, token); + } + + // Other recommendations + if (ExtensionsListView.isRecommendedExtensionsQuery(query.value)) { + return this.getOtherRecommendationsModel(query, options, token); + } + + return new PagedModel([]); + } + + protected async getInstallableRecommendations(recommendations: IExtensionRecommendation[], options: IQueryOptions, token: CancellationToken): Promise { + const extensions: IExtension[] = []; + if (recommendations.length) { + const names = recommendations.map(({ extensionId }) => extensionId); + const pager = await this.extensionsWorkbenchService.queryGallery({ ...options, names, pageSize: names.length }, token); + for (const extension of pager.firstPage) { + if (extension.gallery && (await this.extensionManagementService.canInstall(extension.gallery))) { + extensions.push(extension); + } + } + } + return extensions; + } + + protected async getWorkspaceRecommendations(): Promise { + const recommendations = await this.extensionRecommendationsService.getWorkspaceRecommendations(); + const { important } = await this.extensionRecommendationsService.getConfigBasedRecommendations(); + for (const configBasedRecommendation of important) { + if (recommendations.some(r => r.extensionId !== configBasedRecommendation.extensionId)) { + recommendations.push(configBasedRecommendation); + } + } + return recommendations; + } + + private async getWorkspaceRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { + const value = query.value.replace(/@recommended:workspace/g, '').trim().toLowerCase(); + const recommendations = await this.getWorkspaceRecommendations(); + const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations-workspace' }, token)) + .filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1); + this.telemetryService.publicLog2<{ count: number }, WorkspaceRecommendationsClassification>('extensionWorkspaceRecommendations:open', { count: installableRecommendations.length }); + const result: IExtension[] = coalesce(recommendations.map(({ extensionId: id }) => installableRecommendations.find(i => areSameExtensions(i.identifier, { id })))); + return new PagedModel(result); + } + + private async getKeymapRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { + const value = query.value.replace(/@recommended:keymaps/g, '').trim().toLowerCase(); + const recommendations = this.extensionRecommendationsService.getKeymapRecommendations(); + const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations-keymaps' }, token)) + .filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1); + return new PagedModel(installableRecommendations); + } + + private async getExeRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { + const exe = query.value.replace(/@exe:/g, '').trim().toLowerCase(); + const { important, others } = await this.extensionRecommendationsService.getExeBasedRecommendations(exe.startsWith('"') ? exe.substring(1, exe.length - 1) : exe); + const installableRecommendations = await this.getInstallableRecommendations([...important, ...others], { ...options, source: 'recommendations-exe' }, token); + return new PagedModel(installableRecommendations); + } + + private async getOtherRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { + const value = query.value.replace(/@recommended/g, '').trim().toLowerCase(); + + const local = (await this.extensionsWorkbenchService.queryLocal(this.server)) + .filter(e => e.type === ExtensionType.User) + .map(e => e.identifier.id.toLowerCase()); + const workspaceRecommendations = (await this.getWorkspaceRecommendations()) + .map(r => r.extensionId.toLowerCase()); + + const otherRecommendations = distinct( + flatten(await Promise.all([ + // Order is important + this.extensionRecommendationsService.getImportantRecommendations(), + this.extensionRecommendationsService.getFileBasedRecommendations(), + this.extensionRecommendationsService.getOtherRecommendations() + ])).filter(({ extensionId }) => !local.includes(extensionId.toLowerCase()) && !workspaceRecommendations.includes(extensionId.toLowerCase()) + ), r => r.extensionId.toLowerCase()); + + const installableRecommendations = (await this.getInstallableRecommendations(otherRecommendations, { ...options, source: 'recommendations-other', sortBy: undefined }, token)) + .filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1); + + const result: IExtension[] = coalesce(otherRecommendations.map(({ extensionId: id }) => installableRecommendations.find(i => areSameExtensions(i.identifier, { id })))); + return new PagedModel(result); + } + + // Get All types of recommendations, trimmed to show a max of 8 at any given time + private async getAllRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { + const local = (await this.extensionsWorkbenchService.queryLocal(this.server)) + .filter(e => e.type === ExtensionType.User) + .map(e => e.identifier.id.toLowerCase()); + + const allRecommendations = distinct( + flatten(await Promise.all([ + // Order is important + this.getWorkspaceRecommendations(), + this.extensionRecommendationsService.getImportantRecommendations(), + this.extensionRecommendationsService.getFileBasedRecommendations(), + this.extensionRecommendationsService.getOtherRecommendations() + ])).filter(({ extensionId }) => !local.includes(extensionId.toLowerCase()) + ), r => r.extensionId.toLowerCase()); + + const installableRecommendations = await this.getInstallableRecommendations(allRecommendations, { ...options, source: 'recommendations-all', sortBy: undefined }, token); + const result: IExtension[] = coalesce(allRecommendations.map(({ extensionId: id }) => installableRecommendations.find(i => areSameExtensions(i.identifier, { id })))); + return new PagedModel(result.slice(0, 8)); } // Sorts the firstPage of the pager in the same order as given array of extension ids @@ -942,6 +896,10 @@ export class ExtensionsListView extends ViewPane { return /@recommended:workspace/i.test(query); } + static isExeRecommendedExtensionsQuery(query: string): boolean { + return /@exe:.+/i.test(query); + } + static isKeymapsRecommendedExtensionsQuery(query: string): boolean { return /@recommended:keymaps/i.test(query); } @@ -983,6 +941,7 @@ export class ServerExtensionsView extends ExtensionsListView { @IExperimentService experimentService: IExperimentService, @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, @IExtensionManagementServerService extensionManagementServerService: IExtensionManagementServerService, + @IExtensionManagementService extensionManagementService: IExtensionManagementService, @IProductService productService: IProductService, @IContextKeyService contextKeyService: IContextKeyService, @IMenuService menuService: IMenuService, @@ -991,7 +950,9 @@ export class ServerExtensionsView extends ExtensionsListView { @IPreferencesService preferencesService: IPreferencesService, ) { options.server = server; - super(options, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, tipsService, telemetryService, configurationService, contextService, experimentService, extensionManagementServerService, productService, contextKeyService, viewDescriptorService, menuService, openerService, preferencesService); + super(options, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, tipsService, + telemetryService, configurationService, contextService, experimentService, extensionManagementServerService, extensionManagementService, productService, + contextKeyService, viewDescriptorService, menuService, openerService, preferencesService); this._register(onDidChangeTitle(title => this.updateTitle(title))); } @@ -1076,7 +1037,7 @@ export class DefaultRecommendedExtensionsView extends ExtensionsListView { renderBody(container: HTMLElement): void { super.renderBody(container); - this._register(this.tipsService.onRecommendationChange(() => { + this._register(this.extensionRecommendationsService.onRecommendationChange(() => { this.show(''); })); } @@ -1101,7 +1062,7 @@ export class RecommendedExtensionsView extends ExtensionsListView { renderBody(container: HTMLElement): void { super.renderBody(container); - this._register(this.tipsService.onRecommendationChange(() => { + this._register(this.extensionRecommendationsService.onRecommendationChange(() => { this.show(''); })); } @@ -1114,20 +1075,18 @@ export class RecommendedExtensionsView extends ExtensionsListView { export class WorkspaceRecommendedExtensionsView extends ExtensionsListView { private readonly recommendedExtensionsQuery = '@recommended:workspace'; - private installAllAction: InstallWorkspaceRecommendedExtensionsAction | undefined; + private installAllAction: Action | undefined; renderBody(container: HTMLElement): void { super.renderBody(container); - this._register(this.tipsService.onRecommendationChange(() => this.update())); - this._register(this.extensionsWorkbenchService.onChange(() => this.setRecommendationsToInstall())); - this._register(this.contextService.onDidChangeWorkbenchState(() => this.update())); + this._register(this.extensionRecommendationsService.onRecommendationChange(() => this.show(this.recommendedExtensionsQuery))); + this._register(this.contextService.onDidChangeWorkbenchState(() => this.show(this.recommendedExtensionsQuery))); } getActions(): IAction[] { if (!this.installAllAction) { - this.installAllAction = this._register(this.instantiationService.createInstance(InstallWorkspaceRecommendedExtensionsAction, [])); - this.installAllAction.class = 'codicon codicon-cloud-download'; + this.installAllAction = this._register(new Action('workbench.extensions.action.installWorkspaceRecommendedExtensions', localize('installWorkspaceRecommendedExtensions', "Install Workspace Recommended Extensions"), 'codicon codicon-cloud-download', false, () => this.installWorkspaceRecommendations())); } const configureWorkspaceFolderAction = this._register(this.instantiationService.createInstance(ConfigureWorkspaceFolderRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction.ID, ConfigureWorkspaceFolderRecommendedExtensionsAction.LABEL)); @@ -1139,33 +1098,28 @@ export class WorkspaceRecommendedExtensionsView extends ExtensionsListView { let shouldShowEmptyView = query && query.trim() !== '@recommended' && query.trim() !== '@recommended:workspace'; let model = await (shouldShowEmptyView ? this.showEmptyModel() : super.show(this.recommendedExtensionsQuery)); this.setExpanded(model.length > 0); + await this.setRecommendationsToInstall(); return model; } - private update(): void { - this.show(this.recommendedExtensionsQuery); - this.setRecommendationsToInstall(); - } - private async setRecommendationsToInstall(): Promise { - const recommendations = await this.getRecommendationsToInstall(); + const installableRecommendations = await this.getInstallableWorkspaceRecommendations(); if (this.installAllAction) { - this.installAllAction.recommendations = recommendations.map(({ extensionId }) => extensionId); + this.installAllAction.enabled = installableRecommendations.length > 0; } } - private getRecommendationsToInstall(): Promise { - return this.tipsService.getWorkspaceRecommendations() - .then(recommendations => recommendations.filter(({ extensionId }) => { - const extension = this.extensionsWorkbenchService.local.filter(i => areSameExtensions({ id: extensionId }, i.identifier))[0]; - if (!extension - || !extension.local - || extension.state !== ExtensionState.Installed - || extension.enablementState === EnablementState.DisabledByExtensionKind - ) { - return true; - } - return false; - })); + private async getInstallableWorkspaceRecommendations() { + const installed = (await this.extensionsWorkbenchService.queryLocal()) + .filter(l => l.enablementState !== EnablementState.DisabledByExtensionKind); // Filter extensions disabled by kind + const recommendations = (await this.getWorkspaceRecommendations()) + .filter(({ extensionId }) => installed.every(local => !areSameExtensions({ id: extensionId }, local.identifier))); + return this.getInstallableRecommendations(recommendations, { source: 'install-all-workspace-recommendations' }, CancellationToken.None); } + + private async installWorkspaceRecommendations(): Promise { + const installableRecommendations = await this.getInstallableWorkspaceRecommendations(); + await Promise.all(installableRecommendations.map(extension => this.extensionManagementService.installFromGallery(extension.gallery!))); + } + } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 8fa285997a..88f4a6f175 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -235,7 +235,11 @@ class Extension implements IExtension { return Promise.resolve(null); } - return Promise.resolve(this.local!.manifest); + if (this.local) { + return Promise.resolve(this.local.manifest); + } + + return Promise.resolve(null); } hasReadme(): boolean { @@ -694,9 +698,18 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension } const extensionsToChoose = enabledExtensions.length ? enabledExtensions : extensions; + const manifest = extensionsToChoose.find(e => e.local && e.local.manifest)?.local?.manifest; + + // Manifest is not found which should not happen. + // In which case return the first extension. + if (!manifest) { + return extensionsToChoose[0]; + } + + const extensionKinds = getExtensionKind(manifest, this.productService, this.configurationService); let extension = extensionsToChoose.find(extension => { - for (const extensionKind of getExtensionKind(extension.local!.manifest, this.productService, this.configurationService)) { + for (const extensionKind of extensionKinds) { switch (extensionKind) { case 'ui': /* UI extension is chosen only if it is installed locally */ @@ -723,7 +736,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (!extension && this.extensionManagementServerService.localExtensionManagementServer) { extension = extensionsToChoose.find(extension => { - for (const extensionKind of getExtensionKind(extension.local!.manifest, this.productService, this.configurationService)) { + for (const extensionKind of extensionKinds) { switch (extensionKind) { case 'workspace': /* Choose local workspace extension if exists */ @@ -745,7 +758,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (!extension && this.extensionManagementServerService.remoteExtensionManagementServer) { extension = extensionsToChoose.find(extension => { - for (const extensionKind of getExtensionKind(extension.local!.manifest, this.productService, this.configurationService)) { + for (const extensionKind of extensionKinds) { switch (extensionKind) { case 'web': /* Choose remote web extension if exists */ diff --git a/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts index f7733d27f0..fb8902f12c 100644 --- a/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts @@ -4,28 +4,26 @@ *--------------------------------------------------------------------------------------------*/ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ExtensionRecommendations, ExtensionRecommendation, PromptedExtensionRecommendations } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { ExtensionRecommendationSource, ExtensionRecommendationReason, EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IExtensionsViewPaneContainer, IExtensionsWorkbenchService, IExtension } from 'vs/workbench/contrib/extensions/common/extensions'; import { CancellationToken } from 'vs/base/common/cancellation'; import { localize } from 'vs/nls'; import { StorageScope, IStorageService } from 'vs/platform/storage/common/storage'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IProductService } from 'vs/platform/product/common/productService'; +import { ImportantExtensionTip, IProductService } from 'vs/platform/product/common/productService'; import { forEach, IStringDictionary } from 'vs/base/common/collections'; import { ITextModel } from 'vs/editor/common/model'; import { Schemas } from 'vs/base/common/network'; -import { extname } from 'vs/base/common/resources'; +import { basename, extname } from 'vs/base/common/resources'; import { match } from 'vs/base/common/glob'; import { URI } from 'vs/base/common/uri'; import { MIME_UNKNOWN, guessMimeTypes } from 'vs/base/common/mime'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IModelService } from 'vs/editor/common/services/modelService'; -import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { setImmediate } from 'vs/base/common/platform'; +import { IModeService } from 'vs/editor/common/services/modeService'; type FileExtensionSuggestionClassification = { userReaction: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; @@ -35,32 +33,34 @@ type FileExtensionSuggestionClassification = { const recommendationsStorageKey = 'extensionsAssistant/recommendations'; const searchMarketplace = localize('searchMarketplace', "Search Marketplace"); const milliSecondsInADay = 1000 * 60 * 60 * 24; -const processedFileExtensions: string[] = []; export class FileBasedRecommendations extends ExtensionRecommendations { - private readonly extensionTips: IStringDictionary = Object.create(null); - private readonly importantExtensionTips: IStringDictionary<{ name: string; pattern: string; isExtensionPack?: boolean }> = Object.create(null); + private readonly extensionTips = new Map(); + private readonly importantExtensionTips = new Map(); - private fileBasedRecommendationsByPattern: IStringDictionary = Object.create(null); - private fileBasedRecommendations: IStringDictionary<{ recommendedTime: number, sources: ExtensionRecommendationSource[] }> = Object.create(null); + private readonly fileBasedRecommendationsByPattern = new Map(); + private readonly fileBasedRecommendationsByLanguage = new Map(); + private readonly fileBasedRecommendations = new Map(); + private readonly processedFileExtensions: string[] = []; + private readonly processedLanguages: string[] = []; get recommendations(): ReadonlyArray { const recommendations: ExtensionRecommendation[] = []; - Object.keys(this.fileBasedRecommendations) + [...this.fileBasedRecommendations.keys()] .sort((a, b) => { - if (this.fileBasedRecommendations[a].recommendedTime === this.fileBasedRecommendations[b].recommendedTime) { - if (this.importantExtensionTips[a]) { + if (this.fileBasedRecommendations.get(a)!.recommendedTime === this.fileBasedRecommendations.get(b)!.recommendedTime) { + if (this.importantExtensionTips.has(a)) { return -1; } - if (this.importantExtensionTips[b]) { + if (this.importantExtensionTips.has(b)) { return 1; } } - return this.fileBasedRecommendations[a].recommendedTime > this.fileBasedRecommendations[b].recommendedTime ? -1 : 1; + return this.fileBasedRecommendations.get(a)!.recommendedTime > this.fileBasedRecommendations.get(b)!.recommendedTime ? -1 : 1; }) .forEach(extensionId => { - for (const source of this.fileBasedRecommendations[extensionId].sources) { + for (const source of this.fileBasedRecommendations.get(extensionId)!.sources) { recommendations.push({ extensionId, source, @@ -75,53 +75,62 @@ export class FileBasedRecommendations extends ExtensionRecommendations { } get importantRecommendations(): ReadonlyArray { - return this.recommendations.filter(e => this.importantExtensionTips[e.extensionId]); + return this.recommendations.filter(e => this.importantExtensionTips.has(e.extensionId)); } get otherRecommendations(): ReadonlyArray { - return this.recommendations.filter(e => !this.importantExtensionTips[e.extensionId]); + return this.recommendations.filter(e => !this.importantExtensionTips.has(e.extensionId)); } constructor( - isExtensionAllowedToBeRecommended: (extensionId: string) => boolean, + promptedExtensionRecommendations: PromptedExtensionRecommendations, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IExtensionService private readonly extensionService: IExtensionService, @IViewletService private readonly viewletService: IViewletService, @IModelService private readonly modelService: IModelService, + @IModeService private readonly modeService: IModeService, @IProductService productService: IProductService, - @IInstantiationService instantiationService: IInstantiationService, - @IConfigurationService configurationService: IConfigurationService, - @INotificationService notificationService: INotificationService, - @ITelemetryService telemetryService: ITelemetryService, - @IStorageService storageService: IStorageService, - @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, + @INotificationService private readonly notificationService: INotificationService, + @ITelemetryService private readonly telemetryService: ITelemetryService, + @IStorageService private readonly storageService: IStorageService, ) { - super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService); + super(promptedExtensionRecommendations); if (productService.extensionTips) { - forEach(productService.extensionTips, ({ key, value }) => this.extensionTips[key.toLowerCase()] = value); + forEach(productService.extensionTips, ({ key, value }) => this.extensionTips.set(key.toLowerCase(), value)); } if (productService.extensionImportantTips) { - forEach(productService.extensionImportantTips, ({ key, value }) => this.importantExtensionTips[key.toLowerCase()] = value); + forEach(productService.extensionImportantTips, ({ key, value }) => this.importantExtensionTips.set(key.toLowerCase(), value)); } } protected async doActivate(): Promise { + await this.extensionService.whenInstalledExtensionsRegistered(); + const allRecommendations: string[] = []; // group extension recommendations by pattern, like {**/*.md} -> [ext.foo1, ext.bar2] - forEach(this.extensionTips, ({ key: extensionId, value: pattern }) => { - const ids = this.fileBasedRecommendationsByPattern[pattern] || []; + for (const [extensionId, pattern] of this.extensionTips) { + const ids = this.fileBasedRecommendationsByPattern.get(pattern) || []; ids.push(extensionId); - this.fileBasedRecommendationsByPattern[pattern] = ids; + this.fileBasedRecommendationsByPattern.set(pattern, ids); allRecommendations.push(extensionId); - }); - forEach(this.importantExtensionTips, ({ key: extensionId, value }) => { - const ids = this.fileBasedRecommendationsByPattern[value.pattern] || []; - ids.push(extensionId); - this.fileBasedRecommendationsByPattern[value.pattern] = ids; + } + for (const [extensionId, value] of this.importantExtensionTips) { + if (value.pattern) { + const ids = this.fileBasedRecommendationsByPattern.get(value.pattern) || []; + ids.push(extensionId); + this.fileBasedRecommendationsByPattern.set(value.pattern, ids); + } + if (value.languages) { + for (const language of value.languages) { + const ids = this.fileBasedRecommendationsByLanguage.get(language) || []; + ids.push(extensionId); + this.fileBasedRecommendationsByLanguage.set(language, ids); + } + } allRecommendations.push(extensionId); - }); + } const cachedRecommendations = this.getCachedRecommendations(); const now = Date.now(); @@ -129,12 +138,17 @@ export class FileBasedRecommendations extends ExtensionRecommendations { forEach(cachedRecommendations, ({ key, value }) => { const diff = (now - value) / milliSecondsInADay; if (diff <= 7 && allRecommendations.indexOf(key) > -1) { - this.fileBasedRecommendations[key] = { recommendedTime: value, sources: ['cached'] }; + this.fileBasedRecommendations.set(key.toLowerCase(), { recommendedTime: value, sources: ['cached'] }); } }); - this._register(this.modelService.onModelAdded(this.promptRecommendationsForModel, this)); - this.modelService.getModels().forEach(model => this.promptRecommendationsForModel(model)); + this._register(this.modelService.onModelAdded(model => this.onModelAdded(model))); + this.modelService.getModels().forEach(model => this.onModelAdded(model)); + } + + private onModelAdded(model: ITextModel): void { + this.promptRecommendationsForModel(model); + this._register(model.onDidChangeLanguage(() => this.promptRecommendationsForModel(model))); } /** @@ -144,63 +158,72 @@ export class FileBasedRecommendations extends ExtensionRecommendations { private promptRecommendationsForModel(model: ITextModel): void { const uri = model.uri; const supportedSchemes = [Schemas.untitled, Schemas.file, Schemas.vscodeRemote]; - if (!uri || supportedSchemes.indexOf(uri.scheme) === -1) { + if (!uri || !supportedSchemes.includes(uri.scheme)) { return; } - let fileExtension = extname(uri); - if (fileExtension) { - if (processedFileExtensions.indexOf(fileExtension) > -1) { - return; - } - processedFileExtensions.push(fileExtension); + const language = model.getLanguageIdentifier().language; + const fileExtension = extname(uri); + if (this.processedLanguages.includes(language) && this.processedFileExtensions.includes(fileExtension)) { + return; } + this.processedLanguages.push(language); + this.processedFileExtensions.push(fileExtension); + // re-schedule this bit of the operation to be off the critical path - in case glob-match is slow - setImmediate(() => this.promptRecommendations(uri, fileExtension)); + setImmediate(() => this.promptRecommendations(uri, language, fileExtension)); } - private async promptRecommendations(uri: URI, fileExtension: string): Promise { - const recommendationsToPrompt: string[] = []; - forEach(this.fileBasedRecommendationsByPattern, ({ key: pattern, value: extensionIds }) => { - if (match(pattern, uri.toString())) { - for (const extensionId of extensionIds) { - // Add to recommendation to prompt if it is an important tip - // Only prompt if the pattern matches the extensionImportantTips pattern - // Otherwise, assume pattern is from extensionTips, which means it should be a file based "passive" recommendation - if (this.importantExtensionTips[extensionId]?.pattern === pattern) { - recommendationsToPrompt.push(extensionId); - } - // Update file based recommendations - const filedBasedRecommendation = this.fileBasedRecommendations[extensionId] || { recommendedTime: Date.now(), sources: [] }; - filedBasedRecommendation.recommendedTime = Date.now(); - if (!filedBasedRecommendation.sources.some(s => s instanceof URI && s.toString() === uri.toString())) { - filedBasedRecommendation.sources.push(uri); - } - this.fileBasedRecommendations[extensionId.toLowerCase()] = filedBasedRecommendation; + private async promptRecommendations(uri: URI, language: string, fileExtension: string): Promise { + const importantRecommendations: string[] = (this.fileBasedRecommendationsByLanguage.get(language) || []).filter(extensionId => this.importantExtensionTips.has(extensionId)); + let languageName: string | null = importantRecommendations.length ? this.modeService.getLanguageName(language) : null; + + const fileBasedRecommendations: string[] = [...importantRecommendations]; + for (let [pattern, extensionIds] of this.fileBasedRecommendationsByPattern) { + extensionIds = extensionIds.filter(extensionId => !importantRecommendations.includes(extensionId)); + if (!extensionIds.length) { + continue; + } + if (!match(pattern, uri.toString())) { + continue; + } + for (const extensionId of extensionIds) { + fileBasedRecommendations.push(extensionId); + const importantExtensionTip = this.importantExtensionTips.get(extensionId); + if (importantExtensionTip && importantExtensionTip.pattern === pattern) { + importantRecommendations.push(extensionId); } } - }); + } + + // Update file based recommendations + for (const recommendation of fileBasedRecommendations) { + const filedBasedRecommendation = this.fileBasedRecommendations.get(recommendation) || { recommendedTime: Date.now(), sources: [] }; + filedBasedRecommendation.recommendedTime = Date.now(); + if (!filedBasedRecommendation.sources.some(s => s instanceof URI && s.toString() === uri.toString())) { + filedBasedRecommendation.sources.push(uri); + } + this.fileBasedRecommendations.set(recommendation, filedBasedRecommendation); + } this.storeCachedRecommendations(); - if (this.hasToIgnoreRecommendationNotifications()) { + if (this.promptedExtensionRecommendations.hasToIgnoreRecommendationNotifications()) { return; } const installed = await this.extensionsWorkbenchService.queryLocal(); - if (await this.promptRecommendedExtensionForFileType(recommendationsToPrompt, installed)) { + if (importantRecommendations.length && + await this.promptRecommendedExtensionForFileType(languageName || basename(uri), importantRecommendations, installed)) { return; } - if (fileExtension) { - fileExtension = fileExtension.substr(1); // Strip the dot - } + fileExtension = fileExtension.substr(1); // Strip the dot if (!fileExtension) { return; } - await this.extensionService.whenInstalledExtensionsRegistered(); const mimeTypes = guessMimeTypes(uri); if (mimeTypes.length !== 1 || mimeTypes[0] !== MIME_UNKNOWN) { return; @@ -209,9 +232,9 @@ export class FileBasedRecommendations extends ExtensionRecommendations { this.promptRecommendedExtensionForFileExtension(fileExtension, installed); } - private async promptRecommendedExtensionForFileType(recommendations: string[], installed: IExtension[]): Promise { + private async promptRecommendedExtensionForFileType(name: string, recommendations: string[], installed: IExtension[]): Promise { - recommendations = this.filterIgnoredOrNotAllowed(recommendations); + recommendations = this.promptedExtensionRecommendations.filterIgnoredOrNotAllowed(recommendations); if (recommendations.length === 0) { return false; } @@ -222,17 +245,12 @@ export class FileBasedRecommendations extends ExtensionRecommendations { } const extensionId = recommendations[0]; - const entry = this.importantExtensionTips[extensionId]; + const entry = this.importantExtensionTips.get(extensionId); if (!entry) { return false; } - const extensionName = entry.name; - let message = localize('reallyRecommended2', "The '{0}' extension is recommended for this file type.", extensionName); - if (entry.isExtensionPack) { - message = localize('reallyRecommendedExtensionPack', "The '{0}' extension pack is recommended for this file type.", extensionName); - } - this.promptImportantExtensionsInstallNotification([extensionId], message); + this.promptedExtensionRecommendations.promptImportantExtensionsInstallNotification([extensionId], localize('reallyRecommended', "Do you want to install the recommended extensions for {0}?", name), `@id:${extensionId}`); return true; } @@ -310,7 +328,7 @@ export class FileBasedRecommendations extends ExtensionRecommendations { private getCachedRecommendations(): IStringDictionary { let storedRecommendations = JSON.parse(this.storageService.get(recommendationsStorageKey, StorageScope.GLOBAL, '[]')); - if (Array.isArray(storedRecommendations)) { + if (Array.isArray(storedRecommendations)) { storedRecommendations = storedRecommendations.reduce((result, id) => { result[id] = Date.now(); return result; }, >{}); } const result: IStringDictionary = {}; @@ -324,7 +342,7 @@ export class FileBasedRecommendations extends ExtensionRecommendations { private storeCachedRecommendations(): void { const storedRecommendations: IStringDictionary = {}; - forEach(this.fileBasedRecommendations, ({ key, value }) => storedRecommendations[key] = value.recommendedTime); + this.fileBasedRecommendations.forEach((value, key) => storedRecommendations[key] = value.recommendedTime); this.storageService.store(recommendationsStorageKey, JSON.stringify(storedRecommendations), StorageScope.GLOBAL); } } diff --git a/src/vs/workbench/contrib/extensions/browser/keymapRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/keymapRecommendations.ts index bd88263085..860c901019 100644 --- a/src/vs/workbench/contrib/extensions/browser/keymapRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/keymapRecommendations.ts @@ -3,15 +3,9 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ExtensionRecommendations, ExtensionRecommendation, PromptedExtensionRecommendations } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; import { IProductService } from 'vs/platform/product/common/productService'; import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { IStorageService } from 'vs/platform/storage/common/storage'; -import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; export class KeymapRecommendations extends ExtensionRecommendations { @@ -19,16 +13,10 @@ export class KeymapRecommendations extends ExtensionRecommendations { get recommendations(): ReadonlyArray { return this._recommendations; } constructor( - isExtensionAllowedToBeRecommended: (extensionId: string) => boolean, + promptedExtensionRecommendations: PromptedExtensionRecommendations, @IProductService private readonly productService: IProductService, - @IInstantiationService instantiationService: IInstantiationService, - @IConfigurationService configurationService: IConfigurationService, - @INotificationService notificationService: INotificationService, - @ITelemetryService telemetryService: ITelemetryService, - @IStorageService storageService: IStorageService, - @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, ) { - super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService); + super(promptedExtensionRecommendations); } protected async doActivate(): Promise { diff --git a/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts index 3ad8024b97..5fc12e7ebe 100644 --- a/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts @@ -3,63 +3,45 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EXTENSION_IDENTIFIER_PATTERN, IExtensionGalleryService, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { EXTENSION_IDENTIFIER_PATTERN, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkspaceContextService, IWorkspaceFolder, IWorkspace, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; import { IFileService } from 'vs/platform/files/common/files'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { distinct, flatten, coalesce } from 'vs/base/common/arrays'; -import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { IExtensionsConfigContent, ExtensionRecommendationSource, ExtensionRecommendationReason, IWorkbenchExtensionEnablementService, EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { ExtensionRecommendations, ExtensionRecommendation, PromptedExtensionRecommendations } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IExtensionsConfigContent, ExtensionRecommendationSource, ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { parse } from 'vs/base/common/json'; import { EXTENSIONS_CONFIG } from 'vs/workbench/contrib/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; import { CancellationToken } from 'vs/base/common/cancellation'; import { localize } from 'vs/nls'; -import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { ShowRecommendedExtensionsAction, InstallWorkspaceRecommendedExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; -import { StorageScope, IStorageService } from 'vs/platform/storage/common/storage'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; - -type ExtensionWorkspaceRecommendationsNotificationClassification = { - userReaction: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; -}; - -const choiceNever = localize('neverShowAgain', "Don't Show Again"); -const ignoreWorkspaceRecommendationsStorageKey = 'extensionsAssistant/workspaceRecommendationsIgnore'; +import { Emitter } from 'vs/base/common/event'; export class WorkspaceRecommendations extends ExtensionRecommendations { private _recommendations: ExtensionRecommendation[] = []; get recommendations(): ReadonlyArray { return this._recommendations; } + private _onDidChangeRecommendations = this._register(new Emitter()); + readonly onDidChangeRecommendations = this._onDidChangeRecommendations.event; + private _ignoredRecommendations: string[] = []; get ignoredRecommendations(): ReadonlyArray { return this._ignoredRecommendations; } constructor( - isExtensionAllowedToBeRecommended: (extensionId: string) => boolean, + promptedExtensionRecommendations: PromptedExtensionRecommendations, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @ILogService private readonly logService: ILogService, @IFileService private readonly fileService: IFileService, - @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, - @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, - @IInstantiationService instantiationService: IInstantiationService, - @IConfigurationService configurationService: IConfigurationService, - @INotificationService notificationService: INotificationService, - @ITelemetryService telemetryService: ITelemetryService, - @IStorageService storageService: IStorageService, - @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, + @INotificationService private readonly notificationService: INotificationService, ) { - super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService); + super(promptedExtensionRecommendations); } protected async doActivate(): Promise { await this.fetch(); this._register(this.contextService.onDidChangeWorkspaceFolders(e => this.onWorkspaceFoldersChanged(e))); - this.promptWorkspaceRecommendations(); } /** @@ -71,7 +53,7 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { const { invalidRecommendations, message } = await this.validateExtensions(extensionsConfigBySource.map(({ contents }) => contents)); if (invalidRecommendations.length) { - this.notificationService.warn(`The below ${invalidRecommendations.length} extension(s) in workspace recommendations have issues:\n${message}`); + this.notificationService.warn(`The ${invalidRecommendations.length} extension(s) below, in workspace recommendations have issues:\n${message}`); } this._ignoredRecommendations = []; @@ -97,63 +79,6 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { } } - private async promptWorkspaceRecommendations(): Promise { - const allowedRecommendations = this.recommendations.filter(rec => this.isExtensionAllowedToBeRecommended(rec.extensionId)); - - if (allowedRecommendations.length === 0 || this.hasToIgnoreWorkspaceRecommendationNotifications()) { - return; - } - - let installed = await this.extensionManagementService.getInstalled(); - installed = installed.filter(l => this.extensionEnablementService.getEnablementState(l) !== EnablementState.DisabledByExtensionKind); // Filter extensions disabled by kind - const recommendations = allowedRecommendations.filter(({ extensionId }) => installed.every(local => !areSameExtensions({ id: extensionId }, local.identifier))); - - if (!recommendations.length) { - return; - } - - return new Promise(c => { - this.notificationService.prompt( - Severity.Info, - localize('workspaceRecommended', "This workspace has extension recommendations."), - [{ - label: localize('installAll', "Install All"), - run: () => { - this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'install' }); - const installAllAction = this.instantiationService.createInstance(InstallWorkspaceRecommendedExtensionsAction, recommendations.map(({ extensionId }) => extensionId)); - installAllAction.run(); - installAllAction.dispose(); - c(undefined); - } - }, { - label: localize('showRecommendations', "Show Recommendations"), - run: () => { - this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'show' }); - const showAction = this.instantiationService.createInstance(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, localize('showRecommendations', "Show Recommendations")); - showAction.run(); - showAction.dispose(); - c(undefined); - } - }, { - label: choiceNever, - isSecondary: true, - run: () => { - this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'neverShowAgain' }); - this.storageService.store(ignoreWorkspaceRecommendationsStorageKey, true, StorageScope.WORKSPACE); - c(undefined); - } - }], - { - sticky: true, - onCancel: () => { - this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'cancelled' }); - c(undefined); - } - } - ); - }); - } - private async fetchExtensionsConfigBySource(): Promise<{ contents: IExtensionsConfigContent, source: ExtensionRecommendationSource }[]> { const workspace = this.contextService.getWorkspace(); const result = await Promise.all([ @@ -235,7 +160,7 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { await this.fetch(); // Suggest only if at least one of the newly added recommendations was not suggested before if (this._recommendations.some(current => oldWorkspaceRecommended.every(old => current.extensionId !== old.extensionId))) { - this.promptWorkspaceRecommendations(); + this._onDidChangeRecommendations.fire(); } } } @@ -250,8 +175,5 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { return null; } - private hasToIgnoreWorkspaceRecommendationNotifications(): boolean { - return this.hasToIgnoreRecommendationNotifications() || this.storageService.getBoolean(ignoreWorkspaceRecommendationsStorageKey, StorageScope.WORKSPACE, false); - } } diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts index cf551b2b8b..9a1d2f5a4e 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts @@ -24,8 +24,11 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; import { OpenExtensionsFolderAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsActions'; import { ExtensionsLabel } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService'; +import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; // Singletons +registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); // TODO@sandbox TODO@ben move back into common/extensions.contribution.ts when 'semver-umd' can be loaded registerSingleton(IExtensionHostProfileService, ExtensionHostProfileService, true); const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); diff --git a/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts index f660bfd4e8..4ad6495396 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts @@ -8,7 +8,7 @@ import * as nls from 'vs/nls'; import * as os from 'os'; import { IProductService } from 'vs/platform/product/common/productService'; import { Action, IAction, Separator } from 'vs/base/common/actions'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionsWorkbenchService, IExtension } from 'vs/workbench/contrib/extensions/common/extensions'; @@ -17,7 +17,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IExtensionService, IExtensionsStatus, IExtensionHostProfile } from 'vs/workbench/services/extensions/common/extensions'; import { IListVirtualDelegate, IListRenderer } from 'vs/base/browser/ui/list/list'; import { WorkbenchList } from 'vs/platform/list/browser/listService'; -import { append, $, addClass, toggleClass, Dimension, clearNode } from 'vs/base/browser/dom'; +import { append, $, reset, addClass, toggleClass, Dimension, clearNode } from 'vs/base/browser/dom'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { RunOnceScheduler } from 'vs/base/common/async'; @@ -38,8 +38,7 @@ import { randomPort } from 'vs/base/node/ports'; import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ILabelService } from 'vs/platform/label/common/label'; -import { renderCodicons } from 'vs/base/common/codicons'; -import { escape } from 'vs/base/common/strings'; +import { renderCodiconsAsElement } from 'vs/base/browser/codicons'; import { ExtensionIdentifier, ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; import { SlowExtensionAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsSlowActions'; @@ -100,7 +99,7 @@ interface IRuntimeExtension { unresponsiveProfile?: IExtensionHostProfile; } -export class RuntimeExtensionsEditor extends BaseEditor { +export class RuntimeExtensionsEditor extends EditorPane { public static readonly ID: string = 'workbench.editor.runtimeExtensions'; @@ -233,11 +232,24 @@ export class RuntimeExtensionsEditor extends BaseEditor { result = result.filter(element => element.status.activationTimes); // bubble up extensions that have caused slowness + + const isUnresponsive = (extension: IRuntimeExtension): boolean => + extension.unresponsiveProfile === this._profileInfo; + + const profileTime = (extension: IRuntimeExtension): number => + extension.profileInfo?.totalTime ?? 0; + + const activationTime = (extension: IRuntimeExtension): number => + (extension.status.activationTimes?.codeLoadingTime ?? 0) + + (extension.status.activationTimes?.activateCallTime ?? 0); + result = result.sort((a, b) => { - if (a.unresponsiveProfile === this._profileInfo && !b.unresponsiveProfile) { - return -1; - } else if (!a.unresponsiveProfile && b.unresponsiveProfile === this._profileInfo) { - return 1; + if (isUnresponsive(a) || isUnresponsive(b)) { + return +isUnresponsive(b) - +isUnresponsive(a); + } else if (profileTime(a) || profileTime(b)) { + return profileTime(b) - profileTime(a); + } else if (activationTime(a) || activationTime(b)) { + return activationTime(b) - activationTime(a); } return a.originalIndex - b.originalIndex; }); @@ -397,32 +409,28 @@ export class RuntimeExtensionsEditor extends BaseEditor { clearNode(data.msgContainer); if (this._extensionHostProfileService.getUnresponsiveProfile(element.description.identifier)) { - const el = $('span'); - el.innerHTML = renderCodicons(escape(` $(alert) Unresponsive`)); + const el = $('span', undefined, ...renderCodiconsAsElement(` $(alert) Unresponsive`)); el.title = nls.localize('unresponsive.title', "Extension has caused the extension host to freeze."); data.msgContainer.appendChild(el); } if (isNonEmptyArray(element.status.runtimeErrors)) { - const el = $('span'); - el.innerHTML = renderCodicons(escape(`$(bug) ${nls.localize('errors', "{0} uncaught errors", element.status.runtimeErrors.length)}`)); + const el = $('span', undefined, ...renderCodiconsAsElement(`$(bug) ${nls.localize('errors', "{0} uncaught errors", element.status.runtimeErrors.length)}`)); data.msgContainer.appendChild(el); } if (element.status.messages && element.status.messages.length > 0) { - const el = $('span'); - el.innerHTML = renderCodicons(escape(`$(alert) ${element.status.messages[0].message}`)); + const el = $('span', undefined, ...renderCodiconsAsElement(`$(alert) ${element.status.messages[0].message}`)); data.msgContainer.appendChild(el); } if (element.description.extensionLocation.scheme !== 'file') { - const el = $('span'); - el.innerHTML = renderCodicons(escape(`$(remote) ${element.description.extensionLocation.authority}`)); + const el = $('span', undefined, ...renderCodiconsAsElement(`$(remote) ${element.description.extensionLocation.authority}`)); data.msgContainer.appendChild(el); const hostLabel = this._labelService.getHostLabel(REMOTE_HOST_SCHEME, this._environmentService.configuration.remoteAuthority); if (hostLabel) { - el.innerHTML = renderCodicons(escape(`$(remote) ${hostLabel}`)); + reset(el, ...renderCodiconsAsElement(`$(remote) ${hostLabel}`)); } } diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts index d4521f11e5..d4981b317b 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts @@ -33,8 +33,7 @@ import { IPager } from 'vs/base/common/paging'; import { assign } from 'vs/base/common/objects'; import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { ConfigurationKey } from 'vs/workbench/contrib/extensions/common/extensions'; -import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; +import { ConfigurationKey, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; import { TestExtensionEnablementService } from 'vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test'; import { IURLService } from 'vs/platform/url/common/url'; import { ITextModel } from 'vs/editor/common/model'; @@ -58,6 +57,8 @@ import { ExtensionRecommendationsService } from 'vs/workbench/contrib/extensions import { NoOpWorkspaceTagsService } from 'vs/workbench/contrib/tags/browser/workspaceTagsService'; import { IWorkspaceTagsService } from 'vs/workbench/contrib/tags/common/workspaceTags'; import { IStorageKeysSyncRegistryService, StorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; +import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; const mockExtensionGallery: IGalleryExtension[] = [ aGalleryExtension('MockExtension1', { @@ -199,11 +200,18 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT} testConfigurationService = new TestConfigurationService(); instantiationService.stub(IConfigurationService, testConfigurationService); instantiationService.stub(INotificationService, new TestNotificationService()); - instantiationService.stub(IExtensionManagementService, ExtensionManagementService); - instantiationService.stub(IExtensionManagementService, 'onInstallExtension', installEvent.event); - instantiationService.stub(IExtensionManagementService, 'onDidInstallExtension', didInstallEvent.event); - instantiationService.stub(IExtensionManagementService, 'onUninstallExtension', uninstallEvent.event); - instantiationService.stub(IExtensionManagementService, 'onDidUninstallExtension', didUninstallEvent.event); + instantiationService.stub(IExtensionManagementService, >{ + onInstallExtension: installEvent.event, + onDidInstallExtension: didInstallEvent.event, + onUninstallExtension: uninstallEvent.event, + onDidUninstallExtension: didUninstallEvent.event, + async getInstalled() { return []; }, + async canInstall() { return true; }, + async getExtensionsReport() { return []; }, + }); + instantiationService.stub(IExtensionService, >{ + async whenInstalledExtensionsRegistered() { return true; } + }); instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); instantiationService.stub(ITelemetryService, NullTelemetryService); instantiationService.stub(IURLService, NativeURLService); @@ -231,6 +239,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT} experimentService = instantiationService.createInstance(TestExperimentService); instantiationService.stub(IExperimentService, experimentService); + instantiationService.set(IExtensionsWorkbenchService, instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.stub(IExtensionTipsService, instantiationService.createInstance(ExtensionTipsService)); onModelAddedEvent = new Emitter(); @@ -302,7 +311,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT} function testNoPromptForValidRecommendations(recommendations: string[]) { return setUpFolderWorkspace('myFolder', recommendations).then(() => { testObject = instantiationService.createInstance(ExtensionRecommendationsService); - return testObject.loadWorkspaceConfigPromise.then(() => { + return testObject.activationPromise.then(() => { assert.equal(Object.keys(testObject.getAllRecommendationsWithReason()).length, recommendations.length); assert.ok(!prompted); }); @@ -338,20 +347,18 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT} return testNoPromptForValidRecommendations([]); }); - test('ExtensionRecommendationsService: Prompt for valid workspace recommendations', () => { - return setUpFolderWorkspace('myFolder', mockTestData.recommendedExtensions).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); - return testObject.loadWorkspaceConfigPromise.then(() => { - const recommendations = Object.keys(testObject.getAllRecommendationsWithReason()); + test('ExtensionRecommendationsService: Prompt for valid workspace recommendations', async () => { + await setUpFolderWorkspace('myFolder', mockTestData.recommendedExtensions); + testObject = instantiationService.createInstance(ExtensionRecommendationsService); + await testObject.activationPromise; - assert.equal(recommendations.length, mockTestData.validRecommendedExtensions.length); - mockTestData.validRecommendedExtensions.forEach(x => { - assert.equal(recommendations.indexOf(x.toLowerCase()) > -1, true); - }); - - assert.ok(prompted); - }); + const recommendations = Object.keys(testObject.getAllRecommendationsWithReason()); + assert.equal(recommendations.length, mockTestData.validRecommendedExtensions.length); + mockTestData.validRecommendedExtensions.forEach(x => { + assert.equal(recommendations.indexOf(x.toLowerCase()) > -1, true); }); + + assert.ok(prompted); }); test('ExtensionRecommendationsService: No Prompt for valid workspace recommendations if they are already installed', () => { @@ -373,7 +380,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT} testConfigurationService.setUserConfiguration(ConfigurationKey, { showRecommendationsOnlyOnDemand: true }); return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { testObject = instantiationService.createInstance(ExtensionRecommendationsService); - return testObject.loadWorkspaceConfigPromise.then(() => { + return testObject.activationPromise.then(() => { assert.ok(!prompted); }); }); @@ -391,7 +398,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT} return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { testObject = instantiationService.createInstance(ExtensionRecommendationsService); - return testObject.loadWorkspaceConfigPromise.then(() => { + return testObject.activationPromise.then(() => { const recommendations = testObject.getAllRecommendationsWithReason(); assert.ok(!recommendations['ms-dotnettools.csharp']); // stored recommendation that has been globally ignored assert.ok(recommendations['ms-python.python']); // stored recommendation @@ -409,7 +416,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT} return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions, ignoredRecommendations).then(() => { testObject = instantiationService.createInstance(ExtensionRecommendationsService); - return testObject.loadWorkspaceConfigPromise.then(() => { + return testObject.activationPromise.then(() => { const recommendations = testObject.getAllRecommendationsWithReason(); assert.ok(!recommendations['ms-dotnettools.csharp']); // stored recommendation that has been workspace ignored assert.ok(recommendations['ms-python.python']); // stored recommendation @@ -430,7 +437,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT} return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions, workspaceIgnoredRecommendations).then(() => { testObject = instantiationService.createInstance(ExtensionRecommendationsService); - return testObject.loadWorkspaceConfigPromise.then(() => { + return testObject.activationPromise.then(() => { const recommendations = testObject.getAllRecommendationsWithReason(); assert.ok(recommendations['ms-python.python']); @@ -449,7 +456,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT} return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { testObject = instantiationService.createInstance(ExtensionRecommendationsService); - return testObject.loadWorkspaceConfigPromise.then(() => { + return testObject.activationPromise.then(() => { const recommendations = testObject.getAllRecommendationsWithReason(); assert.ok(recommendations['ms-python.python']); assert.ok(recommendations['mockpublisher1.mockextension1']); @@ -486,7 +493,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT} testObject = instantiationService.createInstance(ExtensionRecommendationsService); testObject.onRecommendationChange(changeHandlerTarget); testObject.toggleIgnoredRecommendation(ignoredExtensionId, true); - await testObject.loadWorkspaceConfigPromise; + await testObject.activationPromise; assert.ok(changeHandlerTarget.calledOnce); assert.ok(changeHandlerTarget.getCall(0).calledWithMatch({ extensionId: ignoredExtensionId.toLowerCase(), isRecommended: false })); @@ -498,7 +505,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT} return setUpFolderWorkspace('myFolder', []).then(() => { testObject = instantiationService.createInstance(ExtensionRecommendationsService); - return testObject.loadWorkspaceConfigPromise.then(() => { + return testObject.activationPromise.then(() => { const recommendations = testObject.getFileBasedRecommendations(); assert.equal(recommendations.length, 2); assert.ok(recommendations.some(({ extensionId }) => extensionId === 'ms-dotnettools.csharp')); // stored recommendation that exists in product.extensionTips @@ -517,7 +524,7 @@ suite.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT} return setUpFolderWorkspace('myFolder', []).then(() => { testObject = instantiationService.createInstance(ExtensionRecommendationsService); - return testObject.loadWorkspaceConfigPromise.then(() => { + return testObject.activationPromise.then(() => { const recommendations = testObject.getFileBasedRecommendations(); assert.equal(recommendations.length, 2); assert.ok(recommendations.some(({ extensionId }) => extensionId === 'ms-dotnettools.csharp')); // stored recommendation that exists in product.extensionTips 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 94efcc49e2..d1f8f71fad 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 @@ -101,7 +101,7 @@ async function setupTest() { instantiationService.stub(IExtensionManagementServerService, new class extends ExtensionManagementServerService { #localExtensionManagementServer: IExtensionManagementServer = { extensionManagementService: instantiationService.get(IExtensionManagementService), label: 'local', id: 'vscode-local' }; constructor() { - super(instantiationService.get(ISharedProcessService), instantiationService.get(IRemoteAgentService), instantiationService.get(IExtensionGalleryService), instantiationService.get(IConfigurationService), instantiationService.get(IProductService), instantiationService.get(ILogService), instantiationService.get(ILabelService)); + super(instantiationService.get(ISharedProcessService), instantiationService.get(IRemoteAgentService), instantiationService.get(ILabelService), instantiationService.get(IExtensionGalleryService), instantiationService.get(IProductService), instantiationService.get(IConfigurationService), instantiationService.get(ILogService)); } get localExtensionManagementServer(): IExtensionManagementServer { return this.#localExtensionManagementServer; } set localExtensionManagementServer(server: IExtensionManagementServer) { } 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 05cbbd1cb7..eac94d01bd 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 @@ -16,7 +16,6 @@ import { } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IExtensionRecommendationsService, ExtensionRecommendationReason, IExtensionRecommendation } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; import { TestExtensionEnablementService } from 'vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test'; import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; import { IURLService } from 'vs/platform/url/common/url'; @@ -40,13 +39,13 @@ import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browse import { ExtensionIdentifier, ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { ExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/electron-browser/extensionManagementServerService'; -import { IProductService } from 'vs/platform/product/common/productService'; 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'; import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; +import { IProductService } from 'vs/platform/product/common/productService'; suite('ExtensionsListView Tests', () => { @@ -68,6 +67,7 @@ suite('ExtensionsListView Tests', () => { const workspaceRecommendationA = aGalleryExtension('workspace-recommendation-A'); const workspaceRecommendationB = aGalleryExtension('workspace-recommendation-B'); const configBasedRecommendationA = aGalleryExtension('configbased-recommendation-A'); + const configBasedRecommendationB = aGalleryExtension('configbased-recommendation-B'); const fileBasedRecommendationA = aGalleryExtension('filebased-recommendation-A'); const fileBasedRecommendationB = aGalleryExtension('filebased-recommendation-B'); const otherRecommendationA = aGalleryExtension('other-recommendation-A'); @@ -89,11 +89,15 @@ suite('ExtensionsListView Tests', () => { instantiationService.stub(ISharedProcessService, TestSharedProcessService); instantiationService.stub(IExperimentService, ExperimentService); - instantiationService.stub(IExtensionManagementService, ExtensionManagementService); - instantiationService.stub(IExtensionManagementService, 'onInstallExtension', installEvent.event); - instantiationService.stub(IExtensionManagementService, 'onDidInstallExtension', didInstallEvent.event); - instantiationService.stub(IExtensionManagementService, 'onUninstallExtension', uninstallEvent.event); - instantiationService.stub(IExtensionManagementService, 'onDidUninstallExtension', didUninstallEvent.event); + instantiationService.stub(IExtensionManagementService, >{ + onInstallExtension: installEvent.event, + onDidInstallExtension: didInstallEvent.event, + onUninstallExtension: uninstallEvent.event, + onDidUninstallExtension: didUninstallEvent.event, + async getInstalled() { return []; }, + async canInstall() { return true; }, + async getExtensionsReport() { return []; }, + }); instantiationService.stub(IRemoteAgentService, RemoteAgentService); instantiationService.stub(IContextKeyService, new MockContextKeyService()); instantiationService.stub(IMenuService, new TestMenuService()); @@ -101,7 +105,7 @@ suite('ExtensionsListView Tests', () => { instantiationService.stub(IExtensionManagementServerService, new class extends ExtensionManagementServerService { #localExtensionManagementServer: IExtensionManagementServer = { extensionManagementService: instantiationService.get(IExtensionManagementService), label: 'local', id: 'vscode-local' }; constructor() { - super(instantiationService.get(ISharedProcessService), instantiationService.get(IRemoteAgentService), instantiationService.get(IExtensionGalleryService), instantiationService.get(IConfigurationService), instantiationService.get(IProductService), instantiationService.get(ILogService), instantiationService.get(ILabelService)); + super(instantiationService.get(ISharedProcessService), instantiationService.get(IRemoteAgentService), instantiationService.get(ILabelService), instantiationService.get(IExtensionGalleryService), instantiationService.get(IProductService), instantiationService.get(IConfigurationService), instantiationService.get(ILogService)); } get localExtensionManagementServer(): IExtensionManagementServer { return this.#localExtensionManagementServer; } set localExtensionManagementServer(server: IExtensionManagementServer) { } @@ -123,9 +127,10 @@ suite('ExtensionsListView Tests', () => { { extensionId: workspaceRecommendationB.identifier.id }]); }, getConfigBasedRecommendations() { - return Promise.resolve([ - { extensionId: configBasedRecommendationA.identifier.id } - ]); + return Promise.resolve({ + important: [{ extensionId: configBasedRecommendationA.identifier.id }], + others: [{ extensionId: configBasedRecommendationB.identifier.id }], + }); }, getImportantRecommendations(): Promise { return Promise.resolve([]); @@ -138,6 +143,7 @@ suite('ExtensionsListView Tests', () => { }, getOtherRecommendations() { return Promise.resolve([ + { extensionId: configBasedRecommendationB.identifier.id }, { extensionId: otherRecommendationA.identifier.id } ]); }, @@ -333,7 +339,8 @@ suite('ExtensionsListView Tests', () => { test('Test @recommended:workspace query', () => { const workspaceRecommendedExtensions = [ workspaceRecommendationA, - workspaceRecommendationB + workspaceRecommendationB, + configBasedRecommendationA, ]; const target = instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(...workspaceRecommendedExtensions)); @@ -351,9 +358,9 @@ suite('ExtensionsListView Tests', () => { test('Test @recommended query', () => { const allRecommendedExtensions = [ - configBasedRecommendationA, fileBasedRecommendationA, fileBasedRecommendationB, + configBasedRecommendationB, otherRecommendationA ]; const target = instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(...allRecommendedExtensions)); @@ -379,7 +386,8 @@ suite('ExtensionsListView Tests', () => { configBasedRecommendationA, fileBasedRecommendationA, fileBasedRecommendationB, - otherRecommendationA + configBasedRecommendationB, + otherRecommendationA, ]; const target = instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage(...allRecommendedExtensions)); diff --git a/src/vs/workbench/contrib/externalTerminal/node/externalTerminalService.ts b/src/vs/workbench/contrib/externalTerminal/node/externalTerminalService.ts index 9d9ecb7a87..973c88d82e 100644 --- a/src/vs/workbench/contrib/externalTerminal/node/externalTerminalService.ts +++ b/src/vs/workbench/contrib/externalTerminal/node/externalTerminalService.ts @@ -18,18 +18,6 @@ import { DEFAULT_TERMINAL_OSX } from 'vs/workbench/contrib/externalTerminal/node const TERMINAL_TITLE = nls.localize('console.title', "VS Code Console"); -type LazyProcess = { - - /** - * The lazy environment is a promise that resolves to `process.env` - * once the process is resolved. The use-case is VS Code running - * on Linux/macOS when being launched via a launcher. Then the env - * (as defined in .bashrc etc) isn't properly set and needs to be - * resolved lazy. - */ - lazyEnv: Promise | undefined; -}; - export class WindowsExternalTerminalService implements IExternalTerminalService { public _serviceBrand: undefined; @@ -318,7 +306,6 @@ export class LinuxExternalTerminalService implements IExternalTerminalService { LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY = new Promise(async r => { if (env.isLinux) { const isDebian = await pfs.exists('/etc/debian_version'); - await (process as unknown as LazyProcess).lazyEnv; if (isDebian) { r('x-terminal-emulator'); } else if (process.env.DESKTOP_SESSION === 'gnome' || process.env.DESKTOP_SESSION === 'gnome-classic') { diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts index 747ace1095..0219f5f304 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts @@ -12,7 +12,7 @@ import { Action } from 'vs/base/common/actions'; import { VIEWLET_ID, TEXT_FILE_EDITOR_ID, IExplorerService } from 'vs/workbench/contrib/files/common/files'; import { ITextFileService, TextFileOperationError, TextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; -import { EditorOptions, TextEditorOptions, IEditorInput } from 'vs/workbench/common/editor'; +import { EditorOptions, TextEditorOptions, IEditorInput, IEditorOpenContext } from 'vs/workbench/common/editor'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; @@ -91,13 +91,13 @@ export class TextFileEditor extends BaseTextEditor { return this._input as FileEditorInput; } - async setInput(input: FileEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + async setInput(input: FileEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { // Update/clear view settings if input changes this.doSaveOrClearTextEditorViewState(this.input); // Set input and resolve - await super.setInput(input, options, token); + await super.setInput(input, options, context, token); try { const resolvedModel = await input.resolve(); @@ -119,10 +119,12 @@ export class TextFileEditor extends BaseTextEditor { const textEditor = assertIsDefined(this.getControl()); textEditor.setModel(textFileModel.textEditorModel); - // Always restore View State if any associated - const editorViewState = this.loadTextEditorViewState(input.resource); - if (editorViewState) { - textEditor.restoreViewState(editorViewState); + // Always restore View State if any associated and not disabled via settings + if (this.shouldRestoreTextEditorViewState(input, context)) { + const editorViewState = this.loadTextEditorViewState(input.resource); + if (editorViewState) { + textEditor.restoreViewState(editorViewState); + } } // TextOptions (avoiding instanceof here for a reason, do not change!) @@ -242,7 +244,7 @@ export class TextFileEditor extends BaseTextEditor { // If the user configured to not restore view state, we clear the view // state unless the editor is still opened in the group. - if (!this.shouldRestoreViewState && (!this.group || !this.group.isOpened(input))) { + if (!this.shouldRestoreTextEditorViewState(input) && (!this.group || !this.group.isOpened(input))) { this.clearTextEditorViewState([input.resource], this.group); } diff --git a/src/vs/workbench/contrib/files/browser/fileCommands.ts b/src/vs/workbench/contrib/files/browser/fileCommands.ts index 5f67f6efbb..a7c402cd9c 100644 --- a/src/vs/workbench/contrib/files/browser/fileCommands.ts +++ b/src/vs/workbench/contrib/files/browser/fileCommands.ts @@ -643,7 +643,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ handler: async (accessor, args?: { viewType: string }) => { const editorService = accessor.get(IEditorService); - if (args && args.viewType) { // {{SQL CARBON EDIT}} explicitly check for viewtype + if (typeof args?.viewType === 'string') { const editorGroupsService = accessor.get(IEditorGroupsService); const configurationService = accessor.get(IConfigurationService); const quickInputService = accessor.get(IQuickInputService); diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 7e5def985b..1540cd4ab7 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -34,7 +34,7 @@ import { fillResourceDataTransfers, CodeDataTransfers, extractResources, contain import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IDragAndDropData, DataTransfers } from 'vs/base/browser/dnd'; import { Schemas } from 'vs/base/common/network'; -import { DesktopDragAndDropData, ExternalElementsDragAndDropData, ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; +import { NativeDragAndDropData, ExternalElementsDragAndDropData, ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; import { isMacintosh, isWeb } from 'vs/base/common/platform'; import { IDialogService, IConfirmation, getFileNamesMessage } from 'vs/platform/dialogs/common/dialogs'; import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; @@ -839,11 +839,11 @@ export class FileDragAndDrop implements ITreeDragAndDrop { private handleDragOver(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction { const isCopy = originalEvent && ((originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh)); - const fromDesktop = data instanceof DesktopDragAndDropData; - const effect = (fromDesktop || isCopy) ? ListDragOverEffect.Copy : ListDragOverEffect.Move; + const isNative = data instanceof NativeDragAndDropData; + const effect = (isNative || isCopy) ? ListDragOverEffect.Copy : ListDragOverEffect.Move; - // Desktop DND - if (fromDesktop) { + // Native DND + if (isNative) { if (!containsDragType(originalEvent, DataTransfers.FILES, CodeDataTransfers.FILES)) { return false; } @@ -979,7 +979,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { } // Desktop DND (Import file) - if (data instanceof DesktopDragAndDropData) { + if (data instanceof NativeDragAndDropData) { if (isWeb) { this.handleWebExternalDrop(data, target, originalEvent).then(undefined, e => this.notificationService.warn(e)); } else { @@ -992,7 +992,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { } } - private async handleWebExternalDrop(data: DesktopDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise { + private async handleWebExternalDrop(data: NativeDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise { const items = (originalEvent.dataTransfer as unknown as IWebkitDataTransfer).items; // Somehow the items thing is being modified at random, maybe as a security @@ -1205,7 +1205,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { }); } - private async handleExternalDrop(data: DesktopDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise { + private async handleExternalDrop(data: NativeDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise { // Check for dropped external files to be folders const droppedResources = extractResources(originalEvent, true); diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index f17b1f6904..8d32e3a6d1 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -38,7 +38,7 @@ 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'; -import { ElementsDragAndDropData, DesktopDragAndDropData } from 'vs/base/browser/ui/list/listView'; +import { ElementsDragAndDropData, NativeDragAndDropData } from 'vs/base/browser/ui/list/listView'; import { URI } from 'vs/base/common/uri'; import { withUndefinedAsNull } from 'vs/base/common/types'; import { isWeb } from 'vs/base/common/platform'; @@ -667,7 +667,7 @@ class OpenEditorsDragAndDrop implements IListDragAndDrop { let description: string; if (context.cell.cellKind === CellKind.Markdown ? (languageId === 'markdown') : (languageId === context.cell.language)) { @@ -1446,7 +1446,7 @@ registerAction2(class extends NotebookCellAction { title: localize('notebookActions.splitCell', "Split Cell"), menu: { id: MenuId.NotebookCellTitle, - when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_FOCUSED, InputFocusedContext), + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_EDITOR_FOCUSED), order: CellToolbarOrder.SplitCell, group: CELL_TITLE_CELL_GROUP_ID, // alt: { @@ -1456,7 +1456,7 @@ registerAction2(class extends NotebookCellAction { }, icon: { id: 'codicon/split-vertical' }, keybinding: { - when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE, InputFocusedContext), + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_EDITOR_FOCUSED), primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_BACKSLASH), weight: KeybindingWeight.WorkbenchContrib }, @@ -1554,7 +1554,7 @@ registerAction2(class extends NotebookCellAction { title: localize('notebookActions.collapseCellInput', "Collapse Cell Input"), keybinding: { when: ContextKeyExpr.and(NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_INPUT_COLLAPSED.toNegated(), InputFocusedContext.toNegated()), - primary: KeyChord(KeyCode.KEY_C, KeyCode.KEY_C), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_C), weight: KeybindingWeight.WorkbenchContrib }, menu: { @@ -1566,7 +1566,7 @@ registerAction2(class extends NotebookCellAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { - context.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(context.cell.handle, { inputCollapsed: true }); + context.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(context.cell.handle, { inputCollapsed: true }); } }); @@ -1577,7 +1577,7 @@ registerAction2(class extends NotebookCellAction { title: localize('notebookActions.expandCellContent', "Expand Cell Content"), keybinding: { when: ContextKeyExpr.and(NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_INPUT_COLLAPSED), - primary: KeyChord(KeyCode.KEY_C, KeyCode.KEY_C), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_C), weight: KeybindingWeight.WorkbenchContrib }, menu: { @@ -1589,7 +1589,7 @@ registerAction2(class extends NotebookCellAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { - context.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(context.cell.handle, { inputCollapsed: false }); + context.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(context.cell.handle, { inputCollapsed: false }); } }); @@ -1600,7 +1600,7 @@ registerAction2(class extends NotebookCellAction { title: localize('notebookActions.collapseCellOutput', "Collapse Cell Output"), keybinding: { when: ContextKeyExpr.and(NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_OUTPUT_COLLAPSED.toNegated(), InputFocusedContext.toNegated(), NOTEBOOK_CELL_HAS_OUTPUTS), - primary: KeyChord(KeyCode.KEY_C, KeyCode.KEY_O), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_T), weight: KeybindingWeight.WorkbenchContrib }, menu: { @@ -1612,7 +1612,7 @@ registerAction2(class extends NotebookCellAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { - context.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(context.cell.handle, { outputCollapsed: true }); + context.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(context.cell.handle, { outputCollapsed: true }); } }); @@ -1623,7 +1623,7 @@ registerAction2(class extends NotebookCellAction { title: localize('notebookActions.expandCellOutput', "Expand Cell Output"), keybinding: { when: ContextKeyExpr.and(NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_OUTPUT_COLLAPSED), - primary: KeyChord(KeyCode.KEY_C, KeyCode.KEY_O), + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_T), weight: KeybindingWeight.WorkbenchContrib }, menu: { @@ -1635,7 +1635,7 @@ registerAction2(class extends NotebookCellAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { - context.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(context.cell.handle, { outputCollapsed: false }); + context.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(context.cell.handle, { outputCollapsed: false }); } }); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts b/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts index 62f5d7ba77..fee6a6c045 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { INotebookEditor, INotebookEditorMouseEvent, ICellRange, INotebookEditorContribution, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookEditor, INotebookEditorMouseEvent, INotebookEditorContribution, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import * as DOM from 'vs/base/browser/dom'; import { CellFoldingState, FoldingModel } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel'; -import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; import { registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; @@ -18,6 +18,7 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { getActiveNotebookEditor, NOTEBOOK_ACTIONS_CATEGORY } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; import { localize } from 'vs/nls'; +import { FoldingRegion } from 'vs/editor/contrib/folding/foldingRanges'; export class FoldingController extends Disposable implements INotebookEditorContribution { static id: string = 'workbench.notebook.findController'; @@ -65,19 +66,31 @@ export class FoldingController extends Disposable implements INotebookEditorCont this._updateEditorFoldingRanges(); } - setFoldingState(index: number, state: CellFoldingState) { + setFoldingStateDown(index: number, state: CellFoldingState, levels: number) { + const doCollapse = state === CellFoldingState.Collapsed; + let region = this._foldingModel!.getRegionAtLine(index + 1); + let regions: FoldingRegion[] = []; + if (region) { + if (region.isCollapsed !== doCollapse) { + regions.push(region); + } + if (levels > 1) { + let regionsInside = this._foldingModel!.getRegionsInside(region, (r, level: number) => r.isCollapsed !== doCollapse && level < levels); + regions.push(...regionsInside); + } + } + + regions.forEach(r => this._foldingModel!.setCollapsed(r.regionIndex, state === CellFoldingState.Collapsed)); + this._updateEditorFoldingRanges(); + } + + setFoldingStateUp(index: number, state: CellFoldingState, levels: number) { if (!this._foldingModel) { return; } - const range = this._foldingModel.regions.findRange(index + 1); - const startIndex = this._foldingModel.regions.getStartLineNumber(range) - 1; - - if (startIndex !== index) { - return; - } - - this._foldingModel.setCollapsed(range, state === CellFoldingState.Collapsed); + let regions = this._foldingModel.getAllRegionsAtLine(index + 1, (region, level) => region.isCollapsed !== (state === CellFoldingState.Collapsed) && level <= levels); + regions.forEach(r => this._foldingModel!.setCollapsed(r.regionIndex, state === CellFoldingState.Collapsed)); this._updateEditorFoldingRanges(); } @@ -121,7 +134,7 @@ export class FoldingController extends Disposable implements INotebookEditorCont return; } - this.setFoldingState(modelIndex, state === CellFoldingState.Collapsed ? CellFoldingState.Expanded : CellFoldingState.Collapsed); + this.setFoldingStateUp(modelIndex, state === CellFoldingState.Collapsed ? CellFoldingState.Expanded : CellFoldingState.Collapsed, 1); } return; @@ -130,6 +143,10 @@ export class FoldingController extends Disposable implements INotebookEditorCont registerNotebookContribution(FoldingController.id, FoldingController); + +const NOTEBOOK_FOLD_COMMAND_LABEL = localize('fold.cell', "Fold Cell"); +const NOTEBOOK_UNFOLD_COMMAND_LABEL = localize('unfold.cell', "Unfold Cell"); + registerAction2(class extends Action2 { constructor() { super({ @@ -146,12 +163,39 @@ registerAction2(class extends Action2 { secondary: [KeyCode.LeftArrow], weight: KeybindingWeight.WorkbenchContrib }, + description: { + description: NOTEBOOK_FOLD_COMMAND_LABEL, + args: [ + { + name: 'index', + description: 'The cell index', + schema: { + 'type': 'object', + 'required': ['index', 'direction'], + 'properties': { + 'index': { + 'type': 'number' + }, + 'direction': { + 'type': 'string', + 'enum': ['up', 'down'], + 'default': 'down' + }, + 'levels': { + 'type': 'number', + 'default': 1 + }, + } + } + } + ] + }, precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true }); } - async run(accessor: ServicesAccessor): Promise { + async run(accessor: ServicesAccessor, args?: { index: number, levels: number, direction: 'up' | 'down' }): Promise { const editorService = accessor.get(IEditorService); const editor = getActiveNotebookEditor(editorService); @@ -159,17 +203,27 @@ registerAction2(class extends Action2 { return; } - const activeCell = editor.getActiveCell(); - if (!activeCell) { - return; + const levels = args && args.levels || 1; + const direction = args && args.direction === 'up' ? 'up' : 'down'; + let index: number | undefined = undefined; + + if (args) { + index = args.index; + } else { + const activeCell = editor.getActiveCell(); + if (!activeCell) { + return; + } + index = editor.viewModel?.viewCells.indexOf(activeCell); } const controller = editor.getContribution(FoldingController.id); - - const index = editor.viewModel?.viewCells.indexOf(activeCell); - if (index !== undefined) { - controller.setFoldingState(index, CellFoldingState.Collapsed); + if (direction === 'up') { + controller.setFoldingStateUp(index, CellFoldingState.Collapsed, levels); + } else { + controller.setFoldingStateDown(index, CellFoldingState.Collapsed, levels); + } } } }); @@ -178,7 +232,7 @@ registerAction2(class extends Action2 { constructor() { super({ id: 'notebook.unfold', - title: { value: localize('unfold.cell', "Unfold Cell"), original: 'Unfold Cell' }, + title: { value: NOTEBOOK_UNFOLD_COMMAND_LABEL, original: 'Unfold Cell' }, category: NOTEBOOK_ACTIONS_CATEGORY, keybinding: { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), @@ -190,12 +244,39 @@ registerAction2(class extends Action2 { secondary: [KeyCode.RightArrow], weight: KeybindingWeight.WorkbenchContrib }, + description: { + description: NOTEBOOK_UNFOLD_COMMAND_LABEL, + args: [ + { + name: 'index', + description: 'The cell index', + schema: { + 'type': 'object', + 'required': ['index', 'direction'], + 'properties': { + 'index': { + 'type': 'number' + }, + 'direction': { + 'type': 'string', + 'enum': ['up', 'down'], + 'default': 'down' + }, + 'levels': { + 'type': 'number', + 'default': 1 + }, + } + } + } + ] + }, precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true }); } - async run(accessor: ServicesAccessor): Promise { + async run(accessor: ServicesAccessor, args?: { index: number, levels: number, direction: 'up' | 'down' }): Promise { const editorService = accessor.get(IEditorService); const editor = getActiveNotebookEditor(editorService); @@ -203,17 +284,27 @@ registerAction2(class extends Action2 { return; } - const activeCell = editor.getActiveCell(); - if (!activeCell) { - return; + const levels = args && args.levels || 1; + const direction = args && args.direction === 'up' ? 'up' : 'down'; + let index: number | undefined = undefined; + + if (args) { + index = args.index; + } else { + const activeCell = editor.getActiveCell(); + if (!activeCell) { + return; + } + index = editor.viewModel?.viewCells.indexOf(activeCell); } const controller = editor.getContribution(FoldingController.id); - - const index = editor.viewModel?.viewCells.indexOf(activeCell); - if (index !== undefined) { - controller.setFoldingState(index, CellFoldingState.Expanded); + if (direction === 'up') { + controller.setFoldingStateUp(index, CellFoldingState.Expanded, levels); + } else { + controller.setFoldingStateDown(index, CellFoldingState.Expanded, levels); + } } } }); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel.ts b/src/vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel.ts index 7e05765e4d..4a7e66d85c 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel.ts @@ -6,11 +6,14 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { TrackedRangeStickiness } from 'vs/editor/common/model'; -import { FoldingRegions } from 'vs/editor/contrib/folding/foldingRanges'; +import { FoldingRegion, FoldingRegions } from 'vs/editor/contrib/folding/foldingRanges'; import { IFoldingRangeData, sanitizeRanges } from 'vs/editor/contrib/folding/syntaxRangeProvider'; -import { ICellRange } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon'; + +type RegionFilter = (r: FoldingRegion) => boolean; +type RegionFilterWithLevel = (r: FoldingRegion, level: number) => boolean; + export class FoldingModel extends Disposable { private _viewModel: NotebookViewModel | null = null; @@ -73,7 +76,70 @@ export class FoldingModel extends Disposable { this.recompute(); } - public setCollapsed(index: number, newState: boolean) { + getRegionAtLine(lineNumber: number): FoldingRegion | null { + if (this._regions) { + let index = this._regions.findRange(lineNumber); + if (index >= 0) { + return this._regions.toRegion(index); + } + } + return null; + } + + getRegionsInside(region: FoldingRegion | null, filter?: RegionFilter | RegionFilterWithLevel): FoldingRegion[] { + let result: FoldingRegion[] = []; + let index = region ? region.regionIndex + 1 : 0; + let endLineNumber = region ? region.endLineNumber : Number.MAX_VALUE; + + if (filter && filter.length === 2) { + const levelStack: FoldingRegion[] = []; + for (let i = index, len = this._regions.length; i < len; i++) { + let current = this._regions.toRegion(i); + if (this._regions.getStartLineNumber(i) < endLineNumber) { + while (levelStack.length > 0 && !current.containedBy(levelStack[levelStack.length - 1])) { + levelStack.pop(); + } + levelStack.push(current); + if (filter(current, levelStack.length)) { + result.push(current); + } + } else { + break; + } + } + } else { + for (let i = index, len = this._regions.length; i < len; i++) { + let current = this._regions.toRegion(i); + if (this._regions.getStartLineNumber(i) < endLineNumber) { + if (!filter || (filter as RegionFilter)(current)) { + result.push(current); + } + } else { + break; + } + } + } + return result; + } + + getAllRegionsAtLine(lineNumber: number, filter?: (r: FoldingRegion, level: number) => boolean): FoldingRegion[] { + let result: FoldingRegion[] = []; + if (this._regions) { + let index = this._regions.findRange(lineNumber); + let level = 1; + while (index >= 0) { + let current = this._regions.toRegion(index); + if (!filter || filter(current, level)) { + result.push(current); + } + level++; + index = current.parentIndex; + } + } + return result; + } + + setCollapsed(index: number, newState: boolean) { this._regions.setCollapsed(index, newState); } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/fold/test/notebookFolding.test.ts b/src/vs/workbench/contrib/notebook/browser/contrib/fold/test/notebookFolding.test.ts index 1545f1d7ad..1219315833 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/fold/test/notebookFolding.test.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/fold/test/notebookFolding.test.ts @@ -4,9 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { withTestNotebook } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; +import { setupInstantiationService, withTestNotebook } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { FoldingModel } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel'; @@ -17,7 +16,7 @@ function updateFoldingStateAtIndex(foldingModel: FoldingModel, index: number, co } suite('Notebook Folding', () => { - const instantiationService = new TestInstantiationService(); + const instantiationService = setupInstantiationService(); const blukEditService = instantiationService.get(IBulkEditService); const undoRedoService = instantiationService.stub(IUndoRedoService, () => { }); instantiationService.spy(IUndoRedoService, 'pushElement'); @@ -28,13 +27,13 @@ suite('Notebook Folding', () => { blukEditService, undoRedoService, [ - [['# header 1'], 'markdown', CellKind.Markdown, [], {}], - [['body'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.1'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markdown, [], {}], + ['body', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.1', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { const foldingController = new FoldingModel(); @@ -57,13 +56,13 @@ suite('Notebook Folding', () => { blukEditService, undoRedoService, [ - [['# header 1'], 'markdown', CellKind.Markdown, [], {}], - [['body'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.1\n# header3'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markdown, [], {}], + ['body', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.1\n# header3', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { const foldingController = new FoldingModel(); @@ -91,13 +90,13 @@ suite('Notebook Folding', () => { blukEditService, undoRedoService, [ - [['# header 1'], 'markdown', CellKind.Markdown, [], {}], - [['body'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.1'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markdown, [], {}], + ['body', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.1', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { const foldingModel = new FoldingModel(); @@ -115,13 +114,13 @@ suite('Notebook Folding', () => { blukEditService, undoRedoService, [ - [['# header 1'], 'markdown', CellKind.Markdown, [], {}], - [['body'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markdown, [], {}], + ['body', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { const foldingModel = new FoldingModel(); @@ -140,13 +139,13 @@ suite('Notebook Folding', () => { blukEditService, undoRedoService, [ - [['# header 1'], 'markdown', CellKind.Markdown, [], {}], - [['body'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markdown, [], {}], + ['body', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { const foldingModel = new FoldingModel(); @@ -167,13 +166,13 @@ suite('Notebook Folding', () => { blukEditService, undoRedoService, [ - [['# header 1'], 'markdown', CellKind.Markdown, [], {}], - [['body'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markdown, [], {}], + ['body', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { const foldingModel = new FoldingModel(); @@ -224,18 +223,18 @@ suite('Notebook Folding', () => { blukEditService, undoRedoService, [ - [['# header 1'], 'markdown', CellKind.Markdown, [], {}], - [['body'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markdown, [], {}], + ['body', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { const foldingModel = new FoldingModel(); @@ -255,18 +254,18 @@ suite('Notebook Folding', () => { blukEditService, undoRedoService, [ - [['# header 1'], 'markdown', CellKind.Markdown, [], {}], - [['body'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markdown, [], {}], + ['body', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { const foldingModel = new FoldingModel(); @@ -290,18 +289,18 @@ suite('Notebook Folding', () => { blukEditService, undoRedoService, [ - [['# header 1'], 'markdown', CellKind.Markdown, [], {}], - [['body'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markdown, [], {}], + ['body', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { const foldingModel = new FoldingModel(); @@ -327,18 +326,18 @@ suite('Notebook Folding', () => { blukEditService, undoRedoService, [ - [['# header 1'], 'markdown', CellKind.Markdown, [], {}], - [['body'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markdown, [], {}], + ['body', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { const foldingModel = new FoldingModel(); @@ -366,18 +365,18 @@ suite('Notebook Folding', () => { blukEditService, undoRedoService, [ - [['# header 1'], 'markdown', CellKind.Markdown, [], {}], - [['body'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markdown, [], {}], + ['body', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { const foldingModel = new FoldingModel(); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts b/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts index 87211ffdbd..8f5ce97357 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts @@ -17,8 +17,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { getDocumentFormattingEditsUntilResult, formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; -import { WorkspaceTextEdit } from 'vs/editor/common/modes'; +import { IBulkEditService, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { registerEditorAction, EditorAction } from 'vs/editor/browser/editorExtensions'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -63,7 +62,7 @@ registerAction2(class extends Action2 { const dispoables = new DisposableStore(); try { - const edits: WorkspaceTextEdit[] = []; + const edits: ResourceTextEdit[] = []; for (const cell of notebook.cells) { @@ -78,18 +77,13 @@ registerAction2(class extends Action2 { ); if (formatEdits) { - formatEdits.forEach(edit => edits.push({ - edit, - resource: model.uri, - modelVersionId: model.getVersionId() - })); + for (let edit of formatEdits) { + edits.push(new ResourceTextEdit(model.uri, edit, model.getVersionId())); + } } } - await bulkEditService.apply( - { edits }, - { label: localize('label', "Format Notebook") } - ); + await bulkEditService.apply(edits, { label: localize('label', "Format Notebook") }); } finally { dispoables.dispose(); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/scm/scm.ts b/src/vs/workbench/contrib/notebook/browser/contrib/scm/scm.ts new file mode 100644 index 0000000000..798acc1a4d --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/contrib/scm/scm.ts @@ -0,0 +1,167 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { INotebookEditorContribution, INotebookEditor } from '../../notebookBrowser'; +import { registerNotebookContribution } from '../../notebookEditorExtensions'; +import { ISCMService } from 'vs/workbench/contrib/scm/common/scm'; +import { createProviderComparer } from 'vs/workbench/contrib/scm/browser/dirtydiffDecorator'; +import { first, ThrottledDelayer } from 'vs/base/common/async'; +import { INotebookService } from '../../../common/notebookService'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { IFileService } from 'vs/platform/files/common/files'; +import { URI } from 'vs/base/common/uri'; + +export class SCMController extends Disposable implements INotebookEditorContribution { + static id: string = 'workbench.notebook.findController'; + private _lastDecorationId: string[] = []; + private _localDisposable = new DisposableStore(); + private _originalDocument: NotebookTextModel | undefined = undefined; + private _originalResourceDisposableStore = new DisposableStore(); + private _diffDelayer = new ThrottledDelayer(200); + + private _lastVersion = -1; + + + constructor( + private readonly _notebookEditor: INotebookEditor, + @IFileService private readonly _fileService: FileService, + @ISCMService private readonly _scmService: ISCMService, + @INotebookService private readonly _notebookService: INotebookService + + ) { + super(); + + if (!this._notebookEditor.isEmbedded) { + this._register(this._notebookEditor.onDidChangeModel(() => { + this._localDisposable.clear(); + this._originalResourceDisposableStore.clear(); + this._diffDelayer.cancel(); + this.update(); + + if (this._notebookEditor.textModel) { + this._localDisposable.add(this._notebookEditor.textModel.onDidChangeContent(() => { + this.update(); + })); + + this._localDisposable.add(this._notebookEditor.textModel.onDidChangeCells(() => { + this.update(); + })); + } + })); + + this._register(this._notebookEditor.onWillDispose(() => { + this._localDisposable.clear(); + this._originalResourceDisposableStore.clear(); + })); + + this.update(); + } + } + + private async _resolveNotebookDocument(uri: URI, viewType: string) { + const providers = this._scmService.repositories.map(r => r.provider); + const rootedProviders = providers.filter(p => !!p.rootUri); + + rootedProviders.sort(createProviderComparer(uri)); + + const result = await first(rootedProviders.map(p => () => p.getOriginalResource(uri))); + + if (!result) { + this._originalDocument = undefined; + this._originalResourceDisposableStore.clear(); + return; + } + + if (result.toString() === this._originalDocument?.uri.toString()) { + // original document not changed + return; + } + + this._originalResourceDisposableStore.add(this._fileService.watch(result)); + this._originalResourceDisposableStore.add(this._fileService.onDidFilesChange(e => { + if (e.changes.find(change => change.resource.toString() === result.toString())) { + this._originalDocument = undefined; + this._originalResourceDisposableStore.clear(); + this.update(); + } + })); + + const originalDocument = await this._notebookService.resolveNotebook(viewType, result, false); + this._originalResourceDisposableStore.add({ + dispose: () => { + this._originalDocument?.dispose(); + this._originalDocument = undefined; + } + }); + + this._originalDocument = originalDocument; + } + + async update() { + if (!this._diffDelayer) { + return; + } + + await this._diffDelayer + .trigger(async () => { + const modifiedDocument = this._notebookEditor.textModel; + if (!modifiedDocument) { + return; + } + + if (this._lastVersion >= modifiedDocument.versionId) { + return; + } + + this._lastVersion = modifiedDocument.versionId; + await this._resolveNotebookDocument(modifiedDocument.uri, modifiedDocument.viewType); + + if (!this._originalDocument) { + this._clear(); + return; + } + + // const diff = new LcsDiff(new CellSequence(this._originalDocument), new CellSequence(modifiedDocument)); + // const diffResult = diff.ComputeDiff(false); + + // const decorations: INotebookDeltaDecoration[] = []; + // diffResult.changes.forEach(change => { + // if (change.originalLength === 0) { + // // doesn't exist in original + // for (let i = 0; i < change.modifiedLength; i++) { + // decorations.push({ + // handle: modifiedDocument.cells[change.modifiedStart + i].handle, + // options: { gutterClassName: 'nb-gutter-cell-inserted' } + // }); + // } + // } else { + // if (change.modifiedLength === 0) { + // // diff.deleteCount + // // removed from original + // } else { + // // modification + // for (let i = 0; i < change.modifiedLength; i++) { + // decorations.push({ + // handle: modifiedDocument.cells[change.modifiedStart + i].handle, + // options: { gutterClassName: 'nb-gutter-cell-changed' } + // }); + // } + // } + // } + // }); + + + // this._lastDecorationId = this._notebookEditor.deltaCellDecorations(this._lastDecorationId, decorations); + }); + } + + private _clear() { + this._lastDecorationId = this._notebookEditor.deltaCellDecorations(this._lastDecorationId, []); + } +} + +registerNotebookContribution(SCMController.id, SCMController); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts b/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts index 46963b0bc2..393bdb35fc 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts @@ -14,7 +14,7 @@ import { INotebookEditor, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { INotebookKernelInfo2, INotebookKernelInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookKernelInfo2 } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; @@ -50,24 +50,22 @@ registerAction2(class extends Action2 { const tokenSource = new CancellationTokenSource(); const availableKernels2 = await notebookService.getContributedNotebookKernels2(editor.viewModel!.viewType, editor.viewModel!.uri, tokenSource.token); - const availableKernels = notebookService.getContributedNotebookKernels(editor.viewModel!.viewType, editor.viewModel!.uri); - const picks: QuickPickInput[] = [...availableKernels2, ...availableKernels].map((a) => { + const picks: QuickPickInput[] = [...availableKernels2].map((a) => { return { id: a.id, label: a.label, picked: a.id === activeKernel?.id, description: - (a as INotebookKernelInfo2).description - ? (a as INotebookKernelInfo2).description + a.description + ? a.description : a.extension.value + (a.id === activeKernel?.id ? nls.localize('currentActiveKernel', " (Currently Active)") : ''), + detail: a.detail, kernelProviderId: a.extension.value, run: async () => { editor.activeKernel = a; - if ((a as any).resolve) { - (a as INotebookKernelInfo2).resolve(editor.uri!, editor.getId(), tokenSource.token); - } + a.resolve(editor.uri!, editor.getId(), tokenSource.token); }, buttons: [{ iconClass: 'codicon-settings-gear', @@ -76,27 +74,6 @@ registerAction2(class extends Action2 { }; }); - const provider = notebookService.getContributedNotebookProviders(editor.viewModel!.uri)[0]; - - if (provider.kernel) { - picks.unshift({ - id: provider.id, - label: provider.displayName, - picked: !activeKernel, // no active kernel, the builtin kernel of the provider is used - description: activeKernel === undefined - ? nls.localize('currentActiveBuiltinKernel', " (Currently Active)") - : '', - kernelProviderId: provider.providerExtensionId, - run: () => { - editor.activeKernel = undefined; - }, - buttons: [{ - iconClass: 'codicon-settings-gear', - tooltip: nls.localize('notebook.promptKernel.setDefaultTooltip', "Set as default kernel provider for '{0}'", editor.viewModel!.viewType) - }] - }); - } - const picker = quickInputService.createQuickPick<(IQuickPickItem & { run(): void; kernelProviderId?: string })>(); picker.items = picks; picker.activeItems = picks.filter(pick => (pick as IQuickPickItem).picked) as (IQuickPickItem & { run(): void; kernelProviderId?: string; })[]; @@ -192,7 +169,7 @@ export class KernelStatus extends Disposable implements IWorkbenchContribution { } } - showKernelStatus(kernel: INotebookKernelInfo | INotebookKernelInfo2 | undefined) { + showKernelStatus(kernel: INotebookKernelInfo2 | undefined) { this.kernelInfoElement.value = this._statusbarService.addEntry({ text: kernel ? kernel.label : 'Choose Kernel', ariaLabel: kernel ? kernel.label : 'Choose Kernel', diff --git a/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts new file mode 100644 index 0000000000..eca6546fe6 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts @@ -0,0 +1,932 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from 'vs/base/browser/dom'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { IDiffEditorOptions, IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { CellDiffViewModel, PropertyFoldingState } from 'vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel'; +import { CellDiffRenderTemplate, CellDiffViewModelLayoutChangeEvent, DIFF_CELL_MARGIN, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/common'; +import { EDITOR_BOTTOM_PADDING, EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; +import { renderCodiconsAsElement } from 'vs/base/browser/codicons'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { format } from 'vs/base/common/jsonFormatter'; +import { applyEdits } from 'vs/base/common/jsonEdit'; +import { CellUri, NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { hash } from 'vs/base/common/hash'; +import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IMenu, IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { CodiconActionViewItem } from 'vs/workbench/contrib/notebook/browser/view/renderers/commonViewComponents'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IAction } from 'vs/base/common/actions'; +import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; + +const fixedEditorOptions: IEditorOptions = { + padding: { + top: 12, + bottom: 12 + }, + scrollBeyondLastLine: false, + scrollbar: { + verticalScrollbarSize: 14, + horizontal: 'auto', + useShadows: true, + verticalHasArrows: false, + horizontalHasArrows: false, + alwaysConsumeMouseWheel: false + }, + renderLineHighlightOnlyWhenFocus: true, + overviewRulerLanes: 0, + selectOnLineNumbers: false, + wordWrap: 'off', + lineNumbers: 'off', + lineDecorationsWidth: 0, + glyphMargin: false, + fixedOverflowWidgets: true, + minimap: { enabled: false }, + renderValidationDecorations: 'on', + renderLineHighlight: 'none', + readOnly: true +}; + +const fixedDiffEditorOptions: IDiffEditorOptions = { + ...fixedEditorOptions, + glyphMargin: true, + enableSplitViewResizing: false, + renderIndicators: false, + readOnly: false +}; + + + +class PropertyHeader extends Disposable { + protected _foldingIndicator!: HTMLElement; + protected _statusSpan!: HTMLElement; + protected _toolbar!: ToolBar; + protected _menu!: IMenu; + + constructor( + readonly cell: CellDiffViewModel, + readonly metadataHeaderContainer: HTMLElement, + readonly notebookEditor: INotebookTextDiffEditor, + readonly accessor: { + updateInfoRendering: () => void; + checkIfModified: (cell: CellDiffViewModel) => boolean; + getFoldingState: (cell: CellDiffViewModel) => PropertyFoldingState; + updateFoldingState: (cell: CellDiffViewModel, newState: PropertyFoldingState) => void; + unChangedLabel: string; + changedLabel: string; + prefix: string; + menuId: MenuId; + }, + @IContextMenuService readonly contextMenuService: IContextMenuService, + @IKeybindingService readonly keybindingService: IKeybindingService, + @INotificationService readonly notificationService: INotificationService, + @IMenuService readonly menuService: IMenuService, + @IContextKeyService readonly contextKeyService: IContextKeyService + ) { + super(); + } + + buildHeader(): void { + let metadataChanged = this.accessor.checkIfModified(this.cell); + this._foldingIndicator = DOM.append(this.metadataHeaderContainer, DOM.$('.property-folding-indicator')); + DOM.addClass(this._foldingIndicator, this.accessor.prefix); + this._updateFoldingIcon(); + const metadataStatus = DOM.append(this.metadataHeaderContainer, DOM.$('div.property-status')); + this._statusSpan = DOM.append(metadataStatus, DOM.$('span')); + + if (metadataChanged) { + this._statusSpan.textContent = this.accessor.changedLabel; + this._statusSpan.style.fontWeight = 'bold'; + DOM.addClass(this.metadataHeaderContainer, 'modified'); + } else { + this._statusSpan.textContent = this.accessor.unChangedLabel; + } + + const cellToolbarContainer = DOM.append(this.metadataHeaderContainer, DOM.$('div.property-toolbar')); + this._toolbar = new ToolBar(cellToolbarContainer, this.contextMenuService, { + actionViewItemProvider: action => { + if (action instanceof MenuItemAction) { + const item = new CodiconActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService); + return item; + } + + return undefined; + } + }); + this._toolbar.context = { + cell: this.cell + }; + + this._menu = this.menuService.createMenu(this.accessor.menuId, this.contextKeyService); + + if (metadataChanged) { + const actions: IAction[] = []; + createAndFillInActionBarActions(this._menu, { shouldForwardArgs: true }, actions); + this._toolbar.setActions(actions); + } + + this._register(this.notebookEditor.onMouseUp(e => { + if (!e.event.target) { + return; + } + + const target = e.event.target as HTMLElement; + + if (DOM.hasClass(target, 'codicon-chevron-down') || DOM.hasClass(target, 'codicon-chevron-right')) { + const parent = target.parentElement as HTMLElement; + + if (!parent) { + return; + } + + if (!DOM.hasClass(parent, this.accessor.prefix)) { + return; + } + + if (!DOM.hasClass(parent, 'property-folding-indicator')) { + return; + } + + // folding icon + + const cellViewModel = e.target; + + if (cellViewModel === this.cell) { + const oldFoldingState = this.accessor.getFoldingState(this.cell); + this.accessor.updateFoldingState(this.cell, oldFoldingState === PropertyFoldingState.Expanded ? PropertyFoldingState.Collapsed : PropertyFoldingState.Expanded); + this._updateFoldingIcon(); + this.accessor.updateInfoRendering(); + } + } + + return; + })); + + this._updateFoldingIcon(); + this.accessor.updateInfoRendering(); + } + + refresh() { + let metadataChanged = this.accessor.checkIfModified(this.cell); + if (metadataChanged) { + this._statusSpan.textContent = this.accessor.changedLabel; + this._statusSpan.style.fontWeight = 'bold'; + DOM.addClass(this.metadataHeaderContainer, 'modified'); + const actions: IAction[] = []; + createAndFillInActionBarActions(this._menu, undefined, actions); + this._toolbar.setActions(actions); + } else { + this._statusSpan.textContent = this.accessor.unChangedLabel; + this._statusSpan.style.fontWeight = 'normal'; + this._toolbar.setActions([]); + } + } + + private _updateFoldingIcon() { + if (this.accessor.getFoldingState(this.cell) === PropertyFoldingState.Collapsed) { + DOM.reset(this._foldingIndicator, ...renderCodiconsAsElement('$(chevron-right)')); + } else { + DOM.reset(this._foldingIndicator, ...renderCodiconsAsElement('$(chevron-down)')); + } + } +} + +abstract class AbstractCellRenderer extends Disposable { + protected _metadataHeaderContainer!: HTMLElement; + protected _metadataHeader!: PropertyHeader; + protected _metadataInfoContainer!: HTMLElement; + protected _metadataEditorContainer?: HTMLElement; + protected _metadataEditorDisposeStore!: DisposableStore; + protected _metadataEditor?: CodeEditorWidget | DiffEditorWidget; + + protected _outputHeaderContainer!: HTMLElement; + protected _outputHeader!: PropertyHeader; + protected _outputInfoContainer!: HTMLElement; + protected _outputEditorContainer?: HTMLElement; + protected _outputEditorDisposeStore!: DisposableStore; + protected _outputEditor?: CodeEditorWidget | DiffEditorWidget; + + + protected _diffEditorContainer!: HTMLElement; + protected _diagonalFill?: HTMLElement; + protected _layoutInfo!: { + editorHeight: number; + editorMargin: number; + metadataStatusHeight: number; + metadataHeight: number; + outputStatusHeight: number; + outputHeight: number; + bodyMargin: number; + }; + + constructor( + readonly notebookEditor: INotebookTextDiffEditor, + readonly cell: CellDiffViewModel, + readonly templateData: CellDiffRenderTemplate, + readonly style: 'left' | 'right' | 'full', + protected readonly instantiationService: IInstantiationService, + protected readonly modeService: IModeService, + protected readonly modelService: IModelService, + + ) { + super(); + // init + this._layoutInfo = { + editorHeight: 0, + editorMargin: 0, + metadataHeight: 0, + metadataStatusHeight: 25, + outputHeight: 0, + outputStatusHeight: 25, + bodyMargin: 32 + }; + this._metadataEditorDisposeStore = new DisposableStore(); + this._outputEditorDisposeStore = new DisposableStore(); + this._register(this._metadataEditorDisposeStore); + this.initData(); + this.buildBody(templateData.container); + this._register(cell.onDidLayoutChange(e => this.onDidLayoutChange(e))); + } + + buildBody(container: HTMLElement) { + const body = DOM.$('.cell-body'); + DOM.append(container, body); + this._diffEditorContainer = DOM.$('.cell-diff-editor-container'); + switch (this.style) { + case 'left': + DOM.addClass(body, 'left'); + break; + case 'right': + DOM.addClass(body, 'right'); + break; + default: + DOM.addClass(body, 'full'); + break; + } + + DOM.append(body, this._diffEditorContainer); + this._diagonalFill = DOM.append(body, DOM.$('.diagonal-fill')); + this.styleContainer(this._diffEditorContainer); + const sourceContainer = DOM.append(this._diffEditorContainer, DOM.$('.source-container')); + this.buildSourceEditor(sourceContainer); + + this._metadataHeaderContainer = DOM.append(this._diffEditorContainer, DOM.$('.metadata-header-container')); + this._metadataInfoContainer = DOM.append(this._diffEditorContainer, DOM.$('.metadata-info-container')); + + this._metadataHeader = this.instantiationService.createInstance( + PropertyHeader, + this.cell, + this._metadataHeaderContainer, + this.notebookEditor, + { + updateInfoRendering: this.updateMetadataRendering.bind(this), + checkIfModified: (cell) => { + return cell.type !== 'delete' && cell.type !== 'insert' && hash(this._getFormatedMetadataJSON(cell.original?.metadata || {}, cell.original?.language)) !== hash(this._getFormatedMetadataJSON(cell.modified?.metadata ?? {}, cell.modified?.language)); + }, + getFoldingState: (cell) => { + return cell.metadataFoldingState; + }, + updateFoldingState: (cell, state) => { + cell.metadataFoldingState = state; + }, + unChangedLabel: 'Metadata', + changedLabel: 'Metadata changed', + prefix: 'metadata', + menuId: MenuId.NotebookDiffCellMetadataTitle + } + ); + this._register(this._metadataHeader); + this._metadataHeader.buildHeader(); + + this._outputHeaderContainer = DOM.append(this._diffEditorContainer, DOM.$('.output-header-container')); + this._outputInfoContainer = DOM.append(this._diffEditorContainer, DOM.$('.output-info-container')); + + this._outputHeader = this.instantiationService.createInstance( + PropertyHeader, + this.cell, + this._outputHeaderContainer, + this.notebookEditor, + { + updateInfoRendering: this.updateOutputRendering.bind(this), + checkIfModified: (cell) => { + return cell.type !== 'delete' && cell.type !== 'insert' && !this.notebookEditor.textModel!.transientOptions.transientOutputs && cell.type === 'modified' && hash(cell.original?.outputs ?? []) !== hash(cell.modified?.outputs ?? []); + }, + getFoldingState: (cell) => { + return this.cell.outputFoldingState; + }, + updateFoldingState: (cell, state) => { + cell.outputFoldingState = state; + }, + unChangedLabel: 'Outputs', + changedLabel: 'Outputs changed', + prefix: 'output', + menuId: MenuId.NotebookDiffCellOutputsTitle + } + ); + this._register(this._outputHeader); + this._outputHeader.buildHeader(); + } + + updateMetadataRendering() { + if (this.cell.metadataFoldingState === PropertyFoldingState.Expanded) { + // we should expand the metadata editor + this._metadataInfoContainer.style.display = 'block'; + + if (!this._metadataEditorContainer || !this._metadataEditor) { + // create editor + this._metadataEditorContainer = DOM.append(this._metadataInfoContainer, DOM.$('.metadata-editor-container')); + this._buildMetadataEditor(); + } else { + this._layoutInfo.metadataHeight = this._metadataEditor.getContentHeight(); + this.layout({ metadataEditor: true }); + } + } else { + // we should collapse the metadata editor + this._metadataInfoContainer.style.display = 'none'; + this._metadataEditorDisposeStore.clear(); + this._layoutInfo.metadataHeight = 0; + this.layout({}); + } + } + + updateOutputRendering() { + if (this.cell.outputFoldingState === PropertyFoldingState.Expanded) { + this._outputInfoContainer.style.display = 'block'; + + if (!this._outputEditorContainer || !this._outputEditor) { + // create editor + this._outputEditorContainer = DOM.append(this._outputInfoContainer, DOM.$('.output-editor-container')); + this._buildOutputEditor(); + } else { + this._layoutInfo.outputHeight = this._outputEditor.getContentHeight(); + this.layout({ outputEditor: true }); + } + } else { + this._outputInfoContainer.style.display = 'none'; + this._outputEditorDisposeStore.clear(); + this._layoutInfo.outputHeight = 0; + this.layout({}); + } + } + + protected _getFormatedMetadataJSON(metadata: NotebookCellMetadata, language?: string) { + const filteredMetadata: { [key: string]: any } = metadata; + const content = JSON.stringify({ + language, + ...filteredMetadata + }); + + const edits = format(content, undefined, {}); + const metadataSource = applyEdits(content, edits); + + return metadataSource; + } + + private _applySanitizedMetadataChanges(currentMetadata: NotebookCellMetadata, newMetadata: any) { + let result: { [key: string]: any } = {}; + let newLangauge: string | undefined = undefined; + try { + const newMetadataObj = JSON.parse(newMetadata); + const keys = new Set([...Object.keys(newMetadataObj)]); + for (let key of keys) { + switch (key as keyof NotebookCellMetadata) { + case 'breakpointMargin': + case 'editable': + case 'hasExecutionOrder': + case 'inputCollapsed': + case 'outputCollapsed': + case 'runnable': + // boolean + if (typeof newMetadataObj[key] === 'boolean') { + result[key] = newMetadataObj[key]; + } else { + result[key] = currentMetadata[key as keyof NotebookCellMetadata]; + } + break; + + case 'executionOrder': + case 'lastRunDuration': + // number + if (typeof newMetadataObj[key] === 'number') { + result[key] = newMetadataObj[key]; + } else { + result[key] = currentMetadata[key as keyof NotebookCellMetadata]; + } + break; + case 'runState': + // enum + if (typeof newMetadataObj[key] === 'number' && [1, 2, 3, 4].indexOf(newMetadataObj[key]) >= 0) { + result[key] = newMetadataObj[key]; + } else { + result[key] = currentMetadata[key as keyof NotebookCellMetadata]; + } + break; + case 'statusMessage': + // string + if (typeof newMetadataObj[key] === 'string') { + result[key] = newMetadataObj[key]; + } else { + result[key] = currentMetadata[key as keyof NotebookCellMetadata]; + } + break; + default: + if (key === 'language') { + newLangauge = newMetadataObj[key]; + } + result[key] = newMetadataObj[key]; + break; + } + } + + if (newLangauge !== undefined && newLangauge !== this.cell.modified!.language) { + this.notebookEditor.textModel!.changeCellLanguage(this.cell.modified!.handle, newLangauge); + } + this.notebookEditor.textModel!.changeCellMetadata(this.cell.modified!.handle, result, false); + } catch { + } + } + + private _buildMetadataEditor() { + if (this.cell.type === 'modified' || this.cell.type === 'unchanged') { + const originalMetadataSource = this._getFormatedMetadataJSON(this.cell.original?.metadata || {}, this.cell.original?.language); + const modifiedMetadataSource = this._getFormatedMetadataJSON(this.cell.modified?.metadata || {}, this.cell.modified?.language); + this._metadataEditor = this.instantiationService.createInstance(DiffEditorWidget, this._metadataEditorContainer!, { + ...fixedDiffEditorOptions, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), + readOnly: false, + originalEditable: false, + ignoreTrimWhitespace: false + }); + + DOM.addClass(this._metadataEditorContainer!, 'diff'); + + const mode = this.modeService.create('json'); + const originalMetadataModel = this.modelService.createModel(originalMetadataSource, mode, CellUri.generateCellMetadataUri(this.cell.original!.uri, this.cell.original!.handle), false); + const modifiedMetadataModel = this.modelService.createModel(modifiedMetadataSource, mode, CellUri.generateCellMetadataUri(this.cell.modified!.uri, this.cell.modified!.handle), false); + this._metadataEditor.setModel({ + original: originalMetadataModel, + modified: modifiedMetadataModel + }); + + this._register(originalMetadataModel); + this._register(modifiedMetadataModel); + + this._layoutInfo.metadataHeight = this._metadataEditor.getContentHeight(); + this.layout({ metadataEditor: true }); + + this._register(this._metadataEditor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged && this.cell.metadataFoldingState === PropertyFoldingState.Expanded) { + this._layoutInfo.metadataHeight = e.contentHeight; + this.layout({ metadataEditor: true }); + } + })); + + let respondingToContentChange = false; + + this._register(modifiedMetadataModel.onDidChangeContent(() => { + respondingToContentChange = true; + const value = modifiedMetadataModel.getValue(); + this._applySanitizedMetadataChanges(this.cell.modified!.metadata, value); + this._metadataHeader.refresh(); + respondingToContentChange = false; + })); + + this._register(this.cell.modified!.onDidChangeMetadata(() => { + if (respondingToContentChange) { + return; + } + + const modifiedMetadataSource = this._getFormatedMetadataJSON(this.cell.modified?.metadata || {}, this.cell.modified?.language); + modifiedMetadataModel.setValue(modifiedMetadataSource); + })); + + return; + } + + this._metadataEditor = this.instantiationService.createInstance(CodeEditorWidget, this._metadataEditorContainer!, { + ...fixedEditorOptions, + dimension: { + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true), + height: 0 + }, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), + readOnly: false + }, {}); + + const mode = this.modeService.create('jsonc'); + const originalMetadataSource = this._getFormatedMetadataJSON( + this.cell.type === 'insert' + ? this.cell.modified!.metadata || {} + : this.cell.original!.metadata || {}); + const uri = this.cell.type === 'insert' + ? this.cell.modified!.uri + : this.cell.original!.uri; + const handle = this.cell.type === 'insert' + ? this.cell.modified!.handle + : this.cell.original!.handle; + + const modelUri = CellUri.generateCellMetadataUri(uri, handle); + const metadataModel = this.modelService.createModel(originalMetadataSource, mode, modelUri, false); + this._metadataEditor.setModel(metadataModel); + this._register(metadataModel); + + this._layoutInfo.metadataHeight = this._metadataEditor.getContentHeight(); + this.layout({ metadataEditor: true }); + + this._register(this._metadataEditor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged && this.cell.metadataFoldingState === PropertyFoldingState.Expanded) { + this._layoutInfo.metadataHeight = e.contentHeight; + this.layout({ metadataEditor: true }); + } + })); + } + + private _getFormatedOutputJSON(outputs: any[]) { + const content = JSON.stringify(outputs); + + const edits = format(content, undefined, {}); + const source = applyEdits(content, edits); + + return source; + } + + private _buildOutputEditor() { + if ((this.cell.type === 'modified' || this.cell.type === 'unchanged') && !this.notebookEditor.textModel!.transientOptions.transientOutputs) { + const originalOutputsSource = this._getFormatedOutputJSON(this.cell.original?.outputs || []); + const modifiedOutputsSource = this._getFormatedOutputJSON(this.cell.modified?.outputs || []); + if (originalOutputsSource !== modifiedOutputsSource) { + this._outputEditor = this.instantiationService.createInstance(DiffEditorWidget, this._outputEditorContainer!, { + ...fixedDiffEditorOptions, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), + readOnly: true, + ignoreTrimWhitespace: false + }); + + DOM.addClass(this._outputEditorContainer!, 'diff'); + + const mode = this.modeService.create('json'); + const originalModel = this.modelService.createModel(originalOutputsSource, mode, undefined, true); + const modifiedModel = this.modelService.createModel(modifiedOutputsSource, mode, undefined, true); + this._outputEditor.setModel({ + original: originalModel, + modified: modifiedModel + }); + + this._layoutInfo.outputHeight = this._outputEditor.getContentHeight(); + this.layout({ outputEditor: true }); + + this._register(this._outputEditor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged && this.cell.outputFoldingState === PropertyFoldingState.Expanded) { + this._layoutInfo.outputHeight = e.contentHeight; + this.layout({ outputEditor: true }); + } + })); + + this._register(this.cell.modified!.onDidChangeOutputs(() => { + const modifiedOutputsSource = this._getFormatedOutputJSON(this.cell.modified?.outputs || []); + modifiedModel.setValue(modifiedOutputsSource); + this._outputHeader.refresh(); + })); + + return; + } + } + + this._outputEditor = this.instantiationService.createInstance(CodeEditorWidget, this._outputEditorContainer!, { + ...fixedEditorOptions, + dimension: { + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true), + height: 0 + }, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode() + }, {}); + + const mode = this.modeService.create('json'); + const originaloutputSource = this._getFormatedOutputJSON( + this.notebookEditor.textModel!.transientOptions + ? [] + : this.cell.type === 'insert' + ? this.cell.modified!.outputs || [] + : this.cell.original!.outputs || []); + const outputModel = this.modelService.createModel(originaloutputSource, mode, undefined, true); + this._outputEditor.setModel(outputModel); + + this._layoutInfo.outputHeight = this._outputEditor.getContentHeight(); + this.layout({ outputEditor: true }); + + this._register(this._outputEditor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged && this.cell.outputFoldingState === PropertyFoldingState.Expanded) { + this._layoutInfo.outputHeight = e.contentHeight; + this.layout({ outputEditor: true }); + } + })); + } + + protected layoutNotebookCell() { + this.notebookEditor.layoutNotebookCell( + this.cell, + this._layoutInfo.editorHeight + + this._layoutInfo.editorMargin + + this._layoutInfo.metadataHeight + + this._layoutInfo.metadataStatusHeight + + this._layoutInfo.outputHeight + + this._layoutInfo.outputStatusHeight + + this._layoutInfo.bodyMargin + ); + } + + abstract initData(): void; + abstract styleContainer(container: HTMLElement): void; + abstract buildSourceEditor(sourceContainer: HTMLElement): void; + abstract onDidLayoutChange(event: CellDiffViewModelLayoutChangeEvent): void; + abstract layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataEditor?: boolean, outputEditor?: boolean }): void; +} + +export class DeletedCell extends AbstractCellRenderer { + private _editor!: CodeEditorWidget; + constructor( + readonly notebookEditor: INotebookTextDiffEditor, + readonly cell: CellDiffViewModel, + readonly templateData: CellDiffRenderTemplate, + @IModeService readonly modeService: IModeService, + @IModelService readonly modelService: IModelService, + @IInstantiationService protected readonly instantiationService: IInstantiationService, + ) { + super(notebookEditor, cell, templateData, 'left', instantiationService, modeService, modelService); + } + + initData(): void { + } + + styleContainer(container: HTMLElement) { + DOM.addClass(container, 'removed'); + } + + buildSourceEditor(sourceContainer: HTMLElement): void { + const originalCell = this.cell.original!; + const lineCount = originalCell.textBuffer.getLineCount(); + const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; + const editorHeight = lineCount * lineHeight + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING; + + const editorContainer = DOM.append(sourceContainer, DOM.$('.editor-container')); + + this._editor = this.instantiationService.createInstance(CodeEditorWidget, editorContainer, { + ...fixedEditorOptions, + dimension: { + width: (this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN) / 2 - 18, + height: editorHeight + }, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode() + }, {}); + this._layoutInfo.editorHeight = editorHeight; + + this._register(this._editor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged) { + this._layoutInfo.editorHeight = e.contentHeight; + this.layout({ editorHeight: true }); + } + })); + + originalCell.resolveTextModelRef().then(ref => { + this._register(ref); + + const textModel = ref.object.textEditorModel; + this._editor.setModel(textModel); + this._layoutInfo.editorHeight = this._editor.getContentHeight(); + this.layout({ editorHeight: true }); + }); + + } + + onDidLayoutChange(e: CellDiffViewModelLayoutChangeEvent) { + if (e.outerWidth !== undefined) { + this.layout({ outerWidth: true }); + } + } + layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataEditor?: boolean, outputEditor?: boolean }) { + if (state.editorHeight || state.outerWidth) { + this._editor.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), + height: this._layoutInfo.editorHeight + }); + } + + if (state.metadataEditor || state.outerWidth) { + this._metadataEditor?.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), + height: this._layoutInfo.metadataHeight + }); + } + + if (state.outputEditor || state.outerWidth) { + this._outputEditor?.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), + height: this._layoutInfo.outputHeight + }); + } + + this.layoutNotebookCell(); + } +} + +export class InsertCell extends AbstractCellRenderer { + private _editor!: CodeEditorWidget; + constructor( + readonly notebookEditor: INotebookTextDiffEditor, + readonly cell: CellDiffViewModel, + readonly templateData: CellDiffRenderTemplate, + @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IModeService readonly modeService: IModeService, + @IModelService readonly modelService: IModelService, + ) { + super(notebookEditor, cell, templateData, 'right', instantiationService, modeService, modelService); + } + + initData(): void { + } + + styleContainer(container: HTMLElement): void { + DOM.addClass(container, 'inserted'); + } + + buildSourceEditor(sourceContainer: HTMLElement): void { + const modifiedCell = this.cell.modified!; + const lineCount = modifiedCell.textBuffer.getLineCount(); + const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; + const editorHeight = lineCount * lineHeight + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING; + const editorContainer = DOM.append(sourceContainer, DOM.$('.editor-container')); + + this._editor = this.instantiationService.createInstance(CodeEditorWidget, editorContainer, { + ...fixedEditorOptions, + dimension: { + width: (this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN) / 2 - 18, + height: editorHeight + }, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), + readOnly: false + }, {}); + + this._layoutInfo.editorHeight = editorHeight; + + this._register(this._editor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged) { + this._layoutInfo.editorHeight = e.contentHeight; + this.layout({ editorHeight: true }); + } + })); + + modifiedCell.resolveTextModelRef().then(ref => { + this._register(ref); + + const textModel = ref.object.textEditorModel; + this._editor.setModel(textModel); + this._layoutInfo.editorHeight = this._editor.getContentHeight(); + this.layout({ editorHeight: true }); + }); + } + + onDidLayoutChange(e: CellDiffViewModelLayoutChangeEvent) { + if (e.outerWidth !== undefined) { + this.layout({ outerWidth: true }); + } + } + + layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataEditor?: boolean, outputEditor?: boolean }) { + if (state.editorHeight || state.outerWidth) { + this._editor.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), + height: this._layoutInfo.editorHeight + }); + } + + if (state.metadataEditor || state.outerWidth) { + this._metadataEditor?.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true), + height: this._layoutInfo.metadataHeight + }); + } + + if (state.outputEditor || state.outerWidth) { + this._outputEditor?.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true), + height: this._layoutInfo.outputHeight + }); + } + + this.layoutNotebookCell(); + } +} + +export class ModifiedCell extends AbstractCellRenderer { + private _editor?: DiffEditorWidget; + private _editorContainer!: HTMLElement; + constructor( + readonly notebookEditor: INotebookTextDiffEditor, + readonly cell: CellDiffViewModel, + readonly templateData: CellDiffRenderTemplate, + @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IModeService readonly modeService: IModeService, + @IModelService readonly modelService: IModelService, + ) { + super(notebookEditor, cell, templateData, 'full', instantiationService, modeService, modelService); + } + + initData(): void { + } + + styleContainer(container: HTMLElement): void { + } + + buildSourceEditor(sourceContainer: HTMLElement): void { + const modifiedCell = this.cell.modified!; + const lineCount = modifiedCell.textBuffer.getLineCount(); + const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; + const editorHeight = lineCount * lineHeight + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING; + this._editorContainer = DOM.append(sourceContainer, DOM.$('.editor-container')); + + this._editor = this.instantiationService.createInstance(DiffEditorWidget, this._editorContainer, { + ...fixedDiffEditorOptions, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), + originalEditable: false, + ignoreTrimWhitespace: false + }); + DOM.addClass(this._editorContainer, 'diff'); + + this._editor.layout({ + width: this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN, + height: editorHeight + }); + + this._editorContainer.style.height = `${editorHeight}px`; + + this._register(this._editor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged) { + this._layoutInfo.editorHeight = e.contentHeight; + this.layout({ editorHeight: true }); + } + })); + + this._initializeSourceDiffEditor(); + } + + private async _initializeSourceDiffEditor() { + const originalCell = this.cell.original!; + const modifiedCell = this.cell.modified!; + + const originalRef = await originalCell.resolveTextModelRef(); + const modifiedRef = await modifiedCell.resolveTextModelRef(); + const textModel = originalRef.object.textEditorModel; + const modifiedTextModel = modifiedRef.object.textEditorModel; + this._register(originalRef); + this._register(modifiedRef); + + this._editor!.setModel({ + original: textModel, + modified: modifiedTextModel + }); + + const contentHeight = this._editor!.getContentHeight(); + this._layoutInfo.editorHeight = contentHeight; + this.layout({ editorHeight: true }); + + } + + onDidLayoutChange(e: CellDiffViewModelLayoutChangeEvent) { + if (e.outerWidth !== undefined) { + this.layout({ outerWidth: true }); + } + } + + layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataEditor?: boolean, outputEditor?: boolean }) { + if (state.editorHeight || state.outerWidth) { + this._editorContainer.style.height = `${this._layoutInfo.editorHeight}px`; + this._editor!.layout(); + } + + if (state.metadataEditor || state.outerWidth) { + if (this._metadataEditorContainer) { + this._metadataEditorContainer.style.height = `${this._layoutInfo.metadataHeight}px`; + this._metadataEditor?.layout(); + } + } + + if (state.outputEditor || state.outerWidth) { + if (this._outputEditorContainer) { + this._outputEditorContainer.style.height = `${this._layoutInfo.outputHeight}px`; + this._outputEditor?.layout(); + } + } + + this.layoutNotebookCell(); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel.ts new file mode 100644 index 0000000000..e880450f78 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; +import { NotebookDiffEditorEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { CellDiffViewModelLayoutChangeEvent, DIFF_CELL_MARGIN } from 'vs/workbench/contrib/notebook/browser/diff/common'; +import { NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; + +export enum PropertyFoldingState { + Expanded, + Collapsed +} + +export class CellDiffViewModel extends Disposable { + public metadataFoldingState: PropertyFoldingState; + public outputFoldingState: PropertyFoldingState; + private _layoutInfoEmitter = new Emitter(); + + onDidLayoutChange = this._layoutInfoEmitter.event; + + constructor( + readonly original: NotebookCellTextModel | undefined, + readonly modified: NotebookCellTextModel | undefined, + readonly type: 'unchanged' | 'insert' | 'delete' | 'modified', + readonly editorEventDispatcher: NotebookDiffEditorEventDispatcher + ) { + super(); + this.metadataFoldingState = PropertyFoldingState.Collapsed; + this.outputFoldingState = PropertyFoldingState.Collapsed; + + this._register(this.editorEventDispatcher.onDidChangeLayout(e => { + this._layoutInfoEmitter.fire({ outerWidth: e.value.width }); + })); + } + + getComputedCellContainerWidth(layoutInfo: NotebookLayoutInfo, diffEditor: boolean, fullWidth: boolean) { + if (fullWidth) { + return layoutInfo.width - 2 * DIFF_CELL_MARGIN + (diffEditor ? DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH : 0) - 2; + } + + return (layoutInfo.width - 2 * DIFF_CELL_MARGIN + (diffEditor ? DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH : 0)) / 2 - 18 - 2; + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/common.ts b/src/vs/workbench/contrib/notebook/browser/diff/common.ts new file mode 100644 index 0000000000..b6dc4dab7a --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/common.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellDiffViewModel } from 'vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel'; +import { Event } from 'vs/base/common/event'; +import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; + +export interface INotebookTextDiffEditor { + readonly textModel?: NotebookTextModel; + onMouseUp: Event<{ readonly event: MouseEvent; readonly target: CellDiffViewModel; }>; + getOverflowContainerDomNode(): HTMLElement; + getLayoutInfo(): NotebookLayoutInfo; + layoutNotebookCell(cell: CellDiffViewModel, height: number): void; +} + +export interface CellDiffRenderTemplate { + readonly container: HTMLElement; + readonly elementDisposables: DisposableStore; +} + +export interface CellDiffViewModelLayoutChangeEvent { + font?: BareFontInfo; + outerWidth?: number; +} + +export const DIFF_CELL_MARGIN = 16; diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiff.css b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiff.css new file mode 100644 index 0000000000..5963ac3479 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiff.css @@ -0,0 +1,113 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* .notebook-diff-editor { + display: flex; + flex-direction: row; + height: 100%; + width: 100%; +} +.notebook-diff-editor-modified, +.notebook-diff-editor-original { + display: flex; + height: 100%; + width: 50%; +} */ + +.notebook-text-diff-editor .cell-body { + display: flex; + flex-direction: row; +} + +.notebook-text-diff-editor .cell-body.right { + flex-direction: row-reverse; +} + +.notebook-text-diff-editor .cell-body .diagonal-fill { + display: none; + width: 50%; +} + +.notebook-text-diff-editor .cell-body .cell-diff-editor-container { + width: 100%; + overflow: hidden; +} + +.notebook-text-diff-editor .cell-body .cell-diff-editor-container .metadata-editor-container.diff, +.notebook-text-diff-editor .cell-body .cell-diff-editor-container .output-editor-container.diff, +.notebook-text-diff-editor .cell-body .cell-diff-editor-container .editor-container.diff { + /** 100% + diffOverviewWidth */ + width: calc(100% + 30px); +} + +.notebook-text-diff-editor .cell-body .cell-diff-editor-container .metadata-editor-container .monaco-diff-editor .diffOverview, +.notebook-text-diff-editor .cell-body .cell-diff-editor-container .editor-container.diff .monaco-diff-editor .diffOverview { + display: none; +} + +.notebook-text-diff-editor .cell-body .cell-diff-editor-container .metadata-editor-container, +.notebook-text-diff-editor .cell-body .cell-diff-editor-container .editor-container { + box-sizing: border-box; +} + +.notebook-text-diff-editor .cell-body.left .cell-diff-editor-container, +.notebook-text-diff-editor .cell-body.right .cell-diff-editor-container { + display: inline-block; + width: 50%; +} + +.notebook-text-diff-editor .cell-body.left .diagonal-fill, +.notebook-text-diff-editor .cell-body.right .diagonal-fill { + display: inline-block; + width: 50%; +} + +.notebook-text-diff-editor .cell-diff-editor-container .output-header-container, +.notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container { + display: flex; + height: 24px; + align-items: center; + cursor: default; +} + +.notebook-text-diff-editor .cell-diff-editor-container .output-header-container .property-folding-indicator .codicon, +.notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container .property-folding-indicator .codicon { + visibility: visible; + padding: 4px 0 0 10px; + cursor: pointer; +} + +.notebook-text-diff-editor .cell-diff-editor-container .output-header-container, +.notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container { + display: flex; + flex-direction: row; + align-items: center; +} + +.notebook-text-diff-editor .cell-diff-editor-container .output-header-container .property-toolbar, +.notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container .property-toolbar { + margin-left: auto; +} + +.notebook-text-diff-editor .cell-diff-editor-container .output-header-container .property-status, +.notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container .property-status { + font-size: 12px; +} + +.notebook-text-diff-editor .cell-diff-editor-container .output-header-container .property-status span, +.notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container .property-status span { + margin: 0 8px; + line-height: 21px; +} + +.notebook-text-diff-editor { + overflow: hidden; +} + +.monaco-workbench .notebook-text-diff-editor > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row, +.monaco-workbench .notebook-text-diff-editor > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover, +.monaco-workbench .notebook-text-diff-editor > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { + outline: none !important; +} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts new file mode 100644 index 0000000000..384ec7686b --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts @@ -0,0 +1,111 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { viewColumnToEditorGroup } from 'vs/workbench/api/common/shared/editor'; +import { ActiveEditorContext } from 'vs/workbench/common/editor'; +import { CellDiffViewModel } from 'vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel'; +import { NotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor'; +import { NotebookDiffEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookDiffEditorInput'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; + +// ActiveEditorContext.isEqualTo(SearchEditorConstants.SearchEditorID) + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'notebook.diff.switchToText', + icon: { id: 'codicon/file-code' }, + title: { value: localize('notebook.diff.switchToText', "Open Text Diff Editor"), original: 'Open Text Diff Editor' }, + precondition: ActiveEditorContext.isEqualTo(NotebookTextDiffEditor.ID), + menu: [{ + id: MenuId.EditorTitle, + group: 'navigation', + when: ActiveEditorContext.isEqualTo(NotebookTextDiffEditor.ID) + }] + }); + } + + async run(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + const editorGroupService = accessor.get(IEditorGroupsService); + + const activeEditor = editorService.activeEditorPane; + if (activeEditor && activeEditor instanceof NotebookTextDiffEditor) { + const leftResource = (activeEditor.input as NotebookDiffEditorInput).originalResource; + const rightResource = (activeEditor.input as NotebookDiffEditorInput).resource; + const options = { + preserveFocus: false + }; + + const label = localize('diffLeftRightLabel', "{0} ⟷ {1}", leftResource.toString(true), rightResource.toString(true)); + + await editorService.openEditor({ leftResource, rightResource, label, options }, viewColumnToEditorGroup(editorGroupService, undefined)); + } + } +}); + +registerAction2(class extends Action2 { + constructor() { + super( + { + id: 'notebook.diff.cell.revertMetadata', + title: localize('notebook.diff.cell.revertMetadata', "Revert Metadata"), + icon: { id: 'codicon/discard' }, + f1: false, + menu: { + id: MenuId.NotebookDiffCellMetadataTitle + } + } + ); + } + run(accessor: ServicesAccessor, context?: { cell: CellDiffViewModel }) { + if (!context) { + return; + } + + const original = context.cell.original; + const modified = context.cell.modified; + + if (!original || !modified) { + return; + } + + modified.metadata = original.metadata; + } +}); + +registerAction2(class extends Action2 { + constructor() { + super( + { + id: 'notebook.diff.cell.revertOutputs', + title: localize('notebook.diff.cell.revertOutputs', "Revert Outputs"), + icon: { id: 'codicon/discard' }, + f1: false, + menu: { + id: MenuId.NotebookDiffCellOutputsTitle + } + } + ); + } + run(accessor: ServicesAccessor, context?: { cell: CellDiffViewModel }) { + if (!context) { + return; + } + + const original = context.cell.original; + const modified = context.cell.modified; + + if (!original || !modified) { + return; + } + + modified.spliceNotebookCellOutputs([[0, modified.outputs.length, original.outputs]]); + } +}); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts new file mode 100644 index 0000000000..964ab17c56 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts @@ -0,0 +1,478 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import * as DOM from 'vs/base/browser/dom'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { notebookCellBorder, NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { NotebookDiffEditorInput } from '../notebookDiffEditorInput'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { WorkbenchList } from 'vs/platform/list/browser/listService'; +import { CellDiffViewModel } from 'vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { CellDiffRenderer, NotebookCellTextDiffListDelegate, NotebookTextDiffList } from 'vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList'; +import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { diffDiagonalFill, diffInserted, diffRemoved, editorBackground, focusBorder, foreground } from 'vs/platform/theme/common/colorRegistry'; +import { INotebookEditorWorkerService } from 'vs/workbench/contrib/notebook/common/services/notebookWorkerService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; +import { getZoomLevel } from 'vs/base/browser/browser'; +import { NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { DIFF_CELL_MARGIN, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/common'; +import { Emitter } from 'vs/base/common/event'; +import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { NotebookDiffEditorEventDispatcher, NotebookLayoutChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; +import { INotebookDiffEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { IFileService } from 'vs/platform/files/common/files'; +import { URI } from 'vs/base/common/uri'; +import { Schemas } from 'vs/base/common/network'; + +export const IN_NOTEBOOK_TEXT_DIFF_EDITOR = new RawContextKey('isInNotebookTextDiffEditor', false); + +export class NotebookTextDiffEditor extends EditorPane implements INotebookTextDiffEditor { + static readonly ID: string = 'workbench.editor.notebookTextDiffEditor'; + + private _rootElement!: HTMLElement; + private _overflowContainer!: HTMLElement; + private _dimension: DOM.Dimension | null = null; + private _list!: WorkbenchList; + private _fontInfo: BareFontInfo | undefined; + + private readonly _onMouseUp = this._register(new Emitter<{ readonly event: MouseEvent; readonly target: CellDiffViewModel; }>()); + public readonly onMouseUp = this._onMouseUp.event; + private _eventDispatcher: NotebookDiffEditorEventDispatcher | undefined; + protected _scopeContextKeyService!: IContextKeyService; + private _model: INotebookDiffEditorModel | null = null; + private _modifiedResourceDisposableStore = new DisposableStore(); + + get textModel() { + return this._model?.modified.notebook; + } + + constructor( + @IInstantiationService readonly instantiationService: IInstantiationService, + @IThemeService readonly themeService: IThemeService, + @IContextKeyService readonly contextKeyService: IContextKeyService, + @INotebookEditorWorkerService readonly notebookEditorWorkerService: INotebookEditorWorkerService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IFileService private readonly _fileService: FileService, + + @ITelemetryService telemetryService: ITelemetryService, + @IStorageService storageService: IStorageService, + ) { + super(NotebookTextDiffEditor.ID, telemetryService, themeService, storageService); + const editorOptions = this.configurationService.getValue('editor'); + this._fontInfo = BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel()); + + this._register(this._modifiedResourceDisposableStore); + } + + protected createEditor(parent: HTMLElement): void { + this._rootElement = DOM.append(parent, DOM.$('.notebook-text-diff-editor')); + this._overflowContainer = document.createElement('div'); + DOM.addClass(this._overflowContainer, 'notebook-overflow-widget-container'); + DOM.addClass(this._overflowContainer, 'monaco-editor'); + DOM.append(parent, this._overflowContainer); + + const renderer = this.instantiationService.createInstance(CellDiffRenderer, this); + + this._list = this.instantiationService.createInstance( + NotebookTextDiffList, + 'NotebookTextDiff', + this._rootElement, + this.instantiationService.createInstance(NotebookCellTextDiffListDelegate), + [ + renderer + ], + this.contextKeyService, + { + setRowLineHeight: false, + setRowHeight: false, + supportDynamicHeights: true, + horizontalScrolling: false, + keyboardSupport: false, + mouseSupport: true, + multipleSelectionSupport: false, + enableKeyboardNavigation: true, + additionalScrollHeight: 0, + // transformOptimization: (isMacintosh && isNative) || getTitleBarStyle(this.configurationService, this.environmentService) === 'native', + styleController: (_suffix: string) => { return this._list!; }, + overrideStyles: { + listBackground: editorBackground, + listActiveSelectionBackground: editorBackground, + listActiveSelectionForeground: foreground, + listFocusAndSelectionBackground: editorBackground, + listFocusAndSelectionForeground: foreground, + listFocusBackground: editorBackground, + listFocusForeground: foreground, + listHoverForeground: foreground, + listHoverBackground: editorBackground, + listHoverOutline: focusBorder, + listFocusOutline: focusBorder, + listInactiveSelectionBackground: editorBackground, + listInactiveSelectionForeground: foreground, + listInactiveFocusBackground: editorBackground, + listInactiveFocusOutline: editorBackground, + }, + accessibilityProvider: { + getAriaLabel() { return null; }, + getWidgetAriaLabel() { + return nls.localize('notebookTreeAriaLabel', "Notebook Text Diff"); + } + }, + // focusNextPreviousDelegate: { + // onFocusNext: (applyFocusNext: () => void) => this._updateForCursorNavigationMode(applyFocusNext), + // onFocusPrevious: (applyFocusPrevious: () => void) => this._updateForCursorNavigationMode(applyFocusPrevious), + // } + } + ); + + this._register(this._list.onMouseUp(e => { + if (e.element) { + this._onMouseUp.fire({ event: e.browserEvent, target: e.element }); + } + })); + } + + async setInput(input: NotebookDiffEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + await super.setInput(input, options, context, token); + + this._model = await input.resolve(); + if (this._model === null) { + return; + } + + this._modifiedResourceDisposableStore.add(this._fileService.watch(this._model.modified.resource)); + this._modifiedResourceDisposableStore.add(this._fileService.onDidFilesChange(async e => { + if (this._model === null) { + return; + } + + if (e.contains(this._model!.modified.resource)) { + if (this._model.modified.isDirty()) { + return; + } + + const modified = this._model.modified; + const lastResolvedFileStat = modified.lastResolvedFileStat; + const currFileStat = await this._resolveStats(modified.resource); + + if (lastResolvedFileStat && currFileStat && currFileStat.mtime > lastResolvedFileStat.mtime) { + await this._model.resolveModifiedFromDisk(); + await this.updateLayout(); + return; + } + } + + if (e.contains(this._model!.original.resource)) { + if (this._model.original.isDirty()) { + return; + } + + const original = this._model.original; + const lastResolvedFileStat = original.lastResolvedFileStat; + const currFileStat = await this._resolveStats(original.resource); + + if (lastResolvedFileStat && currFileStat && currFileStat.mtime > lastResolvedFileStat.mtime) { + await this._model.resolveOriginalFromDisk(); + await this.updateLayout(); + return; + } + } + })); + + + this._eventDispatcher = new NotebookDiffEditorEventDispatcher(); + await this.updateLayout(); + } + + private async _resolveStats(resource: URI) { + if (resource.scheme === Schemas.untitled) { + return undefined; + } + + try { + const newStats = await this._fileService.resolve(resource, { resolveMetadata: true }); + return newStats; + } catch (e) { + return undefined; + } + } + + async updateLayout() { + console.log('update layout'); + if (!this._model) { + return; + } + + const diffResult = await this.notebookEditorWorkerService.computeDiff(this._model.original.resource, this._model.modified.resource); + const cellChanges = diffResult.cellsDiff.changes; + + const cellDiffViewModels: CellDiffViewModel[] = []; + const originalModel = this._model.original.notebook; + const modifiedModel = this._model.modified.notebook; + let originalCellIndex = 0; + let modifiedCellIndex = 0; + + for (let i = 0; i < cellChanges.length; i++) { + const change = cellChanges[i]; + // common cells + + for (let j = 0; j < change.originalStart - originalCellIndex; j++) { + const originalCell = originalModel.cells[originalCellIndex + j]; + const modifiedCell = modifiedModel.cells[modifiedCellIndex + j]; + if (originalCell.getHashValue() === modifiedCell.getHashValue()) { + cellDiffViewModels.push(new CellDiffViewModel( + originalCell, + modifiedCell, + 'unchanged', + this._eventDispatcher! + )); + } else { + cellDiffViewModels.push(new CellDiffViewModel( + originalCell, + modifiedCell, + 'modified', + this._eventDispatcher! + )); + } + } + + // modified cells + const modifiedLen = Math.min(change.originalLength, change.modifiedLength); + + for (let j = 0; j < modifiedLen; j++) { + cellDiffViewModels.push(new CellDiffViewModel( + originalModel.cells[change.originalStart + j], + modifiedModel.cells[change.modifiedStart + j], + 'modified', + this._eventDispatcher! + )); + } + + for (let j = modifiedLen; j < change.originalLength; j++) { + // deletion + cellDiffViewModels.push(new CellDiffViewModel( + originalModel.cells[change.originalStart + j], + undefined, + 'delete', + this._eventDispatcher! + )); + } + + for (let j = modifiedLen; j < change.modifiedLength; j++) { + // insertion + cellDiffViewModels.push(new CellDiffViewModel( + undefined, + modifiedModel.cells[change.modifiedStart + j], + 'insert', + this._eventDispatcher! + )); + } + + originalCellIndex = change.originalStart + change.originalLength; + modifiedCellIndex = change.modifiedStart + change.modifiedLength; + } + + for (let i = originalCellIndex; i < originalModel.cells.length; i++) { + cellDiffViewModels.push(new CellDiffViewModel( + originalModel.cells[i], + modifiedModel.cells[i - originalCellIndex + modifiedCellIndex], + 'unchanged', + this._eventDispatcher! + )); + } + + this._list.splice(0, this._list.length, cellDiffViewModels); + } + + private pendingLayouts = new WeakMap(); + + + layoutNotebookCell(cell: CellDiffViewModel, height: number) { + const relayout = (cell: CellDiffViewModel, height: number) => { + const viewIndex = this._list!.indexOf(cell); + + this._list?.updateElementHeight(viewIndex, height); + }; + + if (this.pendingLayouts.has(cell)) { + this.pendingLayouts.get(cell)!.dispose(); + } + + let r: () => void; + const layoutDisposable = DOM.scheduleAtNextAnimationFrame(() => { + this.pendingLayouts.delete(cell); + + relayout(cell, height); + r(); + }); + + this.pendingLayouts.set(cell, toDisposable(() => { + layoutDisposable.dispose(); + r(); + })); + + return new Promise(resolve => { r = resolve; }); + } + + getDomNode() { + return this._rootElement; + } + + getOverflowContainerDomNode(): HTMLElement { + return this._overflowContainer; + } + + getControl(): NotebookEditorWidget | undefined { + return undefined; + } + + setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { + super.setEditorVisible(visible, group); + } + + focus() { + super.focus(); + } + + clearInput(): void { + super.clearInput(); + + this._modifiedResourceDisposableStore.clear(); + this._list?.splice(0, this._list?.length || 0); + } + + getLayoutInfo(): NotebookLayoutInfo { + if (!this._list) { + throw new Error('Editor is not initalized successfully'); + } + + return { + width: this._dimension!.width, + height: this._dimension!.height, + fontInfo: this._fontInfo! + }; + } + + layout(dimension: DOM.Dimension): void { + this._rootElement.classList.toggle('mid-width', dimension.width < 1000 && dimension.width >= 600); + this._rootElement.classList.toggle('narrow-width', dimension.width < 600); + this._dimension = dimension; + this._rootElement.style.height = `${dimension.height}px`; + + this._list?.layout(this._dimension.height, this._dimension.width); + this._eventDispatcher?.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); + } +} + +registerThemingParticipant((theme, collector) => { + const cellBorderColor = theme.getColor(notebookCellBorder); + if (cellBorderColor) { + collector.addRule(`.notebook-text-diff-editor .cell-body { border: 1px solid ${cellBorderColor};}`); + collector.addRule(`.notebook-text-diff-editor .cell-diff-editor-container .output-header-container, + .notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container { + border-top: 1px solid ${cellBorderColor}; + }`); + } + + const diffDiagonalFillColor = theme.getColor(diffDiagonalFill); + collector.addRule(` + .notebook-text-diff-editor .diagonal-fill { + background-image: linear-gradient( + -45deg, + ${diffDiagonalFillColor} 12.5%, + #0000 12.5%, #0000 50%, + ${diffDiagonalFillColor} 50%, ${diffDiagonalFillColor} 62.5%, + #0000 62.5%, #0000 100% + ); + background-size: 8px 8px; + } + `); + + const added = theme.getColor(diffInserted); + if (added) { + collector.addRule(` + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .source-container { background-color: ${added}; } + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .source-container .monaco-editor .margin, + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .source-container .monaco-editor .monaco-editor-background { + background-color: ${added}; + } + ` + ); + collector.addRule(` + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .metadata-editor-container { background-color: ${added}; } + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .metadata-editor-container .monaco-editor .margin, + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .metadata-editor-container .monaco-editor .monaco-editor-background { + background-color: ${added}; + } + ` + ); + collector.addRule(` + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .output-editor-container { background-color: ${added}; } + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .output-editor-container .monaco-editor .margin, + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .output-editor-container .monaco-editor .monaco-editor-background { + background-color: ${added}; + } + ` + ); + collector.addRule(` + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .metadata-header-container { background-color: ${added}; } + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .output-header-container { background-color: ${added}; } + ` + ); + } + const removed = theme.getColor(diffRemoved); + if (added) { + collector.addRule(` + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .source-container { background-color: ${removed}; } + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .source-container .monaco-editor .margin, + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .source-container .monaco-editor .monaco-editor-background { + background-color: ${removed}; + } + ` + ); + collector.addRule(` + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .metadata-editor-container { background-color: ${removed}; } + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .metadata-editor-container .monaco-editor .margin, + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .metadata-editor-container .monaco-editor .monaco-editor-background { + background-color: ${removed}; + } + ` + ); + collector.addRule(` + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .output-editor-container { background-color: ${removed}; } + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .output-editor-container .monaco-editor .margin, + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .output-editor-container .monaco-editor .monaco-editor-background { + background-color: ${removed}; + } + ` + ); + collector.addRule(` + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .metadata-header-container { background-color: ${removed}; } + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .output-header-container { background-color: ${removed}; } + ` + ); + } + + // const changed = theme.getColor(editorGutterModifiedBackground); + + // if (changed) { + // collector.addRule(` + // .notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container.modified { + // background-color: ${changed}; + // } + // `); + // } + + collector.addRule(`.notebook-text-diff-editor .cell-body { margin: ${DIFF_CELL_MARGIN}px; }`); +}); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts new file mode 100644 index 0000000000..9ab2785a1f --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts @@ -0,0 +1,228 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./notebookDiff'; +import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import * as DOM from 'vs/base/browser/dom'; +import { IListStyles, IStyleController } from 'vs/base/browser/ui/list/listWidget'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IListService, IWorkbenchListOptions, WorkbenchList } from 'vs/platform/list/browser/listService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { CellDiffViewModel } from 'vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel'; +import { CellDiffRenderTemplate, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/common'; +import { isMacintosh } from 'vs/base/common/platform'; +import { DeletedCell, InsertCell, ModifiedCell } from 'vs/workbench/contrib/notebook/browser/diff/cellComponents'; + +export class NotebookCellTextDiffListDelegate implements IListVirtualDelegate { + // private readonly lineHeight: number; + + constructor( + @IConfigurationService readonly configurationService: IConfigurationService + ) { + // const editorOptions = this.configurationService.getValue('editor'); + // this.lineHeight = BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel()).lineHeight; + } + + getHeight(element: CellDiffViewModel): number { + return 100; + } + + hasDynamicHeight(element: CellDiffViewModel): boolean { + return false; + } + + getTemplateId(element: CellDiffViewModel): string { + return CellDiffRenderer.TEMPLATE_ID; + } +} +export class CellDiffRenderer implements IListRenderer { + static readonly TEMPLATE_ID = 'cell_diff'; + + constructor( + readonly notebookEditor: INotebookTextDiffEditor, + @IInstantiationService protected readonly instantiationService: IInstantiationService + ) { } + + get templateId() { + return CellDiffRenderer.TEMPLATE_ID; + } + + renderTemplate(container: HTMLElement): CellDiffRenderTemplate { + return { + container, + elementDisposables: new DisposableStore() + }; + } + + renderElement(element: CellDiffViewModel, index: number, templateData: CellDiffRenderTemplate, height: number | undefined): void { + templateData.container.innerText = ''; + switch (element.type) { + case 'unchanged': + templateData.elementDisposables.add(this.instantiationService.createInstance(ModifiedCell, this.notebookEditor, element, templateData)); + return; + case 'delete': + templateData.elementDisposables.add(this.instantiationService.createInstance(DeletedCell, this.notebookEditor, element, templateData)); + return; + case 'insert': + templateData.elementDisposables.add(this.instantiationService.createInstance(InsertCell, this.notebookEditor, element, templateData)); + return; + case 'modified': + templateData.elementDisposables.add(this.instantiationService.createInstance(ModifiedCell, this.notebookEditor, element, templateData)); + return; + default: + break; + } + } + + disposeTemplate(templateData: CellDiffRenderTemplate): void { + templateData.container.innerText = ''; + } + + disposeElement(element: CellDiffViewModel, index: number, templateData: CellDiffRenderTemplate): void { + templateData.elementDisposables.clear(); + } +} + + +export class NotebookTextDiffList extends WorkbenchList implements IDisposable, IStyleController { + private styleElement?: HTMLStyleElement; + + constructor( + listUser: string, + container: HTMLElement, + delegate: IListVirtualDelegate, + renderers: IListRenderer[], + contextKeyService: IContextKeyService, + options: IWorkbenchListOptions, + @IListService listService: IListService, + @IThemeService themeService: IThemeService, + @IConfigurationService configurationService: IConfigurationService, + @IKeybindingService keybindingService: IKeybindingService) { + super(listUser, container, delegate, renderers, options, contextKeyService, listService, themeService, configurationService, keybindingService); + } + + style(styles: IListStyles) { + const selectorSuffix = this.view.domId; + if (!this.styleElement) { + this.styleElement = DOM.createStyleSheet(this.view.domNode); + } + const suffix = selectorSuffix && `.${selectorSuffix}`; + const content: string[] = []; + + if (styles.listBackground) { + if (styles.listBackground.isOpaque()) { + content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows { background: ${styles.listBackground}; }`); + } else if (!isMacintosh) { // subpixel AA doesn't exist in macOS + console.warn(`List with id '${selectorSuffix}' was styled with a non-opaque background color. This will break sub-pixel antialiasing.`); + } + } + + if (styles.listFocusBackground) { + content.push(`.monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { background-color: ${styles.listFocusBackground}; }`); + content.push(`.monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused:hover { background-color: ${styles.listFocusBackground}; }`); // overwrite :hover style in this case! + } + + if (styles.listFocusForeground) { + content.push(`.monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { color: ${styles.listFocusForeground}; }`); + } + + if (styles.listActiveSelectionBackground) { + content.push(`.monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected { background-color: ${styles.listActiveSelectionBackground}; }`); + content.push(`.monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected:hover { background-color: ${styles.listActiveSelectionBackground}; }`); // overwrite :hover style in this case! + } + + if (styles.listActiveSelectionForeground) { + content.push(`.monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected { color: ${styles.listActiveSelectionForeground}; }`); + } + + if (styles.listFocusAndSelectionBackground) { + content.push(` + .monaco-drag-image, + .monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected.focused { background-color: ${styles.listFocusAndSelectionBackground}; } + `); + } + + if (styles.listFocusAndSelectionForeground) { + content.push(` + .monaco-drag-image, + .monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected.focused { color: ${styles.listFocusAndSelectionForeground}; } + `); + } + + if (styles.listInactiveFocusBackground) { + content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { background-color: ${styles.listInactiveFocusBackground}; }`); + content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused:hover { background-color: ${styles.listInactiveFocusBackground}; }`); // overwrite :hover style in this case! + } + + if (styles.listInactiveSelectionBackground) { + content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected { background-color: ${styles.listInactiveSelectionBackground}; }`); + content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected:hover { background-color: ${styles.listInactiveSelectionBackground}; }`); // overwrite :hover style in this case! + } + + if (styles.listInactiveSelectionForeground) { + content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected { color: ${styles.listInactiveSelectionForeground}; }`); + } + + if (styles.listHoverBackground) { + content.push(`.monaco-list${suffix}:not(.drop-target) > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover:not(.selected):not(.focused) { background-color: ${styles.listHoverBackground}; }`); + } + + if (styles.listHoverForeground) { + content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover:not(.selected):not(.focused) { color: ${styles.listHoverForeground}; }`); + } + + if (styles.listSelectionOutline) { + content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected { outline: 1px dotted ${styles.listSelectionOutline}; outline-offset: -1px; }`); + } + + if (styles.listFocusOutline) { + content.push(` + .monaco-drag-image, + .monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { outline: 1px solid ${styles.listFocusOutline}; outline-offset: -1px; } + `); + } + + if (styles.listInactiveFocusOutline) { + content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { outline: 1px dotted ${styles.listInactiveFocusOutline}; outline-offset: -1px; }`); + } + + if (styles.listHoverOutline) { + content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover { outline: 1px dashed ${styles.listHoverOutline}; outline-offset: -1px; }`); + } + + if (styles.listDropBackground) { + content.push(` + .monaco-list${suffix}.drop-target, + .monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows.drop-target, + .monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-row.drop-target { background-color: ${styles.listDropBackground} !important; color: inherit !important; } + `); + } + + if (styles.listFilterWidgetBackground) { + content.push(`.monaco-list-type-filter { background-color: ${styles.listFilterWidgetBackground} }`); + } + + if (styles.listFilterWidgetOutline) { + content.push(`.monaco-list-type-filter { border: 1px solid ${styles.listFilterWidgetOutline}; }`); + } + + if (styles.listFilterWidgetNoMatchesOutline) { + content.push(`.monaco-list-type-filter.no-matches { border: 1px solid ${styles.listFilterWidgetNoMatchesOutline}; }`); + } + + if (styles.listMatchesShadow) { + content.push(`.monaco-list-type-filter { box-shadow: 1px 1px 1px ${styles.listMatchesShadow}; }`); + } + + const newStyles = content.join('\n'); + if (newStyles !== this.styleElement.innerHTML) { + this.styleElement.innerHTML = newStyles; + } + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts b/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts index 859aed2d5c..61fbf79f96 100644 --- a/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts +++ b/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts @@ -96,7 +96,7 @@ const notebookProviderContribution: IJSONSchema = { const notebookRendererContribution: IJSONSchema = { description: nls.localize('contributes.notebook.renderer', 'Contributes notebook output renderer provider.'), type: 'array', - defaultSnippets: [{ body: [{ id: '', displayName: '', mimeTypes: [''] }] }], + defaultSnippets: [{ body: [{ id: '', displayName: '', mimeTypes: [''], entrypoint: '' }] }], items: { type: 'object', required: [ diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 4730379b9a..c39a36bcc4 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -55,6 +55,12 @@ width: 100%; } +.monaco-workbench .notebookOverlay > .cell-list-container > .notebook-gutter > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row { + cursor: default; + overflow: visible !important; + width: 100%; +} + .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image { position: absolute; top: -500px; @@ -344,19 +350,46 @@ position: relative; } +.monaco-workbench .notebookOverlay.cell-statusbar-hidden .cell-statusbar-container { + display: none; +} + .monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-left { display: flex; flex-grow: 1; } +.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-left, .monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-right { padding-right: 12px; + display: flex; z-index: 26; } -.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-language-picker { +.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-right .cell-contributed-items { + justify-content: flex-end; +} + +.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-contributed-items { + display: flex; + flex-wrap: wrap; + overflow: hidden; +} + +.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-item { + display: flex; + align-items: center; + white-space: pre; + height: 21px; /* Editor outline is -1px in, don't overlap */ padding: 0px 6px; +} + +.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-item.cell-status-item-has-command { + cursor: pointer; +} + +.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-language-picker { cursor: pointer; } @@ -370,6 +403,10 @@ align-items: center; } +.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-message { + margin-right: 6px; +} + .monaco-workbench .notebookOverlay .cell-statusbar-container .cell-run-status { height: 100%; display: flex; @@ -391,34 +428,35 @@ bottom: 0px; top: 0px; } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .run-button-container { position: relative; height: 16px; flex-shrink: 0; top: 9px; - z-index: 27; /* Above the drag handle */ + z-index: 27; + /* Above the drag handle */ } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container .monaco-toolbar { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .run-button-container .monaco-toolbar { visibility: hidden; } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container .monaco-toolbar .codicon { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .run-button-container .monaco-toolbar .codicon { margin: 0; padding-right: 4px; } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container .monaco-toolbar .actions-container { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .run-button-container .monaco-toolbar .actions-container { justify-content: center; } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .cell.runnable .run-button-container .monaco-toolbar, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused .cell.runnable .run-button-container .monaco-toolbar, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover .cell.runnable .run-button-container .monaco-toolbar { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .runnable .run-button-container .monaco-toolbar, +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused .runnable .run-button-container .monaco-toolbar, +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover .runnable .run-button-container .monaco-toolbar { visibility: visible; } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .execution-count-label { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .execution-count-label { position: absolute; font-size: 10px; font-family: var(--monaco-monospace-font); diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index a0f02c2a2a..b72d06aaca 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -30,7 +30,7 @@ import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEd import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { NotebookService } from 'vs/workbench/contrib/notebook/browser/notebookServiceImpl'; -import { CellKind, CellToolbarLocKey, CellUri, DisplayOrderKey, getCellUndoRedoComparisonKey, NotebookDocumentBackupData, NotebookEditorPriority } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, CellToolbarLocKey, CellUri, DisplayOrderKey, getCellUndoRedoComparisonKey, NotebookDocumentBackupData, NotebookEditorPriority, NotebookTextDiffEditorPreview, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService, IOpenEditorOverride } from 'vs/workbench/services/editor/common/editorService'; @@ -40,6 +40,19 @@ import { CustomEditorInfo } from 'vs/workbench/contrib/customEditor/common/custo import { INotebookEditor, NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { INotebookEditorModelResolverService, NotebookModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; +import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; +import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; +import { NotebookDiffEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookDiffEditorInput'; +import { NotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor'; +import { INotebookEditorWorkerService } from 'vs/workbench/contrib/notebook/common/services/notebookWorkerService'; +import { NotebookEditorWorkerServiceImpl } from 'vs/workbench/contrib/notebook/common/services/notebookWorkerServiceImpl'; +import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; +import { NotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/browser/notebookCellStatusBarServiceImpl'; +import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; +import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { Event } from 'vs/base/common/event'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; // Editor Contribution @@ -50,13 +63,16 @@ import 'vs/workbench/contrib/notebook/browser/contrib/format/formatting'; import 'vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider'; import 'vs/workbench/contrib/notebook/browser/contrib/marker/markerProvider'; import 'vs/workbench/contrib/notebook/browser/contrib/status/editorStatus'; +// import 'vs/workbench/contrib/notebook/browser/contrib/scm/scm'; + +// Diff Editor Contribution +import 'vs/workbench/contrib/notebook/browser/diff/notebookDiffActions'; // Output renderers registration import 'vs/workbench/contrib/notebook/browser/view/output/transforms/streamTransform'; import 'vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform'; import 'vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform'; -import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; /*--------------------------------------------------------------------------------------------- */ @@ -71,6 +87,53 @@ Registry.as(EditorExtensions.Editors).registerEditor( ] ); +Registry.as(EditorExtensions.Editors).registerEditor( + EditorDescriptor.create( + NotebookTextDiffEditor, + NotebookTextDiffEditor.ID, + 'Notebook Diff Editor' + ), + [ + new SyncDescriptor(NotebookDiffEditorInput) + ] +); + +class NotebookDiffEditorFactory implements IEditorInputFactory { + canSerialize(): boolean { + return true; + } + + serialize(input: EditorInput): string { + assertType(input instanceof NotebookDiffEditorInput); + return JSON.stringify({ + resource: input.resource, + originalResource: input.originalResource, + name: input.name, + originalName: input.originalName, + viewType: input.viewType, + }); + } + + deserialize(instantiationService: IInstantiationService, raw: string) { + type Data = { resource: URI, originalResource: URI, name: string, originalName: string, viewType: string, group: number }; + const data = parse(raw); + if (!data) { + return undefined; + } + const { resource, originalResource, name, originalName, viewType } = data; + if (!data || !URI.isUri(resource) || !URI.isUri(originalResource) || typeof name !== 'string' || typeof originalName !== 'string' || typeof viewType !== 'string') { + return undefined; + } + + const input = NotebookDiffEditorInput.create(instantiationService, resource, name, originalResource, originalName, viewType); + return input; + } + + static canResolveBackup(editorInput: IEditorInput, backupResource: URI): boolean { + return false; + } + +} class NotebookEditorFactory implements IEditorInputFactory { canSerialize(): boolean { return true; @@ -133,6 +196,11 @@ Registry.as(EditorInputExtensions.EditorInputFactor NotebookEditorFactory ); +Registry.as(EditorInputExtensions.EditorInputFactories).registerEditorInputFactory( + NotebookDiffEditorInput.ID, + NotebookDiffEditorFactory +); + function getFirstNotebookInfo(notebookService: INotebookService, uri: URI): NotebookProviderInfo | undefined { return notebookService.getContributedNotebookProviders(uri)[0]; } @@ -144,6 +212,7 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri @INotebookService private readonly notebookService: INotebookService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IConfigurationService private readonly configurationService: IConfigurationService, + @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, @IUndoRedoService undoRedoService: IUndoRedoService, ) { super(); @@ -225,6 +294,10 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri return undefined; } + if (originalInput instanceof DiffEditorInput && this.configurationService.getValue(NotebookTextDiffEditorPreview) && !this._accessibilityService.isScreenReaderOptimized()) { + return this._handleDiffEditorInput(originalInput, options, group); + } + if (!originalInput.resource) { return undefined; } @@ -300,6 +373,49 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri const notebookOptions = new NotebookEditorOptions({ ...options, cellOptions, override: false, index }); return { override: this.editorService.openEditor(notebookInput, notebookOptions, group) }; } + + private _handleDiffEditorInput(diffEditorInput: DiffEditorInput, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup): IOpenEditorOverride | undefined { + const modifiedInput = diffEditorInput.modifiedInput; + const originalInput = diffEditorInput.originalInput; + const notebookUri = modifiedInput.resource; + const originalNotebookUri = originalInput.resource; + + if (!notebookUri || !originalNotebookUri) { + return undefined; + } + + const existingEditors = group.editors.filter(editor => editor.resource && isEqual(editor.resource, notebookUri) && !(editor instanceof NotebookEditorInput)); + + if (existingEditors.length) { + return { override: this.editorService.openEditor(existingEditors[0]) }; + } + + const userAssociatedEditors = this.getUserAssociatedEditors(notebookUri); + const notebookEditor = userAssociatedEditors.filter(association => this.notebookService.getContributedNotebookProvider(association.viewType)); + + if (userAssociatedEditors.length && !notebookEditor.length) { + // user pick a non-notebook editor for this resource + return undefined; + } + + // user might pick a notebook editor + + const associatedEditors = distinct([ + ...this.getUserAssociatedNotebookEditors(notebookUri), + ...(this.getContributedEditors(notebookUri).filter(editor => editor.priority === NotebookEditorPriority.default)) + ], editor => editor.id); + + if (!associatedEditors.length) { + // there is no notebook editor contribution which is enabled by default + return undefined; + } + + const info = associatedEditors[0]; + + const notebookInput = NotebookDiffEditorInput.create(this.instantiationService, notebookUri, modifiedInput.getName(), originalNotebookUri, originalInput.getName(), info.id); + const notebookOptions = new NotebookEditorOptions({ ...options, override: false }); + return { override: this.editorService.openEditor(notebookInput, notebookOptions, group) }; + } } class CellContentProvider implements ITextModelContentProvider { @@ -371,12 +487,120 @@ class CellContentProvider implements ITextModelContentProvider { } } +class RegisterSchemasContribution extends Disposable implements IWorkbenchContribution { + constructor() { + super(); + this.registerMetadataSchemas(); + } + + private registerMetadataSchemas(): void { + const jsonRegistry = Registry.as(JSONExtensions.JSONContribution); + const metadataSchema: IJSONSchema = { + properties: { + ['language']: { + type: 'string', + description: 'The language for the cell' + }, + ['editable']: { + type: 'boolean', + description: `Controls whether a cell's editor is editable/readonly` + }, + ['runnable']: { + type: 'boolean', + description: 'Controls if the cell is executable' + }, + ['breakpointMargin']: { + type: 'boolean', + description: 'Controls if the cell has a margin to support the breakpoint UI' + }, + ['hasExecutionOrder']: { + type: 'boolean', + description: 'Whether the execution order indicator will be displayed' + }, + ['executionOrder']: { + type: 'number', + description: 'The order in which this cell was executed' + }, + ['statusMessage']: { + type: 'string', + description: `A status message to be shown in the cell's status bar` + }, + ['runState']: { + type: 'integer', + description: `The cell's current run state` + }, + ['runStartTime']: { + type: 'number', + description: 'If the cell is running, the time at which the cell started running' + }, + ['lastRunDuration']: { + type: 'number', + description: `The total duration of the cell's last run` + }, + ['inputCollapsed']: { + type: 'boolean', + description: `Whether a code cell's editor is collapsed` + }, + ['outputCollapsed']: { + type: 'boolean', + description: `Whether a code cell's outputs are collapsed` + } + }, + // patternProperties: allSettings.patternProperties, + additionalProperties: true, + allowTrailingCommas: true, + allowComments: true + }; + + jsonRegistry.registerSchema('vscode://schemas/notebook/cellmetadata', metadataSchema); + } +} + +// makes sure that every dirty notebook gets an editor +class NotebookFileTracker implements IWorkbenchContribution { + + private readonly _dirtyListener: IDisposable; + + constructor( + @INotebookService private readonly _notebookService: INotebookService, + @IEditorService private readonly _editorService: IEditorService, + @IWorkingCopyService workingCopyService: IWorkingCopyService, + ) { + this._dirtyListener = Event.debounce(workingCopyService.onDidChangeDirty, () => { }, 100)(() => { + const inputs = this._createMissingNotebookEditors(); + this._editorService.openEditors(inputs); + }); + } + + dispose(): void { + this._dirtyListener.dispose(); + } + + private _createMissingNotebookEditors(): IResourceEditorInput[] { + const result: IResourceEditorInput[] = []; + + for (const notebook of this._notebookService.getNotebookTextModels()) { + if (notebook.isDirty && !this._editorService.isOpen({ resource: notebook.uri })) { + result.push({ + resource: notebook.uri, + options: { inactive: true, preserveFocus: true, pinned: true } + }); + } + } + return result; + } +} + const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchContributionsRegistry.registerWorkbenchContribution(NotebookContribution, LifecyclePhase.Starting); workbenchContributionsRegistry.registerWorkbenchContribution(CellContentProvider, LifecyclePhase.Starting); +workbenchContributionsRegistry.registerWorkbenchContribution(RegisterSchemasContribution, LifecyclePhase.Starting); +workbenchContributionsRegistry.registerWorkbenchContribution(NotebookFileTracker, LifecyclePhase.Ready); registerSingleton(INotebookService, NotebookService); +registerSingleton(INotebookEditorWorkerService, NotebookEditorWorkerServiceImpl); registerSingleton(INotebookEditorModelResolverService, NotebookModelResolverService, true); +registerSingleton(INotebookCellStatusBarService, NotebookCellStatusBarService, true); const configurationRegistry = Registry.as(Extensions.Configuration); configurationRegistry.registerConfiguration({ @@ -398,6 +622,16 @@ configurationRegistry.registerConfiguration({ type: 'string', enum: ['left', 'right', 'hidden'], default: 'right' + }, + [ShowCellStatusBarKey]: { + description: nls.localize('notebook.showCellStatusbar.description', "Whether the cell status bar should be shown."), + type: 'boolean', + default: true + }, + [NotebookTextDiffEditorPreview]: { + description: nls.localize('notebook.diff.enablePreview.description', "Whether to use the enhanced text diff editor for notebook."), + type: 'boolean', + default: true } } }); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index eed8ed8d3b..c9250a80c5 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -22,15 +22,15 @@ import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/outpu import { RunStateRenderer, TimerRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer'; import { CellViewModel, IModelDecorationsChangeAccessor, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { CellKind, IProcessedOutput, IRenderOutput, NotebookCellMetadata, NotebookDocumentMetadata, INotebookKernelInfo, IEditor, INotebookKernelInfo2, IInsetRenderOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, IProcessedOutput, IRenderOutput, NotebookCellMetadata, NotebookDocumentMetadata, IEditor, INotebookKernelInfo2, IInsetRenderOutput, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { IMenu } from 'vs/platform/actions/common/actions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { CellLanguageStatusBarItem } from 'vs/workbench/contrib/notebook/browser/view/renderers/commonViewComponents'; import { EditorOptions } from 'vs/workbench/common/editor'; import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; import { IConstructorSignature1 } from 'vs/platform/instantiation/common/instantiation'; +import { CellEditorStatusBar } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets'; export const KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED = new RawContextKey('notebookFindWidgetFocused', false); @@ -50,6 +50,7 @@ export const NOTEBOOK_VIEW_TYPE = new RawContextKey('notebookViewType', export const NOTEBOOK_CELL_TYPE = new RawContextKey('notebookCellType', undefined); // code, markdown export const NOTEBOOK_CELL_EDITABLE = new RawContextKey('notebookCellEditable', false); // bool export const NOTEBOOK_CELL_FOCUSED = new RawContextKey('notebookCellFocused', false); // bool +export const NOTEBOOK_CELL_EDITOR_FOCUSED = new RawContextKey('notebookCellEditorFocused', false); // bool export const NOTEBOOK_CELL_RUNNABLE = new RawContextKey('notebookCellRunnable', false); // bool export const NOTEBOOK_CELL_MARKDOWN_EDIT_MODE = new RawContextKey('notebookCellMarkdownEditMode', false); // bool export const NOTEBOOK_CELL_RUN_STATE = new RawContextKey('notebookCellRunState', undefined); // idle, running @@ -165,6 +166,7 @@ export interface INotebookEditorContribution { export interface INotebookCellDecorationOptions { className?: string; + gutterClassName?: string; outputClassName?: string; } @@ -195,12 +197,13 @@ export interface INotebookEditorContributionDescription { ctor: INotebookEditorContributionCtor; } -export interface INotebookEditorWidgetOptions { - - contributions?: INotebookEditorContributionDescription[]; +export interface INotebookEditorCreationOptions { + readonly isEmbedded?: boolean; + readonly contributions?: INotebookEditorContributionDescription[]; } export interface INotebookEditor extends IEditor { + isEmbedded: boolean; cursorNavigationMode: boolean; @@ -215,13 +218,14 @@ export interface INotebookEditor extends IEditor { */ readonly onDidChangeModel: Event; readonly onDidFocusEditorWidget: Event; - isNotebookEditor: boolean; - activeKernel: INotebookKernelInfo | INotebookKernelInfo2 | undefined; + readonly isNotebookEditor: boolean; + activeKernel: INotebookKernelInfo2 | undefined; multipleKernelsAvailable: boolean; readonly onDidChangeAvailableKernels: Event; readonly onDidChangeKernel: Event; readonly onDidChangeActiveCell: Event; readonly onDidScroll: Event; + readonly onWillDispose: Event; isDisposed: boolean; @@ -425,6 +429,8 @@ export interface INotebookEditor extends IEditor { setCellSelection(cell: ICellViewModel, selection: Range): void; + deltaCellDecorations(oldDecorations: string[], newDecorations: INotebookDeltaDecoration[]): string[]; + /** * Change the decorations on cells. * The notebook is virtualized and this method should be called to create/delete editor decorations safely. @@ -452,7 +458,7 @@ export interface INotebookEditor extends IEditor { } export interface INotebookCellList { - isDisposed: boolean + isDisposed: boolean; readonly contextKeyService: IContextKeyService; elementAt(position: number): ICellViewModel | undefined; elementHeight(element: ICellViewModel): number; @@ -460,6 +466,8 @@ export interface INotebookCellList { onDidScroll: Event; onDidChangeFocus: Event>; onDidChangeContentHeight: Event; + onDidChangeVisibleRanges: Event; + visibleRanges: ICellRange[]; scrollTop: number; scrollHeight: number; scrollLeft: number; @@ -512,6 +520,7 @@ export interface BaseCellRenderTemplate { contextKeyService: IContextKeyService; container: HTMLElement; cellContainer: HTMLElement; + decorationContainer: HTMLElement; toolbar: ToolBar; deleteToolbar: ToolBar; betweenCellToolbar: ToolBar; @@ -520,8 +529,7 @@ export interface BaseCellRenderTemplate { elementDisposables: DisposableStore; bottomCellContainer: HTMLElement; currentRenderedCell?: ICellViewModel; - statusBarContainer: HTMLElement; - languageStatusBarItem: CellLanguageStatusBarItem; + statusBar: CellEditorStatusBar; titleMenu: IMenu; toJSON: () => object; } @@ -534,7 +542,6 @@ export interface MarkdownCellRenderTemplate extends BaseCellRenderTemplate { export interface CodeCellRenderTemplate extends BaseCellRenderTemplate { cellRunState: RunStateRenderer; - cellStatusMessageContainer: HTMLElement; runToolbar: ToolBar; runButtonContainer: HTMLElement; executionOrderLabel: HTMLElement; @@ -563,7 +570,7 @@ export interface IOutputTransformContribution { * This call is allowed to have side effects, such as placing output * directly into the container element. */ - render(output: IProcessedOutput, container: HTMLElement, preferredMimeType: string | undefined): IRenderOutput; + render(output: IProcessedOutput, container: HTMLElement, preferredMimeType: string | undefined, notebookUri: URI | undefined): IRenderOutput; } export interface CellFindMatch { @@ -619,19 +626,20 @@ export interface CellViewModelStateChangeEvent { outputIsHoveredChanged?: boolean; } -/** - * [start, end] - */ -export interface ICellRange { - /** - * zero based index - */ - start: number; +export function cellRangesEqual(a: ICellRange[], b: ICellRange[]) { + a = reduceCellRanges(a); + b = reduceCellRanges(b); + if (a.length !== b.length) { + return false; + } - /** - * zero based index - */ - end: number; + for (let i = 0; i < a.length; i++) { + if (a[i].start !== b[i].start || a[i].end !== b[i].end) { + return false; + } + } + + return true; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookCellStatusBarServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookCellStatusBarServiceImpl.ts new file mode 100644 index 0000000000..6071393283 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebookCellStatusBarServiceImpl.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { ResourceMap } from 'vs/base/common/map'; +import { URI } from 'vs/base/common/uri'; +import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; +import { INotebookCellStatusBarEntry } from 'vs/workbench/contrib/notebook/common/notebookCommon'; + +export class NotebookCellStatusBarService extends Disposable implements INotebookCellStatusBarService { + + private _onDidChangeEntriesForCell = new Emitter(); + readonly onDidChangeEntriesForCell: Event = this._onDidChangeEntriesForCell.event; + + private _entries = new ResourceMap>(); + + private removeEntry(entry: INotebookCellStatusBarEntry) { + const existingEntries = this._entries.get(entry.cellResource); + if (existingEntries) { + existingEntries.delete(entry); + if (!existingEntries.size) { + this._entries.delete(entry.cellResource); + } + } + + this._onDidChangeEntriesForCell.fire(entry.cellResource); + } + + addEntry(entry: INotebookCellStatusBarEntry): IDisposable { + const existingEntries = this._entries.get(entry.cellResource) ?? new Set(); + existingEntries.add(entry); + this._entries.set(entry.cellResource, existingEntries); + + this._onDidChangeEntriesForCell.fire(entry.cellResource); + + return { + dispose: () => { + this.removeEntry(entry); + } + }; + } + + getEntries(cell: URI): INotebookCellStatusBarEntry[] { + const existingEntries = this._entries.get(cell); + return existingEntries ? + Array.from(existingEntries.values()) : + []; + } + + readonly _serviceBrand: undefined; +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebookDiffEditorInput.ts b/src/vs/workbench/contrib/notebook/browser/notebookDiffEditorInput.ts new file mode 100644 index 0000000000..8b423fea14 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebookDiffEditorInput.ts @@ -0,0 +1,228 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import { EditorInput, IEditorInput, GroupIdentifier, ISaveOptions, IMoveResult, IRevertOptions, EditorModel } from 'vs/workbench/common/editor'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { URI } from 'vs/base/common/uri'; +import { isEqual } from 'vs/base/common/resources'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; +import { IReference } from 'vs/base/common/lifecycle'; +import { INotebookEditorModel, INotebookDiffEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; + +interface NotebookEditorInputOptions { + startDirty?: boolean; +} + +class NotebookDiffEditorModel extends EditorModel implements INotebookDiffEditorModel { + constructor( + readonly original: NotebookEditorModel, + readonly modified: NotebookEditorModel, + ) { + super(); + } + + async load(): Promise { + await this.original.load(); + await this.modified.load(); + + return this; + } + + async resolveOriginalFromDisk() { + await this.original.load({ forceReadFromDisk: true }); + } + + async resolveModifiedFromDisk() { + await this.modified.load({ forceReadFromDisk: true }); + } + + dispose(): void { + + } + +} + +export class NotebookDiffEditorInput extends EditorInput { + static create(instantiationService: IInstantiationService, resource: URI, name: string, originalResource: URI, originalName: string, viewType: string | undefined, options: NotebookEditorInputOptions = {}) { + return instantiationService.createInstance(NotebookDiffEditorInput, resource, name, originalResource, originalName, viewType, options); + } + + static readonly ID: string = 'workbench.input.diffNotebookInput'; + + private _textModel: IReference | null = null; + private _originalTextModel: IReference | null = null; + private _defaultDirtyState: boolean = false; + + constructor( + public readonly resource: URI, + public readonly name: string, + public readonly originalResource: URI, + public readonly originalName: string, + public readonly viewType: string | undefined, + public readonly options: NotebookEditorInputOptions, + @INotebookService private readonly _notebookService: INotebookService, + @INotebookEditorModelResolverService private readonly _notebookModelResolverService: INotebookEditorModelResolverService, + @IFilesConfigurationService private readonly _filesConfigurationService: IFilesConfigurationService, + @IFileDialogService private readonly _fileDialogService: IFileDialogService, + // @IInstantiationService private readonly _instantiationService: IInstantiationService + ) { + super(); + this._defaultDirtyState = !!options.startDirty; + } + + getTypeId(): string { + return NotebookDiffEditorInput.ID; + } + + getName(): string { + return nls.localize('sideBySideLabels', "{0} ↔ {1}", this.originalName, this.name); + } + + isDirty() { + if (!this._textModel) { + return !!this._defaultDirtyState; + } + return this._textModel.object.isDirty(); + } + + isUntitled(): boolean { + return this._textModel?.object.isUntitled() || false; + } + + isReadonly() { + return false; + } + + isSaving(): boolean { + if (this.isUntitled()) { + return false; // untitled is never saving automatically + } + + if (!this.isDirty()) { + return false; // the editor needs to be dirty for being saved + } + + if (this._filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY) { + return true; // a short auto save is configured, treat this as being saved + } + + return false; + } + + async save(group: GroupIdentifier, options?: ISaveOptions): Promise { + if (this._textModel) { + + if (this.isUntitled()) { + return this.saveAs(group, options); + } else { + await this._textModel.object.save(); + } + + return this; + } + + return undefined; + } + + async saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise { + if (!this._textModel || !this.viewType) { + return undefined; + } + + const provider = this._notebookService.getContributedNotebookProvider(this.viewType!); + + if (!provider) { + return undefined; + } + + const dialogPath = this._textModel.object.resource; + const target = await this._fileDialogService.pickFileToSave(dialogPath, options?.availableFileSystems); + if (!target) { + return undefined; // save cancelled + } + + if (!provider.matches(target)) { + const patterns = provider.selector.map(pattern => { + if (pattern.excludeFileNamePattern) { + return `${pattern.filenamePattern} (exclude: ${pattern.excludeFileNamePattern})`; + } + + return pattern.filenamePattern; + }).join(', '); + throw new Error(`File name ${target} is not supported by ${provider.providerDisplayName}. + +Please make sure the file name matches following patterns: +${patterns} +`); + } + + if (!await this._textModel.object.saveAs(target)) { + return undefined; + } + + return this._move(group, target)?.editor; + } + + // called when users rename a notebook document + rename(group: GroupIdentifier, target: URI): IMoveResult | undefined { + if (this._textModel) { + const contributedNotebookProviders = this._notebookService.getContributedNotebookProviders(target); + + if (contributedNotebookProviders.find(provider => provider.id === this._textModel!.object.viewType)) { + return this._move(group, target); + } + } + return undefined; + } + + private _move(group: GroupIdentifier, newResource: URI): { editor: IEditorInput } | undefined { + return undefined; + } + + async revert(group: GroupIdentifier, options?: IRevertOptions): Promise { + if (this._textModel && this._textModel.object.isDirty()) { + await this._textModel.object.revert(options); + } + + return; + } + + async resolve(editorId?: string): Promise { + if (!await this._notebookService.canResolve(this.viewType!)) { + return null; + } + + if (!this._textModel) { + this._textModel = await this._notebookModelResolverService.resolve(this.resource, this.viewType!, editorId); + this._originalTextModel = await this._notebookModelResolverService.resolve(this.originalResource, this.viewType!, editorId); + } + + return new NotebookDiffEditorModel(this._originalTextModel!.object as NotebookEditorModel, this._textModel.object as NotebookEditorModel); + } + + matches(otherInput: unknown): boolean { + if (this === otherInput) { + return true; + } + if (otherInput instanceof NotebookDiffEditorInput) { + return this.viewType === otherInput.viewType + && isEqual(this.resource, otherInput.resource); + } + return false; + } + + dispose() { + if (this._textModel) { + this._textModel.dispose(); + this._textModel = null; + } + super.dispose(); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index 72de994f87..bc00730166 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -15,8 +15,8 @@ import { INotificationService, Severity } from 'vs/platform/notification/common/ import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { EditorOptions, IEditorInput, IEditorMemento } from 'vs/workbench/common/editor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; +import { EditorOptions, IEditorInput, IEditorMemento, IEditorOpenContext } from 'vs/workbench/common/editor'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; import { IBorrowValue, INotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidgetService'; @@ -28,7 +28,7 @@ import { NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/not const NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'NotebookEditorViewState'; -export class NotebookEditor extends BaseEditor { +export class NotebookEditor extends EditorPane { static readonly ID: string = 'workbench.editor.notebook'; private readonly _editorMemento: IEditorMemento; @@ -74,7 +74,7 @@ export class NotebookEditor extends BaseEditor { get minimumWidth(): number { return 375; } get maximumWidth(): number { return Number.POSITIVE_INFINITY; } - // these setters need to exist because this extends from BaseEditor + // these setters need to exist because this extends from EditorPane set minimumWidth(value: number) { /*noop*/ } set maximumWidth(value: number) { /*noop*/ } @@ -126,12 +126,12 @@ export class NotebookEditor extends BaseEditor { this._widget.value?.focus(); } - async setInput(input: NotebookEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + async setInput(input: NotebookEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { const group = this.group!; this._saveEditorViewState(this.input); - await super.setInput(input, options, token); + await super.setInput(input, options, context, token); // Check for cancellation if (token.isCancellationRequested) { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index db55924317..88fcae877c 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -6,11 +6,17 @@ import { getZoomLevel } from 'vs/base/browser/browser'; import * as DOM from 'vs/base/browser/dom'; import { IMouseWheelEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent'; +import { IListContextMenuEvent } from 'vs/base/browser/ui/list/list'; +import { IAction, Separator } from 'vs/base/common/actions'; +import { SequencerByKey } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Color, RGBA } from 'vs/base/common/color'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { combinedDisposable, DisposableStore, Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { combinedDisposable, Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ScrollEvent } from 'vs/base/common/scrollable'; +import { URI } from 'vs/base/common/uri'; +import { generateUuid } from 'vs/base/common/uuid'; import 'vs/css!./media/notebook'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; @@ -18,47 +24,41 @@ import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; import { Range } from 'vs/editor/common/core/range'; import { IEditor } from 'vs/editor/common/editorCommon'; import * as nls from 'vs/nls'; +import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; +import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { contrastBorder, editorBackground, focusBorder, foreground, registerColor, textBlockQuoteBackground, textBlockQuoteBorder, textLinkActiveForeground, textLinkForeground, textPreformatForeground, errorForeground, transparent, listFocusBackground, listInactiveSelectionBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, scrollbarSliderActiveBackground } from 'vs/platform/theme/common/colorRegistry'; +import { contrastBorder, diffInserted, diffRemoved, editorBackground, errorForeground, focusBorder, foreground, listFocusBackground, listInactiveSelectionBackground, registerColor, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, textBlockQuoteBackground, textBlockQuoteBorder, textLinkActiveForeground, textLinkForeground, textPreformatForeground, transparent } from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { EditorMemento } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorMemento } from 'vs/workbench/browser/parts/editor/editorPane'; import { IEditorMemento } from 'vs/workbench/common/editor'; -import { CELL_MARGIN, CELL_RUN_GUTTER, CELL_TOP_MARGIN, SCROLLABLE_ELEMENT_PADDING_TOP, BOTTOM_CELL_TOOLBAR_GAP, CELL_BOTTOM_MARGIN, CODE_CELL_LEFT_MARGIN, COLLAPSED_INDICATOR_HEIGHT, BOTTOM_CELL_TOOLBAR_HEIGHT } from 'vs/workbench/contrib/notebook/browser/constants'; -import { CellEditState, CellFocusMode, ICellRange, ICellViewModel, INotebookCellList, INotebookEditor, INotebookEditorContribution, INotebookEditorMouseEvent, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_HAS_MULTIPLE_KERNELS, NOTEBOOK_OUTPUT_FOCUSED, INotebookDeltaDecoration, NotebookEditorOptions, INotebookEditorWidgetOptions, INotebookEditorContributionDescription } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { Memento, MementoObject } from 'vs/workbench/common/memento'; +import { PANEL_BORDER } from 'vs/workbench/common/theme'; +import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/debugToolBar'; +import { BOTTOM_CELL_TOOLBAR_GAP, BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_BOTTOM_MARGIN, CELL_MARGIN, CELL_RUN_GUTTER, CELL_TOP_MARGIN, CODE_CELL_LEFT_MARGIN, COLLAPSED_INDICATOR_HEIGHT, SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants'; +import { CellEditState, CellFocusMode, ICellViewModel, INotebookCellList, INotebookDeltaDecoration, INotebookEditor, INotebookEditorContribution, INotebookEditorContributionDescription, INotebookEditorCreationOptions, INotebookEditorMouseEvent, NotebookEditorOptions, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_HAS_MULTIPLE_KERNELS, NOTEBOOK_OUTPUT_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; +import { NotebookKernelProviderAssociation, NotebookKernelProviderAssociations, notebookKernelProviderAssociationsSettingId } from 'vs/workbench/contrib/notebook/browser/notebookKernelAssociation'; import { NotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList'; import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; import { BackLayerWebView } from 'vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView'; -import { CodeCellRenderer, MarkdownCellRenderer, NotebookCellListDelegate, ListTopCellToolbar } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer'; +import { CellContextKeyManager } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys'; +import { CodeCellRenderer, ListTopCellToolbar, MarkdownCellRenderer, NotebookCellListDelegate } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer'; +import { CellDragAndDropController } from 'vs/workbench/contrib/notebook/browser/view/renderers/dnd'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { NotebookEventDispatcher, NotebookLayoutChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; import { CellViewModel, IModelDecorationsChangeAccessor, INotebookEditorViewState, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { CellKind, IProcessedOutput, INotebookKernelInfo, INotebookKernelInfoDto, INotebookKernelInfo2, NotebookRunState, NotebookCellRunState, IInsetRenderOutput, CellToolbarLocKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { CellKind, CellToolbarLocKey, ICellRange, IInsetRenderOutput, INotebookKernelInfo2, IProcessedOutput, isTransformedDisplayOutput, NotebookCellRunState, NotebookRunState, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { editorGutterModifiedBackground } from 'vs/workbench/contrib/scm/browser/dirtydiffDecorator'; import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; -import { generateUuid } from 'vs/base/common/uuid'; -import { Memento, MementoObject } from 'vs/workbench/common/memento'; -import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { URI } from 'vs/base/common/uri'; -import { PANEL_BORDER } from 'vs/workbench/common/theme'; -import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/debugToolBar'; -import { CellContextKeyManager } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys'; -import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; -import { notebookKernelProviderAssociationsSettingId, NotebookKernelProviderAssociations } from 'vs/workbench/contrib/notebook/browser/notebookKernelAssociation'; -import { IListContextMenuEvent } from 'vs/base/browser/ui/list/list'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; -import { IAction, Separator } from 'vs/base/common/actions'; -import { isMacintosh, isNative } from 'vs/base/common/platform'; -import { getTitleBarStyle } from 'vs/platform/windows/common/windows'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { ScrollEvent } from 'vs/base/common/scrollable'; -import { CellDragAndDropController } from 'vs/workbench/contrib/notebook/browser/view/renderers/dnd'; const $ = DOM.$; @@ -98,6 +98,19 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private readonly _activeKernelMemento: Memento; private readonly _onDidFocusEmitter = this._register(new Emitter()); public readonly onDidFocus = this._onDidFocusEmitter.event; + private readonly _onWillScroll = this._register(new Emitter()); + public readonly onWillScroll: Event = this._onWillScroll.event; + private readonly _onWillDispose = this._register(new Emitter()); + public readonly onWillDispose: Event = this._onWillDispose.event; + + private readonly _insetModifyQueueByOutputId = new SequencerByKey(); + + set scrollTop(top: number) { + if (this._list) { + this._list.scrollTop = top; + } + } + private _cellContextKeyManager: CellContextKeyManager | null = null; private _isVisible = false; private readonly _uuid = generateUuid(); @@ -132,7 +145,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return this._notebookViewModel?.notebookDocument; } - private _activeKernel: INotebookKernelInfo | INotebookKernelInfo2 | undefined = undefined; + private _activeKernel: INotebookKernelInfo2 | undefined = undefined; private readonly _onDidChangeKernel = this._register(new Emitter()); readonly onDidChangeKernel: Event = this._onDidChangeKernel.event; private readonly _onDidChangeAvailableKernels = this._register(new Emitter()); @@ -142,7 +155,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return this._activeKernel; } - set activeKernel(kernel: INotebookKernelInfo | INotebookKernelInfo2 | undefined) { + set activeKernel(kernel: INotebookKernelInfo2 | undefined) { if (this._isDisposed) { return; } @@ -203,19 +216,29 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._cursorNavigationMode = v; } + private readonly _onDidChangeVisibleRanges = this._register(new Emitter()); + onDidChangeVisibleRanges: Event = this._onDidChangeVisibleRanges.event; + + get visibleRanges() { + return this._list?.visibleRanges || []; + } + + readonly isEmbedded: boolean; + constructor( - private readonly editorWidgetOptions: INotebookEditorWidgetOptions, + readonly creationOptions: INotebookEditorCreationOptions, @IInstantiationService private readonly instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @INotebookService private notebookService: INotebookService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, @IContextKeyService readonly contextKeyService: IContextKeyService, @ILayoutService private readonly layoutService: ILayoutService, @IContextMenuService private readonly contextMenuService: IContextMenuService, @IMenuService private readonly menuService: IMenuService, + @IQuickInputService private readonly quickInputService: IQuickInputService ) { super(); + this.isEmbedded = creationOptions.isEmbedded || false; this._memento = new Memento(NotebookEditorWidget.ID, storageService); this._activeKernelMemento = new Memento(NotebookEditorActiveKernelCache, storageService); @@ -231,7 +254,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } } - if (e.affectsConfiguration(CellToolbarLocKey)) { + if (e.affectsConfiguration(CellToolbarLocKey) || e.affectsConfiguration(ShowCellStatusBarKey)) { this._updateForNotebookConfiguration(); } }); @@ -285,6 +308,9 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor if (cellToolbarLocation === 'left' || cellToolbarLocation === 'right' || cellToolbarLocation === 'hidden') { this._overlayContainer.classList.add(`cell-title-toolbar-${cellToolbarLocation}`); } + + const showCellStatusBar = this.configurationService.getValue(ShowCellStatusBarKey); + this._overlayContainer.classList.toggle('cell-statusbar-hidden', !showCellStatusBar); } updateEditorFocus() { @@ -361,8 +387,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._notebookHasMultipleKernels.set(false); let contributions: INotebookEditorContributionDescription[]; - if (Array.isArray(this.editorWidgetOptions.contributions)) { - contributions = this.editorWidgetOptions.contributions; + if (Array.isArray(this.creationOptions.contributions)) { + contributions = this.creationOptions.contributions; } else { contributions = NotebookEditorExtensionsRegistry.getEditorContributions(); } @@ -409,6 +435,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._list = this.instantiationService.createInstance( NotebookCellList, 'NotebookCellList', + this._overlayContainer, this._body, this.instantiationService.createInstance(NotebookCellListDelegate), renderers, @@ -423,7 +450,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor multipleSelectionSupport: false, enableKeyboardNavigation: true, additionalScrollHeight: 0, - transformOptimization: (isMacintosh && isNative) || getTitleBarStyle(this.configurationService, this.environmentService) === 'native', + transformOptimization: false, //(isMacintosh && isNative) || getTitleBarStyle(this.configurationService, this.environmentService) === 'native', styleController: (_suffix: string) => { return this._list!; }, overrideStyles: { listBackground: editorBackground, @@ -507,6 +534,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._onDidScroll.fire(e); })); + this._register(this._list.onDidChangeVisibleRanges(() => { + this._onDidChangeVisibleRanges.fire(); + })); + const widgetFocusTracker = DOM.trackFocus(this.getDomNode()); this._register(widgetFocusTracker); this._register(widgetFocusTracker.onDidFocus(() => this._onDidFocusEmitter.fire())); @@ -610,7 +641,11 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor // we don't await for it, otherwise it will slow down the file opening this._setKernels(textModel, this._currentKernelTokenSource); - this._localStore.add(this.notebookService.onDidChangeKernels(async () => { + this._localStore.add(this.notebookService.onDidChangeKernels(async (e) => { + if (e && e.toString() !== this.textModel?.uri.toString()) { + // kernel update is not for current document. + return; + } this._currentKernelTokenSource?.cancel(); this._currentKernelTokenSource = new CancellationTokenSource(); await this._setKernels(textModel, this._currentKernelTokenSource); @@ -680,16 +715,11 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return; } - const availableKernels = this.notebookService.getContributedNotebookKernels(textModel.viewType, textModel.uri); - if (tokenSource.token.isCancellationRequested) { return; } - if (provider.kernel && (availableKernels.length + availableKernels2.length) > 0) { - this._notebookHasMultipleKernels!.set(true); - this.multipleKernelsAvailable = true; - } else if ((availableKernels.length + availableKernels2.length) > 1) { + if ((availableKernels2.length) > 1) { this._notebookHasMultipleKernels!.set(true); this.multipleKernelsAvailable = true; } else { @@ -697,15 +727,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this.multipleKernelsAvailable = false; } - // @deprecated - if (provider && provider.kernel) { - // it has a builtin kernel, don't automatically choose a kernel - await this._loadKernelPreloads(provider.providerExtensionLocation, provider.kernel); - tokenSource.dispose(); - return; - } - - const activeKernelStillExist = [...availableKernels2, ...availableKernels].find(kernel => kernel.id === this.activeKernel?.id && this.activeKernel?.id !== undefined); + const activeKernelStillExist = [...availableKernels2].find(kernel => kernel.id === this.activeKernel?.id && this.activeKernel?.id !== undefined); if (activeKernelStillExist) { // the kernel still exist, we don't want to modify the selection otherwise user's temporary preference is lost @@ -717,10 +739,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } // the provider doesn't have a builtin kernel, choose a kernel - this.activeKernel = availableKernels[0]; - if (this.activeKernel) { - await this._loadKernelPreloads(this.activeKernel.extensionLocation, this.activeKernel); - } + // this.activeKernel = availableKernels[0]; + // if (this.activeKernel) { + // await this._loadKernelPreloads(this.activeKernel.extensionLocation, this.activeKernel); + // } tokenSource.dispose(); } @@ -809,7 +831,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor tokenSource.dispose(); } - private async _loadKernelPreloads(extensionLocation: URI, kernel: INotebookKernelInfoDto) { + private async _loadKernelPreloads(extensionLocation: URI, kernel: INotebookKernelInfo2) { if (kernel.preloads && kernel.preloads.length) { await this._resolveWebview(); this._webview?.updateKernelPreloads([extensionLocation], kernel.preloads.map(preload => URI.revive(preload))); @@ -913,6 +935,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } this._localStore.add(this._list!.onWillScroll(e => { + this._onWillScroll.fire(e); if (!this._webviewResolved) { return; } @@ -1239,14 +1262,15 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor const index = cell ? this._notebookViewModel!.getCellIndex(cell) : 0; const nextIndex = ui ? this._notebookViewModel!.getNextVisibleCellIndex(index) : index + 1; - const newLanguages = this._notebookViewModel!.languages; + const newLanguages = this._notebookViewModel!.resolvedLanguages; const language = (cell?.cellKind === CellKind.Code && type === CellKind.Code) ? cell.language : ((type === CellKind.Code && newLanguages && newLanguages.length) ? newLanguages[0] : 'markdown'); const insertIndex = cell ? (direction === 'above' ? index : nextIndex) : index; - const newCell = this._notebookViewModel!.createCell(insertIndex, initialText.split(/\r?\n/g), language, type, undefined, true); + const focused = this._list?.getFocusedElements(); + const newCell = this._notebookViewModel!.createCell(insertIndex, initialText, language, type, undefined, true, undefined, focused); return newCell as CellViewModel; } @@ -1400,26 +1424,98 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return undefined; } + private async _ensureActiveKernel() { + if (this._activeKernel) { + if (this._activeKernelResolvePromise) { + await this._activeKernelResolvePromise; + } + + return; + } + + // pick active kernel + + const tokenSource = new CancellationTokenSource(); + const availableKernels2 = await this.notebookService.getContributedNotebookKernels2(this.viewModel!.viewType, this.viewModel!.uri, tokenSource.token); + const picks: QuickPickInput[] = availableKernels2.map((a) => { + return { + id: a.id, + label: a.label, + picked: false, + description: + a.description + ? a.description + : a.extension.value, + detail: a.detail, + kernelProviderId: a.extension.value, + run: async () => { + this.activeKernel = a; + this._activeKernelResolvePromise = this.activeKernel.resolve(this.viewModel!.uri, this.getId(), tokenSource.token); + }, + buttons: [{ + iconClass: 'codicon-settings-gear', + tooltip: nls.localize('notebook.promptKernel.setDefaultTooltip', "Set as default kernel provider for '{0}'", this.viewModel!.viewType) + }] + }; + }); + + const picker = this.quickInputService.createQuickPick<(IQuickPickItem & { run(): void; kernelProviderId?: string })>(); + picker.items = picks; + picker.placeholder = nls.localize('notebook.runCell.selectKernel', "Select a notebook kernel to run this notebook"); + picker.matchOnDetail = true; + + const pickedItem = await new Promise<(IQuickPickItem & { run(): void; kernelProviderId?: string; }) | undefined>(resolve => { + picker.onDidAccept(() => { + resolve(picker.selectedItems.length === 1 ? picker.selectedItems[0] : undefined); + picker.dispose(); + }); + + picker.onDidTriggerItemButton(e => { + const pick = e.item; + const id = pick.id; + resolve(pick); // open the view + picker.dispose(); + + // And persist the setting + if (pick && id && pick.kernelProviderId) { + const newAssociation: NotebookKernelProviderAssociation = { viewType: this.viewModel!.viewType, kernelProvider: pick.kernelProviderId }; + const currentAssociations = [...this.configurationService.getValue(notebookKernelProviderAssociationsSettingId)]; + + // First try updating existing association + for (let i = 0; i < currentAssociations.length; ++i) { + const existing = currentAssociations[i]; + if (existing.viewType === newAssociation.viewType) { + currentAssociations.splice(i, 1, newAssociation); + this.configurationService.updateValue(notebookKernelProviderAssociationsSettingId, currentAssociations); + return; + } + } + + // Otherwise, create a new one + currentAssociations.unshift(newAssociation); + this.configurationService.updateValue(notebookKernelProviderAssociationsSettingId, currentAssociations); + } + }); + + picker.show(); + }); + + tokenSource.dispose(); + + if (pickedItem) { + await pickedItem.run(); + } + + return; + } + async cancelNotebookExecution(): Promise { if (this._notebookViewModel?.metadata.runState !== NotebookRunState.Running) { return; } - return this._cancelNotebookExecution(); - } - - private async _cancelNotebookExecution(): Promise { - const provider = this.notebookService.getContributedNotebookProviders(this.viewModel!.uri)[0]; - if (provider) { - const viewType = provider.id; - const notebookUri = this._notebookViewModel!.uri; - - if (this._activeKernel) { - await (this._activeKernel as INotebookKernelInfo2).cancelNotebookCell!(this._notebookViewModel!.uri, undefined); - } else if (provider.kernel) { - return await this.notebookService.cancelNotebook(viewType, notebookUri); - } - } + await this._ensureActiveKernel(); + await this._activeKernel?.cancelNotebookCell!(this._notebookViewModel!.uri, undefined); } async executeNotebook(): Promise { @@ -1427,30 +1523,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return; } - return this._executeNotebook(); - } - - private async _executeNotebook(): Promise { - const provider = this.notebookService.getContributedNotebookProviders(this.viewModel!.uri)[0]; - if (provider) { - const viewType = provider.id; - const notebookUri = this._notebookViewModel!.uri; - - if (this._activeKernel) { - // TODO@rebornix temp any cast, should be removed once we remove legacy kernel support - if ((this._activeKernel as INotebookKernelInfo2).executeNotebookCell) { - if (this._activeKernelResolvePromise) { - await this._activeKernelResolvePromise; - } - - await (this._activeKernel as INotebookKernelInfo2).executeNotebookCell!(this._notebookViewModel!.uri, undefined); - } else { - await this.notebookService.executeNotebook2(this._notebookViewModel!.viewType, this._notebookViewModel!.uri, this._activeKernel.id); - } - } else if (provider.kernel) { - return await this.notebookService.executeNotebook(viewType, notebookUri); - } - } + await this._ensureActiveKernel(); + await this._activeKernel?.executeNotebookCell!(this._notebookViewModel!.uri, undefined); } async cancelNotebookCellExecution(cell: ICellViewModel): Promise { @@ -1467,21 +1541,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return; } - await this._cancelNotebookCell(cell); - } - - private async _cancelNotebookCell(cell: ICellViewModel): Promise { - const provider = this.notebookService.getContributedNotebookProviders(this.viewModel!.uri)[0]; - if (provider) { - const viewType = provider.id; - const notebookUri = this._notebookViewModel!.uri; - - if (this._activeKernel) { - return await (this._activeKernel as INotebookKernelInfo2).cancelNotebookCell!(this._notebookViewModel!.uri, cell.handle); - } else if (provider.kernel) { - return await this.notebookService.cancelNotebookCell(viewType, notebookUri, cell.handle); - } - } + await this._ensureActiveKernel(); + await this._activeKernel?.cancelNotebookCell!(this._notebookViewModel!.uri, cell.handle); } async executeNotebookCell(cell: ICellViewModel): Promise { @@ -1494,27 +1555,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return; } - await this._executeNotebookCell(cell); - } - - private async _executeNotebookCell(cell: ICellViewModel): Promise { - const provider = this.notebookService.getContributedNotebookProviders(this.viewModel!.uri)[0]; - if (provider) { - const viewType = provider.id; - const notebookUri = this._notebookViewModel!.uri; - - if (this._activeKernel) { - // TODO@rebornix temp any cast, should be removed once we remove legacy kernel support - if ((this._activeKernel as INotebookKernelInfo2).executeNotebookCell) { - await (this._activeKernel as INotebookKernelInfo2).executeNotebookCell!(this._notebookViewModel!.uri, cell.handle); - } else { - - return await this.notebookService.executeNotebookCell2(viewType, notebookUri, cell.handle, this._activeKernel.id); - } - } else if (provider.kernel) { - return await this.notebookService.executeNotebookCell(viewType, notebookUri, cell.handle); - } - } + await this._ensureActiveKernel(); + await this._activeKernel?.executeNotebookCell!(this._notebookViewModel!.uri, cell.handle); } focusNotebookCell(cell: ICellViewModel, focusItem: 'editor' | 'container' | 'output') { @@ -1584,30 +1626,37 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._list?.triggerScrollFromMouseWheelEvent(event); } - async createInset(cell: CodeCellViewModel, output: IInsetRenderOutput, offset: number) { - if (!this._webview) { - return; - } + async createInset(cell: CodeCellViewModel, output: IInsetRenderOutput, offset: number): Promise { + this._insetModifyQueueByOutputId.queue(output.source.outputId, async () => { + if (!this._webview) { + return; + } - await this._resolveWebview(); + await this._resolveWebview(); - if (!this._webview!.insetMapping.has(output.source)) { - const cellTop = this._list?.getAbsoluteTopOfElement(cell) || 0; - await this._webview!.createInset(cell, output, cellTop, offset); - } else { - const cellTop = this._list?.getAbsoluteTopOfElement(cell) || 0; - const scrollTop = this._list?.scrollTop || 0; + if (!this._webview!.insetMapping.has(output.source)) { + const cellTop = this._list?.getAbsoluteTopOfElement(cell) || 0; + await this._webview!.createInset(cell, output, cellTop, offset); + } else { + const cellTop = this._list?.getAbsoluteTopOfElement(cell) || 0; + const scrollTop = this._list?.scrollTop || 0; - this._webview!.updateViewScrollTop(-scrollTop, true, [{ cell, output: output.source, cellTop }]); - } + this._webview!.updateViewScrollTop(-scrollTop, true, [{ cell, output: output.source, cellTop }]); + } + }); } removeInset(output: IProcessedOutput) { - if (!this._webview || !this._webviewResolved) { + if (!isTransformedDisplayOutput(output)) { return; } - this._webview!.removeInset(output); + this._insetModifyQueueByOutputId.queue(output.outputId, async () => { + if (!this._webview || !this._webviewResolved) { + return; + } + this._webview!.removeInset(output); + }); } hideInset(output: IProcessedOutput) { @@ -1615,7 +1664,13 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return; } - this._webview!.hideInset(output); + if (!isTransformedDisplayOutput(output)) { + return; + } + + this._insetModifyQueueByOutputId.queue(output.outputId, async () => { + this._webview!.hideInset(output); + }); } getOutputRenderer(): OutputRenderer { @@ -1658,6 +1713,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor dispose() { this._isDisposed = true; + this._onWillDispose.fire(); // dispose webview first this._webview?.dispose(); @@ -1786,7 +1842,8 @@ export const cellSymbolHighlight = registerColor('notebook.symbolHighlightBackgr }, nls.localize('notebook.symbolHighlightBackground', "Background color of highlighted cell")); registerThemingParticipant((theme, collector) => { - collector.addRule(`.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element { + collector.addRule(`.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element, + .notebookOverlay > .cell-list-container > .notebook-gutter > .monaco-list > .monaco-scrollable-element { padding-top: ${SCROLLABLE_ELEMENT_PADDING_TOP}px; box-sizing: border-box; }`); @@ -1864,10 +1921,10 @@ registerThemingParticipant((theme, collector) => { } const focusedCellBorderColor = theme.getColor(focusedCellBorder); - collector.addRule(`.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.focused .cell-focus-indicator-top:before, - .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.focused .cell-focus-indicator-bottom:before, - .monaco-workbench .notebookOverlay .monaco-list .markdown-cell-row.focused:before, - .monaco-workbench .notebookOverlay .monaco-list .markdown-cell-row.focused:after { + collector.addRule(`.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-focus-indicator-top:before, + .monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row.focused .cell-focus-indicator-bottom:before, + .monaco-workbench .notebookOverlay .monaco-list:focus-within .markdown-cell-row.focused:before, + .monaco-workbench .notebookOverlay .monaco-list:focus-within .markdown-cell-row.focused:after { border-color: ${focusedCellBorderColor} !important; }`); @@ -1907,7 +1964,8 @@ registerThemingParticipant((theme, collector) => { const cellStatusBarHoverBg = theme.getColor(cellStatusBarItemHover); if (cellStatusBarHoverBg) { - collector.addRule(`.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-language-picker:hover { background-color: ${cellStatusBarHoverBg}; }`); + collector.addRule(`.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-language-picker:hover, + .monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-item.cell-status-item-has-command:hover { background-color: ${cellStatusBarHoverBg}; }`); } const cellInsertionIndicatorColor = theme.getColor(cellInsertionIndicator); @@ -1933,6 +1991,46 @@ registerThemingParticipant((theme, collector) => { collector.addRule(` .notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .scrollbar > .slider.active:before { content: ""; width: 100%; height: 100%; position: absolute; background: ${scrollbarSliderActiveBackgroundColor}; } `); /* hack to not have cells see through scroller */ } + // case ChangeType.Modify: return theme.getColor(editorGutterModifiedBackground); + // case ChangeType.Add: return theme.getColor(editorGutterAddedBackground); + // case ChangeType.Delete: return theme.getColor(editorGutterDeletedBackground); + // diff + + const modifiedBackground = theme.getColor(editorGutterModifiedBackground); + if (modifiedBackground) { + collector.addRule(` + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row .nb-cell-modified .cell-focus-indicator { + background-color: ${modifiedBackground} !important; + } + + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.markdown-cell-row .nb-cell-modified { + background-color: ${modifiedBackground} !important; + }`); + } + + const addedBackground = theme.getColor(diffInserted); + if (addedBackground) { + collector.addRule(` + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row .nb-cell-added .cell-focus-indicator { + background-color: ${addedBackground} !important; + } + + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.markdown-cell-row .nb-cell-added { + background-color: ${addedBackground} !important; + }`); + } + const deletedBackground = theme.getColor(diffRemoved); + if (deletedBackground) { + collector.addRule(` + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row .nb-cell-deleted .cell-focus-indicator { + background-color: ${deletedBackground} !important; + } + + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.markdown-cell-row .nb-cell-deleted { + background-color: ${deletedBackground} !important; + }`); + } + // Cell Margin collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row div.cell { margin: 0px ${CELL_MARGIN * 2}px 0px ${CELL_MARGIN}px; }`); collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row div.cell.code { margin-left: ${CODE_CELL_LEFT_MARGIN}px; }`); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetService.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetService.ts index 07b2a79304..bf5da95337 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetService.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetService.ts @@ -126,7 +126,7 @@ class NotebookEditorWidgetService implements INotebookEditorWidgetService { if (!value) { // NEW widget const instantiationService = accessor.get(IInstantiationService); - const widget = instantiationService.createInstance(NotebookEditorWidget, {}); + const widget = instantiationService.createInstance(NotebookEditorWidget, { isEmbedded: false }); widget.createEditor(); const token = this._tokenPool++; value = { widget, token }; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts index 9919ec4e67..a9ad013bde 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts @@ -6,10 +6,9 @@ import { flatten } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; -import * as glob from 'vs/base/common/glob'; import { Iterable } from 'vs/base/common/iterator'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { basename } from 'vs/base/common/path'; +import { ResourceMap } from 'vs/base/common/map'; import { URI } from 'vs/base/common/uri'; import { RedoCommand, UndoCommand } from 'vs/editor/browser/editorExtensions'; import { CopyAction, CutAction, PasteAction } from 'vs/editor/contrib/clipboard/clipboard'; @@ -27,7 +26,7 @@ import { NotebookKernelProviderAssociationRegistry, NotebookViewTypesExtensionRe import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, BUILTIN_RENDERER_ID, CellEditType, CellOutputKind, CellUri, DisplayOrderKey, ICellEditOperation, IDisplayOutput, INotebookKernelInfo, INotebookKernelInfo2, INotebookKernelProvider, INotebookRendererInfo, INotebookTextModel, IOrderedMimeType, ITransformedDisplayOutputDto, mimeTypeSupportedByCore, NotebookCellOutputsSplice, notebookDocumentFilterMatch, NotebookEditorPriority, NOTEBOOK_DISPLAY_ORDER, sortMimeTypes } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, BUILTIN_RENDERER_ID, CellEditType, CellOutputKind, CellUri, DisplayOrderKey, ICellEditOperation, IDisplayOutput, INotebookKernelInfo2, INotebookKernelProvider, INotebookRendererInfo, INotebookTextModel, IOrderedMimeType, ITransformedDisplayOutputDto, mimeTypeSupportedByCore, NotebookCellOutputsSplice, notebookDocumentFilterMatch, NotebookEditorPriority, NOTEBOOK_DISPLAY_ORDER, sortMimeTypes } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookOutputRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookOutputRenderer'; import { NotebookEditorDescriptor, NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; import { IMainNotebookController, INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; @@ -35,10 +34,6 @@ import { ICustomEditorInfo, ICustomEditorViewTypesHandler, IEditorService } from import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -function MODEL_ID(resource: URI): string { - return resource.toString(); -} - export class NotebookKernelProviderInfoStore extends Disposable { private readonly _notebookKernelProviders: INotebookKernelProvider[] = []; @@ -232,11 +227,10 @@ export class NotebookService extends Disposable implements INotebookService, ICu declare readonly _serviceBrand: undefined; static mainthreadNotebookDocumentHandle: number = 0; private readonly _notebookProviders = new Map(); - private readonly _notebookKernels = new Map(); notebookProviderInfoStore: NotebookProviderInfoStore; notebookRenderersInfoStore: NotebookOutputRendererInfoStore = new NotebookOutputRendererInfoStore(); notebookKernelProviderInfoStore: NotebookKernelProviderInfoStore = new NotebookKernelProviderInfoStore(); - private readonly _models = new Map(); + private readonly _models = new ResourceMap(); private _onDidChangeActiveEditor = new Emitter(); onDidChangeActiveEditor: Event = this._onDidChangeActiveEditor.event; private _activeEditorDisposables = new DisposableStore(); @@ -257,8 +251,8 @@ export class NotebookService extends Disposable implements INotebookService, ICu private readonly _onDidChangeViewTypes = new Emitter(); onDidChangeViewTypes: Event = this._onDidChangeViewTypes.event; - private readonly _onDidChangeKernels = new Emitter(); - onDidChangeKernels: Event = this._onDidChangeKernels.event; + private readonly _onDidChangeKernels = new Emitter(); + onDidChangeKernels: Event = this._onDidChangeKernels.event; private readonly _onDidChangeNotebookActiveKernel = new Emitter<{ uri: URI, providerHandle: number | undefined, kernelId: string | undefined }>(); onDidChangeNotebookActiveKernel: Event<{ uri: URI, providerHandle: number | undefined, kernelId: string | undefined }> = this._onDidChangeNotebookActiveKernel.event; private cutItems: NotebookCellTextModel[] | undefined; @@ -541,6 +535,9 @@ export class NotebookService extends Disposable implements INotebookService, ICu // notebook providers/kernels/renderers might use `*` as activation event. await this._extensionService.activateByEvent(`*`); // this awaits full activation of all matching extensions + await this._extensionService.activateByEvent(`onNotebook:${viewType}`); + + // TODO@jrieken deprecated, remove this await this._extensionService.activateByEvent(`onNotebookEditor:${viewType}`); } return this._notebookProviders.has(viewType); @@ -548,7 +545,6 @@ export class NotebookService extends Disposable implements INotebookService, ICu registerNotebookController(viewType: string, extensionData: NotebookExtensionDescription, controller: IMainNotebookController) { this._notebookProviders.set(viewType, { extensionData, controller }); - this.notebookProviderInfoStore.get(viewType)!.kernel = controller.kernel; this._onDidChangeViewTypes.fire(); } @@ -557,23 +553,13 @@ export class NotebookService extends Disposable implements INotebookService, ICu this._onDidChangeViewTypes.fire(); } - registerNotebookKernel(notebook: INotebookKernelInfo): void { - this._notebookKernels.set(notebook.id, notebook); - this._onDidChangeKernels.fire(); - } - - unregisterNotebookKernel(id: string): void { - this._notebookKernels.delete(id); - this._onDidChangeKernels.fire(); - } - registerNotebookKernelProvider(provider: INotebookKernelProvider): IDisposable { const d = this.notebookKernelProviderInfoStore.add(provider); - const kernelChangeEventListener = provider.onDidChangeKernels(() => { - this._onDidChangeKernels.fire(); + const kernelChangeEventListener = provider.onDidChangeKernels((e) => { + this._onDidChangeKernels.fire(e); }); - this._onDidChangeKernels.fire(); + this._onDidChangeKernels.fire(undefined); return toDisposable(() => { kernelChangeEventListener.dispose(); d.dispose(); @@ -593,6 +579,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu id: dto.id, label: dto.label, description: dto.description, + detail: dto.detail, isPreferred: dto.isPreferred, preloads: dto.preloads, providerHandle: dto.providerHandle, @@ -614,86 +601,41 @@ export class NotebookService extends Disposable implements INotebookService, ICu return flatten(result); } - getContributedNotebookKernels(viewType: string, resource: URI): INotebookKernelInfo[] { - let kernelInfos: INotebookKernelInfo[] = []; - this._notebookKernels.forEach(kernel => { - if (this._notebookKernelMatch(resource, kernel!.selectors)) { - kernelInfos.push(kernel!); - } - }); - - // sort by extensions - - const notebookContentProvider = this._notebookProviders.get(viewType); - - if (!notebookContentProvider) { - return kernelInfos; - } - - kernelInfos = kernelInfos.sort((a, b) => { - if (a.extension.value === notebookContentProvider!.extensionData.id.value) { - return -1; - } else if (b.extension.value === notebookContentProvider!.extensionData.id.value) { - return 1; - } else { - return 0; - } - }); - - return kernelInfos; - } - - private _notebookKernelMatch(resource: URI, selectors: (string | glob.IRelativePattern)[]): boolean { - for (let i = 0; i < selectors.length; i++) { - const pattern = typeof selectors[i] !== 'string' ? selectors[i] : selectors[i].toString(); - if (glob.match(pattern, basename(resource.fsPath).toLowerCase())) { - return true; - } - } - - return false; - } - getRendererInfo(id: string): INotebookRendererInfo | undefined { return this.notebookRenderersInfoStore.get(id); } - async resolveNotebook(viewType: string, uri: URI, forceReload: boolean, editorId?: string, backupId?: string): Promise { - const provider = this._notebookProviders.get(viewType); - if (!provider) { - return undefined; + async resolveNotebook(viewType: string, uri: URI, forceReload: boolean, editorId?: string, backupId?: string): Promise { + + if (!await this.canResolve(viewType)) { + throw new Error(`CANNOT load notebook, no provider for '${viewType}'`); } - const modelId = MODEL_ID(uri); - - let notebookModel: NotebookTextModel | undefined = undefined; - if (this._models.has(modelId)) { + const provider = this._notebookProviders.get(viewType)!; + let notebookModel: NotebookTextModel; + if (this._models.has(uri)) { // the model already exists - notebookModel = this._models.get(modelId)!.model; + notebookModel = this._models.get(uri)!.model; if (forceReload) { await provider.controller.reloadNotebook(notebookModel); } - return notebookModel; + } else { notebookModel = this._instantiationService.createInstance(NotebookTextModel, NotebookService.mainthreadNotebookDocumentHandle++, viewType, provider.controller.supportBackup, uri); await provider.controller.createNotebook(notebookModel, backupId); - - if (!notebookModel) { - return undefined; - } } // new notebook model created const modelData = new ModelData( - notebookModel!, + notebookModel, (model) => this._onWillDisposeDocument(model), ); - this._models.set(modelId, modelData); - this._onNotebookDocumentAdd.fire([notebookModel!.uri]); + this._models.set(uri, modelData); + this._onNotebookDocumentAdd.fire([notebookModel.uri]); // after the document is added to the store and sent to ext host, we transform the ouputs - await this.transformTextModelOutputs(notebookModel!); + await this.transformTextModelOutputs(notebookModel); if (editorId) { await provider.controller.resolveNotebookEditor(viewType, uri, editorId); @@ -703,9 +645,11 @@ export class NotebookService extends Disposable implements INotebookService, ICu } getNotebookTextModel(uri: URI): NotebookTextModel | undefined { - const modelId = MODEL_ID(uri); + return this._models.get(uri)?.model; + } - return this._models.get(modelId)?.model; + getNotebookTextModels(): Iterable { + return Iterable.map(this._models.values(), data => data.model); } private async transformTextModelOutputs(textModel: NotebookTextModel) { @@ -727,7 +671,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu transformEditsOutputs(textModel: NotebookTextModel, edits: ICellEditOperation[]) { edits.forEach((edit) => { - if (edit.editType === CellEditType.Insert) { + if (edit.editType === CellEditType.Replace) { edit.cells.forEach((cell) => { const outputs = cell.outputs; outputs.map((output) => { @@ -740,6 +684,16 @@ export class NotebookService extends Disposable implements INotebookService, ICu } }); }); + } else if (edit.editType === CellEditType.Output) { + edit.outputs.map((output) => { + if (output.outputKind === CellOutputKind.Rich) { + const ret = this._transformMimeTypes(output, output.outputId, textModel.metadata.displayOrder as string[] || []); + const orderedMimeTypes = ret.orderedMimeTypes!; + const pickedMimeTypeIndex = ret.pickedMimeTypeIndex!; + output.pickedMimeTypeIndex = pickedMimeTypeIndex; + output.orderedMimeTypes = orderedMimeTypes; + } + }); } }); } @@ -811,54 +765,6 @@ export class NotebookService extends Disposable implements INotebookService, ICu return this.notebookRenderersInfoStore.getContributedRenderer(mimeType); } - async executeNotebook(viewType: string, uri: URI): Promise { - const provider = this._notebookProviders.get(viewType); - - if (provider) { - return provider.controller.executeNotebookByAttachedKernel(viewType, uri); - } - - return; - } - - async executeNotebookCell(viewType: string, uri: URI, handle: number): Promise { - const provider = this._notebookProviders.get(viewType); - if (provider) { - await provider.controller.executeNotebookCell(uri, handle); - } - } - - async cancelNotebook(viewType: string, uri: URI): Promise { - const provider = this._notebookProviders.get(viewType); - - if (provider) { - return provider.controller.cancelNotebookByAttachedKernel(viewType, uri); - } - - return; - } - - async cancelNotebookCell(viewType: string, uri: URI, handle: number): Promise { - const provider = this._notebookProviders.get(viewType); - if (provider) { - await provider.controller.cancelNotebookCell(uri, handle); - } - } - - async executeNotebook2(viewType: string, uri: URI, kernelId: string): Promise { - const kernel = this._notebookKernels.get(kernelId); - if (kernel) { - await kernel.executeNotebook(viewType, uri, undefined); - } - } - - async executeNotebookCell2(viewType: string, uri: URI, handle: number, kernelId: string): Promise { - const kernel = this._notebookKernels.get(kernelId); - if (kernel) { - await kernel.executeNotebook(viewType, uri, handle); - } - } - getContributedNotebookProviders(resource: URI): readonly NotebookProviderInfo[] { return this.notebookProviderInfoStore.getContributedNotebook(resource); } @@ -1000,10 +906,9 @@ export class NotebookService extends Disposable implements INotebookService, ICu } private _onWillDisposeDocument(model: INotebookTextModel): void { - const modelId = MODEL_ID(model.uri); - const modelData = this._models.get(modelId); - this._models.delete(modelId); + const modelData = this._models.get(model.uri); + this._models.delete(model.uri); if (modelData) { // delete editors and documents diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index 5ec81be9b2..67f66667b6 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -19,9 +19,9 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IListService, IWorkbenchListOptions, WorkbenchList } from 'vs/platform/list/browser/listService'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { CellRevealPosition, CellRevealType, CursorAtBoundary, getVisibleCells, ICellRange, ICellViewModel, INotebookCellList, reduceCellRanges, CellEditState, CellFocusMode, BaseCellRenderTemplate, NOTEBOOK_CELL_LIST_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellRevealPosition, CellRevealType, CursorAtBoundary, getVisibleCells, ICellViewModel, INotebookCellList, reduceCellRanges, CellEditState, CellFocusMode, BaseCellRenderTemplate, NOTEBOOK_CELL_LIST_FOCUSED, cellRangesEqual } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { diff, IProcessedOutput, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { diff, IProcessedOutput, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, CellKind, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { clamp } from 'vs/base/common/numbers'; import { SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants'; @@ -54,16 +54,37 @@ export class NotebookCellList extends WorkbenchList implements ID private _hiddenRangeIds: string[] = []; private hiddenRangesPrefixSum: PrefixSumComputer | null = null; + private readonly _onDidChangeVisibleRanges = new Emitter(); + + onDidChangeVisibleRanges: Event = this._onDidChangeVisibleRanges.event; + private _visibleRanges: ICellRange[] = []; + + get visibleRanges() { + return this._visibleRanges; + } + + set visibleRanges(ranges: ICellRange[]) { + if (cellRangesEqual(this._visibleRanges, ranges)) { + return; + } + + this._visibleRanges = ranges; + this._onDidChangeVisibleRanges.fire(); + } + private _isDisposed = false; get isDisposed() { return this._isDisposed; } + private _isInLayout: boolean = false; + private readonly _focusNextPreviousDelegate: IFocusNextPreviousDelegate; constructor( private listUser: string, + parentContainer: HTMLElement, container: HTMLElement, delegate: IListVirtualDelegate, renderers: IListRenderer[], @@ -151,6 +172,86 @@ export class NotebookCellList extends WorkbenchList implements ID focus.focusMode = CellFocusMode.Editor; } })); + + // update visibleRanges + const updateVisibleRanges = () => { + if (!this.view.length) { + return; + } + + const top = this.getViewScrollTop(); + const bottom = this.getViewScrollBottom(); + const topViewIndex = clamp(this.view.indexAt(top), 0, this.view.length - 1); + const topElement = this.view.element(topViewIndex); + const topModelIndex = this._viewModel!.getCellIndex(topElement); + const bottomViewIndex = clamp(this.view.indexAt(bottom), 0, this.view.length - 1); + const bottomElement = this.view.element(bottomViewIndex); + const bottomModelIndex = this._viewModel!.getCellIndex(bottomElement); + + if (bottomModelIndex - topModelIndex === bottomViewIndex - topViewIndex) { + this.visibleRanges = [{ start: topModelIndex, end: bottomModelIndex }]; + } else { + let stack: number[] = []; + const ranges: ICellRange[] = []; + // there are hidden ranges + let index = topViewIndex; + let modelIndex = topModelIndex; + + while (index <= bottomViewIndex) { + const accu = this.hiddenRangesPrefixSum!.getAccumulatedValue(index); + if (accu === modelIndex + 1) { + // no hidden area after it + if (stack.length) { + if (stack[stack.length - 1] === modelIndex - 1) { + ranges.push({ start: stack[stack.length - 1], end: modelIndex }); + } else { + ranges.push({ start: stack[stack.length - 1], end: stack[stack.length - 1] }); + } + } + + stack.push(modelIndex); + index++; + modelIndex++; + } else { + // there are hidden ranges after it + if (stack.length) { + if (stack[stack.length - 1] === modelIndex - 1) { + ranges.push({ start: stack[stack.length - 1], end: modelIndex }); + } else { + ranges.push({ start: stack[stack.length - 1], end: stack[stack.length - 1] }); + } + } + + stack.push(modelIndex); + index++; + modelIndex = accu; + } + } + + if (stack.length) { + ranges.push({ start: stack[stack.length - 1], end: stack[stack.length - 1] }); + } + + this.visibleRanges = reduceCellRanges(ranges); + } + }; + + this._localDisposableStore.add(this.view.onDidChangeContentHeight(() => { + if (this._isInLayout) { + DOM.scheduleAtNextAnimationFrame(() => { + updateVisibleRanges(); + }); + } + updateVisibleRanges(); + })); + this._localDisposableStore.add(this.view.onDidScroll(() => { + if (this._isInLayout) { + DOM.scheduleAtNextAnimationFrame(() => { + updateVisibleRanges(); + }); + } + updateVisibleRanges(); + })); } elementAt(position: number): ICellViewModel | undefined { @@ -374,7 +475,11 @@ export class NotebookCellList extends WorkbenchList implements ID return; } + const focusInside = DOM.isAncestor(document.activeElement, this.rowsContainer); super.splice(start, deleteCount, elements); + if (focusInside) { + this.domFocus(); + } const selectionsLeft = []; this._viewModel!.selectionHandles.forEach(handle => { @@ -932,6 +1037,12 @@ export class NotebookCellList extends WorkbenchList implements ID } } + layout(height?: number, width?: number): void { + this._isInLayout = true; + super.layout(height, width); + this._isInLayout = false; + } + dispose() { this._isDisposed = true; this._viewModelStore.dispose(); diff --git a/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts index 90e1c3c2ae..034b83472e 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts @@ -8,6 +8,7 @@ import { IProcessedOutput, IRenderOutput, RenderOutputType } from 'vs/workbench/ import { NotebookRegistry } from 'vs/workbench/contrib/notebook/browser/notebookRegistry'; import { onUnexpectedError } from 'vs/base/common/errors'; import { INotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { URI } from 'vs/base/common/uri'; export class OutputRenderer { protected readonly _contributions: { [key: string]: IOutputTransformContribution; }; @@ -41,11 +42,11 @@ export class OutputRenderer { return { type: RenderOutputType.None, hasDynamicHeight: false }; } - render(output: IProcessedOutput, container: HTMLElement, preferredMimeType: string | undefined): IRenderOutput { + render(output: IProcessedOutput, container: HTMLElement, preferredMimeType: string | undefined, notebookUri: URI | undefined): IRenderOutput { const transform = this._mimeTypeMapping[output.outputKind]; if (transform) { - return transform.render(output, container, preferredMimeType); + return transform.render(output, container, preferredMimeType, notebookUri); } else { return this.renderNoop(output, container); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts index 596ee320f1..407f2b76ee 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts @@ -17,10 +17,10 @@ import { URI } from 'vs/base/common/uri'; import { MarkdownRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/mdRenderer'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { handleANSIOutput } from 'vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform'; +import { dirname } from 'vs/base/common/resources'; class RichRenderer implements IOutputTransformContribution { - private _mdRenderer: MarkdownRenderer; - private _richMimeTypeRenderers = new Map IRenderOutput>(); + private _richMimeTypeRenderers = new Map IRenderOutput>(); constructor( public notebookEditor: INotebookEditor, @@ -29,7 +29,6 @@ class RichRenderer implements IOutputTransformContribution { @IModeService private readonly modeService: IModeService, @IThemeService private readonly themeService: IThemeService ) { - this._mdRenderer = instantiationService.createInstance(MarkdownRenderer, undefined); this._richMimeTypeRenderers.set('application/json', this.renderJSON.bind(this)); this._richMimeTypeRenderers.set('application/javascript', this.renderJavaScript.bind(this)); this._richMimeTypeRenderers.set('text/html', this.renderHTML.bind(this)); @@ -41,7 +40,7 @@ class RichRenderer implements IOutputTransformContribution { this._richMimeTypeRenderers.set('text/x-javascript', this.renderCode.bind(this)); } - render(output: ITransformedDisplayOutputDto, container: HTMLElement, preferredMimeType: string | undefined): IRenderOutput { + render(output: ITransformedDisplayOutputDto, container: HTMLElement, preferredMimeType: string | undefined, notebookUri: URI): IRenderOutput { if (!output.data) { const contentNode = document.createElement('p'); contentNode.innerText = `No data could be found for output.`; @@ -69,10 +68,10 @@ class RichRenderer implements IOutputTransformContribution { } const renderer = this._richMimeTypeRenderers.get(preferredMimeType); - return renderer!(output, container); + return renderer!(output, notebookUri, container); } - renderJSON(output: ITransformedDisplayOutputDto, container: HTMLElement): IRenderOutput { + renderJSON(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { const data = output.data['application/json']; const str = JSON.stringify(data, null, '\t'); @@ -105,7 +104,7 @@ class RichRenderer implements IOutputTransformContribution { return { type: RenderOutputType.None, hasDynamicHeight: true }; } - renderCode(output: ITransformedDisplayOutputDto, container: HTMLElement): IRenderOutput { + renderCode(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { const data = output.data['text/x-javascript']; const str = (isArray(data) ? data.join('') : data) as string; @@ -138,7 +137,7 @@ class RichRenderer implements IOutputTransformContribution { return { type: RenderOutputType.None, hasDynamicHeight: true }; } - renderJavaScript(output: ITransformedDisplayOutputDto, container: HTMLElement): IRenderOutput { + renderJavaScript(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { const data = output.data['application/javascript']; const str = isArray(data) ? data.join('') : data; const scriptVal = ``; @@ -150,7 +149,7 @@ class RichRenderer implements IOutputTransformContribution { }; } - renderHTML(output: ITransformedDisplayOutputDto, container: HTMLElement): IRenderOutput { + renderHTML(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { const data = output.data['text/html']; const str = (isArray(data) ? data.join('') : data) as string; return { @@ -161,7 +160,7 @@ class RichRenderer implements IOutputTransformContribution { }; } - renderSVG(output: ITransformedDisplayOutputDto, container: HTMLElement): IRenderOutput { + renderSVG(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { const data = output.data['image/svg+xml']; const str = (isArray(data) ? data.join('') : data) as string; return { @@ -172,17 +171,18 @@ class RichRenderer implements IOutputTransformContribution { }; } - renderMarkdown(output: ITransformedDisplayOutputDto, container: HTMLElement): IRenderOutput { + renderMarkdown(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { const data = output.data['text/markdown']; const str = (isArray(data) ? data.join('') : data) as string; const mdOutput = document.createElement('div'); - mdOutput.appendChild(this._mdRenderer.render({ value: str, isTrusted: true, supportThemeIcons: true }).element); + const mdRenderer = this.instantiationService.createInstance(MarkdownRenderer, dirname(notebookUri)); + mdOutput.appendChild(mdRenderer.render({ value: str, isTrusted: true, supportThemeIcons: true }).element); container.appendChild(mdOutput); return { type: RenderOutputType.None, hasDynamicHeight: true }; } - renderPNG(output: ITransformedDisplayOutputDto, container: HTMLElement): IRenderOutput { + renderPNG(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { const image = document.createElement('img'); image.src = `data:image/png;base64,${output.data['image/png']}`; const display = document.createElement('div'); @@ -192,7 +192,7 @@ class RichRenderer implements IOutputTransformContribution { return { type: RenderOutputType.None, hasDynamicHeight: true }; } - renderJPEG(output: ITransformedDisplayOutputDto, container: HTMLElement): IRenderOutput { + renderJPEG(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { const image = document.createElement('img'); image.src = `data:image/jpeg;base64,${output.data['image/jpeg']}`; const display = document.createElement('div'); @@ -202,7 +202,7 @@ class RichRenderer implements IOutputTransformContribution { return { type: RenderOutputType.None, hasDynamicHeight: true }; } - renderPlainText(output: ITransformedDisplayOutputDto, container: HTMLElement): IRenderOutput { + renderPlainText(output: ITransformedDisplayOutputDto, notebookUri: URI, container: HTMLElement): IRenderOutput { const data = output.data['text/plain']; const str = (isArray(data) ? data.join('') : data) as string; const contentNode = DOM.$('.output-plaintext'); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellActionView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellActionView.ts index 3ff3869488..30cb5c3688 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellActionView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellActionView.ts @@ -45,7 +45,7 @@ function fillInActions(groups: ReadonlyArray<[string, ReadonlyArray(target) ? target : target.primary; + const to = Array.isArray(target) ? target : target.primary; if (to.length > 0) { to.push(new VerticalSeparator()); @@ -55,7 +55,7 @@ function fillInActions(groups: ReadonlyArray<[string, ReadonlyArray(target) ? target : target.secondary; + const to = Array.isArray(target) ? target : target.secondary; if (to.length > 0) { to.push(new Separator()); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys.ts index fabd1498c5..4534abd7b4 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys.ts @@ -6,7 +6,7 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { INotebookTextModel, NotebookCellRunState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { BaseCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel'; -import { NOTEBOOK_CELL_TYPE, NOTEBOOK_VIEW_TYPE, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_RUNNABLE, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_RUN_STATE, NOTEBOOK_CELL_HAS_OUTPUTS, CellViewModelStateChangeEvent, CellEditState, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_FOCUSED, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NOTEBOOK_CELL_TYPE, NOTEBOOK_VIEW_TYPE, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_RUNNABLE, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_RUN_STATE, NOTEBOOK_CELL_HAS_OUTPUTS, CellViewModelStateChangeEvent, CellEditState, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_FOCUSED, INotebookEditor, NOTEBOOK_CELL_EDITOR_FOCUSED, CellFocusMode } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; @@ -18,6 +18,7 @@ export class CellContextKeyManager extends Disposable { private cellEditable!: IContextKey; private cellRunnable!: IContextKey; private cellFocused!: IContextKey; + private cellEditorFocused!: IContextKey; private cellRunState!: IContextKey; private cellHasOutputs!: IContextKey; private cellContentCollapsed!: IContextKey; @@ -40,6 +41,7 @@ export class CellContextKeyManager extends Disposable { this.viewType = NOTEBOOK_VIEW_TYPE.bindTo(this.contextKeyService); this.cellEditable = NOTEBOOK_CELL_EDITABLE.bindTo(this.contextKeyService); this.cellFocused = NOTEBOOK_CELL_FOCUSED.bindTo(this.contextKeyService); + this.cellEditorFocused = NOTEBOOK_CELL_EDITOR_FOCUSED.bindTo(this.contextKeyService); this.cellRunnable = NOTEBOOK_CELL_RUNNABLE.bindTo(this.contextKeyService); this.markdownEditMode = NOTEBOOK_CELL_MARKDOWN_EDIT_MODE.bindTo(this.contextKeyService); this.cellRunState = NOTEBOOK_CELL_RUN_STATE.bindTo(this.contextKeyService); @@ -90,6 +92,10 @@ export class CellContextKeyManager extends Disposable { this.updateForEditState(); } + if (e.focusModeChanged) { + this.updateForFocusState(); + } + // if (e.collapseStateChanged) { // this.updateForCollapseState(); // } @@ -97,7 +103,15 @@ export class CellContextKeyManager extends Disposable { } private updateForFocusState() { + const activeCell = this.notebookEditor.getActiveCell(); this.cellFocused.set(this.notebookEditor.getActiveCell() === this.element); + + if (activeCell === this.element) { + this.cellEditorFocused.set(this.element.focusMode === CellFocusMode.Editor); + } else { + this.cellEditorFocused.set(false); + } + } private updateForMetadata() { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index 90d43cf53d..c9b6b165c6 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -10,7 +10,7 @@ import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/lis import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { IAction } from 'vs/base/common/actions'; -import { renderCodicons } from 'vs/base/common/codicons'; +import { renderCodiconsAsElement } from 'vs/base/browser/codicons'; import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -35,20 +35,21 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { BOTTOM_CELL_TOOLBAR_GAP, CELL_BOTTOM_MARGIN, CELL_TOP_MARGIN, EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; +import { BOTTOM_CELL_TOOLBAR_GAP, CELL_BOTTOM_MARGIN, CELL_TOP_MARGIN, EDITOR_BOTTOM_PADDING, EDITOR_BOTTOM_PADDING_WITHOUT_STATUSBAR, EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; import { CancelCellAction, DeleteCellAction, ExecuteCellAction, INotebookCellActionContext } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; import { BaseCellRenderTemplate, CellEditState, CodeCellRenderTemplate, EXPAND_CELL_CONTENT_COMMAND_ID, ICellViewModel, INotebookEditor, isCodeCellRenderTemplate, MarkdownCellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellContextKeyManager } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys'; import { CellMenus } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellMenus'; +import { CellEditorStatusBar } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets'; import { CodeCell } from 'vs/workbench/contrib/notebook/browser/view/renderers/codeCell'; +import { CodiconActionViewItem } from 'vs/workbench/contrib/notebook/browser/view/renderers/commonViewComponents'; +import { CellDragAndDropController, DRAGGING_CLASS } from 'vs/workbench/contrib/notebook/browser/view/renderers/dnd'; import { StatefulMarkdownCell } from 'vs/workbench/contrib/notebook/browser/view/renderers/markdownCell'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { CellKind, NotebookCellMetadata, NotebookCellRunState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, NotebookCellMetadata, NotebookCellRunState, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { createAndFillInActionBarActionsWithVerticalSeparators, VerticalSeparator, VerticalSeparatorViewItem } from './cellActionView'; -import { CodiconActionViewItem, CellLanguageStatusBarItem } from 'vs/workbench/contrib/notebook/browser/view/renderers/commonViewComponents'; -import { CellDragAndDropController, DRAGGING_CLASS } from 'vs/workbench/contrib/notebook/browser/view/renderers/dnd'; const $ = DOM.$; @@ -82,10 +83,6 @@ export class NotebookCellListDelegate implements IListVirtualDelegate { - if (e.affectsConfiguration('editor')) { + if (e.affectsConfiguration('editor') || e.affectsConfiguration(ShowCellStatusBarKey)) { this._value = computeEditorOptions(); this._onDidChange.fire(this.value); } }); const computeEditorOptions = () => { + const showCellStatusBar = configurationService.getValue(ShowCellStatusBarKey); + const editorPadding = { + top: EDITOR_TOP_PADDING, + bottom: showCellStatusBar ? EDITOR_BOTTOM_PADDING : EDITOR_BOTTOM_PADDING_WITHOUT_STATUSBAR + }; + const editorOptions = deepClone(configurationService.getValue('editor', { overrideIdentifier: language })); const computed = { ...editorOptions, - ...CellEditorOptions.fixedEditorOptions + ...CellEditorOptions.fixedEditorOptions, + ...{ padding: editorPadding } }; if (!computed.folding) { @@ -329,16 +333,15 @@ abstract class AbstractCellRenderer { } if (templateData.currentRenderedCell.metadata?.inputCollapsed) { - this.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(templateData.currentRenderedCell.handle, { inputCollapsed: false }); + this.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(templateData.currentRenderedCell.handle, { inputCollapsed: false }); } else if (templateData.currentRenderedCell.metadata?.outputCollapsed) { - this.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(templateData.currentRenderedCell.handle, { outputCollapsed: false }); + this.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(templateData.currentRenderedCell.handle, { outputCollapsed: false }); } })); } protected setupCollapsedPart(container: HTMLElement): { collapsedPart: HTMLElement, expandButton: HTMLElement } { - const collapsedPart = DOM.append(container, $('.cell.cell-collapsed-part')); - collapsedPart.innerHTML = renderCodicons('$(unfold)'); + const collapsedPart = DOM.append(container, $('.cell.cell-collapsed-part', undefined, ...renderCodiconsAsElement('$(unfold)'))); const expandButton = collapsedPart.querySelector('.codicon') as HTMLElement; const keybinding = this.keybindingService.lookupKeybinding(EXPAND_CELL_CONTENT_COMMAND_ID); let title = localize('cellExpandButtonLabel', "Expand"); @@ -379,7 +382,7 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR const container = DOM.append(rootContainer, DOM.$('.cell-inner-container')); const disposables = new DisposableStore(); const contextKeyService = disposables.add(this.contextKeyServiceProvider(container)); - + const decorationContainer = DOM.append(container, $('.cell-decoration')); const titleToolbarContainer = DOM.append(container, $('.cell-title-toolbar')); const toolbar = disposables.add(this.createToolbar(titleToolbarContainer)); const deleteToolbar = disposables.add(this.createToolbar(titleToolbarContainer, 'cell-delete-toolbar')); @@ -400,7 +403,7 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR const bottomCellContainer = DOM.append(container, $('.cell-bottom-toolbar-container')); const betweenCellToolbar = disposables.add(this.createBetweenCellToolbar(bottomCellContainer, disposables, contextKeyService)); - const statusBar = this.instantiationService.createInstance(CellEditorStatusBar, editorPart); + const statusBar = disposables.add(this.instantiationService.createInstance(CellEditorStatusBar, editorPart)); const titleMenu = disposables.add(this.cellMenus.getCellTitleMenu(contextKeyService)); const templateData: MarkdownCellRenderTemplate = { @@ -408,6 +411,7 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR expandButton, contextKeyService, container, + decorationContainer, cellContainer: innerContent, editorPart, editorContainer, @@ -419,9 +423,8 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR deleteToolbar, betweenCellToolbar, bottomCellContainer, - statusBarContainer: statusBar.statusBarContainer, - languageStatusBarItem: statusBar.languageStatusBarItem, titleMenu, + statusBar, toJSON: () => { return {}; } }; this.dndController.registerDragHandle(templateData, rootContainer, container, () => this.getDragImage(templateData)); @@ -490,7 +493,7 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR elementDisposables.add(this.editorOptions.onDidChange(newValue => markdownCell.updateEditorOptions(newValue))); elementDisposables.add(markdownCell); - templateData.languageStatusBarItem.update(element, this.notebookEditor); + templateData.statusBar.update(toolbarContext); } disposeTemplate(templateData: MarkdownCellRenderTemplate): void { @@ -602,27 +605,6 @@ class CodeCellDragImageRenderer { } } -class CellEditorStatusBar { - readonly cellStatusMessageContainer: HTMLElement; - readonly cellRunStatusContainer: HTMLElement; - readonly statusBarContainer: HTMLElement; - readonly languageStatusBarItem: CellLanguageStatusBarItem; - readonly durationContainer: HTMLElement; - - constructor( - container: HTMLElement, - @IInstantiationService instantiationService: IInstantiationService - ) { - this.statusBarContainer = DOM.append(container, $('.cell-statusbar-container')); - const leftStatusBarItems = DOM.append(this.statusBarContainer, $('.cell-status-left')); - const rightStatusBarItems = DOM.append(this.statusBarContainer, $('.cell-status-right')); - this.cellRunStatusContainer = DOM.append(leftStatusBarItems, $('.cell-run-status')); - this.durationContainer = DOM.append(leftStatusBarItems, $('.cell-run-duration')); - this.cellStatusMessageContainer = DOM.append(leftStatusBarItems, $('.cell-status-message')); - this.languageStatusBarItem = instantiationService.createInstance(CellLanguageStatusBarItem, rightStatusBarItems); - } -} - export class CodeCellRenderer extends AbstractCellRenderer implements IListRenderer { static readonly TEMPLATE_ID = 'code_cell'; @@ -649,7 +631,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende const container = DOM.append(rootContainer, DOM.$('.cell-inner-container')); const disposables = new DisposableStore(); const contextKeyService = disposables.add(this.contextKeyServiceProvider(container)); - + const decorationContainer = DOM.append(container, $('.cell-decoration')); DOM.append(container, $('.cell-focus-indicator.cell-focus-indicator-top')); const titleToolbarContainer = DOM.append(container, $('.cell-title-toolbar')); const toolbar = disposables.add(this.createToolbar(titleToolbarContainer)); @@ -678,7 +660,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende width: 0, height: 0 }, - overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode() + // overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode() }, {}); disposables.add(this.editorOptions.onDidChange(newValue => editor.updateOptions(newValue))); @@ -689,7 +671,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende progressBar.hide(); disposables.add(progressBar); - const statusBar = this.instantiationService.createInstance(CellEditorStatusBar, editorPart); + const statusBar = disposables.add(this.instantiationService.createInstance(CellEditorStatusBar, editorPart)); const timer = new TimerRenderer(statusBar.durationContainer); const cellRunState = new RunStateRenderer(statusBar.cellRunStatusContainer, runToolbar, this.instantiationService); @@ -711,12 +693,11 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende expandButton, contextKeyService, container, + decorationContainer, cellContainer, - statusBarContainer: statusBar.statusBarContainer, cellRunState, - cellStatusMessageContainer: statusBar.cellStatusMessageContainer, - languageStatusBarItem: statusBar.languageStatusBarItem, progressBar, + statusBar, focusIndicatorLeft: focusIndicator, focusIndicatorRight, focusIndicatorBottom, @@ -761,9 +742,9 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende private updateForMetadata(element: CodeCellViewModel, templateData: CodeCellRenderTemplate): void { const metadata = element.getEvaluatedMetadata(this.notebookEditor.viewModel!.notebookDocument.metadata); - DOM.toggleClass(templateData.cellContainer, 'runnable', !!metadata.runnable); + DOM.toggleClass(templateData.container, 'runnable', !!metadata.runnable); this.updateExecutionOrder(metadata, templateData); - templateData.cellStatusMessageContainer.textContent = metadata?.statusMessage || ''; + templateData.statusBar.cellStatusMessageContainer.textContent = metadata?.statusMessage || ''; templateData.cellRunState.renderState(element.metadata?.runState); @@ -877,7 +858,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende this.setBetweenCellToolbarContext(templateData, element, toolbarContext); - templateData.languageStatusBarItem.update(element, this.notebookEditor); + templateData.statusBar.update(toolbarContext); } disposeTemplate(templateData: CodeCellRenderTemplate): void { @@ -967,11 +948,11 @@ export class RunStateRenderer { } if (runState === NotebookCellRunState.Success) { - this.element.innerHTML = renderCodicons('$(check)'); + DOM.reset(this.element, ...renderCodiconsAsElement('$(check)')); } else if (runState === NotebookCellRunState.Error) { - this.element.innerHTML = renderCodicons('$(error)'); + DOM.reset(this.element, ...renderCodiconsAsElement('$(error)')); } else if (runState === NotebookCellRunState.Running) { - this.element.innerHTML = renderCodicons('$(sync~spin)'); + DOM.reset(this.element, ...renderCodiconsAsElement('$(sync~spin)')); this.spinnerTimer = setTimeout(() => { this.spinnerTimer = undefined; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets.ts new file mode 100644 index 0000000000..01bca607db --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets.ts @@ -0,0 +1,217 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from 'vs/base/browser/dom'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { CodiconLabel } from 'vs/base/browser/ui/codicons/codiconLabel'; +import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; +import { stripCodicons } from 'vs/base/common/codicons'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { extUri } from 'vs/base/common/resources'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { localize } from 'vs/nls'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { ChangeCellLanguageAction, INotebookCellActionContext } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; +import { ICellViewModel, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; +import { CellKind, CellStatusbarAlignment, INotebookCellStatusBarEntry } from 'vs/workbench/contrib/notebook/common/notebookCommon'; + +const $ = DOM.$; + +export class CellEditorStatusBar extends Disposable { + readonly cellStatusMessageContainer: HTMLElement; + readonly cellRunStatusContainer: HTMLElement; + readonly statusBarContainer: HTMLElement; + readonly languageStatusBarItem: CellLanguageStatusBarItem; + readonly durationContainer: HTMLElement; + + private readonly leftContributedItemsContainer: HTMLElement; + private readonly rightContributedItemsContainer: HTMLElement; + private readonly itemsDisposable: DisposableStore; + + private currentContext: INotebookCellActionContext | undefined; + + constructor( + container: HTMLElement, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @INotebookCellStatusBarService private readonly notebookCellStatusBarService: INotebookCellStatusBarService + ) { + super(); + this.statusBarContainer = DOM.append(container, $('.cell-statusbar-container')); + const leftItemsContainer = DOM.append(this.statusBarContainer, $('.cell-status-left')); + const rightItemsContainer = DOM.append(this.statusBarContainer, $('.cell-status-right')); + this.cellRunStatusContainer = DOM.append(leftItemsContainer, $('.cell-run-status')); + this.durationContainer = DOM.append(leftItemsContainer, $('.cell-run-duration')); + this.cellStatusMessageContainer = DOM.append(leftItemsContainer, $('.cell-status-message')); + this.leftContributedItemsContainer = DOM.append(leftItemsContainer, $('.cell-contributed-items.cell-contributed-items-left')); + this.rightContributedItemsContainer = DOM.append(rightItemsContainer, $('.cell-contributed-items.cell-contributed-items-right')); + this.languageStatusBarItem = instantiationService.createInstance(CellLanguageStatusBarItem, rightItemsContainer); + + this.itemsDisposable = this._register(new DisposableStore()); + this._register(this.notebookCellStatusBarService.onDidChangeEntriesForCell(e => { + if (this.currentContext && extUri.isEqual(e, this.currentContext.cell.uri)) { + this.updateStatusBarItems(); + } + })); + } + + update(context: INotebookCellActionContext) { + this.currentContext = context; + this.languageStatusBarItem.update(context.cell, context.notebookEditor); + this.updateStatusBarItems(); + } + + layout(width: number): void { + this.statusBarContainer.style.width = `${width}px`; + } + + private updateStatusBarItems() { + if (!this.currentContext) { + return; + } + + this.leftContributedItemsContainer.innerHTML = ''; + this.rightContributedItemsContainer.innerHTML = ''; + this.itemsDisposable.clear(); + + const items = this.notebookCellStatusBarService.getEntries(this.currentContext.cell.uri); + items.sort((itemA, itemB) => { + return (itemB.priority ?? 0) - (itemA.priority ?? 0); + }); + items.forEach(item => { + const itemView = this.itemsDisposable.add(this.instantiationService.createInstance(CellStatusBarItem, this.currentContext!, item)); + if (item.alignment === CellStatusbarAlignment.LEFT) { + this.leftContributedItemsContainer.appendChild(itemView.container); + } else { + this.rightContributedItemsContainer.appendChild(itemView.container); + } + }); + } +} + +class CellStatusBarItem extends Disposable { + + readonly container = $('.cell-status-item'); + + constructor( + private readonly _context: INotebookCellActionContext, + private readonly _itemModel: INotebookCellStatusBarEntry, + @ITelemetryService private readonly telemetryService: ITelemetryService, + @ICommandService private readonly commandService: ICommandService, + @INotificationService private readonly notificationService: INotificationService + ) { + super(); + new CodiconLabel(this.container).text = this._itemModel.text; + + let ariaLabel: string; + let role: string | undefined; + if (this._itemModel.accessibilityInformation) { + ariaLabel = this._itemModel.accessibilityInformation.label; + role = this._itemModel.accessibilityInformation.role; + } else { + ariaLabel = this._itemModel.text ? stripCodicons(this._itemModel.text).trim() : ''; + } + + if (ariaLabel) { + this.container.setAttribute('aria-label', ariaLabel); + } + + if (role) { + this.container.setAttribute('role', role); + } + + this.container.title = this._itemModel.tooltip ?? ''; + + if (this._itemModel.command) { + this.container.classList.add('cell-status-item-has-command'); + this.container.tabIndex = 0; + + this._register(DOM.addDisposableListener(this.container, DOM.EventType.CLICK, _e => { + this.executeCommand(); + })); + this._register(DOM.addDisposableListener(this.container, DOM.EventType.KEY_UP, e => { + const event = new StandardKeyboardEvent(e); + if (event.equals(KeyCode.Space) || event.equals(KeyCode.Enter)) { + this.executeCommand(); + } + })); + } + } + + private async executeCommand(): Promise { + const command = this._itemModel.command; + if (!command) { + return; + } + + const id = typeof command === 'string' ? command : command.id; + const args = typeof command === 'string' ? [] : command.arguments ?? []; + + args.unshift(this._context); + + this.telemetryService.publicLog2('workbenchActionExecuted', { id, from: 'cell status bar' }); + try { + await this.commandService.executeCommand(id, ...args); + } catch (error) { + this.notificationService.error(toErrorMessage(error)); + } + } +} + +export class CellLanguageStatusBarItem extends Disposable { + private readonly labelElement: HTMLElement; + + private cell: ICellViewModel | undefined; + private editor: INotebookEditor | undefined; + + private cellDisposables: DisposableStore; + + constructor( + readonly container: HTMLElement, + @IModeService private readonly modeService: IModeService, + @IInstantiationService private readonly instantiationService: IInstantiationService + ) { + super(); + this.labelElement = DOM.append(container, $('.cell-language-picker.cell-status-item')); + this.labelElement.tabIndex = 0; + + this._register(DOM.addDisposableListener(this.labelElement, DOM.EventType.CLICK, () => { + this.run(); + })); + this._register(DOM.addDisposableListener(this.labelElement, DOM.EventType.KEY_UP, e => { + const event = new StandardKeyboardEvent(e); + if (event.equals(KeyCode.Space) || event.equals(KeyCode.Enter)) { + this.run(); + } + })); + this._register(this.cellDisposables = new DisposableStore()); + } + + private run() { + this.instantiationService.invokeFunction(accessor => { + new ChangeCellLanguageAction().run(accessor, { notebookEditor: this.editor!, cell: this.cell! }); + }); + } + + update(cell: ICellViewModel, editor: INotebookEditor): void { + this.cellDisposables.clear(); + this.cell = cell; + this.editor = editor; + + this.render(); + this.cellDisposables.add(this.cell.model.onDidChangeLanguage(() => this.render())); + } + + private render(): void { + const modeId = this.cell?.cellKind === CellKind.Markdown ? 'markdown' : this.modeService.getModeIdForLanguageName(this.cell!.language) || this.cell!.language; + this.labelElement.textContent = this.modeService.getLanguageName(modeId) || this.modeService.getLanguageName('plaintext'); + this.labelElement.title = localize('notebook.cell.status.language', "Select Cell Language Mode"); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts index beb8df13a9..3cc12531e3 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts @@ -4,21 +4,22 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { raceCancellation } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { IDimension } from 'vs/editor/common/editorCommon'; import { IModeService } from 'vs/editor/common/services/modeService'; import * as nls from 'vs/nls'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { EDITOR_BOTTOM_PADDING, EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; import { CellFocusMode, CodeCellRenderTemplate, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { getResizesObserver } from 'vs/workbench/contrib/notebook/browser/view/renderers/sizeObserver'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; -import { CellOutputKind, IProcessedOutput, IRenderOutput, ITransformedDisplayOutputDto, BUILTIN_RENDERER_ID, RenderOutputType, outputHasDynamicHeight } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { KeyCode } from 'vs/base/common/keyCodes'; -import { IDimension } from 'vs/editor/common/editorCommon'; +import { BUILTIN_RENDERER_ID, CellOutputKind, CellUri, IInsetRenderOutput, IProcessedOutput, IRenderOutput, ITransformedDisplayOutputDto, outputHasDynamicHeight, RenderOutputType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; interface IMimeTypeRenderer extends IQuickPickItem { index: number; @@ -32,6 +33,7 @@ interface IRenderedOutput { export class CodeCell extends Disposable { private outputResizeListeners = new Map(); private outputElements = new Map(); + constructor( private notebookEditor: INotebookEditor, private viewCell: CodeCellViewModel, @@ -193,7 +195,7 @@ export class CodeCell extends Disposable { // newly added element const currIndex = this.viewCell.outputs.indexOf(output); this.renderOutput(output, currIndex, prevElement); - prevElement = this.outputElements.get(output)!.element; + prevElement = this.outputElements.get(output)?.element; }); const editorHeight = templateData.editor!.getContentHeight(); @@ -323,13 +325,14 @@ export class CodeCell extends Disposable { const renderedOutput = this.outputElements.get(currOutput); if (renderedOutput) { if (renderedOutput.renderResult.type !== RenderOutputType.None) { - // Show inset in webview, or render output that isn't rendered - // TODO@roblou skipHeightInit flag is a hack - the webview only sends the real height once. Don't wipe it out here. - this.renderOutput(currOutput, index, undefined, true); + this.notebookEditor.createInset(this.viewCell, renderedOutput.renderResult as IInsetRenderOutput, this.viewCell.getOutputOffset(index)); } else { // Anything else, just update the height this.viewCell.updateOutputHeight(index, renderedOutput.element.clientHeight); } + } else { + // Wasn't previously rendered, render it now + this.renderOutput(currOutput, index); } } @@ -338,6 +341,7 @@ export class CodeCell extends Disposable { private viewUpdateInputCollapsed(): void { DOM.hide(this.templateData.cellContainer); + DOM.hide(this.templateData.runButtonContainer); DOM.show(this.templateData.collapsedPart); DOM.show(this.templateData.outputContainer); this.templateData.container.classList.toggle('collapsed', true); @@ -355,6 +359,7 @@ export class CodeCell extends Disposable { private viewUpdateOutputCollapsed(): void { DOM.show(this.templateData.cellContainer); + DOM.show(this.templateData.runButtonContainer); DOM.show(this.templateData.collapsedPart); DOM.hide(this.templateData.outputContainer); @@ -368,6 +373,7 @@ export class CodeCell extends Disposable { private viewUpdateAllCollapsed(): void { DOM.hide(this.templateData.cellContainer); + DOM.hide(this.templateData.runButtonContainer); DOM.show(this.templateData.collapsedPart); DOM.hide(this.templateData.outputContainer); this.templateData.container.classList.toggle('collapsed', true); @@ -382,6 +388,7 @@ export class CodeCell extends Disposable { private viewUpdateExpanded(): void { DOM.show(this.templateData.cellContainer); + DOM.show(this.templateData.runButtonContainer); DOM.hide(this.templateData.collapsedPart); DOM.show(this.templateData.outputContainer); this.templateData.container.classList.toggle('collapsed', false); @@ -394,7 +401,7 @@ export class CodeCell extends Disposable { private layoutEditor(dimension: IDimension): void { this.templateData.editor?.layout(dimension); - this.templateData.statusBarContainer.style.width = `${dimension.width}px`; + this.templateData.statusBar.layout(dimension.width); } private onCellWidthChange(): void { @@ -430,7 +437,15 @@ export class CodeCell extends Disposable { ); } - private renderOutput(currOutput: IProcessedOutput, index: number, beforeElement?: HTMLElement, skipHeightInit = false) { + private getNotebookUri(): URI | undefined { + return CellUri.parse(this.viewCell.uri)?.notebook; + } + + private renderOutput(currOutput: IProcessedOutput, index: number, beforeElement?: HTMLElement) { + if (this.viewCell.metadata.outputCollapsed) { + return; + } + if (!this.outputResizeListeners.has(currOutput)) { this.outputResizeListeners.set(currOutput, new DisposableStore()); } @@ -476,16 +491,16 @@ export class CodeCell extends Disposable { const renderer = this.notebookService.getRendererInfo(pickedMimeTypeRenderer.rendererId); result = renderer ? { type: RenderOutputType.Extension, renderer, source: currOutput, mimeType: pickedMimeTypeRenderer.mimeType } - : this.notebookEditor.getOutputRenderer().render(currOutput, innerContainer, pickedMimeTypeRenderer.mimeType); + : this.notebookEditor.getOutputRenderer().render(currOutput, innerContainer, pickedMimeTypeRenderer.mimeType, this.getNotebookUri(),); } else { - result = this.notebookEditor.getOutputRenderer().render(currOutput, innerContainer, pickedMimeTypeRenderer.mimeType); + result = this.notebookEditor.getOutputRenderer().render(currOutput, innerContainer, pickedMimeTypeRenderer.mimeType, this.getNotebookUri(),); } } else { // for text and error, there is no mimetype const innerContainer = DOM.$('.output-inner-container'); DOM.append(outputItemDiv, innerContainer); - result = this.notebookEditor.getOutputRenderer().render(currOutput, innerContainer, undefined); + result = this.notebookEditor.getOutputRenderer().render(currOutput, innerContainer, undefined, this.getNotebookUri(),); } if (!result) { @@ -503,49 +518,47 @@ export class CodeCell extends Disposable { if (result.type !== RenderOutputType.None) { this.viewCell.selfSizeMonitoring = true; - this.notebookEditor.createInset(this.viewCell, result, this.viewCell.getOutputOffset(index)); + this.notebookEditor.createInset(this.viewCell, result as any, this.viewCell.getOutputOffset(index)); } else { DOM.addClass(outputItemDiv, 'foreground'); DOM.addClass(outputItemDiv, 'output-element'); outputItemDiv.style.position = 'absolute'; } - if (!skipHeightInit) { - if (outputHasDynamicHeight(result)) { - this.viewCell.selfSizeMonitoring = true; + if (outputHasDynamicHeight(result)) { + this.viewCell.selfSizeMonitoring = true; - const clientHeight = outputItemDiv.clientHeight; - const dimension = { - width: this.viewCell.layoutInfo.editorWidth, - height: clientHeight - }; - const elementSizeObserver = getResizesObserver(outputItemDiv, dimension, () => { - if (this.templateData.outputContainer && document.body.contains(this.templateData.outputContainer!)) { - const height = Math.ceil(elementSizeObserver.getHeight()); + const clientHeight = outputItemDiv.clientHeight; + const dimension = { + width: this.viewCell.layoutInfo.editorWidth, + height: clientHeight + }; + const elementSizeObserver = getResizesObserver(outputItemDiv, dimension, () => { + if (this.templateData.outputContainer && document.body.contains(this.templateData.outputContainer!)) { + const height = Math.ceil(elementSizeObserver.getHeight()); - if (clientHeight === height) { - return; - } - - const currIndex = this.viewCell.outputs.indexOf(currOutput); - if (currIndex < 0) { - return; - } - - this.viewCell.updateOutputHeight(currIndex, height); - this.relayoutCell(); + if (clientHeight === height) { + return; } - }); - elementSizeObserver.startObserving(); - this.outputResizeListeners.get(currOutput)!.add(elementSizeObserver); - this.viewCell.updateOutputHeight(index, clientHeight); - } else if (result.type !== RenderOutputType.None) { // no-op if it's a webview - const clientHeight = Math.ceil(outputItemDiv.clientHeight); - this.viewCell.updateOutputHeight(index, clientHeight); - const top = this.viewCell.getOutputOffsetInContainer(index); - outputItemDiv.style.top = `${top}px`; - } + const currIndex = this.viewCell.outputs.indexOf(currOutput); + if (currIndex < 0) { + return; + } + + this.viewCell.updateOutputHeight(currIndex, height); + this.relayoutCell(); + } + }); + elementSizeObserver.startObserving(); + this.outputResizeListeners.get(currOutput)!.add(elementSizeObserver); + this.viewCell.updateOutputHeight(index, clientHeight); + } else if (result.type === RenderOutputType.None) { // no-op if it's a webview + const clientHeight = Math.ceil(outputItemDiv.clientHeight); + this.viewCell.updateOutputHeight(index, clientHeight); + + const top = this.viewCell.getOutputOffsetInContainer(index); + outputItemDiv.style.top = `${top}px`; } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/commonViewComponents.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/commonViewComponents.ts index 8297075678..6f66b8d30f 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/commonViewComponents.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/commonViewComponents.ts @@ -3,22 +3,13 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from 'vs/nls'; import * as DOM from 'vs/base/browser/dom'; import { MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { MenuItemAction } from 'vs/platform/actions/common/actions'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { renderCodicons } from 'vs/base/common/codicons'; -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { ICellViewModel, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { IModeService } from 'vs/editor/common/services/modeService'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ChangeCellLanguageAction } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; -import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; - -const $ = DOM.$; +import { renderCodiconsAsElement } from 'vs/base/browser/codicons'; export class CodiconActionViewItem extends MenuEntryActionViewItem { constructor( @@ -31,49 +22,7 @@ export class CodiconActionViewItem extends MenuEntryActionViewItem { } updateLabel(): void { if (this.options.label && this.label) { - this.label.innerHTML = renderCodicons(this._commandAction.label ?? ''); + DOM.reset(this.label, ...renderCodiconsAsElement(this._commandAction.label ?? '')); } } } - - -export class CellLanguageStatusBarItem extends Disposable { - private readonly labelElement: HTMLElement; - - private cell: ICellViewModel | undefined; - private editor: INotebookEditor | undefined; - - private cellDisposables: DisposableStore; - - constructor( - readonly container: HTMLElement, - @IModeService private readonly modeService: IModeService, - @IInstantiationService private readonly instantiationService: IInstantiationService - ) { - super(); - this.labelElement = DOM.append(container, $('.cell-language-picker')); - this.labelElement.tabIndex = 0; - - this._register(DOM.addDisposableListener(this.labelElement, DOM.EventType.CLICK, () => { - this.instantiationService.invokeFunction(accessor => { - new ChangeCellLanguageAction().run(accessor, { notebookEditor: this.editor!, cell: this.cell! }); - }); - })); - this._register(this.cellDisposables = new DisposableStore()); - } - - update(cell: ICellViewModel, editor: INotebookEditor): void { - this.cellDisposables.clear(); - this.cell = cell; - this.editor = editor; - - this.render(); - this.cellDisposables.add(this.cell.model.onDidChangeLanguage(() => this.render())); - } - - private render(): void { - const modeId = this.cell?.cellKind === CellKind.Markdown ? 'markdown' : this.modeService.getModeIdForLanguageName(this.cell!.language) || this.cell!.language; - this.labelElement.textContent = this.modeService.getLanguageName(modeId) || this.modeService.getLanguageName('plaintext'); - this.labelElement.title = localize('notebook.cell.status.language', "Select Cell Language Mode"); - } -} diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts index 2d6a1c02d1..baf76da88f 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts @@ -6,7 +6,7 @@ import * as DOM from 'vs/base/browser/dom'; import { raceCancellation } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { renderCodicons } from 'vs/base/common/codicons'; +import { renderCodiconsAsElement } from 'vs/base/browser/codicons'; import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; @@ -190,7 +190,7 @@ export class StatefulMarkdownCell extends Disposable { width: width, height: editorHeight }, - overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode() + // overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode() }, {}); this.templateData.currentEditor = this.editor; @@ -282,7 +282,7 @@ export class StatefulMarkdownCell extends Disposable { private layoutEditor(dimension: DOM.IDimension): void { this.editor?.layout(dimension); - this.templateData.statusBarContainer.style.width = `${dimension.width}px`; + this.templateData.statusBar.layout(dimension.width); } private onCellEditorWidthChange(): void { @@ -315,10 +315,10 @@ export class StatefulMarkdownCell extends Disposable { this.templateData.foldingIndicator.innerText = ''; break; case CellFoldingState.Collapsed: - this.templateData.foldingIndicator.innerHTML = renderCodicons('$(chevron-right)'); + DOM.reset(this.templateData.foldingIndicator, ...renderCodiconsAsElement('$(chevron-right)')); break; case CellFoldingState.Expanded: - this.templateData.foldingIndicator.innerHTML = renderCodicons('$(chevron-down)'); + DOM.reset(this.templateData.foldingIndicator, ...renderCodiconsAsElement('$(chevron-down)')); break; default: diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts index 0cd8316fa5..3f9841a66a 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts @@ -12,12 +12,14 @@ import { IPosition } from 'vs/editor/common/core/position'; import * as editorCommon from 'vs/editor/common/editorCommon'; import * as model from 'vs/editor/common/model'; import { SearchParams } from 'vs/editor/common/model/textModelSearch'; -import { EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; +import { CELL_STATUSBAR_HEIGHT, EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; import { CellEditState, CellFocusMode, CursorAtBoundary, CellViewModelStateChangeEvent, IEditableCellViewModel, INotebookCellDecorationOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { CellKind, NotebookCellMetadata, NotebookDocumentMetadata, INotebookSearchOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, NotebookCellMetadata, NotebookDocumentMetadata, INotebookSearchOptions, ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export abstract class BaseCellViewModel extends Disposable { + protected readonly _onDidChangeEditorAttachState = new Emitter(); // Do not merge this event with `onDidChangeState` as we are using `Event.once(onDidChangeEditorAttachState)` elsewhere. readonly onDidChangeEditorAttachState = this._onDidChangeEditorAttachState.event; @@ -106,7 +108,12 @@ export abstract class BaseCellViewModel extends Disposable { this._dragging = v; } - constructor(readonly viewType: string, readonly model: NotebookCellTextModel, public id: string) { + constructor( + readonly viewType: string, + readonly model: NotebookCellTextModel, + public id: string, + private readonly _configurationService: IConfigurationService + ) { super(); this._register(model.onDidChangeLanguage(() => { @@ -116,12 +123,24 @@ export abstract class BaseCellViewModel extends Disposable { this._register(model.onDidChangeMetadata(() => { this._onDidChangeState.fire({ metadataChanged: true }); })); + + this._register(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(ShowCellStatusBarKey)) { + this.layoutChange({}); + } + })); + } + + protected getEditorStatusbarHeight() { + const showCellStatusBar = this._configurationService.getValue(ShowCellStatusBarKey); + return showCellStatusBar ? CELL_STATUSBAR_HEIGHT : 0; } // abstract resolveTextModel(): Promise; abstract hasDynamicHeight(): boolean; abstract getHeight(lineHeight: number): number; abstract onDeselect(): void; + abstract layoutChange(change: any): void; assertTextModelAttached(): boolean { if (this.textModel && this._textEditor && this._textEditor.getModel() === this.textModel) { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts index c9c03a1825..23595a8f9a 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts @@ -8,12 +8,13 @@ import * as UUID from 'vs/base/common/uuid'; import * as editorCommon from 'vs/editor/common/editorCommon'; import * as model from 'vs/editor/common/model'; import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; -import { BOTTOM_CELL_TOOLBAR_GAP, CELL_MARGIN, CELL_RUN_GUTTER, CELL_STATUSBAR_HEIGHT, EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT, CELL_TOP_MARGIN, EDITOR_TOP_PADDING, CELL_BOTTOM_MARGIN, CODE_CELL_LEFT_MARGIN, BOTTOM_CELL_TOOLBAR_HEIGHT, COLLAPSED_INDICATOR_HEIGHT } from 'vs/workbench/contrib/notebook/browser/constants'; -import { CellEditState, CellFindMatch, CodeCellLayoutChangeEvent, CodeCellLayoutInfo, ICellViewModel, NotebookLayoutInfo, CodeCellLayoutState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { CellKind, NotebookCellOutputsSplice, INotebookSearchOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { BaseCellViewModel } from './baseCellViewModel'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { BOTTOM_CELL_TOOLBAR_GAP, BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_BOTTOM_MARGIN, CELL_MARGIN, CELL_RUN_GUTTER, CELL_TOP_MARGIN, CODE_CELL_LEFT_MARGIN, COLLAPSED_INDICATOR_HEIGHT, EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; +import { CellEditState, CellFindMatch, CodeCellLayoutChangeEvent, CodeCellLayoutInfo, CodeCellLayoutState, ICellViewModel, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; +import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; +import { CellKind, INotebookSearchOptions, NotebookCellOutputsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { BaseCellViewModel } from './baseCellViewModel'; export class CodeCellViewModel extends BaseCellViewModel implements ICellViewModel { readonly cellKind = CellKind.Code; @@ -68,9 +69,10 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod readonly viewType: string, readonly model: NotebookCellTextModel, initialNotebookLayoutInfo: NotebookLayoutInfo | null, - readonly eventDispatcher: NotebookEventDispatcher + readonly eventDispatcher: NotebookEventDispatcher, + @IConfigurationService configurationService: IConfigurationService ) { - super(viewType, model, UUID.generateUuid()); + super(viewType, model, UUID.generateUuid(), configurationService); this._register(this.model.onDidChangeOutputs((splices) => { this._outputCollection = new Array(this.model.outputs.length); this._outputsTop = null; @@ -121,8 +123,9 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod newState = CodeCellLayoutState.Estimated; } - const indicatorHeight = editorHeight + CELL_STATUSBAR_HEIGHT + outputTotalHeight; - const outputContainerOffset = EDITOR_TOOLBAR_HEIGHT + CELL_TOP_MARGIN + editorHeight + CELL_STATUSBAR_HEIGHT; + const statusbarHeight = this.getEditorStatusbarHeight(); + const indicatorHeight = editorHeight + statusbarHeight + outputTotalHeight; + const outputContainerOffset = EDITOR_TOOLBAR_HEIGHT + CELL_TOP_MARGIN + editorHeight + statusbarHeight; const bottomToolbarOffset = totalHeight - BOTTOM_CELL_TOOLBAR_GAP - BOTTOM_CELL_TOOLBAR_HEIGHT / 2; const editorWidth = state.outerWidth !== undefined ? this.computeEditorWidth(state.outerWidth) : this._layoutInfo?.editorWidth; @@ -209,7 +212,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod } private computeTotalHeight(editorHeight: number, outputsTotalHeight: number): number { - return EDITOR_TOOLBAR_HEIGHT + CELL_TOP_MARGIN + editorHeight + CELL_STATUSBAR_HEIGHT + outputsTotalHeight + BOTTOM_CELL_TOOLBAR_GAP + CELL_BOTTOM_MARGIN; + return EDITOR_TOOLBAR_HEIGHT + CELL_TOP_MARGIN + editorHeight + this.getEditorStatusbarHeight() + outputsTotalHeight + BOTTOM_CELL_TOOLBAR_GAP + CELL_BOTTOM_MARGIN; } /** @@ -221,8 +224,10 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod this.textModel = ref.object.textEditorModel; this._register(ref); this._register(this.textModel.onDidChangeContent(() => { - this.editState = CellEditState.Editing; - this._onDidChangeState.fire({ contentChanged: true }); + if (this.editState !== CellEditState.Editing) { + this.editState = CellEditState.Editing; + this._onDidChangeState.fire({ contentChanged: true }); + } })); } diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher.ts index 99d70d5953..2ecf109fe4 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher.ts @@ -70,3 +70,24 @@ export class NotebookEventDispatcher { } } } + +export class NotebookDiffEditorEventDispatcher { + protected readonly _onDidChangeLayout = new Emitter(); + readonly onDidChangeLayout = this._onDidChangeLayout.event; + + constructor() { + } + + emit(events: NotebookViewEvent[]) { + for (let i = 0, len = events.length; i < len; i++) { + const e = events[i]; + + switch (e.type) { + case NotebookViewEventType.LayoutChanged: + this._onDidChangeLayout.fire(e); + break; + } + } + } + +} diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts index 250365312b..d73fdb3292 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts @@ -3,19 +3,20 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import { Emitter, Event } from 'vs/base/common/event'; import * as UUID from 'vs/base/common/uuid'; import * as editorCommon from 'vs/editor/common/editorCommon'; import * as model from 'vs/editor/common/model'; -import { BOTTOM_CELL_TOOLBAR_GAP, CELL_MARGIN, CELL_STATUSBAR_HEIGHT, CELL_TOP_MARGIN, CELL_BOTTOM_MARGIN, CODE_CELL_LEFT_MARGIN, BOTTOM_CELL_TOOLBAR_HEIGHT, COLLAPSED_INDICATOR_HEIGHT } from 'vs/workbench/contrib/notebook/browser/constants'; +import * as nls from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { BOTTOM_CELL_TOOLBAR_GAP, BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_BOTTOM_MARGIN, CELL_MARGIN, CELL_TOP_MARGIN, CODE_CELL_LEFT_MARGIN, COLLAPSED_INDICATOR_HEIGHT } from 'vs/workbench/contrib/notebook/browser/constants'; +import { EditorFoldingStateDelegate } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel'; import { CellFindMatch, ICellViewModel, MarkdownCellLayoutChangeEvent, MarkdownCellLayoutInfo, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { MarkdownRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/mdRenderer'; import { BaseCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel'; -import { EditorFoldingStateDelegate } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel'; +import { NotebookCellStateChangedEvent, NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { CellKind, INotebookSearchOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { NotebookEventDispatcher, NotebookCellStateChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; export class MarkdownCellViewModel extends BaseCellViewModel implements ICellViewModel { readonly cellKind = CellKind.Markdown; @@ -45,7 +46,7 @@ export class MarkdownCellViewModel extends BaseCellViewModel implements ICellVie set editorHeight(newHeight: number) { this._editorHeight = newHeight; - this.totalHeight = this._editorHeight + CELL_TOP_MARGIN + CELL_BOTTOM_MARGIN + BOTTOM_CELL_TOOLBAR_GAP + CELL_STATUSBAR_HEIGHT; + this.totalHeight = this._editorHeight + CELL_TOP_MARGIN + CELL_BOTTOM_MARGIN + BOTTOM_CELL_TOOLBAR_GAP + this.getEditorStatusbarHeight(); } get editorHeight() { @@ -65,9 +66,10 @@ export class MarkdownCellViewModel extends BaseCellViewModel implements ICellVie initialNotebookLayoutInfo: NotebookLayoutInfo | null, readonly foldingDelegate: EditorFoldingStateDelegate, readonly eventDispatcher: NotebookEventDispatcher, - private readonly _mdRenderer: MarkdownRenderer + private readonly _mdRenderer: MarkdownRenderer, + @IConfigurationService configurationService: IConfigurationService ) { - super(viewType, model, UUID.generateUuid()); + super(viewType, model, UUID.generateUuid(), configurationService); this._layoutInfo = { editorHeight: 0, diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts index dc5b1b2443..9f9b3d0039 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts @@ -8,7 +8,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import * as strings from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; -import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { IBulkEditService, ResourceEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { Range } from 'vs/editor/common/core/range'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { IModelDecorationOptions, IModelDeltaDecoration, TrackedRangeStickiness, IReadonlyTextBuffer } from 'vs/editor/common/model'; @@ -17,13 +17,13 @@ import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { WorkspaceTextEdit } from 'vs/editor/common/modes'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; -import { CellEditState, CellFindMatch, ICellRange, ICellViewModel, NotebookLayoutInfo, IEditableCellViewModel, INotebookDeltaDecoration } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellEditState, CellFindMatch, ICellViewModel, NotebookLayoutInfo, IEditableCellViewModel, INotebookDeltaDecoration } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { NotebookEventDispatcher, NotebookMetadataChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; import { CellFoldingState, EditorFoldingStateDelegate } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel'; import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { CellKind, NotebookCellMetadata, INotebookSearchOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, NotebookCellMetadata, INotebookSearchOptions, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { FoldingRegions } from 'vs/editor/contrib/folding/foldingRanges'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { MarkdownRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/mdRenderer'; @@ -174,8 +174,8 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD return this._notebook.handle; } - get languages() { - return this._notebook.languages; + get resolvedLanguages() { + return this._notebook.resolvedLanguages; } get uri() { @@ -609,8 +609,9 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD return result; } - createCell(index: number, source: string | string[], language: string, type: CellKind, metadata: NotebookCellMetadata | undefined, synchronous: boolean, pushUndoStop: boolean = true) { - this._notebook.createCell2(index, source, language, type, metadata, synchronous, pushUndoStop, undefined, undefined); + createCell(index: number, source: string, language: string, type: CellKind, metadata: NotebookCellMetadata | undefined, synchronous: boolean, pushUndoStop: boolean = true, previouslyFocused: ICellViewModel[] = []) { + const beforeSelections = previouslyFocused.map(e => e.handle); + this._notebook.createCell2(index, source, language, type, metadata, synchronous, pushUndoStop, beforeSelections, undefined); // TODO, rely on createCell to be sync return this.viewCells[index]; } @@ -755,7 +756,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD language, kind, { - createCell: (index: number, source: string | string[], language: string, type: CellKind) => { + createCell: (index: number, source: string, language: string, type: CellKind) => { return this.createCell(index, source, language, type, undefined, true, false) as BaseCellViewModel; }, deleteCell: (index: number) => { @@ -1027,7 +1028,10 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD const viewCell = cell as CellViewModel; this._lastNotebookEditResource.push(viewCell.uri); return viewCell.resolveTextModel().then(() => { - this._bulkEditService.apply({ edits: [{ edit: { range: range, text: text }, resource: cell.uri }] }, { quotableLabel: 'Notebook Replace' }); + this._bulkEditService.apply( + [new ResourceTextEdit(cell.uri, { range, text })], + { quotableLabel: 'Notebook Replace' } + ); }); } @@ -1051,7 +1055,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD return Promise.all(matches.map(match => { return match.cell.resolveTextModel(); })).then(async () => { - this._bulkEditService.apply({ edits: textEdits }, { quotableLabel: 'Notebook Replace All' }); + this._bulkEditService.apply(ResourceEdit.convert({ edits: textEdits }), { quotableLabel: 'Notebook Replace All' }); return; }); } diff --git a/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts b/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts index a520f99e60..fbc9c78208 100644 --- a/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts +++ b/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts @@ -6,6 +6,7 @@ import { IResourceUndoRedoElement, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo'; import { URI } from 'vs/base/common/uri'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; +import { NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; /** * It should not modify Undo/Redo stack @@ -14,6 +15,7 @@ export interface ITextCellEditingDelegate { insertCell?(index: number, cell: NotebookCellTextModel): void; deleteCell?(index: number): void; moveCell?(fromIndex: number, length: number, toIndex: number, beforeSelections: number[] | undefined, endSelections: number[] | undefined): void; + updateCellMetadata?(index: number, newMetadata: NotebookCellMetadata): void; emitSelections(selections: number[]): void; } @@ -183,3 +185,33 @@ export class SpliceCellsEdit implements IResourceUndoRedoElement { } } } + +export class CellMetadataEdit implements IResourceUndoRedoElement { + type: UndoRedoElementType.Resource = UndoRedoElementType.Resource; + label: string = 'Update Cell Metadata'; + constructor( + public resource: URI, + readonly index: number, + readonly oldMetadata: NotebookCellMetadata, + readonly newMetadata: NotebookCellMetadata, + private editingDelegate: ITextCellEditingDelegate, + ) { + + } + + undo(): void { + if (!this.editingDelegate.updateCellMetadata) { + return; + } + + this.editingDelegate.updateCellMetadata(this.index, this.oldMetadata); + } + + redo(): void | Promise { + if (!this.editingDelegate.updateCellMetadata) { + return; + } + + this.editingDelegate.updateCellMetadata(this.index, this.newMetadata); + } +} diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts index 706465779f..aa03ba69c5 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts @@ -4,13 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; -import { ICell, IProcessedOutput, NotebookCellOutputsSplice, CellKind, NotebookCellMetadata, NotebookDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ICell, IProcessedOutput, NotebookCellOutputsSplice, CellKind, NotebookCellMetadata, NotebookDocumentMetadata, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; import { URI } from 'vs/base/common/uri'; import * as model from 'vs/editor/common/model'; import { Range } from 'vs/editor/common/core/range'; import { Disposable } from 'vs/base/common/lifecycle'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { hash } from 'vs/base/common/hash'; export class NotebookCellTextModel extends Disposable implements ICell { private _onDidChangeOutputs = new Emitter(); @@ -31,14 +32,15 @@ export class NotebookCellTextModel extends Disposable implements ICell { return this._outputs; } - private _metadata: NotebookCellMetadata | undefined; + private _metadata: NotebookCellMetadata; get metadata() { return this._metadata; } - set metadata(newMetadata: NotebookCellMetadata | undefined) { + set metadata(newMetadata: NotebookCellMetadata) { this._metadata = newMetadata; + this._hash = null; this._onDidChangeMetadata.fire(); } @@ -48,6 +50,7 @@ export class NotebookCellTextModel extends Disposable implements ICell { set language(newLanguage: string) { this._language = newLanguage; + this._hash = null; this._onDidChangeLanguage.fire(newLanguage); } @@ -59,31 +62,35 @@ export class NotebookCellTextModel extends Disposable implements ICell { } const builder = new PieceTreeTextBufferBuilder(); - builder.acceptChunk(Array.isArray(this._source) ? this._source.join('\n') : this._source); + builder.acceptChunk(this._source); const bufferFactory = builder.finish(true); this._textBuffer = bufferFactory.create(model.DefaultEndOfLine.LF); this._register(this._textBuffer.onDidChangeContent(() => { + this._hash = null; this._onDidChangeContent.fire(); })); return this._textBuffer; } + private _hash: number | null = null; + constructor( readonly uri: URI, public handle: number, - private _source: string | string[], + private _source: string, private _language: string, public cellKind: CellKind, outputs: IProcessedOutput[], metadata: NotebookCellMetadata | undefined, + public readonly transientOptions: TransientOptions, private readonly _modelService: ITextModelService ) { super(); this._outputs = outputs; - this._metadata = metadata; + this._metadata = metadata || {}; } getValue(): string { @@ -96,6 +103,31 @@ export class NotebookCellTextModel extends Disposable implements ICell { } } + getHashValue(): number { + if (this._hash !== null) { + return this._hash; + } + + // TODO, raw outputs + this._hash = hash([hash(this.getValue()), this._getPersisentMetadata, this.transientOptions.transientOutputs ? [] : this._outputs]); + return this._hash; + } + + private _getPersisentMetadata() { + let filteredMetadata: { [key: string]: any } = {}; + const transientMetadata = this.transientOptions.transientMetadata; + + const keys = new Set([...Object.keys(this.metadata)]); + for (let key of keys) { + if (!(transientMetadata[key as keyof NotebookCellMetadata]) + ) { + filteredMetadata[key] = this.metadata[key as keyof NotebookCellMetadata]; + } + } + + return filteredMetadata; + } + getTextLength(): number { return this.textBuffer.getLength(); } diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index cc9c6c46b5..0d7cb947b0 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -8,19 +8,12 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { INotebookTextModel, NotebookCellOutputsSplice, NotebookCellTextModelSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, CellEditType, CellUri, ICellInsertEdit, NotebookCellsChangedEvent, CellKind, IProcessedOutput, notebookDocumentMetadataDefaults, diff, ICellDeleteEdit, NotebookCellsChangeType, ICellDto2, IMainCellDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookTextModel, NotebookCellOutputsSplice, NotebookCellTextModelSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, CellEditType, CellUri, NotebookCellsChangedEvent, CellKind, IProcessedOutput, notebookDocumentMetadataDefaults, diff, NotebookCellsChangeType, ICellDto2, IMainCellDto, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ITextSnapshot } from 'vs/editor/common/model'; import { IUndoRedoService, UndoRedoElementType, IUndoRedoElement, IResourceUndoRedoElement } from 'vs/platform/undoRedo/common/undoRedo'; -import { InsertCellEdit, DeleteCellEdit, MoveCellEdit, SpliceCellsEdit } from 'vs/workbench/contrib/notebook/common/model/cellEdit'; +import { InsertCellEdit, DeleteCellEdit, MoveCellEdit, SpliceCellsEdit, CellMetadataEdit } from 'vs/workbench/contrib/notebook/common/model/cellEdit'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; - -function compareRangesUsingEnds(a: [number, number], b: [number, number]): number { - if (a[1] === b[1]) { - return a[1] - b[1]; - - } - return a[1] - b[1]; -} +import { IModeService } from 'vs/editor/common/services/modeService'; export class NotebookTextModelSnapshot implements ITextSnapshot { // private readonly _pieces: Ce[] = []; @@ -134,8 +127,23 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel private _mapping: Map = new Map(); private _cellListeners: Map = new Map(); cells: NotebookCellTextModel[]; - languages: string[] = []; + private _languages: string[] = []; + private _allLanguages: boolean = false; + + get languages() { + return this._languages; + } + + get resolvedLanguages() { + if (this._allLanguages) { + return this._modeService.getRegisteredModes(); + } + + return this._languages; + } + metadata: NotebookDocumentMetadata = notebookDocumentMetadataDefaults; + transientOptions: TransientOptions = { transientMetadata: {}, transientOutputs: false }; private _isUntitled: boolean | undefined = undefined; private _versionId = 0; @@ -166,7 +174,8 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel public supportBackup: boolean, public uri: URI, @IUndoRedoService private _undoService: IUndoRedoService, - @ITextModelService private _modelService: ITextModelService + @ITextModelService private _modelService: ITextModelService, + @IModeService private readonly _modeService: IModeService, ) { super(); this.cells = []; @@ -186,7 +195,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel } createCellTextModel( - source: string | string[], + source: string, language: string, cellKind: CellKind, outputs: IProcessedOutput[], @@ -194,7 +203,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel ) { const cellHandle = this._cellhandlePool++; const cellUri = CellUri.generate(this.uri, cellHandle); - return new NotebookCellTextModel(cellUri, cellHandle, source, language, cellKind, outputs || [], metadata, this._modelService); + return new NotebookCellTextModel(cellUri, cellHandle, source, language, cellKind, outputs || [], metadata || {}, this.transientOptions, this._modelService); } initialize(cells: ICellDto2[]) { @@ -204,7 +213,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel const mainCells = cells.map(cell => { const cellHandle = this._cellhandlePool++; const cellUri = CellUri.generate(this.uri, cellHandle); - return new NotebookCellTextModel(cellUri, cellHandle, cell.source, cell.language, cell.cellKind, cell.outputs || [], cell.metadata, this._modelService); + return new NotebookCellTextModel(cellUri, cellHandle, cell.source, cell.language, cell.cellKind, cell.outputs || [], cell.metadata, this.transientOptions, this._modelService); }); this._isUntitled = false; @@ -213,6 +222,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._mapping.set(mainCells[i].handle, mainCells[i]); const dirtyStateListener = mainCells[i].onDidChangeContent(() => { this.setDirty(true); + this._increaseVersionId(); this._onDidChangeContent.fire(); }); @@ -227,7 +237,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._operationManager.pushStackElement(label); } - $applyEdit(modelVersionId: number, rawEdits: ICellEditOperation[], synchronous: boolean): boolean { + applyEdit(modelVersionId: number, rawEdits: ICellEditOperation[], synchronous: boolean): boolean { if (modelVersionId !== this._versionId) { return false; } @@ -235,49 +245,30 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel const oldViewCells = this.cells.slice(0); const oldMap = new Map(this._mapping); - let operations: ({ sortIndex: number; start: number; end: number; } & ICellEditOperation)[] = []; - for (let i = 0; i < rawEdits.length; i++) { - if (rawEdits[i].editType === CellEditType.Insert) { - const edit = rawEdits[i] as ICellInsertEdit; - operations.push({ - sortIndex: i, - start: edit.index, - end: edit.index, - ...edit - }); - } else { - const edit = rawEdits[i] as ICellDeleteEdit; - operations.push({ - sortIndex: i, - start: edit.index, - end: edit.index + edit.count, - ...edit - }); - } - } - - // const edits - operations = operations.sort((a, b) => { - const r = compareRangesUsingEnds([a.start, a.end], [b.start, b.end]); - if (r === 0) { - return b.sortIndex - a.sortIndex; - } - return -r; + const edits = rawEdits.map((edit, index) => { + return { + edit, + end: edit.editType === CellEditType.Replace ? edit.index + edit.count : edit.index, + originalIndex: index, + }; + }).sort((a, b) => { + return b.end - a.end || b.originalIndex - a.originalIndex; }); - for (let i = 0; i < operations.length; i++) { - switch (operations[i].editType) { - case CellEditType.Insert: - const insertEdit = operations[i] as ICellInsertEdit; - const mainCells = insertEdit.cells.map(cell => { - const cellHandle = this._cellhandlePool++; - const cellUri = CellUri.generate(this.uri, cellHandle); - return new NotebookCellTextModel(cellUri, cellHandle, cell.source, cell.language, cell.cellKind, cell.outputs || [], cell.metadata, this._modelService); - }); - this.insertNewCell(insertEdit.index, mainCells, false); + for (const { edit } of edits) { + switch (edit.editType) { + case CellEditType.Replace: + this._replaceCells(edit.index, edit.count, edit.cells); break; - case CellEditType.Delete: - this.removeCell(operations[i].index, operations[i].end - operations[i].start, false); + case CellEditType.Output: + //TODO@joh,@rebornix no event, no undo stop (?) + this.assertIndex(edit.index); + const cell = this.cells[edit.index]; + this.spliceNotebookCellOutputs(cell.handle, [[0, cell.outputs.length, edit.outputs]]); + break; + case CellEditType.Metadata: + this.assertIndex(edit.index); + this.deltaCellMetadata(this.cells[edit.index].handle, edit.metadata); break; } } @@ -319,7 +310,48 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return true; } - $handleEdit(label: string | undefined, undo: () => void, redo: () => void): void { + private _replaceCells(index: number, count: number, cellDtos: ICellDto2[]): void { + + if (count === 0 && cellDtos.length === 0) { + return; + } + + this._isUntitled = false; //TODO@rebornix fishy? + + // prepare remove + for (let i = index; i < index + count; i++) { + const cell = this.cells[i]; + this._cellListeners.get(cell.handle)?.dispose(); + this._cellListeners.delete(cell.handle); + } + + // prepare add + const cells = cellDtos.map(cellDto => { + const cellHandle = this._cellhandlePool++; + const cellUri = CellUri.generate(this.uri, cellHandle); + const cell = new NotebookCellTextModel( + cellUri, cellHandle, + cellDto.source, cellDto.language, cellDto.cellKind, cellDto.outputs || [], cellDto.metadata, this.transientOptions, + this._modelService + ); + const dirtyStateListener = cell.onDidChangeContent(() => { + this.setDirty(true); + this._increaseVersionId(); + this._onDidChangeContent.fire(); + }); + this._cellListeners.set(cell.handle, dirtyStateListener); + this._mapping.set(cell.handle, cell); + return cell; + }); + + // make change + this.cells.splice(index, count, ...cells); + this.setDirty(true); + this._increaseVersionId(); + this._onDidChangeContent.fire(); + } + + handleEdit(label: string | undefined, undo: () => void, redo: () => void): void { this._operationManager.pushEditOperation({ type: UndoRedoElementType.Resource, resource: this.uri, @@ -347,11 +379,13 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel } updateLanguages(languages: string[]) { - this.languages = languages; + const allLanguages = languages.find(lan => lan === '*'); + this._allLanguages = allLanguages !== undefined; + this._languages = languages; - // TODO@rebornix metadata: default language for cell - if (this._isUntitled && languages.length && this.cells.length) { - this.cells[0].language = languages[0]; + const resolvedLanguages = this.resolvedLanguages; + if (this._isUntitled && resolvedLanguages.length && this.cells.length) { + this.cells[0].language = resolvedLanguages[0]; } } @@ -360,14 +394,6 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._onDidChangeMetadata.fire(this.metadata); } - updateNotebookCellMetadata(handle: number, metadata: NotebookCellMetadata) { - const cell = this.cells.find(cell => cell.handle === handle); - - if (cell) { - cell.metadata = metadata; - } - } - insertTemplateCell(cell: NotebookCellTextModel) { if (this.cells.length > 0 || this._isUntitled !== undefined) { return; @@ -380,6 +406,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel const dirtyStateListener = cell.onDidChangeContent(() => { this._isUntitled = false; this.setDirty(true); + this._increaseVersionId(); this._onDidChangeContent.fire(); }); @@ -416,6 +443,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._mapping.set(cells[i].handle, cells[i]); const dirtyStateListener = cells[i].onDidChangeContent(() => { this.setDirty(true); + this._increaseVersionId(); this._onDidChangeContent.fire(); }); @@ -493,9 +521,26 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel } // TODO@rebornix should this trigger content change event? - $spliceNotebookCellOutputs(cellHandle: number, splices: NotebookCellOutputsSplice[]): void { + spliceNotebookCellOutputs(cellHandle: number, splices: NotebookCellOutputsSplice[]): void { const cell = this._mapping.get(cellHandle); - cell?.spliceNotebookCellOutputs(splices); + if (cell) { + + cell.spliceNotebookCellOutputs(splices); + + if (!this.transientOptions.transientOutputs) { + this._increaseVersionId(); + this.setDirty(true); + this._onDidChangeContent.fire(); + } + + this._onDidModelChangeProxy.fire({ + kind: NotebookCellsChangeType.Output, + versionId: this.versionId, + index: this.cells.indexOf(cell), + outputs: cell.outputs ?? [] + }); + } + } clearCellOutput(handle: number) { @@ -520,16 +565,62 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel } } - changeCellMetadata(handle: number, newMetadata: NotebookCellMetadata) { + private _isCellMetadataChanged(a: NotebookCellMetadata, b: NotebookCellMetadata) { + const keys = new Set([...Object.keys(a || {}), ...Object.keys(b || {})]); + for (let key of keys) { + if ( + (a[key as keyof NotebookCellMetadata] !== b[key as keyof NotebookCellMetadata]) + && + !(this.transientOptions.transientMetadata[key as keyof NotebookCellMetadata]) + ) { + return true; + } + } + + return false; + } + + changeCellMetadata(handle: number, metadata: NotebookCellMetadata, pushUndoStop: boolean) { + const cell = this.cells.find(cell => cell.handle === handle); + + if (!cell) { + return; + } + + const triggerDirtyChange = this._isCellMetadataChanged(cell.metadata, metadata); + + if (triggerDirtyChange) { + if (pushUndoStop) { + const index = this.cells.indexOf(cell); + this._operationManager.pushEditOperation(new CellMetadataEdit(this.uri, index, Object.freeze(cell.metadata), Object.freeze(metadata), { + updateCellMetadata: (index, newMetadata) => { + const cell = this.cells[index]; + if (!cell) { + return; + } + this.changeCellMetadata(cell.handle, newMetadata, false); + }, + emitSelections: this._emitSelectionsDelegate.bind(this) + })); + } + cell.metadata = metadata; + this.setDirty(true); + this._onDidChangeContent.fire(); + } else { + cell.metadata = metadata; + } + + this._increaseVersionId(); + this._onDidModelChangeProxy.fire({ kind: NotebookCellsChangeType.ChangeMetadata, versionId: this._versionId, index: this.cells.indexOf(cell), metadata: cell.metadata }); + } + + deltaCellMetadata(handle: number, newMetadata: NotebookCellMetadata) { const cell = this._mapping.get(handle); if (cell) { - cell.metadata = { + this.changeCellMetadata(handle, { ...cell.metadata, ...newMetadata - }; - - this._increaseVersionId(); - this._onDidModelChangeProxy.fire({ kind: NotebookCellsChangeType.ChangeMetadata, versionId: this._versionId, index: this.cells.indexOf(cell), metadata: cell.metadata }); + }, true); } } @@ -559,7 +650,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._emitSelections.fire(selections); } - createCell2(index: number, source: string | string[], language: string, type: CellKind, metadata: NotebookCellMetadata | undefined, synchronous: boolean, pushUndoStop: boolean, beforeSelections: number[] | undefined, endSelections: number[] | undefined) { + createCell2(index: number, source: string, language: string, type: CellKind, metadata: NotebookCellMetadata | undefined, synchronous: boolean, pushUndoStop: boolean, beforeSelections: number[] | undefined, endSelections: number[] | undefined) { const cell = this.createCellTextModel(source, language, type, [], metadata); if (pushUndoStop) { diff --git a/src/vs/workbench/contrib/notebook/common/notebookCellStatusBarService.ts b/src/vs/workbench/contrib/notebook/common/notebookCellStatusBarService.ts new file mode 100644 index 0000000000..ad808345ad --- /dev/null +++ b/src/vs/workbench/contrib/notebook/common/notebookCellStatusBarService.ts @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from 'vs/base/common/event'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { INotebookCellStatusBarEntry } from 'vs/workbench/contrib/notebook/common/notebookCommon'; + +export const INotebookCellStatusBarService = createDecorator('notebookCellStatusBarService'); + +export interface INotebookCellStatusBarService { + readonly _serviceBrand: undefined; + + onDidChangeEntriesForCell: Event; + + addEntry(entry: INotebookCellStatusBarEntry): IDisposable; + getEntries(cell: URI): INotebookCellStatusBarEntry[]; +} diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 9ef3a2c7fd..a1767ec49e 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -3,21 +3,26 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IDiffResult, ISequence } from 'vs/base/common/diff/diff'; import { Event } from 'vs/base/common/event'; import * as glob from 'vs/base/common/glob'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import * as UUID from 'vs/base/common/uuid'; +import { Schemas } from 'vs/base/common/network'; +import { basename } from 'vs/base/common/path'; import { isWindows } from 'vs/base/common/platform'; import { ISplice } from 'vs/base/common/sequence'; import { URI, UriComponents } from 'vs/base/common/uri'; import * as editorCommon from 'vs/editor/common/editorCommon'; -import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { Command } from 'vs/editor/common/modes'; +import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IEditorModel } from 'vs/platform/editor/common/editor'; -import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { Schemas } from 'vs/base/common/network'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IRevertOptions } from 'vs/workbench/common/editor'; -import { basename } from 'vs/base/common/path'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { IFileStatWithMetadata } from 'vs/platform/files/common/files'; export enum CellKind { Markdown = 1, @@ -102,6 +107,13 @@ export interface NotebookCellMetadata { custom?: { [key: string]: unknown }; } +export type TransientMetadata = { [K in keyof NotebookCellMetadata]?: boolean }; + +export interface TransientOptions { + transientOutputs: boolean; + transientMetadata: TransientMetadata; +} + export interface INotebookDisplayOrder { defaultOrder: string[]; userOrder?: string[]; @@ -122,29 +134,6 @@ export interface INotebookRendererInfo { matches(mimeType: string): boolean; } -export interface INotebookKernelInfo { - id: string; - label: string, - selectors: (string | glob.IRelativePattern)[], - extension: ExtensionIdentifier; - extensionLocation: URI, - preloads: URI[]; - providerHandle?: number; - executeNotebook(viewType: string, uri: URI, handle: number | undefined): Promise; - -} - -export interface INotebookKernelInfoDto { - id: string; - label: string, - extensionLocation: URI; - preloads?: UriComponents[]; -} - -export interface INotebookSelectors { - readonly filenamePattern?: string; -} - export interface IStreamOutput { outputKind: CellOutputKind.Text; text: string; @@ -155,15 +144,15 @@ export interface IErrorOutput { /** * Exception Name */ - ename?: string; + ename: string; /** * Exception Value */ - evalue?: string; + evalue: string; /** * Exception call stacks */ - traceback?: string[]; + traceback: string[]; } export interface NotebookCellOutputMetadata { @@ -204,6 +193,10 @@ export interface ITransformedDisplayOutputDto { pickedMimeTypeIndex?: number; } +export function isTransformedDisplayOutput(thing: unknown): thing is ITransformedDisplayOutputDto { + return (thing as ITransformedDisplayOutputDto).outputKind === CellOutputKind.Rich && !!(thing as ITransformedDisplayOutputDto).outputId; +} + export interface IGenericOutput { outputKind: CellOutputKind; pickedMimeType?: string; @@ -211,6 +204,11 @@ export interface IGenericOutput { transformedOutput?: { [key: string]: IDisplayOutput }; } + +export const addIdToOutput = (output: IRawOutput, id = UUID.generateUuid()): IProcessedOutput => output.outputKind === CellOutputKind.Rich + ? ({ ...output, outputId: id }) : output; + + export type IProcessedOutput = ITransformedDisplayOutputDto | IStreamOutput | IErrorOutput; export type IRawOutput = IDisplayOutput | IStreamOutput | IErrorOutput; @@ -297,14 +295,14 @@ export interface IRenderNoOutput { export interface IRenderPlainHtmlOutput { type: RenderOutputType.Html; - source: IProcessedOutput; + source: ITransformedDisplayOutputDto; htmlContent: string; hasDynamicHeight: boolean; } export interface IRenderOutputViaExtension { type: RenderOutputType.Extension; - source: IProcessedOutput; + source: ITransformedDisplayOutputDto; mimeType: string; renderer: INotebookRendererInfo; } @@ -312,7 +310,7 @@ export interface IRenderOutputViaExtension { export type IInsetRenderOutput = IRenderPlainHtmlOutput | IRenderOutputViaExtension; export type IRenderOutput = IRenderNoOutput | IInsetRenderOutput; -export const outputHasDynamicHeight = (o: IRenderOutput) => o.type === RenderOutputType.Extension || o.hasDynamicHeight; +export const outputHasDynamicHeight = (o: IRenderOutput) => o.type !== RenderOutputType.Extension && o.hasDynamicHeight; export type NotebookCellTextModelSplice = [ number /* start */, @@ -350,7 +348,8 @@ export enum NotebookCellsChangeType { CellsClearOutput = 4, ChangeLanguage = 5, Initialize = 6, - ChangeMetadata = 7 + ChangeMetadata = 7, + Output = 8, } export interface NotebookCellsInitializeEvent { @@ -372,6 +371,13 @@ export interface NotebookCellsModelMoveEvent { readonly versionId: number; } +export interface NotebookOutputChangedEvent { + readonly kind: NotebookCellsChangeType.Output; + readonly index: number; + readonly versionId: number; + readonly outputs: IProcessedOutput[]; +} + export interface NotebookCellClearOutputEvent { readonly kind: NotebookCellsChangeType.CellClearOutput; readonly index: number; @@ -394,41 +400,49 @@ export interface NotebookCellsChangeMetadataEvent { readonly kind: NotebookCellsChangeType.ChangeMetadata; readonly versionId: number; readonly index: number; - readonly metadata: NotebookCellMetadata; + readonly metadata: NotebookCellMetadata | undefined; } -export type NotebookCellsChangedEvent = NotebookCellsInitializeEvent | NotebookCellsModelChangedEvent | NotebookCellsModelMoveEvent | NotebookCellClearOutputEvent | NotebookCellsClearOutputEvent | NotebookCellsChangeLanguageEvent | NotebookCellsChangeMetadataEvent; -export enum CellEditType { - Insert = 1, - Delete = 2 +export type NotebookCellsChangedEvent = NotebookCellsInitializeEvent | NotebookCellsModelChangedEvent | NotebookCellsModelMoveEvent | NotebookOutputChangedEvent | NotebookCellClearOutputEvent | NotebookCellsClearOutputEvent | NotebookCellsChangeLanguageEvent | NotebookCellsChangeMetadataEvent; + +export const enum CellEditType { + Replace = 1, + Output = 2, + Metadata = 3, } export interface ICellDto2 { - source: string | string[]; + source: string; language: string; cellKind: CellKind; outputs: IProcessedOutput[]; metadata?: NotebookCellMetadata; } -export interface ICellInsertEdit { - editType: CellEditType.Insert; +export interface ICellReplaceEdit { + editType: CellEditType.Replace; index: number; + count: number; cells: ICellDto2[]; } -export interface ICellDeleteEdit { - editType: CellEditType.Delete; +export interface ICellOutputEdit { + editType: CellEditType.Output; index: number; - count: number; + outputs: IProcessedOutput[]; } -export type ICellEditOperation = ICellInsertEdit | ICellDeleteEdit; +export interface ICellMetadataEdit { + editType: CellEditType.Metadata; + index: number; + metadata: NotebookCellMetadata; +} + +export type ICellEditOperation = ICellReplaceEdit | ICellOutputEdit | ICellMetadataEdit; export interface INotebookEditData { documentVersionId: number; edits: ICellEditOperation[]; - renderers: number[]; } export interface NotebookDataDto { @@ -450,6 +464,7 @@ export function getCellUndoRedoComparisonKey(uri: URI) { export namespace CellUri { export const scheme = Schemas.vscodeNotebookCell; + const _regex = /^\d{7,}/; export function generate(notebook: URI, handle: number): URI { @@ -459,6 +474,14 @@ export namespace CellUri { }); } + export function generateCellMetadataUri(notebook: URI, handle: number): URI { + return notebook.with({ + scheme: Schemas.vscode, + authority: 'vscode-notebook-cell-metadata', + fragment: `${handle.toString().padStart(7, '0')}${notebook.scheme !== Schemas.file ? notebook.scheme : ''}` + }); + } + export function parse(cell: URI): { notebook: URI, handle: number } | undefined { if (cell.scheme !== scheme) { return undefined; @@ -550,7 +573,7 @@ interface IMutableSplice extends ISplice { deleteCount: number; } -export function diff(before: T[], after: T[], contains: (a: T) => boolean): ISplice[] { +export function diff(before: T[], after: T[], contains: (a: T) => boolean, equal: (a: T, b: T) => boolean = (a: T, b: T) => a === b): ISplice[] { const result: IMutableSplice[] = []; function pushSplice(start: number, deleteCount: number, toInsert: T[]): void { @@ -585,7 +608,7 @@ export function diff(before: T[], after: T[], contains: (a: T) => boolean): I const beforeElement = before[beforeIdx]; const afterElement = after[afterIdx]; - if (beforeElement === afterElement) { + if (equal(beforeElement, afterElement)) { // equal beforeIdx += 1; afterIdx += 1; @@ -618,6 +641,7 @@ export interface INotebookEditorModel extends IEditorModel { readonly resource: URI; readonly viewType: string; readonly notebook: NotebookTextModel; + readonly lastResolvedFileStat: IFileStatWithMetadata | undefined; isDirty(): boolean; isUntitled(): boolean; save(): Promise; @@ -625,6 +649,13 @@ export interface INotebookEditorModel extends IEditorModel { revert(options?: IRevertOptions | undefined): Promise; } +export interface INotebookDiffEditorModel extends IEditorModel { + original: INotebookEditorModel; + modified: INotebookEditorModel; + resolveOriginalFromDisk(): Promise; + resolveModifiedFromDisk(): Promise; +} + export interface INotebookTextModelBackup { metadata: NotebookDocumentMetadata; languages: string[]; @@ -638,10 +669,27 @@ export interface NotebookDocumentBackupData { readonly mtime?: number; } +/** + * [start, end] + */ +export interface ICellRange { + /** + * zero based index + */ + start: number; + + /** + * zero based index + */ + end: number; +} + export interface IEditor extends editorCommon.ICompositeCodeEditor { readonly onDidChangeModel: Event; readonly onDidFocusEditorWidget: Event; + readonly onDidChangeVisibleRanges: Event; isNotebookEditor: boolean; + visibleRanges: ICellRange[]; uri?: URI; textModel?: NotebookTextModel; getId(): string; @@ -661,22 +709,44 @@ export interface INotebookSearchOptions { wordSeparators?: string; } +export interface INotebookExclusiveDocumentFilter { + include?: string | glob.IRelativePattern; + exclude?: string | glob.IRelativePattern; +} + export interface INotebookDocumentFilter { - viewType?: string; - filenamePattern?: string | glob.IRelativePattern; - excludeFileNamePattern?: string | glob.IRelativePattern; + viewType?: string | string[]; + filenamePattern?: string | glob.IRelativePattern | INotebookExclusiveDocumentFilter; } //TODO@rebornix test + +function isDocumentExcludePattern(filenamePattern: string | glob.IRelativePattern | INotebookExclusiveDocumentFilter): filenamePattern is { include: string | glob.IRelativePattern; exclude: string | glob.IRelativePattern; } { + const arg = filenamePattern as INotebookExclusiveDocumentFilter; + + if ((typeof arg.include === 'string' || glob.isRelativePattern(arg.include)) + && (typeof arg.exclude === 'string' || glob.isRelativePattern(arg.exclude))) { + return true; + } + + return false; +} export function notebookDocumentFilterMatch(filter: INotebookDocumentFilter, viewType: string, resource: URI): boolean { + if (Array.isArray(filter.viewType) && filter.viewType.indexOf(viewType) >= 0) { + return true; + } + if (filter.viewType === viewType) { return true; } if (filter.filenamePattern) { - if (glob.match(filter.filenamePattern, basename(resource.fsPath).toLowerCase())) { - if (filter.excludeFileNamePattern) { - if (glob.match(filter.excludeFileNamePattern, basename(resource.fsPath).toLowerCase())) { + let filenamePattern = isDocumentExcludePattern(filter.filenamePattern) ? filter.filenamePattern.include : (filter.filenamePattern as string | glob.IRelativePattern); + let excludeFilenamePattern = isDocumentExcludePattern(filter.filenamePattern) ? filter.filenamePattern.exclude : undefined; + + if (glob.match(filenamePattern, basename(resource.fsPath).toLowerCase())) { + if (excludeFilenamePattern) { + if (glob.match(excludeFilenamePattern, basename(resource.fsPath).toLowerCase())) { // should exclude return false; @@ -695,26 +765,65 @@ export interface INotebookKernelInfoDto2 { extensionLocation: URI; providerHandle?: number; description?: string; + detail?: string; isPreferred?: boolean; preloads?: UriComponents[]; } export interface INotebookKernelInfo2 extends INotebookKernelInfoDto2 { resolve(uri: URI, editorId: string, token: CancellationToken): Promise; - executeNotebookCell?(uri: URI, handle: number | undefined): Promise; - cancelNotebookCell?(uri: URI, handle: number | undefined): Promise; + executeNotebookCell(uri: URI, handle: number | undefined): Promise; + cancelNotebookCell(uri: URI, handle: number | undefined): Promise; } export interface INotebookKernelProvider { providerExtensionId: string; providerDescription?: string; selector: INotebookDocumentFilter; - onDidChangeKernels: Event; + onDidChangeKernels: Event; provideKernels(uri: URI, token: CancellationToken): Promise; resolveKernel(editorId: string, uri: UriComponents, kernelId: string, token: CancellationToken): Promise; executeNotebook(uri: URI, kernelId: string, handle: number | undefined): Promise; cancelNotebook(uri: URI, kernelId: string, handle: number | undefined): Promise; } +export class CellSequence implements ISequence { + + constructor(readonly textModel: NotebookTextModel) { + } + + getElements(): string[] | number[] | Int32Array { + const hashValue = new Int32Array(this.textModel.cells.length); + for (let i = 0; i < this.textModel.cells.length; i++) { + hashValue[i] = this.textModel.cells[i].getHashValue(); + } + + return hashValue; + } +} + +export interface INotebookDiffResult { + cellsDiff: IDiffResult, + linesDiff?: { originalCellhandle: number, modifiedCellhandle: number, lineChanges: editorCommon.ILineChange[] }[]; +} + +export interface INotebookCellStatusBarEntry { + readonly cellResource: URI; + readonly alignment: CellStatusbarAlignment; + readonly priority?: number; + readonly text: string; + readonly tooltip: string | undefined; + readonly command: string | Command | undefined; + readonly accessibilityInformation?: IAccessibilityInformation; + readonly visible: boolean; +} + export const DisplayOrderKey = 'notebook.displayOrder'; export const CellToolbarLocKey = 'notebook.cellToolbarLocation'; +export const ShowCellStatusBarKey = 'notebook.showCellStatusBar'; +export const NotebookTextDiffEditorPreview = 'notebook.diff.enablePreview'; + +export const enum CellStatusbarAlignment { + LEFT, + RIGHT +} diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index 8106991bc6..cc7e03c176 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -11,20 +11,13 @@ import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/no import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { URI } from 'vs/base/common/uri'; import { IWorkingCopyService, IWorkingCopy, IWorkingCopyBackup, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { basename } from 'vs/base/common/resources'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { Schemas } from 'vs/base/common/network'; import { IFileStatWithMetadata, IFileService } from 'vs/platform/files/common/files'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { ILabelService } from 'vs/platform/label/common/label'; -export interface INotebookEditorModelManager { - models: NotebookEditorModel[]; - - resolve(resource: URI, viewType: string, editorId?: string): Promise; - - get(resource: URI): NotebookEditorModel | undefined; -} export interface INotebookLoadOptions { /** @@ -36,7 +29,7 @@ export interface INotebookLoadOptions { } -export class NotebookEditorModel extends EditorModel implements IWorkingCopy, INotebookEditorModel { +export class NotebookEditorModel extends EditorModel implements INotebookEditorModel { protected readonly _onDidChangeDirty = this._register(new Emitter()); readonly onDidChangeDirty = this._onDidChangeDirty.event; private readonly _onDidChangeContent = this._register(new Emitter()); @@ -44,35 +37,37 @@ export class NotebookEditorModel extends EditorModel implements IWorkingCopy, IN private _notebook!: NotebookTextModel; private _lastResolvedFileStat: IFileStatWithMetadata | undefined; + get lastResolvedFileStat() { + return this._lastResolvedFileStat; + } + get notebook() { return this._notebook; } - private _name!: string; - - get name() { - return this._name; - } - - private _workingCopyResource: URI; + private readonly _name: string; + private readonly _workingCopyResource: URI; constructor( - public readonly resource: URI, - public readonly viewType: string, + readonly resource: URI, + readonly viewType: string, @INotebookService private readonly _notebookService: INotebookService, @IWorkingCopyService private readonly _workingCopyService: IWorkingCopyService, @IBackupFileService private readonly _backupFileService: IBackupFileService, @IFileService private readonly _fileService: IFileService, - @INotificationService private readonly _notificationService: INotificationService + @INotificationService private readonly _notificationService: INotificationService, + @ILabelService labelService: ILabelService, ) { super(); + this._name = labelService.getUriBasenameLabel(resource); + const input = this; this._workingCopyResource = resource.with({ scheme: Schemas.vscodeNotebook }); const workingCopyAdapter = new class implements IWorkingCopy { readonly resource = input._workingCopyResource; - get name() { return input.name; } - readonly capabilities = input.isUntitled() ? WorkingCopyCapabilities.Untitled : input.capabilities; + get name() { return input._name; } + readonly capabilities = input.isUntitled() ? WorkingCopyCapabilities.Untitled : WorkingCopyCapabilities.None; readonly onDidChangeDirty = input.onDidChangeDirty; readonly onDidChangeContent = input.onDidChangeContent; isDirty(): boolean { return input.isDirty(); } @@ -84,8 +79,6 @@ export class NotebookEditorModel extends EditorModel implements IWorkingCopy, IN this._register(this._workingCopyService.registerWorkingCopy(workingCopyAdapter)); } - capabilities = 0; - async backup(): Promise> { if (this._notebook.supportBackup) { const tokenSource = new CancellationTokenSource(); @@ -128,7 +121,7 @@ export class NotebookEditorModel extends EditorModel implements IWorkingCopy, IN async load(options?: INotebookLoadOptions): Promise { if (options?.forceReadFromDisk) { - return this.loadFromProvider(true, undefined, undefined); + return this._loadFromProvider(true, undefined, undefined); } if (this.isResolved()) { @@ -141,19 +134,17 @@ export class NotebookEditorModel extends EditorModel implements IWorkingCopy, IN return this; // Make sure meanwhile someone else did not succeed in loading } - return this.loadFromProvider(false, options?.editorId, backup?.meta?.backupId); + return this._loadFromProvider(false, options?.editorId, backup?.meta?.backupId); } - private async loadFromProvider(forceReloadFromDisk: boolean, editorId: string | undefined, backupId: string | undefined) { - const notebook = await this._notebookService.resolveNotebook(this.viewType!, this.resource, forceReloadFromDisk, editorId, backupId); - this._notebook = notebook!; + private async _loadFromProvider(forceReloadFromDisk: boolean, editorId: string | undefined, backupId: string | undefined) { + this._notebook = await this._notebookService.resolveNotebook(this.viewType!, this.resource, forceReloadFromDisk, editorId, backupId); + const newStats = await this._resolveStats(this.resource); this._lastResolvedFileStat = newStats; this._register(this._notebook); - this._name = basename(this._notebook!.uri); - this._register(this._notebook.onDidChangeContent(() => { this._onDidChangeContent.fire(); })); @@ -161,6 +152,10 @@ export class NotebookEditorModel extends EditorModel implements IWorkingCopy, IN this._onDidChangeDirty.fire(); })); + if (forceReloadFromDisk) { + this._notebook.setDirty(false); + } + if (backupId) { await this._backupFileService.discardBackup(this._workingCopyResource); this._notebook.setDirty(true); @@ -187,7 +182,7 @@ export class NotebookEditorModel extends EditorModel implements IWorkingCopy, IN return new Promise<'overwrite' | 'revert' | 'none'>(resolve => { const handle = this._notificationService.prompt( Severity.Info, - nls.localize('notebook.staleSaveError', "The content of the file is newer. Please revert your version with the file contents or overwrite the content of the file with your changes"), + nls.localize('notebook.staleSaveError', "The contents of the file has changed on disk. Would you like to open the updated version or overwrite the file with your changes?"), [{ label: nls.localize('notebook.staleSaveError.revert', "Revert"), run: () => { @@ -261,10 +256,5 @@ export class NotebookEditorModel extends EditorModel implements IWorkingCopy, IN } catch (e) { return undefined; } - - } - - dispose() { - super.dispose(); } } diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts index 6ed66eee86..a4c9d9a395 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts @@ -7,15 +7,16 @@ import { createDecorator, IInstantiationService } from 'vs/platform/instantiatio import { URI } from 'vs/base/common/uri'; import { INotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; -import { IReference, ReferenceCollection } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable, IReference, ReferenceCollection } from 'vs/base/common/lifecycle'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { ILogService } from 'vs/platform/log/common/log'; +import { Event } from 'vs/base/common/event'; export const INotebookEditorModelResolverService = createDecorator('INotebookModelResolverService'); export interface INotebookEditorModelResolverService { readonly _serviceBrand: undefined; - resolve(resource: URI, viewType: string, editorId?: string): Promise>; + resolve(resource: URI, viewType?: string, editorId?: string): Promise>; } @@ -30,9 +31,16 @@ export class NotebookModelReferenceCollection extends ReferenceCollection { - const [viewType, editorId] = args as [string, string | undefined]; - const resource = URI.parse(key); + + let [viewType, editorId] = args as [string | undefined, string | undefined]; + if (!viewType) { + viewType = this._notebookService.getContributedNotebookProviders(resource)[0]?.id; + } + if (!viewType) { + throw new Error('Missing viewType'); + } + const model = this._instantiationService.createInstance(NotebookEditorModel, resource, viewType); const promise = model.load({ editorId }); return promise; @@ -60,12 +68,30 @@ export class NotebookModelResolverService implements INotebookEditorModelResolve this._data = instantiationService.createInstance(NotebookModelReferenceCollection); } - async resolve(resource: URI, viewType: string, editorId?: string | undefined): Promise> { + async resolve(resource: URI, viewType?: string, editorId?: string | undefined): Promise> { const reference = this._data.acquire(resource.toString(), viewType, editorId); const model = await reference.object; + NotebookModelResolverService._autoReferenceDirtyModel(model, () => this._data.acquire(resource.toString(), viewType, editorId)); return { object: model, dispose() { reference.dispose(); } }; } + + private static _autoReferenceDirtyModel(model: INotebookEditorModel, ref: () => IDisposable) { + + const references = new DisposableStore(); + const listener = model.notebook.onDidChangeDirty(() => { + if (model.notebook.isDirty) { + references.add(ref()); + } else { + references.clear(); + } + }); + + Event.once(model.notebook.onWillDispose)(() => { + listener.dispose(); + references.dispose(); + }); + } } diff --git a/src/vs/workbench/contrib/notebook/common/notebookProvider.ts b/src/vs/workbench/contrib/notebook/common/notebookProvider.ts index e3f39adbd1..e0dd8f9875 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookProvider.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookProvider.ts @@ -6,7 +6,7 @@ import * as glob from 'vs/base/common/glob'; import { URI } from 'vs/base/common/uri'; import { basename } from 'vs/base/common/path'; -import { INotebookKernelInfoDto, NotebookEditorPriority } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookEditorPriority } from 'vs/workbench/contrib/notebook/common/notebookCommon'; export interface NotebookSelector { readonly filenamePattern?: string; @@ -22,7 +22,6 @@ export interface NotebookEditorDescriptor { readonly providerDescription?: string; readonly providerDisplayName: string; readonly providerExtensionLocation: URI; - kernel?: INotebookKernelInfoDto; } export class NotebookProviderInfo implements NotebookEditorDescriptor { @@ -36,7 +35,6 @@ export class NotebookProviderInfo implements NotebookEditorDescriptor { readonly providerDescription?: string; readonly providerDisplayName: string; readonly providerExtensionLocation: URI; - kernel?: INotebookKernelInfoDto; constructor(descriptor: NotebookEditorDescriptor) { this.id = descriptor.id; diff --git a/src/vs/workbench/contrib/notebook/common/notebookService.ts b/src/vs/workbench/contrib/notebook/common/notebookService.ts index c6132b0b08..b867a86b06 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookService.ts @@ -9,8 +9,8 @@ import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/noteb import { NotebookExtensionDescription } from 'vs/workbench/api/common/extHost.protocol'; import { Event } from 'vs/base/common/event'; import { - INotebookTextModel, INotebookRendererInfo, INotebookKernelInfo, INotebookKernelInfoDto, - IEditor, ICellEditOperation, NotebookCellOutputsSplice, INotebookKernelProvider, INotebookKernelInfo2 + INotebookTextModel, INotebookRendererInfo, + IEditor, ICellEditOperation, NotebookCellOutputsSplice, INotebookKernelProvider, INotebookKernelInfo2, TransientMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -22,16 +22,12 @@ import { NotebookOutputRendererInfo } from 'vs/workbench/contrib/notebook/common export const INotebookService = createDecorator('notebookService'); export interface IMainNotebookController { - kernel: INotebookKernelInfoDto | undefined; supportBackup: boolean; + options: { transientOutputs: boolean; transientMetadata: TransientMetadata; }; createNotebook(textModel: NotebookTextModel, editorId?: string, backupId?: string): Promise; reloadNotebook(mainthreadTextModel: NotebookTextModel): Promise; resolveNotebookEditor(viewType: string, uri: URI, editorId: string): Promise; - executeNotebookByAttachedKernel(viewType: string, uri: URI): Promise; - cancelNotebookByAttachedKernel(viewType: string, uri: URI): Promise; onDidReceiveMessage(editorId: string, rendererType: string | undefined, message: any): void; - executeNotebookCell(uri: URI, handle: number): Promise; - cancelNotebookCell(uri: URI, handle: number): Promise; removeNotebookDocument(uri: URI): Promise; save(uri: URI, token: CancellationToken): Promise; saveAs(uri: URI, target: URI, token: CancellationToken): Promise; @@ -48,28 +44,20 @@ export interface INotebookService { onNotebookDocumentRemove: Event; onNotebookDocumentAdd: Event; onNotebookDocumentSaved: Event; - onDidChangeKernels: Event; + onDidChangeKernels: Event; onDidChangeNotebookActiveKernel: Event<{ uri: URI, providerHandle: number | undefined, kernelId: string | undefined }>; registerNotebookController(viewType: string, extensionData: NotebookExtensionDescription, controller: IMainNotebookController): void; unregisterNotebookProvider(viewType: string): void; transformEditsOutputs(textModel: NotebookTextModel, edits: ICellEditOperation[]): void; transformSpliceOutputs(textModel: NotebookTextModel, splices: NotebookCellOutputsSplice[]): void; - registerNotebookKernel(kernel: INotebookKernelInfo): void; - unregisterNotebookKernel(id: string): void; registerNotebookKernelProvider(provider: INotebookKernelProvider): IDisposable; - getContributedNotebookKernels(viewType: string, resource: URI): readonly INotebookKernelInfo[]; getContributedNotebookKernels2(viewType: string, resource: URI, token: CancellationToken): Promise; getContributedNotebookOutputRenderers(id: string): NotebookOutputRendererInfo | undefined; getRendererInfo(id: string): INotebookRendererInfo | undefined; - resolveNotebook(viewType: string, uri: URI, forceReload: boolean, editorId?: string, backupId?: string): Promise; + resolveNotebook(viewType: string, uri: URI, forceReload: boolean, editorId?: string, backupId?: string): Promise; getNotebookTextModel(uri: URI): NotebookTextModel | undefined; - executeNotebook(viewType: string, uri: URI): Promise; - cancelNotebook(viewType: string, uri: URI): Promise; - executeNotebookCell(viewType: string, uri: URI, handle: number): Promise; - cancelNotebookCell(viewType: string, uri: URI, handle: number): Promise; - executeNotebook2(viewType: string, uri: URI, kernelId: string): Promise; - executeNotebookCell2(viewType: string, uri: URI, handle: number, kernelId: string): Promise; + getNotebookTextModels(): Iterable; getContributedNotebookProviders(resource: URI): readonly NotebookProviderInfo[]; getContributedNotebookProvider(viewType: string): NotebookProviderInfo | undefined; getNotebookProviderResourceRoots(): URI[]; diff --git a/src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorker.ts b/src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorker.ts new file mode 100644 index 0000000000..a2292346d4 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorker.ts @@ -0,0 +1,221 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { ISequence, LcsDiff } from 'vs/base/common/diff/diff'; +import { hash } from 'vs/base/common/hash'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { IRequestHandler } from 'vs/base/common/worker/simpleWorker'; +import * as model from 'vs/editor/common/model'; +import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; +import { CellKind, ICellDto2, IMainCellDto, INotebookDiffResult, IProcessedOutput, NotebookCellMetadata, NotebookDataDto, NotebookDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { Range } from 'vs/editor/common/core/range'; +import { EditorWorkerHost } from 'vs/workbench/contrib/notebook/common/services/notebookWorkerServiceImpl'; + +class MirrorCell { + private _textBuffer!: model.IReadonlyTextBuffer; + + get textBuffer() { + if (this._textBuffer) { + return this._textBuffer; + } + + const builder = new PieceTreeTextBufferBuilder(); + builder.acceptChunk(Array.isArray(this._source) ? this._source.join('\n') : this._source); + const bufferFactory = builder.finish(true); + this._textBuffer = bufferFactory.create(model.DefaultEndOfLine.LF); + + return this._textBuffer; + } + + private _primaryKey?: number | null = null; + primaryKey(): number | null { + if (this._primaryKey === undefined) { + this._primaryKey = hash(this.getValue()); + } + + return this._primaryKey; + } + + private _hash: number | null = null; + + constructor( + readonly handle: number, + private _source: string | string[], + readonly language: string, + readonly cellKind: CellKind, + readonly outputs: IProcessedOutput[], + readonly metadata?: NotebookCellMetadata + + ) { } + + getFullModelRange() { + const lineCount = this.textBuffer.getLineCount(); + return new Range(1, 1, lineCount, this.textBuffer.getLineLength(lineCount) + 1); + } + + getValue(): string { + const fullRange = this.getFullModelRange(); + const eol = this.textBuffer.getEOL(); + if (eol === '\n') { + return this.textBuffer.getValueInRange(fullRange, model.EndOfLinePreference.LF); + } else { + return this.textBuffer.getValueInRange(fullRange, model.EndOfLinePreference.CRLF); + } + } + + getComparisonValue(): number { + if (this._primaryKey !== null) { + return this._primaryKey!; + } + + this._hash = hash([hash(this.getValue()), this.metadata]); + return this._hash; + } + + getHashValue() { + if (this._hash !== null) { + return this._hash; + } + + this._hash = hash([hash(this.getValue()), this.language, this.metadata]); + return this._hash; + } +} + +class MirrorNotebookDocument { + constructor( + readonly uri: URI, + readonly cells: MirrorCell[], + readonly languages: string[], + readonly metadata: NotebookDocumentMetadata, + ) { + } +} + +export class CellSequence implements ISequence { + + constructor(readonly textModel: MirrorNotebookDocument) { + } + + getElements(): string[] | number[] | Int32Array { + const hashValue = new Int32Array(this.textModel.cells.length); + for (let i = 0; i < this.textModel.cells.length; i++) { + hashValue[i] = this.textModel.cells[i].getComparisonValue(); + } + + return hashValue; + } + + getCellHash(cell: ICellDto2) { + const source = Array.isArray(cell.source) ? cell.source.join('\n') : cell.source; + const hashVal = hash([hash(source), cell.metadata]); + return hashVal; + } +} + +export class NotebookEditorSimpleWorker implements IRequestHandler, IDisposable { + _requestHandlerBrand: any; + + private _models: { [uri: string]: MirrorNotebookDocument; }; + + constructor() { + this._models = Object.create(null); + } + dispose(): void { + } + + public acceptNewModel(uri: string, data: NotebookDataDto): void { + this._models[uri] = new MirrorNotebookDocument(URI.parse(uri), data.cells.map(dto => new MirrorCell( + (dto as unknown as IMainCellDto).handle, + dto.source, + dto.language, + dto.cellKind, + dto.outputs, + dto.metadata + )), data.languages, data.metadata); + } + + public acceptRemovedModel(strURL: string): void { + if (!this._models[strURL]) { + return; + } + delete this._models[strURL]; + } + + computeDiff(originalUrl: string, modifiedUrl: string): INotebookDiffResult { + const original = this._getModel(originalUrl); + const modified = this._getModel(modifiedUrl); + + const diff = new LcsDiff(new CellSequence(original), new CellSequence(modified)); + const diffResult = diff.ComputeDiff(false); + + /* let cellLineChanges: { originalCellhandle: number, modifiedCellhandle: number, lineChanges: editorCommon.ILineChange[] }[] = []; + + diffResult.changes.forEach(change => { + if (change.modifiedLength === 0) { + // deletion ... + return; + } + + if (change.originalLength === 0) { + // insertion + return; + } + + for (let i = 0, len = Math.min(change.modifiedLength, change.originalLength); i < len; i++) { + let originalIndex = change.originalStart + i; + let modifiedIndex = change.modifiedStart + i; + + const originalCell = original.cells[originalIndex]; + const modifiedCell = modified.cells[modifiedIndex]; + + if (originalCell.getValue() !== modifiedCell.getValue()) { + // console.log(`original cell ${originalIndex} content change`); + const originalLines = originalCell.textBuffer.getLinesContent(); + const modifiedLines = modifiedCell.textBuffer.getLinesContent(); + const diffComputer = new DiffComputer(originalLines, modifiedLines, { + shouldComputeCharChanges: true, + shouldPostProcessCharChanges: true, + shouldIgnoreTrimWhitespace: false, + shouldMakePrettyDiff: true, + maxComputationTime: 5000 + }); + + const lineChanges = diffComputer.computeDiff().changes; + + cellLineChanges.push({ + originalCellhandle: originalCell.handle, + modifiedCellhandle: modifiedCell.handle, + lineChanges + }); + + // console.log(lineDecorations); + + } else { + // console.log(`original cell ${originalIndex} metadata change`); + } + + } + }); + */ + return { + cellsDiff: diffResult, + // linesDiff: cellLineChanges + }; + } + + protected _getModel(uri: string): MirrorNotebookDocument { + return this._models[uri]; + } +} + +/** + * Called on the worker side + * @internal + */ +export function create(host: EditorWorkerHost): IRequestHandler { + return new NotebookEditorSimpleWorker(); +} + diff --git a/src/vs/workbench/contrib/notebook/common/services/notebookWorkerService.ts b/src/vs/workbench/contrib/notebook/common/services/notebookWorkerService.ts new file mode 100644 index 0000000000..197143420c --- /dev/null +++ b/src/vs/workbench/contrib/notebook/common/services/notebookWorkerService.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from 'vs/base/common/uri'; +import { ILineChange } from 'vs/editor/common/editorCommon'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { INotebookDiffResult } from 'vs/workbench/contrib/notebook/common/notebookCommon'; + +export const ID_NOTEBOOK_EDITOR_WORKER_SERVICE = 'notebookEditorWorkerService'; +export const INotebookEditorWorkerService = createDecorator(ID_NOTEBOOK_EDITOR_WORKER_SERVICE); + +export interface IDiffComputationResult { + quitEarly: boolean; + identical: boolean; + changes: ILineChange[]; +} + +export interface INotebookEditorWorkerService { + readonly _serviceBrand: undefined; + + canComputeDiff(original: URI, modified: URI): boolean; + computeDiff(original: URI, modified: URI): Promise; +} diff --git a/src/vs/workbench/contrib/notebook/common/services/notebookWorkerServiceImpl.ts b/src/vs/workbench/contrib/notebook/common/services/notebookWorkerServiceImpl.ts new file mode 100644 index 0000000000..96b8fa837e --- /dev/null +++ b/src/vs/workbench/contrib/notebook/common/services/notebookWorkerServiceImpl.ts @@ -0,0 +1,219 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, DisposableStore, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { SimpleWorkerClient } from 'vs/base/common/worker/simpleWorker'; +import { DefaultWorkerFactory } from 'vs/base/worker/defaultWorkerFactory'; +import { INotebookDiffResult } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { NotebookEditorSimpleWorker } from 'vs/workbench/contrib/notebook/common/services/notebookSimpleWorker'; +import { INotebookEditorWorkerService } from 'vs/workbench/contrib/notebook/common/services/notebookWorkerService'; + +export class NotebookEditorWorkerServiceImpl extends Disposable implements INotebookEditorWorkerService { + declare readonly _serviceBrand: undefined; + + private readonly _workerManager: WorkerManager; + + constructor( + @INotebookService notebookService: INotebookService + ) { + super(); + + this._workerManager = this._register(new WorkerManager(notebookService)); + } + canComputeDiff(original: URI, modified: URI): boolean { + throw new Error('Method not implemented.'); + } + + computeDiff(original: URI, modified: URI): Promise { + return this._workerManager.withWorker().then(client => { + return client.computeDiff(original, modified); + }); + } +} + +export class WorkerManager extends Disposable { + private _editorWorkerClient: NotebookWorkerClient | null; + // private _lastWorkerUsedTime: number; + + constructor( + private readonly _notebookService: INotebookService + ) { + super(); + this._editorWorkerClient = null; + // this._lastWorkerUsedTime = (new Date()).getTime(); + } + + withWorker(): Promise { + // this._lastWorkerUsedTime = (new Date()).getTime(); + if (!this._editorWorkerClient) { + this._editorWorkerClient = new NotebookWorkerClient(this._notebookService, 'notebookEditorWorkerService'); + } + return Promise.resolve(this._editorWorkerClient); + } +} + +export interface IWorkerClient { + getProxyObject(): Promise; + dispose(): void; +} + +export class NotebookEditorModelManager extends Disposable { + private _syncedModels: { [modelUrl: string]: IDisposable; } = Object.create(null); + private _syncedModelsLastUsedTime: { [modelUrl: string]: number; } = Object.create(null); + + constructor( + private readonly _proxy: NotebookEditorSimpleWorker, + private readonly _notebookService: INotebookService + ) { + super(); + } + + public ensureSyncedResources(resources: URI[]): void { + for (const resource of resources) { + let resourceStr = resource.toString(); + + if (!this._syncedModels[resourceStr]) { + this._beginModelSync(resource); + } + if (this._syncedModels[resourceStr]) { + this._syncedModelsLastUsedTime[resourceStr] = (new Date()).getTime(); + } + } + } + + private _beginModelSync(resource: URI): void { + let model = this._notebookService.listNotebookDocuments().find(document => document.uri.toString() === resource.toString()); + if (!model) { + return; + } + + let modelUrl = resource.toString(); + + this._proxy.acceptNewModel( + model.uri.toString(), + { + cells: model.cells.map(cell => ({ + handle: cell.handle, + uri: cell.uri, + source: cell.getValue(), + eol: cell.textBuffer.getEOL(), + language: cell.language, + cellKind: cell.cellKind, + outputs: cell.outputs, + metadata: cell.metadata + })), + languages: model.languages, + metadata: model.metadata + } + ); + + const toDispose = new DisposableStore(); + + // TODO, accept Model change + + // toDispose.add(model.onDidChangeContent((e) => { + // this._proxy.acceptModelChanged(modelUrl.toString(), e); + // })); + toDispose.add(model.onWillDispose(() => { + this._stopModelSync(modelUrl); + })); + toDispose.add(toDisposable(() => { + this._proxy.acceptRemovedModel(modelUrl); + })); + + this._syncedModels[modelUrl] = toDispose; + } + + private _stopModelSync(modelUrl: string): void { + let toDispose = this._syncedModels[modelUrl]; + delete this._syncedModels[modelUrl]; + delete this._syncedModelsLastUsedTime[modelUrl]; + dispose(toDispose); + } +} + +export class EditorWorkerHost { + + private readonly _workerClient: NotebookWorkerClient; + + constructor(workerClient: NotebookWorkerClient) { + this._workerClient = workerClient; + } + + // foreign host request + public fhr(method: string, args: any[]): Promise { + return this._workerClient.fhr(method, args); + } +} + +export class NotebookWorkerClient extends Disposable { + private _worker: IWorkerClient | null; + private readonly _workerFactory: DefaultWorkerFactory; + private _modelManager: NotebookEditorModelManager | null; + + + constructor(private readonly _notebookService: INotebookService, label: string) { + super(); + this._workerFactory = new DefaultWorkerFactory(label); + this._worker = null; + this._modelManager = null; + + } + + // foreign host request + public fhr(method: string, args: any[]): Promise { + throw new Error(`Not implemented!`); + } + + computeDiff(original: URI, modified: URI) { + return this._withSyncedResources([original, modified]).then(proxy => { + return proxy.computeDiff(original.toString(), modified.toString()); + }); + } + + private _getOrCreateModelManager(proxy: NotebookEditorSimpleWorker): NotebookEditorModelManager { + if (!this._modelManager) { + this._modelManager = this._register(new NotebookEditorModelManager(proxy, this._notebookService)); + } + return this._modelManager; + } + + protected _withSyncedResources(resources: URI[]): Promise { + return this._getProxy().then((proxy) => { + this._getOrCreateModelManager(proxy).ensureSyncedResources(resources); + return proxy; + }); + } + + private _getOrCreateWorker(): IWorkerClient { + if (!this._worker) { + try { + this._worker = this._register(new SimpleWorkerClient( + this._workerFactory, + 'vs/workbench/contrib/notebook/common/services/notebookSimpleWorker', + new EditorWorkerHost(this) + )); + } catch (err) { + // logOnceWebWorkerWarning(err); + // this._worker = new SynchronousWorkerClient(new EditorSimpleWorker(new EditorWorkerHost(this), null)); + throw (err); + } + } + return this._worker; + } + + protected _getProxy(): Promise { + return this._getOrCreateWorker().getProxyObject().then(undefined, (err) => { + // logOnceWebWorkerWarning(err); + // this._worker = new SynchronousWorkerClient(new EditorSimpleWorker(new EditorWorkerHost(this), null)); + // return this._getOrCreateWorker().getProxyObject(); + throw (err); + }); + } + + +} diff --git a/src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts b/src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts index 8e004b9bc2..75202e987e 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts @@ -269,7 +269,7 @@ suite('NotebookCommon', () => { for (let i = 0; i < 5; i++) { cells.push( - new TestCell('notebook', i, [`var a = ${i};`], 'javascript', CellKind.Code, [], textModelService) + new TestCell('notebook', i, `var a = ${i};`, 'javascript', CellKind.Code, [], textModelService) ); } @@ -295,8 +295,8 @@ suite('NotebookCommon', () => { ] ); - const cellA = new TestCell('notebook', 6, ['var a = 6;'], 'javascript', CellKind.Code, [], textModelService); - const cellB = new TestCell('notebook', 7, ['var a = 7;'], 'javascript', CellKind.Code, [], textModelService); + const cellA = new TestCell('notebook', 6, 'var a = 6;', 'javascript', CellKind.Code, [], textModelService); + const cellB = new TestCell('notebook', 7, 'var a = 7;', 'javascript', CellKind.Code, [], textModelService); const modifiedCells = [ cells[0], diff --git a/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts index 1a0fc4ba62..0319886a2d 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { CellKind, CellEditType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, CellEditType, CellOutputKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { withTestNotebook, TestCell, setupInstantiationService } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; @@ -23,15 +23,15 @@ suite('NotebookTextModel', () => { blukEditService, undoRedoService, [ - [['var a = 1;'], 'javascript', CellKind.Code, [], { editable: true }], - [['var b = 2;'], 'javascript', CellKind.Code, [], { editable: false }], - [['var c = 3;'], 'javascript', CellKind.Code, [], { editable: false }], - [['var d = 4;'], 'javascript', CellKind.Code, [], { editable: false }] + ['var a = 1;', 'javascript', CellKind.Code, [], { editable: true }], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: false }], + ['var c = 3;', 'javascript', CellKind.Code, [], { editable: false }], + ['var d = 4;', 'javascript', CellKind.Code, [], { editable: false }] ], (editor, viewModel, textModel) => { - textModel.$applyEdit(textModel.versionId, [ - { editType: CellEditType.Insert, index: 1, cells: [new TestCell(viewModel.viewType, 5, ['var e = 5;'], 'javascript', CellKind.Code, [], textModelService)] }, - { editType: CellEditType.Insert, index: 3, cells: [new TestCell(viewModel.viewType, 6, ['var f = 6;'], 'javascript', CellKind.Code, [], textModelService)] }, + textModel.applyEdit(textModel.versionId, [ + { editType: CellEditType.Replace, index: 1, count: 0, cells: [new TestCell(viewModel.viewType, 5, 'var e = 5;', 'javascript', CellKind.Code, [], textModelService)] }, + { editType: CellEditType.Replace, index: 3, count: 0, cells: [new TestCell(viewModel.viewType, 6, 'var f = 6;', 'javascript', CellKind.Code, [], textModelService)] }, ], true); assert.equal(textModel.cells.length, 6); @@ -48,15 +48,15 @@ suite('NotebookTextModel', () => { blukEditService, undoRedoService, [ - [['var a = 1;'], 'javascript', CellKind.Code, [], { editable: true }], - [['var b = 2;'], 'javascript', CellKind.Code, [], { editable: false }], - [['var c = 3;'], 'javascript', CellKind.Code, [], { editable: false }], - [['var d = 4;'], 'javascript', CellKind.Code, [], { editable: false }] + ['var a = 1;', 'javascript', CellKind.Code, [], { editable: true }], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: false }], + ['var c = 3;', 'javascript', CellKind.Code, [], { editable: false }], + ['var d = 4;', 'javascript', CellKind.Code, [], { editable: false }] ], (editor, viewModel, textModel) => { - textModel.$applyEdit(textModel.versionId, [ - { editType: CellEditType.Insert, index: 1, cells: [new TestCell(viewModel.viewType, 5, ['var e = 5;'], 'javascript', CellKind.Code, [], textModelService)] }, - { editType: CellEditType.Insert, index: 1, cells: [new TestCell(viewModel.viewType, 6, ['var f = 6;'], 'javascript', CellKind.Code, [], textModelService)] }, + textModel.applyEdit(textModel.versionId, [ + { editType: CellEditType.Replace, index: 1, count: 0, cells: [new TestCell(viewModel.viewType, 5, 'var e = 5;', 'javascript', CellKind.Code, [], textModelService)] }, + { editType: CellEditType.Replace, index: 1, count: 0, cells: [new TestCell(viewModel.viewType, 6, 'var f = 6;', 'javascript', CellKind.Code, [], textModelService)] }, ], true); assert.equal(textModel.cells.length, 6); @@ -73,15 +73,15 @@ suite('NotebookTextModel', () => { blukEditService, undoRedoService, [ - [['var a = 1;'], 'javascript', CellKind.Code, [], { editable: true }], - [['var b = 2;'], 'javascript', CellKind.Code, [], { editable: false }], - [['var c = 3;'], 'javascript', CellKind.Code, [], { editable: false }], - [['var d = 4;'], 'javascript', CellKind.Code, [], { editable: false }] + ['var a = 1;', 'javascript', CellKind.Code, [], { editable: true }], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: false }], + ['var c = 3;', 'javascript', CellKind.Code, [], { editable: false }], + ['var d = 4;', 'javascript', CellKind.Code, [], { editable: false }] ], (editor, viewModel, textModel) => { - textModel.$applyEdit(textModel.versionId, [ - { editType: CellEditType.Delete, index: 1, count: 1 }, - { editType: CellEditType.Delete, index: 3, count: 1 }, + textModel.applyEdit(textModel.versionId, [ + { editType: CellEditType.Replace, index: 1, count: 1, cells: [] }, + { editType: CellEditType.Replace, index: 3, count: 1, cells: [] }, ], true); assert.equal(textModel.cells[0].getValue(), 'var a = 1;'); @@ -96,15 +96,15 @@ suite('NotebookTextModel', () => { blukEditService, undoRedoService, [ - [['var a = 1;'], 'javascript', CellKind.Code, [], { editable: true }], - [['var b = 2;'], 'javascript', CellKind.Code, [], { editable: false }], - [['var c = 3;'], 'javascript', CellKind.Code, [], { editable: false }], - [['var d = 4;'], 'javascript', CellKind.Code, [], { editable: false }] + ['var a = 1;', 'javascript', CellKind.Code, [], { editable: true }], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: false }], + ['var c = 3;', 'javascript', CellKind.Code, [], { editable: false }], + ['var d = 4;', 'javascript', CellKind.Code, [], { editable: false }] ], (editor, viewModel, textModel) => { - textModel.$applyEdit(textModel.versionId, [ - { editType: CellEditType.Delete, index: 1, count: 1 }, - { editType: CellEditType.Insert, index: 3, cells: [new TestCell(viewModel.viewType, 5, ['var e = 5;'], 'javascript', CellKind.Code, [], textModelService)] }, + textModel.applyEdit(textModel.versionId, [ + { editType: CellEditType.Replace, index: 1, count: 1, cells: [] }, + { editType: CellEditType.Replace, index: 3, count: 0, cells: [new TestCell(viewModel.viewType, 5, 'var e = 5;', 'javascript', CellKind.Code, [], textModelService)] }, ], true); assert.equal(textModel.cells.length, 4); @@ -121,15 +121,15 @@ suite('NotebookTextModel', () => { blukEditService, undoRedoService, [ - [['var a = 1;'], 'javascript', CellKind.Code, [], { editable: true }], - [['var b = 2;'], 'javascript', CellKind.Code, [], { editable: false }], - [['var c = 3;'], 'javascript', CellKind.Code, [], { editable: false }], - [['var d = 4;'], 'javascript', CellKind.Code, [], { editable: false }] + ['var a = 1;', 'javascript', CellKind.Code, [], { editable: true }], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: false }], + ['var c = 3;', 'javascript', CellKind.Code, [], { editable: false }], + ['var d = 4;', 'javascript', CellKind.Code, [], { editable: false }] ], (editor, viewModel, textModel) => { - textModel.$applyEdit(textModel.versionId, [ - { editType: CellEditType.Delete, index: 1, count: 1 }, - { editType: CellEditType.Insert, index: 1, cells: [new TestCell(viewModel.viewType, 5, ['var e = 5;'], 'javascript', CellKind.Code, [], textModelService)] }, + textModel.applyEdit(textModel.versionId, [ + { editType: CellEditType.Replace, index: 1, count: 1, cells: [] }, + { editType: CellEditType.Replace, index: 1, count: 0, cells: [new TestCell(viewModel.viewType, 5, 'var e = 5;', 'javascript', CellKind.Code, [], textModelService)] }, ], true); assert.equal(textModel.cells.length, 4); @@ -139,5 +139,113 @@ suite('NotebookTextModel', () => { } ); }); -}); + test('(replace) delete + insert at same position', function () { + withTestNotebook( + instantiationService, + blukEditService, + undoRedoService, + [ + ['var a = 1;', 'javascript', CellKind.Code, [], { editable: true }], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: false }], + ['var c = 3;', 'javascript', CellKind.Code, [], { editable: false }], + ['var d = 4;', 'javascript', CellKind.Code, [], { editable: false }] + ], + (editor, viewModel, textModel) => { + textModel.applyEdit(textModel.versionId, [ + { editType: CellEditType.Replace, index: 1, count: 1, cells: [new TestCell(viewModel.viewType, 5, 'var e = 5;', 'javascript', CellKind.Code, [], textModelService)] }, + ], true); + + assert.equal(textModel.cells.length, 4); + assert.equal(textModel.cells[0].getValue(), 'var a = 1;'); + assert.equal(textModel.cells[1].getValue(), 'var e = 5;'); + assert.equal(textModel.cells[2].getValue(), 'var c = 3;'); + } + ); + }); + + test('output', function () { + withTestNotebook( + instantiationService, + blukEditService, + undoRedoService, + [ + ['var a = 1;', 'javascript', CellKind.Code, [], { editable: true }], + ], + (editor, viewModel, textModel) => { + + // invalid index 1 + assert.throws(() => { + textModel.applyEdit(textModel.versionId, [{ + index: Number.MAX_VALUE, + editType: CellEditType.Output, + outputs: [] + }], true); + }); + + // invalid index 2 + assert.throws(() => { + textModel.applyEdit(textModel.versionId, [{ + index: -1, + editType: CellEditType.Output, + outputs: [] + }], true); + }); + + textModel.applyEdit(textModel.versionId, [{ + index: 0, + editType: CellEditType.Output, + outputs: [{ + outputKind: CellOutputKind.Rich, + outputId: 'someId', + data: { 'text/markdown': '_Hello_' } + }] + }], true); + + assert.equal(textModel.cells.length, 1); + assert.equal(textModel.cells[0].outputs.length, 1); + assert.equal(textModel.cells[0].outputs[0].outputKind, CellOutputKind.Rich); + } + ); + }); + + test('metadata', function () { + withTestNotebook( + instantiationService, + blukEditService, + undoRedoService, + [ + ['var a = 1;', 'javascript', CellKind.Code, [], { editable: true }], + ], + (editor, viewModel, textModel) => { + + // invalid index 1 + assert.throws(() => { + textModel.applyEdit(textModel.versionId, [{ + index: Number.MAX_VALUE, + editType: CellEditType.Metadata, + metadata: { editable: false } + }], true); + }); + + // invalid index 2 + assert.throws(() => { + textModel.applyEdit(textModel.versionId, [{ + index: -1, + editType: CellEditType.Metadata, + metadata: { editable: false } + }], true); + }); + + textModel.applyEdit(textModel.versionId, [{ + index: 0, + editType: CellEditType.Metadata, + metadata: { editable: false }, + }], true); + + assert.equal(textModel.cells.length, 1); + assert.equal(textModel.cells[0].metadata?.editable, false); + } + ); + }); +}); diff --git a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts index bd8357a79f..ca1a923e6c 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts @@ -6,24 +6,26 @@ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { CellKind, NotebookCellMetadata, diff } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, NotebookCellMetadata, diff, ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { withTestNotebook, TestCell, NotebookEditorTestModel, setupInstantiationService } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; import { TrackedRangeStickiness } from 'vs/editor/common/model'; -import { reduceCellRanges, ICellRange } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { reduceCellRanges } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { IModeService } from 'vs/editor/common/services/modeService'; suite('NotebookViewModel', () => { const instantiationService = setupInstantiationService(); const textModelService = instantiationService.get(ITextModelService); const blukEditService = instantiationService.get(IBulkEditService); const undoRedoService = instantiationService.get(IUndoRedoService); + const modeService = instantiationService.get(IModeService); test('ctor', function () { - const notebook = new NotebookTextModel(0, 'notebook', false, URI.parse('test'), undoRedoService, textModelService); + const notebook = new NotebookTextModel(0, 'notebook', false, URI.parse('test'), undoRedoService, textModelService, modeService); const model = new NotebookEditorTestModel(notebook); const eventDispatcher = new NotebookEventDispatcher(); const viewModel = new NotebookViewModel('notebook', model.notebook, eventDispatcher, null, instantiationService, blukEditService, undoRedoService); @@ -36,14 +38,14 @@ suite('NotebookViewModel', () => { blukEditService, undoRedoService, [ - [['var a = 1;'], 'javascript', CellKind.Code, [], { editable: true }], - [['var b = 2;'], 'javascript', CellKind.Code, [], { editable: false }] + ['var a = 1;', 'javascript', CellKind.Code, [], { editable: true }], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: false }] ], (editor, viewModel) => { assert.equal(viewModel.viewCells[0].metadata?.editable, true); assert.equal(viewModel.viewCells[1].metadata?.editable, false); - const cell = viewModel.insertCell(1, new TestCell(viewModel.viewType, 0, ['var c = 3;'], 'javascript', CellKind.Code, [], textModelService), true); + const cell = viewModel.insertCell(1, new TestCell(viewModel.viewType, 0, 'var c = 3;', 'javascript', CellKind.Code, [], textModelService), true); assert.equal(viewModel.viewCells.length, 3); assert.equal(viewModel.notebookDocument.cells.length, 3); assert.equal(viewModel.getCellIndex(cell), 1); @@ -62,9 +64,9 @@ suite('NotebookViewModel', () => { blukEditService, undoRedoService, [ - [['//a'], 'javascript', CellKind.Code, [], { editable: true }], - [['//b'], 'javascript', CellKind.Code, [], { editable: true }], - [['//c'], 'javascript', CellKind.Code, [], { editable: true }], + ['//a', 'javascript', CellKind.Code, [], { editable: true }], + ['//b', 'javascript', CellKind.Code, [], { editable: true }], + ['//c', 'javascript', CellKind.Code, [], { editable: true }], ], (editor, viewModel) => { viewModel.moveCellToIdx(0, 1, 0, false); @@ -93,9 +95,9 @@ suite('NotebookViewModel', () => { blukEditService, undoRedoService, [ - [['//a'], 'javascript', CellKind.Code, [], { editable: true }], - [['//b'], 'javascript', CellKind.Code, [], { editable: true }], - [['//c'], 'javascript', CellKind.Code, [], { editable: true }], + ['//a', 'javascript', CellKind.Code, [], { editable: true }], + ['//b', 'javascript', CellKind.Code, [], { editable: true }], + ['//c', 'javascript', CellKind.Code, [], { editable: true }], ], (editor, viewModel) => { viewModel.moveCellToIdx(1, 1, 0, false); @@ -118,21 +120,21 @@ suite('NotebookViewModel', () => { blukEditService, undoRedoService, [ - [['var a = 1;'], 'javascript', CellKind.Code, [], { editable: true }], - [['var b = 2;'], 'javascript', CellKind.Code, [], { editable: true }] + ['var a = 1;', 'javascript', CellKind.Code, [], { editable: true }], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: true }] ], (editor, viewModel) => { const firstViewCell = viewModel.viewCells[0]; const lastViewCell = viewModel.viewCells[viewModel.viewCells.length - 1]; const insertIndex = viewModel.getCellIndex(firstViewCell) + 1; - const cell = viewModel.insertCell(insertIndex, new TestCell(viewModel.viewType, 3, ['var c = 3;'], 'javascript', CellKind.Code, [], textModelService), true); + const cell = viewModel.insertCell(insertIndex, new TestCell(viewModel.viewType, 3, 'var c = 3;', 'javascript', CellKind.Code, [], textModelService), true); const addedCellIndex = viewModel.getCellIndex(cell); viewModel.deleteCell(addedCellIndex, true); const secondInsertIndex = viewModel.getCellIndex(lastViewCell) + 1; - const cell2 = viewModel.insertCell(secondInsertIndex, new TestCell(viewModel.viewType, 4, ['var d = 4;'], 'javascript', CellKind.Code, [], textModelService), true); + const cell2 = viewModel.insertCell(secondInsertIndex, new TestCell(viewModel.viewType, 4, 'var d = 4;', 'javascript', CellKind.Code, [], textModelService), true); assert.equal(viewModel.viewCells.length, 3); assert.equal(viewModel.notebookDocument.cells.length, 3); @@ -147,11 +149,11 @@ suite('NotebookViewModel', () => { blukEditService, undoRedoService, [ - [['var a = 1;'], 'javascript', CellKind.Code, [], {}], - [['var b = 2;'], 'javascript', CellKind.Code, [], { editable: true, runnable: true }], - [['var c = 3;'], 'javascript', CellKind.Code, [], { editable: true, runnable: false }], - [['var d = 4;'], 'javascript', CellKind.Code, [], { editable: false, runnable: true }], - [['var e = 5;'], 'javascript', CellKind.Code, [], { editable: false, runnable: false }], + ['var a = 1;', 'javascript', CellKind.Code, [], {}], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: true, runnable: true }], + ['var c = 3;', 'javascript', CellKind.Code, [], { editable: true, runnable: false }], + ['var d = 4;', 'javascript', CellKind.Code, [], { editable: false, runnable: true }], + ['var e = 5;', 'javascript', CellKind.Code, [], { editable: false, runnable: false }], ], (editor, viewModel) => { viewModel.notebookDocument.metadata = { editable: true, runnable: true, cellRunnable: true, cellEditable: true, cellHasExecutionOrder: true }; @@ -269,11 +271,11 @@ suite('NotebookViewModel Decorations', () => { blukEditService, undoRedoService, [ - [['var a = 1;'], 'javascript', CellKind.Code, [], {}], - [['var b = 2;'], 'javascript', CellKind.Code, [], { editable: true, runnable: true }], - [['var c = 3;'], 'javascript', CellKind.Code, [], { editable: true, runnable: false }], - [['var d = 4;'], 'javascript', CellKind.Code, [], { editable: false, runnable: true }], - [['var e = 5;'], 'javascript', CellKind.Code, [], { editable: false, runnable: false }], + ['var a = 1;', 'javascript', CellKind.Code, [], {}], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: true, runnable: true }], + ['var c = 3;', 'javascript', CellKind.Code, [], { editable: true, runnable: false }], + ['var d = 4;', 'javascript', CellKind.Code, [], { editable: false, runnable: true }], + ['var e = 5;', 'javascript', CellKind.Code, [], { editable: false, runnable: false }], ], (editor, viewModel) => { const trackedId = viewModel.setTrackedRange('test', { start: 1, end: 2 }, TrackedRangeStickiness.GrowsOnlyWhenTypingAfter); @@ -283,7 +285,7 @@ suite('NotebookViewModel Decorations', () => { end: 2, }); - viewModel.insertCell(0, new TestCell(viewModel.viewType, 5, ['var d = 6;'], 'javascript', CellKind.Code, [], textModelService), true); + viewModel.insertCell(0, new TestCell(viewModel.viewType, 5, 'var d = 6;', 'javascript', CellKind.Code, [], textModelService), true); assert.deepEqual(viewModel.getTrackedRange(trackedId!), { start: 2, @@ -297,7 +299,7 @@ suite('NotebookViewModel Decorations', () => { end: 2 }); - viewModel.insertCell(3, new TestCell(viewModel.viewType, 6, ['var d = 7;'], 'javascript', CellKind.Code, [], textModelService), true); + viewModel.insertCell(3, new TestCell(viewModel.viewType, 6, 'var d = 7;', 'javascript', CellKind.Code, [], textModelService), true); assert.deepEqual(viewModel.getTrackedRange(trackedId!), { start: 1, @@ -327,13 +329,13 @@ suite('NotebookViewModel Decorations', () => { blukEditService, undoRedoService, [ - [['var a = 1;'], 'javascript', CellKind.Code, [], {}], - [['var b = 2;'], 'javascript', CellKind.Code, [], { editable: true, runnable: true }], - [['var c = 3;'], 'javascript', CellKind.Code, [], { editable: true, runnable: false }], - [['var d = 4;'], 'javascript', CellKind.Code, [], { editable: false, runnable: true }], - [['var e = 5;'], 'javascript', CellKind.Code, [], { editable: false, runnable: false }], - [['var e = 6;'], 'javascript', CellKind.Code, [], { editable: false, runnable: false }], - [['var e = 7;'], 'javascript', CellKind.Code, [], { editable: false, runnable: false }], + ['var a = 1;', 'javascript', CellKind.Code, [], {}], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: true, runnable: true }], + ['var c = 3;', 'javascript', CellKind.Code, [], { editable: true, runnable: false }], + ['var d = 4;', 'javascript', CellKind.Code, [], { editable: false, runnable: true }], + ['var e = 5;', 'javascript', CellKind.Code, [], { editable: false, runnable: false }], + ['var e = 6;', 'javascript', CellKind.Code, [], { editable: false, runnable: false }], + ['var e = 7;', 'javascript', CellKind.Code, [], { editable: false, runnable: false }], ], (editor, viewModel) => { const trackedId = viewModel.setTrackedRange('test', { start: 1, end: 3 }, TrackedRangeStickiness.GrowsOnlyWhenTypingAfter); @@ -343,14 +345,14 @@ suite('NotebookViewModel Decorations', () => { end: 3 }); - viewModel.insertCell(5, new TestCell(viewModel.viewType, 8, ['var d = 9;'], 'javascript', CellKind.Code, [], textModelService), true); + viewModel.insertCell(5, new TestCell(viewModel.viewType, 8, 'var d = 9;', 'javascript', CellKind.Code, [], textModelService), true); assert.deepEqual(viewModel.getTrackedRange(trackedId!), { start: 1, end: 3 }); - viewModel.insertCell(4, new TestCell(viewModel.viewType, 9, ['var d = 10;'], 'javascript', CellKind.Code, [], textModelService), true); + viewModel.insertCell(4, new TestCell(viewModel.viewType, 9, 'var d = 10;', 'javascript', CellKind.Code, [], textModelService), true); assert.deepEqual(viewModel.getTrackedRange(trackedId!), { start: 1, diff --git a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts index 30e08a8d8f..cc6a24c931 100644 --- a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts @@ -12,13 +12,13 @@ import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; import { Range } from 'vs/editor/common/core/range'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { EditorModel } from 'vs/workbench/common/editor'; -import { ICellRange, ICellViewModel, INotebookEditor, INotebookEditorContribution, INotebookEditorMouseEvent, NotebookLayoutInfo, NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { ICellViewModel, INotebookEditor, INotebookEditorContribution, INotebookEditorMouseEvent, NotebookLayoutInfo, INotebookDeltaDecoration, INotebookEditorCreationOptions, NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; import { NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; import { CellViewModel, IModelDecorationsChangeAccessor, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { CellKind, CellUri, INotebookEditorModel, IProcessedOutput, NotebookCellMetadata, INotebookKernelInfo, IInsetRenderOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, CellUri, INotebookEditorModel, IProcessedOutput, NotebookCellMetadata, IInsetRenderOutput, ICellRange, INotebookKernelInfo2 } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; import { ICompositeCodeEditor, IEditor } from 'vs/editor/common/editorCommon'; import { NotImplementedError } from 'vs/base/common/errors'; @@ -34,22 +34,25 @@ import { TestConfigurationService } from 'vs/platform/configuration/test/common/ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { ScrollEvent } from 'vs/base/common/scrollable'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { IFileStatWithMetadata } from 'vs/platform/files/common/files'; export class TestCell extends NotebookCellTextModel { constructor( public viewType: string, handle: number, - public source: string[], + public source: string, language: string, cellKind: CellKind, outputs: IProcessedOutput[], modelService: ITextModelService ) { - super(CellUri.generate(URI.parse('test:///fake/notebook'), handle), handle, source, language, cellKind, outputs, undefined, modelService); + super(CellUri.generate(URI.parse('test:///fake/notebook'), handle), handle, source, language, cellKind, outputs, undefined, { transientMetadata: {}, transientOutputs: false }, modelService); } } export class TestNotebookEditor implements INotebookEditor { + isEmbedded = false; private _isDisposed = false; get isDisposed() { @@ -59,10 +62,12 @@ export class TestNotebookEditor implements INotebookEditor { get viewModel() { return undefined; } + creationOptions: INotebookEditorCreationOptions = { isEmbedded: false }; constructor( ) { } + setOptions(options: NotebookEditorOptions | undefined): Promise { throw new Error('Method not implemented.'); } @@ -75,7 +80,9 @@ export class TestNotebookEditor implements INotebookEditor { onDidChangeAvailableKernels: Event = new Emitter().event; onDidChangeActiveCell: Event = new Emitter().event; onDidScroll = new Emitter().event; - + onWillDispose = new Emitter().event; + onDidChangeVisibleRanges: Event = new Emitter().event; + visibleRanges: ICellRange[] = []; uri?: URI | undefined; textModel?: NotebookTextModel | undefined; @@ -101,7 +108,7 @@ export class TestNotebookEditor implements INotebookEditor { } cursorNavigationMode = false; - activeKernel: INotebookKernelInfo | undefined; + activeKernel: INotebookKernelInfo2 | undefined; onDidChangeKernel: Event = new Emitter().event; onDidChangeActiveEditor: Event = new Emitter().event; activeCodeEditor: IEditor | undefined; @@ -281,6 +288,10 @@ export class TestNotebookEditor implements INotebookEditor { throw new Error('Method not implemented.'); } + deltaCellDecorations(oldDecorations: string[], newDecorations: INotebookDeltaDecoration[]): string[] { + throw new Error('Method not implemented.'); + } + deltaCellOutputContainerClassNames(cellId: string, added: string[], removed: string[]): void { throw new Error('Method not implemented.'); } @@ -330,6 +341,7 @@ export class NotebookEditorTestModel extends EditorModel implements INotebookEdi })); } } + lastResolvedFileStat: IFileStatWithMetadata | undefined; isDirty() { return this._dirty; @@ -375,15 +387,22 @@ export function setupInstantiationService() { return instantiationService; } -export function withTestNotebook(instantiationService: TestInstantiationService, blukEditService: IBulkEditService, undoRedoService: IUndoRedoService, cells: [string[], string, CellKind, IProcessedOutput[], NotebookCellMetadata][], callback: (editor: TestNotebookEditor, viewModel: NotebookViewModel, textModel: NotebookTextModel) => void) { +export function withTestNotebook(instantiationService: TestInstantiationService, blukEditService: IBulkEditService, undoRedoService: IUndoRedoService, cells: [string, string, CellKind, IProcessedOutput[], NotebookCellMetadata][], callback: (editor: TestNotebookEditor, viewModel: NotebookViewModel, textModel: NotebookTextModel) => void) { const textModelService = instantiationService.get(ITextModelService); + const modeService = instantiationService.get(IModeService); const viewType = 'notebook'; const editor = new TestNotebookEditor(); - const notebook = new NotebookTextModel(0, viewType, false, URI.parse('test'), undoRedoService, textModelService); - notebook.cells = cells.map((cell, index) => { - return new NotebookCellTextModel(notebook.uri, index, cell[0], cell[1], cell[2], cell[3], cell[4], textModelService); - }); + const notebook = new NotebookTextModel(0, viewType, false, URI.parse('test'), undoRedoService, textModelService, modeService); + notebook.initialize(cells.map(cell => { + return { + source: cell[0], + language: cell[1], + cellKind: cell[2], + outputs: cell[3], + metadata: cell[4] + }; + })); const model = new NotebookEditorTestModel(notebook); const eventDispatcher = new NotebookEventDispatcher(); const viewModel = new NotebookViewModel(viewType, model.notebook, eventDispatcher, null, instantiationService, blukEditService, undoRedoService); diff --git a/src/vs/workbench/contrib/output/browser/output.contribution.ts b/src/vs/workbench/contrib/output/browser/output.contribution.ts index da6b425a57..fcda053f4b 100644 --- a/src/vs/workbench/contrib/output/browser/output.contribution.ts +++ b/src/vs/workbench/contrib/output/browser/output.contribution.ts @@ -322,7 +322,7 @@ Registry.as(ConfigurationExtensions.Configuration).regis type: 'boolean', description: nls.localize('output.smartScroll.enabled', "Enable/disable the ability of smart scrolling in the output view. Smart scrolling allows you to lock scrolling automatically when you click in the output view and unlocks when you click in the last line."), default: true, - scope: ConfigurationScope.APPLICATION, + scope: ConfigurationScope.WINDOW, tags: ['output'] } } diff --git a/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index 1a43d790db..b56bcc94ce 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -13,7 +13,7 @@ import { ITextResourceConfigurationService } from 'vs/editor/common/services/tex import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { EditorInput, EditorOptions } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, IEditorOpenContext } 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, CONTEXT_OUTPUT_SCROLL_LOCK } from 'vs/workbench/contrib/output/common/output'; import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; @@ -146,7 +146,7 @@ export class OutputViewPane extends ViewPane { this.channelId = channel.id; const descriptor = this.outputService.getChannelDescriptor(channel.id); CONTEXT_ACTIVE_LOG_OUTPUT.bindTo(this.contextKeyService).set(!!descriptor?.file && descriptor?.log); - this.editorPromise = this.editor.setInput(this.createInput(channel), EditorOptions.create({ preserveFocus: true }), CancellationToken.None) + this.editorPromise = this.editor.setInput(this.createInput(channel), EditorOptions.create({ preserveFocus: true }), Object.create(null), CancellationToken.None) .then(() => this.editor); } @@ -228,7 +228,7 @@ export class OutputEditor extends AbstractTextResourceEditor { return channel ? nls.localize('outputViewWithInputAriaLabel', "{0}, Output panel", channel.label) : nls.localize('outputViewAriaLabel', "Output panel"); } - async setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { const focus = !(options && options.preserveFocus); if (input.matches(this.input)) { return; @@ -238,7 +238,7 @@ export class OutputEditor extends AbstractTextResourceEditor { // Dispose previous input (Output panel is not a workbench editor) this.input.dispose(); } - await super.setInput(input, options, token); + await super.setInput(input, options, context, token); if (focus) { this.focus(); } diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts index 1a9a2d4888..15bc97a97a 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts @@ -14,8 +14,8 @@ import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlighte import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; import { IAction, Action, Separator } from 'vs/base/common/actions'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { EditorOptions } from 'vs/workbench/common/editor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; +import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { KeybindingsEditorModel, IKeybindingItemEntry, IListEntry, KEYBINDING_ENTRY_TEMPLATE_ID } from 'vs/workbench/services/preferences/common/keybindingsEditorModel'; @@ -58,7 +58,7 @@ interface ColumnItem { const oddRowBackgroundColor = new Color(new RGBA(130, 130, 130, 0.04)); -export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditorPane { +export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorPane { static readonly ID: string = 'workbench.editor.keybindings'; @@ -138,9 +138,9 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditorP this.createBody(keybindingsEditorElement); } - setInput(input: KeybindingsEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + setInput(input: KeybindingsEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { this.keybindingsEditorContextKey.set(true); - return super.setInput(input, options, token) + return super.setInput(input, options, context, token) .then(() => this.render(!!(options && options.preserveFocus))); } diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css index d8f3c51fc7..0f68d20a2d 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css @@ -175,14 +175,14 @@ .settings-editor > .settings-body .settings-tree-container .setting-toolbar-container { position: absolute; - left: -22px; + left: -32px; top: 11px; bottom: 0px; width: 26px; } .settings-editor > .settings-body .settings-tree-container .monaco-list-row .mouseover .setting-toolbar-container > .monaco-toolbar .codicon, -.settings-editor > .settings-body .settings-tree-container .monaco-list-row.focused .setting-item-contents .setting-toolbar-container > .monaco-toolbar .codicon, +.settings-editor > .settings-body .settings-tree-container .monaco-list-row .setting-item-contents.focused .setting-toolbar-container > .monaco-toolbar .codicon, .settings-editor > .settings-body .settings-tree-container .monaco-list-row .setting-toolbar-container:hover > .monaco-toolbar .codicon, .settings-editor > .settings-body .settings-tree-container .monaco-list-row .setting-toolbar-container > .monaco-toolbar .active .codicon { opacity: 1; @@ -283,34 +283,15 @@ max-width: 1000px; margin: auto; box-sizing: border-box; - padding-left: 204px; - padding-right: 5px; + padding-left: 219px; + padding-right: 20px; overflow: visible; } -.settings-editor > .settings-body > .settings-tree-container .settings-group-title-label::before, -.settings-editor > .settings-body > .settings-tree-container .settings-group-title-label::after, -.settings-editor > .settings-body > .settings-tree-container .setting-item-contents::before, -.settings-editor > .settings-body > .settings-tree-container .setting-item-contents::after { - content: ' '; - position: absolute; - left: 0px; - right: 0px; -} - -.settings-editor > .settings-body > .settings-tree-container .settings-group-title-label::before, -.settings-editor > .settings-body > .settings-tree-container .setting-item-contents::before { - top: 0px; -} - -.settings-editor > .settings-body > .settings-tree-container .settings-group-title-label::after, -.settings-editor > .settings-body > .settings-tree-container .setting-item-contents::after { - bottom: 0px; -} - .settings-editor > .settings-body > .settings-tree-container .setting-item-contents { position: relative; - padding: 12px 15px 18px; + padding-top: 12px; + padding-bottom: 18px; white-space: normal; } @@ -318,9 +299,11 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - display: inline-block; /* size to contents for hover to show context button */ + display: inline-block; + /* size to contents for hover to show context button */ } + .settings-editor > .settings-body > .settings-tree-container .setting-item-contents .setting-item-modified-indicator { display: none; } @@ -332,7 +315,7 @@ width: 6px; border-left-width: 2px; border-left-style: solid; - left: 5px; + left: -9px; top: 15px; bottom: 16px; } @@ -545,18 +528,12 @@ } .settings-editor > .settings-body > .settings-tree-container .settings-group-title-label { - display: inline-block; margin: 0px; font-weight: 600; - height: 100%; - box-sizing: border-box; - padding: 10px; - padding-left: 15px; - width: 100%; - position: relative; } .settings-editor > .settings-body > .settings-tree-container .settings-group-level-1 { + padding-top: 23px; font-size: 24px; } @@ -565,6 +542,10 @@ font-size: 20px; } +.settings-editor > .settings-body > .settings-tree-container .settings-group-level-1.settings-group-first { + padding-top: 7px; +} + .settings-editor.search-mode > .settings-body .settings-toc-container .monaco-list-row .settings-toc-count { display: block; } diff --git a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index 89a582a360..5e766fa0e7 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts @@ -12,7 +12,7 @@ import * as nls from 'vs/nls'; import { Action2, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { InputFocusedContext, IsMacNativeContext } from 'vs/platform/contextkey/common/contextkeys'; +import { IsMacNativeContext } from 'vs/platform/contextkey/common/contextkeys'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -50,8 +50,6 @@ const SETTINGS_EDITOR_COMMAND_EDIT_FOCUSED_SETTING = 'settings.action.editFocuse const SETTINGS_EDITOR_COMMAND_FOCUS_SETTINGS_FROM_SEARCH = 'settings.action.focusSettingsFromSearch'; const SETTINGS_EDITOR_COMMAND_FOCUS_SETTINGS_LIST = 'settings.action.focusSettingsList'; const SETTINGS_EDITOR_COMMAND_FOCUS_TOC = 'settings.action.focusTOC'; -const SETTINGS_EDITOR_COMMAND_FOCUS_TOC2 = 'settings.action.focusTOC2'; -const SETTINGS_EDITOR_COMMAND_FOCUS_CONTROL = 'settings.action.focusSettingControl'; const SETTINGS_EDITOR_COMMAND_SWITCH_TO_JSON = 'settings.switchToJSON'; const SETTINGS_EDITOR_COMMAND_FILTER_MODIFIED = 'settings.filterByModified'; @@ -509,14 +507,6 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon } return null; } - - function settingsEditorFocusSearch(accessor: ServicesAccessor) { - const preferencesEditor = getPreferencesEditor(accessor); - if (preferencesEditor) { - preferencesEditor.focusSearch(); - } - } - registerAction2(class extends Action2 { constructor() { super({ @@ -531,24 +521,12 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon }); } - run(accessor: ServicesAccessor) { settingsEditorFocusSearch(accessor); } - }); - - registerAction2(class extends Action2 { - constructor() { - super({ - id: SETTINGS_EDITOR_COMMAND_SEARCH, - precondition: ContextKeyExpr.and(CONTEXT_SETTINGS_EDITOR, CONTEXT_TOC_ROW_FOCUS), - keybinding: { - primary: KeyCode.Escape, - weight: KeybindingWeight.WorkbenchContrib, - when: null - }, - title: nls.localize('settings.focusSearch', "Focus settings search") - }); + run(accessor: ServicesAccessor) { + const preferencesEditor = getPreferencesEditor(accessor); + if (preferencesEditor) { + preferencesEditor.focusSearch(); + } } - - run(accessor: ServicesAccessor) { settingsEditorFocusSearch(accessor); } }); registerAction2(class extends Action2 { @@ -713,79 +691,19 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon constructor() { super({ id: SETTINGS_EDITOR_COMMAND_FOCUS_TOC, - keybinding: [ - { - primary: KeyCode.Escape, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(CONTEXT_SETTINGS_EDITOR, CONTEXT_TOC_ROW_FOCUS.negate()), - }, - { - primary: KeyCode.LeftArrow, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(CONTEXT_SETTINGS_EDITOR, CONTEXT_TOC_ROW_FOCUS.negate(), InputFocusedContext.negate()) - }], + precondition: CONTEXT_SETTINGS_EDITOR, title: nls.localize('settings.focusSettingsTOC', "Focus settings TOC tree") }); } run(accessor: ServicesAccessor): void { const preferencesEditor = getPreferencesEditor(accessor); - if (!(preferencesEditor instanceof SettingsEditor2)) { - return; - } - - if (document.activeElement?.classList.contains('monaco-list')) { + if (preferencesEditor instanceof SettingsEditor2) { preferencesEditor.focusTOC(); - } else { - preferencesEditor.focusSettings(); } } }); - registerAction2(class extends Action2 { - constructor() { - super({ - id: SETTINGS_EDITOR_COMMAND_FOCUS_CONTROL, - precondition: ContextKeyExpr.and(CONTEXT_SETTINGS_EDITOR, CONTEXT_TOC_ROW_FOCUS.negate()), - keybinding: { - primary: KeyCode.Enter, - weight: KeybindingWeight.WorkbenchContrib, - }, - title: nls.localize('settings.focusSettingControl', "Focus setting control") - }); - } - - run(accessor: ServicesAccessor): void { - const preferencesEditor = getPreferencesEditor(accessor); - if (!(preferencesEditor instanceof SettingsEditor2)) { - return; - } - - if (document.activeElement?.classList.contains('monaco-list')) { - preferencesEditor.focusSettings(true); - } - } - }); - - registerAction2(class extends Action2 { - constructor() { - super({ - id: SETTINGS_EDITOR_COMMAND_FOCUS_TOC2, - - title: nls.localize('settings.focusSettingsTOC', "Focus settings TOC tree") - }); - } - - run(accessor: ServicesAccessor): void { - const preferencesEditor = getPreferencesEditor(accessor); - if (!(preferencesEditor instanceof SettingsEditor2)) { - return; - } - - preferencesEditor.focusTOC(); - } - }); - registerAction2(class extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts b/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts index b4ae66fdfe..8ef366f46d 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts @@ -40,9 +40,9 @@ import { attachStylerCallback } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; -import { EditorInput, EditorOptions, IEditorControl } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, IEditorControl, IEditorOpenContext } from 'vs/workbench/common/editor'; import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorModel'; import { DefaultSettingsRenderer, FolderSettingsRenderer, IPreferencesRenderer, UserSettingsRenderer, WorkspaceSettingsRenderer } from 'vs/workbench/contrib/preferences/browser/preferencesRenderers'; import { SearchWidget, SettingsTarget, SettingsTargetsWidget } from 'vs/workbench/contrib/preferences/browser/preferencesWidgets'; @@ -53,7 +53,7 @@ import { IFilterResult, IPreferencesService, ISetting, ISettingsEditorModel, ISe import { DefaultPreferencesEditorInput, PreferencesEditorInput } from 'vs/workbench/services/preferences/common/preferencesEditorInput'; import { DefaultSettingsEditorModel, SettingsEditorModel } from 'vs/workbench/services/preferences/common/preferencesModels'; -export class PreferencesEditor extends BaseEditor { +export class PreferencesEditor extends EditorPane { static readonly ID: string = 'workbench.editor.preferencesEditor'; @@ -75,7 +75,7 @@ export class PreferencesEditor extends BaseEditor { get minimumWidth(): number { return this.sideBySidePreferencesWidget ? this.sideBySidePreferencesWidget.minimumWidth : 0; } get maximumWidth(): number { return this.sideBySidePreferencesWidget ? this.sideBySidePreferencesWidget.maximumWidth : Number.POSITIVE_INFINITY; } - // these setters need to exist because this extends from BaseEditor + // these setters need to exist because this extends from EditorPane set minimumWidth(value: number) { /*noop*/ } set maximumWidth(value: number) { /*noop*/ } @@ -151,14 +151,14 @@ export class PreferencesEditor extends BaseEditor { this.preferencesRenderers.editFocusedPreference(); } - setInput(newInput: EditorInput, options: SettingsEditorOptions | undefined, token: CancellationToken): Promise { + setInput(newInput: EditorInput, options: SettingsEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { this.defaultSettingsEditorContextKey.set(true); this.defaultSettingsJSONEditorContextKey.set(true); if (options && options.query) { this.focusSearch(options.query); } - return super.setInput(newInput, options, token).then(() => this.updateInput(newInput as PreferencesEditorInput, options, token)); + return super.setInput(newInput, options, context, token).then(() => this.updateInput(newInput as PreferencesEditorInput, options, context, token)); } layout(dimension: DOM.Dimension): void { @@ -204,8 +204,8 @@ export class PreferencesEditor extends BaseEditor { super.setEditorVisible(visible, group); } - private updateInput(newInput: PreferencesEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { - return this.sideBySidePreferencesWidget.setInput(newInput.secondary, newInput.primary, options, token).then(({ defaultPreferencesRenderer, editablePreferencesRenderer }) => { + private updateInput(newInput: PreferencesEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + return this.sideBySidePreferencesWidget.setInput(newInput.secondary, newInput.primary, options, context, token).then(({ defaultPreferencesRenderer, editablePreferencesRenderer }) => { if (token.isCancellationRequested) { return; } @@ -762,7 +762,7 @@ class SideBySidePreferencesWidget extends Widget { private defaultPreferencesHeader: HTMLElement; private defaultPreferencesEditor: DefaultPreferencesEditor; - private editablePreferencesEditor: BaseEditor | null = null; + private editablePreferencesEditor: EditorPane | null = null; private defaultPreferencesEditorContainer: HTMLElement; private editablePreferencesEditorContainer: HTMLElement; @@ -837,12 +837,12 @@ class SideBySidePreferencesWidget extends Widget { this._register(focusTracker.onDidFocus(() => this._onFocus.fire())); } - setInput(defaultPreferencesEditorInput: DefaultPreferencesEditorInput, editablePreferencesEditorInput: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise<{ defaultPreferencesRenderer?: IPreferencesRenderer, editablePreferencesRenderer?: IPreferencesRenderer; }> { + setInput(defaultPreferencesEditorInput: DefaultPreferencesEditorInput, editablePreferencesEditorInput: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<{ defaultPreferencesRenderer?: IPreferencesRenderer, editablePreferencesRenderer?: IPreferencesRenderer; }> { this.getOrCreateEditablePreferencesEditor(editablePreferencesEditorInput); this.settingsTargetsWidget.settingsTarget = this.getSettingsTarget(editablePreferencesEditorInput.resource!); return Promise.all([ - this.updateInput(this.defaultPreferencesEditor, defaultPreferencesEditorInput, DefaultSettingsEditorContribution.ID, editablePreferencesEditorInput.resource!, options, token), - this.updateInput(this.editablePreferencesEditor!, editablePreferencesEditorInput, SettingsEditorContribution.ID, defaultPreferencesEditorInput.resource!, options, token) + this.updateInput(this.defaultPreferencesEditor, defaultPreferencesEditorInput, DefaultSettingsEditorContribution.ID, editablePreferencesEditorInput.resource!, options, context, token), + this.updateInput(this.editablePreferencesEditor!, editablePreferencesEditorInput, SettingsEditorContribution.ID, defaultPreferencesEditorInput.resource!, options, context, token) ]) .then(([defaultPreferencesRenderer, editablePreferencesRenderer]) => { if (token.isCancellationRequested) { @@ -906,7 +906,7 @@ class SideBySidePreferencesWidget extends Widget { } } - private getOrCreateEditablePreferencesEditor(editorInput: EditorInput): BaseEditor { + private getOrCreateEditablePreferencesEditor(editorInput: EditorInput): EditorPane { if (this.editablePreferencesEditor) { return this.editablePreferencesEditor; } @@ -920,8 +920,8 @@ class SideBySidePreferencesWidget extends Widget { return editor; } - private updateInput(editor: BaseEditor, input: EditorInput, editorContributionId: string, associatedPreferencesModelUri: URI, options: EditorOptions | undefined, token: CancellationToken): Promise | undefined> { - return editor.setInput(input, options, token) + private updateInput(editor: EditorPane, input: EditorInput, editorContributionId: string, associatedPreferencesModelUri: URI, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise | undefined> { + return editor.setInput(input, options, context, token) .then(() => { if (token.isCancellationRequested) { return undefined; @@ -1025,8 +1025,8 @@ export class DefaultPreferencesEditor extends BaseTextEditor { return options; } - setInput(input: DefaultPreferencesEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { - return super.setInput(input, options, token) + setInput(input: DefaultPreferencesEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + return super.setInput(input, options, context, token) .then(() => this.input!.resolve() .then(editorModel => { if (token.isCancellationRequested) { diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index e21ecb2d59..5902e0097d 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -39,12 +39,12 @@ import { attachButtonStyler, attachStylerCallback } from 'vs/platform/theme/comm import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { IUserDataAutoSyncService, IUserDataSyncService, SyncStatus } from 'vs/platform/userDataSync/common/userDataSync'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { IEditorMemento, IEditorPane } from 'vs/workbench/common/editor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; +import { IEditorMemento, IEditorOpenContext, IEditorPane } from 'vs/workbench/common/editor'; import { attachSuggestEnabledInputBoxStyler, SuggestEnabledInput } from 'vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput'; import { SettingsTarget, SettingsTargetsWidget } from 'vs/workbench/contrib/preferences/browser/preferencesWidgets'; import { commonlyUsedData } from 'vs/workbench/contrib/preferences/browser/settingsLayout'; -import { AbstractSettingRenderer, ISettingLinkClickEvent, ISettingOverrideClickEvent, resolveExtensionsSettings, resolveSettingsTree, SettingsTree, SettingTreeRenderers, updateSettingTreeTabOrder } from 'vs/workbench/contrib/preferences/browser/settingsTree'; +import { AbstractSettingRenderer, ISettingLinkClickEvent, ISettingOverrideClickEvent, resolveExtensionsSettings, resolveSettingsTree, SettingsTree, SettingTreeRenderers } from 'vs/workbench/contrib/preferences/browser/settingsTree'; import { ISettingsEditorViewState, parseQuery, SearchResultIdx, SearchResultModel, SettingsTreeElement, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeModel, SettingsTreeSettingElement } from 'vs/workbench/contrib/preferences/browser/settingsTreeModels'; import { settingsTextInputBorder } from 'vs/workbench/contrib/preferences/browser/settingsWidgets'; import { createTOCIterator, TOCTree, TOCTreeModel } from 'vs/workbench/contrib/preferences/browser/tocTree'; @@ -77,7 +77,7 @@ const searchBoxLabel = localize('SearchSettings.AriaLabel', "Search settings"); const SETTINGS_AUTOSAVE_NOTIFIED_KEY = 'hasNotifiedOfSettingsAutosave'; const SETTINGS_EDITOR_STATE_KEY = 'settingsEditorState'; -export class SettingsEditor2 extends BaseEditor { +export class SettingsEditor2 extends EditorPane { static readonly ID: string = 'workbench.editor.settings2'; private static NUM_INSTANCES: number = 0; @@ -151,7 +151,6 @@ export class SettingsEditor2 extends BaseEditor { private editorMemento: IEditorMemento; private tocFocusedElement: SettingsTreeGroupElement | null = null; - private treeFocusedElement: SettingsTreeElement | null = null; private settingsTreeScrollTop = 0; private dimension!: DOM.Dimension; @@ -203,7 +202,7 @@ export class SettingsEditor2 extends BaseEditor { get minimumWidth(): number { return 375; } get maximumWidth(): number { return Number.POSITIVE_INFINITY; } - // these setters need to exist because this extends from BaseEditor + // these setters need to exist because this extends from EditorPane set minimumWidth(value: number) { /*noop*/ } set maximumWidth(value: number) { /*noop*/ } @@ -236,9 +235,9 @@ export class SettingsEditor2 extends BaseEditor { this.updateStyles(); } - setInput(input: SettingsEditor2Input, options: SettingsEditorOptions | undefined, token: CancellationToken): Promise { + setInput(input: SettingsEditor2Input, options: SettingsEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { this.inSettingsEditorContextKey.set(true); - return super.setInput(input, options, token) + return super.setInput(input, options, context, token) .then(() => timeout(0)) // Force setInput to be async .then(() => { // Don't block setInput on render (which can trigger an async search) @@ -354,8 +353,7 @@ export class SettingsEditor2 extends BaseEditor { } } - focusSettings(focusSettingInput = false): void { - // TODO@roblourens is this in the right place? + focusSettings(): void { // Update ARIA global labels const labelElement = this.settingsAriaExtraLabelsContainer.querySelector('#settings_aria_more_actions_shortcut_label'); if (labelElement) { @@ -365,18 +363,9 @@ export class SettingsEditor2 extends BaseEditor { } } - const focused = this.settingsTree.getFocus(); - if (!focused.length) { - this.settingsTree.focusFirst(); - } - - this.settingsTree.domFocus(); - - if (focusSettingInput) { - const controlInFocusedRow = this.settingsTree.getHTMLElement().querySelector(`.focused ${AbstractSettingRenderer.CONTROL_SELECTOR}`); - if (controlInFocusedRow) { - (controlInFocusedRow).focus(); - } + const firstFocusable = this.settingsTree.getHTMLElement().querySelector(AbstractSettingRenderer.CONTROL_SELECTOR); + if (firstFocusable) { + (firstFocusable).focus(); } } @@ -526,11 +515,6 @@ export class SettingsEditor2 extends BaseEditor { this.settingsTree.reveal(elements[0], sourceTop); - // We need to shift focus from the setting that contains the link to the setting that's - // linked. Clicking on the link sets focus on the setting that contains the link, - // which is why we need the setTimeout - setTimeout(() => this.settingsTree.setFocus([elements[0]]), 50); - const domElements = this.settingRenderers.getDOMElementsForSettingKey(this.settingsTree.getHTMLElement(), evt.targetKey); if (domElements && domElements[0]) { const control = domElements[0].querySelector(AbstractSettingRenderer.CONTROL_SELECTOR); @@ -591,7 +575,48 @@ export class SettingsEditor2 extends BaseEditor { })); this.createTOC(bodyContainer); + + this.createFocusSink( + bodyContainer, + e => { + if (DOM.findParentWithClass(e.relatedTarget, 'settings-editor-tree')) { + if (this.settingsTree.scrollTop > 0) { + const firstElement = this.settingsTree.firstVisibleElement; + + if (typeof firstElement !== 'undefined') { + this.settingsTree.reveal(firstElement, 0.1); + } + + return true; + } + } else { + const firstControl = this.settingsTree.getHTMLElement().querySelector(AbstractSettingRenderer.CONTROL_SELECTOR); + if (firstControl) { + (firstControl).focus(); + } + } + + return false; + }, + 'settings list focus helper'); + this.createSettingsTree(bodyContainer); + + this.createFocusSink( + bodyContainer, + e => { + if (DOM.findParentWithClass(e.relatedTarget, 'settings-editor-tree')) { + if (this.settingsTree.scrollTop < this.settingsTree.scrollHeight) { + const lastElement = this.settingsTree.lastVisibleElement; + this.settingsTree.reveal(lastElement, 0.9); + return true; + } + } + + return false; + }, + 'settings list focus helper' + ); } private addCtrlAInterceptor(container: HTMLElement): void { @@ -609,6 +634,19 @@ export class SettingsEditor2 extends BaseEditor { })); } + private createFocusSink(container: HTMLElement, callback: (e: any) => boolean, label: string): HTMLElement { + const listFocusSink = DOM.append(container, $('.settings-tree-focus-sink')); + listFocusSink.setAttribute('aria-label', label); + listFocusSink.tabIndex = 0; + this._register(DOM.addDisposableListener(listFocusSink, 'focus', (e: any) => { + if (e.relatedTarget && callback(e)) { + e.relatedTarget.focus(); + } + })); + + return listFocusSink; + } + private createTOC(parent: HTMLElement): void { this.tocTreeModel = this.instantiationService.createInstance(TOCTreeModel, this.viewState); this.tocTreeContainer = DOM.append(parent, $('.settings-toc-container')); @@ -636,7 +674,6 @@ export class SettingsEditor2 extends BaseEditor { } } else if (element && (!e.browserEvent || !(e.browserEvent).fromScroll)) { this.settingsTree.reveal(element, 0); - this.settingsTree.setFocus([element]); } })); @@ -686,6 +723,7 @@ export class SettingsEditor2 extends BaseEditor { this.settingsTreeContainer, this.viewState, this.settingRenderers.allRenderers)); + this.settingsTree.getHTMLElement().attributes.removeNamedItem('tabindex'); this._register(this.settingsTree.onDidScroll(() => { if (this.settingsTree.scrollTop === this.settingsTreeScrollTop) { @@ -693,7 +731,6 @@ export class SettingsEditor2 extends BaseEditor { } this.settingsTreeScrollTop = this.settingsTree.scrollTop; - updateSettingTreeTabOrder(this.settingsTreeContainer); // setTimeout because calling setChildren on the settingsTree can trigger onDidScroll, so it fires when // setChildren has called on the settings tree but not the toc tree yet, so their rendered elements are out of sync @@ -701,20 +738,6 @@ export class SettingsEditor2 extends BaseEditor { this.updateTreeScrollSync(); }, 0); })); - - // There is no different select state in the settings tree - this._register(this.settingsTree.onDidChangeFocus(e => { - const element = e.elements[0]; - if (this.treeFocusedElement === element) { - return; - } - - this.treeFocusedElement = element; - this.settingsTree.setSelection(element ? [element] : []); - - // Wait for rendering to complete - setTimeout(() => updateSettingTreeTabOrder(this.settingsTreeContainer), 0); - })); } private notifyNoSaveNeeded() { diff --git a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts index 84b4000b8c..d77a71e8c0 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsLayout.ts @@ -219,7 +219,7 @@ export const tocData: ITOCEntry = { { id: 'application/settingsSync', label: localize('settingsSync', "Settings Sync"), - settings: ['settingsSync.*'] + settings: ['settingsSync.*', 'sync.*'] } ] } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index 923e3c385c..9e09d0da24 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -5,6 +5,7 @@ import { BrowserFeatures } from 'vs/base/browser/canIUse'; import * as DOM from 'vs/base/browser/dom'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { alert as ariaAlert } from 'vs/base/browser/ui/aria/aria'; @@ -15,7 +16,7 @@ import { CachedListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { DefaultStyleController } from 'vs/base/browser/ui/list/listWidget'; import { ISelectOptionItem, SelectBox } from 'vs/base/browser/ui/selectBox/selectBox'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; -import { IObjectTreeOptions } from 'vs/base/browser/ui/tree/objectTree'; +import { IObjectTreeOptions, ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; import { ObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel'; import { ITreeFilter, ITreeModel, ITreeNode, ITreeRenderer, TreeFilterResult, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; import { Action, IAction, Separator } from 'vs/base/common/actions'; @@ -42,7 +43,7 @@ import { ICssStyleCollector, IColorTheme, IThemeService, registerThemingParticip import { getIgnoredSettings } from 'vs/platform/userDataSync/common/settingsMerge'; import { ITOCEntry } from 'vs/workbench/contrib/preferences/browser/settingsLayout'; import { ISettingsEditorViewState, settingKeyToDisplayFormat, SettingsTreeElement, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeNewExtensionsElement, SettingsTreeSettingElement } from 'vs/workbench/contrib/preferences/browser/settingsTreeModels'; -import { ExcludeSettingWidget, ISettingListChangeEvent, IListDataItem, ListSettingWidget, settingsNumberInputBackground, settingsNumberInputBorder, settingsNumberInputForeground, settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground, ObjectSettingWidget, IObjectDataItem, IObjectEnumOption, ObjectValue, IObjectValueSuggester, IObjectKeySuggester, focusedRowBackground, focusedRowBorder, settingsHeaderForeground, rowHoverBackground } from 'vs/workbench/contrib/preferences/browser/settingsWidgets'; +import { ExcludeSettingWidget, ISettingListChangeEvent, IListDataItem, ListSettingWidget, settingsHeaderForeground, settingsNumberInputBackground, settingsNumberInputBorder, settingsNumberInputForeground, settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground, ObjectSettingWidget, IObjectDataItem, IObjectEnumOption, ObjectValue, IObjectValueSuggester, IObjectKeySuggester } from 'vs/workbench/contrib/preferences/browser/settingsWidgets'; import { SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU } from 'vs/workbench/contrib/preferences/common/preferences'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { ISetting, ISettingsGroup, SettingValueType } from 'vs/workbench/services/preferences/common/preferences'; @@ -52,9 +53,6 @@ import { Codicon } from 'vs/base/common/codicons'; import { CodiconLabel } from 'vs/base/browser/ui/codicons/codiconLabel'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { IList } from 'vs/base/browser/ui/tree/indexTreeModel'; -import { IListService, WorkbenchObjectTree } from 'vs/platform/list/browser/listService'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; const $ = DOM.$; @@ -453,45 +451,6 @@ export interface ISettingOverrideClickEvent { targetKey: string; } -function removeChildrenFromTabOrder(node: Element): void { - const focusableElements = node.querySelectorAll(` - [tabindex="0"], - input:not([tabindex="-1"]), - select:not([tabindex="-1"]), - textarea:not([tabindex="-1"]), - a:not([tabindex="-1"]), - button:not([tabindex="-1"]), - area:not([tabindex="-1"]) - `); - - focusableElements.forEach(element => { - element.setAttribute(AbstractSettingRenderer.ELEMENT_FOCUSABLE_ATTR, 'true'); - element.setAttribute('tabindex', '-1'); - }); -} - -function addChildrenToTabOrder(node: Element): void { - const focusableElements = node.querySelectorAll( - `[${AbstractSettingRenderer.ELEMENT_FOCUSABLE_ATTR}="true"]` - ); - - focusableElements.forEach(element => { - element.removeAttribute(AbstractSettingRenderer.ELEMENT_FOCUSABLE_ATTR); - element.setAttribute('tabindex', '0'); - }); -} - -export function updateSettingTreeTabOrder(container: Element): void { - const allRows = [...container.querySelectorAll(AbstractSettingRenderer.ALL_ROWS_SELECTOR)]; - const focusedRow = allRows.find(row => row.classList.contains('focused')); - - allRows.forEach(removeChildrenFromTabOrder); - - if (isDefined(focusedRow)) { - addChildrenToTabOrder(focusedRow); - } -} - export abstract class AbstractSettingRenderer extends Disposable implements ITreeRenderer { /** To override */ abstract get templateId(): string; @@ -500,11 +459,9 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre static readonly CONTROL_SELECTOR = '.' + AbstractSettingRenderer.CONTROL_CLASS; static readonly CONTENTS_CLASS = 'setting-item-contents'; static readonly CONTENTS_SELECTOR = '.' + AbstractSettingRenderer.CONTENTS_CLASS; - static readonly ALL_ROWS_SELECTOR = '.monaco-list-row'; static readonly SETTING_KEY_ATTR = 'data-key'; static readonly SETTING_ID_ATTR = 'data-id'; - static readonly ELEMENT_FOCUSABLE_ATTR = 'data-focusable'; private readonly _onDidClickOverrideElement = this._register(new Emitter()); readonly onDidClickOverrideElement: Event = this._onDidClickOverrideElement.event; @@ -650,7 +607,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre private fixToolbarIcon(toolbar: ToolBar): void { const button = toolbar.getElement().querySelector('.codicon-toolbar-more'); if (button) { - (button).tabIndex = 0; + (button).tabIndex = -1; // change icon from ellipsis to gear (button).classList.add('codicon-gear'); @@ -1291,15 +1248,6 @@ export class SettingTextRenderer extends AbstractSettingRenderer implements ITre })); common.toDispose.add(inputBox); inputBox.inputElement.classList.add(AbstractSettingRenderer.CONTROL_CLASS); - inputBox.inputElement.tabIndex = 0; - - // TODO@9at8: listWidget filters out all key events from input boxes, so we need to come up with a better way - // Disable ArrowUp and ArrowDown behaviour in favor of list navigation - common.toDispose.add(DOM.addStandardDisposableListener(inputBox.inputElement, DOM.EventType.KEY_DOWN, e => { - if (e.equals(KeyCode.UpArrow) || e.equals(KeyCode.DownArrow)) { - e.preventDefault(); - } - })); const template: ISettingTextItemTemplate = { ...common, @@ -1352,7 +1300,6 @@ export class SettingEnumRenderer extends AbstractSettingRenderer implements ITre const selectElement = common.controlElement.querySelector('select'); if (selectElement) { selectElement.classList.add(AbstractSettingRenderer.CONTROL_CLASS); - selectElement.tabIndex = 0; } common.toDispose.add( @@ -1445,7 +1392,6 @@ export class SettingNumberRenderer extends AbstractSettingRenderer implements IT })); common.toDispose.add(inputBox); inputBox.inputElement.classList.add(AbstractSettingRenderer.CONTROL_CLASS); - inputBox.inputElement.tabIndex = 0; const template: ISettingNumberItemTemplate = { ...common, @@ -1558,6 +1504,13 @@ export class SettingBoolRenderer extends AbstractSettingRenderer implements ITre // Prevent clicks from being handled by list toDispose.add(DOM.addDisposableListener(controlElement, 'mousedown', (e: IMouseEvent) => e.stopPropagation())); + + toDispose.add(DOM.addStandardDisposableListener(controlElement, 'keydown', (e: StandardKeyboardEvent) => { + if (e.keyCode === KeyCode.Escape) { + e.browserEvent.stopPropagation(); + } + })); + toDispose.add(DOM.addDisposableListener(titleElement, DOM.EventType.MOUSE_ENTER, e => container.classList.add('mouseover'))); toDispose.add(DOM.addDisposableListener(titleElement, DOM.EventType.MOUSE_LEAVE, e => container.classList.remove('mouseover'))); @@ -1881,7 +1834,11 @@ class SettingsTreeDelegate extends CachedListVirtualDelegate extends ObjectTreeModel { } } -export class SettingsTree extends WorkbenchObjectTree { +export class SettingsTree extends ObjectTree { constructor( container: HTMLElement, viewState: ISettingsEditorViewState, renderers: ITreeRenderer[], - @IContextKeyService contextKeyService: IContextKeyService, - @IListService listService: IListService, @IThemeService themeService: IThemeService, @IConfigurationService configurationService: IConfigurationService, - @IKeybindingService keybindingService: IKeybindingService, - @IAccessibilityService accessibilityService: IAccessibilityService, @IInstantiationService instantiationService: IInstantiationService, ) { super('SettingsTree', container, new SettingsTreeDelegate(), renderers, { - horizontalScrolling: false, supportDynamicHeights: true, identityProvider: { getId(e) { @@ -1923,6 +1875,9 @@ export class SettingsTree extends WorkbenchObjectTree { } }, accessibilityProvider: { + getWidgetRole() { + return 'form'; + }, getAriaLabel() { // TODO@roblourens https://github.com/microsoft/vscode/issues/95862 return ''; @@ -1934,16 +1889,9 @@ export class SettingsTree extends WorkbenchObjectTree { styleController: id => new DefaultStyleController(DOM.createStyleSheet(container), id), filter: instantiationService.createInstance(SettingsTreeFilter, viewState), smoothScrolling: configurationService.getValue('workbench.list.smoothScrolling'), - multipleSelectionSupport: false, - }, - contextKeyService, - listService, - themeService, - configurationService, - keybindingService, - accessibilityService, - ); + }); + this.disposables.clear(); this.disposables.add(registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { const activeBorderColor = theme.getColor(focusBorder); if (activeBorderColor) { @@ -1982,26 +1930,6 @@ export class SettingsTree extends WorkbenchObjectTree { collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item.invalid-input .setting-item-control .monaco-inputbox.idle { outline-width: 0; border-style:solid; border-width: 1px; border-color: ${invalidInputBorder}; }`); } - const focusedRowBackgroundColor = theme.getColor(focusedRowBackground); - if (focusedRowBackgroundColor) { - collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-list-row.focused .setting-item-contents, - .settings-editor > .settings-body > .settings-tree-container .monaco-list-row.focused .settings-group-title-label { background-color: ${focusedRowBackgroundColor}; }`); - } - - const rowHoverBackgroundColor = theme.getColor(rowHoverBackground); - if (rowHoverBackgroundColor) { - collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-list-row .setting-item-contents:hover, - .settings-editor > .settings-body > .settings-tree-container .monaco-list-row .settings-group-title-label:hover { background-color: ${rowHoverBackgroundColor}; }`); - } - - const focusedRowBorderColor = theme.getColor(focusedRowBorder); - if (focusedRowBorderColor) { - collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-list:focus-within .monaco-list-row.focused .setting-item-contents::before, - .settings-editor > .settings-body > .settings-tree-container .monaco-list:focus-within .monaco-list-row.focused .setting-item-contents::after { border-top: 1px solid ${focusedRowBorderColor} }`); - collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-list:focus-within .monaco-list-row.focused .settings-group-title-label::before, - .settings-editor > .settings-body > .settings-tree-container .monaco-list:focus-within .monaco-list-row.focused .settings-group-title-label::after { border-top: 1px solid ${focusedRowBorderColor} }`); - } - const headerForegroundColor = theme.getColor(settingsHeaderForeground); if (headerForegroundColor) { collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .settings-group-title-label { color: ${headerForegroundColor}; }`); @@ -2012,12 +1940,6 @@ export class SettingsTree extends WorkbenchObjectTree { if (focusBorderColor) { collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item-contents .setting-item-markdown a:focus { outline-color: ${focusBorderColor} }`); } - - // const listActiveSelectionBackgroundColor = theme.getColor(listActiveSelectionBackground); - // if (listActiveSelectionBackgroundColor) { - // collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-list-row.selected .setting-item-contents .setting-item-title { background-color: ${listActiveSelectionBackgroundColor}; }`); - // collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-list-row.selected .settings-group-title-label { background-color: ${listActiveSelectionBackgroundColor}; }`); - // } })); this.getHTMLElement().classList.add('settings-editor-tree'); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts index d85bf6abd0..1eca6daff0 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts @@ -16,7 +16,7 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import 'vs/css!./media/settingsWidgets'; import { localize } from 'vs/nls'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { foreground, inputBorder, inputForeground, listActiveSelectionBackground, listActiveSelectionForeground, listHoverBackground, listHoverForeground, listInactiveSelectionBackground, listInactiveSelectionForeground, registerColor, selectBackground, selectBorder, selectForeground, textLinkForeground, textPreformatForeground, editorWidgetBorder, textLinkActiveForeground, simpleCheckboxBackground, simpleCheckboxForeground, simpleCheckboxBorder, listFocusBackground, transparent, focusBorder } from 'vs/platform/theme/common/colorRegistry'; +import { foreground, inputBackground, inputBorder, inputForeground, listActiveSelectionBackground, listActiveSelectionForeground, listHoverBackground, listHoverForeground, listInactiveSelectionBackground, listInactiveSelectionForeground, registerColor, selectBackground, selectBorder, selectForeground, textLinkForeground, textPreformatForeground, editorWidgetBorder, textLinkActiveForeground, simpleCheckboxBackground, simpleCheckboxForeground, simpleCheckboxBorder } from 'vs/platform/theme/common/colorRegistry'; import { attachButtonStyler, attachInputBoxStyler, attachSelectBoxStyler } from 'vs/platform/theme/common/styler'; import { ICssStyleCollector, IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { disposableTimeout } from 'vs/base/common/async'; @@ -25,7 +25,6 @@ import { preferencesEditIcon } from 'vs/workbench/contrib/preferences/browser/pr import { SelectBox } from 'vs/base/browser/ui/selectBox/selectBox'; import { isIOS } from 'vs/base/common/platform'; import { BrowserFeatures } from 'vs/base/browser/canIUse'; -import { PANEL_BORDER } from 'vs/workbench/common/theme'; const $ = DOM.$; export const settingsHeaderForeground = registerColor('settings.headerForeground', { light: '#444444', dark: '#e7e7e7', hc: '#ffffff' }, localize('headerForeground', "The foreground color for a section header or active title.")); @@ -47,33 +46,15 @@ export const settingsCheckboxForeground = registerColor('settings.checkboxForegr export const settingsCheckboxBorder = registerColor('settings.checkboxBorder', { dark: simpleCheckboxBorder, light: simpleCheckboxBorder, hc: simpleCheckboxBorder }, localize('settingsCheckboxBorder', "Settings editor checkbox border.")); // Text control colors -export const settingsTextInputBackground = settingsSelectBackground; //registerColor('settings.textInputBackground', { dark: inputBackground, light: inputBackground, hc: inputBackground }, localize('textInputBoxBackground', "Settings editor text input box background.")); +export const settingsTextInputBackground = registerColor('settings.textInputBackground', { dark: inputBackground, light: inputBackground, hc: inputBackground }, localize('textInputBoxBackground', "Settings editor text input box background.")); export const settingsTextInputForeground = registerColor('settings.textInputForeground', { dark: inputForeground, light: inputForeground, hc: inputForeground }, localize('textInputBoxForeground', "Settings editor text input box foreground.")); export const settingsTextInputBorder = registerColor('settings.textInputBorder', { dark: inputBorder, light: inputBorder, hc: inputBorder }, localize('textInputBoxBorder', "Settings editor text input box border.")); // Number control colors -export const settingsNumberInputBackground = settingsSelectBackground; // registerColor('settings.numberInputBackground', { dark: inputBackground, light: inputBackground, hc: inputBackground }, localize('numberInputBoxBackground', "Settings editor number input box background.")); +export const settingsNumberInputBackground = registerColor('settings.numberInputBackground', { dark: inputBackground, light: inputBackground, hc: inputBackground }, localize('numberInputBoxBackground', "Settings editor number input box background.")); export const settingsNumberInputForeground = registerColor('settings.numberInputForeground', { dark: inputForeground, light: inputForeground, hc: inputForeground }, localize('numberInputBoxForeground', "Settings editor number input box foreground.")); export const settingsNumberInputBorder = registerColor('settings.numberInputBorder', { dark: inputBorder, light: inputBorder, hc: inputBorder }, localize('numberInputBoxBorder', "Settings editor number input box border.")); -export const focusedRowBackground = registerColor('settings.focusedRowBackground', { - dark: transparent(PANEL_BORDER, .4), - light: transparent(listFocusBackground, .4), - hc: null -}, localize('focusedRowBackground', "The background color of a cell when the row is focused.")); - -export const rowHoverBackground = registerColor('notebook.rowHoverBackground', { - dark: transparent(focusedRowBackground, .5), - light: transparent(focusedRowBackground, .7), - hc: null -}, localize('notebook.rowHoverBackground', "The background color of a row when the row is hovered.")); - -export const focusedRowBorder = registerColor('notebook.focusedRowBorder', { - dark: Color.white.transparent(0.12), - light: Color.black.transparent(0.12), - hc: focusBorder -}, localize('notebook.focusedRowBorder', "The color of the row's top and bottom border when the row is focused.")); - registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { const checkboxBackgroundColor = theme.getColor(settingsCheckboxBackground); if (checkboxBackgroundColor) { @@ -546,7 +527,7 @@ export class ListSettingWidget extends AbstractListSettingWidget valueInput.element.classList.add('setting-list-valueInput'); this.listDisposables.add(attachInputBoxStyler(valueInput, this.themeService, { - inputBackground: settingsSelectBackground, + inputBackground: settingsTextInputBackground, inputForeground: settingsTextInputForeground, inputBorder: settingsTextInputBorder })); @@ -565,7 +546,7 @@ export class ListSettingWidget extends AbstractListSettingWidget siblingInput.element.classList.add('setting-list-siblingInput'); this.listDisposables.add(siblingInput); this.listDisposables.add(attachInputBoxStyler(siblingInput, this.themeService, { - inputBackground: settingsSelectBackground, + inputBackground: settingsTextInputBackground, inputForeground: settingsTextInputForeground, inputBorder: settingsTextInputBorder })); @@ -927,7 +908,7 @@ export class ObjectSettingWidget extends AbstractListSettingWidget { +export class TOCTree extends ObjectTree { constructor( container: HTMLElement, viewState: ISettingsEditorViewState, - @IContextKeyService contextKeyService: IContextKeyService, - @IListService listService: IListService, @IThemeService themeService: IThemeService, - @IConfigurationService configurationService: IConfigurationService, - @IKeybindingService keybindingService: IKeybindingService, - @IAccessibilityService accessibilityService: IAccessibilityService, - @IInstantiationService instantiationService: IInstantiationService, + @IInstantiationService instantiationService: IInstantiationService ) { // test open mode const filter = instantiationService.createInstance(SettingsTreeFilter, viewState); - const options: IWorkbenchObjectTreeOptions = { + const options: IObjectTreeOptions = { filter, multipleSelectionSupport: false, identityProvider: { @@ -216,23 +207,13 @@ export class TOCTree extends WorkbenchObjectTree { }, styleController: id => new DefaultStyleController(DOM.createStyleSheet(container), id), accessibilityProvider: instantiationService.createInstance(SettingsAccessibilityProvider), - collapseByDefault: true, - horizontalScrolling: false + collapseByDefault: true }; - super( - 'SettingsTOC', - container, + super('SettingsTOC', container, new TOCTreeDelegate(), [new TOCRenderer()], - options, - contextKeyService, - listService, - themeService, - configurationService, - keybindingService, - accessibilityService, - ); + options); this.disposables.add(attachStyler(themeService, { listBackground: editorBackground, diff --git a/src/vs/workbench/contrib/remote/browser/remote.ts b/src/vs/workbench/contrib/remote/browser/remote.ts index b91c7a9db0..182e667082 100644 --- a/src/vs/workbench/contrib/remote/browser/remote.ts +++ b/src/vs/workbench/contrib/remote/browser/remote.ts @@ -281,7 +281,10 @@ class HelpItemValue { if (url.authority) { this._url = this.urlOrCommand; } else { - this._url = await this.commandService.executeCommand(this.urlOrCommand); + const urlCommand: Promise = this.commandService.executeCommand(this.urlOrCommand); + // We must be defensive. The command may never return, meaning that no help at all is ever shown! + const emptyString: Promise = new Promise(resolve => setTimeout(() => resolve(''), 500)); + this._url = await Promise.race([urlCommand, emptyString]); } } else { this._url = ''; @@ -326,13 +329,13 @@ abstract class HelpItemBase implements IHelpItem { } if (this.values.length > 1) { - let actions = await Promise.all(this.values.map(async (value) => { + let actions = (await Promise.all(this.values.map(async (value) => { return { label: value.extensionDescription.displayName || value.extensionDescription.identifier.value, description: await value.url, extensionDescription: value.extensionDescription }; - })); + }))).filter(item => item.description); const action = await this.quickInputService.pick(actions, { placeHolder: nls.localize('pickRemoteExtension', "Select url to open") }); diff --git a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts index a10ddd284f..2d6e8409dd 100644 --- a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts @@ -12,7 +12,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import * as ext from 'vs/workbench/common/contributions'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -46,7 +46,7 @@ import { IMarginData } from 'vs/editor/browser/controller/mouseTarget'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { ISplice } from 'vs/base/common/sequence'; import { createStyleSheet } from 'vs/base/browser/dom'; -import { ITextFileEditorModel, IResolvedTextFileEditorModel, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileEditorModel, IResolvedTextFileEditorModel, ITextFileService, isTextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; import { EncodingMode } from 'vs/workbench/common/editor'; class DiffActionRunner extends ActionRunner { @@ -982,7 +982,7 @@ function compareChanges(a: IChange, b: IChange): number { return a.originalEndLineNumber - b.originalEndLineNumber; } -function createProviderComparer(uri: URI): (a: ISCMProvider, b: ISCMProvider) => number { +export function createProviderComparer(uri: URI): (a: ISCMProvider, b: ISCMProvider) => number { return (a, b) => { const aIsParent = isEqualOrParent(uri, a.rootUri!); const bIsParent = isEqualOrParent(uri, b.rootUri!); @@ -1017,7 +1017,8 @@ export async function getOriginalResource(scmService: ISCMService, uri: URI): Pr export class DirtyDiffModel extends Disposable { - private _originalModel: IResolvedTextFileEditorModel | null = null; + private _originalResource: URI | null = null; + private _originalModel: IResolvedTextEditorModel | null = null; private _model: ITextFileEditorModel; get original(): ITextModel | null { return this._originalModel?.textEditorModel || null; } get modified(): ITextModel | null { return this._model.textEditorModel || null; } @@ -1049,6 +1050,7 @@ export class DirtyDiffModel extends Disposable { this._register(this._model.onDidChangeEncoding(() => { this.diffDelayer.cancel(); + this._originalResource = null; this._originalModel = null; this._originalURIPromise = undefined; this.setChanges([]); @@ -1128,11 +1130,12 @@ export class DirtyDiffModel extends Disposable { } if (!originalUri) { + this._originalResource = null; this._originalModel = null; return null; } - if (this._originalModel && this._originalModel.resource.toString() === originalUri.toString()) { + if (this._originalResource?.toString() === originalUri.toString()) { return originalUri; } @@ -1142,12 +1145,15 @@ export class DirtyDiffModel extends Disposable { return null; } - this._originalModel = ref.object as IResolvedTextFileEditorModel; + this._originalResource = originalUri; + this._originalModel = ref.object; - const encoding = this._model.getEncoding(); + if (isTextFileEditorModel(this._originalModel)) { + const encoding = this._model.getEncoding(); - if (encoding) { - this._originalModel.setEncoding(encoding, EncodingMode.Decode); + if (encoding) { + this._originalModel.setEncoding(encoding, EncodingMode.Decode); + } } this.originalModelDisposables.clear(); @@ -1214,6 +1220,7 @@ export class DirtyDiffModel extends Disposable { super.dispose(); this._disposed = true; + this._originalResource = null; this._originalModel = null; this.diffDelayer.cancel(); this.repositoryDisposables.forEach(d => dispose(d)); diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index 3fa53669d5..3303fa1b01 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -81,7 +81,7 @@ .scm-view .scm-provider > .actions > .monaco-toolbar > .monaco-action-bar > .actions-container > .action-item > .action-label > .codicon { font-size: 12px; - padding-right: 2px; + margin-right: 2px; } .scm-view .scm-provider > .actions > .monaco-toolbar > .monaco-action-bar > .actions-container > .action-item:last-of-type { diff --git a/src/vs/workbench/contrib/scm/browser/menus.ts b/src/vs/workbench/contrib/scm/browser/menus.ts index c7ed5c0ae7..4171b80a78 100644 --- a/src/vs/workbench/contrib/scm/browser/menus.ts +++ b/src/vs/workbench/contrib/scm/browser/menus.ts @@ -180,6 +180,7 @@ export class SCMRepositoryMenus implements ISCMRepositoryMenus, IDisposable { ) { this.contextKeyService = contextKeyService.createScoped(); this.contextKeyService.createKey('scmProvider', provider.contextValue); + this.contextKeyService.createKey('scmProviderRootUri', provider.rootUri?.toString()); this.contextKeyService.createKey('scmProviderHasRootUri', !!provider.rootUri); const serviceCollection = new ServiceCollection([IContextKeyService, this.contextKeyService]); diff --git a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts index f0b2534ba1..6caf9ac07a 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewPane.ts @@ -1434,7 +1434,7 @@ class SCMInputWidget extends Disposable { } hasFocus(): boolean { - return this.inputEditor.hasWidgetFocus(); + return this.inputEditor.hasTextFocus(); } private renderValidation(): void { @@ -1512,7 +1512,7 @@ class SCMCollapseAction extends Action { this.enabled = isAnyProviderCollapsible; this.allCollapsed = isAnyProviderCollapsible && this.viewModel.areAllProvidersCollapsed(); - this.label = this.allCollapsed ? localize('expand all', "Expand All Providers") : localize('collapse all', "Collapse All Providers"); + this.label = this.allCollapsed ? localize('expand all', "Expand All Repositories") : localize('collapse all', "Collapse All Repositories"); this.class = this.allCollapsed ? Codicon.expandAll.classNames : Codicon.collapseAll.classNames; } } diff --git a/src/vs/workbench/contrib/scm/browser/util.ts b/src/vs/workbench/contrib/scm/browser/util.ts index dc8cd3d956..11e2ef3999 100644 --- a/src/vs/workbench/contrib/scm/browser/util.ts +++ b/src/vs/workbench/contrib/scm/browser/util.ts @@ -12,12 +12,12 @@ import { createAndFillInActionBarActions, createAndFillInContextMenuActions } fr import { equals } from 'vs/base/common/arrays'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; -import { renderCodicons } from 'vs/base/common/codicons'; +import { renderCodiconsAsElement } from 'vs/base/browser/codicons'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { Command } from 'vs/editor/common/modes'; -import { escape } from 'vs/base/common/strings'; import { basename } from 'vs/base/common/resources'; import { Iterable } from 'vs/base/common/iterator'; +import { reset } from 'vs/base/browser/dom'; export function isSCMRepository(element: any): element is ISCMRepository { return !!(element as ISCMRepository).provider && typeof (element as ISCMRepository).setSelected === 'function'; @@ -105,7 +105,7 @@ export class StatusBarActionViewItem extends ActionViewItem { updateLabel(): void { if (this.options.label && this.label) { - this.label.innerHTML = renderCodicons(escape(this.getAction().label)); + reset(this.label, ...renderCodiconsAsElement(this.getAction().label)); } } } diff --git a/src/vs/workbench/contrib/search/browser/replaceService.ts b/src/vs/workbench/contrib/search/browser/replaceService.ts index 7c19d1bbe7..67adc4153e 100644 --- a/src/vs/workbench/contrib/search/browser/replaceService.ts +++ b/src/vs/workbench/contrib/search/browser/replaceService.ts @@ -18,10 +18,9 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { ScrollType } from 'vs/editor/common/editorCommon'; import { ITextModel, IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { WorkspaceTextEdit } from 'vs/editor/common/modes'; import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { IBulkEditService, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { Range } from 'vs/editor/common/core/range'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { mergeSort } from 'vs/base/common/arrays'; @@ -101,8 +100,8 @@ export class ReplaceService implements IReplaceService { replace(files: FileMatch[], progress?: IProgress): Promise; replace(match: FileMatchOrMatch, progress?: IProgress, resource?: URI): Promise; async replace(arg: any, progress: IProgress | undefined = undefined, resource: URI | null = null): Promise { - const edits: WorkspaceTextEdit[] = this.createEdits(arg, resource); - await this.bulkEditorService.apply({ edits }, { progress }); + const edits = this.createEdits(arg, resource); + await this.bulkEditorService.apply(edits, { progress }); return Promise.all(edits.map(e => this.textFileService.files.get(e.resource)?.save())); } @@ -162,15 +161,15 @@ export class ReplaceService implements IReplaceService { const modelEdits: IIdentifiedSingleEditOperation[] = []; for (const resourceEdit of resourceEdits) { modelEdits.push(EditOperation.replaceMove( - Range.lift(resourceEdit.edit.range), - resourceEdit.edit.text) + Range.lift(resourceEdit.textEdit.range), + resourceEdit.textEdit.text) ); } replaceModel.pushEditOperations([], mergeSort(modelEdits, (a, b) => Range.compareRangesUsingStarts(a.range, b.range)), () => []); } - private createEdits(arg: FileMatchOrMatch | FileMatch[], resource: URI | null = null): WorkspaceTextEdit[] { - const edits: WorkspaceTextEdit[] = []; + private createEdits(arg: FileMatchOrMatch | FileMatch[], resource: URI | null = null): ResourceTextEdit[] { + const edits: ResourceTextEdit[] = []; if (arg instanceof Match) { const match = arg; @@ -193,15 +192,11 @@ export class ReplaceService implements IReplaceService { return edits; } - private createEdit(match: Match, text: string, resource: URI | null = null): WorkspaceTextEdit { + private createEdit(match: Match, text: string, resource: URI | null = null): ResourceTextEdit { const fileMatch: FileMatch = match.parent(); - const resourceEdit: WorkspaceTextEdit = { - resource: resource !== null ? resource : fileMatch.resource, - edit: { - range: match.range(), - text: text - } - }; - return resourceEdit; + return new ResourceTextEdit( + resource ?? fileMatch.resource, + { range: match.range(), text }, undefined, undefined + ); } } diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts index 4802c1c771..3f8f8bddab 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts @@ -37,7 +37,7 @@ import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; -import { EditorOptions } from 'vs/workbench/common/editor'; +import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; import { ExcludePatternInputWidget, PatternInputWidget } from 'vs/workbench/contrib/search/browser/patternInputWidget'; import { SearchWidget } from 'vs/workbench/contrib/search/browser/searchWidget'; import { InputBoxFocusedKey } from 'vs/workbench/contrib/search/common/constants'; @@ -556,10 +556,10 @@ export class SearchEditor extends BaseTextEditor { return this._input as SearchEditorInput; } - async setInput(newInput: SearchEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + async setInput(newInput: SearchEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { this.saveViewState(); - await super.setInput(newInput, options, token); + await super.setInput(newInput, options, context, token); if (token.isCancellationRequested) { return; } const { body, config } = await newInput.getModels(); diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts index e589347879..759cbb011c 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts @@ -112,7 +112,7 @@ export class SearchEditorInput extends EditorInput { const workingCopyAdapter = new class implements IWorkingCopy { readonly resource = input.modelUri; get name() { return input.getName(); } - readonly capabilities = input.isUntitled() ? WorkingCopyCapabilities.Untitled : 0; + readonly capabilities = input.isUntitled() ? WorkingCopyCapabilities.Untitled : WorkingCopyCapabilities.None; readonly onDidChangeDirty = input.onDidChangeDirty; readonly onDidChangeContent = input.onDidChangeContent; isDirty(): boolean { return input.isDirty(); } diff --git a/src/vs/workbench/contrib/tags/electron-browser/workspaceTagsService.ts b/src/vs/workbench/contrib/tags/electron-browser/workspaceTagsService.ts index 07845fb5f0..5705a680bd 100644 --- a/src/vs/workbench/contrib/tags/electron-browser/workspaceTagsService.ts +++ b/src/vs/workbench/contrib/tags/electron-browser/workspaceTagsService.ts @@ -62,7 +62,7 @@ const ModulesToLookFor = [ 'firebase', '@google-cloud/common', 'heroku-cli', - //Office and Sharepoint packages + // Office and Sharepoint packages '@microsoft/teams-js', '@microsoft/office-js', '@microsoft/office-js-helpers', @@ -118,7 +118,8 @@ const PyModulesToLookFor = [ 'pydocumentdb', 'botbuilder-core', 'botbuilder-schema', - 'botframework-connector' + 'botframework-connector', + 'playwright' ]; export class WorkspaceTagsService implements IWorkspaceTagsService { @@ -292,7 +293,8 @@ export class WorkspaceTagsService implements IWorkspaceTagsService { "workspace.py.pydocumentdb" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.botbuilder-core" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspace.py.botbuilder-schema" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "workspace.py.botframework-connector" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } + "workspace.py.botframework-connector" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "workspace.py.playwright" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } } */ private resolveWorkspaceTags(configuration: IEnvironmentConfiguration, participant?: (rootFiles: string[]) => void): Promise { diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 553c5b63e0..e417c993ce 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -3059,7 +3059,8 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (selectedTask) { selectedEntry = { label: nls.localize('TaskService.defaultBuildTaskExists', '{0} is already marked as the default build task', selectedTask.getQualifiedLabel()), - task: selectedTask + task: selectedTask, + detail: this.showDetail() ? selectedTask.configurationProperties.detail : undefined }; } this.showIgnoredFoldersMessage().then(() => { @@ -3110,7 +3111,8 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (selectedTask) { selectedEntry = { label: nls.localize('TaskService.defaultTestTaskExists', '{0} is already marked as the default test task.', selectedTask.getQualifiedLabel()), - task: selectedTask + task: selectedTask, + detail: this.showDetail() ? selectedTask.configurationProperties.detail : undefined }; } diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index c4a5e47480..64faf4480e 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -1338,12 +1338,12 @@ export class TerminalTaskSystem implements ITaskSystem { } private collectDefinitionVariables(variables: Set, definition: any): void { - for (const key in definition) { - if (Types.isString(definition[key])) { - this.collectVariables(variables, definition[key]); - } else if (Types.isArray(definition[key])) { - definition[key].forEach((element: any) => this.collectDefinitionVariables(variables, element)); - } else if (Types.isObject(definition[key])) { + if (Types.isString(definition)) { + this.collectVariables(variables, definition); + } else if (Types.isArray(definition)) { + definition.forEach((element: any) => this.collectDefinitionVariables(variables, element)); + } else if (Types.isObject(definition)) { + for (const key in definition) { this.collectDefinitionVariables(variables, definition[key]); } } diff --git a/src/vs/workbench/contrib/terminal/browser/addons/commandTrackerAddon.ts b/src/vs/workbench/contrib/terminal/browser/addons/commandTrackerAddon.ts index 1fcc3e8481..0eebde5fd5 100644 --- a/src/vs/workbench/contrib/terminal/browser/addons/commandTrackerAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/addons/commandTrackerAddon.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Terminal, IMarker, ITerminalAddon } from 'xterm'; +import type { Terminal, IMarker, ITerminalAddon } from 'xterm'; import { ICommandTracker } from 'vs/workbench/contrib/terminal/common/terminal'; /** diff --git a/src/vs/workbench/contrib/terminal/browser/addons/navigationModeAddon.ts b/src/vs/workbench/contrib/terminal/browser/addons/navigationModeAddon.ts index 0d6262b6ba..f4f68e3daf 100644 --- a/src/vs/workbench/contrib/terminal/browser/addons/navigationModeAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/addons/navigationModeAddon.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { Terminal, ITerminalAddon } from 'xterm'; +import type { Terminal, ITerminalAddon } from 'xterm'; import { addDisposableListener } from 'vs/base/browser/dom'; import { INavigationMode } from 'vs/workbench/contrib/terminal/common/terminal'; diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalBaseLinkProvider.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalBaseLinkProvider.ts index d27e704572..93f3def7bc 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalBaseLinkProvider.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalBaseLinkProvider.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ILinkProvider, ILink } from 'xterm'; +import type { ILinkProvider, ILink } from 'xterm'; import { TerminalLink } from 'vs/workbench/contrib/terminal/browser/links/terminalLink'; export abstract class TerminalBaseLinkProvider implements ILinkProvider { diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalExternalLinkProviderAdapter.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalExternalLinkProviderAdapter.ts index 64f50f390f..40157f2e08 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalExternalLinkProviderAdapter.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalExternalLinkProviderAdapter.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Terminal, IViewportRange, IBufferLine } from 'xterm'; +import type { Terminal, IViewportRange, IBufferLine } from 'xterm'; import { getXtermLineContent, convertLinkRangeToBuffer } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers'; import { TerminalLink } from 'vs/workbench/contrib/terminal/browser/links/terminalLink'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLink.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLink.ts index 898366ad19..968041e3b8 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalLink.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLink.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IViewportRange, IBufferRange, ILink, ILinkDecorations } from 'xterm'; +import type { IViewportRange, IBufferRange, ILink, ILinkDecorations } from 'xterm'; import { DisposableStore } from 'vs/base/common/lifecycle'; import * as dom from 'vs/base/browser/dom'; import { RunOnceScheduler } from 'vs/base/common/async'; diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts index 4d092cd325..b8b98ca45e 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IViewportRange, IBufferRange, IBufferLine, IBuffer, IBufferCellPosition } from 'xterm'; +import type { IViewportRange, IBufferRange, IBufferLine, IBuffer, IBufferCellPosition } from 'xterm'; import { IRange } from 'vs/editor/common/core/range'; export function convertLinkRangeToBuffer(lines: IBufferLine[], bufferWidth: number, range: IRange, startLine: number) { @@ -54,6 +54,12 @@ export function convertLinkRangeToBuffer(lines: IBufferLine[], bufferWidth: numb const startLineOffset = (y === startWrappedLineCount - 1 ? startOffset : 0); let lineOffset = 0; const line = lines[y]; + // Sanity check for line, apparently this can happen but it's not clear under what + // circumstances this happens. Continue on, skipping the remainder of start offset if this + // happens to minimize impact. + if (!line) { + break; + } for (let x = start; x < Math.min(bufferWidth, lineLength + lineOffset + startLineOffset); x++) { const cell = line.getCell(x)!; const width = cell.getWidth(); diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts index 481dcc55d3..1387da2b17 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkManager.ts @@ -13,7 +13,7 @@ import { ITerminalProcessManager, ITerminalConfiguration, TERMINAL_CONFIG_SECTIO import { ITextEditorSelection } from 'vs/platform/editor/common/editor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IFileService } from 'vs/platform/files/common/files'; -import { Terminal, IViewportRange, ILinkProvider } from 'xterm'; +import type { Terminal, IViewportRange, ILinkProvider } from 'xterm'; import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; import { posix, win32 } from 'vs/base/common/path'; import { ITerminalExternalLinkProvider, ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalProtocolLinkProvider.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalProtocolLinkProvider.ts index ed2c7de3f1..ad94205e1e 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalProtocolLinkProvider.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalProtocolLinkProvider.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Terminal, IViewportRange, IBufferLine } from 'xterm'; +import type { Terminal, IViewportRange, IBufferLine } from 'xterm'; import { ILinkComputerTarget, LinkComputer } from 'vs/editor/common/modes/linkComputer'; import { getXtermLineContent, convertLinkRangeToBuffer } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers'; import { TerminalLink, OPEN_FILE_LABEL } from 'vs/workbench/contrib/terminal/browser/links/terminalLink'; diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider.ts index 4093dc34f8..97e6c6bd93 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalValidatedLocalLinkProvider.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Terminal, IViewportRange, IBufferLine } from 'xterm'; +import type { Terminal, IViewportRange, IBufferLine } from 'xterm'; import { getXtermLineContent, convertLinkRangeToBuffer } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers'; import { OperatingSystem } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkProvider.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkProvider.ts index 5af514b181..08843bded1 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkProvider.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalWordLinkProvider.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Terminal, IViewportRange } from 'xterm'; +import type { Terminal, IViewportRange } from 'xterm'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITerminalConfiguration, TERMINAL_CONFIG_SECTION } from 'vs/workbench/contrib/terminal/common/terminal'; import { TerminalLink } from 'vs/workbench/contrib/terminal/browser/links/terminalLink'; diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index d357c40237..57942a9c8c 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -196,3 +196,7 @@ padding: 0 22px 0 6px; } +/* HACK: Can remove when fixed upstream https://github.com/xtermjs/xterm.js/issues/3058 */ +.xterm-helper-textarea { + border: 0px; +} diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index 69bd29b81a..f906636bca 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -21,7 +21,7 @@ import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/wor import { Extensions as ViewContainerExtensions, IViewContainersRegistry, ViewContainerLocation, IViewsRegistry } from 'vs/workbench/common/views'; import { registerTerminalActions, ClearTerminalAction, CopyTerminalSelectionAction, CreateNewTerminalAction, KillTerminalAction, SelectAllTerminalAction, SelectDefaultShellWindowsTerminalAction, SplitInActiveWorkspaceTerminalAction, SplitTerminalAction, TerminalPasteAction, ToggleTerminalAction, terminalSendSequenceCommand } from 'vs/workbench/contrib/terminal/browser/terminalActions'; import { TerminalViewPane } from 'vs/workbench/contrib/terminal/browser/terminalView'; -import { KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE_KEY, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, TERMINAL_VIEW_ID, TERMINAL_ACTION_CATEGORY, TERMINAL_COMMAND_ID } from 'vs/workbench/contrib/terminal/common/terminal'; +import { KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE_KEY, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, TERMINAL_VIEW_ID, TERMINAL_ACTION_CATEGORY, TERMINAL_COMMAND_ID, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED } from 'vs/workbench/contrib/terminal/common/terminal'; import { registerColors } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; import { setupTerminalCommands } from 'vs/workbench/contrib/terminal/browser/terminalCommands'; import { setupTerminalMenu } from 'vs/workbench/contrib/terminal/common/terminalMenu'; @@ -84,7 +84,7 @@ Registry.as(ViewContainerExtensions.ViewsRegistry).registerViews const actionRegistry = Registry.as(ActionExtensions.WorkbenchActions); registerTerminalActions(); const category = TERMINAL_ACTION_CATEGORY; -actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(KillTerminalAction), 'Terminal: Kill the Active Terminal Instance', category); +actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(KillTerminalAction), 'Terminal: Kill the Active Terminal Instance', category, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED); actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(CreateNewTerminalAction, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_BACKTICK, mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.US_BACKTICK } @@ -97,7 +97,7 @@ actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(SelectAllTermin // behavior anyway when handed to xterm.js, having this handled by VS Code // makes it easier for users to see how it works though. mac: { primary: KeyMod.CtrlCmd | KeyCode.KEY_A } -}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Select All', category); +}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Select All', category, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED); actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleTerminalAction, { primary: KeyMod.CtrlCmd | KeyCode.US_BACKTICK, mac: { primary: KeyMod.WinCtrl | KeyCode.US_BACKTICK } @@ -107,16 +107,16 @@ actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleTerminalA actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(ClearTerminalAction, { primary: 0, mac: { primary: KeyMod.CtrlCmd | KeyCode.KEY_K } -}, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KeybindingWeight.WorkbenchContrib + 1), 'Terminal: Clear', category); -actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(SelectDefaultShellWindowsTerminalAction), 'Terminal: Select Default Shell', category); +}, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KeybindingWeight.WorkbenchContrib + 1), 'Terminal: Clear', category, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED); +actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(SelectDefaultShellWindowsTerminalAction), 'Terminal: Select Default Shell', category, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED); actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(SplitTerminalAction, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_5, mac: { primary: KeyMod.CtrlCmd | KeyCode.US_BACKSLASH, secondary: [KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KEY_5] } -}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Split Terminal', category); -actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(SplitInActiveWorkspaceTerminalAction), 'Terminal: Split Terminal (In Active Workspace)', category); +}, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Split Terminal', category, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED); +actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(SplitInActiveWorkspaceTerminalAction), 'Terminal: Split Terminal (In Active Workspace)', category, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED); // Commands might be affected by Web restrictons if (BrowserFeatures.clipboard.writeText) { @@ -124,7 +124,7 @@ if (BrowserFeatures.clipboard.writeText) { primary: KeyMod.CtrlCmd | KeyCode.KEY_C, win: { primary: KeyMod.CtrlCmd | KeyCode.KEY_C, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_C] }, linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_C } - }, ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, KEYBINDING_CONTEXT_TERMINAL_FOCUS)), 'Terminal: Copy Selection', category); + }, ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, KEYBINDING_CONTEXT_TERMINAL_FOCUS)), 'Terminal: Copy Selection', category, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED); } function registerSendSequenceKeybinding(text: string, rule: { when?: ContextKeyExpression } & IKeybindings): void { @@ -149,7 +149,7 @@ if (BrowserFeatures.clipboard.readText) { primary: KeyMod.CtrlCmd | KeyCode.KEY_V, win: { primary: KeyMod.CtrlCmd | KeyCode.KEY_V, secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_V] }, linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_V } - }, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Paste into Active Terminal', category); + }, KEYBINDING_CONTEXT_TERMINAL_FOCUS), 'Terminal: Paste into Active Terminal', category, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED); // An extra Windows-only ctrl+v keybinding is used for pwsh that sends ctrl+v directly to the // shell, this gets handled by PSReadLine which properly handles multi-line pastes. This is // disabled in accessibility mode as PowerShell does not run PSReadLine when it detects a screen diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 5247810ab8..1e38ec1d23 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -3,10 +3,10 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Terminal as XTermTerminal } from 'xterm'; -import { SearchAddon as XTermSearchAddon } from 'xterm-addon-search'; -import { Unicode11Addon as XTermUnicode11Addon } from 'xterm-addon-unicode11'; -import { WebglAddon as XTermWebglAddon } from 'xterm-addon-webgl'; +import type { Terminal as XTermTerminal } from 'xterm'; +import type { SearchAddon as XTermSearchAddon } from 'xterm-addon-search'; +import type { Unicode11Addon as XTermUnicode11Addon } from 'xterm-addon-unicode11'; +import type { WebglAddon as XTermWebglAddon } from 'xterm-addon-webgl'; import { IWindowsShellHelper, ITerminalConfigHelper, ITerminalChildProcess, IShellLaunchConfig, IDefaultShellAndArgsRequest, ISpawnExtHostProcessRequest, IStartExtensionTerminalRequest, IAvailableShellsRequest, ITerminalProcessExtHostProxy, ICommandTracker, INavigationMode, TitleEventSource, ITerminalDimensions, ITerminalLaunchError, ITerminalNativeWindowsDelegate, LinuxDistro } from 'vs/workbench/contrib/terminal/common/terminal'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IProcessEnvironment, Platform } from 'vs/base/common/platform'; @@ -76,6 +76,7 @@ export interface ITerminalService { configHelper: ITerminalConfigHelper; terminalInstances: ITerminalInstance[]; terminalTabs: ITerminalTab[]; + isProcessSupportRegistered: boolean; onActiveTabChanged: Event; onTabDisposed: Event; @@ -90,6 +91,7 @@ export interface ITerminalService { onInstanceTitleChanged: Event; onActiveInstanceChanged: Event; onRequestAvailableShells: Event; + onDidRegisterProcessSupport: Event; /** * Creates a terminal. @@ -136,6 +138,7 @@ export interface ITerminalService { findNext(): void; findPrevious(): void; + registerProcessSupport(isSupported: boolean): void; /** * Registers a link provider that enables integrators to add links to the terminal. * @param linkProvider When registered, the link provider is asked whenever a cell is hovered diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index 83941e5eec..554752b802 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -6,7 +6,7 @@ import { Action, IAction } from 'vs/base/common/actions'; import { EndOfLinePreference } from 'vs/editor/common/model'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { TERMINAL_VIEW_ID, ITerminalConfigHelper, TitleEventSource, TERMINAL_COMMAND_ID, KEYBINDING_CONTEXT_TERMINAL_FIND_FOCUSED, TERMINAL_ACTION_CATEGORY, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, KEYBINDING_CONTEXT_TERMINAL_FIND_NOT_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_A11Y_TREE_FOCUS } from 'vs/workbench/contrib/terminal/common/terminal'; +import { TERMINAL_VIEW_ID, ITerminalConfigHelper, TitleEventSource, TERMINAL_COMMAND_ID, KEYBINDING_CONTEXT_TERMINAL_FIND_FOCUSED, TERMINAL_ACTION_CATEGORY, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, KEYBINDING_CONTEXT_TERMINAL_FIND_NOT_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_A11Y_TREE_FOCUS, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED } from 'vs/workbench/contrib/terminal/common/terminal'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { attachSelectBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -84,7 +84,7 @@ export class ToggleTerminalAction extends ToggleViewAction { } async run() { - if (this.terminalService.terminalInstances.length === 0) { + if (this.terminalService.isProcessSupportRegistered && this.terminalService.terminalInstances.length === 0) { // If there is not yet an instance attempt to create it here so that we can suggest a // new shell on Windows (and not do so when the panel is restored on reload). const newTerminalInstance = this.terminalService.createTerminal(undefined); @@ -201,23 +201,25 @@ export class CreateNewTerminalAction extends Action { } } - let instance: ITerminalInstance | undefined; - if (folders.length <= 1) { - // Allow terminal service to handle the path when there is only a - // single root - instance = this._terminalService.createTerminal(undefined); - } else { - const options: IPickOptions = { - placeHolder: localize('workbench.action.terminal.newWorkspacePlaceholder', "Select current working directory for new terminal") - }; - const workspace = await this._commandService.executeCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID, [options]); - if (!workspace) { - // Don't create the instance if the workspace picker was canceled - return; + if (this._terminalService.isProcessSupportRegistered) { + let instance: ITerminalInstance | undefined; + if (folders.length <= 1) { + // Allow terminal service to handle the path when there is only a + // single root + instance = this._terminalService.createTerminal(undefined); + } else { + const options: IPickOptions = { + placeHolder: localize('workbench.action.terminal.newWorkspacePlaceholder', "Select current working directory for new terminal") + }; + const workspace = await this._commandService.executeCommand(PICK_WORKSPACE_FOLDER_COMMAND_ID, [options]); + if (!workspace) { + // Don't create the instance if the workspace picker was canceled + return; + } + instance = this._terminalService.createTerminal({ cwd: workspace.uri }); } - instance = this._terminalService.createTerminal({ cwd: workspace.uri }); + this._terminalService.setActiveInstance(instance); } - this._terminalService.setActiveInstance(instance); await this._terminalService.showPanel(true); } } @@ -367,7 +369,7 @@ export class SwitchTerminalActionViewItem extends SelectActionViewItem { this._register(_terminalService.onActiveTabChanged(this._updateItems, this)); this._register(_terminalService.onInstanceTitleChanged(this._updateItems, this)); this._register(_terminalService.onTabDisposed(this._updateItems, this)); - this._register(attachSelectBoxStyler(this.selectBox, _themeService)); + this._register(attachSelectBoxStyler(this.selectBox, this._themeService)); } render(container: HTMLElement): void { @@ -442,11 +444,13 @@ export function registerTerminalActions() { } async run(accessor: ServicesAccessor) { const terminalService = accessor.get(ITerminalService); - const instance = terminalService.createTerminal(undefined); - if (!instance) { - return; + if (terminalService.isProcessSupportRegistered) { + const instance = terminalService.createTerminal(undefined); + if (!instance) { + return; + } + terminalService.setActiveInstance(instance); } - terminalService.setActiveInstance(instance); await terminalService.showPanel(true); } }); @@ -466,7 +470,8 @@ export function registerTerminalActions() { }, when: KEYBINDING_CONTEXT_TERMINAL_FOCUS, weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } async run(accessor: ServicesAccessor) { @@ -491,7 +496,8 @@ export function registerTerminalActions() { }, when: KEYBINDING_CONTEXT_TERMINAL_FOCUS, weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } async run(accessor: ServicesAccessor) { @@ -512,7 +518,8 @@ export function registerTerminalActions() { mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.LeftArrow }, when: KEYBINDING_CONTEXT_TERMINAL_FOCUS, weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } async run(accessor: ServicesAccessor) { @@ -531,7 +538,8 @@ export function registerTerminalActions() { mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.RightArrow }, when: KEYBINDING_CONTEXT_TERMINAL_FOCUS, weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } async run(accessor: ServicesAccessor) { @@ -549,7 +557,8 @@ export function registerTerminalActions() { mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.UpArrow }, when: KEYBINDING_CONTEXT_TERMINAL_FOCUS, weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } async run(accessor: ServicesAccessor) { @@ -567,7 +576,8 @@ export function registerTerminalActions() { mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.DownArrow }, when: KEYBINDING_CONTEXT_TERMINAL_FOCUS, weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } async run(accessor: ServicesAccessor) { @@ -580,7 +590,8 @@ export function registerTerminalActions() { id: TERMINAL_COMMAND_ID.FOCUS, title: { value: localize('workbench.action.terminal.focus', "Focus Terminal"), original: 'Focus Terminal' }, f1: true, - category + category, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } async run(accessor: ServicesAccessor) { @@ -599,7 +610,8 @@ export function registerTerminalActions() { id: TERMINAL_COMMAND_ID.FOCUS_NEXT, title: { value: localize('workbench.action.terminal.focusNext', "Focus Next Terminal"), original: 'Focus Next Terminal' }, f1: true, - category + category, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } async run(accessor: ServicesAccessor) { @@ -614,7 +626,8 @@ export function registerTerminalActions() { id: TERMINAL_COMMAND_ID.FOCUS_PREVIOUS, title: { value: localize('workbench.action.terminal.focusPrevious', "Focus Previous Terminal"), original: 'Focus Previous Terminal' }, f1: true, - category + category, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } async run(accessor: ServicesAccessor) { @@ -629,7 +642,8 @@ export function registerTerminalActions() { id: TERMINAL_COMMAND_ID.RUN_SELECTED_TEXT, title: { value: localize('workbench.action.terminal.runSelectedText', "Run Selected Text In Active Terminal"), original: 'Run Selected Text In Active Terminal' }, f1: true, - category + category, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } async run(accessor: ServicesAccessor) { @@ -659,7 +673,8 @@ export function registerTerminalActions() { id: TERMINAL_COMMAND_ID.RUN_ACTIVE_FILE, title: { value: localize('workbench.action.terminal.runActiveFile', "Run Active File In Active Terminal"), original: 'Run Active File In Active Terminal' }, f1: true, - category + category, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } async run(accessor: ServicesAccessor) { @@ -699,7 +714,8 @@ export function registerTerminalActions() { linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.DownArrow }, when: KEYBINDING_CONTEXT_TERMINAL_FOCUS, weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -718,7 +734,8 @@ export function registerTerminalActions() { mac: { primary: KeyCode.PageDown }, when: KEYBINDING_CONTEXT_TERMINAL_FOCUS, weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -737,7 +754,8 @@ export function registerTerminalActions() { linux: { primary: KeyMod.Shift | KeyCode.End }, when: KEYBINDING_CONTEXT_TERMINAL_FOCUS, weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -756,7 +774,8 @@ export function registerTerminalActions() { linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.UpArrow }, when: KEYBINDING_CONTEXT_TERMINAL_FOCUS, weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -775,7 +794,8 @@ export function registerTerminalActions() { mac: { primary: KeyCode.PageUp }, when: KEYBINDING_CONTEXT_TERMINAL_FOCUS, weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -794,7 +814,8 @@ export function registerTerminalActions() { linux: { primary: KeyMod.Shift | KeyCode.Home }, when: KEYBINDING_CONTEXT_TERMINAL_FOCUS, weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -812,7 +833,8 @@ export function registerTerminalActions() { primary: KeyCode.Escape, when: ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_A11Y_TREE_FOCUS, CONTEXT_ACCESSIBILITY_MODE_ENABLED), weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -833,7 +855,8 @@ export function registerTerminalActions() { ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_FOCUS, CONTEXT_ACCESSIBILITY_MODE_ENABLED) ), weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -854,7 +877,8 @@ export function registerTerminalActions() { ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_FOCUS, CONTEXT_ACCESSIBILITY_MODE_ENABLED) ), weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -872,7 +896,8 @@ export function registerTerminalActions() { primary: KeyCode.Escape, when: ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, KEYBINDING_CONTEXT_TERMINAL_FIND_NOT_VISIBLE), weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -888,7 +913,8 @@ export function registerTerminalActions() { id: TERMINAL_COMMAND_ID.MANAGE_WORKSPACE_SHELL_PERMISSIONS, title: { value: localize('workbench.action.terminal.manageWorkspaceShellPermissions', "Manage Workspace Shell Permissions"), original: 'Manage Workspace Shell Permissions' }, f1: true, - category + category, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -901,7 +927,8 @@ export function registerTerminalActions() { id: TERMINAL_COMMAND_ID.RENAME, title: { value: localize('workbench.action.terminal.rename', "Rename"), original: 'Rename' }, f1: true, - category + category, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } async run(accessor: ServicesAccessor) { @@ -927,7 +954,8 @@ export function registerTerminalActions() { primary: KeyMod.CtrlCmd | KeyCode.KEY_F, when: ContextKeyExpr.or(KEYBINDING_CONTEXT_TERMINAL_FIND_FOCUSED, KEYBINDING_CONTEXT_TERMINAL_FOCUS), weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -946,7 +974,8 @@ export function registerTerminalActions() { secondary: [KeyMod.Shift | KeyCode.Escape], when: ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_VISIBLE), weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -959,7 +988,8 @@ export function registerTerminalActions() { id: TERMINAL_COMMAND_ID.QUICK_OPEN_TERM, title: { value: localize('quickAccessTerminal', "Switch Active Terminal"), original: 'Switch Active Terminal' }, f1: true, - category + category, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -977,7 +1007,8 @@ export function registerTerminalActions() { mac: { primary: KeyMod.CtrlCmd | KeyCode.UpArrow }, when: ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_FOCUS, CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate()), weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -998,7 +1029,8 @@ export function registerTerminalActions() { mac: { primary: KeyMod.CtrlCmd | KeyCode.DownArrow }, when: ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_FOCUS, CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate()), weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -1019,7 +1051,8 @@ export function registerTerminalActions() { mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.UpArrow }, when: KEYBINDING_CONTEXT_TERMINAL_FOCUS, weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -1040,7 +1073,8 @@ export function registerTerminalActions() { mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.DownArrow }, when: KEYBINDING_CONTEXT_TERMINAL_FOCUS, weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -1056,7 +1090,8 @@ export function registerTerminalActions() { id: TERMINAL_COMMAND_ID.SELECT_TO_PREVIOUS_LINE, title: { value: localize('workbench.action.terminal.selectToPreviousLine', "Select To Previous Line"), original: 'Select To Previous Line' }, f1: true, - category + category, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -1072,7 +1107,8 @@ export function registerTerminalActions() { id: TERMINAL_COMMAND_ID.SELECT_TO_NEXT_LINE, title: { value: localize('workbench.action.terminal.selectToNextLine', "Select To Next Line"), original: 'Select To Next Line' }, f1: true, - category + category, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -1088,7 +1124,8 @@ export function registerTerminalActions() { id: TERMINAL_COMMAND_ID.TOGGLE_ESCAPE_SEQUENCE_LOGGING, title: { value: localize('workbench.action.terminal.toggleEscapeSequenceLogging', "Toggle Escape Sequence Logging"), original: 'Toggle Escape Sequence Logging' }, f1: true, - category + category, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -1114,7 +1151,8 @@ export function registerTerminalActions() { }, } }] - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor, args?: { text?: string }) { @@ -1143,16 +1181,19 @@ export function registerTerminalActions() { }, } }] - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } async run(accessor: ServicesAccessor, args?: { cwd?: string }) { const terminalService = accessor.get(ITerminalService); - const instance = terminalService.createTerminal({ cwd: args?.cwd }); - if (!instance) { - return; + if (terminalService.isProcessSupportRegistered) { + const instance = terminalService.createTerminal({ cwd: args?.cwd }); + if (!instance) { + return; + } + terminalService.setActiveInstance(instance); } - terminalService.setActiveInstance(instance); return terminalService.showPanel(true); } }); @@ -1179,7 +1220,8 @@ export function registerTerminalActions() { } } }] - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor, args?: { name?: string }) { @@ -1203,7 +1245,8 @@ export function registerTerminalActions() { mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_R }, when: ContextKeyExpr.or(KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_FOCUSED), weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -1224,6 +1267,7 @@ export function registerTerminalActions() { when: ContextKeyExpr.or(KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_FOCUSED), weight: KeybindingWeight.WorkbenchContrib }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -1243,7 +1287,8 @@ export function registerTerminalActions() { mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_C }, when: ContextKeyExpr.or(KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_FOCUSED), weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -1270,7 +1315,8 @@ export function registerTerminalActions() { when: KEYBINDING_CONTEXT_TERMINAL_FIND_FOCUSED, weight: KeybindingWeight.WorkbenchContrib } - ] + ], + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -1296,7 +1342,8 @@ export function registerTerminalActions() { when: KEYBINDING_CONTEXT_TERMINAL_FIND_FOCUSED, weight: KeybindingWeight.WorkbenchContrib } - ] + ], + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -1309,7 +1356,8 @@ export function registerTerminalActions() { id: TERMINAL_COMMAND_ID.RELAUNCH, title: { value: localize('workbench.action.terminal.relaunch', "Relaunch Active Terminal"), original: 'Relaunch Active Terminal' }, f1: true, - category + category, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { @@ -1322,7 +1370,8 @@ export function registerTerminalActions() { id: TERMINAL_COMMAND_ID.SHOW_ENVIRONMENT_INFORMATION, title: { value: localize('workbench.action.terminal.showEnvironmentInformation', "Show Environment Information"), original: 'Show Environment Information' }, f1: true, - category + category, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } run(accessor: ServicesAccessor) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 4ad2c36ea2..c049940fbd 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -32,9 +32,9 @@ import { TerminalLinkManager } from 'vs/workbench/contrib/terminal/browser/links import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { ITerminalInstanceService, ITerminalInstance, TerminalShellType, WindowsShellType, ITerminalExternalLinkProvider } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalProcessManager } from 'vs/workbench/contrib/terminal/browser/terminalProcessManager'; -import { Terminal as XTermTerminal, IBuffer, ITerminalAddon } from 'xterm'; -import { SearchAddon, ISearchOptions } from 'xterm-addon-search'; -import { Unicode11Addon } from 'xterm-addon-unicode11'; +import type { Terminal as XTermTerminal, IBuffer, ITerminalAddon } from 'xterm'; +import type { SearchAddon, ISearchOptions } from 'xterm-addon-search'; +import type { Unicode11Addon } from 'xterm-addon-unicode11'; import { CommandTrackerAddon } from 'vs/workbench/contrib/terminal/browser/addons/commandTrackerAddon'; import { NavigationModeAddon } from 'vs/workbench/contrib/terminal/browser/addons/navigationModeAddon'; import { XTermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private'; @@ -1128,9 +1128,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { if (!reset) { // HACK: Force initialText to be non-falsy for reused terminals such that the - // conptyInheritCursor flag is passed to the node-pty, this flag can cause a Window to hang - // in Windows 10 1903 so we only want to use it when something is definitely written to the - // terminal. + // conptyInheritCursor flag is passed to the node-pty, this flag can cause a Window to stop + // responding in Windows 10 1903 so we only want to use it when something is definitely written + // to the terminal. shell.initialText = ' '; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts index 28c6a9128d..0970ce5fb5 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts @@ -5,10 +5,10 @@ import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IWindowsShellHelper, ITerminalChildProcess, IDefaultShellAndArgsRequest } from 'vs/workbench/contrib/terminal/common/terminal'; -import { Terminal as XTermTerminal } from 'xterm'; -import { SearchAddon as XTermSearchAddon } from 'xterm-addon-search'; -import { Unicode11Addon as XTermUnicode11Addon } from 'xterm-addon-unicode11'; -import { WebglAddon as XTermWebglAddon } from 'xterm-addon-webgl'; +import type { Terminal as XTermTerminal } from 'xterm'; +import type { SearchAddon as XTermSearchAddon } from 'xterm-addon-search'; +import type { Unicode11Addon as XTermUnicode11Addon } from 'xterm-addon-unicode11'; +import type { WebglAddon as XTermWebglAddon } from 'xterm-addon-webgl'; import { IProcessEnvironment } from 'vs/base/common/platform'; import { Emitter, Event } from 'vs/base/common/event'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index 63b8d64fd7..41c73553ba 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { TERMINAL_VIEW_ID, IShellLaunchConfig, ITerminalConfigHelper, ISpawnExtHostProcessRequest, IStartExtensionTerminalRequest, IAvailableShellsRequest, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_IS_OPEN, ITerminalProcessExtHostProxy, IShellDefinition, LinuxDistro, KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE, ITerminalLaunchError, ITerminalNativeWindowsDelegate } from 'vs/workbench/contrib/terminal/common/terminal'; +import { TERMINAL_VIEW_ID, IShellLaunchConfig, ITerminalConfigHelper, ISpawnExtHostProcessRequest, IStartExtensionTerminalRequest, IAvailableShellsRequest, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_FIND_VISIBLE, KEYBINDING_CONTEXT_TERMINAL_IS_OPEN, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED, ITerminalProcessExtHostProxy, IShellDefinition, LinuxDistro, KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE, ITerminalLaunchError, ITerminalNativeWindowsDelegate } from 'vs/workbench/contrib/terminal/common/terminal'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; @@ -23,12 +23,13 @@ import { Event, Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { FindReplaceState } from 'vs/editor/contrib/find/findState'; import { escapeNonWindowsPath } from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; -import { isWindows, isMacintosh, OperatingSystem } from 'vs/base/common/platform'; +import { isWindows, isMacintosh, OperatingSystem, isWeb } from 'vs/base/common/platform'; import { basename } from 'vs/base/common/path'; import { find } from 'vs/base/common/arrays'; import { timeout } from 'vs/base/common/async'; import { IViewsService, ViewContainerLocation, IViewDescriptorService } from 'vs/workbench/common/views'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; interface IExtHostReadyEntry { promise: Promise; @@ -52,10 +53,12 @@ export class TerminalService implements ITerminalService { private _activeTabIndex: number; private _linkProviders: Set = new Set(); private _linkProviderDisposables: Map = new Map(); + private _processSupportContextKey: IContextKey; public get activeTabIndex(): number { return this._activeTabIndex; } public get terminalInstances(): ITerminalInstance[] { return this._terminalInstances; } public get terminalTabs(): ITerminalTab[] { return this._terminalTabs; } + public get isProcessSupportRegistered(): boolean { return !!this._processSupportContextKey.get(); } private _configHelper: TerminalConfigHelper; private _terminalContainer: HTMLElement | undefined; @@ -91,6 +94,8 @@ export class TerminalService implements ITerminalService { public get onTabDisposed(): Event { return this._onTabDisposed.event; } private readonly _onRequestAvailableShells = new Emitter(); public get onRequestAvailableShells(): Event { return this._onRequestAvailableShells.event; } + private readonly _onDidRegisterProcessSupport = new Emitter(); + public get onDidRegisterProcessSupport(): Event { return this._onDidRegisterProcessSupport.event; } constructor( @IContextKeyService private _contextKeyService: IContextKeyService, @@ -103,7 +108,8 @@ export class TerminalService implements ITerminalService { @IQuickInputService private _quickInputService: IQuickInputService, @IConfigurationService private _configurationService: IConfigurationService, @IViewsService private _viewsService: IViewsService, - @IViewDescriptorService private readonly _viewDescriptorService: IViewDescriptorService + @IViewDescriptorService private readonly _viewDescriptorService: IViewDescriptorService, + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService ) { this._activeTabIndex = 0; this._isShuttingDown = false; @@ -121,7 +127,9 @@ export class TerminalService implements ITerminalService { }); this.onInstanceLinksReady(instance => this._setInstanceLinkProviders(instance)); - this._handleContextKeys(); + this._handleInstanceContextKeys(); + this._processSupportContextKey = KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED.bindTo(this._contextKeyService); + this._processSupportContextKey.set(!isWeb || this._remoteAgentService.getConnection() !== null); } public setNativeWindowsDelegate(delegate: ITerminalNativeWindowsDelegate): void { @@ -132,13 +140,11 @@ export class TerminalService implements ITerminalService { this._configHelper.setLinuxDistro(linuxDistro); } - private _handleContextKeys(): void { + private _handleInstanceContextKeys(): void { const terminalIsOpenContext = KEYBINDING_CONTEXT_TERMINAL_IS_OPEN.bindTo(this._contextKeyService); - const updateTerminalContextKeys = () => { terminalIsOpenContext.set(this.terminalInstances.length > 0); }; - this.onInstancesChanged(() => updateTerminalContextKeys()); } @@ -411,6 +417,14 @@ export class TerminalService implements ITerminalService { instance.addDisposable(instance.onFocus(this._onActiveInstanceChanged.fire, this._onActiveInstanceChanged)); } + public registerProcessSupport(isSupported: boolean): void { + if (!isSupported) { + return; + } + this._processSupportContextKey.set(isSupported); + this._onDidRegisterProcessSupport.fire(); + } + public registerLinkProvider(linkProvider: ITerminalExternalLinkProvider): IDisposable { const disposables: IDisposable[] = []; this._linkProviders.add(linkProvider); @@ -593,6 +607,9 @@ export class TerminalService implements ITerminalService { } public createTerminal(shell: IShellLaunchConfig = {}): ITerminalInstance { + if (!this.isProcessSupportRegistered) { + throw new Error('Could not create terminal when process support is not registered'); + } if (shell.hideFromUser) { const instance = this.createInstance(undefined, shell); this._backgroundedTerminalInstances.push(instance); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalView.ts b/src/vs/workbench/contrib/terminal/browser/terminalView.ts index 57b8b4c058..79271676ef 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalView.ts @@ -19,7 +19,6 @@ import { URI } from 'vs/base/common/uri'; import { TERMINAL_BACKGROUND_COLOR, TERMINAL_BORDER_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; import { DataTransfers } from 'vs/base/browser/dnd'; import { INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification'; -import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { BrowserFeatures } from 'vs/base/browser/canIUse'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; @@ -56,14 +55,25 @@ export class TerminalViewPane extends ViewPane { @IThemeService protected readonly themeService: IThemeService, @ITelemetryService telemetryService: ITelemetryService, @INotificationService private readonly _notificationService: INotificationService, - @IStorageService storageService: IStorageService, @IOpenerService openerService: IOpenerService, ) { super(options, keybindingService, _contextMenuService, configurationService, contextKeyService, viewDescriptorService, _instantiationService, openerService, themeService, telemetryService); + this._terminalService.onDidRegisterProcessSupport(() => { + if (this._actions) { + for (const action of this._actions) { + action.enabled = true; + } + } + this._onDidChangeViewWelcomeState.fire(); + }); } protected renderBody(container: HTMLElement): void { super.renderBody(container); + if (this.shouldShowWelcome()) { + return; + } + this._parentDomElement = container; dom.addClass(this._parentDomElement, 'integrated-terminal'); this._fontStyleElement = document.createElement('style'); @@ -120,6 +130,10 @@ export class TerminalViewPane extends ViewPane { protected layoutBody(height: number, width: number): void { super.layoutBody(height, width); + if (this.shouldShowWelcome()) { + return; + } + this._bodyDimensions.width = width; this._bodyDimensions.height = height; this._terminalService.terminalTabs.forEach(t => t.layout(width, height)); @@ -138,9 +152,12 @@ export class TerminalViewPane extends ViewPane { this._splitTerminalAction, this._instantiationService.createInstance(KillTerminalAction, KillTerminalAction.ID, KillTerminalAction.PANEL_LABEL) ]; - this._actions.forEach(a => { - this._register(a); - }); + for (const action of this._actions) { + if (!this._terminalService.isProcessSupportRegistered) { + action.enabled = false; + } + this._register(action); + } } return this._actions; } @@ -188,10 +205,7 @@ export class TerminalViewPane extends ViewPane { } public focus(): void { - const activeInstance = this._terminalService.getActiveInstance(); - if (activeInstance) { - activeInstance.focusWhenReady(true); - } + this._terminalService.getActiveInstance()?.focusWhenReady(true); } public focusFindWidget() { @@ -331,9 +345,11 @@ export class TerminalViewPane extends ViewPane { theme = this.themeService.getColorTheme(); } - if (this._findWidget) { - this._findWidget.updateTheme(theme); - } + this._findWidget?.updateTheme(theme); + } + + shouldShowWelcome(): boolean { + return !this._terminalService.isProcessSupportRegistered; } } diff --git a/src/vs/workbench/contrib/terminal/browser/widgets/terminalHoverWidget.ts b/src/vs/workbench/contrib/terminal/browser/widgets/terminalHoverWidget.ts index cba57b7907..8856e7fbbc 100644 --- a/src/vs/workbench/contrib/terminal/browser/widgets/terminalHoverWidget.ts +++ b/src/vs/workbench/contrib/terminal/browser/widgets/terminalHoverWidget.ts @@ -8,7 +8,7 @@ import { IMarkdownString } from 'vs/base/common/htmlContent'; import { Widget } from 'vs/base/browser/ui/widget'; import { ITerminalWidget } from 'vs/workbench/contrib/terminal/browser/widgets/widgets'; import * as dom from 'vs/base/browser/dom'; -import { IViewportRange } from 'xterm'; +import type { IViewportRange } from 'xterm'; import { IHoverTarget, IHoverService } from 'vs/workbench/services/hover/browser/hover'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { editorHoverHighlight } from 'vs/platform/theme/common/colorRegistry'; diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 5eb5f314c6..1ddf483e9a 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -12,7 +12,7 @@ import { OperatingSystem } from 'vs/base/common/platform'; import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { IExtensionPointDescriptor } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -export const TERMINAL_VIEW_ID = 'workbench.panel.terminal'; +export const TERMINAL_VIEW_ID = 'terminal'; /** A context key that is set when there is at least one opened integrated terminal. */ export const KEYBINDING_CONTEXT_TERMINAL_IS_OPEN = new RawContextKey('terminalIsOpen', false); @@ -46,6 +46,8 @@ export const KEYBINDING_CONTEXT_TERMINAL_FIND_FOCUSED = new RawContextKey('terminalProcessSupported', false); + export const IS_WORKSPACE_SHELL_ALLOWED_STORAGE_KEY = 'terminal.integrated.isWorkspaceShellAllowed'; export const NEVER_MEASURE_RENDER_TIME_STORAGE_KEY = 'terminal.integrated.neverMeasureRenderTime'; diff --git a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts index df08c60abd..ee693d0acb 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts @@ -93,7 +93,7 @@ export function shouldSetLangEnvVariable(env: platform.IProcessEnvironment, dete return true; } if (detectLocale === 'auto') { - return !env['LANG'] || env['LANG'].search(/\.UTF\-8$/) === -1; + return !env['LANG'] || (env['LANG'].search(/\.UTF\-8$/) === -1 && env['LANG'].search(/\.utf8$/) === -1); } return false; // 'off' } diff --git a/src/vs/workbench/contrib/terminal/common/terminalMenu.ts b/src/vs/workbench/contrib/terminal/common/terminalMenu.ts index 8d79b06022..d6fd74d828 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalMenu.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalMenu.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; -import { TERMINAL_COMMAND_ID } from 'vs/workbench/contrib/terminal/common/terminal'; +import { KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED, TERMINAL_COMMAND_ID } from 'vs/workbench/contrib/terminal/common/terminal'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; export function setupTerminalMenu() { @@ -38,7 +38,8 @@ export function setupTerminalMenu() { title: nls.localize({ key: 'miSplitTerminal', comment: ['&& denotes a mnemonic'] }, "&&Split Terminal"), precondition: ContextKeyExpr.has('terminalIsOpen') }, - order: 2 + order: 2, + when: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); // Run @@ -49,7 +50,8 @@ export function setupTerminalMenu() { id: TERMINAL_COMMAND_ID.RUN_ACTIVE_FILE, title: nls.localize({ key: 'miRunActiveFile', comment: ['&& denotes a mnemonic'] }, "Run &&Active File") }, - order: 3 + order: 3, + when: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { group: runGroup, @@ -57,6 +59,7 @@ export function setupTerminalMenu() { id: TERMINAL_COMMAND_ID.RUN_SELECTED_TEXT, title: nls.localize({ key: 'miRunSelectedText', comment: ['&& denotes a mnemonic'] }, "Run &&Selected Text") }, - order: 4 + order: 4, + when: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } \ No newline at end of file diff --git a/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts b/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts index a23a097fec..f352c1cd45 100644 --- a/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts +++ b/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts @@ -10,10 +10,10 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IProcessEnvironment, platform, Platform } from 'vs/base/common/platform'; import { TerminalProcess } from 'vs/workbench/contrib/terminal/node/terminalProcess'; import { getSystemShell } from 'vs/workbench/contrib/terminal/node/terminal'; -import { Terminal as XTermTerminal } from 'xterm'; -import { SearchAddon as XTermSearchAddon } from 'xterm-addon-search'; -import { Unicode11Addon as XTermUnicode11Addon } from 'xterm-addon-unicode11'; -import { WebglAddon as XTermWebglAddon } from 'xterm-addon-webgl'; +import type { Terminal as XTermTerminal } from 'xterm'; +import type { SearchAddon as XTermSearchAddon } from 'xterm-addon-search'; +import type { Unicode11Addon as XTermUnicode11Addon } from 'xterm-addon-unicode11'; +import type { WebglAddon as XTermWebglAddon } from 'xterm-addon-webgl'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { getDefaultShell, getDefaultShellArgs } from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; import { StorageScope, IStorageService } from 'vs/platform/storage/common/storage'; diff --git a/src/vs/workbench/contrib/terminal/electron-browser/terminalNativeContribution.ts b/src/vs/workbench/contrib/terminal/electron-browser/terminalNativeContribution.ts index 1339cef9ea..645690a33f 100644 --- a/src/vs/workbench/contrib/terminal/electron-browser/terminalNativeContribution.ts +++ b/src/vs/workbench/contrib/terminal/electron-browser/terminalNativeContribution.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; -import { IOpenFileRequest } from 'vs/platform/windows/common/windows'; +import { INativeOpenFileRequest } from 'vs/platform/windows/common/windows'; import { URI } from 'vs/base/common/uri'; import { IFileService } from 'vs/platform/files/common/files'; import { getWindowsBuildNumber, linuxDistro } from 'vs/workbench/contrib/terminal/node/terminal'; @@ -15,7 +15,6 @@ import { registerRemoteContributions } from 'vs/workbench/contrib/terminal/elect import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron'; import { Disposable } from 'vs/base/common/lifecycle'; -import { INativeOpenFileRequest } from 'vs/platform/windows/node/window'; import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -31,7 +30,7 @@ export class TerminalNativeContribution extends Disposable implements IWorkbench ) { super(); - ipcRenderer.on('vscode:openFiles', (_: unknown, request: IOpenFileRequest) => this._onOpenFileRequest(request)); + ipcRenderer.on('vscode:openFiles', (_: unknown, request: INativeOpenFileRequest) => this._onOpenFileRequest(request)); this._register(electronService.onOSResume(() => this._onOsResume())); this._terminalService.setLinuxDistro(linuxDistro); diff --git a/src/vs/workbench/contrib/terminal/electron-browser/windowsShellHelper.ts b/src/vs/workbench/contrib/terminal/electron-browser/windowsShellHelper.ts index 50e2e997e8..7f6bdd01e9 100644 --- a/src/vs/workbench/contrib/terminal/electron-browser/windowsShellHelper.ts +++ b/src/vs/workbench/contrib/terminal/electron-browser/windowsShellHelper.ts @@ -6,8 +6,8 @@ import * as platform from 'vs/base/common/platform'; import { Emitter, Event } from 'vs/base/common/event'; import { IWindowsShellHelper } from 'vs/workbench/contrib/terminal/common/terminal'; -import { Terminal as XTermTerminal } from 'xterm'; -import * as WindowsProcessTreeType from 'windows-process-tree'; +import type { Terminal as XTermTerminal } from 'xterm'; +import type * as WindowsProcessTreeType from 'windows-process-tree'; import { Disposable } from 'vs/base/common/lifecycle'; import { timeout } from 'vs/base/common/async'; diff --git a/src/vs/workbench/contrib/terminal/node/terminalProcess.ts b/src/vs/workbench/contrib/terminal/node/terminalProcess.ts index c87474b6e4..6637e73291 100644 --- a/src/vs/workbench/contrib/terminal/node/terminalProcess.ts +++ b/src/vs/workbench/contrib/terminal/node/terminalProcess.ts @@ -5,7 +5,7 @@ import * as path from 'vs/base/common/path'; import * as platform from 'vs/base/common/platform'; -import * as pty from 'node-pty'; +import type * as pty from 'node-pty'; import * as fs from 'fs'; import { Event, Emitter } from 'vs/base/common/event'; import { getWindowsBuildNumber } from 'vs/workbench/contrib/terminal/node/terminal'; @@ -90,7 +90,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } try { - this.setupPtyProcess(this._shellLaunchConfig, this._ptyOptions); + await this.setupPtyProcess(this._shellLaunchConfig, this._ptyOptions); return undefined; } catch (err) { this._logService.trace('IPty#spawn native exception', err); @@ -136,10 +136,10 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess return undefined; } - private setupPtyProcess(shellLaunchConfig: IShellLaunchConfig, options: pty.IPtyForkOptions): void { + private async setupPtyProcess(shellLaunchConfig: IShellLaunchConfig, options: pty.IPtyForkOptions): Promise { const args = shellLaunchConfig.args || []; this._logService.trace('IPty#spawn', shellLaunchConfig.executable, args, options); - const ptyProcess = pty.spawn(shellLaunchConfig.executable!, args, options); + const ptyProcess = (await import('node-pty')).spawn(shellLaunchConfig.executable!, args, options); this._ptyProcess = ptyProcess; this._processStartupComplete = new Promise(c => { this.onProcessReady(() => c()); diff --git a/src/vs/workbench/contrib/terminal/test/node/terminalEnvironment.test.ts b/src/vs/workbench/contrib/terminal/test/node/terminalEnvironment.test.ts index 833b14e4c2..e8cecade2c 100644 --- a/src/vs/workbench/contrib/terminal/test/node/terminalEnvironment.test.ts +++ b/src/vs/workbench/contrib/terminal/test/node/terminalEnvironment.test.ts @@ -45,16 +45,22 @@ suite('Workbench - TerminalEnvironment', () => { test('auto', () => { assert.equal(shouldSetLangEnvVariable({}, 'auto'), true); assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US' }, 'auto'), true); + assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US.utf' }, 'auto'), true); + assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US.utf8' }, 'auto'), false); assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US.UTF-8' }, 'auto'), false); }); test('off', () => { assert.equal(shouldSetLangEnvVariable({}, 'off'), false); assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US' }, 'off'), false); + assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US.utf' }, 'off'), false); + assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US.utf8' }, 'off'), false); assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US.UTF-8' }, 'off'), false); }); test('on', () => { assert.equal(shouldSetLangEnvVariable({}, 'on'), true); assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US' }, 'on'), true); + assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US.utf' }, 'on'), true); + assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US.utf8' }, 'on'), true); assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US.UTF-8' }, 'on'), true); }); }); diff --git a/src/vs/workbench/contrib/timeline/browser/media/timelinePane.css b/src/vs/workbench/contrib/timeline/browser/media/timelinePane.css index b7c3f473e6..0e3d783fea 100644 --- a/src/vs/workbench/contrib/timeline/browser/media/timelinePane.css +++ b/src/vs/workbench/contrib/timeline/browser/media/timelinePane.css @@ -7,26 +7,6 @@ position: relative; } -.monaco-workbench .timeline-view.pane-header .description { - display: block; - font-weight: normal; - margin-left: 10px; - opacity: 0.6; - overflow: hidden; - text-overflow: ellipsis; - text-transform: none; - white-space: nowrap; -} - -.monaco-workbench .timeline-view.pane-header:not(.expanded) .description { - display: none; -} - -.monaco-workbench .timeline-view.pane-header .description span.codicon { - font-size: 9px; - margin-left: 2px; -} - .monaco-workbench .timeline-tree-view .message.timeline-subtle { opacity: 0.5; padding: 10px 22px 0 22px; diff --git a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts index 93acec8ef7..55d7f96b19 100644 --- a/src/vs/workbench/contrib/timeline/browser/timelinePane.ts +++ b/src/vs/workbench/contrib/timeline/browser/timelinePane.ts @@ -219,7 +219,6 @@ export class TimelinePane extends ViewPane { private $container!: HTMLElement; private $message!: HTMLDivElement; - private $titleDescription!: HTMLSpanElement; private $tree!: HTMLDivElement; private tree!: WorkbenchObjectTree; private treeRenderer: TimelineTreeRenderer | undefined; @@ -276,7 +275,7 @@ export class TimelinePane extends ViewPane { this._followActiveEditor = value; this.followActiveEditorContext.set(value); - this.titleDescription = this.titleDescription; + this.updateFilename(this._filename); if (value) { this.onActiveEditorChanged(); @@ -315,7 +314,7 @@ export class TimelinePane extends ViewPane { } this.uri = uri; - this.titleDescription = uri ? basename(uri.fsPath) : ''; + this.updateFilename(uri ? basename(uri.fsPath) : undefined); this.treeRenderer?.setUri(uri); this.loadTimeline(true); } @@ -407,17 +406,13 @@ export class TimelinePane extends ViewPane { } } - private _titleDescription: string | undefined; - get titleDescription(): string | undefined { - return this._titleDescription; - } - - set titleDescription(description: string | undefined) { - this._titleDescription = description; - if (this.followActiveEditor || !description) { - this.$titleDescription.textContent = description ?? ''; + private _filename: string | undefined; + updateFilename(filename: string | undefined) { + this._filename = filename; + if (this.followActiveEditor || !filename) { + this.updateTitleDescription(filename); } else { - this.$titleDescription.textContent = `${description} (pinned)`; + this.updateTitleDescription(`${filename} (pinned)`); } } @@ -781,17 +776,17 @@ export class TimelinePane extends ViewPane { this._isEmpty = !this.hasVisibleItems; if (this.uri === undefined) { - this.titleDescription = undefined; + this.updateFilename(undefined); this.message = localize('timeline.editorCannotProvideTimeline', "The active editor cannot provide timeline information."); } else if (this._isEmpty) { if (this.pendingRequests.size !== 0) { this.setLoadingUriMessage(); } else { - this.titleDescription = basename(this.uri.fsPath); + this.updateFilename(basename(this.uri.fsPath)); this.message = localize('timeline.noTimelineInfo', "No timeline information was provided."); } } else { - this.titleDescription = basename(this.uri.fsPath); + this.updateFilename(basename(this.uri.fsPath)); this.message = undefined; } @@ -849,7 +844,6 @@ export class TimelinePane extends ViewPane { super.renderHeaderTitle(container, this.title); DOM.addClass(container, 'timeline-view'); - this.$titleDescription = DOM.append(container, DOM.$('span.description', undefined, this.titleDescription ?? '')); } protected renderBody(container: HTMLElement): void { @@ -956,7 +950,7 @@ export class TimelinePane extends ViewPane { setLoadingUriMessage() { const file = this.uri && basename(this.uri.fsPath); - this.titleDescription = file ?? ''; + this.updateFilename(file); this.message = file ? localize('timeline.loading', "Loading timeline for {0}...", file) : ''; } diff --git a/src/vs/workbench/contrib/update/browser/update.ts b/src/vs/workbench/contrib/update/browser/update.ts index a9086adb25..de3e114f16 100644 --- a/src/vs/workbench/contrib/update/browser/update.ts +++ b/src/vs/workbench/contrib/update/browser/update.ts @@ -14,7 +14,6 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IUpdateService, State as UpdateState, StateType, IUpdate } from 'vs/platform/update/common/update'; -import * as semver from 'semver-umd'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -131,7 +130,7 @@ export class ProductContribution implements IWorkbenchContribution { @IHostService hostService: IHostService, @IProductService productService: IProductService ) { - hostService.hadLastFocus().then(hadLastFocus => { + hostService.hadLastFocus().then(async hadLastFocus => { if (!hadLastFocus) { return; } @@ -160,6 +159,7 @@ export class ProductContribution implements IWorkbenchContribution { }*/ // should we show the new license? + const semver = await import('semver-umd'); if (productService.licenseUrl && lastVersion && semver.satisfies(lastVersion, '<1.0.0') && semver.satisfies(productService.version, '>=1.0.0')) { notificationService.info(nls.localize('licenseChanged', "Our license terms have changed, please click [here]({0}) to go through them.", productService.licenseUrl)); } diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSyncService.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSyncService.ts deleted file mode 100644 index 1de03ea30f..0000000000 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSyncService.ts +++ /dev/null @@ -1,42 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IUserDataSyncService, IUserDataSyncLogService, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService, IUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSync'; -import { Event } from 'vs/base/common/event'; -import { UserDataAutoSyncService as BaseUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataAutoSyncService'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { IUserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { UserDataSyncTrigger } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger'; -import { IStorageService } from 'vs/platform/storage/common/storage'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines'; - -export class UserDataAutoSyncService extends BaseUserDataAutoSyncService { - - constructor( - @IUserDataSyncStoreManagementService userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, - @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, - @IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService, - @IUserDataSyncService userDataSyncService: IUserDataSyncService, - @IUserDataSyncLogService logService: IUserDataSyncLogService, - @IUserDataSyncAccountService authTokenService: IUserDataSyncAccountService, - @IInstantiationService instantiationService: IInstantiationService, - @IHostService hostService: IHostService, - @ITelemetryService telemetryService: ITelemetryService, - @IUserDataSyncMachinesService userDataSyncMachinesService: IUserDataSyncMachinesService, - @IStorageService storageService: IStorageService, - @IEnvironmentService environmentService: IEnvironmentService, - ) { - super(userDataSyncStoreManagementService, userDataSyncStoreService, userDataSyncResourceEnablementService, userDataSyncService, logService, authTokenService, telemetryService, userDataSyncMachinesService, storageService, environmentService); - - this._register(Event.debounce(Event.any( - Event.map(hostService.onDidChangeFocus, () => 'windowFocus'), - instantiationService.createInstance(UserDataSyncTrigger).onDidTriggerSync, - ), (last, source) => last ? [...last, source] : [source], 1000)(sources => this.triggerSync(sources, true))); - } - -} diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts index 2407e263a7..53f107f7fd 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.contribution.ts @@ -13,6 +13,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { isWeb } from 'vs/base/common/platform'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; +import { UserDataSyncTrigger } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger'; class UserDataSyncReportIssueContribution extends Disposable implements IWorkbenchContribution { @@ -67,6 +68,7 @@ export class UserDataSyncSettingsMigrationContribution implements IWorkbenchCont const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(UserDataSyncWorkbenchContribution, LifecyclePhase.Ready); workbenchRegistry.registerWorkbenchContribution(UserDataSyncSettingsMigrationContribution, LifecyclePhase.Eventually); +workbenchRegistry.registerWorkbenchContribution(UserDataSyncTrigger, LifecyclePhase.Eventually); if (isWeb) { workbenchRegistry.registerWorkbenchContribution(UserDataSyncReportIssueContribution, LifecyclePhase.Ready); diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index a79780f91b..a96e5d14e8 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -431,6 +431,9 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo private async turnOn(): Promise { try { + if (!this.userDataSyncWorkbenchService.authenticationProviders.length) { + throw new Error(localize('no authentication providers', "No authentication providers are available.")); + } if (!this.storageService.getBoolean('sync.donotAskPreviewConfirmation', StorageScope.GLOBAL, false)) { if (!await this.askForConfirmation()) { return; @@ -478,7 +481,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo return; } } - this.notificationService.error(localize('turn on failed', "Error while starting Sync: {0}", toErrorMessage(e))); + this.notificationService.error(localize('turn on failed', "Error while starting Settings Sync: {0}", toErrorMessage(e))); } } @@ -1031,7 +1034,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo }); } run(accessor: ServicesAccessor): Promise { - return that.userDataAutoSyncService.triggerSync([syncNowCommand.id], false); + return that.userDataAutoSyncService.triggerSync([syncNowCommand.id], false, true); } })); } diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts index 16728885f7..a6435cd5ed 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event, Emitter } from 'vs/base/common/event'; +import { Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { SettingsEditor2Input, KeybindingsEditorInput, PreferencesEditorInput } from 'vs/workbench/services/preferences/common/preferencesEditorInput'; @@ -12,24 +12,36 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { IEditorInput } from 'vs/workbench/common/editor'; import { IViewsService } from 'vs/workbench/common/views'; +import { IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; +import { isWeb } from 'vs/base/common/platform'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; -export class UserDataSyncTrigger extends Disposable { - - private readonly _onDidTriggerSync: Emitter = this._register(new Emitter()); - readonly onDidTriggerSync: Event = this._onDidTriggerSync.event; +export class UserDataSyncTrigger extends Disposable implements IWorkbenchContribution { constructor( @IEditorService editorService: IEditorService, @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService, @IViewsService viewsService: IViewsService, + @IUserDataAutoSyncService userDataAutoSyncService: IUserDataAutoSyncService, + @IHostService hostService: IHostService, ) { super(); - this._register( - Event.filter( - Event.any( - Event.map(editorService.onDidActiveEditorChange, () => this.getUserDataEditorInputSource(editorService.activeEditor)), - Event.map(Event.filter(viewsService.onDidChangeViewContainerVisibility, e => e.id === VIEWLET_ID && e.visible), e => e.id) - ), source => source !== undefined)(source => this._onDidTriggerSync.fire(source!))); + const event = Event.filter( + Event.any( + Event.map(editorService.onDidActiveEditorChange, () => this.getUserDataEditorInputSource(editorService.activeEditor)), + Event.map(Event.filter(viewsService.onDidChangeViewContainerVisibility, e => e.id === VIEWLET_ID && e.visible), e => e.id) + ), source => source !== undefined); + if (isWeb) { + this._register(Event.debounce( + Event.any( + Event.map(hostService.onDidChangeFocus, () => 'windowFocus'), + Event.map(event, source => source!), + ), (last, source) => last ? [...last, source] : [source], 1000) + (sources => userDataAutoSyncService.triggerSync(sources, true, false))); + } else { + this._register(event(source => userDataAutoSyncService.triggerSync([source!], true, false))); + } } private getUserDataEditorInputSource(editorInput: IEditorInput | undefined): string | undefined { diff --git a/src/vs/workbench/contrib/views/browser/treeView.ts b/src/vs/workbench/contrib/views/browser/treeView.ts index 3f975a595c..82241fc9a9 100644 --- a/src/vs/workbench/contrib/views/browser/treeView.ts +++ b/src/vs/workbench/contrib/views/browser/treeView.ts @@ -40,6 +40,7 @@ import { isFalsyOrWhitespace } from 'vs/base/common/strings'; import { SIDE_BAR_BACKGROUND, PANEL_BACKGROUND } from 'vs/workbench/common/theme'; import { IHoverService, IHoverOptions, IHoverTarget } from 'vs/workbench/services/hover/browser/hover'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; class Root implements ITreeItem { label = { label: 'root' }; @@ -766,7 +767,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer('explorer.decorations'); templateData.resourceLabel.setResource({ name: label, description, resource: resource ? resource : URI.parse('missing:_icon_resource') }, { fileKind: this.getFileKind(node), - title, + title: undefined, hideIcon: !!iconUrl, fileDecorations, extraClasses: ['custom-view-tree-node-item-resourceLabel'], @@ -775,7 +776,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer{ $treeViewId: this.treeViewId, $treeItemHandle: node.handle }; templateData.actionBar.push(this.menus.getResourceActions(node), { icon: true, label: false }); @@ -811,10 +812,6 @@ class TreeRenderer extends Disposable implements ITreeRenderer { - await resolvableNode.resolve(); - const tooltip = resolvableNode.tooltip ?? label; + if (node instanceof ResolvableTreeItem) { + await node.resolve(); + } + let tooltip: IMarkdownString | string | undefined = node.tooltip ?? label; if (isHovering && tooltip) { if (!hoverOptions) { const target: IHoverTarget = { targetElements: [this], dispose: () => { } }; - hoverOptions = { text: isString(tooltip) ? { value: tooltip } : tooltip, target }; + hoverOptions = { text: tooltip, target }; } (hoverOptions.target).x = e.x; hoverService.showHover(hoverOptions); diff --git a/src/vs/workbench/contrib/webview/browser/pre/main.js b/src/vs/workbench/contrib/webview/browser/pre/main.js index a31fc91b89..ffef96f9d4 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/main.js +++ b/src/vs/workbench/contrib/webview/browser/pre/main.js @@ -19,6 +19,11 @@ (function () { 'use strict'; + const isSafari = navigator.vendor && navigator.vendor.indexOf('Apple') > -1 && + navigator.userAgent && + navigator.userAgent.indexOf('CriOS') === -1 && + navigator.userAgent.indexOf('FxiOS') === -1; + /** * Use polling to track focus of main webview and iframes within the webview * @@ -53,7 +58,7 @@ const defaultCssRules = ` body { - background-color: var(--vscode-editor-background); + background-color: transparent; color: var(--vscode-editor-foreground); font-family: var(--vscode-font-family); font-weight: var(--vscode-font-weight); @@ -480,7 +485,7 @@ const newFrame = document.createElement('iframe'); newFrame.setAttribute('id', 'pending-frame'); newFrame.setAttribute('frameborder', '0'); - newFrame.setAttribute('sandbox', options.allowScripts ? 'allow-scripts allow-forms allow-same-origin' : 'allow-same-origin'); + newFrame.setAttribute('sandbox', options.allowScripts ? 'allow-scripts allow-forms allow-same-origin allow-pointer-lock' : 'allow-same-origin allow-pointer-lock'); if (host.fakeLoad) { // We should just be able to use srcdoc, but I wasn't // seeing the service worker applying properly. @@ -514,7 +519,7 @@ }, 0); } - if (host.fakeLoad && false) { + if (host.fakeLoad && !options.allowScripts && isSafari) { // On Safari for iframes with scripts disabled, the `DOMContentLoaded` never seems to be fired. // Use polling instead. const interval = setInterval(() => { @@ -524,7 +529,7 @@ return; } - if (newFrame.contentDocument.readyState === 'complete') { + if (newFrame.contentDocument.readyState !== 'loading') { clearInterval(interval); onFrameLoaded(newFrame.contentDocument); } diff --git a/src/vs/workbench/contrib/webview/browser/webview.contribution.ts b/src/vs/workbench/contrib/webview/browser/webview.contribution.ts index 43c4cdbdcb..fa4f1b2a68 100644 --- a/src/vs/workbench/contrib/webview/browser/webview.contribution.ts +++ b/src/vs/workbench/contrib/webview/browser/webview.contribution.ts @@ -14,7 +14,7 @@ import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } fro import { Extensions as EditorInputExtensions, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; import { Webview, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview'; import { WebviewEditorInputFactory } from 'vs/workbench/contrib/webview/browser/webviewEditorInputFactory'; -import { getActiveWebview, HideWebViewEditorFindCommand, ReloadWebviewAction, SelectAllWebviewEditorCommand, ShowWebViewEditorFindWidgetAction, WebViewEditorFindNextCommand, WebViewEditorFindPreviousCommand } from '../browser/webviewCommands'; +import { getActiveWebviewEditor, HideWebViewEditorFindCommand, ReloadWebviewAction, ShowWebViewEditorFindWidgetAction, WebViewEditorFindNextCommand, WebViewEditorFindPreviousCommand } from '../browser/webviewCommands'; import { WebviewEditor } from './webviewEditor'; import { WebviewInput } from './webviewEditorInput'; import { IWebviewWorkbenchService, WebviewEditorService } from './webviewWorkbenchService'; @@ -35,12 +35,11 @@ registerAction2(ShowWebViewEditorFindWidgetAction); registerAction2(HideWebViewEditorFindCommand); registerAction2(WebViewEditorFindNextCommand); registerAction2(WebViewEditorFindPreviousCommand); -registerAction2(SelectAllWebviewEditorCommand); registerAction2(ReloadWebviewAction); -function getActiveElectronBasedWebview(accessor: ServicesAccessor): Webview | undefined { - const webview = getActiveWebview(accessor); +function getInnerActiveWebview(accessor: ServicesAccessor): Webview | undefined { + const webview = getActiveWebviewEditor(accessor); if (!webview) { return undefined; } @@ -63,8 +62,8 @@ const PRIORITY = 100; function overrideCommandForWebview(command: MultiCommand | undefined, f: (webview: Webview) => void) { command?.addImplementation(PRIORITY, accessor => { - const webview = getActiveElectronBasedWebview(accessor); - if (webview) { + const webview = getInnerActiveWebview(accessor); + if (webview && webview.isFocused) { f(webview); return true; } diff --git a/src/vs/workbench/contrib/webview/browser/webviewCommands.ts b/src/vs/workbench/contrib/webview/browser/webviewCommands.ts index f05f4e5612..a4dcddd8da 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewCommands.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewCommands.ts @@ -7,7 +7,6 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import * as nls from 'vs/nls'; import { Action2, MenuId } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_FOCUSED, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE, Webview, webviewDeveloperCategory } from 'vs/workbench/contrib/webview/browser/webview'; @@ -34,7 +33,7 @@ export class ShowWebViewEditorFindWidgetAction extends Action2 { } public run(accessor: ServicesAccessor): void { - getActiveWebview(accessor)?.showFind(); + getActiveWebviewEditor(accessor)?.showFind(); } } @@ -55,7 +54,7 @@ export class HideWebViewEditorFindCommand extends Action2 { } public run(accessor: ServicesAccessor): void { - getActiveWebview(accessor)?.hideFind(); + getActiveWebviewEditor(accessor)?.hideFind(); } } @@ -76,7 +75,7 @@ export class WebViewEditorFindNextCommand extends Action2 { } public run(accessor: ServicesAccessor): void { - getActiveWebview(accessor)?.runFindAction(false); + getActiveWebviewEditor(accessor)?.runFindAction(false); } } @@ -97,29 +96,7 @@ export class WebViewEditorFindPreviousCommand extends Action2 { } public run(accessor: ServicesAccessor): void { - getActiveWebview(accessor)?.runFindAction(true); - } -} - -export class SelectAllWebviewEditorCommand extends Action2 { - public static readonly ID = 'editor.action.webvieweditor.selectAll'; - public static readonly LABEL = nls.localize('editor.action.webvieweditor.selectAll', 'Select all'); - - constructor() { - const precondition = ContextKeyExpr.and(webviewActiveContextKeyExpr, ContextKeyExpr.not(InputFocusedContextKey)); - super({ - id: SelectAllWebviewEditorCommand.ID, - title: SelectAllWebviewEditorCommand.LABEL, - keybinding: { - when: precondition, - primary: KeyMod.CtrlCmd | KeyCode.KEY_A, - weight: KeybindingWeight.EditorContrib - } - }); - } - - public run(accessor: ServicesAccessor): void { - getActiveWebview(accessor)?.selectAll(); + getActiveWebviewEditor(accessor)?.runFindAction(true); } } @@ -148,7 +125,7 @@ export class ReloadWebviewAction extends Action2 { } } -export function getActiveWebview(accessor: ServicesAccessor): Webview | undefined { +export function getActiveWebviewEditor(accessor: ServicesAccessor): Webview | undefined { const editorService = accessor.get(IEditorService); const activeEditor = editorService.activeEditor; return activeEditor instanceof WebviewInput ? activeEditor.webview : undefined; diff --git a/src/vs/workbench/contrib/webview/browser/webviewEditor.ts b/src/vs/workbench/contrib/webview/browser/webviewEditor.ts index a41b9e2e67..0b2844e0c9 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewEditor.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewEditor.ts @@ -11,9 +11,9 @@ import { isWeb } from 'vs/base/common/platform'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { IEditorDropService } from 'vs/workbench/services/editor/browser/editorDropService'; -import { EditorInput, EditorOptions } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; import { WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview'; import { WebviewInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -21,7 +21,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; -export class WebviewEditor extends BaseEditor { +export class WebviewEditor extends EditorPane { public static readonly ID = 'WebviewEditor'; @@ -107,7 +107,7 @@ export class WebviewEditor extends BaseEditor { super.clearInput(); } - public async setInput(input: EditorInput, options: EditorOptions, token: CancellationToken): Promise { + public async setInput(input: EditorInput, options: EditorOptions, context: IEditorOpenContext, token: CancellationToken): Promise { if (input.matches(this.input)) { return; } @@ -117,7 +117,7 @@ export class WebviewEditor extends BaseEditor { this.webview.release(this); } - await super.setInput(input, options, token); + await super.setInput(input, options, context, token); await input.resolve(); if (token.isCancellationRequested) { diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index 0a1e2dfe4f..31030652d7 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -54,7 +54,7 @@ export class IFrameWebview extends BaseWebview implements Web this._register(this.on(WebviewMessageChannels.loadResource, (entry: any) => { const rawPath = entry.path; const normalizedPath = decodeURIComponent(rawPath); - const uri = URI.parse(normalizedPath.replace(/^\/(\w+)\/(.+)$/, (_, scheme, path) => scheme + ':/' + path)); + const uri = URI.parse(normalizedPath.replace(/^\/([\w\-]+)\/(.+)$/, (_, scheme, path) => scheme + ':/' + path)); this.loadResource(rawPath, uri); })); @@ -70,7 +70,7 @@ export class IFrameWebview extends BaseWebview implements Web // Wait the end of the ctor when all listeners have been hooked up. const element = document.createElement('iframe'); element.className = `webview ${options.customClasses || ''}`; - element.sandbox.add('allow-scripts', 'allow-same-origin', 'allow-forms'); + element.sandbox.add('allow-scripts', 'allow-same-origin', 'allow-forms', 'allow-pointer-lock'); element.style.border = 'none'; element.style.width = '100%'; element.style.height = '100%'; diff --git a/src/vs/workbench/contrib/webview/electron-browser/iframeWebviewElement.ts b/src/vs/workbench/contrib/webview/electron-browser/iframeWebviewElement.ts index c32b1e2201..93d7c4a7d3 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/iframeWebviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/iframeWebviewElement.ts @@ -52,7 +52,7 @@ export class ElectronIframeWebview extends IFrameWebview { super(id, options, contentOptions, extension, webviewThemeDataProvider, noficationService, tunnelService, fileService, requestService, telemetryService, environmentService, _workbenchEnvironmentService, _remoteAuthorityResolverService, logService); - this._resourceRequestManager = this._register(instantiationService.createInstance(WebviewResourceRequestManager, id, extension, this.content.options, Promise.resolve(undefined))); + this._resourceRequestManager = this._register(instantiationService.createInstance(WebviewResourceRequestManager, id, extension, this.content.options)); } protected createElement(options: WebviewOptions, contentOptions: WebviewContentOptions) { diff --git a/src/vs/workbench/contrib/webview/electron-browser/resourceLoading.ts b/src/vs/workbench/contrib/webview/electron-browser/resourceLoading.ts index d8d45527e3..c8530905ec 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/resourceLoading.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/resourceLoading.ts @@ -58,7 +58,6 @@ export class WebviewResourceRequestManager extends Disposable { private readonly id: string, private readonly extension: WebviewExtensionDescription | undefined, initialContentOptions: WebviewContentOptions, - getWebContentsId: Promise, @ILogService private readonly _logService: ILogService, @IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, @@ -79,15 +78,13 @@ export class WebviewResourceRequestManager extends Disposable { const remoteAuthority = environmentService.configuration.remoteAuthority; const remoteConnectionData = remoteAuthority ? remoteAuthorityResolverService.getConnectionData(remoteAuthority) : null; - this._ready = getWebContentsId.then(async (webContentsId) => { - this._logService.debug(`WebviewResourceRequestManager(${this.id}): did-start-loading`); - await this._webviewManagerService.registerWebview(this.id, webContentsId, electronService.windowId, { - extensionLocation: this.extension?.location.toJSON(), - localResourceRoots: this._localResourceRoots.map(x => x.toJSON()), - remoteConnectionData: remoteConnectionData, - portMappings: this._portMappings, - }); - + this._logService.debug(`WebviewResourceRequestManager(${this.id}): did-start-loading`); + this._ready = this._webviewManagerService.registerWebview(this.id, electronService.windowId, { + extensionLocation: this.extension?.location.toJSON(), + localResourceRoots: this._localResourceRoots.map(x => x.toJSON()), + remoteConnectionData: remoteConnectionData, + portMappings: this._portMappings, + }).then(() => { this._logService.debug(`WebviewResourceRequestManager(${this.id}): did register`); }); diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts index 2b229dcf44..a2b38e09a7 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts @@ -142,17 +142,7 @@ export class ElectronWebviewBasedWebview extends BaseWebview impleme this._myLogService.debug(`Webview(${this.id}): init`); - const webviewId = new Promise((resolve, reject) => { - const sub = this._register(addDisposableListener(this.element!, 'dom-ready', once(() => { - if (!this.element) { - reject(); - throw new Error('No element'); - } - resolve(this.element.getWebContentsId()); - sub.dispose(); - }))); - }); - this._resourceRequestManager = this._register(instantiationService.createInstance(WebviewResourceRequestManager, id, extension, this.content.options, webviewId)); + this._resourceRequestManager = this._register(instantiationService.createInstance(WebviewResourceRequestManager, id, extension, this.content.options)); this._register(addDisposableListener(this.element!, 'dom-ready', once(() => { this._register(ElectronWebviewBasedWebview.getWebviewKeyboardHandler(configurationService, mainProcessService).add(this.element!)); diff --git a/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts b/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts index 0622bed554..14ef590b38 100644 --- a/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts +++ b/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts @@ -7,7 +7,6 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; import { toDisposable } from 'vs/base/common/lifecycle'; import { setImmediate } from 'vs/base/common/platform'; -import { generateUuid } from 'vs/base/common/uuid'; import { MenuId } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; @@ -16,18 +15,21 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IProgressService } from 'vs/platform/progress/common/progress'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; +import { Memento, MementoObject } from 'vs/workbench/common/memento'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IWebviewService, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview'; import { IWebviewViewService } from 'vs/workbench/contrib/webviewView/browser/webviewViewService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; - declare const ResizeObserver: any; +const webviewStateKey = 'webviewState'; + export class WebviewViewPane extends ViewPane { private _webview?: WebviewOverlay; @@ -36,6 +38,9 @@ export class WebviewViewPane extends ViewPane { private _container?: HTMLElement; private _resizeObserver?: any; + private readonly memento: Memento; + private readonly viewState: MementoObject; + constructor( options: IViewletViewOptions, @IKeybindingService keybindingService: IKeybindingService, @@ -47,6 +52,7 @@ export class WebviewViewPane extends ViewPane { @IOpenerService openerService: IOpenerService, @IThemeService themeService: IThemeService, @ITelemetryService telemetryService: ITelemetryService, + @IStorageService storageService: IStorageService, @IExtensionService private readonly extensionService: IExtensionService, @IProgressService private readonly progressService: IProgressService, @IWebviewService private readonly webviewService: IWebviewService, @@ -54,6 +60,9 @@ export class WebviewViewPane extends ViewPane { ) { super({ ...options, titleMenuId: MenuId.ViewTitle }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + this.memento = new Memento(`webviewView.${this.id}`, storageService); + this.viewState = this.memento.getMemento(StorageScope.WORKSPACE); + this._register(this.onDidChangeBodyVisibility(() => this.updateTreeVisibility())); this.updateTreeVisibility(); } @@ -96,6 +105,15 @@ export class WebviewViewPane extends ViewPane { } } + public saveState() { + if (this._webview) { + this.viewState[webviewStateKey] = this._webview.state; + } + + this.memento.saveMemento(); + super.saveState(); + } + protected layoutBody(height: number, width: number): void { super.layoutBody(height, width); @@ -121,13 +139,19 @@ export class WebviewViewPane extends ViewPane { if (!this._activated) { this._activated = true; - const webview = this.webviewService.createWebviewOverlay(generateUuid(), {}, {}, undefined); + const webviewId = `webviewView-${this.id.replace(/[^a-z0-9]/gi, '-')}`.toLowerCase(); + const webview = this.webviewService.createWebviewOverlay(webviewId, {}, {}, undefined); + webview.state = this.viewState['webviewState']; this._webview = webview; this._register(toDisposable(() => { this._webview?.release(this); })); + this._register(webview.onDidUpdateState(() => { + this.viewState[webviewStateKey] = webview.state; + })); + const source = this._register(new CancellationTokenSource()); this.withProgress(async () => { @@ -149,5 +173,3 @@ export class WebviewViewPane extends ViewPane { return this.progressService.withProgress({ location: this.id, delay: 500 }, task); } } - - diff --git a/src/vs/workbench/contrib/welcome/overlay/browser/welcomeOverlay.ts b/src/vs/workbench/contrib/welcome/overlay/browser/welcomeOverlay.ts index 2adf5fbe3d..529f4780f3 100644 --- a/src/vs/workbench/contrib/welcome/overlay/browser/welcomeOverlay.ts +++ b/src/vs/workbench/contrib/welcome/overlay/browser/welcomeOverlay.ts @@ -38,31 +38,31 @@ interface Key { const keys: Key[] = [ { id: 'explorer', - arrow: '←', + arrow: '\u2190', // ← label: localize('welcomeOverlay.explorer', "File explorer"), command: 'workbench.view.explorer' }, { id: 'search', - arrow: '←', + arrow: '\u2190', // ← label: localize('welcomeOverlay.search', "Search across files"), command: 'workbench.view.search' }, { id: 'git', - arrow: '←', + arrow: '\u2190', // ← label: localize('welcomeOverlay.git', "Source code management"), command: 'workbench.view.scm' }, { id: 'debug', - arrow: '←', + arrow: '\u2190', // ← label: localize('welcomeOverlay.debug', "Launch and debug"), command: 'workbench.view.debug' }, { id: 'extensions', - arrow: '←', + arrow: '\u2190', // ← label: localize('welcomeOverlay.extensions', "Manage extensions"), command: 'workbench.view.extensions' }, @@ -74,7 +74,7 @@ const keys: Key[] = [ // }, { id: 'problems', - arrow: '⤹', + arrow: '\u2939', // ⤹ label: localize('welcomeOverlay.problems', "View errors and warnings"), command: 'workbench.actions.view.problems' }, @@ -92,13 +92,13 @@ const keys: Key[] = [ // }, { id: 'commandPalette', - arrow: '↖', + arrow: '\u2196', // ↖ label: localize('welcomeOverlay.commandPalette', "Find and run all commands"), command: ShowAllCommandsAction.ID }, { id: 'notifications', - arrow: '⤵', + arrow: '\u2935', // ⤵ arrowLast: true, label: localize('welcomeOverlay.notifications', "Show notifications"), command: 'notifications.showList' @@ -186,7 +186,7 @@ class WelcomeOverlay extends Disposable { .forEach(({ id, arrow, label, command, arrowLast }) => { const div = dom.append(this._overlay, $(`.key.${id}`)); if (arrow && !arrowLast) { - dom.append(div, $('span.arrow')).innerHTML = arrow; + dom.append(div, $('span.arrow', undefined, arrow)); } dom.append(div, $('span.label')).textContent = label; if (command) { @@ -196,7 +196,7 @@ class WelcomeOverlay extends Disposable { } } if (arrow && arrowLast) { - dom.append(div, $('span.arrow')).innerHTML = arrow; + dom.append(div, $('span.arrow', undefined, arrow)); } }); } diff --git a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts index ded0e230a7..cecddfc29b 100644 --- a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts +++ b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts @@ -7,7 +7,7 @@ import 'vs/css!./welcomePage'; import 'vs/workbench/contrib/welcome/page/browser/vs_code_welcome_page'; import { URI } from 'vs/base/common/uri'; import * as strings from 'vs/base/common/strings'; -import { ICommandService } from 'vs/platform/commands/common/commands'; +import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import * as arrays from 'vs/base/common/arrays'; import { WalkThroughInput } from 'vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -31,7 +31,7 @@ import { splitName } from 'vs/base/common/labels'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { registerColor, focusBorder, textLinkForeground, textLinkActiveForeground, foreground, descriptionForeground, contrastBorder, activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { getExtraColor } from 'vs/workbench/contrib/welcome/walkThrough/common/walkThroughUtils'; -import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IExtensionsViewPaneContainer, IExtensionsWorkbenchService, VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { IEditorInputFactory, EditorInput } from 'vs/workbench/common/editor'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { TimeoutTimer } from 'vs/base/common/async'; @@ -47,6 +47,7 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IProductService } from 'vs/platform/product/common/productService'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; +import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; const configurationKey = 'workbench.startupEditor'; const oldConfigurationKey = 'workbench.welcome.enabled'; @@ -223,6 +224,16 @@ const extensionPackStrings: Strings = { extensionNotFound: localize('welcomePage.extensionPackNotFound', "Support for {0} with id {1} could not be found."), }; +CommandsRegistry.registerCommand('workbench.extensions.action.showAzureExtensions', accessor => { + const viewletService = accessor.get(IViewletService); + return viewletService.openViewlet(VIEWLET_ID, true) + .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) + .then(viewlet => { + viewlet.search('@sort:installs azure '); + viewlet.focus(); + }); +}); + /* __GDPR__ "installKeymap" : { "${include}": [ diff --git a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts index 9339c661a4..837cfbaf3b 100644 --- a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts +++ b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts @@ -10,8 +10,8 @@ import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import * as strings from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { IDisposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { EditorOptions, IEditorMemento } from 'vs/workbench/common/editor'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorOptions, IEditorMemento, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { WalkThroughInput } from 'vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput'; import { IOpenerService } from 'vs/platform/opener/common/opener'; @@ -55,7 +55,7 @@ interface IWalkThroughEditorViewState { viewState: IViewState; } -export class WalkThroughPart extends BaseEditor { +export class WalkThroughPart extends EditorPane { static readonly ID: string = 'workbench.editor.walkThroughPart'; @@ -262,7 +262,7 @@ export class WalkThroughPart extends BaseEditor { this.scrollbar.setScrollPosition({ scrollTop: scrollPosition.scrollTop + scrollDimensions.height }); } - setInput(input: WalkThroughInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + setInput(input: WalkThroughInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { if (this.input instanceof WalkThroughInput) { this.saveTextEditorViewState(this.input); } @@ -270,7 +270,7 @@ export class WalkThroughPart extends BaseEditor { this.contentDisposables = dispose(this.contentDisposables); this.content.innerText = ''; - return super.setInput(input, options, token) + return super.setInput(input, options, context, token) .then(() => { return input.resolve(); }) diff --git a/src/vs/workbench/electron-browser/desktop.main.ts b/src/vs/workbench/electron-browser/desktop.main.ts index 5ac8978fdb..e2f73bbc60 100644 --- a/src/vs/workbench/electron-browser/desktop.main.ts +++ b/src/vs/workbench/electron-browser/desktop.main.ts @@ -17,7 +17,6 @@ import { WorkspaceService } from 'vs/workbench/services/configuration/browser/co import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { KeyboardMapperFactory } from 'vs/workbench/services/keybinding/electron-browser/nativeKeymapService'; import { INativeWindowConfiguration } from 'vs/platform/windows/node/window'; import { ISingleFolderWorkspaceIdentifier, IWorkspaceInitializationPayload, ISingleFolderWorkspaceInitializationPayload, reviveWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { ILogService } from 'vs/platform/log/common/log'; @@ -48,7 +47,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import product from 'vs/platform/product/common/product'; import { NativeResourceIdentityService } from 'vs/platform/resource/node/resourceIdentityServiceImpl'; import { IResourceIdentityService } from 'vs/platform/resource/common/resourceIdentityService'; -import { DesktopLogService } from 'vs/workbench/services/log/electron-browser/logService'; +import { NativeLogService } from 'vs/workbench/services/log/electron-browser/logService'; import { IElectronService, ElectronService } from 'vs/platform/electron/electron-sandbox/electron'; class DesktopMain extends Disposable { @@ -77,9 +76,6 @@ class DesktopMain extends Disposable { setZoomFactor(zoomLevelToZoomFactor(zoomLevel)); setZoomLevel(zoomLevel, true /* isTrusted */); setFullscreen(!!this.environmentService.configuration.fullscreen); - - // Keyboard support - KeyboardMapperFactory.INSTANCE._onKeyboardLayoutChanged(); } private reviveUris() { @@ -183,7 +179,7 @@ class DesktopMain extends Disposable { serviceCollection.set(IProductService, productService); // Log - const logService = this._register(new DesktopLogService(this.configuration.windowId, mainProcessService, this.environmentService)); + const logService = this._register(new NativeLogService(this.configuration.windowId, mainProcessService, this.environmentService)); serviceCollection.set(ILogService, logService); // Remote diff --git a/src/vs/workbench/electron-browser/window.ts b/src/vs/workbench/electron-browser/window.ts index 0c616bbb92..e14bcb4325 100644 --- a/src/vs/workbench/electron-browser/window.ts +++ b/src/vs/workbench/electron-browser/window.ts @@ -13,15 +13,13 @@ import { IFileService } from 'vs/platform/files/common/files'; import { toResource, IUntitledTextResourceEditorInput, SideBySideEditor, pathsToEditors } from 'vs/workbench/common/editor'; import { IEditorService, IResourceEditorInputType } from 'vs/workbench/services/editor/common/editorService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { IWindowSettings, IOpenFileRequest, IWindowsConfiguration, getTitleBarStyle, IAddFoldersRequest } from 'vs/platform/windows/common/windows'; -import { IRunActionInWindowRequest, IRunKeybindingInWindowRequest, INativeOpenFileRequest } from 'vs/platform/windows/node/window'; +import { IOpenFileRequest, IWindowsConfiguration, getTitleBarStyle, IAddFoldersRequest, INativeRunActionInWindowRequest, INativeRunKeybindingInWindowRequest, INativeOpenFileRequest } from 'vs/platform/windows/common/windows'; import { ITitleService } from 'vs/workbench/services/title/common/titleService'; -import { IWorkbenchThemeService, VS_HC_THEME } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { applyZoom } from 'vs/platform/windows/electron-sandbox/window'; import { setFullscreen, getZoomLevel } from 'vs/base/browser/browser'; import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; -import { KeyboardMapperFactory } from 'vs/workbench/services/keybinding/electron-browser/nativeKeymapService'; import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; import { IMenuService, MenuId, IMenu, MenuItemAction, ICommandAction, SubmenuItemAction, MenuRegistry } from 'vs/platform/actions/common/actions'; @@ -129,7 +127,7 @@ export class NativeWindow extends Disposable { }); // Support runAction event - ipcRenderer.on('vscode:runAction', async (event: unknown, request: IRunActionInWindowRequest) => { + ipcRenderer.on('vscode:runAction', async (event: unknown, request: INativeRunActionInWindowRequest) => { const args: unknown[] = request.args || []; // If we run an action from the touchbar, we fill in the currently active resource @@ -160,7 +158,7 @@ export class NativeWindow extends Disposable { }); // Support runKeybinding event - ipcRenderer.on('vscode:runKeybinding', (event: unknown, request: IRunKeybindingInWindowRequest) => { + ipcRenderer.on('vscode:runKeybinding', (event: unknown, request: INativeRunKeybindingInWindowRequest) => { if (document.activeElement) { this.keybindingService.dispatchByUserSettingsLabel(request.userSettingsLabel, document.activeElement); } @@ -202,24 +200,13 @@ export class NativeWindow extends Disposable { // High Contrast Events ipcRenderer.on('vscode:enterHighContrast', async () => { - const windowConfig = this.configurationService.getValue('window'); - if (windowConfig?.autoDetectHighContrast) { - await this.lifecycleService.when(LifecyclePhase.Ready); - this.themeService.setColorTheme(VS_HC_THEME, undefined); - } + await this.lifecycleService.when(LifecyclePhase.Ready); + this.themeService.setOSHighContrast(true); }); ipcRenderer.on('vscode:leaveHighContrast', async () => { - const windowConfig = this.configurationService.getValue('window'); - if (windowConfig?.autoDetectHighContrast) { - await this.lifecycleService.when(LifecyclePhase.Ready); - this.themeService.restoreColorTheme(); - } - }); - - // keyboard layout changed event - ipcRenderer.on('vscode:keyboardLayoutChanged', () => { - KeyboardMapperFactory.INSTANCE._onKeyboardLayoutChanged(); + await this.lifecycleService.when(LifecyclePhase.Ready); + this.themeService.setOSHighContrast(false); }); // accessibility support changed event @@ -617,7 +604,7 @@ class NativeMenubarControl extends MenubarControl { @IStorageService storageService: IStorageService, @INotificationService notificationService: INotificationService, @IPreferencesService preferencesService: IPreferencesService, - @IWorkbenchEnvironmentService protected readonly environmentService: INativeWorkbenchEnvironmentService, + @IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService, @IAccessibilityService accessibilityService: IAccessibilityService, @IMenubarService private readonly menubarService: IMenubarService, @IHostService hostService: IHostService, @@ -654,13 +641,13 @@ class NativeMenubarControl extends MenubarControl { (async () => { this.recentlyOpened = await this.workspacesService.getRecentlyOpened(); - this.doUpdateMenubar(true); + this.doUpdateMenubar(); })(); this.registerListeners(); } - protected doUpdateMenubar(firstTime: boolean): void { + protected doUpdateMenubar(): void { // Since the native menubar is shared between windows (main process) // only allow the focused window to update the menubar if (!this.hostService.hasFocus) { diff --git a/src/vs/workbench/electron-sandbox/desktop.contribution.ts b/src/vs/workbench/electron-sandbox/desktop.contribution.ts index 69ae27fdcd..67c26c3014 100644 --- a/src/vs/workbench/electron-sandbox/desktop.contribution.ts +++ b/src/vs/workbench/electron-sandbox/desktop.contribution.ts @@ -271,9 +271,9 @@ import { InstallVSIXAction } from 'vs/workbench/contrib/extensions/browser/exten 'window.autoDetectHighContrast': { 'type': 'boolean', 'default': true, - 'description': nls.localize('autoDetectHighContrast', "If enabled, will automatically change to high contrast theme if Windows is using a high contrast theme, and to dark theme when switching away from a Windows high contrast theme."), + 'description': nls.localize('autoDetectHighContrast', "If enabled, will automatically change to high contrast theme if the OS is using a high contrast theme, and to dark theme when switching away from a high contrast theme."), 'scope': ConfigurationScope.APPLICATION, - 'included': isWindows + 'included': isWindows || isMacintosh }, 'window.doubleClickIconToClose': { 'type': 'boolean', @@ -327,6 +327,31 @@ import { InstallVSIXAction } from 'vs/workbench/contrib/extensions/browser/exten } } }); + + // Keybinding + registry.registerConfiguration({ + 'id': 'keyboard', + 'order': 15, + 'type': 'object', + 'title': nls.localize('keyboardConfigurationTitle', "Keyboard"), + 'properties': { + 'keyboard.touchbar.enabled': { + 'type': 'boolean', + 'default': true, + 'description': nls.localize('touchbar.enabled', "Enables the macOS touchbar buttons on the keyboard if available."), + 'included': isMacintosh + }, + 'keyboard.touchbar.ignored': { + 'type': 'array', + 'items': { + 'type': 'string' + }, + 'default': [], + 'markdownDescription': nls.localize('touchbar.ignored', 'A set of identifiers for entries in the touchbar that should not show up (for example `workbench.action.navigateBack`.'), + 'included': isMacintosh + } + } + }); })(); // JSON Schemas diff --git a/src/vs/workbench/electron-sandbox/desktop.main.ts b/src/vs/workbench/electron-sandbox/desktop.main.ts new file mode 100644 index 0000000000..0ff3f44ebe --- /dev/null +++ b/src/vs/workbench/electron-sandbox/desktop.main.ts @@ -0,0 +1,201 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { zoomLevelToZoomFactor } from 'vs/platform/windows/common/windows'; +import { importEntries, mark } from 'vs/base/common/performance'; +import { Workbench } from 'vs/workbench/browser/workbench'; +import { setZoomLevel, setZoomFactor, setFullscreen } from 'vs/base/browser/browser'; +import { domContentLoaded, addDisposableListener, EventType, scheduleAtNextAnimationFrame } from 'vs/base/browser/dom'; +import { URI } from 'vs/base/common/uri'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { ILogService } from 'vs/platform/log/common/log'; +import { Schemas } from 'vs/base/common/network'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IMainProcessService, MainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; +import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { IFileService } from 'vs/platform/files/common/files'; +import { RemoteFileSystemProvider } from 'vs/workbench/services/remote/common/remoteAgentFileSystemChannel'; +import { ISignService } from 'vs/platform/sign/common/sign'; +import { FileUserDataProvider } from 'vs/workbench/services/userData/common/fileUserDataProvider'; +import { IProductService } from 'vs/platform/product/common/productService'; +import product from 'vs/platform/product/common/product'; +import { IResourceIdentityService } from 'vs/platform/resource/common/resourceIdentityService'; +import { IElectronService, ElectronService } from 'vs/platform/electron/electron-sandbox/electron'; +import { SimpleConfigurationService, simpleFileSystemProvider, SimpleLogService, SimpleRemoteAgentService, SimpleRemoteAuthorityResolverService, SimpleResourceIdentityService, SimpleSignService, SimpleStorageService, SimpleWorkspaceService } from 'vs/workbench/electron-sandbox/sandbox.simpleservices'; +import { BrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; + +class DesktopMain extends Disposable { + + private readonly environmentService = new BrowserWorkbenchEnvironmentService({ + logsPath: URI.file('logs-path'), + workspaceId: '' + }); + + constructor(private configuration: any /*INativeWindowConfiguration*/) { + super(); + + this.init(); + } + + private init(): void { + + // Setup perf + importEntries(this.configuration.perfEntries); + + // Browser config + const zoomLevel = this.configuration.zoomLevel || 0; + setZoomFactor(zoomLevelToZoomFactor(zoomLevel)); + setZoomLevel(zoomLevel, true /* isTrusted */); + setFullscreen(!!this.configuration.fullscreen); + } + + async open(): Promise { + const services = await this.initServices(); + + await domContentLoaded(); + mark('willStartWorkbench'); + + // Create Workbench + const workbench = new Workbench(document.body, services.serviceCollection, services.logService); + + // Listeners + this.registerListeners(workbench, services.storageService); + + // Startup + workbench.startup(); + + // Logging + services.logService.trace('workbench configuration', JSON.stringify(this.environmentService.configuration)); + } + + private registerListeners(workbench: Workbench, storageService: SimpleStorageService): void { + + // Layout + this._register(addDisposableListener(window, EventType.RESIZE, e => this.onWindowResize(e, true, workbench))); + + // Workbench Lifecycle + this._register(workbench.onShutdown(() => this.dispose())); + this._register(workbench.onWillShutdown(event => event.join(storageService.close()))); + } + + private onWindowResize(e: Event, retry: boolean, workbench: Workbench): void { + if (e.target === window) { + if (window.document && window.document.body && window.document.body.clientWidth === 0) { + // TODO@Ben this is an electron issue on macOS when simple fullscreen is enabled + // where for some reason the window clientWidth is reported as 0 when switching + // between simple fullscreen and normal screen. In that case we schedule the layout + // call at the next animation frame once, in the hope that the dimensions are + // proper then. + if (retry) { + scheduleAtNextAnimationFrame(() => this.onWindowResize(e, false, workbench)); + } + return; + } + + workbench.layout(); + } + } + + private async initServices(): Promise<{ serviceCollection: ServiceCollection, logService: ILogService, storageService: SimpleStorageService }> { + const serviceCollection = new ServiceCollection(); + + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // NOTE: DO NOT ADD ANY OTHER SERVICE INTO THE COLLECTION HERE. + // CONTRIBUTE IT VIA WORKBENCH.DESKTOP.MAIN.TS AND registerSingleton(). + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + // Main Process + const mainProcessService = this._register(new MainProcessService(this.configuration.windowId)); + serviceCollection.set(IMainProcessService, mainProcessService); + + // Environment + serviceCollection.set(IWorkbenchEnvironmentService, this.environmentService); + + // Product + const productService: IProductService = { _serviceBrand: undefined, ...product }; + serviceCollection.set(IProductService, productService); + + // Log + const logService = new SimpleLogService(); + serviceCollection.set(ILogService, logService); + + // Remote + const remoteAuthorityResolverService = new SimpleRemoteAuthorityResolverService(); + serviceCollection.set(IRemoteAuthorityResolverService, remoteAuthorityResolverService); + + // Sign + const signService = new SimpleSignService(); + serviceCollection.set(ISignService, signService); + + // Remote Agent + const remoteAgentService = new SimpleRemoteAgentService(); + serviceCollection.set(IRemoteAgentService, remoteAgentService); + + // Electron + const electronService = new ElectronService(this.configuration.windowId, mainProcessService) as IElectronService; + serviceCollection.set(IElectronService, electronService); + + // Files + const fileService = this._register(new FileService(logService)); + serviceCollection.set(IFileService, fileService); + + fileService.registerProvider(Schemas.file, simpleFileSystemProvider); + + // User Data Provider + fileService.registerProvider(Schemas.userData, new FileUserDataProvider(URI.file('user-home'), this.environmentService.backupHome, simpleFileSystemProvider, this.environmentService, logService)); + + const connection = remoteAgentService.getConnection(); + if (connection) { + const remoteFileSystemProvider = this._register(new RemoteFileSystemProvider(remoteAgentService)); + fileService.registerProvider(Schemas.vscodeRemote, remoteFileSystemProvider); + } + + const resourceIdentityService = new SimpleResourceIdentityService(); + serviceCollection.set(IResourceIdentityService, resourceIdentityService); + + const services = await Promise.all([ + this.createWorkspaceService().then(service => { + + // Workspace + serviceCollection.set(IWorkspaceContextService, service); + + // Configuration + serviceCollection.set(IConfigurationService, new SimpleConfigurationService()); + + return service; + }), + + this.createStorageService().then(service => { + + // Storage + serviceCollection.set(IStorageService, service); + + return service; + }) + ]); + + return { serviceCollection, logService, storageService: services[1] }; + } + + private async createWorkspaceService(): Promise { + return new SimpleWorkspaceService(); + } + + private async createStorageService(): Promise { + return new SimpleStorageService(); + } +} + +export function main(configuration: any /*INativeWindowConfiguration*/): Promise { + const workbench = new DesktopMain(configuration); + + return workbench.open(); +} diff --git a/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts b/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts new file mode 100644 index 0000000000..d18e5a74f5 --- /dev/null +++ b/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts @@ -0,0 +1,907 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* eslint-disable code-no-standalone-editor */ +/* eslint-disable code-import-patterns */ + +import { ConsoleLogService } from 'vs/platform/log/common/log'; +import { IResourceIdentityService } from 'vs/platform/resource/common/resourceIdentityService'; +import { ISignService } from 'vs/platform/sign/common/sign'; +import { hash } from 'vs/base/common/hash'; +import { URI } from 'vs/base/common/uri'; +import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; +import { IRemoteAuthorityResolverService, IRemoteConnectionData, ResolvedAuthority, ResolvedOptions, ResolverResult } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { Event } from 'vs/base/common/event'; +import { IRemoteAgentConnection, IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnostics'; +import { IAddressProvider, ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection'; +import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; +import { ITelemetryData, ITelemetryInfo, ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { BrowserSocketFactory } from 'vs/platform/remote/browser/browserSocketFactory'; +import { ExtensionIdentifier, ExtensionType, IExtension, IExtensionDescription, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; +import { SimpleConfigurationService as BaseSimpleConfigurationService } from 'vs/editor/standalone/browser/simpleServices'; +import { InMemoryStorageService } from 'vs/platform/storage/common/storage'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IBackupFileService, IResolvedBackup } from 'vs/workbench/services/backup/common/backup'; +import { ITextSnapshot } from 'vs/editor/common/model'; +import { IExtensionService, NullExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { ClassifiedEvent, GDPRClassification, StrictPropertyChecker } from 'vs/platform/telemetry/common/gdprTypings'; +import { IKeyboardLayoutInfo, IKeymapService, ILinuxKeyboardLayoutInfo, ILinuxKeyboardMapping, IMacKeyboardLayoutInfo, IMacKeyboardMapping, IWindowsKeyboardLayoutInfo, IWindowsKeyboardMapping } from 'vs/workbench/services/keybinding/common/keymapInfo'; +import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding'; +import { DispatchConfig } from 'vs/workbench/services/keybinding/common/dispatchConfig'; +import { IKeyboardMapper } from 'vs/workbench/services/keybinding/common/keyboardMapper'; +import { ChordKeybinding, ResolvedKeybinding, SimpleKeybinding } from 'vs/base/common/keyCodes'; +import { ScanCodeBinding } from 'vs/base/common/scanCode'; +import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding'; +import { isWindows, OperatingSystem, OS } from 'vs/base/common/platform'; +import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { posix, win32 } from 'vs/base/common/path'; +import { IConfirmation, IConfirmationResult, IDialogOptions, IDialogService, IShowResult } from 'vs/platform/dialogs/common/dialogs'; +import Severity from 'vs/base/common/severity'; +import { IWebviewService, WebviewContentOptions, WebviewElement, WebviewExtensionDescription, WebviewIcons, WebviewOptions, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { AbstractTextFileService } from 'vs/workbench/services/textfile/browser/textFileService'; +import { EnablementState, ExtensionRecommendationReason, IExtensionManagementServer, IExtensionManagementServerService, IExtensionRecommendation } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { LanguageId, TokenizationRegistry } from 'vs/editor/common/modes'; +import { IGrammar, ITextMateService } from 'vs/workbench/services/textMate/common/textMateService'; +import { AccessibilitySupport, IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { ITunnelProvider, ITunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { IManualSyncTask, IResourcePreview, ISyncResourceHandle, ISyncTask, IUserDataAutoSyncService, IUserDataSyncService, IUserDataSyncStore, IUserDataSyncStoreManagementService, SyncResource, SyncStatus, UserDataSyncStoreType } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncAccount, IUserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount'; +import { AbstractTimerService, IStartupMetrics, ITimerService, Writeable } from 'vs/workbench/services/timer/browser/timerService'; +import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; +import { ISingleFolderWorkspaceIdentifier, IWorkspaceFolderCreationData, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { ITaskProvider, ITaskService, ITaskSummary, ProblemMatcherRunOptions, Task, TaskFilter, TaskTerminateResponse, WorkspaceFolderTaskResult } from 'vs/workbench/contrib/tasks/common/taskService'; +import { Action } from 'vs/base/common/actions'; +import { LinkedMap } from 'vs/base/common/map'; +import { IWorkspace, IWorkspaceContextService, IWorkspaceFolder, WorkbenchState, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { CustomTask, ContributedTask, InMemoryTask, TaskRunSource, ConfiguringTask, TaskIdentifier, TaskSorter } from 'vs/workbench/contrib/tasks/common/tasks'; +import { TaskSystemInfo } from 'vs/workbench/contrib/tasks/common/taskSystem'; +import { IExtensionManagementService, ILocalExtension, IGalleryExtension, IReportedExtension, IGalleryMetadata, IExtensionIdentifier, IExtensionTipsService, IConfigBasedExtensionTip, IExecutableBasedExtensionTip, IWorkspaceTips } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IWorkspaceTagsService, Tags } from 'vs/workbench/contrib/tags/common/workspaceTags'; +import { AsbtractOutputChannelModelService, IOutputChannelModelService } from 'vs/workbench/services/output/common/outputChannelModel'; +import { Color, RGBA } from 'vs/base/common/color'; +import { joinPath } from 'vs/base/common/resources'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; + +//#region Workspace + +export const workspaceResource = URI.file(isWindows ? '\\simpleWorkspace' : '/simpleWorkspace'); + +export class SimpleWorkspaceService implements IWorkspaceContextService { + + declare readonly _serviceBrand: undefined; + + readonly onDidChangeWorkspaceName = Event.None; + readonly onDidChangeWorkspaceFolders = Event.None; + readonly onDidChangeWorkbenchState = Event.None; + + private readonly workspace: IWorkspace; + + constructor() { + this.workspace = { id: '4064f6ec-cb38-4ad0-af64-ee6467e63c82', folders: [new WorkspaceFolder({ uri: workspaceResource, name: '', index: 0 })] }; + } + + async getCompleteWorkspace(): Promise { return this.getWorkspace(); } + + getWorkspace(): IWorkspace { return this.workspace; } + + getWorkbenchState(): WorkbenchState { + if (this.workspace) { + if (this.workspace.configuration) { + return WorkbenchState.WORKSPACE; + } + return WorkbenchState.FOLDER; + } + return WorkbenchState.EMPTY; + } + + getWorkspaceFolder(resource: URI): IWorkspaceFolder | null { return resource && resource.scheme === workspaceResource.scheme ? this.workspace.folders[0] : null; } + isInsideWorkspace(resource: URI): boolean { return resource && resource.scheme === workspaceResource.scheme; } + isCurrentWorkspace(workspaceIdentifier: ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier): boolean { return true; } +} + +//#endregion + + +//#region Configuration + +export class SimpleStorageService extends InMemoryStorageService { } + +//#endregion + + +//#region Configuration + +export class SimpleConfigurationService extends BaseSimpleConfigurationService { } + +//#endregion + + +//#region Logger + +export class SimpleLogService extends ConsoleLogService { } + +export class SimpleSignService implements ISignService { + + declare readonly _serviceBrand: undefined; + + async sign(value: string): Promise { return value; } +} + +//#endregion + + +//#region Files + +class SimpleFileSystemProvider extends InMemoryFileSystemProvider { } + +export const simpleFileSystemProvider = new SimpleFileSystemProvider(); + +function createFile(parent: string, name: string, content: string = ''): void { + simpleFileSystemProvider.writeFile(joinPath(workspaceResource, parent, name), VSBuffer.fromString(content).buffer, { create: true, overwrite: true }); +} + +function createFolder(name: string): void { + simpleFileSystemProvider.mkdir(joinPath(workspaceResource, name)); +} + +createFolder(''); +createFolder('src'); +createFolder('test'); + +createFile('', '.gitignore', `out +node_modules +.vscode-test/ +*.vsix +`); + +createFile('', '.vscodeignore', `.vscode/** +.vscode-test/** +out/test/** +src/** +.gitignore +vsc-extension-quickstart.md +**/tsconfig.json +**/tslint.json +**/*.map +**/*.ts`); + +createFile('', 'CHANGELOG.md', `# Change Log +All notable changes to the "test-ts" extension will be documented in this file. + +Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. + +## [Unreleased] +- Initial release`); +createFile('', 'package.json', `{ + "name": "test-ts", + "displayName": "test-ts", + "description": "", + "version": "0.0.1", + "engines": { + "vscode": "^1.31.0" + }, + "categories": [ + "Other" + ], + "activationEvents": [ + "onCommand:extension.helloWorld" + ], + "main": "./out/extension.js", + "contributes": { + "commands": [ + { + "command": "extension.helloWorld", + "title": "Hello World" + } + ] + }, + "scripts": { + "vscode:prepublish": "npm run compile", + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./", + "postinstall": "node ./node_modules/vscode/bin/install", + "test": "npm run compile && node ./node_modules/vscode/bin/test" + }, + "devDependencies": { + "typescript": "^3.3.1", + "vscode": "^1.1.28", + "tslint": "^5.12.1", + "@types/node": "^8.10.25", + "@types/mocha": "^2.2.42" + } +} +`); + +createFile('', 'tsconfig.json', `{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "outDir": "out", + "lib": [ + "es6" + ], + "sourceMap": true, + "rootDir": "src", + "strict": true /* enable all strict type-checking options */ + /* Additional Checks */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + }, + "exclude": [ + "node_modules", + ".vscode-test" + ] +} +`); + +createFile('', 'tslint.json', `{ + "rules": { + "no-string-throw": true, + "no-unused-expression": true, + "no-duplicate-variable": true, + "curly": true, + "class-name": true, + "semicolon": [ + true, + "always" + ], + "triple-equals": true + }, + "defaultSeverity": "warning" +} +`); + +createFile('src', 'extension.ts', `// The module 'vscode' contains the VS Code extensibility API +// Import the module and reference it with the alias vscode in your code below +import * as vscode from 'vscode'; + +// this method is called when your extension is activated +// your extension is activated the very first time the command is executed +export function activate(context: vscode.ExtensionContext) { + + // Use the console to output diagnostic information (console.log) and errors (console.error) + // This line of code will only be executed once when your extension is activated + console.log('Congratulations, your extension "test-ts" is now active!'); + + // The command has been defined in the package.json file + // Now provide the implementation of the command with registerCommand + // The commandId parameter must match the command field in package.json + let disposable = vscode.commands.registerCommand('extension.helloWorld', () => { + // The code you place here will be executed every time your command is executed + + // Display a message box to the user + vscode.window.showInformationMessage('Hello World!'); + }); + + context.subscriptions.push(disposable); +} + +// this method is called when your extension is deactivated +export function deactivate() {} +`); + +createFile('test', 'extension.test.ts', `// +// Note: This example test is leveraging the Mocha test framework. +// Please refer to their documentation on https://mochajs.org/ for help. +// + +// The module 'assert' provides assertion methods from node +import * as assert from 'assert'; + +// You can import and use all API from the 'vscode' module +// as well as import your extension to test it +// import * as vscode from 'vscode'; +// import * as myExtension from '../extension'; + +// Defines a Mocha test suite to group tests of similar kind together +suite("Extension Tests", function () { + + // Defines a Mocha unit test + test("Something 1", function() { + assert.equal(-1, [1, 2, 3].indexOf(5)); + assert.equal(-1, [1, 2, 3].indexOf(0)); + }); +});`); + +createFile('test', 'index.ts', `// +// PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING +// +// This file is providing the test runner to use when running extension tests. +// By default the test runner in use is Mocha based. +// +// You can provide your own test runner if you want to override it by exporting +// a function run(testRoot: string, clb: (error:Error) => void) that the extension +// host can call to run the tests. The test runner is expected to use console.log +// to report the results back to the caller. When the tests are finished, return +// a possible error to the callback or null if none. + +import * as testRunner from 'vscode/lib/testrunner'; + +// You can directly control Mocha options by configuring the test runner below +// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options +// for more info +testRunner.configure({ + ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) + useColors: true // colored output from test results +}); + +module.exports = testRunner;`); + +//#endregion + + +//#region Resource Identity + +export class SimpleResourceIdentityService implements IResourceIdentityService { + + declare readonly _serviceBrand: undefined; + + async resolveResourceIdentity(resource: URI): Promise { return hash(resource.toString()).toString(16); } +} + +//#endregion + + +//#region Remote + +export class SimpleRemoteAuthorityResolverService implements IRemoteAuthorityResolverService { + + declare readonly _serviceBrand: undefined; + + onDidChangeConnectionData: Event = Event.None; + resolveAuthority(authority: string): Promise { throw new Error('Method not implemented.'); } + getConnectionData(authority: string): IRemoteConnectionData | null { return null; } + _clearResolvedAuthority(authority: string): void { } + _setResolvedAuthority(resolvedAuthority: ResolvedAuthority, resolvedOptions?: ResolvedOptions): void { } + _setResolvedAuthorityError(authority: string, err: any): void { } + _setAuthorityConnectionToken(authority: string, connectionToken: string): void { } +} + +export class SimpleRemoteAgentService implements IRemoteAgentService { + + declare readonly _serviceBrand: undefined; + + socketFactory: ISocketFactory = new BrowserSocketFactory(null); + + getConnection(): IRemoteAgentConnection | null { return null; } + async getEnvironment(bail?: boolean): Promise { return null; } + async getDiagnosticInfo(options: IDiagnosticInfoOptions): Promise { return undefined; } + async disableTelemetry(): Promise { } + async logTelemetry(eventName: string, data?: ITelemetryData): Promise { } + async flushTelemetry(): Promise { } + async getRawEnvironment(): Promise { return null; } + async scanExtensions(skipExtensions?: ExtensionIdentifier[]): Promise { return []; } +} + +//#endregion + + +//#region Backup File + +class SimpleBackupFileService implements IBackupFileService { + + declare readonly _serviceBrand: undefined; + + async hasBackups(): Promise { return false; } + async discardResourceBackup(resource: URI): Promise { } + async discardAllWorkspaceBackups(): Promise { } + toBackupResource(resource: URI): URI { return resource; } + hasBackupSync(resource: URI, versionId?: number): boolean { return false; } + async getBackups(): Promise { return []; } + async resolve(resource: URI): Promise | undefined> { return undefined; } + async backup(resource: URI, content?: ITextSnapshot, versionId?: number, meta?: T): Promise { } + async discardBackup(resource: URI): Promise { } + async discardBackups(): Promise { } +} + +registerSingleton(IBackupFileService, SimpleBackupFileService); + +//#endregion + + +//#region Extensions + +class SimpleExtensionService extends NullExtensionService { } + +registerSingleton(IExtensionService, SimpleExtensionService); + +//#endregion + + +//#region Extensions Workbench (TODO@sandbox TODO@ben remove when 'semver-umd' can be loaded) + +class SimpleExtensionsWorkbenchService implements IExtensionsWorkbenchService { + + declare readonly _serviceBrand: undefined; + + onChange = Event.None; + + local = []; + installed = []; + outdated = []; + + queryGallery(...args: any[]): any { throw new Error('Method not implemented.'); } + install(...args: any[]): any { throw new Error('Method not implemented.'); } + queryLocal(server?: IExtensionManagementServer): Promise { throw new Error('Method not implemented.'); } + canInstall(extension: any): boolean { throw new Error('Method not implemented.'); } + uninstall(extension: any): Promise { throw new Error('Method not implemented.'); } + installVersion(extension: any, version: string): Promise { throw new Error('Method not implemented.'); } + reinstall(extension: any): Promise { throw new Error('Method not implemented.'); } + setEnablement(extensions: any | any[], enablementState: EnablementState): Promise { throw new Error('Method not implemented.'); } + open(extension: any, options?: { sideByside?: boolean | undefined; preserveFocus?: boolean | undefined; pinned?: boolean | undefined; }): Promise { throw new Error('Method not implemented.'); } + checkForUpdates(): Promise { throw new Error('Method not implemented.'); } + isExtensionIgnoredToSync(extension: any): boolean { throw new Error('Method not implemented.'); } + toggleExtensionIgnoredToSync(extension: any): Promise { throw new Error('Method not implemented.'); } +} + +registerSingleton(IExtensionsWorkbenchService, SimpleExtensionsWorkbenchService); + +//#endregion + +//#region Telemetry + +class SimpleTelemetryService implements ITelemetryService { + + declare readonly _serviceBrand: undefined; + + readonly sendErrorTelemetry = false; + readonly isOptedIn = false; + + async publicLog(eventName: string, data?: ITelemetryData, anonymizeFilePaths?: boolean): Promise { } + async publicLog2 = never, T extends GDPRClassification = never>(eventName: string, data?: StrictPropertyChecker, 'Type of classified event does not match event properties'>, anonymizeFilePaths?: boolean): Promise { } + async publicLogError(errorEventName: string, data?: ITelemetryData): Promise { } + async publicLogError2 = never, T extends GDPRClassification = never>(eventName: string, data?: StrictPropertyChecker, 'Type of classified event does not match event properties'>): Promise { } + setEnabled(value: boolean): void { } + setExperimentProperty(name: string, value: string): void { } + async getTelemetryInfo(): Promise { + return { + instanceId: 'someValue.instanceId', + sessionId: 'someValue.sessionId', + machineId: 'someValue.machineId' + }; + } +} + +registerSingleton(ITelemetryService, SimpleTelemetryService); + +//#endregion + + +//#region Keymap Service + +class SimpleKeyboardMapper implements IKeyboardMapper { + dumpDebugInfo(): string { return ''; } + resolveKeybinding(keybinding: ChordKeybinding): ResolvedKeybinding[] { return []; } + resolveKeyboardEvent(keyboardEvent: IKeyboardEvent): ResolvedKeybinding { + let keybinding = new SimpleKeybinding( + keyboardEvent.ctrlKey, + keyboardEvent.shiftKey, + keyboardEvent.altKey, + keyboardEvent.metaKey, + keyboardEvent.keyCode + ).toChord(); + return new USLayoutResolvedKeybinding(keybinding, OS); + } + resolveUserBinding(firstPart: (SimpleKeybinding | ScanCodeBinding)[]): ResolvedKeybinding[] { return []; } +} + +class SimpleKeymapService implements IKeymapService { + + declare readonly _serviceBrand: undefined; + + onDidChangeKeyboardMapper = Event.None; + getKeyboardMapper(dispatchConfig: DispatchConfig): IKeyboardMapper { return new SimpleKeyboardMapper(); } + getCurrentKeyboardLayout(): (IWindowsKeyboardLayoutInfo & { isUserKeyboardLayout?: boolean | undefined; isUSStandard?: true | undefined; }) | (ILinuxKeyboardLayoutInfo & { isUserKeyboardLayout?: boolean | undefined; isUSStandard?: true | undefined; }) | (IMacKeyboardLayoutInfo & { isUserKeyboardLayout?: boolean | undefined; isUSStandard?: true | undefined; }) | null { return null; } + getAllKeyboardLayouts(): IKeyboardLayoutInfo[] { return []; } + getRawKeyboardMapping(): IWindowsKeyboardMapping | ILinuxKeyboardMapping | IMacKeyboardMapping | null { return null; } + validateCurrentKeyboardMapping(keyboardEvent: IKeyboardEvent): void { } +} + +registerSingleton(IKeymapService, SimpleKeymapService); + +//#endregion + + +//#region Path + +class SimplePathService implements IPathService { + + declare readonly _serviceBrand: undefined; + + readonly resolvedUserHome = URI.file('user-home'); + readonly path = Promise.resolve(OS === OperatingSystem.Windows ? win32 : posix); + + async fileURI(path: string): Promise { return URI.file(path); } + async userHome(options?: { preferLocal: boolean; }): Promise { return this.resolvedUserHome; } +} + +registerSingleton(IPathService, SimplePathService); + +//#endregion + + +//#region Dialog + +class SimpleDialogService implements IDialogService { + + declare readonly _serviceBrand: undefined; + + async confirm(confirmation: IConfirmation): Promise { return { confirmed: false }; } + async show(severity: Severity, message: string, buttons: string[], options?: IDialogOptions): Promise { return { choice: 1 }; } + async about(): Promise { } +} + +registerSingleton(IDialogService, SimpleDialogService); + +//#endregion + + +//#region Webview + +class SimpleWebviewService implements IWebviewService { + declare readonly _serviceBrand: undefined; + + createWebviewElement(id: string, options: WebviewOptions, contentOptions: WebviewContentOptions, extension: WebviewExtensionDescription | undefined): WebviewElement { throw new Error('Method not implemented.'); } + createWebviewOverlay(id: string, options: WebviewOptions, contentOptions: WebviewContentOptions, extension: WebviewExtensionDescription | undefined): WebviewOverlay { throw new Error('Method not implemented.'); } + setIcons(id: string, value: WebviewIcons | undefined): void { } +} + +registerSingleton(IWebviewService, SimpleWebviewService); + +//#endregion + + +//#region Textfiles + +class SimpleTextFileService extends AbstractTextFileService { + declare readonly _serviceBrand: undefined; +} + +registerSingleton(ITextFileService, SimpleTextFileService); + +//#endregion + + +//#region extensions management + +class SimpleExtensionManagementServerService implements IExtensionManagementServerService { + + declare readonly _serviceBrand: undefined; + + readonly localExtensionManagementServer = null; + readonly remoteExtensionManagementServer = null; + readonly webExtensionManagementServer = null; + + getExtensionManagementServer(extension: IExtension): IExtensionManagementServer | null { return null; } +} + +registerSingleton(IExtensionManagementServerService, SimpleExtensionManagementServerService); + +//#endregion + + +//#region Textmate + +TokenizationRegistry.setColorMap([null!, new Color(new RGBA(212, 212, 212, 1)), new Color(new RGBA(30, 30, 30, 1))]); + +class SimpleTextMateService implements ITextMateService { + + declare readonly _serviceBrand: undefined; + + readonly onDidEncounterLanguage: Event = Event.None; + + async createGrammar(modeId: string): Promise { return null; } + startDebugMode(printFn: (str: string) => void, onStop: () => void): void { } +} + +registerSingleton(ITextMateService, SimpleTextMateService); + +//#endregion + + +//#region Accessibility + +class SimpleAccessibilityService implements IAccessibilityService { + + declare readonly _serviceBrand: undefined; + + onDidChangeScreenReaderOptimized = Event.None; + + isScreenReaderOptimized(): boolean { return false; } + async alwaysUnderlineAccessKeys(): Promise { return false; } + setAccessibilitySupport(accessibilitySupport: AccessibilitySupport): void { } + getAccessibilitySupport(): AccessibilitySupport { return AccessibilitySupport.Unknown; } +} + +registerSingleton(IAccessibilityService, SimpleAccessibilityService); + +//#endregion + + +//#region Tunnel + +class SimpleTunnelService implements ITunnelService { + + declare readonly _serviceBrand: undefined; + + tunnels: Promise = Promise.resolve([]); + + onTunnelOpened = Event.None; + onTunnelClosed = Event.None; + + openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number): Promise | undefined { return undefined; } + async closeTunnel(remoteHost: string, remotePort: number): Promise { } + setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable { return Disposable.None; } +} + +registerSingleton(ITunnelService, SimpleTunnelService); + +//#endregion + + +//#region User Data Sync + +class SimpleUserDataSyncService implements IUserDataSyncService { + + declare readonly _serviceBrand: undefined; + + onDidChangeStatus = Event.None; + onDidChangeConflicts = Event.None; + onDidChangeLocal = Event.None; + onSyncErrors = Event.None; + onDidChangeLastSyncTime = Event.None; + onDidResetRemote = Event.None; + onDidResetLocal = Event.None; + + status: SyncStatus = SyncStatus.Idle; + conflicts: [SyncResource, IResourcePreview[]][] = []; + lastSyncTime = undefined; + + createSyncTask(): Promise { throw new Error('Method not implemented.'); } + createManualSyncTask(): Promise { throw new Error('Method not implemented.'); } + + async replace(uri: URI): Promise { } + async reset(): Promise { } + async resetRemote(): Promise { } + async resetLocal(): Promise { } + async hasLocalData(): Promise { return false; } + async hasPreviouslySynced(): Promise { return false; } + async resolveContent(resource: URI): Promise { return null; } + async accept(resource: SyncResource, conflictResource: URI, content: string | null | undefined, apply: boolean): Promise { } + async getLocalSyncResourceHandles(resource: SyncResource): Promise { return []; } + async getRemoteSyncResourceHandles(resource: SyncResource): Promise { return []; } + async getAssociatedResources(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI; comparableResource: URI; }[]> { return []; } + async getMachineId(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise { return undefined; } +} + +registerSingleton(IUserDataSyncService, SimpleUserDataSyncService); + +//#endregion + + +//#region User Data Sync Account + +class SimpleUserDataSyncAccountService implements IUserDataSyncAccountService { + + declare readonly _serviceBrand: undefined; + + onTokenFailed = Event.None; + onDidChangeAccount = Event.None; + + account: IUserDataSyncAccount | undefined = undefined; + + async updateAccount(account: IUserDataSyncAccount | undefined): Promise { } +} + +registerSingleton(IUserDataSyncAccountService, SimpleUserDataSyncAccountService); + +//#endregion + + +//#region User Data Auto Sync Account + +class SimpleUserDataAutoSyncAccountService implements IUserDataAutoSyncService { + + declare readonly _serviceBrand: undefined; + + onError = Event.None; + onDidChangeEnablement = Event.None; + + isEnabled(): boolean { return false; } + canToggleEnablement(): boolean { return false; } + async turnOn(): Promise { } + async turnOff(everywhere: boolean): Promise { } + async triggerSync(sources: string[], hasToLimitSync: boolean, disableCache: boolean): Promise { } +} + +registerSingleton(IUserDataAutoSyncService, SimpleUserDataAutoSyncAccountService); + +//#endregion + + +//#region User Data Sync Store Management + +class SimpleIUserDataSyncStoreManagementService implements IUserDataSyncStoreManagementService { + + declare readonly _serviceBrand: undefined; + + userDataSyncStore: IUserDataSyncStore | undefined = undefined; + + async switch(type: UserDataSyncStoreType): Promise { } + + async getPreviousUserDataSyncStore(): Promise { return undefined; } +} + +registerSingleton(IUserDataSyncStoreManagementService, SimpleIUserDataSyncStoreManagementService); + +//#endregion + + +//#region Timer + +class SimpleTimerService extends AbstractTimerService { + protected _isInitialStartup(): boolean { return true; } + protected _didUseCachedData(): boolean { return false; } + protected async _getWindowCount(): Promise { return 1; } + protected async _extendStartupInfo(info: Writeable): Promise { } +} + +registerSingleton(ITimerService, SimpleTimerService); + +//#endregion + + +//#region Workspace Editing + +class SimpleWorkspaceEditingService implements IWorkspaceEditingService { + + declare readonly _serviceBrand: undefined; + + async addFolders(folders: IWorkspaceFolderCreationData[], donotNotifyError?: boolean): Promise { } + async removeFolders(folders: URI[], donotNotifyError?: boolean): Promise { } + async updateFolders(index: number, deleteCount?: number, foldersToAdd?: IWorkspaceFolderCreationData[], donotNotifyError?: boolean): Promise { } + async enterWorkspace(path: URI): Promise { } + async createAndEnterWorkspace(folders: IWorkspaceFolderCreationData[], path?: URI): Promise { } + async saveAndEnterWorkspace(path: URI): Promise { } + async copyWorkspaceSettings(toWorkspace: IWorkspaceIdentifier): Promise { } + async pickNewWorkspacePath(): Promise { return undefined!; } +} + +registerSingleton(IWorkspaceEditingService, SimpleWorkspaceEditingService); + +//#endregion + + +//#region Task + +class SimpleTaskService implements ITaskService { + + declare readonly _serviceBrand: undefined; + + onDidStateChange = Event.None; + supportsMultipleTaskExecutions = false; + + configureAction(): Action { throw new Error('Method not implemented.'); } + build(): Promise { throw new Error('Method not implemented.'); } + runTest(): Promise { throw new Error('Method not implemented.'); } + run(task: CustomTask | ContributedTask | InMemoryTask | undefined, options?: ProblemMatcherRunOptions): Promise { throw new Error('Method not implemented.'); } + inTerminal(): boolean { throw new Error('Method not implemented.'); } + isActive(): Promise { throw new Error('Method not implemented.'); } + getActiveTasks(): Promise { throw new Error('Method not implemented.'); } + getBusyTasks(): Promise { throw new Error('Method not implemented.'); } + restart(task: Task): void { throw new Error('Method not implemented.'); } + terminate(task: Task): Promise { throw new Error('Method not implemented.'); } + terminateAll(): Promise { throw new Error('Method not implemented.'); } + tasks(filter?: TaskFilter): Promise { throw new Error('Method not implemented.'); } + taskTypes(): string[] { throw new Error('Method not implemented.'); } + getWorkspaceTasks(runSource?: TaskRunSource): Promise> { throw new Error('Method not implemented.'); } + readRecentTasks(): Promise<(CustomTask | ContributedTask | InMemoryTask | ConfiguringTask)[]> { throw new Error('Method not implemented.'); } + getTask(workspaceFolder: string | IWorkspace | IWorkspaceFolder, alias: string | TaskIdentifier, compareId?: boolean): Promise { throw new Error('Method not implemented.'); } + tryResolveTask(configuringTask: ConfiguringTask): Promise { throw new Error('Method not implemented.'); } + getTasksForGroup(group: string): Promise { throw new Error('Method not implemented.'); } + getRecentlyUsedTasks(): LinkedMap { throw new Error('Method not implemented.'); } + migrateRecentTasks(tasks: Task[]): Promise { throw new Error('Method not implemented.'); } + createSorter(): TaskSorter { throw new Error('Method not implemented.'); } + getTaskDescription(task: CustomTask | ContributedTask | InMemoryTask | ConfiguringTask): string | undefined { throw new Error('Method not implemented.'); } + canCustomize(task: CustomTask | ContributedTask): boolean { throw new Error('Method not implemented.'); } + customize(task: CustomTask | ContributedTask | ConfiguringTask, properties?: {}, openConfig?: boolean): Promise { throw new Error('Method not implemented.'); } + openConfig(task: CustomTask | ConfiguringTask | undefined): Promise { throw new Error('Method not implemented.'); } + registerTaskProvider(taskProvider: ITaskProvider, type: string): IDisposable { throw new Error('Method not implemented.'); } + registerTaskSystem(scheme: string, taskSystemInfo: TaskSystemInfo): void { throw new Error('Method not implemented.'); } + registerSupportedExecutions(custom?: boolean, shell?: boolean, process?: boolean): void { throw new Error('Method not implemented.'); } + setJsonTasksSupported(areSuppored: Promise): void { throw new Error('Method not implemented.'); } + extensionCallbackTaskComplete(task: Task, result: number | undefined): Promise { throw new Error('Method not implemented.'); } +} + +registerSingleton(ITaskService, SimpleTaskService); + +//#endregion + + +//#region Extension Management + +class SimpleExtensionManagementService implements IExtensionManagementService { + + declare readonly _serviceBrand: undefined; + + onInstallExtension = Event.None; + onDidInstallExtension = Event.None; + onUninstallExtension = Event.None; + onDidUninstallExtension = Event.None; + + async zip(extension: ILocalExtension): Promise { throw new Error('Method not implemented.'); } + async unzip(zipLocation: URI): Promise { throw new Error('Method not implemented.'); } + async getManifest(vsix: URI): Promise { throw new Error('Method not implemented.'); } + async install(vsix: URI, isMachineScoped?: boolean): Promise { throw new Error('Method not implemented.'); } + async canInstall(extension: IGalleryExtension): Promise { throw new Error('Method not implemented.'); } + async installFromGallery(extension: IGalleryExtension, isMachineScoped?: boolean): Promise { throw new Error('Method not implemented.'); } + async uninstall(extension: ILocalExtension, force?: boolean): Promise { } + async reinstallFromGallery(extension: ILocalExtension): Promise { } + async getInstalled(type?: ExtensionType): Promise { return []; } + async getExtensionsReport(): Promise { return []; } + async updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise { throw new Error('Method not implemented.'); } +} + +registerSingleton(IExtensionManagementService, SimpleExtensionManagementService); + +//#endregion + + +//#region Extension Tips + +class SimpleExtensionTipsService implements IExtensionTipsService { + + declare readonly _serviceBrand: undefined; + + onRecommendationChange = Event.None; + + getAllRecommendationsWithReason(): { [id: string]: { reasonId: ExtensionRecommendationReason; reasonText: string; }; } { return Object.create(null); } + getFileBasedRecommendations(): IExtensionRecommendation[] { return []; } + async getOtherRecommendations(): Promise { return []; } + async getWorkspaceRecommendations(): Promise { return []; } + getKeymapRecommendations(): IExtensionRecommendation[] { return []; } + toggleIgnoredRecommendation(extensionId: string, shouldIgnore: boolean): void { } + getAllIgnoredRecommendations(): { global: string[]; workspace: string[]; } { return Object.create(null); } + async getConfigBasedTips(folder: URI): Promise { return []; } + async getImportantExecutableBasedTips(): Promise { return []; } + async getOtherExecutableBasedTips(): Promise { return []; } + async getAllWorkspacesTips(): Promise { return []; } +} + +registerSingleton(IExtensionTipsService, SimpleExtensionTipsService); + +//#endregion + + +//#region Workspace Tags + +class SimpleWorkspaceTagsService implements IWorkspaceTagsService { + + declare readonly _serviceBrand: undefined; + + async getTags(): Promise { return Object.create(null); } + getTelemetryWorkspaceId(workspace: IWorkspace, state: WorkbenchState): string | undefined { return undefined; } + async getHashedRemotesFromUri(workspaceUri: URI, stripEndingDotGit?: boolean): Promise { return []; } +} + +registerSingleton(IWorkspaceTagsService, SimpleWorkspaceTagsService); + +//#endregion + + +//#region Output Channel + +class SimpleOutputChannelModelService extends AsbtractOutputChannelModelService { + declare readonly _serviceBrand: undefined; +} + +registerSingleton(IOutputChannelModelService, SimpleOutputChannelModelService); + +//#endregion diff --git a/src/vs/workbench/services/accessibility/electron-browser/accessibilityService.ts b/src/vs/workbench/services/accessibility/electron-browser/accessibilityService.ts index 7e62ca11f1..dcb8fd696c 100644 --- a/src/vs/workbench/services/accessibility/electron-browser/accessibilityService.ts +++ b/src/vs/workbench/services/accessibility/electron-browser/accessibilityService.ts @@ -41,23 +41,21 @@ export class NativeAccessibilityService extends AccessibilityService implements this.setAccessibilitySupport(environmentService.configuration.accessibilitySupport ? AccessibilitySupport.Enabled : AccessibilitySupport.Disabled); } - alwaysUnderlineAccessKeys(): Promise { + async alwaysUnderlineAccessKeys(): Promise { if (!isWindows) { - return Promise.resolve(false); + return false; } - return new Promise(async (resolve) => { - const Registry = await import('vscode-windows-registry'); + const Registry = await import('vscode-windows-registry'); - let value; - try { - value = Registry.GetStringRegKey('HKEY_CURRENT_USER', 'Control Panel\\Accessibility\\Keyboard Preference', 'On'); - } catch { - resolve(false); - } + let value: string | undefined = undefined; + try { + value = Registry.GetStringRegKey('HKEY_CURRENT_USER', 'Control Panel\\Accessibility\\Keyboard Preference', 'On'); + } catch { + return false; + } - resolve(value === '1'); - }); + return value === '1'; } setAccessibilitySupport(accessibilitySupport: AccessibilitySupport): void { diff --git a/src/vs/workbench/services/authentication/browser/authenticationService.ts b/src/vs/workbench/services/authentication/browser/authenticationService.ts index 98584d7e4f..f6c986e573 100644 --- a/src/vs/workbench/services/authentication/browser/authenticationService.ts +++ b/src/vs/workbench/services/authentication/browser/authenticationService.ts @@ -18,6 +18,11 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IProductService } from 'vs/platform/product/common/productService'; import { isString } from 'vs/base/common/types'; +import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { flatten } from 'vs/base/common/arrays'; +import { isFalsyOrWhitespace } from 'vs/base/common/strings'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; export function getAuthenticationProviderActivationEvent(id: string): string { return `onAuthenticationRequest:${id}`; } @@ -55,6 +60,10 @@ export interface IAuthenticationService { readonly onDidUnregisterAuthenticationProvider: Event; readonly onDidChangeSessions: Event<{ providerId: string, label: string, event: AuthenticationSessionsChangeEvent }>; + + declaredProviders: AuthenticationProviderInformation[]; + readonly onDidChangeDeclaredProviders: Event; + getSessions(providerId: string): Promise>; getLabel(providerId: string): string; supportsMultipleAccounts(providerId: string): boolean; @@ -96,6 +105,30 @@ CommandsRegistry.registerCommand('workbench.getCodeExchangeProxyEndpoints', func return environmentService.options?.codeExchangeProxyEndpoints; }); +const authenticationDefinitionSchema: IJSONSchema = { + type: 'object', + additionalProperties: false, + properties: { + id: { + type: 'string', + description: nls.localize('authentication.id', 'The id of the authentication provider.') + }, + label: { + type: 'string', + description: nls.localize('authentication.label', 'The human readable name of the authentication provider.'), + } + } +}; + +const authenticationExtPoint = ExtensionsRegistry.registerExtensionPoint({ + extensionPoint: 'authentication', + jsonSchema: { + description: nls.localize('authenticationExtensionPoint', 'Contributes authentication'), + type: 'array', + items: authenticationDefinitionSchema + } +}); + export class AuthenticationService extends Disposable implements IAuthenticationService { declare readonly _serviceBrand: undefined; private _placeholderMenuItem: IDisposable | undefined; @@ -105,6 +138,11 @@ export class AuthenticationService extends Disposable implements IAuthentication private _authenticationProviders: Map = new Map(); + /** + * All providers that have been statically declared by extensions. These may not be registered. + */ + declaredProviders: AuthenticationProviderInformation[] = []; + private _onDidRegisterAuthenticationProvider: Emitter = this._register(new Emitter()); readonly onDidRegisterAuthenticationProvider: Event = this._onDidRegisterAuthenticationProvider.event; @@ -114,7 +152,13 @@ export class AuthenticationService extends Disposable implements IAuthentication private _onDidChangeSessions: Emitter<{ providerId: string, label: string, event: AuthenticationSessionsChangeEvent }> = this._register(new Emitter<{ providerId: string, label: string, event: AuthenticationSessionsChangeEvent }>()); readonly onDidChangeSessions: Event<{ providerId: string, label: string, event: AuthenticationSessionsChangeEvent }> = this._onDidChangeSessions.event; - constructor(@IActivityService private readonly activityService: IActivityService) { + private _onDidChangeDeclaredProviders: Emitter = this._register(new Emitter()); + readonly onDidChangeDeclaredProviders: Event = this._onDidChangeDeclaredProviders.event; + + constructor( + @IActivityService private readonly activityService: IActivityService, + @IExtensionService private readonly extensionService: IExtensionService + ) { super(); this._placeholderMenuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, { command: { @@ -123,6 +167,38 @@ export class AuthenticationService extends Disposable implements IAuthentication precondition: ContextKeyExpr.false() }, }); + + authenticationExtPoint.setHandler((extensions, { added, removed }) => { + added.forEach(point => { + for (const provider of point.value) { + if (isFalsyOrWhitespace(provider.id)) { + point.collector.error(nls.localize('authentication.missingId', 'An authentication contribution must specify an id.')); + continue; + } + + if (isFalsyOrWhitespace(provider.label)) { + point.collector.error(nls.localize('authentication.missingLabel', 'An authentication contribution must specify a label.')); + continue; + } + + if (!this.declaredProviders.some(p => p.id === provider.id)) { + this.declaredProviders.push(provider); + } else { + point.collector.error(nls.localize('authentication.idConflict', "This authentication id '{0}' has already been registered", provider.id)); + } + } + }); + + const removedExtPoints = flatten(removed.map(r => r.value)); + removedExtPoints.forEach(point => { + const index = this.declaredProviders.findIndex(provider => provider.id === point.id); + if (index > -1) { + this.declaredProviders.splice(index, 1); + } + }); + + this._onDidChangeDeclaredProviders.fire(this.declaredProviders); + }); } getProviderIds(): string[] { @@ -339,11 +415,11 @@ export class AuthenticationService extends Disposable implements IAuthentication } } getLabel(id: string): string { - const authProvider = this._authenticationProviders.get(id); + const authProvider = this.declaredProviders.find(provider => provider.id === id); if (authProvider) { return authProvider.label; } else { - throw new Error(`No authentication provider '${id}' is currently registered.`); + throw new Error(`No authentication provider '${id}' has been declared.`); } } @@ -356,20 +432,46 @@ export class AuthenticationService extends Disposable implements IAuthentication } } + private async tryActivateProvider(providerId: string): Promise { + await this.extensionService.activateByEvent(getAuthenticationProviderActivationEvent(providerId)); + let provider = this._authenticationProviders.get(providerId); + if (provider) { + return provider; + } + + // When activate has completed, the extension has made the call to `registerAuthenticationProvider`. + // However, activate cannot block on this, so the renderer may not have gotten the event yet. + const didRegister: Promise = new Promise((resolve, _) => { + this.onDidRegisterAuthenticationProvider(e => { + if (e.id === providerId) { + resolve(this._authenticationProviders.get(providerId)); + } + }); + }); + + const didTimeout: Promise = new Promise((_, reject) => { + setTimeout(() => { + reject(); + }, 2000); + }); + + return Promise.race([didRegister, didTimeout]); + } + async getSessions(id: string): Promise> { - const authProvider = this._authenticationProviders.get(id); - if (authProvider) { + try { + const authProvider = this._authenticationProviders.get(id) || await this.tryActivateProvider(id); return await authProvider.getSessions(); - } else { + } catch (_) { throw new Error(`No authentication provider '${id}' is currently registered.`); } } async login(id: string, scopes: string[]): Promise { - const authProvider = this._authenticationProviders.get(id); - if (authProvider) { - return authProvider.login(scopes); - } else { + try { + const authProvider = this._authenticationProviders.get(id) || await this.tryActivateProvider(id); + return await authProvider.login(scopes); + } catch (_) { throw new Error(`No authentication provider '${id}' is currently registered.`); } } diff --git a/src/vs/workbench/services/configuration/browser/configurationService.ts b/src/vs/workbench/services/configuration/browser/configurationService.ts index 3147f7e8c9..60512ea8a0 100644 --- a/src/vs/workbench/services/configuration/browser/configurationService.ts +++ b/src/vs/workbench/services/configuration/browser/configurationService.ts @@ -22,12 +22,14 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ConfigurationEditingService, EditableConfigurationTarget } from 'vs/workbench/services/configuration/common/configurationEditingService'; import { WorkspaceConfiguration, FolderConfiguration, RemoteUserConfiguration, UserConfiguration } from 'vs/workbench/services/configuration/browser/configuration'; import { JSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditingService'; -import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema'; import { isEqual, dirname } from 'vs/base/common/resources'; import { mark } from 'vs/base/common/performance'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IFileService } from 'vs/platform/files/common/files'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; export class WorkspaceService extends Disposable implements IConfigurationService, IWorkspaceContextService { @@ -73,6 +75,13 @@ export class WorkspaceService extends Disposable implements IConfigurationServic ) { super(); + const configurationRegistry = Registry.as(Extensions.Configuration); + // register defaults before creating default configuration model + // so that the model is not required to be updated after registering + if (environmentService.options?.configurationDefaults) { + configurationRegistry.registerDefaultConfigurations([environmentService.options.configurationDefaults]); + } + this.completeWorkspaceBarrier = new Barrier(); this.defaultConfiguration = new DefaultConfigurationModel(); this.configurationCache = configurationCache; @@ -94,11 +103,6 @@ export class WorkspaceService extends Disposable implements IConfigurationServic }); })); - const configurationRegistry = Registry.as(Extensions.Configuration); - if (environmentService.options?.configurationDefaults) { - configurationRegistry.registerDefaultConfigurations([environmentService.options.configurationDefaults]); - } - this._register(configurationRegistry.onDidSchemaChange(e => this.registerConfigurationSchemas())); this._register(configurationRegistry.onDidUpdateConfiguration(configurationProperties => this.onDefaultConfigurationChanged(configurationProperties))); this.workspaceEditingQueue = new Queue(); @@ -423,7 +427,6 @@ export class WorkspaceService extends Disposable implements IConfigurationServic } private initializeConfiguration(): Promise { - this.registerConfigurationSchemas(); return this.initializeUserConfiguration() .then(({ local, remote }) => this.loadConfiguration(local, remote)); } @@ -498,7 +501,6 @@ export class WorkspaceService extends Disposable implements IConfigurationServic private onDefaultConfigurationChanged(keys: string[]): void { this.defaultConfiguration = new DefaultConfigurationModel(); - this.registerConfigurationSchemas(); if (this.workspace) { const previousData = this._configuration.toData(); const change = this._configuration.compareAndUpdateDefaultConfiguration(this.defaultConfiguration, keys); @@ -525,30 +527,6 @@ export class WorkspaceService extends Disposable implements IConfigurationServic } } - private registerConfigurationSchemas(): void { - if (this.workspace) { - const jsonRegistry = Registry.as(JSONExtensions.JSONContribution); - const defaultSettingsSchema: IJSONSchema = { additionalProperties: true, allowTrailingCommas: true, allowComments: true }; - const allSettingsSchema: IJSONSchema = { properties: allSettings.properties, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true }; - const userSettingsSchema: IJSONSchema = this.remoteUserConfiguration ? { properties: { ...applicationSettings.properties, ...windowSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true } : allSettingsSchema; - const machineSettingsSchema: IJSONSchema = { properties: { ...machineSettings.properties, ...machineOverridableSettings.properties, ...windowSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true }; - const workspaceSettingsSchema: IJSONSchema = { properties: { ...machineOverridableSettings.properties, ...windowSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true }; - - jsonRegistry.registerSchema(defaultSettingsSchemaId, defaultSettingsSchema); - jsonRegistry.registerSchema(userSettingsSchemaId, userSettingsSchema); - jsonRegistry.registerSchema(machineSettingsSchemaId, machineSettingsSchema); - - if (WorkbenchState.WORKSPACE === this.getWorkbenchState()) { - const folderSettingsSchema: IJSONSchema = { properties: { ...machineOverridableSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true }; - jsonRegistry.registerSchema(workspaceSettingsSchemaId, workspaceSettingsSchema); - jsonRegistry.registerSchema(folderSettingsSchemaId, folderSettingsSchema); - } else { - jsonRegistry.registerSchema(workspaceSettingsSchemaId, workspaceSettingsSchema); - jsonRegistry.registerSchema(folderSettingsSchemaId, workspaceSettingsSchema); - } - } - } - private onLocalUserConfigurationChanged(userConfiguration: ConfigurationModel): void { const previous = { data: this._configuration.toData(), workspace: this.workspace }; const change = this._configuration.compareAndUpdateLocalUserConfiguration(userConfiguration); @@ -774,3 +752,45 @@ export class WorkspaceService extends Disposable implements IConfigurationServic return null; } } + +class RegisterConfigurationSchemasContribution extends Disposable implements IWorkbenchContribution { + constructor( + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService, + ) { + super(); + this.registerConfigurationSchemas(); + const configurationRegistry = Registry.as(Extensions.Configuration); + this._register(configurationRegistry.onDidUpdateConfiguration(e => this.registerConfigurationSchemas())); + this._register(configurationRegistry.onDidSchemaChange(e => this.registerConfigurationSchemas())); + } + + private registerConfigurationSchemas(): void { + const jsonRegistry = Registry.as(JSONExtensions.JSONContribution); + const allSettingsSchema: IJSONSchema = { properties: allSettings.properties, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true }; + const userSettingsSchema: IJSONSchema = this.workbenchEnvironmentService.configuration.remoteAuthority ? { properties: { ...applicationSettings.properties, ...windowSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true } : allSettingsSchema; + const machineSettingsSchema: IJSONSchema = { properties: { ...machineSettings.properties, ...machineOverridableSettings.properties, ...windowSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true }; + const workspaceSettingsSchema: IJSONSchema = { properties: { ...machineOverridableSettings.properties, ...windowSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true }; + + jsonRegistry.registerSchema(defaultSettingsSchemaId, { + properties: Object.keys(allSettings.properties).reduce((result, key) => { result[key] = { ...allSettings.properties[key], deprecationMessage: undefined }; return result; }, {}), + patternProperties: Object.keys(allSettings.patternProperties).reduce((result, key) => { result[key] = { ...allSettings.patternProperties[key], deprecationMessage: undefined }; return result; }, {}), + additionalProperties: true, + allowTrailingCommas: true, + allowComments: true + }); + jsonRegistry.registerSchema(userSettingsSchemaId, userSettingsSchema); + jsonRegistry.registerSchema(machineSettingsSchemaId, machineSettingsSchema); + + if (WorkbenchState.WORKSPACE === this.workspaceContextService.getWorkbenchState()) { + const folderSettingsSchema: IJSONSchema = { properties: { ...machineOverridableSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true }; + jsonRegistry.registerSchema(workspaceSettingsSchemaId, workspaceSettingsSchema); + jsonRegistry.registerSchema(folderSettingsSchemaId, folderSettingsSchema); + } else { + jsonRegistry.registerSchema(workspaceSettingsSchemaId, workspaceSettingsSchema); + jsonRegistry.registerSchema(folderSettingsSchemaId, workspaceSettingsSchema); + } + } +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(RegisterConfigurationSchemasContribution, LifecyclePhase.Restored); diff --git a/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts b/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts index 5441a5fa62..8dfd84b173 100644 --- a/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts @@ -39,6 +39,7 @@ import { FileService } from 'vs/platform/files/common/fileService'; import { NullLogService } from 'vs/platform/log/common/log'; import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; import { ConfigurationCache } from 'vs/workbench/services/configuration/node/configurationCache'; +import { ConfigurationCache as BrowserConfigurationCache } from 'vs/workbench/services/configuration/browser/configurationCache'; import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; import { IConfigurationCache } from 'vs/workbench/services/configuration/common/configuration'; import { SignService } from 'vs/platform/sign/browser/signService'; @@ -50,6 +51,7 @@ import { timeout } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { DisposableStore } from 'vs/base/common/lifecycle'; import product from 'vs/platform/product/common/product'; +import { BrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; class TestEnvironmentService extends NativeWorkbenchEnvironmentService { @@ -2059,6 +2061,46 @@ suite('WorkspaceConfigurationService - Remote Folder', () => { }); +suite('ConfigurationService - Configuration Defaults', () => { + + const disposableStore: DisposableStore = new DisposableStore(); + + suiteSetup(() => { + Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ + 'id': '_test', + 'type': 'object', + 'properties': { + 'configurationService.defaultOverridesSetting': { + 'type': 'string', + 'default': 'isSet', + }, + } + }); + }); + + teardown(() => { + disposableStore.clear(); + }); + + test('when default value is not overriden', () => { + const testObject = createConfiurationService({}); + assert.deepEqual(testObject.getValue('configurationService.defaultOverridesSetting'), 'isSet'); + }); + + test('when default value is overriden', () => { + const testObject = createConfiurationService({ 'configurationService.defaultOverridesSetting': 'overriddenValue' }); + assert.deepEqual(testObject.getValue('configurationService.defaultOverridesSetting'), 'overriddenValue'); + }); + + function createConfiurationService(configurationDefaults: Record): IConfigurationService { + const remoteAgentService = (workbenchInstantiationService()).createInstance(RemoteAgentService); + const environmentService = new BrowserWorkbenchEnvironmentService({ logsPath: URI.file(''), workspaceId: '', configurationDefaults }); + const fileService = new FileService(new NullLogService()); + return disposableStore.add(new WorkspaceService({ configurationCache: new BrowserConfigurationCache() }, environmentService, fileService, remoteAgentService)); + } + +}); + function getWorkspaceId(configPath: URI): string { let workspaceConfigPath = configPath.scheme === Schemas.file ? originalFSPath(configPath) : configPath.toString(); if (!isLinux) { 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 1152696061..122041fa93 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { EditorActivation } from 'vs/platform/editor/common/editor'; import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { EditorInput, EditorsOrder, SideBySideEditorInput } from 'vs/workbench/common/editor'; import { workbenchInstantiationService, TestServiceAccessor, registerTestEditor, TestFileEditorInput } from 'vs/workbench/test/browser/workbenchTestServices'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; @@ -386,7 +386,7 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite test('delegate', function (done) { const instantiationService = workbenchInstantiationService(); - class MyEditor extends BaseEditor { + class MyEditor extends EditorPane { constructor(id: string) { super(id, undefined!, new TestThemeService(), new TestStorageService()); diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index b476316e21..74979ddfe2 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -14,7 +14,6 @@ import { IWorkbenchConstructionOptions } from 'vs/workbench/workbench.web.api'; import product from 'vs/platform/product/common/product'; import { memoize } from 'vs/base/common/decorators'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { LIGHT } from 'vs/platform/theme/common/themeService'; import { parseLineAndColumnAware } from 'vs/base/common/extpath'; export class BrowserEnvironmentConfiguration implements IEnvironmentConfiguration { @@ -78,10 +77,6 @@ export class BrowserEnvironmentConfiguration implements IEnvironmentConfiguratio get highContrast() { return false; // could investigate to detect high contrast theme automatically } - - get defaultThemeType() { - return LIGHT; - } } interface IBrowserWorkbenchEnvironmentConstructionOptions extends IWorkbenchConstructionOptions { diff --git a/src/vs/workbench/services/experiment/electron-browser/experimentService.ts b/src/vs/workbench/services/experiment/electron-browser/experimentService.ts index c4b42f0782..470fcbbae6 100644 --- a/src/vs/workbench/services/experiment/electron-browser/experimentService.ts +++ b/src/vs/workbench/services/experiment/electron-browser/experimentService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as platform from 'vs/base/common/platform'; -import { IKeyValueStorage, IExperimentationTelemetry, IExperimentationFilterProvider, ExperimentationService as TASClient } from 'tas-client'; +import type { IKeyValueStorage, IExperimentationTelemetry, IExperimentationFilterProvider, ExperimentationService as TASClient } from 'tas-client'; import { MementoObject, Memento } from 'vs/workbench/common/memento'; import { IProductService } from 'vs/platform/product/common/productService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -206,7 +206,7 @@ export class ExperimentService implements ITASExperimentService { const telemetry = new ExperimentServiceTelemetry(this.telemetryService); const tasConfig = this.productService.tasConfig!; - const tasClient = new TASClient({ + const tasClient = new (await import('tas-client')).ExperimentationService({ filterProviders: [filterProvider], telemetry: telemetry, storageKey: storageKey, diff --git a/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts index 4b7fd19a5c..37d8645508 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionEnablementService.ts @@ -12,13 +12,15 @@ import { areSameExtensions } from 'vs/platform/extensionManagement/common/extens import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { ExtensionType, IExtension } from 'vs/platform/extensions/common/extensions'; +import { ExtensionType, IExtension, isAuthenticaionProviderExtension, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { getExtensionKind } from 'vs/workbench/services/extensions/common/extensionsUtil'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IProductService } from 'vs/platform/product/common/productService'; import { StorageManager } from 'vs/platform/extensionManagement/common/extensionEnablementService'; import { webWorkerExtHostConfig } from 'vs/workbench/services/extensions/common/extensions'; +import { IUserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount'; +import { IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync'; const SOURCE = 'IWorkbenchExtensionEnablementService'; @@ -40,6 +42,8 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench @IConfigurationService private readonly configurationService: IConfigurationService, @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, @IProductService private readonly productService: IProductService, + @IUserDataAutoSyncService private readonly userDataAutoSyncService: IUserDataAutoSyncService, + @IUserDataSyncAccountService private readonly userDataSyncAccountService: IUserDataSyncAccountService, ) { super(); this.storageManger = this._register(new StorageManager(storageService)); @@ -66,7 +70,9 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench } canChangeEnablement(extension: IExtension): boolean { - if (extension.manifest && extension.manifest.contributes && extension.manifest.contributes.localizations && extension.manifest.contributes.localizations.length) { + try { + this.throwErrorIfCannotChangeEnablement(extension); + } catch (error) { return false; } const enablementState = this.getEnablementState(extension); @@ -76,11 +82,47 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench return true; } + private throwErrorIfCannotChangeEnablement(extension: IExtension): void { + if (isLanguagePackExtension(extension.manifest)) { + throw new Error(localize('cannot disable language pack extension', "Cannot disable {0} extension because it contributes language packs.", extension.manifest.displayName || extension.identifier.id)); + } + + if (this.userDataAutoSyncService.isEnabled() && this.userDataSyncAccountService.account && + isAuthenticaionProviderExtension(extension.manifest) && extension.manifest.contributes!.authentication!.some(a => a.id === this.userDataSyncAccountService.account!.authenticationProviderId)) { + throw new Error(localize('cannot disable auth extension', "Cannot disable {0} extension because Settings Sync depends on it.", extension.manifest.displayName || extension.identifier.id)); + } + } + + canChangeWorkspaceEnablement(extension: IExtension): boolean { + if (!this.canChangeEnablement(extension)) { + return false; + } + try { + this.throwErrorIfCannotChangeWorkspaceEnablement(extension); + } catch (error) { + return false; + } + return true; + } + + private throwErrorIfCannotChangeWorkspaceEnablement(extension: IExtension): void { + if (!this.hasWorkspace) { + throw new Error(localize('noWorkspace', "No workspace.")); + } + if (isAuthenticaionProviderExtension(extension.manifest)) { + throw new Error(localize('cannot disable auth extension in workspace', "Cannot disable {0} extension in workspace because it contributes authentication providers", extension.manifest.displayName || extension.identifier.id)); + } + } + async setEnablement(extensions: IExtension[], newState: EnablementState): Promise { const workspace = newState === EnablementState.DisabledWorkspace || newState === EnablementState.EnabledWorkspace; - if (workspace && !this.hasWorkspace) { - return Promise.reject(new Error(localize('noWorkspace', "No workspace."))); + for (const extension of extensions) { + if (workspace) { + this.throwErrorIfCannotChangeWorkspaceEnablement(extension); + } else { + this.throwErrorIfCannotChangeEnablement(extension); + } } const result = await Promise.all(extensions.map(e => this._setEnablement(e, newState))); @@ -316,4 +358,4 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench } } -registerSingleton(IWorkbenchExtensionEnablementService, ExtensionEnablementService, true); +registerSingleton(IWorkbenchExtensionEnablementService, ExtensionEnablementService); diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts index 02f05a04ec..ff9435b9d2 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts @@ -58,6 +58,11 @@ export interface IWorkbenchExtensionEnablementService { */ canChangeEnablement(extension: IExtension): boolean; + /** + * Returns `true` if the enablement can be changed. + */ + canChangeWorkspaceEnablement(extension: IExtension): boolean; + /** * Returns `true` if the given extension identifier is enabled. */ @@ -127,10 +132,11 @@ export interface IExtensionRecommendationsService { readonly _serviceBrand: undefined; getAllRecommendationsWithReason(): IStringDictionary; - getFileBasedRecommendations(): IExtensionRecommendation[]; getImportantRecommendations(): Promise; - getConfigBasedRecommendations(): Promise; getOtherRecommendations(): Promise; + getFileBasedRecommendations(): IExtensionRecommendation[]; + getExeBasedRecommendations(exe?: string): Promise<{ important: IExtensionRecommendation[], others: IExtensionRecommendation[] }>; + getConfigBasedRecommendations(): Promise<{ important: IExtensionRecommendation[], others: IExtensionRecommendation[] }>; getWorkspaceRecommendations(): Promise; getKeymapRecommendations(): IExtensionRecommendation[]; @@ -147,6 +153,7 @@ export interface IWebExtensionsScannerService { readonly _serviceBrand: undefined; scanExtensions(type?: ExtensionType): Promise; scanAndTranslateExtensions(type?: ExtensionType): Promise; + canAddExtension(galleryExtension: IGalleryExtension): Promise; addExtension(galleryExtension: IGalleryExtension): Promise; removeExtension(identifier: IExtensionIdentifier, version?: string): Promise; } diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementServerService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementServerService.ts index 7e21f5ffbf..ae0042321e 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementServerService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementServerService.ts @@ -5,7 +5,6 @@ import { localize } from 'vs/nls'; import { IExtensionManagementServer, IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { ExtensionManagementChannelClient } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; @@ -15,6 +14,10 @@ import { isWeb } from 'vs/base/common/platform'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { WebExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/webExtensionManagementService'; import { IExtension } from 'vs/platform/extensions/common/extensions'; +import { WebRemoteExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/remoteExtensionManagementService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IProductService } from 'vs/platform/product/common/productService'; export class ExtensionManagementServerService implements IExtensionManagementServerService { @@ -27,11 +30,14 @@ export class ExtensionManagementServerService implements IExtensionManagementSer constructor( @IRemoteAgentService remoteAgentService: IRemoteAgentService, @ILabelService labelService: ILabelService, + @IExtensionGalleryService galleryService: IExtensionGalleryService, + @IProductService productService: IProductService, + @IConfigurationService configurationService: IConfigurationService, @IInstantiationService instantiationService: IInstantiationService, ) { const remoteAgentConnection = remoteAgentService.getConnection(); if (remoteAgentConnection) { - const extensionManagementService = new ExtensionManagementChannelClient(remoteAgentConnection!.getChannel('extensions')); + const extensionManagementService = new WebRemoteExtensionManagementService(remoteAgentConnection.getChannel('extensions'), galleryService, configurationService, productService); this.remoteExtensionManagementServer = { id: 'remote', extensionManagementService, diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index dfce39abd2..2815d9c0d9 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -189,6 +189,15 @@ export class ExtensionManagementService extends Disposable implements IExtension return Promise.reject('No Servers'); } + async canInstall(gallery: IGalleryExtension): Promise { + for (const server of this.servers) { + if (await server.extensionManagementService.canInstall(gallery)) { + return true; + } + } + return false; + } + async installFromGallery(gallery: IGalleryExtension): Promise { // Only local server, install without any checks diff --git a/src/vs/workbench/services/extensionManagement/common/remoteExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/remoteExtensionManagementService.ts new file mode 100644 index 0000000000..68a4fbf11d --- /dev/null +++ b/src/vs/workbench/services/extensionManagement/common/remoteExtensionManagementService.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IChannel } from 'vs/base/parts/ipc/common/ipc'; +import { IExtensionManagementService, IGalleryExtension, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { canExecuteOnWorkspace } from 'vs/workbench/services/extensions/common/extensionsUtil'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ExtensionManagementChannelClient } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; + +export class WebRemoteExtensionManagementService extends ExtensionManagementChannelClient implements IExtensionManagementService { + + constructor( + channel: IChannel, + @IExtensionGalleryService protected readonly galleryService: IExtensionGalleryService, + @IConfigurationService protected readonly configurationService: IConfigurationService, + @IProductService protected readonly productService: IProductService + ) { + super(channel); + } + + async canInstall(extension: IGalleryExtension): Promise { + const manifest = await this.galleryService.getManifest(extension, CancellationToken.None); + return !!manifest && canExecuteOnWorkspace(manifest, this.productService, this.configurationService); + } + +} diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts index 50a607030c..c4a30bf6c7 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts @@ -4,13 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { ExtensionType, IExtensionIdentifier, IExtensionManifest, ITranslatedScannedExtension } from 'vs/platform/extensions/common/extensions'; -import { IExtensionManagementService, ILocalExtension, InstallExtensionEvent, DidInstallExtensionEvent, DidUninstallExtensionEvent, IGalleryExtension, IReportedExtension, IGalleryMetadata, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionManagementService, ILocalExtension, InstallExtensionEvent, DidInstallExtensionEvent, DidUninstallExtensionEvent, IGalleryExtension, IReportedExtension, IGalleryMetadata, InstallOperation, INSTALL_ERROR_NOT_SUPPORTED } from 'vs/platform/extensionManagement/common/extensionManagement'; import { Event, Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IWebExtensionsScannerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ILogService } from 'vs/platform/log/common/log'; import { Disposable } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; export class WebExtensionManagementService extends Disposable implements IExtensionManagementService { @@ -40,16 +41,25 @@ export class WebExtensionManagementService extends Disposable implements IExtens return Promise.all(extensions.map(e => this.toLocalExtension(e))); } + async canInstall(gallery: IGalleryExtension): Promise { + return this.webExtensionsScannerService.canAddExtension(gallery); + } + async installFromGallery(gallery: IGalleryExtension): Promise { + if (!(await this.canInstall(gallery))) { + const error = new Error(localize('cannot be installed', "Cannot install '{0}' because this extension is not a web extension.", gallery.displayName || gallery.name)); + error.name = INSTALL_ERROR_NOT_SUPPORTED; + throw error; + } this.logService.info('Installing extension:', gallery.identifier.id); this._onInstallExtension.fire({ identifier: gallery.identifier, gallery }); try { const existingExtension = await this.getUserExtension(gallery.identifier); + const scannedExtension = await this.webExtensionsScannerService.addExtension(gallery); + const local = await this.toLocalExtension(scannedExtension); if (existingExtension && existingExtension.manifest.version !== gallery.version) { await this.webExtensionsScannerService.removeExtension(existingExtension.identifier, existingExtension.manifest.version); } - const scannedExtension = await this.webExtensionsScannerService.addExtension(gallery); - const local = await this.toLocalExtension(scannedExtension); this._onDidInstallExtension.fire({ local, identifier: gallery.identifier, operation: InstallOperation.Install, gallery }); return local; } catch (error) { diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts index 6aeca44e18..26c5caafce 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts @@ -3,7 +3,6 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as semver from 'semver-umd'; import { IBuiltinExtensionsScannerService, IScannedExtension, ExtensionType, IExtensionIdentifier, ITranslatedScannedExtension } from 'vs/platform/extensions/common/extensions'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IWebExtensionsScannerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; @@ -17,18 +16,19 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { asText, isSuccess, IRequestService } from 'vs/platform/request/common/request'; import { ILogService } from 'vs/platform/log/common/log'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IGalleryExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IGalleryExtension, INSTALL_ERROR_NOT_SUPPORTED } from 'vs/platform/extensionManagement/common/extensionManagement'; import { groupByExtension, areSameExtensions, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IStaticExtension } from 'vs/workbench/workbench.web.api'; import { Disposable } from 'vs/base/common/lifecycle'; import { Event } from 'vs/base/common/event'; import { localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls'; +import { localize } from 'vs/nls'; interface IUserExtension { identifier: IExtensionIdentifier; version: string; - uri: URI; + location: URI; readmeUri?: URI; changelogUri?: URI; packageNLSUri?: URI; @@ -37,16 +37,12 @@ interface IUserExtension { interface IStoredUserExtension { identifier: IExtensionIdentifier; version: string; - uri: UriComponents; + location: UriComponents; readmeUri?: UriComponents; changelogUri?: UriComponents; packageNLSUri?: UriComponents; } -const AssetTypeWebResource = 'Microsoft.VisualStudio.Code.WebResources'; - -function getExtensionLocation(assetUri: URI): URI { return joinPath(assetUri, AssetTypeWebResource, 'extension'); } - export class WebExtensionsScannerService extends Disposable implements IWebExtensionsScannerService { declare readonly _serviceBrand: undefined; @@ -87,29 +83,45 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten */ private getStaticExtensions(builtin: boolean): IScannedExtension[] { const staticExtensions = this.environmentService.options && Array.isArray(this.environmentService.options.staticExtensions) ? this.environmentService.options.staticExtensions : []; - return ( - staticExtensions - .filter(e => Boolean(e.isBuiltin) === builtin) - .map(e => ({ - identifier: { id: getGalleryExtensionId(e.packageJSON.publisher, e.packageJSON.name) }, - location: e.extensionLocation, - type: e.isBuiltin ? ExtensionType.System : ExtensionType.User, - packageJSON: e.packageJSON, - })) - ); + const result: IScannedExtension[] = []; + for (const e of staticExtensions) { + if (Boolean(e.isBuiltin) === builtin) { + const scannedExtension = this.parseStaticExtension(e, builtin); + if (scannedExtension) { + result.push(scannedExtension); + } + } + } + return result; } private async readDefaultExtensions(): Promise { const defaultUserWebExtensions = await this.readDefaultUserWebExtensions(); - const extensions = defaultUserWebExtensions.map(e => ({ - identifier: { id: getGalleryExtensionId(e.packageJSON.publisher, e.packageJSON.name) }, - location: e.extensionLocation, - type: ExtensionType.User, - packageJSON: e.packageJSON, - })); + const extensions: IScannedExtension[] = []; + for (const e of defaultUserWebExtensions) { + const scannedExtension = this.parseStaticExtension(e, false); + if (scannedExtension) { + extensions.push(scannedExtension); + } + } return extensions.concat(this.getStaticExtensions(false)); } + private parseStaticExtension(e: IStaticExtension, builtin: boolean): IScannedExtension | null { + try { + return { + identifier: { id: getGalleryExtensionId(e.packageJSON.publisher, e.packageJSON.name) }, + location: e.extensionLocation, + type: builtin ? ExtensionType.System : ExtensionType.User, + packageJSON: e.packageJSON, + }; + } catch (error) { + this.logService.error(`Error while parsing extension ${e.extensionLocation.toString()}`); + this.logService.error(error); + } + return null; + } + private async readDefaultUserWebExtensions(): Promise { const result: IStaticExtension[] = []; const defaultUserWebExtensions = this.configurationService.getValue<{ location: string }[]>('_extensions.defaultUserWebExtensions') || []; @@ -190,12 +202,19 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten }; } + async canAddExtension(galleryExtension: IGalleryExtension): Promise { + return !!galleryExtension.properties.webExtension && !!galleryExtension.webResource; + } + async addExtension(galleryExtension: IGalleryExtension): Promise { - if (!galleryExtension.assetTypes.some(type => type.startsWith(AssetTypeWebResource))) { - throw new Error(`Missing ${AssetTypeWebResource} asset type`); + if (!(await this.canAddExtension(galleryExtension))) { + const error = new Error(localize('cannot be installed', "Cannot install '{0}' because this extension is not a web extension.", galleryExtension.displayName || galleryExtension.name)); + error.name = INSTALL_ERROR_NOT_SUPPORTED; + throw error; } - const packageNLSUri = joinPath(getExtensionLocation(galleryExtension.assetUri), 'package.nls.json'); + const extensionLocation = galleryExtension.webResource!; + const packageNLSUri = joinPath(extensionLocation, 'package.nls.json'); const context = await this.requestService.request({ type: 'GET', url: packageNLSUri.toString() }, CancellationToken.None); const packageNLSExists = isSuccess(context); @@ -203,7 +222,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten const userExtension: IUserExtension = { identifier: galleryExtension.identifier, version: galleryExtension.version, - uri: galleryExtension.assetUri, + location: extensionLocation, readmeUri: galleryExtension.assets.readme ? URI.parse(galleryExtension.assets.readme.uri) : undefined, changelogUri: galleryExtension.assets.changelog ? URI.parse(galleryExtension.assets.changelog.uri) : undefined, packageNLSUri: packageNLSExists ? packageNLSUri : undefined @@ -225,6 +244,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten } private async scanUserExtensions(): Promise { + const semver = await import('semver-umd'); let userExtensions = await this.readUserExtensions(); const byExtension: IUserExtension[][] = groupByExtension(userExtensions, e => e.identifier); userExtensions = byExtension.map(p => p.sort((a, b) => semver.rcompare(a.version, b.version))[0]); @@ -243,14 +263,14 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten } private async toScannedExtension(userExtension: IUserExtension): Promise { - const context = await this.requestService.request({ type: 'GET', url: joinPath(userExtension.uri, 'Microsoft.VisualStudio.Code.Manifest').toString() }, CancellationToken.None); + const context = await this.requestService.request({ type: 'GET', url: joinPath(userExtension.location, 'package.json').toString() }, CancellationToken.None); if (isSuccess(context)) { const content = await asText(context); if (content) { const packageJSON = JSON.parse(content); return { identifier: userExtension.identifier, - location: getExtensionLocation(userExtension.uri), + location: userExtension.location, packageJSON, type: ExtensionType.User, readmeUrl: userExtension.readmeUri, @@ -269,11 +289,11 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten return this.userExtensionsResourceLimiter.queue(async () => { try { const content = await this.fileService.readFile(this.extensionsResource!); - const storedUserExtensions: IStoredUserExtension[] = JSON.parse(content.value.toString()); + const storedUserExtensions: IStoredUserExtension[] = this.parseExtensions(content.value.toString()); return storedUserExtensions.map(e => ({ identifier: e.identifier, version: e.version, - uri: URI.revive(e.uri), + location: URI.revive(e.location), readmeUri: URI.revive(e.readmeUri), changelogUri: URI.revive(e.changelogUri), packageNLSUri: URI.revive(e.packageNLSUri), @@ -291,7 +311,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten const storedUserExtensions: IStoredUserExtension[] = userExtensions.map(e => ({ identifier: e.identifier, version: e.version, - uri: e.uri.toJSON(), + location: e.location.toJSON(), readmeUri: e.readmeUri?.toJSON(), changelogUri: e.changelogUri?.toJSON(), packageNLSUri: e.packageNLSUri?.toJSON(), @@ -302,6 +322,14 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten }); } + private parseExtensions(content: string): IStoredUserExtension[] { + const storedUserExtensions: (IStoredUserExtension & { uri?: UriComponents })[] = JSON.parse(content.toString()); + return storedUserExtensions.map(e => { + const location = e.uri ? joinPath(URI.revive(e.uri), 'Microsoft.VisualStudio.Code.WebResources', 'extension') : e.location; + return { ...e, location }; + }); + } + } registerSingleton(IWebExtensionsScannerService, WebExtensionsScannerService); diff --git a/src/vs/workbench/services/extensionManagement/electron-browser/extensionManagementServerService.ts b/src/vs/workbench/services/extensionManagement/electron-browser/extensionManagementServerService.ts index 779e8f08c9..cfe21ad67b 100644 --- a/src/vs/workbench/services/extensionManagement/electron-browser/extensionManagementServerService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-browser/extensionManagementServerService.ts @@ -5,7 +5,6 @@ import { localize } from 'vs/nls'; import { Schemas } from 'vs/base/common/network'; -import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionManagementServer, IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionManagementChannelClient } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; @@ -13,12 +12,13 @@ import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { ILogService } from 'vs/platform/log/common/log'; -import { RemoteExtensionManagementChannelClient } from 'vs/workbench/services/extensions/electron-browser/remoteExtensionManagementIpc'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IProductService } from 'vs/platform/product/common/productService'; +import { NativeRemoteExtensionManagementService } from 'vs/workbench/services/extensionManagement/electron-browser/remoteExtensionManagementService'; import { ILabelService } from 'vs/platform/label/common/label'; import { IExtension } from 'vs/platform/extensions/common/extensions'; +import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ILogService } from 'vs/platform/log/common/log'; export class ExtensionManagementServerService implements IExtensionManagementServerService { @@ -32,18 +32,18 @@ export class ExtensionManagementServerService implements IExtensionManagementSer constructor( @ISharedProcessService sharedProcessService: ISharedProcessService, @IRemoteAgentService remoteAgentService: IRemoteAgentService, - @IExtensionGalleryService galleryService: IExtensionGalleryService, - @IConfigurationService configurationService: IConfigurationService, - @IProductService productService: IProductService, - @ILogService logService: ILogService, @ILabelService labelService: ILabelService, + @IExtensionGalleryService galleryService: IExtensionGalleryService, + @IProductService productService: IProductService, + @IConfigurationService configurationService: IConfigurationService, + @ILogService logService: ILogService, ) { const localExtensionManagementService = new ExtensionManagementChannelClient(sharedProcessService.getChannel('extensions')); this._localExtensionManagementServer = { extensionManagementService: localExtensionManagementService, id: 'local', label: localize('local', "Local") }; const remoteAgentConnection = remoteAgentService.getConnection(); if (remoteAgentConnection) { - const extensionManagementService = new RemoteExtensionManagementChannelClient(remoteAgentConnection.getChannel('extensions'), this.localExtensionManagementServer.extensionManagementService, galleryService, logService, configurationService, productService); + const extensionManagementService = new NativeRemoteExtensionManagementService(remoteAgentConnection.getChannel('extensions'), this.localExtensionManagementServer, logService, galleryService, configurationService, productService); this.remoteExtensionManagementServer = { id: 'remote', extensionManagementService, diff --git a/src/vs/workbench/services/extensions/electron-browser/remoteExtensionManagementIpc.ts b/src/vs/workbench/services/extensionManagement/electron-browser/remoteExtensionManagementService.ts similarity index 86% rename from src/vs/workbench/services/extensions/electron-browser/remoteExtensionManagementIpc.ts rename to src/vs/workbench/services/extensionManagement/electron-browser/remoteExtensionManagementService.ts index cf9895a4b7..c142f691d8 100644 --- a/src/vs/workbench/services/extensions/electron-browser/remoteExtensionManagementIpc.ts +++ b/src/vs/workbench/services/extensionManagement/electron-browser/remoteExtensionManagementService.ts @@ -12,28 +12,30 @@ import { areSameExtensions } from 'vs/platform/extensionManagement/common/extens import { ILogService } from 'vs/platform/log/common/log'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { prefersExecuteOnUI } from 'vs/workbench/services/extensions/common/extensionsUtil'; -import { isNonEmptyArray, toArray } from 'vs/base/common/arrays'; +import { isNonEmptyArray } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { localize } from 'vs/nls'; import { IProductService } from 'vs/platform/product/common/productService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ExtensionManagementChannelClient } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; import { generateUuid } from 'vs/base/common/uuid'; import { joinPath } from 'vs/base/common/resources'; +import { WebRemoteExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/remoteExtensionManagementService'; +import { IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -export class RemoteExtensionManagementChannelClient extends ExtensionManagementChannelClient { +export class NativeRemoteExtensionManagementService extends WebRemoteExtensionManagementService implements IExtensionManagementService { - declare readonly _serviceBrand: undefined; + private readonly localExtensionManagementService: IExtensionManagementService; constructor( channel: IChannel, - private readonly localExtensionManagementService: IExtensionManagementService, - private readonly galleryService: IExtensionGalleryService, - private readonly logService: ILogService, - private readonly configurationService: IConfigurationService, - private readonly productService: IProductService + localExtensionManagementServer: IExtensionManagementServer, + @ILogService private readonly logService: ILogService, + @IExtensionGalleryService galleryService: IExtensionGalleryService, + @IConfigurationService configurationService: IConfigurationService, + @IProductService productService: IProductService ) { - super(channel); + super(channel, galleryService, configurationService, productService); + this.localExtensionManagementService = localExtensionManagementServer.extensionManagementService; } async install(vsix: URI): Promise { @@ -101,14 +103,14 @@ export class RemoteExtensionManagementChannelClient extends ExtensionManagementC const result = new Map(); const extensions = [...(manifest.extensionPack || []), ...(manifest.extensionDependencies || [])]; await this.getDependenciesAndPackedExtensionsRecursively(extensions, result, true, token); - return toArray(result.values()); + return [...result.values()]; } private async getAllWorkspaceDependenciesAndPackedExtensions(manifest: IExtensionManifest, token: CancellationToken): Promise { const result = new Map(); const extensions = [...(manifest.extensionPack || []), ...(manifest.extensionDependencies || [])]; await this.getDependenciesAndPackedExtensionsRecursively(extensions, result, false, token); - return toArray(result.values()); + return [...result.values()]; } private async getDependenciesAndPackedExtensionsRecursively(toGet: string[], result: Map, uiExtension: boolean, token: CancellationToken): Promise { diff --git a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts index 40cd02ac66..e683258da6 100644 --- a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts +++ b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts @@ -23,6 +23,8 @@ import { assign } from 'vs/base/common/objects'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { productService } from 'vs/workbench/test/browser/workbenchTestServices'; import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionEnablementService'; +import { IUserDataSyncAccountService, UserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount'; +import { IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync'; function createStorageService(instantiationService: TestInstantiationService): IStorageService { let service = instantiationService.get(IStorageService); @@ -51,7 +53,9 @@ export class TestExtensionEnablementService extends ExtensionEnablementService { extensionManagementService, instantiationService.get(IConfigurationService), extensionManagementServerService, - productService + productService, + instantiationService.get(IUserDataAutoSyncService) || instantiationService.stub(IUserDataAutoSyncService, >{ isEnabled() { return false; } }), + instantiationService.get(IUserDataSyncAccountService) || instantiationService.stub(IUserDataSyncAccountService, UserDataSyncAccountService) ); } @@ -371,6 +375,48 @@ suite('ExtensionEnablementService Test', () => { assert.equal(testObject.canChangeEnablement(aLocalExtension('pub.a', { localizations: [{ languageId: 'gr', translations: [{ id: 'vscode', path: 'path' }] }] })), false); }); + test('test canChangeEnablement return true for auth extension', () => { + assert.equal(testObject.canChangeEnablement(aLocalExtension('pub.a', { authentication: [{ id: 'a', label: 'a' }] })), true); + }); + + test('test canChangeEnablement return true for auth extension when user data sync account does not depends on it', () => { + instantiationService.stub(IUserDataSyncAccountService, >{ + account: { authenticationProviderId: 'b' } + }); + testObject = new TestExtensionEnablementService(instantiationService); + assert.equal(testObject.canChangeEnablement(aLocalExtension('pub.a', { authentication: [{ id: 'a', label: 'a' }] })), true); + }); + + test('test canChangeEnablement return true for auth extension when user data sync account depends on it but auto sync is off', () => { + instantiationService.stub(IUserDataSyncAccountService, >{ + account: { authenticationProviderId: 'a' } + }); + testObject = new TestExtensionEnablementService(instantiationService); + assert.equal(testObject.canChangeEnablement(aLocalExtension('pub.a', { authentication: [{ id: 'a', label: 'a' }] })), true); + }); + + test('test canChangeEnablement return false for auth extension and user data sync account depends on it and auto sync is on', () => { + instantiationService.stub(IUserDataAutoSyncService, >{ isEnabled() { return true; } }); + instantiationService.stub(IUserDataSyncAccountService, >{ + account: { authenticationProviderId: 'a' } + }); + testObject = new TestExtensionEnablementService(instantiationService); + assert.equal(testObject.canChangeEnablement(aLocalExtension('pub.a', { authentication: [{ id: 'a', label: 'a' }] })), false); + }); + + test('test canChangeWorkspaceEnablement return true', () => { + assert.equal(testObject.canChangeWorkspaceEnablement(aLocalExtension('pub.a')), true); + }); + + test('test canChangeWorkspaceEnablement return false if there is no workspace', () => { + instantiationService.stub(IWorkspaceContextService, 'getWorkbenchState', WorkbenchState.EMPTY); + assert.equal(testObject.canChangeWorkspaceEnablement(aLocalExtension('pub.a')), false); + }); + + test('test canChangeWorkspaceEnablement return false for auth extension', () => { + assert.equal(testObject.canChangeWorkspaceEnablement(aLocalExtension('pub.a', { authentication: [{ id: 'a', label: 'a' }] })), false); + }); + test('test canChangeEnablement return false when extensions are disabled in environment', () => { instantiationService.stub(IWorkbenchEnvironmentService, { disableExtensions: true } as IWorkbenchEnvironmentService); testObject = new TestExtensionEnablementService(instantiationService); diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index f97d94da8e..3916acbd5d 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -15,7 +15,7 @@ import { BetterMergeId } from 'vs/platform/extensionManagement/common/extensionM import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { ActivationTimes, ExtensionPointContribution, IExtensionService, IExtensionsStatus, IMessage, IWillActivateEvent, IResponsiveStateChangeEvent, toExtension, IExtensionHost } from 'vs/workbench/services/extensions/common/extensions'; +import { ActivationTimes, ExtensionPointContribution, IExtensionService, IExtensionsStatus, IMessage, IWillActivateEvent, IResponsiveStateChangeEvent, toExtension, IExtensionHost, ActivationKind } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionMessageCollector, ExtensionPoint, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import { ResponsiveState } from 'vs/workbench/services/extensions/common/rpcProtocol'; @@ -186,7 +186,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx this._startExtensionHosts(false, Array.from(this._allRequestedActivateEvents.keys())); } - public activateByEvent(activationEvent: string): Promise { + public activateByEvent(activationEvent: string, activationKind: ActivationKind = ActivationKind.Normal): Promise { if (this._installedExtensionsReady.isOpen()) { // Extensions have been scanned and interpreted @@ -198,20 +198,25 @@ export abstract class AbstractExtensionService extends Disposable implements IEx return NO_OP_VOID_PROMISE; } - return this._activateByEvent(activationEvent); + return this._activateByEvent(activationEvent, activationKind); } else { // Extensions have not been scanned yet. // Record the fact that this activationEvent was requested (in case of a restart) this._allRequestedActivateEvents.add(activationEvent); - return this._installedExtensionsReady.wait().then(() => this._activateByEvent(activationEvent)); + if (activationKind === ActivationKind.Immediate) { + // Do not wait for the normal start-up of the extension host(s) + return this._activateByEvent(activationEvent, activationKind); + } + + return this._installedExtensionsReady.wait().then(() => this._activateByEvent(activationEvent, activationKind)); } } - private _activateByEvent(activationEvent: string): Promise { + private _activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise { const result = Promise.all( - this._extensionHostManagers.map(extHostManager => extHostManager.activateByEvent(activationEvent)) + this._extensionHostManagers.map(extHostManager => extHostManager.activateByEvent(activationEvent, activationKind)) ).then(() => { }); this._onWillActivateByEvent.fire({ event: activationEvent, diff --git a/src/vs/workbench/services/extensions/common/extensionHostManager.ts b/src/vs/workbench/services/extensions/common/extensionHostManager.ts index 1571d0e46d..10baa5bcef 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostManager.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostManager.ts @@ -21,7 +21,7 @@ import { registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { StopWatch } from 'vs/base/common/stopwatch'; import { VSBuffer } from 'vs/base/common/buffer'; -import { IExtensionHost, ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionHost, ExtensionHostKind, ActivationKind } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtensionActivator'; // Enable to see detailed message communication between window and extension host @@ -48,6 +48,7 @@ export class ExtensionHostManager extends Disposable { */ private _proxy: Promise<{ value: ExtHostExtensionServiceShape; } | null> | null; private _resolveAuthorityAttempt: number; + private _hasStarted = false; constructor( extensionHost: IExtensionHost, @@ -65,6 +66,7 @@ export class ExtensionHostManager extends Disposable { this.onDidExit = this._extensionHost.onExit; this._proxy = this._extensionHost.start()!.then( (protocol) => { + this._hasStarted = true; return { value: this._createExtensionHostCustomers(protocol) }; }, (err) => { @@ -74,7 +76,7 @@ export class ExtensionHostManager extends Disposable { } ); this._proxy.then(() => { - initialActivationEvents.forEach((activationEvent) => this.activateByEvent(activationEvent)); + initialActivationEvents.forEach((activationEvent) => this.activateByEvent(activationEvent, ActivationKind.Normal)); this._register(registerLatencyTestProvider({ measure: () => this.measure() })); @@ -219,14 +221,18 @@ export class ExtensionHostManager extends Disposable { return proxy.$activate(extension, reason); } - public activateByEvent(activationEvent: string): Promise { + public activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise { + if (activationKind === ActivationKind.Immediate && !this._hasStarted) { + return Promise.resolve(); + } + if (!this._cachedActivationEvents.has(activationEvent)) { - this._cachedActivationEvents.set(activationEvent, this._activateByEvent(activationEvent)); + this._cachedActivationEvents.set(activationEvent, this._activateByEvent(activationEvent, activationKind)); } return this._cachedActivationEvents.get(activationEvent)!; } - private async _activateByEvent(activationEvent: string): Promise { + private async _activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise { if (!this._proxy) { return; } @@ -236,7 +242,7 @@ export class ExtensionHostManager extends Disposable { // i.e. the extension host could not be started return; } - return proxy.value.$activateByEvent(activationEvent); + return proxy.value.$activateByEvent(activationEvent, activationKind); } public async getInspectPort(tryEnableInspector: boolean): Promise { diff --git a/src/vs/workbench/services/extensions/common/extensions.ts b/src/vs/workbench/services/extensions/common/extensions.ts index 897790a87d..72763db111 100644 --- a/src/vs/workbench/services/extensions/common/extensions.ts +++ b/src/vs/workbench/services/extensions/common/extensions.ts @@ -140,6 +140,11 @@ export interface IResponsiveStateChangeEvent { isResponsive: boolean; } +export const enum ActivationKind { + Normal = 0, + Immediate = 1 +} + export interface IExtensionService { readonly _serviceBrand: undefined; @@ -177,8 +182,15 @@ export interface IExtensionService { /** * Send an activation event and activate interested extensions. + * + * This will wait for the normal startup of the extension host(s). + * + * In extraordinary circumstances, if the activation event needs to activate + * one or more extensions before the normal startup is finished, then you can use + * `ActivationKind.Immediate`. Please do not use this flag unless really necessary + * and you understand all consequences. */ - activateByEvent(activationEvent: string): Promise; + activateByEvent(activationEvent: string, activationKind?: ActivationKind): Promise; /** * An promise that resolves when the installed extensions are registered after diff --git a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts index 1d33bd57da..d573adc60b 100644 --- a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts +++ b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts @@ -12,7 +12,6 @@ import { Extensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/c import { Registry } from 'vs/platform/registry/common/platform'; import { IMessage } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionIdentifier, IExtensionDescription, EXTENSION_CATEGORIES } from 'vs/platform/extensions/common/extensions'; -import { toArray } from 'vs/base/common/arrays'; const schemaRegistry = Registry.as(Extensions.JSONContribution); export type ExtensionKind = 'workspace' | 'ui' | undefined; @@ -446,7 +445,7 @@ export class ExtensionsRegistryImpl { } public getExtensionPoints(): ExtensionPoint[] { - return toArray(this._extensionPoints.values()); + return Array.from(this._extensionPoints.values()); } } diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionHostProfiler.ts b/src/vs/workbench/services/extensions/electron-browser/extensionHostProfiler.ts index e9f3570373..70dfb2f7a0 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionHostProfiler.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionHostProfiler.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Profile, ProfileNode } from 'v8-inspect-profiler'; +import type { Profile, ProfileNode } from 'v8-inspect-profiler'; import { TernarySearchTree } from 'vs/base/common/map'; import { realpathSync } from 'vs/base/node/extpath'; import { IExtensionHostProfile, IExtensionService, ProfileSegmentId, ProfileSession } from 'vs/workbench/services/extensions/common/extensions'; diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index f5cb6393d5..afebe3a389 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -41,6 +41,7 @@ import { WebWorkerExtensionHost } from 'vs/workbench/services/extensions/browser import { IExtensionActivationHost as IWorkspaceContainsActivationHost, checkGlobFileExists, checkActivateWorkspaceContainsExtension } from 'vs/workbench/api/common/shared/workspaceContains'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { exists } from 'vs/base/node/pfs'; +import { ILogService } from 'vs/platform/log/common/log'; class DeltaExtensionsQueueItem { constructor( @@ -76,6 +77,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten @IRemoteExplorerService private readonly _remoteExplorerService: IRemoteExplorerService, @IExtensionGalleryService private readonly _extensionGalleryService: IExtensionGalleryService, @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, + @ILogService private readonly _logService: ILogService, ) { super( instantiationService, @@ -436,6 +438,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten } protected _onExtensionHostCrashed(extensionHost: ExtensionHostManager, code: number, signal: string | null): void { + const activatedExtensions = Array.from(this._extensionHostActiveExtensions.values()); super._onExtensionHostCrashed(extensionHost, code, signal); if (extensionHost.kind === ExtensionHostKind.LocalProcess) { @@ -456,6 +459,9 @@ export class ExtensionService extends AbstractExtensionService implements IExten return; } + const message = `Extension host terminated unexpectedly. The following extensions were running: ${activatedExtensions.map(id => id.value).join(', ')}`; + this._logService.error(message); + this._notificationService.prompt(Severity.Error, nls.localize('extensionService.crash', "Extension host terminated unexpectedly."), [{ label: nls.localize('devTools', "Open Developer Tools"), @@ -466,6 +472,22 @@ export class ExtensionService extends AbstractExtensionService implements IExten run: () => this.startExtensionHost() }] ); + + type ExtensionHostCrashClassification = { + code: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' }; + signal: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' }; + extensionIds: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' }; + }; + type ExtensionHostCrashEvent = { + code: number; + signal: string | null; + extensionIds: string[]; + }; + this._telemetryService.publicLog2('extensionHostCrash', { + code, + signal, + extensionIds: activatedExtensions.map(e => e.value) + }); } } diff --git a/src/vs/workbench/services/extensions/node/proxyResolver.ts b/src/vs/workbench/services/extensions/node/proxyResolver.ts index 882ed7397d..975dffeeef 100644 --- a/src/vs/workbench/services/extensions/node/proxyResolver.ts +++ b/src/vs/workbench/services/extensions/node/proxyResolver.ts @@ -493,9 +493,8 @@ async function readCaCertificates() { } async function readWindowsCaCertificates() { - const winCA = await new Promise((resolve, reject) => { - require(['vscode-windows-ca-certs'], resolve, reject); - }); + // @ts-ignore Windows only + const winCA = await import('vscode-windows-ca-certs'); let ders: any[] = []; const store = winCA(); diff --git a/src/vs/workbench/services/extensions/worker/extensionHostWorker.ts b/src/vs/workbench/services/extensions/worker/extensionHostWorker.ts index 8c57e03e46..800117f268 100644 --- a/src/vs/workbench/services/extensions/worker/extensionHostWorker.ts +++ b/src/vs/workbench/services/extensions/worker/extensionHostWorker.ts @@ -36,6 +36,15 @@ self.postMessage = () => console.trace(`'postMessage' has been blocked`); const nativeAddEventLister = addEventListener.bind(self); self.addEventLister = () => console.trace(`'addEventListener' has been blocked`); +(self)['AMDLoader'] = undefined; +(self)['NLSLoaderPlugin'] = undefined; +(self)['define'] = undefined; +(self)['require'] = undefined; +(self)['webkitRequestFileSystem'] = undefined; +(self)['webkitRequestFileSystemSync'] = undefined; +(self)['webkitResolveLocalFileSystemSyncURL'] = undefined; +(self)['webkitResolveLocalFileSystemURL'] = undefined; + if (location.protocol === 'data:') { // make sure new Worker(...) always uses data: const _Worker = Worker; diff --git a/src/vs/workbench/services/host/browser/host.ts b/src/vs/workbench/services/host/browser/host.ts index 53d7e0823f..92fa826ccf 100644 --- a/src/vs/workbench/services/host/browser/host.ts +++ b/src/vs/workbench/services/host/browser/host.ts @@ -32,8 +32,14 @@ export interface IHostService { /** * Attempt to bring the window to the foreground and focus it. + * + * @param options Pass `force: true` if you want to make the window take + * focus even if the application does not have focus currently. This option + * should only be used if it is necessary to steal focus from the current + * focused application which may not be VSCode. It may not be supported + * in all environments. */ - focus(): Promise; + focus(options?: { force: boolean }): Promise; //#endregion diff --git a/src/vs/workbench/services/host/electron-sandbox/desktopHostService.ts b/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts similarity index 93% rename from src/vs/workbench/services/host/electron-sandbox/desktopHostService.ts rename to src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts index 0cf8235da3..ae9960811e 100644 --- a/src/vs/workbench/services/host/electron-sandbox/desktopHostService.ts +++ b/src/vs/workbench/services/host/electron-sandbox/nativeHostService.ts @@ -12,7 +12,7 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { IWindowOpenable, IOpenWindowOptions, isFolderToOpen, isWorkspaceToOpen, IOpenEmptyWindowOptions } from 'vs/platform/windows/common/windows'; import { Disposable } from 'vs/base/common/lifecycle'; -export class DesktopHostService extends Disposable implements IHostService { +export class NativeHostService extends Disposable implements IHostService { declare readonly _serviceBrand: undefined; @@ -82,8 +82,8 @@ export class DesktopHostService extends Disposable implements IHostService { return this.electronService.toggleFullScreen(); } - focus(): Promise { - return this.electronService.focusWindow(); + focus(options?: { force: boolean }): Promise { + return this.electronService.focusWindow(options); } restart(): Promise { @@ -95,4 +95,4 @@ export class DesktopHostService extends Disposable implements IHostService { } } -registerSingleton(IHostService, DesktopHostService, true); +registerSingleton(IHostService, NativeHostService, true); diff --git a/src/vs/workbench/services/hover/browser/hover.ts b/src/vs/workbench/services/hover/browser/hover.ts index 0453864e52..322c89470a 100644 --- a/src/vs/workbench/services/hover/browser/hover.ts +++ b/src/vs/workbench/services/hover/browser/hover.ts @@ -39,9 +39,10 @@ export interface IHoverService { export interface IHoverOptions { /** - * The text to display in the primary section of the hover. + * The text to display in the primary section of the hover. The type of text determines the + * default `hideOnHover` behavior. */ - text: IMarkdownString; + text: IMarkdownString | string; /** * The target for the hover. This determines the position of the hover and it will only be @@ -69,8 +70,13 @@ export interface IHoverOptions { /** * Whether to hide the hover when the mouse leaves the `target` and enters the actual hover. - * This is false by default and note that it will be ignored if any `actions` are provided such - * that they are accessible. + * This is false by default when text is an `IMarkdownString` and true when `text` is a + * `string`. Note that this will be ignored if any `actions` are provided as hovering is + * required to make them accessible. + * + * In general hiding on hover is desired for: + * - Regular text where selection is not important + * - Markdown that contains no links where selection is not important */ hideOnHover?: boolean; } diff --git a/src/vs/workbench/services/hover/browser/hoverWidget.ts b/src/vs/workbench/services/hover/browser/hoverWidget.ts index b4c6273545..babb2bda0d 100644 --- a/src/vs/workbench/services/hover/browser/hoverWidget.ts +++ b/src/vs/workbench/services/hover/browser/hoverWidget.ts @@ -16,6 +16,8 @@ import { HoverWidget as BaseHoverWidget, renderHoverAction } from 'vs/base/brows import { Widget } from 'vs/base/browser/ui/widget'; import { AnchorPosition } from 'vs/base/browser/ui/contextview/contextview'; import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; +import { MarkdownString } from 'vs/base/common/htmlContent'; const $ = dom.$; @@ -48,7 +50,8 @@ export class HoverWidget extends Widget { options: IHoverOptions, @IKeybindingService private readonly _keybindingService: IKeybindingService, @IConfigurationService private readonly _configurationService: IConfigurationService, - @IOpenerService private readonly _openerService: IOpenerService + @IOpenerService private readonly _openerService: IOpenerService, + @IWorkbenchLayoutService private readonly _workbenchLayoutService: IWorkbenchLayoutService, ) { super(); @@ -76,7 +79,8 @@ export class HoverWidget extends Widget { const rowElement = $('div.hover-row.markdown-hover'); const contentsElement = $('div.hover-contents'); - const markdownElement = renderMarkdown(options.text, { + const markdown = typeof options.text === 'string' ? new MarkdownString().appendText(options.text) : options.text; + const markdownElement = renderMarkdown(markdown, { actionHandler: { callback: (content) => this._linkHandler(content), disposeables: this._messageListeners @@ -116,7 +120,20 @@ export class HoverWidget extends Widget { } const mouseTrackerTargets = [...this._target.targetElements]; - if (!options.hideOnHover || (options.actions && options.actions.length > 0)) { + let hideOnHover: boolean; + if (options.hideOnHover === undefined) { + if (options.actions && options.actions.length > 0) { + // If there are actions, require hover so they can be accessed + hideOnHover = false; + } else { + // Defaults to true when string, false when markdown as it may contain links + hideOnHover = typeof options.text === 'string'; + } + } else { + // It's set explicitly + hideOnHover = options.hideOnHover; + } + if (!hideOnHover) { mouseTrackerTargets.push(this._hover.containerDomNode); } this._mouseTracker = new CompositeMouseTracker(mouseTrackerTargets); @@ -141,7 +158,7 @@ export class HoverWidget extends Widget { // Get horizontal alignment and position let targetLeft = this._target.x !== undefined ? this._target.x : Math.min(...targetBounds.map(e => e.left)); if (targetLeft + this._hover.containerDomNode.clientWidth >= document.documentElement.clientWidth) { - this._x = document.documentElement.clientWidth - 1; + this._x = document.documentElement.clientWidth - this._workbenchLayoutService.getWindowBorderWidth() - 1; this._hover.containerDomNode.classList.add('right-aligned'); } else { this._x = targetLeft; diff --git a/src/vs/workbench/services/keybinding/browser/keybindingService.ts b/src/vs/workbench/services/keybinding/browser/keybindingService.ts index d459378097..fe4a710bac 100644 --- a/src/vs/workbench/services/keybinding/browser/keybindingService.ts +++ b/src/vs/workbench/services/keybinding/browser/keybindingService.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import * as browser from 'vs/base/browser/browser'; import * as dom from 'vs/base/browser/dom'; -import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { printKeyboardEvent, printStandardKeyboardEvent, StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { Emitter, Event } from 'vs/base/common/event'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { Keybinding, ResolvedKeybinding, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; @@ -31,7 +31,7 @@ import { IUserKeybindingItem, KeybindingIO, OutputBuilder } from 'vs/workbench/s import { IKeyboardMapper } from 'vs/workbench/services/keybinding/common/keyboardMapper'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { MenuRegistry } from 'vs/platform/actions/common/actions'; +import { Action2, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { commandsExtensionPoint } from 'vs/workbench/api/common/menusExtensionPoint'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -48,6 +48,8 @@ import { ScanCode, ScanCodeUtils, IMMUTABLE_CODE_TO_KEY_CODE } from 'vs/base/com import { flatten } from 'vs/base/common/arrays'; import { BrowserFeatures, KeyboardSupport } from 'vs/base/browser/canIUse'; import { ILogService } from 'vs/platform/log/common/log'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; interface ContributedKeyBinding { command: string; @@ -194,7 +196,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService { @ILogService logService: ILogService, @IKeymapService private readonly keymapService: IKeymapService ) { - super(contextKeyService, commandService, telemetryService, notificationService); + super(contextKeyService, commandService, telemetryService, notificationService, logService); this.updateSchema(); @@ -236,7 +238,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService { let keybindings: IKeybindingRule2[] = []; for (let extension of extensions) { - this._handleKeybindingsExtensionPointUser(extension.description.isBuiltin, extension.value, extension.collector, keybindings); + this._handleKeybindingsExtensionPointUser(extension.description.identifier, extension.description.isBuiltin, extension.value, extension.collector, keybindings); } KeybindingsRegistry.setExtensionKeybindings(keybindings); @@ -247,8 +249,10 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService { this._register(extensionService.onDidRegisterExtensions(() => this.updateSchema())); this._register(dom.addDisposableListener(window, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { - let keyEvent = new StandardKeyboardEvent(e); - let shouldPreventDefault = this._dispatch(keyEvent, keyEvent.target); + const keyEvent = new StandardKeyboardEvent(e); + this._log(`/ Received keydown event - ${printKeyboardEvent(e)}`); + this._log(`| Converted keydown event - ${printStandardKeyboardEvent(keyEvent)}`); + const shouldPreventDefault = this._dispatch(keyEvent, keyEvent.target); if (shouldPreventDefault) { keyEvent.preventDefault(); } @@ -345,7 +349,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService { if (!this._cachedResolver) { const defaults = this._resolveKeybindingItems(KeybindingsRegistry.getDefaultKeybindings(), true); const overrides = this._resolveUserKeybindingItems(this.userKeybindings.keybindings.map((k) => KeybindingIO.readUserKeybindingItem(k)), false); - this._cachedResolver = new KeybindingResolver(defaults, overrides); + this._cachedResolver = new KeybindingResolver(defaults, overrides, (str) => this._log(str)); } return this._cachedResolver; } @@ -364,7 +368,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService { const keybinding = item.keybinding; if (!keybinding) { // This might be a removal keybinding item in user settings => accept it - result[resultLen++] = new ResolvedKeybindingItem(undefined, item.command, item.commandArgs, when, isDefault); + result[resultLen++] = new ResolvedKeybindingItem(undefined, item.command, item.commandArgs, when, isDefault, item.extensionId); } else { if (this._assertBrowserConflicts(keybinding, item.command)) { continue; @@ -373,7 +377,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService { const resolvedKeybindings = this.resolveKeybinding(keybinding); for (let i = resolvedKeybindings.length - 1; i >= 0; i--) { const resolvedKeybinding = resolvedKeybindings[i]; - result[resultLen++] = new ResolvedKeybindingItem(resolvedKeybinding, item.command, item.commandArgs, when, isDefault); + result[resultLen++] = new ResolvedKeybindingItem(resolvedKeybinding, item.command, item.commandArgs, when, isDefault, item.extensionId); } } } @@ -388,11 +392,11 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService { const parts = item.parts; if (parts.length === 0) { // This might be a removal keybinding item in user settings => accept it - result[resultLen++] = new ResolvedKeybindingItem(undefined, item.command, item.commandArgs, when, isDefault); + result[resultLen++] = new ResolvedKeybindingItem(undefined, item.command, item.commandArgs, when, isDefault, null); } else { const resolvedKeybindings = this._keyboardMapper.resolveUserBinding(parts); for (const resolvedKeybinding of resolvedKeybindings) { - result[resultLen++] = new ResolvedKeybindingItem(resolvedKeybinding, item.command, item.commandArgs, when, isDefault); + result[resultLen++] = new ResolvedKeybindingItem(resolvedKeybinding, item.command, item.commandArgs, when, isDefault, null); } } } @@ -481,22 +485,22 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService { return this._keyboardMapper.resolveUserBinding(parts); } - private _handleKeybindingsExtensionPointUser(isBuiltin: boolean, keybindings: ContributedKeyBinding | ContributedKeyBinding[], collector: ExtensionMessageCollector, result: IKeybindingRule2[]): void { + private _handleKeybindingsExtensionPointUser(extensionId: ExtensionIdentifier, isBuiltin: boolean, keybindings: ContributedKeyBinding | ContributedKeyBinding[], collector: ExtensionMessageCollector, result: IKeybindingRule2[]): void { if (isContributedKeyBindingsArray(keybindings)) { for (let i = 0, len = keybindings.length; i < len; i++) { - this._handleKeybinding(isBuiltin, i + 1, keybindings[i], collector, result); + this._handleKeybinding(extensionId, isBuiltin, i + 1, keybindings[i], collector, result); } } else { - this._handleKeybinding(isBuiltin, 1, keybindings, collector, result); + this._handleKeybinding(extensionId, isBuiltin, 1, keybindings, collector, result); } } - private _handleKeybinding(isBuiltin: boolean, idx: number, keybindings: ContributedKeyBinding, collector: ExtensionMessageCollector, result: IKeybindingRule2[]): void { + private _handleKeybinding(extensionId: ExtensionIdentifier, isBuiltin: boolean, idx: number, keybindings: ContributedKeyBinding, collector: ExtensionMessageCollector, result: IKeybindingRule2[]): void { let rejects: string[] = []; if (isValidContributedKeyBinding(keybindings, rejects)) { - let rule = this._asCommandRule(isBuiltin, idx++, keybindings); + let rule = this._asCommandRule(extensionId, isBuiltin, idx++, keybindings); if (rule) { result.push(rule); } @@ -512,7 +516,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService { } } - private _asCommandRule(isBuiltin: boolean, idx: number, binding: ContributedKeyBinding): IKeybindingRule2 | undefined { + private _asCommandRule(extensionId: ExtensionIdentifier, isBuiltin: boolean, idx: number, binding: ContributedKeyBinding): IKeybindingRule2 | undefined { let { command, args, when, key, mac, linux, win } = binding; @@ -542,7 +546,8 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService { primary: KeybindingParser.parseKeybinding(key, OS), mac: mac ? { primary: KeybindingParser.parseKeybinding(mac, OS) } : null, linux: linux ? { primary: KeybindingParser.parseKeybinding(linux, OS) } : null, - win: win ? { primary: KeybindingParser.parseKeybinding(win, OS) } : null + win: win ? { primary: KeybindingParser.parseKeybinding(win, OS) } : null, + extensionId: extensionId.value }; if (!desc.primary && !desc.mac && !desc.linux && !desc.win) { @@ -740,6 +745,26 @@ let schema: IJSONSchema = { } }; +const preferencesCategory = nls.localize('preferences', "Preferences"); + +class ToggleKeybindingsLogAction extends Action2 { + + constructor() { + super({ + id: 'workbench.action.toggleKeybindingsLog', + title: { value: nls.localize('toggleKeybindingsLog', "Toggle Keyboard Shortcuts Troubleshooting"), original: 'Toggle Keyboard Shortcuts Troubleshooting' }, + category: preferencesCategory, + f1: true + }); + } + + run(accessor: ServicesAccessor): void { + accessor.get(IKeybindingService).toggleLogging(); + } +} + +registerAction2(ToggleKeybindingsLogAction); + let schemaRegistry = Registry.as(Extensions.JSONContribution); schemaRegistry.registerSchema(schemaId, schema); diff --git a/src/vs/workbench/services/keybinding/electron-browser/keybinding.contribution.ts b/src/vs/workbench/services/keybinding/electron-browser/keybinding.contribution.ts deleted file mode 100644 index c7c99d9493..0000000000 --- a/src/vs/workbench/services/keybinding/electron-browser/keybinding.contribution.ts +++ /dev/null @@ -1,37 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as nls from 'vs/nls'; -import { release } from 'os'; -import { OS, OperatingSystem } from 'vs/base/common/platform'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { Extensions as ConfigExtensions, IConfigurationNode, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; - -const configurationRegistry = Registry.as(ConfigExtensions.Configuration); -const keyboardConfiguration: IConfigurationNode = { - 'id': 'keyboard', - 'order': 15, - 'type': 'object', - 'title': nls.localize('keyboardConfigurationTitle', "Keyboard"), - 'properties': { - 'keyboard.touchbar.enabled': { - 'type': 'boolean', - 'default': true, - 'description': nls.localize('touchbar.enabled', "Enables the macOS touchbar buttons on the keyboard if available."), - 'included': OS === OperatingSystem.Macintosh && parseFloat(release()) >= 16 // Minimum: macOS Sierra (10.12.x = darwin 16.x) - }, - 'keyboard.touchbar.ignored': { - 'type': 'array', - 'items': { - 'type': 'string' - }, - 'default': [], - 'markdownDescription': nls.localize('touchbar.ignored', 'A set of identifiers for entries in the touchbar that should not show up (for example `workbench.action.navigateBack`.'), - 'included': OS === OperatingSystem.Macintosh && parseFloat(release()) >= 16 // Minimum: macOS Sierra (10.12.x = darwin 16.x) - } - } -}; - -configurationRegistry.registerConfiguration(keyboardConfiguration); diff --git a/src/vs/workbench/services/keybinding/electron-browser/nativeKeymapService.ts b/src/vs/workbench/services/keybinding/electron-browser/nativeKeymapService.ts index 13597f866d..7d2f4e7966 100644 --- a/src/vs/workbench/services/keybinding/electron-browser/nativeKeymapService.ts +++ b/src/vs/workbench/services/keybinding/electron-browser/nativeKeymapService.ts @@ -15,6 +15,7 @@ import { OS, OperatingSystem } from 'vs/base/common/platform'; import { WindowsKeyboardMapper, windowsKeyboardMappingEquals } from 'vs/workbench/services/keybinding/common/windowsKeyboardMapper'; import { MacLinuxKeyboardMapper, macLinuxKeyboardMappingEquals, IMacLinuxKeyboardMapping } from 'vs/workbench/services/keybinding/common/macLinuxKeyboardMapper'; import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding'; +import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; export class KeyboardMapperFactory { public static readonly INSTANCE = new KeyboardMapperFactory(); @@ -134,7 +135,7 @@ export class KeyboardMapperFactory { class NativeKeymapService extends Disposable implements IKeymapService { public _serviceBrand: undefined; - private readonly _onDidChangeKeyboardMapper = new Emitter(); + private readonly _onDidChangeKeyboardMapper = this._register(new Emitter()); public readonly onDidChangeKeyboardMapper: Event = this._onDidChangeKeyboardMapper.event; constructor() { @@ -143,6 +144,10 @@ class NativeKeymapService extends Disposable implements IKeymapService { this._register(KeyboardMapperFactory.INSTANCE.onDidChangeKeyboardMapper(() => { this._onDidChangeKeyboardMapper.fire(); })); + + ipcRenderer.on('vscode:keyboardLayoutChanged', () => { + KeyboardMapperFactory.INSTANCE._onKeyboardLayoutChanged(); + }); } getKeyboardMapper(dispatchConfig: DispatchConfig): IKeyboardMapper { 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 8436af0817..ea88c99e84 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 @@ -312,7 +312,7 @@ suite('KeybindingsEditing', () => { } } const keybinding = parts.length > 0 ? new USLayoutResolvedKeybinding(new ChordKeybinding(parts), OS) : undefined; - return new ResolvedKeybindingItem(keybinding, command || 'some command', null, when ? ContextKeyExpr.deserialize(when) : undefined, isDefault === undefined ? true : isDefault); + return new ResolvedKeybindingItem(keybinding, command || 'some command', null, when ? ContextKeyExpr.deserialize(when) : undefined, isDefault === undefined ? true : isDefault, null); } }); diff --git a/src/vs/workbench/services/layout/browser/layoutService.ts b/src/vs/workbench/services/layout/browser/layoutService.ts index 52248d72ea..eb37a3092a 100644 --- a/src/vs/workbench/services/layout/browser/layoutService.ts +++ b/src/vs/workbench/services/layout/browser/layoutService.ts @@ -144,6 +144,11 @@ export interface IWorkbenchLayoutService extends ILayoutService { */ hasWindowBorder(): boolean; + /** + * Returns the window border width. + */ + getWindowBorderWidth(): number; + /** * Returns the window border radius if any. */ diff --git a/src/vs/workbench/services/log/electron-browser/logService.ts b/src/vs/workbench/services/log/electron-browser/logService.ts index dcd1dde3d0..c82da6f1b1 100644 --- a/src/vs/workbench/services/log/electron-browser/logService.ts +++ b/src/vs/workbench/services/log/electron-browser/logService.ts @@ -14,7 +14,7 @@ import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions } f import { Registry } from 'vs/platform/registry/common/platform'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -export class DesktopLogService extends DelegatedLogService { +export class NativeLogService extends DelegatedLogService { private readonly bufferSpdLogService: BufferLogService | undefined; private readonly windowId: number; @@ -62,11 +62,11 @@ export class DesktopLogService extends DelegatedLogService { } } -class DesktopLogServiceInitContribution implements IWorkbenchContribution { +class NativeLogServiceInitContribution implements IWorkbenchContribution { constructor(@ILogService logService: ILogService) { - if (logService instanceof DesktopLogService) { + if (logService instanceof NativeLogService) { logService.init(); } } } -Registry.as(Extensions.Workbench).registerWorkbenchContribution(DesktopLogServiceInitContribution, LifecyclePhase.Restored); +Registry.as(Extensions.Workbench).registerWorkbenchContribution(NativeLogServiceInitContribution, LifecyclePhase.Restored); diff --git a/src/vs/workbench/services/path/common/pathService.ts b/src/vs/workbench/services/path/common/pathService.ts index 580f284440..d363203bd9 100644 --- a/src/vs/workbench/services/path/common/pathService.ts +++ b/src/vs/workbench/services/path/common/pathService.ts @@ -9,7 +9,7 @@ import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -export const IPathService = createDecorator('path'); +export const IPathService = createDecorator('pathService'); /** * Provides access to path related properties that will match the diff --git a/src/vs/workbench/services/preferences/common/keybindingsEditorModel.ts b/src/vs/workbench/services/preferences/common/keybindingsEditorModel.ts index e7ddb63035..d4564aeae1 100644 --- a/src/vs/workbench/services/preferences/common/keybindingsEditorModel.ts +++ b/src/vs/workbench/services/preferences/common/keybindingsEditorModel.ts @@ -176,7 +176,7 @@ export class KeybindingsEditorModel extends EditorModel { const commandsWithDefaultKeybindings = this.keybindingsService.getDefaultKeybindings().map(keybinding => keybinding.command); for (const command of KeybindingResolver.getAllUnboundCommands(boundCommands)) { - const keybindingItem = new ResolvedKeybindingItem(undefined, command, null, undefined, commandsWithDefaultKeybindings.indexOf(command) === -1); + const keybindingItem = new ResolvedKeybindingItem(undefined, command, null, undefined, commandsWithDefaultKeybindings.indexOf(command) === -1, null); this._keybindingItemsSortedByPrecedence.push(KeybindingsEditorModel.toKeybindingEntry(command, keybindingItem, workbenchActionsRegistry, actionLabels)); } this._keybindingItems = this._keybindingItemsSortedByPrecedence.slice(0).sort((a, b) => KeybindingsEditorModel.compareKeybindingData(a, b)); diff --git a/src/vs/workbench/services/preferences/test/common/keybindingsEditorModel.test.ts b/src/vs/workbench/services/preferences/test/common/keybindingsEditorModel.test.ts index c00504ee01..d9c34ee09a 100644 --- a/src/vs/workbench/services/preferences/test/common/keybindingsEditorModel.test.ts +++ b/src/vs/workbench/services/preferences/test/common/keybindingsEditorModel.test.ts @@ -658,7 +658,7 @@ suite('KeybindingsEditorModel', () => { } } const keybinding = parts.length > 0 ? new USLayoutResolvedKeybinding(new ChordKeybinding(parts), OS) : undefined; - return new ResolvedKeybindingItem(keybinding, command || 'some command', null, when ? ContextKeyExpr.deserialize(when) : undefined, isDefault === undefined ? true : isDefault); + return new ResolvedKeybindingItem(keybinding, command || 'some command', null, when ? ContextKeyExpr.deserialize(when) : undefined, isDefault === undefined ? true : isDefault, null); } function asResolvedKeybindingItems(keybindingEntries: IKeybindingItemEntry[], keepUnassigned: boolean = false): ResolvedKeybindingItem[] { diff --git a/src/vs/workbench/services/search/common/replace.ts b/src/vs/workbench/services/search/common/replace.ts index 79c23933b0..a9e627f7a6 100644 --- a/src/vs/workbench/services/search/common/replace.ts +++ b/src/vs/workbench/services/search/common/replace.ts @@ -13,6 +13,7 @@ export class ReplacePattern { private _replacePattern: string; private _hasParameters: boolean = false; private _regExp: RegExp; + private _caseOpsRegExp: RegExp; constructor(replaceString: string, searchPatternInfo: IPatternInfo) constructor(replaceString: string, parseParameters: boolean, regEx: RegExp) @@ -37,6 +38,8 @@ export class ReplacePattern { if (this._regExp.global) { this._regExp = strings.createRegExp(this._regExp.source, true, { matchCase: !this._regExp.ignoreCase, wholeWord: false, multiline: this._regExp.multiline, global: false }); } + + this._caseOpsRegExp = new RegExp(/([^\\]*?)((?:\\[uUlL])+?|)(\$[0-9]+)(.*?)/g); } get hasParameters(): boolean { @@ -60,10 +63,10 @@ export class ReplacePattern { const match = this._regExp.exec(text); if (match) { if (this.hasParameters) { + const replaceString = this.replaceWithCaseOperations(text, this._regExp, this.buildReplaceString(match, preserveCase)); if (match[0] === text) { - return text.replace(this._regExp, this.buildReplaceString(match, preserveCase)); + return replaceString; } - const replaceString = text.replace(this._regExp, this.buildReplaceString(match, preserveCase)); return replaceString.substr(match.index, match[0].length - (text.length - replaceString.length)); } return this.buildReplaceString(match, preserveCase); @@ -72,6 +75,84 @@ export class ReplacePattern { return null; } + /** + * replaceWithCaseOperations applies case operations to relevant replacement strings and applies + * the affected $N arguments. It then passes unaffected $N arguments through to string.replace(). + * + * \u => upper-cases one character in a match. + * \U => upper-cases ALL remaining characters in a match. + * \l => lower-cases one character in a match. + * \L => lower-cases ALL remaining characters in a match. + */ + private replaceWithCaseOperations(text: string, regex: RegExp, replaceString: string): string { + // Short-circuit the common path. + if (!/\\[uUlL]/.test(replaceString)) { + return text.replace(regex, replaceString); + } + // Store the values of the search parameters. + const firstMatch = regex.exec(text); + if (firstMatch === null) { + return text.replace(regex, replaceString); + } + + let patMatch: RegExpExecArray | null; + let newReplaceString = ''; + let lastIndex = 0; + let lastMatch = ''; + // For each annotated $N, perform text processing on the parameters and perform the substitution. + while ((patMatch = this._caseOpsRegExp.exec(replaceString)) !== null) { + lastIndex = patMatch.index; + const fullMatch = patMatch[0]; + lastMatch = fullMatch; + let caseOps = patMatch[2]; // \u, \l\u, etc. + const money = patMatch[3]; // $1, $2, etc. + + if (!caseOps) { + newReplaceString += fullMatch; + continue; + } + const replacement = firstMatch[parseInt(money.slice(1))]; + if (!replacement) { + newReplaceString += fullMatch; + continue; + } + const replacementLen = replacement.length; + + newReplaceString += patMatch[1]; // prefix + caseOps = caseOps.replace(/\\/g, ''); + let i = 0; + for (; i < caseOps.length; i++) { + switch (caseOps[i]) { + case 'U': + newReplaceString += replacement.slice(i).toUpperCase(); + i = replacementLen; + break; + case 'u': + newReplaceString += replacement[i].toUpperCase(); + break; + case 'L': + newReplaceString += replacement.slice(i).toLowerCase(); + i = replacementLen; + break; + case 'l': + newReplaceString += replacement[i].toLowerCase(); + break; + } + } + // Append any remaining replacement string content not covered by case operations. + if (i < replacementLen) { + newReplaceString += replacement.slice(i); + } + + newReplaceString += patMatch[4]; // suffix + } + + // Append any remaining trailing content after the final regex match. + newReplaceString += replaceString.slice(lastIndex + lastMatch.length); + + return text.replace(regex, newReplaceString); + } + public buildReplaceString(matches: string[] | null, preserveCase?: boolean): string { if (preserveCase) { return buildReplaceStringWithCasePreserved(matches, this._replacePattern); diff --git a/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts b/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts index 610403ec96..9231388bf2 100644 --- a/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts +++ b/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts @@ -504,7 +504,7 @@ export function spreadGlobComponents(globArg: string): string[] { export function unicodeEscapesToPCRE2(pattern: string): string { // Match \u1234 - const unicodePattern = /((?:[^\\]|^)(?:\\\\)*)\\u([a-z0-9]{4})/g; + const unicodePattern = /((?:[^\\]|^)(?:\\\\)*)\\u([a-z0-9]{4})/gi; while (pattern.match(unicodePattern)) { pattern = pattern.replace(unicodePattern, `$1\\x{$2}`); @@ -512,7 +512,7 @@ export function unicodeEscapesToPCRE2(pattern: string): string { // Match \u{1234} // \u with 5-6 characters will be left alone because \x only takes 4 characters. - const unicodePatternWithBraces = /((?:[^\\]|^)(?:\\\\)*)\\u\{([a-z0-9]{4})\}/g; + const unicodePatternWithBraces = /((?:[^\\]|^)(?:\\\\)*)\\u\{([a-z0-9]{4})\}/gi; while (pattern.match(unicodePatternWithBraces)) { pattern = pattern.replace(unicodePatternWithBraces, `$1\\x{$2}`); } diff --git a/src/vs/workbench/services/search/test/common/replace.test.ts b/src/vs/workbench/services/search/test/common/replace.test.ts index 25ba2063d2..b7ebcd1322 100644 --- a/src/vs/workbench/services/search/test/common/replace.test.ts +++ b/src/vs/workbench/services/search/test/common/replace.test.ts @@ -140,6 +140,12 @@ suite('Replace Pattern test', () => { assert.equal('cat ()', actual); }); + test('case operations', () => { + let testObject = new ReplacePattern('a\\u$1l\\u\\l\\U$2M$3n', { pattern: 'a(l)l(good)m(e)n', isRegExp: true }); + let actual = testObject.getReplaceString('allgoodmen'); + assert.equal('aLlGoODMen', actual); + }); + test('get replace string for no matches', () => { let testObject = new ReplacePattern('hello', { pattern: 'bla', isRegExp: true }); let actual = testObject.getReplaceString('foo'); diff --git a/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngine.test.ts b/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngine.test.ts index d8dd09b803..8acbeb8504 100644 --- a/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngine.test.ts +++ b/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngine.test.ts @@ -20,6 +20,7 @@ suite('RipgrepTextSearchEngine', () => { assert.equal(unicodeEscapesToPCRE2('\\u{1234}'), '\\x{1234}'); assert.equal(unicodeEscapesToPCRE2('\\u{1234}\\u{0001}'), '\\x{1234}\\x{0001}'); assert.equal(unicodeEscapesToPCRE2('foo\\u{1234}bar'), 'foo\\x{1234}bar'); + assert.equal(unicodeEscapesToPCRE2('[\\u00A0-\\u00FF]'), '[\\x{00A0}-\\x{00FF}]'); assert.equal(unicodeEscapesToPCRE2('foo\\u{123456}7bar'), 'foo\\u{123456}7bar'); assert.equal(unicodeEscapesToPCRE2('\\u123'), '\\u123'); diff --git a/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts b/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts index 3b7ebe7159..b0557ab0ce 100644 --- a/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts +++ b/src/vs/workbench/services/textMate/browser/abstractTextMateService.ts @@ -24,7 +24,7 @@ import { ExtensionMessageCollector } from 'vs/workbench/services/extensions/comm import { ITMSyntaxExtensionPoint, grammarsExtPoint } from 'vs/workbench/services/textMate/common/TMGrammars'; import { ITextMateService } from 'vs/workbench/services/textMate/common/textMateService'; import { ITextMateThemingRule, IWorkbenchThemeService, IWorkbenchColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; -import { IGrammar, StackElement, IOnigLib, IRawTheme } from 'vscode-textmate'; +import type { IGrammar, StackElement, IOnigLib, IRawTheme } from 'vscode-textmate'; import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IValidGrammarDefinition, IValidEmbeddedLanguagesMap, IValidTokenTypeMap } from 'vs/workbench/services/textMate/common/TMScopeRegistry'; diff --git a/src/vs/workbench/services/textMate/common/TMGrammarFactory.ts b/src/vs/workbench/services/textMate/common/TMGrammarFactory.ts index d05c97f7f0..64ddaed8ea 100644 --- a/src/vs/workbench/services/textMate/common/TMGrammarFactory.ts +++ b/src/vs/workbench/services/textMate/common/TMGrammarFactory.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import { LanguageId } from 'vs/editor/common/modes'; -import { IGrammar, Registry, StackElement, IRawTheme, IOnigLib } from 'vscode-textmate'; +import type { IGrammar, Registry, StackElement, IRawTheme, IOnigLib } from 'vscode-textmate'; import { Disposable } from 'vs/base/common/lifecycle'; import { TMScopeRegistry, IValidGrammarDefinition, IValidEmbeddedLanguagesMap } from 'vs/workbench/services/textMate/common/TMScopeRegistry'; diff --git a/src/vs/workbench/services/textMate/electron-browser/textMateService.ts b/src/vs/workbench/services/textMate/electron-browser/textMateService.ts index 0d27b8f650..6ca325da18 100644 --- a/src/vs/workbench/services/textMate/electron-browser/textMateService.ts +++ b/src/vs/workbench/services/textMate/electron-browser/textMateService.ts @@ -13,7 +13,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { createWebWorker, MonacoWebWorker } from 'vs/editor/common/services/webWorker'; import { IModelService } from 'vs/editor/common/services/modelService'; -import { IRawTheme } from 'vscode-textmate'; +import type { IRawTheme } from 'vscode-textmate'; import { IValidGrammarDefinition } from 'vs/workbench/services/textMate/common/TMScopeRegistry'; import { TextMateWorker } from 'vs/workbench/services/textMate/electron-browser/textMateWorker'; import { ITextModel } from 'vs/editor/common/model'; diff --git a/src/vs/workbench/services/textMate/electron-browser/textMateWorker.ts b/src/vs/workbench/services/textMate/electron-browser/textMateWorker.ts index 5708546c33..93286fd2b1 100644 --- a/src/vs/workbench/services/textMate/electron-browser/textMateWorker.ts +++ b/src/vs/workbench/services/textMate/electron-browser/textMateWorker.ts @@ -11,7 +11,7 @@ import { TMGrammarFactory, ICreateGrammarResult } from 'vs/workbench/services/te import { IModelChangedEvent, MirrorTextModel } from 'vs/editor/common/model/mirrorTextModel'; import { TextMateWorkerHost } from 'vs/workbench/services/textMate/electron-browser/textMateService'; import { TokenizationStateStore } from 'vs/editor/common/model/textModelTokens'; -import { IGrammar, StackElement, IRawTheme, IOnigLib } from 'vscode-textmate'; +import type { IGrammar, StackElement, IRawTheme, IOnigLib } from 'vscode-textmate'; import { MultilineTokensBuilder, countEOL } from 'vs/editor/common/model/tokensStore'; import { LineTokens } from 'vs/editor/common/core/lineTokens'; diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index 6373e49041..88ba2b30bc 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -19,7 +19,7 @@ import { ITextBufferFactory, ITextModel } from 'vs/editor/common/model'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ILogService } from 'vs/platform/log/common/log'; import { basename } from 'vs/base/common/path'; -import { IWorkingCopyService, IWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopyService, IWorkingCopyBackup, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { ILabelService } from 'vs/platform/label/common/label'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; @@ -65,7 +65,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil //#endregion - readonly capabilities = 0; + readonly capabilities = WorkingCopyCapabilities.None; readonly name = basename(this.labelService.getUriLabel(this.resource)); @@ -86,7 +86,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil private inConflictMode = false; private inOrphanMode = false; private inErrorMode = false; - private disposed = false; constructor( public readonly resource: URI, @@ -147,7 +146,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // file is really gone and not just a faulty file event. await timeout(100); - if (this.disposed) { + if (this.isDisposed()) { newInOrphanModeValidated = true; } else { const exists = await this.fileService.exists(this.resource); @@ -155,7 +154,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } } - if (this.inOrphanMode !== newInOrphanModeValidated && !this.disposed) { + if (this.inOrphanMode !== newInOrphanModeValidated && !this.isDisposed()) { this.setOrphaned(newInOrphanModeValidated); } } @@ -507,7 +506,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil //#region Dirty - isDirty(): this is IResolvedTextFileEditorModel { + isDirty(): boolean { // {{SQL CARBON EDIT}} strict-null-checks return this.dirty; } @@ -697,7 +696,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // one after the other without waiting for the save() to complete. If we are disposed(), we risk // saving contents to disk that are stale (see https://github.com/Microsoft/vscode/issues/50942). // To fix this issue, we will not store the contents to disk when we got disposed. - if (this.disposed) { + if (this.isDisposed()) { return undefined; // {{SQL CARBON EDIT}} @anthonydresser strict-null-check } @@ -947,7 +946,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil //#endregion - isResolved(): this is IResolvedTextFileEditorModel { + isResolved(): boolean { // {{SQL CARBON EDIT}} strict-null-checks return !!this.textEditorModel; } @@ -955,10 +954,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil return this.fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly); } - isDisposed(): boolean { - return this.disposed; - } - getStat(): IFileStatWithMetadata | undefined { return this.lastResolvedFileStat; } @@ -966,7 +961,6 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil dispose(): void { this.logService.trace('[text file model] dispose()', this.resource.toString(true)); - this.disposed = true; this.inConflictMode = false; this.inOrphanMode = false; this.inErrorMode = false; diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index 49f01b9a8a..ea691e70df 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -12,7 +12,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { ITextEditorModel } from 'vs/editor/common/services/resolverService'; import { ITextBufferFactory, ITextModel, ITextSnapshot } from 'vs/editor/common/model'; import { VSBuffer, VSBufferReadable } from 'vs/base/common/buffer'; -import { isUndefinedOrNull } from 'vs/base/common/types'; +import { areFunctions, isUndefinedOrNull } from 'vs/base/common/types'; import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IUntitledTextEditorModelManager } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -425,7 +425,12 @@ export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport getMode(): string | undefined; isResolved(): this is IResolvedTextFileEditorModel; - isDisposed(): boolean; +} + +export function isTextFileEditorModel(model: ITextEditorModel): model is ITextFileEditorModel { + const candidate = model as ITextFileEditorModel; + + return areFunctions(candidate.setEncoding, candidate.getEncoding, candidate.save, candidate.revert, candidate.isDirty, candidate.getMode); } export interface IResolvedTextFileEditorModel extends ITextFileEditorModel { diff --git a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts index e247ac9331..45901847d7 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { EncodingMode } from 'vs/workbench/common/editor'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; -import { TextFileEditorModelState, snapshotToString } from 'vs/workbench/services/textfile/common/textfiles'; +import { TextFileEditorModelState, snapshotToString, isTextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; import { createFileEditorInput, workbenchInstantiationService, TestServiceAccessor, TestReadonlyTextFileEditorModel } from 'vs/workbench/test/browser/workbenchTestServices'; import { toResource } from 'vs/base/test/common/utils'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; @@ -73,6 +73,14 @@ suite('Files - TextFileEditorModel', () => { model.dispose(); }); + test('isTextFileEditorModel', async function () { + const model = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); + + assert.equal(isTextFileEditorModel(model), true); + + model.dispose(); + }); + test('save', async function () { const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined); diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index cac8848b30..15e4d64c36 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -33,6 +33,7 @@ import { updateColorThemeConfigurationSchemas, updateFileIconThemeConfigurationS import { ProductIconThemeData, DEFAULT_PRODUCT_ICON_THEME_ID } from 'vs/workbench/services/themes/browser/productIconThemeData'; import { registerProductIconThemeSchemas } from 'vs/workbench/services/themes/common/productIconThemeSchema'; import { ILogService } from 'vs/platform/log/common/log'; +import { isWeb } from 'vs/base/common/platform'; // implementation @@ -92,20 +93,24 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { private readonly onProductIconThemeChange: Emitter; private readonly productIconThemeWatcher: ThemeFileWatcher; + private isOSInHighContrast: boolean; // tracking the high contrast state of the OS eventauilly should go out to a seperate service + private themeSettingIdBeforeSchemeSwitch: string | undefined; + constructor( @IExtensionService extensionService: IExtensionService, @IStorageService private readonly storageService: IStorageService, @IConfigurationService private readonly configurationService: IConfigurationService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IWorkbenchEnvironmentService readonly environmentService: IWorkbenchEnvironmentService, @IFileService private readonly fileService: IFileService, @IExtensionResourceLoaderService private readonly extensionResourceLoaderService: IExtensionResourceLoaderService, @IWorkbenchLayoutService readonly layoutService: IWorkbenchLayoutService, @ILogService private readonly logService: ILogService ) { this.container = layoutService.container; - const defaultThemeType = environmentService.configuration.defaultThemeType || LIGHT; // {{SQL CARBON EDIT}} default to light theme - this.settings = new ThemeConfiguration(configurationService, defaultThemeType); + this.settings = new ThemeConfiguration(configurationService); + + this.isOSInHighContrast = !!environmentService.configuration.highContrast; this.colorThemeRegistry = new ThemeRegistry(extensionService, colorThemesExtPoint, ColorThemeData.fromExtensionTheme); this.colorThemeWatcher = new ThemeFileWatcher(fileService, environmentService, this.reloadCurrentColorTheme.bind(this)); @@ -126,11 +131,21 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { // themes are loaded asynchronously, we need to initialize // a color theme document with good defaults until the theme is loaded let themeData: ColorThemeData | undefined = ColorThemeData.fromStorageData(this.storageService); - if (environmentService.configuration.highContrast && themeData?.baseTheme !== HIGH_CONTRAST) { - themeData = ColorThemeData.createUnloadedThemeForThemeType(HIGH_CONTRAST); + + // the preferred color scheme (high contrast, light, dark) has changed since the last start + const preferredColorScheme = this.getPreferredColorScheme(); + + if (preferredColorScheme && themeData?.type !== preferredColorScheme && this.storageService.get(PERSISTED_OS_COLOR_SCHEME, StorageScope.GLOBAL) !== preferredColorScheme) { + themeData = ColorThemeData.createUnloadedThemeForThemeType(preferredColorScheme); } if (!themeData) { - themeData = ColorThemeData.createUnloadedThemeForThemeType(defaultThemeType); + const initialColorTheme = environmentService.options?.initialColorTheme; + if (initialColorTheme) { + themeData = ColorThemeData.createUnloadedThemeForThemeType(initialColorTheme.themeType, initialColorTheme.colors); + } + } + if (!themeData) { + themeData = ColorThemeData.createUnloadedThemeForThemeType(isWeb ? LIGHT : DARK); } themeData.setCustomizations(this.settings); this.applyTheme(themeData, undefined, true); @@ -163,10 +178,13 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { } 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(); - if (persistedColorScheme && preferredColorScheme && persistedColorScheme !== preferredColorScheme) { - return this.applyPreferredColorTheme(preferredColorScheme); + const prevScheme = this.storageService.get(PERSISTED_OS_COLOR_SCHEME, StorageScope.GLOBAL); + if (preferredColorScheme !== prevScheme) { + this.storageService.store(PERSISTED_OS_COLOR_SCHEME, preferredColorScheme, StorageScope.GLOBAL); + if (preferredColorScheme && theme?.type !== preferredColorScheme) { + return this.applyPreferredColorTheme(preferredColorScheme); + } } return this.setColorTheme(theme && theme.id, undefined); }; @@ -197,7 +215,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { if (e.affectsConfiguration(ThemeSettings.COLOR_THEME)) { this.restoreColorTheme(); } - if (e.affectsConfiguration(ThemeSettings.DETECT_COLOR_SCHEME)) { + if (e.affectsConfiguration(ThemeSettings.DETECT_COLOR_SCHEME) || e.affectsConfiguration(ThemeSettings.DETECT_HC)) { this.handlePreferredSchemeUpdated(); } if (e.affectsConfiguration(ThemeSettings.PREFERRED_DARK_THEME) && this.getPreferredColorScheme() === DARK) { @@ -311,18 +329,38 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { window.matchMedia('(prefers-color-scheme: dark)').addListener(async () => this.handlePreferredSchemeUpdated()); } + public setOSHighContrast(highContrast: boolean): void { + if (this.isOSInHighContrast !== highContrast) { + this.isOSInHighContrast = highContrast; + this.handlePreferredSchemeUpdated(); + } + } + private async handlePreferredSchemeUpdated() { const scheme = this.getPreferredColorScheme(); - this.storageService.store(PERSISTED_OS_COLOR_SCHEME, scheme, StorageScope.GLOBAL); - if (scheme) { - return this.applyPreferredColorTheme(scheme); + const prevScheme = this.storageService.get(PERSISTED_OS_COLOR_SCHEME, StorageScope.GLOBAL); + if (scheme !== prevScheme) { + this.storageService.store(PERSISTED_OS_COLOR_SCHEME, scheme, StorageScope.GLOBAL); + if (scheme) { + if (!prevScheme) { + // remember the theme before scheme switching + this.themeSettingIdBeforeSchemeSwitch = this.settings.colorTheme; + } + return this.applyPreferredColorTheme(scheme); + } else if (prevScheme && this.themeSettingIdBeforeSchemeSwitch) { + // reapply the theme before scheme switching + const theme = await this.colorThemeRegistry.findThemeBySettingsId(this.themeSettingIdBeforeSchemeSwitch, undefined); + if (theme) { + this.setColorTheme(theme.id, 'auto'); + } + } } return undefined; } private getPreferredColorScheme(): ThemeType | undefined { const detectHCThemeSetting = this.configurationService.getValue(ThemeSettings.DETECT_HC); - if (this.environmentService.configuration.highContrast && detectHCThemeSetting) { + if (this.isOSInHighContrast && detectHCThemeSetting) { return HIGH_CONTRAST; } if (this.configurationService.getValue(ThemeSettings.DETECT_COLOR_SCHEME)) { @@ -341,7 +379,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { if (themeSettingId) { const theme = await this.colorThemeRegistry.findThemeBySettingsId(themeSettingId, undefined); if (theme) { - return this.setColorTheme(theme.id, 'auto'); + return this.setColorTheme(theme.id, ConfigurationTarget.USER); } } return null; @@ -400,7 +438,6 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { return false; } - private updateDynamicCSSRules(themeData: IColorTheme) { const cssRules = new Set(); const ruleCollector = { diff --git a/src/vs/workbench/services/themes/common/colorThemeData.ts b/src/vs/workbench/services/themes/common/colorThemeData.ts index 04b8be28e4..c2752c4e57 100644 --- a/src/vs/workbench/services/themes/common/colorThemeData.ts +++ b/src/vs/workbench/services/themes/common/colorThemeData.ts @@ -550,15 +550,20 @@ export class ColorThemeData implements IWorkbenchColorTheme { // constructors - static createUnloadedThemeForThemeType(themeType: ThemeType): ColorThemeData { - return ColorThemeData.createUnloadedTheme(getThemeTypeSelector(themeType)); + static createUnloadedThemeForThemeType(themeType: ThemeType, colorMap?: { [id: string]: string }): ColorThemeData { + return ColorThemeData.createUnloadedTheme(getThemeTypeSelector(themeType), colorMap); } - static createUnloadedTheme(id: string): ColorThemeData { + static createUnloadedTheme(id: string, colorMap?: { [id: string]: string }): ColorThemeData { let themeData = new ColorThemeData(id, '', '__' + id); themeData.isLoaded = false; themeData.themeTokenColors = []; themeData.watch = false; + if (colorMap) { + for (let id in colorMap) { + themeData.colorMap[id] = Color.fromHex(colorMap[id]); + } + } return themeData; } diff --git a/src/vs/workbench/services/themes/common/themeConfiguration.ts b/src/vs/workbench/services/themes/common/themeConfiguration.ts index 9c1e2f862f..12589c85c5 100644 --- a/src/vs/workbench/services/themes/common/themeConfiguration.ts +++ b/src/vs/workbench/services/themes/common/themeConfiguration.ts @@ -14,7 +14,7 @@ import { workbenchColorsSchemaId } from 'vs/platform/theme/common/colorRegistry' import { tokenStylingSchemaId } from 'vs/platform/theme/common/tokenClassificationRegistry'; import { ThemeSettings, IWorkbenchColorTheme, IWorkbenchFileIconTheme, IColorCustomizations, ITokenColorCustomizations, IWorkbenchProductIconTheme, ISemanticTokenColorCustomizations, IExperimentalSemanticTokenColorCustomizations } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; -import { ThemeType, HIGH_CONTRAST, LIGHT } from 'vs/platform/theme/common/themeService'; +import { isMacintosh, isWeb, isWindows } from 'vs/base/common/platform'; const DEFAULT_THEME_DARK_SETTING_VALUE = 'Default Dark Azure Data Studio'; // {{SQL CARBON EDIT}} replace default theme const DEFAULT_THEME_LIGHT_SETTING_VALUE = 'Default Light Azure Data Studio'; // {{SQL CARBON EDIT}} replace default theme @@ -33,7 +33,7 @@ const colorThemeSettingEnumDescriptions: string[] = []; const colorThemeSettingSchema: IConfigurationPropertySchema = { type: 'string', description: nls.localize('colorTheme', "Specifies the color theme used in the workbench."), - default: DEFAULT_THEME_DARK_SETTING_VALUE, + default: isWeb ? DEFAULT_THEME_LIGHT_SETTING_VALUE : DEFAULT_THEME_DARK_SETTING_VALUE, enum: colorThemeSettingEnum, enumDescriptions: colorThemeSettingEnumDescriptions, errorMessage: nls.localize('colorThemeError', "Theme is unknown or not installed."), @@ -60,6 +60,7 @@ const preferredHCThemeSettingSchema: IConfigurationPropertySchema = { default: DEFAULT_THEME_HC_SETTING_VALUE, enum: colorThemeSettingEnum, enumDescriptions: colorThemeSettingEnumDescriptions, + included: isWindows || isMacintosh, errorMessage: nls.localize('colorThemeError', "Theme is unknown or not installed."), }; const detectColorSchemeSettingSchema: IConfigurationPropertySchema = { @@ -110,6 +111,7 @@ const themeSettingsConfiguration: IConfigurationNode = { [ThemeSettings.PRODUCT_ICON_THEME]: productIconThemeSettingSchema } }; +configurationRegistry.registerConfiguration(themeSettingsConfiguration); function tokenGroupSettings(description: string): IJSONSchema { return { @@ -231,19 +233,7 @@ export function updateProductIconThemeConfigurationSchemas(themes: IWorkbenchPro export class ThemeConfiguration { - constructor(private configurationService: IConfigurationService, themeType: ThemeType) { - switch (themeType) { - case LIGHT: - colorThemeSettingSchema.default = DEFAULT_THEME_LIGHT_SETTING_VALUE; - break; - case HIGH_CONTRAST: - colorThemeSettingSchema.default = DEFAULT_THEME_HC_SETTING_VALUE; - break; - default: - colorThemeSettingSchema.default = DEFAULT_THEME_DARK_SETTING_VALUE; - break; - } - configurationRegistry.registerConfiguration(themeSettingsConfiguration); + constructor(private configurationService: IConfigurationService) { } public get colorTheme(): string { diff --git a/src/vs/workbench/services/themes/common/workbenchThemeService.ts b/src/vs/workbench/services/themes/common/workbenchThemeService.ts index f6d7c155c4..fc1cd7aa18 100644 --- a/src/vs/workbench/services/themes/common/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/common/workbenchThemeService.ts @@ -78,6 +78,7 @@ export interface IWorkbenchThemeService extends IThemeService { getProductIconThemes(): Promise; onDidProductIconThemeChange: Event; + setOSHighContrast(highContrast: boolean): void; } export interface IColorCustomizations { diff --git a/src/vs/workbench/services/url/electron-sandbox/urlService.ts b/src/vs/workbench/services/url/electron-sandbox/urlService.ts index 512afcef14..6f1db9deee 100644 --- a/src/vs/workbench/services/url/electron-sandbox/urlService.ts +++ b/src/vs/workbench/services/url/electron-sandbox/urlService.ts @@ -66,7 +66,7 @@ export class RelayURLService extends NativeURLService implements IURLHandler, IO const result = await super.open(uri, options); if (result) { - await this.electronService.focusWindow(); + await this.electronService.focusWindow({ force: true /* Application may not be active */ }); } return result; diff --git a/src/vs/workbench/services/userData/browser/userDataInit.ts b/src/vs/workbench/services/userData/browser/userDataInit.ts index 095812e579..e2bbb9bfab 100644 --- a/src/vs/workbench/services/userData/browser/userDataInit.ts +++ b/src/vs/workbench/services/userData/browser/userDataInit.ts @@ -30,6 +30,7 @@ export const IUserDataInitializationService = createDecorator; initializeRequiredResources(): Promise; initializeOtherResources(): Promise; initializeExtensions(instantiationService: IInstantiationService): Promise; @@ -105,6 +106,11 @@ export class UserDataInitializationService implements IUserDataInitializationSer return this._userDataSyncStoreClientPromise; } + async requiresInitialization(): Promise { + const userDataSyncStoreClient = await this.createUserDataSyncStoreClient(); + return !!userDataSyncStoreClient; + } + async initializeRequiredResources(): Promise { return this.initialize([SyncResource.Settings, SyncResource.GlobalState]); } diff --git a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts index 1141538d74..f095c9e4a9 100644 --- a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts +++ b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts @@ -66,11 +66,10 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat private static DONOT_USE_WORKBENCH_SESSION_STORAGE_KEY = 'userDataSyncAccount.donotUseWorkbenchSession'; private static CACHED_SESSION_STORAGE_KEY = 'userDataSyncAccountPreference'; - private _authenticationProviders: IAuthenticationProvider[] = []; - get enabled() { return this._authenticationProviders.length > 0; } + get enabled() { return !!this.userDataSyncStoreManagementService.userDataSyncStore; } - private availableAuthenticationProviders: IAuthenticationProvider[] = []; - get authenticationProviders() { return this.availableAuthenticationProviders; } + private _authenticationProviders: IAuthenticationProvider[] = []; + get authenticationProviders() { return this._authenticationProviders; } private _accountStatus: AccountStatus = AccountStatus.Uninitialized; get accountStatus(): AccountStatus { return this._accountStatus; } @@ -113,14 +112,13 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat @ILifecycleService private readonly lifecycleService: ILifecycleService, ) { super(); - this._authenticationProviders = this.userDataSyncStoreManagementService.userDataSyncStore?.authenticationProviders || []; this.syncEnablementContext = CONTEXT_SYNC_ENABLEMENT.bindTo(contextKeyService); this.syncStatusContext = CONTEXT_SYNC_STATE.bindTo(contextKeyService); this.accountStatusContext = CONTEXT_ACCOUNT_STATE.bindTo(contextKeyService); this.activityViewsEnablementContext = CONTEXT_ENABLE_ACTIVITY_VIEWS.bindTo(contextKeyService); this.mergesViewEnablementContext = CONTEXT_ENABLE_SYNC_MERGES_VIEW.bindTo(contextKeyService); - if (this._authenticationProviders.length) { + if (this.userDataSyncStoreManagementService.userDataSyncStore) { this.syncStatusContext.set(this.userDataSyncService.status); this._register(userDataSyncService.onDidChangeStatus(status => this.syncStatusContext.set(status))); this.syncEnablementContext.set(userDataAutoSyncService.isEnabled()); @@ -130,22 +128,28 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat } } + private updateAuthenticationProviders(): void { + this._authenticationProviders = (this.userDataSyncStoreManagementService.userDataSyncStore?.authenticationProviders || []).filter(({ id }) => this.authenticationService.declaredProviders.some(provider => provider.id === id)); + } + private isSupportedAuthenticationProviderId(authenticationProviderId: string): boolean { - return this._authenticationProviders.some(({ id }) => id === authenticationProviderId); + return this.authenticationProviders.some(({ id }) => id === authenticationProviderId); } private async waitAndInitialize(): Promise { await this.extensionService.whenInstalledExtensionsRegistered(); + this.updateAuthenticationProviders(); + /* activate unregistered providers */ - const unregisteredProviders = this._authenticationProviders.filter(({ id }) => !this.authenticationService.isAuthenticationProviderRegistered(id)); + const unregisteredProviders = this.authenticationProviders.filter(({ id }) => !this.authenticationService.isAuthenticationProviderRegistered(id)); if (unregisteredProviders.length) { await Promise.all(unregisteredProviders.map(({ id }) => this.extensionService.activateByEvent(getAuthenticationProviderActivationEvent(id)))); } - /* wait until all providers are availabe */ - if (this._authenticationProviders.some(({ id }) => !this.authenticationService.isAuthenticationProviderRegistered(id))) { - await Event.toPromise(Event.filter(this.authenticationService.onDidRegisterAuthenticationProvider, () => this._authenticationProviders.every(({ id }) => this.authenticationService.isAuthenticationProviderRegistered(id)))); + /* wait until all providers are registered */ + if (this.authenticationProviders.some(({ id }) => !this.authenticationService.isAuthenticationProviderRegistered(id))) { + await Event.toPromise(Event.filter(this.authenticationService.onDidRegisterAuthenticationProvider, () => this.authenticationProviders.every(({ id }) => this.authenticationService.isAuthenticationProviderRegistered(id)))); } /* initialize */ @@ -161,6 +165,8 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat await this.update(); + this._register(this.authenticationService.onDidChangeDeclaredProviders(() => this.updateAuthenticationProviders())); + this._register( Event.any( Event.filter( @@ -178,10 +184,10 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat private async update(): Promise { - this.availableAuthenticationProviders = this._authenticationProviders.filter(({ id }) => this.authenticationService.isAuthenticationProviderRegistered(id)); + this.updateAuthenticationProviders(); const allAccounts: Map = new Map(); - for (const { id } of this.availableAuthenticationProviders) { + for (const { id } of this.authenticationProviders) { const accounts = await this.getAccounts(id); allAccounts.set(id, accounts); } @@ -240,6 +246,9 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat } async turnOn(): Promise { + if (!this.authenticationProviders.length) { + throw new Error(localize('no authentication providers', "Settings sync cannot be turned on because there are no authentication providers available.")); + } if (this.userDataAutoSyncService.isEnabled()) { return; } @@ -498,15 +507,15 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat } private async doPick(): Promise { - if (this.availableAuthenticationProviders.length === 0) { + if (this.authenticationProviders.length === 0) { return undefined; } await this.update(); // Single auth provider and no accounts available - if (this.availableAuthenticationProviders.length === 1 && !this.all.length) { - return this.availableAuthenticationProviders[0]; + if (this.authenticationProviders.length === 1 && !this.all.length) { + return this.authenticationProviders[0]; } return new Promise(async (c, e) => { @@ -538,7 +547,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat // Signed in Accounts if (this.all.length) { - const authenticationProviders = [...this.availableAuthenticationProviders].sort(({ id }) => id === this.current?.authenticationProviderId ? -1 : 1); + const authenticationProviders = [...this.authenticationProviders].sort(({ id }) => id === this.current?.authenticationProviderId ? -1 : 1); quickPickItems.push({ type: 'separator', label: localize('signed in', "Signed in") }); for (const authenticationProvider of authenticationProviders) { const accounts = (this._all.get(authenticationProvider.id) || []).sort(({ sessionId }) => sessionId === this.current?.sessionId ? -1 : 1); @@ -556,7 +565,7 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat } // Account proviers - for (const authenticationProvider of this.availableAuthenticationProviders) { + for (const authenticationProvider of this.authenticationProviders) { const signedInForProvider = this.all.some(account => account.authenticationProviderId === authenticationProvider.id); if (!signedInForProvider || this.authenticationService.supportsMultipleAccounts(authenticationProvider.id)) { const providerName = this.authenticationService.getLabel(authenticationProvider.id); @@ -582,13 +591,15 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat this.currentSessionId = undefined; await this.update(); - this.notificationService.notify({ - severity: Severity.Error, - message: localize('successive auth failures', "Settings sync was turned off because of successive authorization failures. Please sign in again to continue synchronizing"), - actions: { - primary: [new Action('sign in', localize('sign in', "Sign in"), undefined, true, () => this.signIn())] - } - }); + if (this.userDataAutoSyncService.isEnabled()) { + this.notificationService.notify({ + severity: Severity.Error, + message: localize('successive auth failures', "Settings sync is suspended because of successive authorization failures. Please sign in again to continue synchronizing"), + actions: { + primary: [new Action('sign in', localize('sign in', "Sign in"), undefined, true, () => this.signIn())] + } + }); + } } private onDidChangeSessions(e: AuthenticationSessionsChangeEvent): void { diff --git a/src/vs/workbench/contrib/userDataSync/electron-browser/userDataAutoSyncService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataAutoSyncService.ts similarity index 76% rename from src/vs/workbench/contrib/userDataSync/electron-browser/userDataAutoSyncService.ts rename to src/vs/workbench/services/userDataSync/electron-browser/userDataAutoSyncService.ts index a61f13cc49..3fa257a84c 100644 --- a/src/vs/workbench/contrib/userDataSync/electron-browser/userDataAutoSyncService.ts +++ b/src/vs/workbench/services/userDataSync/electron-browser/userDataAutoSyncService.ts @@ -7,13 +7,12 @@ import { IUserDataAutoSyncService, UserDataSyncError, IUserDataSyncStoreManageme import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { Event } from 'vs/base/common/event'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { UserDataSyncTrigger } from 'vs/workbench/contrib/userDataSync/browser/userDataSyncTrigger'; import { UserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataAutoSyncService'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService implements IUserDataAutoSyncService { +class UserDataAutoSyncService extends UserDataAutoSyncEnablementService implements IUserDataAutoSyncService { declare readonly _serviceBrand: undefined; @@ -24,16 +23,14 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i @IStorageService storageService: IStorageService, @IEnvironmentService environmentService: IEnvironmentService, @IUserDataSyncStoreManagementService userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, - @IInstantiationService instantiationService: IInstantiationService, @ISharedProcessService sharedProcessService: ISharedProcessService, ) { super(storageService, environmentService, userDataSyncStoreManagementService); this.channel = sharedProcessService.getChannel('userDataAutoSync'); - this._register(instantiationService.createInstance(UserDataSyncTrigger).onDidTriggerSync(source => this.triggerSync([source], true))); } - triggerSync(sources: string[], hasToLimitSync: boolean): Promise { - return this.channel.call('triggerSync', [sources, hasToLimitSync]); + triggerSync(sources: string[], hasToLimitSync: boolean, disableCache: boolean): Promise { + return this.channel.call('triggerSync', [sources, hasToLimitSync, disableCache]); } turnOn(): Promise { @@ -45,3 +42,5 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i } } + +registerSingleton(IUserDataAutoSyncService, UserDataAutoSyncService); diff --git a/src/vs/workbench/contrib/userDataSync/electron-browser/userDataSyncStoreManagementService.ts b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncStoreManagementService.ts similarity index 87% rename from src/vs/workbench/contrib/userDataSync/electron-browser/userDataSyncStoreManagementService.ts rename to src/vs/workbench/services/userDataSync/electron-browser/userDataSyncStoreManagementService.ts index 09f48b7b56..c6a99698a9 100644 --- a/src/vs/workbench/contrib/userDataSync/electron-browser/userDataSyncStoreManagementService.ts +++ b/src/vs/workbench/services/userDataSync/electron-browser/userDataSyncStoreManagementService.ts @@ -11,8 +11,9 @@ import { AbstractUserDataSyncStoreManagementService } from 'vs/platform/userData import { IProductService } from 'vs/platform/product/common/productService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { URI } from 'vs/base/common/uri'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -export class UserDataSyncStoreManagementService extends AbstractUserDataSyncStoreManagementService implements IUserDataSyncStoreManagementService { +class UserDataSyncStoreManagementService extends AbstractUserDataSyncStoreManagementService implements IUserDataSyncStoreManagementService { private readonly channel: IChannel; @@ -46,3 +47,5 @@ export class UserDataSyncStoreManagementService extends AbstractUserDataSyncStor } } + +registerSingleton(IUserDataSyncStoreManagementService, UserDataSyncStoreManagementService); diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts index c38b36c25d..c849108f44 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts @@ -15,6 +15,11 @@ import { Schemas } from 'vs/base/common/network'; // {{SQL CARBON EDIT}} @chlafr export const enum WorkingCopyCapabilities { + /** + * Signals no specific capability for the working copy. + */ + None = 0, + /** * Signals that the working copy requires * additional input when saving, e.g. an diff --git a/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts b/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts index ba441f5af6..953a9390c2 100644 --- a/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { IWorkingCopy, IWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopy, IWorkingCopyBackup, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { URI } from 'vs/base/common/uri'; import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -23,7 +23,7 @@ export class TestWorkingCopy extends Disposable implements IWorkingCopy { private readonly _onDispose = this._register(new Emitter()); readonly onDispose = this._onDispose.event; - readonly capabilities = 0; + readonly capabilities = WorkingCopyCapabilities.None; readonly name = basename(this.resource); diff --git a/src/vs/workbench/test/browser/api/extHostNotebook.test.ts b/src/vs/workbench/test/browser/api/extHostNotebook.test.ts index 5f22683ed2..1d4f417370 100644 --- a/src/vs/workbench/test/browser/api/extHostNotebook.test.ts +++ b/src/vs/workbench/test/browser/api/extHostNotebook.test.ts @@ -81,7 +81,8 @@ suite('NotebookCell#Document', function () { addedEditors: [{ documentUri: notebookUri, id: '_notebook_editor_0', - selections: [0] + selections: [0], + visibleRanges: [] }] }); extHostNotebooks.$acceptDocumentAndEditorsDelta({ newActiveEditor: '_notebook_editor_0' }); @@ -96,26 +97,26 @@ suite('NotebookCell#Document', function () { test('cell document is vscode.TextDocument', async function () { - assert.strictEqual(notebook.cells.length, 2); + assert.strictEqual(notebook.notebookDocument.cells.length, 2); - const [c1, c2] = notebook.cells; + const [c1, c2] = notebook.notebookDocument.cells; const d1 = extHostDocuments.getDocument(c1.uri); assert.ok(d1); assert.equal(d1.languageId, c1.language); assert.equal(d1.version, 1); - assert.ok(d1.notebook === notebook); + assert.ok(d1.notebook === notebook.notebookDocument); const d2 = extHostDocuments.getDocument(c2.uri); assert.ok(d2); assert.equal(d2.languageId, c2.language); assert.equal(d2.version, 1); - assert.ok(d2.notebook === notebook); + assert.ok(d2.notebook === notebook.notebookDocument); }); test('cell document goes when notebook closes', async function () { const cellUris: string[] = []; - for (let cell of notebook.cells) { + for (let cell of notebook.notebookDocument.cells) { assert.ok(extHostDocuments.getDocument(cell.uri)); cellUris.push(cell.uri.toString()); } @@ -160,7 +161,7 @@ suite('NotebookCell#Document', function () { extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[0, 0, [{ handle: 2, uri: CellUri.generate(notebookUri, 2), @@ -178,7 +179,7 @@ suite('NotebookCell#Document', function () { cellKind: CellKind.Code, outputs: [], }]]] - }); + }, false); await p; @@ -188,7 +189,7 @@ suite('NotebookCell#Document', function () { const docs: vscode.TextDocument[] = []; const addData: IModelAddedData[] = []; - for (let cell of notebook.cells) { + for (let cell of notebook.notebookDocument.cells) { const doc = extHostDocuments.getDocument(cell.uri); assert.ok(doc); assert.equal(extHostDocuments.getDocument(cell.uri).isClosed, false); @@ -210,14 +211,14 @@ suite('NotebookCell#Document', function () { extHostDocumentsAndEditors.$acceptDocumentsAndEditorsDelta({ removedDocuments: docs.map(d => d.uri) }); // notebook is still open -> cell documents stay open - for (let cell of notebook.cells) { + for (let cell of notebook.notebookDocument.cells) { assert.ok(extHostDocuments.getDocument(cell.uri)); assert.equal(extHostDocuments.getDocument(cell.uri).isClosed, false); } // close notebook -> docs are closed extHostNotebooks.$acceptDocumentAndEditorsDelta({ removedDocuments: [notebook.uri] }); - for (let cell of notebook.cells) { + for (let cell of notebook.notebookDocument.cells) { assert.throws(() => extHostDocuments.getDocument(cell.uri)); } for (let doc of docs) { @@ -227,16 +228,16 @@ suite('NotebookCell#Document', function () { test('cell document goes when cell is removed', async function () { - assert.equal(notebook.cells.length, 2); - const [cell1, cell2] = notebook.cells; + assert.equal(notebook.notebookDocument.cells.length, 2); + const [cell1, cell2] = notebook.notebookDocument.cells; extHostNotebooks.$acceptModelChanged(notebook.uri, { kind: NotebookCellsChangeType.ModelChange, versionId: 2, changes: [[0, 1, []]] - }); + }, false); - assert.equal(notebook.cells.length, 1); + assert.equal(notebook.notebookDocument.cells.length, 1); assert.equal(cell1.document.isClosed, true); // ref still alive! assert.equal(cell2.document.isClosed, false); @@ -244,8 +245,8 @@ suite('NotebookCell#Document', function () { }); test('cell document knows notebook', function () { - for (let cells of notebook.cells) { - assert.equal(cells.document.notebook === notebook, true); + for (let cells of notebook.notebookDocument.cells) { + assert.equal(cells.document.notebook === notebook.notebookDocument, true); } }); }); diff --git a/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts b/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts index 023945b45a..e724ecb7ca 100644 --- a/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts +++ b/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts @@ -74,7 +74,8 @@ suite('NotebookConcatDocument', function () { { documentUri: notebookUri, id: '_notebook_editor_0', - selections: [0] + selections: [0], + visibleRanges: [] } ] }); @@ -88,7 +89,7 @@ suite('NotebookConcatDocument', function () { }); test('empty', function () { - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, undefined); assert.equal(doc.getText(), ''); assert.equal(doc.version, 0); @@ -125,7 +126,7 @@ suite('NotebookConcatDocument', function () { extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[0, 0, [{ handle: 1, uri: cellUri1, @@ -143,12 +144,12 @@ suite('NotebookConcatDocument', function () { cellKind: CellKind.Code, outputs: [], }]]] - }); + }, false); - assert.equal(notebook.cells.length, 1 + 2); // markdown and code + assert.equal(notebook.notebookDocument.cells.length, 1 + 2); // markdown and code - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, undefined); assert.equal(doc.contains(cellUri1), true); assert.equal(doc.contains(cellUri2), true); @@ -159,7 +160,7 @@ suite('NotebookConcatDocument', function () { extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[0, 0, [{ handle: 1, uri: CellUri.generate(notebook.uri, 1), @@ -177,30 +178,30 @@ suite('NotebookConcatDocument', function () { cellKind: CellKind.Code, outputs: [], }]]] - }); + }, false); - assert.equal(notebook.cells.length, 1 + 2); // markdown and code + assert.equal(notebook.notebookDocument.cells.length, 1 + 2); // markdown and code - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, undefined); assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); - assertLocation(doc, new Position(0, 0), new Location(notebook.cells[0].uri, new Position(0, 0))); - assertLocation(doc, new Position(4, 0), new Location(notebook.cells[1].uri, new Position(1, 0))); - assertLocation(doc, new Position(4, 3), new Location(notebook.cells[1].uri, new Position(1, 3))); - assertLocation(doc, new Position(5, 11), new Location(notebook.cells[1].uri, new Position(2, 11))); - assertLocation(doc, new Position(5, 12), new Location(notebook.cells[1].uri, new Position(2, 11)), false); // don't check identity because position will be clamped + assertLocation(doc, new Position(0, 0), new Location(notebook.notebookDocument.cells[0].uri, new Position(0, 0))); + assertLocation(doc, new Position(4, 0), new Location(notebook.notebookDocument.cells[1].uri, new Position(1, 0))); + assertLocation(doc, new Position(4, 3), new Location(notebook.notebookDocument.cells[1].uri, new Position(1, 3))); + assertLocation(doc, new Position(5, 11), new Location(notebook.notebookDocument.cells[1].uri, new Position(2, 11))); + assertLocation(doc, new Position(5, 12), new Location(notebook.notebookDocument.cells[1].uri, new Position(2, 11)), false); // don't check identity because position will be clamped }); test('location, position mapping, cell changes', function () { - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, undefined); // UPDATE 1 extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[0, 0, [{ handle: 1, uri: CellUri.generate(notebook.uri, 1), @@ -210,20 +211,20 @@ suite('NotebookConcatDocument', function () { cellKind: CellKind.Code, outputs: [], }]]] - }); - assert.equal(notebook.cells.length, 1 + 1); + }, false); + assert.equal(notebook.notebookDocument.cells.length, 1 + 1); assert.equal(doc.version, 1); assertLines(doc, 'Hello', 'World', 'Hello World!'); - assertLocation(doc, new Position(0, 0), new Location(notebook.cells[0].uri, new Position(0, 0))); - assertLocation(doc, new Position(2, 2), new Location(notebook.cells[0].uri, new Position(2, 2))); - assertLocation(doc, new Position(4, 0), new Location(notebook.cells[0].uri, new Position(2, 12)), false); // clamped + assertLocation(doc, new Position(0, 0), new Location(notebook.notebookDocument.cells[0].uri, new Position(0, 0))); + assertLocation(doc, new Position(2, 2), new Location(notebook.notebookDocument.cells[0].uri, new Position(2, 2))); + assertLocation(doc, new Position(4, 0), new Location(notebook.notebookDocument.cells[0].uri, new Position(2, 12)), false); // clamped // UPDATE 2 extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[1, 0, [{ handle: 2, uri: CellUri.generate(notebook.uri, 2), @@ -233,39 +234,39 @@ suite('NotebookConcatDocument', function () { cellKind: CellKind.Code, outputs: [], }]]] - }); + }, false); - assert.equal(notebook.cells.length, 1 + 2); + assert.equal(notebook.notebookDocument.cells.length, 1 + 2); assert.equal(doc.version, 2); assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); - assertLocation(doc, new Position(0, 0), new Location(notebook.cells[0].uri, new Position(0, 0))); - assertLocation(doc, new Position(4, 0), new Location(notebook.cells[1].uri, new Position(1, 0))); - assertLocation(doc, new Position(4, 3), new Location(notebook.cells[1].uri, new Position(1, 3))); - assertLocation(doc, new Position(5, 11), new Location(notebook.cells[1].uri, new Position(2, 11))); - assertLocation(doc, new Position(5, 12), new Location(notebook.cells[1].uri, new Position(2, 11)), false); // don't check identity because position will be clamped + assertLocation(doc, new Position(0, 0), new Location(notebook.notebookDocument.cells[0].uri, new Position(0, 0))); + assertLocation(doc, new Position(4, 0), new Location(notebook.notebookDocument.cells[1].uri, new Position(1, 0))); + assertLocation(doc, new Position(4, 3), new Location(notebook.notebookDocument.cells[1].uri, new Position(1, 3))); + assertLocation(doc, new Position(5, 11), new Location(notebook.notebookDocument.cells[1].uri, new Position(2, 11))); + assertLocation(doc, new Position(5, 12), new Location(notebook.notebookDocument.cells[1].uri, new Position(2, 11)), false); // don't check identity because position will be clamped // UPDATE 3 (remove cell #2 again) extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[1, 1, []]] - }); - assert.equal(notebook.cells.length, 1 + 1); + }, false); + assert.equal(notebook.notebookDocument.cells.length, 1 + 1); assert.equal(doc.version, 3); assertLines(doc, 'Hello', 'World', 'Hello World!'); - assertLocation(doc, new Position(0, 0), new Location(notebook.cells[0].uri, new Position(0, 0))); - assertLocation(doc, new Position(2, 2), new Location(notebook.cells[0].uri, new Position(2, 2))); - assertLocation(doc, new Position(4, 0), new Location(notebook.cells[0].uri, new Position(2, 12)), false); // clamped + assertLocation(doc, new Position(0, 0), new Location(notebook.notebookDocument.cells[0].uri, new Position(0, 0))); + assertLocation(doc, new Position(2, 2), new Location(notebook.notebookDocument.cells[0].uri, new Position(2, 2))); + assertLocation(doc, new Position(4, 0), new Location(notebook.notebookDocument.cells[0].uri, new Position(2, 12)), false); // clamped }); test('location, position mapping, cell-document changes', function () { - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, undefined); // UPDATE 1 extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[0, 0, [{ handle: 1, uri: CellUri.generate(notebook.uri, 1), @@ -283,22 +284,22 @@ suite('NotebookConcatDocument', function () { cellKind: CellKind.Code, outputs: [], }]]] - }); - assert.equal(notebook.cells.length, 1 + 2); + }, false); + assert.equal(notebook.notebookDocument.cells.length, 1 + 2); assert.equal(doc.version, 1); assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); - assertLocation(doc, new Position(0, 0), new Location(notebook.cells[0].uri, new Position(0, 0))); - assertLocation(doc, new Position(2, 2), new Location(notebook.cells[0].uri, new Position(2, 2))); - assertLocation(doc, new Position(2, 12), new Location(notebook.cells[0].uri, new Position(2, 12))); - assertLocation(doc, new Position(4, 0), new Location(notebook.cells[1].uri, new Position(1, 0))); - assertLocation(doc, new Position(4, 3), new Location(notebook.cells[1].uri, new Position(1, 3))); + assertLocation(doc, new Position(0, 0), new Location(notebook.notebookDocument.cells[0].uri, new Position(0, 0))); + assertLocation(doc, new Position(2, 2), new Location(notebook.notebookDocument.cells[0].uri, new Position(2, 2))); + assertLocation(doc, new Position(2, 12), new Location(notebook.notebookDocument.cells[0].uri, new Position(2, 12))); + assertLocation(doc, new Position(4, 0), new Location(notebook.notebookDocument.cells[1].uri, new Position(1, 0))); + assertLocation(doc, new Position(4, 3), new Location(notebook.notebookDocument.cells[1].uri, new Position(1, 3))); // offset math let cell1End = doc.offsetAt(new Position(2, 12)); assert.equal(doc.positionAt(cell1End).isEqual(new Position(2, 12)), true); - extHostDocuments.$acceptModelChanged(notebook.cells[0].uri, { + extHostDocuments.$acceptModelChanged(notebook.notebookDocument.cells[0].uri, { versionId: 0, eol: '\n', changes: [{ @@ -309,7 +310,7 @@ suite('NotebookConcatDocument', function () { }] }, false); assertLines(doc, 'Hello', 'World', 'Hi World!', 'Hallo', 'Welt', 'Hallo Welt!'); - assertLocation(doc, new Position(2, 12), new Location(notebook.cells[0].uri, new Position(2, 9)), false); + assertLocation(doc, new Position(2, 12), new Location(notebook.notebookDocument.cells[0].uri, new Position(2, 9)), false); assert.equal(doc.positionAt(cell1End).isEqual(new Position(3, 2)), true); @@ -319,7 +320,7 @@ suite('NotebookConcatDocument', function () { extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[0, 0, [{ handle: 1, uri: CellUri.generate(notebook.uri, 1), @@ -337,11 +338,11 @@ suite('NotebookConcatDocument', function () { cellKind: CellKind.Code, outputs: [], }]]] - }); + }, false); - const mixedDoc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); - const fooLangDoc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, 'fooLang'); - const barLangDoc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, 'barLang'); + const mixedDoc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, undefined); + const fooLangDoc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, 'fooLang'); + const barLangDoc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, 'barLang'); assertLines(mixedDoc, 'fooLang-document', 'barLang-document'); assertLines(fooLangDoc, 'fooLang-document'); @@ -349,7 +350,7 @@ suite('NotebookConcatDocument', function () { extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[2, 0, [{ handle: 3, uri: CellUri.generate(notebook.uri, 3), @@ -359,7 +360,7 @@ suite('NotebookConcatDocument', function () { cellKind: CellKind.Code, outputs: [], }]]] - }); + }, false); assertLines(mixedDoc, 'fooLang-document', 'barLang-document', 'barLang-document2'); assertLines(fooLangDoc, 'fooLang-document'); @@ -383,7 +384,7 @@ suite('NotebookConcatDocument', function () { extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[0, 0, [{ handle: 1, uri: CellUri.generate(notebook.uri, 1), @@ -401,11 +402,11 @@ suite('NotebookConcatDocument', function () { cellKind: CellKind.Code, outputs: [], }]]] - }); + }, false); - assert.equal(notebook.cells.length, 1 + 2); // markdown and code + assert.equal(notebook.notebookDocument.cells.length, 1 + 2); // markdown and code - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, undefined); assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); assertOffsetAtPosition(doc, 0, { line: 0, character: 0 }); @@ -436,7 +437,7 @@ suite('NotebookConcatDocument', function () { extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[0, 0, [{ handle: 1, uri: CellUri.generate(notebook.uri, 1), @@ -454,26 +455,26 @@ suite('NotebookConcatDocument', function () { cellKind: CellKind.Code, outputs: [], }]]] - }); + }, false); - assert.equal(notebook.cells.length, 1 + 2); // markdown and code + assert.equal(notebook.notebookDocument.cells.length, 1 + 2); // markdown and code - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, undefined); assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); - assertLocationAtPosition(doc, { line: 0, character: 0 }, { uri: notebook.cells[0].uri, line: 0, character: 0 }); - assertLocationAtPosition(doc, { line: 2, character: 0 }, { uri: notebook.cells[0].uri, line: 2, character: 0 }); - assertLocationAtPosition(doc, { line: 2, character: 12 }, { uri: notebook.cells[0].uri, line: 2, character: 12 }); - assertLocationAtPosition(doc, { line: 3, character: 0 }, { uri: notebook.cells[1].uri, line: 0, character: 0 }); - assertLocationAtPosition(doc, { line: 5, character: 0 }, { uri: notebook.cells[1].uri, line: 2, character: 0 }); - assertLocationAtPosition(doc, { line: 5, character: 11 }, { uri: notebook.cells[1].uri, line: 2, character: 11 }); + assertLocationAtPosition(doc, { line: 0, character: 0 }, { uri: notebook.notebookDocument.cells[0].uri, line: 0, character: 0 }); + assertLocationAtPosition(doc, { line: 2, character: 0 }, { uri: notebook.notebookDocument.cells[0].uri, line: 2, character: 0 }); + assertLocationAtPosition(doc, { line: 2, character: 12 }, { uri: notebook.notebookDocument.cells[0].uri, line: 2, character: 12 }); + assertLocationAtPosition(doc, { line: 3, character: 0 }, { uri: notebook.notebookDocument.cells[1].uri, line: 0, character: 0 }); + assertLocationAtPosition(doc, { line: 5, character: 0 }, { uri: notebook.notebookDocument.cells[1].uri, line: 2, character: 0 }); + assertLocationAtPosition(doc, { line: 5, character: 11 }, { uri: notebook.notebookDocument.cells[1].uri, line: 2, character: 11 }); }); test('getText(range)', function () { extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[0, 0, [{ handle: 1, uri: CellUri.generate(notebook.uri, 1), @@ -491,11 +492,11 @@ suite('NotebookConcatDocument', function () { cellKind: CellKind.Code, outputs: [], }]]] - }); + }, false); - assert.equal(notebook.cells.length, 1 + 2); // markdown and code + assert.equal(notebook.notebookDocument.cells.length, 1 + 2); // markdown and code - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, undefined); assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); assert.equal(doc.getText(new Range(0, 0, 0, 0)), ''); @@ -507,7 +508,7 @@ suite('NotebookConcatDocument', function () { extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[0, 0, [{ handle: 1, uri: CellUri.generate(notebook.uri, 1), @@ -525,11 +526,11 @@ suite('NotebookConcatDocument', function () { cellKind: CellKind.Code, outputs: [], }]]] - }); + }, false); - assert.equal(notebook.cells.length, 1 + 2); // markdown and code + assert.equal(notebook.notebookDocument.cells.length, 1 + 2); // markdown and code - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, undefined); assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); diff --git a/src/vs/workbench/test/browser/api/extHostTextEditors.test.ts b/src/vs/workbench/test/browser/api/extHostTextEditors.test.ts index 27fb63d505..391680a2dd 100644 --- a/src/vs/workbench/test/browser/api/extHostTextEditors.test.ts +++ b/src/vs/workbench/test/browser/api/extHostTextEditors.test.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; -import { MainContext, MainThreadTextEditorsShape, IWorkspaceEditDto } from 'vs/workbench/api/common/extHost.protocol'; +import { MainContext, MainThreadTextEditorsShape, IWorkspaceEditDto, WorkspaceEditType } from 'vs/workbench/api/common/extHost.protocol'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { SingleProxyRPCProtocol, TestRPCProtocol } from 'vs/workbench/test/browser/api/testRPCProtocol'; import { ExtHostEditors } from 'vs/workbench/api/common/extHostTextEditors'; -import { WorkspaceTextEdit } from 'vs/editor/common/modes'; import { NullLogService } from 'vs/platform/log/common/log'; +import { assertType } from 'vs/base/common/types'; suite('ExtHostTextEditors.applyWorkspaceEdit', () => { @@ -40,7 +40,7 @@ suite('ExtHostTextEditors.applyWorkspaceEdit', () => { EOL: '\n', }] }); - editors = new ExtHostEditors(rpcProtocol, documentsAndEditors); + editors = new ExtHostEditors(rpcProtocol, documentsAndEditors, null!); }); test('uses version id if document available', async () => { @@ -48,7 +48,9 @@ suite('ExtHostTextEditors.applyWorkspaceEdit', () => { edit.replace(resource, new extHostTypes.Range(0, 0, 0, 0), 'hello'); await editors.applyWorkspaceEdit(edit); assert.equal(workspaceResourceEdits.edits.length, 1); - assert.equal((workspaceResourceEdits.edits[0]).modelVersionId, 1337); + const [first] = workspaceResourceEdits.edits; + assertType(first._type === WorkspaceEditType.Text); + assert.equal(first.modelVersionId, 1337); }); test('does not use version id if document is not available', async () => { @@ -56,7 +58,9 @@ suite('ExtHostTextEditors.applyWorkspaceEdit', () => { edit.replace(URI.parse('foo:bar2'), new extHostTypes.Range(0, 0, 0, 0), 'hello'); await editors.applyWorkspaceEdit(edit); assert.equal(workspaceResourceEdits.edits.length, 1); - assert.ok(typeof (workspaceResourceEdits.edits[0]).modelVersionId === 'undefined'); + const [first] = workspaceResourceEdits.edits; + assertType(first._type === WorkspaceEditType.Text); + assert.ok(typeof first.modelVersionId === 'undefined'); }); }); diff --git a/src/vs/workbench/test/browser/api/extHostTypes.test.ts b/src/vs/workbench/test/browser/api/extHostTypes.test.ts index a85603eddc..5536faec7c 100644 --- a/src/vs/workbench/test/browser/api/extHostTypes.test.ts +++ b/src/vs/workbench/test/browser/api/extHostTypes.test.ts @@ -384,21 +384,21 @@ suite('ExtHostTypes', function () { edit.replace(URI.parse('foo:a'), new types.Range(2, 1, 2, 1), 'bar'); edit.replace(URI.parse('foo:b'), new types.Range(3, 1, 3, 1), 'bazz'); - const all = edit.allEntries(); + const all = edit._allEntries(); assert.equal(all.length, 4); const [first, second, third, fourth] = all; - assertType(first._type === 2); + assertType(first._type === types.FileEditType.Text); assert.equal(first.uri.toString(), 'foo:a'); - assertType(second._type === 1); + assertType(second._type === types.FileEditType.File); assert.equal(second.from!.toString(), 'foo:a'); assert.equal(second.to!.toString(), 'foo:b'); - assertType(third._type === 2); + assertType(third._type === types.FileEditType.Text); assert.equal(third.uri.toString(), 'foo:a'); - assertType(fourth._type === 2); + assertType(fourth._type === types.FileEditType.Text); assert.equal(fourth.uri.toString(), 'foo:b'); }); @@ -408,11 +408,11 @@ suite('ExtHostTypes', function () { edit.insert(uri, new types.Position(0, 0), 'Hello'); edit.insert(uri, new types.Position(0, 0), 'Foo'); - assert.equal(edit.allEntries().length, 2); - let [first, second] = edit.allEntries(); + assert.equal(edit._allEntries().length, 2); + let [first, second] = edit._allEntries(); - assertType(first._type === 2); - assertType(second._type === 2); + assertType(first._type === types.FileEditType.Text); + assertType(second._type === types.FileEditType.Text); assert.equal(first.edit.newText, 'Hello'); assert.equal(second.edit.newText, 'Foo'); }); diff --git a/src/vs/workbench/test/browser/api/extHostWebview.test.ts b/src/vs/workbench/test/browser/api/extHostWebview.test.ts index cf41d0b0fc..f92805e9d2 100644 --- a/src/vs/workbench/test/browser/api/extHostWebview.test.ts +++ b/src/vs/workbench/test/browser/api/extHostWebview.test.ts @@ -8,12 +8,12 @@ import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { NullLogService } from 'vs/platform/log/common/log'; -import { MainThreadWebviews } from 'vs/workbench/api/browser/mainThreadWebview'; +import { MainThreadWebviewManager } from 'vs/workbench/api/browser/mainThreadWebviewManager'; import { IExtHostContext } from 'vs/workbench/api/common/extHost.protocol'; import { NullApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { ExtHostWebviews } from 'vs/workbench/api/common/extHostWebview'; -import { ExtHostWebviewSerializer } from 'vs/workbench/api/common/extHostWebviewSerializer'; +import { ExtHostWebviewPanels } from 'vs/workbench/api/common/extHostWebviewPanels'; import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor'; import type * as vscode from 'vscode'; import { SingleProxyRPCProtocol } from './testRPCProtocol'; @@ -36,7 +36,7 @@ suite('ExtHostWebview', () => { isExtensionDevelopmentDebug: false, }, undefined, new NullLogService(), NullApiDeprecationService); - const extHostWebviewSerializer = new ExtHostWebviewSerializer(rpcProtocol!, extHostWebviews); + const extHostWebviewPanels = new ExtHostWebviewPanels(rpcProtocol!, extHostWebviews, undefined); let lastInvokedDeserializer: vscode.WebviewPanelSerializer | undefined = undefined; @@ -51,20 +51,20 @@ suite('ExtHostWebview', () => { const serializerA = new NoopSerializer(); const serializerB = new NoopSerializer(); - const serializerARegistration = extHostWebviewSerializer.registerWebviewPanelSerializer(extension, viewType, serializerA); + const serializerARegistration = extHostWebviewPanels.registerWebviewPanelSerializer(extension, viewType, serializerA); - await extHostWebviewSerializer.$deserializeWebviewPanel('x', viewType, 'title', {}, 0 as EditorViewColumn, {}); + await extHostWebviewPanels.$deserializeWebviewPanel('x', viewType, 'title', {}, 0 as EditorViewColumn, {}); assert.strictEqual(lastInvokedDeserializer, serializerA); assert.throws( - () => extHostWebviewSerializer.registerWebviewPanelSerializer(extension, viewType, serializerB), + () => extHostWebviewPanels.registerWebviewPanelSerializer(extension, viewType, serializerB), 'Should throw when registering two serializers for the same view'); serializerARegistration.dispose(); - extHostWebviewSerializer.registerWebviewPanelSerializer(extension, viewType, serializerB); + extHostWebviewPanels.registerWebviewPanelSerializer(extension, viewType, serializerB); - await extHostWebviewSerializer.$deserializeWebviewPanel('x', viewType, 'title', {}, 0 as EditorViewColumn, {}); + await extHostWebviewPanels.$deserializeWebviewPanel('x', viewType, 'title', {}, 0 as EditorViewColumn, {}); assert.strictEqual(lastInvokedDeserializer, serializerB); }); @@ -74,7 +74,10 @@ suite('ExtHostWebview', () => { webviewResourceRoot: 'vscode-resource://{{resource}}', isExtensionDevelopmentDebug: false, }, undefined, new NullLogService(), NullApiDeprecationService); - const webview = extHostWebviews.createWebviewPanel({} as any, 'type', 'title', 1, {}); + + const extHostWebviewPanels = new ExtHostWebviewPanels(rpcProtocol!, extHostWebviews, undefined); + + const webview = extHostWebviewPanels.createWebviewPanel({} as any, 'type', 'title', 1, {}); assert.strictEqual( webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html')).toString(), @@ -113,7 +116,10 @@ suite('ExtHostWebview', () => { webviewResourceRoot: `https://{{uuid}}.webview.contoso.com/commit/{{resource}}`, isExtensionDevelopmentDebug: false, }, undefined, new NullLogService(), NullApiDeprecationService); - const webview = extHostWebviews.createWebviewPanel({} as any, 'type', 'title', 1, {}); + + const extHostWebviewPanels = new ExtHostWebviewPanels(rpcProtocol!, extHostWebviews, undefined); + + const webview = extHostWebviewPanels.createWebviewPanel({} as any, 'type', 'title', 1, {}); function stripEndpointUuid(input: string) { return input.replace(/^https:\/\/[^\.]+?\./, ''); @@ -153,7 +159,7 @@ suite('ExtHostWebview', () => { function createNoopMainThreadWebviews() { - return new class extends mock() { + return new class extends mock() { $createWebviewPanel() { /* noop */ } $registerSerializer() { /* noop */ } $unregisterSerializer() { /* noop */ } diff --git a/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts b/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts index af909747ae..0c06f7b8cf 100644 --- a/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts +++ b/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts @@ -10,7 +10,7 @@ import { TestConfigurationService } from 'vs/platform/configuration/test/common/ import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; import { TestCodeEditorService } from 'vs/editor/test/browser/editorTestServices'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { ExtHostDocumentsAndEditorsShape, ExtHostContext, ExtHostDocumentsShape, IWorkspaceTextEditDto } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostDocumentsAndEditorsShape, ExtHostContext, ExtHostDocumentsShape, IWorkspaceTextEditDto, WorkspaceEditType } from 'vs/workbench/api/common/extHost.protocol'; import { mock } from 'vs/base/test/common/mock'; import { Event } from 'vs/base/common/event'; import { MainThreadTextEditors } from 'vs/workbench/api/browser/mainThreadEditors'; @@ -20,7 +20,7 @@ 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 } from 'vs/workbench/test/browser/workbenchTestServices'; -import { BulkEditService } from 'vs/workbench/services/bulkEdit/browser/bulkEditService'; +import { BulkEditService } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditService'; import { NullLogService, ILogService } from 'vs/platform/log/common/log'; import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; import { IReference, ImmortalReference } from 'vs/base/common/lifecycle'; @@ -170,6 +170,7 @@ suite('MainThreadEditors', () => { let model = modelService.createModel('something', null, resource); let workspaceResourceEdit: IWorkspaceTextEditDto = { + _type: WorkspaceEditType.Text, resource: resource, modelVersionId: model.getVersionId(), edit: { @@ -191,6 +192,7 @@ suite('MainThreadEditors', () => { let model = modelService.createModel('something', null, resource); let workspaceResourceEdit1: IWorkspaceTextEditDto = { + _type: WorkspaceEditType.Text, resource: resource, modelVersionId: model.getVersionId(), edit: { @@ -199,6 +201,7 @@ suite('MainThreadEditors', () => { } }; let workspaceResourceEdit2: IWorkspaceTextEditDto = { + _type: WorkspaceEditType.Text, resource: resource, modelVersionId: model.getVersionId(), edit: { @@ -221,9 +224,9 @@ suite('MainThreadEditors', () => { test(`applyWorkspaceEdit with only resource edit`, () => { return editors.$tryApplyWorkspaceEdit({ edits: [ - { oldUri: resource, newUri: resource, options: undefined }, - { oldUri: undefined, newUri: resource, options: undefined }, - { oldUri: resource, newUri: undefined, options: undefined } + { _type: WorkspaceEditType.File, oldUri: resource, newUri: resource, options: undefined }, + { _type: WorkspaceEditType.File, oldUri: undefined, newUri: resource, options: undefined }, + { _type: WorkspaceEditType.File, oldUri: resource, newUri: undefined, options: undefined } ] }).then((result) => { assert.equal(result, true); 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 8e474a08b8..b49ea2571f 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts @@ -464,8 +464,9 @@ suite('Workbench editor groups', () => { // Active && Pinned const input1 = input(); - const openedEditor = group.openEditor(input1, { active: true, pinned: true }); + const { editor: openedEditor, isNew } = group.openEditor(input1, { active: true, pinned: true }); assert.equal(openedEditor, input1); + assert.equal(isNew, true); assert.equal(group.count, 1); assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 1); @@ -575,11 +576,13 @@ suite('Workbench editor groups', () => { const input3 = input('3'); // Pinned and Active - let openedEditor = group.openEditor(input1, { pinned: true, active: true }); - assert.equal(openedEditor, input1); + let openedEditorResult = group.openEditor(input1, { pinned: true, active: true }); + assert.equal(openedEditorResult.editor, input1); + assert.equal(openedEditorResult.isNew, true); - openedEditor = group.openEditor(input1Copy, { pinned: true, active: true }); // opening copy of editor should still return existing one - assert.equal(openedEditor, input1); + openedEditorResult = group.openEditor(input1Copy, { pinned: true, active: true }); // opening copy of editor should still return existing one + assert.equal(openedEditorResult.editor, input1); + assert.equal(openedEditorResult.isNew, false); group.openEditor(input2, { pinned: true, active: true }); group.openEditor(input3, { pinned: true, active: true }); @@ -1145,7 +1148,7 @@ suite('Workbench editor groups', () => { // [] -> /index.html/ const indexHtml = input('index.html'); - let openedEditor = group.openEditor(indexHtml); + let openedEditor = group.openEditor(indexHtml).editor; assert.equal(openedEditor, indexHtml); assert.equal(group.activeEditor, indexHtml); assert.equal(group.previewEditor, indexHtml); @@ -1154,7 +1157,7 @@ suite('Workbench editor groups', () => { // /index.html/ -> /index.html/ const sameIndexHtml = input('index.html'); - openedEditor = group.openEditor(sameIndexHtml); + openedEditor = group.openEditor(sameIndexHtml).editor; assert.equal(openedEditor, indexHtml); assert.equal(group.activeEditor, indexHtml); assert.equal(group.previewEditor, indexHtml); @@ -1163,7 +1166,7 @@ suite('Workbench editor groups', () => { // /index.html/ -> /style.css/ const styleCss = input('style.css'); - openedEditor = group.openEditor(styleCss); + openedEditor = group.openEditor(styleCss).editor; assert.equal(openedEditor, styleCss); assert.equal(group.activeEditor, styleCss); assert.equal(group.previewEditor, styleCss); @@ -1172,7 +1175,7 @@ suite('Workbench editor groups', () => { // /style.css/ -> [/style.css/, test.js] const testJs = input('test.js'); - openedEditor = group.openEditor(testJs, { active: true, pinned: true }); + openedEditor = group.openEditor(testJs, { active: true, pinned: true }).editor; assert.equal(openedEditor, testJs); assert.equal(group.previewEditor, styleCss); assert.equal(group.activeEditor, testJs); 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 46c101c058..6bdbe6c1ff 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorModel.test.ts @@ -60,9 +60,11 @@ suite('Workbench editor model', () => { const model = await m.load(); assert(model === m); + assert.equal(model.isDisposed(), false); assert.strictEqual(m.isResolved(), true); m.dispose(); assert.equal(counter, 1); + assert.equal(model.isDisposed(), true); }); test('BaseTextEditorModel', async () => { diff --git a/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts b/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts similarity index 97% rename from src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts rename to src/vs/workbench/test/browser/parts/editor/editorPane.test.ts index 72d6b174e6..bd651d5579 100644 --- a/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { BaseEditor, EditorMemento } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorPane, EditorMemento } from 'vs/workbench/browser/parts/editor/editorPane'; import { EditorInput, EditorOptions, IEditorInputFactory, IEditorInputFactoryRegistry, Extensions as EditorExtensions } from 'vs/workbench/common/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import * as Platform from 'vs/platform/registry/common/platform'; @@ -27,7 +27,7 @@ const NullThemeService = new TestThemeService(); let EditorRegistry: IEditorRegistry = Platform.Registry.as(Extensions.Editors); let EditorInputRegistry: IEditorInputFactoryRegistry = Platform.Registry.as(EditorExtensions.EditorInputFactories); -export class MyEditor extends BaseEditor { +export class MyEditor extends EditorPane { constructor(@ITelemetryService telemetryService: ITelemetryService) { super('MyEditor', NullTelemetryService, NullThemeService, new TestStorageService()); @@ -38,7 +38,7 @@ export class MyEditor extends BaseEditor { createEditor(): any { } } -export class MyOtherEditor extends BaseEditor { +export class MyOtherEditor extends EditorPane { constructor(@ITelemetryService telemetryService: ITelemetryService) { super('myOtherEditor', NullTelemetryService, NullThemeService, new TestStorageService()); @@ -96,9 +96,9 @@ class MyOtherInput extends EditorInput { } class MyResourceEditorInput extends ResourceEditorInput { } -suite('Workbench base editor', () => { +suite('Workbench EditorPane', () => { - test('BaseEditor API', async () => { + test('EditorPane API', async () => { let e = new MyEditor(NullTelemetryService); let input = new MyOtherInput(); let options = new EditorOptions(); @@ -106,7 +106,7 @@ suite('Workbench base editor', () => { assert(!e.isVisible()); assert(!e.input); - await e.setInput(input, options, CancellationToken.None); + await e.setInput(input, options, Object.create(null), CancellationToken.None); assert.strictEqual(input, e.input); const group = new TestEditorGroupView(1); e.setVisible(true, group); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 6d230b9437..aae9b42a06 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -10,7 +10,7 @@ import * as resources from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { IEditorInputWithOptions, IEditorIdentifier, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, IEditorInput, IEditorPane, IEditorCloseEvent, IEditorPartOptions, IRevertOptions, GroupIdentifier, EditorInput, EditorOptions, EditorsOrder, IFileEditorInput, IEditorInputFactoryRegistry, IEditorInputFactory, Extensions as EditorExtensions, ISaveOptions, IMoveResult, ITextEditorPane, ITextDiffEditorPane, IVisibleEditorPane } from 'vs/workbench/common/editor'; +import { IEditorInputWithOptions, IEditorIdentifier, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, IEditorInput, IEditorPane, IEditorCloseEvent, IEditorPartOptions, IRevertOptions, GroupIdentifier, EditorInput, EditorOptions, EditorsOrder, IFileEditorInput, IEditorInputFactoryRegistry, IEditorInputFactory, Extensions as EditorExtensions, ISaveOptions, IMoveResult, ITextEditorPane, ITextDiffEditorPane, IVisibleEditorPane, IEditorOpenContext } from 'vs/workbench/common/editor'; import { IEditorOpeningEvent, EditorServiceImpl, IEditorGroupView, IEditorGroupsAccessor } from 'vs/workbench/browser/parts/editor/editor'; import { Event, Emitter } from 'vs/base/common/event'; import { IBackupFileService, IResolvedBackup } from 'vs/workbench/services/backup/common/backup'; @@ -91,7 +91,7 @@ import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { Registry } from 'vs/platform/registry/common/platform'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { CancellationToken } from 'vs/base/common/cancellation'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; @@ -429,6 +429,7 @@ export class TestLayoutService implements IWorkbenchLayoutService { hasFocus(_part: Parts): boolean { return false; } focusPart(_part: Parts): void { } hasWindowBorder(): boolean { return false; } + getWindowBorderWidth(): number { return 0; } getWindowBorderRadius(): string | undefined { return undefined; } isVisible(_part: Parts): boolean { return true; } getDimension(_part: Parts): Dimension { return new Dimension(0, 0); } @@ -1031,7 +1032,7 @@ export class TestHostService implements IHostService { async restart(): Promise { } async reload(): Promise { } - async focus(): Promise { } + async focus(options?: { force: boolean }): Promise { } async openWindow(arg1?: IOpenEmptyWindowOptions | IWindowOpenable[], arg2?: IOpenWindowOptions): Promise { } @@ -1068,12 +1069,12 @@ export class TestEditorInput extends EditorInput { } export function registerTestEditor(id: string, inputs: SyncDescriptor[], factoryInputId?: string): IDisposable { - class TestEditorControl extends BaseEditor { + class TestEditor extends EditorPane { constructor() { super(id, NullTelemetryService, new TestThemeService(), new TestStorageService()); } - async setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { - super.setInput(input, options, token); + async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + super.setInput(input, options, context, token); await input.resolve(); } @@ -1085,7 +1086,7 @@ export function registerTestEditor(id: string, inputs: SyncDescriptor(Extensions.Editors).registerEditor(EditorDescriptor.create(TestEditorControl, id, 'Test Editor Control'), inputs)); + disposables.add(Registry.as(Extensions.Editors).registerEditor(EditorDescriptor.create(TestEditor, id, 'Test Editor Control'), inputs)); if (factoryInputId) { diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index cf65b3bd69..3bb799e8a2 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -56,7 +56,6 @@ import 'vs/workbench/browser/parts/views/viewsService'; import 'vs/platform/undoRedo/common/undoRedoService'; import 'vs/workbench/services/uriIdentity/common/uriIdentityService'; import 'vs/workbench/services/extensions/browser/extensionUrlHandler'; -import 'vs/workbench/services/bulkEdit/browser/bulkEditService'; import 'vs/workbench/services/keybinding/common/keybindingEditing'; import 'vs/workbench/services/decorations/browser/decorationsService'; import 'vs/workbench/services/progress/browser/progressService'; @@ -271,7 +270,8 @@ import 'vs/workbench/contrib/files/browser/files.contribution'; import 'vs/workbench/contrib/backup/common/backup.contribution'; // bulkEdit -import 'vs/workbench/contrib/bulkEdit/browser/bulkEdit.contribution'; +import 'vs/workbench/contrib/bulkEdit/browser/bulkEditService'; +import 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.contribution'; // Search import 'vs/workbench/contrib/search/browser/search.contribution'; diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index e9bee0d13b..e7cc0a1f47 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -41,7 +41,6 @@ import 'vs/workbench/services/output/electron-browser/outputChannelModelService' import 'vs/workbench/services/textfile/electron-browser/nativeTextFileService'; import 'vs/workbench/services/dialogs/electron-browser/dialogService'; import 'vs/workbench/services/keybinding/electron-browser/nativeKeymapService'; -import 'vs/workbench/services/keybinding/electron-browser/keybinding.contribution'; import 'vs/workbench/services/extensions/electron-browser/extensionService'; import 'vs/workbench/services/extensionManagement/electron-browser/extensionManagementServerService'; import 'vs/workbench/services/extensionManagement/electron-browser/extensionTipsService'; @@ -55,6 +54,8 @@ import 'vs/workbench/services/workspaces/electron-browser/workspaceEditingServic import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncMachinesService'; import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncService'; import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncAccountService'; +import 'vs/workbench/services/userDataSync/electron-browser/userDataSyncStoreManagementService'; +import 'vs/workbench/services/userDataSync/electron-browser/userDataAutoSyncService'; import 'vs/workbench/services/sharedProcess/electron-browser/sharedProcessService'; import 'vs/workbench/services/localizations/electron-browser/localizationsService'; import 'vs/workbench/services/path/electron-browser/pathService'; @@ -63,8 +64,6 @@ import 'vs/workbench/services/experiment/electron-browser/experimentService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; import { KeytarCredentialsService } from 'vs/platform/credentials/node/credentialsService'; -import { IUserDataAutoSyncService, IUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSync'; -import { UserDataAutoSyncService } from 'vs/workbench/contrib/userDataSync/electron-browser/userDataAutoSyncService'; import { ITunnelService } from 'vs/platform/remote/common/tunnel'; import { TunnelService } from 'vs/platform/remote/node/tunnelService'; import { ITimerService } from 'vs/workbench/services/timer/browser/timerService'; @@ -72,8 +71,6 @@ import { TimerService } from 'vs/workbench/services/timer/electron-browser/timer import { IUserDataInitializationService, UserDataInitializationService } from 'vs/workbench/services/userData/browser/userDataInit'; registerSingleton(ICredentialsService, KeytarCredentialsService, true); -registerSingleton(IUserDataSyncStoreManagementService, UserDataSyncStoreManagementService); -registerSingleton(IUserDataAutoSyncService, UserDataAutoSyncService); registerSingleton(ITunnelService, TunnelService); registerSingleton(ITimerService, TimerService); registerSingleton(IUserDataInitializationService, UserDataInitializationService); @@ -149,7 +146,6 @@ import 'vs/workbench/contrib/userDataSync/electron-browser/userDataSync.contribu // Configuration Exporter import 'vs/workbench/contrib/configExporter/electron-browser/configurationExportHelper.contribution'; -import { UserDataSyncStoreManagementService } from 'vs/workbench/contrib/userDataSync/electron-browser/userDataSyncStoreManagementService'; //#endregion diff --git a/src/vs/workbench/workbench.desktop.sandbox.main.ts b/src/vs/workbench/workbench.desktop.sandbox.main.ts new file mode 100644 index 0000000000..f9f0108b04 --- /dev/null +++ b/src/vs/workbench/workbench.desktop.sandbox.main.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +// ####################################################################### +// ### ### +// ### !!! PLEASE ADD COMMON IMPORTS INTO WORKBENCH.COMMON.MAIN.TS !!! ### +// ### ### +// ####################################################################### + + +//#region --- workbench common & sandbox + +import 'vs/workbench/workbench.sandbox.main'; + +//#endregion + + +//#region --- workbench actions + + +//#endregion + + +//#region --- workbench (desktop main) + +import 'vs/workbench/electron-sandbox/desktop.main'; + +//#endregion + + +//#region --- workbench services + + +//#endregion + + +//#region --- workbench contributions + + +//#endregion diff --git a/src/vs/workbench/workbench.sandbox.main.ts b/src/vs/workbench/workbench.sandbox.main.ts index 2162b89b00..0c26d211aa 100644 --- a/src/vs/workbench/workbench.sandbox.main.ts +++ b/src/vs/workbench/workbench.sandbox.main.ts @@ -28,7 +28,7 @@ import 'vs/workbench/services/update/electron-sandbox/updateService'; import 'vs/workbench/services/url/electron-sandbox/urlService'; import 'vs/workbench/services/lifecycle/electron-sandbox/lifecycleService'; import 'vs/workbench/services/title/electron-sandbox/titleService'; -import 'vs/workbench/services/host/electron-sandbox/desktopHostService'; +import 'vs/workbench/services/host/electron-sandbox/nativeHostService'; import 'vs/workbench/services/request/electron-sandbox/requestService'; import 'vs/workbench/services/extensionResourceLoader/electron-sandbox/extensionResourceLoaderService'; import 'vs/workbench/services/clipboard/electron-sandbox/clipboardService'; diff --git a/src/vs/workbench/workbench.web.api.ts b/src/vs/workbench/workbench.web.api.ts index 63b51c3cf9..551a60897a 100644 --- a/src/vs/workbench/workbench.web.api.ts +++ b/src/vs/workbench/workbench.web.api.ts @@ -147,6 +147,15 @@ interface IWindowIndicator { command?: string; } +interface IInitialColorTheme { + themeType: 'light' | 'dark' | 'hc'; + + /** + * a list of workbench colors + */ + colors?: { [colorId: string]: string }; +} + interface IDefaultSideBarLayout { visible?: boolean; containers?: ({ @@ -381,6 +390,15 @@ interface IWorkbenchConstructionOptions { */ readonly windowIndicator?: IWindowIndicator; + /** + * Specifies the default theme type (LIGHT, DARK..) and allows to provide initial colors that are shown + * until the color theme that is specified in the settings (`editor.colorTheme`) is loaded and applied. + * Once there are persisted colors from a last run these will be used. + * + * The idea is that the colors match the main colors from the theme defined in the `configurationDefaults`. + */ + readonly initialColorTheme?: IInitialColorTheme; + //#endregion diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index cfa7099ed8..f6407e59f7 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -71,7 +71,7 @@ import { UserDataSyncStoreService, UserDataSyncStoreManagementService } from 'vs import { UserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSyncBackupStoreService'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; import { IUserDataSyncAccountService, UserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount'; -import { UserDataAutoSyncService } from 'vs/workbench/contrib/userDataSync/browser/userDataAutoSyncService'; +import { UserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataAutoSyncService'; import { AccessibilityService } from 'vs/platform/accessibility/common/accessibilityService'; import { ITitleService } from 'vs/workbench/services/title/common/titleService'; import { TitlebarPart } from 'vs/workbench/browser/parts/titlebar/titlebarPart'; @@ -90,8 +90,8 @@ registerSingleton(IUserDataSyncMachinesService, UserDataSyncMachinesService); registerSingleton(IUserDataSyncBackupStoreService, UserDataSyncBackupStoreService); registerSingleton(IStorageKeysSyncRegistryService, StorageKeysSyncRegistryService); registerSingleton(IUserDataSyncAccountService, UserDataSyncAccountService); -registerSingleton(IUserDataAutoSyncService, UserDataAutoSyncService); -registerSingleton(IUserDataSyncService, UserDataSyncService); +registerSingleton(IUserDataSyncService, UserDataSyncService, true); +registerSingleton(IUserDataAutoSyncService, UserDataAutoSyncService, true); registerSingleton(ITitleService, TitlebarPart); registerSingleton(IExtensionTipsService, ExtensionTipsService); registerSingleton(ITimerService, TimerService); @@ -138,6 +138,9 @@ import 'vs/workbench/contrib/welcome/telemetryOptOut/browser/telemetryOptOut.con // Issues import 'vs/workbench/contrib/issue/browser/issue.web.contribution'; +// Extensions Management (// TODO@sandbox TODO@ben move back into common/extensions.contribution.ts when 'semver-umd' can be loaded) +import 'vs/workbench/contrib/extensions/browser/extensions.web.contribution'; + //#endregion //#region diff --git a/test/smoke/src/areas/notebook/notebook.test.ts b/test/smoke/src/areas/notebook/notebook.test.ts index 8780586fd9..f85cba4af7 100644 --- a/test/smoke/src/areas/notebook/notebook.test.ts +++ b/test/smoke/src/areas/notebook/notebook.test.ts @@ -63,7 +63,7 @@ export function setup() { await app.workbench.notebook.waitForActiveCellEditorContents('code()'); }); - it('cell action execution', async function () { + it.skip('cell action execution', async function () { const app = this.app as Application; await app.workbench.notebook.openNotebook(); await app.workbench.notebook.insertNotebookCell('code'); diff --git a/yarn.lock b/yarn.lock index 64521bd627..54fffed91d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -258,6 +258,14 @@ resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== +"@types/glob@^7.1.1": + version "7.1.3" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" + integrity sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w== + dependencies: + "@types/minimatch" "*" + "@types/node" "*" + "@types/graceful-fs@4.1.2": version "4.1.2" resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.2.tgz#fbc9575dbcc6d1d91dd768d30c5fc0c19f6c50bd" @@ -289,6 +297,11 @@ resolved "https://registry.yarnpkg.com/@types/keytar/-/keytar-4.4.0.tgz#ca24e6ee6d0df10c003aafe26e93113b8faf0d8e" integrity sha512-cq/NkUUy6rpWD8n7PweNQQBpw2o0cf5v6fbkUVEpOB9VzzIvyPvSEId1/goIj+MciW2v1Lw5mRimKO01XgE9EA== +"@types/minimatch@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" + integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== + "@types/minimist@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6" @@ -319,6 +332,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.14.tgz#1c1d6e3c75dba466e0326948d56e8bd72a1903d2" integrity sha512-u/SJDyXwuihpwjXy7hOOghagLEV1KdAST6syfnOk6QZAMzZuWZqXy5aYYZbh8Jdpd4escVFP0MvftHNDb9pruA== +"@types/node@^13.13.5": + version "13.13.15" + resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.15.tgz#fe1cc3aa465a3ea6858b793fd380b66c39919766" + integrity sha512-kwbcs0jySLxzLsa2nWUAGOd/s21WU1jebrEdtzhsj1D4Yps1EOuyI1Qcu+FD56dL7NRNIJtDDjcqIG22NwkgLw== + "@types/plotly.js@^1.44.9": version "1.44.9" resolved "https://registry.yarnpkg.com/@types/plotly.js/-/plotly.js-1.44.9.tgz#d20bd229b409f83b5e9bc06df0c948a27b8fbc0d" @@ -1053,19 +1071,17 @@ arrify@^1.0.0, arrify@^1.0.1: resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= -asar@^0.14.0: - version "0.14.0" - resolved "https://registry.yarnpkg.com/asar/-/asar-0.14.0.tgz#998b36a26abd0e590e55d9f92cfd3fd7a6051652" - integrity sha512-l21mf5pG65qbtD5WhymthfbE7ash0goQ+5ayo3lIncxtFNYH1PVArqsGXoAUXOd877mJplWSD9nGumByzQqVSA== +asar@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/asar/-/asar-3.0.3.tgz#1fef03c2d6d2de0cbad138788e4f7ae03b129c7b" + integrity sha512-k7zd+KoR+n8pl71PvgElcoKHrVNiSXtw7odKbyNpmgKe7EGRF9Pnu3uLOukD37EvavKwVFxOUpqXTIZC5B5Pmw== dependencies: chromium-pickle-js "^0.2.0" - commander "^2.9.0" - cuint "^0.2.1" - glob "^6.0.4" - minimatch "^3.0.3" - mkdirp "^0.5.0" - mksnapshot "^0.3.0" - tmp "0.0.28" + commander "^5.0.0" + glob "^7.1.6" + minimatch "^3.0.4" + optionalDependencies: + "@types/glob" "^7.1.1" asn1.js@^4.0.0: version "4.10.1" @@ -1288,14 +1304,6 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== -binary@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79" - integrity sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk= - dependencies: - buffers "~0.1.1" - chainsaw "~0.1.0" - binaryextensions@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-1.0.1.tgz#1e637488b35b58bda5f4774bf96a5212a8c90755" @@ -1527,11 +1535,6 @@ buffer@^5.5.0: base64-js "^1.0.2" ieee754 "^1.1.4" -buffers@~0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" - integrity sha1-skV5w77U1tOWru5tmorn9Ugqt7s= - builtin-modules@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" @@ -1645,13 +1648,6 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= -chainsaw@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.1.0.tgz#5eab50b28afe58074d0d58291388828b5e5fbc98" - integrity sha1-XqtQsor+WAdNDVgpE4iCi15fvJg= - dependencies: - traverse ">=0.3.0 <0.4" - chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -2082,7 +2078,7 @@ commander@0.6.1: resolved "https://registry.yarnpkg.com/commander/-/commander-0.6.1.tgz#fa68a14f6a945d54dbbe50d8cdb3320e9e3b1a06" integrity sha1-+mihT2qUXVTbvlDYzbMyDp47GgY= -commander@2.11.x, commander@^2.8.1, commander@^2.9.0: +commander@2.11.x, commander@^2.8.1: version "2.11.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" integrity sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ== @@ -2102,6 +2098,11 @@ commander@^2.20.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== +commander@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" + integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== + commandpost@^1.0.0: version "1.2.1" resolved "https://registry.yarnpkg.com/commandpost/-/commandpost-1.2.1.tgz#2e9c4c7508b9dc704afefaa91cab92ee6054cc68" @@ -2439,11 +2440,6 @@ cssom@0.3.x, "cssom@>= 0.3.0 < 0.4.0": dependencies: cssom "0.3.x" -cuint@^0.2.1: - version "0.2.2" - resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b" - integrity sha1-QICG1AlVDCYxFVYZ6fp7ytw7mRs= - cyclist@~0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" @@ -2537,19 +2533,6 @@ decompress-response@^4.2.0: dependencies: mimic-response "^2.0.0" -decompress-zip@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/decompress-zip/-/decompress-zip-0.3.0.tgz#ae3bcb7e34c65879adfe77e19c30f86602b4bdb0" - integrity sha1-rjvLfjTGWHmt/nfhnDD4ZgK0vbA= - dependencies: - binary "^0.3.0" - graceful-fs "^4.1.3" - mkpath "^0.1.0" - nopt "^3.0.1" - q "^1.1.2" - readable-stream "^1.1.8" - touch "0.0.3" - deemon@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/deemon/-/deemon-1.4.0.tgz#01c09cc23eec41e5d7ddac082eb52c3611d38dff" @@ -2863,10 +2846,10 @@ electron-to-chromium@^1.2.7: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.27.tgz#78ecb8a399066187bb374eede35d9c70565a803d" integrity sha1-eOy4o5kGYYe7N07t412ccFZagD0= -electron@9.2.0: - version "9.2.0" - resolved "https://registry.yarnpkg.com/electron/-/electron-9.2.0.tgz#d9fc8c8c9e5109669c366bd7b9ba83b06095d7a4" - integrity sha512-4ecZ3rcGg//Gk4fAK3Jo61T+uh36JhU6HHR/PTujQqQiBw1g4tNPd4R2hGGth2d+7FkRIs5GdRNef7h64fQEMw== +electron@9.2.1: + version "9.2.1" + resolved "https://registry.yarnpkg.com/electron/-/electron-9.2.1.tgz#54ef574e1af4ae967b5efa94312f1b6458d44a02" + integrity sha512-ZsetaQjXB8+9/EFW1FnfK4ukpkwXCxMEaiKiUZhZ0ZLFlLnFCpe0Bg4vdDf7e4boWGcnlgN1jAJpBw7w0eXuqA== dependencies: "@electron/get" "^1.0.1" "@types/node" "^12.0.12" @@ -3778,17 +3761,6 @@ fs-constants@^1.0.0: resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== -fs-extra@0.26.7: - version "0.26.7" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.26.7.tgz#9ae1fdd94897798edab76d0918cf42d0c3184fa9" - integrity sha1-muH92UiXeY7at20JGM9C0MMYT6k= - dependencies: - graceful-fs "^4.1.2" - jsonfile "^2.1.0" - klaw "^1.0.0" - path-is-absolute "^1.0.0" - rimraf "^2.2.8" - fs-extra@^7.0.0: version "7.0.1" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" @@ -4036,17 +4008,6 @@ glob@^5.0.13, glob@^5.0.3: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^6.0.4: - version "6.0.4" - resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" - integrity sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI= - dependencies: - inflight "^1.0.4" - inherits "2" - minimatch "2 || 3" - once "^1.3.0" - path-is-absolute "^1.0.0" - glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1, glob@^7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" @@ -4219,7 +4180,7 @@ graceful-fs@4.2.3, graceful-fs@^4.2.0: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== -graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.6, graceful-fs@^4.1.9: +graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" integrity sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg= @@ -5527,13 +5488,6 @@ json5@^2.1.0: dependencies: minimist "^1.2.0" -jsonfile@^2.1.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" - integrity sha1-NzaitCi4e72gzIO1P6PWM6NcKug= - optionalDependencies: - graceful-fs "^4.1.6" - jsonfile@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" @@ -5617,13 +5571,6 @@ kind-of@^6.0.0, kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA== -klaw@^1.0.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" - integrity sha1-QIhDO0azsbolnXh4XY6W9zugJDk= - optionalDependencies: - graceful-fs "^4.1.9" - last-run@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/last-run/-/last-run-1.1.1.tgz#45b96942c17b1c79c772198259ba943bebf8ca5b" @@ -6272,20 +6219,6 @@ mkdirp@^0.5.3: dependencies: minimist "^1.2.5" -mkpath@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/mkpath/-/mkpath-0.1.0.tgz#7554a6f8d871834cc97b5462b122c4c124d6de91" - integrity sha1-dVSm+Nhxg0zJe1RisSLEwSTW3pE= - -mksnapshot@^0.3.0: - version "0.3.1" - resolved "https://registry.yarnpkg.com/mksnapshot/-/mksnapshot-0.3.1.tgz#2501c05657436d742ce958a4ff92c77e40dd37e6" - integrity sha1-JQHAVldDbXQs6Vik/5LHfkDdN+Y= - dependencies: - decompress-zip "0.3.0" - fs-extra "0.26.7" - request "^2.79.0" - mocha-junit-reporter@^1.23.1: version "1.23.1" resolved "https://registry.yarnpkg.com/mocha-junit-reporter/-/mocha-junit-reporter-1.23.1.tgz#ba11519c0b967f404e4123dd69bc4ba022ab0f12" @@ -6407,10 +6340,10 @@ native-is-elevated@0.4.1: resolved "https://registry.yarnpkg.com/native-is-elevated/-/native-is-elevated-0.4.1.tgz#f6391aafb13441f5b949b39ae0b466b06e7f3986" integrity sha512-2vBXCXCXYKLDjP0WzrXs/AFjDb2njPR31EbGiZ1mR2fMJg211xClK1Xm19RXve35kvAL4dBKOFGCMIyc2+pPsw== -native-keymap@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/native-keymap/-/native-keymap-2.1.2.tgz#9773313f619d4c2b66b452cf036310a145523b59" - integrity sha512-n+oe+sxaauCFxomkl9Xrw1iUp88jTamMaGJSHNSGZ8rkIN9N+Wi6KIvBO8x3nmFxLI27KWu1d8IrLBxFKPNQag== +native-keymap@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/native-keymap/-/native-keymap-2.2.0.tgz#940aeb4ae05776dd44dbd9a80dba5342fd49fb8c" + integrity sha512-rWT9mf5f4vMGluXoIoxKSZy76fcVgMvk5jC4meBaOP2GfMJAI7Obtdzpa1Fa1qZCBtZa+OAYV8vlc8dKPOhUNw== native-watchdog@1.3.0: version "1.3.0" @@ -6530,13 +6463,6 @@ noop-logger@^0.1.1: resolved "https://registry.yarnpkg.com/noop-logger/-/noop-logger-0.1.1.tgz#94a2b1633c4f1317553007d8966fd0e841b6a4c2" integrity sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI= -nopt@^3.0.1: - version "3.0.6" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" - integrity sha1-xkZdvwirzU2zWTF/eaxopkayj/k= - dependencies: - abbrev "1" - nopt@^4.0.1, nopt@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" @@ -6545,13 +6471,6 @@ nopt@^4.0.1, nopt@~4.0.1: abbrev "1" osenv "^0.1.4" -nopt@~1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" - integrity sha1-bd0hvSoxQXuScn3Vhfim83YI6+4= - dependencies: - abbrev "1" - normalize-package-data@^2.3.2: version "2.4.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" @@ -7929,16 +7848,6 @@ read@^1.0.7: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@^1.1.8: - version "1.1.14" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" - integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "0.0.1" - string_decoder "~0.10.x" - readable-stream@~2.0.0: version "2.0.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" @@ -8098,7 +8007,7 @@ replacestream@^4.0.0: object-assign "^4.0.1" readable-stream "^2.0.2" -"request@>= 2.44.0 < 3.0.0", request@^2.79.0: +"request@>= 2.44.0 < 3.0.0": version "2.83.0" resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356" integrity sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw== @@ -9367,13 +9276,6 @@ timers-browserify@^2.0.4: dependencies: setimmediate "^1.0.4" -tmp@0.0.28: - version "0.0.28" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.28.tgz#172735b7f614ea7af39664fa84cf0de4e515d120" - integrity sha1-Fyc1t/YU6nrzlmT6hM8N5OUV0SA= - dependencies: - os-tmpdir "~1.0.1" - tmp@0.0.29: version "0.0.29" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.29.tgz#f25125ff0dd9da3ccb0c2dd371ee1288bb9128c0" @@ -9462,13 +9364,6 @@ to-through@^2.0.0: dependencies: through2 "^2.0.3" -touch@0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/touch/-/touch-0.0.3.tgz#51aef3d449571d4f287a5d87c9c8b49181a0db1d" - integrity sha1-Ua7z1ElXHU8oel2Hyci0kYGg2x0= - dependencies: - nopt "~1.0.10" - tough-cookie@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561" @@ -9484,11 +9379,6 @@ tough-cookie@~2.4.3: psl "^1.1.24" punycode "^1.4.1" -"traverse@>=0.3.0 <0.4": - version "0.3.9" - resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" - integrity sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk= - tree-kill@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" @@ -9512,6 +9402,12 @@ ts-loader@^4.4.2: micromatch "^3.1.4" semver "^5.0.1" +tsec@googleinterns/tsec: + version "0.0.1" + resolved "https://codeload.github.com/googleinterns/tsec/tar.gz/eb8abc0a58b16f97bb499833c21467fc6425260f" + dependencies: + "@types/node" "^13.13.5" + tslib@^1.8.1, tslib@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" @@ -9596,10 +9492,10 @@ typescript@^2.6.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.2.tgz#3c5b6fd7f6de0914269027f03c0946758f7673a4" integrity sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q= -typescript@^4.0.1-rc: - version "4.0.1-rc" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.1-rc.tgz#8adc78223eae56fe71d906a5fa90c3543b07a677" - integrity sha512-TCkspT3dSKOykbzS3/WSK7pqU2h1d/lEO6i45Afm5Y3XNAEAo8YXTG3kHOQk/wFq/5uPyO1+X8rb/Q+g7UsxJw== +typescript@^4.1.0-dev.20200824: + version "4.1.0-dev.20200901" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.0-dev.20200901.tgz#26eeb80e8c6e3c8712b1db51643507096590c15d" + integrity sha512-hh0i9TSQtEhHg7HqbOFvAczleOwZu9xSYL1vi4se1ruHLTKafT1+pecOC/TOIPz4RZNJ8I820ORBlOnDWWOPfg== uc.micro@^1.0.1, uc.micro@^1.0.3: version "1.0.3"