diff --git a/.eslintrc.json b/.eslintrc.json index acea42273e7..97333387f86 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -693,6 +693,7 @@ "when": "hasNode", "allow": [ "@parcel/watcher", + "@bpasero/watcher", "@vscode/sqlite3", "@vscode/vscode-languagedetection", "@vscode/ripgrep", @@ -846,7 +847,8 @@ "vs/platform/*/~", "vs/editor/~", "vs/editor/contrib/*/~", - "vs/editor/standalone/~" + "vs/editor/standalone/~", + "@vscode/tree-sitter-wasm" // type import ] }, { @@ -932,6 +934,7 @@ "tas-client-umd", // node module allowed even in /common/ "vscode-textmate", // node module allowed even in /common/ "@vscode/vscode-languagedetection", // node module allowed even in /common/ + "@vscode/tree-sitter-wasm", // type import { "when": "hasBrowser", "pattern": "@xterm/xterm" diff --git a/.github/classifier.json b/.github/classifier.json index d0a2c778997..5f322e25edd 100644 --- a/.github/classifier.json +++ b/.github/classifier.json @@ -124,7 +124,7 @@ "languages-basic": {"assign": ["aeschli"]}, "languages-diagnostics": {"assign": ["jrieken"]}, "languages-guessing": {"assign": ["TylerLeonhardt"]}, - "layout": {"assign": ["sbatten"]}, + "layout": {"assign": ["benibenj"]}, "lcd-text-rendering": {"assign": []}, "list-widget": {"assign": ["joaomoreno"]}, "live-preview": {"assign": ["andreamah"]}, @@ -249,7 +249,7 @@ "timeline": {"assign": ["lramos15"]}, "timeline-git": {"assign": ["lszomoru"]}, "timeline-local-history": {"assign": ["bpasero"]}, - "titlebar": {"assign": ["sbatten"]}, + "titlebar": {"assign": ["benibenj"]}, "tokenization": {"assign": ["alexdima"]}, "touch/pointer": {"assign": []}, "trackpad/scroll": {"assign": []}, @@ -278,7 +278,7 @@ "workbench-cli": {"assign": ["bpasero"]}, "workbench-diagnostics": {"assign": ["Tyriar"]}, "workbench-dnd": {"assign": ["bpasero"]}, - "workbench-editor-grid": {"assign": ["sbatten"]}, + "workbench-editor-grid": {"assign": ["benibenj"]}, "workbench-editor-groups": {"assign": ["bpasero"]}, "workbench-editor-resolver": {"assign": ["lramos15"]}, "workbench-editors": {"assign": ["bpasero"]}, @@ -299,11 +299,11 @@ "workbench-tabs": {"assign": ["benibenj"]}, "workbench-touchbar": {"assign": ["bpasero"]}, "workbench-untitled-editors": {"assign": ["bpasero"]}, - "workbench-views": {"assign": ["sbatten"]}, + "workbench-views": {"assign": ["benibenj"]}, "workbench-welcome": {"assign": ["lramos15"]}, "workbench-window": {"assign": ["bpasero"]}, "workbench-workspace": {"assign": []}, - "workbench-zen": {"assign": ["sbatten"]}, + "workbench-zen": {"assign": ["benibenj"]}, "workspace-edit": {"assign": ["jrieken"]}, "workspace-symbols": {"assign": []}, "workspace-trust": {"assign": ["lszomoru", "sbatten"]}, diff --git a/.vscode/extensions/vscode-selfhost-test-provider/src/sourceUtils.ts b/.vscode/extensions/vscode-selfhost-test-provider/src/sourceUtils.ts index 56b26cafda8..3ed21cf5810 100644 --- a/.vscode/extensions/vscode-selfhost-test-provider/src/sourceUtils.ts +++ b/.vscode/extensions/vscode-selfhost-test-provider/src/sourceUtils.ts @@ -8,6 +8,7 @@ import * as vscode from 'vscode'; import { TestCase, TestConstruct, TestSuite, VSCodeTest } from './testTree'; const suiteNames = new Set(['suite', 'flakySuite']); +const testNames = new Set(['test']); export const enum Action { Skip, @@ -19,22 +20,19 @@ export const extractTestFromNode = (src: ts.SourceFile, node: ts.Node, parent: V return Action.Recurse; } - let lhs = node.expression; - if (isSkipCall(lhs)) { + const asSuite = identifyCall(node.expression, suiteNames); + const asTest = identifyCall(node.expression, testNames); + const either = asSuite || asTest; + if (either === IdentifiedCall.Skipped) { return Action.Skip; } - - if (isPropertyCall(lhs) && lhs.name.text === 'only') { - lhs = lhs.expression; + if (either === IdentifiedCall.Nothing) { + return Action.Recurse; } const name = node.arguments[0]; const func = node.arguments[1]; - if (!name || !ts.isIdentifier(lhs) || !ts.isStringLiteralLike(name)) { - return Action.Recurse; - } - - if (!func) { + if (!name || !ts.isStringLiteralLike(name) || !func) { return Action.Recurse; } @@ -46,23 +44,45 @@ export const extractTestFromNode = (src: ts.SourceFile, node: ts.Node, parent: V ); const cparent = parent instanceof TestConstruct ? parent : undefined; - if (lhs.escapedText === 'test') { + + // we know this is either a suite or a test because we checked for skipped/nothing above + + if (asTest) { return new TestCase(name.text, range, cparent); } - if (suiteNames.has(lhs.escapedText.toString())) { + if (asSuite) { return new TestSuite(name.text, range, cparent); } - return Action.Recurse; + throw new Error('unreachable'); +}; + +const enum IdentifiedCall { + Nothing, + Skipped, + IsThing, +} + +const identifyCall = (lhs: ts.Node, needles: ReadonlySet): IdentifiedCall => { + if (ts.isIdentifier(lhs)) { + return needles.has(lhs.escapedText || lhs.text) ? IdentifiedCall.IsThing : IdentifiedCall.Nothing; + } + + if (isPropertyCall(lhs) && lhs.name.text === 'skip') { + return needles.has(lhs.expression.text) ? IdentifiedCall.Skipped : IdentifiedCall.Nothing; + } + + if (ts.isParenthesizedExpression(lhs) && ts.isConditionalExpression(lhs.expression)) { + return Math.max(identifyCall(lhs.expression.whenTrue, needles), identifyCall(lhs.expression.whenFalse, needles)); + } + + return IdentifiedCall.Nothing; }; const isPropertyCall = ( - lhs: ts.LeftHandSideExpression + lhs: ts.Node ): lhs is ts.PropertyAccessExpression & { expression: ts.Identifier; name: ts.Identifier } => ts.isPropertyAccessExpression(lhs) && ts.isIdentifier(lhs.expression) && ts.isIdentifier(lhs.name); - -const isSkipCall = (lhs: ts.LeftHandSideExpression) => - isPropertyCall(lhs) && suiteNames.has(lhs.expression.text) && lhs.name.text === 'skip'; diff --git a/build/.moduleignore b/build/.moduleignore index 3f573e06078..026c0d3b51d 100644 --- a/build/.moduleignore +++ b/build/.moduleignore @@ -110,6 +110,12 @@ node-pty/third_party/** @parcel/watcher/src/** !@parcel/watcher/build/Release/*.node +@bpasero/watcher/binding.gyp +@bpasero/watcher/build/** +@bpasero/watcher/prebuilds/** +@bpasero/watcher/src/** +!@bpasero/watcher/build/Release/*.node + vsda/build/** vsda/ci/** vsda/src/** diff --git a/build/azure-pipelines/cli/install-rust-posix.yml b/build/azure-pipelines/cli/install-rust-posix.yml index 89867143938..fee56e028f7 100644 --- a/build/azure-pipelines/cli/install-rust-posix.yml +++ b/build/azure-pipelines/cli/install-rust-posix.yml @@ -1,7 +1,7 @@ parameters: - name: channel type: string - default: 1.77 + default: 1.81 - name: targets default: [] type: object diff --git a/build/azure-pipelines/cli/install-rust-win32.yml b/build/azure-pipelines/cli/install-rust-win32.yml index 22fba8d7f6a..45a1cfd188e 100644 --- a/build/azure-pipelines/cli/install-rust-win32.yml +++ b/build/azure-pipelines/cli/install-rust-win32.yml @@ -1,7 +1,7 @@ parameters: - name: channel type: string - default: 1.77 + default: 1.81 - name: targets default: [] type: object diff --git a/build/gulpfile.extensions.js b/build/gulpfile.extensions.js index f00fa7a1ac3..4f745aebdc7 100644 --- a/build/gulpfile.extensions.js +++ b/build/gulpfile.extensions.js @@ -101,7 +101,6 @@ const tasks = compilations.map(function (tsconfigFile) { } function createPipeline(build, emitError, transpileOnly) { - const nlsDev = require('vscode-nls-dev'); const tsb = require('./lib/tsb'); const sourcemaps = require('gulp-sourcemaps'); @@ -126,7 +125,6 @@ const tasks = compilations.map(function (tsconfigFile) { .pipe(tsFilter) .pipe(util.loadSourcemaps()) .pipe(compilation()) - .pipe(build ? nlsDev.rewriteLocalizeCalls() : es.through()) .pipe(build ? util.stripSourceMappingURL() : es.through()) .pipe(sourcemaps.write('.', { sourceMappingURL: !build ? null : f => `${baseUrl}/${f.relative}.map`, @@ -136,9 +134,6 @@ const tasks = compilations.map(function (tsconfigFile) { sourceRoot: '../src/', })) .pipe(tsFilter.restore) - .pipe(build ? nlsDev.bundleMetaDataFiles(headerId, headerOut) : es.through()) - // Filter out *.nls.json file. We needed them only to bundle meta data file. - .pipe(filter(['**', '!**/*.nls.json'], { dot: true })) .pipe(reporter.end(emitError)); return es.duplex(input, output); diff --git a/build/gulpfile.scan.js b/build/gulpfile.scan.js index 582481bf1eb..be685a8b145 100644 --- a/build/gulpfile.scan.js +++ b/build/gulpfile.scan.js @@ -83,7 +83,8 @@ function nodeModules(destinationExe, destinationPdb, platform) { // We don't build the prebuilt node files so we don't scan them '!**/prebuilds/**/*.node', // These are 3rd party modules that we should ignore - '!**/@parcel/watcher/**/*'])) + '!**/@parcel/watcher/**/*', + '!**/@bpasero/watcher/**/*'])) .pipe(gulp.dest(destinationExe)); }; diff --git a/build/gulpfile.vscode.win32.js b/build/gulpfile.vscode.win32.js index 5adfdfbfe18..98175f530dd 100644 --- a/build/gulpfile.vscode.win32.js +++ b/build/gulpfile.vscode.win32.js @@ -16,7 +16,6 @@ const pkg = require('../package.json'); const product = require('../product.json'); const vfs = require('vinyl-fs'); const rcedit = require('rcedit'); -const mkdirp = require('mkdirp'); const repoPath = path.dirname(__dirname); const buildPath = (/** @type {string} */ arch) => path.join(path.dirname(repoPath), `VSCode-win32-${arch}`); @@ -75,7 +74,7 @@ function buildWin32Setup(arch, target) { const sourcePath = buildPath(arch); const outputPath = setupDir(arch, target); - mkdirp.sync(outputPath); + fs.mkdirSync(outputPath, { recursive: true }); const originalProductJsonPath = path.join(sourcePath, 'resources/app/product.json'); const productJsonPath = path.join(outputPath, 'product.json'); diff --git a/build/lib/builtInExtensions.js b/build/lib/builtInExtensions.js index 463ce16e18d..ac784c03506 100644 --- a/build/lib/builtInExtensions.js +++ b/build/lib/builtInExtensions.js @@ -16,7 +16,6 @@ const vfs = require("vinyl-fs"); const ext = require("./extensions"); const fancyLog = require("fancy-log"); const ansiColors = require("ansi-colors"); -const mkdirp = require('mkdirp'); const root = path.dirname(path.dirname(__dirname)); const productjson = JSON.parse(fs.readFileSync(path.join(__dirname, '../../product.json'), 'utf8')); const builtInExtensions = productjson.builtInExtensions || []; @@ -107,7 +106,7 @@ function readControlFile() { } } function writeControlFile(control) { - mkdirp.sync(path.dirname(controlFilePath)); + fs.mkdirSync(path.dirname(controlFilePath), { recursive: true }); fs.writeFileSync(controlFilePath, JSON.stringify(control, null, 2)); } function getBuiltInExtensions() { diff --git a/build/lib/builtInExtensions.ts b/build/lib/builtInExtensions.ts index fefed436bb9..8b831d42d44 100644 --- a/build/lib/builtInExtensions.ts +++ b/build/lib/builtInExtensions.ts @@ -15,8 +15,6 @@ import * as fancyLog from 'fancy-log'; import * as ansiColors from 'ansi-colors'; import { Stream } from 'stream'; -const mkdirp = require('mkdirp'); - export interface IExtensionDefinition { name: string; version: string; @@ -147,7 +145,7 @@ function readControlFile(): IControlFile { } function writeControlFile(control: IControlFile): void { - mkdirp.sync(path.dirname(controlFilePath)); + fs.mkdirSync(path.dirname(controlFilePath), { recursive: true }); fs.writeFileSync(controlFilePath, JSON.stringify(control, null, 2)); } diff --git a/build/lib/standalone.js b/build/lib/standalone.js index d106585d28c..b724a009e8a 100644 --- a/build/lib/standalone.js +++ b/build/lib/standalone.js @@ -51,7 +51,12 @@ function extractEditor(options) { // Add extra .d.ts files from `node_modules/@types/` if (Array.isArray(options.compilerOptions?.types)) { options.compilerOptions.types.forEach((type) => { - options.typings.push(`../node_modules/@types/${type}/index.d.ts`); + if (type === '@webgpu/types') { + options.typings.push(`../node_modules/${type}/dist/index.d.ts`); + } + else { + options.typings.push(`../node_modules/@types/${type}/index.d.ts`); + } }); } const result = tss.shake(options); diff --git a/build/lib/standalone.ts b/build/lib/standalone.ts index 546afcbb589..9563cd6670b 100644 --- a/build/lib/standalone.ts +++ b/build/lib/standalone.ts @@ -59,7 +59,11 @@ export function extractEditor(options: tss.ITreeShakingOptions & { destRoot: str // Add extra .d.ts files from `node_modules/@types/` if (Array.isArray(options.compilerOptions?.types)) { options.compilerOptions.types.forEach((type: string) => { - options.typings.push(`../node_modules/@types/${type}/index.d.ts`); + if (type === '@webgpu/types') { + options.typings.push(`../node_modules/${type}/dist/index.d.ts`); + } else { + options.typings.push(`../node_modules/@types/${type}/index.d.ts`); + } }); } diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index b1a96e14f0a..56aa02ebfb7 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -546,9 +546,9 @@ "--vscode-scmGraph-foreground1", "--vscode-scmGraph-foreground2", "--vscode-scmGraph-foreground3", - "--vscode-scmGraph-historyItemGroupBase", - "--vscode-scmGraph-historyItemGroupLocal", - "--vscode-scmGraph-historyItemGroupRemote", + "--vscode-scmGraph-historyItemBaseRefColor", + "--vscode-scmGraph-historyItemRefColor", + "--vscode-scmGraph-historyItemRemoteRefColor", "--vscode-scmGraph-historyItemHoverAdditionsForeground", "--vscode-scmGraph-historyItemHoverDefaultLabelBackground", "--vscode-scmGraph-historyItemHoverDefaultLabelForeground", @@ -881,4 +881,4 @@ "--widget-color", "--text-link-decoration" ] -} \ No newline at end of file +} diff --git a/build/package-lock.json b/build/package-lock.json index c13ecdcabc5..4499f7cf541 100644 --- a/build/package-lock.json +++ b/build/package-lock.json @@ -31,7 +31,6 @@ "@types/mime": "0.0.29", "@types/minimatch": "^3.0.3", "@types/minimist": "^1.2.1", - "@types/mkdirp": "^1.0.1", "@types/mocha": "^9.1.1", "@types/node": "20.x", "@types/pump": "^1.0.1", @@ -54,7 +53,6 @@ "gulp-sort": "^2.0.0", "jsonc-parser": "^2.3.0", "mime": "^1.4.1", - "mkdirp": "^1.0.4", "source-map": "0.6.1", "ternary-stream": "^3.0.0", "through2": "^4.0.2", @@ -133,9 +131,10 @@ } }, "node_modules/@azure/core-http": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-3.0.0.tgz", - "integrity": "sha512-BxI2SlGFPPz6J1XyZNIVUf0QZLBKFX+ViFjKOkzqD18J1zOINIQ8JSBKKr+i+v8+MB6LacL6Nn/sP/TE13+s2Q==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@azure/core-http/-/core-http-3.0.4.tgz", + "integrity": "sha512-Fok9VVhMdxAFOtqiiAtg74fL0UJkt0z3D+ouUUxcRLzZNBioPRAMJFVxiWoJljYpXsRi4GDQHzQHDc9AiYaIUQ==", + "deprecated": "deprecating as we migrated to core v2", "dev": true, "dependencies": { "@azure/abort-controller": "^1.0.0", @@ -151,7 +150,7 @@ "tslib": "^2.2.0", "tunnel": "^0.0.6", "uuid": "^8.3.0", - "xml2js": "^0.4.19" + "xml2js": "^0.5.0" }, "engines": { "node": ">=14.0.0" @@ -1123,15 +1122,6 @@ "integrity": "sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg==", "dev": true }, - "node_modules/@types/mkdirp": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-1.0.1.tgz", - "integrity": "sha512-HkGSK7CGAXncr8Qn/0VqNtExEE+PHMWb+qlR1faHMao7ng6P3tAaoWWBMdva0gL5h4zprjIO89GJOLXsMcDm1Q==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/mocha": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.1.tgz", @@ -1372,28 +1362,6 @@ "node": ">=10" } }, - "node_modules/@vscode/vsce/node_modules/xml2js": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", - "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", - "dev": true, - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/@vscode/vsce/node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/@xmldom/xmldom": { "version": "0.8.10", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", @@ -3374,18 +3342,6 @@ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "devOptional": true }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -4594,9 +4550,9 @@ "devOptional": true }, "node_modules/xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", "dev": true, "dependencies": { "sax": ">=0.6.0", diff --git a/build/package.json b/build/package.json index 5e637eaebf4..7017b9144e0 100644 --- a/build/package.json +++ b/build/package.json @@ -25,7 +25,6 @@ "@types/mime": "0.0.29", "@types/minimatch": "^3.0.3", "@types/minimist": "^1.2.1", - "@types/mkdirp": "^1.0.1", "@types/mocha": "^9.1.1", "@types/node": "20.x", "@types/pump": "^1.0.1", @@ -48,7 +47,6 @@ "gulp-sort": "^2.0.0", "jsonc-parser": "^2.3.0", "mime": "^1.4.1", - "mkdirp": "^1.0.4", "source-map": "0.6.1", "ternary-stream": "^3.0.0", "through2": "^4.0.2", diff --git a/cglicenses.json b/cglicenses.json index d75a53bf172..0b4e03e502b 100644 --- a/cglicenses.json +++ b/cglicenses.json @@ -241,33 +241,6 @@ "CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." ] }, - { - // Reason: The substack org has been deleted on GH - "name": "mkdirp", - "fullLicenseText": [ - "Copyright 2010 James Halliday (mail@substack.net)", - "", - "This project is free software released under the MIT/X11 license:", - "", - "Permission is hereby granted, free of charge, to any person obtaining a copy", - "of this software and associated documentation files (the \"Software\"), to deal", - "in the Software without restriction, including without limitation the rights", - "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell", - "copies of the Software, and to permit persons to whom the Software is", - "furnished to do so, subject to the following conditions:", - "", - "The above copyright notice and this permission notice shall be included in", - "all copies or substantial portions of the Software.", - "", - "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR", - "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,", - "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE", - "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER", - "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,", - "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN", - "THE SOFTWARE." - ] - }, { // Reason: repo URI is wrong on crate, pending https://github.com/warp-tech/russh/pull/53 "name": "russh-cryptovec", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index b820ffcc50f..2907ff3d7e7 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -81,4 +81,5 @@ codegen-units = 1 [features] default = [] +vsda = [] vscode-encrypt = [] diff --git a/cli/src/auth.rs b/cli/src/auth.rs index 2d9162c5483..51942c96c75 100644 --- a/cli/src/auth.rs +++ b/cli/src/auth.rs @@ -723,7 +723,7 @@ impl Auth { match &init_code_json.message { Some(m) => self.log.result(m), - None => self.log.result(&format!( + None => self.log.result(format!( "To grant access to the server, please log into {} and use code {}", init_code_json.verification_uri, init_code_json.user_code )), diff --git a/cli/src/commands/serve_web.rs b/cli/src/commands/serve_web.rs index d8d2a49bb1a..4acb9a18c73 100644 --- a/cli/src/commands/serve_web.rs +++ b/cli/src/commands/serve_web.rs @@ -15,7 +15,7 @@ use std::time::{Duration, Instant}; use hyper::service::{make_service_fn, service_fn}; use hyper::{Body, Request, Response, Server}; use tokio::io::{AsyncBufReadExt, BufReader}; -use tokio::pin; +use tokio::{pin, time}; use crate::async_pipe::{ get_socket_name, get_socket_rw_stream, listen_socket_rw_stream, AsyncPipe, @@ -50,7 +50,7 @@ const SERVER_IDLE_TIMEOUT_SECS: u64 = 60 * 60; /// (should be large enough to basically never happen) const SERVER_ACTIVE_TIMEOUT_SECS: u64 = SERVER_IDLE_TIMEOUT_SECS * 24 * 30 * 12; /// How long to cache the "latest" version we get from the update service. -const RELEASE_CACHE_SECS: u64 = 60 * 60; +const RELEASE_CHECK_INTERVAL: u64 = 60 * 60; /// Number of bytes for the secret keys. See workbench.ts for their usage. const SECRET_KEY_BYTES: usize = 32; @@ -86,7 +86,11 @@ pub async fn serve_web(ctx: CommandContext, mut args: ServeWebArgs) -> Result = ConnectionManager::new(&ctx, platform, args.clone()); + let update_check_interval = 3600; + cm.clone() + .start_update_checker(Duration::from_secs(update_check_interval)); + let key = get_server_key_half(&ctx.paths); let make_svc = move || { let ctx = HandleContext { @@ -175,7 +179,7 @@ async fn handle_proxied(ctx: &HandleContext, req: Request) -> Response r, Err(e) => { error!(ctx.log, "error getting latest version: {}", e); @@ -538,21 +542,67 @@ impl ConnectionManager { pub fn new(ctx: &CommandContext, platform: Platform, args: ServeWebArgs) -> Arc { let base_path = normalize_base_path(args.server_base_path.as_deref().unwrap_or_default()); + let cache = DownloadCache::new(ctx.paths.web_server_storage()); + let target_kind = TargetKind::Web; + + let quality = VSCODE_CLI_QUALITY.map_or(Quality::Stable, |q| match Quality::try_from(q) { + Ok(q) => q, + Err(_) => Quality::Stable, + }); + + let latest_version = tokio::sync::Mutex::new(cache.get().first().map(|latest_commit| { + ( + Instant::now() - Duration::from_secs(RELEASE_CHECK_INTERVAL), + Release { + name: String::from("0.0.0"), // Version information not stored on cache + commit: latest_commit.clone(), + platform, + target: target_kind, + quality, + }, + ) + })); + Arc::new(Self { platform, args, base_path, log: ctx.log.clone(), - cache: DownloadCache::new(ctx.paths.web_server_storage()), + cache, update_service: UpdateService::new( ctx.log.clone(), Arc::new(ReqwestSimpleHttp::with_client(ctx.http.clone())), ), state: ConnectionStateMap::default(), - latest_version: tokio::sync::Mutex::default(), + latest_version, }) } + // spawns a task that checks for updates every n seconds duration + pub fn start_update_checker(self: Arc, duration: Duration) { + tokio::spawn(async move { + let mut interval = time::interval(duration); + loop { + interval.tick().await; + + if let Err(e) = self.get_latest_release().await { + warning!(self.log, "error getting latest version: {}", e); + } + } + }); + } + + // Returns the latest release from the cache, if one exists. + pub async fn get_release_from_cache(&self) -> Result { + let latest = self.latest_version.lock().await; + if let Some((_, release)) = &*latest { + return Ok(release.clone()); + } + + drop(latest); + self.get_latest_release().await + } + /// Gets a connection to a server version pub async fn get_connection( &self, @@ -571,11 +621,7 @@ impl ConnectionManager { pub async fn get_latest_release(&self) -> Result { let mut latest = self.latest_version.lock().await; let now = Instant::now(); - if let Some((checked_at, release)) = &*latest { - if checked_at.elapsed() < Duration::from_secs(RELEASE_CACHE_SECS) { - return Ok(release.clone()); - } - } + let target_kind = TargetKind::Web; let quality = VSCODE_CLI_QUALITY .ok_or_else(|| CodeError::UpdatesNotConfigured("no configured quality")) @@ -585,13 +631,14 @@ impl ConnectionManager { let release = self .update_service - .get_latest_commit(self.platform, TargetKind::Web, quality) + .get_latest_commit(self.platform, target_kind, quality) .await .map_err(|e| CodeError::UpdateCheckFailed(e.to_string())); // If the update service is unavailable and we have stale data, use that - if let (Err(e), Some((_, previous))) = (&release, &*latest) { + if let (Err(e), Some((_, previous))) = (&release, latest.clone()) { warning!(self.log, "error getting latest release, using stale: {}", e); + *latest = Some((now, previous.clone())); return Ok(previous.clone()); } diff --git a/cli/src/download_cache.rs b/cli/src/download_cache.rs index d3f05d2237f..cd02b02d75a 100644 --- a/cli/src/download_cache.rs +++ b/cli/src/download_cache.rs @@ -20,6 +20,7 @@ const KEEP_LRU: usize = 5; const STAGING_SUFFIX: &str = ".staging"; const RENAME_ATTEMPTS: u32 = 20; const RENAME_DELAY: std::time::Duration = std::time::Duration::from_millis(200); +const PERSISTED_STATE_FILE_NAME: &str = "lru.json"; #[derive(Clone)] pub struct DownloadCache { @@ -30,11 +31,16 @@ pub struct DownloadCache { impl DownloadCache { pub fn new(path: PathBuf) -> DownloadCache { DownloadCache { - state: PersistedState::new(path.join("lru.json")), + state: PersistedState::new(path.join(PERSISTED_STATE_FILE_NAME)), path, } } + /// Gets the value stored on the state + pub fn get(&self) -> Vec { + self.state.load() + } + /// Gets the download cache path. Names of cache entries can be formed by /// joining them to the path. pub fn path(&self) -> &Path { diff --git a/cli/src/tunnels/code_server.rs b/cli/src/tunnels/code_server.rs index 0579f8ef0dc..465f6e24230 100644 --- a/cli/src/tunnels/code_server.rs +++ b/cli/src/tunnels/code_server.rs @@ -674,7 +674,7 @@ where let write_line = |line: &str| -> std::io::Result<()> { if let Some(mut f) = log_file.as_ref() { f.write_all(line.as_bytes())?; - f.write_all(&[b'\n'])?; + f.write_all(b"\n")?; } if write_directly { println!("{}", line); diff --git a/extensions/configuration-editing/.vscodeignore b/extensions/configuration-editing/.vscodeignore index ba1754c45fa..679a6d6859f 100644 --- a/extensions/configuration-editing/.vscodeignore +++ b/extensions/configuration-editing/.vscodeignore @@ -4,7 +4,6 @@ tsconfig.json out/** extension.webpack.config.js extension-browser.webpack.config.js -yarn.lock package-lock.json build/** schemas/devContainer.codespaces.schema.json diff --git a/extensions/css-language-features/.vscodeignore b/extensions/css-language-features/.vscodeignore index 73d4b9ba388..f6411e76fdb 100644 --- a/extensions/css-language-features/.vscodeignore +++ b/extensions/css-language-features/.vscodeignore @@ -11,10 +11,8 @@ server/tsconfig.json server/test/** server/bin/** server/build/** -server/yarn.lock server/package-lock.json server/.npmignore -yarn.lock package-lock.json server/extension.webpack.config.js extension.webpack.config.js diff --git a/extensions/debug-auto-launch/.vscodeignore b/extensions/debug-auto-launch/.vscodeignore index 74215d83d91..360fcfd1c99 100644 --- a/extensions/debug-auto-launch/.vscodeignore +++ b/extensions/debug-auto-launch/.vscodeignore @@ -2,5 +2,4 @@ src/** tsconfig.json out/** extension.webpack.config.js -yarn.lock package-lock.json diff --git a/extensions/debug-server-ready/.vscodeignore b/extensions/debug-server-ready/.vscodeignore index ca4c65e8cce..536dd0720a3 100644 --- a/extensions/debug-server-ready/.vscodeignore +++ b/extensions/debug-server-ready/.vscodeignore @@ -2,6 +2,5 @@ src/** tsconfig.json out/** extension.webpack.config.js -yarn.lock package-lock.json .vscode diff --git a/extensions/emmet/.vscodeignore b/extensions/emmet/.vscodeignore index 1ced958403c..ccf478fb97f 100644 --- a/extensions/emmet/.vscodeignore +++ b/extensions/emmet/.vscodeignore @@ -7,6 +7,5 @@ extension.webpack.config.js extension-browser.webpack.config.js CONTRIBUTING.md cgmanifest.json -yarn.lock package-lock.json .vscode diff --git a/extensions/extension-editing/.vscodeignore b/extensions/extension-editing/.vscodeignore index 15a37fd80be..8d4da76b9cb 100644 --- a/extensions/extension-editing/.vscodeignore +++ b/extensions/extension-editing/.vscodeignore @@ -4,5 +4,4 @@ tsconfig.json out/** extension.webpack.config.js extension-browser.webpack.config.js -yarn.lock package-lock.json diff --git a/extensions/git/.vscodeignore b/extensions/git/.vscodeignore index 61b7b9eae5b..1e6130d5c7d 100644 --- a/extensions/git/.vscodeignore +++ b/extensions/git/.vscodeignore @@ -4,5 +4,4 @@ out/** tsconfig.json build/** extension.webpack.config.js -yarn.lock package-lock.json diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index d80a203b5c3..2b4e7d2dfaa 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -3059,7 +3059,7 @@ export class CommandCenter { @command('git.fetchRef', { repository: true }) async fetchRef(repository: Repository, ref?: string): Promise { - ref = ref ?? repository?.historyProvider.currentHistoryItemGroup?.remote?.id; + ref = ref ?? repository?.historyProvider.currentHistoryItemRemoteRef?.id; if (!repository || !ref) { return; } @@ -3132,7 +3132,7 @@ export class CommandCenter { @command('git.pullRef', { repository: true }) async pullRef(repository: Repository, ref?: string): Promise { - ref = ref ?? repository?.historyProvider.currentHistoryItemGroup?.remote?.id; + ref = ref ?? repository?.historyProvider.currentHistoryItemRemoteRef?.id; if (!repository || !ref) { return; } diff --git a/extensions/git/src/decorationProvider.ts b/extensions/git/src/decorationProvider.ts index 8c86834c92c..f1c675ceff1 100644 --- a/extensions/git/src/decorationProvider.ts +++ b/extensions/git/src/decorationProvider.ts @@ -164,11 +164,11 @@ class GitIncomingChangesFileDecorationProvider implements FileDecorationProvider constructor(private readonly repository: Repository) { this.disposables.push( window.registerFileDecorationProvider(this), - runAndSubscribeEvent(repository.historyProvider.onDidChangeCurrentHistoryItemGroup, () => this.onDidChangeCurrentHistoryItemGroup()) + runAndSubscribeEvent(repository.historyProvider.onDidChangeCurrentHistoryItemRefs, () => this.onDidChangeCurrentHistoryItemRefs()) ); } - private async onDidChangeCurrentHistoryItemGroup(): Promise { + private async onDidChangeCurrentHistoryItemRefs(): Promise { const newDecorations = new Map(); await this.collectIncomingChangesFileDecorations(newDecorations); const uris = new Set([...this.decorations.keys()].concat([...newDecorations.keys()])); @@ -218,18 +218,19 @@ class GitIncomingChangesFileDecorationProvider implements FileDecorationProvider private async getIncomingChanges(): Promise { try { const historyProvider = this.repository.historyProvider; - const currentHistoryItemGroup = historyProvider.currentHistoryItemGroup; + const currentHistoryItemRef = historyProvider.currentHistoryItemRef; + const currentHistoryItemRemoteRef = historyProvider.currentHistoryItemRemoteRef; - if (!currentHistoryItemGroup?.remote) { + if (!currentHistoryItemRef || !currentHistoryItemRemoteRef) { return []; } - const ancestor = await historyProvider.resolveHistoryItemGroupCommonAncestor([currentHistoryItemGroup.id, currentHistoryItemGroup.remote.id]); + const ancestor = await historyProvider.resolveHistoryItemRefsCommonAncestor([currentHistoryItemRef.id, currentHistoryItemRemoteRef.id]); if (!ancestor) { return []; } - const changes = await this.repository.diffBetween(ancestor, currentHistoryItemGroup.remote.id); + const changes = await this.repository.diffBetween(ancestor, currentHistoryItemRemoteRef.id); return changes; } catch (err) { return []; diff --git a/extensions/git/src/historyProvider.ts b/extensions/git/src/historyProvider.ts index 7d79e128844..8beaeef0132 100644 --- a/extensions/git/src/historyProvider.ts +++ b/extensions/git/src/historyProvider.ts @@ -4,36 +4,69 @@ *--------------------------------------------------------------------------------------------*/ -import { Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryItemGroup, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel, SourceControlHistoryItemLabel } from 'vscode'; +import { Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, SourceControlHistoryItem, SourceControlHistoryItemChange, SourceControlHistoryOptions, SourceControlHistoryProvider, ThemeIcon, Uri, window, LogOutputChannel, SourceControlHistoryItemRef, l10n, SourceControlHistoryItemRefsChangeEvent } from 'vscode'; import { Repository, Resource } from './repository'; -import { IDisposable, dispose } from './util'; +import { IDisposable, deltaHistoryItemRefs, dispose } from './util'; import { toGitUri } from './uri'; -import { Branch, LogOptions, RefType } from './api/git'; +import { Branch, LogOptions, Ref, RefType } from './api/git'; import { emojify, ensureEmojis } from './emoji'; import { Commit } from './git'; +function toSourceControlHistoryItemRef(ref: Ref): SourceControlHistoryItemRef { + switch (ref.type) { + case RefType.RemoteHead: + return { + id: `refs/remotes/${ref.name}`, + name: ref.name ?? '', + description: ref.commit ? l10n.t('Remote branch at {0}', ref.commit.substring(0, 8)) : undefined, + revision: ref.commit, + icon: new ThemeIcon('cloud'), + category: l10n.t('remote branches') + }; + case RefType.Tag: + return { + id: `refs/tags/${ref.name}`, + name: ref.name ?? '', + description: ref.commit ? l10n.t('Tag at {0}', ref.commit.substring(0, 8)) : undefined, + revision: ref.commit, + icon: new ThemeIcon('tag'), + category: l10n.t('tags') + }; + default: + return { + id: `refs/heads/${ref.name}`, + name: ref.name ?? '', + description: ref.commit ? ref.commit.substring(0, 8) : undefined, + revision: ref.commit, + icon: new ThemeIcon('git-branch'), + category: l10n.t('branches') + }; + } +} + export class GitHistoryProvider implements SourceControlHistoryProvider, FileDecorationProvider, IDisposable { - - private readonly _onDidChangeCurrentHistoryItemGroup = new EventEmitter(); - readonly onDidChangeCurrentHistoryItemGroup: Event = this._onDidChangeCurrentHistoryItemGroup.event; - private readonly _onDidChangeDecorations = new EventEmitter(); readonly onDidChangeFileDecorations: Event = this._onDidChangeDecorations.event; - private _currentHistoryItemGroup: SourceControlHistoryItemGroup | undefined; - get currentHistoryItemGroup(): SourceControlHistoryItemGroup | undefined { return this._currentHistoryItemGroup; } - set currentHistoryItemGroup(value: SourceControlHistoryItemGroup | undefined) { - this._currentHistoryItemGroup = value; - this._onDidChangeCurrentHistoryItemGroup.fire(); - } + private _currentHistoryItemRef: SourceControlHistoryItemRef | undefined; + get currentHistoryItemRef(): SourceControlHistoryItemRef | undefined { return this._currentHistoryItemRef; } + + private _currentHistoryItemRemoteRef: SourceControlHistoryItemRef | undefined; + get currentHistoryItemRemoteRef(): SourceControlHistoryItemRef | undefined { return this._currentHistoryItemRemoteRef; } + + private _currentHistoryItemBaseRef: SourceControlHistoryItemRef | undefined; + get currentHistoryItemBaseRef(): SourceControlHistoryItemRef | undefined { return this._currentHistoryItemBaseRef; } + + private readonly _onDidChangeCurrentHistoryItemRefs = new EventEmitter(); + readonly onDidChangeCurrentHistoryItemRefs: Event = this._onDidChangeCurrentHistoryItemRefs.event; + + private readonly _onDidChangeHistoryItemRefs = new EventEmitter(); + readonly onDidChangeHistoryItemRefs: Event = this._onDidChangeHistoryItemRefs.event; + + private _HEAD: Branch | undefined; + private historyItemRefs: SourceControlHistoryItemRef[] = []; private historyItemDecorations = new Map(); - private historyItemLabels = new Map([ - ['HEAD -> refs/heads/', new ThemeIcon('target')], - ['tag: refs/tags/', new ThemeIcon('tag')], - ['refs/heads/', new ThemeIcon('git-branch')], - ['refs/remotes/', new ThemeIcon('cloud')], - ]); private disposables: Disposable[] = []; @@ -45,53 +78,124 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec private async onDidRunGitStatus(): Promise { if (!this.repository.HEAD) { this.logger.trace('[GitHistoryProvider][onDidRunGitStatus] repository.HEAD is undefined'); - this.currentHistoryItemGroup = undefined; + this._currentHistoryItemRef = this._currentHistoryItemRemoteRef = this._currentHistoryItemBaseRef = undefined; + this._onDidChangeCurrentHistoryItemRefs.fire(); + return; } - // Get the merge base of the current history item group - const mergeBase = await this.resolveHEADMergeBase(); + let historyItemRefId = ''; + let historyItemRefName = ''; - // Handle tag, and detached commit - const currentHistoryItemGroupId = - this.repository.HEAD.name === undefined ? - this.repository.HEAD.commit : - this.repository.HEAD.type === RefType.Tag ? - `refs/tags/${this.repository.HEAD.name}` : - `refs/heads/${this.repository.HEAD.name}`; + switch (this.repository.HEAD.type) { + case RefType.Head: { + if (this.repository.HEAD.name !== undefined) { + // Branch + historyItemRefId = `refs/heads/${this.repository.HEAD.name}`; + historyItemRefName = this.repository.HEAD.name; - // Detached commit - const currentHistoryItemGroupName = - this.repository.HEAD.name ?? this.repository.HEAD.commit; + // Remote + this._currentHistoryItemRemoteRef = this.repository.HEAD.upstream ? { + id: `refs/remotes/${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, + name: `${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, + revision: this.repository.HEAD.upstream.commit, + icon: new ThemeIcon('cloud') + } : undefined; - this.currentHistoryItemGroup = { - id: currentHistoryItemGroupId ?? '', - name: currentHistoryItemGroupName ?? '', + // Base - compute only if the branch has changed + if (this._HEAD?.name !== this.repository.HEAD.name) { + const mergeBase = await this.resolveHEADMergeBase(); + + this._currentHistoryItemBaseRef = mergeBase && + (mergeBase.remote !== this.repository.HEAD.upstream?.remote || + mergeBase.name !== this.repository.HEAD.upstream?.name) ? { + id: `refs/remotes/${mergeBase.remote}/${mergeBase.name}`, + name: `${mergeBase.remote}/${mergeBase.name}`, + revision: mergeBase.commit + } : undefined; + } + } else { + // Detached commit + historyItemRefId = this.repository.HEAD.commit ?? ''; + historyItemRefName = this.repository.HEAD.commit ?? ''; + + this._currentHistoryItemRemoteRef = undefined; + this._currentHistoryItemBaseRef = undefined; + } + break; + } + case RefType.Tag: { + // Tag + historyItemRefId = `refs/tags/${this.repository.HEAD.name}`; + historyItemRefName = this.repository.HEAD.name ?? this.repository.HEAD.commit ?? ''; + + this._currentHistoryItemRemoteRef = undefined; + this._currentHistoryItemBaseRef = undefined; + break; + } + } + + this._HEAD = this.repository.HEAD; + + this._currentHistoryItemRef = { + id: historyItemRefId, + name: historyItemRefName, revision: this.repository.HEAD.commit, - remote: this.repository.HEAD.upstream ? { - id: `refs/remotes/${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, - name: `${this.repository.HEAD.upstream.remote}/${this.repository.HEAD.upstream.name}`, - revision: this.repository.HEAD.upstream.commit - } : undefined, - base: mergeBase && - (mergeBase.remote !== this.repository.HEAD.upstream?.remote || - mergeBase.name !== this.repository.HEAD.upstream?.name) ? { - id: `refs/remotes/${mergeBase.remote}/${mergeBase.name}`, - name: `${mergeBase.remote}/${mergeBase.name}`, - revision: mergeBase.commit - } : undefined + icon: new ThemeIcon('target'), }; - this.logger.trace(`[GitHistoryProvider][onDidRunGitStatus] currentHistoryItemGroup: ${JSON.stringify(this.currentHistoryItemGroup)}`); + this._onDidChangeCurrentHistoryItemRefs.fire(); + this.logger.trace(`[GitHistoryProvider][onDidRunGitStatus] currentHistoryItemRef: ${JSON.stringify(this._currentHistoryItemRef)}`); + this.logger.trace(`[GitHistoryProvider][onDidRunGitStatus] currentHistoryItemRemoteRef: ${JSON.stringify(this._currentHistoryItemRemoteRef)}`); + this.logger.trace(`[GitHistoryProvider][onDidRunGitStatus] currentHistoryItemBaseRef: ${JSON.stringify(this._currentHistoryItemBaseRef)}`); + + // Refs (alphabetically) + const refs = await this.repository.getRefs({ sort: 'alphabetically' }); + const historyItemRefs = refs.map(ref => toSourceControlHistoryItemRef(ref)); + + const delta = deltaHistoryItemRefs(this.historyItemRefs, historyItemRefs); + this._onDidChangeHistoryItemRefs.fire(delta); + this.historyItemRefs = historyItemRefs; + + const deltaLog = { + added: delta.added.map(ref => ref.id), + modified: delta.modified.map(ref => ref.id), + removed: delta.removed.map(ref => ref.id) + }; + this.logger.trace(`[GitHistoryProvider][onDidRunGitStatus] historyItemRefs: ${JSON.stringify(deltaLog)}`); + } + + async provideHistoryItemRefs(): Promise { + const refs = await this.repository.getRefs(); + + const branches: SourceControlHistoryItemRef[] = []; + const remoteBranches: SourceControlHistoryItemRef[] = []; + const tags: SourceControlHistoryItemRef[] = []; + + for (const ref of refs) { + switch (ref.type) { + case RefType.RemoteHead: + remoteBranches.push(toSourceControlHistoryItemRef(ref)); + break; + case RefType.Tag: + tags.push(toSourceControlHistoryItemRef(ref)); + break; + default: + branches.push(toSourceControlHistoryItemRef(ref)); + break; + } + } + + return [...branches, ...remoteBranches, ...tags]; } async provideHistoryItems(options: SourceControlHistoryOptions): Promise { - if (!this.currentHistoryItemGroup || !options.historyItemGroupIds) { + if (!this.currentHistoryItemRef || !options.historyItemRefs) { return []; } // Deduplicate refNames - const refNames = Array.from(new Set(options.historyItemGroupIds)); + const refNames = Array.from(new Set(options.historyItemRefs)); let logOptions: LogOptions = { refNames, shortStats: true }; @@ -115,7 +219,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec await ensureEmojis(); return commits.map(commit => { - const labels = this.resolveHistoryItemLabels(commit); + const references = this.resolveHistoryItemRefs(commit); return { id: commit.hash, @@ -126,7 +230,7 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec displayId: commit.hash.substring(0, 8), timestamp: commit.authorDate?.getTime(), statistics: commit.shortStat ?? { files: 0, insertions: 0, deletions: 0 }, - labels: labels.length !== 0 ? labels : undefined + references: references.length !== 0 ? references : undefined }; }); } catch (err) { @@ -169,21 +273,21 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec return historyItemChanges; } - async resolveHistoryItemGroupCommonAncestor(historyItemGroupIds: string[]): Promise { + async resolveHistoryItemRefsCommonAncestor(historyItemRefs: string[]): Promise { try { - if (historyItemGroupIds.length === 0) { + if (historyItemRefs.length === 0) { // TODO@lszomoru - log return undefined; - } else if (historyItemGroupIds.length === 1 && historyItemGroupIds[0] === this.currentHistoryItemGroup?.id) { + } else if (historyItemRefs.length === 1 && historyItemRefs[0] === this.currentHistoryItemRef?.id) { // Remote - if (this.currentHistoryItemGroup.remote) { - const ancestor = await this.repository.getMergeBase(historyItemGroupIds[0], this.currentHistoryItemGroup.remote.id); + if (this.currentHistoryItemRemoteRef) { + const ancestor = await this.repository.getMergeBase(historyItemRefs[0], this.currentHistoryItemRemoteRef.id); return ancestor; } // Base - if (this.currentHistoryItemGroup.base) { - const ancestor = await this.repository.getMergeBase(historyItemGroupIds[0], this.currentHistoryItemGroup.base.id); + if (this.currentHistoryItemBaseRef) { + const ancestor = await this.repository.getMergeBase(historyItemRefs[0], this.currentHistoryItemBaseRef.id); return ancestor; } @@ -192,13 +296,13 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec if (commits.length > 0) { return commits[0].hash; } - } else if (historyItemGroupIds.length > 1) { - const ancestor = await this.repository.getMergeBase(historyItemGroupIds[0], historyItemGroupIds[1], ...historyItemGroupIds.slice(2)); + } else if (historyItemRefs.length > 1) { + const ancestor = await this.repository.getMergeBase(historyItemRefs[0], historyItemRefs[1], ...historyItemRefs.slice(2)); return ancestor; } } catch (err) { - this.logger.error(`[GitHistoryProvider][resolveHistoryItemGroupCommonAncestor] Failed to resolve common ancestor for ${historyItemGroupIds.join(',')}: ${err}`); + this.logger.error(`[GitHistoryProvider][resolveHistoryItemRefsCommonAncestor] Failed to resolve common ancestor for ${historyItemRefs.join(',')}: ${err}`); } return undefined; @@ -208,19 +312,47 @@ export class GitHistoryProvider implements SourceControlHistoryProvider, FileDec return this.historyItemDecorations.get(uri.toString()); } - private resolveHistoryItemLabels(commit: Commit): SourceControlHistoryItemLabel[] { - const labels: SourceControlHistoryItemLabel[] = []; + private resolveHistoryItemRefs(commit: Commit): SourceControlHistoryItemRef[] { + const references: SourceControlHistoryItemRef[] = []; - for (const label of commit.refNames) { - for (const [key, value] of this.historyItemLabels) { - if (label.startsWith(key)) { - labels.push({ title: label.substring(key.length), icon: value }); + for (const ref of commit.refNames) { + switch (true) { + case ref.startsWith('HEAD -> refs/heads/'): + references.push({ + id: ref.substring('HEAD -> '.length), + name: ref.substring('HEAD -> refs/heads/'.length), + revision: commit.hash, + icon: new ThemeIcon('target') + }); + break; + case ref.startsWith('tag: refs/tags/'): + references.push({ + id: ref.substring('tag: '.length), + name: ref.substring('tag: refs/tags/'.length), + revision: commit.hash, + icon: new ThemeIcon('tag') + }); + break; + case ref.startsWith('refs/heads/'): + references.push({ + id: ref, + name: ref.substring('refs/heads/'.length), + revision: commit.hash, + icon: new ThemeIcon('git-branch') + }); + break; + case ref.startsWith('refs/remotes/'): + references.push({ + id: ref, + name: ref.substring('refs/remotes/'.length), + revision: commit.hash, + icon: new ThemeIcon('cloud') + }); break; - } } } - return labels; + return references; } private async resolveHEADMergeBase(): Promise { diff --git a/extensions/git/src/util.ts b/extensions/git/src/util.ts index eac6f0384b9..bf33411c3b3 100644 --- a/extensions/git/src/util.ts +++ b/extensions/git/src/util.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event, Disposable, EventEmitter } from 'vscode'; +import { Event, Disposable, EventEmitter, SourceControlHistoryItemRef } from 'vscode'; import { dirname, sep, relative } from 'path'; import { Readable } from 'stream'; import { promises as fs, createReadStream } from 'fs'; @@ -513,3 +513,58 @@ export namespace Versions { return from(major, minor, patch, pre); } } + +export function deltaHistoryItemRefs(before: SourceControlHistoryItemRef[], after: SourceControlHistoryItemRef[]): { + added: SourceControlHistoryItemRef[]; + modified: SourceControlHistoryItemRef[]; + removed: SourceControlHistoryItemRef[]; +} { + if (before.length === 0) { + return { added: after, modified: [], removed: [] }; + } + + const added: SourceControlHistoryItemRef[] = []; + const modified: SourceControlHistoryItemRef[] = []; + const removed: SourceControlHistoryItemRef[] = []; + + let beforeIdx = 0; + let afterIdx = 0; + + while (true) { + if (beforeIdx === before.length) { + added.push(...after.slice(afterIdx)); + break; + } + if (afterIdx === after.length) { + removed.push(...before.slice(beforeIdx)); + break; + } + + const beforeElement = before[beforeIdx]; + const afterElement = after[afterIdx]; + + const result = beforeElement.id.localeCompare(afterElement.id); + + if (result === 0) { + if (beforeElement.revision !== afterElement.revision) { + // modified + modified.push(afterElement); + } + + beforeIdx += 1; + afterIdx += 1; + } else if (result < 0) { + // beforeElement is smaller -> before element removed + removed.push(beforeElement); + + beforeIdx += 1; + } else if (result > 0) { + // beforeElement is greater -> after element added + added.push(afterElement); + + afterIdx += 1; + } + } + + return { added, modified, removed }; +} diff --git a/extensions/github-authentication/.vscodeignore b/extensions/github-authentication/.vscodeignore index f4dce5682cf..0d0ea746e79 100644 --- a/extensions/github-authentication/.vscodeignore +++ b/extensions/github-authentication/.vscodeignore @@ -6,5 +6,4 @@ build/** extension.webpack.config.js extension-browser.webpack.config.js tsconfig.json -yarn.lock package-lock.json diff --git a/extensions/github-authentication/src/github.ts b/extensions/github-authentication/src/github.ts index ed584c65f8b..231c793e6a4 100644 --- a/extensions/github-authentication/src/github.ts +++ b/extensions/github-authentication/src/github.ts @@ -18,7 +18,9 @@ interface SessionData { account?: { label?: string; displayName?: string; - id: string; + // Unfortunately, for some time the id was a number, so we need to support both. + // This can be removed once we are confident that all users have migrated to the new id. + id: string | number; }; scopes: string[]; accessToken: string; @@ -239,9 +241,14 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid return []; } + // Unfortunately, we were using a number secretly for the account id for some time... this is due to a bad `any`. + // AuthenticationSession's account id is a string, so we need to detect when there is a number accountId and re-store + // the sessions to migrate away from the bad number usage. + // TODO@TylerLeonhardt: Remove this after we are confident that all users have migrated to the new id. + let seenNumberAccountId: boolean = false; // TODO: eventually remove this Set because we should only have one session per set of scopes. const scopesSeen = new Set(); - const sessionPromises = sessionData.map(async (session: SessionData) => { + const sessionPromises = sessionData.map(async (session: SessionData): Promise => { // For GitHub scope list, order doesn't matter so we immediately sort the scopes const scopesStr = [...session.scopes].sort().join(' '); if (!this._supportsMultipleAccounts && scopesSeen.has(scopesStr)) { @@ -262,13 +269,23 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid this._logger.trace(`Read the following session from the keychain with the following scopes: ${scopesStr}`); scopesSeen.add(scopesStr); + + let accountId: string; + if (session.account?.id) { + if (typeof session.account.id === 'number') { + seenNumberAccountId = true; + } + accountId = `${session.account.id}`; + } else { + accountId = userInfo?.id ?? ''; + } return { id: session.id, account: { label: session.account ? session.account.label ?? session.account.displayName ?? '' : userInfo?.accountName ?? '', - id: session.account?.id ?? userInfo?.id ?? '' + id: accountId }, // we set this to session.scopes to maintain the original order of the scopes requested // by the extension that called getSession() @@ -283,7 +300,7 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid .filter((p?: T): p is T => Boolean(p)); this._logger.info(`Got ${verifiedSessions.length} verified sessions.`); - if (verifiedSessions.length !== sessionData.length) { + if (seenNumberAccountId || verifiedSessions.length !== sessionData.length) { await this.storeSessions(verifiedSessions); } diff --git a/extensions/github-authentication/src/githubServer.ts b/extensions/github-authentication/src/githubServer.ts index c9f0a8c07d5..a9f94ccf65d 100644 --- a/extensions/github-authentication/src/githubServer.ts +++ b/extensions/github-authentication/src/githubServer.ts @@ -228,9 +228,9 @@ export class GitHubServer implements IGitHubServer { if (result.ok) { try { - const json = await result.json(); + const json = await result.json() as { id: number; login: string }; this._logger.info('Got account info!'); - return { id: json.id, accountName: json.login }; + return { id: `${json.id}`, accountName: json.login }; } catch (e) { this._logger.error(`Unexpected error parsing response from GitHub: ${e.message ?? e}`); throw e; diff --git a/extensions/github/.vscodeignore b/extensions/github/.vscodeignore index 1ee958238d0..24b1bd4e73a 100644 --- a/extensions/github/.vscodeignore +++ b/extensions/github/.vscodeignore @@ -4,5 +4,4 @@ out/** build/** extension.webpack.config.js tsconfig.json -yarn.lock package-lock.json diff --git a/extensions/grunt/.vscodeignore b/extensions/grunt/.vscodeignore index 211cee1f60a..698898bf9df 100644 --- a/extensions/grunt/.vscodeignore +++ b/extensions/grunt/.vscodeignore @@ -3,5 +3,4 @@ src/** tsconfig.json out/** extension.webpack.config.js -yarn.lock package-lock.json diff --git a/extensions/gulp/.vscodeignore b/extensions/gulp/.vscodeignore index 74215d83d91..360fcfd1c99 100644 --- a/extensions/gulp/.vscodeignore +++ b/extensions/gulp/.vscodeignore @@ -2,5 +2,4 @@ src/** tsconfig.json out/** extension.webpack.config.js -yarn.lock package-lock.json diff --git a/extensions/html-language-features/.vscodeignore b/extensions/html-language-features/.vscodeignore index dd12019eef7..b1586b6bc84 100644 --- a/extensions/html-language-features/.vscodeignore +++ b/extensions/html-language-features/.vscodeignore @@ -13,10 +13,8 @@ server/test/** server/bin/** server/build/** server/lib/cgmanifest.json -server/yarn.lock server/package-lock.json server/.npmignore -yarn.lock package-lock.json server/extension.webpack.config.js extension.webpack.config.js diff --git a/extensions/ipynb/.vscodeignore b/extensions/ipynb/.vscodeignore index f157f8e9653..2d13f5a0c22 100644 --- a/extensions/ipynb/.vscodeignore +++ b/extensions/ipynb/.vscodeignore @@ -5,7 +5,6 @@ out/** tsconfig.json extension.webpack.config.js extension-browser.webpack.config.js -yarn.lock package-lock.json .gitignore esbuild.js diff --git a/extensions/jake/.vscodeignore b/extensions/jake/.vscodeignore index 74215d83d91..360fcfd1c99 100644 --- a/extensions/jake/.vscodeignore +++ b/extensions/jake/.vscodeignore @@ -2,5 +2,4 @@ src/** tsconfig.json out/** extension.webpack.config.js -yarn.lock package-lock.json diff --git a/extensions/json-language-features/.vscodeignore b/extensions/json-language-features/.vscodeignore index 39da57a9aa4..807b3e4cbae 100644 --- a/extensions/json-language-features/.vscodeignore +++ b/extensions/json-language-features/.vscodeignore @@ -10,11 +10,9 @@ server/tsconfig.json server/test/** server/bin/** server/build/** -server/yarn.lock server/package-lock.json server/.npmignore server/README.md -yarn.lock package-lock.json CONTRIBUTING.md server/extension.webpack.config.js diff --git a/extensions/json-language-features/server/.npmignore b/extensions/json-language-features/server/.npmignore index 9b97a6afd92..960a01cc7b5 100644 --- a/extensions/json-language-features/server/.npmignore +++ b/extensions/json-language-features/server/.npmignore @@ -5,7 +5,6 @@ src/ test/ tsconfig.json .gitignore -yarn.lock package-lock.json extension.webpack.config.js vscode-json-languageserver-*.tgz diff --git a/extensions/markdown-language-features/.vscodeignore b/extensions/markdown-language-features/.vscodeignore index bbd727a2479..0d35b62002d 100644 --- a/extensions/markdown-language-features/.vscodeignore +++ b/extensions/markdown-language-features/.vscodeignore @@ -9,7 +9,6 @@ out/** extension.webpack.config.js extension-browser.webpack.config.js cgmanifest.json -yarn.lock package-lock.json preview-src/** webpack.config.js diff --git a/extensions/markdown-math/.vscodeignore b/extensions/markdown-math/.vscodeignore index 4a8c70b1633..85f550b7d7b 100644 --- a/extensions/markdown-math/.vscodeignore +++ b/extensions/markdown-math/.vscodeignore @@ -4,7 +4,6 @@ extension-browser.webpack.config.js extension.webpack.config.js esbuild.js cgmanifest.json -yarn.lock package-lock.json webpack.config.js tsconfig.json diff --git a/extensions/media-preview/.vscodeignore b/extensions/media-preview/.vscodeignore index 601e6fa300c..532c87f6f2e 100644 --- a/extensions/media-preview/.vscodeignore +++ b/extensions/media-preview/.vscodeignore @@ -6,7 +6,6 @@ out/** extension.webpack.config.js extension-browser.webpack.config.js cgmanifest.json -yarn.lock package-lock.json preview-src/** webpack.config.js diff --git a/extensions/media-preview/media/imagePreview.css b/extensions/media-preview/media/imagePreview.css index 49a01b8d969..67c73e39595 100644 --- a/extensions/media-preview/media/imagePreview.css +++ b/extensions/media-preview/media/imagePreview.css @@ -12,6 +12,7 @@ html, body { body img { max-width: none; max-height: none; + vertical-align: middle; } .container:focus { @@ -31,20 +32,20 @@ body img { box-sizing: border-box; } -.container.image img { +.container.image .transparency-grid { padding: 0; background-position: 0 0, 8px 8px; background-size: 16px 16px; border: 1px solid var(--vscode-imagePreview-border); } -.container.image img { +.container.image .transparency-grid { background-image: linear-gradient(45deg, rgb(230, 230, 230) 25%, transparent 25%, transparent 75%, rgb(230, 230, 230) 75%, rgb(230, 230, 230)), linear-gradient(45deg, rgb(230, 230, 230) 25%, transparent 25%, transparent 75%, rgb(230, 230, 230) 75%, rgb(230, 230, 230)); } -.vscode-dark.container.image img { +.vscode-dark.container.image .transparency-grid { background-image: linear-gradient(45deg, rgb(20, 20, 20) 25%, transparent 25%, transparent 75%, rgb(20, 20, 20) 75%, rgb(20, 20, 20)), linear-gradient(45deg, rgb(20, 20, 20) 25%, transparent 25%, transparent 75%, rgb(20, 20, 20) 75%, rgb(20, 20, 20)); @@ -54,13 +55,13 @@ body img { image-rendering: pixelated; } -.container img.scale-to-fit { +.container .transparency-grid.scale-to-fit { max-width: calc(100% - 20px); max-height: calc(100% - 20px); object-fit: contain; } -.container img { +.container .transparency-grid { margin: auto; } diff --git a/extensions/media-preview/media/imagePreview.js b/extensions/media-preview/media/imagePreview.js index ab8ad542a2d..7179283d052 100644 --- a/extensions/media-preview/media/imagePreview.js +++ b/extensions/media-preview/media/imagePreview.js @@ -76,6 +76,8 @@ // Elements const container = document.body; + const transparencyGrid = document.createElement('div'); + transparencyGrid.classList.add('transparency-grid'); const image = document.createElement('img'); function updateScale(newScale) { @@ -85,7 +87,7 @@ if (newScale === 'fit') { scale = 'fit'; - image.classList.add('scale-to-fit'); + transparencyGrid.classList.add('scale-to-fit'); image.classList.remove('pixelated'); // @ts-ignore Non-standard CSS property image.style.zoom = 'normal'; @@ -292,7 +294,9 @@ document.body.classList.remove('loading'); document.body.classList.add('ready'); - document.body.append(image); + + document.body.append(transparencyGrid); + transparencyGrid.appendChild(image); updateScale(scale); diff --git a/extensions/merge-conflict/.vscodeignore b/extensions/merge-conflict/.vscodeignore index b12e30a1894..3a8a2a96a6c 100644 --- a/extensions/merge-conflict/.vscodeignore +++ b/extensions/merge-conflict/.vscodeignore @@ -3,5 +3,4 @@ tsconfig.json out/** extension.webpack.config.js extension-browser.webpack.config.js -yarn.lock package-lock.json diff --git a/extensions/microsoft-authentication/.vscodeignore b/extensions/microsoft-authentication/.vscodeignore index 0e10c404a9a..98b90d34d82 100644 --- a/extensions/microsoft-authentication/.vscodeignore +++ b/extensions/microsoft-authentication/.vscodeignore @@ -4,7 +4,6 @@ out/test/** out/** extension.webpack.config.js extension-browser.webpack.config.js -yarn.lock package-lock.json src/** .gitignore diff --git a/extensions/microsoft-authentication/src/common/publicClientCache.ts b/extensions/microsoft-authentication/src/common/publicClientCache.ts index cb9339f926d..925a4d1a88c 100644 --- a/extensions/microsoft-authentication/src/common/publicClientCache.ts +++ b/extensions/microsoft-authentication/src/common/publicClientCache.ts @@ -7,6 +7,8 @@ import type { Disposable, Event } from 'vscode'; export interface ICachedPublicClientApplication extends Disposable { initialize(): Promise; + onDidAccountsChange: Event<{ added: AccountInfo[]; changed: AccountInfo[]; deleted: AccountInfo[] }>; + onDidRemoveLastAccount: Event; acquireTokenSilent(request: SilentFlowRequest): Promise; acquireTokenInteractive(request: InteractiveRequest): Promise; removeAccount(account: AccountInfo): Promise; @@ -16,6 +18,7 @@ export interface ICachedPublicClientApplication extends Disposable { } export interface ICachedPublicClientApplicationManager { + onDidAccountsChange: Event<{ added: AccountInfo[]; changed: AccountInfo[]; deleted: AccountInfo[] }>; getOrCreate(clientId: string, authority: string): Promise; getAll(): ICachedPublicClientApplication[]; } diff --git a/extensions/microsoft-authentication/src/node/authProvider.ts b/extensions/microsoft-authentication/src/node/authProvider.ts index 3925f6f58cf..02bb66863be 100644 --- a/extensions/microsoft-authentication/src/node/authProvider.ts +++ b/extensions/microsoft-authentication/src/node/authProvider.ts @@ -48,15 +48,12 @@ export class MsalAuthProvider implements AuthenticationProvider { private readonly _env: Environment = Environment.AzureCloud ) { this._disposables = context.subscriptions; - this._publicClientManager = new CachedPublicClientApplicationManager( - context.globalState, - context.secrets, - this._logger, - (e) => this._handleAccountChange(e) + this._publicClientManager = new CachedPublicClientApplicationManager(context.globalState, context.secrets, this._logger); + this._disposables.push( + this._onDidChangeSessionsEmitter, + this._publicClientManager, + this._publicClientManager.onDidAccountsChange(e => this._handleAccountChange(e)) ); - this._disposables.push(this._publicClientManager); - this._disposables.push(this._onDidChangeSessionsEmitter); - } async initialize(): Promise { @@ -79,40 +76,43 @@ export class MsalAuthProvider implements AuthenticationProvider { * See {@link onDidChangeSessions} for more information on how this is used. * @param param0 Event that contains the added and removed accounts */ - private _handleAccountChange({ added, deleted }: { added: AccountInfo[]; deleted: AccountInfo[] }) { - const process = (a: AccountInfo) => ({ - // This shouldn't be needed - accessToken: '1234', - id: a.homeAccountId, - scopes: [], - account: { - id: a.homeAccountId, - label: a.username - }, - idToken: a.idToken, + private _handleAccountChange({ added, changed, deleted }: { added: AccountInfo[]; changed: AccountInfo[]; deleted: AccountInfo[] }) { + this._onDidChangeSessionsEmitter.fire({ + added: added.map(this.sessionFromAccountInfo), + changed: changed.map(this.sessionFromAccountInfo), + removed: deleted.map(this.sessionFromAccountInfo) }); - this._onDidChangeSessionsEmitter.fire({ added: added.map(process), changed: [], removed: deleted.map(process) }); } //#region AuthenticationProvider methods async getSessions(scopes: string[] | undefined, options?: AuthenticationGetSessionOptions): Promise { + const askingForAll = scopes === undefined; const scopeData = new ScopeData(scopes); - this._logger.info('[getSessions]', scopes ? scopeData.scopeStr : 'all', 'starting'); - if (!scopes) { - // Do NOT use `scopes` beyond this place in the code. Use `scopeData` instead. + // Do NOT use `scopes` beyond this place in the code. Use `scopeData` instead. + this._logger.info('[getSessions]', askingForAll ? '[all]' : `[${scopeData.scopeStr}]`, 'starting'); - const allSessions: AuthenticationSession[] = []; + // This branch only gets called by Core for sign out purposes and initial population of the account menu. Since we are + // living in a world where a "session" from Core's perspective is an account, we return 1 session per account. + // See the large comment on `onDidChangeSessions` for more information. + if (askingForAll) { + const allSessionsForAccounts = new Map(); for (const cachedPca of this._publicClientManager.getAll()) { - const sessions = await this.getAllSessionsForPca(cachedPca, scopeData.originalScopes, scopeData.scopesToSend, options?.account); - allSessions.push(...sessions); + for (const account of cachedPca.accounts) { + if (allSessionsForAccounts.has(account.homeAccountId)) { + continue; + } + allSessionsForAccounts.set(account.homeAccountId, this.sessionFromAccountInfo(account)); + } } + const allSessions = Array.from(allSessionsForAccounts.values()); + this._logger.info('[getSessions] [all]', `returned ${allSessions.length} session(s)`); return allSessions; } const cachedPca = await this.getOrCreatePublicClientApplication(scopeData.clientId, scopeData.tenant); const sessions = await this.getAllSessionsForPca(cachedPca, scopeData.originalScopes, scopeData.scopesToSend, options?.account); - this._logger.info(`[getSessions] returned ${sessions.length} sessions`); + this._logger.info(`[getSessions] [${scopeData.scopeStr}] returned ${sessions.length} session(s)`); return sessions; } @@ -121,7 +121,7 @@ export class MsalAuthProvider implements AuthenticationProvider { const scopeData = new ScopeData(scopes); // Do NOT use `scopes` beyond this place in the code. Use `scopeData` instead. - this._logger.info('[createSession]', scopeData.scopeStr, 'starting'); + this._logger.info('[createSession]', `[${scopeData.scopeStr}]`, 'starting'); const cachedPca = await this.getOrCreatePublicClientApplication(scopeData.clientId, scopeData.tenant); let result: AuthenticationResult; try { @@ -169,32 +169,43 @@ export class MsalAuthProvider implements AuthenticationProvider { } } - const session = this.toAuthenticationSession(result, scopeData.originalScopes); + const session = this.sessionFromAuthenticationResult(result, scopeData.originalScopes); this._telemetryReporter.sendLoginEvent(session.scopes); - this._logger.info('[createSession]', scopeData.scopeStr, 'returned session'); + this._logger.info('[createSession]', `[${scopeData.scopeStr}]`, 'returned session'); + // This is the only scenario in which we need to fire the _onDidChangeSessionsEmitter out of band... + // the badge flow (when the client passes no options in to getSession) will only remove a badge if a session + // was created that _matches the scopes_ that that badge requests. See `onDidChangeSessions` for more info. + // TODO: This should really be fixed in Core. this._onDidChangeSessionsEmitter.fire({ added: [session], changed: [], removed: [] }); return session; } async removeSession(sessionId: string): Promise { this._logger.info('[removeSession]', sessionId, 'starting'); + const promises = new Array>(); for (const cachedPca of this._publicClientManager.getAll()) { const accounts = cachedPca.accounts; for (const account of accounts) { if (account.homeAccountId === sessionId) { this._telemetryReporter.sendLogoutEvent(); - try { - await cachedPca.removeAccount(account); - } catch (e) { - this._telemetryReporter.sendLogoutFailedEvent(); - throw e; - } - this._logger.info('[removeSession]', sessionId, 'removed session'); - return; + promises.push(cachedPca.removeAccount(account)); + this._logger.info(`[removeSession] [${sessionId}] [${cachedPca.clientId}] [${cachedPca.authority}] removing session...`); } } } - this._logger.info('[removeSession]', sessionId, 'session not found'); + if (!promises.length) { + this._logger.info('[removeSession]', sessionId, 'session not found'); + return; + } + const results = await Promise.allSettled(promises); + for (const result of results) { + if (result.status === 'rejected') { + this._telemetryReporter.sendLogoutFailedEvent(); + this._logger.error('[removeSession]', sessionId, 'error removing session', result.reason); + } + } + + this._logger.info('[removeSession]', sessionId, `attempted to remove ${promises.length} sessions`); } //#endregion @@ -217,7 +228,7 @@ export class MsalAuthProvider implements AuthenticationProvider { for (const account of accounts) { try { const result = await cachedPca.acquireTokenSilent({ account, scopes: scopesToSend, redirectUri }); - sessions.push(this.toAuthenticationSession(result, originalScopes)); + sessions.push(this.sessionFromAuthenticationResult(result, originalScopes)); } catch (e) { // If we can't get a token silently, the account is probably in a bad state so we should skip it // MSAL will log this already, so we don't need to log it again @@ -227,7 +238,7 @@ export class MsalAuthProvider implements AuthenticationProvider { return sessions; } - private toAuthenticationSession(result: AuthenticationResult, scopes: readonly string[]): AuthenticationSession & { idToken: string } { + private sessionFromAuthenticationResult(result: AuthenticationResult, scopes: readonly string[]): AuthenticationSession & { idToken: string } { return { accessToken: result.accessToken, idToken: result.idToken, @@ -239,4 +250,17 @@ export class MsalAuthProvider implements AuthenticationProvider { scopes }; } + + private sessionFromAccountInfo(account: AccountInfo): AuthenticationSession { + return { + accessToken: '1234', + id: account.homeAccountId, + scopes: [], + account: { + id: account.homeAccountId, + label: account.username + }, + idToken: account.idToken, + }; + } } diff --git a/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts b/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts new file mode 100644 index 00000000000..62882d68ca0 --- /dev/null +++ b/extensions/microsoft-authentication/src/node/cachedPublicClientApplication.ts @@ -0,0 +1,148 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { PublicClientApplication, AccountInfo, Configuration, SilentFlowRequest, AuthenticationResult, InteractiveRequest } from '@azure/msal-node'; +import { Disposable, Memento, SecretStorage, LogOutputChannel, window, ProgressLocation, l10n, EventEmitter } from 'vscode'; +import { raceCancellationAndTimeoutError } from '../common/async'; +import { SecretStorageCachePlugin } from '../common/cachePlugin'; +import { MsalLoggerOptions } from '../common/loggerOptions'; +import { ICachedPublicClientApplication } from '../common/publicClientCache'; + +export class CachedPublicClientApplication implements ICachedPublicClientApplication { + private _pca: PublicClientApplication; + + private _accounts: AccountInfo[] = []; + private readonly _disposable: Disposable; + + private readonly _loggerOptions = new MsalLoggerOptions(this._logger); + private readonly _secretStorageCachePlugin = new SecretStorageCachePlugin( + this._secretStorage, + // Include the prefix as a differentiator to other secrets + `pca:${JSON.stringify({ clientId: this._clientId, authority: this._authority })}` + ); + private readonly _config: Configuration = { + auth: { clientId: this._clientId, authority: this._authority }, + system: { + loggerOptions: { + correlationId: `${this._clientId}] [${this._authority}`, + loggerCallback: (level, message, containsPii) => this._loggerOptions.loggerCallback(level, message, containsPii), + } + }, + cache: { + cachePlugin: this._secretStorageCachePlugin + } + }; + + /** + * We keep track of the last time an account was removed so we can recreate the PCA if we detect that an account was removed. + * This is due to MSAL-node not providing a way to detect when an account is removed from the cache. An internal issue has been + * filed to track this. If MSAL-node ever provides a way to detect this or handle this better in the Persistant Cache Plugin, + * we can remove this logic. + */ + private _lastCreated: Date; + + //#region Events + + private readonly _onDidAccountsChangeEmitter = new EventEmitter<{ added: AccountInfo[]; changed: AccountInfo[]; deleted: AccountInfo[] }>; + readonly onDidAccountsChange = this._onDidAccountsChangeEmitter.event; + + private readonly _onDidRemoveLastAccountEmitter = new EventEmitter(); + readonly onDidRemoveLastAccount = this._onDidRemoveLastAccountEmitter.event; + + //#endregion + + constructor( + private readonly _clientId: string, + private readonly _authority: string, + private readonly _globalMemento: Memento, + private readonly _secretStorage: SecretStorage, + private readonly _logger: LogOutputChannel + ) { + this._pca = new PublicClientApplication(this._config); + this._lastCreated = new Date(); + this._disposable = Disposable.from( + this._registerOnSecretStorageChanged(), + this._onDidAccountsChangeEmitter, + this._onDidRemoveLastAccountEmitter + ); + } + + get accounts(): AccountInfo[] { return this._accounts; } + get clientId(): string { return this._clientId; } + get authority(): string { return this._authority; } + + initialize(): Promise { + return this._update(); + } + + dispose(): void { + this._disposable.dispose(); + } + + async acquireTokenSilent(request: SilentFlowRequest): Promise { + this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}]`); + const result = await this._pca.acquireTokenSilent(request); + if (result.account && !result.fromCache) { + this._onDidAccountsChangeEmitter.fire({ added: [], changed: [result.account], deleted: [] }); + } + return result; + } + + async acquireTokenInteractive(request: InteractiveRequest): Promise { + this._logger.debug(`[acquireTokenInteractive] [${this._clientId}] [${this._authority}] [${request.scopes?.join(' ')}] loopbackClientOverride: ${request.loopbackClient ? 'true' : 'false'}`); + return await window.withProgress( + { + location: ProgressLocation.Notification, + cancellable: true, + title: l10n.t('Signing in to Microsoft...') + }, + (_process, token) => raceCancellationAndTimeoutError( + this._pca.acquireTokenInteractive(request), + token, + 1000 * 60 * 5 + ) + ); + } + + removeAccount(account: AccountInfo): Promise { + this._globalMemento.update(`lastRemoval:${this._clientId}:${this._authority}`, new Date()); + return this._pca.getTokenCache().removeAccount(account); + } + + private _registerOnSecretStorageChanged() { + return this._secretStorageCachePlugin.onDidChange(() => this._update()); + } + + private async _update() { + const before = this._accounts; + this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication update before: ${before.length}`); + // Dates are stored as strings in the memento + const lastRemovalDate = this._globalMemento.get(`lastRemoval:${this._clientId}:${this._authority}`); + if (lastRemovalDate && this._lastCreated && Date.parse(lastRemovalDate) > this._lastCreated.getTime()) { + this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication removal detected... recreating PCA...`); + this._pca = new PublicClientApplication(this._config); + this._lastCreated = new Date(); + } + + const after = await this._pca.getAllAccounts(); + this._accounts = after; + this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication update after: ${after.length}`); + + const beforeSet = new Set(before.map(b => b.homeAccountId)); + const afterSet = new Set(after.map(a => a.homeAccountId)); + + const added = after.filter(a => !beforeSet.has(a.homeAccountId)); + const deleted = before.filter(b => !afterSet.has(b.homeAccountId)); + if (added.length > 0 || deleted.length > 0) { + this._onDidAccountsChangeEmitter.fire({ added, changed: [], deleted }); + this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication accounts changed. added: ${added.length}, deleted: ${deleted.length}`); + if (!after.length) { + this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication final account deleted. Firing event.`); + this._onDidRemoveLastAccountEmitter.fire(); + } + } + this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication update complete`); + } +} diff --git a/extensions/microsoft-authentication/src/node/publicClientCache.ts b/extensions/microsoft-authentication/src/node/publicClientCache.ts index 34bf2c3c73b..6ecc34501f7 100644 --- a/extensions/microsoft-authentication/src/node/publicClientCache.ts +++ b/extensions/microsoft-authentication/src/node/publicClientCache.ts @@ -3,77 +3,84 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { AccountInfo, AuthenticationResult, Configuration, InteractiveRequest, PublicClientApplication, SilentFlowRequest } from '@azure/msal-node'; -import { SecretStorageCachePlugin } from '../common/cachePlugin'; -import { SecretStorage, LogOutputChannel, Disposable, SecretStorageChangeEvent, EventEmitter, Memento, window, ProgressLocation, l10n } from 'vscode'; -import { MsalLoggerOptions } from '../common/loggerOptions'; +import { AccountInfo } from '@azure/msal-node'; +import { SecretStorage, LogOutputChannel, Disposable, EventEmitter, Memento, Event } from 'vscode'; import { ICachedPublicClientApplication, ICachedPublicClientApplicationManager } from '../common/publicClientCache'; -import { raceCancellationAndTimeoutError } from '../common/async'; +import { CachedPublicClientApplication } from './cachedPublicClientApplication'; export interface IPublicClientApplicationInfo { clientId: string; authority: string; } -const _keyPrefix = 'pca:'; - export class CachedPublicClientApplicationManager implements ICachedPublicClientApplicationManager { - // The key is the clientId and authority stringified + // The key is the clientId and authority JSON stringified private readonly _pcas = new Map(); + private readonly _pcaDisposables = new Map(); - private _initialized = false; private _disposable: Disposable; + private _pcasSecretStorage: PublicClientApplicationsSecretStorage; + + private readonly _onDidAccountsChangeEmitter = new EventEmitter<{ added: AccountInfo[]; changed: AccountInfo[]; deleted: AccountInfo[] }>(); + readonly onDidAccountsChange = this._onDidAccountsChangeEmitter.event; constructor( private readonly _globalMemento: Memento, private readonly _secretStorage: SecretStorage, - private readonly _logger: LogOutputChannel, - private readonly _accountChangeHandler: (e: { added: AccountInfo[]; deleted: AccountInfo[] }) => void + private readonly _logger: LogOutputChannel ) { - this._disposable = _secretStorage.onDidChange(e => this._handleSecretStorageChange(e)); + this._pcasSecretStorage = new PublicClientApplicationsSecretStorage(_secretStorage); + this._disposable = Disposable.from( + this._pcasSecretStorage, + this._registerSecretStorageHandler(), + this._onDidAccountsChangeEmitter + ); + } + + private _registerSecretStorageHandler() { + return this._pcasSecretStorage.onDidChange(() => this._handleSecretStorageChange()); } async initialize() { this._logger.debug('[initialize] Initializing PublicClientApplicationManager'); - const keys = await this._secretStorage.get('publicClientApplications'); + let keys: string[] | undefined; + try { + keys = await this._pcasSecretStorage.get(); + } catch (e) { + // data is corrupted + this._logger.error('[initialize] Error initializing PublicClientApplicationManager:', e); + await this._pcasSecretStorage.delete(); + } if (!keys) { - this._initialized = true; return; } const promises = new Array>(); - try { - for (const key of JSON.parse(keys) as string[]) { - try { - const { clientId, authority } = JSON.parse(key) as IPublicClientApplicationInfo; - // Load the PCA in memory - promises.push(this.getOrCreate(clientId, authority)); - } catch (e) { - // ignore - } + for (const key of keys) { + try { + const { clientId, authority } = JSON.parse(key) as IPublicClientApplicationInfo; + // Load the PCA in memory + promises.push(this._doCreatePublicClientApplication(clientId, authority, key)); + } catch (e) { + this._logger.error('[initialize] Error intitializing PCA:', key); } - } catch (e) { - // data is corrupted - this._logger.error('[initialize] Error initializing PublicClientApplicationManager:', e); - await this._secretStorage.delete('publicClientApplications'); } - // TODO: should we do anything for when this fails? - await Promise.allSettled(promises); + const results = await Promise.allSettled(promises); + for (const result of results) { + if (result.status === 'rejected') { + this._logger.error('[initialize] Error getting PCA:', result.reason); + } + } this._logger.debug('[initialize] PublicClientApplicationManager initialized'); - this._initialized = true; } dispose() { this._disposable.dispose(); - Disposable.from(...this._pcas.values()).dispose(); + Disposable.from(...this._pcaDisposables.values()).dispose(); } async getOrCreate(clientId: string, authority: string): Promise { - if (!this._initialized) { - throw new Error('PublicClientApplicationManager not initialized'); - } - // Use the clientId and authority as the key const pcasKey = JSON.stringify({ clientId, authority }); let pca = this._pcas.get(pcasKey); @@ -83,170 +90,127 @@ export class CachedPublicClientApplicationManager implements ICachedPublicClient } this._logger.debug(`[getOrCreate] [${clientId}] [${authority}] PublicClientApplicationManager cache miss, creating new PCA...`); - pca = new CachedPublicClientApplication(clientId, authority, this._globalMemento, this._secretStorage, this._accountChangeHandler, this._logger); - this._pcas.set(pcasKey, pca); - await pca.initialize(); + pca = await this._doCreatePublicClientApplication(clientId, authority, pcasKey); await this._storePublicClientApplications(); - this._logger.debug(`[getOrCreate] [${clientId}] [${authority}] PublicClientApplicationManager PCA created`); + this._logger.debug(`[getOrCreate] [${clientId}] [${authority}] PCA created.`); + return pca; + } + + private async _doCreatePublicClientApplication(clientId: string, authority: string, pcasKey: string) { + const pca = new CachedPublicClientApplication(clientId, authority, this._globalMemento, this._secretStorage, this._logger); + this._pcas.set(pcasKey, pca); + const disposable = Disposable.from( + pca, + pca.onDidAccountsChange(e => this._onDidAccountsChangeEmitter.fire(e)), + pca.onDidRemoveLastAccount(() => { + // The PCA has no more accounts, so we can dispose it so we're not keeping it + // around forever. + disposable.dispose(); + this._pcas.delete(pcasKey); + this._logger.debug(`[_doCreatePublicClientApplication] [${clientId}] [${authority}] PCA disposed. Firing off storing of PCAs...`); + void this._storePublicClientApplications(); + }) + ); + this._pcaDisposables.set(pcasKey, disposable); + // Intialize the PCA after the `onDidAccountsChange` is set so we get initial state. + await pca.initialize(); return pca; } getAll(): ICachedPublicClientApplication[] { - if (!this._initialized) { - throw new Error('PublicClientApplicationManager not initialized'); - } return Array.from(this._pcas.values()); } - private async _handleSecretStorageChange(e: SecretStorageChangeEvent) { - if (!e.key.startsWith(_keyPrefix)) { - return; - } - - this._logger.debug(`[handleSecretStorageChange] PublicClientApplicationManager secret storage change: ${e.key}`); - const result = await this._secretStorage.get(e.key); - const pcasKey = e.key.split(_keyPrefix)[1]; - - // If the cache was deleted, or the PCA has zero accounts left, remove the PCA - if (!result || this._pcas.get(pcasKey)?.accounts.length === 0) { - this._logger.debug(`[handleSecretStorageChange] PublicClientApplicationManager removing PCA: ${pcasKey}`); - this._pcas.delete(pcasKey); + private async _handleSecretStorageChange() { + this._logger.debug(`[_handleSecretStorageChange] Handling PCAs secret storage change...`); + let result: string[] | undefined; + try { + result = await this._pcasSecretStorage.get(); + } catch (_e) { + // The data in secret storage has been corrupted somehow so + // we store what we have in this window await this._storePublicClientApplications(); - this._logger.debug(`[handleSecretStorageChange] PublicClientApplicationManager PCA removed: ${pcasKey}`); + return; + } + if (!result) { + this._logger.debug(`[_handleSecretStorageChange] PCAs deleted in secret storage. Disposing all...`); + Disposable.from(...this._pcaDisposables.values()).dispose(); + this._pcas.clear(); + this._pcaDisposables.clear(); + this._logger.debug(`[_handleSecretStorageChange] Finished PCAs secret storage change.`); return; } - // Load the PCA in memory if it's not already loaded - const { clientId, authority } = JSON.parse(pcasKey) as IPublicClientApplicationInfo; - this._logger.debug(`[handleSecretStorageChange] PublicClientApplicationManager loading PCA: ${pcasKey}`); - await this.getOrCreate(clientId, authority); - this._logger.debug(`[handleSecretStorageChange] PublicClientApplicationManager PCA loaded: ${pcasKey}`); + const pcaKeysFromStorage = new Set(result); + // Handle the deleted ones + for (const pcaKey of this._pcas.keys()) { + if (!pcaKeysFromStorage.delete(pcaKey)) { + // This PCA has been removed in another window + this._pcaDisposables.get(pcaKey)?.dispose(); + this._pcaDisposables.delete(pcaKey); + this._pcas.delete(pcaKey); + this._logger.debug(`[_handleSecretStorageChange] Disposed PCA that was deleted in another window: ${pcaKey}`); + } + } + + // Handle the new ones + for (const newPca of pcaKeysFromStorage) { + try { + const { clientId, authority } = JSON.parse(newPca); + this._logger.debug(`[_handleSecretStorageChange] [${clientId}] [${authority}] Creating new PCA that was created in another window...`); + await this._doCreatePublicClientApplication(clientId, authority, newPca); + this._logger.debug(`[_handleSecretStorageChange] [${clientId}] [${authority}] PCA created.`); + } catch (_e) { + // This really shouldn't happen, but should we do something about this? + this._logger.error(`Failed to parse new PublicClientApplication: ${newPca}`); + continue; + } + } + + this._logger.debug('[_handleSecretStorageChange] Finished handling PCAs secret storage change.'); } - private async _storePublicClientApplications() { - await this._secretStorage.store( - 'publicClientApplications', - JSON.stringify(Array.from(this._pcas.keys())) - ); + private _storePublicClientApplications() { + return this._pcasSecretStorage.store(Array.from(this._pcas.keys())); } } -class CachedPublicClientApplication implements ICachedPublicClientApplication { - private _pca: PublicClientApplication; +class PublicClientApplicationsSecretStorage { + private static key = 'publicClientApplications'; - private _accounts: AccountInfo[] = []; - private readonly _disposable: Disposable; + private _disposable: Disposable; - private readonly _loggerOptions = new MsalLoggerOptions(this._logger); - private readonly _secretStorageCachePlugin = new SecretStorageCachePlugin( - this._secretStorage, - // Include the prefix in the key so we can easily identify it later - `${_keyPrefix}${JSON.stringify({ clientId: this._clientId, authority: this._authority })}` - ); - private readonly _config: Configuration = { - auth: { clientId: this._clientId, authority: this._authority }, - system: { - loggerOptions: { - correlationId: `${this._clientId}] [${this._authority}`, - loggerCallback: (level, message, containsPii) => this._loggerOptions.loggerCallback(level, message, containsPii), - } - }, - cache: { - cachePlugin: this._secretStorageCachePlugin + private readonly _onDidChangeEmitter = new EventEmitter; + readonly onDidChange: Event = this._onDidChangeEmitter.event; + + constructor(private readonly _secretStorage: SecretStorage) { + this._disposable = Disposable.from( + this._onDidChangeEmitter, + this._secretStorage.onDidChange(e => { + if (e.key === PublicClientApplicationsSecretStorage.key) { + this._onDidChangeEmitter.fire(); + } + }) + ); + } + + async get(): Promise { + const value = await this._secretStorage.get(PublicClientApplicationsSecretStorage.key); + if (!value) { + return undefined; } - }; - - /** - * We keep track of the last time an account was removed so we can recreate the PCA if we detect that an account was removed. - * This is due to MSAL-node not providing a way to detect when an account is removed from the cache. An internal issue has been - * filed to track this. If MSAL-node ever provides a way to detect this or handle this better in the Persistant Cache Plugin, - * we can remove this logic. - */ - private _lastCreated: Date; - - constructor( - private readonly _clientId: string, - private readonly _authority: string, - private readonly _globalMemento: Memento, - private readonly _secretStorage: SecretStorage, - private readonly _accountChangeHandler: (e: { added: AccountInfo[]; changed: AccountInfo[]; deleted: AccountInfo[] }) => void, - private readonly _logger: LogOutputChannel - ) { - this._pca = new PublicClientApplication(this._config); - this._lastCreated = new Date(); - this._disposable = this._registerOnSecretStorageChanged(); + return JSON.parse(value); } - get accounts(): AccountInfo[] { return this._accounts; } - get clientId(): string { return this._clientId; } - get authority(): string { return this._authority; } - - initialize(): Promise { - return this._update(); + store(value: string[]): Thenable { + return this._secretStorage.store(PublicClientApplicationsSecretStorage.key, JSON.stringify(value)); } - dispose(): void { + delete(): Thenable { + return this._secretStorage.delete(PublicClientApplicationsSecretStorage.key); + } + + dispose() { this._disposable.dispose(); } - - async acquireTokenSilent(request: SilentFlowRequest): Promise { - this._logger.debug(`[acquireTokenSilent] [${this._clientId}] [${this._authority}] [${request.scopes.join(' ')}]`); - const result = await this._pca.acquireTokenSilent(request); - if (result.account && !result.fromCache) { - this._accountChangeHandler({ added: [], changed: [result.account], deleted: [] }); - } - return result; - } - - async acquireTokenInteractive(request: InteractiveRequest): Promise { - this._logger.debug(`[acquireTokenInteractive] [${this._clientId}] [${this._authority}] [${request.scopes?.join(' ')}] loopbackClientOverride: ${request.loopbackClient ? 'true' : 'false'}`); - return await window.withProgress( - { - location: ProgressLocation.Notification, - cancellable: true, - title: l10n.t('Signing in to Microsoft...') - }, - (_process, token) => raceCancellationAndTimeoutError( - this._pca.acquireTokenInteractive(request), - token, - 1000 * 60 * 5 - ), // 5 minutes - ); - } - - removeAccount(account: AccountInfo): Promise { - this._globalMemento.update(`lastRemoval:${this._clientId}:${this._authority}`, new Date()); - return this._pca.getTokenCache().removeAccount(account); - } - - private _registerOnSecretStorageChanged() { - return this._secretStorageCachePlugin.onDidChange(() => this._update()); - } - - private async _update() { - const before = this._accounts; - this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication update before: ${before.length}`); - // Dates are stored as strings in the memento - const lastRemovalDate = this._globalMemento.get(`lastRemoval:${this._clientId}:${this._authority}`); - if (lastRemovalDate && this._lastCreated && Date.parse(lastRemovalDate) > this._lastCreated.getTime()) { - this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication removal detected... recreating PCA...`); - this._pca = new PublicClientApplication(this._config); - this._lastCreated = new Date(); - } - - const after = await this._pca.getAllAccounts(); - this._accounts = after; - this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication update after: ${after.length}`); - - const beforeSet = new Set(before.map(b => b.homeAccountId)); - const afterSet = new Set(after.map(a => a.homeAccountId)); - - const added = after.filter(a => !beforeSet.has(a.homeAccountId)); - const deleted = before.filter(b => !afterSet.has(b.homeAccountId)); - if (added.length > 0 || deleted.length > 0) { - this._accountChangeHandler({ added, changed: [], deleted }); - this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication accounts changed. added: ${added.length}, deleted: ${deleted.length}`); - } - this._logger.debug(`[update] [${this._clientId}] [${this._authority}] CachedPublicClientApplication update complete`); - } } diff --git a/extensions/npm/.vscodeignore b/extensions/npm/.vscodeignore index c7df59ef78a..f05a79416be 100644 --- a/extensions/npm/.vscodeignore +++ b/extensions/npm/.vscodeignore @@ -4,5 +4,4 @@ tsconfig.json .vscode/** extension.webpack.config.js extension-browser.webpack.config.js -yarn.lock package-lock.json diff --git a/extensions/npm/package-lock.json b/extensions/npm/package-lock.json index 21a7c83df8e..4ee7ee7a0e3 100644 --- a/extensions/npm/package-lock.json +++ b/extensions/npm/package-lock.json @@ -193,15 +193,16 @@ } }, "node_modules/micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", "dependencies": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" + "braces": "^3.0.3", + "picomatch": "^2.3.1" }, "engines": { - "node": ">=8" + "node": ">=8.6" } }, "node_modules/minimatch": { @@ -252,9 +253,10 @@ } }, "node_modules/picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", "engines": { "node": ">=8.6" }, diff --git a/extensions/package-lock.json b/extensions/package-lock.json index 63476fbb316..792ee4281c2 100644 --- a/extensions/package-lock.json +++ b/extensions/package-lock.json @@ -547,12 +547,13 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { diff --git a/extensions/php-language-features/.vscodeignore b/extensions/php-language-features/.vscodeignore index 7cddadb7bcc..e326d20ef52 100644 --- a/extensions/php-language-features/.vscodeignore +++ b/extensions/php-language-features/.vscodeignore @@ -3,5 +3,4 @@ src/** out/** tsconfig.json extension.webpack.config.js -yarn.lock package-lock.json diff --git a/extensions/references-view/.vscodeignore b/extensions/references-view/.vscodeignore index fd73b3fe680..4d2ffa699e4 100644 --- a/extensions/references-view/.vscodeignore +++ b/extensions/references-view/.vscodeignore @@ -3,5 +3,4 @@ src/** out/** tsconfig.json *.webpack.config.js -yarn.lock package-lock.json diff --git a/extensions/search-result/.vscodeignore b/extensions/search-result/.vscodeignore index 005af0b037c..35b808e16f7 100644 --- a/extensions/search-result/.vscodeignore +++ b/extensions/search-result/.vscodeignore @@ -3,6 +3,5 @@ out/** tsconfig.json extension.webpack.config.js extension-browser.webpack.config.js -yarn.lock package-lock.json syntaxes/generateTMLanguage.js diff --git a/extensions/shared.webpack.config.js b/extensions/shared.webpack.config.js index 4c258280812..6e5b9fd95ac 100644 --- a/extensions/shared.webpack.config.js +++ b/extensions/shared.webpack.config.js @@ -12,7 +12,6 @@ const path = require('path'); const fs = require('fs'); const merge = require('merge-options'); const CopyWebpackPlugin = require('copy-webpack-plugin'); -const { NLSBundlePlugin } = require('vscode-nls-dev/lib/webpack-bundler'); const { DefinePlugin, optimize } = require('webpack'); const tsLoaderOptions = { @@ -40,13 +39,6 @@ function withNodeDefaults(/**@type WebpackConfig & { context: string }*/extConfi test: /\.ts$/, exclude: /node_modules/, use: [{ - // vscode-nls-dev loader: - // * rewrite nls-calls - loader: 'vscode-nls-dev/lib/webpack-loader', - options: { - base: path.join(extConfig.context, 'src') - } - }, { // configure TypeScript loader: // * enable sources maps for end-to-end source maps loader: 'ts-loader', @@ -97,8 +89,7 @@ function nodePlugins(context) { patterns: [ { from: 'src', to: '.', globOptions: { ignore: ['**/test/**', '**/*.ts'] }, noErrorOnMissing: true } ] - }), - new NLSBundlePlugin(id) + }) ]; } /** @@ -196,9 +187,7 @@ function browserPlugins(context) { 'process.platform': JSON.stringify('web'), 'process.env': JSON.stringify({}), 'process.env.BROWSER_ENV': JSON.stringify('true') - }), - // TODO: bring this back once vscode-nls-dev supports browser - // new NLSBundlePlugin(id) + }) ]; } diff --git a/extensions/simple-browser/.vscodeignore b/extensions/simple-browser/.vscodeignore index ddcbdff84f4..c69acedcc24 100644 --- a/extensions/simple-browser/.vscodeignore +++ b/extensions/simple-browser/.vscodeignore @@ -8,7 +8,6 @@ extension.webpack.config.js extension-browser.webpack.config.js cgmanifest.json .gitignore -yarn.lock package-lock.json preview-src/** webpack.config.js diff --git a/extensions/tunnel-forwarding/.vscodeignore b/extensions/tunnel-forwarding/.vscodeignore index 74215d83d91..360fcfd1c99 100644 --- a/extensions/tunnel-forwarding/.vscodeignore +++ b/extensions/tunnel-forwarding/.vscodeignore @@ -2,5 +2,4 @@ src/** tsconfig.json out/** extension.webpack.config.js -yarn.lock package-lock.json diff --git a/extensions/typescript-language-features/.vscodeignore b/extensions/typescript-language-features/.vscodeignore index 4c9a34b7edc..5c4f06d4cd3 100644 --- a/extensions/typescript-language-features/.vscodeignore +++ b/extensions/typescript-language-features/.vscodeignore @@ -8,5 +8,4 @@ tsconfig.json extension.webpack.config.js extension-browser.webpack.config.js cgmanifest.json -yarn.lock package-lock.json diff --git a/package-lock.json b/package-lock.json index c1363d5928a..0edf97d784c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { + "@bpasero/watcher": "https://github.com/bpasero/watcher.git#5d29cc732a03c91ecc2c861940a240b01e765c65", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "2.1.0", @@ -21,7 +22,7 @@ "@vscode/spdlog": "^0.15.0", "@vscode/sqlite3": "5.1.6-vscode", "@vscode/sudo-prompt": "9.3.1", - "@vscode/tree-sitter-wasm": "^0.0.2", + "@vscode/tree-sitter-wasm": "^0.0.3", "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", @@ -84,6 +85,7 @@ "@vscode/test-web": "^0.0.56", "@vscode/v8-heap-parser": "^0.1.0", "@vscode/vscode-perf": "^0.0.14", + "@webgpu/types": "^0.1.44", "ansi-colors": "^3.2.3", "asar": "^3.0.3", "chromium-pickle-js": "^0.2.0", @@ -131,7 +133,6 @@ "mime": "^1.4.1", "minimatch": "^3.0.4", "minimist": "^1.2.6", - "mkdirp": "^1.0.4", "mocha": "^10.2.0", "mocha-junit-reporter": "^2.2.1", "mocha-multi-reporters": "^1.5.1", @@ -156,7 +157,6 @@ "tslib": "^2.6.3", "typescript": "^5.7.0-dev.20240903", "util": "^0.12.4", - "vscode-nls-dev": "^3.3.1", "webpack": "^5.94.0", "webpack-cli": "^5.1.4", "webpack-stream": "^7.0.0", @@ -941,6 +941,44 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@bpasero/watcher": { + "version": "2.4.2-alpha.0", + "resolved": "git+ssh://git@github.com/bpasero/watcher.git#5d29cc732a03c91ecc2c861940a240b01e765c65", + "integrity": "sha512-8AWyO22MDRxp6zAJxDWXGFCHtBoioaku+x/IQrDT3VADhBRAeCVp/47qCVrHFyU0ixo0m8KGDBN6MtxqsFFU2g==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@bpasero/watcher/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/@bpasero/watcher/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -3167,9 +3205,9 @@ } }, "node_modules/@vscode/tree-sitter-wasm": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.0.2.tgz", - "integrity": "sha512-N57MR/kt4jR0H/TXeDsVYeJmvvUiK7avow0fjy+/EeKcyNBJcM2BFhj4XOAaaMbhGsOcIeSvJFouRWctXI7sKw==" + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.0.3.tgz", + "integrity": "sha512-K5YmUdohFCavPqEzG2cUPcZ6555KE1qwMDCjkvSUSz+s+8Wro2xfg+syLq90y6Tq0ZSUVvpuX6yq6ukToeGaLg==" }, "node_modules/@vscode/v8-heap-parser": { "version": "0.1.0", @@ -3414,6 +3452,12 @@ "@xtuc/long": "4.2.2" } }, + "node_modules/@webgpu/types": { + "version": "0.1.44", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.44.tgz", + "integrity": "sha512-JDpYJN5E/asw84LTYhKyvPpxGnD+bAKPtpW9Ilurf7cZpxaTbxkQcGwOd7jgB9BPBrTYQ+32ufo4HiuomTjHNQ==", + "dev": true + }, "node_modules/@webpack-cli/configtest": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", @@ -6699,119 +6743,6 @@ "node": ">=0.8.x" } }, - "node_modules/execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "dependencies": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/execa/node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/execa/node_modules/get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/execa/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/execa/node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/execa/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/execa/node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "dev": true, - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/execa/node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/execa/node_modules/signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", - "dev": true - }, - "node_modules/execa/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, "node_modules/expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -8836,7 +8767,7 @@ "node_modules/gulp-cli/node_modules/ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8= sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -8845,7 +8776,7 @@ "node_modules/gulp-cli/node_modules/camelcase": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo= sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg==", + "integrity": "sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg==", "dev": true, "engines": { "node": ">=0.10.0" @@ -8947,9 +8878,9 @@ "dev": true }, "node_modules/gulp-cli/node_modules/yargs": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.1.tgz", - "integrity": "sha512-huO4Fr1f9PmiJJdll5kwoS2e4GqzGSsMT3PPMpOwoVkOK8ckqAewMTZyA6LXVQWflleb/Z8oPBEvNsMft0XE+g==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.2.tgz", + "integrity": "sha512-ZEjj/dQYQy0Zx0lgLMLR8QuaqTihnxirir7EwUHp1Axq4e3+k8jXU5K0VLbNvedv1f4EWtBonDIZm0NUr+jCcA==", "dev": true, "dependencies": { "camelcase": "^3.0.0", @@ -8964,13 +8895,13 @@ "string-width": "^1.0.2", "which-module": "^1.0.0", "y18n": "^3.2.1", - "yargs-parser": "5.0.0-security.0" + "yargs-parser": "^5.0.1" } }, "node_modules/gulp-cli/node_modules/yargs-parser": { - "version": "5.0.0-security.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0-security.0.tgz", - "integrity": "sha512-T69y4Ps64LNesYxeYGYPvfoMTt/7y1XtfpIslUeK4um+9Hu7hlGoRtaDLvdXb7+/tfq4opVa2HRY5xGip022rQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.1.tgz", + "integrity": "sha512-wpav5XYiddjXxirPoCTUPbqM0PXvJ9hiBMvuJgInvo4/lAOTZzUprArw17q2O1P2+GHhbBr18/iQwjL5Z9BqfA==", "dev": true, "dependencies": { "camelcase": "^3.0.0", @@ -9066,9 +8997,9 @@ } }, "node_modules/gulp-eslint/node_modules/ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= sha512-wFUFA5bg5dviipbQQ32yOQhl6gcJaJXiHE7dvR8VYPG97+J/GNC5FKGepKdEDUFeXRzDxPF1X/Btc8L+v7oqIQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", "dev": true, "engines": { "node": ">=4" @@ -9838,7 +9769,7 @@ "node_modules/gulp-plumber/node_modules/ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8= sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -10389,7 +10320,7 @@ "node_modules/has-ansi/node_modules/ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8= sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -10685,7 +10616,7 @@ "node_modules/husky/node_modules/ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8= sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -10912,9 +10843,9 @@ } }, "node_modules/inquirer/node_modules/ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", "dev": true, "engines": { "node": ">=6" @@ -11045,9 +10976,9 @@ } }, "node_modules/inquirer/node_modules/string-width/node_modules/ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= sha512-wFUFA5bg5dviipbQQ32yOQhl6gcJaJXiHE7dvR8VYPG97+J/GNC5FKGepKdEDUFeXRzDxPF1X/Btc8L+v7oqIQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", "dev": true, "engines": { "node": ">=4" @@ -11119,15 +11050,6 @@ "node": ">= 12" } }, - "node_modules/is": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/is/-/is-3.2.1.tgz", - "integrity": "sha1-0Kwq1V63sL7JJqUmb2xmKqqD3KU= sha512-NgGnzF+/wucMxle6i32obg/UjUqQruwlJUtjBpDEMXNq8vPLuaf28U4q+yes1I6J89FoErr7kDtBGICoNaY7rQ==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/is-absolute": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", @@ -12565,18 +12487,6 @@ "node": ">=0.10.0" } }, - "node_modules/map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dev": true, - "dependencies": { - "p-defer": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -12847,20 +12757,6 @@ "node": ">= 0.6" } }, - "node_modules/mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "dev": true, - "dependencies": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/memoizee": { "version": "0.4.15", "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz", @@ -12951,11 +12847,12 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -13872,27 +13769,6 @@ "which": "bin/which" } }, - "node_modules/npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", - "dev": true, - "dependencies": { - "path-key": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/nth-check": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", @@ -14401,33 +14277,6 @@ "node": ">=8" } }, - "node_modules/p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/p-is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -17746,15 +17595,6 @@ "node": ">=0.10.0" } }, - "node_modules/strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -17971,9 +17811,9 @@ } }, "node_modules/table/node_modules/ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", "dev": true, "engines": { "node": ">=6" @@ -19258,362 +19098,6 @@ "node": ">= 0.10" } }, - "node_modules/vscode-nls-dev": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/vscode-nls-dev/-/vscode-nls-dev-3.3.1.tgz", - "integrity": "sha512-fug18D7CXb8pv8JoQ0D0JmZaIYDQoKLiyZxkAy5P8Cln/FwlNsdzwQILDph62EdGY5pvsJ2Jd1T5qgHAExe/tg==", - "dev": true, - "dependencies": { - "ansi-colors": "^3.2.3", - "clone": "^2.1.1", - "event-stream": "^3.3.4", - "fancy-log": "^1.3.3", - "glob": "^7.1.2", - "iconv-lite": "^0.4.19", - "is": "^3.2.1", - "source-map": "^0.6.1", - "typescript": "^2.6.2", - "vinyl": "^2.1.0", - "xml2js": "^0.4.19", - "yargs": "^13.2.4" - }, - "bin": { - "vscl": "lib/vscl.js" - } - }, - "node_modules/vscode-nls-dev/node_modules/ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/vscode-nls-dev/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/vscode-nls-dev/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/vscode-nls-dev/node_modules/cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "dev": true, - "dependencies": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - } - }, - "node_modules/vscode-nls-dev/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/vscode-nls-dev/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/vscode-nls-dev/node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/vscode-nls-dev/node_modules/emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "node_modules/vscode-nls-dev/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/vscode-nls-dev/node_modules/glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/vscode-nls-dev/node_modules/invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/vscode-nls-dev/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/vscode-nls-dev/node_modules/lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "dependencies": { - "invert-kv": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/vscode-nls-dev/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/vscode-nls-dev/node_modules/os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "dependencies": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/vscode-nls-dev/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/vscode-nls-dev/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/vscode-nls-dev/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/vscode-nls-dev/node_modules/replace-ext": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", - "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/vscode-nls-dev/node_modules/string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "dependencies": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/vscode-nls-dev/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/vscode-nls-dev/node_modules/typescript": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz", - "integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, - "node_modules/vscode-nls-dev/node_modules/vinyl": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", - "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", - "dev": true, - "dependencies": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/vscode-nls-dev/node_modules/wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/vscode-nls-dev/node_modules/xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", - "dev": true, - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/vscode-nls-dev/node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/vscode-nls-dev/node_modules/y18n": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", - "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", - "dev": true - }, - "node_modules/vscode-nls-dev/node_modules/yargs": { - "version": "13.2.4", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz", - "integrity": "sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==", - "dev": true, - "dependencies": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "os-locale": "^3.1.0", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.0" - } - }, - "node_modules/vscode-nls-dev/node_modules/yargs-parser": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", - "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", - "dev": true, - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - }, "node_modules/vscode-oniguruma": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", diff --git a/package.json b/package.json index 4e23f76c32d..407ac9a8689 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.94.0", - "distro": "1f99a7e25e381bc566d839c0259770ef5735d8ed", + "distro": "94e8178dbd2d80f72a25452eff94052eb9fbcf68", "author": { "name": "Microsoft Corporation" }, @@ -28,11 +28,11 @@ "kill-watch-webd": "deemon --kill npm run watch-web", "restart-watchd": "deemon --restart npm run watch", "restart-watch-webd": "deemon --restart npm run watch-web", - "watch-client": "node ./node_modules/gulp/bin/gulp.js watch-client", - "watch-client-amd": "node ./node_modules/gulp/bin/gulp.js watch-client-amd", + "watch-client": "node --max-old-space-size=8192 ./node_modules/gulp/bin/gulp.js watch-client", + "watch-client-amd": "node --max-old-space-size=8192 ./node_modules/gulp/bin/gulp.js watch-client-amd", "watch-clientd": "deemon npm run watch-client", "kill-watch-clientd": "deemon --kill npm run watch-client", - "watch-extensions": "node ./node_modules/gulp/bin/gulp.js watch-extensions watch-extension-media", + "watch-extensions": "node --max-old-space-size=8192 ./node_modules/gulp/bin/gulp.js watch-extensions watch-extension-media", "watch-extensionsd": "deemon npm run watch-extensions", "kill-watch-extensionsd": "deemon --kill npm run watch-extensions", "precommit": "node build/hygiene.js", @@ -75,6 +75,7 @@ "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "2.1.0", + "@bpasero/watcher": "https://github.com/bpasero/watcher.git#5d29cc732a03c91ecc2c861940a240b01e765c65", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/policy-watcher": "^1.1.4", @@ -83,7 +84,7 @@ "@vscode/spdlog": "^0.15.0", "@vscode/sqlite3": "5.1.6-vscode", "@vscode/sudo-prompt": "9.3.1", - "@vscode/tree-sitter-wasm": "^0.0.2", + "@vscode/tree-sitter-wasm": "^0.0.3", "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-mutex": "^0.5.0", "@vscode/windows-process-tree": "^0.6.0", @@ -146,6 +147,7 @@ "@vscode/test-web": "^0.0.56", "@vscode/v8-heap-parser": "^0.1.0", "@vscode/vscode-perf": "^0.0.14", + "@webgpu/types": "^0.1.44", "ansi-colors": "^3.2.3", "asar": "^3.0.3", "chromium-pickle-js": "^0.2.0", @@ -193,7 +195,6 @@ "mime": "^1.4.1", "minimatch": "^3.0.4", "minimist": "^1.2.6", - "mkdirp": "^1.0.4", "mocha": "^10.2.0", "mocha-junit-reporter": "^2.2.1", "mocha-multi-reporters": "^1.5.1", @@ -218,7 +219,6 @@ "tslib": "^2.6.3", "typescript": "^5.7.0-dev.20240903", "util": "^0.12.4", - "vscode-nls-dev": "^3.3.1", "webpack": "^5.94.0", "webpack-cli": "^5.1.4", "webpack-stream": "^7.0.0", diff --git a/remote/package-lock.json b/remote/package-lock.json index f7f57d71ba8..f11f798759c 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -8,6 +8,7 @@ "name": "vscode-reh", "version": "0.0.0", "dependencies": { + "@bpasero/watcher": "https://github.com/bpasero/watcher.git#5d29cc732a03c91ecc2c861940a240b01e765c65", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "2.1.0", @@ -16,7 +17,7 @@ "@vscode/proxy-agent": "^0.23.0", "@vscode/ripgrep": "^1.15.9", "@vscode/spdlog": "^0.15.0", - "@vscode/tree-sitter-wasm": "^0.0.2", + "@vscode/tree-sitter-wasm": "^0.0.3", "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", @@ -44,6 +45,44 @@ "yazl": "^2.4.3" } }, + "node_modules/@bpasero/watcher": { + "version": "2.4.2-alpha.0", + "resolved": "git+ssh://git@github.com/bpasero/watcher.git#5d29cc732a03c91ecc2c861940a240b01e765c65", + "integrity": "sha512-8AWyO22MDRxp6zAJxDWXGFCHtBoioaku+x/IQrDT3VADhBRAeCVp/47qCVrHFyU0ixo0m8KGDBN6MtxqsFFU2g==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@bpasero/watcher/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/@bpasero/watcher/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/@microsoft/1ds-core-js": { "version": "3.2.13", "resolved": "https://registry.npmjs.org/@microsoft/1ds-core-js/-/1ds-core-js-3.2.13.tgz", @@ -184,9 +223,9 @@ } }, "node_modules/@vscode/tree-sitter-wasm": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.0.2.tgz", - "integrity": "sha512-N57MR/kt4jR0H/TXeDsVYeJmvvUiK7avow0fjy+/EeKcyNBJcM2BFhj4XOAaaMbhGsOcIeSvJFouRWctXI7sKw==" + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.0.3.tgz", + "integrity": "sha512-K5YmUdohFCavPqEzG2cUPcZ6555KE1qwMDCjkvSUSz+s+8Wro2xfg+syLq90y6Tq0ZSUVvpuX6yq6ukToeGaLg==" }, "node_modules/@vscode/vscode-languagedetection": { "version": "1.0.21", diff --git a/remote/package.json b/remote/package.json index ab787bb880d..8f29232fea9 100644 --- a/remote/package.json +++ b/remote/package.json @@ -6,12 +6,13 @@ "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@parcel/watcher": "2.1.0", + "@bpasero/watcher": "https://github.com/bpasero/watcher.git#5d29cc732a03c91ecc2c861940a240b01e765c65", "@vscode/deviceid": "^0.1.1", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/proxy-agent": "^0.23.0", "@vscode/ripgrep": "^1.15.9", "@vscode/spdlog": "^0.15.0", - "@vscode/tree-sitter-wasm": "^0.0.2", + "@vscode/tree-sitter-wasm": "^0.0.3", "@vscode/vscode-languagedetection": "1.0.21", "@vscode/windows-process-tree": "^0.6.0", "@vscode/windows-registry": "^1.1.0", diff --git a/remote/web/package-lock.json b/remote/web/package-lock.json index a454672bb4d..332f3ccf36b 100644 --- a/remote/web/package-lock.json +++ b/remote/web/package-lock.json @@ -11,7 +11,7 @@ "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@vscode/iconv-lite-umd": "0.7.0", - "@vscode/tree-sitter-wasm": "^0.0.2", + "@vscode/tree-sitter-wasm": "^0.0.3", "@vscode/vscode-languagedetection": "1.0.21", "@xterm/addon-clipboard": "0.2.0-beta.39", "@xterm/addon-image": "0.9.0-beta.56", @@ -74,9 +74,9 @@ "integrity": "sha512-bRRFxLfg5dtAyl5XyiVWz/ZBPahpOpPrNYnnHpOpUZvam4tKH35wdhP4Kj6PbM0+KdliOsPzbGWpkxcdpNB/sg==" }, "node_modules/@vscode/tree-sitter-wasm": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.0.2.tgz", - "integrity": "sha512-N57MR/kt4jR0H/TXeDsVYeJmvvUiK7avow0fjy+/EeKcyNBJcM2BFhj4XOAaaMbhGsOcIeSvJFouRWctXI7sKw==" + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.0.3.tgz", + "integrity": "sha512-K5YmUdohFCavPqEzG2cUPcZ6555KE1qwMDCjkvSUSz+s+8Wro2xfg+syLq90y6Tq0ZSUVvpuX6yq6ukToeGaLg==" }, "node_modules/@vscode/vscode-languagedetection": { "version": "1.0.21", diff --git a/remote/web/package.json b/remote/web/package.json index 66daa8251e9..c4d2eaeb540 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -6,7 +6,7 @@ "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@vscode/iconv-lite-umd": "0.7.0", - "@vscode/tree-sitter-wasm": "^0.0.2", + "@vscode/tree-sitter-wasm": "^0.0.3", "@vscode/vscode-languagedetection": "1.0.21", "@xterm/addon-clipboard": "0.2.0-beta.39", "@xterm/addon-image": "0.9.0-beta.56", diff --git a/resources/server/bin/helpers/check-requirements-linux.sh b/resources/server/bin/helpers/check-requirements-linux.sh index 31a618fbd85..8ef07a2fb1f 100644 --- a/resources/server/bin/helpers/check-requirements-linux.sh +++ b/resources/server/bin/helpers/check-requirements-linux.sh @@ -125,6 +125,9 @@ elif [ -z "$(ldd --version 2>&1 | grep 'musl libc')" ]; then elif [ -f /usr/lib/libc.so.6 ]; then # Typical path libc_path='/usr/lib/libc.so.6' + elif [ -f /lib64/libc.so.6 ]; then + # Typical path (OpenSUSE) + libc_path='/lib64/libc.so.6' elif [ -f /usr/lib64/libc.so.6 ]; then # Typical path libc_path='/usr/lib64/libc.so.6' diff --git a/src/tsconfig.json b/src/tsconfig.json index 904afc7199b..88514beab2e 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -10,11 +10,12 @@ "isolatedModules": false, "outDir": "../out/vs", "types": [ + "@webgpu/types", "mocha", "semver", "sinon", - "winreg", "trusted-types", + "winreg", "wicg-file-system-access" ], "plugins": [ diff --git a/src/tsconfig.monaco.json b/src/tsconfig.monaco.json index 92d2e2adfa3..1e138cf9085 100644 --- a/src/tsconfig.monaco.json +++ b/src/tsconfig.monaco.json @@ -3,6 +3,7 @@ "compilerOptions": { "noEmit": true, "types": [ + "@webgpu/types", "trusted-types", "wicg-file-system-access" ], diff --git a/src/vs/base/browser/defaultWorkerFactory.ts b/src/vs/base/browser/defaultWorkerFactory.ts index 6add6ba6288..a8740a589e9 100644 --- a/src/vs/base/browser/defaultWorkerFactory.ts +++ b/src/vs/base/browser/defaultWorkerFactory.ts @@ -93,15 +93,18 @@ function getWorkerBootstrapUrl(label: string, workerScriptUrl: string, workerBas } } + // In below blob code, we are using JSON.stringify to ensure the passed + // in values are not breaking our script. The values may contain string + // terminating characters (such as ' or "). const blob = new Blob([coalesce([ `/*${label}*/`, - workerBaseUrl ? `globalThis.MonacoEnvironment = { baseUrl: '${workerBaseUrl}' };` : undefined, + workerBaseUrl ? `globalThis.MonacoEnvironment = { baseUrl: ${JSON.stringify(workerBaseUrl)} };` : undefined, `globalThis._VSCODE_NLS_MESSAGES = ${JSON.stringify(getNLSMessages())};`, `globalThis._VSCODE_NLS_LANGUAGE = ${JSON.stringify(getNLSLanguage())};`, - `globalThis._VSCODE_FILE_ROOT = '${globalThis._VSCODE_FILE_ROOT}';`, + `globalThis._VSCODE_FILE_ROOT = ${JSON.stringify(globalThis._VSCODE_FILE_ROOT)};`, `const ttPolicy = globalThis.trustedTypes?.createPolicy('defaultWorkerFactory', { createScriptURL: value => value });`, `globalThis.workerttPolicy = ttPolicy;`, - isESM ? `await import(ttPolicy?.createScriptURL('${workerScriptUrl}') ?? '${workerScriptUrl}');` : `importScripts(ttPolicy?.createScriptURL('${workerScriptUrl}') ?? '${workerScriptUrl}');`, // + isESM ? `await import(ttPolicy?.createScriptURL(${JSON.stringify(workerScriptUrl)}) ?? ${JSON.stringify(workerScriptUrl)});` : `importScripts(ttPolicy?.createScriptURL(${JSON.stringify(workerScriptUrl)}) ?? ${JSON.stringify(workerScriptUrl)});`, isESM ? `globalThis.postMessage({ type: 'vscode-worker-ready' });` : undefined, // in ESM signal we are ready after the async import `/*${label}*/` ]).join('')], { type: 'application/javascript' }); diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 1b937448c24..e502b62da26 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -1951,10 +1951,10 @@ const defaultDomPurifyConfig = Object.freeze unescapeInfo.get(m) ?? m); - - return sanitizeRenderedMarkdown({ isTrusted: false }, html).toString(); + const html = marked.parse(value, { async: false, renderer: withCodeBlocks ? plainTextWithCodeBlocksRenderer.value : plainTextRenderer.value }); + return sanitizeRenderedMarkdown({ isTrusted: false }, html).toString().replace(/&(#\d+|[a-zA-Z]+);/g, m => unescapeInfo.get(m) ?? m); } const unescapeInfo = new Map([ diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 67b8e18a3a6..6b6f6bcdd6a 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -892,7 +892,7 @@ export class DefaultStyleController implements IStyleController { } if (styles.listActiveSelectionIconForeground) { - content.push(`.monaco-list${suffix}:focus .monaco-list-row.selected .codicon { color: ${styles.listActiveSelectionIconForeground}; }`); + content.push(`@layer monaco-list { .monaco-list${suffix}:focus .monaco-list-row.selected .codicon { color: ${styles.listActiveSelectionIconForeground}; } }`); } if (styles.listFocusAndSelectionBackground) { @@ -915,7 +915,7 @@ export class DefaultStyleController implements IStyleController { } if (styles.listInactiveSelectionIconForeground) { - content.push(`.monaco-list${suffix} .monaco-list-row.focused .codicon { color: ${styles.listInactiveSelectionIconForeground}; }`); + content.push(`@layer monaco-list { .monaco-list${suffix} .monaco-list-row.focused .codicon { color: ${styles.listInactiveSelectionIconForeground}; } }`); } if (styles.listInactiveFocusBackground) { diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index 97f3c6015fb..023b00af3a4 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -3144,6 +3144,10 @@ export abstract class AbstractTree implements IDisposable this.renderers.forEach(r => r.setModel(newModel)); this.stickyScrollController?.setModel(newModel); + this.focus.set([]); + this.selection.set([]); + this.anchor.set([]); + this.view.splice(0, oldModel.getListRenderCount(oldModel.rootRef)); this.model.refilter(); diff --git a/src/vs/base/common/collections.ts b/src/vs/base/common/collections.ts index d0df190c75b..a47e629160e 100644 --- a/src/vs/base/common/collections.ts +++ b/src/vs/base/common/collections.ts @@ -32,7 +32,7 @@ export function groupBy(data: V[], groupF return result; } -export function diffSets(before: Set, after: Set): { removed: T[]; added: T[] } { +export function diffSets(before: ReadonlySet, after: ReadonlySet): { removed: T[]; added: T[] } { const removed: T[] = []; const added: T[] = []; for (const element of before) { diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index e40c0caace9..915f481fba6 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from './cancellation.js'; +import { diffSets } from './collections.js'; import { onUnexpectedError } from './errors.js'; import { createSingleCallFunction } from './functional.js'; import { combinedDisposable, Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from './lifecycle.js'; @@ -12,7 +13,6 @@ import { IObservable, IObserver } from './observable.js'; import { StopWatch } from './stopwatch.js'; import { MicrotaskDelay } from './symbols.js'; - // ----------------------------------------------------------------------------------------------------------------------- // Uncomment the next line to print warnings whenever a listener is GC'ed without having been disposed. This is a LEAK. // ----------------------------------------------------------------------------------------------------------------------- @@ -1791,3 +1791,30 @@ class ConstValueWithChangeEvent implements IValueWithChangeEvent { constructor(readonly value: T) { } } + +/** + * @param handleItem Is called for each item in the set (but only the first time the item is seen in the set). + * The returned disposable is disposed if the item is no longer in the set. + */ +export function trackSetChanges(getData: () => ReadonlySet, onDidChangeData: Event, handleItem: (d: T) => IDisposable): IDisposable { + const map = new DisposableMap(); + let oldData = new Set(getData()); + for (const d of oldData) { + map.set(d, handleItem(d)); + } + + const store = new DisposableStore(); + store.add(onDidChangeData(() => { + const newData = getData(); + const diff = diffSets(oldData, newData); + for (const r of diff.removed) { + map.deleteAndDispose(r); + } + for (const a of diff.added) { + map.set(a, handleItem(a)); + } + oldData = new Set(newData); + })); + store.add(map); + return store; +} diff --git a/src/vs/base/common/hotReload.ts b/src/vs/base/common/hotReload.ts index fbf76480587..80901b470f0 100644 --- a/src/vs/base/common/hotReload.ts +++ b/src/vs/base/common/hotReload.ts @@ -6,8 +6,12 @@ import { IDisposable } from './lifecycle.js'; import { env } from './process.js'; +function hotReloadDisabled() { + return true; // TODO@hediet fix hot reload. +} + export function isHotReloadEnabled(): boolean { - return env && !!env['VSCODE_DEV']; + return !hotReloadDisabled() && env && !!env['VSCODE_DEV']; } export function registerHotReloadHandler(handler: HotReloadHandler): IDisposable { if (!isHotReloadEnabled()) { diff --git a/src/vs/base/common/map.ts b/src/vs/base/common/map.ts index 0da06668ec4..bb515d63219 100644 --- a/src/vs/base/common/map.ts +++ b/src/vs/base/common/map.ts @@ -875,3 +875,60 @@ export function mapsStrictEqualIgnoreOrder(a: Map, b: Map { + private _data: { [key: string | number]: { [key: string | number]: TValue | undefined } | undefined } = {}; + + public set(first: TFirst, second: TSecond, value: TValue): void { + if (!this._data[first]) { + this._data[first] = {}; + } + this._data[first as string | number]![second] = value; + } + + public get(first: TFirst, second: TSecond): TValue | undefined { + return this._data[first as string | number]?.[second]; + } + + public clear(): void { + this._data = {}; + } + + public *values(): IterableIterator { + for (const first in this._data) { + for (const second in this._data[first]) { + const value = this._data[first]![second]; + if (value) { + yield value; + } + } + } + } +} + +/** + * A map that is addressable with 4 separate keys. This is useful in high performance scenarios + * where creating a composite key is too expensive. + */ +export class FourKeyMap { + private _data: TwoKeyMap> = new TwoKeyMap(); + + public set(first: TFirst, second: TSecond, third: TThird, fourth: TFourth, value: TValue): void { + if (!this._data.get(first, second)) { + this._data.set(first, second, new TwoKeyMap()); + } + this._data.get(first, second)!.set(third, fourth, value); + } + + public get(first: TFirst, second: TSecond, third: TThird, fourth: TFourth): TValue | undefined { + return this._data.get(first, second)?.get(third, fourth); + } + + public clear(): void { + this._data.clear(); + } +} diff --git a/src/vs/base/common/observable.ts b/src/vs/base/common/observable.ts index 1b8cecf7e69..86ac5971a27 100644 --- a/src/vs/base/common/observable.ts +++ b/src/vs/base/common/observable.ts @@ -5,72 +5,4 @@ // This is a facade for the observable implementation. Only import from here! -export type { - IObservable, - IObserver, - IReader, - ISettable, - ISettableObservable, - ITransaction, - IChangeContext, - IChangeTracker, -} from './observableInternal/base.js'; - -export { - observableValue, - disposableObservableValue, - transaction, - subtransaction, -} from './observableInternal/base.js'; -export { - derived, - derivedOpts, - derivedHandleChanges, - derivedWithStore, -} from './observableInternal/derived.js'; -export { - autorun, - autorunDelta, - autorunHandleChanges, - autorunWithStore, - autorunOpts, - autorunWithStoreHandleChanges, -} from './observableInternal/autorun.js'; -export type { - IObservableSignal, -} from './observableInternal/utils.js'; -export { - constObservable, - debouncedObservable, - derivedObservableWithCache, - derivedObservableWithWritableCache, - keepObserved, - recomputeInitiallyAndOnChange, - observableFromEvent, - observableFromPromise, - observableSignal, - observableSignalFromEvent, - wasEventTriggeredRecently, -} from './observableInternal/utils.js'; -export { - ObservableLazy, - ObservableLazyPromise, - ObservablePromise, - PromiseResult, - waitForState, - derivedWithCancellationToken, -} from './observableInternal/promise.js'; -export { - observableValueOpts -} from './observableInternal/api.js'; - -import { ConsoleObservableLogger, setLogger } from './observableInternal/logging.js'; - -// Remove "//" in the next line to enable logging -const enableLogging = false - // || Boolean("true") // done "weirdly" so that a lint warning prevents you from pushing this - ; - -if (enableLogging) { - setLogger(new ConsoleObservableLogger()); -} +export * from './observableInternal/index.js'; diff --git a/src/vs/base/common/observableInternal/api.ts b/src/vs/base/common/observableInternal/api.ts index 57cb2a5994d..0f7e63c3fa9 100644 --- a/src/vs/base/common/observableInternal/api.ts +++ b/src/vs/base/common/observableInternal/api.ts @@ -3,10 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EqualityComparer, strictEquals } from '../equals.js'; -import { ISettableObservable } from '../observable.js'; -import { ObservableValue } from './base.js'; -import { IDebugNameData, DebugNameData } from './debugName.js'; +import { ISettableObservable, ObservableValue } from './base.js'; +import { DebugNameData, IDebugNameData } from './debugName.js'; +import { EqualityComparer, strictEquals } from './commonFacade/deps.js'; import { LazyObservableValue } from './lazyObservableValue.js'; export function observableValueOpts( diff --git a/src/vs/base/common/observableInternal/autorun.ts b/src/vs/base/common/observableInternal/autorun.ts index dba2eb7edf3..f2011ef433d 100644 --- a/src/vs/base/common/observableInternal/autorun.ts +++ b/src/vs/base/common/observableInternal/autorun.ts @@ -3,11 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { assertFn } from '../assert.js'; -import { onBugIndicatingError } from '../errors.js'; -import { DisposableStore, IDisposable, markAsDisposed, toDisposable, trackDisposable } from '../lifecycle.js'; import { IChangeContext, IObservable, IObserver, IReader } from './base.js'; import { DebugNameData, IDebugNameData } from './debugName.js'; +import { assertFn, DisposableStore, IDisposable, markAsDisposed, onBugIndicatingError, toDisposable, trackDisposable } from './commonFacade/deps.js'; import { getLogger } from './logging.js'; /** diff --git a/src/vs/base/common/observableInternal/base.ts b/src/vs/base/common/observableInternal/base.ts index 6821b4f3934..3ce43ead2ae 100644 --- a/src/vs/base/common/observableInternal/base.ts +++ b/src/vs/base/common/observableInternal/base.ts @@ -3,12 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { strictEquals, EqualityComparer } from '../equals.js'; -import { DisposableStore, IDisposable } from '../lifecycle.js'; -import { keepObserved, recomputeInitiallyAndOnChange } from '../observable.js'; import { DebugNameData, DebugOwner, getFunctionName } from './debugName.js'; +import { DisposableStore, EqualityComparer, IDisposable, strictEquals } from './commonFacade/deps.js'; import type { derivedOpts } from './derived.js'; import { getLogger } from './logging.js'; +import { keepObserved, recomputeInitiallyAndOnChange } from './utils.js'; /** * Represents an observable value. diff --git a/src/vs/workbench/contrib/chat/browser/media/chatInlineFileLinkWidget.css b/src/vs/base/common/observableInternal/commonFacade/cancellation.ts similarity index 66% rename from src/vs/workbench/contrib/chat/browser/media/chatInlineFileLinkWidget.css rename to src/vs/base/common/observableInternal/commonFacade/cancellation.ts index d4746e3afe3..af1686a3206 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatInlineFileLinkWidget.css +++ b/src/vs/base/common/observableInternal/commonFacade/cancellation.ts @@ -3,10 +3,5 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.chat-inline-file-link-widget .monaco-icon-label { - display: inline-flex; -} - -.chat-inline-file-link-widget .monaco-icon-label .monaco-highlighted-label { - white-space: normal; -} +export { CancellationError } from '../../errors.js'; +export { CancellationToken, CancellationTokenSource } from '../../cancellation.js'; diff --git a/src/vs/base/common/observableInternal/commonFacade/deps.ts b/src/vs/base/common/observableInternal/commonFacade/deps.ts new file mode 100644 index 00000000000..1ed8b1cc594 --- /dev/null +++ b/src/vs/base/common/observableInternal/commonFacade/deps.ts @@ -0,0 +1,10 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export { assertFn } from '../../assert.js'; +export { type EqualityComparer, strictEquals } from '../../equals.js'; +export { BugIndicatingError, onBugIndicatingError } from '../../errors.js'; +export { Event, type IValueWithChangeEvent } from '../../event.js'; +export { DisposableStore, type IDisposable, markAsDisposed, toDisposable, trackDisposable } from '../../lifecycle.js'; diff --git a/src/vs/base/common/observableInternal/derived.ts b/src/vs/base/common/observableInternal/derived.ts index 88a060eb694..ed32875f8ba 100644 --- a/src/vs/base/common/observableInternal/derived.ts +++ b/src/vs/base/common/observableInternal/derived.ts @@ -3,12 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { assertFn } from '../assert.js'; -import { EqualityComparer, strictEquals } from '../equals.js'; -import { onBugIndicatingError } from '../errors.js'; -import { DisposableStore, IDisposable } from '../lifecycle.js'; import { BaseObservable, IChangeContext, IObservable, IObserver, IReader, ISettableObservable, ITransaction, _setDerivedOpts, } from './base.js'; -import { DebugNameData, IDebugNameData, DebugOwner } from './debugName.js'; +import { DebugNameData, DebugOwner, IDebugNameData } from './debugName.js'; +import { DisposableStore, EqualityComparer, IDisposable, assertFn, onBugIndicatingError, strictEquals } from './commonFacade/deps.js'; import { getLogger } from './logging.js'; /** diff --git a/src/vs/base/common/observableInternal/index.ts b/src/vs/base/common/observableInternal/index.ts new file mode 100644 index 00000000000..9295f2697d5 --- /dev/null +++ b/src/vs/base/common/observableInternal/index.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// This is a facade for the observable implementation. Only import from here! + +export { observableValueOpts } from './api.js'; +export { autorun, autorunDelta, autorunHandleChanges, autorunOpts, autorunWithStore, autorunWithStoreHandleChanges } from './autorun.js'; +export { asyncTransaction, disposableObservableValue, globalTransaction, observableValue, subtransaction, transaction, TransactionImpl, type IChangeContext, type IChangeTracker, type IObservable, type IObserver, type IReader, type ISettable, type ISettableObservable, type ITransaction, } from './base.js'; +export { derived, derivedDisposable, derivedHandleChanges, derivedOpts, derivedWithSetter, derivedWithStore } from './derived.js'; +export { ObservableLazy, ObservableLazyPromise, ObservablePromise, PromiseResult, } from './promise.js'; +export { derivedWithCancellationToken, waitForState } from './utilsCancellation.js'; +export { constObservable, debouncedObservable, derivedConstOnceDefined, derivedObservableWithCache, derivedObservableWithWritableCache, keepObserved, latestChangedValue, mapObservableArrayCached, observableFromEvent, observableFromEventOpts, observableFromPromise, observableFromValueWithChangeEvent, observableSignal, observableSignalFromEvent, recomputeInitiallyAndOnChange, runOnChange, runOnChangeWithStore, signalFromObservable, ValueWithChangeEventFromObservable, wasEventTriggeredRecently, type IObservableSignal, } from './utils.js'; +export { type DebugOwner } from './debugName.js'; + +import { + ConsoleObservableLogger, + setLogger +} from './logging.js'; + +// Remove "//" in the next line to enable logging +const enableLogging = false + // || Boolean("true") // done "weirdly" so that a lint warning prevents you from pushing this + ; + +if (enableLogging) { + setLogger(new ConsoleObservableLogger()); +} diff --git a/src/vs/base/common/observableInternal/lazyObservableValue.ts b/src/vs/base/common/observableInternal/lazyObservableValue.ts index 363fd7f8c5a..8a3f63c05d7 100644 --- a/src/vs/base/common/observableInternal/lazyObservableValue.ts +++ b/src/vs/base/common/observableInternal/lazyObservableValue.ts @@ -3,9 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EqualityComparer } from '../equals.js'; -import { ISettableObservable, ITransaction } from '../observable.js'; -import { BaseObservable, IObserver, TransactionImpl } from './base.js'; +import { EqualityComparer } from './commonFacade/deps.js'; +import { BaseObservable, IObserver, ISettableObservable, ITransaction, TransactionImpl } from './base.js'; import { DebugNameData } from './debugName.js'; /** diff --git a/src/vs/base/common/observableInternal/promise.ts b/src/vs/base/common/observableInternal/promise.ts index 6989a58731f..6551c21664c 100644 --- a/src/vs/base/common/observableInternal/promise.ts +++ b/src/vs/base/common/observableInternal/promise.ts @@ -2,13 +2,8 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { autorun } from './autorun.js'; -import { IObservable, IReader, observableValue, transaction } from './base.js'; -import { Derived, derived } from './derived.js'; -import { CancellationToken, CancellationTokenSource } from '../cancellation.js'; -import { DebugNameData, DebugOwner } from './debugName.js'; -import { strictEquals } from '../equals.js'; -import { CancellationError } from '../errors.js'; +import { IObservable, observableValue, transaction } from './base.js'; +import { derived } from './derived.js'; export class ObservableLazy { private readonly _value = observableValue(this, undefined); @@ -120,90 +115,3 @@ export class ObservableLazyPromise { return this._lazyValue.getValue().promise; } } - -/** - * Resolves the promise when the observables state matches the predicate. - */ -export function waitForState(observable: IObservable): Promise; -export function waitForState(observable: IObservable, predicate: (state: T) => state is TState, isError?: (state: T) => boolean | unknown | undefined, cancellationToken?: CancellationToken): Promise; -export function waitForState(observable: IObservable, predicate: (state: T) => boolean, isError?: (state: T) => boolean | unknown | undefined, cancellationToken?: CancellationToken): Promise; -export function waitForState(observable: IObservable, predicate?: (state: T) => boolean, isError?: (state: T) => boolean | unknown | undefined, cancellationToken?: CancellationToken): Promise { - if (!predicate) { - predicate = state => state !== null && state !== undefined; - } - return new Promise((resolve, reject) => { - let isImmediateRun = true; - let shouldDispose = false; - const stateObs = observable.map(state => { - /** @description waitForState.state */ - return { - isFinished: predicate(state), - error: isError ? isError(state) : false, - state - }; - }); - const d = autorun(reader => { - /** @description waitForState */ - const { isFinished, error, state } = stateObs.read(reader); - if (isFinished || error) { - if (isImmediateRun) { - // The variable `d` is not initialized yet - shouldDispose = true; - } else { - d.dispose(); - } - if (error) { - reject(error === true ? state : error); - } else { - resolve(state); - } - } - }); - if (cancellationToken) { - const dc = cancellationToken.onCancellationRequested(() => { - d.dispose(); - dc.dispose(); - reject(new CancellationError()); - }); - if (cancellationToken.isCancellationRequested) { - d.dispose(); - dc.dispose(); - reject(new CancellationError()); - return; - } - } - isImmediateRun = false; - if (shouldDispose) { - d.dispose(); - } - }); -} - -export function derivedWithCancellationToken(computeFn: (reader: IReader, cancellationToken: CancellationToken) => T): IObservable; -export function derivedWithCancellationToken(owner: object, computeFn: (reader: IReader, cancellationToken: CancellationToken) => T): IObservable; -export function derivedWithCancellationToken(computeFnOrOwner: ((reader: IReader, cancellationToken: CancellationToken) => T) | object, computeFnOrUndefined?: ((reader: IReader, cancellationToken: CancellationToken) => T)): IObservable { - let computeFn: (reader: IReader, store: CancellationToken) => T; - let owner: DebugOwner; - if (computeFnOrUndefined === undefined) { - computeFn = computeFnOrOwner as any; - owner = undefined; - } else { - owner = computeFnOrOwner; - computeFn = computeFnOrUndefined as any; - } - - let cancellationTokenSource: CancellationTokenSource | undefined = undefined; - return new Derived( - new DebugNameData(owner, undefined, computeFn), - r => { - if (cancellationTokenSource) { - cancellationTokenSource.dispose(true); - } - cancellationTokenSource = new CancellationTokenSource(); - return computeFn(r, cancellationTokenSource.token); - }, undefined, - undefined, - () => cancellationTokenSource?.dispose(), - strictEquals, - ); -} diff --git a/src/vs/base/common/observableInternal/utils.ts b/src/vs/base/common/observableInternal/utils.ts index b4230118e88..2fb57d1a42a 100644 --- a/src/vs/base/common/observableInternal/utils.ts +++ b/src/vs/base/common/observableInternal/utils.ts @@ -3,15 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event, IValueWithChangeEvent } from '../event.js'; -import { DisposableStore, IDisposable, toDisposable } from '../lifecycle.js'; import { autorun, autorunOpts, autorunWithStoreHandleChanges } from './autorun.js'; import { BaseObservable, ConvenientObservable, IObservable, IObserver, IReader, ITransaction, _setKeepObserved, _setRecomputeInitiallyAndOnChange, observableValue, subtransaction, transaction } from './base.js'; -import { DebugNameData, IDebugNameData, DebugOwner, getDebugName, } from './debugName.js'; +import { DebugNameData, DebugOwner, IDebugNameData, getDebugName, } from './debugName.js'; +import { BugIndicatingError, DisposableStore, EqualityComparer, Event, IDisposable, IValueWithChangeEvent, strictEquals, toDisposable } from './commonFacade/deps.js'; import { derived, derivedOpts } from './derived.js'; import { getLogger } from './logging.js'; -import { BugIndicatingError } from '../errors.js'; -import { EqualityComparer, strictEquals } from '../equals.js'; /** * Represents an efficient observable whose value never changes. @@ -301,6 +298,15 @@ class ObservableSignal extends BaseObservable implements } } +export function signalFromObservable(owner: DebugOwner | undefined, observable: IObservable): IObservable { + return derivedOpts({ + owner, + equalsFn: () => false, + }, reader => { + observable.read(reader); + }); +} + /** * @deprecated Use `debouncedObservable2` instead. */ @@ -619,7 +625,8 @@ export function derivedConstOnceDefined(owner: DebugOwner, fn: (reader: IRead type RemoveUndefined = T extends undefined ? never : T; -export function runOnChange(observable: IObservable, cb: (value: T, deltas: RemoveUndefined[]) => void): IDisposable { +export function runOnChange(observable: IObservable, cb: (value: T, previousValue: undefined | T, deltas: RemoveUndefined[]) => void): IDisposable { + let _previousValue: T | undefined; return autorunWithStoreHandleChanges({ createEmptyChangeSummary: () => ({ deltas: [] as RemoveUndefined[], didChange: false }), handleChange: (context, changeSummary) => { @@ -634,17 +641,19 @@ export function runOnChange(observable: IObservable, cb: }, }, (reader, changeSummary) => { const value = observable.read(reader); + const previousValue = _previousValue; if (changeSummary.didChange) { - cb(value, changeSummary.deltas); + _previousValue = value; + cb(value, previousValue, changeSummary.deltas); } }); } -export function runOnChangeWithStore(observable: IObservable, cb: (value: T, deltas: RemoveUndefined[], store: DisposableStore) => void): IDisposable { +export function runOnChangeWithStore(observable: IObservable, cb: (value: T, previousValue: undefined | T, deltas: RemoveUndefined[], store: DisposableStore) => void): IDisposable { const store = new DisposableStore(); - const disposable = runOnChange(observable, (value, deltas) => { + const disposable = runOnChange(observable, (value, previousValue: undefined | T, deltas) => { store.clear(); - cb(value, deltas, store); + cb(value, previousValue, deltas, store); }); return { dispose() { diff --git a/src/vs/base/common/observableInternal/utilsCancellation.ts b/src/vs/base/common/observableInternal/utilsCancellation.ts new file mode 100644 index 00000000000..17e4ba9e308 --- /dev/null +++ b/src/vs/base/common/observableInternal/utilsCancellation.ts @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IReader, IObservable } from './base.js'; +import { DebugOwner, DebugNameData } from './debugName.js'; +import { CancellationError, CancellationToken, CancellationTokenSource } from './commonFacade/cancellation.js'; +import { Derived } from './derived.js'; +import { strictEquals } from './commonFacade/deps.js'; +import { autorun } from './autorun.js'; + +/** + * Resolves the promise when the observables state matches the predicate. + */ +export function waitForState(observable: IObservable): Promise; +export function waitForState(observable: IObservable, predicate: (state: T) => state is TState, isError?: (state: T) => boolean | unknown | undefined, cancellationToken?: CancellationToken): Promise; +export function waitForState(observable: IObservable, predicate: (state: T) => boolean, isError?: (state: T) => boolean | unknown | undefined, cancellationToken?: CancellationToken): Promise; +export function waitForState(observable: IObservable, predicate?: (state: T) => boolean, isError?: (state: T) => boolean | unknown | undefined, cancellationToken?: CancellationToken): Promise { + if (!predicate) { + predicate = state => state !== null && state !== undefined; + } + return new Promise((resolve, reject) => { + let isImmediateRun = true; + let shouldDispose = false; + const stateObs = observable.map(state => { + /** @description waitForState.state */ + return { + isFinished: predicate(state), + error: isError ? isError(state) : false, + state + }; + }); + const d = autorun(reader => { + /** @description waitForState */ + const { isFinished, error, state } = stateObs.read(reader); + if (isFinished || error) { + if (isImmediateRun) { + // The variable `d` is not initialized yet + shouldDispose = true; + } else { + d.dispose(); + } + if (error) { + reject(error === true ? state : error); + } else { + resolve(state); + } + } + }); + if (cancellationToken) { + const dc = cancellationToken.onCancellationRequested(() => { + d.dispose(); + dc.dispose(); + reject(new CancellationError()); + }); + if (cancellationToken.isCancellationRequested) { + d.dispose(); + dc.dispose(); + reject(new CancellationError()); + return; + } + } + isImmediateRun = false; + if (shouldDispose) { + d.dispose(); + } + }); +} + +export function derivedWithCancellationToken(computeFn: (reader: IReader, cancellationToken: CancellationToken) => T): IObservable; +export function derivedWithCancellationToken(owner: object, computeFn: (reader: IReader, cancellationToken: CancellationToken) => T): IObservable; +export function derivedWithCancellationToken(computeFnOrOwner: ((reader: IReader, cancellationToken: CancellationToken) => T) | object, computeFnOrUndefined?: ((reader: IReader, cancellationToken: CancellationToken) => T)): IObservable { + let computeFn: (reader: IReader, store: CancellationToken) => T; + let owner: DebugOwner; + if (computeFnOrUndefined === undefined) { + computeFn = computeFnOrOwner as any; + owner = undefined; + } else { + owner = computeFnOrOwner; + computeFn = computeFnOrUndefined as any; + } + + let cancellationTokenSource: CancellationTokenSource | undefined = undefined; + return new Derived( + new DebugNameData(owner, undefined, computeFn), + r => { + if (cancellationTokenSource) { + cancellationTokenSource.dispose(true); + } + cancellationTokenSource = new CancellationTokenSource(); + return computeFn(r, cancellationTokenSource.token); + }, undefined, + undefined, + () => cancellationTokenSource?.dispose(), + strictEquals + ); +} diff --git a/src/vs/base/test/common/map.test.ts b/src/vs/base/test/common/map.test.ts index 62a29746148..2cf1b0ae7bb 100644 --- a/src/vs/base/test/common/map.test.ts +++ b/src/vs/base/test/common/map.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { BidirectionalMap, LinkedMap, LRUCache, mapsStrictEqualIgnoreOrder, MRUCache, ResourceMap, SetMap, Touch } from '../../common/map.js'; +import { BidirectionalMap, FourKeyMap, LinkedMap, LRUCache, mapsStrictEqualIgnoreOrder, MRUCache, ResourceMap, SetMap, Touch, TwoKeyMap } from '../../common/map.js'; import { extUriIgnorePathCase } from '../../common/resources.js'; import { URI } from '../../common/uri.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js'; @@ -681,5 +681,65 @@ suite('SetMap', () => { const setMap = new SetMap(); assert.deepStrictEqual([...setMap.get('a')], []); }); - +}); + +suite('TwoKeyMap', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('set and get', () => { + const map = new TwoKeyMap(); + map.set('a', 'b', 1); + map.set('a', 'c', 2); + map.set('b', 'c', 3); + assert.strictEqual(map.get('a', 'b'), 1); + assert.strictEqual(map.get('a', 'c'), 2); + assert.strictEqual(map.get('b', 'c'), 3); + assert.strictEqual(map.get('a', 'd'), undefined); + }); + + test('clear', () => { + const map = new TwoKeyMap(); + map.set('a', 'b', 1); + map.set('a', 'c', 2); + map.set('b', 'c', 3); + map.clear(); + assert.strictEqual(map.get('a', 'b'), undefined); + assert.strictEqual(map.get('a', 'c'), undefined); + assert.strictEqual(map.get('b', 'c'), undefined); + }); + + test('values', () => { + const map = new TwoKeyMap(); + map.set('a', 'b', 1); + map.set('a', 'c', 2); + map.set('b', 'c', 3); + assert.deepStrictEqual(Array.from(map.values()), [1, 2, 3]); + }); +}); + + +suite('FourKeyMap', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + test('set and get', () => { + const map = new FourKeyMap(); + map.set('a', 'b', 'c', 'd', 1); + map.set('a', 'c', 'c', 'd', 2); + map.set('b', 'e', 'f', 'g', 3); + assert.strictEqual(map.get('a', 'b', 'c', 'd'), 1); + assert.strictEqual(map.get('a', 'c', 'c', 'd'), 2); + assert.strictEqual(map.get('b', 'e', 'f', 'g'), 3); + assert.strictEqual(map.get('a', 'b', 'c', 'a'), undefined); + }); + + test('clear', () => { + const map = new FourKeyMap(); + map.set('a', 'b', 'c', 'd', 1); + map.set('a', 'c', 'c', 'd', 2); + map.set('b', 'e', 'f', 'g', 3); + map.clear(); + assert.strictEqual(map.get('a', 'b', 'c', 'd'), undefined); + assert.strictEqual(map.get('a', 'c', 'c', 'd'), undefined); + assert.strictEqual(map.get('b', 'e', 'f', 'g'), undefined); + }); }); diff --git a/src/vs/base/test/common/observable.test.ts b/src/vs/base/test/common/observable.test.ts index 306f2a43a79..91d09493c15 100644 --- a/src/vs/base/test/common/observable.test.ts +++ b/src/vs/base/test/common/observable.test.ts @@ -4,13 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; +import { setUnexpectedErrorHandler } from '../../common/errors.js'; import { Emitter, Event } from '../../common/event.js'; import { DisposableStore } from '../../common/lifecycle.js'; -import { ISettableObservable, autorun, derived, ITransaction, observableFromEvent, observableValue, transaction, keepObserved, waitForState, autorunHandleChanges, observableSignal } from '../../common/observable.js'; -import { BaseObservable, IObservable, IObserver } from '../../common/observableInternal/base.js'; -import { derivedDisposable } from '../../common/observableInternal/derived.js'; +import { autorun, autorunHandleChanges, derived, derivedDisposable, IObservable, IObserver, ISettableObservable, ITransaction, keepObserved, observableFromEvent, observableSignal, observableValue, transaction, waitForState } from '../../common/observable.js'; +import { BaseObservable } from '../../common/observableInternal/base.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from './utils.js'; -import { setUnexpectedErrorHandler } from '../../common/errors.js'; suite('observables', () => { const ds = ensureNoDisposablesAreLeakedInTestSuite(); diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 7b6961ddf88..46b2d0b7256 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -211,11 +211,7 @@ class CodeMain { services.set(ILifecycleMainService, new SyncDescriptor(LifecycleMainService, undefined, false)); // Request - const networkLogger = loggerService.createLogger('network-main', { - name: localize('network-main', "Network (Main)"), - hidden: true - }); - services.set(IRequestService, new SyncDescriptor(RequestService, [networkLogger], true)); + services.set(IRequestService, new SyncDescriptor(RequestService, undefined, true)); // Themes services.set(IThemeMainService, new SyncDescriptor(ThemeMainService)); diff --git a/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts index ac39f266781..2a99598a73d 100644 --- a/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts @@ -270,11 +270,7 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter { ]); // Request - const networkLogger = loggerService.createLogger('network-shared', { - name: localize('network-shared', "Network (Shared)"), - hidden: true, - }); - const requestService = new RequestService(networkLogger, configurationService, environmentService, logService); + const requestService = new RequestService(configurationService, environmentService, logService); services.set(IRequestService, requestService); // Checksum diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index ffd48670edc..b25377a804d 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -117,7 +117,7 @@ export async function main(argv: string[]): Promise { // Extensions Management else if (shouldSpawnCliProcess(args)) { - const cli = await import(['vs', 'code', 'node', 'cliProcessMain'].join('/') /* TODO@esm workaround to prevent esbuild from inlining this */); + const cli = await import(['./cliProcessMain.js'].join('/') /* TODO@esm workaround to prevent esbuild from inlining this */); await cli.main(args); return; diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index 535c724b438..d3b20ecea47 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -38,7 +38,7 @@ import { InstantiationService } from '../../platform/instantiation/common/instan import { ServiceCollection } from '../../platform/instantiation/common/serviceCollection.js'; import { ILanguagePackService } from '../../platform/languagePacks/common/languagePacks.js'; import { NativeLanguagePackService } from '../../platform/languagePacks/node/languagePacks.js'; -import { ConsoleLogger, getLogLevel, ILogger, ILoggerService, ILogService, LogLevel, NullLogger } from '../../platform/log/common/log.js'; +import { ConsoleLogger, getLogLevel, ILogger, ILoggerService, ILogService, LogLevel } from '../../platform/log/common/log.js'; import { FilePolicyService } from '../../platform/policy/common/filePolicyService.js'; import { IPolicyService, NullPolicyService } from '../../platform/policy/common/policy.js'; import { NativePolicyService } from '../../platform/policy/node/nativePolicyService.js'; @@ -195,7 +195,7 @@ class CliMain extends Disposable { services.set(IUriIdentityService, new UriIdentityService(fileService)); // Request - const requestService = new RequestService(new NullLogger(), configurationService, environmentService, logService); + const requestService = new RequestService(configurationService, environmentService, logService); services.set(IRequestService, requestService); // Download Service diff --git a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts index 01353f1ceb8..be0cc5053a8 100644 --- a/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts +++ b/src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts @@ -78,14 +78,12 @@ export class NativeEditContext extends AbstractEditContext { if (standardKeyboardEvent.keyCode === KeyCode.KEY_IN_COMPOSITION) { standardKeyboardEvent.stopPropagation(); } - // Enter key presses are not sent as text update events, hence we need to handle them outside of the text update event - // The beforeinput and input events send `insertParagraph` and `insertLineBreak` events but only on input elements - // Hence we handle the enter key press in the keydown event - if (standardKeyboardEvent.keyCode === KeyCode.Enter) { + viewController.emitKeyDown(standardKeyboardEvent); + })); + this._register(addDisposableListener(this.domNode.domNode, 'beforeinput', async (e) => { + if (e.inputType === 'insertParagraph') { this._onType(viewController, { text: '\n', replacePrevCharCnt: 0, replaceNextCharCnt: 0, positionDelta: 0 }); } - - viewController.emitKeyDown(standardKeyboardEvent); })); // Edit context events @@ -94,7 +92,7 @@ export class NativeEditContext extends AbstractEditContext { this._register(editContextAddDisposableListener(this._editContext, 'textupdate', (e) => { const compositionRangeWithinEditor = this._compositionRangeWithinEditor; if (compositionRangeWithinEditor) { - const position = this._context.viewModel.getPrimaryCursorState().viewState.position; + const position = this._context.viewModel.getPrimaryCursorState().modelState.position; const newCompositionRangeWithinEditor = Range.fromPositions(compositionRangeWithinEditor.getStartPosition(), position); this._compositionRangeWithinEditor = newCompositionRangeWithinEditor; } @@ -105,7 +103,7 @@ export class NativeEditContext extends AbstractEditContext { this._screenReaderSupport.writeScreenReaderContent(); })); this._register(editContextAddDisposableListener(this._editContext, 'compositionstart', (e) => { - const position = this._context.viewModel.getPrimaryCursorState().viewState.position; + const position = this._context.viewModel.getPrimaryCursorState().modelState.position; const newCompositionRange = Range.fromPositions(position, position); this._compositionRangeWithinEditor = newCompositionRange; // Utlimately fires onDidCompositionStart() on the editor to notify for example suggest model of composition state @@ -262,12 +260,12 @@ export class NativeEditContext extends AbstractEditContext { if (i === this._primarySelection.endLineNumber) { selectionEndOffset += this._primarySelection.endColumn - 1; } else { - selectionEndOffset += this._context.viewModel.getLineMaxColumn(i); + selectionEndOffset += this._context.viewModel.model.getLineMaxColumn(i); } } - const endColumnOfEndLineNumber = this._context.viewModel.getLineMaxColumn(this._primarySelection.endLineNumber); + const endColumnOfEndLineNumber = this._context.viewModel.model.getLineMaxColumn(this._primarySelection.endLineNumber); const rangeOfText = new Range(this._primarySelection.startLineNumber, 1, this._primarySelection.endLineNumber, endColumnOfEndLineNumber); - const text = this._context.viewModel.getValueInRange(rangeOfText, EndOfLinePreference.TextDefined); + const text = this._context.viewModel.model.getValueInRange(rangeOfText, EndOfLinePreference.TextDefined); const textStartPositionWithinEditor = rangeOfText.getStartPosition(); return { text, diff --git a/src/vs/editor/browser/controller/editContext/native/screenReaderSupport.ts b/src/vs/editor/browser/controller/editContext/native/screenReaderSupport.ts index 1db00edef5f..f103d8172a2 100644 --- a/src/vs/editor/browser/controller/editContext/native/screenReaderSupport.ts +++ b/src/vs/editor/browser/controller/editContext/native/screenReaderSupport.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getActiveWindow } from '../../../../../base/browser/dom.js'; +import { getActiveWindow, isHTMLElement } from '../../../../../base/browser/dom.js'; import { FastDomNode } from '../../../../../base/browser/fastDomNode.js'; import { AccessibilitySupport } from '../../../../../platform/accessibility/common/accessibility.js'; import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; @@ -149,10 +149,14 @@ export class ScreenReaderSupport { if (!textContent) { return; } + const focusedElement = getActiveWindow().document.activeElement; const range = new globalThis.Range(); range.setStart(textContent, selectionOffsetStart); range.setEnd(textContent, selectionOffsetEnd); activeDocumentSelection.removeAllRanges(); activeDocumentSelection.addRange(range); + if (isHTMLElement(focusedElement)) { + focusedElement.focus(); + } } } diff --git a/src/vs/editor/browser/gpu/atlas/atlas.ts b/src/vs/editor/browser/gpu/atlas/atlas.ts new file mode 100644 index 00000000000..397924d9971 --- /dev/null +++ b/src/vs/editor/browser/gpu/atlas/atlas.ts @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { IBoundingBox, IRasterizedGlyph } from '../raster/raster.js'; + +/** + * Information about a {@link IRasterizedGlyph rasterized glyph} that has been drawn to a texture + * atlas page. + */ +export interface ITextureAtlasPageGlyph { + /** + * The page index of the texture atlas page that the glyph was drawn to. + */ + pageIndex: number; + /** + * The index of the glyph in the texture atlas page. + */ + glyphIndex: number; + /** The x coordinate of the glyph on the texture atlas page. */ + x: number; + /** The y coordinate of the glyph on the texture atlas page. */ + y: number; + /** The width of the glyph in pixels. */ + w: number; + /** The height of the glyph in pixels. */ + h: number; + /** The x offset from {@link x} of the glyph's origin. */ + originOffsetX: number; + /** The y offset from {@link y} of the glyph's origin. */ + originOffsetY: number; +} + +/** + * A texture atlas allocator is responsible for taking rasterized glyphs, drawing them to a texture + * atlas page canvas and return information on the texture atlas glyph. + */ +export interface ITextureAtlasAllocator { + /** + * Allocates a rasterized glyph to the canvas, drawing it and returning information on its + * position in the canvas. This will return undefined if the glyph does not fit on the canvas. + */ + allocate(rasterizedGlyph: Readonly): Readonly | undefined; + /** + * Gets a usage preview of the atlas for debugging purposes. + */ + getUsagePreview(): Promise; + /** + * Gets statistics about the allocator's current state for debugging purposes. + */ + getStats(): string; +} + +/** + * A texture atlas page that can be read from but not modified. + */ +export interface IReadableTextureAtlasPage { + /** + * A unique identifier for the current state of the texture atlas page. This is a number that + * increments whenever a glyph is drawn to the page. + */ + readonly version: number; + /** + * A bounding box representing the area of the texture atlas page that is currently in use. + */ + readonly usedArea: Readonly; + /** + * An iterator over all glyphs that have been drawn to the page. This will iterate through + * glyphs in the order they have been drawn. + */ + readonly glyphs: IterableIterator>; + /** + * The source canvas for the texture atlas page. + */ + readonly source: OffscreenCanvas; + /** + * Gets a usage preview of the atlas for debugging purposes. + */ + getUsagePreview(): Promise; + /** + * Gets statistics about the allocator's current state for debugging purposes. + */ + getStats(): string; +} + +export const enum UsagePreviewColors { + Unused = '#808080', + Used = '#4040FF', + Wasted = '#FF0000', + Restricted = '#FF000088', +} diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlas.ts b/src/vs/editor/browser/gpu/atlas/textureAtlas.ts new file mode 100644 index 00000000000..47583ab791b --- /dev/null +++ b/src/vs/editor/browser/gpu/atlas/textureAtlas.ts @@ -0,0 +1,155 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getActiveWindow } from '../../../../base/browser/dom.js'; +import { CharCode } from '../../../../base/common/charCode.js'; +import { Event } from '../../../../base/common/event.js'; +import { Disposable, dispose, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { TwoKeyMap } from '../../../../base/common/map.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IThemeService } from '../../../../platform/theme/common/themeService.js'; +import { MetadataConsts } from '../../../common/encodedTokenAttributes.js'; +import { GlyphRasterizer } from '../raster/glyphRasterizer.js'; +import type { IGlyphRasterizer } from '../raster/raster.js'; +import { IdleTaskQueue } from '../taskQueue.js'; +import type { IReadableTextureAtlasPage, ITextureAtlasPageGlyph } from './atlas.js'; +import { AllocatorType, TextureAtlasPage } from './textureAtlasPage.js'; + +export interface ITextureAtlasOptions { + allocatorType?: AllocatorType; +} + +export class TextureAtlas extends Disposable { + private _colorMap!: string[]; + private readonly _warmUpTask: MutableDisposable = this._register(new MutableDisposable()); + private readonly _warmedUpRasterizers = new Set(); + private readonly _allocatorType: AllocatorType; + + /** + * The main texture atlas pages which are both larger textures and more efficiently packed + * relative to the scratch page. The idea is the main pages are drawn to and uploaded to the GPU + * much less frequently so as to not drop frames. + */ + private readonly _pages: TextureAtlasPage[] = []; + get pages(): IReadableTextureAtlasPage[] { return this._pages; } + + readonly pageSize: number; + + /** + * A maps of glyph keys to the page to start searching for the glyph. This is set before + * searching to have as little runtime overhead (branching, intermediate variables) as possible, + * so it is not guaranteed to be the actual page the glyph is on. But it is guaranteed that all + * pages with a lower index do not contain the glyph. + */ + private readonly _glyphPageIndex: TwoKeyMap = new TwoKeyMap(); + + constructor( + /** The maximum texture size supported by the GPU. */ + private readonly _maxTextureSize: number, + options: ITextureAtlasOptions | undefined, + @IThemeService private readonly _themeService: IThemeService, + @IInstantiationService private readonly _instantiationService: IInstantiationService + ) { + super(); + + this._allocatorType = options?.allocatorType ?? 'slab'; + + this._register(Event.runAndSubscribe(this._themeService.onDidColorThemeChange, () => { + // TODO: Clear entire atlas on theme change + this._colorMap = this._themeService.getColorTheme().tokenColorMap; + })); + + const dprFactor = Math.max(1, Math.floor(getActiveWindow().devicePixelRatio)); + + this.pageSize = Math.min(1024 * dprFactor, this._maxTextureSize); + const firstPage = this._instantiationService.createInstance(TextureAtlasPage, 0, this.pageSize, this._allocatorType); + this._pages.push(firstPage); + + // IMPORTANT: The first glyph on the first page must be an empty glyph such that zeroed out + // cells end up rendering nothing + // TODO: This currently means the first slab is for 0x0 glyphs and is wasted + const nullRasterizer = new GlyphRasterizer(1, ''); + firstPage.getGlyph(nullRasterizer, '', 0); + nullRasterizer.dispose(); + + this._register(toDisposable(() => dispose(this._pages))); + } + + public getGlyph(rasterizer: IGlyphRasterizer, chars: string, metadata: number): Readonly { + // TODO: Encode font size and family into key + // Ignore metadata that doesn't affect the glyph + metadata &= ~(MetadataConsts.LANGUAGEID_MASK | MetadataConsts.TOKEN_TYPE_MASK | MetadataConsts.BALANCED_BRACKETS_MASK); + + // Warm up common glyphs + if (!this._warmedUpRasterizers.has(rasterizer.id)) { + this._warmUpAtlas(rasterizer); + this._warmedUpRasterizers.add(rasterizer.id); + } + + // Try get the glyph, overflowing to a new page if necessary + return this._tryGetGlyph(this._glyphPageIndex.get(chars, metadata) ?? 0, rasterizer, chars, metadata); + } + + private _tryGetGlyph(pageIndex: number, rasterizer: IGlyphRasterizer, chars: string, metadata: number): Readonly { + this._glyphPageIndex.set(chars, metadata, pageIndex); + return ( + this._pages[pageIndex].getGlyph(rasterizer, chars, metadata) + ?? (pageIndex + 1 < this._pages.length + ? this._tryGetGlyph(pageIndex + 1, rasterizer, chars, metadata) + : undefined) + ?? this._getGlyphFromNewPage(rasterizer, chars, metadata) + ); + } + + private _getGlyphFromNewPage(rasterizer: IGlyphRasterizer, chars: string, metadata: number): Readonly { + // TODO: Support more than 2 pages and the GPU texture layer limit + this._pages.push(this._instantiationService.createInstance(TextureAtlasPage, this._pages.length, this.pageSize, this._allocatorType)); + this._glyphPageIndex.set(chars, metadata, this._pages.length - 1); + return this._pages[this._pages.length - 1].getGlyph(rasterizer, chars, metadata)!; + } + + public getUsagePreview(): Promise { + return Promise.all(this._pages.map(e => e.getUsagePreview())); + } + + public getStats(): string[] { + return this._pages.map(e => e.getStats()); + } + + /** + * Warms up the atlas by rasterizing all printable ASCII characters for each token color. This + * is distrubuted over multiple idle callbacks to avoid blocking the main thread. + */ + private _warmUpAtlas(rasterizer: IGlyphRasterizer): void { + this._warmUpTask.value?.clear(); + const taskQueue = this._warmUpTask.value = new IdleTaskQueue(); + // Warm up using roughly the larger glyphs first to help optimize atlas allocation + // A-Z + for (let code = CharCode.A; code <= CharCode.Z; code++) { + taskQueue.enqueue(() => { + for (const fgColor of this._colorMap.keys()) { + this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK); + } + }); + } + // a-z + for (let code = CharCode.a; code <= CharCode.z; code++) { + taskQueue.enqueue(() => { + for (const fgColor of this._colorMap.keys()) { + this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK); + } + }); + } + // Remaining ascii + for (let code = CharCode.ExclamationMark; code <= CharCode.Tilde; code++) { + taskQueue.enqueue(() => { + for (const fgColor of this._colorMap.keys()) { + this.getGlyph(rasterizer, String.fromCharCode(code), (fgColor << MetadataConsts.FOREGROUND_OFFSET) & MetadataConsts.FOREGROUND_MASK); + } + }); + } + } +} + diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts b/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts new file mode 100644 index 00000000000..9d92a5a588c --- /dev/null +++ b/src/vs/editor/browser/gpu/atlas/textureAtlasPage.ts @@ -0,0 +1,122 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from '../../../../base/common/event.js'; +import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { TwoKeyMap } from '../../../../base/common/map.js'; +import { ILogService, LogLevel } from '../../../../platform/log/common/log.js'; +import { IThemeService } from '../../../../platform/theme/common/themeService.js'; +import type { IBoundingBox, IGlyphRasterizer } from '../raster/raster.js'; +import type { IReadableTextureAtlasPage, ITextureAtlasAllocator, ITextureAtlasPageGlyph } from './atlas.js'; +import { TextureAtlasShelfAllocator } from './textureAtlasShelfAllocator.js'; +import { TextureAtlasSlabAllocator } from './textureAtlasSlabAllocator.js'; + +export type AllocatorType = 'shelf' | 'slab' | ((canvas: OffscreenCanvas, textureIndex: number) => ITextureAtlasAllocator); + +export class TextureAtlasPage extends Disposable implements IReadableTextureAtlasPage { + + private _version: number = 0; + get version(): number { return this._version; } + + /** + * The maximum number of glyphs that can be drawn to the page. This is currently a hard static + * cap that must not be reached as it will cause the GPU buffer to overflow. + */ + static readonly maximumGlyphCount = 5_000; + + private _usedArea: IBoundingBox = { left: 0, top: 0, right: 0, bottom: 0 }; + public get usedArea(): Readonly { return this._usedArea; } + + private readonly _canvas: OffscreenCanvas; + get source(): OffscreenCanvas { return this._canvas; } + + private readonly _glyphMap: TwoKeyMap = new TwoKeyMap(); + private readonly _glyphInOrderSet: Set = new Set(); + get glyphs(): IterableIterator { + return this._glyphInOrderSet.values(); + } + + private readonly _allocator: ITextureAtlasAllocator; + private _colorMap!: string[]; + + constructor( + textureIndex: number, + pageSize: number, + allocatorType: AllocatorType, + @ILogService private readonly _logService: ILogService, + @IThemeService private readonly _themeService: IThemeService, + ) { + super(); + + this._canvas = new OffscreenCanvas(pageSize, pageSize); + + switch (allocatorType) { + case 'shelf': this._allocator = new TextureAtlasShelfAllocator(this._canvas, textureIndex); break; + case 'slab': this._allocator = new TextureAtlasSlabAllocator(this._canvas, textureIndex); break; + default: this._allocator = allocatorType(this._canvas, textureIndex); break; + } + + this._register(Event.runAndSubscribe(this._themeService.onDidColorThemeChange, () => { + // TODO: Clear entire atlas on theme change + this._colorMap = this._themeService.getColorTheme().tokenColorMap; + })); + + // Reduce impact of a memory leak if this object is not released + this._register(toDisposable(() => { + this._canvas.width = 1; + this._canvas.height = 1; + })); + } + + public getGlyph(rasterizer: IGlyphRasterizer, chars: string, metadata: number): Readonly | undefined { + // IMPORTANT: There are intentionally no intermediate variables here to aid in runtime + // optimization as it's a very hot function + return this._glyphMap.get(chars, metadata) ?? this._createGlyph(rasterizer, chars, metadata); + } + + private _createGlyph(rasterizer: IGlyphRasterizer, chars: string, metadata: number): Readonly | undefined { + // Ensure the glyph can fit on the page + if (this._glyphInOrderSet.size >= TextureAtlasPage.maximumGlyphCount) { + return undefined; + } + + // Rasterize and allocate the glyph + const rasterizedGlyph = rasterizer.rasterizeGlyph(chars, metadata, this._colorMap); + const glyph = this._allocator.allocate(rasterizedGlyph); + + // Ensure the glyph was allocated + if (glyph === undefined) { + return undefined; + } + + // Save the glyph + this._glyphMap.set(chars, metadata, glyph); + this._glyphInOrderSet.add(glyph); + + // Update page version and it's tracked used area + this._version++; + this._usedArea.right = Math.max(this._usedArea.right, glyph.x + glyph.w - 1); + this._usedArea.bottom = Math.max(this._usedArea.bottom, glyph.y + glyph.h - 1); + + if (this._logService.getLevel() === LogLevel.Trace) { + this._logService.trace('New glyph', { + chars, + metadata, + rasterizedGlyph, + glyph + }); + } + + return glyph; + } + + getUsagePreview(): Promise { + return this._allocator.getUsagePreview(); + } + + getStats(): string { + return this._allocator.getStats(); + } +} diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlasShelfAllocator.ts b/src/vs/editor/browser/gpu/atlas/textureAtlasShelfAllocator.ts new file mode 100644 index 00000000000..1bbf920997f --- /dev/null +++ b/src/vs/editor/browser/gpu/atlas/textureAtlasShelfAllocator.ts @@ -0,0 +1,162 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BugIndicatingError } from '../../../../base/common/errors.js'; +import { ensureNonNullable } from '../gpuUtils.js'; +import type { IRasterizedGlyph } from '../raster/raster.js'; +import { UsagePreviewColors, type ITextureAtlasAllocator, type ITextureAtlasPageGlyph } from './atlas.js'; + +/** + * The shelf allocator is a simple allocator that places glyphs in rows, starting a new row when the + * current row is full. Due to its simplicity, it can waste space but it is very fast. + */ +export class TextureAtlasShelfAllocator implements ITextureAtlasAllocator { + + private readonly _ctx: OffscreenCanvasRenderingContext2D; + + private _currentRow: ITextureAtlasShelf = { + x: 0, + y: 0, + h: 0 + }; + + /** A set of all glyphs allocated, this is only tracked to enable debug related functionality */ + private readonly _allocatedGlyphs: Set> = new Set(); + + private _nextIndex = 0; + + constructor( + private readonly _canvas: OffscreenCanvas, + private readonly _textureIndex: number, + ) { + this._ctx = ensureNonNullable(this._canvas.getContext('2d', { + willReadFrequently: true + })); + } + + public allocate(rasterizedGlyph: IRasterizedGlyph): ITextureAtlasPageGlyph | undefined { + // The glyph does not fit into the atlas page + const glyphWidth = rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1; + const glyphHeight = rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + 1; + if (glyphWidth > this._canvas.width || glyphHeight > this._canvas.height) { + throw new BugIndicatingError('Glyph is too large for the atlas page'); + } + + // Finalize and increment row if it doesn't fix horizontally + if (rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1 > this._canvas.width - this._currentRow.x) { + this._currentRow.x = 0; + this._currentRow.y += this._currentRow.h; + this._currentRow.h = 1; + } + + // Return undefined if there isn't any room left + if (this._currentRow.y + rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + 1 > this._canvas.height) { + return undefined; + } + + // Draw glyph + this._ctx.drawImage( + rasterizedGlyph.source, + // source + rasterizedGlyph.boundingBox.left, + rasterizedGlyph.boundingBox.top, + glyphWidth, + glyphHeight, + // destination + this._currentRow.x, + this._currentRow.y, + glyphWidth, + glyphHeight + ); + + // Create glyph object + const glyph: ITextureAtlasPageGlyph = { + pageIndex: this._textureIndex, + glyphIndex: this._nextIndex++, + x: this._currentRow.x, + y: this._currentRow.y, + w: glyphWidth, + h: glyphHeight, + originOffsetX: rasterizedGlyph.originOffset.x, + originOffsetY: rasterizedGlyph.originOffset.y + }; + + // Shift current row + this._currentRow.x += glyphWidth; + this._currentRow.h = Math.max(this._currentRow.h, glyphHeight); + + // Set the glyph + this._allocatedGlyphs.add(glyph); + + return glyph; + } + + public getUsagePreview(): Promise { + const w = this._canvas.width; + const h = this._canvas.height; + const canvas = new OffscreenCanvas(w, h); + const ctx = ensureNonNullable(canvas.getContext('2d')); + ctx.fillStyle = UsagePreviewColors.Unused; + ctx.fillRect(0, 0, w, h); + + const rowHeight: Map = new Map(); // y -> h + const rowWidth: Map = new Map(); // y -> w + for (const g of this._allocatedGlyphs) { + rowHeight.set(g.y, Math.max(rowHeight.get(g.y) ?? 0, g.h)); + rowWidth.set(g.y, Math.max(rowWidth.get(g.y) ?? 0, g.x + g.w)); + } + for (const g of this._allocatedGlyphs) { + ctx.fillStyle = UsagePreviewColors.Used; + ctx.fillRect(g.x, g.y, g.w, g.h); + ctx.fillStyle = UsagePreviewColors.Wasted; + ctx.fillRect(g.x, g.y + g.h, g.w, rowHeight.get(g.y)! - g.h); + } + for (const [rowY, rowW] of rowWidth.entries()) { + if (rowY !== this._currentRow.y) { + ctx.fillStyle = UsagePreviewColors.Wasted; + ctx.fillRect(rowW, rowY, w - rowW, rowHeight.get(rowY)!); + } + } + return canvas.convertToBlob(); + } + + getStats(): string { + const w = this._canvas.width; + const h = this._canvas.height; + + let usedPixels = 0; + let wastedPixels = 0; + const totalPixels = w * h; + + const rowHeight: Map = new Map(); // y -> h + const rowWidth: Map = new Map(); // y -> w + for (const g of this._allocatedGlyphs) { + rowHeight.set(g.y, Math.max(rowHeight.get(g.y) ?? 0, g.h)); + rowWidth.set(g.y, Math.max(rowWidth.get(g.y) ?? 0, g.x + g.w)); + } + for (const g of this._allocatedGlyphs) { + usedPixels += g.w * g.h; + wastedPixels += g.w * (rowHeight.get(g.y)! - g.h); + } + for (const [rowY, rowW] of rowWidth.entries()) { + if (rowY !== this._currentRow.y) { + wastedPixels += (w - rowW) * rowHeight.get(rowY)!; + } + } + return [ + `page${this._textureIndex}:`, + ` Total: ${totalPixels} (${w}x${h})`, + ` Used: ${usedPixels} (${((usedPixels / totalPixels) * 100).toPrecision(2)}%)`, + ` Wasted: ${wastedPixels} (${((wastedPixels / totalPixels) * 100).toPrecision(2)}%)`, + `Efficiency: ${((usedPixels / (usedPixels + wastedPixels)) * 100).toPrecision(2)}%`, + ].join('\n'); + } +} + +interface ITextureAtlasShelf { + x: number; + y: number; + h: number; +} diff --git a/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts b/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts new file mode 100644 index 00000000000..b41fed6978c --- /dev/null +++ b/src/vs/editor/browser/gpu/atlas/textureAtlasSlabAllocator.ts @@ -0,0 +1,433 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getActiveWindow } from '../../../../base/browser/dom.js'; +import { BugIndicatingError } from '../../../../base/common/errors.js'; +import { TwoKeyMap } from '../../../../base/common/map.js'; +import { ensureNonNullable } from '../gpuUtils.js'; +import type { IRasterizedGlyph } from '../raster/raster.js'; +import { UsagePreviewColors, type ITextureAtlasAllocator, type ITextureAtlasPageGlyph } from './atlas.js'; + +export interface TextureAtlasSlabAllocatorOptions { + slabW?: number; + slabH?: number; +} + +/** + * The slab allocator is a more complex allocator that places glyphs in square slabs of a fixed + * size. Slabs are defined by a small range of glyphs sizes they can house, this places like-sized + * glyphs in the same slab which reduces wasted space. + * + * Slabs also may contain "unused" regions on the left and bottom depending on the size of the + * glyphs they include. This space is used to place very thin or short glyphs, which would otherwise + * waste a lot of space in their own slab. + */ +export class TextureAtlasSlabAllocator implements ITextureAtlasAllocator { + + private readonly _ctx: OffscreenCanvasRenderingContext2D; + + private readonly _slabs: ITextureAtlasSlab[] = []; + private readonly _activeSlabsByDims: TwoKeyMap = new TwoKeyMap(); + + private readonly _unusedRects: ITextureAtlasSlabUnusedRect[] = []; + + private readonly _openRegionsByHeight: Map = new Map(); + private readonly _openRegionsByWidth: Map = new Map(); + + /** A set of all glyphs allocated, this is only tracked to enable debug related functionality */ + private readonly _allocatedGlyphs: Set> = new Set(); + + private _slabW: number; + private _slabH: number; + private _slabsPerRow: number; + private _slabsPerColumn: number; + private _nextIndex = 0; + + constructor( + private readonly _canvas: OffscreenCanvas, + private readonly _textureIndex: number, + options?: TextureAtlasSlabAllocatorOptions + ) { + this._ctx = ensureNonNullable(this._canvas.getContext('2d', { + willReadFrequently: true + })); + + this._slabW = Math.min( + options?.slabW ?? (64 << (Math.floor(getActiveWindow().devicePixelRatio) - 1)), + this._canvas.width + ); + this._slabH = Math.min( + options?.slabH ?? this._slabW, + this._canvas.height + ); + this._slabsPerRow = Math.floor(this._canvas.width / this._slabW); + this._slabsPerColumn = Math.floor(this._canvas.height / this._slabH); + } + + public allocate(rasterizedGlyph: IRasterizedGlyph): ITextureAtlasPageGlyph | undefined { + // Find ideal slab, creating it if there is none suitable + const glyphWidth = rasterizedGlyph.boundingBox.right - rasterizedGlyph.boundingBox.left + 1; + const glyphHeight = rasterizedGlyph.boundingBox.bottom - rasterizedGlyph.boundingBox.top + 1; + + // The glyph does not fit into the atlas page, glyphs should never be this large in practice + if (glyphWidth > this._canvas.width || glyphHeight > this._canvas.height) { + throw new BugIndicatingError('Glyph is too large for the atlas page'); + } + + // The glyph does not fit into a slab + if (glyphWidth > this._slabW || glyphHeight > this._slabH) { + // Only if this is the allocator's first glyph, resize the slab size to fit the glyph. + if (this._allocatedGlyphs.size > 0) { + return undefined; + } + // Find the largest power of 2 devisor that the glyph fits into, this ensure there is no + // wasted space outside the allocated slabs. + let sizeCandidate = this._canvas.width; + while (glyphWidth < sizeCandidate / 2 && glyphHeight < sizeCandidate / 2) { + sizeCandidate /= 2; + } + this._slabW = sizeCandidate; + this._slabH = sizeCandidate; + this._slabsPerRow = Math.floor(this._canvas.width / this._slabW); + this._slabsPerColumn = Math.floor(this._canvas.height / this._slabH); + } + + // const dpr = getActiveWindow().devicePixelRatio; + + // TODO: Include font size as well as DPR in nearestXPixels calculation + + // Round slab glyph dimensions to the nearest x pixels, where x scaled with device pixel ratio + // const nearestXPixels = Math.max(1, Math.floor(dpr / 0.5)); + // const nearestXPixels = Math.max(1, Math.floor(dpr)); + const desiredSlabSize = { + // Nearest square number + // TODO: This can probably be optimized + // w: 1 << Math.ceil(Math.sqrt(glyphWidth)), + // h: 1 << Math.ceil(Math.sqrt(glyphHeight)), + + // Nearest x px + // w: Math.ceil(glyphWidth / nearestXPixels) * nearestXPixels, + // h: Math.ceil(glyphHeight / nearestXPixels) * nearestXPixels, + + // Round odd numbers up + // w: glyphWidth % 0 === 1 ? glyphWidth + 1 : glyphWidth, + // h: glyphHeight % 0 === 1 ? glyphHeight + 1 : glyphHeight, + + // Exact number only + w: glyphWidth, + h: glyphHeight, + }; + + // Get any existing slab + let slab = this._activeSlabsByDims.get(desiredSlabSize.w, desiredSlabSize.h); + + // Check if the slab is full + if (slab) { + const glyphsPerSlab = Math.floor(this._slabW / slab.entryW) * Math.floor(this._slabH / slab.entryH); + if (slab.count >= glyphsPerSlab) { + slab = undefined; + } + } + + let dx: number | undefined; + let dy: number | undefined; + + // Search for suitable space in unused rectangles + if (!slab) { + // Only check availability for the smallest side + if (glyphWidth < glyphHeight) { + const openRegions = this._openRegionsByWidth.get(glyphWidth); + if (openRegions?.length) { + // TODO: Don't search everything? + // Search from the end so we can typically pop it off the stack + for (let i = openRegions.length - 1; i >= 0; i--) { + const r = openRegions[i]; + if (r.w >= glyphWidth && r.h >= glyphHeight) { + dx = r.x; + dy = r.y; + if (glyphWidth < r.w) { + this._unusedRects.push({ + x: r.x + glyphWidth, + y: r.y, + w: r.w - glyphWidth, + h: glyphHeight + }); + } + r.y += glyphHeight; + r.h -= glyphHeight; + if (r.h === 0) { + if (i === openRegions.length - 1) { + openRegions.pop(); + } else { + this._unusedRects.splice(i, 1); + } + } + break; + } + } + } + } else { + const openRegions = this._openRegionsByHeight.get(glyphHeight); + if (openRegions?.length) { + // TODO: Don't search everything? + // Search from the end so we can typically pop it off the stack + for (let i = openRegions.length - 1; i >= 0; i--) { + const r = openRegions[i]; + if (r.w >= glyphWidth && r.h >= glyphHeight) { + dx = r.x; + dy = r.y; + if (glyphHeight < r.h) { + this._unusedRects.push({ + x: r.x, + y: r.y + glyphHeight, + w: glyphWidth, + h: r.h - glyphHeight + }); + } + r.x += glyphWidth; + r.w -= glyphWidth; + if (r.h === 0) { + if (i === openRegions.length - 1) { + openRegions.pop(); + } else { + this._unusedRects.splice(i, 1); + } + } + break; + } + } + } + } + } + + // Create a new slab + if (dx === undefined || dy === undefined) { + if (!slab) { + if (this._slabs.length >= this._slabsPerRow * this._slabsPerColumn) { + return undefined; + } + + slab = { + x: Math.floor(this._slabs.length % this._slabsPerRow) * this._slabW, + y: Math.floor(this._slabs.length / this._slabsPerRow) * this._slabH, + entryW: desiredSlabSize.w, + entryH: desiredSlabSize.h, + count: 0 + }; + // Track unused regions to use for small glyphs + // +-------------+----+ + // | | | + // | | | <- Unused W region + // | | | + // |-------------+----+ + // | | <- Unused H region + // +------------------+ + const unusedW = this._slabW % slab.entryW; + const unusedH = this._slabH % slab.entryH; + if (unusedW) { + addEntryToMapArray(this._openRegionsByWidth, unusedW, { + x: slab.x + this._slabW - unusedW, + w: unusedW, + y: slab.y, + h: this._slabH - (unusedH ?? 0) + }); + } + if (unusedH) { + addEntryToMapArray(this._openRegionsByHeight, unusedH, { + x: slab.x, + w: this._slabW, + y: slab.y + this._slabH - unusedH, + h: unusedH + }); + } + this._slabs.push(slab); + this._activeSlabsByDims.set(desiredSlabSize.w, desiredSlabSize.h, slab); + } + + const glyphsPerRow = Math.floor(this._slabW / slab.entryW); + dx = slab.x + Math.floor(slab.count % glyphsPerRow) * slab.entryW; + dy = slab.y + Math.floor(slab.count / glyphsPerRow) * slab.entryH; + + // Shift current row + slab.count++; + } + + // Draw glyph + this._ctx.drawImage( + rasterizedGlyph.source, + // source + rasterizedGlyph.boundingBox.left, + rasterizedGlyph.boundingBox.top, + glyphWidth, + glyphHeight, + // destination + dx, + dy, + glyphWidth, + glyphHeight + ); + + // Create glyph object + const glyph: ITextureAtlasPageGlyph = { + pageIndex: this._textureIndex, + glyphIndex: this._nextIndex++, + x: dx, + y: dy, + w: glyphWidth, + h: glyphHeight, + originOffsetX: rasterizedGlyph.originOffset.x, + originOffsetY: rasterizedGlyph.originOffset.y + }; + + // Set the glyph + this._allocatedGlyphs.add(glyph); + + return glyph; + } + + public getUsagePreview(): Promise { + const w = this._canvas.width; + const h = this._canvas.height; + const canvas = new OffscreenCanvas(w, h); + const ctx = ensureNonNullable(canvas.getContext('2d')); + + ctx.fillStyle = UsagePreviewColors.Unused; + ctx.fillRect(0, 0, w, h); + + let slabEntryPixels = 0; + let usedPixels = 0; + let slabEdgePixels = 0; + let restrictedPixels = 0; + const slabW = 64 << (Math.floor(getActiveWindow().devicePixelRatio) - 1); + const slabH = slabW; + + // Draw wasted underneath glyphs first + for (const slab of this._slabs) { + let x = 0; + let y = 0; + for (let i = 0; i < slab.count; i++) { + if (x + slab.entryW > slabW) { + x = 0; + y += slab.entryH; + } + ctx.fillStyle = UsagePreviewColors.Wasted; + ctx.fillRect(slab.x + x, slab.y + y, slab.entryW, slab.entryH); + + slabEntryPixels += slab.entryW * slab.entryH; + x += slab.entryW; + } + const entriesPerRow = Math.floor(slabW / slab.entryW); + const entriesPerCol = Math.floor(slabH / slab.entryH); + const thisSlabPixels = slab.entryW * entriesPerRow * slab.entryH * entriesPerCol; + slabEdgePixels += (slabW * slabH) - thisSlabPixels; + } + + // Draw glyphs + for (const g of this._allocatedGlyphs) { + usedPixels += g.w * g.h; + ctx.fillStyle = UsagePreviewColors.Used; + ctx.fillRect(g.x, g.y, g.w, g.h); + } + + // Draw unused space on side + const unusedRegions = Array.from(this._openRegionsByWidth.values()).flat().concat(Array.from(this._openRegionsByHeight.values()).flat()); + for (const r of unusedRegions) { + ctx.fillStyle = UsagePreviewColors.Restricted; + ctx.fillRect(r.x, r.y, r.w, r.h); + restrictedPixels += r.w * r.h; + } + + + // Overlay actual glyphs on top + ctx.globalAlpha = 0.5; + ctx.drawImage(this._canvas, 0, 0); + ctx.globalAlpha = 1; + + return canvas.convertToBlob(); + } + + public getStats(): string { + const w = this._canvas.width; + const h = this._canvas.height; + + let slabEntryPixels = 0; + let usedPixels = 0; + let slabEdgePixels = 0; + let wastedPixels = 0; + let restrictedPixels = 0; + const totalPixels = w * h; + const slabW = 64 << (Math.floor(getActiveWindow().devicePixelRatio) - 1); + const slabH = slabW; + + // Draw wasted underneath glyphs first + for (const slab of this._slabs) { + let x = 0; + let y = 0; + for (let i = 0; i < slab.count; i++) { + if (x + slab.entryW > slabW) { + x = 0; + y += slab.entryH; + } + slabEntryPixels += slab.entryW * slab.entryH; + x += slab.entryW; + } + const entriesPerRow = Math.floor(slabW / slab.entryW); + const entriesPerCol = Math.floor(slabH / slab.entryH); + const thisSlabPixels = slab.entryW * entriesPerRow * slab.entryH * entriesPerCol; + slabEdgePixels += (slabW * slabH) - thisSlabPixels; + } + + // Draw glyphs + for (const g of this._allocatedGlyphs) { + usedPixels += g.w * g.h; + } + + // Draw unused space on side + const unusedRegions = Array.from(this._openRegionsByWidth.values()).flat().concat(Array.from(this._openRegionsByHeight.values()).flat()); + for (const r of unusedRegions) { + restrictedPixels += r.w * r.h; + } + + const edgeUsedPixels = slabEdgePixels - restrictedPixels; + wastedPixels = slabEntryPixels - (usedPixels - edgeUsedPixels); + + // usedPixels += slabEdgePixels - restrictedPixels; + const efficiency = usedPixels / (usedPixels + wastedPixels + restrictedPixels); + + return [ + `page[${this._textureIndex}]:`, + ` Total: ${totalPixels}px (${w}x${h})`, + ` Used: ${usedPixels}px (${((usedPixels / totalPixels) * 100).toFixed(2)}%)`, + ` Wasted: ${wastedPixels}px (${((wastedPixels / totalPixels) * 100).toFixed(2)}%)`, + `Restricted: ${restrictedPixels}px (${((restrictedPixels / totalPixels) * 100).toFixed(2)}%) (hard to allocate)`, + `Efficiency: ${efficiency === 1 ? '100' : (efficiency * 100).toFixed(2)}%`, + ` Slabs: ${this._slabs.length} of ${Math.floor(this._canvas.width / slabW) * Math.floor(this._canvas.height / slabH)}` + ].join('\n'); + } +} + +interface ITextureAtlasSlab { + x: number; + y: number; + entryH: number; + entryW: number; + count: number; +} + +interface ITextureAtlasSlabUnusedRect { + x: number; + y: number; + w: number; + h: number; +} + +function addEntryToMapArray(map: Map, key: K, entry: V) { + let list = map.get(key); + if (!list) { + list = []; + map.set(key, list); + } + list.push(entry); +} diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts new file mode 100644 index 00000000000..2771004b423 --- /dev/null +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.ts @@ -0,0 +1,284 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getActiveWindow } from '../../../base/browser/dom.js'; +import { BugIndicatingError } from '../../../base/common/errors.js'; +import { Disposable } from '../../../base/common/lifecycle.js'; +import { EditorOption } from '../../common/config/editorOptions.js'; +import type { IViewLineTokens } from '../../common/tokens/lineTokens.js'; +import type { ViewportData } from '../../common/viewLayout/viewLinesViewportData.js'; +import type { ViewLineRenderingData } from '../../common/viewModel.js'; +import type { ViewContext } from '../../common/viewModel/viewContext.js'; +import type { ViewLineOptions } from '../viewParts/lines/viewLineOptions.js'; +import type { ITextureAtlasPageGlyph } from './atlas/atlas.js'; +import type { TextureAtlas } from './atlas/textureAtlas.js'; +import { fullFileRenderStrategyWgsl } from './fullFileRenderStrategy.wgsl.js'; +import { BindingId, type IGpuRenderStrategy } from './gpu.js'; +import { GPULifecycle } from './gpuDisposable.js'; +import { quadVertices } from './gpuUtils.js'; +import { GlyphRasterizer } from './raster/glyphRasterizer.js'; + + +const enum Constants { + IndicesPerCell = 6, +} + +const enum CellBufferInfo { + FloatsPerEntry = 6, + BytesPerEntry = CellBufferInfo.FloatsPerEntry * 4, + Offset_X = 0, + Offset_Y = 1, + Offset_Unused1 = 2, + Offset_Unused2 = 3, + GlyphIndex = 4, + TextureIndex = 5, +} + +export class FullFileRenderStrategy extends Disposable implements IGpuRenderStrategy { + + private static _lineCount = 3000; + private static _columnCount = 200; + + readonly wgsl: string = fullFileRenderStrategyWgsl; + + private readonly _glyphRasterizer: GlyphRasterizer; + + private _cellBindBuffer!: GPUBuffer; + + /** + * The cell value buffers, these hold the cells and their glyphs. It's double buffers such that + * the thread doesn't block when one is being uploaded to the GPU. + */ + private _cellValueBuffers!: [ArrayBuffer, ArrayBuffer]; + private _activeDoubleBufferIndex: 0 | 1 = 0; + + private readonly _upToDateLines: [Set, Set] = [new Set(), new Set()]; + private _visibleObjectCount: number = 0; + + private _scrollOffsetBindBuffer!: GPUBuffer; + private _scrollOffsetValueBuffers!: [Float32Array, Float32Array]; + + get bindGroupEntries(): GPUBindGroupEntry[] { + return [ + { binding: BindingId.Cells, resource: { buffer: this._cellBindBuffer } }, + { binding: BindingId.ScrollOffset, resource: { buffer: this._scrollOffsetBindBuffer } } + ]; + } + + constructor( + private readonly _context: ViewContext, + private readonly _device: GPUDevice, + private readonly _canvas: HTMLCanvasElement, + private readonly _atlas: TextureAtlas, + ) { + super(); + + // TODO: Detect when lines have been tokenized and clear _upToDateLines + const activeWindow = getActiveWindow(); + const fontFamily = this._context.configuration.options.get(EditorOption.fontFamily); + const fontSize = Math.ceil(this._context.configuration.options.get(EditorOption.fontSize) * activeWindow.devicePixelRatio); + + this._glyphRasterizer = this._register(new GlyphRasterizer(fontSize, fontFamily)); + + const bufferSize = FullFileRenderStrategy._lineCount * FullFileRenderStrategy._columnCount * Constants.IndicesPerCell * Float32Array.BYTES_PER_ELEMENT; + this._cellBindBuffer = this._register(GPULifecycle.createBuffer(this._device, { + label: 'Monaco full file cell buffer', + size: bufferSize, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + })).object; + this._cellValueBuffers = [ + new ArrayBuffer(bufferSize), + new ArrayBuffer(bufferSize), + ]; + + const scrollOffsetBufferSize = 2; + this._scrollOffsetBindBuffer = this._register(GPULifecycle.createBuffer(this._device, { + label: 'Monaco scroll offset buffer', + size: scrollOffsetBufferSize * Float32Array.BYTES_PER_ELEMENT, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + })).object; + this._scrollOffsetValueBuffers = [ + new Float32Array(scrollOffsetBufferSize), + new Float32Array(scrollOffsetBufferSize), + ]; + } + + update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): number { + // Pre-allocate variables to be shared within the loop - don't trust the JIT compiler to do + // this optimization to avoid additional blocking time in garbage collector + let chars = ''; + let y = 0; + let x = 0; + let screenAbsoluteX = 0; + let screenAbsoluteY = 0; + let zeroToOneX = 0; + let zeroToOneY = 0; + let wgslX = 0; + let wgslY = 0; + let xOffset = 0; + let glyph: Readonly; + let cellIndex = 0; + + let tokenStartIndex = 0; + let tokenEndIndex = 0; + let tokenMetadata = 0; + + let lineData: ViewLineRenderingData; + let content: string = ''; + let fillStartIndex = 0; + let fillEndIndex = 0; + + let tokens: IViewLineTokens; + + const activeWindow = getActiveWindow(); + + // Update scroll offset + const scrollTop = this._context.viewLayout.getCurrentScrollTop() * activeWindow.devicePixelRatio; + const scrollOffsetBuffer = this._scrollOffsetValueBuffers[this._activeDoubleBufferIndex]; + scrollOffsetBuffer[1] = scrollTop; + this._device.queue.writeBuffer(this._scrollOffsetBindBuffer, 0, scrollOffsetBuffer); + + // Update cell data + const cellBuffer = new Float32Array(this._cellValueBuffers[this._activeDoubleBufferIndex]); + const lineIndexCount = FullFileRenderStrategy._columnCount * Constants.IndicesPerCell; + + const upToDateLines = this._upToDateLines[this._activeDoubleBufferIndex]; + let dirtyLineStart = Number.MAX_SAFE_INTEGER; + let dirtyLineEnd = 0; + + for (y = viewportData.startLineNumber; y <= viewportData.endLineNumber; y++) { + // TODO: Update on dirty lines; is this known by line before rendering? + // if (upToDateLines.has(y)) { + // continue; + // } + dirtyLineStart = Math.min(dirtyLineStart, y); + dirtyLineEnd = Math.max(dirtyLineEnd, y); + + lineData = viewportData.getViewLineRenderingData(y); + content = lineData.content; + xOffset = 0; + + // See ViewLine#renderLine + // const renderLineInput = new RenderLineInput( + // options.useMonospaceOptimizations, + // options.canUseHalfwidthRightwardsArrow, + // lineData.content, + // lineData.continuesWithWrappedLine, + // lineData.isBasicASCII, + // lineData.containsRTL, + // lineData.minColumn - 1, + // lineData.tokens, + // actualInlineDecorations, + // lineData.tabSize, + // lineData.startVisibleColumn, + // options.spaceWidth, + // options.middotWidth, + // options.wsmiddotWidth, + // options.stopRenderingLineAfter, + // options.renderWhitespace, + // options.renderControlCharacters, + // options.fontLigatures !== EditorFontLigatures.OFF, + // selectionsOnLine + // ); + + tokens = lineData.tokens; + tokenStartIndex = lineData.minColumn - 1; + tokenEndIndex = 0; + for (let tokenIndex = 0, tokensLen = tokens.getCount(); tokenIndex < tokensLen; tokenIndex++) { + tokenEndIndex = tokens.getEndOffset(tokenIndex); + if (tokenEndIndex <= tokenStartIndex) { + // The faux indent part of the line should have no token type + continue; + } + + + tokenMetadata = tokens.getMetadata(tokenIndex); + + // console.log(`token: start=${tokenStartIndex}, end=${tokenEndIndex}, fg=${colorMap[tokenFg]}`); + + + for (x = tokenStartIndex; x < tokenEndIndex; x++) { + // HACK: Prevent rendering past the end of the render buffer + // TODO: This needs to move to a dynamic long line rendering strategy + if (x > FullFileRenderStrategy._columnCount) { + break; + } + chars = content.charAt(x); + if (chars === ' ') { + continue; + } + if (chars === '\t') { + // TODO: Pull actual tab size + xOffset += 3; + continue; + } + + glyph = this._atlas.getGlyph(this._glyphRasterizer, chars, tokenMetadata); + + // TODO: Support non-standard character widths + screenAbsoluteX = Math.round((x + xOffset) * viewLineOptions.spaceWidth * activeWindow.devicePixelRatio); + screenAbsoluteY = ( + Math.ceil(( + // Top of line including line height + viewportData.relativeVerticalOffset[y - viewportData.startLineNumber] + + // Delta to top of line after line height + Math.floor((viewportData.lineHeight - this._context.configuration.options.get(EditorOption.fontSize)) / 2) + ) * activeWindow.devicePixelRatio) + ); + zeroToOneX = screenAbsoluteX / this._canvas.width; + zeroToOneY = screenAbsoluteY / this._canvas.height; + wgslX = zeroToOneX * 2 - 1; + wgslY = zeroToOneY * 2 - 1; + + cellIndex = ((y - 1) * FullFileRenderStrategy._columnCount + (x + xOffset)) * Constants.IndicesPerCell; + cellBuffer[cellIndex + CellBufferInfo.Offset_X] = wgslX; + cellBuffer[cellIndex + CellBufferInfo.Offset_Y] = -wgslY; + cellBuffer[cellIndex + CellBufferInfo.GlyphIndex] = glyph.glyphIndex; + cellBuffer[cellIndex + CellBufferInfo.TextureIndex] = glyph.pageIndex; + } + + tokenStartIndex = tokenEndIndex; + } + + // Clear to end of line + fillStartIndex = ((y - 1) * FullFileRenderStrategy._columnCount + (tokenEndIndex + xOffset)) * Constants.IndicesPerCell; + fillEndIndex = (y * FullFileRenderStrategy._columnCount) * Constants.IndicesPerCell; + cellBuffer.fill(0, fillStartIndex, fillEndIndex); + + upToDateLines.add(y); + } + + const visibleObjectCount = (viewportData.endLineNumber - viewportData.startLineNumber + 1) * lineIndexCount; + + // Only write when there is changed data + if (dirtyLineStart <= dirtyLineEnd) { + // Write buffer and swap it out to unblock writes + this._device.queue.writeBuffer( + this._cellBindBuffer, + (dirtyLineStart - 1) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT, + cellBuffer.buffer, + (dirtyLineStart - 1) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT, + (dirtyLineEnd - dirtyLineStart + 1) * lineIndexCount * Float32Array.BYTES_PER_ELEMENT + ); + } + + this._activeDoubleBufferIndex = this._activeDoubleBufferIndex ? 0 : 1; + + this._visibleObjectCount = visibleObjectCount; + return visibleObjectCount; + } + + draw(pass: GPURenderPassEncoder, viewportData: ViewportData): void { + if (this._visibleObjectCount <= 0) { + throw new BugIndicatingError('Attempt to draw 0 objects'); + } + pass.draw( + quadVertices.length / 2, + this._visibleObjectCount, + undefined, + (viewportData.startLineNumber - 1) * FullFileRenderStrategy._columnCount + ); + } +} diff --git a/src/vs/editor/browser/gpu/fullFileRenderStrategy.wgsl.ts b/src/vs/editor/browser/gpu/fullFileRenderStrategy.wgsl.ts new file mode 100644 index 00000000000..dbf10934c47 --- /dev/null +++ b/src/vs/editor/browser/gpu/fullFileRenderStrategy.wgsl.ts @@ -0,0 +1,94 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BindingId } from './gpu.js'; + +export const fullFileRenderStrategyWgsl = /*wgsl*/ ` +struct GlyphInfo { + position: vec2f, + size: vec2f, + origin: vec2f, +}; + +struct Vertex { + @location(0) position: vec2f, +}; + +struct Cell { + position: vec2f, + unused1: vec2f, + glyphIndex: f32, + textureIndex: f32 +}; + +struct LayoutInfo { + canvasDims: vec2f, + viewportOffset: vec2f, + viewportDims: vec2f, +} + +struct ScrollOffset { + offset: vec2f +} + +struct VSOutput { + @builtin(position) position: vec4f, + @location(1) layerIndex: f32, + @location(0) texcoord: vec2f, +}; + +// Uniforms +@group(0) @binding(${BindingId.ViewportUniform}) var layoutInfo: LayoutInfo; +@group(0) @binding(${BindingId.AtlasDimensionsUniform}) var atlasDims: vec2f; +@group(0) @binding(${BindingId.ScrollOffset}) var scrollOffset: ScrollOffset; + +// Storage buffers +@group(0) @binding(${BindingId.GlyphInfo0}) var glyphInfo0: array; +@group(0) @binding(${BindingId.GlyphInfo1}) var glyphInfo1: array; +@group(0) @binding(${BindingId.Cells}) var cells: array; + +@vertex fn vs( + vert: Vertex, + @builtin(instance_index) instanceIndex: u32, + @builtin(vertex_index) vertexIndex : u32 +) -> VSOutput { + let cell = cells[instanceIndex]; + // TODO: Is there a nicer way to init this? + var glyph = glyphInfo0[0]; + let glyphIndex = u32(cell.glyphIndex); + if (u32(cell.textureIndex) == 0) { + glyph = glyphInfo0[glyphIndex]; + } else { + glyph = glyphInfo1[glyphIndex]; + } + + var vsOut: VSOutput; + // Multiple vert.position by 2,-2 to get it into clipspace which ranged from -1 to 1 + vsOut.position = vec4f( + (((vert.position * vec2f(2, -2)) / layoutInfo.canvasDims)) * glyph.size + cell.position + ((glyph.origin * vec2f(2, -2)) / layoutInfo.canvasDims) + (((scrollOffset.offset + layoutInfo.viewportOffset) * 2) / layoutInfo.canvasDims), + 0.0, + 1.0 + ); + + vsOut.layerIndex = cell.textureIndex; + // Textures are flipped from natural direction on the y-axis, so flip it back + vsOut.texcoord = vert.position; + vsOut.texcoord = ( + // Glyph offset (0-1) + (glyph.position / atlasDims) + + // Glyph coordinate (0-1) + (vsOut.texcoord * (glyph.size / atlasDims)) + ); + + return vsOut; +} + +@group(0) @binding(${BindingId.TextureSampler}) var ourSampler: sampler; +@group(0) @binding(${BindingId.Texture}) var ourTexture: texture_2d_array; + +@fragment fn fs(vsOut: VSOutput) -> @location(0) vec4f { + return textureSample(ourTexture, ourSampler, vsOut.texcoord, u32(vsOut.layerIndex)); +} +`; diff --git a/src/vs/editor/browser/gpu/gpu.ts b/src/vs/editor/browser/gpu/gpu.ts new file mode 100644 index 00000000000..c0a5fc6845c --- /dev/null +++ b/src/vs/editor/browser/gpu/gpu.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { ViewportData } from '../../common/viewLayout/viewLinesViewportData.js'; +import type { ViewLineOptions } from '../viewParts/lines/viewLineOptions.js'; + +export const enum BindingId { + GlyphInfo0, + GlyphInfo1, + Cells, + TextureSampler, + Texture, + ViewportUniform, + AtlasDimensionsUniform, + ScrollOffset, +} + +export interface IGpuRenderStrategy { + readonly wgsl: string; + readonly bindGroupEntries: GPUBindGroupEntry[]; + + update(viewportData: ViewportData, viewLineOptions: ViewLineOptions): number; + draw?(pass: GPURenderPassEncoder, viewportData: ViewportData): void; +} diff --git a/src/vs/editor/browser/gpu/gpuDisposable.ts b/src/vs/editor/browser/gpu/gpuDisposable.ts new file mode 100644 index 00000000000..7837dbb9934 --- /dev/null +++ b/src/vs/editor/browser/gpu/gpuDisposable.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { IReference } from '../../../base/common/lifecycle.js'; +import { isFunction } from '../../../base/common/types.js'; + +export namespace GPULifecycle { + export async function requestDevice(): Promise> { + if (!navigator.gpu) { + throw new Error('This browser does not support WebGPU'); + } + const adapter = (await navigator.gpu.requestAdapter())!; + if (!adapter) { + throw new Error('This browser supports WebGPU but it appears to be disabled'); + } + return wrapDestroyableInDisposable(await adapter.requestDevice()); + } + + export function createBuffer(device: GPUDevice, descriptor: GPUBufferDescriptor, initialValues?: Float32Array | (() => Float32Array)): IReference { + const buffer = device.createBuffer(descriptor); + if (initialValues) { + device.queue.writeBuffer(buffer, 0, isFunction(initialValues) ? initialValues() : initialValues); + } + return wrapDestroyableInDisposable(buffer); + } + + export function createTexture(device: GPUDevice, descriptor: GPUTextureDescriptor): IReference { + return wrapDestroyableInDisposable(device.createTexture(descriptor)); + } +} + +function wrapDestroyableInDisposable(value: T): IReference { + return { + object: value, + dispose: () => value.destroy() + }; +} diff --git a/src/vs/editor/browser/gpu/gpuUtils.ts b/src/vs/editor/browser/gpu/gpuUtils.ts new file mode 100644 index 00000000000..6ced420acc2 --- /dev/null +++ b/src/vs/editor/browser/gpu/gpuUtils.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BugIndicatingError } from '../../../base/common/errors.js'; +import { toDisposable, type IDisposable } from '../../../base/common/lifecycle.js'; + +export const quadVertices = new Float32Array([ + 1, 0, + 1, 1, + 0, 1, + 0, 0, + 0, 1, + 1, 0, +]); + +export function ensureNonNullable(value: T | null): T { + if (!value) { + throw new Error(`Value "${value}" cannot be null`); + } + return value; +} + +// TODO: Move capabilities into ElementSizeObserver? +export function observeDevicePixelDimensions(element: HTMLElement, parentWindow: Window & typeof globalThis, callback: (deviceWidth: number, deviceHeight: number) => void): IDisposable { + // Observe any resizes to the element and extract the actual pixel size of the element if the + // devicePixelContentBoxSize API is supported. This allows correcting rounding errors when + // converting between CSS pixels and device pixels which causes blurry rendering when device + // pixel ratio is not a round number. + let observer: ResizeObserver | undefined = new parentWindow.ResizeObserver((entries) => { + const entry = entries.find((entry) => entry.target === element); + if (!entry) { + return; + } + + // Disconnect if devicePixelContentBoxSize isn't supported by the browser + if (!('devicePixelContentBoxSize' in entry)) { + observer?.disconnect(); + observer = undefined; + return; + } + + // Fire the callback, ignore events where the dimensions are 0x0 as the canvas is likely hidden + const width = entry.devicePixelContentBoxSize[0].inlineSize; + const height = entry.devicePixelContentBoxSize[0].blockSize; + if (width > 0 && height > 0) { + callback(width, height); + } + }); + try { + observer.observe(element, { box: ['device-pixel-content-box'] } as any); + } catch { + observer.disconnect(); + observer = undefined; + throw new BugIndicatingError('Could not observe device pixel dimensions'); + } + return toDisposable(() => observer?.disconnect()); +} diff --git a/src/vs/editor/browser/gpu/objectCollectionBuffer.ts b/src/vs/editor/browser/gpu/objectCollectionBuffer.ts new file mode 100644 index 00000000000..947c04172a2 --- /dev/null +++ b/src/vs/editor/browser/gpu/objectCollectionBuffer.ts @@ -0,0 +1,162 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from '../../../base/common/event.js'; +import { Disposable, dispose, toDisposable, type IDisposable } from '../../../base/common/lifecycle.js'; +import { LinkedList } from '../../../base/common/linkedList.js'; + +export interface ObjectCollectionBufferPropertySpec { + name: string; +} + +export type ObjectCollectionPropertyValues = { + [K in T[number]['name']]: number; +}; + +export interface IObjectCollectionBuffer extends IDisposable { + /** + * The underlying buffer. This **should not** be modified externally. + */ + readonly buffer: ArrayBuffer; + /** + * A view of the underlying buffer. This **should not** be modified externally. + */ + readonly view: Float32Array; + /** + * The size of the used portion of the buffer (in bytes). + */ + readonly bufferUsedSize: number; + /** + * The size of the used portion of the view (in float32s). + */ + readonly viewUsedSize: number; + + /** + * Fires when the buffer is modified. + */ + readonly onDidChange: Event; + + /** + * Creates an entry in the collection. This will return a managed object that can be modified + * which will update the underlying buffer. + * @param data The data of the entry. + */ + createEntry(data: ObjectCollectionPropertyValues): IObjectCollectionBufferEntry; +} + +/** + * An entry in an {@link ObjectCollectionBuffer}. Property values on the entry can be changed and + * their values will be updated automatically in the buffer. + */ +export interface IObjectCollectionBufferEntry extends IDisposable { + set(propertyName: T[number]['name'], value: number): void; + get(propertyName: T[number]['name']): number; +} + +export function createObjectCollectionBuffer( + propertySpecs: T, + capacity: number +): IObjectCollectionBuffer { + return new ObjectCollectionBuffer(propertySpecs, capacity); +} + +class ObjectCollectionBuffer extends Disposable implements IObjectCollectionBuffer { + buffer: ArrayBuffer; + view: Float32Array; + + get bufferUsedSize() { + return this.viewUsedSize * Float32Array.BYTES_PER_ELEMENT; + } + get viewUsedSize() { + return this._entries.size * this._entrySize; + } + + private readonly _propertySpecsMap: Map = new Map(); + private readonly _entrySize: number; + private readonly _entries: LinkedList> = new LinkedList(); + + private readonly _onDidChange = this._register(new Emitter()); + readonly onDidChange = this._onDidChange.event; + + constructor( + public propertySpecs: T, + public capacity: number + ) { + super(); + + this.view = new Float32Array(capacity * 2); + this.buffer = this.view.buffer; + this._entrySize = propertySpecs.length; + for (let i = 0; i < propertySpecs.length; i++) { + const spec = { + offset: i, + ...propertySpecs[i] + }; + this._propertySpecsMap.set(spec.name, spec); + } + this._register(toDisposable(() => dispose(this._entries))); + } + + createEntry(data: ObjectCollectionPropertyValues): IObjectCollectionBufferEntry { + if (this._entries.size === this.capacity) { + throw new Error(`Cannot create more entries ObjectCollectionBuffer entries (capacity=${this.capacity})`); + } + + const value = new ObjectCollectionBufferEntry(this.view, this._propertySpecsMap, this._entries.size, data); + const removeFromEntries = this._entries.push(value); + const listeners: IDisposable[] = []; + listeners.push(Event.forward(value.onDidChange, this._onDidChange)); + listeners.push(value.onWillDispose(() => { + const deletedEntryIndex = value.i; + removeFromEntries(); + + // Shift all entries after the deleted entry to the left + this.view.set(this.view.subarray(deletedEntryIndex * this._entrySize + 2, this._entries.size * this._entrySize + 2), deletedEntryIndex * this._entrySize); + + // Update entries to reflect the new i + for (const entry of this._entries) { + if (entry.i > deletedEntryIndex) { + entry.i--; + } + } + dispose(listeners); + })); + return value; + } +} + +class ObjectCollectionBufferEntry extends Disposable implements IObjectCollectionBufferEntry { + + private readonly _onDidChange = this._register(new Emitter()); + readonly onDidChange = this._onDidChange.event; + private readonly _onWillDispose = this._register(new Emitter()); + readonly onWillDispose = this._onWillDispose.event; + + constructor( + private _view: Float32Array, + private _propertySpecsMap: Map, + public i: number, + data: ObjectCollectionPropertyValues, + ) { + super(); + for (const propertySpec of this._propertySpecsMap.values()) { + this._view[this.i * this._propertySpecsMap.size + propertySpec.offset] = data[propertySpec.name as keyof typeof data]; + } + } + + override dispose() { + this._onWillDispose.fire(); + super.dispose(); + } + + set(propertyName: T[number]['name'], value: number): void { + this._view[this.i * this._propertySpecsMap.size + this._propertySpecsMap.get(propertyName)!.offset] = value; + this._onDidChange.fire(); + } + + get(propertyName: T[number]['name']): number { + return this._view[this.i * this._propertySpecsMap.size + this._propertySpecsMap.get(propertyName)!.offset]; + } +} diff --git a/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts new file mode 100644 index 00000000000..4d33abcd8f6 --- /dev/null +++ b/src/vs/editor/browser/gpu/raster/glyphRasterizer.ts @@ -0,0 +1,225 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { StringBuilder } from '../../../common/core/stringBuilder.js'; +import { FontStyle, TokenMetadata } from '../../../common/encodedTokenAttributes.js'; +import { ensureNonNullable } from '../gpuUtils.js'; +import type { IBoundingBox, IGlyphRasterizer, IRasterizedGlyph } from './raster.js'; + +let nextId = 0; + +export class GlyphRasterizer extends Disposable implements IGlyphRasterizer { + public readonly id = nextId++; + + private _canvas: OffscreenCanvas; + private _ctx: OffscreenCanvasRenderingContext2D; + + private _workGlyph: IRasterizedGlyph = { + source: null!, + boundingBox: { + left: 0, + bottom: 0, + right: 0, + top: 0, + }, + originOffset: { + x: 0, + y: 0, + } + }; + private _workGlyphConfig: { chars: string | undefined; metadata: number } = { chars: undefined, metadata: 0 }; + + constructor( + private readonly _fontSize: number, + private readonly _fontFamily: string, + ) { + super(); + + this._canvas = new OffscreenCanvas(this._fontSize * 3, this._fontSize * 3); + this._ctx = ensureNonNullable(this._canvas.getContext('2d', { + willReadFrequently: true + })); + this._ctx.textBaseline = 'top'; + this._ctx.fillStyle = '#FFFFFF'; + } + + // TODO: Support drawing multiple fonts and sizes + /** + * Rasterizes a glyph. Note that the returned object is reused across different glyphs and + * therefore is only safe for synchronous access. + */ + public rasterizeGlyph( + chars: string, + metadata: number, + colorMap: string[], + ): Readonly { + if (chars === '') { + return { + source: this._canvas, + boundingBox: { top: 0, left: 0, bottom: -1, right: -1 }, + originOffset: { x: 0, y: 0 } + }; + } + // Check if the last glyph matches the config, reuse if so. This helps avoid unnecessary + // work when the rasterizer is called multiple times like when the glyph doesn't fit into a + // page. + if (this._workGlyphConfig.chars === chars && this._workGlyphConfig.metadata === metadata) { + return this._workGlyph; + } + this._workGlyphConfig.chars = chars; + this._workGlyphConfig.metadata = metadata; + return this._rasterizeGlyph(chars, metadata, colorMap); + } + + public _rasterizeGlyph( + chars: string, + metadata: number, + colorMap: string[], + ): Readonly { + // TODO: Support workbench.fontAliasing + this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height); + + const fontSb = new StringBuilder(200); + const fontStyle = TokenMetadata.getFontStyle(metadata); + if (fontStyle & FontStyle.Italic) { + fontSb.appendString('italic '); + } + if (fontStyle & FontStyle.Bold) { + fontSb.appendString('bold '); + } + fontSb.appendString(`${this._fontSize}px ${this._fontFamily}`); + this._ctx.font = fontSb.build(); + + // TODO: Support FontStyle.Strikethrough and FontStyle.Underline text decorations, these + // need to be drawn manually to the canvas. See xterm.js for "dodging" the text for + // underlines. + + const originX = this._fontSize; + const originY = this._fontSize; + this._ctx.fillStyle = colorMap[TokenMetadata.getForeground(metadata)]; + // TODO: This might actually be slower + // const textMetrics = this._ctx.measureText(chars); + this._ctx.fillText(chars, originX, originY); + + const imageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height); + this._findGlyphBoundingBox(imageData, this._workGlyph.boundingBox); + // const offset = { + // x: textMetrics.actualBoundingBoxLeft, + // y: textMetrics.actualBoundingBoxAscent + // }; + // const size = { + // w: textMetrics.actualBoundingBoxRight + textMetrics.actualBoundingBoxLeft, + // y: textMetrics.actualBoundingBoxDescent + textMetrics.actualBoundingBoxAscent, + // wInt: Math.ceil(textMetrics.actualBoundingBoxRight + textMetrics.actualBoundingBoxLeft), + // yInt: Math.ceil(textMetrics.actualBoundingBoxDescent + textMetrics.actualBoundingBoxAscent), + // }; + // console.log(`${chars}_${fg}`, textMetrics, boundingBox, originX, originY, { width: boundingBox.right - boundingBox.left, height: boundingBox.bottom - boundingBox.top }); + this._workGlyph.source = this._canvas; + this._workGlyph.originOffset.x = this._workGlyph.boundingBox.left - originX; + this._workGlyph.originOffset.y = this._workGlyph.boundingBox.top - originY; + + // const result2: IRasterizedGlyph = { + // source: this._canvas, + // boundingBox: { + // left: Math.floor(originX - textMetrics.actualBoundingBoxLeft), + // right: Math.ceil(originX + textMetrics.actualBoundingBoxRight), + // top: Math.floor(originY - textMetrics.actualBoundingBoxAscent), + // bottom: Math.ceil(originY + textMetrics.actualBoundingBoxDescent), + // }, + // originOffset: { + // x: Math.floor(boundingBox.left - originX), + // y: Math.floor(boundingBox.top - originY) + // } + // }; + + // TODO: Verify result 1 and 2 are the same + + // if (result2.boundingBox.left > result.boundingBox.left) { + // debugger; + // } + // if (result2.boundingBox.top > result.boundingBox.top) { + // debugger; + // } + // if (result2.boundingBox.right < result.boundingBox.right) { + // debugger; + // } + // if (result2.boundingBox.bottom < result.boundingBox.bottom) { + // debugger; + // } + // if (JSON.stringify(result2.originOffset) !== JSON.stringify(result.originOffset)) { + // debugger; + // } + + + + return this._workGlyph; + } + + // TODO: Does this even need to happen when measure text is used? + private _findGlyphBoundingBox(imageData: ImageData, outBoundingBox: IBoundingBox) { + const height = this._canvas.height; + const width = this._canvas.width; + let found = false; + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const alphaOffset = y * width * 4 + x * 4 + 3; + if (imageData.data[alphaOffset] !== 0) { + outBoundingBox.top = y; + found = true; + break; + } + } + if (found) { + break; + } + } + outBoundingBox.left = 0; + found = false; + for (let x = 0; x < width; x++) { + for (let y = 0; y < height; y++) { + const alphaOffset = y * width * 4 + x * 4 + 3; + if (imageData.data[alphaOffset] !== 0) { + outBoundingBox.left = x; + found = true; + break; + } + } + if (found) { + break; + } + } + outBoundingBox.right = width; + found = false; + for (let x = width - 1; x >= outBoundingBox.left; x--) { + for (let y = 0; y < height; y++) { + const alphaOffset = y * width * 4 + x * 4 + 3; + if (imageData.data[alphaOffset] !== 0) { + outBoundingBox.right = x; + found = true; + break; + } + } + if (found) { + break; + } + } + outBoundingBox.bottom = outBoundingBox.top; + found = false; + for (let y = height - 1; y >= 0; y--) { + for (let x = 0; x < width; x++) { + const alphaOffset = y * width * 4 + x * 4 + 3; + if (imageData.data[alphaOffset] !== 0) { + outBoundingBox.bottom = y; + found = true; + break; + } + } + if (found) { + break; + } + } + } +} diff --git a/src/vs/editor/browser/gpu/raster/raster.ts b/src/vs/editor/browser/gpu/raster/raster.ts new file mode 100644 index 00000000000..1a7c5d1add6 --- /dev/null +++ b/src/vs/editor/browser/gpu/raster/raster.ts @@ -0,0 +1,59 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { MetadataConsts } from '../../../common/encodedTokenAttributes.js'; + +export interface IGlyphRasterizer { + /** + * A unique identifier for the rasterizer. + */ + id: number; + + /** + * Rasterizes a glyph. + * @param chars The character(s) to rasterize. This can be a single character, a ligature, an + * emoji, etc. + * @param metadata The metadata of the glyph to rasterize. See {@link MetadataConsts} for how + * this works. + * @param colorMap A theme's color map. + */ + rasterizeGlyph( + chars: string, + metadata: number, + colorMap: string[], + ): Readonly; +} + +/** + * A simple bounding box in a 2D plane. + */ +export interface IBoundingBox { + /** The left x coordinate (inclusive). */ + left: number; + /** The top y coordinate (inclusive). */ + top: number; + /** The right x coordinate (inclusive). */ + right: number; + /** The bottom y coordinate (inclusive). */ + bottom: number; +} + +/** + * A glyph that has been rasterized to a canvas. + */ +export interface IRasterizedGlyph { + /** + * The source canvas the glyph was rasterized to. + */ + source: OffscreenCanvas; + /** + * The bounding box of the glyph within {@link source}. + */ + boundingBox: IBoundingBox; + /** + * The offset to the glyph's origin (where it should be drawn to). + */ + originOffset: { x: number; y: number }; +} diff --git a/src/vs/editor/browser/gpu/taskQueue.ts b/src/vs/editor/browser/gpu/taskQueue.ts new file mode 100644 index 00000000000..27d64d01fe2 --- /dev/null +++ b/src/vs/editor/browser/gpu/taskQueue.ts @@ -0,0 +1,172 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getActiveWindow } from '../../../base/browser/dom.js'; +import { Disposable, toDisposable } from '../../../base/common/lifecycle.js'; + +/** + * Copyright (c) 2022 The xterm.js authors. All rights reserved. + * @license MIT + */ + +interface ITaskQueue { + /** + * Adds a task to the queue which will run in a future idle callback. + * To avoid perceivable stalls on the mainthread, tasks with heavy workload + * should split their work into smaller pieces and return `true` to get + * called again until the work is done (on falsy return value). + */ + enqueue(task: () => boolean | void): void; + + /** + * Flushes the queue, running all remaining tasks synchronously. + */ + flush(): void; + + /** + * Clears any remaining tasks from the queue, these will not be run. + */ + clear(): void; +} + +interface ITaskDeadline { + timeRemaining(): number; +} +type CallbackWithDeadline = (deadline: ITaskDeadline) => void; + +abstract class TaskQueue extends Disposable implements ITaskQueue { + private _tasks: (() => boolean | void)[] = []; + private _idleCallback?: number; + private _i = 0; + + constructor() { + super(); + this._register(toDisposable(() => this.clear())); + } + + protected abstract _requestCallback(callback: CallbackWithDeadline): number; + protected abstract _cancelCallback(identifier: number): void; + + public enqueue(task: () => boolean | void): void { + this._tasks.push(task); + this._start(); + } + + public flush(): void { + while (this._i < this._tasks.length) { + if (!this._tasks[this._i]()) { + this._i++; + } + } + this.clear(); + } + + public clear(): void { + if (this._idleCallback) { + this._cancelCallback(this._idleCallback); + this._idleCallback = undefined; + } + this._i = 0; + this._tasks.length = 0; + } + + private _start(): void { + if (!this._idleCallback) { + this._idleCallback = this._requestCallback(this._process.bind(this)); + } + } + + private _process(deadline: ITaskDeadline): void { + this._idleCallback = undefined; + let taskDuration = 0; + let longestTask = 0; + let lastDeadlineRemaining = deadline.timeRemaining(); + let deadlineRemaining = 0; + while (this._i < this._tasks.length) { + taskDuration = Date.now(); + if (!this._tasks[this._i]()) { + this._i++; + } + // other than performance.now, Date.now might not be stable (changes on wall clock changes), + // this is not an issue here as a clock change during a short running task is very unlikely + // in case it still happened and leads to negative duration, simply assume 1 msec + taskDuration = Math.max(1, Date.now() - taskDuration); + longestTask = Math.max(taskDuration, longestTask); + // Guess the following task will take a similar time to the longest task in this batch, allow + // additional room to try avoid exceeding the deadline + deadlineRemaining = deadline.timeRemaining(); + if (longestTask * 1.5 > deadlineRemaining) { + // Warn when the time exceeding the deadline is over 20ms, if this happens in practice the + // task should be split into sub-tasks to ensure the UI remains responsive. + if (lastDeadlineRemaining - taskDuration < -20) { + console.warn(`task queue exceeded allotted deadline by ${Math.abs(Math.round(lastDeadlineRemaining - taskDuration))}ms`); + } + this._start(); + return; + } + lastDeadlineRemaining = deadlineRemaining; + } + this.clear(); + } +} + +/** + * A queue of that runs tasks over several tasks via setTimeout, trying to maintain above 60 frames + * per second. The tasks will run in the order they are enqueued, but they will run some time later, + * and care should be taken to ensure they're non-urgent and will not introduce race conditions. + */ +export class PriorityTaskQueue extends TaskQueue { + protected _requestCallback(callback: CallbackWithDeadline): number { + return getActiveWindow().setTimeout(() => callback(this._createDeadline(16))); + } + + protected _cancelCallback(identifier: number): void { + getActiveWindow().clearTimeout(identifier); + } + + private _createDeadline(duration: number): ITaskDeadline { + const end = Date.now() + duration; + return { + timeRemaining: () => Math.max(0, end - Date.now()) + }; + } +} + +/** + * A queue of that runs tasks over several idle callbacks, trying to respect the idle callback's + * deadline given by the environment. The tasks will run in the order they are enqueued, but they + * will run some time later, and care should be taken to ensure they're non-urgent and will not + * introduce race conditions. + */ +export class IdleTaskQueue extends TaskQueue { + protected _requestCallback(callback: IdleRequestCallback): number { + return getActiveWindow().requestIdleCallback(callback); + } + + protected _cancelCallback(identifier: number): void { + getActiveWindow().cancelIdleCallback(identifier); + } +} + +/** + * An object that tracks a single debounced task that will run on the next idle frame. When called + * multiple times, only the last set task will run. + */ +export class DebouncedIdleTask { + private _queue: ITaskQueue; + + constructor() { + this._queue = new IdleTaskQueue(); + } + + public set(task: () => boolean | void): void { + this._queue.clear(); + this._queue.enqueue(task); + } + + public flush(): void { + this._queue.flush(); + } +} diff --git a/src/vs/editor/browser/gpu/viewGpuContext.ts b/src/vs/editor/browser/gpu/viewGpuContext.ts new file mode 100644 index 00000000000..cfd877e46f2 --- /dev/null +++ b/src/vs/editor/browser/gpu/viewGpuContext.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getActiveWindow } from '../../../base/browser/dom.js'; +import { createFastDomNode, type FastDomNode } from '../../../base/browser/fastDomNode.js'; +import { Emitter } from '../../../base/common/event.js'; +import { Disposable } from '../../../base/common/lifecycle.js'; +import { GPULifecycle } from './gpuDisposable.js'; +import { ensureNonNullable, observeDevicePixelDimensions } from './gpuUtils.js'; + +export class ViewGpuContext extends Disposable { + readonly canvas: FastDomNode; + readonly ctx: GPUCanvasContext; + + readonly device: Promise; + + private readonly _onDidChangeCanvasDevicePixelDimensions = this._register(new Emitter<{ width: number; height: number }>()); + readonly onDidChangeCanvasDevicePixelDimensions = this._onDidChangeCanvasDevicePixelDimensions.event; + + constructor() { + super(); + + this.canvas = createFastDomNode(document.createElement('canvas')); + this.canvas.setClassName('editorCanvas'); + + this.ctx = ensureNonNullable(this.canvas.domNode.getContext('webgpu')); + + this.device = GPULifecycle.requestDevice().then(ref => this._register(ref).object); + + this._register(observeDevicePixelDimensions(this.canvas.domNode, getActiveWindow(), (width, height) => { + this.canvas.domNode.width = width; + this.canvas.domNode.height = height; + this._onDidChangeCanvasDevicePixelDimensions.fire({ width, height }); + })); + } +} diff --git a/src/vs/editor/browser/observableCodeEditor.ts b/src/vs/editor/browser/observableCodeEditor.ts index 868f6bca9aa..9641c87228b 100644 --- a/src/vs/editor/browser/observableCodeEditor.ts +++ b/src/vs/editor/browser/observableCodeEditor.ts @@ -5,16 +5,14 @@ import { equalsIfDefined, itemsEquals } from '../../base/common/equals.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../base/common/lifecycle.js'; -import { IObservable, ITransaction, autorun, autorunOpts, derived, derivedOpts, observableFromEvent, observableSignal, observableValue, observableValueOpts } from '../../base/common/observable.js'; -import { TransactionImpl } from '../../base/common/observableInternal/base.js'; -import { derivedWithSetter } from '../../base/common/observableInternal/derived.js'; -import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from './editorBrowser.js'; +import { IObservable, ITransaction, TransactionImpl, autorun, autorunOpts, derived, derivedOpts, derivedWithSetter, observableFromEvent, observableSignal, observableValue, observableValueOpts } from '../../base/common/observable.js'; import { EditorOption, FindComputedEditorOptionValueById } from '../common/config/editorOptions.js'; import { Position } from '../common/core/position.js'; import { Selection } from '../common/core/selection.js'; import { ICursorSelectionChangedEvent } from '../common/cursorEvents.js'; import { IModelDeltaDecoration, ITextModel } from '../common/model.js'; import { IModelContentChangedEvent } from '../common/textModelEvents.js'; +import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from './editorBrowser.js'; /** * Returns a facade for the code editor that provides observables for various states/events. diff --git a/src/vs/editor/browser/services/treeSitter/treeSitterParserService.ts b/src/vs/editor/browser/services/treeSitter/treeSitterParserService.ts index e6d3695efac..26a61c41c1a 100644 --- a/src/vs/editor/browser/services/treeSitter/treeSitterParserService.ts +++ b/src/vs/editor/browser/services/treeSitter/treeSitterParserService.ts @@ -21,7 +21,7 @@ import { CancellationToken, cancelOnDispose } from '../../../../base/common/canc import { IEnvironmentService } from '../../../../platform/environment/common/environment.js'; import { canASAR } from '../../../../base/common/amd.js'; import { CancellationError, isCancellationError } from '../../../../base/common/errors.js'; -import { PromiseResult } from '../../../../base/common/observableInternal/promise.js'; +import { PromiseResult } from '../../../../base/common/observable.js'; const EDITOR_TREESITTER_TELEMETRY = 'editor.experimental.treeSitterTelemetry'; const MODULE_LOCATION_SUBPATH = `@vscode/tree-sitter-wasm/wasm`; diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index 13a63145c34..ab059a2b47d 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -55,6 +55,8 @@ import { IViewModel } from '../common/viewModel.js'; import { ViewContext } from '../common/viewModel/viewContext.js'; import { IInstantiationService } from '../../platform/instantiation/common/instantiation.js'; import { IColorTheme, getThemeTypeSelector } from '../../platform/theme/common/themeService.js'; +import { ViewGpuContext } from './gpu/viewGpuContext.js'; +import { ViewLinesGpu } from './viewParts/linesGpu/viewLinesGpu.js'; import { AbstractEditContext } from './controller/editContext/editContextUtils.js'; import { IVisibleRangeProvider, TextAreaEditContext } from './controller/editContext/textArea/textAreaEditContext.js'; import { NativeEditContext } from './controller/editContext/native/nativeEditContext.js'; @@ -79,10 +81,12 @@ export class View extends ViewEventHandler { private readonly _scrollbar: EditorScrollbar; private readonly _context: ViewContext; + private readonly _viewGpuContext?: ViewGpuContext; private _selections: Selection[]; // The view lines private readonly _viewLines: ViewLines; + private readonly _viewLinesGpu?: ViewLinesGpu; // These are parts, but we must do some API related calls on them, so we keep a reference private readonly _viewZones: ViewZones; @@ -91,7 +95,9 @@ export class View extends ViewEventHandler { private readonly _glyphMarginWidgets: GlyphMarginWidgets; private readonly _viewCursors: ViewCursors; private readonly _viewParts: ViewPart[]; + private readonly _viewController: ViewController; + private _experimentalEditContextEnabled: boolean; private _editContext: AbstractEditContext; private readonly _pointerHandler: PointerHandler; @@ -117,7 +123,7 @@ export class View extends ViewEventHandler { this._selections = [new Selection(1, 1, 1, 1)]; this._renderAnimationFrame = null; - const viewController = new ViewController(configuration, model, userInputEvents, commandDelegate); + this._viewController = new ViewController(configuration, model, userInputEvents, commandDelegate); // The view context is passed on to most classes (basically to reduce param. counts in ctors) this._context = new ViewContext(configuration, colorTheme, model); @@ -128,10 +134,8 @@ export class View extends ViewEventHandler { this._viewParts = []; // Keyboard handler - const editContextEnabled = this._context.configuration.options.get(EditorOption.experimentalEditContextEnabled); - this._editContext = editContextEnabled - ? this._instantiationService.createInstance(NativeEditContext, this._context, viewController) - : this._instantiationService.createInstance(TextAreaEditContext, this._context, viewController, this._createTextAreaHandlerHelper()); + this._experimentalEditContextEnabled = this._context.configuration.options.get(EditorOption.experimentalEditContextEnabled); + this._editContext = this._instantiateEditContext(this._experimentalEditContextEnabled); this._viewParts.push(this._editContext); @@ -145,6 +149,10 @@ export class View extends ViewEventHandler { // Set role 'code' for better screen reader support https://github.com/microsoft/vscode/issues/93438 this.domNode.setAttribute('role', 'code'); + if (this._context.configuration.options.get(EditorOption.experimentalGpuAcceleration) === 'on') { + this._viewGpuContext = new ViewGpuContext(); + } + this._overflowGuardContainer = createFastDomNode(document.createElement('div')); PartFingerprints.write(this._overflowGuardContainer, PartFingerprint.OverflowGuard); this._overflowGuardContainer.setClassName('overflow-guard'); @@ -154,6 +162,9 @@ export class View extends ViewEventHandler { // View Lines this._viewLines = new ViewLines(this._context, this._linesContent); + if (this._viewGpuContext) { + this._viewLinesGpu = this._instantiationService.createInstance(ViewLinesGpu, this._context, this._viewGpuContext); + } // View Zones this._viewZones = new ViewZones(this._context); @@ -227,6 +238,9 @@ export class View extends ViewEventHandler { this._linesContent.appendChild(this._viewCursors.getDomNode()); this._overflowGuardContainer.appendChild(margin.getDomNode()); this._overflowGuardContainer.appendChild(this._scrollbar.getDomNode()); + if (this._viewGpuContext) { + this._overflowGuardContainer.appendChild(this._viewGpuContext.canvas); + } this._overflowGuardContainer.appendChild(scrollDecoration.getDomNode()); this._editContext.appendTo(this._overflowGuardContainer); this._overflowGuardContainer.appendChild(this._overlayWidgets.getDomNode()); @@ -245,7 +259,29 @@ export class View extends ViewEventHandler { this._applyLayout(); // Pointer handler - this._pointerHandler = this._register(new PointerHandler(this._context, viewController, this._createPointerHandlerHelper())); + this._pointerHandler = this._register(new PointerHandler(this._context, this._viewController, this._createPointerHandlerHelper())); + } + + private _instantiateEditContext(experimentalEditContextEnabled: boolean): AbstractEditContext { + return experimentalEditContextEnabled + ? this._instantiationService.createInstance(NativeEditContext, this._context, this._viewController) + : this._instantiationService.createInstance(TextAreaEditContext, this._context, this._viewController, this._createTextAreaHandlerHelper()); + } + + private _updateEditContext(): void { + const experimentalEditContextEnabled = this._context.configuration.options.get(EditorOption.experimentalEditContextEnabled); + if (this._experimentalEditContextEnabled === experimentalEditContextEnabled) { + return; + } + this._experimentalEditContextEnabled = experimentalEditContextEnabled; + this._editContext.dispose(); + this._editContext = this._instantiateEditContext(experimentalEditContextEnabled); + this._editContext.appendTo(this._overflowGuardContainer); + // Replace the view parts with the new edit context + const indexOfEditContextHandler = this._viewParts.indexOf(this._editContext); + if (indexOfEditContextHandler !== -1) { + this._viewParts.splice(indexOfEditContextHandler, 1, this._editContext); + } } private _computeGlyphMarginLanes(): IGlyphMarginLanesModel { @@ -361,6 +397,7 @@ export class View extends ViewEventHandler { } public override onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { this.domNode.setClassName(this._getEditorClassName()); + this._updateEditContext(); this._applyLayout(); return false; } @@ -395,8 +432,10 @@ export class View extends ViewEventHandler { this._contentWidgets.overflowingContentWidgetsDomNode.domNode.remove(); this._context.removeEventHandler(this); + this._viewGpuContext?.dispose(); this._viewLines.dispose(); + this._viewLinesGpu?.dispose(); // Destroy view parts for (const viewPart of this._viewParts) { @@ -510,6 +549,11 @@ export class View extends ViewEventHandler { viewPartsToRender = this._getViewPartsToRender(); } + if (this._viewLinesGpu?.shouldRender()) { + this._viewLinesGpu.renderText(viewportData); + this._viewLinesGpu.onDidRender(); + } + return [viewPartsToRender, new RenderingContext(this._context.viewLayout, viewportData, this._viewLines)]; }, prepareRender: (viewPartsToRender: ViewPart[], ctx: RenderingContext) => { diff --git a/src/vs/editor/browser/view/viewOverlays.ts b/src/vs/editor/browser/view/viewOverlays.ts index 71b922442be..6b8e10f341c 100644 --- a/src/vs/editor/browser/view/viewOverlays.ts +++ b/src/vs/editor/browser/view/viewOverlays.ts @@ -24,7 +24,7 @@ export class ViewOverlays extends ViewPart { constructor(context: ViewContext) { super(context); - this._visibleLines = new VisibleLinesCollection({ + this._visibleLines = new VisibleLinesCollection({ createLine: () => new ViewOverlayLine(this._dynamicOverlays) }); this.domNode = this._visibleLines.domNode; diff --git a/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts b/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts index 6e99b4c1f90..8d627025769 100644 --- a/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts +++ b/src/vs/editor/browser/viewParts/currentLineHighlight/currentLineHighlight.ts @@ -201,6 +201,9 @@ export abstract class AbstractLineHighlightOverlay extends DynamicViewOverlay { protected abstract _renderOne(ctx: RenderingContext, exact: boolean): string; } +/** + * Emphasizes the current line by drawing a border around it. + */ export class CurrentLineHighlightOverlay extends AbstractLineHighlightOverlay { protected _renderOne(ctx: RenderingContext, exact: boolean): string { @@ -215,6 +218,9 @@ export class CurrentLineHighlightOverlay extends AbstractLineHighlightOverlay { } } +/** + * Emphasizes the current line margin/gutter by drawing a border around it. + */ export class CurrentLineMarginHighlightOverlay extends AbstractLineHighlightOverlay { protected _renderOne(ctx: RenderingContext, exact: boolean): string { const className = 'current-line' + (this._shouldRenderInMargin() ? ' current-line-margin' : '') + (this._shouldRenderOther() ? ' current-line-margin-both' : '') + (this._shouldRenderInMargin() && exact ? ' current-line-exact-margin' : ''); diff --git a/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts b/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts index 459ad1f12b1..0e0f9413ab0 100644 --- a/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts +++ b/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts @@ -16,6 +16,10 @@ import { getThemeTypeSelector } from '../../../../platform/theme/common/themeSer import { EditorOption } from '../../../common/config/editorOptions.js'; import { IMouseWheelEvent } from '../../../../base/browser/mouseEvent.js'; +/** + * The editor scrollbar built on VS Code's scrollable element that sits beside + * the minimap. + */ export class EditorScrollbar extends ViewPart { private readonly scrollbar: SmoothScrollableElement; diff --git a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts index 6d936b92c3b..a140386e6ca 100644 --- a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts +++ b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts @@ -18,6 +18,10 @@ import { isDefined } from '../../../../base/common/types.js'; import { BracketPairGuidesClassNames } from '../../../common/model/guidesTextModelPart.js'; import { IndentGuide, HorizontalGuidesState } from '../../../common/textModelGuides.js'; +/** + * Indent guides are vertical lines that help identify the indentation level of + * the code. + */ export class IndentGuidesOverlay extends DynamicViewOverlay { private readonly _context: ViewContext; diff --git a/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts b/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts index 9321db6339b..ee6461fd7f8 100644 --- a/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts +++ b/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts @@ -15,6 +15,9 @@ import * as viewEvents from '../../../common/viewEvents.js'; import { registerThemingParticipant } from '../../../../platform/theme/common/themeService.js'; import { editorDimmedLineNumber, editorLineNumbers } from '../../../common/core/editorColorRegistry.js'; +/** + * Renders line numbers to the left of the main view lines content. + */ export class LineNumbersOverlay extends DynamicViewOverlay { public static readonly CLASS_NAME = 'line-numbers'; diff --git a/src/vs/editor/browser/viewParts/lines/viewLine.ts b/src/vs/editor/browser/viewParts/lines/viewLine.ts index 71cacf00696..209a85d8d5b 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLine.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLine.ts @@ -9,15 +9,16 @@ import * as platform from '../../../../base/common/platform.js'; import { IVisibleLine } from '../../view/viewLayer.js'; import { RangeUtil } from './rangeUtil.js'; import { StringBuilder } from '../../../common/core/stringBuilder.js'; -import { IEditorConfiguration } from '../../../common/config/editorConfiguration.js'; import { FloatHorizontalRange, VisibleRanges } from '../../view/renderingContext.js'; import { LineDecoration } from '../../../common/viewLayout/lineDecorations.js'; import { CharacterMapping, ForeignElementType, RenderLineInput, renderViewLine, LineRange, DomPosition } from '../../../common/viewLayout/viewLineRenderer.js'; import { ViewportData } from '../../../common/viewLayout/viewLinesViewportData.js'; import { InlineDecorationType } from '../../../common/viewModel.js'; -import { ColorScheme, isHighContrast } from '../../../../platform/theme/common/theme.js'; -import { EditorOption, EditorFontLigatures } from '../../../common/config/editorOptions.js'; +import { isHighContrast } from '../../../../platform/theme/common/theme.js'; +import { EditorFontLigatures } from '../../../common/config/editorOptions.js'; import { DomReadingContext } from './domReadingContext.js'; +import type { ViewLineOptions } from './viewLineOptions.js'; +import { ViewLinesGpu } from '../linesGpu/viewLinesGpu.js'; const canUseFastRenderedViewLine = (function () { if (platform.isNative) { @@ -45,61 +46,6 @@ const canUseFastRenderedViewLine = (function () { let monospaceAssumptionsAreValid = true; -export class ViewLineOptions { - public readonly themeType: ColorScheme; - public readonly renderWhitespace: 'none' | 'boundary' | 'selection' | 'trailing' | 'all'; - public readonly renderControlCharacters: boolean; - public readonly spaceWidth: number; - public readonly middotWidth: number; - public readonly wsmiddotWidth: number; - public readonly useMonospaceOptimizations: boolean; - public readonly canUseHalfwidthRightwardsArrow: boolean; - public readonly lineHeight: number; - public readonly stopRenderingLineAfter: number; - public readonly fontLigatures: string; - - constructor(config: IEditorConfiguration, themeType: ColorScheme) { - this.themeType = themeType; - const options = config.options; - const fontInfo = options.get(EditorOption.fontInfo); - const experimentalWhitespaceRendering = options.get(EditorOption.experimentalWhitespaceRendering); - if (experimentalWhitespaceRendering === 'off') { - this.renderWhitespace = options.get(EditorOption.renderWhitespace); - } else { - // whitespace is rendered in a different layer - this.renderWhitespace = 'none'; - } - this.renderControlCharacters = options.get(EditorOption.renderControlCharacters); - this.spaceWidth = fontInfo.spaceWidth; - this.middotWidth = fontInfo.middotWidth; - this.wsmiddotWidth = fontInfo.wsmiddotWidth; - this.useMonospaceOptimizations = ( - fontInfo.isMonospace - && !options.get(EditorOption.disableMonospaceOptimizations) - ); - this.canUseHalfwidthRightwardsArrow = fontInfo.canUseHalfwidthRightwardsArrow; - this.lineHeight = options.get(EditorOption.lineHeight); - this.stopRenderingLineAfter = options.get(EditorOption.stopRenderingLineAfter); - this.fontLigatures = options.get(EditorOption.fontLigatures); - } - - public equals(other: ViewLineOptions): boolean { - return ( - this.themeType === other.themeType - && this.renderWhitespace === other.renderWhitespace - && this.renderControlCharacters === other.renderControlCharacters - && this.spaceWidth === other.spaceWidth - && this.middotWidth === other.middotWidth - && this.wsmiddotWidth === other.wsmiddotWidth - && this.useMonospaceOptimizations === other.useMonospaceOptimizations - && this.canUseHalfwidthRightwardsArrow === other.canUseHalfwidthRightwardsArrow - && this.lineHeight === other.lineHeight - && this.stopRenderingLineAfter === other.stopRenderingLineAfter - && this.fontLigatures === other.fontLigatures - ); - } -} - export class ViewLine implements IVisibleLine { public static readonly CLASS_NAME = 'view-line'; @@ -152,6 +98,12 @@ export class ViewLine implements IVisibleLine { } public renderLine(lineNumber: number, deltaTop: number, lineHeight: number, viewportData: ViewportData, sb: StringBuilder): boolean { + if (this._options.useGpu && ViewLinesGpu.canRender(this._options, viewportData, lineNumber)) { + this._renderedViewLine?.domNode?.domNode.remove(); + this._renderedViewLine = null; + return false; + } + if (this._isMaybeInvalid === false) { // it appears that nothing relevant has changed return false; diff --git a/src/vs/editor/browser/viewParts/lines/viewLineOptions.ts b/src/vs/editor/browser/viewParts/lines/viewLineOptions.ts new file mode 100644 index 00000000000..c75ea1740fe --- /dev/null +++ b/src/vs/editor/browser/viewParts/lines/viewLineOptions.ts @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { ColorScheme } from '../../../../platform/theme/common/theme.js'; +import type { IEditorConfiguration } from '../../../common/config/editorConfiguration.js'; +import { EditorOption } from '../../../common/config/editorOptions.js'; + +export class ViewLineOptions { + public readonly themeType: ColorScheme; + public readonly renderWhitespace: 'none' | 'boundary' | 'selection' | 'trailing' | 'all'; + public readonly renderControlCharacters: boolean; + public readonly spaceWidth: number; + public readonly middotWidth: number; + public readonly wsmiddotWidth: number; + public readonly useMonospaceOptimizations: boolean; + public readonly canUseHalfwidthRightwardsArrow: boolean; + public readonly lineHeight: number; + public readonly stopRenderingLineAfter: number; + public readonly fontLigatures: string; + public readonly useGpu: boolean; + + constructor(config: IEditorConfiguration, themeType: ColorScheme) { + this.themeType = themeType; + const options = config.options; + const fontInfo = options.get(EditorOption.fontInfo); + const experimentalWhitespaceRendering = options.get(EditorOption.experimentalWhitespaceRendering); + if (experimentalWhitespaceRendering === 'off') { + this.renderWhitespace = options.get(EditorOption.renderWhitespace); + } else { + // whitespace is rendered in a different layer + this.renderWhitespace = 'none'; + } + this.renderControlCharacters = options.get(EditorOption.renderControlCharacters); + this.spaceWidth = fontInfo.spaceWidth; + this.middotWidth = fontInfo.middotWidth; + this.wsmiddotWidth = fontInfo.wsmiddotWidth; + this.useMonospaceOptimizations = ( + fontInfo.isMonospace + && !options.get(EditorOption.disableMonospaceOptimizations) + ); + this.canUseHalfwidthRightwardsArrow = fontInfo.canUseHalfwidthRightwardsArrow; + this.lineHeight = options.get(EditorOption.lineHeight); + this.stopRenderingLineAfter = options.get(EditorOption.stopRenderingLineAfter); + this.fontLigatures = options.get(EditorOption.fontLigatures); + this.useGpu = options.get(EditorOption.experimentalGpuAcceleration) === 'on'; + } + + public equals(other: ViewLineOptions): boolean { + return ( + this.themeType === other.themeType + && this.renderWhitespace === other.renderWhitespace + && this.renderControlCharacters === other.renderControlCharacters + && this.spaceWidth === other.spaceWidth + && this.middotWidth === other.middotWidth + && this.wsmiddotWidth === other.wsmiddotWidth + && this.useMonospaceOptimizations === other.useMonospaceOptimizations + && this.canUseHalfwidthRightwardsArrow === other.canUseHalfwidthRightwardsArrow + && this.lineHeight === other.lineHeight + && this.stopRenderingLineAfter === other.stopRenderingLineAfter + && this.fontLigatures === other.fontLigatures + && this.useGpu === other.useGpu + ); + } +} diff --git a/src/vs/editor/browser/viewParts/lines/viewLines.ts b/src/vs/editor/browser/viewParts/lines/viewLines.ts index ba40ee237be..b93a0028b0b 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLines.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLines.ts @@ -14,7 +14,7 @@ import { HorizontalPosition, HorizontalRange, IViewLines, LineVisibleRanges, Vis import { VisibleLinesCollection } from '../../view/viewLayer.js'; import { PartFingerprint, PartFingerprints, ViewPart } from '../../view/viewPart.js'; import { DomReadingContext } from './domReadingContext.js'; -import { ViewLine, ViewLineOptions } from './viewLine.js'; +import { ViewLine } from './viewLine.js'; import { EditorOption } from '../../../common/config/editorOptions.js'; import { Position } from '../../../common/core/position.js'; import { Range } from '../../../common/core/range.js'; @@ -24,6 +24,7 @@ import * as viewEvents from '../../../common/viewEvents.js'; import { ViewportData } from '../../../common/viewLayout/viewLinesViewportData.js'; import { Viewport } from '../../../common/viewModel.js'; import { ViewContext } from '../../../common/viewModel/viewContext.js'; +import { ViewLineOptions } from './viewLineOptions.js'; class LastRenderedData { @@ -87,6 +88,10 @@ class HorizontalRevealSelectionsRequest { type HorizontalRevealRequest = HorizontalRevealRangeRequest | HorizontalRevealSelectionsRequest; +/** + * The view lines part is responsible for rendering the actual content of a + * file. + */ export class ViewLines extends ViewPart implements IViewLines { /** * Adds this amount of pixels to the right of lines (no-one wants to type near the edge of the viewport) diff --git a/src/vs/editor/browser/viewParts/linesGpu/viewLinesGpu.ts b/src/vs/editor/browser/viewParts/linesGpu/viewLinesGpu.ts new file mode 100644 index 00000000000..60c42904ed2 --- /dev/null +++ b/src/vs/editor/browser/viewParts/linesGpu/viewLinesGpu.ts @@ -0,0 +1,389 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getActiveWindow } from '../../../../base/browser/dom.js'; +import { BugIndicatingError } from '../../../../base/common/errors.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { EditorOption } from '../../../common/config/editorOptions.js'; +import type { ViewLinesChangedEvent, ViewScrollChangedEvent } from '../../../common/viewEvents.js'; +import type { ViewportData } from '../../../common/viewLayout/viewLinesViewportData.js'; +import type { ViewContext } from '../../../common/viewModel/viewContext.js'; +import { TextureAtlas } from '../../gpu/atlas/textureAtlas.js'; +import { TextureAtlasPage } from '../../gpu/atlas/textureAtlasPage.js'; +import { FullFileRenderStrategy } from '../../gpu/fullFileRenderStrategy.js'; +import { BindingId, type IGpuRenderStrategy } from '../../gpu/gpu.js'; +import { GPULifecycle } from '../../gpu/gpuDisposable.js'; +import { observeDevicePixelDimensions, quadVertices } from '../../gpu/gpuUtils.js'; +import type { ViewGpuContext } from '../../gpu/viewGpuContext.js'; +import type { RenderingContext, RestrictedRenderingContext } from '../../view/renderingContext.js'; +import { ViewPart } from '../../view/viewPart.js'; +import { ViewLineOptions } from '../lines/viewLineOptions.js'; + + +const enum GlyphStorageBufferInfo { + FloatsPerEntry = 2 + 2 + 2, + BytesPerEntry = GlyphStorageBufferInfo.FloatsPerEntry * 4, + Offset_TexturePosition = 0, + Offset_TextureSize = 2, + Offset_OriginPosition = 4, +} + +/** + * The GPU implementation of the ViewLines part. + */ +export class ViewLinesGpu extends ViewPart { + + private readonly canvas: HTMLCanvasElement; + + private _device!: GPUDevice; + private _renderPassDescriptor!: GPURenderPassDescriptor; + private _renderPassColorAttachment!: GPURenderPassColorAttachment; + private _bindGroup!: GPUBindGroup; + private _pipeline!: GPURenderPipeline; + + private _vertexBuffer!: GPUBuffer; + + static atlas: TextureAtlas; + + private readonly _glyphStorageBuffer: GPUBuffer[] = []; + private _atlasGpuTexture!: GPUTexture; + private readonly _atlasGpuTextureVersions: number[] = []; + + private _initialized = false; + + private _renderStrategy!: IGpuRenderStrategy; + + constructor( + context: ViewContext, + private readonly _viewGpuContext: ViewGpuContext, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @ILogService private readonly _logService: ILogService, + ) { + super(context); + + this.canvas = this._viewGpuContext.canvas.domNode; + + this._register(this._viewGpuContext.onDidChangeCanvasDevicePixelDimensions(({ width, height }) => { + // TODO: Request render, should this just call renderText with the last viewportData + })); + + this.initWebgpu(); + } + + async initWebgpu() { + // #region General + + this._device = await this._viewGpuContext.device; + + const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); + this._viewGpuContext.ctx.configure({ + device: this._device, + format: presentationFormat, + alphaMode: 'premultiplied', + }); + + // TODO: Should the texture atlas (shared across all editors) should be part of the gpu context (shared across view parts of this editor)? + // Create texture atlas + if (!ViewLinesGpu.atlas) { + ViewLinesGpu.atlas = this._instantiationService.createInstance(TextureAtlas, this._device.limits.maxTextureDimension2D, undefined); + } + const atlas = ViewLinesGpu.atlas; + + this._renderPassColorAttachment = { + view: null!, // Will be filled at render time + loadOp: 'load', + storeOp: 'store', + }; + this._renderPassDescriptor = { + label: 'Monaco render pass', + colorAttachments: [this._renderPassColorAttachment], + }; + + // #endregion General + + // #region Uniforms + + let layoutInfoUniformBuffer: GPUBuffer; + { + const enum Info { + FloatsPerEntry = 6, + BytesPerEntry = Info.FloatsPerEntry * 4, + Offset_CanvasWidth____ = 0, + Offset_CanvasHeight___ = 1, + Offset_ViewportOffsetX = 2, + Offset_ViewportOffsetY = 3, + Offset_ViewportWidth__ = 4, + Offset_ViewportHeight_ = 5, + } + const bufferValues = new Float32Array(Info.FloatsPerEntry); + const updateBufferValues = (canvasDevicePixelWidth: number = this.canvas.width, canvasDevicePixelHeight: number = this.canvas.height) => { + bufferValues[Info.Offset_CanvasWidth____] = canvasDevicePixelWidth; + bufferValues[Info.Offset_CanvasHeight___] = canvasDevicePixelHeight; + bufferValues[Info.Offset_ViewportOffsetX] = Math.ceil(this._context.configuration.options.get(EditorOption.layoutInfo).contentLeft * getActiveWindow().devicePixelRatio); + bufferValues[Info.Offset_ViewportOffsetY] = 0; + bufferValues[Info.Offset_ViewportWidth__] = bufferValues[Info.Offset_CanvasWidth____] - bufferValues[Info.Offset_ViewportOffsetX]; + bufferValues[Info.Offset_ViewportHeight_] = bufferValues[Info.Offset_CanvasHeight___] - bufferValues[Info.Offset_ViewportOffsetY]; + return bufferValues; + }; + layoutInfoUniformBuffer = this._register(GPULifecycle.createBuffer(this._device, { + label: 'Monaco uniform buffer', + size: Info.BytesPerEntry, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }, () => updateBufferValues())).object; + this._register(observeDevicePixelDimensions(this.canvas, getActiveWindow(), (w, h) => { + this._device.queue.writeBuffer(layoutInfoUniformBuffer, 0, updateBufferValues(w, h)); + })); + } + + let atlasInfoUniformBuffer: GPUBuffer; + { + const enum Info { + FloatsPerEntry = 2, + BytesPerEntry = Info.FloatsPerEntry * 4, + Offset_Width_ = 0, + Offset_Height = 1, + } + atlasInfoUniformBuffer = this._register(GPULifecycle.createBuffer(this._device, { + label: 'Monaco atlas info uniform buffer', + size: Info.BytesPerEntry, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }, () => { + const values = new Float32Array(Info.FloatsPerEntry); + values[Info.Offset_Width_] = atlas.pageSize; + values[Info.Offset_Height] = atlas.pageSize; + return values; + })).object; + } + + // #endregion Uniforms + + // #region Storage buffers + + this._renderStrategy = this._register(this._instantiationService.createInstance(FullFileRenderStrategy, this._context, this._device, this.canvas, ViewLinesGpu.atlas)); + + this._glyphStorageBuffer[0] = this._register(GPULifecycle.createBuffer(this._device, { + label: 'Monaco glyph storage buffer', + size: GlyphStorageBufferInfo.BytesPerEntry * TextureAtlasPage.maximumGlyphCount, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + })).object; + this._glyphStorageBuffer[1] = this._register(GPULifecycle.createBuffer(this._device, { + label: 'Monaco glyph storage buffer', + size: GlyphStorageBufferInfo.BytesPerEntry * TextureAtlasPage.maximumGlyphCount, + usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, + })).object; + this._atlasGpuTextureVersions[0] = 0; + this._atlasGpuTextureVersions[1] = 0; + this._atlasGpuTexture = this._register(GPULifecycle.createTexture(this._device, { + label: 'Monaco atlas texture', + format: 'rgba8unorm', + // TODO: Dynamically grow/shrink layer count + size: { width: atlas.pageSize, height: atlas.pageSize, depthOrArrayLayers: 2 }, + dimension: '2d', + usage: GPUTextureUsage.TEXTURE_BINDING | + GPUTextureUsage.COPY_DST | + GPUTextureUsage.RENDER_ATTACHMENT, + })).object; + + this._updateAtlasStorageBufferAndTexture(); + + // #endregion Storage buffers + + // #region Vertex buffer + + this._vertexBuffer = this._register(GPULifecycle.createBuffer(this._device, { + label: 'Monaco vertex buffer', + size: quadVertices.byteLength, + usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, + }, quadVertices)).object; + + // #endregion Vertex buffer + + // #region Shader module + + const module = this._device.createShaderModule({ + label: 'Monaco shader module', + code: this._renderStrategy.wgsl, + }); + + // #endregion Shader module + + // #region Pipeline + + this._pipeline = this._device.createRenderPipeline({ + label: 'Monaco render pipeline', + layout: 'auto', + vertex: { + module, + entryPoint: 'vs', + buffers: [ + { + arrayStride: 2 * Float32Array.BYTES_PER_ELEMENT, // 2 floats, 4 bytes each + attributes: [ + { shaderLocation: 0, offset: 0, format: 'float32x2' }, // position + ], + } + ] + }, + fragment: { + module, + entryPoint: 'fs', + targets: [ + { + format: presentationFormat, + blend: { + color: { + srcFactor: 'src-alpha', + dstFactor: 'one-minus-src-alpha' + }, + alpha: { + srcFactor: 'src-alpha', + dstFactor: 'one-minus-src-alpha' + }, + }, + } + ], + }, + }); + + // #endregion Pipeline + + // #region Bind group + + this._bindGroup = this._device.createBindGroup({ + label: 'Monaco bind group', + layout: this._pipeline.getBindGroupLayout(0), + entries: [ + // TODO: Pass in generically as array? + { binding: BindingId.GlyphInfo0, resource: { buffer: this._glyphStorageBuffer[0] } }, + { binding: BindingId.GlyphInfo1, resource: { buffer: this._glyphStorageBuffer[1] } }, + { + binding: BindingId.TextureSampler, resource: this._device.createSampler({ + label: 'Monaco atlas sampler', + magFilter: 'nearest', + minFilter: 'nearest', + }) + }, + { binding: BindingId.Texture, resource: this._atlasGpuTexture.createView() }, + { binding: BindingId.ViewportUniform, resource: { buffer: layoutInfoUniformBuffer } }, + { binding: BindingId.AtlasDimensionsUniform, resource: { buffer: atlasInfoUniformBuffer } }, + ...this._renderStrategy.bindGroupEntries + ], + }); + + // endregion Bind group + + this._initialized = true; + } + + private _updateAtlasStorageBufferAndTexture() { + const atlas = ViewLinesGpu.atlas; + + for (const [layerIndex, page] of atlas.pages.entries()) { + // Skip the update if it's already the latest version + if (page.version === this._atlasGpuTextureVersions[layerIndex]) { + continue; + } + + this._logService.trace('Updating atlas page[', layerIndex, '] from version ', this._atlasGpuTextureVersions[layerIndex], ' to version ', page.version); + + // TODO: Reuse buffer instead of reconstructing each time + // TODO: Dynamically set buffer size + const values = new Float32Array(GlyphStorageBufferInfo.FloatsPerEntry * TextureAtlasPage.maximumGlyphCount); + let entryOffset = 0; + for (const glyph of page.glyphs) { + values[entryOffset + GlyphStorageBufferInfo.Offset_TexturePosition] = glyph.x; + values[entryOffset + GlyphStorageBufferInfo.Offset_TexturePosition + 1] = glyph.y; + values[entryOffset + GlyphStorageBufferInfo.Offset_TextureSize] = glyph.w; + values[entryOffset + GlyphStorageBufferInfo.Offset_TextureSize + 1] = glyph.h; + values[entryOffset + GlyphStorageBufferInfo.Offset_OriginPosition] = glyph.originOffsetX; + values[entryOffset + GlyphStorageBufferInfo.Offset_OriginPosition + 1] = glyph.originOffsetY; + entryOffset += GlyphStorageBufferInfo.FloatsPerEntry; + } + if (entryOffset / GlyphStorageBufferInfo.FloatsPerEntry > TextureAtlasPage.maximumGlyphCount) { + throw new Error(`Attempting to write more glyphs (${entryOffset / GlyphStorageBufferInfo.FloatsPerEntry}) than the GPUBuffer can hold (${TextureAtlasPage.maximumGlyphCount})`); + } + this._device.queue.writeBuffer(this._glyphStorageBuffer[layerIndex], 0, values); + if (page.usedArea.right - page.usedArea.left > 0 && page.usedArea.bottom - page.usedArea.top > 0) { + this._device.queue.copyExternalImageToTexture( + { source: page.source }, + { + texture: this._atlasGpuTexture, + origin: { + x: page.usedArea.left, + y: page.usedArea.top, + z: layerIndex + } + }, + { + width: page.usedArea.right - page.usedArea.left, + height: page.usedArea.bottom - page.usedArea.top + }, + ); + } + this._atlasGpuTextureVersions[layerIndex] = page.version; + } + } + + public static canRender(options: ViewLineOptions, viewportData: ViewportData, lineNumber: number): boolean { + const d = viewportData.getViewLineRenderingData(lineNumber); + // TODO + return d.content.indexOf('e') !== -1; + } + + public prepareRender(ctx: RenderingContext): void { + throw new BugIndicatingError('Should not be called'); + } + + public override render(ctx: RestrictedRenderingContext): void { + throw new BugIndicatingError('Should not be called'); + } + + override onLinesChanged(e: ViewLinesChangedEvent): boolean { + return true; + } + + override onScrollChanged(e: ViewScrollChangedEvent): boolean { + return true; + } + + // subscribe to more events + + public renderText(viewportData: ViewportData): void { + if (this._initialized) { + return this._renderText(viewportData); + } + } + + private _renderText(viewportData: ViewportData): void { + const options = new ViewLineOptions(this._context.configuration, this._context.theme.type); + + const visibleObjectCount = this._renderStrategy.update(viewportData, options); + + this._updateAtlasStorageBufferAndTexture(); + + const encoder = this._device.createCommandEncoder({ label: 'Monaco command encoder' }); + + this._renderPassColorAttachment.view = this._viewGpuContext.ctx.getCurrentTexture().createView({ label: 'Monaco canvas texture view' }); + const pass = encoder.beginRenderPass(this._renderPassDescriptor); + pass.setPipeline(this._pipeline); + pass.setVertexBuffer(0, this._vertexBuffer); + + pass.setBindGroup(0, this._bindGroup); + + if (this._renderStrategy?.draw) { + // TODO: Don't draw lines if ViewLinesGpu.canRender is false + this._renderStrategy.draw(pass, viewportData); + } else { + pass.draw(quadVertices.length / 2, visibleObjectCount); + } + + pass.end(); + + const commandBuffer = encoder.finish(); + + this._device.queue.submit([commandBuffer]); + } +} diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.ts b/src/vs/editor/browser/viewParts/minimap/minimap.ts index d6aa690bc3b..a54d780d996 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.ts +++ b/src/vs/editor/browser/viewParts/minimap/minimap.ts @@ -803,6 +803,10 @@ class MinimapSamplingState { } } +/** + * The minimap appears beside the editor scroll bar and visualizes a zoomed out + * view of the file. + */ export class Minimap extends ViewPart implements IMinimapModel { public readonly tokensColorTracker: MinimapTokensColorTracker; diff --git a/src/vs/editor/browser/viewParts/overviewRuler/overviewRuler.ts b/src/vs/editor/browser/viewParts/overviewRuler/overviewRuler.ts index 2643979cb97..11292eb56a1 100644 --- a/src/vs/editor/browser/viewParts/overviewRuler/overviewRuler.ts +++ b/src/vs/editor/browser/viewParts/overviewRuler/overviewRuler.ts @@ -11,6 +11,10 @@ import { ViewContext } from '../../../common/viewModel/viewContext.js'; import * as viewEvents from '../../../common/viewEvents.js'; import { ViewEventHandler } from '../../../common/viewEventHandler.js'; +/** + * The overview ruler appears underneath the editor scroll bar and shows things + * like the cursor, various decorations, etc. + */ export class OverviewRuler extends ViewEventHandler implements IOverviewRuler { private readonly _context: ViewContext; diff --git a/src/vs/editor/browser/viewParts/rulers/rulers.ts b/src/vs/editor/browser/viewParts/rulers/rulers.ts index eb03b1a9ea9..c0a46927d17 100644 --- a/src/vs/editor/browser/viewParts/rulers/rulers.ts +++ b/src/vs/editor/browser/viewParts/rulers/rulers.ts @@ -11,6 +11,10 @@ import { ViewContext } from '../../../common/viewModel/viewContext.js'; import * as viewEvents from '../../../common/viewEvents.js'; import { EditorOption, IRulerOption } from '../../../common/config/editorOptions.js'; +/** + * Rulers are vertical lines that appear at certain columns in the editor. There can be >= 0 rulers + * at a time. + */ export class Rulers extends ViewPart { public domNode: FastDomNode; diff --git a/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts b/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts index b5ac061aab5..6f16ed5253b 100644 --- a/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts +++ b/src/vs/editor/browser/viewParts/viewCursors/viewCursors.ts @@ -23,6 +23,10 @@ import { isHighContrast } from '../../../../platform/theme/common/theme.js'; import { CursorChangeReason } from '../../../common/cursorEvents.js'; import { WindowIntervalTimer, getWindow } from '../../../../base/browser/dom.js'; +/** + * View cursors is a view part responsible for rendering the primary cursor and + * any secondary cursors that are currently active. + */ export class ViewCursors extends ViewPart { static readonly BLINK_INTERVAL = 500; diff --git a/src/vs/editor/browser/viewParts/viewZones/viewZones.ts b/src/vs/editor/browser/viewParts/viewZones/viewZones.ts index 4bf3ee4029b..4b94e5660d3 100644 --- a/src/vs/editor/browser/viewParts/viewZones/viewZones.ts +++ b/src/vs/editor/browser/viewParts/viewZones/viewZones.ts @@ -32,6 +32,11 @@ interface IComputedViewZoneProps { const invalidFunc = () => { throw new Error(`Invalid change accessor`); }; +/** + * A view zone is a rectangle that is a section that is inserted into the editor + * lines that can be used for various purposes such as showing a diffs, peeking + * an implementation, etc. + */ export class ViewZones extends ViewPart { private _zones: { [id: string]: IMyViewZone }; diff --git a/src/vs/editor/browser/viewParts/whitespace/whitespace.ts b/src/vs/editor/browser/viewParts/whitespace/whitespace.ts index 1fcdff94ed4..56cc4692dc0 100644 --- a/src/vs/editor/browser/viewParts/whitespace/whitespace.ts +++ b/src/vs/editor/browser/viewParts/whitespace/whitespace.ts @@ -18,6 +18,10 @@ import { LineRange } from '../../../common/viewLayout/viewLineRenderer.js'; import { Position } from '../../../common/core/position.js'; import { editorWhitespaces } from '../../../common/core/editorColorRegistry.js'; +/** + * The whitespace overlay will visual certain whitespace depending on the + * current editor configuration (boundary, selection, etc.). + */ export class WhitespaceOverlay extends DynamicViewOverlay { private readonly _context: ViewContext; diff --git a/src/vs/editor/browser/widget/codeEditor/editor.css b/src/vs/editor/browser/widget/codeEditor/editor.css index a6d82d5845f..d33122122de 100644 --- a/src/vs/editor/browser/widget/codeEditor/editor.css +++ b/src/vs/editor/browser/widget/codeEditor/editor.css @@ -45,6 +45,14 @@ border-style: dotted; } +.monaco-editor .editorCanvas { + position: absolute; + width: 100%; + height: 100%; + z-index: 0; + pointer-events: none; +} + /* -------------------- Misc -------------------- */ .monaco-editor .overflow-guard { diff --git a/src/vs/editor/browser/widget/diffEditor/components/diffEditorSash.ts b/src/vs/editor/browser/widget/diffEditor/components/diffEditorSash.ts index beb8bcc0761..8abaddcceb9 100644 --- a/src/vs/editor/browser/widget/diffEditor/components/diffEditorSash.ts +++ b/src/vs/editor/browser/widget/diffEditor/components/diffEditorSash.ts @@ -5,9 +5,8 @@ import { IBoundarySashes, ISashEvent, Orientation, Sash, SashState } from '../../../../../base/browser/ui/sash/sash.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; -import { IObservable, IReader, ISettableObservable, autorun, observableValue } from '../../../../../base/common/observable.js'; +import { IObservable, IReader, ISettableObservable, autorun, derivedWithSetter, observableValue } from '../../../../../base/common/observable.js'; import { DiffEditorOptions } from '../diffEditorOptions.js'; -import { derivedWithSetter } from '../../../../../base/common/observableInternal/derived.js'; export class SashLayout { public readonly sashLeft = derivedWithSetter(this, reader => { diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts index 97260fa8c83..0c1a533681f 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorOptions.ts @@ -3,15 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IObservable, ISettableObservable, derived, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; -import { derivedConstOnceDefined } from '../../../../base/common/observableInternal/utils.js'; +import { IObservable, ISettableObservable, derived, derivedConstOnceDefined, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; import { Constants } from '../../../../base/common/uint.js'; -import { allowsTrueInlineDiffRendering } from './components/diffEditorViewZones/diffEditorViewZones.js'; -import { DiffEditorViewModel, DiffState } from './diffEditorViewModel.js'; +import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; import { diffEditorDefaultOptions } from '../../../common/config/diffEditor.js'; import { IDiffEditorBaseOptions, IDiffEditorOptions, IEditorOptions, ValidDiffEditorBaseOptions, clampedFloat, clampedInt, boolean as validateBooleanOption, stringSet as validateStringSetOption } from '../../../common/config/editorOptions.js'; import { LineRangeMapping } from '../../../common/diff/rangeMapping.js'; -import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; +import { allowsTrueInlineDiffRendering } from './components/diffEditorViewZones/diffEditorViewZones.js'; +import { DiffEditorViewModel, DiffState } from './diffEditorViewModel.js'; export class DiffEditorOptions { private readonly _options: ISettableObservable, { changedOptions: IDiffEditorOptions }>; diff --git a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts index 6b69885c757..da2e28f1c44 100644 --- a/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditor/diffEditorWidget.ts @@ -7,28 +7,15 @@ import { IBoundarySashes } from '../../../../base/browser/ui/sash/sash.js'; import { findLast } from '../../../../base/common/arraysFind.js'; import { BugIndicatingError, onUnexpectedError } from '../../../../base/common/errors.js'; import { Event } from '../../../../base/common/event.js'; -import { toDisposable } from '../../../../base/common/lifecycle.js'; -import { IObservable, ITransaction, autorun, autorunWithStore, derived, disposableObservableValue, observableFromEvent, observableValue, recomputeInitiallyAndOnChange, subtransaction, transaction } from '../../../../base/common/observable.js'; -import { derivedDisposable } from '../../../../base/common/observableInternal/derived.js'; -import './style.css'; -import { IEditorConstructionOptions } from '../../config/editorConfiguration.js'; -import { ICodeEditor, IDiffEditor, IDiffEditorConstructionOptions } from '../../editorBrowser.js'; -import { EditorExtensionsRegistry, IDiffEditorContributionDescription } from '../../editorExtensions.js'; -import { ICodeEditorService } from '../../services/codeEditorService.js'; -import { StableEditorScrollState } from '../../stableEditorScroll.js'; -import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../codeEditor/codeEditorWidget.js'; -import { AccessibleDiffViewer, AccessibleDiffViewerModelFromEditors } from './components/accessibleDiffViewer.js'; -import { DiffEditorDecorations } from './components/diffEditorDecorations.js'; -import { DiffEditorSash, SashLayout } from './components/diffEditorSash.js'; -import { DiffEditorViewZones } from './components/diffEditorViewZones/diffEditorViewZones.js'; -import { DiffEditorGutter } from './features/gutterFeature.js'; -import { HideUnchangedRegionsFeature } from './features/hideUnchangedRegionsFeature.js'; -import { MovedBlocksLinesFeature } from './features/movedBlocksLinesFeature.js'; -import { OverviewRulerFeature } from './features/overviewRulerFeature.js'; -import { RevertButtonsFeature } from './features/revertButtonsFeature.js'; -import { CSSStyle, ObservableElementSizeObserver, RefCounted, applyStyle, applyViewZones, translatePosition } from './utils.js'; import { readHotReloadableExport } from '../../../../base/common/hotReloadHelpers.js'; +import { toDisposable } from '../../../../base/common/lifecycle.js'; +import { IObservable, ITransaction, autorun, autorunWithStore, derived, derivedDisposable, disposableObservableValue, observableFromEvent, observableValue, recomputeInitiallyAndOnChange, subtransaction, transaction } from '../../../../base/common/observable.js'; +import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; import { bindContextKey } from '../../../../platform/observable/common/platformObservableUtils.js'; +import { IEditorProgressService } from '../../../../platform/progress/common/progress.js'; import { IDiffEditorOptions } from '../../../common/config/editorOptions.js'; import { IDimension } from '../../../common/core/dimension.js'; import { Position } from '../../../common/core/position.js'; @@ -39,15 +26,27 @@ import { LineRangeMapping, RangeMapping } from '../../../common/diff/rangeMappin import { EditorType, IDiffEditorModel, IDiffEditorViewModel, IDiffEditorViewState } from '../../../common/editorCommon.js'; import { EditorContextKeys } from '../../../common/editorContextKeys.js'; import { IIdentifiedSingleEditOperation } from '../../../common/model.js'; -import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; -import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; -import { IEditorProgressService } from '../../../../platform/progress/common/progress.js'; +import { IEditorConstructionOptions } from '../../config/editorConfiguration.js'; +import { ICodeEditor, IDiffEditor, IDiffEditorConstructionOptions } from '../../editorBrowser.js'; +import { EditorExtensionsRegistry, IDiffEditorContributionDescription } from '../../editorExtensions.js'; +import { ICodeEditorService } from '../../services/codeEditorService.js'; +import { StableEditorScrollState } from '../../stableEditorScroll.js'; +import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../codeEditor/codeEditorWidget.js'; +import { AccessibleDiffViewer, AccessibleDiffViewerModelFromEditors } from './components/accessibleDiffViewer.js'; +import { DiffEditorDecorations } from './components/diffEditorDecorations.js'; import { DiffEditorEditors } from './components/diffEditorEditors.js'; +import { DiffEditorSash, SashLayout } from './components/diffEditorSash.js'; +import { DiffEditorViewZones } from './components/diffEditorViewZones/diffEditorViewZones.js'; import { DelegatingEditor } from './delegatingEditorImpl.js'; import { DiffEditorOptions } from './diffEditorOptions.js'; import { DiffEditorViewModel, DiffMapping, DiffState } from './diffEditorViewModel.js'; +import { DiffEditorGutter } from './features/gutterFeature.js'; +import { HideUnchangedRegionsFeature } from './features/hideUnchangedRegionsFeature.js'; +import { MovedBlocksLinesFeature } from './features/movedBlocksLinesFeature.js'; +import { OverviewRulerFeature } from './features/overviewRulerFeature.js'; +import { RevertButtonsFeature } from './features/revertButtonsFeature.js'; +import './style.css'; +import { CSSStyle, ObservableElementSizeObserver, RefCounted, applyStyle, applyViewZones, translatePosition } from './utils.js'; export interface IDiffCodeEditorWidgetOptions { originalEditor?: ICodeEditorWidgetOptions; diff --git a/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts b/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts index a2a4eb15f27..556378aebf3 100644 --- a/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts +++ b/src/vs/editor/browser/widget/diffEditor/features/gutterFeature.ts @@ -9,16 +9,13 @@ import { ActionsOrientation } from '../../../../../base/browser/ui/actionbar/act import { HoverPosition } from '../../../../../base/browser/ui/hover/hoverWidget.js'; import { IBoundarySashes } from '../../../../../base/browser/ui/sash/sash.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; -import { IObservable, autorun, autorunWithStore, derived, observableFromEvent, observableValue } from '../../../../../base/common/observable.js'; -import { derivedDisposable, derivedWithSetter } from '../../../../../base/common/observableInternal/derived.js'; +import { IObservable, autorun, autorunWithStore, derived, derivedDisposable, derivedWithSetter, observableFromEvent, observableValue } from '../../../../../base/common/observable.js'; import { URI } from '../../../../../base/common/uri.js'; -import { DiffEditorEditors } from '../components/diffEditorEditors.js'; -import { DiffEditorSash, SashLayout } from '../components/diffEditorSash.js'; -import { DiffEditorOptions } from '../diffEditorOptions.js'; -import { DiffEditorViewModel } from '../diffEditorViewModel.js'; -import { appendRemoveOnDispose, applyStyle, prependRemoveOnDispose } from '../utils.js'; -import { EditorGutter, IGutterItemInfo, IGutterItemView } from '../utils/editorGutter.js'; -import { ActionRunnerWithContext } from '../../multiDiffEditor/utils.js'; +import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js'; +import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js'; +import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; +import { WorkbenchHoverDelegate } from '../../../../../platform/hover/browser/hover.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { EditorOption } from '../../../../common/config/editorOptions.js'; import { LineRange, LineRangeSet } from '../../../../common/core/lineRange.js'; import { OffsetRange } from '../../../../common/core/offsetRange.js'; @@ -26,11 +23,13 @@ import { Range } from '../../../../common/core/range.js'; import { TextEdit } from '../../../../common/core/textEdit.js'; import { DetailedLineRangeMapping } from '../../../../common/diff/rangeMapping.js'; import { TextModelText } from '../../../../common/model/textModelText.js'; -import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js'; -import { IMenuService, MenuId } from '../../../../../platform/actions/common/actions.js'; -import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; -import { WorkbenchHoverDelegate } from '../../../../../platform/hover/browser/hover.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ActionRunnerWithContext } from '../../multiDiffEditor/utils.js'; +import { DiffEditorEditors } from '../components/diffEditorEditors.js'; +import { DiffEditorSash, SashLayout } from '../components/diffEditorSash.js'; +import { DiffEditorOptions } from '../diffEditorOptions.js'; +import { DiffEditorViewModel } from '../diffEditorViewModel.js'; +import { appendRemoveOnDispose, applyStyle, prependRemoveOnDispose } from '../utils.js'; +import { EditorGutter, IGutterItemInfo, IGutterItemView } from '../utils/editorGutter.js'; const emptyArr: never[] = []; const width = 35; diff --git a/src/vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature.ts b/src/vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature.ts index 559c40ec8eb..e87fe47e055 100644 --- a/src/vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature.ts +++ b/src/vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature.ts @@ -8,16 +8,11 @@ import { renderIcon, renderLabelWithIcons } from '../../../../../base/browser/ui import { Codicon } from '../../../../../base/common/codicons.js'; import { MarkdownString } from '../../../../../base/common/htmlContent.js'; import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js'; -import { IObservable, IReader, autorun, derived, derivedWithStore, observableValue, transaction } from '../../../../../base/common/observable.js'; -import { derivedDisposable } from '../../../../../base/common/observableInternal/derived.js'; +import { IObservable, IReader, autorun, derived, derivedDisposable, derivedWithStore, observableValue, transaction } from '../../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; import { isDefined } from '../../../../../base/common/types.js'; -import { ICodeEditor } from '../../../editorBrowser.js'; -import { observableCodeEditor } from '../../../observableCodeEditor.js'; -import { DiffEditorEditors } from '../components/diffEditorEditors.js'; -import { DiffEditorOptions } from '../diffEditorOptions.js'; -import { DiffEditorViewModel, RevealPreference, UnchangedRegion } from '../diffEditorViewModel.js'; -import { IObservableViewZone, PlaceholderViewZone, ViewZoneOverlayWidget, applyObservableDecorations, applyStyle } from '../utils.js'; +import { localize } from '../../../../../nls.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { EditorOption } from '../../../../common/config/editorOptions.js'; import { LineRange } from '../../../../common/core/lineRange.js'; import { Position } from '../../../../common/core/position.js'; @@ -25,8 +20,12 @@ import { Range } from '../../../../common/core/range.js'; import { CursorChangeReason } from '../../../../common/cursorEvents.js'; import { SymbolKind, SymbolKinds } from '../../../../common/languages.js'; import { IModelDecorationOptions, IModelDeltaDecoration, ITextModel } from '../../../../common/model.js'; -import { localize } from '../../../../../nls.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ICodeEditor } from '../../../editorBrowser.js'; +import { observableCodeEditor } from '../../../observableCodeEditor.js'; +import { DiffEditorEditors } from '../components/diffEditorEditors.js'; +import { DiffEditorOptions } from '../diffEditorOptions.js'; +import { DiffEditorViewModel, RevealPreference, UnchangedRegion } from '../diffEditorViewModel.js'; +import { IObservableViewZone, PlaceholderViewZone, ViewZoneOverlayWidget, applyObservableDecorations, applyStyle } from '../utils.js'; /** * Make sure to add the view zones to the editor! diff --git a/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts b/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts index 6e35b12dbb0..48ac35815d4 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/diffEditorItemTemplate.ts @@ -6,22 +6,21 @@ import { h } from '../../../../base/browser/dom.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; -import { autorun, derived } from '../../../../base/common/observable.js'; -import { globalTransaction, observableValue } from '../../../../base/common/observableInternal/base.js'; -import { observableCodeEditor } from '../../observableCodeEditor.js'; -import { DiffEditorWidget } from '../diffEditor/diffEditorWidget.js'; -import { DocumentDiffItemViewModel } from './multiDiffEditorViewModel.js'; -import { IWorkbenchUIElementFactory } from './workbenchUIElementFactory.js'; -import { IDiffEditorOptions } from '../../../common/config/editorOptions.js'; -import { OffsetRange } from '../../../common/core/offsetRange.js'; +import { autorun, derived, globalTransaction, observableValue } from '../../../../base/common/observable.js'; import { createActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; import { MenuId } from '../../../../platform/actions/common/actions.js'; +import { IContextKeyService, type IScopedContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; +import { IDiffEditorOptions } from '../../../common/config/editorOptions.js'; +import { OffsetRange } from '../../../common/core/offsetRange.js'; +import { observableCodeEditor } from '../../observableCodeEditor.js'; +import { DiffEditorWidget } from '../diffEditor/diffEditorWidget.js'; +import { DocumentDiffItemViewModel } from './multiDiffEditorViewModel.js'; import { IObjectData, IPooledObject } from './objectPool.js'; import { ActionRunnerWithContext } from './utils.js'; -import { IContextKeyService, type IScopedContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; +import { IWorkbenchUIElementFactory } from './workbenchUIElementFactory.js'; export class TemplateData implements IObjectData { constructor( diff --git a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts index 518b4720698..49c674d0477 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorViewModel.ts @@ -4,19 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; -import { IObservable, ITransaction, derived, observableValue, transaction } from '../../../../base/common/observable.js'; -import { constObservable, derivedObservableWithWritableCache, mapObservableArrayCached, observableFromValueWithChangeEvent } from '../../../../base/common/observableInternal/utils.js'; +import { IObservable, ITransaction, constObservable, derived, derivedObservableWithWritableCache, mapObservableArrayCached, observableFromValueWithChangeEvent, observableValue, transaction } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; -import { DiffEditorOptions } from '../diffEditor/diffEditorOptions.js'; -import { DiffEditorViewModel } from '../diffEditor/diffEditorViewModel.js'; -import { RefCounted } from '../diffEditor/utils.js'; -import { IDocumentDiffItem, IMultiDiffEditorModel } from './model.js'; +import { ContextKeyValue } from '../../../../platform/contextkey/common/contextkey.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IDiffEditorOptions } from '../../../common/config/editorOptions.js'; import { Selection } from '../../../common/core/selection.js'; import { IDiffEditorViewModel } from '../../../common/editorCommon.js'; import { IModelService } from '../../../common/services/model.js'; -import { ContextKeyValue } from '../../../../platform/contextkey/common/contextkey.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { DiffEditorOptions } from '../diffEditor/diffEditorOptions.js'; +import { DiffEditorViewModel } from '../diffEditor/diffEditorViewModel.js'; +import { RefCounted } from '../diffEditor/utils.js'; +import { IDocumentDiffItem, IMultiDiffEditorModel } from './model.js'; export class MultiDiffEditorViewModel extends Disposable { private readonly _documents: IObservable[] | 'loading'> = observableFromValueWithChangeEvent(this.model, this.model.documents); diff --git a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidget.ts b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidget.ts index 6236c75a2f8..d23ad8c87f6 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidget.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidget.ts @@ -4,22 +4,22 @@ *--------------------------------------------------------------------------------------------*/ import { Dimension } from '../../../../base/browser/dom.js'; +import { Event } from '../../../../base/common/event.js'; +import { readHotReloadableExport } from '../../../../base/common/hotReloadHelpers.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { derived, derivedWithStore, observableValue, recomputeInitiallyAndOnChange } from '../../../../base/common/observable.js'; -import { readHotReloadableExport } from '../../../../base/common/hotReloadHelpers.js'; -import { IDocumentDiffItem, IMultiDiffEditorModel } from './model.js'; -import { IMultiDiffEditorViewState, IMultiDiffResourceId, MultiDiffEditorWidgetImpl } from './multiDiffEditorWidgetImpl.js'; -import { MultiDiffEditorViewModel } from './multiDiffEditorViewModel.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import './colors.js'; -import { DiffEditorItemTemplate } from './diffEditorItemTemplate.js'; -import { IWorkbenchUIElementFactory } from './workbenchUIElementFactory.js'; -import { Event } from '../../../../base/common/event.js'; import { URI } from '../../../../base/common/uri.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { Range } from '../../../common/core/range.js'; import { IDiffEditor } from '../../../common/editorCommon.js'; import { ICodeEditor } from '../../editorBrowser.js'; import { DiffEditorWidget } from '../diffEditor/diffEditorWidget.js'; -import { Range } from '../../../common/core/range.js'; +import './colors.js'; +import { DiffEditorItemTemplate } from './diffEditorItemTemplate.js'; +import { IDocumentDiffItem, IMultiDiffEditorModel } from './model.js'; +import { MultiDiffEditorViewModel } from './multiDiffEditorViewModel.js'; +import { IMultiDiffEditorViewState, IMultiDiffResourceId, MultiDiffEditorWidgetImpl } from './multiDiffEditorWidgetImpl.js'; +import { IWorkbenchUIElementFactory } from './workbenchUIElementFactory.js'; export class MultiDiffEditorWidget extends Disposable { private readonly _dimension = observableValue(this, undefined); diff --git a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts index 4069feb23d1..ec0bf3cf64c 100644 --- a/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts +++ b/src/vs/editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.ts @@ -9,29 +9,28 @@ import { compareBy, numberComparator } from '../../../../base/common/arrays.js'; import { findFirstMax } from '../../../../base/common/arraysFind.js'; import { BugIndicatingError } from '../../../../base/common/errors.js'; import { Disposable, IReference, toDisposable } from '../../../../base/common/lifecycle.js'; -import { IObservable, IReader, autorun, autorunWithStore, derived, derivedWithStore, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; -import { ITransaction, disposableObservableValue, globalTransaction, transaction } from '../../../../base/common/observableInternal/base.js'; +import { IObservable, IReader, ITransaction, autorun, autorunWithStore, derived, derivedWithStore, disposableObservableValue, globalTransaction, observableFromEvent, observableValue, transaction } from '../../../../base/common/observable.js'; import { Scrollable, ScrollbarVisibility } from '../../../../base/common/scrollable.js'; import { URI } from '../../../../base/common/uri.js'; -import './style.css'; -import { ICodeEditor } from '../../editorBrowser.js'; -import { ObservableElementSizeObserver } from '../diffEditor/utils.js'; -import { RevealOptions } from './multiDiffEditorWidget.js'; -import { IWorkbenchUIElementFactory } from './workbenchUIElementFactory.js'; +import { localize } from '../../../../nls.js'; +import { ContextKeyValue, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { ITextEditorOptions } from '../../../../platform/editor/common/editor.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; import { OffsetRange } from '../../../common/core/offsetRange.js'; import { IRange } from '../../../common/core/range.js'; import { ISelection, Selection } from '../../../common/core/selection.js'; import { IDiffEditor } from '../../../common/editorCommon.js'; import { EditorContextKeys } from '../../../common/editorContextKeys.js'; -import { ContextKeyValue, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { ITextEditorOptions } from '../../../../platform/editor/common/editor.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; +import { ICodeEditor } from '../../editorBrowser.js'; +import { ObservableElementSizeObserver } from '../diffEditor/utils.js'; import { DiffEditorItemTemplate, TemplateData } from './diffEditorItemTemplate.js'; -import { DocumentDiffItemViewModel, MultiDiffEditorViewModel } from './multiDiffEditorViewModel.js'; -import { ObjectPool } from './objectPool.js'; -import { localize } from '../../../../nls.js'; import { IDocumentDiffItem } from './model.js'; +import { DocumentDiffItemViewModel, MultiDiffEditorViewModel } from './multiDiffEditorViewModel.js'; +import { RevealOptions } from './multiDiffEditorWidget.js'; +import { ObjectPool } from './objectPool.js'; +import './style.css'; +import { IWorkbenchUIElementFactory } from './workbenchUIElementFactory.js'; export class MultiDiffEditorWidgetImpl extends Disposable { private readonly _scrollableElements = h('div.scrollContent', [ diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 41f1aa9ed19..0594257d976 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -634,6 +634,11 @@ export interface IEditorOptions { * Defaults to 'always'. */ matchBrackets?: 'never' | 'near' | 'always'; + /** + * Enable experimental rendering using WebGPU. + * Defaults to 'off'. + */ + experimentalGpuAcceleration?: 'on' | 'off'; /** * Enable experimental whitespace rendering. * Defaults to 'svg'. @@ -5380,6 +5385,7 @@ export const enum EditorOption { dropIntoEditor, experimentalEditContextEnabled, emptySelectionClipboard, + experimentalGpuAcceleration, experimentalWhitespaceRendering, extraEditorClassName, fastScrollSensitivity, @@ -5756,6 +5762,20 @@ export const EditorOptions = { } )), stickyScroll: register(new EditorStickyScroll()), + experimentalGpuAcceleration: register(new EditorStringEnumOption( + EditorOption.experimentalGpuAcceleration, 'experimentalGpuAcceleration', + 'off' as 'off' | 'on', + ['off', 'on'] as const, + undefined + // TODO: Uncomment when we want to expose the setting to VS Code users + // { + // enumDescriptions: [ + // nls.localize('experimentalGpuAcceleration.off', "Use regular DOM-based rendering."), + // nls.localize('experimentalGpuAcceleration.on', "Use GPU acceleration."), + // ], + // description: nls.localize('experimentalGpuAcceleration', "Controls whether to use the (very) experimental GPU acceleration to render the editor.") + // } + )), experimentalWhitespaceRendering: register(new EditorStringEnumOption( EditorOption.experimentalWhitespaceRendering, 'experimentalWhitespaceRendering', 'svg' as 'svg' | 'font' | 'off', diff --git a/src/vs/editor/common/encodedTokenAttributes.ts b/src/vs/editor/common/encodedTokenAttributes.ts index c0f2fad70f1..bf144bd8a17 100644 --- a/src/vs/editor/common/encodedTokenAttributes.ts +++ b/src/vs/editor/common/encodedTokenAttributes.ts @@ -65,26 +65,26 @@ export const enum StandardTokenType { * */ export const enum MetadataConsts { - LANGUAGEID_MASK = 0b00000000000000000000000011111111, - TOKEN_TYPE_MASK = 0b00000000000000000000001100000000, - BALANCED_BRACKETS_MASK = 0b00000000000000000000010000000000, - FONT_STYLE_MASK = 0b00000000000000000111100000000000, - FOREGROUND_MASK = 0b00000000111111111000000000000000, - BACKGROUND_MASK = 0b11111111000000000000000000000000, + LANGUAGEID_MASK /* */ = 0b00000000000000000000000011111111, + TOKEN_TYPE_MASK /* */ = 0b00000000000000000000001100000000, + BALANCED_BRACKETS_MASK /* */ = 0b00000000000000000000010000000000, + FONT_STYLE_MASK /* */ = 0b00000000000000000111100000000000, + FOREGROUND_MASK /* */ = 0b00000000111111111000000000000000, + BACKGROUND_MASK /* */ = 0b11111111000000000000000000000000, - ITALIC_MASK = 0b00000000000000000000100000000000, - BOLD_MASK = 0b00000000000000000001000000000000, - UNDERLINE_MASK = 0b00000000000000000010000000000000, - STRIKETHROUGH_MASK = 0b00000000000000000100000000000000, + ITALIC_MASK /* */ = 0b00000000000000000000100000000000, + BOLD_MASK /* */ = 0b00000000000000000001000000000000, + UNDERLINE_MASK /* */ = 0b00000000000000000010000000000000, + STRIKETHROUGH_MASK /* */ = 0b00000000000000000100000000000000, // Semantic tokens cannot set the language id, so we can // use the first 8 bits for control purposes - SEMANTIC_USE_ITALIC = 0b00000000000000000000000000000001, - SEMANTIC_USE_BOLD = 0b00000000000000000000000000000010, - SEMANTIC_USE_UNDERLINE = 0b00000000000000000000000000000100, - SEMANTIC_USE_STRIKETHROUGH = 0b00000000000000000000000000001000, - SEMANTIC_USE_FOREGROUND = 0b00000000000000000000000000010000, - SEMANTIC_USE_BACKGROUND = 0b00000000000000000000000000100000, + SEMANTIC_USE_ITALIC /* */ = 0b00000000000000000000000000000001, + SEMANTIC_USE_BOLD /* */ = 0b00000000000000000000000000000010, + SEMANTIC_USE_UNDERLINE /* */ = 0b00000000000000000000000000000100, + SEMANTIC_USE_STRIKETHROUGH /* */ = 0b00000000000000000000000000001000, + SEMANTIC_USE_FOREGROUND /* */ = 0b00000000000000000000000000010000, + SEMANTIC_USE_BACKGROUND /* */ = 0b00000000000000000000000000100000, LANGUAGEID_OFFSET = 0, TOKEN_TYPE_OFFSET = 8, diff --git a/src/vs/editor/common/languages/highlights/typescript.scm b/src/vs/editor/common/languages/highlights/typescript.scm index e50bc9dec97..56b842837ad 100644 --- a/src/vs/editor/common/languages/highlights/typescript.scm +++ b/src/vs/editor/common/languages/highlights/typescript.scm @@ -15,14 +15,16 @@ ; Keywords +("typeof") @keyword.operator.expression.typeof + +(binary_expression "instanceof" @keyword.operator.expression.instanceof) + [ "delete" "in" "infer" - "instanceof" "keyof" "of" - "typeof" ] @keyword.operator.expression [ @@ -91,7 +93,7 @@ [ "void" -] @support.type +] @support.type.primitive [ "new" @@ -108,56 +110,79 @@ "?" ] @punctuation.delimiter +[ + "!" + "~" + "===" + "!==" + "&&" + "||" + "??" +] @keyword.operator.logical + [ "-" - "--" - "-=" "+" - "++" - "+=" "*" - "*=" - "**" - "**=" "/" - "/=" "%" - "%=" + "^" +] @keyword.operator.arithmetic + +(binary_expression ([ "<" "<=" - "<<" - "<<=" - "=" - "==" - "===" - "!" - "!=" - "!==" - "=>" ">" ">=" +]) @keyword.operator.relational) + +[ + "=" +] @keyword.operator.assignment + +(augmented_assignment_expression ([ + "-=" + "+=" + "*=" + "/=" + "%=" + "^=" + "&=" + "|=" + "&&=" + "||=" + "??=" +]) @keyword.operator.assignment.compound) + +[ + "++" +] @keyword.operator.increment + +[ + "--" +] @keyword.operator.decrement + +[ + "**" + "**=" + "<<" + "<<=" + "==" + "!=" + "=>" ">>" ">>=" ">>>" ">>>=" "~" - "^" "&" "|" - "^=" - "&=" - "|=" - "&&" - "||" - "??" - "&&=" - "||=" - "??=" ] @keyword.operator ; Special identifiers (type_identifier) @entity.name.type +(predefined_type (["string" "boolean" "number"])) @support.type.primitive (predefined_type) @support.type (("const") diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index dbfa92805d8..600112188c3 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -212,119 +212,120 @@ export enum EditorOption { dropIntoEditor = 36, experimentalEditContextEnabled = 37, emptySelectionClipboard = 38, - experimentalWhitespaceRendering = 39, - extraEditorClassName = 40, - fastScrollSensitivity = 41, - find = 42, - fixedOverflowWidgets = 43, - folding = 44, - foldingStrategy = 45, - foldingHighlight = 46, - foldingImportsByDefault = 47, - foldingMaximumRegions = 48, - unfoldOnClickAfterEndOfLine = 49, - fontFamily = 50, - fontInfo = 51, - fontLigatures = 52, - fontSize = 53, - fontWeight = 54, - fontVariations = 55, - formatOnPaste = 56, - formatOnType = 57, - glyphMargin = 58, - gotoLocation = 59, - hideCursorInOverviewRuler = 60, - hover = 61, - inDiffEditor = 62, - inlineSuggest = 63, - inlineEdit = 64, - letterSpacing = 65, - lightbulb = 66, - lineDecorationsWidth = 67, - lineHeight = 68, - lineNumbers = 69, - lineNumbersMinChars = 70, - linkedEditing = 71, - links = 72, - matchBrackets = 73, - minimap = 74, - mouseStyle = 75, - mouseWheelScrollSensitivity = 76, - mouseWheelZoom = 77, - multiCursorMergeOverlapping = 78, - multiCursorModifier = 79, - multiCursorPaste = 80, - multiCursorLimit = 81, - occurrencesHighlight = 82, - overviewRulerBorder = 83, - overviewRulerLanes = 84, - padding = 85, - pasteAs = 86, - parameterHints = 87, - peekWidgetDefaultFocus = 88, - placeholder = 89, - definitionLinkOpensInPeek = 90, - quickSuggestions = 91, - quickSuggestionsDelay = 92, - readOnly = 93, - readOnlyMessage = 94, - renameOnType = 95, - renderControlCharacters = 96, - renderFinalNewline = 97, - renderLineHighlight = 98, - renderLineHighlightOnlyWhenFocus = 99, - renderValidationDecorations = 100, - renderWhitespace = 101, - revealHorizontalRightPadding = 102, - roundedSelection = 103, - rulers = 104, - scrollbar = 105, - scrollBeyondLastColumn = 106, - scrollBeyondLastLine = 107, - scrollPredominantAxis = 108, - selectionClipboard = 109, - selectionHighlight = 110, - selectOnLineNumbers = 111, - showFoldingControls = 112, - showUnused = 113, - snippetSuggestions = 114, - smartSelect = 115, - smoothScrolling = 116, - stickyScroll = 117, - stickyTabStops = 118, - stopRenderingLineAfter = 119, - suggest = 120, - suggestFontSize = 121, - suggestLineHeight = 122, - suggestOnTriggerCharacters = 123, - suggestSelection = 124, - tabCompletion = 125, - tabIndex = 126, - unicodeHighlighting = 127, - unusualLineTerminators = 128, - useShadowDOM = 129, - useTabStops = 130, - wordBreak = 131, - wordSegmenterLocales = 132, - wordSeparators = 133, - wordWrap = 134, - wordWrapBreakAfterCharacters = 135, - wordWrapBreakBeforeCharacters = 136, - wordWrapColumn = 137, - wordWrapOverride1 = 138, - wordWrapOverride2 = 139, - wrappingIndent = 140, - wrappingStrategy = 141, - showDeprecated = 142, - inlayHints = 143, - editorClassName = 144, - pixelRatio = 145, - tabFocusMode = 146, - layoutInfo = 147, - wrappingInfo = 148, - defaultColorDecorators = 149, - colorDecoratorsActivatedOn = 150, - inlineCompletionsAccessibilityVerbose = 151 + experimentalGpuAcceleration = 39, + experimentalWhitespaceRendering = 40, + extraEditorClassName = 41, + fastScrollSensitivity = 42, + find = 43, + fixedOverflowWidgets = 44, + folding = 45, + foldingStrategy = 46, + foldingHighlight = 47, + foldingImportsByDefault = 48, + foldingMaximumRegions = 49, + unfoldOnClickAfterEndOfLine = 50, + fontFamily = 51, + fontInfo = 52, + fontLigatures = 53, + fontSize = 54, + fontWeight = 55, + fontVariations = 56, + formatOnPaste = 57, + formatOnType = 58, + glyphMargin = 59, + gotoLocation = 60, + hideCursorInOverviewRuler = 61, + hover = 62, + inDiffEditor = 63, + inlineSuggest = 64, + inlineEdit = 65, + letterSpacing = 66, + lightbulb = 67, + lineDecorationsWidth = 68, + lineHeight = 69, + lineNumbers = 70, + lineNumbersMinChars = 71, + linkedEditing = 72, + links = 73, + matchBrackets = 74, + minimap = 75, + mouseStyle = 76, + mouseWheelScrollSensitivity = 77, + mouseWheelZoom = 78, + multiCursorMergeOverlapping = 79, + multiCursorModifier = 80, + multiCursorPaste = 81, + multiCursorLimit = 82, + occurrencesHighlight = 83, + overviewRulerBorder = 84, + overviewRulerLanes = 85, + padding = 86, + pasteAs = 87, + parameterHints = 88, + peekWidgetDefaultFocus = 89, + placeholder = 90, + definitionLinkOpensInPeek = 91, + quickSuggestions = 92, + quickSuggestionsDelay = 93, + readOnly = 94, + readOnlyMessage = 95, + renameOnType = 96, + renderControlCharacters = 97, + renderFinalNewline = 98, + renderLineHighlight = 99, + renderLineHighlightOnlyWhenFocus = 100, + renderValidationDecorations = 101, + renderWhitespace = 102, + revealHorizontalRightPadding = 103, + roundedSelection = 104, + rulers = 105, + scrollbar = 106, + scrollBeyondLastColumn = 107, + scrollBeyondLastLine = 108, + scrollPredominantAxis = 109, + selectionClipboard = 110, + selectionHighlight = 111, + selectOnLineNumbers = 112, + showFoldingControls = 113, + showUnused = 114, + snippetSuggestions = 115, + smartSelect = 116, + smoothScrolling = 117, + stickyScroll = 118, + stickyTabStops = 119, + stopRenderingLineAfter = 120, + suggest = 121, + suggestFontSize = 122, + suggestLineHeight = 123, + suggestOnTriggerCharacters = 124, + suggestSelection = 125, + tabCompletion = 126, + tabIndex = 127, + unicodeHighlighting = 128, + unusualLineTerminators = 129, + useShadowDOM = 130, + useTabStops = 131, + wordBreak = 132, + wordSegmenterLocales = 133, + wordSeparators = 134, + wordWrap = 135, + wordWrapBreakAfterCharacters = 136, + wordWrapBreakBeforeCharacters = 137, + wordWrapColumn = 138, + wordWrapOverride1 = 139, + wordWrapOverride2 = 140, + wrappingIndent = 141, + wrappingStrategy = 142, + showDeprecated = 143, + inlayHints = 144, + editorClassName = 145, + pixelRatio = 146, + tabFocusMode = 147, + layoutInfo = 148, + wrappingInfo = 149, + defaultColorDecorators = 150, + colorDecoratorsActivatedOn = 151, + inlineCompletionsAccessibilityVerbose = 152 } /** diff --git a/src/vs/editor/contrib/folding/browser/folding.ts b/src/vs/editor/contrib/folding/browser/folding.ts index 2380c725389..ee657d44e14 100644 --- a/src/vs/editor/contrib/folding/browser/folding.ts +++ b/src/vs/editor/contrib/folding/browser/folding.ts @@ -1231,6 +1231,35 @@ class RemoveFoldRangeFromSelectionAction extends FoldingAction { } +class ToggleImportFoldAction extends FoldingAction { + + constructor() { + super({ + id: 'editor.toggleImportFold', + label: nls.localize('toggleImportFold.label', "Toggle Import Fold"), + alias: 'Toggle Import Fold', + precondition: CONTEXT_FOLDING_ENABLED, + kbOpts: { + kbExpr: EditorContextKeys.editorTextFocus, + weight: KeybindingWeight.EditorContrib + } + }); + } + + async invoke(foldingController: FoldingController, foldingModel: FoldingModel): Promise { + const regionsToToggle: FoldingRegion[] = []; + const regions = foldingModel.regions; + for (let i = regions.length - 1; i >= 0; i--) { + if (regions.getType(i) === FoldingRangeKind.Imports.value) { + regionsToToggle.push(regions.toRegion(i)); + } + } + foldingModel.toggleCollapseState(regionsToToggle); + foldingController.triggerFoldingModelChanged(); + } +} + + registerEditorContribution(FoldingController.ID, FoldingController, EditorContributionInstantiation.Eager); // eager because it uses `saveViewState`/`restoreViewState` registerEditorAction(UnfoldAction); registerEditorAction(UnFoldRecursivelyAction); @@ -1250,6 +1279,7 @@ registerEditorAction(GotoPreviousFoldAction); registerEditorAction(GotoNextFoldAction); registerEditorAction(FoldRangeFromSelectionAction); registerEditorAction(RemoveFoldRangeFromSelectionAction); +registerEditorAction(ToggleImportFoldAction); for (let i = 1; i <= 7; i++) { registerInstantiatedEditorAction( diff --git a/src/vs/editor/contrib/gpu/browser/gpuActions.ts b/src/vs/editor/contrib/gpu/browser/gpuActions.ts new file mode 100644 index 00000000000..b28a6cf6915 --- /dev/null +++ b/src/vs/editor/contrib/gpu/browser/gpuActions.ts @@ -0,0 +1,151 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBuffer } from '../../../../base/common/buffer.js'; +import { URI } from '../../../../base/common/uri.js'; +import { localize } from '../../../../nls.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; +import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import type { ICodeEditor } from '../../../browser/editorBrowser.js'; +import { EditorAction, registerEditorAction, type ServicesAccessor } from '../../../browser/editorExtensions.js'; +import { ensureNonNullable } from '../../../browser/gpu/gpuUtils.js'; +import { GlyphRasterizer } from '../../../browser/gpu/raster/glyphRasterizer.js'; +import { ViewLinesGpu } from '../../../browser/viewParts/linesGpu/viewLinesGpu.js'; + +class DebugEditorGpuRendererAction extends EditorAction { + + constructor() { + super({ + id: 'editor.action.debugEditorGpuRenderer', + label: localize('gpuDebug.label', "Developer: Debug Editor GPU Renderer"), + alias: 'Developer: Debug Editor GPU Renderer', + // TODO: Why doesn't `ContextKeyExpr.equals('config:editor.experimentalGpuAcceleration', 'on')` work? + precondition: ContextKeyExpr.true(), + }); + } + + async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { + const instantiationService = accessor.get(IInstantiationService); + const quickInputService = accessor.get(IQuickInputService); + const choice = await quickInputService.pick([ + { + label: localize('logTextureAtlasStats.label', "Log Texture Atlas Stats"), + id: 'logTextureAtlasStats', + }, + { + label: localize('saveTextureAtlas.label', "Save Texture Atlas"), + id: 'saveTextureAtlas', + }, + { + label: localize('drawGlyph.label', "Draw Glyph"), + id: 'drawGlyph', + }, + ], { canPickMany: false }); + if (!choice) { + return; + } + switch (choice.id) { + case 'logTextureAtlasStats': + instantiationService.invokeFunction(accessor => { + const logService = accessor.get(ILogService); + + const atlas = ViewLinesGpu.atlas; + if (!ViewLinesGpu.atlas) { + logService.error('No texture atlas found'); + return; + } + + const stats = atlas.getStats(); + logService.info(['Texture atlas stats', ...stats].join('\n\n')); + }); + break; + case 'saveTextureAtlas': + instantiationService.invokeFunction(async accessor => { + const workspaceContextService = accessor.get(IWorkspaceContextService); + const fileService = accessor.get(IFileService); + const folders = workspaceContextService.getWorkspace().folders; + if (folders.length > 0) { + const atlas = ViewLinesGpu.atlas; + const promises = []; + for (const [layerIndex, page] of atlas.pages.entries()) { + promises.push(...[ + fileService.writeFile( + URI.joinPath(folders[0].uri, `textureAtlasPage${layerIndex}_actual.png`), + VSBuffer.wrap(new Uint8Array(await (await page.source.convertToBlob()).arrayBuffer())) + ), + fileService.writeFile( + URI.joinPath(folders[0].uri, `textureAtlasPage${layerIndex}_usage.png`), + VSBuffer.wrap(new Uint8Array(await (await page.getUsagePreview()).arrayBuffer())) + ), + ]); + } + await Promise.all(promises); + } + }); + break; + case 'drawGlyph': + instantiationService.invokeFunction(async accessor => { + const configurationService = accessor.get(IConfigurationService); + const fileService = accessor.get(IFileService); + const logService = accessor.get(ILogService); + const quickInputService = accessor.get(IQuickInputService); + const workspaceContextService = accessor.get(IWorkspaceContextService); + + const folders = workspaceContextService.getWorkspace().folders; + if (folders.length === 0) { + return; + } + + const atlas = ViewLinesGpu.atlas; + if (!ViewLinesGpu.atlas) { + logService.error('No texture atlas found'); + return; + } + + const fontFamily = configurationService.getValue('editor.fontFamily'); + const fontSize = configurationService.getValue('editor.fontSize'); + const rasterizer = new GlyphRasterizer(fontSize, fontFamily); + let chars = await quickInputService.input({ + prompt: 'Enter a character to draw (prefix with 0x for code point))' + }); + if (!chars) { + return; + } + const codePoint = chars.match(/0x(?[0-9a-f]+)/i)?.groups?.codePoint; + if (codePoint !== undefined) { + chars = String.fromCodePoint(parseInt(codePoint, 16)); + } + const metadata = 0; + const rasterizedGlyph = atlas.getGlyph(rasterizer, chars, metadata); + if (!rasterizedGlyph) { + return; + } + const imageData = atlas.pages[rasterizedGlyph.pageIndex].source.getContext('2d')?.getImageData( + rasterizedGlyph.x, + rasterizedGlyph.y, + rasterizedGlyph.w, + rasterizedGlyph.h + ); + if (!imageData) { + return; + } + const canvas = new OffscreenCanvas(imageData.width, imageData.height); + const ctx = ensureNonNullable(canvas.getContext('2d')); + ctx.putImageData(imageData, 0, 0); + const blob = await canvas.convertToBlob({ type: 'image/png' }); + const resource = URI.joinPath(folders[0].uri, `glyph_${chars}_${metadata}_${fontSize}px_${fontFamily.replaceAll(/[,\\\/\.'\s]/g, '_')}.png`); + await fileService.writeFile(resource, VSBuffer.wrap(new Uint8Array(await blob.arrayBuffer()))); + }); + break; + } + } +} + +registerEditorAction(DebugEditorGpuRendererAction); diff --git a/src/vs/editor/contrib/hover/browser/contentHoverWidget.ts b/src/vs/editor/contrib/hover/browser/contentHoverWidget.ts index 2ac48a73c46..5dc30190980 100644 --- a/src/vs/editor/contrib/hover/browser/contentHoverWidget.ts +++ b/src/vs/editor/contrib/hover/browser/contentHoverWidget.ts @@ -291,7 +291,7 @@ export class ContentHoverWidget extends ResizableContentWidget { private _updateMaxDimensions() { const height = Math.max(this._editor.getLayoutInfo().height / 4, 250, ContentHoverWidget._lastDimensions.height); - const width = Math.max(this._editor.getLayoutInfo().width * 0.66, 500, ContentHoverWidget._lastDimensions.width); + const width = Math.max(this._editor.getLayoutInfo().width * 0.66, 750, ContentHoverWidget._lastDimensions.width); this._setHoverWidgetMaxDimensions(width, height); } diff --git a/src/vs/editor/contrib/hover/browser/hover.css b/src/vs/editor/contrib/hover/browser/hover.css index 2520856e59d..958f1ee9cf4 100644 --- a/src/vs/editor/contrib/hover/browser/hover.css +++ b/src/vs/editor/contrib/hover/browser/hover.css @@ -43,7 +43,7 @@ flex-direction: column; padding-left: 5px; padding-right: 5px; - justify-content: end; + justify-content: flex-end; border-right: 1px solid var(--vscode-editorHoverWidget-border); } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts b/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts index d41aea21727..595ab703f28 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/controller/commands.ts @@ -4,20 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; -import { transaction } from '../../../../../base/common/observable.js'; -import { asyncTransaction } from '../../../../../base/common/observableInternal/base.js'; -import { ICodeEditor } from '../../../../browser/editorBrowser.js'; -import { EditorAction, ServicesAccessor } from '../../../../browser/editorExtensions.js'; -import { EditorContextKeys } from '../../../../common/editorContextKeys.js'; -import { showNextInlineSuggestionActionId, showPreviousInlineSuggestionActionId, inlineSuggestCommitId } from './commandIds.js'; -import { InlineCompletionContextKeys } from './inlineCompletionContextKeys.js'; -import { InlineCompletionsController } from './inlineCompletionsController.js'; -import { Context as SuggestContext } from '../../../suggest/browser/suggest.js'; +import { asyncTransaction, transaction } from '../../../../../base/common/observable.js'; import * as nls from '../../../../../nls.js'; -import { MenuId, Action2 } from '../../../../../platform/actions/common/actions.js'; +import { Action2, MenuId } from '../../../../../platform/actions/common/actions.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; +import { ICodeEditor } from '../../../../browser/editorBrowser.js'; +import { EditorAction, ServicesAccessor } from '../../../../browser/editorExtensions.js'; +import { EditorContextKeys } from '../../../../common/editorContextKeys.js'; +import { Context as SuggestContext } from '../../../suggest/browser/suggest.js'; +import { inlineSuggestCommitId, showNextInlineSuggestionActionId, showPreviousInlineSuggestionActionId } from './commandIds.js'; +import { InlineCompletionContextKeys } from './inlineCompletionContextKeys.js'; +import { InlineCompletionsController } from './inlineCompletionsController.js'; export class ShowNextInlineSuggestionAction extends EditorAction { public static ID = showNextInlineSuggestionActionId; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts index 97064c64434..dcd8e176d5b 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts @@ -9,10 +9,7 @@ import { timeout } from '../../../../../base/common/async.js'; import { cancelOnDispose } from '../../../../../base/common/cancellation.js'; import { readHotReloadableExport } from '../../../../../base/common/hotReloadHelpers.js'; import { Disposable, DisposableStore, toDisposable } from '../../../../../base/common/lifecycle.js'; -import { IObservable, ITransaction, autorun, constObservable, derived, observableFromEvent, observableSignal, observableValue, transaction, waitForState } from '../../../../../base/common/observable.js'; -import { ISettableObservable } from '../../../../../base/common/observableInternal/base.js'; -import { derivedDisposable } from '../../../../../base/common/observableInternal/derived.js'; -import { derivedObservableWithCache, mapObservableArrayCached, runOnChange, runOnChangeWithStore } from '../../../../../base/common/observableInternal/utils.js'; +import { IObservable, ISettableObservable, ITransaction, autorun, constObservable, derived, derivedDisposable, derivedObservableWithCache, mapObservableArrayCached, observableFromEvent, observableSignal, observableValue, runOnChange, runOnChangeWithStore, transaction, waitForState } from '../../../../../base/common/observable.js'; import { isUndefined } from '../../../../../base/common/types.js'; import { localize } from '../../../../../nls.js'; import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js'; @@ -158,7 +155,7 @@ export class InlineCompletionsController extends Disposable { } })); - this._register(runOnChange(this._editorObs.selections, (_value, changes) => { + this._register(runOnChange(this._editorObs.selections, (_value, _, changes) => { if (changes.some(e => e.reason === CursorChangeReason.Explicit || e.source === 'api')) { this.model.get()?.stop(); } @@ -206,7 +203,7 @@ export class InlineCompletionsController extends Disposable { this._playAccessibilitySignal.read(reader); currentInlineCompletionBySemanticId.read(reader); return {}; - }), async (_value, _deltas, store) => { + }), async (_value, _, _deltas, store) => { /** @description InlineCompletionsController.playAccessibilitySignalAndReadSuggestion */ const model = this.model.get(); const state = model?.state.get(); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/hintsWidget/inlineCompletionsHintsWidget.ts b/src/vs/editor/contrib/inlineCompletions/browser/hintsWidget/inlineCompletionsHintsWidget.ts index 7d613257c56..1625099889f 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/hintsWidget/inlineCompletionsHintsWidget.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/hintsWidget/inlineCompletionsHintsWidget.ts @@ -11,18 +11,9 @@ import { equals } from '../../../../../base/common/arrays.js'; import { RunOnceScheduler } from '../../../../../base/common/async.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { Disposable, toDisposable } from '../../../../../base/common/lifecycle.js'; -import { IObservable, autorun, autorunWithStore, derived, derivedObservableWithCache, observableFromEvent } from '../../../../../base/common/observable.js'; -import { derivedWithStore } from '../../../../../base/common/observableInternal/derived.js'; +import { IObservable, autorun, autorunWithStore, derived, derivedObservableWithCache, derivedWithStore, observableFromEvent } from '../../../../../base/common/observable.js'; import { OS } from '../../../../../base/common/platform.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; -import './inlineCompletionsHintsWidget.css'; -import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from '../../../../browser/editorBrowser.js'; -import { EditorOption } from '../../../../common/config/editorOptions.js'; -import { Position } from '../../../../common/core/position.js'; -import { Command, InlineCompletionTriggerKind } from '../../../../common/languages.js'; -import { PositionAffinity } from '../../../../common/model.js'; -import { showNextInlineSuggestionActionId, showPreviousInlineSuggestionActionId } from '../controller/commandIds.js'; -import { InlineCompletionsModel } from '../model/inlineCompletionsModel.js'; import { localize } from '../../../../../nls.js'; import { MenuEntryActionViewItem, createAndFillInActionBarActions } from '../../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { IMenuWorkbenchToolBarOptions, WorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js'; @@ -34,6 +25,14 @@ import { IInstantiationService } from '../../../../../platform/instantiation/com import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; import { registerIcon } from '../../../../../platform/theme/common/iconRegistry.js'; +import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from '../../../../browser/editorBrowser.js'; +import { EditorOption } from '../../../../common/config/editorOptions.js'; +import { Position } from '../../../../common/core/position.js'; +import { Command, InlineCompletionTriggerKind } from '../../../../common/languages.js'; +import { PositionAffinity } from '../../../../common/model.js'; +import { showNextInlineSuggestionActionId, showPreviousInlineSuggestionActionId } from '../controller/commandIds.js'; +import { InlineCompletionsModel } from '../model/inlineCompletionsModel.js'; +import './inlineCompletionsHintsWidget.css'; export class InlineCompletionsHintsWidget extends Disposable { private readonly alwaysShowToolbar = observableFromEvent(this, this.editor.onDidChangeConfiguration, () => this.editor.getOption(EditorOption.inlineSuggest).showToolbar === 'always'); diff --git a/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts b/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts index 362fc07a365..b0cf6e51db8 100644 --- a/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts +++ b/src/vs/editor/contrib/inlineEdit/browser/inlineEditController.ts @@ -3,29 +3,28 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { createStyleSheet2 } from '../../../../base/browser/dom.js'; +import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; +import { onUnexpectedExternalError } from '../../../../base/common/errors.js'; import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; -import { ISettableObservable, autorun, constObservable, observableFromEvent, observableSignalFromEvent, observableValue, transaction } from '../../../../base/common/observable.js'; +import { ISettableObservable, autorun, constObservable, derivedDisposable, observableFromEvent, observableSignalFromEvent, observableValue, transaction } from '../../../../base/common/observable.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ICodeEditor } from '../../../browser/editorBrowser.js'; +import { IDiffProviderFactoryService } from '../../../browser/widget/diffEditor/diffProviderFactoryService.js'; +import { EditorOption } from '../../../common/config/editorOptions.js'; import { EditOperation } from '../../../common/core/editOperation.js'; import { Position } from '../../../common/core/position.js'; import { Range } from '../../../common/core/range.js'; -import { GhostTextWidget } from './ghostTextWidget.js'; -import { IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IInlineEdit, InlineEditTriggerKind } from '../../../common/languages.js'; import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js'; -import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; -import { GhostText, GhostTextPart } from '../../inlineCompletions/browser/model/ghostText.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { InlineEditHintsWidget } from './inlineEditHintsWidget.js'; -import { EditorOption } from '../../../common/config/editorOptions.js'; -import { createStyleSheet2 } from '../../../../base/browser/dom.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { onUnexpectedExternalError } from '../../../../base/common/errors.js'; -import { derivedDisposable } from '../../../../base/common/observableInternal/derived.js'; -import { InlineEditSideBySideWidget } from './inlineEditSideBySideWidget.js'; -import { IDiffProviderFactoryService } from '../../../browser/widget/diffEditor/diffProviderFactoryService.js'; import { IModelService } from '../../../common/services/model.js'; +import { GhostText, GhostTextPart } from '../../inlineCompletions/browser/model/ghostText.js'; +import { GhostTextWidget } from './ghostTextWidget.js'; +import { InlineEditHintsWidget } from './inlineEditHintsWidget.js'; +import { InlineEditSideBySideWidget } from './inlineEditSideBySideWidget.js'; export class InlineEditController extends Disposable { static ID = 'editor.contrib.inlineEditController'; diff --git a/src/vs/editor/contrib/inlineEdit/browser/inlineEditSideBySideWidget.ts b/src/vs/editor/contrib/inlineEdit/browser/inlineEditSideBySideWidget.ts index a4daba0c44e..4655e81186d 100644 --- a/src/vs/editor/contrib/inlineEdit/browser/inlineEditSideBySideWidget.ts +++ b/src/vs/editor/contrib/inlineEdit/browser/inlineEditSideBySideWidget.ts @@ -6,10 +6,9 @@ import { $ } from '../../../../base/browser/dom.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { IObservable, ObservablePromise, autorun, autorunWithStore, derived, observableSignalFromEvent } from '../../../../base/common/observable.js'; -import { derivedDisposable } from '../../../../base/common/observableInternal/derived.js'; +import { IObservable, ObservablePromise, autorun, autorunWithStore, derived, derivedDisposable, observableSignalFromEvent } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; -import './inlineEditSideBySideWidget.css'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from '../../../browser/editorBrowser.js'; import { observableCodeEditor } from '../../../browser/observableCodeEditor.js'; import { EmbeddedCodeEditorWidget } from '../../../browser/widget/codeEditor/embeddedCodeEditorWidget.js'; @@ -24,7 +23,7 @@ import { PLAINTEXT_LANGUAGE_ID } from '../../../common/languages/modesRegistry.j import { IModelDeltaDecoration } from '../../../common/model.js'; import { TextModel } from '../../../common/model/textModel.js'; import { IModelService } from '../../../common/services/model.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import './inlineEditSideBySideWidget.css'; function* range(start: number, end: number, step = 1) { if (end === undefined) { [end, start] = [start, 0]; } diff --git a/src/vs/editor/contrib/inlineEdits/browser/commands.ts b/src/vs/editor/contrib/inlineEdits/browser/commands.ts index 4314aad0e5f..36ebdbf1aa1 100644 --- a/src/vs/editor/contrib/inlineEdits/browser/commands.ts +++ b/src/vs/editor/contrib/inlineEdits/browser/commands.ts @@ -5,17 +5,16 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; -import { transaction } from '../../../../base/common/observable.js'; -import { asyncTransaction } from '../../../../base/common/observableInternal/base.js'; +import { asyncTransaction, transaction } from '../../../../base/common/observable.js'; +import * as nls from '../../../../nls.js'; +import { MenuId } from '../../../../platform/actions/common/actions.js'; +import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; import { ICodeEditor } from '../../../browser/editorBrowser.js'; import { EditorAction, ServicesAccessor } from '../../../browser/editorExtensions.js'; import { EmbeddedCodeEditorWidget } from '../../../browser/widget/codeEditor/embeddedCodeEditorWidget.js'; import { EditorContextKeys } from '../../../common/editorContextKeys.js'; import { inlineEditAcceptId, inlineEditVisible, showNextInlineEditActionId, showPreviousInlineEditActionId } from './consts.js'; import { InlineEditsController } from './inlineEditsController.js'; -import * as nls from '../../../../nls.js'; -import { MenuId } from '../../../../platform/actions/common/actions.js'; -import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; function labelAndAlias(str: nls.ILocalizedString): { label: string; alias: string } { diff --git a/src/vs/editor/contrib/inlineEdits/browser/inlineEditsController.ts b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsController.ts index 784c3d57231..893cb11d931 100644 --- a/src/vs/editor/contrib/inlineEdits/browser/inlineEditsController.ts +++ b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsController.ts @@ -3,22 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { readHotReloadableExport } from '../../../../base/common/hotReloadHelpers.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; -import { derived, derivedObservableWithCache, IReader, ISettableObservable, observableValue } from '../../../../base/common/observable.js'; -import { derivedDisposable, derivedWithSetter } from '../../../../base/common/observableInternal/derived.js'; +import { derived, derivedDisposable, derivedObservableWithCache, derivedWithSetter, IReader, ISettableObservable, observableValue } from '../../../../base/common/observable.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { bindContextKey, observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; import { ICodeEditor } from '../../../browser/editorBrowser.js'; import { observableCodeEditor } from '../../../browser/observableCodeEditor.js'; -import { readHotReloadableExport } from '../../../../base/common/hotReloadHelpers.js'; import { Selection } from '../../../common/core/selection.js'; import { ILanguageFeatureDebounceService } from '../../../common/services/languageFeatureDebounce.js'; import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js'; import { inlineEditVisible, isPinnedContextKey } from './consts.js'; import { InlineEditsModel } from './inlineEditsModel.js'; import { InlineEditsWidget } from './inlineEditsWidget.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { bindContextKey, observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; export class InlineEditsController extends Disposable { static ID = 'editor.contrib.inlineEditsController'; diff --git a/src/vs/editor/contrib/inlineEdits/browser/inlineEditsModel.ts b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsModel.ts index 98e8b9909be..5b226750016 100644 --- a/src/vs/editor/contrib/inlineEdits/browser/inlineEditsModel.ts +++ b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsModel.ts @@ -8,8 +8,7 @@ import { CancellationToken, cancelOnDispose } from '../../../../base/common/canc import { itemsEquals, structuralEquals } from '../../../../base/common/equals.js'; import { BugIndicatingError } from '../../../../base/common/errors.js'; import { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; -import { IObservable, ISettableObservable, ITransaction, ObservablePromise, derived, derivedHandleChanges, derivedOpts, disposableObservableValue, observableSignal, observableValue, recomputeInitiallyAndOnChange, subtransaction } from '../../../../base/common/observable.js'; -import { derivedDisposable } from '../../../../base/common/observableInternal/derived.js'; +import { IObservable, ISettableObservable, ITransaction, ObservablePromise, derived, derivedDisposable, derivedHandleChanges, derivedOpts, disposableObservableValue, observableSignal, observableValue, recomputeInitiallyAndOnChange, subtransaction } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; import { ICodeEditor } from '../../../browser/editorBrowser.js'; import { IDiffProviderFactoryService } from '../../../browser/widget/diffEditor/diffProviderFactoryService.js'; diff --git a/src/vs/editor/contrib/inlineEdits/browser/inlineEditsWidget.ts b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsWidget.ts index f974af7800c..3da0aedd94a 100644 --- a/src/vs/editor/contrib/inlineEdits/browser/inlineEditsWidget.ts +++ b/src/vs/editor/contrib/inlineEdits/browser/inlineEditsWidget.ts @@ -6,9 +6,10 @@ import { h, svgElem } from '../../../../base/browser/dom.js'; import { DEFAULT_FONT_FAMILY } from '../../../../base/browser/fonts.js'; import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; -import { autorun, constObservable, derived, IObservable, ISettableObservable } from '../../../../base/common/observable.js'; -import { derivedWithSetter } from '../../../../base/common/observableInternal/derived.js'; -import './inlineEditsWidget.css'; +import { autorun, constObservable, derived, derivedWithSetter, IObservable, ISettableObservable } from '../../../../base/common/observable.js'; +import { MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; +import { MenuId } from '../../../../platform/actions/common/actions.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ICodeEditor } from '../../../browser/editorBrowser.js'; import { EditorExtensionsRegistry } from '../../../browser/editorExtensions.js'; import { observableCodeEditor } from '../../../browser/observableCodeEditor.js'; @@ -24,9 +25,7 @@ import { TextModel } from '../../../common/model/textModel.js'; import { ContextMenuController } from '../../contextmenu/browser/contextmenu.js'; import { PlaceholderTextContribution } from '../../placeholderText/browser/placeholderTextContribution.js'; import { SuggestController } from '../../suggest/browser/suggestController.js'; -import { MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; -import { MenuId } from '../../../../platform/actions/common/actions.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import './inlineEditsWidget.css'; export class InlineEdit { constructor( diff --git a/src/vs/editor/contrib/placeholderText/browser/placeholderTextContribution.ts b/src/vs/editor/contrib/placeholderText/browser/placeholderTextContribution.ts index 492d439ed23..b06224c3e9d 100644 --- a/src/vs/editor/contrib/placeholderText/browser/placeholderTextContribution.ts +++ b/src/vs/editor/contrib/placeholderText/browser/placeholderTextContribution.ts @@ -6,9 +6,7 @@ import { h } from '../../../../base/browser/dom.js'; import { structuralEquals } from '../../../../base/common/equals.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; -import { autorun, constObservable, derivedObservableWithCache, derivedOpts, IObservable, IReader } from '../../../../base/common/observable.js'; -import { DebugOwner } from '../../../../base/common/observableInternal/debugName.js'; -import { derivedWithStore } from '../../../../base/common/observableInternal/derived.js'; +import { autorun, constObservable, DebugOwner, derivedObservableWithCache, derivedOpts, derivedWithStore, IObservable, IReader } from '../../../../base/common/observable.js'; import { ICodeEditor } from '../../../browser/editorBrowser.js'; import { observableCodeEditor } from '../../../browser/observableCodeEditor.js'; import { EditorOption } from '../../../common/config/editorOptions.js'; diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts index ccdce386167..882f7a9dfdb 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts @@ -139,6 +139,7 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { const isWidgetHeightZero = this._isWidgetHeightZero(_state); const state = isWidgetHeightZero ? undefined : _state; const rebuildFromLine = isWidgetHeightZero ? 0 : this._findLineToRebuildWidgetFrom(_state, _rebuildFromLine); + this._renderRootNode(state, foldingModel, rebuildFromLine); this._previousState = _state; } diff --git a/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts b/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts index 656b2cf8ef9..4af05cb5aa3 100644 --- a/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts +++ b/src/vs/editor/contrib/wordHighlighter/browser/wordHighlighter.ts @@ -10,6 +10,13 @@ import { CancellationToken } from '../../../../base/common/cancellation.js'; import { onUnexpectedError, onUnexpectedExternalError } from '../../../../base/common/errors.js'; import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; +import { ResourceMap } from '../../../../base/common/map.js'; +import { matchesScheme, Schemas } from '../../../../base/common/network.js'; +import { isEqual } from '../../../../base/common/resources.js'; +import { URI } from '../../../../base/common/uri.js'; +import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { IActiveCodeEditor, ICodeEditor, isDiffEditor } from '../../../browser/editorBrowser.js'; import { EditorAction, EditorContributionInstantiation, IActionOptions, registerEditorAction, registerEditorContribution, registerModelAndPositionCommand } from '../../../browser/editorExtensions.js'; import { ICodeEditorService } from '../../../browser/services/codeEditorService.js'; @@ -21,20 +28,15 @@ import { IWordAtPosition } from '../../../common/core/wordHelper.js'; import { CursorChangeReason, ICursorPositionChangedEvent } from '../../../common/cursorEvents.js'; import { IDiffEditor, IEditorContribution, IEditorDecorationsCollection } from '../../../common/editorCommon.js'; import { EditorContextKeys } from '../../../common/editorContextKeys.js'; +import { registerEditorFeature } from '../../../common/editorFeatures.js'; import { LanguageFeatureRegistry } from '../../../common/languageFeatureRegistry.js'; import { DocumentHighlight, DocumentHighlightProvider, MultiDocumentHighlightProvider } from '../../../common/languages.js'; +import { score } from '../../../common/languageSelector.js'; import { IModelDeltaDecoration, ITextModel, shouldSynchronizeModel } from '../../../common/model.js'; import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js'; +import { ITextModelService } from '../../../common/services/resolverService.js'; import { getHighlightDecorationOptions } from './highlightDecorations.js'; -import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; -import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { matchesScheme, Schemas } from '../../../../base/common/network.js'; -import { ResourceMap } from '../../../../base/common/map.js'; -import { score } from '../../../common/languageSelector.js'; -import { isEqual } from '../../../../base/common/resources.js'; import { TextualMultiDocumentHighlightFeature } from './textualHighlightProvider.js'; -import { registerEditorFeature } from '../../../common/editorFeatures.js'; const ctxHasWordHighlights = new RawContextKey('hasWordHighlights', false); @@ -58,7 +60,7 @@ export function getOccurrencesAtPosition(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, wordSeparators: string, token: CancellationToken, otherModels: ITextModel[]): Promise | null | undefined> { +export function getOccurrencesAcrossMultipleModels(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, token: CancellationToken, otherModels: ITextModel[]): Promise | null | undefined> { const orderedByScore = registry.ordered(model); // in order of score ask the occurrences provider @@ -84,10 +86,9 @@ interface IOccurenceAtPositionRequest { interface IWordHighlighterQuery { modelInfo: { - model: ITextModel; + modelURI: URI; selection: Selection; } | null; - readonly word: IWordAtPosition | null; } abstract class OccurenceAtPositionRequest implements IOccurenceAtPositionRequest { @@ -175,7 +176,7 @@ class MultiModelOccurenceRequest extends OccurenceAtPositionRequest { } protected override _compute(model: ITextModel, selection: Selection, wordSeparators: string, token: CancellationToken): Promise> { - return getOccurrencesAcrossMultipleModels(this._providers, model, selection.getPosition(), wordSeparators, token, this._otherModels).then(value => { + return getOccurrencesAcrossMultipleModels(this._providers, model, selection.getPosition(), token, this._otherModels).then(value => { if (!value) { return new ResourceMap(); } @@ -185,11 +186,11 @@ class MultiModelOccurenceRequest extends OccurenceAtPositionRequest { } -function computeOccurencesAtPosition(registry: LanguageFeatureRegistry, model: ITextModel, selection: Selection, word: IWordAtPosition | null, wordSeparators: string): IOccurenceAtPositionRequest { +function computeOccurencesAtPosition(registry: LanguageFeatureRegistry, model: ITextModel, selection: Selection, wordSeparators: string): IOccurenceAtPositionRequest { return new SemanticOccurenceAtPositionRequest(model, selection, wordSeparators, registry); } -function computeOccurencesMultiModel(registry: LanguageFeatureRegistry, model: ITextModel, selection: Selection, word: IWordAtPosition | null, wordSeparators: string, otherModels: ITextModel[]): IOccurenceAtPositionRequest { +function computeOccurencesMultiModel(registry: LanguageFeatureRegistry, model: ITextModel, selection: Selection, wordSeparators: string, otherModels: ITextModel[]): IOccurenceAtPositionRequest { return new MultiModelOccurenceRequest(model, selection, wordSeparators, registry, otherModels); } @@ -207,7 +208,10 @@ class WordHighlighter { private readonly model: ITextModel; private readonly decorations: IEditorDecorationsCollection; private readonly toUnhook = new DisposableStore(); + + private readonly textModelService: ITextModelService; private readonly codeEditorService: ICodeEditorService; + private occurrencesHighlight: string; private workerRequestTokenId: number = 0; @@ -221,20 +225,31 @@ class WordHighlighter { private readonly _hasWordHighlights: IContextKey; private _ignorePositionChangeEvent: boolean; - private readonly runDelayer: Delayer = this.toUnhook.add(new Delayer(50)); + private readonly runDelayer: Delayer = this.toUnhook.add(new Delayer(25)); private static storedDecorationIDs: ResourceMap = new ResourceMap(); private static query: IWordHighlighterQuery | null = null; - constructor(editor: IActiveCodeEditor, providers: LanguageFeatureRegistry, multiProviders: LanguageFeatureRegistry, contextKeyService: IContextKeyService, @ICodeEditorService codeEditorService: ICodeEditorService) { + constructor( + editor: IActiveCodeEditor, + providers: LanguageFeatureRegistry, + multiProviders: LanguageFeatureRegistry, + contextKeyService: IContextKeyService, + @ITextModelService textModelService: ITextModelService, + @ICodeEditorService codeEditorService: ICodeEditorService, + ) { this.editor = editor; this.providers = providers; this.multiDocumentProviders = multiProviders; + this.codeEditorService = codeEditorService; + this.textModelService = textModelService; + this._hasWordHighlights = ctxHasWordHighlights.bindTo(contextKeyService); this._ignorePositionChangeEvent = false; this.occurrencesHighlight = this.editor.getOption(EditorOption.occurrencesHighlight); this.model = this.editor.getModel(); + this.toUnhook.add(editor.onDidChangeCursorPosition((e: ICursorPositionChangedEvent) => { if (this._ignorePositionChangeEvent) { // We are changing the position => ignore this event @@ -267,10 +282,8 @@ class WordHighlighter { this.toUnhook.add(editor.onDidChangeModel((e) => { if (!e.newModelUrl && e.oldModelUrl) { this._stopSingular(); - } else { - if (WordHighlighter.query) { - this._run(); - } + } else if (WordHighlighter.query) { + this._run(); } })); this.toUnhook.add(editor.onDidChangeConfiguration((e) => { @@ -282,7 +295,7 @@ class WordHighlighter { this._stopAll(); break; case 'singleFile': - this._stopAll(WordHighlighter.query?.modelInfo?.model); + this._stopAll(WordHighlighter.query?.modelInfo?.modelURI); break; case 'multiFile': if (WordHighlighter.query) { @@ -295,6 +308,19 @@ class WordHighlighter { } } })); + this.toUnhook.add(editor.onDidBlurEditorWidget(() => { + // logic is as follows + // - didBlur => active null => stopall + // - didBlur => active nb => if this.editor is notebook, do nothing (new cell, so we don't want to stopAll) + // active nb => if this.editor is NOT nb, stopAll + + const activeEditor = this.codeEditorService.getFocusedCodeEditor(); + if (!activeEditor) { // clicked into nb cell list, outline, terminal, etc + this._stopAll(); + } else if (activeEditor.getModel()?.uri.scheme === Schemas.vscodeNotebookCell && this.editor.getModel()?.uri.scheme !== Schemas.vscodeNotebookCell) { // switched tabs from non-nb to nb + this._stopAll(); + } + })); this.decorations = this.editor.createDecorationsCollection(); this.workerRequestTokenId = 0; @@ -396,12 +422,12 @@ class WordHighlighter { } } - private _removeAllDecorations(preservedModel?: ITextModel): void { + private _removeAllDecorations(preservedModel?: URI): void { const currentEditors = this.codeEditorService.listCodeEditors(); const deleteURI = []; // iterate over editors and store models in currentModels for (const editor of currentEditors) { - if (!editor.hasModel() || isEqual(editor.getModel().uri, preservedModel?.uri)) { + if (!editor.hasModel() || isEqual(editor.getModel().uri, preservedModel)) { continue; } @@ -435,7 +461,7 @@ class WordHighlighter { this._removeSingleDecorations(); if (this.editor.hasTextFocus()) { - if (this.editor.getModel()?.uri.scheme !== Schemas.vscodeNotebookCell && WordHighlighter.query?.modelInfo?.model.uri.scheme !== Schemas.vscodeNotebookCell) { // clear query if focused non-nb editor + if (this.editor.getModel()?.uri.scheme !== Schemas.vscodeNotebookCell && WordHighlighter.query?.modelInfo?.modelURI.scheme !== Schemas.vscodeNotebookCell) { // clear query if focused non-nb editor WordHighlighter.query = null; this._run(); // TODO: @Yoyokrazy -- investigate why we need a full rerun here. likely addressed a case/patch in the first iteration of this feature } else { // remove modelInfo to account for nb cell being disposed @@ -464,7 +490,7 @@ class WordHighlighter { } } - private _stopAll(preservedModel?: ITextModel): void { + private _stopAll(preservedModel?: URI): void { // Remove any existing decorations // TODO: @Yoyokrazy -- this triggers as notebooks scroll, causing highlights to disappear momentarily. // maybe a nb type check? @@ -582,9 +608,8 @@ class WordHighlighter { return currentModels; } - private _run(multiFileConfigChange?: boolean): void { + private async _run(multiFileConfigChange?: boolean): Promise { - let workerRequestIsValid; const hasTextFocus = this.editor.hasTextFocus(); if (!hasTextFocus) { // new nb cell scrolled in, didChangeModel fires @@ -615,41 +640,18 @@ class WordHighlighter { return; } - // All the effort below is trying to achieve this: - // - when cursor is moved to a word, trigger immediately a findOccurrences request - // - 250ms later after the last cursor move event, render the occurrences - // - no flickering! - workerRequestIsValid = (this.workerRequest && this.workerRequest.isValid(this.model, editorSelection, this.decorations)); - WordHighlighter.query = { modelInfo: { - model: this.model, + modelURI: this.model.uri, selection: editorSelection, - }, - word: word + } }; } - // There are 4 cases: - // a) old workerRequest is valid & completed, renderDecorationsTimer fired - // b) old workerRequest is valid & completed, renderDecorationsTimer not fired - // c) old workerRequest is valid, but not completed - // d) old workerRequest is not valid - - // For a) no action is needed - // For c), member 'lastCursorPositionChangeTime' will be used when installing the timer so no action is needed this.lastCursorPositionChangeTime = (new Date()).getTime(); - if (workerRequestIsValid) { - if (this.workerRequestCompleted && this.renderDecorationsTimer !== -1) { - // case b) - // Delay the firing of renderDecorationsTimer by an extra 250 ms - clearTimeout(this.renderDecorationsTimer); - this.renderDecorationsTimer = -1; - this._beginRenderDecorations(); - } - } else if (isEqual(this.editor.getModel().uri, WordHighlighter.query.modelInfo?.model.uri)) { // only trigger new worker requests from the primary model that initiated the query + if (isEqual(this.editor.getModel().uri, WordHighlighter.query.modelInfo?.modelURI)) { // only trigger new worker requests from the primary model that initiated the query // case d) // check if the new queried word is contained in the range of a stored decoration for this model @@ -664,7 +666,7 @@ class WordHighlighter { // stop all previous actions if new word is highlighted // if we trigger the run off a setting change -> multifile highlighting, we do not want to remove decorations from this model - this._stopAll(multiFileConfigChange ? this.model : undefined); + this._stopAll(multiFileConfigChange ? this.model.uri : undefined); const myRequestId = ++this.workerRequestTokenId; this.workerRequestCompleted = false; @@ -675,10 +677,35 @@ class WordHighlighter { // 1) we have text focus, and a valid query was updated. // 2) we do not have text focus, and a valid query is cached. // the query will ALWAYS have the correct data for the current highlight request, so it can always be passed to the workerRequest safely - if (!WordHighlighter.query || !WordHighlighter.query.modelInfo || WordHighlighter.query.modelInfo.model.isDisposed()) { + if (!WordHighlighter.query || !WordHighlighter.query.modelInfo) { return; } - this.workerRequest = this.computeWithModel(WordHighlighter.query.modelInfo.model, WordHighlighter.query.modelInfo.selection, WordHighlighter.query.word, otherModelsToHighlight); + const queryModelRef = await this.textModelService.createModelReference(WordHighlighter.query.modelInfo.modelURI); + const queryModel = queryModelRef.object.textEditorModel; + this.workerRequest = this.computeWithModel(queryModel, WordHighlighter.query.modelInfo.selection, otherModelsToHighlight); + + this.workerRequest?.result.then(data => { + if (myRequestId === this.workerRequestTokenId) { + this.workerRequestCompleted = true; + this.workerRequestValue = data || []; + this._beginRenderDecorations(); + } + }, onUnexpectedError); + } else if (this.model.uri.scheme === Schemas.vscodeNotebookCell) { + // new wordHighlighter coming from a different model, NOT the query model, need to create a textModel ref + + // this._stopAll(multiFileConfigChange ? this.model.uri : undefined); + + const myRequestId = ++this.workerRequestTokenId; + this.workerRequestCompleted = false; + + if (!WordHighlighter.query || !WordHighlighter.query.modelInfo) { + return; + } + + const queryModelRef = await this.textModelService.createModelReference(WordHighlighter.query.modelInfo.modelURI); + const queryModel = queryModelRef.object.textEditorModel; + this.workerRequest = this.computeWithModel(queryModel, WordHighlighter.query.modelInfo.selection, [this.model]); this.workerRequest?.result.then(data => { if (myRequestId === this.workerRequestTokenId) { @@ -690,11 +717,11 @@ class WordHighlighter { } } - private computeWithModel(model: ITextModel, selection: Selection, word: IWordAtPosition | null, otherModels: ITextModel[]): IOccurenceAtPositionRequest | null { + private computeWithModel(model: ITextModel, selection: Selection, otherModels: ITextModel[]): IOccurenceAtPositionRequest | null { if (!otherModels.length) { - return computeOccurencesAtPosition(this.providers, model, selection, word, this.editor.getOption(EditorOption.wordSeparators)); + return computeOccurencesAtPosition(this.providers, model, selection, this.editor.getOption(EditorOption.wordSeparators)); } else { - return computeOccurencesMultiModel(this.multiDocumentProviders, model, selection, word, this.editor.getOption(EditorOption.wordSeparators), otherModels); + return computeOccurencesMultiModel(this.multiDocumentProviders, model, selection, this.editor.getOption(EditorOption.wordSeparators), otherModels); } } @@ -755,6 +782,9 @@ class WordHighlighter { } } } + + // clear the worker request when decorations are completed + this.workerRequest = null; } public dispose(): void { @@ -773,16 +803,26 @@ export class WordHighlighterContribution extends Disposable implements IEditorCo private _wordHighlighter: WordHighlighter | null; - constructor(editor: ICodeEditor, @IContextKeyService contextKeyService: IContextKeyService, @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, @ICodeEditorService codeEditorService: ICodeEditorService) { + constructor( + editor: ICodeEditor, + @IContextKeyService contextKeyService: IContextKeyService, + @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, + @ICodeEditorService codeEditorService: ICodeEditorService, + @ITextModelService textModelService: ITextModelService, + ) { super(); this._wordHighlighter = null; const createWordHighlighterIfPossible = () => { if (editor.hasModel() && !editor.getModel().isTooLargeForTokenization()) { - this._wordHighlighter = new WordHighlighter(editor, languageFeaturesService.documentHighlightProvider, languageFeaturesService.multiDocumentHighlightProvider, contextKeyService, codeEditorService); + this._wordHighlighter = new WordHighlighter(editor, languageFeaturesService.documentHighlightProvider, languageFeaturesService.multiDocumentHighlightProvider, contextKeyService, textModelService, codeEditorService); } }; this._register(editor.onDidChangeModel((e) => { if (this._wordHighlighter) { + if (!e.newModelUrl && e.oldModelUrl?.scheme !== Schemas.vscodeNotebookCell) { // happens when switching tabs to a notebook that has focus in the cell list, no new model URI (this also doesn't make it to the wordHighlighter, bc no editor.hasModel) + this.wordHighlighter?.stop(); + } + this._wordHighlighter.dispose(); this._wordHighlighter = null; } diff --git a/src/vs/editor/editor.all.ts b/src/vs/editor/editor.all.ts index df0027f1683..f7aca2c64c0 100644 --- a/src/vs/editor/editor.all.ts +++ b/src/vs/editor/editor.all.ts @@ -30,6 +30,7 @@ import './contrib/inlineProgress/browser/inlineProgress.js'; import './contrib/gotoSymbol/browser/goToCommands.js'; import './contrib/gotoSymbol/browser/link/goToDefinitionAtPosition.js'; import './contrib/gotoError/browser/gotoError.js'; +import './contrib/gpu/browser/gpuActions.js'; import './contrib/hover/browser/hoverContribution.js'; import './contrib/indentation/browser/indentation.js'; import './contrib/inlayHints/browser/inlayHintsContribution.js'; diff --git a/src/vs/editor/standalone/browser/standaloneTreeSitterService.ts b/src/vs/editor/standalone/browser/standaloneTreeSitterService.ts index aedfbdceb0d..90257943088 100644 --- a/src/vs/editor/standalone/browser/standaloneTreeSitterService.ts +++ b/src/vs/editor/standalone/browser/standaloneTreeSitterService.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// eslint-disable-next-line local/code-import-patterns import type { Parser } from '@vscode/tree-sitter-wasm'; import { Event } from '../../../base/common/event.js'; import { ITextModel } from '../../common/model.js'; diff --git a/src/vs/editor/test/browser/testCodeEditor.ts b/src/vs/editor/test/browser/testCodeEditor.ts index f00eee40437..72da1933953 100644 --- a/src/vs/editor/test/browser/testCodeEditor.ts +++ b/src/vs/editor/test/browser/testCodeEditor.ts @@ -179,7 +179,7 @@ function _withTestCodeEditor(arg: ITextModel | string | string[] | ITextBufferFa disposables.dispose(); } -export function createCodeEditorServices(disposables: DisposableStore, services: ServiceCollection = new ServiceCollection()): TestInstantiationService { +export function createCodeEditorServices(disposables: Pick, services: ServiceCollection = new ServiceCollection()): TestInstantiationService { const serviceIdentifiers: ServiceIdentifier[] = []; const define = (id: ServiceIdentifier, ctor: new (...args: any[]) => T) => { if (!services.has(id)) { diff --git a/src/vs/editor/test/browser/view/gpu/atlas/testUtil.ts b/src/vs/editor/test/browser/view/gpu/atlas/testUtil.ts new file mode 100644 index 00000000000..73d1e167f1e --- /dev/null +++ b/src/vs/editor/test/browser/view/gpu/atlas/testUtil.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { fail, ok } from 'assert'; +import type { ITextureAtlasPageGlyph } from '../../../../../browser/gpu/atlas/atlas.js'; +import { TextureAtlas } from '../../../../../browser/gpu/atlas/textureAtlas.js'; +import { isNumber } from '../../../../../../base/common/types.js'; +import { ensureNonNullable } from '../../../../../browser/gpu/gpuUtils.js'; + +export function assertIsValidGlyph(glyph: Readonly | undefined, atlasOrSource: TextureAtlas | OffscreenCanvas) { + if (glyph === undefined) { + fail('glyph is undefined'); + } + const pageW = atlasOrSource instanceof TextureAtlas ? atlasOrSource.pageSize : atlasOrSource.width; + const pageH = atlasOrSource instanceof TextureAtlas ? atlasOrSource.pageSize : atlasOrSource.width; + const source = atlasOrSource instanceof TextureAtlas ? atlasOrSource.pages[glyph.pageIndex].source : atlasOrSource; + + // (x,y) are valid coordinates + ok(isNumber(glyph.x)); + ok(glyph.x >= 0); + ok(glyph.x < pageW); + ok(isNumber(glyph.y)); + ok(glyph.y >= 0); + ok(glyph.y < pageH); + + // (w,h) are valid dimensions + ok(isNumber(glyph.w)); + ok(glyph.w > 0); + ok(glyph.w <= pageW); + ok(isNumber(glyph.h)); + ok(glyph.h > 0); + ok(glyph.h <= pageH); + + // (originOffsetX, originOffsetY) are valid offsets + ok(isNumber(glyph.originOffsetX)); + ok(isNumber(glyph.originOffsetY)); + + // (x,y) + (w,h) are within the bounds of the atlas + ok(glyph.x + glyph.w <= pageW); + ok(glyph.y + glyph.h <= pageH); + + // Each of the glyph's outer pixel edges contain at least 1 non-transparent pixel + const ctx = ensureNonNullable(source.getContext('2d')); + const edges = [ + ctx.getImageData(glyph.x, glyph.y, glyph.w, 1).data, + ctx.getImageData(glyph.x, glyph.y + glyph.h - 1, glyph.w, 1).data, + ctx.getImageData(glyph.x, glyph.y, 1, glyph.h).data, + ctx.getImageData(glyph.x + glyph.w - 1, glyph.y, 1, glyph.h).data, + ]; + for (const edge of edges) { + ok(edge.some(color => (color & 0xFF) !== 0)); + } +} diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts new file mode 100644 index 00000000000..c745bb7b4cd --- /dev/null +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlas.test.ts @@ -0,0 +1,129 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { strictEqual, throws } from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import type { IGlyphRasterizer, IRasterizedGlyph } from '../../../../../browser/gpu/raster/raster.js'; +import { ensureNonNullable } from '../../../../../browser/gpu/gpuUtils.js'; +import type { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; +import { TextureAtlas } from '../../../../../browser/gpu/atlas/textureAtlas.js'; +import { createCodeEditorServices } from '../../../testCodeEditor.js'; +import { assertIsValidGlyph } from './testUtil.js'; +import { TextureAtlasSlabAllocator } from '../../../../../browser/gpu/atlas/textureAtlasSlabAllocator.js'; + +const blackInt = 0x000000FF; + +let lastUniqueGlyph: string | undefined; +function getUniqueGlyphId(): [chars: string, tokenFg: number] { + if (!lastUniqueGlyph) { + lastUniqueGlyph = 'a'; + } else { + lastUniqueGlyph = String.fromCharCode(lastUniqueGlyph.charCodeAt(0) + 1); + } + return [lastUniqueGlyph, blackInt]; +} + +class TestGlyphRasterizer implements IGlyphRasterizer { + readonly id = 0; + nextGlyphColor: [number, number, number, number] = [0, 0, 0, 0]; + nextGlyphDimensions: [number, number] = [0, 0]; + rasterizeGlyph(chars: string, metadata: number, colorMap: string[]): Readonly { + const w = this.nextGlyphDimensions[0]; + const h = this.nextGlyphDimensions[1]; + if (w === 0 || h === 0) { + throw new Error('TestGlyphRasterizer.nextGlyphDimensions must be set to a non-zero value before calling rasterizeGlyph'); + } + const imageData = new ImageData(w, h); + let i = 0; + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + const [r, g, b, a] = this.nextGlyphColor; + i = (y * w + x) * 4; + imageData.data[i + 0] = r; + imageData.data[i + 1] = g; + imageData.data[i + 2] = b; + imageData.data[i + 3] = a; + } + } + const canvas = new OffscreenCanvas(w, h); + const ctx = ensureNonNullable(canvas.getContext('2d')); + ctx.putImageData(imageData, 0, 0); + return { + source: canvas, + boundingBox: { top: 0, left: 0, bottom: h - 1, right: w - 1 }, + originOffset: { x: 0, y: 0 }, + }; + } +} + +suite('TextureAtlas', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + suiteSetup(() => { + lastUniqueGlyph = undefined; + }); + + let instantiationService: IInstantiationService; + + let atlas: TextureAtlas; + let glyphRasterizer: TestGlyphRasterizer; + + setup(() => { + instantiationService = createCodeEditorServices(store); + atlas = store.add(instantiationService.createInstance(TextureAtlas, 2, undefined)); + glyphRasterizer = new TestGlyphRasterizer(); + glyphRasterizer.nextGlyphDimensions = [1, 1]; + glyphRasterizer.nextGlyphColor = [0, 0, 0, 0xFF]; + }); + + test('get single glyph', () => { + assertIsValidGlyph(atlas.getGlyph(glyphRasterizer, ...getUniqueGlyphId()), atlas); + }); + + test('get multiple glyphs', () => { + atlas = store.add(instantiationService.createInstance(TextureAtlas, 32, undefined)); + for (let i = 0; i < 10; i++) { + assertIsValidGlyph(atlas.getGlyph(glyphRasterizer, ...getUniqueGlyphId()), atlas); + } + }); + + test('adding glyph to full page creates new page', () => { + let pageCount: number | undefined; + for (let i = 0; i < 4; i++) { + assertIsValidGlyph(atlas.getGlyph(glyphRasterizer, ...getUniqueGlyphId()), atlas); + if (pageCount === undefined) { + pageCount = atlas.pages.length; + } else { + strictEqual(atlas.pages.length, pageCount, 'the number of pages should not change when the page is being filled'); + } + } + assertIsValidGlyph(atlas.getGlyph(glyphRasterizer, ...getUniqueGlyphId()), atlas); + strictEqual(atlas.pages.length, pageCount! + 1, 'the 5th glyph should overflow to a new page'); + }); + + test('adding a glyph larger than the atlas', () => { + glyphRasterizer.nextGlyphDimensions = [3, 2]; + throws(() => atlas.getGlyph(glyphRasterizer, ...getUniqueGlyphId()), 'should throw when the glyph is too large, this should not happen in practice'); + }); + + test('adding a glyph larger than the standard slab size', () => { + glyphRasterizer.nextGlyphDimensions = [2, 2]; + atlas = store.add(instantiationService.createInstance(TextureAtlas, 32, { + allocatorType: (canvas, textureIndex) => new TextureAtlasSlabAllocator(canvas, textureIndex, { slabW: 1, slabH: 1 }) + })); + assertIsValidGlyph(atlas.getGlyph(glyphRasterizer, ...getUniqueGlyphId()), atlas); + }); + + test('adding a non-first glyph larger than the standard slab size, causing an overflow to a new page', () => { + atlas = store.add(instantiationService.createInstance(TextureAtlas, 2, { + allocatorType: (canvas, textureIndex) => new TextureAtlasSlabAllocator(canvas, textureIndex, { slabW: 1, slabH: 1 }) + })); + assertIsValidGlyph(atlas.getGlyph(glyphRasterizer, ...getUniqueGlyphId()), atlas); + strictEqual(atlas.pages.length, 1); + glyphRasterizer.nextGlyphDimensions = [2, 2]; + assertIsValidGlyph(atlas.getGlyph(glyphRasterizer, ...getUniqueGlyphId()), atlas); + strictEqual(atlas.pages.length, 2, 'the 2nd glyph should overflow to a new page with a larger slab size'); + }); +}); diff --git a/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts new file mode 100644 index 00000000000..92d354a5f78 --- /dev/null +++ b/src/vs/editor/test/browser/view/gpu/atlas/textureAtlasAllocator.test.ts @@ -0,0 +1,202 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { deepStrictEqual, strictEqual, throws } from 'assert'; +import type { IRasterizedGlyph } from '../../../../../browser/gpu/raster/raster.js'; +import { ensureNonNullable } from '../../../../../browser/gpu/gpuUtils.js'; +import type { ITextureAtlasAllocator } from '../../../../../browser/gpu/atlas/atlas.js'; +import { TextureAtlasShelfAllocator } from '../../../../../browser/gpu/atlas/textureAtlasShelfAllocator.js'; +import { TextureAtlasSlabAllocator, type TextureAtlasSlabAllocatorOptions } from '../../../../../browser/gpu/atlas/textureAtlasSlabAllocator.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; +import { assertIsValidGlyph } from './testUtil.js'; +import { BugIndicatingError } from '../../../../../../base/common/errors.js'; + +const blackArr = [0x00, 0x00, 0x00, 0xFF]; + +const pixel1x1 = createRasterizedGlyph(1, 1, [...blackArr]); +const pixel2x1 = createRasterizedGlyph(2, 1, [...blackArr, ...blackArr]); +const pixel1x2 = createRasterizedGlyph(1, 2, [...blackArr, ...blackArr]); + +function createRasterizedGlyph(w: number, h: number, data: ArrayLike): IRasterizedGlyph { + strictEqual(w * h * 4, data.length); + const source = new OffscreenCanvas(w, h); + const imageData = new ImageData(w, h); + imageData.data.set(data); + ensureNonNullable(source.getContext('2d')).putImageData(imageData, 0, 0); + return { + source, + boundingBox: { top: 0, left: 0, bottom: h - 1, right: w - 1 }, + originOffset: { x: 0, y: 0 }, + }; +} + +function allocateAndAssert(allocator: ITextureAtlasAllocator, rasterizedGlyph: IRasterizedGlyph, expected: { x: number; y: number; w: number; h: number } | undefined): void { + const actual = allocator.allocate(rasterizedGlyph); + if (!actual) { + strictEqual(actual, expected); + return; + } + deepStrictEqual({ + x: actual.x, + y: actual.y, + w: actual.w, + h: actual.h, + }, expected); +} + +function initShelfAllocator(w: number, h: number): { canvas: OffscreenCanvas; allocator: TextureAtlasShelfAllocator } { + const canvas = new OffscreenCanvas(w, h); + const allocator = new TextureAtlasShelfAllocator(canvas, 0); + return { canvas, allocator }; +} + +function initSlabAllocator(w: number, h: number, options?: TextureAtlasSlabAllocatorOptions): { canvas: OffscreenCanvas; allocator: TextureAtlasSlabAllocator } { + const canvas = new OffscreenCanvas(w, h); + const allocator = new TextureAtlasSlabAllocator(canvas, 0, options); + return { canvas, allocator }; +} + +const allocatorDefinitions: { name: string; initAllocator: (w: number, h: number) => { canvas: OffscreenCanvas; allocator: ITextureAtlasAllocator } }[] = [ + { name: 'shelf', initAllocator: initShelfAllocator }, + { name: 'slab', initAllocator: initSlabAllocator }, +]; + +suite('TextureAtlasAllocator', () => { + ensureNoDisposablesAreLeakedInTestSuite(); + + suite('shared tests', () => { + for (const { name, initAllocator } of allocatorDefinitions) { + test(`(${name}) single allocation`, () => { + const { canvas, allocator } = initAllocator(2, 2); + assertIsValidGlyph(allocator.allocate(pixel1x1), canvas); + }); + // Skipping because it fails unexpectedly on web only when asserting the error message + test.skip(`(${name}) glyph too large for canvas`, () => { + const { allocator } = initAllocator(1, 1); + throws(() => allocateAndAssert(allocator, pixel2x1, undefined), new BugIndicatingError('Glyph is too large for the atlas page')); + }); + } + }); + + suite('TextureAtlasShelfAllocator', () => { + const initAllocator = initShelfAllocator; + + test('single allocation', () => { + const { allocator } = initAllocator(2, 2); + // 1o + // oo + allocateAndAssert(allocator, pixel1x1, { x: 0, y: 0, w: 1, h: 1 }); + }); + test('wrapping', () => { + const { allocator } = initAllocator(5, 4); + + // 1233o + // o2ooo + // ooooo + // ooooo + allocateAndAssert(allocator, pixel1x1, { x: 0, y: 0, w: 1, h: 1 }); + allocateAndAssert(allocator, pixel1x2, { x: 1, y: 0, w: 1, h: 2 }); + allocateAndAssert(allocator, pixel2x1, { x: 2, y: 0, w: 2, h: 1 }); + + // 1233x + // x2xxx + // 44556 + // ooooo + allocateAndAssert(allocator, pixel2x1, { x: 0, y: 2, w: 2, h: 1 }); + allocateAndAssert(allocator, pixel2x1, { x: 2, y: 2, w: 2, h: 1 }); + allocateAndAssert(allocator, pixel1x1, { x: 4, y: 2, w: 1, h: 1 }); + + // 1233x + // x2xxx + // 44556 + // 7oooo + allocateAndAssert(allocator, pixel1x1, { x: 0, y: 3, w: 1, h: 1 }); + }); + test('full', () => { + const { allocator } = initAllocator(3, 2); + // 122 + // 1oo + allocateAndAssert(allocator, pixel1x2, { x: 0, y: 0, w: 1, h: 2 }); + allocateAndAssert(allocator, pixel2x1, { x: 1, y: 0, w: 2, h: 1 }); + allocateAndAssert(allocator, pixel1x1, undefined); + }); + }); + + suite('TextureAtlasSlabAllocator', () => { + const initAllocator = initSlabAllocator; + + test('single allocation', () => { + const { allocator } = initAllocator(2, 2); + // 1o + // oo + allocateAndAssert(allocator, pixel1x1, { x: 0, y: 0, w: 1, h: 1 }); + }); + + test('single slab single glyph full', () => { + const { allocator } = initAllocator(1, 1, { slabW: 1, slabH: 1 }); + + // 1 + allocateAndAssert(allocator, pixel1x1, { x: 0, y: 0, w: 1, h: 1 }); + + allocateAndAssert(allocator, pixel1x1, undefined); + }); + + test('single slab multiple glyph full', () => { + const { allocator } = initAllocator(2, 2, { slabW: 2, slabH: 2 }); + + // 1 + // 1 + allocateAndAssert(allocator, pixel1x2, { x: 0, y: 0, w: 1, h: 2 }); + allocateAndAssert(allocator, pixel1x2, { x: 1, y: 0, w: 1, h: 2 }); + + allocateAndAssert(allocator, pixel1x2, undefined); + }); + + test('allocate 1x1 to multiple slabs until full', () => { + const { allocator } = initAllocator(4, 2, { slabW: 2, slabH: 2 }); + + // 12│oo + // 34│oo + allocateAndAssert(allocator, pixel1x1, { x: 0, y: 0, w: 1, h: 1 }); + allocateAndAssert(allocator, pixel1x1, { x: 1, y: 0, w: 1, h: 1 }); + allocateAndAssert(allocator, pixel1x1, { x: 0, y: 1, w: 1, h: 1 }); + allocateAndAssert(allocator, pixel1x1, { x: 1, y: 1, w: 1, h: 1 }); + + // 12│56 + // 34│78 + allocateAndAssert(allocator, pixel1x1, { x: 2, y: 0, w: 1, h: 1 }); + allocateAndAssert(allocator, pixel1x1, { x: 3, y: 0, w: 1, h: 1 }); + allocateAndAssert(allocator, pixel1x1, { x: 2, y: 1, w: 1, h: 1 }); + allocateAndAssert(allocator, pixel1x1, { x: 3, y: 1, w: 1, h: 1 }); + + allocateAndAssert(allocator, pixel1x1, undefined); + }); + + test('glyph too large for slab (increase slab size for first glyph)', () => { + const { allocator } = initAllocator(2, 2, { slabW: 1, slabH: 1 }); + allocateAndAssert(allocator, pixel2x1, { x: 0, y: 0, w: 2, h: 1 }); + }); + + test('glyph too large for slab (undefined as it\'s not the first glyph)', () => { + const { allocator } = initAllocator(2, 2, { slabW: 1, slabH: 1 }); + allocateAndAssert(allocator, pixel1x1, { x: 0, y: 0, w: 1, h: 1 }); + allocateAndAssert(allocator, pixel2x1, undefined); + }); + + test('separate slabs for different sized glyphs', () => { + const { allocator } = initAllocator(4, 2, { slabW: 2, slabH: 2 }); + + // 10│2o + // 00│2o + allocateAndAssert(allocator, pixel1x1, { x: 0, y: 0, w: 1, h: 1 }); + allocateAndAssert(allocator, pixel1x2, { x: 2, y: 0, w: 1, h: 2 }); + + // 14│23 + // 00│23 + allocateAndAssert(allocator, pixel1x2, { x: 3, y: 0, w: 1, h: 2 }); + allocateAndAssert(allocator, pixel1x1, { x: 1, y: 0, w: 1, h: 1 }); + }); + }); +}); diff --git a/src/vs/editor/test/browser/view/gpu/objectCollectionBuffer.test.ts b/src/vs/editor/test/browser/view/gpu/objectCollectionBuffer.test.ts new file mode 100644 index 00000000000..a0f112c9656 --- /dev/null +++ b/src/vs/editor/test/browser/view/gpu/objectCollectionBuffer.test.ts @@ -0,0 +1,82 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { deepStrictEqual, strictEqual, throws } from 'assert'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; +import { createObjectCollectionBuffer, type IObjectCollectionBuffer } from '../../../../browser/gpu/objectCollectionBuffer.js'; + +suite('ObjectCollectionBuffer', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + + function assertUsedData(buffer: IObjectCollectionBuffer, expected: number[]) { + deepStrictEqual(Array.from(buffer.view.subarray(0, buffer.viewUsedSize)), expected); + } + + test('createEntry', () => { + const buffer = store.add(createObjectCollectionBuffer([ + { name: 'a' }, + { name: 'b' }, + ] as const, 5)); + assertUsedData(buffer, []); + + store.add(buffer.createEntry({ a: 1, b: 2 })); + store.add(buffer.createEntry({ a: 3, b: 4 })); + store.add(buffer.createEntry({ a: 5, b: 6 })); + store.add(buffer.createEntry({ a: 7, b: 8 })); + store.add(buffer.createEntry({ a: 9, b: 10 })); + assertUsedData(buffer, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + }); + + test('createEntry beyond capacity', () => { + const buffer = store.add(createObjectCollectionBuffer([ + { name: 'a' }, + { name: 'b' }, + ] as const, 1)); + store.add(buffer.createEntry({ a: 1, b: 2 })); + throws(() => buffer.createEntry({ a: 3, b: 4 })); + }); + + test('dispose entry', () => { + const buffer = store.add(createObjectCollectionBuffer([ + { name: 'a' }, + { name: 'b' }, + ] as const, 5)); + store.add(buffer.createEntry({ a: 1, b: 2 })); + const entry1 = buffer.createEntry({ a: 3, b: 4 }); + store.add(buffer.createEntry({ a: 5, b: 6 })); + const entry2 = buffer.createEntry({ a: 7, b: 8 }); + store.add(buffer.createEntry({ a: 9, b: 10 })); + entry1.dispose(); + entry2.dispose(); + // Data from disposed entries is stale and doesn't need to be validated + assertUsedData(buffer, [1, 2, 5, 6, 9, 10]); + }); + + test('entry.get', () => { + const buffer = store.add(createObjectCollectionBuffer([ + { name: 'foo' }, + { name: 'bar' }, + ] as const, 5)); + const entry = store.add(buffer.createEntry({ foo: 1, bar: 2 })); + strictEqual(entry.get('foo'), 1); + strictEqual(entry.get('bar'), 2); + }); + + test('entry.set', () => { + const buffer = store.add(createObjectCollectionBuffer([ + { name: 'foo' }, + { name: 'bar' }, + ] as const, 5)); + const entry = store.add(buffer.createEntry({ foo: 1, bar: 2 })); + let changeCount = 0; + store.add(buffer.onDidChange(() => changeCount++)); + entry.set('foo', 3); + strictEqual(changeCount, 1); + strictEqual(entry.get('foo'), 3); + entry.set('bar', 4); + strictEqual(changeCount, 2); + strictEqual(entry.get('bar'), 4); + }); +}); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index a166882b2fa..0aeba15fab0 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -3680,6 +3680,11 @@ declare namespace monaco.editor { * Defaults to 'always'. */ matchBrackets?: 'never' | 'near' | 'always'; + /** + * Enable experimental rendering using WebGPU. + * Defaults to 'off'. + */ + experimentalGpuAcceleration?: 'on' | 'off'; /** * Enable experimental whitespace rendering. * Defaults to 'svg'. @@ -4908,119 +4913,120 @@ declare namespace monaco.editor { dropIntoEditor = 36, experimentalEditContextEnabled = 37, emptySelectionClipboard = 38, - experimentalWhitespaceRendering = 39, - extraEditorClassName = 40, - fastScrollSensitivity = 41, - find = 42, - fixedOverflowWidgets = 43, - folding = 44, - foldingStrategy = 45, - foldingHighlight = 46, - foldingImportsByDefault = 47, - foldingMaximumRegions = 48, - unfoldOnClickAfterEndOfLine = 49, - fontFamily = 50, - fontInfo = 51, - fontLigatures = 52, - fontSize = 53, - fontWeight = 54, - fontVariations = 55, - formatOnPaste = 56, - formatOnType = 57, - glyphMargin = 58, - gotoLocation = 59, - hideCursorInOverviewRuler = 60, - hover = 61, - inDiffEditor = 62, - inlineSuggest = 63, - inlineEdit = 64, - letterSpacing = 65, - lightbulb = 66, - lineDecorationsWidth = 67, - lineHeight = 68, - lineNumbers = 69, - lineNumbersMinChars = 70, - linkedEditing = 71, - links = 72, - matchBrackets = 73, - minimap = 74, - mouseStyle = 75, - mouseWheelScrollSensitivity = 76, - mouseWheelZoom = 77, - multiCursorMergeOverlapping = 78, - multiCursorModifier = 79, - multiCursorPaste = 80, - multiCursorLimit = 81, - occurrencesHighlight = 82, - overviewRulerBorder = 83, - overviewRulerLanes = 84, - padding = 85, - pasteAs = 86, - parameterHints = 87, - peekWidgetDefaultFocus = 88, - placeholder = 89, - definitionLinkOpensInPeek = 90, - quickSuggestions = 91, - quickSuggestionsDelay = 92, - readOnly = 93, - readOnlyMessage = 94, - renameOnType = 95, - renderControlCharacters = 96, - renderFinalNewline = 97, - renderLineHighlight = 98, - renderLineHighlightOnlyWhenFocus = 99, - renderValidationDecorations = 100, - renderWhitespace = 101, - revealHorizontalRightPadding = 102, - roundedSelection = 103, - rulers = 104, - scrollbar = 105, - scrollBeyondLastColumn = 106, - scrollBeyondLastLine = 107, - scrollPredominantAxis = 108, - selectionClipboard = 109, - selectionHighlight = 110, - selectOnLineNumbers = 111, - showFoldingControls = 112, - showUnused = 113, - snippetSuggestions = 114, - smartSelect = 115, - smoothScrolling = 116, - stickyScroll = 117, - stickyTabStops = 118, - stopRenderingLineAfter = 119, - suggest = 120, - suggestFontSize = 121, - suggestLineHeight = 122, - suggestOnTriggerCharacters = 123, - suggestSelection = 124, - tabCompletion = 125, - tabIndex = 126, - unicodeHighlighting = 127, - unusualLineTerminators = 128, - useShadowDOM = 129, - useTabStops = 130, - wordBreak = 131, - wordSegmenterLocales = 132, - wordSeparators = 133, - wordWrap = 134, - wordWrapBreakAfterCharacters = 135, - wordWrapBreakBeforeCharacters = 136, - wordWrapColumn = 137, - wordWrapOverride1 = 138, - wordWrapOverride2 = 139, - wrappingIndent = 140, - wrappingStrategy = 141, - showDeprecated = 142, - inlayHints = 143, - editorClassName = 144, - pixelRatio = 145, - tabFocusMode = 146, - layoutInfo = 147, - wrappingInfo = 148, - defaultColorDecorators = 149, - colorDecoratorsActivatedOn = 150, - inlineCompletionsAccessibilityVerbose = 151 + experimentalGpuAcceleration = 39, + experimentalWhitespaceRendering = 40, + extraEditorClassName = 41, + fastScrollSensitivity = 42, + find = 43, + fixedOverflowWidgets = 44, + folding = 45, + foldingStrategy = 46, + foldingHighlight = 47, + foldingImportsByDefault = 48, + foldingMaximumRegions = 49, + unfoldOnClickAfterEndOfLine = 50, + fontFamily = 51, + fontInfo = 52, + fontLigatures = 53, + fontSize = 54, + fontWeight = 55, + fontVariations = 56, + formatOnPaste = 57, + formatOnType = 58, + glyphMargin = 59, + gotoLocation = 60, + hideCursorInOverviewRuler = 61, + hover = 62, + inDiffEditor = 63, + inlineSuggest = 64, + inlineEdit = 65, + letterSpacing = 66, + lightbulb = 67, + lineDecorationsWidth = 68, + lineHeight = 69, + lineNumbers = 70, + lineNumbersMinChars = 71, + linkedEditing = 72, + links = 73, + matchBrackets = 74, + minimap = 75, + mouseStyle = 76, + mouseWheelScrollSensitivity = 77, + mouseWheelZoom = 78, + multiCursorMergeOverlapping = 79, + multiCursorModifier = 80, + multiCursorPaste = 81, + multiCursorLimit = 82, + occurrencesHighlight = 83, + overviewRulerBorder = 84, + overviewRulerLanes = 85, + padding = 86, + pasteAs = 87, + parameterHints = 88, + peekWidgetDefaultFocus = 89, + placeholder = 90, + definitionLinkOpensInPeek = 91, + quickSuggestions = 92, + quickSuggestionsDelay = 93, + readOnly = 94, + readOnlyMessage = 95, + renameOnType = 96, + renderControlCharacters = 97, + renderFinalNewline = 98, + renderLineHighlight = 99, + renderLineHighlightOnlyWhenFocus = 100, + renderValidationDecorations = 101, + renderWhitespace = 102, + revealHorizontalRightPadding = 103, + roundedSelection = 104, + rulers = 105, + scrollbar = 106, + scrollBeyondLastColumn = 107, + scrollBeyondLastLine = 108, + scrollPredominantAxis = 109, + selectionClipboard = 110, + selectionHighlight = 111, + selectOnLineNumbers = 112, + showFoldingControls = 113, + showUnused = 114, + snippetSuggestions = 115, + smartSelect = 116, + smoothScrolling = 117, + stickyScroll = 118, + stickyTabStops = 119, + stopRenderingLineAfter = 120, + suggest = 121, + suggestFontSize = 122, + suggestLineHeight = 123, + suggestOnTriggerCharacters = 124, + suggestSelection = 125, + tabCompletion = 126, + tabIndex = 127, + unicodeHighlighting = 128, + unusualLineTerminators = 129, + useShadowDOM = 130, + useTabStops = 131, + wordBreak = 132, + wordSegmenterLocales = 133, + wordSeparators = 134, + wordWrap = 135, + wordWrapBreakAfterCharacters = 136, + wordWrapBreakBeforeCharacters = 137, + wordWrapColumn = 138, + wordWrapOverride1 = 139, + wordWrapOverride2 = 140, + wrappingIndent = 141, + wrappingStrategy = 142, + showDeprecated = 143, + inlayHints = 144, + editorClassName = 145, + pixelRatio = 146, + tabFocusMode = 147, + layoutInfo = 148, + wrappingInfo = 149, + defaultColorDecorators = 150, + colorDecoratorsActivatedOn = 151, + inlineCompletionsAccessibilityVerbose = 152 } export const EditorOptions: { @@ -5066,6 +5072,7 @@ declare namespace monaco.editor { dropIntoEditor: IEditorOption>>; experimentalEditContextEnabled: IEditorOption; stickyScroll: IEditorOption>>; + experimentalGpuAcceleration: IEditorOption; experimentalWhitespaceRendering: IEditorOption; extraEditorClassName: IEditorOption; fastScrollSensitivity: IEditorOption; diff --git a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts index 6cf0e711027..ed312b7113e 100644 --- a/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts +++ b/src/vs/platform/accessibilitySignal/browser/accessibilitySignalService.ts @@ -8,8 +8,7 @@ import { getStructuralKey } from '../../../base/common/equals.js'; import { Event, IValueWithChangeEvent } from '../../../base/common/event.js'; import { Disposable, IDisposable, toDisposable } from '../../../base/common/lifecycle.js'; import { FileAccess } from '../../../base/common/network.js'; -import { derived, observableFromEvent } from '../../../base/common/observable.js'; -import { ValueWithChangeEventFromObservable } from '../../../base/common/observableInternal/utils.js'; +import { derived, observableFromEvent, ValueWithChangeEventFromObservable } from '../../../base/common/observable.js'; import { localize } from '../../../nls.js'; import { IAccessibilityService } from '../../accessibility/common/accessibility.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 62d68f28f16..504d38b9679 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -82,6 +82,7 @@ export class MenuId { static readonly ExplorerContext = new MenuId('ExplorerContext'); static readonly ExplorerContextShare = new MenuId('ExplorerContextShare'); static readonly ExtensionContext = new MenuId('ExtensionContext'); + static readonly ExtensionEditorContextMenu = new MenuId('ExtensionEditorContextMenu'); static readonly GlobalActivity = new MenuId('GlobalActivity'); static readonly CommandCenter = new MenuId('CommandCenter'); static readonly CommandCenterCenter = new MenuId('CommandCenterCenter'); @@ -222,6 +223,8 @@ export class MenuId { static readonly ChatExecute = new MenuId('ChatExecute'); static readonly ChatExecuteSecondary = new MenuId('ChatExecuteSecondary'); static readonly ChatInputSide = new MenuId('ChatInputSide'); + static readonly ChatInlineResourceAnchorContext = new MenuId('ChatInlineResourceAnchorContext'); + static readonly ChatInlineSymbolAnchorContext = new MenuId('ChatInlineSymbolAnchorContext'); static readonly AccessibleView = new MenuId('AccessibleView'); static readonly MultiDiffEditorFileToolbar = new MenuId('MultiDiffEditorFileToolbar'); static readonly DiffEditorHunkToolbar = new MenuId('DiffEditorHunkToolbar'); diff --git a/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts b/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts index 95aa93b8617..43c09df91e5 100644 --- a/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts +++ b/src/vs/platform/environment/test/node/nativeModules.integrationTest.ts @@ -91,6 +91,11 @@ flakySuite('Native Modules (all platforms)', () => { assert.ok(typeof parcelWatcher.subscribe === 'function', testErrorMessage('@parcel/watcher')); }); + test('@bpasero/watcher', async () => { + const parcelWatcher2 = await import('@bpasero/watcher'); + assert.ok(typeof parcelWatcher2.subscribe === 'function', testErrorMessage('@bpasero/watcher')); + }); + test('@vscode/deviceid', async () => { const deviceIdPackage = await import('@vscode/deviceid'); assert.ok(typeof deviceIdPackage.getDeviceId === 'function', testErrorMessage('@vscode/deviceid')); diff --git a/src/vs/platform/extensionManagement/node/extensionDownloader.ts b/src/vs/platform/extensionManagement/node/extensionDownloader.ts index e997163ed00..8bfb48b8c46 100644 --- a/src/vs/platform/extensionManagement/node/extensionDownloader.ts +++ b/src/vs/platform/extensionManagement/node/extensionDownloader.ts @@ -9,12 +9,10 @@ import { Disposable } from '../../../base/common/lifecycle.js'; import { Schemas } from '../../../base/common/network.js'; import { joinPath } from '../../../base/common/resources.js'; import * as semver from '../../../base/common/semver/semver.js'; -import { isBoolean } from '../../../base/common/types.js'; import { URI } from '../../../base/common/uri.js'; import { generateUuid } from '../../../base/common/uuid.js'; import { Promises as FSPromises } from '../../../base/node/pfs.js'; import { buffer, CorruptZipMessage } from '../../../base/node/zip.js'; -import { IConfigurationService } from '../../configuration/common/configuration.js'; import { INativeEnvironmentService } from '../../environment/common/environment.js'; import { ExtensionVerificationStatus, toExtensionManagementError } from '../common/abstractExtensionManagementService.js'; import { ExtensionManagementError, ExtensionManagementErrorCode, IExtensionGalleryService, IGalleryExtension, InstallOperation } from '../common/extensionManagement.js'; @@ -49,7 +47,6 @@ export class ExtensionsDownloader extends Disposable { @INativeEnvironmentService environmentService: INativeEnvironmentService, @IFileService private readonly fileService: IFileService, @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, - @IConfigurationService private readonly configurationService: IConfigurationService, @IExtensionSignatureVerificationService private readonly extensionSignatureVerificationService: IExtensionSignatureVerificationService, @ITelemetryService private readonly telemetryService: ITelemetryService, @ILogService private readonly logService: ILogService, @@ -65,59 +62,53 @@ export class ExtensionsDownloader extends Disposable { const location = await this.downloadVSIX(extension, operation); - let verificationStatus: ExtensionVerificationStatus = false; + if (!verifySignature) { + return { location, verificationStatus: false }; + } - if (verifySignature && this.shouldVerifySignature(extension)) { + if (!extension.isSigned) { + return { location, verificationStatus: 'PackageIsUnsigned' }; + } - let signatureArchiveLocation; + let signatureArchiveLocation; + try { + signatureArchiveLocation = await this.downloadSignatureArchive(extension); + } catch (error) { try { - signatureArchiveLocation = await this.downloadSignatureArchive(extension); + // Delete the downloaded VSIX if signature archive download fails + await this.delete(location); } catch (error) { + this.logService.error(error); + } + throw error; + } + + let verificationStatus; + try { + verificationStatus = await this.extensionSignatureVerificationService.verify(extension, location.fsPath, signatureArchiveLocation.fsPath, clientTargetPlatform); + } catch (error) { + verificationStatus = (error as ExtensionSignatureVerificationError).code; + if (verificationStatus === ExtensionSignatureVerificationCode.PackageIsInvalidZip || verificationStatus === ExtensionSignatureVerificationCode.SignatureArchiveIsInvalidZip) { try { - // Delete the downloaded VSIX if signature archive download fails + // Delete the downloaded vsix if VSIX or signature archive is invalid await this.delete(location); } catch (error) { this.logService.error(error); } - throw error; + throw new ExtensionManagementError(CorruptZipMessage, ExtensionManagementErrorCode.CorruptZip); } - + } finally { try { - verificationStatus = await this.extensionSignatureVerificationService.verify(extension, location.fsPath, signatureArchiveLocation.fsPath, clientTargetPlatform); + // Delete signature archive always + await this.delete(signatureArchiveLocation); } catch (error) { - verificationStatus = (error as ExtensionSignatureVerificationError).code; - if (verificationStatus === ExtensionSignatureVerificationCode.PackageIsInvalidZip || verificationStatus === ExtensionSignatureVerificationCode.SignatureArchiveIsInvalidZip) { - try { - // Delete the downloaded vsix if VSIX or signature archive is invalid - await this.delete(location); - } catch (error) { - this.logService.error(error); - } - throw new ExtensionManagementError(CorruptZipMessage, ExtensionManagementErrorCode.CorruptZip); - } - } finally { - try { - // Delete signature archive always - await this.delete(signatureArchiveLocation); - } catch (error) { - this.logService.error(error); - } + this.logService.error(error); } } return { location, verificationStatus }; } - private shouldVerifySignature(extension: IGalleryExtension): boolean { - if (!extension.isSigned) { - this.logService.info(`Extension is not signed: ${extension.identifier.id}`); - return false; - } - - const value = this.configurationService.getValue('extensions.verifySignature'); - return isBoolean(value) ? value : true; - } - private async downloadVSIX(extension: IGalleryExtension, operation: InstallOperation): Promise { try { const location = joinPath(this.extensionsDownloadDir, this.getName(extension)); diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 7cc41125987..515e10149dd 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -50,6 +50,8 @@ import { IProductService } from '../../product/common/productService.js'; import { ITelemetryService } from '../../telemetry/common/telemetry.js'; import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js'; import { IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js'; +import { IConfigurationService } from '../../configuration/common/configuration.js'; +import { isLinux } from '../../../base/common/platform.js'; export const INativeServerExtensionManagementService = refineServiceDecorator(IExtensionManagementService); export interface INativeServerExtensionManagementService extends IExtensionManagementService { @@ -75,12 +77,13 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi @IExtensionGalleryService galleryService: IExtensionGalleryService, @ITelemetryService telemetryService: ITelemetryService, @ILogService logService: ILogService, - @INativeEnvironmentService environmentService: INativeEnvironmentService, + @INativeEnvironmentService private readonly environmentService: INativeEnvironmentService, @IExtensionsScannerService private readonly extensionsScannerService: IExtensionsScannerService, @IExtensionsProfileScannerService private readonly extensionsProfileScannerService: IExtensionsProfileScannerService, @IDownloadService private downloadService: IDownloadService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IFileService private readonly fileService: IFileService, + @IConfigurationService private readonly configurationService: IConfigurationService, @IProductService productService: IProductService, @IUriIdentityService uriIdentityService: IUriIdentityService, @IUserDataProfilesService userDataProfilesService: IUserDataProfilesService @@ -246,7 +249,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } async download(extension: IGalleryExtension, operation: InstallOperation, donotVerifySignature: boolean): Promise { - const { location } = await this.extensionsDownloader.download(extension, operation, !donotVerifySignature); + const { location } = await this.downloadExtension(extension, operation, !donotVerifySignature); return location; } @@ -292,7 +295,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } private async downloadAndExtractGalleryExtension(extensionKey: ExtensionKey, gallery: IGalleryExtension, operation: InstallOperation, options: InstallExtensionTaskOptions, token: CancellationToken): Promise { - const { verificationStatus, location } = await this.extensionsDownloader.download(gallery, operation, !options.donotVerifySignature, options.context?.[EXTENSION_INSTALL_CLIENT_TARGET_PLATFORM_CONTEXT]); + const { verificationStatus, location } = await this.downloadExtension(gallery, operation, !options.donotVerifySignature, options.context?.[EXTENSION_INSTALL_CLIENT_TARGET_PLATFORM_CONTEXT]); try { if (token.isCancellationRequested) { @@ -339,6 +342,20 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi } } + private async downloadExtension(extension: IGalleryExtension, operation: InstallOperation, verifySignature: boolean, clientTargetPlatform?: TargetPlatform): Promise<{ readonly location: URI; readonly verificationStatus: ExtensionVerificationStatus }> { + if (verifySignature) { + const value = this.configurationService.getValue('extensions.verifySignature'); + verifySignature = isBoolean(value) ? value : true; + } + const { location, verificationStatus } = await this.extensionsDownloader.download(extension, operation, verifySignature, clientTargetPlatform); + + if (verificationStatus !== true && verifySignature && this.environmentService.isBuilt && !isLinux) { + throw new ExtensionManagementError(nls.localize('download failed', "Signature verification failed with '{0}' error.", verificationStatus === false ? 'NotExecuted' : verificationStatus), ExtensionManagementErrorCode.Signature); + } + + return { location, verificationStatus }; + } + private async extractVSIX(extensionKey: ExtensionKey, location: URI, options: InstallExtensionTaskOptions, token: CancellationToken): Promise { const local = await this.extensionsScanner.extractUserExtension( extensionKey, diff --git a/src/vs/platform/extensionManagement/test/node/extensionDownloader.test.ts b/src/vs/platform/extensionManagement/test/node/extensionDownloader.test.ts index 8bce158e6d3..e7450242fc6 100644 --- a/src/vs/platform/extensionManagement/test/node/extensionDownloader.test.ts +++ b/src/vs/platform/extensionManagement/test/node/extensionDownloader.test.ts @@ -13,8 +13,6 @@ import { URI } from '../../../../base/common/uri.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { mock } from '../../../../base/test/common/mock.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js'; -import { IConfigurationService } from '../../../configuration/common/configuration.js'; -import { TestConfigurationService } from '../../../configuration/test/common/testConfigurationService.js'; import { INativeEnvironmentService } from '../../../environment/common/environment.js'; import { getTargetPlatform, IExtensionGalleryService, IGalleryExtension, IGalleryExtensionAssets, InstallOperation } from '../../common/extensionManagement.js'; import { getGalleryExtensionId } from '../../common/extensionManagementUtil.js'; @@ -76,16 +74,8 @@ suite('ExtensionDownloader Tests', () => { }); }); - test('download completes successfully if verification is disabled by setting set to false', async () => { - const testObject = aTestObject({ isSignatureVerificationEnabled: false, verificationResult: 'error' }); - - const actual = await testObject.download(aGalleryExtension('a', { isSigned: true }), InstallOperation.Install, true); - - assert.strictEqual(actual.verificationStatus, false); - }); - test('download completes successfully if verification is disabled by options', async () => { - const testObject = aTestObject({ isSignatureVerificationEnabled: true, verificationResult: 'error' }); + const testObject = aTestObject({ verificationResult: 'error' }); const actual = await testObject.download(aGalleryExtension('a', { isSigned: true }), InstallOperation.Install, false); @@ -93,7 +83,7 @@ suite('ExtensionDownloader Tests', () => { }); test('download completes successfully if verification is disabled because the module is not loaded', async () => { - const testObject = aTestObject({ isSignatureVerificationEnabled: true, verificationResult: false }); + const testObject = aTestObject({ verificationResult: false }); const actual = await testObject.download(aGalleryExtension('a', { isSigned: true }), InstallOperation.Install, true); @@ -102,7 +92,7 @@ suite('ExtensionDownloader Tests', () => { test('download completes successfully if verification fails to execute', async () => { const errorCode = 'ENOENT'; - const testObject = aTestObject({ isSignatureVerificationEnabled: true, verificationResult: errorCode }); + const testObject = aTestObject({ verificationResult: errorCode }); const actual = await testObject.download(aGalleryExtension('a', { isSigned: true }), InstallOperation.Install, true); @@ -111,7 +101,7 @@ suite('ExtensionDownloader Tests', () => { test('download completes successfully if verification fails ', async () => { const errorCode = 'IntegrityCheckFailed'; - const testObject = aTestObject({ isSignatureVerificationEnabled: true, verificationResult: errorCode }); + const testObject = aTestObject({ verificationResult: errorCode }); const actual = await testObject.download(aGalleryExtension('a', { isSigned: true }), InstallOperation.Install, true); @@ -119,7 +109,7 @@ suite('ExtensionDownloader Tests', () => { }); test('download completes successfully if verification succeeds', async () => { - const testObject = aTestObject({ isSignatureVerificationEnabled: true, verificationResult: true }); + const testObject = aTestObject({ verificationResult: true }); const actual = await testObject.download(aGalleryExtension('a', { isSigned: true }), InstallOperation.Install, true); @@ -127,23 +117,22 @@ suite('ExtensionDownloader Tests', () => { }); test('download completes successfully for unsigned extension', async () => { - const testObject = aTestObject({ isSignatureVerificationEnabled: true, verificationResult: true }); + const testObject = aTestObject({ verificationResult: true }); const actual = await testObject.download(aGalleryExtension('a', { isSigned: false }), InstallOperation.Install, true); - assert.strictEqual(actual.verificationStatus, false); + assert.strictEqual(actual.verificationStatus, 'PackageIsUnsigned'); }); test('download completes successfully for an unsigned extension even when signature verification throws error', async () => { - const testObject = aTestObject({ isSignatureVerificationEnabled: true, verificationResult: 'error' }); + const testObject = aTestObject({ verificationResult: 'error' }); const actual = await testObject.download(aGalleryExtension('a', { isSigned: false }), InstallOperation.Install, true); - assert.strictEqual(actual.verificationStatus, false); + assert.strictEqual(actual.verificationStatus, 'PackageIsUnsigned'); }); - function aTestObject(options: { isSignatureVerificationEnabled: boolean; verificationResult: boolean | string }): ExtensionsDownloader { - instantiationService.stub(IConfigurationService, new TestConfigurationService(isBoolean(options.isSignatureVerificationEnabled) ? { extensions: { verifySignature: options.isSignatureVerificationEnabled } } : undefined)); + function aTestObject(options: { verificationResult: boolean | string }): ExtensionsDownloader { instantiationService.stub(IExtensionSignatureVerificationService, new TestExtensionSignatureVerificationService(options.verificationResult)); return disposables.add(instantiationService.createInstance(TestExtensionDownloader)); } diff --git a/src/vs/platform/extensions/common/extensionValidator.ts b/src/vs/platform/extensions/common/extensionValidator.ts index d66cf72af3b..4a9d3295696 100644 --- a/src/vs/platform/extensions/common/extensionValidator.ts +++ b/src/vs/platform/extensions/common/extensionValidator.ts @@ -369,7 +369,7 @@ export function areApiProposalsCompatible(apiProposals: string[], arg1?: any): b continue; } if (existingProposal.version !== version) { - incompatibleNotices.push(nls.localize('apiProposalMismatch', "Extension is using an API proposal '{0}' that is not compatible with the current version of VS Code.", proposalName)); + incompatibleNotices.push(nls.localize('apiProposalMismatch', "This extension is using the API proposal '{0}' that is not compatible with the current version of VS Code.", proposalName)); } } notices?.push(...incompatibleNotices); diff --git a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts index 40ba9732603..ee38b02a765 100644 --- a/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts +++ b/src/vs/platform/files/node/watcher/parcel/parcelWatcher.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as parcelWatcher from '@parcel/watcher'; +import * as parcelWatcher2 from '@bpasero/watcher'; import { existsSync, statSync, unlinkSync } from 'fs'; import { tmpdir, homedir } from 'os'; import { URI } from '../../../../../base/common/uri.js'; @@ -24,6 +25,9 @@ import { FileChangeType, IFileChange } from '../../../common/files.js'; import { coalesceEvents, IRecursiveWatchRequest, parseWatcherPatterns, IRecursiveWatcherWithSubscribe, isFiltered, IWatcherErrorEvent } from '../../../common/watcher.js'; import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +const useParcelWatcher2 = process.env.VSCODE_USE_WATCHER2 === 'true'; +const parcelWatcherLib = useParcelWatcher2 ? parcelWatcher2 : parcelWatcher; + export class ParcelWatcherInstance extends Disposable { private readonly _onDidStop = this._register(new Emitter<{ joinRestart?: Promise }>()); @@ -302,7 +306,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS // We already ran before, check for events since if (counter > 1) { - const parcelEvents = await parcelWatcher.getEventsSince(realPath, snapshotFile, { ignore: this.addPredefinedExcludes(request.excludes), backend: ParcelWatcher.PARCEL_WATCHER_BACKEND }); + const parcelEvents = await parcelWatcherLib.getEventsSince(realPath, snapshotFile, { ignore: this.addPredefinedExcludes(request.excludes), backend: ParcelWatcher.PARCEL_WATCHER_BACKEND }); if (cts.token.isCancellationRequested) { return; @@ -313,7 +317,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS } // Store a snapshot of files to the snapshot file - await parcelWatcher.writeSnapshot(realPath, snapshotFile, { ignore: this.addPredefinedExcludes(request.excludes), backend: ParcelWatcher.PARCEL_WATCHER_BACKEND }); + await parcelWatcherLib.writeSnapshot(realPath, snapshotFile, { ignore: this.addPredefinedExcludes(request.excludes), backend: ParcelWatcher.PARCEL_WATCHER_BACKEND }); // Signal we are ready now when the first snapshot was written if (counter === 1) { @@ -358,7 +362,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS const { realPath, realPathDiffers, realPathLength } = this.normalizePath(request); try { - const parcelWatcherInstance = await parcelWatcher.subscribe(realPath, (error, parcelEvents) => { + const parcelWatcherInstance = await parcelWatcherLib.subscribe(realPath, (error, parcelEvents) => { if (watcher.token.isCancellationRequested) { return; // return early when disposed } @@ -858,7 +862,7 @@ export class ParcelWatcher extends BaseWatcher implements IRecursiveWatcherWithS } private toMessage(message: string, request?: IRecursiveWatchRequest): string { - return request ? `[File Watcher (parcel)] ${message} (path: ${request.path})` : `[File Watcher (parcel)] ${message}`; + return request ? `[File Watcher (${useParcelWatcher2 ? 'parcel-next' : 'parcel-classic'})] ${message} (path: ${request.path})` : `[File Watcher (${useParcelWatcher2 ? 'parcel-next' : 'parcel-classic'})] ${message}`; } protected get recursiveWatcher() { return this; } diff --git a/src/vs/platform/files/node/watcher/watcherStats.ts b/src/vs/platform/files/node/watcher/watcherStats.ts index 4e56825fb19..af911739c47 100644 --- a/src/vs/platform/files/node/watcher/watcherStats.ts +++ b/src/vs/platform/files/node/watcher/watcherStats.ts @@ -7,6 +7,8 @@ import { IUniversalWatchRequest, requestFilterToString } from '../../common/watc import { INodeJSWatcherInstance, NodeJSWatcher } from './nodejs/nodejsWatcher.js'; import { ParcelWatcher, ParcelWatcherInstance } from './parcel/parcelWatcher.js'; +const useParcelWatcher2 = process.env.VSCODE_USE_WATCHER2 === 'true'; + export function computeStats( requests: IUniversalWatchRequest[], recursiveWatcher: ParcelWatcher, @@ -59,7 +61,7 @@ export function computeStats( fillNonRecursiveWatcherStats(nonRecursiveWatcheLines, nonRecursiveWatcher); lines.push(...alignTextColumns(nonRecursiveWatcheLines)); - return `\n\n[File Watcher] request stats:\n\n${lines.join('\n')}\n\n`; + return useParcelWatcher2 ? `\n\n[File Watcher NEXT] request stats:\n\n${lines.join('\n')}\n\n` : `\n\n[File Watcher CLASSIC] request stats:\n\n${lines.join('\n')}\n\n`; } function alignTextColumns(lines: string[]) { diff --git a/src/vs/platform/observable/common/platformObservableUtils.ts b/src/vs/platform/observable/common/platformObservableUtils.ts index a75b0fcc3fb..225357badc9 100644 --- a/src/vs/platform/observable/common/platformObservableUtils.ts +++ b/src/vs/platform/observable/common/platformObservableUtils.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IDisposable } from '../../../base/common/lifecycle.js'; -import { autorunOpts, IObservable, IReader } from '../../../base/common/observable.js'; -import { observableFromEventOpts } from '../../../base/common/observableInternal/utils.js'; +import { autorunOpts, IObservable, IReader, observableFromEventOpts } from '../../../base/common/observable.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; import { ContextKeyValue, IContextKeyService, RawContextKey } from '../../contextkey/common/contextkey.js'; diff --git a/src/vs/platform/request/common/request.ts b/src/vs/platform/request/common/request.ts index 25da055924c..46a78f000ba 100644 --- a/src/vs/platform/request/common/request.ts +++ b/src/vs/platform/request/common/request.ts @@ -11,7 +11,7 @@ import { IHeaders, IRequestContext, IRequestOptions } from '../../../base/parts/ import { localize } from '../../../nls.js'; import { ConfigurationScope, Extensions, IConfigurationNode, IConfigurationRegistry } from '../../configuration/common/configurationRegistry.js'; import { createDecorator } from '../../instantiation/common/instantiation.js'; -import { ILogger } from '../../log/common/log.js'; +import { ILogService } from '../../log/common/log.js'; import { Registry } from '../../registry/common/platform.js'; export const IRequestService = createDecorator('requestService'); @@ -70,19 +70,19 @@ export abstract class AbstractRequestService extends Disposable implements IRequ private counter = 0; - constructor(protected readonly logger: ILogger) { + constructor(protected readonly logService: ILogService) { super(); } protected async logAndRequest(options: IRequestOptions, request: () => Promise): Promise { - const prefix = `#${++this.counter}: ${options.url}`; - this.logger.info(`${prefix} - begin`, options.type, new LoggableHeaders(options.headers ?? {})); + const prefix = `[network] #${++this.counter}: ${options.url}`; + this.logService.trace(`${prefix} - begin`, options.type, new LoggableHeaders(options.headers ?? {})); try { const result = await request(); - this.logger.info(`${prefix} - end`, options.type, result.res.statusCode, result.res.headers); + this.logService.trace(`${prefix} - end`, options.type, result.res.statusCode, result.res.headers); return result; } catch (error) { - this.logger.error(`${prefix} - error`, options.type, getErrorMessage(error)); + this.logService.error(`${prefix} - error`, options.type, getErrorMessage(error)); throw error; } } diff --git a/src/vs/platform/request/node/requestService.ts b/src/vs/platform/request/node/requestService.ts index 981c60e48c5..010acda0546 100644 --- a/src/vs/platform/request/node/requestService.ts +++ b/src/vs/platform/request/node/requestService.ts @@ -16,7 +16,7 @@ import { IRequestContext, IRequestOptions } from '../../../base/parts/request/co import { IConfigurationService } from '../../configuration/common/configuration.js'; import { INativeEnvironmentService } from '../../environment/common/environment.js'; import { getResolvedShellEnv } from '../../shell/node/shellEnv.js'; -import { ILogService, ILogger } from '../../log/common/log.js'; +import { ILogService } from '../../log/common/log.js'; import { AbstractRequestService, AuthInfo, Credentials, IRequestService } from '../common/request.js'; import { Agent, getProxyAgent } from './proxy.js'; import { createGunzip } from 'zlib'; @@ -52,12 +52,11 @@ export class RequestService extends AbstractRequestService implements IRequestSe private shellEnvErrorLogged?: boolean; constructor( - logger: ILogger, @IConfigurationService private readonly configurationService: IConfigurationService, @INativeEnvironmentService private readonly environmentService: INativeEnvironmentService, - @ILogService private readonly logService: ILogService, + @ILogService logService: ILogService, ) { - super(logger); + super(logService); this.configure(); this._register(configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('http')) { diff --git a/src/vs/platform/utilityProcess/electron-main/utilityProcessWorkerMainService.ts b/src/vs/platform/utilityProcess/electron-main/utilityProcessWorkerMainService.ts index 9ab01b9be15..2a622905d4b 100644 --- a/src/vs/platform/utilityProcess/electron-main/utilityProcessWorkerMainService.ts +++ b/src/vs/platform/utilityProcess/electron-main/utilityProcessWorkerMainService.ts @@ -8,12 +8,13 @@ import { createDecorator } from '../../instantiation/common/instantiation.js'; import { ILogService } from '../../log/common/log.js'; import { IUtilityProcessWorkerCreateConfiguration, IOnDidTerminateUtilityrocessWorkerProcess, IUtilityProcessWorkerConfiguration, IUtilityProcessWorkerProcessExit, IUtilityProcessWorkerService } from '../common/utilityProcessWorkerService.js'; import { IWindowsMainService } from '../../windows/electron-main/windows.js'; -import { WindowUtilityProcess } from './utilityProcess.js'; +import { IWindowUtilityProcessConfiguration, WindowUtilityProcess } from './utilityProcess.js'; import { ITelemetryService } from '../../telemetry/common/telemetry.js'; import { hash } from '../../../base/common/hash.js'; import { Event, Emitter } from '../../../base/common/event.js'; import { DeferredPromise } from '../../../base/common/async.js'; import { ILifecycleMainService } from '../../lifecycle/electron-main/lifecycleMainService.js'; +import { IConfigurationService } from '../../configuration/common/configuration.js'; export const IUtilityProcessWorkerMainService = createDecorator('utilityProcessWorker'); @@ -32,7 +33,8 @@ export class UtilityProcessWorkerMainService extends Disposable implements IUtil @ILogService private readonly logService: ILogService, @IWindowsMainService private readonly windowsMainService: IWindowsMainService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService + @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, + @IConfigurationService private readonly configurationService: IConfigurationService ) { super(); } @@ -50,7 +52,7 @@ export class UtilityProcessWorkerMainService extends Disposable implements IUtil } // Create new worker - const worker = new UtilityProcessWorker(this.logService, this.windowsMainService, this.telemetryService, this.lifecycleMainService, configuration); + const worker = new UtilityProcessWorker(this.logService, this.windowsMainService, this.telemetryService, this.lifecycleMainService, this.configurationService, configuration); if (!worker.spawn()) { return { reason: { code: 1, signal: 'EINVALID' } }; } @@ -106,6 +108,7 @@ class UtilityProcessWorker extends Disposable { @IWindowsMainService private readonly windowsMainService: IWindowsMainService, @ITelemetryService private readonly telemetryService: ITelemetryService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, + @IConfigurationService private readonly configurationService: IConfigurationService, private readonly configuration: IUtilityProcessWorkerCreateConfiguration ) { super(); @@ -122,7 +125,7 @@ class UtilityProcessWorker extends Disposable { const window = this.windowsMainService.getWindowById(this.configuration.reply.windowId); const windowPid = window?.win?.webContents.getOSProcessId(); - return this.utilityProcess.start({ + let configuration: IWindowUtilityProcessConfiguration = { type: this.configuration.process.type, entryPoint: this.configuration.process.moduleId, parentLifecycleBound: windowPid, @@ -131,7 +134,18 @@ class UtilityProcessWorker extends Disposable { responseWindowId: this.configuration.reply.windowId, responseChannel: this.configuration.reply.channel, responseNonce: this.configuration.reply.nonce - }); + }; + + if (this.configuration.process.type === 'fileWatcher' && this.configurationService.getValue('files.experimentalWatcherNext') === true) { + configuration = { + ...configuration, + env: { + VSCODE_USE_WATCHER2: 'true' + } + }; + } + + return this.utilityProcess.start(configuration); } kill() { diff --git a/src/vs/server/node/serverServices.ts b/src/vs/server/node/serverServices.ts index c66509b593e..70ebf61f857 100644 --- a/src/vs/server/node/serverServices.ts +++ b/src/vs/server/node/serverServices.ts @@ -148,11 +148,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken services.set(IExtensionHostStatusService, extensionHostStatusService); // Request - const networkLogger = loggerService.createLogger('network-server', { - name: localize('network-server', "Network (Server)"), - hidden: true - }); - const requestService = new RequestService(networkLogger, configurationService, environmentService, logService); + const requestService = new RequestService(configurationService, environmentService, logService); services.set(IRequestService, requestService); let oneDsAppender: ITelemetryAppender = NullAppender; diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index 83db6b1d91b..a325853c237 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -39,7 +39,7 @@ interface AgentData { hasFollowups?: boolean; } -class MainThreadChatTask implements IChatTask { +export class MainThreadChatTask implements IChatTask { public readonly kind = 'progressTask'; public readonly deferred = new DeferredPromise(); diff --git a/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts b/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts index 4dd68ff5ffe..c7dd92cd7de 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageModelTools.ts @@ -4,10 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from '../../../base/common/cancellation.js'; +import { MarkdownString } from '../../../base/common/htmlContent.js'; import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js'; -import { ExtHostContext, ExtHostLanguageModelToolsShape, MainContext, MainThreadLanguageModelToolsShape } from '../common/extHost.protocol.js'; +import { ChatModel } from '../../contrib/chat/common/chatModel.js'; +import { IChatService, IChatTask } from '../../contrib/chat/common/chatService.js'; import { CountTokensCallback, ILanguageModelToolsService, IToolData, IToolInvocation, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; import { IExtHostContext, extHostNamedCustomer } from '../../services/extensions/common/extHostCustomers.js'; +import { ExtHostContext, ExtHostLanguageModelToolsShape, MainContext, MainThreadLanguageModelToolsShape } from '../common/extHost.protocol.js'; +import { MainThreadChatTask } from './mainThreadChatAgents2.js'; @extHostNamedCustomer(MainContext.MainThreadLanguageModelTools) export class MainThreadLanguageModelTools extends Disposable implements MainThreadLanguageModelToolsShape { @@ -19,6 +23,7 @@ export class MainThreadLanguageModelTools extends Disposable implements MainThre constructor( extHostContext: IExtHostContext, @ILanguageModelToolsService private readonly _languageModelToolsService: ILanguageModelToolsService, + @IChatService private readonly _chatService: IChatService, ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostLanguageModelTools); @@ -30,12 +35,27 @@ export class MainThreadLanguageModelTools extends Disposable implements MainThre return Array.from(this._languageModelToolsService.getTools()); } - $invokeTool(dto: IToolInvocation, token: CancellationToken): Promise { - return this._languageModelToolsService.invokeTool( - dto, - (input, token) => this._proxy.$countTokensForInvocation(dto.callId, input, token), - token, - ); + async $invokeTool(dto: IToolInvocation, token: CancellationToken): Promise { + // Shortcut to write to the model directly here, but could call all the way back to use the real stream. + // TODO move this to the tools service? + let task: IChatTask | undefined; + if (dto.context) { + const model = this._chatService.getSession(dto.context?.sessionId) as ChatModel; + const request = model.getRequests().at(-1)!; + const tool = this._languageModelToolsService.getTool(dto.toolId); + task = new MainThreadChatTask(new MarkdownString(`Using ${tool?.displayName ?? dto.toolId}`)); + model.acceptResponseProgress(request, task); + } + + try { + return await this._languageModelToolsService.invokeTool( + dto, + (input, token) => this._proxy.$countTokensForInvocation(dto.callId, input, token), + token, + ); + } finally { + task?.complete(); + } } $countTokensForInvocation(callId: string, input: string, token: CancellationToken): Promise { diff --git a/src/vs/workbench/api/browser/mainThreadSCM.ts b/src/vs/workbench/api/browser/mainThreadSCM.ts index 221202313bd..bbb27f6f9b5 100644 --- a/src/vs/workbench/api/browser/mainThreadSCM.ts +++ b/src/vs/workbench/api/browser/mainThreadSCM.ts @@ -6,10 +6,10 @@ import { Barrier } from '../../../base/common/async.js'; import { URI, UriComponents } from '../../../base/common/uri.js'; import { Event, Emitter } from '../../../base/common/event.js'; -import { observableValue, observableValueOpts } from '../../../base/common/observable.js'; +import { IObservable, observableValue, observableValueOpts, transaction } from '../../../base/common/observable.js'; import { IDisposable, DisposableStore, combinedDisposable, dispose, Disposable } from '../../../base/common/lifecycle.js'; import { ISCMService, ISCMRepository, ISCMProvider, ISCMResource, ISCMResourceGroup, ISCMResourceDecorations, IInputValidation, ISCMViewService, InputValidationType, ISCMActionButtonDescriptor } from '../../contrib/scm/common/scm.js'; -import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, SCMHistoryItemGroupDto, SCMHistoryItemDto } from '../common/extHost.protocol.js'; +import { ExtHostContext, MainThreadSCMShape, ExtHostSCMShape, SCMProviderFeatures, SCMRawResourceSplices, SCMGroupFeatures, MainContext, SCMHistoryItemDto, SCMHistoryItemRefsChangeEventDto, SCMHistoryItemRefDto } from '../common/extHost.protocol.js'; import { Command } from '../../../editor/common/languages.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; @@ -17,7 +17,7 @@ import { MarshalledId } from '../../../base/common/marshallingIds.js'; import { ThemeIcon } from '../../../base/common/themables.js'; import { IMarkdownString } from '../../../base/common/htmlContent.js'; import { IQuickDiffService, QuickDiffProvider } from '../../contrib/scm/common/quickDiff.js'; -import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemGroup, ISCMHistoryOptions, ISCMHistoryProvider } from '../../contrib/scm/common/history.js'; +import { ISCMHistoryItem, ISCMHistoryItemChange, ISCMHistoryItemRef, ISCMHistoryItemRefsChangeEvent, ISCMHistoryOptions, ISCMHistoryProvider } from '../../contrib/scm/common/history.js'; import { ResourceTree } from '../../../base/common/resourceTree.js'; import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIdentity.js'; import { IWorkspaceContextService } from '../../../platform/workspace/common/workspace.js'; @@ -28,6 +28,8 @@ import { ITextModelContentProvider, ITextModelService } from '../../../editor/co import { Schemas } from '../../../base/common/network.js'; import { ITextModel } from '../../../editor/common/model.js'; import { structuralEquals } from '../../../base/common/equals.js'; +import { historyItemBaseRefColor, historyItemRefColor, historyItemRemoteRefColor } from '../../contrib/scm/browser/scmHistory.js'; +import { ColorIdentifier } from '../../../platform/theme/common/colorUtils.js'; function getIconFromIconDto(iconDto?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon): URI | { light: URI; dark: URI } | ThemeIcon | undefined { if (iconDto === undefined) { @@ -43,15 +45,19 @@ function getIconFromIconDto(iconDto?: UriComponents | { light: UriComponents; da } function toISCMHistoryItem(historyItemDto: SCMHistoryItemDto): ISCMHistoryItem { - const labels = historyItemDto.labels?.map(l => ({ - title: l.title, icon: getIconFromIconDto(l.icon) + const references = historyItemDto.references?.map(r => ({ + ...r, icon: getIconFromIconDto(r.icon) })); const newLineIndex = historyItemDto.message.indexOf('\n'); const subject = newLineIndex === -1 ? historyItemDto.message : `${historyItemDto.message.substring(0, newLineIndex)}\u2026`; - return { ...historyItemDto, subject, labels }; + return { ...historyItemDto, subject, references }; +} + +function toISCMHistoryItemRef(historyItemRefDto?: SCMHistoryItemRefDto, color?: ColorIdentifier): ISCMHistoryItemRef | undefined { + return historyItemRefDto ? { ...historyItemRefDto, icon: getIconFromIconDto(historyItemRefDto.icon), color: color } : undefined; } class SCMInputBoxContentProvider extends Disposable implements ITextModelContentProvider { @@ -166,15 +172,36 @@ class MainThreadSCMResource implements ISCMResource { } class MainThreadSCMHistoryProvider implements ISCMHistoryProvider { - private readonly _currentHistoryItemGroup = observableValueOpts({ - owner: this, equalsFn: structuralEquals + private readonly _historyItemRef = observableValueOpts({ + owner: this, + equalsFn: structuralEquals }, undefined); - get currentHistoryItemGroup() { return this._currentHistoryItemGroup; } + get historyItemRef(): IObservable { return this._historyItemRef; } + + private readonly _historyItemRemoteRef = observableValueOpts({ + owner: this, + equalsFn: structuralEquals + }, undefined); + get historyItemRemoteRef(): IObservable { return this._historyItemRemoteRef; } + + private readonly _historyItemBaseRef = observableValueOpts({ + owner: this, + equalsFn: structuralEquals + }, undefined); + get historyItemBaseRef(): IObservable { return this._historyItemBaseRef; } + + private readonly _historyItemRefChanges = observableValue(this, { added: [], modified: [], removed: [] }); + get historyItemRefChanges(): IObservable { return this._historyItemRefChanges; } constructor(private readonly proxy: ExtHostSCMShape, private readonly handle: number) { } - async resolveHistoryItemGroupCommonAncestor(historyItemGroupIds: string[]): Promise { - return this.proxy.$resolveHistoryItemGroupCommonAncestor(this.handle, historyItemGroupIds, CancellationToken.None); + async resolveHistoryItemRefsCommonAncestor(historyItemRefs: string[]): Promise { + return this.proxy.$resolveHistoryItemRefsCommonAncestor(this.handle, historyItemRefs, CancellationToken.None); + } + + async provideHistoryItemRefs(): Promise { + const historyItemRefs = await this.proxy.$provideHistoryItemRefs(this.handle, CancellationToken.None); + return historyItemRefs?.map(ref => ({ ...ref, icon: getIconFromIconDto(ref.icon) })); } async provideHistoryItems(options: ISCMHistoryOptions): Promise { @@ -192,8 +219,20 @@ class MainThreadSCMHistoryProvider implements ISCMHistoryProvider { })); } - $onDidChangeCurrentHistoryItemGroup(historyItemGroup: ISCMHistoryItemGroup | undefined): void { - this._currentHistoryItemGroup.set(historyItemGroup, undefined); + $onDidChangeCurrentHistoryItemRefs(historyItemRef?: SCMHistoryItemRefDto, historyItemRemoteRef?: SCMHistoryItemRefDto, historyItemBaseRef?: SCMHistoryItemRefDto): void { + transaction(tx => { + this._historyItemRef.set(toISCMHistoryItemRef(historyItemRef, historyItemRefColor), tx); + this._historyItemRemoteRef.set(toISCMHistoryItemRef(historyItemRemoteRef, historyItemRemoteRefColor), tx); + this._historyItemBaseRef.set(toISCMHistoryItemRef(historyItemBaseRef, historyItemBaseRefColor), tx); + }); + } + + $onDidChangeHistoryItemRefs(historyItemRefs: SCMHistoryItemRefsChangeEventDto): void { + const added = historyItemRefs.added.map(ref => toISCMHistoryItemRef(ref)!); + const modified = historyItemRefs.modified.map(ref => toISCMHistoryItemRef(ref)!); + const removed = historyItemRefs.removed.map(ref => toISCMHistoryItemRef(ref)!); + + this._historyItemRefChanges.set({ added, modified, removed }, undefined); } } @@ -424,12 +463,20 @@ class MainThreadSCMProvider implements ISCMProvider, QuickDiffProvider { return result && URI.revive(result); } - $onDidChangeHistoryProviderCurrentHistoryItemGroup(currentHistoryItemGroup?: SCMHistoryItemGroupDto): void { + $onDidChangeHistoryProviderCurrentHistoryItemRefs(historyItemRef?: SCMHistoryItemRefDto, historyItemRemoteRef?: SCMHistoryItemRefDto, historyItemBaseRef?: SCMHistoryItemRefDto): void { if (!this.historyProvider.get()) { return; } - this._historyProvider.get()?.$onDidChangeCurrentHistoryItemGroup(currentHistoryItemGroup); + this._historyProvider.get()?.$onDidChangeCurrentHistoryItemRefs(historyItemRef, historyItemRemoteRef, historyItemBaseRef); + } + + $onDidChangeHistoryProviderHistoryItemRefs(historyItemRefs: SCMHistoryItemRefsChangeEventDto): void { + if (!this.historyProvider.get()) { + return; + } + + this._historyProvider.get()?.$onDidChangeHistoryItemRefs(historyItemRefs); } toJSON(): any { @@ -665,7 +712,7 @@ export class MainThreadSCM implements MainThreadSCMShape { } } - async $onDidChangeHistoryProviderCurrentHistoryItemGroup(sourceControlHandle: number, historyItemGroup: SCMHistoryItemGroupDto | undefined): Promise { + async $onDidChangeHistoryProviderCurrentHistoryItemRefs(sourceControlHandle: number, historyItemRef?: SCMHistoryItemRefDto, historyItemRemoteRef?: SCMHistoryItemRefDto, historyItemBaseRef?: SCMHistoryItemRefDto): Promise { await this._repositoryBarriers.get(sourceControlHandle)?.wait(); const repository = this._repositories.get(sourceControlHandle); @@ -674,6 +721,18 @@ export class MainThreadSCM implements MainThreadSCMShape { } const provider = repository.provider as MainThreadSCMProvider; - provider.$onDidChangeHistoryProviderCurrentHistoryItemGroup(historyItemGroup); + provider.$onDidChangeHistoryProviderCurrentHistoryItemRefs(historyItemRef, historyItemRemoteRef, historyItemBaseRef); + } + + async $onDidChangeHistoryProviderHistoryItemRefs(sourceControlHandle: number, historyItemRefs: SCMHistoryItemRefsChangeEventDto): Promise { + await this._repositoryBarriers.get(sourceControlHandle)?.wait(); + const repository = this._repositories.get(sourceControlHandle); + + if (!repository) { + return; + } + + const provider = repository.provider as MainThreadSCMProvider; + provider.$onDidChangeHistoryProviderHistoryItemRefs(historyItemRefs); } } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index e82d2034bfd..a885606999e 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1751,6 +1751,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ChatResponseReferencePart: extHostTypes.ChatResponseReferencePart, ChatResponseReferencePart2: extHostTypes.ChatResponseReferencePart, ChatResponseCodeCitationPart: extHostTypes.ChatResponseCodeCitationPart, + ChatResponseCodeblockUriPart: extHostTypes.ChatResponseCodeblockUriPart, ChatResponseWarningPart: extHostTypes.ChatResponseWarningPart, ChatResponseTextEditPart: extHostTypes.ChatResponseTextEditPart, ChatResponseMarkdownWithVulnerabilitiesPart: extHostTypes.ChatResponseMarkdownWithVulnerabilitiesPart, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 0a34cf85f18..274e29330d6 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1542,12 +1542,19 @@ export type SCMRawResourceSplices = [ SCMRawResourceSplice[] ]; -export interface SCMHistoryItemGroupDto { +export interface SCMHistoryItemRefDto { readonly id: string; readonly name: string; readonly revision?: string; - readonly base?: Omit, 'remote'>; - readonly remote?: Omit, 'remote'>; + readonly category?: string; + readonly description?: string; + readonly icon?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon; +} + +export interface SCMHistoryItemRefsChangeEventDto { + readonly added: readonly SCMHistoryItemRefDto[]; + readonly modified: readonly SCMHistoryItemRefDto[]; + readonly removed: readonly SCMHistoryItemRefDto[]; } export interface SCMHistoryItemDto { @@ -1562,10 +1569,7 @@ export interface SCMHistoryItemDto { readonly insertions: number; readonly deletions: number; }; - readonly labels?: { - readonly title: string; - readonly icon?: UriComponents | { light: UriComponents; dark: UriComponents } | ThemeIcon; - }[]; + readonly references?: SCMHistoryItemRefDto[]; } export interface SCMHistoryItemChangeDto { @@ -1594,7 +1598,8 @@ export interface MainThreadSCMShape extends IDisposable { $showValidationMessage(sourceControlHandle: number, message: string | IMarkdownString, type: InputValidationType): Promise; $setValidationProviderIsEnabled(sourceControlHandle: number, enabled: boolean): Promise; - $onDidChangeHistoryProviderCurrentHistoryItemGroup(sourceControlHandle: number, historyItemGroup: SCMHistoryItemGroupDto | undefined): Promise; + $onDidChangeHistoryProviderCurrentHistoryItemRefs(sourceControlHandle: number, historyItemRef?: SCMHistoryItemRefDto, historyItemRemoteRef?: SCMHistoryItemRefDto, historyItemBaseRef?: SCMHistoryItemRefDto): Promise; + $onDidChangeHistoryProviderHistoryItemRefs(sourceControlHandle: number, historyItemRefs: SCMHistoryItemRefsChangeEventDto): Promise; } export interface MainThreadQuickDiffShape extends IDisposable { @@ -2358,9 +2363,10 @@ export interface ExtHostSCMShape { $executeResourceCommand(sourceControlHandle: number, groupHandle: number, handle: number, preserveFocus: boolean): Promise; $validateInput(sourceControlHandle: number, value: string, cursorPosition: number): Promise<[string | IMarkdownString, number] | undefined>; $setSelectedSourceControl(selectedSourceControlHandle: number | undefined): Promise; + $provideHistoryItemRefs(sourceControlHandle: number, token: CancellationToken): Promise; $provideHistoryItems(sourceControlHandle: number, options: any, token: CancellationToken): Promise; $provideHistoryItemChanges(sourceControlHandle: number, historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): Promise; - $resolveHistoryItemGroupCommonAncestor(sourceControlHandle: number, historyItemGroupIds: string[], token: CancellationToken): Promise; + $resolveHistoryItemRefsCommonAncestor(sourceControlHandle: number, historyItemRefs: string[], token: CancellationToken): Promise; } export interface ExtHostQuickDiffShape { diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 5940c351d10..27ca9a8bbcc 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -121,6 +121,14 @@ class ChatAgentResponseStream { _report(dto); return this; }, + codeblockUri(value) { + throwIfDone(this.codeblockUri); + checkProposedApiEnabled(that._extension, 'chatParticipantAdditions'); + const part = new extHostTypes.ChatResponseCodeblockUriPart(value); + const dto = typeConvert.ChatResponseCodeblockUriPart.from(part); + _report(dto); + return this; + }, filetree(value, baseUri) { throwIfDone(this.filetree); const part = new extHostTypes.ChatResponseFileTreePart(value, baseUri); diff --git a/src/vs/workbench/api/common/extHostDebugService.ts b/src/vs/workbench/api/common/extHostDebugService.ts index d072857559c..fe33d966e66 100644 --- a/src/vs/workbench/api/common/extHostDebugService.ts +++ b/src/vs/workbench/api/common/extHostDebugService.ts @@ -3,34 +3,34 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import type * as vscode from 'vscode'; +import { coalesce } from '../../../base/common/arrays.js'; import { asPromise } from '../../../base/common/async.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; import { Emitter, Event } from '../../../base/common/event.js'; -import { URI, UriComponents } from '../../../base/common/uri.js'; import { Disposable as DisposableCls, toDisposable } from '../../../base/common/lifecycle.js'; +import { ThemeIcon as ThemeIconUtils } from '../../../base/common/themables.js'; +import { URI, UriComponents } from '../../../base/common/uri.js'; import { ExtensionIdentifier, IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { createDecorator } from '../../../platform/instantiation/common/instantiation.js'; import { ISignService } from '../../../platform/sign/common/sign.js'; import { IWorkspaceFolder } from '../../../platform/workspace/common/workspace.js'; -import { DebugSessionUUID, ExtHostDebugServiceShape, IBreakpointsDeltaDto, IThreadFocusDto, IStackFrameFocusDto, IDebugSessionDto, IFunctionBreakpointDto, ISourceMultiBreakpointDto, MainContext, MainThreadDebugServiceShape } from './extHost.protocol.js'; -import { IExtHostEditorTabs } from './extHostEditorTabs.js'; -import { IExtHostExtensionService } from './extHostExtensionService.js'; -import { IExtHostRpcService } from './extHostRpcService.js'; -import { Breakpoint, DataBreakpoint, DebugAdapterExecutable, DebugAdapterInlineImplementation, DebugAdapterNamedPipeServer, DebugAdapterServer, DebugConsoleMode, Disposable, FunctionBreakpoint, Location, Position, setBreakpointId, SourceBreakpoint, DebugThread, DebugStackFrame, ThemeIcon } from './extHostTypes.js'; -import { IExtHostWorkspace } from './extHostWorkspace.js'; import { AbstractDebugAdapter } from '../../contrib/debug/common/abstractDebugAdapter.js'; -import { MainThreadDebugVisualization, IAdapterDescriptor, IConfig, IDebugAdapter, IDebugAdapterExecutable, IDebugAdapterNamedPipeServer, IDebugAdapterServer, IDebugVisualization, IDebugVisualizationContext, IDebuggerContribution, DebugVisualizationType, IDebugVisualizationTreeItem } from '../../contrib/debug/common/debug.js'; +import { DebugVisualizationType, IAdapterDescriptor, IConfig, IDebugAdapter, IDebugAdapterExecutable, IDebugAdapterImpl, IDebugAdapterNamedPipeServer, IDebugAdapterServer, IDebuggerContribution, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem, MainThreadDebugVisualization } from '../../contrib/debug/common/debug.js'; import { convertToDAPaths, convertToVSCPaths, isDebuggerMainContribution } from '../../contrib/debug/common/debugUtils.js'; import { ExtensionDescriptionRegistry } from '../../services/extensions/common/extensionDescriptionRegistry.js'; import { Dto } from '../../services/extensions/common/proxyIdentifier.js'; -import type * as vscode from 'vscode'; -import { IExtHostConfiguration } from './extHostConfiguration.js'; -import { IExtHostVariableResolverProvider } from './extHostVariableResolverService.js'; -import { ThemeIcon as ThemeIconUtils } from '../../../base/common/themables.js'; +import { DebugSessionUUID, ExtHostDebugServiceShape, IBreakpointsDeltaDto, IDebugSessionDto, IFunctionBreakpointDto, ISourceMultiBreakpointDto, IStackFrameFocusDto, IThreadFocusDto, MainContext, MainThreadDebugServiceShape } from './extHost.protocol.js'; import { IExtHostCommands } from './extHostCommands.js'; -import * as Convert from './extHostTypeConverters.js'; -import { coalesce } from '../../../base/common/arrays.js'; +import { IExtHostConfiguration } from './extHostConfiguration.js'; +import { IExtHostEditorTabs } from './extHostEditorTabs.js'; +import { IExtHostExtensionService } from './extHostExtensionService.js'; +import { IExtHostRpcService } from './extHostRpcService.js'; import { IExtHostTesting } from './extHostTesting.js'; +import * as Convert from './extHostTypeConverters.js'; +import { Breakpoint, DataBreakpoint, DebugAdapterExecutable, DebugAdapterInlineImplementation, DebugAdapterNamedPipeServer, DebugAdapterServer, DebugConsoleMode, DebugStackFrame, DebugThread, Disposable, FunctionBreakpoint, Location, Position, setBreakpointId, SourceBreakpoint, ThemeIcon } from './extHostTypes.js'; +import { IExtHostVariableResolverProvider } from './extHostVariableResolverService.js'; +import { IExtHostWorkspace } from './extHostWorkspace.js'; export const IExtHostDebugService = createDecorator('IExtHostDebugService'); @@ -578,8 +578,8 @@ export abstract class ExtHostDebugServiceBase extends DisposableCls implements I return variableResolver.resolveAnyAsync(ws, config); } - protected createDebugAdapter(adapter: IAdapterDescriptor, session: ExtHostDebugSession): AbstractDebugAdapter | undefined { - if (adapter.type === 'implementation') { + protected createDebugAdapter(adapter: vscode.DebugAdapterDescriptor, session: ExtHostDebugSession): AbstractDebugAdapter | undefined { + if (adapter instanceof DebugAdapterInlineImplementation) { return new DirectDebugAdapter(adapter.implementation); } return undefined; @@ -600,9 +600,7 @@ export abstract class ExtHostDebugServiceBase extends DisposableCls implements I throw new Error(`Couldn't find a debug adapter descriptor for debug type '${session.type}' (extension might have failed to activate)`); } - const adapterDescriptor = this.convertToDto(daDescriptor); - - const da = this.createDebugAdapter(adapterDescriptor, session); + const da = this.createDebugAdapter(daDescriptor, session); if (!da) { throw new Error(`Couldn't create a debug adapter for type '${session.type}'.`); } @@ -891,35 +889,49 @@ export abstract class ExtHostDebugServiceBase extends DisposableCls implements I // private & dto helpers private convertToDto(x: vscode.DebugAdapterDescriptor): Dto { - if (x instanceof DebugAdapterExecutable) { - return { - type: 'executable', - command: x.command, - args: x.args, - options: x.options - } satisfies IDebugAdapterExecutable; + return this.convertExecutableToDto(x); } else if (x instanceof DebugAdapterServer) { - return { - type: 'server', - port: x.port, - host: x.host - } satisfies IDebugAdapterServer; + return this.convertServerToDto(x); } else if (x instanceof DebugAdapterNamedPipeServer) { - return { - type: 'pipeServer', - path: x.path - } satisfies IDebugAdapterNamedPipeServer; + return this.convertPipeServerToDto(x); } else if (x instanceof DebugAdapterInlineImplementation) { - return { - type: 'implementation', - implementation: x.implementation - } as Dto; + return this.convertImplementationToDto(x); } else { throw new Error('convertToDto unexpected type'); } } + protected convertExecutableToDto(x: DebugAdapterExecutable): IDebugAdapterExecutable { + return { + type: 'executable', + command: x.command, + args: x.args, + options: x.options + }; + } + + protected convertServerToDto(x: DebugAdapterServer): IDebugAdapterServer { + return { + type: 'server', + port: x.port, + host: x.host + }; + } + + protected convertPipeServerToDto(x: DebugAdapterNamedPipeServer): IDebugAdapterNamedPipeServer { + return { + type: 'pipeServer', + path: x.path + }; + } + + protected convertImplementationToDto(x: DebugAdapterInlineImplementation): IDebugAdapterImpl { + return { + type: 'implementation', + }; + } + private getAdapterDescriptorFactoryByType(type: string): vscode.DebugAdapterDescriptorFactory | undefined { const results = this._adapterFactories.filter(p => p.type === type); if (results.length > 0) { diff --git a/src/vs/workbench/api/common/extHostLanguageModelTools.ts b/src/vs/workbench/api/common/extHostLanguageModelTools.ts index 351a68134a9..56725b94d09 100644 --- a/src/vs/workbench/api/common/extHostLanguageModelTools.ts +++ b/src/vs/workbench/api/common/extHostLanguageModelTools.ts @@ -12,7 +12,7 @@ import { generateUuid } from '../../../base/common/uuid.js'; import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { ExtHostLanguageModelToolsShape, IMainContext, IToolDataDto, MainContext, MainThreadLanguageModelToolsShape } from './extHost.protocol.js'; import * as typeConvert from './extHostTypeConverters.js'; -import { IToolInvocation, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; +import { IToolInvocation, IToolInvocationContext, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; import type * as vscode from 'vscode'; export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape { @@ -55,6 +55,7 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape callId, parameters: options.parameters, tokenBudget: options.tokenOptions?.tokenBudget, + context: options.toolInvocationToken as IToolInvocationContext | undefined, }, token); return typeConvert.LanguageModelToolResult.to(result); } finally { @@ -80,7 +81,7 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape throw new Error(`Unknown tool ${dto.toolId}`); } - const options: vscode.LanguageModelToolInvocationOptions = { parameters: dto.parameters }; + const options: vscode.LanguageModelToolInvocationOptions = { parameters: dto.parameters, toolInvocationToken: dto.context }; if (dto.tokenBudget !== undefined) { options.tokenOptions = { tokenBudget: dto.tokenBudget, @@ -89,6 +90,12 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape }; } + // Some participant in extHostChatAgents calls invokeTool, goes to extHostLMTools + // mainThreadLMTools invokes the tool, which calls back to extHostLMTools + // The tool requests permission + // The tool in extHostLMTools calls for permission back to mainThreadLMTools + // And back to extHostLMTools, and back to the participant in extHostChatAgents + // Is there a tool call ID to identify the call? const extensionResult = await raceCancellation(Promise.resolve(item.tool.invoke(options, token)), token); if (!extensionResult) { throw new CancellationError(); diff --git a/src/vs/workbench/api/common/extHostSCM.ts b/src/vs/workbench/api/common/extHostSCM.ts index 75453dbe3f2..766f5cc9d5f 100644 --- a/src/vs/workbench/api/common/extHostSCM.ts +++ b/src/vs/workbench/api/common/extHostSCM.ts @@ -11,7 +11,7 @@ import { debounce } from '../../../base/common/decorators.js'; import { DisposableStore, IDisposable, MutableDisposable } from '../../../base/common/lifecycle.js'; import { asPromise } from '../../../base/common/async.js'; import { ExtHostCommands } from './extHostCommands.js'; -import { MainContext, MainThreadSCMShape, SCMRawResource, SCMRawResourceSplice, SCMRawResourceSplices, IMainContext, ExtHostSCMShape, ICommandDto, MainThreadTelemetryShape, SCMGroupFeatures, SCMHistoryItemDto, SCMHistoryItemChangeDto } from './extHost.protocol.js'; +import { MainContext, MainThreadSCMShape, SCMRawResource, SCMRawResourceSplice, SCMRawResourceSplices, IMainContext, ExtHostSCMShape, ICommandDto, MainThreadTelemetryShape, SCMGroupFeatures, SCMHistoryItemDto, SCMHistoryItemChangeDto, SCMHistoryItemRefDto } from './extHost.protocol.js'; import { sortedDiff, equals } from '../../../base/common/arrays.js'; import { comparePaths } from '../../../base/common/comparers.js'; import type * as vscode from 'vscode'; @@ -72,11 +72,15 @@ function getHistoryItemIconDto(icon: vscode.Uri | { light: vscode.Uri; dark: vsc } function toSCMHistoryItemDto(historyItem: vscode.SourceControlHistoryItem): SCMHistoryItemDto { - const labels = historyItem.labels?.map(l => ({ - title: l.title, icon: getHistoryItemIconDto(l.icon) + const references = historyItem.references?.map(r => ({ + ...r, icon: getHistoryItemIconDto(r.icon) })); - return { ...historyItem, labels }; + return { ...historyItem, references }; +} + +function toSCMHistoryItemRefDto(historyItemRef?: vscode.SourceControlHistoryItemRef): SCMHistoryItemRefDto | undefined { + return historyItemRef ? { ...historyItemRef, icon: getHistoryItemIconDto(historyItemRef.icon) } : undefined; } function compareResourceThemableDecorations(a: vscode.SourceControlResourceThemableDecorations, b: vscode.SourceControlResourceThemableDecorations): number { @@ -577,7 +581,6 @@ class ExtHostSourceControl implements vscode.SourceControl { private _historyProvider: vscode.SourceControlHistoryProvider | undefined; private readonly _historyProviderDisposable = new MutableDisposable(); - private _historyProviderCurrentHistoryItemGroup: vscode.SourceControlHistoryItemGroup | undefined; get historyProvider(): vscode.SourceControlHistoryProvider | undefined { checkProposedApiEnabled(this._extension, 'scmHistoryProvider'); @@ -593,9 +596,23 @@ class ExtHostSourceControl implements vscode.SourceControl { this.#proxy.$updateSourceControl(this.handle, { hasHistoryProvider: !!historyProvider }); if (historyProvider) { - this._historyProviderDisposable.value.add(historyProvider.onDidChangeCurrentHistoryItemGroup(() => { - this._historyProviderCurrentHistoryItemGroup = historyProvider?.currentHistoryItemGroup; - this.#proxy.$onDidChangeHistoryProviderCurrentHistoryItemGroup(this.handle, this._historyProviderCurrentHistoryItemGroup); + this._historyProviderDisposable.value.add(historyProvider.onDidChangeCurrentHistoryItemRefs(() => { + const historyItemRef = toSCMHistoryItemRefDto(historyProvider?.currentHistoryItemRef); + const historyItemRemoteRef = toSCMHistoryItemRefDto(historyProvider?.currentHistoryItemRemoteRef); + const historyItemBaseRef = toSCMHistoryItemRefDto(historyProvider?.currentHistoryItemBaseRef); + + this.#proxy.$onDidChangeHistoryProviderCurrentHistoryItemRefs(this.handle, historyItemRef, historyItemRemoteRef, historyItemBaseRef); + })); + this._historyProviderDisposable.value.add(historyProvider.onDidChangeHistoryItemRefs((e) => { + if (e.added.length === 0 && e.modified.length === 0 && e.removed.length === 0) { + return; + } + + const added = e.added.map(ref => ({ ...ref, icon: getHistoryItemIconDto(ref.icon) })); + const modified = e.modified.map(ref => ({ ...ref, icon: getHistoryItemIconDto(ref.icon) })); + const removed = e.removed.map(ref => ({ ...ref, icon: getHistoryItemIconDto(ref.icon) })); + + this.#proxy.$onDidChangeHistoryProviderHistoryItemRefs(this.handle, { added, modified, removed }); })); } } @@ -977,9 +994,16 @@ export class ExtHostSCM implements ExtHostSCMShape { return Promise.resolve(undefined); } - async $resolveHistoryItemGroupCommonAncestor(sourceControlHandle: number, historyItemGroupIds: string[], token: CancellationToken): Promise { + async $resolveHistoryItemRefsCommonAncestor(sourceControlHandle: number, historyItemRefs: string[], token: CancellationToken): Promise { const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider; - return await historyProvider?.resolveHistoryItemGroupCommonAncestor(historyItemGroupIds, token) ?? undefined; + return await historyProvider?.resolveHistoryItemRefsCommonAncestor(historyItemRefs, token) ?? undefined; + } + + async $provideHistoryItemRefs(sourceControlHandle: number, token: CancellationToken): Promise { + const historyProvider = this._sourceControls.get(sourceControlHandle)?.historyProvider; + const historyItemRefs = await historyProvider?.provideHistoryItemRefs(token); + + return historyItemRefs?.map(ref => ({ ...ref, icon: getHistoryItemIconDto(ref.icon) })) ?? undefined; } async $provideHistoryItems(sourceControlHandle: number, options: any, token: CancellationToken): Promise { diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index b71c0f98796..c00c6918af9 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -40,7 +40,7 @@ import { DEFAULT_EDITOR_ASSOCIATION, SaveReason } from '../../common/editor.js'; import { IViewBadge } from '../../common/views.js'; import { ChatAgentLocation, IChatAgentRequest, IChatAgentResult } from '../../contrib/chat/common/chatAgents.js'; import { IChatRequestVariableEntry } from '../../contrib/chat/common/chatModel.js'; -import { IChatAgentDetection, IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatMoveMessage, IChatProgressMessage, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from '../../contrib/chat/common/chatService.js'; +import { IChatAgentDetection, IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatMoveMessage, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from '../../contrib/chat/common/chatService.js'; import { IToolData, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; import * as chatProvider from '../../contrib/chat/common/languageModels.js'; import { DebugTreeItemCollapsibleState, IDebugVisualizationTreeItem } from '../../contrib/debug/common/debug.js'; @@ -2405,6 +2405,18 @@ export namespace ChatResponseMarkdownPart { } } +export namespace ChatResponseCodeblockUriPart { + export function from(part: vscode.ChatResponseCodeblockUriPart): Dto { + return { + kind: 'codeblockUri', + uri: part.value, + }; + } + export function to(part: Dto): vscode.ChatResponseCodeblockUriPart { + return new types.ChatResponseCodeblockUriPart(URI.revive(part.uri)); + } +} + export namespace ChatResponseMarkdownWithVulnerabilitiesPart { export function from(part: vscode.ChatResponseMarkdownWithVulnerabilitiesPart): Dto { return { @@ -2486,18 +2498,27 @@ export namespace ChatResponseAnchorPart { export function from(part: vscode.ChatResponseAnchorPart): Dto { // Work around type-narrowing confusion between vscode.Uri and URI const isUri = (thing: unknown): thing is vscode.Uri => URI.isUri(thing); + const isSymbolInformation = (x: any): x is vscode.SymbolInformation => x instanceof types.SymbolInformation; return { kind: 'inlineReference', name: part.title, - inlineReference: isUri(part.value) ? part.value : Location.from(part.value) + inlineReference: isUri(part.value) + ? part.value + : isSymbolInformation(part.value) + ? WorkspaceSymbol.from(part.value) + : Location.from(part.value) }; } export function to(part: Dto): vscode.ChatResponseAnchorPart { const value = revive(part); return new types.ChatResponseAnchorPart( - URI.isUri(value.inlineReference) ? value.inlineReference : Location.to(value.inlineReference), + URI.isUri(value.inlineReference) + ? value.inlineReference + : 'location' in value.inlineReference + ? WorkspaceSymbol.to(value.inlineReference) as vscode.SymbolInformation + : Location.to(value.inlineReference), part.name ); } @@ -2664,6 +2685,8 @@ export namespace ChatResponsePart { return ChatResponseTextEditPart.from(part); } else if (part instanceof types.ChatResponseMarkdownWithVulnerabilitiesPart) { return ChatResponseMarkdownWithVulnerabilitiesPart.from(part); + } else if (part instanceof types.ChatResponseCodeblockUriPart) { + return ChatResponseCodeblockUriPart.from(part); } else if (part instanceof types.ChatResponseDetectedParticipantPart) { return ChatResponseDetectedParticipantPart.from(part); } else if (part instanceof types.ChatResponseWarningPart) { @@ -2724,6 +2747,7 @@ export namespace ChatAgentRequest { acceptedConfirmationData: request.acceptedConfirmationData, rejectedConfirmationData: request.rejectedConfirmationData, location2, + toolInvocationToken: Object.freeze({ sessionId: request.sessionId }) }; } } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 842dbbf5679..ada4e5e5156 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4423,9 +4423,11 @@ export class ChatResponseFileTreePart { export class ChatResponseAnchorPart { value: vscode.Uri | vscode.Location; + value2: vscode.Uri | vscode.Location | vscode.SymbolInformation; title?: string; - constructor(value: vscode.Uri | vscode.Location, title?: string) { - this.value = value; + constructor(value: vscode.Uri | vscode.Location | vscode.SymbolInformation, title?: string) { + this.value = value as any; + this.value2 = value; this.title = title; } } @@ -4475,6 +4477,13 @@ export class ChatResponseReferencePart { } } +export class ChatResponseCodeblockUriPart { + value: vscode.Uri; + constructor(value: vscode.Uri) { + this.value = value; + } +} + export class ChatResponseCodeCitationPart { value: vscode.Uri; license: string; diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index ed9f4f68826..33e1b4d34ac 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import type * as vscode from 'vscode'; import { createCancelablePromise, firstParallel, timeout } from '../../../base/common/async.js'; import { IDisposable } from '../../../base/common/lifecycle.js'; import * as platform from '../../../base/common/platform.js'; @@ -11,23 +12,21 @@ import { IExternalTerminalService } from '../../../platform/externalTerminal/com import { LinuxExternalTerminalService, MacExternalTerminalService, WindowsExternalTerminalService } from '../../../platform/externalTerminal/node/externalTerminalService.js'; import { ISignService } from '../../../platform/sign/common/sign.js'; import { SignService } from '../../../platform/sign/node/signService.js'; +import { AbstractDebugAdapter } from '../../contrib/debug/common/abstractDebugAdapter.js'; +import { ExecutableDebugAdapter, NamedPipeDebugAdapter, SocketDebugAdapter } from '../../contrib/debug/node/debugAdapter.js'; +import { hasChildProcesses, prepareCommand } from '../../contrib/debug/node/terminals.js'; +import { ExtensionDescriptionRegistry } from '../../services/extensions/common/extensionDescriptionRegistry.js'; +import { IExtHostCommands } from '../common/extHostCommands.js'; +import { ExtHostConfigProvider, IExtHostConfiguration } from '../common/extHostConfiguration.js'; import { ExtHostDebugServiceBase, ExtHostDebugSession } from '../common/extHostDebugService.js'; import { IExtHostEditorTabs } from '../common/extHostEditorTabs.js'; import { IExtHostExtensionService } from '../common/extHostExtensionService.js'; import { IExtHostRpcService } from '../common/extHostRpcService.js'; import { IExtHostTerminalService } from '../common/extHostTerminalService.js'; -import { DebugAdapterExecutable, ThemeIcon } from '../common/extHostTypes.js'; +import { IExtHostTesting } from '../common/extHostTesting.js'; +import { DebugAdapterExecutable, DebugAdapterNamedPipeServer, DebugAdapterServer, ThemeIcon } from '../common/extHostTypes.js'; import { IExtHostVariableResolverProvider } from '../common/extHostVariableResolverService.js'; import { IExtHostWorkspace } from '../common/extHostWorkspace.js'; -import { AbstractDebugAdapter } from '../../contrib/debug/common/abstractDebugAdapter.js'; -import { IAdapterDescriptor } from '../../contrib/debug/common/debug.js'; -import { ExecutableDebugAdapter, NamedPipeDebugAdapter, SocketDebugAdapter } from '../../contrib/debug/node/debugAdapter.js'; -import { hasChildProcesses, prepareCommand } from '../../contrib/debug/node/terminals.js'; -import { ExtensionDescriptionRegistry } from '../../services/extensions/common/extensionDescriptionRegistry.js'; -import type * as vscode from 'vscode'; -import { ExtHostConfigProvider, IExtHostConfiguration } from '../common/extHostConfiguration.js'; -import { IExtHostCommands } from '../common/extHostCommands.js'; -import { IExtHostTesting } from '../common/extHostTesting.js'; export class ExtHostDebugService extends ExtHostDebugServiceBase { @@ -50,16 +49,16 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { super(extHostRpcService, workspaceService, extensionService, configurationService, editorTabs, variableResolver, commands, testing); } - protected override createDebugAdapter(adapter: IAdapterDescriptor, session: ExtHostDebugSession): AbstractDebugAdapter | undefined { - switch (adapter.type) { - case 'server': - return new SocketDebugAdapter(adapter); - case 'pipeServer': - return new NamedPipeDebugAdapter(adapter); - case 'executable': - return new ExecutableDebugAdapter(adapter, session.type); + protected override createDebugAdapter(adapter: vscode.DebugAdapterDescriptor, session: ExtHostDebugSession): AbstractDebugAdapter | undefined { + if (adapter instanceof DebugAdapterExecutable) { + return new ExecutableDebugAdapter(this.convertExecutableToDto(adapter), session.type); + } else if (adapter instanceof DebugAdapterServer) { + return new SocketDebugAdapter(this.convertServerToDto(adapter)); + } else if (adapter instanceof DebugAdapterNamedPipeServer) { + return new NamedPipeDebugAdapter(this.convertPipeServerToDto(adapter)); + } else { + return super.createDebugAdapter(adapter, session); } - return super.createDebugAdapter(adapter, session); } protected override daExecutableFromPackage(session: ExtHostDebugSession, extensionRegistry: ExtensionDescriptionRegistry): DebugAdapterExecutable | undefined { diff --git a/src/vs/workbench/api/node/extHostExtensionService.ts b/src/vs/workbench/api/node/extHostExtensionService.ts index 98c7296433b..af286171bce 100644 --- a/src/vs/workbench/api/node/extHostExtensionService.ts +++ b/src/vs/workbench/api/node/extHostExtensionService.ts @@ -46,6 +46,18 @@ class NodeModuleRequireInterceptor extends RequireInterceptor { return originalLookup.call(this, applyAlternatives(request), parent); }; + const originalResolveFilename = node_module._resolveFilename; + node_module._resolveFilename = function resolveFilename(request: string, parent: unknown, isMain: boolean, options?: { paths?: string[] }) { + if (request === 'vsda' && Array.isArray(options?.paths) && options.paths.length === 0) { + // ESM: ever since we moved to ESM, `require.main` will be `undefined` for extensions + // Some extensions have been using `require.resolve('vsda', { paths: require.main.paths })` + // to find the `vsda` module in our app root. To be backwards compatible with this pattern, + // we help by filling in the `paths` array with the node modules paths of the current module. + options.paths = node_module._nodeModulePaths(import.meta.dirname); + } + return originalResolveFilename.call(this, request, parent, isMain, options); + }; + const applyAlternatives = (request: string) => { for (const alternativeModuleName of that._alternatives) { const alternative = alternativeModuleName(request); diff --git a/src/vs/workbench/browser/actions/helpActions.ts b/src/vs/workbench/browser/actions/helpActions.ts index df5d8f99d3d..3a366903e8a 100644 --- a/src/vs/workbench/browser/actions/helpActions.ts +++ b/src/vs/workbench/browser/actions/helpActions.ts @@ -15,6 +15,7 @@ import { IProductService } from '../../../platform/product/common/productService import { ServicesAccessor } from '../../../platform/instantiation/common/instantiation.js'; import { KeybindingWeight } from '../../../platform/keybinding/common/keybindingsRegistry.js'; import { Categories } from '../../../platform/action/common/actionCommonCategories.js'; +import { ICommandService } from '../../../platform/commands/common/commands.js'; class KeybindingsReferenceAction extends Action2 { @@ -307,6 +308,27 @@ class OpenPrivacyStatementUrlAction extends Action2 { } } +class GetStartedWithAccessibilityFeatures extends Action2 { + static readonly ID = 'workbench.action.getStartedWithAccessibilityFeatures'; + constructor() { + super({ + id: GetStartedWithAccessibilityFeatures.ID, + title: localize2('getStartedWithAccessibilityFeatures', 'Get Started with Accessibility Features'), + category: Categories.Help, + f1: true, + menu: { + id: MenuId.MenubarHelpMenu, + group: '1_welcome', + order: 6 + } + }); + } + run(accessor: ServicesAccessor): void { + const commandService = accessor.get(ICommandService); + commandService.executeCommand('workbench.action.openWalkthrough', 'SetupAccessibility'); + } +} + // --- Actions Registration if (KeybindingsReferenceAction.AVAILABLE) { @@ -344,3 +366,5 @@ if (OpenLicenseUrlAction.AVAILABLE) { if (OpenPrivacyStatementUrlAction.AVAILABE) { registerAction2(OpenPrivacyStatementUrlAction); } + +registerAction2(GetStartedWithAccessibilityFeatures); diff --git a/src/vs/workbench/browser/parts/activitybar/media/activityaction.css b/src/vs/workbench/browser/parts/activitybar/media/activityaction.css index b40341d217c..22cd4083df3 100644 --- a/src/vs/workbench/browser/parts/activitybar/media/activityaction.css +++ b/src/vs/workbench/browser/parts/activitybar/media/activityaction.css @@ -169,23 +169,6 @@ text-align: center; } -.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .profile-badge .profile-icon-overlay { - position: absolute; - top: 27px; - right: 6px; - background-color: var(--vscode-activityBar-background); -} - -.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .profile-badge .profile-icon-overlay .codicon { - color: var(--vscode-activityBar-inactiveForeground); -} - -.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.active .profile-badge .profile-icon-overlay .codicon, -.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item:focus .profile-badge .profile-icon-overlay .codicon, -.monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item:hover .profile-badge .profile-icon-overlay .codicon { - color: var(--vscode-activityBar-foreground) !important; -} - .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .profile-badge .profile-text-overlay { position: absolute; font-weight: 600; diff --git a/src/vs/workbench/browser/parts/compositeBarActions.ts b/src/vs/workbench/browser/parts/compositeBarActions.ts index 61ddfcd61fb..15f6b06331c 100644 --- a/src/vs/workbench/browser/parts/compositeBarActions.ts +++ b/src/vs/workbench/browser/parts/compositeBarActions.ts @@ -10,7 +10,7 @@ import { ICommandService } from '../../../platform/commands/common/commands.js'; import { toDisposable, DisposableStore, MutableDisposable } from '../../../base/common/lifecycle.js'; import { IContextMenuService } from '../../../platform/contextview/browser/contextView.js'; import { IThemeService, IColorTheme } from '../../../platform/theme/common/themeService.js'; -import { NumberBadge, IBadge, IActivity, ProgressBadge } from '../../services/activity/common/activity.js'; +import { NumberBadge, IBadge, IActivity, ProgressBadge, IconBadge } from '../../services/activity/common/activity.js'; import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js'; import { DelayedDragHandler } from '../../../base/browser/dnd.js'; import { IKeybindingService } from '../../../platform/keybinding/common/keybinding.js'; @@ -154,7 +154,7 @@ export class CompositeBarActionViewItem extends BaseActionViewItem { protected override readonly options: ICompositeBarActionViewItemOptions; private badgeContent: HTMLElement | undefined; - private readonly badgeDisposable = this._register(new MutableDisposable()); + private readonly badgeDisposable = this._register(new MutableDisposable()); private mouseUpTimeout: any; private keybindingLabel: string | undefined | null; @@ -214,9 +214,10 @@ export class CompositeBarActionViewItem extends BaseActionViewItem { // Badge if (this.badgeContent) { - const badgeFg = colors.badgeForeground ?? theme.getColor(badgeForeground); - const badgeBg = colors.badgeBackground ?? theme.getColor(badgeBackground); - const contrastBorderColor = theme.getColor(contrastBorder); + const badgeStyles = this.getActivity()?.badge.getColors(theme); + const badgeFg = badgeStyles?.badgeForeground ?? colors.badgeForeground ?? theme.getColor(badgeForeground); + const badgeBg = badgeStyles?.badgeBackground ?? colors.badgeBackground ?? theme.getColor(badgeBackground); + const contrastBorderColor = badgeStyles?.badgeBorder ?? theme.getColor(contrastBorder); this.badgeContent.style.color = badgeFg ? badgeFg.toString() : ''; this.badgeContent.style.backgroundColor = badgeBg ? badgeBg.toString() : ''; @@ -285,15 +286,21 @@ export class CompositeBarActionViewItem extends BaseActionViewItem { this.updateStyles(); } + private getActivity(): IActivity | undefined { + if (this._action instanceof CompositeBarAction) { + return this._action.activity; + } + return undefined; + } + protected updateActivity(): void { - const action = this.action; - if (!this.badge || !this.badgeContent || !(action instanceof CompositeBarAction)) { + if (!this.badge || !this.badgeContent || !(this._action instanceof CompositeBarAction)) { return; } - const activity = action.activity; + const activity = this.getActivity(); - this.badgeDisposable.clear(); + this.badgeDisposable.value = new DisposableStore(); clearNode(this.badgeContent); hide(this.badge); @@ -336,14 +343,24 @@ export class CompositeBarActionViewItem extends BaseActionViewItem { } } + // Icon + else if (badge instanceof IconBadge) { + classes.push('icon-badge'); + const badgeContentClassess = ['icon-overlay', ...ThemeIcon.asClassNameArray(badge.icon)]; + this.badgeContent.classList.add(...badgeContentClassess); + this.badgeDisposable.value.add(toDisposable(() => this.badgeContent?.classList.remove(...badgeContentClassess))); + show(this.badge); + } + if (classes.length) { this.badge.classList.add(...classes); - this.badgeDisposable.value = toDisposable(() => this.badge.classList.remove(...classes)); + this.badgeDisposable.value.add(toDisposable(() => this.badge.classList.remove(...classes))); } } this.updateTitle(); + this.updateStyles(); } protected override updateLabel(): void { diff --git a/src/vs/workbench/browser/parts/editor/editorTabsControl.ts b/src/vs/workbench/browser/parts/editor/editorTabsControl.ts index 477f0115bf1..d03297e004f 100644 --- a/src/vs/workbench/browser/parts/editor/editorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorTabsControl.ts @@ -47,6 +47,8 @@ import { ServiceCollection } from '../../../../platform/instantiation/common/ser import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js'; import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { IBaseActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; +import { MarkdownString } from '../../../../base/common/htmlContent.js'; +import { IManagedHoverTooltipMarkdownString } from '../../../../base/browser/ui/hover/hover.js'; export class EditorCommandsContextActionRunner extends ActionRunner { @@ -452,8 +454,17 @@ export abstract class EditorTabsControl extends Themable implements IEditorTabsC return this.groupsView.partOptions.tabHeight !== 'compact' ? EditorTabsControl.EDITOR_TAB_HEIGHT.normal : EditorTabsControl.EDITOR_TAB_HEIGHT.compact; } - protected getHoverTitle(editor: EditorInput): string { - return editor.getTitle(Verbosity.LONG); + protected getHoverTitle(editor: EditorInput): string | IManagedHoverTooltipMarkdownString { + const title = editor.getTitle(Verbosity.LONG); + if (!this.tabsModel.isPinned(editor)) { + return { + markdown: new MarkdownString('', { supportThemeIcons: true, isTrusted: true }). + appendText(title). + appendMarkdown(' (_preview_ [$(gear)](command:workbench.action.openSettings?%5B%22workbench.editor.enablePreview%22%5D "Configure Preview Mode"))'), + markdownNotSupportedFallback: title + ' (preview)' + }; + } + return title; } protected getHoverDelegate(): IHoverDelegate { diff --git a/src/vs/workbench/browser/parts/editor/noEditorTabsControl.ts b/src/vs/workbench/browser/parts/editor/noEditorTabsControl.ts index 061643f23d1..af16587fdb0 100644 --- a/src/vs/workbench/browser/parts/editor/noEditorTabsControl.ts +++ b/src/vs/workbench/browser/parts/editor/noEditorTabsControl.ts @@ -47,9 +47,17 @@ export class NoEditorTabsControl extends EditorTabsControl { beforeCloseEditor(editor: EditorInput): void { } - closeEditor(editor: EditorInput): void { } + closeEditor(editor: EditorInput): void { + this.handleClosedEditors(); + } - closeEditors(editors: EditorInput[]): void { } + closeEditors(editors: EditorInput[]): void { + this.handleClosedEditors(); + } + + private handleClosedEditors(): void { + this.activeEditor = this.tabsModel.activeEditor; + } moveEditor(editor: EditorInput, fromIndex: number, targetIndex: number): void { } diff --git a/src/vs/workbench/browser/parts/globalCompositeBar.ts b/src/vs/workbench/browser/parts/globalCompositeBar.ts index 34f790d6107..8c96ab3e461 100644 --- a/src/vs/workbench/browser/parts/globalCompositeBar.ts +++ b/src/vs/workbench/browser/parts/globalCompositeBar.ts @@ -602,8 +602,7 @@ export class GlobalActivityActionViewItem extends AbstractGlobalActivityActionVi } show(this.profileBadge); - this.profileBadgeContent.classList.toggle('profile-text-overlay', true); - this.profileBadgeContent.classList.toggle('profile-icon-overlay', false); + this.profileBadgeContent.classList.add('profile-text-overlay'); this.profileBadgeContent.textContent = this.userDataProfileService.currentProfile.name.substring(0, 2).toUpperCase(); } diff --git a/src/vs/workbench/browser/parts/media/paneCompositePart.css b/src/vs/workbench/browser/parts/media/paneCompositePart.css index 52f116455cf..1f2bd9ee9d5 100644 --- a/src/vs/workbench/browser/parts/media/paneCompositePart.css +++ b/src/vs/workbench/browser/parts/media/paneCompositePart.css @@ -205,6 +205,10 @@ position: relative; } +.monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .icon-badge .badge-content { + padding: 3px; +} + .monaco-workbench .pane-composite-part > .title > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact, .monaco-workbench .pane-composite-part > .header-or-footer > .composite-bar-container > .composite-bar > .monaco-action-bar .action-item.icon .badge.compact { position: absolute; diff --git a/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts b/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts index 779187ad2c3..81db9ad010a 100644 --- a/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts @@ -141,7 +141,7 @@ class CommandCenterCenterViewItem extends BaseActionViewItem { override render(container: HTMLElement): void { super.render(container); container.classList.toggle('command-center-quick-pick'); - + container.role = 'button'; const action = this.action; // icon (search) diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 69140e5302e..069793bae0c 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -396,7 +396,7 @@ export class BrowserMain extends Disposable { this._register(workspaceTrustManagementService.onDidChangeTrust(() => configurationService.updateWorkspaceTrust(workspaceTrustManagementService.isWorkspaceTrusted()))); // Request Service - const requestService = new BrowserRequestService(remoteAgentService, configurationService, loggerService); + const requestService = new BrowserRequestService(remoteAgentService, configurationService, logService); serviceCollection.set(IRequestService, requestService); // Userdata Sync Store Management Service diff --git a/src/vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts b/src/vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts index 8f2e6afed40..28d6fe3edef 100644 --- a/src/vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts +++ b/src/vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.ts @@ -5,20 +5,19 @@ import { disposableTimeout } from '../../../../base/common/async.js'; import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; -import { IReader, autorun, autorunWithStore, derived, observableFromEvent, observableFromPromise } from '../../../../base/common/observable.js'; -import { observableFromValueWithChangeEvent, observableSignalFromEvent, wasEventTriggeredRecently } from '../../../../base/common/observableInternal/utils.js'; +import { IReader, autorun, autorunWithStore, derived, observableFromEvent, observableFromPromise, observableFromValueWithChangeEvent, observableSignalFromEvent, wasEventTriggeredRecently } from '../../../../base/common/observable.js'; import { isDefined } from '../../../../base/common/types.js'; import { ICodeEditor, isCodeEditor, isDiffEditor } from '../../../../editor/browser/editorBrowser.js'; import { Position } from '../../../../editor/common/core/position.js'; import { CursorChangeReason } from '../../../../editor/common/cursorEvents.js'; import { ITextModel } from '../../../../editor/common/model.js'; import { FoldingController } from '../../../../editor/contrib/folding/browser/folding.js'; -import { AccessibilitySignal, AccessibilityModality, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; +import { AccessibilityModality, AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; -import { IDebugService } from '../../debug/common/debug.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { IDebugService } from '../../debug/common/debug.js'; export class EditorTextPropertySignalsContribution extends Disposable implements IWorkbenchContribution { private readonly _textProperties: TextProperty[] = [ diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview.ts index 8b9a0c4eb65..ad2d7ebb97e 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview.ts @@ -307,18 +307,26 @@ export class BulkFileOperations { return result; } - getFileEdits(uri: URI): ISingleEditOperation[] { + private async getFileEditOperation(edit: ResourceFileEdit): Promise { + const content = await edit.options.contents; + if (!content) { return undefined; } + return EditOperation.replaceMove(Range.lift({ startLineNumber: 0, startColumn: 0, endLineNumber: Number.MAX_VALUE, endColumn: 0 }), content.toString()); + } + + async getFileEdits(uri: URI): Promise { for (const file of this.fileOperations) { if (file.uri.toString() === uri.toString()) { - const result: ISingleEditOperation[] = []; + const result: Promise[] = []; let ignoreAll = false; for (const edit of file.originalEdits.values()) { - if (edit instanceof ResourceTextEdit) { + if (edit instanceof ResourceFileEdit) { + result.push(this.getFileEditOperation(edit)); + } else if (edit instanceof ResourceTextEdit) { if (this.checked.isChecked(edit)) { - result.push(EditOperation.replaceMove(Range.lift(edit.textEdit.range), !edit.textEdit.insertAsSnippet ? edit.textEdit.text : SnippetParser.asInsertText(edit.textEdit.text))); + result.push(Promise.resolve(EditOperation.replaceMove(Range.lift(edit.textEdit.range), !edit.textEdit.insertAsSnippet ? edit.textEdit.text : SnippetParser.asInsertText(edit.textEdit.text)))); } } else if (!this.checked.isChecked(edit)) { @@ -331,7 +339,7 @@ export class BulkFileOperations { return []; } - return result.sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); + return (await Promise.all(result)).filter(r => r !== undefined).sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); } } return []; @@ -402,7 +410,7 @@ export class BulkEditPreviewProvider implements ITextModelContentProvider { model.applyEdits(undoEdits); } // apply new edits and keep (future) undo edits - const newEdits = this._operations.getFileEdits(uri); + const newEdits = await this._operations.getFileEdits(uri); const newUndoEdits = model.applyEdits(newEdits, true); this._modelPreviewEdits.set(model.id, newUndoEdits); } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index e990dadc0ef..1c06c235045 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -3,10 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { coalesce } from '../../../../../base/common/arrays.js'; +import { AsyncIterableObject } from '../../../../../base/common/async.js'; import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; +import { CharCode } from '../../../../../base/common/charCode.js'; import { Codicon } from '../../../../../base/common/codicons.js'; import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; +import { ResourceMap } from '../../../../../base/common/map.js'; import { isEqual } from '../../../../../base/common/resources.js'; +import * as strings from '../../../../../base/common/strings.js'; +import { URI } from '../../../../../base/common/uri.js'; import { IActiveCodeEditor, ICodeEditor, isCodeEditor, isDiffEditor } from '../../../../../editor/browser/editorBrowser.js'; import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; import { IBulkEditService, ResourceTextEdit } from '../../../../../editor/browser/services/bulkEditService.js'; @@ -28,26 +34,20 @@ import { INotificationService, Severity } from '../../../../../platform/notifica import { IProgressService, ProgressLocation } from '../../../../../platform/progress/common/progress.js'; import { TerminalLocation } from '../../../../../platform/terminal/common/terminal.js'; import { IUntitledTextResourceEditorInput } from '../../../../common/editor.js'; +import { IEditorService } from '../../../../services/editor/common/editorService.js'; +import { ITextFileService } from '../../../../services/textfile/common/textfiles.js'; import { accessibleViewInCodeBlock } from '../../../accessibility/browser/accessibilityConfiguration.js'; -import { CHAT_CATEGORY } from './chatActions.js'; -import { IChatWidgetService, IChatCodeBlockContextProviderService } from '../chat.js'; -import { DefaultChatTextEditor, ICodeBlockActionContext, ICodeCompareBlockActionContext } from '../codeBlockPart.js'; -import { CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_CHAT_ENABLED, CONTEXT_CHAT_EDIT_APPLIED } from '../../common/chatContextKeys.js'; -import { ChatCopyKind, IChatContentReference, IChatService, IDocumentContext } from '../../common/chatService.js'; -import { IChatResponseViewModel, isRequestVM, isResponseVM } from '../../common/chatViewModel.js'; +import { InlineChatController } from '../../../inlineChat/browser/inlineChatController.js'; import { insertCell } from '../../../notebook/browser/controller/cellOperations.js'; import { INotebookEditor } from '../../../notebook/browser/notebookBrowser.js'; import { CellKind, NOTEBOOK_EDITOR_ID } from '../../../notebook/common/notebookCommon.js'; import { ITerminalEditorService, ITerminalGroupService, ITerminalService } from '../../../terminal/browser/terminal.js'; -import { IEditorService } from '../../../../services/editor/common/editorService.js'; -import { ITextFileService } from '../../../../services/textfile/common/textfiles.js'; -import * as strings from '../../../../../base/common/strings.js'; -import { CharCode } from '../../../../../base/common/charCode.js'; -import { InlineChatController } from '../../../inlineChat/browser/inlineChatController.js'; -import { coalesce } from '../../../../../base/common/arrays.js'; -import { AsyncIterableObject } from '../../../../../base/common/async.js'; -import { ResourceMap } from '../../../../../base/common/map.js'; -import { URI } from '../../../../../base/common/uri.js'; +import { CONTEXT_CHAT_EDIT_APPLIED, CONTEXT_CHAT_ENABLED, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION } from '../../common/chatContextKeys.js'; +import { ChatCopyKind, IChatContentReference, IChatService, IDocumentContext } from '../../common/chatService.js'; +import { IChatResponseViewModel, isRequestVM, isResponseVM } from '../../common/chatViewModel.js'; +import { IChatCodeBlockContextProviderService, IChatWidgetService } from '../chat.js'; +import { DefaultChatTextEditor, ICodeBlockActionContext, ICodeCompareBlockActionContext } from '../codeBlockPart.js'; +import { CHAT_CATEGORY } from './chatActions.js'; const shellLangIds = [ 'fish', @@ -158,14 +158,26 @@ abstract class InsertCodeBlockAction extends ChatCodeBlockAction { override async runWithContext(accessor: ServicesAccessor, context: ICodeBlockActionContext) { const editorService = accessor.get(IEditorService); const textFileService = accessor.get(ITextFileService); + const bulkEditService = accessor.get(IBulkEditService); + const codeEditorService = accessor.get(ICodeEditorService); + const chatService = accessor.get(IChatService); + const languageFeaturesService = accessor.get(ILanguageFeaturesService); + const notificationService = accessor.get(INotificationService); + const progressService = accessor.get(IProgressService); + const languageService = accessor.get(ILanguageService); if (isResponseFiltered(context)) { // When run from command palette return; } + if (context.codemapperUri) { + // If the code block is from a code mapper, first reveal the target file + await editorService.openEditor({ resource: context.codemapperUri }); + } + if (editorService.activeEditorPane?.getId() === NOTEBOOK_EDITOR_ID) { - return this.handleNotebookEditor(accessor, editorService.activeEditorPane.getControl() as INotebookEditor, context); + return this.handleNotebookEditor(languageService, progressService, notificationService, languageFeaturesService, bulkEditService, codeEditorService, chatService, editorService.activeEditorPane.getControl() as INotebookEditor, context); } let activeEditorControl = editorService.activeTextEditorControl; @@ -188,10 +200,10 @@ abstract class InsertCodeBlockAction extends ChatCodeBlockAction { return; } - await this.handleTextEditor(accessor, activeEditorControl, context); + await this.handleTextEditor(progressService, notificationService, languageFeaturesService, bulkEditService, codeEditorService, chatService, activeEditorControl, context); } - private async handleNotebookEditor(accessor: ServicesAccessor, notebookEditor: INotebookEditor, context: ICodeBlockActionContext) { + private async handleNotebookEditor(languageService: ILanguageService, progressService: IProgressService, notificationService: INotificationService, languageFeaturesService: ILanguageFeaturesService, bulkEditService: IBulkEditService, codeEditorService: ICodeEditorService, chatService: IChatService, notebookEditor: INotebookEditor, context: ICodeBlockActionContext) { if (!notebookEditor.hasModel()) { return; } @@ -203,37 +215,36 @@ abstract class InsertCodeBlockAction extends ChatCodeBlockAction { if (notebookEditor.activeCodeEditor?.hasTextFocus()) { const codeEditor = notebookEditor.activeCodeEditor; if (codeEditor.hasModel()) { - return this.handleTextEditor(accessor, codeEditor, context); + return this.handleTextEditor(progressService, notificationService, languageFeaturesService, bulkEditService, codeEditorService, chatService, codeEditor, context); } } - const languageService = accessor.get(ILanguageService); - const chatService = accessor.get(IChatService); - const focusRange = notebookEditor.getFocus(); const next = Math.max(focusRange.end - 1, 0); insertCell(languageService, notebookEditor, next, CellKind.Code, 'below', context.code, true); this.notifyUserAction(chatService, context); } - protected async computeEdits(accessor: ServicesAccessor, codeEditor: IActiveCodeEditor, codeBlockActionContext: ICodeBlockActionContext): Promise { + protected async computeEdits(progressService: IProgressService, notificationService: INotificationService, languageFeaturesService: ILanguageFeaturesService, bulkEditService: IBulkEditService, codeEditorService: ICodeEditorService, chatService: IChatService, codeEditor: IActiveCodeEditor, codeBlockActionContext: ICodeBlockActionContext): Promise { const activeModel = codeEditor.getModel(); const range = codeEditor.getSelection() ?? new Range(activeModel.getLineCount(), 1, activeModel.getLineCount(), 1); const text = reindent(codeBlockActionContext.code, activeModel, range.startLineNumber); - return { edits: [new ResourceTextEdit(activeModel.uri, { range, text })] }; + if (text !== undefined) { + return { edits: [new ResourceTextEdit(activeModel.uri, { range, text })] }; + } + return undefined; } protected get showPreview() { return false; } - private async handleTextEditor(accessor: ServicesAccessor, codeEditor: IActiveCodeEditor, codeBlockActionContext: ICodeBlockActionContext) { - const bulkEditService = accessor.get(IBulkEditService); - const codeEditorService = accessor.get(ICodeEditorService); - const chatService = accessor.get(IChatService); - - const result = await this.computeEdits(accessor, codeEditor, codeBlockActionContext); + private async handleTextEditor(progressService: IProgressService, notificationService: INotificationService, languageFeaturesService: ILanguageFeaturesService, bulkEditService: IBulkEditService, codeEditorService: ICodeEditorService, chatService: IChatService, codeEditor: IActiveCodeEditor, codeBlockActionContext: ICodeBlockActionContext) { + const result = await this.computeEdits(progressService, notificationService, languageFeaturesService, bulkEditService, codeEditorService, chatService, codeEditor, codeBlockActionContext); this.notifyUserAction(chatService, codeBlockActionContext, result); + if (!result) { + return; + } if (this.showPreview) { const showWithPreview = await this.applyWithInlinePreview(codeEditorService, result.edits, codeEditor); @@ -295,10 +306,10 @@ abstract class InsertCodeBlockAction extends ChatCodeBlockAction { } -function reindent(codeBlockContent: string, model: ITextModel, seletionStartLine: number) { +function reindent(codeBlockContent: string, model: ITextModel, seletionStartLine: number): string | undefined { const newContent = strings.splitLines(codeBlockContent); if (newContent.length === 0) { - return codeBlockContent; + return undefined; } const formattingOptions = model.getFormattingOptions(); @@ -316,7 +327,7 @@ function reindent(codeBlockContent: string, model: ITextModel, seletionStartLine if (newContentIndentLevel === Number.MAX_VALUE || newContentIndentLevel === codeIndentLevel) { // all lines are empty or the indent is already correct - return codeBlockContent; + return undefined; } const newLines = []; for (let i = 0; i < newContent.length; i++) { @@ -483,14 +494,12 @@ export function registerChatCodeBlockActions() { }); } - protected override async computeEdits(accessor: ServicesAccessor, codeEditor: IActiveCodeEditor, codeBlockActionContext: ICodeBlockActionContext): Promise { + protected override async computeEdits(progressService: IProgressService, notificationService: INotificationService, languageFeaturesService: ILanguageFeaturesService, bulkEditService: IBulkEditService, codeEditorService: ICodeEditorService, chatService: IChatService, codeEditor: IActiveCodeEditor, codeBlockActionContext: ICodeBlockActionContext): Promise { - const progressService = accessor.get(IProgressService); - const notificationService = accessor.get(INotificationService); const activeModel = codeEditor.getModel(); - const mappedEditsProviders = accessor.get(ILanguageFeaturesService).mappedEditsProvider.ordered(activeModel); + const mappedEditsProviders = languageFeaturesService.mappedEditsProvider.ordered(activeModel); if (mappedEditsProviders.length > 0) { // 0th sub-array - editor selections array if there are any selections @@ -543,15 +552,14 @@ export function registerChatCodeBlockActions() { } } catch (e) { notificationService.notify({ severity: Severity.Error, message: localize('applyCodeBlock.error', "Failed to apply code block: {0}", e.message) }); - } finally { cancellationTokenSource.dispose(); } } - // fall back to inserting the code block as is - return super.computeEdits(accessor, codeEditor, codeBlockActionContext); + return undefined; } + protected override get showPreview() { return true; } @@ -814,6 +822,7 @@ function getContextFromEditor(editor: ICodeEditor, accessor: ServicesAccessor): codeBlockIndex: codeBlockInfo.codeBlockIndex, code: editor.getValue(), languageId: editor.getModel()!.getLanguageId(), + codemapperUri: codeBlockInfo.codemapperUri }; } diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index fc3af5c6ee6..05dca109bb9 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -73,13 +73,14 @@ export const IChatAccessibilityService = createDecorator; + acceptInput(query?: string, isVoiceInput?: boolean): Promise; acceptInputWithPrefix(prefix: string): void; setInputPlaceholder(placeholder: string): void; resetInputPlaceholder(): void; diff --git a/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts b/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts index cb7fe9da931..763de2b1200 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts @@ -37,7 +37,7 @@ export class ChatAccessibilityService extends Disposable implements IChatAccessi this._pendingSignalMap.set(this._requestId, this._instantiationService.createInstance(AccessibilityProgressSignalScheduler, CHAT_RESPONSE_PENDING_ALLOWANCE_MS, undefined)); return this._requestId; } - acceptResponse(response: IChatResponseViewModel | string | undefined, requestId: number): void { + acceptResponse(response: IChatResponseViewModel | string | undefined, requestId: number, isVoiceInput?: boolean): void { this._pendingSignalMap.deleteAndDispose(requestId); const isPanelChat = typeof response !== 'string'; const responseContent = typeof response === 'string' ? response : response?.response.toString(); @@ -47,7 +47,7 @@ export class ChatAccessibilityService extends Disposable implements IChatAccessi } const errorDetails = isPanelChat && response.errorDetails ? ` ${response.errorDetails.message}` : ''; const plainTextResponse = renderStringAsPlaintext(new MarkdownString(responseContent)); - if (this._configurationService.getValue(AccessibilityVoiceSettingId.AutoSynthesize) !== 'on') { + if (!isVoiceInput || this._configurationService.getValue(AccessibilityVoiceSettingId.AutoSynthesize) !== 'on') { status(plainTextResponse + errorDetails); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts index e6184f510ae..da42f3e51d3 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatMarkdownContentPart.ts @@ -25,6 +25,7 @@ import { IMarkdownVulnerability } from '../../common/annotations.js'; import { IChatProgressRenderableResponseContent } from '../../common/chatModel.js'; import { isRequestVM, isResponseVM } from '../../common/chatViewModel.js'; import { CodeBlockModelCollection } from '../../common/codeBlockModelCollection.js'; +import { URI } from '../../../../../base/common/uri.js'; const $ = dom.$; @@ -66,6 +67,7 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP let textModel: Promise; let range: Range | undefined; let vulns: readonly IMarkdownVulnerability[] | undefined; + let codemapperUri: URI | undefined; if (equalsIgnoreCase(languageId, localFileLanguageId)) { try { const parsedBody = parseLocalFileData(text); @@ -83,11 +85,12 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP const sessionId = isResponseVM(element) || isRequestVM(element) ? element.sessionId : ''; const modelEntry = this.codeBlockModelCollection.getOrCreate(sessionId, element, index); vulns = modelEntry.vulns; + codemapperUri = modelEntry.codemapperUri; textModel = modelEntry.model; } const hideToolbar = isResponseVM(element) && element.errorDetails?.responseIsFiltered; - const ref = this.renderCodeBlock({ languageId, textModel, codeBlockIndex: index, element, range, hideToolbar, parentContextKeyService: contextKeyService, vulns }, text, currentWidth, rendererOptions.editableCodeBlock); + const ref = this.renderCodeBlock({ languageId, textModel, codeBlockIndex: index, element, range, hideToolbar, parentContextKeyService: contextKeyService, vulns, codemapperUri }, text, currentWidth, rendererOptions.editableCodeBlock); this.allRefs.push(ref); // Attach this after updating text/layout of the editor, so it should only be fired when the size updates later (horizontal scrollbar, wrapping) @@ -100,7 +103,8 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP focus() { ref.object.focus(); }, - uri: ref.object.uri + uri: ref.object.uri, + codemapperUri: undefined }; this.codeblocks.push(info); orderedDisposablesList.push(ref); @@ -119,7 +123,9 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP const ref = this.editorPool.get(); const editorInfo = ref.object; if (isResponseVM(data.element)) { - this.codeBlockModelCollection.update(data.element.sessionId, data.element, data.codeBlockIndex, { text, languageId: data.languageId }); + this.codeBlockModelCollection.update(data.element.sessionId, data.element, data.codeBlockIndex, { text, languageId: data.languageId }).then((e) => { + this.codeblocks[data.codeBlockIndex] = { ...this.codeblocks[data.codeBlockIndex]!, codemapperUri: e.codemapperUri }; + }); } editorInfo.render(data, currentWidth, editableCodeBlock); diff --git a/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts b/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts new file mode 100644 index 00000000000..6f3660886e7 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatInlineAnchorWidget.ts @@ -0,0 +1,206 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../base/browser/dom.js'; +import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js'; +import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; +import { IconLabel } from '../../../../base/browser/ui/iconLabel/iconLabel.js'; +import { IAction } from '../../../../base/common/actions.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { URI } from '../../../../base/common/uri.js'; +import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; +import { IRange } from '../../../../editor/common/core/range.js'; +import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; +import { Location, SymbolKinds } from '../../../../editor/common/languages.js'; +import { ILanguageService } from '../../../../editor/common/languages/language.js'; +import { getIconClasses } from '../../../../editor/common/services/getIconClasses.js'; +import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; +import { IModelService } from '../../../../editor/common/services/model.js'; +import { DefinitionAction } from '../../../../editor/contrib/gotoSymbol/browser/goToCommands.js'; +import * as nls from '../../../../nls.js'; +import { createAndFillInContextMenuActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { Action2, IMenuService, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; +import { IHoverService } from '../../../../platform/hover/browser/hover.js'; +import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { ILabelService } from '../../../../platform/label/common/label.js'; +import { fillEditorsDragData } from '../../../browser/dnd.js'; +import { ContentRefData } from '../common/annotations.js'; + +export class InlineAnchorWidget extends Disposable { + + constructor( + element: HTMLAnchorElement, + data: ContentRefData, + @IHoverService hoverService: IHoverService, + @IInstantiationService instantiationService: IInstantiationService, + @ILabelService labelService: ILabelService, + @ILanguageService languageService: ILanguageService, + @IModelService modelService: IModelService, + @IContextMenuService contextMenuService: IContextMenuService, + @IContextKeyService originalContextKeyService: IContextKeyService, + @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, + @IMenuService menuService: IMenuService, + ) { + super(); + + const contextKeyService = this._register(originalContextKeyService.createScoped(element)); + + element.classList.add('chat-inline-anchor-widget', 'show-file-icons'); + element.replaceChildren(); + + const resourceLabel = this._register(new IconLabel(element, { supportHighlights: false, supportIcons: true })); + + let location: { readonly uri: URI; readonly range?: IRange }; + let contextMenuId: MenuId; + let contextMenuArg: URI | { readonly uri: URI; readonly range?: IRange }; + if (data.kind === 'symbol') { + location = data.symbol.location; + contextMenuId = MenuId.ChatInlineSymbolAnchorContext; + contextMenuArg = location; + + const icon = SymbolKinds.toIcon(data.symbol.kind); + resourceLabel.setLabel(`$(${icon.id}) ${data.symbol.name}`, undefined, {}); + + const model = modelService.getModel(location.uri); + if (model) { + const hasDefinitionProvider = EditorContextKeys.hasDefinitionProvider.bindTo(contextKeyService); + const hasReferenceProvider = EditorContextKeys.hasReferenceProvider.bindTo(contextKeyService); + const updateContents = () => { + if (model.isDisposed()) { + return; + } + + hasDefinitionProvider.set(languageFeaturesService.definitionProvider.has(model)); + hasReferenceProvider.set(languageFeaturesService.definitionProvider.has(model)); + }; + updateContents(); + this._register(languageFeaturesService.definitionProvider.onDidChange(updateContents)); + this._register(languageFeaturesService.referenceProvider.onDidChange(updateContents)); + } + } else { + location = data; + contextMenuId = MenuId.ChatInlineResourceAnchorContext; + contextMenuArg = location.uri; + + const label = labelService.getUriBasenameLabel(location.uri); + const title = location.range && data.kind !== 'symbol' ? + `${label}#${location.range.startLineNumber}-${location.range.endLineNumber}` : + label; + + resourceLabel.setLabel(title, undefined, { + extraClasses: getIconClasses(modelService, languageService, location.uri) + }); + } + + const fragment = location.range ? `${location.range.startLineNumber}-${location.range.endLineNumber}` : ''; + element.setAttribute('data-href', location.uri.with({ fragment }).toString()); + + // Context menu + this._register(dom.addDisposableListener(element, dom.EventType.CONTEXT_MENU, domEvent => { + const event = new StandardMouseEvent(dom.getWindow(domEvent), domEvent); + dom.EventHelper.stop(domEvent, true); + + contextMenuService.showContextMenu({ + contextKeyService, + getAnchor: () => event, + getActions: () => { + const menu = menuService.getMenuActions(contextMenuId, contextKeyService, { arg: contextMenuArg }); + const primary: IAction[] = []; + createAndFillInContextMenuActions(menu, primary); + return primary; + }, + }); + })); + + // Hover + const relativeLabel = labelService.getUriLabel(location.uri, { relative: true }); + this._register(hoverService.setupManagedHover(getDefaultHoverDelegate('element'), element, relativeLabel)); + + // Drag and drop + element.draggable = true; + this._register(dom.addDisposableListener(element, 'dragstart', e => { + instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, [location.uri], e)); + + e.dataTransfer?.setDragImage(element, 0, 0); + })); + } +} + +registerAction2(class GoToDefinitionAction extends Action2 { + + static readonly id = 'chat.inlineSymbolAnchor.goToDefinition'; + + constructor() { + super({ + id: GoToDefinitionAction.id, + title: { + ...nls.localize2('actions.goToDecl.label', "Go to Definition"), + mnemonicTitle: nls.localize({ key: 'miGotoDefinition', comment: ['&& denotes a mnemonic'] }, "Go to &&Definition"), + }, + precondition: EditorContextKeys.hasDefinitionProvider, + menu: [{ + id: MenuId.ChatInlineSymbolAnchorContext, + group: 'navigation', + order: 1.1, + },] + }); + } + + override async run(accessor: ServicesAccessor, location: Location): Promise { + const editorService = accessor.get(ICodeEditorService); + + await editorService.openCodeEditor({ + resource: location.uri, options: { + selection: { + startColumn: location.range.startColumn, + startLineNumber: location.range.startLineNumber, + } + } + }, null); + + const action = new DefinitionAction({ openToSide: false, openInPeek: false, muteMessage: true }, { title: { value: '', original: '' }, id: '', precondition: undefined }); + return action.run(accessor); + } +}); + +registerAction2(class GoToReferencesAction extends Action2 { + + static readonly id = 'chat.inlineSymbolAnchor.goToReferences'; + + constructor() { + super({ + id: GoToReferencesAction.id, + title: { + ...nls.localize2('goToReferences.label', "Go to References"), + mnemonicTitle: nls.localize({ key: 'miGotoReference', comment: ['&& denotes a mnemonic'] }, "Go to &&References"), + }, + precondition: EditorContextKeys.hasReferenceProvider, + menu: [{ + id: MenuId.ChatInlineSymbolAnchorContext, + group: 'navigation', + order: 1.1, + },] + }); + } + + override async run(accessor: ServicesAccessor, location: Location): Promise { + const editorService = accessor.get(ICodeEditorService); + const commandService = accessor.get(ICommandService); + + await editorService.openCodeEditor({ + resource: location.uri, options: { + selection: { + startColumn: location.range.startColumn, + startLineNumber: location.range.startLineNumber, + } + } + }, null); + + await commandService.executeCommand('editor.action.goToReferences'); + } +}); diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 3633aba2768..e703d976f87 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -48,16 +48,16 @@ import { IThemeService } from '../../../../platform/theme/common/themeService.js import { ResourceLabels } from '../../../browser/labels.js'; import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; import { AccessibilityCommandId } from '../../accessibility/common/accessibilityCommands.js'; -import { CancelAction, ChatSubmitSecondaryAgentAction, IChatExecuteActionContext, SubmitAction } from './actions/chatExecuteActions.js'; -import { IChatWidget } from './chat.js'; -import { ChatFollowups } from './chatFollowups.js'; +import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions, setupSimpleEditorSelectionStyling } from '../../codeEditor/browser/simpleEditorOptions.js'; import { ChatAgentLocation, IChatAgentService } from '../common/chatAgents.js'; import { CONTEXT_CHAT_INPUT_CURSOR_AT_TOP, CONTEXT_CHAT_INPUT_HAS_FOCUS, CONTEXT_CHAT_INPUT_HAS_TEXT, CONTEXT_IN_CHAT_INPUT } from '../common/chatContextKeys.js'; import { IChatRequestVariableEntry } from '../common/chatModel.js'; import { IChatFollowup } from '../common/chatService.js'; import { IChatResponseViewModel } from '../common/chatViewModel.js'; import { IChatHistoryEntry, IChatWidgetHistoryService } from '../common/chatWidgetHistoryService.js'; -import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions, setupSimpleEditorSelectionStyling } from '../../codeEditor/browser/simpleEditorOptions.js'; +import { CancelAction, ChatSubmitSecondaryAgentAction, IChatExecuteActionContext, SubmitAction } from './actions/chatExecuteActions.js'; +import { IChatWidget } from './chat.js'; +import { ChatFollowups } from './chatFollowups.js'; const $ = dom.$; @@ -598,7 +598,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge get contentHeight(): number { const data = this.getLayoutData(); - return data.followupsHeight + data.inputPartEditorHeight + data.inputPartVerticalPadding + data.inputEditorBorder + data.implicitContextHeight; + return data.followupsHeight + data.inputPartEditorHeight + data.inputPartVerticalPadding + data.inputEditorBorder + data.implicitContextHeight + data.executeToolbarHeight; } layout(height: number, width: number) { @@ -618,10 +618,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const followupsWidth = width - data.inputPartHorizontalPadding; this.followupsContainer.style.width = `${followupsWidth}px`; - this._inputPartHeight = data.followupsHeight + inputEditorHeight + data.inputPartVerticalPadding + data.inputEditorBorder + data.implicitContextHeight; + this._inputPartHeight = data.followupsHeight + inputEditorHeight + data.inputPartVerticalPadding + data.inputEditorBorder + data.implicitContextHeight + data.executeToolbarHeight; const initialEditorScrollWidth = this._inputEditor.getScrollWidth(); - const newEditorWidth = width - data.inputPartHorizontalPadding - data.editorBorder - data.editorPadding - data.executeToolbarWidth - data.sideToolbarWidth - data.toolbarPadding; + const newEditorWidth = width - data.inputPartHorizontalPadding - data.editorBorder - data.editorPadding - data.executeToolbarWidth - data.sideToolbarWidth; const newDimension = { width: newEditorWidth, height: inputEditorHeight }; if (!this.previousInputEditorDimension || (this.previousInputEditorDimension.width !== newDimension.width || this.previousInputEditorDimension.height !== newDimension.height)) { // This layout call has side-effects that are hard to understand. eg if we are calling this inside a onDidChangeContent handler, this can trigger the next onDidChangeContent handler @@ -637,6 +637,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } private getLayoutData() { + const executeToolbarWidth = this.cachedToolbarWidth = this.toolbar.getItemsWidth(); + const executeToolbarPadding = (this.toolbar.getItemsLength() - 1) * 4; return { inputEditorBorder: 2, followupsHeight: this.followupsContainer.offsetHeight, @@ -646,8 +648,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge implicitContextHeight: this.attachedContextContainer.offsetHeight, editorBorder: 2, editorPadding: 12, - toolbarPadding: (this.toolbar.getItemsLength() - 1) * 4, - executeToolbarWidth: this.cachedToolbarWidth = this.toolbar.getItemsWidth(), + executeToolbarWidth: this.options.renderStyle === 'compact' ? executeToolbarWidth + executeToolbarPadding : 0, + executeToolbarHeight: this.options.renderStyle === 'compact' ? 0 : 22, sideToolbarWidth: this.inputSideToolbarContainer ? dom.getTotalWidth(this.inputSideToolbarContainer) + 4 /*gap*/ : 0, }; } diff --git a/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts index d3e17a2ade9..79555a8099f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatMarkdownDecorationsRenderer.ts @@ -3,31 +3,22 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { applyDragImage } from '../../../../base/browser/dnd.js'; import * as dom from '../../../../base/browser/dom.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; -import { IconLabel } from '../../../../base/browser/ui/iconLabel/iconLabel.js'; import { toErrorMessage } from '../../../../base/common/errorMessage.js'; import { Lazy } from '../../../../base/common/lazy.js'; import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; import { revive } from '../../../../base/common/marshalling.js'; import { URI } from '../../../../base/common/uri.js'; -import { Location } from '../../../../editor/common/languages.js'; -import { ILanguageService } from '../../../../editor/common/languages/language.js'; -import { getIconClasses } from '../../../../editor/common/services/getIconClasses.js'; -import { IModelService } from '../../../../editor/common/services/model.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { ILabelService } from '../../../../platform/label/common/label.js'; import { ILogService } from '../../../../platform/log/common/log.js'; -import { listActiveSelectionBackground, listActiveSelectionForeground } from '../../../../platform/theme/common/colors/listColors.js'; import { asCssVariable } from '../../../../platform/theme/common/colorUtils.js'; -import { IThemeService } from '../../../../platform/theme/common/themeService.js'; -import { fillEditorsDragData } from '../../../browser/dnd.js'; -import { contentRefUrl } from '../common/annotations.js'; +import { ContentRefData, contentRefUrl } from '../common/annotations.js'; import { getFullyQualifiedId, IChatAgentCommand, IChatAgentData, IChatAgentNameService, IChatAgentService } from '../common/chatAgents.js'; import { chatSlashCommandBackground, chatSlashCommandForeground } from '../common/chatColors.js'; import { chatAgentLeader, ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynamicVariablePart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestToolPart, ChatRequestVariablePart, chatSubcommandLeader, IParsedChatRequest, IParsedChatRequestPart } from '../common/chatParserTypes.js'; @@ -36,7 +27,8 @@ import { IChatVariablesService } from '../common/chatVariables.js'; import { ILanguageModelToolsService } from '../common/languageModelToolsService.js'; import { IChatWidgetService } from './chat.js'; import { ChatAgentHover, getChatAgentHoverOptions } from './chatAgentHover.js'; -import './media/chatInlineFileLinkWidget.css'; +import { InlineAnchorWidget } from './chatInlineAnchorWidget.js'; +import './media/chatInlineAnchorWidget.css'; /** For rendering slash commands, variables */ const decorationRefUrl = `http://_vscodedecoration_`; @@ -241,20 +233,20 @@ export class ChatMarkdownDecorationsRenderer extends Disposable { private renderFileWidget(href: string, a: HTMLAnchorElement, store: DisposableStore): void { // TODO this can be a nicer FileLabel widget with an icon. Do a simple link for now. const fullUri = URI.parse(href); - let location: Location | { uri: URI; range: undefined }; + let data: ContentRefData; try { - location = revive(JSON.parse(fullUri.fragment)); + data = revive(JSON.parse(fullUri.fragment)); } catch (err) { this.logService.error('Invalid chat widget render data JSON', toErrorMessage(err)); return; } - if (!location.uri || !URI.isUri(location.uri)) { + if (data.kind !== 'symbol' && !URI.isUri(data.uri)) { this.logService.error(`Invalid chat widget render data: ${fullUri.fragment}`); return; } - store.add(this.instantiationService.createInstance(InlineFileLinkWidget, a, location)); + store.add(this.instantiationService.createInstance(InlineAnchorWidget, a, data)); } private renderResourceWidget(name: string, args: IDecorationWidgetArgs | undefined, store: DisposableStore): HTMLElement { @@ -282,49 +274,3 @@ export class ChatMarkdownDecorationsRenderer extends Disposable { } } } - - -class InlineFileLinkWidget extends Disposable { - - constructor( - element: HTMLAnchorElement, - location: Location | { uri: URI; range: undefined }, - @IHoverService hoverService: IHoverService, - @IInstantiationService instantiationService: IInstantiationService, - @ILabelService labelService: ILabelService, - @ILanguageService languageService: ILanguageService, - @IModelService modelService: IModelService, - @IThemeService themeService: IThemeService, - ) { - super(); - - element.classList.add('chat-inline-file-link-widget'); - - const fragment = location.range ? `${location.range.startLineNumber}-${location.range.endLineNumber}` : ''; - element.setAttribute('data-href', location.uri.with({ fragment }).toString()); - - const label = labelService.getUriLabel(location.uri, { relative: true }); - const title = location.range ? - `${label}#${location.range.startLineNumber}-${location.range.endLineNumber}` : - label; - - element.replaceChildren(); - - const resourceLabel = this._register(new IconLabel(element, { supportHighlights: false, supportIcons: true })); - resourceLabel.setLabel(label, undefined, { - extraClasses: getIconClasses(modelService, languageService, location.uri) - }); - - // Hover - this._register(hoverService.setupManagedHover(getDefaultHoverDelegate('element'), element, title)); - - // Drag and drop - element.draggable = true; - this._register(dom.addDisposableListener(element, 'dragstart', e => { - instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, [location.uri], e)); - - const theme = themeService.getColorTheme(); - applyDragImage(e, label, 'monaco-drag-image', theme.getColor(listActiveSelectionBackground)?.toString(), theme.getColor(listActiveSelectionForeground)?.toString()); - })); - } -} diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 651f68e6f0f..0368011c088 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -762,8 +762,8 @@ export class ChatWidget extends Disposable implements IChatWidget { this.inputPart.logInputHistory(); } - async acceptInput(query?: string): Promise { - return this._acceptInput(query ? { query } : undefined); + async acceptInput(query?: string, isVoiceInput?: boolean): Promise { + return this._acceptInput(query ? { query } : undefined, isVoiceInput); } async acceptInputWithPrefix(prefix: string): Promise { @@ -780,7 +780,7 @@ export class ChatWidget extends Disposable implements IChatWidget { return inputState; } - private async _acceptInput(opts: { query: string } | { prefix: string } | undefined): Promise { + private async _acceptInput(opts: { query: string } | { prefix: string } | undefined, isVoiceInput?: boolean): Promise { if (this.viewModel) { this._onDidAcceptInput.fire(); @@ -803,7 +803,7 @@ export class ChatWidget extends Disposable implements IChatWidget { result.responseCompletePromise.then(() => { const responses = this.viewModel?.getItems().filter(isResponseVM); const lastResponse = responses?.[responses.length - 1]; - this.chatAccessibilityService.acceptResponse(lastResponse, requestId); + this.chatAccessibilityService.acceptResponse(lastResponse, requestId, isVoiceInput); if (lastResponse?.result?.nextQuestion) { const { prompt, participant, command } = lastResponse.result.nextQuestion; const question = formatChatQuestion(this.chatAgentService, this.location, prompt, participant, command); diff --git a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts index b83e09cd7d4..9fdb65576e6 100644 --- a/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts +++ b/src/vs/workbench/contrib/chat/browser/codeBlockPart.ts @@ -79,6 +79,8 @@ export interface ICodeBlockData { readonly textModel: Promise; readonly languageId: string; + readonly codemapperUri?: URI; + readonly vulns?: readonly IMarkdownVulnerability[]; readonly range?: Range; @@ -126,6 +128,7 @@ export function parseLocalFileData(text: string) { export interface ICodeBlockActionContext { code: string; + codemapperUri?: URI; languageId?: string; codeBlockIndex: number; element: unknown; @@ -424,7 +427,8 @@ export class CodeBlockPart extends Disposable { code: textModel.getTextBuffer().getValueInRange(data.range ?? textModel.getFullModelRange(), EndOfLinePreference.TextDefined), codeBlockIndex: data.codeBlockIndex, element: data.element, - languageId: textModel.getLanguageId() + languageId: textModel.getLanguageId(), + codemapperUri: data.codemapperUri, } satisfies ICodeBlockActionContext; this.resourceContextKey.set(textModel.uri); } diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index 96dbb4278d1..95cd5ba9623 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -8,25 +8,35 @@ import { Disposable } from '../../../../../base/common/lifecycle.js'; import { Position } from '../../../../../editor/common/core/position.js'; import { Range } from '../../../../../editor/common/core/range.js'; import { IWordAtPosition, getWordAtText } from '../../../../../editor/common/core/wordHelper.js'; -import { CompletionContext, CompletionItem, CompletionItemKind } from '../../../../../editor/common/languages.js'; +import { CompletionContext, CompletionItem, CompletionItemKind, CompletionList } from '../../../../../editor/common/languages.js'; import { ITextModel } from '../../../../../editor/common/model.js'; import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js'; import { localize } from '../../../../../nls.js'; import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; +import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; import { Registry } from '../../../../../platform/registry/common/platform.js'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from '../../../../common/contributions.js'; import { SubmitAction } from '../actions/chatExecuteActions.js'; import { IChatWidget, IChatWidgetService } from '../chat.js'; import { ChatInputPart } from '../chatInputPart.js'; -import { SelectAndInsertFileAction } from './chatDynamicVariables.js'; +import { ChatDynamicVariableModel, SelectAndInsertFileAction } from './chatDynamicVariables.js'; import { ChatAgentLocation, getFullyQualifiedId, IChatAgentData, IChatAgentNameService, IChatAgentService } from '../../common/chatAgents.js'; import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestTextPart, ChatRequestToolPart, ChatRequestVariablePart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from '../../common/chatParserTypes.js'; import { IChatSlashCommandService } from '../../common/chatSlashCommands.js'; -import { IChatVariablesService } from '../../common/chatVariables.js'; +import { IChatVariablesService, IDynamicVariable } from '../../common/chatVariables.js'; import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js'; import { LifecyclePhase } from '../../../../services/lifecycle/common/lifecycle.js'; +import { IHistoryService } from '../../../../services/history/common/history.js'; +import { ILabelService } from '../../../../../platform/label/common/label.js'; +import { ResourceSet } from '../../../../../base/common/map.js'; +import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js'; +import { isPatternInWord } from '../../../../../base/common/filters.js'; +import { ISearchService } from '../../../../services/search/common/search.js'; +import { QueryBuilder } from '../../../../services/search/common/queryBuilder.js'; +import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { generateUuid } from '../../../../../base/common/uuid.js'; class SlashCommandCompletions extends Disposable { constructor( @@ -301,19 +311,35 @@ class AssignSelectedAgentAction extends Action2 { } registerAction2(AssignSelectedAgentAction); + +class ReferenceArgument { + constructor( + readonly widget: IChatWidget, + readonly variable: IDynamicVariable + ) { } +} + class BuiltinDynamicCompletions extends Disposable { + private static readonly addReferenceCommand = '_addReferenceCmd'; private static readonly VariableNameDef = new RegExp(`${chatVariableLeader}\\w*`, 'g'); // MUST be using `g`-flag + private readonly queryBuilder: QueryBuilder; + constructor( + @IHistoryService private readonly historyService: IHistoryService, + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @ISearchService private readonly searchService: ISearchService, + @ILabelService private readonly labelService: ILabelService, @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, @IChatWidgetService private readonly chatWidgetService: IChatWidgetService, + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { _debugDisplayName: 'chatDynamicCompletions', triggerCharacters: [chatVariableLeader], - provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { + provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken) => { const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); if (!widget || !widget.supportsFileReferences) { return null; @@ -324,22 +350,131 @@ class BuiltinDynamicCompletions extends Disposable { return null; } + const result: CompletionList = { suggestions: [] }; + const afterRange = new Range(position.lineNumber, range.replace.startColumn, position.lineNumber, range.replace.startColumn + '#file:'.length); - return { - suggestions: [ - { - label: `${chatVariableLeader}file`, - insertText: `${chatVariableLeader}file:`, - detail: localize('pickFileLabel', "Pick a file"), - range, - kind: CompletionItemKind.Text, - command: { id: SelectAndInsertFileAction.ID, title: SelectAndInsertFileAction.ID, arguments: [{ widget, range: afterRange }] }, - sortText: 'z' - } satisfies CompletionItem - ] - }; + result.suggestions.push({ + label: `${chatVariableLeader}file`, + insertText: `${chatVariableLeader}file:`, + detail: localize('pickFileLabel', "Pick a file"), + range, + kind: CompletionItemKind.Text, + command: { id: SelectAndInsertFileAction.ID, title: SelectAndInsertFileAction.ID, arguments: [{ widget, range: afterRange }] }, + sortText: 'z' + }); + + + await this.addFileEntries(widget, result, range, token); + + return result; } })); + + this._register(CommandsRegistry.registerCommand(BuiltinDynamicCompletions.addReferenceCommand, (_services, arg) => this.cmdAddReference(arg))); + + this.queryBuilder = this.instantiationService.createInstance(QueryBuilder); + } + + private cacheKey?: { key: string; time: number }; + + private async addFileEntries(widget: IChatWidget, result: CompletionList, info: { insert: Range; replace: Range; varWord: IWordAtPosition | null }, token: CancellationToken) { + + const makeFileCompletionItem = (resource: URI): CompletionItem => { + + const basename = this.labelService.getUriBasenameLabel(resource); + const insertText = `${chatVariableLeader}file:${basename} `; + + return { + label: { label: basename, description: this.labelService.getUriLabel(resource, { relative: true }) }, + filterText: `${chatVariableLeader}${basename}`, + insertText, + range: info, + kind: CompletionItemKind.File, + sortText: '{', // after `z` + command: { + id: BuiltinDynamicCompletions.addReferenceCommand, title: '', arguments: [new ReferenceArgument(widget, { + id: 'vscode.file', + range: { startLineNumber: info.replace.startLineNumber, startColumn: info.replace.startColumn, endLineNumber: info.replace.endLineNumber, endColumn: info.replace.startColumn + insertText.length }, + data: resource + })] + } + }; + }; + + let pattern: string | undefined; + if (info.varWord?.word && info.varWord.word.startsWith(chatVariableLeader)) { + pattern = info.varWord.word.toLowerCase().slice(1); // remove leading # + } + + const seen = new ResourceSet(); + const len = result.suggestions.length; + + // HISTORY + // always take the last N items + for (const item of this.historyService.getHistory()) { + if (!item.resource || !this.workspaceContextService.getWorkspaceFolder(item.resource)) { + // ignore "forgein" editors + continue; + } + + if (pattern) { + // use pattern if available + const basename = this.labelService.getUriBasenameLabel(item.resource).toLowerCase(); + if (!isPatternInWord(pattern, 0, pattern.length, basename, 0, basename.length)) { + continue; + } + } + + seen.add(item.resource); + const newLen = result.suggestions.push(makeFileCompletionItem(item.resource)); + if (newLen - len >= 5) { + break; + } + } + + // SEARCH + // use file search when having a pattern + if (pattern) { + + if (this.cacheKey && Date.now() - this.cacheKey.time > 60000) { + this.searchService.clearCache(this.cacheKey.key); + this.cacheKey = undefined; + } + + if (!this.cacheKey) { + this.cacheKey = { + key: generateUuid(), + time: Date.now() + }; + } + + this.cacheKey.time = Date.now(); + + const query = this.queryBuilder.file(this.workspaceContextService.getWorkspace().folders, { + filePattern: pattern, + sortByScore: true, + maxResults: 250, + cacheKey: this.cacheKey.key + }); + + const data = await this.searchService.fileSearch(query, token); + for (const match of data.results) { + if (seen.has(match.resource)) { + // already included via history + continue; + } + result.suggestions.push(makeFileCompletionItem(match.resource)); + } + } + + // mark results as incomplete because further typing might yield + // in more search results + result.incomplete = true; + } + + private cmdAddReference(arg: ReferenceArgument) { + // invoked via the completion command + arg.widget.getContrib(ChatDynamicVariableModel.ID)?.addReference(arg.variable); } } diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 9a3c450cab8..6b782d5416c 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -427,7 +427,6 @@ have to be updated for changes to the rules above, or to support more deeply nes } .interactive-session .interactive-input-and-execute-toolbar { - display: flex; box-sizing: border-box; cursor: text; background-color: var(--vscode-input-background); @@ -436,11 +435,12 @@ have to be updated for changes to the rules above, or to support more deeply nes position: relative; padding: 0 6px; margin-bottom: 4px; - align-items: flex-end; - justify-content: space-between; } .interactive-session .interactive-input-part.compact .interactive-input-and-execute-toolbar { + display: flex; + align-items: flex-end; + justify-content: space-between; margin-bottom: 0; border-radius: 2px; } @@ -475,6 +475,10 @@ have to be updated for changes to the rules above, or to support more deeply nes margin-bottom: 7px; } +.interactive-session .interactive-input-part .interactive-execute-toolbar .monaco-action-bar { + float: right; +} + .interactive-session .interactive-input-part .interactive-execute-toolbar .monaco-action-bar .actions-container { display: flex; gap: 4px; diff --git a/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css b/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css index 29e38f48cad..75a0cfa7f39 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatAgentHover.css @@ -44,6 +44,8 @@ .chat-agent-hover.verifiedPublisher .extension-verified-publisher { display: flex; + align-items: start; + margin-top: 1px; } .chat-agent-hover .chat-agent-hover-warning .codicon { diff --git a/src/vs/workbench/contrib/chat/browser/media/chatInlineAnchorWidget.css b/src/vs/workbench/contrib/chat/browser/media/chatInlineAnchorWidget.css new file mode 100644 index 00000000000..22bc19f8bd0 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/media/chatInlineAnchorWidget.css @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.chat-inline-anchor-widget, +.chat-inline-anchor-widget a { + color: inherit !important; +} + +.chat-inline-anchor-widget { + border: 1px solid var(--vscode-chat-requestBorder, var(--vscode-input-background, transparent)); + border-radius: 4px; +} + +.chat-inline-anchor-widget:hover { + background-color: var(--vscode-list-hoverBackground); +} + +.chat-inline-anchor-widget .monaco-icon-label { + display: inline-flex; + margin: 0 1px; + vertical-align: text-bottom; + align-items: center; + line-height: normal; +} + +.chat-inline-anchor-widget .monaco-highlighted-label { + padding: 2px; + padding-right: 4px; +} + +.chat-inline-anchor-widget .codicon { + vertical-align: text-bottom; + color: reset-layer !important; +} + +.chat-inline-anchor-widget .monaco-icon-label::before { + height: auto; + padding-right: 3px; +} + +.chat-inline-anchor-widget .monaco-icon-label .monaco-highlighted-label { + white-space: normal; +} diff --git a/src/vs/workbench/contrib/chat/common/annotations.ts b/src/vs/workbench/contrib/chat/common/annotations.ts index 8aede0dc32b..bd95ecb599e 100644 --- a/src/vs/workbench/contrib/chat/common/annotations.ts +++ b/src/vs/workbench/contrib/chat/common/annotations.ts @@ -6,19 +6,42 @@ import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { basename } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; import { IRange } from '../../../../editor/common/core/range.js'; +import { IWorkspaceSymbol } from '../../search/common/search.js'; import { IChatProgressRenderableResponseContent, IChatProgressResponseContent, appendMarkdownString, canMergeMarkdownStrings } from './chatModel.js'; import { IChatAgentVulnerabilityDetails, IChatMarkdownContent } from './chatService.js'; export const contentRefUrl = 'http://_vscodecontentref_'; // must be lowercase for URI +export type ContentRefData = + | { readonly kind: 'symbol'; readonly symbol: IWorkspaceSymbol } + | { + readonly kind?: undefined; + readonly uri: URI; + readonly range?: IRange; + }; + export function annotateSpecialMarkdownContent(response: ReadonlyArray): IChatProgressRenderableResponseContent[] { const result: IChatProgressRenderableResponseContent[] = []; for (const item of response) { const previousItem = result[result.length - 1]; if (item.kind === 'inlineReference') { - const location = 'uri' in item.inlineReference ? item.inlineReference : { uri: item.inlineReference }; + const location: ContentRefData = 'uri' in item.inlineReference + ? item.inlineReference + : 'name' in item.inlineReference + ? { kind: 'symbol', symbol: item.inlineReference } + : { uri: item.inlineReference }; + const printUri = URI.parse(contentRefUrl).with({ fragment: JSON.stringify(location) }); - const markdownText = `[${item.name || basename(location.uri)}](${printUri.toString()})`; + let label: string | undefined = item.name; + if (!label) { + if (location.kind === 'symbol') { + label = location.symbol.name; + } else { + label = basename(location.uri); + } + } + + const markdownText = `[${label}](${printUri.toString()})`; if (previousItem?.kind === 'markdownContent') { const merged = appendMarkdownString(previousItem.content, new MarkdownString(markdownText)); result[result.length - 1] = { content: merged, kind: 'markdownContent' }; @@ -38,6 +61,12 @@ export function annotateSpecialMarkdownContent(response: ReadonlyArray${item.uri.toString()}`; + const merged = appendMarkdownString(previousItem.content, new MarkdownString(markdownText)); + result[result.length - 1] = { content: merged, kind: 'markdownContent' }; + } } else { result.push(item); } @@ -76,6 +105,16 @@ export function annotateVulnerabilitiesInText(response: ReadonlyArray(.*?)<\/vscode_codeblock_uri>/ms.exec(text); + if (match && match[1]) { + const result = URI.parse(match[1]); + const textWithoutResult = text.substring(0, match.index) + text.substring(match.index + match[0].length); + return { uri: result, textWithoutResult }; + } + return undefined; +} + export function extractVulnerabilitiesFromText(text: string): { newText: string; vulnerabilities: IMarkdownVulnerability[] } { const vulnerabilities: IMarkdownVulnerability[] = []; let newText = text; diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index 6d882fdfca3..5a5b1a36679 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -11,8 +11,7 @@ import { IMarkdownString } from '../../../../base/common/htmlContent.js'; import { Iterable } from '../../../../base/common/iterator.js'; import { IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { revive } from '../../../../base/common/marshalling.js'; -import { IObservable } from '../../../../base/common/observable.js'; -import { observableValue } from '../../../../base/common/observableInternal/base.js'; +import { IObservable, observableValue } from '../../../../base/common/observable.js'; import { equalsIgnoreCase } from '../../../../base/common/strings.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { URI } from '../../../../base/common/uri.js'; diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 3952609480e..515156496d7 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -22,7 +22,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { ILogService } from '../../../../platform/log/common/log.js'; import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentResult, IChatAgentService, reviveSerializedAgent } from './chatAgents.js'; import { ChatRequestTextPart, IParsedChatRequest, reviveParsedChatRequest } from './chatParserTypes.js'; -import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatLocationData, IChatMarkdownContent, IChatProgress, IChatProgressMessage, IChatResponseProgressFileTreeData, IChatTask, IChatTextEdit, IChatTreeData, IChatUsedContext, IChatWarningMessage, isIUsedContext } from './chatService.js'; +import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatLocationData, IChatMarkdownContent, IChatProgress, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatResponseProgressFileTreeData, IChatTask, IChatTextEdit, IChatTreeData, IChatUsedContext, IChatWarningMessage, isIUsedContext } from './chatService.js'; import { IChatRequestVariableValue } from './chatVariables.js'; export interface IChatRequestVariableEntry { @@ -74,6 +74,7 @@ export interface IChatTextEditGroup { export type IChatProgressResponseContent = | IChatMarkdownContent | IChatAgentMarkdownContentWithVulnerability + | IChatResponseCodeblockUriPart | IChatTreeData | IChatContentInlineReference | IChatProgressMessage @@ -83,7 +84,7 @@ export type IChatProgressResponseContent = | IChatTextEditGroup | IChatConfirmation; -export type IChatProgressRenderableResponseContent = Exclude; +export type IChatProgressRenderableResponseContent = Exclude; export interface IResponse { readonly value: ReadonlyArray; @@ -203,7 +204,7 @@ export class Response extends Disposable implements IResponse { return this._responseParts; } - constructor(value: IMarkdownString | ReadonlyArray) { + constructor(value: IMarkdownString | ReadonlyArray) { super(); this._responseParts = asArray(value).map((v) => (isMarkdownString(v) ? { content: v, kind: 'markdownContent' } satisfies IChatMarkdownContent : @@ -289,16 +290,19 @@ export class Response extends Disposable implements IResponse { } private _updateRepr(quiet?: boolean) { + const inlineRefToRepr = (part: IChatContentInlineReference) => + 'uri' in part.inlineReference ? basename(part.inlineReference.uri) : 'name' in part.inlineReference ? part.inlineReference.name : basename(part.inlineReference); + this._responseRepr = this._responseParts.map(part => { if (part.kind === 'treeData') { return ''; } else if (part.kind === 'inlineReference') { - return basename('uri' in part.inlineReference ? part.inlineReference.uri : part.inlineReference); + return inlineRefToRepr(part); } else if (part.kind === 'command') { return part.command.title; } else if (part.kind === 'textEditGroup') { return localize('editsSummary', "Made changes."); - } else if (part.kind === 'progressMessage') { + } else if (part.kind === 'progressMessage' || part.kind === 'codeblockUri') { return ''; } else if (part.kind === 'confirmation') { return `${part.title}\n${part.message}`; @@ -313,7 +317,7 @@ export class Response extends Disposable implements IResponse { this._markdownContent = this._responseParts.map(part => { if (part.kind === 'inlineReference') { - return basename('uri' in part.inlineReference ? part.inlineReference.uri : part.inlineReference); + return inlineRefToRepr(part); } else if (part.kind === 'markdownContent' || part.kind === 'markdownVuln') { return part.content.value; } else { @@ -419,7 +423,7 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel } constructor( - _response: IMarkdownString | ReadonlyArray, + _response: IMarkdownString | ReadonlyArray, private _session: ChatModel, private _agent: IChatAgentData | undefined, private _slashCommand: IChatAgentCommand | undefined, @@ -806,28 +810,24 @@ export class ChatModel extends Disposable implements IChatModel { } get requesterUsername(): string { - return (this._defaultAgent ? - this._defaultAgent.metadata.requester?.name : - this.initialData?.requesterUsername) ?? ''; + return this._defaultAgent?.metadata.requester?.name ?? + this.initialData?.requesterUsername ?? ''; } get responderUsername(): string { - return (this._defaultAgent ? - this._defaultAgent.fullName : - this.initialData?.responderUsername) ?? ''; + return this._defaultAgent?.fullName ?? + this.initialData?.responderUsername ?? ''; } private readonly _initialRequesterAvatarIconUri: URI | undefined; get requesterAvatarIconUri(): URI | undefined { - return this._defaultAgent ? - this._defaultAgent.metadata.requester?.icon : + return this._defaultAgent?.metadata.requester?.icon ?? this._initialRequesterAvatarIconUri; } private readonly _initialResponderAvatarIconUri: ThemeIcon | URI | undefined; get responderAvatarIcon(): ThemeIcon | URI | undefined { - return this._defaultAgent ? - this._defaultAgent?.metadata.themeIcon : + return this._defaultAgent?.metadata.themeIcon ?? this._initialResponderAvatarIconUri; } @@ -1048,6 +1048,7 @@ export class ChatModel extends Disposable implements IChatModel { if (progress.kind === 'markdownContent' || progress.kind === 'treeData' || progress.kind === 'inlineReference' || + progress.kind === 'codeblockUri' || progress.kind === 'markdownVuln' || progress.kind === 'progressMessage' || progress.kind === 'command' || diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index abb04d4a0bf..25804510aca 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -14,6 +14,7 @@ import { ISelection } from '../../../../editor/common/core/selection.js'; import { Command, Location, TextEdit } from '../../../../editor/common/languages.js'; import { FileType } from '../../../../platform/files/common/files.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { IWorkspaceSymbol } from '../../search/common/search.js'; import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentResult } from './chatAgents.js'; import { ChatModel, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatRequestVariableEntry, IChatResponseModel, IExportableChatData, ISerializableChatData } from './chatModel.js'; import { IParsedChatRequest } from './chatParserTypes.js'; @@ -96,7 +97,7 @@ export interface IChatCodeCitation { } export interface IChatContentInlineReference { - inlineReference: URI | Location; + inlineReference: URI | Location | IWorkspaceSymbol; name?: string; kind: 'inlineReference'; } @@ -153,6 +154,11 @@ export interface IChatAgentVulnerabilityDetails { description: string; } +export interface IChatResponseCodeblockUriPart { + kind: 'codeblockUri'; + uri: URI; +} + export interface IChatAgentMarkdownContentWithVulnerability { content: IMarkdownString; vulnerabilities: IChatAgentVulnerabilityDetails[]; @@ -201,6 +207,7 @@ export type IChatProgress = | IChatWarningMessage | IChatTextEdit | IChatMoveMessage + | IChatResponseCodeblockUriPart | IChatConfirmation; export interface IChatFollowup { diff --git a/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts b/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts index 92a93ae015a..ad67a6ccc24 100644 --- a/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts +++ b/src/vs/workbench/contrib/chat/common/codeBlockModelCollection.ts @@ -11,8 +11,8 @@ import { Range } from '../../../../editor/common/core/range.js'; import { ILanguageService } from '../../../../editor/common/languages/language.js'; import { EndOfLinePreference } from '../../../../editor/common/model.js'; import { IResolvedTextEditorModel, ITextModelService } from '../../../../editor/common/services/resolverService.js'; +import { extractCodeblockUrisFromText, extractVulnerabilitiesFromText, IMarkdownVulnerability } from './annotations.js'; import { IChatRequestViewModel, IChatResponseViewModel, isResponseVM } from './chatViewModel.js'; -import { extractVulnerabilitiesFromText, IMarkdownVulnerability } from './annotations.js'; export class CodeBlockModelCollection extends Disposable { @@ -20,6 +20,7 @@ export class CodeBlockModelCollection extends Disposable { private readonly _models = new ResourceMap<{ readonly model: Promise>; vulns: readonly IMarkdownVulnerability[]; + codemapperUri?: URI; }>(); /** @@ -41,16 +42,16 @@ export class CodeBlockModelCollection extends Disposable { this.clear(); } - get(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number): { model: Promise; readonly vulns: readonly IMarkdownVulnerability[] } | undefined { + get(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number): { model: Promise; readonly vulns: readonly IMarkdownVulnerability[]; readonly codemapperUri?: URI } | undefined { const uri = this.getUri(sessionId, chat, codeBlockIndex); const entry = this._models.get(uri); if (!entry) { return; } - return { model: entry.model.then(ref => ref.object), vulns: entry.vulns }; + return { model: entry.model.then(ref => ref.object), vulns: entry.vulns, codemapperUri: entry.codemapperUri }; } - getOrCreate(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number): { model: Promise; readonly vulns: readonly IMarkdownVulnerability[] } { + getOrCreate(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number): { model: Promise; readonly vulns: readonly IMarkdownVulnerability[]; readonly codemapperUri?: URI } { const existing = this.get(sessionId, chat, codeBlockIndex); if (existing) { return existing; @@ -58,7 +59,7 @@ export class CodeBlockModelCollection extends Disposable { const uri = this.getUri(sessionId, chat, codeBlockIndex); const ref = this.textModelService.createModelReference(uri); - this._models.set(uri, { model: ref, vulns: [] }); + this._models.set(uri, { model: ref, vulns: [], codemapperUri: undefined }); while (this._models.size > this.maxModelCount) { const first = Array.from(this._models.keys()).at(0); @@ -68,7 +69,7 @@ export class CodeBlockModelCollection extends Disposable { this.delete(first); } - return { model: ref.then(ref => ref.object), vulns: [] }; + return { model: ref.then(ref => ref.object), vulns: [], codemapperUri: undefined }; } private delete(codeBlockUri: URI) { @@ -90,9 +91,15 @@ export class CodeBlockModelCollection extends Disposable { const entry = this.getOrCreate(sessionId, chat, codeBlockIndex); const extractedVulns = extractVulnerabilitiesFromText(content.text); - const newText = fixCodeText(extractedVulns.newText, content.languageId); + let newText = fixCodeText(extractedVulns.newText, content.languageId); this.setVulns(sessionId, chat, codeBlockIndex, extractedVulns.vulnerabilities); + const codeblockUri = extractCodeblockUrisFromText(newText); + if (codeblockUri) { + this.setCodemapperUri(sessionId, chat, codeBlockIndex, codeblockUri.uri); + newText = codeblockUri.textWithoutResult; + } + const textModel = (await entry.model).textEditorModel; if (content.languageId) { const vscodeLanguageId = this.languageService.getLanguageIdByLanguageName(content.languageId); @@ -103,7 +110,7 @@ export class CodeBlockModelCollection extends Disposable { const currentText = textModel.getValue(EndOfLinePreference.LF); if (newText === currentText) { - return; + return entry; } if (newText.startsWith(currentText)) { @@ -115,6 +122,16 @@ export class CodeBlockModelCollection extends Disposable { // console.log(`Failed to optimize setText`); textModel.setValue(newText); } + + return entry; + } + + private setCodemapperUri(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number, codemapperUri: URI) { + const uri = this.getUri(sessionId, chat, codeBlockIndex); + const entry = this._models.get(uri); + if (entry) { + entry.codemapperUri = codemapperUri; + } } private setVulns(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number, vulnerabilities: IMarkdownVulnerability[]) { diff --git a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts index dee8c8a8c9a..a71895b523d 100644 --- a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts @@ -37,6 +37,11 @@ export interface IToolInvocation { toolId: string; parameters: any; tokenBudget?: number; + context: IToolInvocationContext | undefined; +} + +export interface IToolInvocationContext { + sessionId: string; } export interface IToolResult { diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts index 0e906a7ded7..7efde1b127d 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -209,7 +209,7 @@ class VoiceChatSessionControllerFactory { onDidAcceptInput: chatWidget.onDidAcceptInput, onDidHideInput: chatWidget.onDidHide, focusInput: () => chatWidget.focusInput(), - acceptInput: () => chatWidget.acceptInput(), + acceptInput: () => chatWidget.acceptInput(undefined, true), updateInput: text => chatWidget.setInput(text), getInput: () => chatWidget.getInput(), setInputPlaceholder: text => chatWidget.setInputPlaceholder(text), @@ -226,7 +226,7 @@ class VoiceChatSessionControllerFactory { onDidAcceptInput: terminalChat.onDidAcceptInput, onDidHideInput: terminalChat.onDidHide, focusInput: () => terminalChat.focus(), - acceptInput: () => terminalChat.acceptInput(), + acceptInput: () => terminalChat.acceptInput(true), updateInput: text => terminalChat.updateInput(text, false), getInput: () => terminalChat.getInput(), setInputPlaceholder: text => terminalChat.setPlaceholder(text), diff --git a/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts b/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts index 22ecd7ebe4d..1159effdb95 100644 --- a/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/languageModelToolsService.test.ts @@ -106,7 +106,8 @@ suite('LanguageModelToolsService', () => { tokenBudget: 100, parameters: { a: 1 - } + }, + context: { sessionId: 'a' } }; const result = await service.invokeTool(dto, async () => 0, CancellationToken.None); diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index 0ea52c03bf5..84e2e387458 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -176,7 +176,7 @@ export class CommentThreadWidget extends } if (keybinding) { ariaLabel = localize('commentLabelWithKeybinding', "{0}, use ({1}) for accessibility help", ariaLabel, keybinding); - } else { + } else if (verbose) { ariaLabel = localize('commentLabelWithKeybindingNoKeybinding', "{0}, run the command Open Accessibility Help which is currently not triggerable via keybinding.", ariaLabel); } this._body.container.ariaLabel = ariaLabel; diff --git a/src/vs/workbench/contrib/comments/browser/commentsView.ts b/src/vs/workbench/contrib/comments/browser/commentsView.ts index 5db9aebcd4a..f0a12bb0f02 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsView.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsView.ts @@ -37,6 +37,7 @@ import { AccessibilityVerbositySettingId } from '../../accessibility/browser/acc import { AccessibleViewAction } from '../../accessibility/browser/accessibleViewActions.js'; import type { ITreeElement } from '../../../../base/browser/ui/tree/tree.js'; import { IPathService } from '../../../services/path/common/pathService.js'; +import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; export const CONTEXT_KEY_HAS_COMMENTS = new RawContextKey('commentsView.hasComments', false); export const CONTEXT_KEY_SOME_COMMENTS_EXPANDED = new RawContextKey('commentsView.someCommentsExpanded', false); @@ -149,7 +150,7 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { @IHoverService hoverService: IHoverService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @IStorageService storageService: IStorageService, - @IPathService private readonly pathService: IPathService + @IPathService private readonly pathService: IPathService, ) { const stateMemento = new Memento(VIEW_STORAGE_ID, storageService); const viewState = stateMemento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE); @@ -343,32 +344,43 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { } const replyCount = this.getReplyCountAsString(element, forAriaLabel); const replies = this.getRepliesAsString(element, forAriaLabel); + const editor = this.editorService.findEditors(element.resource); + const codeEditor = this.editorService.activeEditorPane?.getControl(); + let content; + if (element.range && editor?.length && isCodeEditor(codeEditor)) { + content = codeEditor.getModel()?.getValueInRange(element.range); + if (content) { + content = '\nCorresponding code: \n' + content; + } + } if (element.range) { if (element.threadRelevance === CommentThreadApplicability.Outdated) { return accessibleViewHint + nls.localize('resourceWithCommentLabelOutdated', - "Outdated from {0} at line {1} column {2} in {3},{4} comment: {5}", - element.comment.userName, - element.range.startLineNumber, - element.range.startColumn, - basename(element.resource), - replyCount, - (typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value - ) + replies; - } else { - return accessibleViewHint + nls.localize('resourceWithCommentLabel', - "{0} at line {1} column {2} in {3},{4} comment: {5}", + "Outdated from {0} at line {1} column {2} in {3}{4}\nComment: {5}{6}", element.comment.userName, element.range.startLineNumber, element.range.startColumn, basename(element.resource), replyCount, (typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value, + content, + ) + replies; + } else { + return accessibleViewHint + nls.localize('resourceWithCommentLabel', + "{0} at line {1} column {2} in {3} {4}\nComment: {5}{6}", + element.comment.userName, + element.range.startLineNumber, + element.range.startColumn, + basename(element.resource), + replyCount, + (typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value, + content, ) + replies; } } else { if (element.threadRelevance === CommentThreadApplicability.Outdated) { return accessibleViewHint + nls.localize('resourceWithCommentLabelFileOutdated', - "Outdated from {0} in {1},{2} comment: {3}", + "Outdated from {0} in {1} {2}\nComment: {3}{4}{5}", element.comment.userName, basename(element.resource), replyCount, @@ -376,11 +388,12 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { ) + replies; } else { return accessibleViewHint + nls.localize('resourceWithCommentLabelFile', - "{0} in {1},{2} comment: {3}", + "{0} in {1} {2}\nComment: {3}{4}", element.comment.userName, basename(element.resource), replyCount, - (typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value + (typeof element.comment.body === 'string') ? element.comment.body : element.comment.body.value, + content ) + replies; } } diff --git a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts index db81687f055..9576b7e5bb7 100644 --- a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts +++ b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts @@ -17,20 +17,15 @@ import { KeyCode } from '../../../../base/common/keyCodes.js'; import { DisposableStore, IDisposable, dispose, toDisposable } from '../../../../base/common/lifecycle.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { localize } from '../../../../nls.js'; -import { CommandsRegistry, ICommandService } from '../../../../platform/commands/common/commands.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IContextViewService } from '../../../../platform/contextview/browser/contextView.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { defaultInputBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; -import { COPY_EVALUATE_PATH_ID, COPY_VALUE_ID } from './debugCommands.js'; -import { DebugLinkHoverBehavior, DebugLinkHoverBehaviorTypeData, LinkDetector } from './linkDetector.js'; -import { IDebugService, IExpression, IExpressionValue } from '../common/debug.js'; -import { Expression, ExpressionContainer, Variable } from '../common/debugModel.js'; +import { IDebugService, IExpression } from '../common/debug.js'; +import { Variable } from '../common/debugModel.js'; import { IDebugVisualizerService } from '../common/debugVisualizers.js'; -import { ReplEvaluationResult } from '../common/replModel.js'; +import { LinkDetector } from './linkDetector.js'; -const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024; -const booleanRegex = /^(true|false)$/i; -const stringRegex = /^(['"]).*\1$/; const $ = dom.$; export interface IRenderValueOptions { @@ -61,117 +56,6 @@ export function renderViewTree(container: HTMLElement): HTMLElement { return treeContainer; } -export function renderExpressionValue(store: DisposableStore, expressionOrValue: IExpressionValue | string, container: HTMLElement, options: IRenderValueOptions, hoverService: IHoverService): void { - let value = typeof expressionOrValue === 'string' ? expressionOrValue : expressionOrValue.value; - - // remove stale classes - container.className = 'value'; - // when resolving expressions we represent errors from the server as a variable with name === null. - if (value === null || ((expressionOrValue instanceof Expression || expressionOrValue instanceof Variable || expressionOrValue instanceof ReplEvaluationResult) && !expressionOrValue.available)) { - container.classList.add('unavailable'); - if (value !== Expression.DEFAULT_VALUE) { - container.classList.add('error'); - } - } else { - if (typeof expressionOrValue !== 'string' && options.showChanged && expressionOrValue.valueChanged && value !== Expression.DEFAULT_VALUE) { - // value changed color has priority over other colors. - container.className = 'value changed'; - expressionOrValue.valueChanged = false; - } - - if (options.colorize && typeof expressionOrValue !== 'string') { - if (expressionOrValue.type === 'number' || expressionOrValue.type === 'boolean' || expressionOrValue.type === 'string') { - container.classList.add(expressionOrValue.type); - } else if (!isNaN(+value)) { - container.classList.add('number'); - } else if (booleanRegex.test(value)) { - container.classList.add('boolean'); - } else if (stringRegex.test(value)) { - container.classList.add('string'); - } - } - } - - if (options.maxValueLength && value && value.length > options.maxValueLength) { - value = value.substring(0, options.maxValueLength) + '...'; - } - if (!value) { - value = ''; - } - - const session = (expressionOrValue instanceof ExpressionContainer) ? expressionOrValue.getSession() : undefined; - // Only use hovers for links if thre's not going to be a hover for the value. - const hoverBehavior: DebugLinkHoverBehaviorTypeData = options.hover === false ? { type: DebugLinkHoverBehavior.Rich, store } : { type: DebugLinkHoverBehavior.None }; - if (expressionOrValue instanceof ExpressionContainer && expressionOrValue.valueLocationReference !== undefined && session && options.linkDetector) { - container.textContent = ''; - container.appendChild(options.linkDetector.linkifyLocation(value, expressionOrValue.valueLocationReference, session, hoverBehavior)); - } else if (options.linkDetector) { - container.textContent = ''; - container.appendChild(options.linkDetector.linkify(value, false, session ? session.root : undefined, true, hoverBehavior)); - } else { - container.textContent = value; - } - - if (options.hover !== false) { - const { commands = [], commandService } = options.hover || {}; - store.add(hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), container, () => { - const container = dom.$('div'); - const markdownHoverElement = dom.$('div.hover-row'); - const hoverContentsElement = dom.append(markdownHoverElement, dom.$('div.hover-contents')); - const hoverContentsPre = dom.append(hoverContentsElement, dom.$('pre.debug-var-hover-pre')); - hoverContentsPre.textContent = value; - container.appendChild(markdownHoverElement); - return container; - }, { - actions: commands.map(({ id, args }) => { - const description = CommandsRegistry.getCommand(id)?.metadata?.description; - return { - label: typeof description === 'string' ? description : description ? description.value : id, - commandId: id, - run: () => commandService!.executeCommand(id, ...args), - }; - }) - })); - } -} - -export function renderVariable(store: DisposableStore, commandService: ICommandService, hoverService: IHoverService, variable: Variable, data: IVariableTemplateData, showChanged: boolean, highlights: IHighlight[], linkDetector?: LinkDetector, displayType?: boolean): void { - if (variable.available) { - data.type.textContent = ''; - let text = variable.name; - if (variable.value && typeof variable.name === 'string') { - if (variable.type && displayType) { - text += ': '; - data.type.textContent = variable.type + ' ='; - } else { - text += ' ='; - } - } - - data.label.set(text, highlights, variable.type && !displayType ? variable.type : variable.name); - data.name.classList.toggle('virtual', variable.presentationHint?.kind === 'virtual'); - data.name.classList.toggle('internal', variable.presentationHint?.visibility === 'internal'); - } else if (variable.value && typeof variable.name === 'string' && variable.name) { - data.label.set(':'); - } - - data.expression.classList.toggle('lazy', !!variable.presentationHint?.lazy); - const commands = [ - { id: COPY_VALUE_ID, args: [variable, [variable]] as unknown[] } - ]; - if (variable.evaluateName) { - commands.push({ id: COPY_EVALUATE_PATH_ID, args: [{ variable }] }); - } - - renderExpressionValue(store, variable, data.value, { - showChanged, - maxValueLength: MAX_VALUE_RENDER_LENGTH_IN_VIEWLET, - hover: { commands, commandService }, - colorize: true, - linkDetector - }, hoverService); -} - export interface IInputBoxOptions { initialValue: string; ariaLabel: string; diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index a5e9075c983..58dd961da31 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -20,7 +20,7 @@ import { RunOnceScheduler } from '../../../../base/common/async.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { KeyCode } from '../../../../base/common/keyCodes.js'; -import { DisposableStore, IDisposable, dispose } from '../../../../base/common/lifecycle.js'; +import { DisposableStore, dispose } from '../../../../base/common/lifecycle.js'; import * as resources from '../../../../base/common/resources.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { Constants } from '../../../../base/common/uint.js'; @@ -39,6 +39,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { ILabelService } from '../../../../platform/label/common/label.js'; import { WorkbenchList } from '../../../../platform/list/browser/listService.js'; +import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; @@ -48,22 +49,21 @@ import { ViewAction, ViewPane } from '../../../browser/parts/views/viewPane.js'; import { IViewletViewOptions } from '../../../browser/parts/views/viewsViewlet.js'; import { IEditorPane } from '../../../common/editor.js'; import { IViewDescriptorService } from '../../../common/views.js'; -import * as icons from './debugIcons.js'; -import { DisassemblyView } from './disassemblyView.js'; +import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js'; +import { IViewsService } from '../../../services/views/common/viewsService.js'; import { BREAKPOINTS_VIEW_ID, BREAKPOINT_EDITOR_CONTRIBUTION_ID, CONTEXT_BREAKPOINTS_EXIST, CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_BREAKPOINT_HAS_MODES, CONTEXT_BREAKPOINT_INPUT_FOCUSED, CONTEXT_BREAKPOINT_ITEM_IS_DATA_BYTES, CONTEXT_BREAKPOINT_ITEM_TYPE, CONTEXT_BREAKPOINT_SUPPORTS_CONDITION, CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_IN_DEBUG_MODE, CONTEXT_SET_DATA_BREAKPOINT_BYTES_SUPPORTED, DEBUG_SCHEME, DataBreakpointSetType, DataBreakpointSource, DebuggerString, IBaseBreakpoint, IBreakpoint, IBreakpointEditorContribution, IBreakpointUpdateData, IDataBreakpoint, IDataBreakpointInfoResponse, IDebugModel, IDebugService, IEnablement, IExceptionBreakpoint, IFunctionBreakpoint, IInstructionBreakpoint, State } from '../common/debug.js'; import { Breakpoint, DataBreakpoint, ExceptionBreakpoint, FunctionBreakpoint, InstructionBreakpoint } from '../common/debugModel.js'; import { DisassemblyViewInput } from '../common/disassemblyViewInput.js'; -import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js'; -import { INotificationService } from '../../../../platform/notification/common/notification.js'; -import { IViewsService } from '../../../services/views/common/viewsService.js'; +import * as icons from './debugIcons.js'; +import { DisassemblyView } from './disassemblyView.js'; const $ = dom.$; -function createCheckbox(disposables: IDisposable[]): HTMLInputElement { +function createCheckbox(disposables: DisposableStore): HTMLInputElement { const checkbox = $('input'); checkbox.type = 'checkbox'; checkbox.tabIndex = -1; - disposables.push(Gesture.ignoreTarget(checkbox)); + disposables.add(Gesture.ignoreTarget(checkbox)); return checkbox; } @@ -432,7 +432,8 @@ interface IBaseBreakpointTemplateData { checkbox: HTMLInputElement; context: BreakpointItem; actionBar: ActionBar; - toDispose: IDisposable[]; + templateDisposables: DisposableStore; + elementDisposables: DisposableStore; badge: HTMLElement; } @@ -466,7 +467,8 @@ interface IFunctionBreakpointInputTemplateData { checkbox: HTMLInputElement; icon: HTMLElement; breakpoint: IFunctionBreakpoint; - toDispose: IDisposable[]; + templateDisposables: DisposableStore; + elementDisposables: DisposableStore; type: 'hitCount' | 'condition' | 'name'; updating?: boolean; } @@ -476,7 +478,8 @@ interface IDataBreakpointInputTemplateData { checkbox: HTMLInputElement; icon: HTMLElement; breakpoint: IDataBreakpoint; - toDispose: IDisposable[]; + elementDisposables: DisposableStore; + templateDisposables: DisposableStore; type: 'hitCount' | 'condition' | 'name'; updating?: boolean; } @@ -485,7 +488,8 @@ interface IExceptionBreakpointInputTemplateData { inputBox: InputBox; checkbox: HTMLInputElement; breakpoint: IExceptionBreakpoint; - toDispose: IDisposable[]; + templateDisposables: DisposableStore; + elementDisposables: DisposableStore; } const breakpointIdToActionBarDomeNode = new Map(); @@ -511,14 +515,16 @@ class BreakpointsRenderer implements IListRenderer { + data.templateDisposables.add(dom.addStandardDisposableListener(data.checkbox, 'change', (e) => { this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context); })); @@ -529,7 +535,7 @@ class BreakpointsRenderer implements IListRenderer { + data.checkbox = createCheckbox(data.templateDisposables); + data.templateDisposables.add(dom.addStandardDisposableListener(data.checkbox, 'change', (e) => { this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context); })); @@ -613,7 +627,7 @@ class ExceptionBreakpointsRenderer implements IListRenderer { + data.checkbox = createCheckbox(data.templateDisposables); + data.templateDisposables.add(dom.addStandardDisposableListener(data.checkbox, 'change', (e) => { this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context); })); @@ -689,7 +709,7 @@ class FunctionBreakpointsRenderer implements IListRenderer { + data.checkbox = createCheckbox(data.templateDisposables); + data.templateDisposables.add(dom.addStandardDisposableListener(data.checkbox, 'change', (e) => { this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context); })); @@ -778,7 +804,7 @@ class DataBreakpointsRenderer implements IListRenderer { + data.checkbox = createCheckbox(data.templateDisposables); + data.templateDisposables.add(dom.addStandardDisposableListener(data.checkbox, 'change', (e) => { this.debugService.enableOrDisableBreakpoints(!data.context.enabled, data.context); })); @@ -870,7 +902,7 @@ class InstructionBreakpointsRenderer implements IListRenderer { template.updating = true; try { @@ -967,7 +1006,7 @@ class FunctionBreakpointInputRenderer implements IListRenderer { + toDispose.add(dom.addStandardDisposableListener(inputBox.inputElement, 'keydown', (e: IKeyboardEvent) => { const isEscape = e.equals(KeyCode.Escape); const isEnter = e.equals(KeyCode.Enter); if (isEscape || isEnter) { @@ -976,14 +1015,16 @@ class FunctionBreakpointInputRenderer implements IListRenderer { + toDispose.add(dom.addDisposableListener(inputBox.inputElement, 'blur', () => { if (!template.updating) { wrapUp(!!inputBox.value); } })); template.inputBox = inputBox; - template.toDispose = toDispose; + template.elementDisposables = new DisposableStore(); + template.templateDisposables = toDispose; + template.templateDisposables.add(template.elementDisposables); return template; } @@ -993,7 +1034,7 @@ class FunctionBreakpointInputRenderer implements IListRenderer { template.updating = true; @@ -1076,7 +1122,7 @@ class DataBreakpointInputRenderer implements IListRenderer { + toDispose.add(dom.addStandardDisposableListener(inputBox.inputElement, 'keydown', (e: IKeyboardEvent) => { const isEscape = e.equals(KeyCode.Escape); const isEnter = e.equals(KeyCode.Enter); if (isEscape || isEnter) { @@ -1085,14 +1131,16 @@ class DataBreakpointInputRenderer implements IListRenderer { + toDispose.add(dom.addDisposableListener(inputBox.inputElement, 'blur', () => { if (!template.updating) { wrapUp(!!inputBox.value); } })); template.inputBox = inputBox; - template.toDispose = toDispose; + template.elementDisposables = new DisposableStore(); + template.templateDisposables = toDispose; + template.templateDisposables.add(template.elementDisposables); return template; } @@ -1102,7 +1150,7 @@ class DataBreakpointInputRenderer implements IListRenderer { this.view.breakpointInputFocused.set(false); let newCondition = template.breakpoint.condition; @@ -1172,7 +1226,7 @@ class ExceptionBreakpointInputRenderer implements IListRenderer { + toDispose.add(dom.addStandardDisposableListener(inputBox.inputElement, 'keydown', (e: IKeyboardEvent) => { const isEscape = e.equals(KeyCode.Escape); const isEnter = e.equals(KeyCode.Enter); if (isEscape || isEnter) { @@ -1181,7 +1235,7 @@ class ExceptionBreakpointInputRenderer implements IListRenderer { + toDispose.add(dom.addDisposableListener(inputBox.inputElement, 'blur', () => { // Need to react with a timeout on the blur event due to possible concurent splices #56443 setTimeout(() => { wrapUp(true); @@ -1189,7 +1243,9 @@ class ExceptionBreakpointInputRenderer implements IListRenderer= 0 && colorNumber <= 15) { if (colorType === 'underline') { // for underline colors we just decode the 0-15 color number to theme color, set and return - const theme = themeService.getColorTheme(); const colorName = ansiColorIdentifiers[colorNumber]; - const color = theme.getColor(colorName); - if (color) { - changeColor(colorType, color.rgba); - } + changeColor(colorType, `--vscode-treminal-${colorName}`); return; } // Need to map to one of the four basic color ranges (30-37, 90-97, 40-47, 100-107) @@ -364,7 +359,6 @@ export function handleANSIOutput(text: string, linkDetector: ILinkDetector, them * nothing. */ function setBasicColor(styleCode: number): void { - const theme = themeService.getColorTheme(); let colorType: 'foreground' | 'background' | undefined; let colorIndex: number | undefined; @@ -384,10 +378,7 @@ export function handleANSIOutput(text: string, linkDetector: ILinkDetector, them if (colorIndex !== undefined && colorType) { const colorName = ansiColorIdentifiers[colorIndex]; - const color = theme.getColor(colorName); - if (color) { - changeColor(colorType, color.rgba); - } + changeColor(colorType, `--vscode-${colorName.replaceAll('.', '-')}`); } } } @@ -407,9 +398,9 @@ export function appendStylizedStringToContainer( cssClasses: string[], linkDetector: ILinkDetector, workspaceFolder: IWorkspaceFolder | undefined, - customTextColor?: RGBA, - customBackgroundColor?: RGBA, - customUnderlineColor?: RGBA + customTextColor?: RGBA | string, + customBackgroundColor?: RGBA | string, + customUnderlineColor?: RGBA | string, ): void { if (!root || !stringContent) { return; @@ -420,16 +411,17 @@ export function appendStylizedStringToContainer( container.className = cssClasses.join(' '); if (customTextColor) { container.style.color = - Color.Format.CSS.formatRGB(new Color(customTextColor)); + typeof customTextColor === 'string' ? `var(${customTextColor})` : Color.Format.CSS.formatRGB(new Color(customTextColor)); } if (customBackgroundColor) { container.style.backgroundColor = - Color.Format.CSS.formatRGB(new Color(customBackgroundColor)); + typeof customBackgroundColor === 'string' ? `var(${customBackgroundColor})` : Color.Format.CSS.formatRGB(new Color(customBackgroundColor)); } if (customUnderlineColor) { container.style.textDecorationColor = - Color.Format.CSS.formatRGB(new Color(customUnderlineColor)); + typeof customUnderlineColor === 'string' ? `var(${customUnderlineColor})` : Color.Format.CSS.formatRGB(new Color(customUnderlineColor)); } + root.appendChild(container); } diff --git a/src/vs/workbench/contrib/debug/browser/debugCommands.ts b/src/vs/workbench/contrib/debug/browser/debugCommands.ts index 33f2b8f83c3..096a909b74a 100644 --- a/src/vs/workbench/contrib/debug/browser/debugCommands.ts +++ b/src/vs/workbench/contrib/debug/browser/debugCommands.ts @@ -10,7 +10,7 @@ import { KeybindingsRegistry, KeybindingWeight } from '../../../../platform/keyb import { IListService } from '../../../../platform/list/browser/listService.js'; import { IDebugService, IEnablement, CONTEXT_BREAKPOINTS_FOCUSED, CONTEXT_WATCH_EXPRESSIONS_FOCUSED, CONTEXT_VARIABLES_FOCUSED, EDITOR_CONTRIBUTION_ID, IDebugEditorContribution, CONTEXT_IN_DEBUG_MODE, CONTEXT_EXPRESSION_SELECTED, IConfig, IStackFrame, IThread, IDebugSession, CONTEXT_DEBUG_STATE, IDebugConfiguration, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, REPL_VIEW_ID, CONTEXT_DEBUGGERS_AVAILABLE, State, getStateLabel, CONTEXT_BREAKPOINT_INPUT_FOCUSED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, VIEWLET_ID, CONTEXT_DISASSEMBLY_VIEW_FOCUS, CONTEXT_IN_DEBUG_REPL, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, isFrameDeemphasized } from '../common/debug.js'; import { Expression, Variable, Breakpoint, FunctionBreakpoint, DataBreakpoint, Thread } from '../common/debugModel.js'; -import { IExtensionsViewPaneContainer, VIEWLET_ID as EXTENSIONS_VIEWLET_ID } from '../../extensions/common/extensions.js'; +import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; import { ICodeEditor, isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { MenuRegistry, MenuId, Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; @@ -937,14 +937,12 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ when: undefined, primary: undefined, handler: async (accessor, query: string) => { - const paneCompositeService = accessor.get(IPaneCompositePartService); - const viewlet = (await paneCompositeService.openPaneComposite(EXTENSIONS_VIEWLET_ID, ViewContainerLocation.Sidebar, true))?.getViewPaneContainer() as IExtensionsViewPaneContainer; + const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); let searchFor = `@category:debuggers`; if (typeof query === 'string') { searchFor += ` ${query}`; } - viewlet.search(searchFor); - viewlet.focus(); + return extensionsWorkbenchService.openSearch(searchFor); } }); diff --git a/src/vs/workbench/contrib/debug/browser/debugExpressionRenderer.ts b/src/vs/workbench/contrib/debug/browser/debugExpressionRenderer.ts new file mode 100644 index 00000000000..38630cd47e5 --- /dev/null +++ b/src/vs/workbench/contrib/debug/browser/debugExpressionRenderer.ts @@ -0,0 +1,193 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from '../../../../base/browser/dom.js'; +import { IHighlight } from '../../../../base/browser/ui/highlightedlabel/highlightedLabel.js'; +import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; +import { DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; +import { IObservable } from '../../../../base/common/observable.js'; +import { CommandsRegistry, ICommandService } from '../../../../platform/commands/common/commands.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IHoverService } from '../../../../platform/hover/browser/hover.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; +import { IDebugSession, IExpressionValue } from '../common/debug.js'; +import { Expression, ExpressionContainer, Variable } from '../common/debugModel.js'; +import { ReplEvaluationResult } from '../common/replModel.js'; +import { IVariableTemplateData } from './baseDebugView.js'; +import { handleANSIOutput } from './debugANSIHandling.js'; +import { COPY_EVALUATE_PATH_ID, COPY_VALUE_ID } from './debugCommands.js'; +import { DebugLinkHoverBehavior, DebugLinkHoverBehaviorTypeData, ILinkDetector, LinkDetector } from './linkDetector.js'; + +export interface IValueHoverOptions { + /** Commands to show in the hover footer. */ + commands?: { id: string; args: unknown[] }[]; +} + +export interface IRenderValueOptions { + showChanged?: boolean; + maxValueLength?: number; + /** If not false, a rich hover will be shown on the element. */ + hover?: false | IValueHoverOptions; + colorize?: boolean; + + /** @deprecated */ + wasANSI?: boolean; + session?: IDebugSession; + locationReference?: number; +} + +export interface IRenderVariableOptions { + showChanged?: boolean; + highlights?: IHighlight[]; +} + + +const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024; +const booleanRegex = /^(true|false)$/i; +const stringRegex = /^(['"]).*\1$/; + +export class DebugExpressionRenderer { + private displayType: IObservable; + private readonly linkDetector: LinkDetector; + + constructor( + @ICommandService private readonly commandService: ICommandService, + @IConfigurationService configurationService: IConfigurationService, + @IInstantiationService instantiationService: IInstantiationService, + @IHoverService private readonly hoverService: IHoverService, + ) { + this.linkDetector = instantiationService.createInstance(LinkDetector); + this.displayType = observableConfigValue('debug.showVariableTypes', false, configurationService); + } + + renderVariable(data: IVariableTemplateData, variable: Variable, options: IRenderVariableOptions = {}): IDisposable { + const displayType = this.displayType.get(); + + if (variable.available) { + data.type.textContent = ''; + let text = variable.name; + if (variable.value && typeof variable.name === 'string') { + if (variable.type && displayType) { + text += ': '; + data.type.textContent = variable.type + ' ='; + } else { + text += ' ='; + } + } + + data.label.set(text, options.highlights, variable.type && !displayType ? variable.type : variable.name); + data.name.classList.toggle('virtual', variable.presentationHint?.kind === 'virtual'); + data.name.classList.toggle('internal', variable.presentationHint?.visibility === 'internal'); + } else if (variable.value && typeof variable.name === 'string' && variable.name) { + data.label.set(':'); + } + + data.expression.classList.toggle('lazy', !!variable.presentationHint?.lazy); + const commands = [ + { id: COPY_VALUE_ID, args: [variable, [variable]] as unknown[] } + ]; + if (variable.evaluateName) { + commands.push({ id: COPY_EVALUATE_PATH_ID, args: [{ variable }] }); + } + + return this.renderValue(data.value, variable, { + showChanged: options.showChanged, + maxValueLength: MAX_VALUE_RENDER_LENGTH_IN_VIEWLET, + hover: { commands }, + colorize: true, + session: variable.getSession(), + }); + } + + renderValue(container: HTMLElement, expressionOrValue: IExpressionValue | string, options: IRenderValueOptions = {}): IDisposable { + const store = new DisposableStore(); + const supportsANSI = !!options.session?.capabilities.supportsANSIStyling; + + let value = typeof expressionOrValue === 'string' ? expressionOrValue : expressionOrValue.value; + + // remove stale classes + container.className = 'value'; + // when resolving expressions we represent errors from the server as a variable with name === null. + if (value === null || ((expressionOrValue instanceof Expression || expressionOrValue instanceof Variable || expressionOrValue instanceof ReplEvaluationResult) && !expressionOrValue.available)) { + container.classList.add('unavailable'); + if (value !== Expression.DEFAULT_VALUE) { + container.classList.add('error'); + } + } else { + if (typeof expressionOrValue !== 'string' && options.showChanged && expressionOrValue.valueChanged && value !== Expression.DEFAULT_VALUE) { + // value changed color has priority over other colors. + container.className = 'value changed'; + expressionOrValue.valueChanged = false; + } + + if (options.colorize && typeof expressionOrValue !== 'string') { + if (expressionOrValue.type === 'number' || expressionOrValue.type === 'boolean' || expressionOrValue.type === 'string') { + container.classList.add(expressionOrValue.type); + } else if (!isNaN(+value)) { + container.classList.add('number'); + } else if (booleanRegex.test(value)) { + container.classList.add('boolean'); + } else if (stringRegex.test(value)) { + container.classList.add('string'); + } + } + } + + if (options.maxValueLength && value && value.length > options.maxValueLength) { + value = value.substring(0, options.maxValueLength) + '...'; + } + if (!value) { + value = ''; + } + + const session = options.session ?? ((expressionOrValue instanceof ExpressionContainer) ? expressionOrValue.getSession() : undefined); + // Only use hovers for links if thre's not going to be a hover for the value. + const hoverBehavior: DebugLinkHoverBehaviorTypeData = options.hover === false ? { type: DebugLinkHoverBehavior.Rich, store } : { type: DebugLinkHoverBehavior.None }; + dom.clearNode(container); + const locationReference = options.locationReference ?? (expressionOrValue instanceof ExpressionContainer && expressionOrValue.valueLocationReference); + + let linkDetector: ILinkDetector = this.linkDetector; + if (locationReference && session) { + linkDetector = this.linkDetector.makeReferencedLinkDetector(locationReference, session); + } + + if (supportsANSI) { + container.appendChild(handleANSIOutput(value, linkDetector, session ? session.root : undefined)); + } else { + container.appendChild(linkDetector.linkify(value, false, session?.root, true, hoverBehavior)); + } + + if (options.hover !== false) { + const { commands = [] } = options.hover || {}; + store.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('mouse'), container, () => { + const container = dom.$('div'); + const markdownHoverElement = dom.$('div.hover-row'); + const hoverContentsElement = dom.append(markdownHoverElement, dom.$('div.hover-contents')); + const hoverContentsPre = dom.append(hoverContentsElement, dom.$('pre.debug-var-hover-pre')); + if (supportsANSI) { + // note: intentionally using `this.linkDetector` so we don't blindly linkify the + // entire contents and instead only link file paths that it contains. + hoverContentsPre.appendChild(handleANSIOutput(value, this.linkDetector, session ? session.root : undefined)); + } else { + hoverContentsPre.textContent = value; + } + container.appendChild(markdownHoverElement); + return container; + }, { + actions: commands.map(({ id, args }) => { + const description = CommandsRegistry.getCommand(id)?.metadata?.description; + return { + label: typeof description === 'string' ? description : description ? description.value : id, + commandId: id, + run: () => this.commandService.executeCommand(id, ...args), + }; + }) + })); + } + + return store; + } +} diff --git a/src/vs/workbench/contrib/debug/browser/debugHover.ts b/src/vs/workbench/contrib/debug/browser/debugHover.ts index f0ba8e94878..0a381676af0 100644 --- a/src/vs/workbench/contrib/debug/browser/debugHover.ts +++ b/src/vs/workbench/contrib/debug/browser/debugHover.ts @@ -29,17 +29,16 @@ import * as nls from '../../../../nls.js'; import { IMenuService, MenuId } from '../../../../platform/actions/common/actions.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; -import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { WorkbenchAsyncDataTree } from '../../../../platform/list/browser/listService.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { asCssVariable, editorHoverBackground, editorHoverBorder, editorHoverForeground } from '../../../../platform/theme/common/colorRegistry.js'; -import { AbstractExpressionDataSource, renderExpressionValue } from './baseDebugView.js'; -import { LinkDetector } from './linkDetector.js'; -import { VariablesRenderer, VisualizedVariableRenderer, openContextMenuForVariableTreeElement } from './variablesView.js'; import { IDebugService, IDebugSession, IExpression, IExpressionContainer, IStackFrame } from '../common/debug.js'; import { Expression, Variable, VisualizedExpression } from '../common/debugModel.js'; import { getEvaluatableExpressionAtPosition } from '../common/debugUtils.js'; +import { AbstractExpressionDataSource } from './baseDebugView.js'; +import { DebugExpressionRenderer } from './debugExpressionRenderer.js'; +import { VariablesRenderer, VisualizedVariableRenderer, openContextMenuForVariableTreeElement } from './variablesView.js'; const $ = dom.$; @@ -102,6 +101,7 @@ export class DebugHoverWidget implements IContentWidget { private toDispose: lifecycle.IDisposable[]; private scrollbar!: DomScrollableElement; private debugHoverComputer: DebugHoverComputer; + private expressionRenderer: DebugExpressionRenderer; private expressionToRender: IExpression | undefined; private isUpdatingTree = false; @@ -117,13 +117,13 @@ export class DebugHoverWidget implements IContentWidget { @IMenuService private readonly menuService: IMenuService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IContextMenuService private readonly contextMenuService: IContextMenuService, - @IHoverService private readonly hoverService: IHoverService, ) { this.toDispose = []; this.showAtPosition = null; this.positionPreference = [ContentWidgetPositionPreference.ABOVE, ContentWidgetPositionPreference.BELOW]; this.debugHoverComputer = this.instantiationService.createInstance(DebugHoverComputer, this.editor); + this.expressionRenderer = this.instantiationService.createInstance(DebugExpressionRenderer); } private create(): void { @@ -135,10 +135,9 @@ export class DebugHoverWidget implements IContentWidget { const tip = dom.append(this.complexValueContainer, $('.tip')); tip.textContent = nls.localize({ key: 'quickTip', comment: ['"switch to editor language hover" means to show the programming language hover widget instead of the debug hover'] }, 'Hold {0} key to switch to editor language hover', isMacintosh ? 'Option' : 'Alt'); const dataSource = this.instantiationService.createInstance(DebugHoverDataSource); - const linkeDetector = this.instantiationService.createInstance(LinkDetector); this.tree = >this.instantiationService.createInstance(WorkbenchAsyncDataTree, 'DebugHover', this.treeContainer, new DebugHoverDelegate(), [ - this.instantiationService.createInstance(VariablesRenderer, linkeDetector), - this.instantiationService.createInstance(VisualizedVariableRenderer, linkeDetector), + this.instantiationService.createInstance(VariablesRenderer, this.expressionRenderer), + this.instantiationService.createInstance(VisualizedVariableRenderer, this.expressionRenderer), ], dataSource, { accessibilityProvider: new DebugHoverAccessibilityProvider(), @@ -282,7 +281,7 @@ export class DebugHoverWidget implements IContentWidget { options: DebugHoverWidget._HOVER_HIGHLIGHT_DECORATION_OPTIONS }]); - return this.doShow(result.range.getStartPosition(), expression, focus, mouseEvent); + return this.doShow(session, result.range.getStartPosition(), expression, focus, mouseEvent); } private static readonly _HOVER_HIGHLIGHT_DECORATION_OPTIONS = ModelDecorationOptions.register({ @@ -290,7 +289,7 @@ export class DebugHoverWidget implements IContentWidget { className: 'hoverHighlight' }); - private async doShow(position: Position, expression: IExpression, focus: boolean, mouseEvent: IMouseEvent | undefined): Promise { + private async doShow(session: IDebugSession | undefined, position: Position, expression: IExpression, focus: boolean, mouseEvent: IMouseEvent | undefined): Promise { if (!this.domNode) { this.create(); } @@ -302,11 +301,12 @@ export class DebugHoverWidget implements IContentWidget { if (!expression.hasChildren) { this.complexValueContainer.hidden = true; this.valueContainer.hidden = false; - renderExpressionValue(store, expression, this.valueContainer, { + store.add(this.expressionRenderer.renderValue(this.valueContainer, expression, { showChanged: false, colorize: true, hover: false, - }, this.hoverService); + session, + })); this.valueContainer.title = ''; this.editor.layoutContentWidget(this); this.scrollbar.scanDomNode(); diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index b275e635fa8..55085e77ea9 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -363,7 +363,8 @@ export class DebugSession implements IDebugSession, IDisposable { supportsMemoryReferences: true, //#129684 supportsArgsCanBeInterpretedByShell: true, // #149910 supportsMemoryEvent: true, // #133643 - supportsStartDebuggingRequest: true + supportsStartDebuggingRequest: true, + supportsANSIStyling: true, }); this.initialized = true; @@ -1201,7 +1202,7 @@ export class DebugSession implements IDebugSession, IDisposable { if (event.body.group === 'start' || event.body.group === 'startCollapsed') { const expanded = event.body.group === 'start'; - this.repl.startGroup(event.body.output || '', expanded, source); + this.repl.startGroup(this, event.body.output || '', expanded, source); return; } if (event.body.group === 'end') { diff --git a/src/vs/workbench/contrib/debug/browser/debugStatus.ts b/src/vs/workbench/contrib/debug/browser/debugStatus.ts index 9c8c5141fba..c98e63ab7b7 100644 --- a/src/vs/workbench/contrib/debug/browser/debugStatus.ts +++ b/src/vs/workbench/contrib/debug/browser/debugStatus.ts @@ -66,7 +66,7 @@ export class DebugStatusContribution implements IWorkbenchContribution { name: nls.localize('status.debug', "Debug"), text: '$(debug-alt-small) ' + text, ariaLabel: nls.localize('debugTarget', "Debug: {0}", text), - tooltip: nls.localize('selectAndStartDebug', "Select and start debug configuration"), + tooltip: nls.localize('selectAndStartDebug', "Select and Start Debug Configuration"), command: 'workbench.action.debug.selectandstart' }; } diff --git a/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css b/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css index b808f8b5a7c..2a8f7661cb2 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css +++ b/src/vs/workbench/contrib/debug/browser/media/debug.contribution.css @@ -59,6 +59,10 @@ margin: 0; } +.debug-var-hover-pre span { + display: inline !important; +} + /* Do not push text with inline decoration when decoration on start of line */ .monaco-editor .debug-top-stack-frame-column.start-of-line { position: absolute; diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index 01133271bdc..7e33db78057 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -12,6 +12,7 @@ import { IAsyncDataSource, ITreeContextMenuEvent, ITreeNode } from '../../../../ import { IAction } from '../../../../base/common/actions.js'; import { RunOnceScheduler } from '../../../../base/common/async.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { Codicon } from '../../../../base/common/codicons.js'; import { memoize } from '../../../../base/common/decorators.js'; import { Emitter } from '../../../../base/common/event.js'; import { FuzzyScore } from '../../../../base/common/filters.js'; @@ -21,7 +22,6 @@ import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; import { removeAnsiEscapeCodes } from '../../../../base/common/strings.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { URI as uri } from '../../../../base/common/uri.js'; -import './media/repl.css'; import { ICodeEditor, isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { EditorAction, registerEditorAction } from '../../../../editor/browser/editorExtensions.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; @@ -38,6 +38,7 @@ import { IModelService } from '../../../../editor/common/services/model.js'; import { ITextResourcePropertiesService } from '../../../../editor/common/services/textResourceConfiguration.js'; import { SuggestController } from '../../../../editor/contrib/suggest/browser/suggestController.js'; import { localize, localize2 } from '../../../../nls.js'; +import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; import { createAndFillInContextMenuActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; import { Action2, IMenu, IMenuService, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; @@ -45,6 +46,7 @@ import { IConfigurationService } from '../../../../platform/configuration/common import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { registerAndCreateHistoryNavigationContext } from '../../../../platform/history/browser/contextScopedHistoryWidget.js'; +import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; @@ -56,25 +58,23 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { editorForeground, resolveColorValue } from '../../../../platform/theme/common/colorRegistry.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; +import { registerNavigableContainer } from '../../../browser/actions/widgetNavigationCommands.js'; import { FilterViewPane, IViewPaneOptions, ViewAction } from '../../../browser/parts/views/viewPane.js'; import { IViewDescriptorService } from '../../../common/views.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; +import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; +import { AccessibilityCommandId } from '../../accessibility/common/accessibilityCommands.js'; import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions } from '../../codeEditor/browser/simpleEditorOptions.js'; -import { FocusSessionActionViewItem } from './debugActionViewItems.js'; -import { debugConsoleClearAll, debugConsoleEvaluationPrompt } from './debugIcons.js'; -import { LinkDetector } from './linkDetector.js'; -import { ReplFilter } from './replFilter.js'; -import { ReplAccessibilityProvider, ReplDataSource, ReplDelegate, ReplEvaluationInputsRenderer, ReplEvaluationResultsRenderer, ReplGroupRenderer, ReplOutputElementRenderer, ReplRawObjectsRenderer, ReplVariablesRenderer } from './replViewer.js'; import { CONTEXT_DEBUG_STATE, CONTEXT_IN_DEBUG_REPL, CONTEXT_MULTI_SESSION_REPL, DEBUG_SCHEME, IDebugConfiguration, IDebugService, IDebugSession, IReplConfiguration, IReplElement, IReplOptions, REPL_VIEW_ID, State, getStateLabel } from '../common/debug.js'; import { Variable } from '../common/debugModel.js'; import { ReplEvaluationResult, ReplGroup } from '../common/replModel.js'; -import { IEditorService } from '../../../services/editor/common/editorService.js'; -import { registerNavigableContainer } from '../../../browser/actions/widgetNavigationCommands.js'; -import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; -import { IHoverService } from '../../../../platform/hover/browser/hover.js'; -import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js'; -import { AccessibilityCommandId } from '../../accessibility/common/accessibilityCommands.js'; -import { Codicon } from '../../../../base/common/codicons.js'; +import { FocusSessionActionViewItem } from './debugActionViewItems.js'; +import { DebugExpressionRenderer } from './debugExpressionRenderer.js'; +import { debugConsoleClearAll, debugConsoleEvaluationPrompt } from './debugIcons.js'; +import './media/repl.css'; +import { ReplFilter } from './replFilter.js'; +import { ReplAccessibilityProvider, ReplDataSource, ReplDelegate, ReplEvaluationInputsRenderer, ReplEvaluationResultsRenderer, ReplGroupRenderer, ReplOutputElementRenderer, ReplRawObjectsRenderer, ReplVariablesRenderer } from './replViewer.js'; const $ = dom.$; @@ -648,7 +648,7 @@ export class Repl extends FilterViewPane implements IHistoryNavigationWidget { this.replDelegate = new ReplDelegate(this.configurationService, this.replOptions); const wordWrap = this.configurationService.getValue('debug').console.wordWrap; this.treeContainer.classList.toggle('word-wrap', wordWrap); - const linkDetector = this.instantiationService.createInstance(LinkDetector); + const expressionRenderer = this.instantiationService.createInstance(DebugExpressionRenderer); this.replDataSource = new ReplDataSource(); const tree = this.tree = >this.instantiationService.createInstance( @@ -657,12 +657,12 @@ export class Repl extends FilterViewPane implements IHistoryNavigationWidget { this.treeContainer, this.replDelegate, [ - this.instantiationService.createInstance(ReplVariablesRenderer, linkDetector), - this.instantiationService.createInstance(ReplOutputElementRenderer, linkDetector), + this.instantiationService.createInstance(ReplVariablesRenderer, expressionRenderer), + this.instantiationService.createInstance(ReplOutputElementRenderer, expressionRenderer), new ReplEvaluationInputsRenderer(), - this.instantiationService.createInstance(ReplGroupRenderer, linkDetector), - new ReplEvaluationResultsRenderer(linkDetector, this.hoverService), - new ReplRawObjectsRenderer(linkDetector, this.hoverService), + this.instantiationService.createInstance(ReplGroupRenderer, expressionRenderer), + new ReplEvaluationResultsRenderer(expressionRenderer), + new ReplRawObjectsRenderer(expressionRenderer), ], this.replDataSource, { diff --git a/src/vs/workbench/contrib/debug/browser/replViewer.ts b/src/vs/workbench/contrib/debug/browser/replViewer.ts index 45df05a87aa..82a76c804bb 100644 --- a/src/vs/workbench/contrib/debug/browser/replViewer.ts +++ b/src/vs/workbench/contrib/debug/browser/replViewer.ts @@ -17,22 +17,19 @@ import { basename } from '../../../../base/common/path.js'; import severity from '../../../../base/common/severity.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { localize } from '../../../../nls.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IContextViewService } from '../../../../platform/contextview/browser/contextView.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ILabelService } from '../../../../platform/label/common/label.js'; import { defaultCountBadgeStyles } from '../../../../platform/theme/browser/defaultStyles.js'; -import { IThemeService } from '../../../../platform/theme/common/themeService.js'; -import { AbstractExpressionsRenderer, IExpressionTemplateData, IInputBoxOptions, renderExpressionValue, renderVariable } from './baseDebugView.js'; -import { handleANSIOutput } from './debugANSIHandling.js'; -import { debugConsoleEvaluationInput } from './debugIcons.js'; -import { ILinkDetector, LinkDetector } from './linkDetector.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IDebugConfiguration, IDebugService, IDebugSession, IExpression, IExpressionContainer, INestingReplElement, IReplElement, IReplElementSource, IReplOptions } from '../common/debug.js'; import { Variable } from '../common/debugModel.js'; import { RawObjectReplElement, ReplEvaluationInput, ReplEvaluationResult, ReplGroup, ReplOutputElement, ReplVariableElement } from '../common/replModel.js'; -import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { AbstractExpressionsRenderer, IExpressionTemplateData, IInputBoxOptions } from './baseDebugView.js'; +import { DebugExpressionRenderer } from './debugExpressionRenderer.js'; +import { debugConsoleEvaluationInput } from './debugIcons.js'; const $ = dom.$; @@ -43,6 +40,7 @@ interface IReplEvaluationInputTemplateData { interface IReplGroupTemplateData { label: HTMLElement; source: SourceWidget; + elementDisposable?: IDisposable; } interface IReplEvaluationResultTemplateData { @@ -57,7 +55,7 @@ interface IOutputReplElementTemplateData { value: HTMLElement; source: SourceWidget; getReplElementSource(): IReplElementSource | undefined; - elementListener: IDisposable; + elementDisposable: DisposableStore; } interface IRawObjectReplTemplateData { @@ -97,8 +95,7 @@ export class ReplGroupRenderer implements ITreeRenderer, _index: number, templateData: IReplGroupTemplateData): void { + + templateData.elementDisposable?.dispose(); const replGroup = element.element; dom.clearNode(templateData.label); - const result = handleANSIOutput(replGroup.name, this.linkDetector, this.themeService, undefined); - templateData.label.appendChild(result); + templateData.elementDisposable = this.expressionRenderer.renderValue(templateData.label, replGroup.name, { wasANSI: true, session: element.element.session }); templateData.source.setSource(replGroup.sourceData); } disposeTemplate(templateData: IReplGroupTemplateData): void { + templateData.elementDisposable?.dispose(); templateData.source.dispose(); } } @@ -135,8 +134,7 @@ export class ReplEvaluationResultsRenderer implements ITreeRenderer, index: number, templateData: IReplEvaluationResultTemplateData): void { templateData.elementStore.clear(); const expression = element.element; - renderExpressionValue(templateData.elementStore, expression, templateData.value, { + templateData.elementStore.add(this.expressionRenderer.renderValue(templateData.value, expression, { colorize: true, hover: false, - linkDetector: this.linkDetector, - }, this.hoverService); + session: element.element.getSession(), + })); } disposeTemplate(templateData: IReplEvaluationResultTemplateData): void { @@ -165,8 +163,7 @@ export class ReplOutputElementRenderer implements ITreeRenderer, index: number, templateData: IOutputReplElementTemplateData): void { + templateData.elementDisposable.clear(); this.setElementCount(element, templateData); - templateData.elementListener = element.onDidChangeCount(() => this.setElementCount(element, templateData)); + templateData.elementDisposable.add(element.onDidChangeCount(() => this.setElementCount(element, templateData))); // value dom.clearNode(templateData.value); // Reset classes to clear ansi decorations since templates are reused templateData.value.className = 'value'; const locationReference = element.expression?.valueLocationReference; - const detector: ILinkDetector = locationReference !== undefined ? this.linkDetector.makeReferencedLinkDetector(locationReference, element.session) : this.linkDetector; - templateData.value.appendChild(handleANSIOutput(element.value, detector, this.themeService, element.session.root)); + templateData.elementDisposable.add(this.expressionRenderer.renderValue(templateData.value, element.value, { wasANSI: true, session: element.session, locationReference })); templateData.value.classList.add((element.severity === severity.Warning) ? 'warn' : (element.severity === severity.Error) ? 'error' : (element.severity === severity.Ignore) ? 'ignore' : 'info'); templateData.source.setSource(element.sourceData); @@ -216,10 +214,11 @@ export class ReplOutputElementRenderer implements ITreeRenderer, _index: number, templateData: IOutputReplElementTemplateData): void { - templateData.elementListener.dispose(); + templateData.elementDisposable.clear(); } } @@ -232,11 +231,10 @@ export class ReplVariablesRenderer extends AbstractExpressionsRenderer>this.instantiationService.createInstance(WorkbenchAsyncDataTree, 'VariablesView', treeContainer, new VariablesDelegate(), [ - this.instantiationService.createInstance(VariablesRenderer, linkDetector), - this.instantiationService.createInstance(VisualizedVariableRenderer, linkDetector), + this.instantiationService.createInstance(VariablesRenderer, expressionRenderer), + this.instantiationService.createInstance(VisualizedVariableRenderer, expressionRenderer), new ScopesRenderer(), new ScopeErrorRenderer(), ], @@ -434,7 +434,7 @@ export class VisualizedVariableRenderer extends AbstractExpressionsRenderer { } constructor( - private readonly linkDetector: LinkDetector, + private readonly expressionRenderer: DebugExpressionRenderer, @IDebugService debugService: IDebugService, @IContextViewService contextViewService: IContextViewService, @IHoverService hoverService: IHoverService, @@ -461,12 +461,12 @@ export class VisualizedVariableRenderer extends AbstractExpressionsRenderer { text += ':'; } data.label.set(text, highlights, viz.name); - renderExpressionValue(data.elementDisposable, viz, data.value, { + data.elementDisposable.add(this.expressionRenderer.renderValue(data.value, viz, { showChanged: false, maxValueLength: 1024, colorize: true, - linkDetector: this.linkDetector - }, this.hoverService); + session: expression.getSession(), + })); } protected override getInputBoxOptions(expression: IExpression): IInputBoxOptions | undefined { @@ -516,16 +516,14 @@ export class VariablesRenderer extends AbstractExpressionsRenderer { static readonly ID = 'variable'; constructor( - private readonly linkDetector: LinkDetector, + private readonly expressionRenderer: DebugExpressionRenderer, @IMenuService private readonly menuService: IMenuService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IDebugVisualizerService private readonly visualization: IDebugVisualizerService, @IContextMenuService private readonly contextMenuService: IContextMenuService, - @ICommandService private readonly commandService: ICommandService, @IDebugService debugService: IDebugService, @IContextViewService contextViewService: IContextViewService, @IHoverService hoverService: IHoverService, - @IConfigurationService private configurationService: IConfigurationService, ) { super(debugService, contextViewService, hoverService); } @@ -535,17 +533,14 @@ export class VariablesRenderer extends AbstractExpressionsRenderer { } protected renderExpression(expression: IExpression, data: IExpressionTemplateData, highlights: IHighlight[]): void { - const showType = this.configurationService.getValue('debug').showVariableTypes; - renderVariable(data.elementDisposable, this.commandService, this.hoverService, expression as Variable, data, true, highlights, this.linkDetector, showType); + data.elementDisposable.add(this.expressionRenderer.renderVariable(data, expression as Variable, { + highlights, + showChanged: true, + })); } public override renderElement(node: ITreeNode, index: number, data: IExpressionTemplateData): void { data.elementDisposable.clear(); - data.elementDisposable.add(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('debug.showVariableTypes')) { - super.renderExpressionElement(node.element, node, data); - } - })); super.renderExpressionElement(node.element, node, data); } diff --git a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts index bec7f644a84..03ec10eedf1 100644 --- a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts +++ b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts @@ -30,12 +30,12 @@ import { IThemeService } from '../../../../platform/theme/common/themeService.js import { ViewAction, ViewPane } from '../../../browser/parts/views/viewPane.js'; import { IViewletViewOptions } from '../../../browser/parts/views/viewsViewlet.js'; import { IViewDescriptorService } from '../../../common/views.js'; -import { AbstractExpressionDataSource, AbstractExpressionsRenderer, IExpressionTemplateData, IInputBoxOptions, renderExpressionValue, renderViewTree } from './baseDebugView.js'; -import { watchExpressionsAdd, watchExpressionsRemoveAll } from './debugIcons.js'; -import { LinkDetector } from './linkDetector.js'; -import { VariablesRenderer, VisualizedVariableRenderer } from './variablesView.js'; import { CONTEXT_CAN_VIEW_MEMORY, CONTEXT_VARIABLE_IS_READONLY, CONTEXT_WATCH_EXPRESSIONS_EXIST, CONTEXT_WATCH_EXPRESSIONS_FOCUSED, CONTEXT_WATCH_ITEM_TYPE, IDebugConfiguration, IDebugService, IExpression, WATCH_VIEW_ID } from '../common/debug.js'; import { Expression, Variable, VisualizedExpression } from '../common/debugModel.js'; +import { AbstractExpressionDataSource, AbstractExpressionsRenderer, IExpressionTemplateData, IInputBoxOptions, renderViewTree } from './baseDebugView.js'; +import { DebugExpressionRenderer } from './debugExpressionRenderer.js'; +import { watchExpressionsAdd, watchExpressionsRemoveAll } from './debugIcons.js'; +import { VariablesRenderer, VisualizedVariableRenderer } from './variablesView.js'; const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024; let ignoreViewUpdates = false; @@ -50,6 +50,7 @@ export class WatchExpressionsView extends ViewPane { private watchItemType: IContextKey; private variableReadonly: IContextKey; private menu: IMenu; + private expressionRenderer: DebugExpressionRenderer; constructor( options: IViewletViewOptions, @@ -78,6 +79,7 @@ export class WatchExpressionsView extends ViewPane { this.variableReadonly = CONTEXT_VARIABLE_IS_READONLY.bindTo(contextKeyService); this.watchExpressionsExist.set(this.debugService.getModel().getWatchExpressions().length > 0); this.watchItemType = CONTEXT_WATCH_ITEM_TYPE.bindTo(contextKeyService); + this.expressionRenderer = instantiationService.createInstance(DebugExpressionRenderer); } protected override renderBody(container: HTMLElement): void { @@ -87,13 +89,12 @@ export class WatchExpressionsView extends ViewPane { container.classList.add('debug-watch'); const treeContainer = renderViewTree(container); - const linkDetector = this.instantiationService.createInstance(LinkDetector); - const expressionsRenderer = this.instantiationService.createInstance(WatchExpressionsRenderer, linkDetector); + const expressionsRenderer = this.instantiationService.createInstance(WatchExpressionsRenderer, this.expressionRenderer); this.tree = >this.instantiationService.createInstance(WorkbenchAsyncDataTree, 'WatchExpressions', treeContainer, new WatchExpressionsDelegate(), [ expressionsRenderer, - this.instantiationService.createInstance(VariablesRenderer, linkDetector), - this.instantiationService.createInstance(VisualizedVariableRenderer, linkDetector), + this.instantiationService.createInstance(VariablesRenderer, this.expressionRenderer), + this.instantiationService.createInstance(VisualizedVariableRenderer, this.expressionRenderer), ], this.instantiationService.createInstance(WatchExpressionsDataSource), { accessibilityProvider: new WatchExpressionsAccessibilityProvider(), @@ -279,7 +280,7 @@ export class WatchExpressionsRenderer extends AbstractExpressionsRenderer { static readonly ID = 'watchexpression'; constructor( - private readonly linkDetector: LinkDetector, + private readonly expressionRenderer: DebugExpressionRenderer, @IMenuService private readonly menuService: IMenuService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IDebugService debugService: IDebugService, @@ -330,12 +331,12 @@ export class WatchExpressionsRenderer extends AbstractExpressionsRenderer { } data.label.set(text, highlights, title); - renderExpressionValue(data.elementDisposable, expression, data.value, { + data.elementDisposable.add(this.expressionRenderer.renderValue(data.value, expression, { showChanged: true, maxValueLength: MAX_VALUE_RENDER_LENGTH_IN_VIEWLET, - linkDetector: this.linkDetector, - colorize: true - }, this.hoverService); + colorize: true, + session: expression.getSession(), + })); } protected getInputBoxOptions(expression: IExpression, settingValue: boolean): IInputBoxOptions { diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 798dcc4e797..b225f145056 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -164,6 +164,7 @@ export interface IExpressionValue { export interface IExpressionContainer extends ITreeElement, IExpressionValue { readonly hasChildren: boolean; + getSession(): IDebugSession | undefined; evaluateLazy(): Promise; getChildren(): Promise; readonly reference?: number; @@ -911,7 +912,6 @@ export interface IDebugAdapterInlineImpl extends IDisposable { export interface IDebugAdapterImpl { readonly type: 'implementation'; - readonly implementation: IDebugAdapterInlineImpl; } export type IAdapterDescriptor = IDebugAdapterExecutable | IDebugAdapterServer | IDebugAdapterNamedPipeServer | IDebugAdapterImpl; diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index e6dba4a911e..4660763365f 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -8,9 +8,9 @@ import { findLastIdx } from '../../../../base/common/arraysFind.js'; import { DeferredPromise, RunOnceScheduler } from '../../../../base/common/async.js'; import { VSBuffer, decodeBase64, encodeBase64 } from '../../../../base/common/buffer.js'; import { CancellationTokenSource } from '../../../../base/common/cancellation.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; +import { Emitter, Event, trackSetChanges } from '../../../../base/common/event.js'; import { stringHash } from '../../../../base/common/hash.js'; -import { Disposable, DisposableMap, IDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; import { mixin } from '../../../../base/common/objects.js'; import { autorun } from '../../../../base/common/observable.js'; import * as resources from '../../../../base/common/resources.js'; @@ -258,7 +258,7 @@ export class VisualizedExpression implements IExpression { return Promise.resolve(); } getChildren(): Promise { - return this.visualizer.getVisualizedChildren(this.treeId, this.treeItem.id); + return this.visualizer.getVisualizedChildren(this.session, this.treeId, this.treeItem.id); } getId(): string { @@ -278,12 +278,17 @@ export class VisualizedExpression implements IExpression { } constructor( + private readonly session: IDebugSession | undefined, private readonly visualizer: IDebugVisualizerService, public readonly treeId: string, public readonly treeItem: IDebugVisualizationTreeItem, public readonly original?: Variable, ) { } + public getSession(): IDebugSession | undefined { + return this.session; + } + /** Edits the value, sets the {@link errorMessage} and returns false if unsuccessful */ public async edit(newValue: string) { try { @@ -1422,7 +1427,6 @@ export class DebugModel extends Disposable implements IDebugModel { private exceptionBreakpoints!: ExceptionBreakpoint[]; private dataBreakpoints!: DataBreakpoint[]; private watchExpressions!: Expression[]; - private watchExpressionChangeListeners: DisposableMap = this._register(new DisposableMap()); private instructionBreakpoints: InstructionBreakpoint[]; constructor( @@ -1446,12 +1450,14 @@ export class DebugModel extends Disposable implements IDebugModel { this._onDidChangeWatchExpressions.fire(undefined); })); + this._register(trackSetChanges( + () => new Set(this.watchExpressions), + this.onDidChangeWatchExpressions, + (we) => we.onDidChangeValue((e) => this._onDidChangeWatchExpressionValue.fire(e))) + ); + this.instructionBreakpoints = []; this.sessions = []; - - for (const we of this.watchExpressions) { - this.watchExpressionChangeListeners.set(we.getId(), we.onDidChangeValue((e) => this._onDidChangeWatchExpressionValue.fire(e))); - } } getId(): string { @@ -2025,7 +2031,6 @@ export class DebugModel extends Disposable implements IDebugModel { addWatchExpression(name?: string): IExpression { const we = new Expression(name || ''); - this.watchExpressionChangeListeners.set(we.getId(), we.onDidChangeValue((e) => this._onDidChangeWatchExpressionValue.fire(e))); this.watchExpressions.push(we); this._onDidChangeWatchExpressions.fire(we); @@ -2043,11 +2048,6 @@ export class DebugModel extends Disposable implements IDebugModel { removeWatchExpressions(id: string | null = null): void { this.watchExpressions = id ? this.watchExpressions.filter(we => we.getId() !== id) : []; this._onDidChangeWatchExpressions.fire(undefined); - if (!id) { - this.watchExpressionChangeListeners.clearAndDisposeAll(); - return; - } - this.watchExpressionChangeListeners.deleteAndDispose(id); } moveWatchExpression(id: string, position: number): void { diff --git a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts index 09dd44e76b6..560d252edd3 100644 --- a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts +++ b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts @@ -221,7 +221,12 @@ declare module DebugProtocol { etc. */ category?: 'console' | 'important' | 'stdout' | 'stderr' | 'telemetry' | string; - /** The output to report. */ + /** The output to report. + + ANSI escape sequences may be used to inflience text color and styling if `supportsANSIStyling` is present in both the adapter's `Capabilities` and the client's `InitializeRequestArguments`. A client may strip any unrecognized ANSI sequences. + + If the `supportsANSIStyling` capabilities are not both true, then the client should display the output literally. + */ output: string; /** Support for keeping an output log organized by grouping related messages. 'start': Start a new group in expanded mode. Subsequent output events are members of the group and should be shown indented. @@ -530,6 +535,8 @@ declare module DebugProtocol { supportsArgsCanBeInterpretedByShell?: boolean; /** Client supports the `startDebugging` request. */ supportsStartDebuggingRequest?: boolean; + /** The client will interpret ANSI escape sequences in the display of `OutputEvent.output` and `Variable.value` fields when `Capabilities.supportsANSIStyling` is also enabled. */ + supportsANSIStyling?: boolean; } /** Response to `initialize` request. */ @@ -1840,6 +1847,8 @@ declare module DebugProtocol { Clients may present the first applicable mode in this array as the 'default' mode in gestures that set breakpoints. */ breakpointModes?: BreakpointMode[]; + /** The debug adapter supports ANSI escape sequences in styling of `OutputEvent.output` and `Variable.value` fields. */ + supportsANSIStyling?: boolean; } /** An `ExceptionBreakpointsFilter` is shown in the UI as an filter option for configuring how exceptions are dealt with. */ diff --git a/src/vs/workbench/contrib/debug/common/debugVisualizers.ts b/src/vs/workbench/contrib/debug/common/debugVisualizers.ts index ec14f81961b..c607fbe166b 100644 --- a/src/vs/workbench/contrib/debug/common/debugVisualizers.ts +++ b/src/vs/workbench/contrib/debug/common/debugVisualizers.ts @@ -10,7 +10,7 @@ import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from '../../ import { ExtensionIdentifier, IExtensionDescription } from '../../../../platform/extensions/common/extensions.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../platform/log/common/log.js'; -import { CONTEXT_VARIABLE_NAME, CONTEXT_VARIABLE_TYPE, CONTEXT_VARIABLE_VALUE, MainThreadDebugVisualization, IDebugVisualization, IDebugVisualizationContext, IExpression, IExpressionContainer, IDebugVisualizationTreeItem } from './debug.js'; +import { CONTEXT_VARIABLE_NAME, CONTEXT_VARIABLE_TYPE, CONTEXT_VARIABLE_VALUE, MainThreadDebugVisualization, IDebugVisualization, IDebugVisualizationContext, IExpression, IExpressionContainer, IDebugVisualizationTreeItem, IDebugSession } from './debug.js'; import { getContextForVariable } from './debugContext.js'; import { Scope, Variable, VisualizedExpression } from './debugModel.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; @@ -84,7 +84,7 @@ export interface IDebugVisualizerService { /** * Gets children for a visualized tree node. */ - getVisualizedChildren(treeId: string, treeElementId: number): Promise; + getVisualizedChildren(session: IDebugSession | undefined, treeId: string, treeElementId: number): Promise; /** * Gets children for a visualized tree node. @@ -202,7 +202,7 @@ export class DebugVisualizerService implements IDebugVisualizerService { return; } - return new VisualizedExpression(this, treeId, treeItem, expr); + return new VisualizedExpression(expr.getSession(), this, treeId, treeItem, expr); } catch (e) { this.logService.warn('Failed to get visualized node', e); return; @@ -210,9 +210,10 @@ export class DebugVisualizerService implements IDebugVisualizerService { } /** @inheritdoc */ - public async getVisualizedChildren(treeId: string, treeElementId: number): Promise { - const children = await this.trees.get(treeId)?.getChildren(treeElementId) || []; - return children.map(c => new VisualizedExpression(this, treeId, c, undefined)); + public async getVisualizedChildren(session: IDebugSession | undefined, treeId: string, treeElementId: number): Promise { + const node = this.trees.get(treeId); + const children = await node?.getChildren(treeElementId) || []; + return children.map(c => new VisualizedExpression(session, this, treeId, c, undefined)); } /** @inheritdoc */ diff --git a/src/vs/workbench/contrib/debug/common/replModel.ts b/src/vs/workbench/contrib/debug/common/replModel.ts index 5e8edf783f2..e2e388eed51 100644 --- a/src/vs/workbench/contrib/debug/common/replModel.ts +++ b/src/vs/workbench/contrib/debug/common/replModel.ts @@ -76,6 +76,7 @@ export class ReplVariableElement implements INestingReplElement { private readonly id = generateUuid(); constructor( + private readonly session: IDebugSession, public readonly expression: IExpression, public readonly severity: severity, public readonly sourceData?: IReplElementSource, @@ -83,6 +84,10 @@ export class ReplVariableElement implements INestingReplElement { this.hasChildren = expression.hasChildren; } + getSession() { + return this.session; + } + getChildren(): IReplElement[] | Promise { return this.expression.getChildren(); } @@ -106,6 +111,10 @@ export class RawObjectReplElement implements IExpression, INestingReplElement { return this.id; } + getSession(): IDebugSession | undefined { + return undefined; + } + get value(): string { if (this.valueObj === null) { return 'null'; @@ -193,6 +202,7 @@ export class ReplGroup implements INestingReplElement { static COUNTER = 0; constructor( + public readonly session: IDebugSession, public name: string, public autoExpand: boolean, public sourceData?: IReplElementSource @@ -291,7 +301,7 @@ export class ReplModel { // have formatted it nicely e.g. with ANSI color codes. this.addReplElement(output ? new ReplOutputElement(session, getUniqueId(), output, sev, source, expression) - : new ReplVariableElement(expression, sev, source)); + : new ReplVariableElement(session, expression, sev, source)); return; } @@ -315,8 +325,8 @@ export class ReplModel { this.addReplElement(element); } - startGroup(name: string, autoExpand: boolean, sourceData?: IReplElementSource): void { - const group = new ReplGroup(name, autoExpand, sourceData); + startGroup(session: IDebugSession, name: string, autoExpand: boolean, sourceData?: IReplElementSource): void { + const group = new ReplGroup(session, name, autoExpand, sourceData); this.addReplElement(group); } diff --git a/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts b/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts index 09ed6d73bed..7c66831722b 100644 --- a/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/baseDebugView.test.ts @@ -9,92 +9,97 @@ import { HighlightedLabel } from '../../../../../base/browser/ui/highlightedlabe import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { isWindows } from '../../../../../base/common/platform.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { NullCommandService } from '../../../../../platform/commands/test/common/nullCommandService.js'; import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; import { NullHoverService } from '../../../../../platform/hover/test/browser/nullHoverService.js'; import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; -import { renderExpressionValue, renderVariable, renderViewTree } from '../../browser/baseDebugView.js'; -import { LinkDetector } from '../../browser/linkDetector.js'; +import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; +import { renderViewTree } from '../../browser/baseDebugView.js'; +import { DebugExpressionRenderer } from '../../browser/debugExpressionRenderer.js'; import { isStatusbarInDebugMode } from '../../browser/statusbarColorProvider.js'; import { State } from '../../common/debug.js'; import { Expression, Scope, StackFrame, Thread, Variable } from '../../common/debugModel.js'; +import { MockSession } from '../common/mockDebug.js'; import { createTestSession } from './callStack.test.js'; import { createMockDebugModel } from './mockDebugModel.js'; -import { MockSession } from '../common/mockDebug.js'; -import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; +import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; const $ = dom.$; -function assertVariable(session: MockSession, scope: Scope, disposables: Pick, linkDetector: LinkDetector, displayType: boolean) { - let variable = new Variable(session, 1, scope, 2, 'foo', 'bar.foo', undefined, 0, 0, undefined, {}, 'string'); - let expression = $('.'); - let name = $('.'); - let type = $('.'); - let value = $('.'); - const label = new HighlightedLabel(name); - const lazyButton = $('.'); - const store = disposables.add(new DisposableStore()); - renderVariable(store, NullCommandService, NullHoverService, variable, { expression, name, type, value, label, lazyButton }, false, [], undefined, displayType); - - assert.strictEqual(label.element.textContent, 'foo'); - assert.strictEqual(value.textContent, ''); - - variable.value = 'hey'; - expression = $('.'); - name = $('.'); - type = $('.'); - value = $('.'); - renderVariable(store, NullCommandService, NullHoverService, variable, { expression, name, type, value, label, lazyButton }, false, [], linkDetector, displayType); - assert.strictEqual(value.textContent, 'hey'); - assert.strictEqual(label.element.textContent, displayType ? 'foo: ' : 'foo ='); - assert.strictEqual(type.textContent, displayType ? 'string =' : ''); - - variable.value = isWindows ? 'C:\\foo.js:5' : '/foo.js:5'; - expression = $('.'); - name = $('.'); - type = $('.'); - value = $('.'); - renderVariable(store, NullCommandService, NullHoverService, variable, { expression, name, type, value, label, lazyButton }, false, [], linkDetector, displayType); - assert.ok(value.querySelector('a')); - assert.strictEqual(value.querySelector('a')!.textContent, variable.value); - - variable = new Variable(session, 1, scope, 2, 'console', 'console', '5', 0, 0, undefined, { kind: 'virtual' }); - expression = $('.'); - name = $('.'); - type = $('.'); - value = $('.'); - renderVariable(store, NullCommandService, NullHoverService, variable, { expression, name, type, value, label, lazyButton }, false, [], linkDetector, displayType); - assert.strictEqual(name.className, 'virtual'); - assert.strictEqual(label.element.textContent, 'console ='); - assert.strictEqual(value.className, 'value number'); - - variable = new Variable(session, 1, scope, 2, 'xpto', 'xpto.xpto', undefined, 0, 0, undefined, {}, 'custom-type'); - renderVariable(store, NullCommandService, NullHoverService, variable, { expression, name, type, value, label, lazyButton }, false, [], linkDetector, displayType); - assert.strictEqual(label.element.textContent, 'xpto'); - assert.strictEqual(value.textContent, ''); - variable.value = '2'; - expression = $('.'); - name = $('.'); - type = $('.'); - value = $('.'); - renderVariable(store, NullCommandService, NullHoverService, variable, { expression, name, type, value, label, lazyButton }, false, [], linkDetector, displayType); - assert.strictEqual(value.textContent, '2'); - assert.strictEqual(label.element.textContent, displayType ? 'xpto: ' : 'xpto ='); - assert.strictEqual(type.textContent, displayType ? 'custom-type =' : ''); - - label.dispose(); -} suite('Debug - Base Debug View', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); - let linkDetector: LinkDetector; + let renderer: DebugExpressionRenderer; + let configurationService: TestConfigurationService; + + function assertVariable(session: MockSession, scope: Scope, disposables: Pick, displayType: boolean) { + let variable = new Variable(session, 1, scope, 2, 'foo', 'bar.foo', undefined, 0, 0, undefined, {}, 'string'); + let expression = $('.'); + let name = $('.'); + let type = $('.'); + let value = $('.'); + const label = new HighlightedLabel(name); + const lazyButton = $('.'); + const store = disposables.add(new DisposableStore()); + store.add(renderer.renderVariable({ expression, name, type, value, label, lazyButton }, variable, { showChanged: false })); + + assert.strictEqual(label.element.textContent, 'foo'); + assert.strictEqual(value.textContent, ''); + + variable.value = 'hey'; + expression = $('.'); + name = $('.'); + type = $('.'); + value = $('.'); + store.add(renderer.renderVariable({ expression, name, type, value, label, lazyButton }, variable, { showChanged: false })); + assert.strictEqual(value.textContent, 'hey'); + assert.strictEqual(label.element.textContent, displayType ? 'foo: ' : 'foo ='); + assert.strictEqual(type.textContent, displayType ? 'string =' : ''); + + variable.value = isWindows ? 'C:\\foo.js:5' : '/foo.js:5'; + expression = $('.'); + name = $('.'); + type = $('.'); + value = $('.'); + store.add(renderer.renderVariable({ expression, name, type, value, label, lazyButton }, variable, { showChanged: false })); + assert.ok(value.querySelector('a')); + assert.strictEqual(value.querySelector('a')!.textContent, variable.value); + + variable = new Variable(session, 1, scope, 2, 'console', 'console', '5', 0, 0, undefined, { kind: 'virtual' }); + expression = $('.'); + name = $('.'); + type = $('.'); + value = $('.'); + store.add(renderer.renderVariable({ expression, name, type, value, label, lazyButton }, variable, { showChanged: false })); + assert.strictEqual(name.className, 'virtual'); + assert.strictEqual(label.element.textContent, 'console ='); + assert.strictEqual(value.className, 'value number'); + + variable = new Variable(session, 1, scope, 2, 'xpto', 'xpto.xpto', undefined, 0, 0, undefined, {}, 'custom-type'); + store.add(renderer.renderVariable({ expression, name, type, value, label, lazyButton }, variable, { showChanged: false })); + assert.strictEqual(label.element.textContent, 'xpto'); + assert.strictEqual(value.textContent, ''); + variable.value = '2'; + expression = $('.'); + name = $('.'); + type = $('.'); + value = $('.'); + store.add(renderer.renderVariable({ expression, name, type, value, label, lazyButton }, variable, { showChanged: false })); + assert.strictEqual(value.textContent, '2'); + assert.strictEqual(label.element.textContent, displayType ? 'xpto: ' : 'xpto ='); + assert.strictEqual(type.textContent, displayType ? 'custom-type =' : ''); + + label.dispose(); + } /** * Instantiate services for use by the functions being tested. */ setup(() => { const instantiationService: TestInstantiationService = workbenchInstantiationService(undefined, disposables); - linkDetector = instantiationService.createInstance(LinkDetector); + configurationService = instantiationService.createInstance(TestConfigurationService); + instantiationService.stub(IConfigurationService, configurationService); instantiationService.stub(IHoverService, NullHoverService); + renderer = instantiationService.createInstance(DebugExpressionRenderer); }); test('render view tree', () => { @@ -110,37 +115,37 @@ suite('Debug - Base Debug View', () => { test('render expression value', () => { let container = $('.container'); const store = disposables.add(new DisposableStore()); - renderExpressionValue(store, 'render \n me', container, {}, NullHoverService); + store.add(renderer.renderValue(container, 'render \n me', {})); assert.strictEqual(container.className, 'value'); assert.strictEqual(container.textContent, 'render \n me'); const expression = new Expression('console'); expression.value = 'Object'; container = $('.container'); - renderExpressionValue(store, expression, container, { colorize: true }, NullHoverService); + store.add(renderer.renderValue(container, expression, { colorize: true })); assert.strictEqual(container.className, 'value unavailable error'); expression.available = true; expression.value = '"string value"'; container = $('.container'); - renderExpressionValue(store, expression, container, { colorize: true, linkDetector }, NullHoverService); + store.add(renderer.renderValue(container, expression, { colorize: true })); assert.strictEqual(container.className, 'value string'); assert.strictEqual(container.textContent, '"string value"'); expression.type = 'boolean'; container = $('.container'); - renderExpressionValue(store, expression, container, { colorize: true }, NullHoverService); + store.add(renderer.renderValue(container, expression, { colorize: true })); assert.strictEqual(container.className, 'value boolean'); assert.strictEqual(container.textContent, expression.value); expression.value = 'this is a long string'; container = $('.container'); - renderExpressionValue(store, expression, container, { colorize: true, maxValueLength: 4, linkDetector }, NullHoverService); + store.add(renderer.renderValue(container, expression, { colorize: true, maxValueLength: 4 })); assert.strictEqual(container.textContent, 'this...'); expression.value = isWindows ? 'C:\\foo.js:5' : '/foo.js:5'; container = $('.container'); - renderExpressionValue(store, expression, container, { colorize: true, linkDetector }, NullHoverService); + store.add(renderer.renderValue(container, expression, { colorize: true })); assert.ok(container.querySelector('a')); assert.strictEqual(container.querySelector('a')!.textContent, expression.value); }); @@ -157,7 +162,8 @@ suite('Debug - Base Debug View', () => { const stackFrame = new StackFrame(thread, 1, null!, 'app.js', 'normal', range, 0, true); const scope = new Scope(stackFrame, 1, 'local', 1, false, 10, 10); - assertVariable(session, scope, disposables, linkDetector, false); + configurationService.setUserConfiguration('debug.showVariableTypes', false); + assertVariable(session, scope, disposables, false); }); @@ -173,7 +179,8 @@ suite('Debug - Base Debug View', () => { const stackFrame = new StackFrame(thread, 1, null!, 'app.js', 'normal', range, 0, true); const scope = new Scope(stackFrame, 1, 'local', 1, false, 10, 10); - assertVariable(session, scope, disposables, linkDetector, true); + configurationService.setUserConfiguration('debug.showVariableTypes', true); + assertVariable(session, scope, disposables, true); }); test('statusbar in debug mode', () => { diff --git a/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts index 6f19be34819..eb31a388b2e 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts @@ -10,16 +10,14 @@ import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { generateUuid } from '../../../../../base/common/uuid.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; -import { IThemeService } from '../../../../../platform/theme/common/themeService.js'; -import { TestColorTheme, TestThemeService } from '../../../../../platform/theme/test/common/testThemeService.js'; +import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; +import { registerColors } from '../../../terminal/common/terminalColorRegistry.js'; import { appendStylizedStringToContainer, calcANSI8bitColor, handleANSIOutput } from '../../browser/debugANSIHandling.js'; import { DebugSession } from '../../browser/debugSession.js'; import { LinkDetector } from '../../browser/linkDetector.js'; import { DebugModel } from '../../common/debugModel.js'; import { createTestSession } from './callStack.test.js'; import { createMockDebugModel } from './mockDebugModel.js'; -import { ansiColorMap, registerColors } from '../../../terminal/common/terminalColorRegistry.js'; -import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; suite('Debug - ANSI Handling', () => { @@ -27,7 +25,6 @@ suite('Debug - ANSI Handling', () => { let model: DebugModel; let session: DebugSession; let linkDetector: LinkDetector; - let themeService: IThemeService; /** * Instantiate services for use by the functions being tested. @@ -39,13 +36,6 @@ suite('Debug - ANSI Handling', () => { const instantiationService: TestInstantiationService = workbenchInstantiationService(undefined, disposables); linkDetector = instantiationService.createInstance(LinkDetector); - - const colors: { [id: string]: string } = {}; - for (const color in ansiColorMap) { - colors[color] = ansiColorMap[color].defaults.dark; - } - const testTheme = new TestColorTheme(colors); - themeService = new TestThemeService(testTheme); registerColors(); }); @@ -92,7 +82,7 @@ suite('Debug - ANSI Handling', () => { * @returns An {@link HTMLSpanElement} that contains the stylized text. */ function getSequenceOutput(sequence: string): HTMLSpanElement { - const root: HTMLSpanElement = handleANSIOutput(sequence, linkDetector, themeService, session.root); + const root: HTMLSpanElement = handleANSIOutput(sequence, linkDetector, session.root); assert.strictEqual(1, root.children.length); const child: Node = root.lastChild!; if (isHTMLSpanElement(child)) { @@ -405,7 +395,7 @@ suite('Debug - ANSI Handling', () => { if (elementsExpected === undefined) { elementsExpected = assertions.length; } - const root: HTMLSpanElement = handleANSIOutput(sequence, linkDetector, themeService, session.root); + const root: HTMLSpanElement = handleANSIOutput(sequence, linkDetector, session.root); assert.strictEqual(elementsExpected, root.children.length); for (let i = 0; i < elementsExpected; i++) { const child: Node = root.children[i]; 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 da8c447483e..7256a353905 100644 --- a/src/vs/workbench/contrib/debug/test/browser/repl.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/repl.test.ts @@ -201,7 +201,7 @@ suite('Debug - REPL', () => { const repl = new ReplModel(configurationService); repl.appendToRepl(session, { output: 'first global line', sev: severity.Info }); - repl.startGroup('group_1', true); + repl.startGroup(session, 'group_1', true); repl.appendToRepl(session, { output: 'first line in group', sev: severity.Info }); repl.appendToRepl(session, { output: 'second line in group', sev: severity.Info }); const elements = repl.getReplElements(); @@ -212,7 +212,7 @@ suite('Debug - REPL', () => { assert.strictEqual(group.hasChildren, true); assert.strictEqual(group.hasEnded, false); - repl.startGroup('group_2', false); + repl.startGroup(session, 'group_2', false); repl.appendToRepl(session, { output: 'first line in subgroup', sev: severity.Info }); repl.appendToRepl(session, { output: 'second line in subgroup', sev: severity.Info }); const children = group.getChildren(); diff --git a/src/vs/workbench/contrib/debug/test/browser/variablesView.test.ts b/src/vs/workbench/contrib/debug/test/browser/variablesView.test.ts index 2120d34641c..0ca1a84601a 100644 --- a/src/vs/workbench/contrib/debug/test/browser/variablesView.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/variablesView.test.ts @@ -8,17 +8,17 @@ import * as dom from '../../../../../base/browser/dom.js'; import { HighlightedLabel } from '../../../../../base/browser/ui/highlightedlabel/highlightedLabel.js'; import { DisposableStore } from '../../../../../base/common/lifecycle.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; -import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; -import { Scope, StackFrame, Thread, Variable } from '../../common/debugModel.js'; -import { MockDebugService, MockSession } from '../common/mockDebug.js'; -import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; import { IHoverService } from '../../../../../platform/hover/browser/hover.js'; import { NullHoverService } from '../../../../../platform/hover/test/browser/nullHoverService.js'; -import { IDebugService, IViewModel } from '../../common/debug.js'; +import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; +import { workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js'; +import { DebugExpressionRenderer } from '../../browser/debugExpressionRenderer.js'; import { VariablesRenderer } from '../../browser/variablesView.js'; -import { LinkDetector } from '../../browser/linkDetector.js'; -import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IDebugService, IViewModel } from '../../common/debug.js'; +import { Scope, StackFrame, Thread, Variable } from '../../common/debugModel.js'; +import { MockDebugService, MockSession } from '../common/mockDebug.js'; const $ = dom.$; @@ -75,18 +75,22 @@ function assertVariable(disposables: Pick, variablesRend assert.strictEqual(value.textContent, 'xpto'); assert.strictEqual(type.textContent, displayType ? 'string =' : ''); assert.strictEqual(label.element.textContent, displayType ? 'foo: ' : 'foo ='); + + variablesRenderer.disposeTemplate(data); } suite('Debug - Variable Debug View', () => { const disposables = ensureNoDisposablesAreLeakedInTestSuite(); let variablesRenderer: VariablesRenderer; let instantiationService: TestInstantiationService; - let linkDetector: LinkDetector; + let expressionRenderer: DebugExpressionRenderer; let configurationService: TestConfigurationService; setup(() => { instantiationService = workbenchInstantiationService(undefined, disposables); - linkDetector = instantiationService.createInstance(LinkDetector); + configurationService = instantiationService.createInstance(TestConfigurationService); + instantiationService.stub(IConfigurationService, configurationService); + expressionRenderer = instantiationService.createInstance(DebugExpressionRenderer); const debugService = new MockDebugService(); instantiationService.stub(IHoverService, NullHoverService); debugService.getViewModel = () => { focusedStackFrame: undefined, getSelectedExpression: () => undefined }; @@ -95,24 +99,16 @@ suite('Debug - Variable Debug View', () => { }); test('variable expressions with display type', () => { - configurationService = new TestConfigurationService({ - debug: { - showVariableTypes: true - } - }); + configurationService.setUserConfiguration('debug.showVariableTypes', true); instantiationService.stub(IConfigurationService, configurationService); - variablesRenderer = instantiationService.createInstance(VariablesRenderer, linkDetector); + variablesRenderer = instantiationService.createInstance(VariablesRenderer, expressionRenderer); assertVariable(disposables, variablesRenderer, true); }); test('variable expressions', () => { - configurationService = new TestConfigurationService({ - debug: { - showVariableTypes: false - } - }); + configurationService.setUserConfiguration('debug.showVariableTypes', false); instantiationService.stub(IConfigurationService, configurationService); - variablesRenderer = instantiationService.createInstance(VariablesRenderer, linkDetector); + variablesRenderer = instantiationService.createInstance(VariablesRenderer, expressionRenderer); assertVariable(disposables, variablesRenderer, false); }); }); diff --git a/src/vs/workbench/contrib/debug/test/browser/watchExpressionView.test.ts b/src/vs/workbench/contrib/debug/test/browser/watchExpressionView.test.ts index 3770ea6b6c5..f3e818393eb 100644 --- a/src/vs/workbench/contrib/debug/test/browser/watchExpressionView.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/watchExpressionView.test.ts @@ -18,6 +18,7 @@ import { NullHoverService } from '../../../../../platform/hover/test/browser/nul import { IDebugService, IViewModel } from '../../common/debug.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; +import { DebugExpressionRenderer } from '../../browser/debugExpressionRenderer.js'; const $ = dom.$; function assertWatchVariable(disposables: Pick, watchExpressionsRenderer: WatchExpressionsRenderer, displayType: boolean) { @@ -80,9 +81,13 @@ suite('Debug - Watch Debug View', () => { let watchExpressionsRenderer: WatchExpressionsRenderer; let instantiationService: TestInstantiationService; let configurationService: TestConfigurationService; + let expressionRenderer: DebugExpressionRenderer; setup(() => { instantiationService = workbenchInstantiationService(undefined, disposables); + configurationService = instantiationService.createInstance(TestConfigurationService); + instantiationService.stub(IConfigurationService, configurationService); + expressionRenderer = instantiationService.createInstance(DebugExpressionRenderer); const debugService = new MockDebugService(); instantiationService.stub(IHoverService, NullHoverService); debugService.getViewModel = () => { focusedStackFrame: undefined, getSelectedExpression: () => undefined }; @@ -91,24 +96,16 @@ suite('Debug - Watch Debug View', () => { }); test('watch expressions with display type', () => { - configurationService = new TestConfigurationService({ - debug: { - showVariableTypes: true - } - }); + configurationService.setUserConfiguration('debug', { showVariableTypes: true }); instantiationService.stub(IConfigurationService, configurationService); - watchExpressionsRenderer = instantiationService.createInstance(WatchExpressionsRenderer, null as any); + watchExpressionsRenderer = instantiationService.createInstance(WatchExpressionsRenderer, expressionRenderer); assertWatchVariable(disposables, watchExpressionsRenderer, true); }); test('watch expressions', () => { - configurationService = new TestConfigurationService({ - debug: { - showVariableTypes: false - } - }); + configurationService.setUserConfiguration('debug', { showVariableTypes: false }); instantiationService.stub(IConfigurationService, configurationService); - watchExpressionsRenderer = instantiationService.createInstance(WatchExpressionsRenderer, null as any); + watchExpressionsRenderer = instantiationService.createInstance(WatchExpressionsRenderer, expressionRenderer); assertWatchVariable(disposables, watchExpressionsRenderer, false); }); }); diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts b/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts index 4dc0f23670a..fba1ac91de1 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts @@ -61,8 +61,7 @@ import { ILocalizedString } from '../../../../platform/action/common/action.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { CancellationError } from '../../../../base/common/errors.js'; import { IRemoteAgentService } from '../../../services/remote/common/remoteAgentService.js'; -import { IExtensionsViewPaneContainer, VIEWLET_ID } from '../../extensions/common/extensions.js'; -import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js'; +import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; import { WorkspaceStateSynchroniser } from '../common/workspaceStateSync.js'; import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js'; import { IRequestService } from '../../../../platform/request/common/request.js'; @@ -101,10 +100,7 @@ registerAction2(class extends Action2 { } async run(accessor: ServicesAccessor): Promise { - const paneCompositePartService = accessor.get(IPaneCompositePartService); - const viewlet = await paneCompositePartService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true); - const view = viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer | undefined; - view?.search('@tag:continueOn'); + return accessor.get(IExtensionsWorkbenchService).openSearch('@tag:continueOn'); } }); diff --git a/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts index 7e7ce15578c..f6873bf5524 100644 --- a/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/abstractRuntimeExtensionsEditor.ts @@ -13,13 +13,12 @@ import { Action, IAction, Separator } from '../../../../base/common/actions.js'; import { isNonEmptyArray } from '../../../../base/common/arrays.js'; import { RunOnceScheduler } from '../../../../base/common/async.js'; import { fromNow } from '../../../../base/common/date.js'; -import { memoize } from '../../../../base/common/decorators.js'; import { IDisposable, dispose } from '../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../base/common/network.js'; -import './media/runtimeExtensionsEditor.css'; import * as nls from '../../../../nls.js'; import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; -import { Action2, MenuId } from '../../../../platform/actions/common/actions.js'; +import { createAndFillInContextMenuActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { Action2, IMenuService, MenuId } from '../../../../platform/actions/common/actions.js'; import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; @@ -35,9 +34,6 @@ import { ITelemetryService } from '../../../../platform/telemetry/common/telemet import { editorBackground } from '../../../../platform/theme/common/colorRegistry.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { EditorPane } from '../../../browser/parts/editor/editorPane.js'; -import { errorIcon, warningIcon } from './extensionsIcons.js'; -import { IExtension, IExtensionsWorkbenchService } from '../common/extensions.js'; -import { RuntimeExtensionsInput } from '../common/runtimeExtensionsInput.js'; import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; @@ -45,6 +41,10 @@ import { Extensions, IExtensionFeaturesManagementService, IExtensionFeaturesRegi import { DefaultIconPath, EnablementState } from '../../../services/extensionManagement/common/extensionManagement.js'; import { LocalWebWorkerRunningLocation } from '../../../services/extensions/common/extensionRunningLocation.js'; import { IExtensionHostProfile, IExtensionService, IExtensionsStatus } from '../../../services/extensions/common/extensions.js'; +import { IExtension, IExtensionsWorkbenchService } from '../common/extensions.js'; +import { RuntimeExtensionsInput } from '../common/runtimeExtensionsInput.js'; +import { errorIcon, warningIcon } from './extensionsIcons.js'; +import './media/runtimeExtensionsEditor.css'; interface IExtensionProfileInformation { /** @@ -81,7 +81,7 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, - @IContextKeyService contextKeyService: IContextKeyService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, @IExtensionsWorkbenchService private readonly _extensionsWorkbenchService: IExtensionsWorkbenchService, @IExtensionService private readonly _extensionService: IExtensionService, @INotificationService private readonly _notificationService: INotificationService, @@ -93,6 +93,7 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { @IClipboardService private readonly _clipboardService: IClipboardService, @IExtensionFeaturesManagementService private readonly _extensionFeaturesManagementService: IExtensionFeaturesManagementService, @IHoverService private readonly _hoverService: IHoverService, + @IMenuService private readonly _menuService: IMenuService, ) { super(AbstractRuntimeExtensionsEditor.ID, group, telemetryService, themeService, storageService); @@ -496,14 +497,9 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { } actions.push(new Separator()); - const profileAction = this._createProfileAction(); - if (profileAction) { - actions.push(profileAction); - } - const saveExtensionHostProfileAction = this.saveExtensionHostProfileAction; - if (saveExtensionHostProfileAction) { - actions.push(saveExtensionHostProfileAction); - } + + const menuActions = this._menuService.getMenuActions(MenuId.ExtensionEditorContextMenu, this.contextKeyService); + createAndFillInContextMenuActions(menuActions, { primary: [], secondary: actions }); this._contextMenuService.showContextMenu({ getAnchor: () => e.anchor, @@ -512,11 +508,6 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { }); } - @memoize - private get saveExtensionHostProfileAction(): IAction | null { - return this._createSaveExtensionHostProfileAction(); - } - public layout(dimension: Dimension): void { this._list?.layout(dimension.height); } @@ -525,8 +516,6 @@ export abstract class AbstractRuntimeExtensionsEditor extends EditorPane { protected abstract _getUnresponsiveProfile(extensionId: ExtensionIdentifier): IExtensionHostProfile | undefined; protected abstract _createSlowExtensionAction(element: IRuntimeExtension): Action | null; protected abstract _createReportExtensionIssueAction(element: IRuntimeExtension): Action | null; - protected abstract _createSaveExtensionHostProfileAction(): Action | null; - protected abstract _createProfileAction(): Action | null; } export class ShowRuntimeExtensionsAction extends Action2 { diff --git a/src/vs/workbench/contrib/extensions/browser/browserRuntimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/browser/browserRuntimeExtensionsEditor.ts index 57e32f123d4..e7fd431b5a7 100644 --- a/src/vs/workbench/contrib/extensions/browser/browserRuntimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/browserRuntimeExtensionsEditor.ts @@ -29,12 +29,4 @@ export class RuntimeExtensionsEditor extends AbstractRuntimeExtensionsEditor { } return null; } - - protected _createSaveExtensionHostProfileAction(): Action | null { - return null; - } - - protected _createProfileAction(): Action | null { - return null; - } } diff --git a/src/vs/workbench/contrib/extensions/browser/deprecatedExtensionsChecker.ts b/src/vs/workbench/contrib/extensions/browser/deprecatedExtensionsChecker.ts index e6d5b02c2da..bf3d9421e15 100644 --- a/src/vs/workbench/contrib/extensions/browser/deprecatedExtensionsChecker.ts +++ b/src/vs/workbench/contrib/extensions/browser/deprecatedExtensionsChecker.ts @@ -8,8 +8,6 @@ import { IWorkbenchContribution } from '../../../common/contributions.js'; import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { localize } from '../../../../nls.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { SearchExtensionsAction } from './extensionsActions.js'; import { distinct } from '../../../../base/common/arrays.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { IExtensionManagementService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; @@ -24,7 +22,6 @@ export class DeprecatedExtensionsChecker extends Disposable implements IWorkbenc @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, @IStorageService private readonly storageService: IStorageService, @INotificationService private readonly notificationService: INotificationService, - @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); this.checkForDeprecatedExtensions(); @@ -56,12 +53,7 @@ export class DeprecatedExtensionsChecker extends Disposable implements IWorkbenc label: localize('showDeprecated', "Show Deprecated Extensions"), run: async () => { this.setNotifiedDeprecatedExtensions(toNotify.map(e => e.identifier.id.toLowerCase())); - const action = this.instantiationService.createInstance(SearchExtensionsAction, toNotify.map(extension => `@id:${extension.identifier.id}`).join(' ')); - try { - await action.run(); - } finally { - action.dispose(); - } + await this.extensionsWorkbenchService.openSearch(toNotify.map(extension => `@id:${extension.identifier.id}`).join(' ')); } }, { label: localize('neverShowAgain', "Don't Show Again"), diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index 0aa9ed34913..a80cf76fe05 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -46,7 +46,6 @@ import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticip import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { EditorPane } from '../../../browser/parts/editor/editorPane.js'; import { IEditorOpenContext } from '../../../common/editor.js'; -import { ViewContainerLocation } from '../../../common/views.js'; import { ExtensionFeaturesTab } from './extensionFeaturesTab.js'; import { ButtonWithDropDownExtensionAction, @@ -76,7 +75,7 @@ import { import { Delegate } from './extensionsList.js'; import { ExtensionData, ExtensionsGridView, ExtensionsTree, getExtensions } from './extensionsViewer.js'; import { ExtensionRecommendationWidget, ExtensionStatusWidget, ExtensionWidget, InstallCountWidget, RatingsWidget, RemoteBadgeWidget, SponsorWidget, VerifiedPublisherWidget, onClick } from './extensionsWidgets.js'; -import { ExtensionContainers, ExtensionEditorTab, ExtensionState, IExtension, IExtensionContainer, IExtensionsViewPaneContainer, IExtensionsWorkbenchService, VIEWLET_ID } from '../common/extensions.js'; +import { ExtensionContainers, ExtensionEditorTab, ExtensionState, IExtension, IExtensionContainer, IExtensionsWorkbenchService } from '../common/extensions.js'; import { ExtensionsInput, IExtensionEditorOptions } from '../common/extensionsInput.js'; import { IExplorerService } from '../../files/browser/files.js'; import { DEFAULT_MARKDOWN_STYLES, renderMarkdownDocument } from '../../markdown/browser/markdownDocumentRenderer.js'; @@ -85,7 +84,6 @@ import { IEditorGroup } from '../../../services/editor/common/editorGroupsServic import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IExtensionRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; -import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; import { VIEW_ID as EXPLORER_VIEW_ID } from '../../files/common/files.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; @@ -240,7 +238,6 @@ export class ExtensionEditor extends EditorPane { group: IEditorGroup, @ITelemetryService telemetryService: ITelemetryService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, @IThemeService themeService: IThemeService, @@ -586,11 +583,7 @@ export class ExtensionEditor extends EditorPane { if (extension.url) { this.transientDisposables.add(onClick(template.name, () => this.openerService.open(URI.parse(extension.url!)))); this.transientDisposables.add(onClick(template.rating, () => this.openerService.open(URI.parse(`${extension.url}&ssr=false#review-details`)))); - this.transientDisposables.add(onClick(template.publisher, () => { - this.paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => viewlet.search(`publisher:"${extension.publisherDisplayName}"`)); - })); + this.transientDisposables.add(onClick(template.publisher, () => this.extensionsWorkbenchService.openSearch(`publisher:"${extension.publisherDisplayName}"`))); } const manifest = await this.extensionManifest.get().promise; @@ -944,11 +937,8 @@ export class ExtensionEditor extends EditorPane { append(categoriesContainer, $('.additional-details-title', undefined, localize('categories', "Categories"))); const categoriesElement = append(categoriesContainer, $('.categories')); for (const category of extension.categories) { - this.transientDisposables.add(onClick(append(categoriesElement, $('span.category', { tabindex: '0' }, category)), () => { - this.paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => viewlet.search(`@category:"${category}"`)); - })); + this.transientDisposables.add(onClick(append(categoriesElement, $('span.category', { tabindex: '0' }, category)), + () => this.extensionsWorkbenchService.openSearch(`@category:"${category}"`))); } } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts index 45ef74f3bd5..1fc880f2465 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationNotificationService.ts @@ -3,13 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IAction } from '../../../../base/common/actions.js'; import { distinct } from '../../../../base/common/arrays.js'; import { CancelablePromise, createCancelablePromise, Promises, raceCancellablePromises, raceCancellation, timeout } from '../../../../base/common/async.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { isCancellationError } from '../../../../base/common/errors.js'; import { Emitter, Event } from '../../../../base/common/event.js'; -import { Disposable, DisposableStore, isDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { isString } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; import { localize } from '../../../../nls.js'; @@ -17,13 +16,11 @@ import { IConfigurationService } from '../../../../platform/configuration/common import { IGalleryExtension } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { areSameExtensions } from '../../../../platform/extensionManagement/common/extensionManagementUtil.js'; import { IExtensionRecommendationNotificationService, IExtensionRecommendations, RecommendationsNotificationResult, RecommendationSource, RecommendationSourceToString } from '../../../../platform/extensionRecommendations/common/extensionRecommendations.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { INotificationHandle, INotificationService, IPromptChoice, IPromptChoiceWithMenu, NotificationPriority, Severity } from '../../../../platform/notification/common/notification.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { IUserDataSyncEnablementService, SyncResource } from '../../../../platform/userDataSync/common/userDataSync.js'; -import { SearchExtensionsAction } from './extensionsActions.js'; import { IExtension, IExtensionsWorkbenchService } from '../common/extensions.js'; import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; import { EnablementState, IWorkbenchExtensionManagementService, IWorkbenchExtensionEnablementService } from '../../../services/extensionManagement/common/extensionManagement.js'; @@ -138,7 +135,6 @@ export class ExtensionRecommendationNotificationService extends Disposable imple @IStorageService private readonly storageService: IStorageService, @INotificationService private readonly notificationService: INotificationService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IInstantiationService private readonly instantiationService: IInstantiationService, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IWorkbenchExtensionManagementService private readonly extensionManagementService: IWorkbenchExtensionManagementService, @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, @@ -282,7 +278,7 @@ export class ExtensionRecommendationNotificationService extends Disposable imple let accepted = false; const choices: (IPromptChoice | IPromptChoiceWithMenu)[] = []; const installExtensions = async (isMachineScoped: boolean) => { - this.runAction(this.instantiationService.createInstance(SearchExtensionsAction, searchValue)); + this.extensionsWorkbenchService.openSearch(searchValue); onDidInstallRecommendedExtensions(extensions); const galleryExtensions: IGalleryExtension[] = [], resourceExtensions: IExtension[] = []; for (const extension of extensions) { @@ -313,7 +309,7 @@ export class ExtensionRecommendationNotificationService extends Disposable imple for (const extension of extensions) { this.extensionsWorkbenchService.open(extension, { pinned: true }); } - this.runAction(this.instantiationService.createInstance(SearchExtensionsAction, searchValue)); + this.extensionsWorkbenchService.openSearch(searchValue); } }, { label: donotShowAgainLabel, @@ -464,16 +460,6 @@ export class ExtensionRecommendationNotificationService extends Disposable imple return result; } - private async runAction(action: IAction): Promise { - try { - await action.run(); - } finally { - if (isDisposable(action)) { - action.dispose(); - } - } - } - private addToImportantRecommendationsIgnore(id: string) { const importantRecommendationsIgnoreList = [...this.ignoredRecommendations]; if (!importantRecommendationsIgnoreList.includes(id.toLowerCase())) { diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index a531a89c251..8e68b570a90 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -8,13 +8,13 @@ import { KeyMod, KeyCode } from '../../../../base/common/keyCodes.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { MenuRegistry, MenuId, registerAction2, Action2, IMenuItem, IAction2Options } from '../../../../platform/actions/common/actions.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { ExtensionsLocalizedLabel, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, InstallOperation, EXTENSION_INSTALL_SOURCE_CONTEXT, ExtensionInstallSource } from '../../../../platform/extensionManagement/common/extensionManagement.js'; +import { ExtensionsLocalizedLabel, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, EXTENSION_INSTALL_SOURCE_CONTEXT, ExtensionInstallSource } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { EnablementState, IExtensionManagementServerService, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService, extensionsConfigurationNodeBase } from '../../../services/extensionManagement/common/extensionManagement.js'; import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from '../../../common/contributions.js'; import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; -import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, WORKSPACE_RECOMMENDATIONS_VIEW_ID, IWorkspaceRecommendedExtensionsView, AutoUpdateConfigurationKey, HasOutdatedExtensionsContext, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID, ExtensionEditorTab, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, OUTDATED_EXTENSIONS_VIEW_ID, CONTEXT_HAS_GALLERY, IExtension, extensionsSearchActionsMenu, UPDATE_ACTIONS_GROUP, IExtensionArg, ExtensionRuntimeActionType } from '../common/extensions.js'; -import { ReinstallAction, InstallSpecificVersionOfExtensionAction, ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, PromptExtensionInstallFailureAction, SearchExtensionsAction, SetColorThemeAction, SetFileIconThemeAction, SetProductIconThemeAction, ClearLanguageAction, ToggleAutoUpdateForExtensionAction, ToggleAutoUpdatesForPublisherAction, TogglePreReleaseExtensionAction, InstallAnotherVersionAction, InstallAction } from './extensionsActions.js'; +import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, WORKSPACE_RECOMMENDATIONS_VIEW_ID, IWorkspaceRecommendedExtensionsView, AutoUpdateConfigurationKey, HasOutdatedExtensionsContext, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID, ExtensionEditorTab, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, OUTDATED_EXTENSIONS_VIEW_ID, CONTEXT_HAS_GALLERY, extensionsSearchActionsMenu, UPDATE_ACTIONS_GROUP, IExtensionArg, ExtensionRuntimeActionType } from '../common/extensions.js'; +import { ReinstallAction, InstallSpecificVersionOfExtensionAction, ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, SetColorThemeAction, SetFileIconThemeAction, SetProductIconThemeAction, ClearLanguageAction, ToggleAutoUpdateForExtensionAction, ToggleAutoUpdatesForPublisherAction, TogglePreReleaseExtensionAction, InstallAnotherVersionAction, InstallAction } from './extensionsActions.js'; import { ExtensionsInput } from '../common/extensionsInput.js'; import { ExtensionEditor } from './extensionEditor.js'; import { StatusUpdater, MaliciousExtensionChecker, ExtensionsViewletViewsContribution, ExtensionsViewPaneContainer, BuiltInExtensionsContext, SearchMarketplaceExtensionsContext, RecommendedExtensionsContext, DefaultViewsContext, ExtensionsSortByContext, SearchHasTextContext } from './extensionsViewlet.js'; @@ -68,9 +68,8 @@ import { WORKSPACE_TRUST_EXTENSION_SUPPORT } from '../../../services/workspaces/ import { ExtensionsCompletionItemsProvider } from './extensionsCompletionItemsProvider.js'; import { IQuickInputService } from '../../../../platform/quickinput/common/quickInput.js'; import { Event } from '../../../../base/common/event.js'; -import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js'; import { UnsupportedExtensionsMigrationContrib } from './unsupportedExtensionsMigrationContribution.js'; -import { isWeb } from '../../../../base/common/platform.js'; +import { isLinux, isNative, isWeb } from '../../../../base/common/platform.js'; import { ExtensionStorageService } from '../../../../platform/extensionManagement/common/extensionStorage.js'; import { IStorageService } from '../../../../platform/storage/common/storage.js'; import { IStringDictionary } from '../../../../base/common/collections.js'; @@ -254,6 +253,13 @@ Registry.as(ConfigurationExtensions.Configuration) type: 'boolean', description: localize('extensionsInQuickAccess', "When enabled, extensions can be searched for via Quick Access and report issues from there."), default: true + }, + 'extensions.verifySignature': { + type: 'boolean', + description: localize('extensions.verifySignature', "When enabled, extensions are verified to be signed before getting installed."), + default: true, + scope: ConfigurationScope.APPLICATION, + included: isNative && !isLinux } } }); @@ -433,15 +439,7 @@ CommandsRegistry.registerCommand({ ] }, handler: async (accessor, query: string = '') => { - const paneCompositeService = accessor.get(IPaneCompositePartService); - const viewlet = await paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true); - - if (!viewlet) { - return; - } - - (viewlet.getViewPaneContainer() as IExtensionsViewPaneContainer).search(query); - viewlet.focus(); + return accessor.get(IExtensionsWorkbenchService).openSearch(query); } }); @@ -489,7 +487,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, @IExtensionGalleryService extensionGalleryService: IExtensionGalleryService, @IContextKeyService contextKeyService: IContextKeyService, - @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, + @IViewsService private readonly viewsService: IViewsService, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -561,7 +559,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi category: ExtensionsLocalizedLabel, f1: true, run: async (accessor: ServicesAccessor) => { - await accessor.get(IPaneCompositePartService).openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true); + await accessor.get(IExtensionsWorkbenchService).openSearch(''); } }); @@ -593,7 +591,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menuTitles: { [MenuId.EditorTitle.id]: localize('importKeyboardShortcutsFroms', "Migrate Keyboard Shortcuts from...") }, - run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@recommended:keymaps ')) + run: () => this.extensionsWorkbenchService.openSearch('@recommended:keymaps ') }); this.registerExtensionAction({ @@ -604,7 +602,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi id: MenuId.CommandPalette, when: CONTEXT_HAS_GALLERY }, - run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@recommended:languages ')) + run: () => this.extensionsWorkbenchService.openSearch('@recommended:languages ') }); this.registerExtensionAction({ @@ -624,7 +622,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi await this.extensionsWorkbenchService.checkForUpdates(); const outdated = this.extensionsWorkbenchService.outdated; if (outdated.length) { - return runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@outdated ')); + return this.extensionsWorkbenchService.openSearch('@outdated '); } else { return this.dialogService.info(localize('noUpdatesAvailable', "All extensions are up to date.")); } @@ -645,7 +643,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi }, { id: MenuId.CommandPalette, }], - run: (accessor: ServicesAccessor) => accessor.get(IExtensionsWorkbenchService).updateAutoUpdateValue(true) + run: (accessor: ServicesAccessor) => accessor.get(IExtensionsWorkbenchService).updateAutoUpdateForAllExtensions(true) }); const disableAutoUpdateWhenCondition = ContextKeyExpr.notEquals(`config.${AutoUpdateConfigurationKey}`, false); @@ -662,7 +660,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi }, { id: MenuId.CommandPalette, }], - run: (accessor: ServicesAccessor) => accessor.get(IExtensionsWorkbenchService).updateAutoUpdateValue(false) + run: (accessor: ServicesAccessor) => accessor.get(IExtensionsWorkbenchService).updateAutoUpdateForAllExtensions(false) }); this.registerExtensionAction({ @@ -688,16 +686,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi ], icon: installWorkspaceRecommendedIcon, run: async () => { - const outdated = this.extensionsWorkbenchService.outdated; - const results = await this.extensionsWorkbenchService.updateAll(); - results.forEach((result) => { - if (result.error) { - const extension: IExtension | undefined = outdated.find((extension) => areSameExtensions(extension.identifier, result.identifier)); - if (extension) { - runAction(this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, extension.latestVersion, InstallOperation.Update, result.error)); - } - } - }); + await this.extensionsWorkbenchService.updateAll(); } }); @@ -937,7 +926,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menuTitles: { [extensionsFilterSubMenu.id]: localize('featured filter', "Featured") }, - run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@featured ')) + run: () => this.extensionsWorkbenchService.openSearch('@featured ') }); this.registerExtensionAction({ @@ -956,7 +945,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menuTitles: { [extensionsFilterSubMenu.id]: localize('most popular filter', "Most Popular") }, - run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@popular ')) + run: () => this.extensionsWorkbenchService.openSearch('@popular ') }); this.registerExtensionAction({ @@ -975,7 +964,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menuTitles: { [extensionsFilterSubMenu.id]: localize('most popular recommended', "Recommended") }, - run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@recommended ')) + run: () => this.extensionsWorkbenchService.openSearch('@recommended ') }); this.registerExtensionAction({ @@ -994,7 +983,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menuTitles: { [extensionsFilterSubMenu.id]: localize('recently published filter', "Recently Published") }, - run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@recentlyPublished ')) + run: () => this.extensionsWorkbenchService.openSearch('@recentlyPublished ') }); const extensionsCategoryFilterSubMenu = new MenuId('extensionsCategoryFilterSubMenu'); @@ -1015,7 +1004,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi when: CONTEXT_HAS_GALLERY, order: index, }], - run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, `@category:"${category.toLowerCase()}"`)) + run: () => this.extensionsWorkbenchService.openSearch(`@category:"${category.toLowerCase()}"`) }); }); @@ -1034,7 +1023,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menuTitles: { [extensionsFilterSubMenu.id]: localize('builtin filter', "Built-in") }, - run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@builtin ')) + run: () => this.extensionsWorkbenchService.openSearch('@builtin ') }); this.registerExtensionAction({ @@ -1052,7 +1041,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menuTitles: { [extensionsFilterSubMenu.id]: localize('extension updates filter', "Updates") }, - run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@updates')) + run: () => this.extensionsWorkbenchService.openSearch('@updates') }); this.registerExtensionAction({ @@ -1071,7 +1060,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menuTitles: { [extensionsFilterSubMenu.id]: localize('workspace unsupported filter', "Workspace Unsupported") }, - run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@workspaceUnsupported')) + run: () => this.extensionsWorkbenchService.openSearch('@workspaceUnsupported') }); this.registerExtensionAction({ @@ -1089,7 +1078,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menuTitles: { [extensionsFilterSubMenu.id]: localize('enabled filter', "Enabled") }, - run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@enabled ')) + run: () => this.extensionsWorkbenchService.openSearch('@enabled ') }); this.registerExtensionAction({ @@ -1107,7 +1096,7 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi menuTitles: { [extensionsFilterSubMenu.id]: localize('disabled filter', "Disabled") }, - run: () => runAction(this.instantiationService.createInstance(SearchExtensionsAction, '@disabled ')) + run: () => this.extensionsWorkbenchService.openSearch('@disabled ') }); const extensionsSortSubMenu = new MenuId('extensionsSortSubMenu'); @@ -1137,11 +1126,10 @@ class ExtensionsContributions extends Disposable implements IWorkbenchContributi }], toggled: ExtensionsSortByContext.isEqualTo(id), run: async () => { - const viewlet = await this.paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true); - const extensionsViewPaneContainer = viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer; - const currentQuery = Query.parse(extensionsViewPaneContainer.searchValue || ''); - extensionsViewPaneContainer.search(new Query(currentQuery.value, id).toString()); - extensionsViewPaneContainer.focus(); + const extensionsViewPaneContainer = ((await this.viewsService.openViewContainer(VIEWLET_ID, true))?.getViewPaneContainer()) as IExtensionsViewPaneContainer | undefined; + const currentQuery = Query.parse(extensionsViewPaneContainer?.searchValue ?? ''); + extensionsViewPaneContainer?.search(new Query(currentQuery.value, id).toString()); + extensionsViewPaneContainer?.focus(); } }); }); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 0e6b8794df2..fd381622bfd 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -12,7 +12,7 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import * as json from '../../../../base/common/json.js'; import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { disposeIfDisposable } from '../../../../base/common/lifecycle.js'; -import { IExtension, ExtensionState, IExtensionsWorkbenchService, VIEWLET_ID, IExtensionsViewPaneContainer, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, UPDATE_ACTIONS_GROUP, ExtensionEditorTab, ExtensionRuntimeActionType, IExtensionArg, AutoUpdateConfigurationKey } from '../common/extensions.js'; +import { IExtension, ExtensionState, IExtensionsWorkbenchService, IExtensionContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, UPDATE_ACTIONS_GROUP, ExtensionEditorTab, ExtensionRuntimeActionType, IExtensionArg, AutoUpdateConfigurationKey } from '../common/extensions.js'; import { ExtensionsConfigurationInitialContent } from '../common/extensionsFileTemplate.js'; import { IGalleryExtension, IExtensionGalleryService, ILocalExtension, InstallOptions, InstallOperation, TargetPlatformToString, ExtensionManagementErrorCode } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService } from '../../../services/extensionManagement/common/extensionManagement.js'; @@ -60,8 +60,6 @@ import { IExtensionManifestPropertiesService } from '../../../services/extension import { IWorkspaceTrustEnablementService, IWorkspaceTrustManagementService } from '../../../../platform/workspace/common/workspaceTrust.js'; import { isVirtualWorkspace } from '../../../../platform/workspace/common/virtualWorkspace.js'; import { escapeMarkdownSyntaxTokens, IMarkdownString, MarkdownString } from '../../../../base/common/htmlContent.js'; -import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js'; -import { ViewContainerLocation } from '../../../common/views.js'; import { fromNow } from '../../../../base/common/date.js'; import { IPreferencesService } from '../../../services/preferences/common/preferences.js'; import { getLocale } from '../../../../platform/languagePacks/common/languagePacks.js'; @@ -78,6 +76,7 @@ export class PromptExtensionInstallFailureAction extends Action { constructor( private readonly extension: IExtension, + private readonly options: InstallOptions | undefined, private readonly version: string, private readonly installOperation: InstallOperation, private readonly error: Error, @@ -139,19 +138,21 @@ export class PromptExtensionInstallFailureAction extends Action { return; } + if (ExtensionManagementErrorCode.Signature === (this.error.name)) { await this.dialogService.prompt({ type: 'error', - message: localize('signature verification failed', "{0} cannot verify the '{1}' extension. Are you sure you want to install it?", this.productService.nameLong, this.extension.displayName || this.extension.identifier.id), + message: localize('signature verification failed', "Signature of '{0}' extension could not be verified. Are you sure you want to install?", this.extension.displayName), + detail: getErrorMessage(this.error), buttons: [{ label: localize('install anyway', "Install Anyway"), run: () => { - const installAction = this.instantiationService.createInstance(InstallAction, { donotVerifySignature: true }); + const installAction = this.instantiationService.createInstance(InstallAction, { ...this.options, donotVerifySignature: true, }); installAction.extension = this.extension; return installAction.run(); } }], - cancelButton: localize('cancel', "Cancel") + cancelButton: true }); return; } @@ -554,7 +555,7 @@ export class InstallAction extends ExtensionAction { try { return await this.extensionsWorkbenchService.install(extension, this.options); } catch (error) { - await this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, extension.latestVersion, InstallOperation.Install, error).run(); + await this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, this.options, extension.latestVersion, InstallOperation.Install, error).run(); return undefined; } } @@ -947,11 +948,12 @@ export class UpdateAction extends ExtensionAction { } private async install(extension: IExtension): Promise { + const options = extension.local?.preRelease ? { installPreReleaseVersion: true } : undefined; try { - await this.extensionsWorkbenchService.install(extension, extension.local?.preRelease ? { installPreReleaseVersion: true } : undefined); + await this.extensionsWorkbenchService.install(extension, options); alert(localize('updateExtensionComplete', "Updating extension {0} to version {1} completed.", extension.displayName, extension.latestVersion)); } catch (err) { - this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, extension.latestVersion, InstallOperation.Update, err).run(); + this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, options, extension.latestVersion, InstallOperation.Update, err).run(); } } } @@ -1503,10 +1505,11 @@ export class InstallAnotherVersionAction extends ExtensionAction { if (this.extension.local?.manifest.version === pick.id) { return; } + const options = { installPreReleaseVersion: pick.isPreReleaseVersion, version: pick.id }; try { - await this.extensionsWorkbenchService.install(this.extension, { installPreReleaseVersion: pick.isPreReleaseVersion, version: pick.id }); + await this.extensionsWorkbenchService.install(this.extension, options); } catch (error) { - this.instantiationService.createInstance(PromptExtensionInstallFailureAction, this.extension, pick.id, InstallOperation.Install, error).run(); + this.instantiationService.createInstance(PromptExtensionInstallFailureAction, this.extension, options, pick.id, InstallOperation.Install, error).run(); } } return null; @@ -2015,7 +2018,6 @@ export class ShowRecommendedExtensionAction extends Action { constructor( extensionId: string, - @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, @IExtensionsWorkbenchService private readonly extensionWorkbenchService: IExtensionsWorkbenchService, ) { super(ShowRecommendedExtensionAction.ID, ShowRecommendedExtensionAction.LABEL, undefined, false); @@ -2023,10 +2025,7 @@ export class ShowRecommendedExtensionAction extends Action { } override async run(): Promise { - const paneComposite = await this.paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true); - const paneContainer = paneComposite?.getViewPaneContainer() as IExtensionsViewPaneContainer; - paneContainer.search(`@id:${this.extensionId}`); - paneContainer.focus(); + await this.extensionWorkbenchService.openSearch(`@id:${this.extensionId}`); const [extension] = await this.extensionWorkbenchService.getExtensions([{ id: this.extensionId }], { source: 'install-recommendation' }, CancellationToken.None); if (extension) { return this.extensionWorkbenchService.open(extension); @@ -2044,7 +2043,6 @@ export class InstallRecommendedExtensionAction extends Action { constructor( extensionId: string, - @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IExtensionsWorkbenchService private readonly extensionWorkbenchService: IExtensionsWorkbenchService, ) { @@ -2053,17 +2051,14 @@ export class InstallRecommendedExtensionAction extends Action { } override async run(): Promise { - const viewlet = await this.paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true); - const viewPaneContainer = viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer; - viewPaneContainer.search(`@id:${this.extensionId}`); - viewPaneContainer.focus(); + await this.extensionWorkbenchService.openSearch(`@id:${this.extensionId}`); const [extension] = await this.extensionWorkbenchService.getExtensions([{ id: this.extensionId }], { source: 'install-recommendation' }, CancellationToken.None); if (extension) { await this.extensionWorkbenchService.open(extension); try { await this.extensionWorkbenchService.install(extension); } catch (err) { - this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, extension.latestVersion, InstallOperation.Install, err).run(); + this.instantiationService.createInstance(PromptExtensionInstallFailureAction, extension, undefined, extension.latestVersion, InstallOperation.Install, err).run(); } } } @@ -2115,22 +2110,6 @@ export class UndoIgnoreExtensionRecommendationAction extends Action { } } -export class SearchExtensionsAction extends Action { - - constructor( - private readonly searchValue: string, - @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService - ) { - super('extensions.searchExtensions', localize('search recommendations', "Search Extensions"), undefined, true); - } - - override async run(): Promise { - const viewPaneContainer = (await this.paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true))?.getViewPaneContainer() as IExtensionsViewPaneContainer; - viewPaneContainer.search(this.searchValue); - viewPaneContainer.focus(); - } -} - export abstract class AbstractConfigureRecommendedExtensionsAction extends Action { constructor( @@ -2358,7 +2337,13 @@ export class ExtensionStatusLabelAction extends Action implements IExtensionCont if (currentStatus !== null) { if (currentStatus === ExtensionState.Installing && this.status === ExtensionState.Installed) { - return canAddExtension() ? this.initialStatus === ExtensionState.Installed && this.version !== currentVersion ? localize('updated', "Updated") : localize('installed', "Installed") : null; + if (this.initialStatus === ExtensionState.Uninstalled && canAddExtension()) { + return localize('installed', "Installed"); + } + if (this.initialStatus === ExtensionState.Installed && this.version !== currentVersion && canAddExtension()) { + return localize('updated', "Updated"); + } + return null; } if (currentStatus === ExtensionState.Uninstalling && this.status === ExtensionState.Uninstalled) { this.initialStatus = this.status; @@ -2571,10 +2556,11 @@ export class ExtensionStatusAction extends ExtensionAction { } } - // Extension is disabled by untrusted workspace - if (this.extension.enablementState === EnablementState.DisabledByTrustRequirement || - // All disabled dependencies of the extension are disabled by untrusted workspace - (this.extension.enablementState === EnablementState.DisabledByExtensionDependency && this.workbenchExtensionEnablementService.getDependenciesEnablementStates(this.extension.local).every(([, enablementState]) => this.workbenchExtensionEnablementService.isEnabledEnablementState(enablementState) || enablementState === EnablementState.DisabledByTrustRequirement))) { + if (!this.workspaceTrustService.isWorkspaceTrusted() && + // Extension is disabled by untrusted workspace + (this.extension.enablementState === EnablementState.DisabledByTrustRequirement || + // All disabled dependencies of the extension are disabled by untrusted workspace + (this.extension.enablementState === EnablementState.DisabledByExtensionDependency && this.workbenchExtensionEnablementService.getDependenciesEnablementStates(this.extension.local).every(([, enablementState]) => this.workbenchExtensionEnablementService.isEnabledEnablementState(enablementState) || enablementState === EnablementState.DisabledByTrustRequirement)))) { this.enabled = true; const untrustedDetails = getWorkspaceSupportTypeMessage(this.extension.local.manifest.capabilities?.untrustedWorkspaces); this.updateStatus({ icon: trustIcon, message: new MarkdownString(untrustedDetails ? escapeMarkdownSyntaxTokens(untrustedDetails) : localize('extension disabled because of trust requirement', "This extension has been disabled because the current workspace is not trusted.")) }, true); @@ -2678,7 +2664,17 @@ export class ExtensionStatusAction extends ExtensionAction { // Extension is disabled by its dependency if (this.extension.enablementState === EnablementState.DisabledByExtensionDependency) { - this.updateStatus({ icon: warningIcon, message: new MarkdownString(localize('extension disabled because of dependency', "This extension has been disabled because it depends on an extension that is disabled.")) }, true); + this.updateStatus({ + icon: warningIcon, + message: new MarkdownString(localize('extension disabled because of dependency', "This extension depends on an extension that is disabled.")) + .appendMarkdown(` [${localize('dependencies', "Show Dependencies")}](${URI.parse(`command:extension.open?${encodeURIComponent(JSON.stringify([this.extension.identifier.id, ExtensionEditorTab.Dependencies]))}`)})`) + }, true); + return; + } + + if (!this.extension.local.isValid) { + const errors = this.extension.local.validations.filter(([severity]) => severity === Severity.Error).map(([, message]) => message); + this.updateStatus({ icon: warningIcon, message: new MarkdownString(errors.join(' ').trim()) }, true); return; } @@ -2711,12 +2707,6 @@ export class ExtensionStatusAction extends ExtensionAction { return; } } - - if (isEnabled && !isRunning && !this.extension.local.isValid) { - const errors = this.extension.local.validations.filter(([severity]) => severity === Severity.Error).map(([, message]) => message); - this.updateStatus({ icon: errorIcon, message: new MarkdownString(errors.join(' ').trim()) }, true); - } - } private updateStatus(status: ExtensionStatus | undefined, updateClass: boolean): void { @@ -2785,7 +2775,6 @@ export class ReinstallAction extends Action { @IQuickInputService private readonly quickInputService: IQuickInputService, @INotificationService private readonly notificationService: INotificationService, @IHostService private readonly hostService: IHostService, - @IInstantiationService private readonly instantiationService: IInstantiationService, @IExtensionService private readonly extensionService: IExtensionService ) { super(id, label); @@ -2818,7 +2807,7 @@ export class ReinstallAction extends Action { } private reinstallExtension(extension: IExtension): Promise { - return this.instantiationService.createInstance(SearchExtensionsAction, '@installed ').run() + return this.extensionsWorkbenchService.openSearch('@installed ') .then(() => { return this.extensionsWorkbenchService.reinstall(extension) .then(extension => { @@ -2864,7 +2853,7 @@ export class InstallSpecificVersionOfExtensionAction extends Action { if (extensionPick && extensionPick.extension) { const action = this.instantiationService.createInstance(InstallAnotherVersionAction, extensionPick.extension, true); await action.run(); - await this.instantiationService.createInstance(SearchExtensionsAction, extensionPick.extension.identifier.id).run(); + await this.extensionsWorkbenchService.openSearch(extensionPick.extension.identifier.id); } } @@ -3106,29 +3095,14 @@ export class InstallRemoteExtensionsInLocalAction extends AbstractInstallExtensi } CommandsRegistry.registerCommand('workbench.extensions.action.showExtensionsForLanguage', function (accessor: ServicesAccessor, fileExtension: string) { - const paneCompositeService = accessor.get(IPaneCompositePartService); - - return paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search(`ext:${fileExtension.replace(/^\./, '')}`); - viewlet.focus(); - }); + const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); + return extensionsWorkbenchService.openSearch(`ext:${fileExtension.replace(/^\./, '')}`); }); export const showExtensionsWithIdsCommandId = 'workbench.extensions.action.showExtensionsWithIds'; CommandsRegistry.registerCommand(showExtensionsWithIdsCommandId, function (accessor: ServicesAccessor, extensionIds: string[]) { - const paneCompositeService = accessor.get(IPaneCompositePartService); - - return paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - const query = extensionIds - .map(id => `@id:${id}`) - .join(' '); - viewlet.search(query); - viewlet.focus(); - }); + const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); + return extensionsWorkbenchService.openSearch(extensionIds.map(id => `@id:${id}`).join(' ')); }); registerColor('extensionButton.background', { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsQuickAccess.ts b/src/vs/workbench/contrib/extensions/browser/extensionsQuickAccess.ts index 23eb15336fe..05166b41268 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsQuickAccess.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsQuickAccess.ts @@ -7,20 +7,18 @@ import { IQuickPickSeparator } from '../../../../platform/quickinput/common/quic import { IPickerQuickAccessItem, PickerQuickAccessProvider } from '../../../../platform/quickinput/browser/pickerQuickAccess.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { localize } from '../../../../nls.js'; -import { VIEWLET_ID, IExtensionsViewPaneContainer } from '../common/extensions.js'; import { IExtensionGalleryService, IExtensionManagementService, IGalleryExtension } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { DisposableStore } from '../../../../base/common/lifecycle.js'; -import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js'; -import { ViewContainerLocation } from '../../../common/views.js'; +import { IExtensionsWorkbenchService } from '../common/extensions.js'; export class InstallExtensionQuickAccessProvider extends PickerQuickAccessProvider { static PREFIX = 'ext install '; constructor( - @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @IExtensionManagementService private readonly extensionsService: IExtensionManagementService, @INotificationService private readonly notificationService: INotificationService, @@ -40,7 +38,7 @@ export class InstallExtensionQuickAccessProvider extends PickerQuickAccessProvid const genericSearchPickItem: IPickerQuickAccessItem = { label: localize('searchFor', "Press Enter to search for extension '{0}'.", filter), - accept: () => this.searchExtension(filter) + accept: () => this.extensionsWorkbenchService.openSearch(filter) }; // Extension ID typed: try to find it @@ -80,37 +78,26 @@ export class InstallExtensionQuickAccessProvider extends PickerQuickAccessProvid private async installExtension(extension: IGalleryExtension, name: string): Promise { try { - await openExtensionsViewlet(this.paneCompositeService, `@id:${name}`); + await this.extensionsWorkbenchService.openSearch(`@id:${name}`); await this.extensionsService.installFromGallery(extension); } catch (error) { this.notificationService.error(error); } } - - private async searchExtension(name: string): Promise { - openExtensionsViewlet(this.paneCompositeService, name); - } } export class ManageExtensionsQuickAccessProvider extends PickerQuickAccessProvider { static PREFIX = 'ext '; - constructor(@IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService) { + constructor(@IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService) { super(ManageExtensionsQuickAccessProvider.PREFIX); } protected _getPicks(): Array { return [{ label: localize('manage', "Press Enter to manage your extensions."), - accept: () => openExtensionsViewlet(this.paneCompositeService) + accept: () => this.extensionsWorkbenchService.openSearch('') }]; } } - -async function openExtensionsViewlet(paneCompositeService: IPaneCompositePartService, search = ''): Promise { - const viewlet = await paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true); - const view = viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer | undefined; - view?.search(search); - view?.focus(); -} diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index e99f31a59ec..9666c2c5ed6 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -9,10 +9,10 @@ import { timeout, Delayer, Promises } from '../../../../base/common/async.js'; import { isCancellationError } from '../../../../base/common/errors.js'; import { createErrorWithActions } from '../../../../base/common/errorMessage.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; -import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; import { Event } from '../../../../base/common/event.js'; import { Action } from '../../../../base/common/actions.js'; -import { append, $, Dimension, hide, show, DragAndDropObserver, trackFocus } from '../../../../base/browser/dom.js'; +import { append, $, Dimension, hide, show, DragAndDropObserver, trackFocus, addDisposableListener, EventType, clearNode } from '../../../../base/browser/dom.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; @@ -25,7 +25,7 @@ import { ExtensionsListView, EnabledExtensionsView, DisabledExtensionsView, Reco import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; import Severity from '../../../../base/common/severity.js'; -import { IActivityService, NumberBadge } from '../../../services/activity/common/activity.js'; +import { IActivityService, IBadge, NumberBadge, WarningBadge } from '../../../services/activity/common/activity.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IViewsRegistry, IViewDescriptor, Extensions, ViewContainer, IViewDescriptorService, IAddedViewDescriptorRef, ViewContainerLocation } from '../../../common/views.js'; @@ -64,6 +64,9 @@ import { ILocalizedString } from '../../../../platform/action/common/action.js'; import { registerNavigableContainer } from '../../../browser/actions/widgetNavigationCommands.js'; import { MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; import { createActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; +import { SeverityIcon } from '../../../../platform/severityIcon/browser/severityIcon.js'; +import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; +import { KeyCode } from '../../../../base/common/keyCodes.js'; export const DefaultViewsContext = new RawContextKey('defaultExtensionViews', true); export const ExtensionsSortByContext = new RawContextKey('extensionsSortByValue', ''); @@ -494,7 +497,9 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE private searchDelayer: Delayer; private root: HTMLElement | undefined; + private header: HTMLElement | undefined; private searchBox: SuggestEnabledInput | undefined; + private notificationContainer: HTMLElement | undefined; private readonly searchViewletState: MementoObject; constructor( @@ -557,12 +562,12 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE overlay.style.backgroundColor = overlayBackgroundColor; hide(overlay); - const header = append(this.root, $('.header')); + this.header = append(this.root, $('.header')); const placeholder = localize('searchExtensions', "Search Extensions in Marketplace"); const searchValue = this.searchViewletState['query.value'] ? this.searchViewletState['query.value'] : ''; - const searchContainer = append(header, $('.extensions-search-container')); + const searchContainer = append(this.header, $('.extensions-search-container')); this.searchBox = this._register(this.instantiationService.createInstance(SuggestEnabledInput, `${VIEWLET_ID}.searchbox`, searchContainer, { triggerCharacters: ['@'], @@ -575,6 +580,10 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE provideResults: (query: string) => Query.suggestions(query) }, placeholder, 'extensions:searchinput', { placeholderText: placeholder, value: searchValue })); + this.notificationContainer = append(this.header, $('.notification-container.hidden', { 'tabindex': '0' })); + this.renderNotificaiton(); + this._register(this.extensionsWorkbenchService.onDidChangeExtensionsNotification(() => this.renderNotificaiton())); + this.updateInstalledExtensionsContexts(); if (this.searchBox.getValue()) { this.triggerSearch(); @@ -657,13 +666,18 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this.searchBox?.focus(); } + private _dimension: Dimension | undefined; override layout(dimension: Dimension): void { + this._dimension = dimension; if (this.root) { this.root.classList.toggle('narrow', dimension.width <= 250); this.root.classList.toggle('mini', dimension.width <= 200); } this.searchBox?.layout(new Dimension(dimension.width - 34 - /*padding*/8 - (24 * 2), 20)); - super.layout(new Dimension(dimension.width, dimension.height - 41)); + const searchBoxHeight = 20 + 21 /*margin*/; + const headerHeight = this.header && !!this.notificationContainer?.childNodes.length ? this.notificationContainer.clientHeight + searchBoxHeight + 10 /*margin*/ : searchBoxHeight; + this.header!.style.height = `${headerHeight}px`; + super.layout(new Dimension(dimension.width, dimension.height - headerHeight)); } override getOptimalWidth(): number { @@ -684,6 +698,46 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE } } + private readonly notificationDisposables = this._register(new MutableDisposable()); + private renderNotificaiton(): void { + if (!this.notificationContainer) { + return; + } + + clearNode(this.notificationContainer); + this.notificationDisposables.value = new DisposableStore(); + const status = this.extensionsWorkbenchService.getExtensionsNotification(); + const query = status?.extensions.map(extension => `@id:${extension.identifier.id}`).join(' '); + if (status && (query === this.searchBox?.getValue() || !this.searchMarketplaceExtensionsContextKey.get())) { + this.notificationContainer.setAttribute('aria-label', status.message); + this.notificationContainer.classList.remove('hidden'); + const messageContainer = append(this.notificationContainer, $('.message-container')); + append(messageContainer, $('span')).className = SeverityIcon.className(status.severity); + append(messageContainer, $('span.message', undefined, status.message)); + const showAction = append(messageContainer, + $('span.message-text-action', { + 'tabindex': '0', + 'role': 'button', + 'aria-label': `${status.message}. ${localize('click show', "Click to Show")}` + }, localize('show', "Show"))); + this.notificationDisposables.value.add(addDisposableListener(showAction, EventType.CLICK, () => this.search(query ?? ''))); + this.notificationDisposables.value.add(addDisposableListener(showAction, EventType.KEY_DOWN, (e: KeyboardEvent) => { + const standardKeyboardEvent = new StandardKeyboardEvent(e); + if (standardKeyboardEvent.keyCode === KeyCode.Enter || standardKeyboardEvent.keyCode === KeyCode.Space) { + this.search(query ?? ''); + } + standardKeyboardEvent.stopPropagation(); + })); + } else { + this.notificationContainer.removeAttribute('aria-label'); + this.notificationContainer.classList.add('hidden'); + } + + if (this._dimension) { + this.layout(this._dimension); + } + } + private async updateInstalledExtensionsContexts(): Promise { const result = await this.extensionsWorkbenchService.queryLocal(); this.hasInstalledExtensionsContextKey.set(result.some(r => !r.isBuiltin)); @@ -737,6 +791,8 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE this.defaultViewsContextKey.set(!value || ExtensionsListView.isSortInstalledExtensionsQuery(value)); }); + this.renderNotificaiton(); + return this.progress(Promise.all(this.panes.map(view => (view).show(this.normalizedQuery(), refresh) .then(model => this.alertSearchResult(model.length, view.id)) @@ -851,27 +907,40 @@ export class StatusUpdater extends Disposable implements IWorkbenchContribution ) { super(); this.onServiceChange(); - this._register(Event.debounce(extensionsWorkbenchService.onChange, () => undefined, 100, undefined, undefined, undefined, this._store)(this.onServiceChange, this)); + this._register(Event.any(Event.debounce(extensionsWorkbenchService.onChange, () => undefined, 100, undefined, undefined, undefined, this._store), extensionsWorkbenchService.onDidChangeExtensionsNotification)(this.onServiceChange, this)); } private onServiceChange(): void { this.badgeHandle.clear(); + let badge: IBadge | undefined; - const actionRequired = this.configurationService.getValue(AutoRestartConfigurationKey) === true ? [] : this.extensionsWorkbenchService.installed.filter(e => e.runtimeState !== undefined); - const outdated = this.extensionsWorkbenchService.outdated.reduce((r, e) => r + (this.extensionEnablementService.isEnabled(e.local!) && !actionRequired.includes(e) ? 1 : 0), 0); - const newBadgeNumber = outdated + actionRequired.length; - if (newBadgeNumber > 0) { - let msg = ''; - if (outdated) { - msg += outdated === 1 ? localize('extensionToUpdate', '{0} requires update', outdated) : localize('extensionsToUpdate', '{0} require update', outdated); + const extensionsNotification = this.extensionsWorkbenchService.getExtensionsNotification(); + if (extensionsNotification) { + if (extensionsNotification.severity === Severity.Warning) { + badge = new WarningBadge(() => extensionsNotification.message); } - if (outdated > 0 && actionRequired.length > 0) { - msg += ', '; + } + + else { + const actionRequired = this.configurationService.getValue(AutoRestartConfigurationKey) === true ? [] : this.extensionsWorkbenchService.installed.filter(e => e.runtimeState !== undefined); + const outdated = this.extensionsWorkbenchService.outdated.reduce((r, e) => r + (this.extensionEnablementService.isEnabled(e.local!) && !actionRequired.includes(e) ? 1 : 0), 0); + const newBadgeNumber = outdated + actionRequired.length; + if (newBadgeNumber > 0) { + let msg = ''; + if (outdated) { + msg += outdated === 1 ? localize('extensionToUpdate', '{0} requires update', outdated) : localize('extensionsToUpdate', '{0} require update', outdated); + } + if (outdated > 0 && actionRequired.length > 0) { + msg += ', '; + } + if (actionRequired.length) { + msg += actionRequired.length === 1 ? localize('extensionToReload', '{0} requires restart', actionRequired.length) : localize('extensionsToReload', '{0} require restart', actionRequired.length); + } + badge = new NumberBadge(newBadgeNumber, () => msg); } - if (actionRequired.length) { - msg += actionRequired.length === 1 ? localize('extensionToReload', '{0} requires restart', actionRequired.length) : localize('extensionsToReload', '{0} require restart', actionRequired.length); - } - const badge = new NumberBadge(newBadgeNumber, () => msg); + } + + if (badge) { this.badgeHandle.value = this.activityService.showViewContainerActivity(VIEWLET_ID, { badge }); } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index a36f9be88bb..32bb10a91bc 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -552,11 +552,24 @@ export class ExtensionsListView extends ViewPane { return isE1Running ? -1 : 1; }; + const incompatible: IExtension[] = []; + const missingDeps: IExtension[] = []; + const deprecated: IExtension[] = []; const outdated: IExtension[] = []; const actionRequired: IExtension[] = []; const noActionRequired: IExtension[] = []; - result.forEach(e => { - if (e.outdated) { + + for (const e of result) { + if (e.enablementState === EnablementState.DisabledByInvalidExtension) { + incompatible.push(e); + } + else if (e.enablementState === EnablementState.DisabledByExtensionDependency) { + missingDeps.push(e); + } + else if (e.deprecationInfo) { + deprecated.push(e); + } + else if (e.outdated) { outdated.push(e); } else if (e.runtimeState) { @@ -565,9 +578,16 @@ export class ExtensionsListView extends ViewPane { else { noActionRequired.push(e); } - }); + } - result = [...outdated.sort(defaultSort), ...actionRequired.sort(defaultSort), ...noActionRequired.sort(defaultSort)]; + result = [ + ...incompatible.sort(defaultSort), + ...missingDeps.sort(defaultSort), + ...deprecated.sort(defaultSort), + ...outdated.sort(defaultSort), + ...actionRequired.sort(defaultSort), + ...noActionRequired.sort(defaultSort) + ]; } return result; } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts index 564e080f583..417c7bba2a7 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWidgets.ts @@ -10,7 +10,7 @@ import { IExtension, IExtensionsWorkbenchService, IExtensionContainer, Extension import { append, $, reset, addDisposableListener, EventType, finalHandler } from '../../../../base/browser/dom.js'; import * as platform from '../../../../base/common/platform.js'; import { localize } from '../../../../nls.js'; -import { EnablementState, IExtensionManagementServerService } from '../../../services/extensionManagement/common/extensionManagement.js'; +import { IExtensionManagementServerService } from '../../../services/extensionManagement/common/extensionManagement.js'; import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js'; import { ILabelService } from '../../../../platform/label/common/label.js'; import { extensionButtonProminentBackground, ExtensionStatusAction } from './extensionsActions.js'; @@ -501,7 +501,7 @@ export class ExtensionActivationStatusWidget extends ExtensionWidget { return; } - const extensionStatus = this.extensionsWorkbenchService.getExtensionStatus(this.extension); + const extensionStatus = this.extensionsWorkbenchService.getExtensionRuntimeStatus(this.extension); if (!extensionStatus || !extensionStatus.activationTimes) { return; } @@ -647,7 +647,7 @@ export class ExtensionHoverWidget extends ExtensionWidget { } const preReleaseMessage = ExtensionHoverWidget.getPreReleaseMessage(this.extension); - const extensionRuntimeStatus = this.extensionsWorkbenchService.getExtensionStatus(this.extension); + const extensionRuntimeStatus = this.extensionsWorkbenchService.getExtensionRuntimeStatus(this.extension); const extensionStatus = this.extensionStatusAction.status; const runtimeState = this.extension.runtimeState; const recommendationMessage = this.getRecommendationMessage(this.extension); @@ -683,9 +683,6 @@ export class ExtensionHoverWidget extends ExtensionWidget { markdown.appendMarkdown(`$(${status.icon.id}) `); } markdown.appendMarkdown(status.message.value); - if (this.extension.enablementState === EnablementState.DisabledByExtensionDependency && this.extension.local) { - markdown.appendMarkdown(` [${localize('dependencies', "Show Dependencies")}](${URI.parse(`command:extension.open?${encodeURIComponent(JSON.stringify([this.extension.identifier.id, ExtensionEditorTab.Dependencies]))}`)})`); - } markdown.appendText(`\n`); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 183f43daa23..1b07ed56148 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -25,7 +25,7 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IHostService } from '../../../services/host/browser/host.js'; import { URI } from '../../../../base/common/uri.js'; -import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey, HasOutdatedExtensionsContext, AutoUpdateConfigurationValue, InstallExtensionOptions, ExtensionRuntimeState, ExtensionRuntimeActionType, AutoRestartConfigurationKey } from '../common/extensions.js'; +import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey, HasOutdatedExtensionsContext, AutoUpdateConfigurationValue, InstallExtensionOptions, ExtensionRuntimeState, ExtensionRuntimeActionType, AutoRestartConfigurationKey, VIEWLET_ID, IExtensionsViewPaneContainer, IExtensionsNotification } from '../common/extensions.js'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from '../../../services/editor/common/editorService.js'; import { IURLService, IURLHandler, IOpenURLOptions } from '../../../../platform/url/common/url.js'; import { ExtensionsInput, IExtensionEditorOptions } from '../common/extensionsInput.js'; @@ -45,7 +45,7 @@ import { IUserDataAutoSyncService, IUserDataSyncEnablementService, SyncResource import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { isBoolean, isDefined, isString, isUndefined } from '../../../../base/common/types.js'; import { IExtensionManifestPropertiesService } from '../../../services/extensions/common/extensionManifestPropertiesService.js'; -import { IExtensionService, IExtensionsStatus, toExtension, toExtensionDescription } from '../../../services/extensions/common/extensions.js'; +import { IExtensionService, IExtensionsStatus as IExtensionRuntimeStatus, toExtension, toExtensionDescription } from '../../../services/extensions/common/extensions.js'; import { isWeb, language } from '../../../../base/common/platform.js'; import { getLocale } from '../../../../platform/languagePacks/common/languagePacks.js'; import { ILocaleService } from '../../../services/localization/common/locale.js'; @@ -55,12 +55,13 @@ import { IUserDataProfileService } from '../../../services/userDataProfile/commo import { mainWindow } from '../../../../base/browser/window.js'; import { IDialogService, IPromptButton } from '../../../../platform/dialogs/common/dialogs.js'; import { IUpdateService, StateType } from '../../../../platform/update/common/update.js'; -import { isEngineValid } from '../../../../platform/extensions/common/extensionValidator.js'; +import { areApiProposalsCompatible, isEngineValid } from '../../../../platform/extensions/common/extensionValidator.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { ShowCurrentReleaseNotesActionId } from '../../update/common/update.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from '../../../../platform/configuration/common/configurationRegistry.js'; +import { IViewsService } from '../../../services/views/common/viewsService.js'; interface IExtensionStateProvider { (extension: Extension): T; @@ -903,9 +904,14 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension private updatesCheckDelayer: ThrottledDelayer; private autoUpdateDelayer: ThrottledDelayer; - private readonly _onChange: Emitter = new Emitter(); + private readonly _onChange = this._register(new Emitter()); get onChange(): Event { return this._onChange.event; } + private extensionsNotification: IExtensionsNotification | undefined; + private dismissedNotifications: string[] = []; + private readonly _onDidChangeExtensionsNotification = new Emitter(); + readonly onDidChangeExtensionsNotification = this._onDidChangeExtensionsNotification.event; + private readonly _onReset = new Emitter(); get onReset() { return this._onReset.event; } @@ -947,6 +953,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension @IUpdateService private readonly updateService: IUpdateService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @IViewsService private readonly viewsService: IViewsService, ) { super(); const preferPreReleasesValue = configurationService.getValue('_extensions.preferPreReleases'); @@ -1031,30 +1038,23 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (this._store.isDisposed) { return; } + this.initializeAutoUpdate(); + this.updateExtensionsNotificaiton(); this.reportInstalledExtensionsTelemetry(); - this._register(Event.debounce(this.onChange, () => undefined, 100)(() => this.reportProgressFromOtherSources())); this._register(this.storageService.onDidChangeValue(StorageScope.APPLICATION, EXTENSIONS_AUTO_UPDATE_KEY, this._store)(e => this.onDidSelectedExtensionToAutoUpdateValueChange())); this._register(this.storageService.onDidChangeValue(StorageScope.APPLICATION, EXTENSIONS_DONOT_AUTO_UPDATE_KEY, this._store)(e => this.onDidSelectedExtensionToAutoUpdateValueChange())); + this._register(Event.debounce(this.onChange, () => undefined, 100)(() => { + this.updateExtensionsNotificaiton(); + this.reportProgressFromOtherSources(); + })); } private initializeAutoUpdate(): void { - // Initialise Auto Update Value - let autoUpdateValue = this.getAutoUpdateValue(); - // Register listeners for auto updates this._register(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration(AutoUpdateConfigurationKey)) { - const wasAutoUpdateEnabled = autoUpdateValue !== false; - autoUpdateValue = this.getAutoUpdateValue(); - const isAutoUpdateEnabled = this.isAutoUpdateEnabled(); - if (wasAutoUpdateEnabled !== isAutoUpdateEnabled) { - this.setEnabledAutoUpdateExtensions([]); - this.setDisabledAutoUpdateExtensions([]); - this._onChange.fire(undefined); - this.updateExtensionsPinnedState(!isAutoUpdateEnabled); - } - if (isAutoUpdateEnabled) { + if (this.isAutoUpdateEnabled()) { this.eventuallyAutoUpdateExtensions(); } } @@ -1116,22 +1116,31 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return isBoolean(autoUpdate) || autoUpdate === 'onlyEnabledExtensions' ? autoUpdate : true; } - async updateAutoUpdateValue(value: AutoUpdateConfigurationValue): Promise { - const wasEnabled = this.isAutoUpdateEnabled(); - const isEnabled = value !== false; - if (wasEnabled !== isEnabled) { - const result = await this.dialogService.confirm({ - title: nls.localize('confirmEnableDisableAutoUpdate', "Auto Update Extensions"), - message: isEnabled - ? nls.localize('confirmEnableAutoUpdate', "Do you want to enable auto update for all extensions?") - : nls.localize('confirmDisableAutoUpdate', "Do you want to disable auto update for all extensions?"), - detail: nls.localize('confirmEnableDisableAutoUpdateDetail', "This will reset any auto update settings you have set for individual extensions."), - }); - if (!result.confirmed) { - return; - } + async updateAutoUpdateForAllExtensions(isAutoUpdateEnabled: boolean): Promise { + const wasAutoUpdateEnabled = this.isAutoUpdateEnabled(); + if (wasAutoUpdateEnabled === isAutoUpdateEnabled) { + return; } - await this.configurationService.updateValue(AutoUpdateConfigurationKey, value); + + const result = await this.dialogService.confirm({ + title: nls.localize('confirmEnableDisableAutoUpdate', "Auto Update Extensions"), + message: isAutoUpdateEnabled + ? nls.localize('confirmEnableAutoUpdate', "Do you want to enable auto update for all extensions?") + : nls.localize('confirmDisableAutoUpdate', "Do you want to disable auto update for all extensions?"), + detail: nls.localize('confirmEnableDisableAutoUpdateDetail', "This will reset any auto update settings you have set for individual extensions."), + }); + if (!result.confirmed) { + return; + } + + // Reset extensions enabled for auto update first to prevent them from being updated + this.setEnabledAutoUpdateExtensions([]); + + await this.configurationService.updateValue(AutoUpdateConfigurationKey, isAutoUpdateEnabled); + + this.setDisabledAutoUpdateExtensions([]); + await this.updateExtensionsPinnedState(!isAutoUpdateEnabled); + this._onChange.fire(undefined); } private readonly autoRestartListenerDisposable = this._register(new MutableDisposable()); @@ -1330,6 +1339,61 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension ?? this.instantiationService.createInstance(Extension, ext => this.getExtensionState(ext), ext => this.getRuntimeState(ext), undefined, undefined, undefined, { resourceExtension, isWorkspaceScoped })); } + private updateExtensionsNotificaiton(): void { + const computedNotificiation = this.computeExtensionsNotification(); + const extensionsNotification = computedNotificiation && !this.dismissedNotifications.includes(computedNotificiation.message) ? + { + ...computedNotificiation, + dismiss: () => { + this.dismissedNotifications.push(computedNotificiation.message); + this.updateExtensionsNotificaiton(); + }, + } + : undefined; + + if (this.extensionsNotification?.message !== extensionsNotification?.message) { + this.extensionsNotification = extensionsNotification; + this._onDidChangeExtensionsNotification.fire(this.extensionsNotification); + } + } + + private computeExtensionsNotification(): Omit | undefined { + + const invalidExtensions = this.local.filter(e => e.enablementState === EnablementState.DisabledByInvalidExtension); + if (invalidExtensions.length) { + if (invalidExtensions.some(e => e.local && + (!isEngineValid(e.local.manifest.engines.vscode, this.productService.version, this.productService.date) || areApiProposalsCompatible([...e.local.manifest.enabledApiProposals ?? []])) + )) { + return { + message: nls.localize('incompatibleExtensions', "Some extensions are disabled due to version incompatibility. Review and update them."), + severity: Severity.Warning, + extensions: invalidExtensions, + }; + } else { + return { + message: nls.localize('invalidExtensions', "Invalid extensions detected. Review them."), + severity: Severity.Warning, + extensions: invalidExtensions, + }; + } + } + + const deprecatedExtensions = this.local.filter(e => !!e.deprecationInfo); + if (deprecatedExtensions.length) { + return { + message: nls.localize('deprecated extensions', "Deprecated extensions detected. Review them and migrate to alternatives."), + severity: Severity.Warning, + extensions: deprecatedExtensions, + }; + } + + return undefined; + } + + getExtensionsNotification(): IExtensionsNotification | undefined { + return this.extensionsNotification; + } + private resolveQueryText(text: string): string { text = text.replace(/@web/g, `tag:"${WEB_EXTENSION_TAG}"`); @@ -1392,7 +1456,15 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension await this.editorService.openEditor(this.instantiationService.createInstance(ExtensionsInput, extension), options, options?.sideByside ? SIDE_GROUP : ACTIVE_GROUP); } - getExtensionStatus(extension: IExtension): IExtensionsStatus | undefined { + async openSearch(searchValue: string, preserveFoucs?: boolean): Promise { + const viewPaneContainer = (await this.viewsService.openViewContainer(VIEWLET_ID, true))?.getViewPaneContainer() as IExtensionsViewPaneContainer; + viewPaneContainer.search(searchValue); + if (!preserveFoucs) { + viewPaneContainer.focus(); + } + } + + getExtensionRuntimeStatus(extension: IExtension): IExtensionRuntimeStatus | undefined { const extensionsStatus = this.extensionService.getExtensionsStatus(); for (const id of Object.keys(extensionsStatus)) { if (areSameExtensions({ id }, extension.identifier)) { @@ -1435,7 +1507,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension if (extension.isUnderDevelopment) { continue; } - if (extensionsToCheck.some(e => areSameExtensions({ id: extension.identifier.value, uuid: extension.uuid }, e.identifier))) { + if (extensionsToCheck.some(e => areSameExtensions({ id: extension.identifier.value, uuid: extension.uuid }, e.local?.identifier ?? e.identifier))) { continue; } // Extension is running but doesn't exist locally. Remove it from running extensions. diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css b/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css index a4514bd07c0..b0141e1c4fa 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css @@ -8,6 +8,11 @@ height: 100%; } +.extensions-viewlet .hidden { + display: none; + visibility: hidden; +} + .extensions-viewlet > .overlay { position: absolute; top: 0; @@ -47,6 +52,44 @@ height: 100%; } +.extensions-viewlet > .header > .notification-container { + margin-top: 10px; + display: flex; + justify-content: space-between; +} + +.extensions-viewlet > .header .notification-container .message-container { + padding-left: 4px; +} + +.extensions-viewlet > .header .notification-container .message-container .codicon { + vertical-align: text-top; + padding-right: 5px; +} + +.extensions-viewlet .notification-container .message-text-action { + cursor: pointer; + margin-left: 5px; + color: var(--vscode-textLink-foreground); + text-decoration: underline; +} + +.extensions-viewlet .notification-container .message-text-action:hover, +.extensions-viewlet .notification-container .message-text-action:active { + color: var(--vscode-textLink-activeForeground); +} + +.extensions-viewlet .notification-container .message-action { + cursor: pointer; + padding: 2px; + border-radius: 5px; + height: 16px; +} + +.extensions-viewlet .notification-container .message-action:hover { + background-color: var(--vscode-toolbar-hoverBackground); + outline: 1px dashed var(--vscode-toolbar-hoverOutline); +} .extensions-viewlet > .extensions { height: calc(100% - 41px); @@ -70,12 +113,6 @@ display: none; } -.extensions-viewlet > .extensions .extensions-list.hidden, -.extensions-viewlet > .extensions .message-container.hidden { - display: none; - visibility: hidden; -} - .extensions-viewlet > .extensions .panel-header { padding-right: 12px; } @@ -151,8 +188,14 @@ opacity: 0.5; } -.extensions-badge.progress-badge > .badge-content { - background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNCIgaGVpZ2h0PSIxNCIgdmlld0JveD0iMiAyIDE0IDE0IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDIgMiAxNCAxNCI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTkgMTZjLTMuODYgMC03LTMuMTQtNy03czMuMTQtNyA3LTdjMy44NTkgMCA3IDMuMTQxIDcgN3MtMy4xNDEgNy03IDd6bTAtMTIuNmMtMy4wODggMC01LjYgMi41MTMtNS42IDUuNnMyLjUxMiA1LjYgNS42IDUuNiA1LjYtMi41MTIgNS42LTUuNi0yLjUxMi01LjYtNS42LTUuNnptMy44NiA3LjFsLTMuMTYtMS44OTZ2LTMuODA0aC0xLjR2NC41OTZsMy44NCAyLjMwNS43Mi0xLjIwMXoiLz48L3N2Zz4="); - background-position: center center; - background-repeat: no-repeat; +.extensions-viewlet .codicon-error::before { + color: var(--vscode-editorError-foreground); +} + +.extensions-viewlet .codicon-warning::before { + color: var(--vscode-editorWarning-foreground); +} + +.extensions-viewlet .codicon-info::before { + color: var(--vscode-editorInfo-foreground); } diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index e727e00d41e..0b880a40d55 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -15,10 +15,11 @@ import { IExtensionManifest, ExtensionType } from '../../../../platform/extensio import { URI } from '../../../../base/common/uri.js'; import { IView, IViewPaneContainer } from '../../../common/views.js'; import { RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; -import { IExtensionsStatus } from '../../../services/extensions/common/extensions.js'; +import { IExtensionsStatus as IExtensionRuntimeStatus } from '../../../services/extensions/common/extensions.js'; import { IExtensionEditorOptions } from './extensionsInput.js'; import { MenuId } from '../../../../platform/actions/common/actions.js'; import { ProgressLocation } from '../../../../platform/progress/common/progress.js'; +import { Severity } from '../../../../platform/notification/common/notification.js'; export const VIEWLET_ID = 'workbench.view.extensions'; @@ -110,6 +111,13 @@ export interface InstallExtensionOptions extends InstallOptions { enable?: boolean; } +export interface IExtensionsNotification { + readonly message: string; + readonly severity: Severity; + readonly extensions: IExtension[]; + dismiss(): void; +} + export interface IExtensionsWorkbenchService { readonly _serviceBrand: undefined; readonly onChange: Event; @@ -139,14 +147,18 @@ export interface IExtensionsWorkbenchService { isAutoUpdateEnabledFor(extensionOrPublisher: IExtension | string): boolean; updateAutoUpdateEnablementFor(extensionOrPublisher: IExtension | string, enable: boolean): Promise; shouldRequireConsentToUpdate(extension: IExtension): Promise; + updateAutoUpdateForAllExtensions(value: boolean): Promise; open(extension: IExtension | string, options?: IExtensionEditorOptions): Promise; - updateAutoUpdateValue(value: AutoUpdateConfigurationValue): Promise; + openSearch(searchValue: string, focus?: boolean): Promise; getAutoUpdateValue(): AutoUpdateConfigurationValue; checkForUpdates(): Promise; - getExtensionStatus(extension: IExtension): IExtensionsStatus | undefined; + getExtensionRuntimeStatus(extension: IExtension): IExtensionRuntimeStatus | undefined; updateAll(): Promise; updateRunningExtensions(): Promise; + readonly onDidChangeExtensionsNotification: Event; + getExtensionsNotification(): IExtensionsNotification | undefined; + // Sync APIs isExtensionIgnoredToSync(extension: IExtension): boolean; toggleExtensionIgnoredToSync(extension: IExtension): Promise; diff --git a/src/vs/workbench/contrib/extensions/electron-sandbox/debugExtensionHostAction.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/debugExtensionHostAction.ts index 9da3189d7da..71291a40f8e 100644 --- a/src/vs/workbench/contrib/extensions/electron-sandbox/debugExtensionHostAction.ts +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/debugExtensionHostAction.ts @@ -3,62 +3,73 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Action } from '../../../../base/common/actions.js'; +import { Codicon } from '../../../../base/common/codicons.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { randomPort } from '../../../../base/common/ports.js'; import * as nls from '../../../../nls.js'; +import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; +import { Action2, MenuId } from '../../../../platform/actions/common/actions.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { INativeHostService } from '../../../../platform/native/common/native.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; +import { ActiveEditorContext } from '../../../common/contextkeys.js'; import { IWorkbenchContribution } from '../../../common/contributions.js'; -import { IConfig, IDebugService } from '../../debug/common/debug.js'; import { ExtensionHostKind } from '../../../services/extensions/common/extensionHostKind.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; import { IHostService } from '../../../services/host/browser/host.js'; +import { IConfig, IDebugService } from '../../debug/common/debug.js'; +import { RuntimeExtensionsEditor } from './runtimeExtensionsEditor.js'; -export class DebugExtensionHostAction extends Action { - static readonly ID = 'workbench.extensions.action.debugExtensionHost'; - static readonly LABEL = nls.localize('debugExtensionHost', "Start Debugging Extension Host In New Window"); - static readonly CSS_CLASS = 'debug-extension-host'; - - constructor( - @INativeHostService private readonly _nativeHostService: INativeHostService, - @IDialogService private readonly _dialogService: IDialogService, - @IExtensionService private readonly _extensionService: IExtensionService, - @IProductService private readonly productService: IProductService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IHostService private readonly _hostService: IHostService, - ) { - super(DebugExtensionHostAction.ID, DebugExtensionHostAction.LABEL, DebugExtensionHostAction.CSS_CLASS); +export class DebugExtensionHostAction extends Action2 { + constructor() { + super({ + id: 'workbench.extensions.action.debugExtensionHost', + title: { value: nls.localize('debugExtensionHost', "Start Debugging Extension Host In New Window"), original: 'Start Debugging Extension Host In New Window' }, + category: Categories.Developer, + f1: true, + icon: Codicon.debugStart, + menu: { + id: MenuId.EditorTitle, + when: ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID), + group: 'navigation', + } + }); } - override async run(_args: unknown): Promise { - const inspectPorts = await this._extensionService.getInspectPorts(ExtensionHostKind.LocalProcess, false); - if (inspectPorts.length === 0) { - const res = await this._dialogService.confirm({ - message: nls.localize('restart1', "Debug Extensions"), - detail: nls.localize('restart2', "In order to debug extensions a restart is required. Do you want to restart '{0}' now?", this.productService.nameLong), - primaryButton: nls.localize({ key: 'restart3', comment: ['&& denotes a mnemonic'] }, "&&Restart") - }); - if (res.confirmed) { - await this._nativeHostService.relaunch({ addArgs: [`--inspect-extensions=${randomPort()}`] }); + run(accessor: ServicesAccessor): void { + const nativeHostService = accessor.get(INativeHostService); + const dialogService = accessor.get(IDialogService); + const extensionService = accessor.get(IExtensionService); + const productService = accessor.get(IProductService); + const instantiationService = accessor.get(IInstantiationService); + const hostService = accessor.get(IHostService); + + extensionService.getInspectPorts(ExtensionHostKind.LocalProcess, false).then(async inspectPorts => { + if (inspectPorts.length === 0) { + const res = await dialogService.confirm({ + message: nls.localize('restart1', "Debug Extensions"), + detail: nls.localize('restart2', "In order to debug extensions a restart is required. Do you want to restart '{0}' now?", productService.nameLong), + primaryButton: nls.localize({ key: 'restart3', comment: ['&& denotes a mnemonic'] }, "&&Restart") + }); + if (res.confirmed) { + await nativeHostService.relaunch({ addArgs: [`--inspect-extensions=${randomPort()}`] }); + } + return; } - return; - } + if (inspectPorts.length > 1) { + // TODO + console.warn(`There are multiple extension hosts available for debugging. Picking the first one...`); + } - if (inspectPorts.length > 1) { - // TODO - console.warn(`There are multiple extension hosts available for debugging. Picking the first one...`); - } + const s = instantiationService.createInstance(Storage); + s.storeDebugOnNewWindow(inspectPorts[0].port); - const s = this._instantiationService.createInstance(Storage); - s.storeDebugOnNewWindow(inspectPorts[0].port); - - this._hostService.openWindow(); + hostService.openWindow(); + }); } } diff --git a/src/vs/workbench/contrib/extensions/electron-sandbox/extensionProfileService.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/extensionProfileService.ts index b7f8774a575..99057a3464c 100644 --- a/src/vs/workbench/contrib/extensions/electron-sandbox/extensionProfileService.ts +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/extensionProfileService.ts @@ -23,6 +23,7 @@ import { ExtensionHostKind } from '../../../services/extensions/common/extension import { IExtensionHostProfile, IExtensionService, ProfileSession } from '../../../services/extensions/common/extensions.js'; import { ExtensionHostProfiler } from '../../../services/extensions/electron-sandbox/extensionHostProfiler.js'; import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from '../../../services/statusbar/browser/statusbar.js'; +import { URI } from '../../../../base/common/uri.js'; export class ExtensionHostProfileService extends Disposable implements IExtensionHostProfileService { @@ -42,6 +43,7 @@ export class ExtensionHostProfileService extends Disposable implements IExtensio private profilingStatusBarIndicator: IStatusbarEntryAccessor | undefined; private readonly profilingStatusBarIndicatorLabelUpdater = this._register(new MutableDisposable()); + public lastProfileSavedTo: URI | undefined; public get state() { return this._state; } public get lastProfile() { return this._profile; } @@ -166,6 +168,7 @@ export class ExtensionHostProfileService extends Disposable implements IExtensio private _setLastProfile(profile: IExtensionHostProfile) { this._profile = profile; + this.lastProfileSavedTo = undefined; this._onDidChangeLastProfile.fire(undefined); } diff --git a/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts index 44bebc97bdd..021c893c2b6 100644 --- a/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/extensions.contribution.ts @@ -3,32 +3,28 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { localize } from '../../../../nls.js'; -import { Registry } from '../../../../platform/registry/common/platform.js'; -import { MenuRegistry, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from '../../../common/contributions.js'; -import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; -import { CommandsRegistry } from '../../../../platform/commands/common/commands.js'; -import { ServicesAccessor, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { EditorPaneDescriptor, IEditorPaneRegistry } from '../../../browser/editor.js'; -import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; -import { RuntimeExtensionsEditor, StartExtensionHostProfileAction, StopExtensionHostProfileAction, CONTEXT_PROFILE_SESSION_STATE, CONTEXT_EXTENSION_HOST_PROFILE_RECORDED, SaveExtensionHostProfileAction, IExtensionHostProfileService } from './runtimeExtensionsEditor.js'; -import { DebugExtensionHostAction, DebugExtensionsContribution } from './debugExtensionHostAction.js'; -import { IEditorSerializer, IEditorFactoryRegistry, EditorExtensions } from '../../../common/editor.js'; -import { ActiveEditorContext } from '../../../common/contextkeys.js'; -import { EditorInput } from '../../../common/editor/editorInput.js'; -import { RuntimeExtensionsInput } from '../common/runtimeExtensionsInput.js'; -import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -import { CleanUpExtensionsFolderAction, OpenExtensionsFolderAction } from './extensionsActions.js'; -import { IExtensionRecommendationNotificationService } from '../../../../platform/extensionRecommendations/common/extensionRecommendations.js'; -import { ISharedProcessService } from '../../../../platform/ipc/electron-sandbox/services.js'; -import { ExtensionRecommendationNotificationServiceChannel } from '../../../../platform/extensionRecommendations/common/extensionRecommendationsIpc.js'; -import { Codicon } from '../../../../base/common/codicons.js'; -import { RemoteExtensionsInitializerContribution } from './remoteExtensionsInit.js'; -import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { ExtensionHostProfileService } from './extensionProfileService.js'; -import { ExtensionsAutoProfiler } from './extensionsAutoProfiler.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; +import { localize } from '../../../../nls.js'; +import { registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { IExtensionRecommendationNotificationService } from '../../../../platform/extensionRecommendations/common/extensionRecommendations.js'; +import { ExtensionRecommendationNotificationServiceChannel } from '../../../../platform/extensionRecommendations/common/extensionRecommendationsIpc.js'; +import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { ISharedProcessService } from '../../../../platform/ipc/electron-sandbox/services.js'; +import { Registry } from '../../../../platform/registry/common/platform.js'; +import { EditorPaneDescriptor, IEditorPaneRegistry } from '../../../browser/editor.js'; +import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from '../../../common/contributions.js'; +import { EditorExtensions, IEditorFactoryRegistry, IEditorSerializer } from '../../../common/editor.js'; +import { EditorInput } from '../../../common/editor/editorInput.js'; +import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; +import { RuntimeExtensionsInput } from '../common/runtimeExtensionsInput.js'; +import { DebugExtensionHostAction, DebugExtensionsContribution } from './debugExtensionHostAction.js'; +import { ExtensionHostProfileService } from './extensionProfileService.js'; +import { CleanUpExtensionsFolderAction, OpenExtensionsFolderAction } from './extensionsActions.js'; +import { ExtensionsAutoProfiler } from './extensionsAutoProfiler.js'; +import { RemoteExtensionsInitializerContribution } from './remoteExtensionsInit.js'; +import { IExtensionHostProfileService, OpenExtensionHostProfileACtion, RuntimeExtensionsEditor, SaveExtensionHostProfileAction, StartExtensionHostProfileAction, StopExtensionHostProfileAction } from './runtimeExtensionsEditor.js'; // Singletons registerSingleton(IExtensionHostProfileService, ExtensionHostProfileService, InstantiationType.Delayed); @@ -76,76 +72,12 @@ workbenchRegistry.registerWorkbenchContribution(ExtensionsContributions, Lifecyc workbenchRegistry.registerWorkbenchContribution(ExtensionsAutoProfiler, LifecyclePhase.Eventually); workbenchRegistry.registerWorkbenchContribution(RemoteExtensionsInitializerContribution, LifecyclePhase.Restored); workbenchRegistry.registerWorkbenchContribution(DebugExtensionsContribution, LifecyclePhase.Restored); + // Register Commands -CommandsRegistry.registerCommand(DebugExtensionHostAction.ID, (accessor: ServicesAccessor, ...args) => { - const instantiationService = accessor.get(IInstantiationService); - return instantiationService.createInstance(DebugExtensionHostAction).run(args); -}); +registerAction2(DebugExtensionHostAction); +registerAction2(StartExtensionHostProfileAction); +registerAction2(StopExtensionHostProfileAction); +registerAction2(SaveExtensionHostProfileAction); +registerAction2(OpenExtensionHostProfileACtion); -CommandsRegistry.registerCommand(StartExtensionHostProfileAction.ID, (accessor: ServicesAccessor) => { - const instantiationService = accessor.get(IInstantiationService); - instantiationService.createInstance(StartExtensionHostProfileAction, StartExtensionHostProfileAction.ID, StartExtensionHostProfileAction.LABEL).run(); -}); - -CommandsRegistry.registerCommand(StopExtensionHostProfileAction.ID, (accessor: ServicesAccessor) => { - const instantiationService = accessor.get(IInstantiationService); - instantiationService.createInstance(StopExtensionHostProfileAction, StopExtensionHostProfileAction.ID, StopExtensionHostProfileAction.LABEL).run(); -}); - -CommandsRegistry.registerCommand(SaveExtensionHostProfileAction.ID, (accessor: ServicesAccessor) => { - const instantiationService = accessor.get(IInstantiationService); - instantiationService.createInstance(SaveExtensionHostProfileAction, SaveExtensionHostProfileAction.ID, SaveExtensionHostProfileAction.LABEL).run(); -}); - -// Running extensions - -MenuRegistry.appendMenuItem(MenuId.EditorTitle, { - command: { - id: DebugExtensionHostAction.ID, - title: DebugExtensionHostAction.LABEL, - icon: Codicon.debugStart - }, - group: 'navigation', - when: ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID) -}); - -MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - command: { - id: DebugExtensionHostAction.ID, - title: localize('debugExtensionHost', "Debug Extensions In New Window"), - category: localize('developer', "Developer"), - icon: Codicon.debugStart - }, -}); - -MenuRegistry.appendMenuItem(MenuId.EditorTitle, { - command: { - id: StartExtensionHostProfileAction.ID, - title: StartExtensionHostProfileAction.LABEL, - icon: Codicon.circleFilled - }, - group: 'navigation', - when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID), CONTEXT_PROFILE_SESSION_STATE.notEqualsTo('running')) -}); - -MenuRegistry.appendMenuItem(MenuId.EditorTitle, { - command: { - id: StopExtensionHostProfileAction.ID, - title: StopExtensionHostProfileAction.LABEL, - icon: Codicon.debugStop - }, - group: 'navigation', - when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID), CONTEXT_PROFILE_SESSION_STATE.isEqualTo('running')) -}); - -MenuRegistry.appendMenuItem(MenuId.EditorTitle, { - command: { - id: SaveExtensionHostProfileAction.ID, - title: SaveExtensionHostProfileAction.LABEL, - icon: Codicon.saveAll, - precondition: CONTEXT_EXTENSION_HOST_PROFILE_RECORDED - }, - group: 'navigation', - when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID)) -}); diff --git a/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts index 2f54dbdb86a..ac1b7ad2bd8 100644 --- a/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/electron-sandbox/runtimeExtensionsEditor.ts @@ -3,35 +3,40 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from '../../../../nls.js'; import { Action } from '../../../../base/common/actions.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; -import { IInstantiationService, createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { IExtensionsWorkbenchService } from '../common/extensions.js'; -import { IThemeService } from '../../../../platform/theme/common/themeService.js'; -import { IExtensionService, IExtensionHostProfile } from '../../../services/extensions/common/extensions.js'; -import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; -import { Event } from '../../../../base/common/event.js'; -import { INotificationService } from '../../../../platform/notification/common/notification.js'; -import { IContextKeyService, RawContextKey, IContextKey } from '../../../../platform/contextkey/common/contextkey.js'; -import { IStorageService } from '../../../../platform/storage/common/storage.js'; -import { ILabelService } from '../../../../platform/label/common/label.js'; -import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; -import { SlowExtensionAction } from './extensionsSlowActions.js'; -import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; -import { ReportExtensionIssueAction } from '../common/reportExtensionIssueAction.js'; -import { AbstractRuntimeExtensionsEditor, IRuntimeExtension } from '../browser/abstractRuntimeExtensionsEditor.js'; import { VSBuffer } from '../../../../base/common/buffer.js'; -import { URI } from '../../../../base/common/uri.js'; -import { IFileService } from '../../../../platform/files/common/files.js'; -import { IV8Profile, Utils } from '../../../../platform/profiling/common/profiling.js'; -import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; -import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js'; +import { Codicon } from '../../../../base/common/codicons.js'; +import { Event } from '../../../../base/common/event.js'; import { Schemas } from '../../../../base/common/network.js'; import { joinPath } from '../../../../base/common/resources.js'; -import { IExtensionFeaturesManagementService } from '../../../services/extensionManagement/common/extensionFeatures.js'; -import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js'; +import { URI } from '../../../../base/common/uri.js'; +import * as nls from '../../../../nls.js'; +import { Action2, IMenuService, MenuId } from '../../../../platform/actions/common/actions.js'; +import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; +import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; +import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js'; +import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; import { IHoverService } from '../../../../platform/hover/browser/hover.js'; +import { IInstantiationService, ServicesAccessor, createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { ILabelService } from '../../../../platform/label/common/label.js'; +import { INotificationService } from '../../../../platform/notification/common/notification.js'; +import { IV8Profile, Utils } from '../../../../platform/profiling/common/profiling.js'; +import { IStorageService } from '../../../../platform/storage/common/storage.js'; +import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; +import { IThemeService } from '../../../../platform/theme/common/themeService.js'; +import { ActiveEditorContext } from '../../../common/contextkeys.js'; +import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js'; +import { IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js'; +import { IWorkbenchEnvironmentService } from '../../../services/environment/common/environmentService.js'; +import { IExtensionFeaturesManagementService } from '../../../services/extensionManagement/common/extensionFeatures.js'; +import { IExtensionHostProfile, IExtensionService } from '../../../services/extensions/common/extensions.js'; +import { AbstractRuntimeExtensionsEditor, IRuntimeExtension } from '../browser/abstractRuntimeExtensionsEditor.js'; +import { IExtensionsWorkbenchService } from '../common/extensions.js'; +import { ReportExtensionIssueAction } from '../common/reportExtensionIssueAction.js'; +import { SlowExtensionAction } from './extensionsSlowActions.js'; export const IExtensionHostProfileService = createDecorator('extensionHostProfileService'); export const CONTEXT_PROFILE_SESSION_STATE = new RawContextKey('profileSessionState', 'none'); @@ -52,6 +57,7 @@ export interface IExtensionHostProfileService { readonly state: ProfileSessionState; readonly lastProfile: IExtensionHostProfile | null; + lastProfileSavedTo: URI | undefined; startProfiling(): void; stopProfiling(): void; @@ -82,9 +88,10 @@ export class RuntimeExtensionsEditor extends AbstractRuntimeExtensionsEditor { @IClipboardService clipboardService: IClipboardService, @IExtensionHostProfileService private readonly _extensionHostProfileService: IExtensionHostProfileService, @IExtensionFeaturesManagementService extensionFeaturesManagementService: IExtensionFeaturesManagementService, - @IHoverService hoverService: IHoverService + @IHoverService hoverService: IHoverService, + @IMenuService menuService: IMenuService, ) { - super(group, telemetryService, themeService, contextKeyService, extensionsWorkbenchService, extensionService, notificationService, contextMenuService, instantiationService, storageService, labelService, environmentService, clipboardService, extensionFeaturesManagementService, hoverService); + super(group, telemetryService, themeService, contextKeyService, extensionsWorkbenchService, extensionService, notificationService, contextMenuService, instantiationService, storageService, labelService, environmentService, clipboardService, extensionFeaturesManagementService, hoverService, menuService); this._profileInfo = this._extensionHostProfileService.lastProfile; this._extensionsHostRecorded = CONTEXT_EXTENSION_HOST_PROFILE_RECORDED.bindTo(contextKeyService); this._profileSessionState = CONTEXT_PROFILE_SESSION_STATE.bindTo(contextKeyService); @@ -121,83 +128,150 @@ export class RuntimeExtensionsEditor extends AbstractRuntimeExtensionsEditor { } return null; } - - protected _createSaveExtensionHostProfileAction(): Action | null { - return this._instantiationService.createInstance(SaveExtensionHostProfileAction, SaveExtensionHostProfileAction.ID, SaveExtensionHostProfileAction.LABEL); - } - - protected _createProfileAction(): Action | null { - const state = this._extensionHostProfileService.state; - const profileAction = ( - state === ProfileSessionState.Running - ? this._instantiationService.createInstance(StopExtensionHostProfileAction, StopExtensionHostProfileAction.ID, StopExtensionHostProfileAction.LABEL) - : this._instantiationService.createInstance(StartExtensionHostProfileAction, StartExtensionHostProfileAction.ID, StartExtensionHostProfileAction.LABEL) - ); - return profileAction; - } } -export class StartExtensionHostProfileAction extends Action { +export class StartExtensionHostProfileAction extends Action2 { static readonly ID = 'workbench.extensions.action.extensionHostProfile'; static readonly LABEL = nls.localize('extensionHostProfileStart', "Start Extension Host Profile"); - constructor( - id: string = StartExtensionHostProfileAction.ID, label: string = StartExtensionHostProfileAction.LABEL, - @IExtensionHostProfileService private readonly _extensionHostProfileService: IExtensionHostProfileService, - ) { - super(id, label); + constructor() { + super({ + id: StartExtensionHostProfileAction.ID, + title: { value: StartExtensionHostProfileAction.LABEL, original: 'Start Extension Host Profile' }, + precondition: CONTEXT_PROFILE_SESSION_STATE.isEqualTo('none'), + icon: Codicon.circleFilled, + menu: [{ + id: MenuId.EditorTitle, + when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID), CONTEXT_PROFILE_SESSION_STATE.notEqualsTo('running')), + group: 'navigation', + }, { + id: MenuId.ExtensionEditorContextMenu, + when: CONTEXT_PROFILE_SESSION_STATE.notEqualsTo('running'), + group: 'profiling', + }] + }); } - override run(): Promise { - this._extensionHostProfileService.startProfiling(); + run(accessor: ServicesAccessor): Promise { + const extensionHostProfileService = accessor.get(IExtensionHostProfileService); + extensionHostProfileService.startProfiling(); return Promise.resolve(); } } -export class StopExtensionHostProfileAction extends Action { +export class StopExtensionHostProfileAction extends Action2 { static readonly ID = 'workbench.extensions.action.stopExtensionHostProfile'; static readonly LABEL = nls.localize('stopExtensionHostProfileStart', "Stop Extension Host Profile"); - constructor( - id: string = StartExtensionHostProfileAction.ID, label: string = StartExtensionHostProfileAction.LABEL, - @IExtensionHostProfileService private readonly _extensionHostProfileService: IExtensionHostProfileService, - ) { - super(id, label); + constructor() { + super({ + id: StopExtensionHostProfileAction.ID, + title: { value: StopExtensionHostProfileAction.LABEL, original: 'Stop Extension Host Profile' }, + icon: Codicon.debugStop, + menu: [{ + id: MenuId.EditorTitle, + when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID), CONTEXT_PROFILE_SESSION_STATE.isEqualTo('running')), + group: 'navigation', + }, { + id: MenuId.ExtensionEditorContextMenu, + when: CONTEXT_PROFILE_SESSION_STATE.isEqualTo('running'), + group: 'profiling', + }] + }); } - override run(): Promise { - this._extensionHostProfileService.stopProfiling(); + run(accessor: ServicesAccessor): Promise { + const extensionHostProfileService = accessor.get(IExtensionHostProfileService); + extensionHostProfileService.stopProfiling(); return Promise.resolve(); } } -export class SaveExtensionHostProfileAction extends Action { +export class OpenExtensionHostProfileACtion extends Action2 { + static readonly LABEL = nls.localize('openExtensionHostProfile', "Open Extension Host Profile"); + static readonly ID = 'workbench.extensions.action.openExtensionHostProfile'; + + constructor() { + super({ + id: OpenExtensionHostProfileACtion.ID, + title: { value: OpenExtensionHostProfileACtion.LABEL, original: 'Open Extension Host Profile' }, + precondition: CONTEXT_EXTENSION_HOST_PROFILE_RECORDED, + icon: Codicon.graph, + menu: [{ + id: MenuId.EditorTitle, + when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID)), + group: 'navigation', + }, { + id: MenuId.ExtensionEditorContextMenu, + when: CONTEXT_EXTENSION_HOST_PROFILE_RECORDED, + group: 'profiling', + }] + }); + } + + async run(accessor: ServicesAccessor): Promise { + const extensionHostProfileService = accessor.get(IExtensionHostProfileService); + const commandService = accessor.get(ICommandService); + const editorService = accessor.get(IEditorService); + if (!extensionHostProfileService.lastProfileSavedTo) { + await commandService.executeCommand(SaveExtensionHostProfileAction.ID); + } + if (!extensionHostProfileService.lastProfileSavedTo) { + return; + } + + await editorService.openEditor({ + resource: extensionHostProfileService.lastProfileSavedTo, + options: { + revealIfOpened: true, + override: 'jsProfileVisualizer.cpuprofile.table', + }, + }, SIDE_GROUP); + } + +} + +export class SaveExtensionHostProfileAction extends Action2 { static readonly LABEL = nls.localize('saveExtensionHostProfile', "Save Extension Host Profile"); static readonly ID = 'workbench.extensions.action.saveExtensionHostProfile'; - constructor( - id: string = SaveExtensionHostProfileAction.ID, label: string = SaveExtensionHostProfileAction.LABEL, - @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, - @IExtensionHostProfileService private readonly _extensionHostProfileService: IExtensionHostProfileService, - @IFileService private readonly _fileService: IFileService, - @IFileDialogService private readonly _fileDialogService: IFileDialogService, - ) { - super(id, label, undefined, false); - this._extensionHostProfileService.onDidChangeLastProfile(() => { - this.enabled = (this._extensionHostProfileService.lastProfile !== null); + constructor() { + super({ + id: SaveExtensionHostProfileAction.ID, + title: { value: SaveExtensionHostProfileAction.LABEL, original: 'Save Extension Host Profile' }, + precondition: CONTEXT_EXTENSION_HOST_PROFILE_RECORDED, + icon: Codicon.saveAll, + menu: [{ + id: MenuId.EditorTitle, + when: ContextKeyExpr.and(ActiveEditorContext.isEqualTo(RuntimeExtensionsEditor.ID)), + group: 'navigation', + }, { + id: MenuId.ExtensionEditorContextMenu, + when: CONTEXT_EXTENSION_HOST_PROFILE_RECORDED, + group: 'profiling', + }] }); } - override run(): Promise { - return Promise.resolve(this._asyncRun()); + run(accessor: ServicesAccessor): Promise { + const environmentService = accessor.get(IWorkbenchEnvironmentService); + const extensionHostProfileService = accessor.get(IExtensionHostProfileService); + const fileService = accessor.get(IFileService); + const fileDialogService = accessor.get(IFileDialogService); + return this._asyncRun(environmentService, extensionHostProfileService, fileService, fileDialogService); } - private async _asyncRun(): Promise { - const picked = await this._fileDialogService.showSaveDialog({ + private async _asyncRun( + environmentService: IWorkbenchEnvironmentService, + extensionHostProfileService: IExtensionHostProfileService, + fileService: IFileService, + fileDialogService: IFileDialogService + ): Promise { + const picked = await fileDialogService.showSaveDialog({ title: nls.localize('saveprofile.dialogTitle', "Save Extension Host Profile"), availableFileSystems: [Schemas.file], - defaultUri: joinPath(await this._fileDialogService.defaultFilePath(), `CPU-${new Date().toISOString().replace(/[\-:]/g, '')}.cpuprofile`), + defaultUri: joinPath(await fileDialogService.defaultFilePath(), `CPU-${new Date().toISOString().replace(/[\-:]/g, '')}.cpuprofile`), filters: [{ name: 'CPU Profiles', extensions: ['cpuprofile', 'txt'] @@ -208,12 +282,12 @@ export class SaveExtensionHostProfileAction extends Action { return; } - const profileInfo = this._extensionHostProfileService.lastProfile; + const profileInfo = extensionHostProfileService.lastProfile; let dataToWrite: object = profileInfo ? profileInfo.data : {}; let savePath = picked.fsPath; - if (this._environmentService.isBuilt) { + if (environmentService.isBuilt) { // when running from a not-development-build we remove // absolute filenames because we don't want to reveal anything // about users. We also append the `.txt` suffix to make it @@ -223,6 +297,9 @@ export class SaveExtensionHostProfileAction extends Action { savePath = savePath + '.txt'; } - return this._fileService.writeFile(URI.file(savePath), VSBuffer.fromString(JSON.stringify(profileInfo ? profileInfo.data : {}, null, '\t'))); + const saveURI = URI.file(savePath); + extensionHostProfileService.lastProfileSavedTo = saveURI; + return fileService.writeFile(saveURI, VSBuffer.fromString(JSON.stringify(profileInfo ? profileInfo.data : {}, null, '\t'))); } } + diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts index f5ca24e8f59..5341efafabf 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsActions.test.ts @@ -2538,7 +2538,8 @@ function aLocalExtension(name: string = 'someext', manifest: any = {}, propertie type: ExtensionType.User, location: URI.file(`pub.${name}`), identifier: { id: getGalleryExtensionId(manifest.publisher, manifest.name) }, - ...properties + ...properties, + isValid: properties.isValid ?? true, }; properties.isBuiltin = properties.type === ExtensionType.System; return Object.create({ manifest, ...properties }); diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts index 46881e77cd8..6507d4cb008 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsViews.test.ts @@ -548,7 +548,8 @@ suite('ExtensionsViews Tests', () => { location: URI.file(`pub.${name}`), identifier: { id: getGalleryExtensionId(manifest.publisher, manifest.name) }, metadata: { id: getGalleryExtensionId(manifest.publisher, manifest.name), publisherId: manifest.publisher, publisherDisplayName: 'somename' }, - ...properties + ...properties, + isValid: properties.isValid ?? true, }; properties.isBuiltin = properties.type === ExtensionType.System; return Object.create({ manifest, ...properties }); diff --git a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts index 15e65b0d1dd..6c0819d809f 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-sandbox/extensionsWorkbenchService.test.ts @@ -1581,7 +1581,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { assert.deepStrictEqual(testObject.getEnabledAutoUpdateExtensions(), []); assert.deepStrictEqual(testObject.getDisabledAutoUpdateExtensions(), ['pub.a']); - await testObject.updateAutoUpdateValue(false); + await testObject.updateAutoUpdateForAllExtensions(false); assert.deepStrictEqual(testObject.getEnabledAutoUpdateExtensions(), []); assert.deepStrictEqual(testObject.getDisabledAutoUpdateExtensions(), []); @@ -1607,7 +1607,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { assert.deepStrictEqual(testObject.getEnabledAutoUpdateExtensions(), ['pub.a']); assert.deepStrictEqual(testObject.getDisabledAutoUpdateExtensions(), []); - await testObject.updateAutoUpdateValue(true); + await testObject.updateAutoUpdateForAllExtensions(true); assert.deepStrictEqual(testObject.getEnabledAutoUpdateExtensions(), []); assert.deepStrictEqual(testObject.getDisabledAutoUpdateExtensions(), []); @@ -1650,7 +1650,8 @@ suite('ExtensionsWorkbenchServiceTest', () => { type: ExtensionType.User, location: URI.file(`pub.${name}`), identifier: { id: getGalleryExtensionId(manifest.publisher, manifest.name) }, - ...properties + ...properties, + isValid: properties.isValid ?? true, }; return Object.create({ manifest, ...properties }); } diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index c1b61149ad8..82c188c79a0 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -757,3 +757,27 @@ MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { }, order: 1 }); + + +// Chat resource anchor context menu + +MenuRegistry.appendMenuItem(MenuId.ChatInlineResourceAnchorContext, { + group: 'navigation', + order: 10, + command: openToSideCommand, + when: ResourceContextKey.HasResource +}); + +MenuRegistry.appendMenuItem(MenuId.ChatInlineResourceAnchorContext, { + group: '1_cutcopypaste', + order: 10, + command: copyPathCommand, + when: ResourceContextKey.IsFileSystemResource +}); + +MenuRegistry.appendMenuItem(MenuId.ChatInlineResourceAnchorContext, { + group: '1_cutcopypaste', + order: 20, + command: copyRelativePathCommand, + when: ResourceContextKey.IsFileSystemResource +}); diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 2a98f7a1286..68ad1b72c93 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -308,6 +308,12 @@ configurationRegistry.registerConfiguration({ 'description': nls.localize('watcherInclude', "Configure extra paths to watch for changes inside the workspace. By default, all workspace folders will be watched recursively, except for folders that are symbolic links. You can explicitly add absolute or relative paths to support watching folders that are symbolic links. Relative paths will be resolved to an absolute path using the currently opened workspace."), 'scope': ConfigurationScope.RESOURCE }, + 'files.experimentalWatcherNext': { // TODO@bpasero decide on default and experiment enlisting + 'type': 'boolean', + 'default': false, + 'markdownDescription': nls.localize('experimentalWatcherNext', "Enables a newer, experimental version of the file watcher."), + scope: ConfigurationScope.APPLICATION + }, 'files.hotExit': hotExitConfiguration, 'files.defaultLanguage': { 'type': 'string', diff --git a/src/vs/workbench/contrib/format/browser/formatActionsNone.ts b/src/vs/workbench/contrib/format/browser/formatActionsNone.ts index 8154ea44b41..a01e47e17c0 100644 --- a/src/vs/workbench/contrib/format/browser/formatActionsNone.ts +++ b/src/vs/workbench/contrib/format/browser/formatActionsNone.ts @@ -12,19 +12,10 @@ import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextke import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; -import { VIEWLET_ID, IExtensionsViewPaneContainer } from '../../extensions/common/extensions.js'; +import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; -import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js'; -import { ViewContainerLocation } from '../../../common/views.js'; import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; -async function showExtensionQuery(paneCompositeService: IPaneCompositePartService, query: string) { - const viewlet = await paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true); - if (viewlet) { - (viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer).search(query); - } -} - registerEditorAction(class FormatDocumentMultipleAction extends EditorAction { constructor() { @@ -48,7 +39,7 @@ registerEditorAction(class FormatDocumentMultipleAction extends EditorAction { } const commandService = accessor.get(ICommandService); - const paneCompositeService = accessor.get(IPaneCompositePartService); + const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); const notificationService = accessor.get(INotificationService); const dialogService = accessor.get(IDialogService); const languageFeaturesService = accessor.get(ILanguageFeaturesService); @@ -70,7 +61,7 @@ registerEditorAction(class FormatDocumentMultipleAction extends EditorAction { primaryButton: nls.localize({ key: 'install.formatter', comment: ['&& denotes a mnemonic'] }, "&&Install Formatter...") }); if (confirmed) { - showExtensionQuery(paneCompositeService, `category:formatters ${langName}`); + extensionsWorkbenchService.openSearch(`category:formatters ${langName}`); } } } diff --git a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts index 36e4187a4b2..28a38f4a198 100644 --- a/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts +++ b/src/vs/workbench/contrib/languageStatus/browser/languageStatus.contribution.ts @@ -331,17 +331,19 @@ class LanguageStatus { // -- pin const actionBar = new ActionBar(right, { hoverDelegate: nativeHoverDelegate }); + const actionLabel: string = isPinned ? localize('unpin', "Remove from Status Bar") : localize('pin', "Add to Status Bar"); + actionBar.setAriaLabel(actionLabel); store.add(actionBar); let action: Action; if (!isPinned) { - action = new Action('pin', localize('pin', "Add to Status Bar"), ThemeIcon.asClassName(Codicon.pin), true, () => { + action = new Action('pin', actionLabel, ThemeIcon.asClassName(Codicon.pin), true, () => { this._dedicated.add(status.id); this._statusBarService.updateEntryVisibility(status.id, true); this._update(); this._storeState(); }); } else { - action = new Action('unpin', localize('unpin', "Remove from Status Bar"), ThemeIcon.asClassName(Codicon.pinned), true, () => { + action = new Action('unpin', actionLabel, ThemeIcon.asClassName(Codicon.pinned), true, () => { this._dedicated.delete(status.id); this._statusBarService.updateEntryVisibility(status.id, false); this._update(); diff --git a/src/vs/workbench/contrib/localization/electron-sandbox/localization.contribution.ts b/src/vs/workbench/contrib/localization/electron-sandbox/localization.contribution.ts index 39795a322bf..9dfa4afa29a 100644 --- a/src/vs/workbench/contrib/localization/electron-sandbox/localization.contribution.ts +++ b/src/vs/workbench/contrib/localization/electron-sandbox/localization.contribution.ts @@ -12,12 +12,10 @@ import { IExtensionManagementService, IExtensionGalleryService, InstallOperation import { INotificationService, NeverShowAgainScope } from '../../../../platform/notification/common/notification.js'; import Severity from '../../../../base/common/severity.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { VIEWLET_ID as EXTENSIONS_VIEWLET_ID, IExtensionsViewPaneContainer } from '../../extensions/common/extensions.js'; +import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; import { minimumTranslatedStrings } from './minimalTranslations.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js'; -import { ViewContainerLocation } from '../../../common/views.js'; import { ILocaleService } from '../../../services/localization/common/locale.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; import { BaseLocalizationWorkbenchContribution } from '../common/localization.contribution.js'; @@ -32,7 +30,7 @@ class NativeLocalizationWorkbenchContribution extends BaseLocalizationWorkbenchC @IStorageService private readonly storageService: IStorageService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, - @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @ITelemetryService private readonly telemetryService: ITelemetryService, ) { super(); @@ -168,16 +166,7 @@ class NativeLocalizationWorkbenchContribution extends BaseLocalizationWorkbenchC label: translations['searchMarketplace'], run: async () => { logUserReaction('search'); - const viewlet = await this.paneCompositeService.openPaneComposite(EXTENSIONS_VIEWLET_ID, ViewContainerLocation.Sidebar, true); - if (!viewlet) { - return; - } - const container = viewlet.getViewPaneContainer(); - if (!container) { - return; - } - (container as IExtensionsViewPaneContainer).search(`tag:lp-${locale}`); - container.focus(); + await this.extensionsWorkbenchService.openSearch(`tag:lp-${locale}`); } }; diff --git a/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts b/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts index e55f0410798..585e5f0833a 100644 --- a/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts +++ b/src/vs/workbench/contrib/markdown/browser/markdownDocumentRenderer.ts @@ -13,6 +13,7 @@ import { escape } from '../../../../base/common/strings.js'; import { ILanguageService } from '../../../../editor/common/languages/language.js'; import { tokenizeToString } from '../../../../editor/common/languages/textToHtmlTokenizer.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; +import { markedGfmHeadingIdPlugin } from './markedGfmHeadingIdPlugin.js'; export const DEFAULT_MARKDOWN_STYLES = ` body { @@ -190,7 +191,7 @@ interface IRenderMarkdownDocumentOptions { readonly token?: CancellationToken; } -/*marked.* +/** * Renders a string of markdown as a document. * * Uses VS Code's syntax highlighting code blocks. @@ -218,7 +219,7 @@ export async function renderMarkdownDocument( return tokenizeToString(languageService, code, languageId); } }), - MarkedGfmHeadings.gfmHeadingId(), + markedGfmHeadingIdPlugin(), ...(options?.markedExtensions ?? []), ); @@ -315,64 +316,3 @@ namespace MarkedHighlight { return html; } } - -namespace MarkedGfmHeadings { - - // Copied from https://github.com/Flet/github-slugger since we can't use esm yet. - // eslint-disable-next-line no-control-regex, no-misleading-character-class - const githubSlugReplaceRegex = /[\0-\x1F!-,\.\/:-@\[-\^`\{-\xA9\xAB-\xB4\xB6-\xB9\xBB-\xBF\xD7\xF7\u02C2-\u02C5\u02D2-\u02DF\u02E5-\u02EB\u02ED\u02EF-\u02FF\u0375\u0378\u0379\u037E\u0380-\u0385\u0387\u038B\u038D\u03A2\u03F6\u0482\u0530\u0557\u0558\u055A-\u055F\u0589-\u0590\u05BE\u05C0\u05C3\u05C6\u05C8-\u05CF\u05EB-\u05EE\u05F3-\u060F\u061B-\u061F\u066A-\u066D\u06D4\u06DD\u06DE\u06E9\u06FD\u06FE\u0700-\u070F\u074B\u074C\u07B2-\u07BF\u07F6-\u07F9\u07FB\u07FC\u07FE\u07FF\u082E-\u083F\u085C-\u085F\u086B-\u089F\u08B5\u08C8-\u08D2\u08E2\u0964\u0965\u0970\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09F2-\u09FB\u09FD\u09FF\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF0-\u0AF8\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B54\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B70\u0B72-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BF0-\u0BFF\u0C0D\u0C11\u0C29\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5B-\u0C5F\u0C64\u0C65\u0C70-\u0C7F\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0CFF\u0D0D\u0D11\u0D45\u0D49\u0D4F-\u0D53\u0D58-\u0D5E\u0D64\u0D65\u0D70-\u0D79\u0D80\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DE5\u0DF0\u0DF1\u0DF4-\u0E00\u0E3B-\u0E3F\u0E4F\u0E5A-\u0E80\u0E83\u0E85\u0E8B\u0EA4\u0EA6\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F01-\u0F17\u0F1A-\u0F1F\u0F2A-\u0F34\u0F36\u0F38\u0F3A-\u0F3D\u0F48\u0F6D-\u0F70\u0F85\u0F98\u0FBD-\u0FC5\u0FC7-\u0FFF\u104A-\u104F\u109E\u109F\u10C6\u10C8-\u10CC\u10CE\u10CF\u10FB\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u1360-\u137F\u1390-\u139F\u13F6\u13F7\u13FE-\u1400\u166D\u166E\u1680\u169B-\u169F\u16EB-\u16ED\u16F9-\u16FF\u170D\u1715-\u171F\u1735-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17D4-\u17D6\u17D8-\u17DB\u17DE\u17DF\u17EA-\u180A\u180E\u180F\u181A-\u181F\u1879-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191F\u192C-\u192F\u193C-\u1945\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DA-\u19FF\u1A1C-\u1A1F\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1AA6\u1AA8-\u1AAF\u1AC1-\u1AFF\u1B4C-\u1B4F\u1B5A-\u1B6A\u1B74-\u1B7F\u1BF4-\u1BFF\u1C38-\u1C3F\u1C4A-\u1C4C\u1C7E\u1C7F\u1C89-\u1C8F\u1CBB\u1CBC\u1CC0-\u1CCF\u1CD3\u1CFB-\u1CFF\u1DFA\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FBD\u1FBF-\u1FC1\u1FC5\u1FCD-\u1FCF\u1FD4\u1FD5\u1FDC-\u1FDF\u1FED-\u1FF1\u1FF5\u1FFD-\u203E\u2041-\u2053\u2055-\u2070\u2072-\u207E\u2080-\u208F\u209D-\u20CF\u20F1-\u2101\u2103-\u2106\u2108\u2109\u2114\u2116-\u2118\u211E-\u2123\u2125\u2127\u2129\u212E\u213A\u213B\u2140-\u2144\u214A-\u214D\u214F-\u215F\u2189-\u24B5\u24EA-\u2BFF\u2C2F\u2C5F\u2CE5-\u2CEA\u2CF4-\u2CFF\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D70-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E00-\u2E2E\u2E30-\u3004\u3008-\u3020\u3030\u3036\u3037\u303D-\u3040\u3097\u3098\u309B\u309C\u30A0\u30FB\u3100-\u3104\u3130\u318F-\u319F\u31C0-\u31EF\u3200-\u33FF\u4DC0-\u4DFF\u9FFD-\u9FFF\uA48D-\uA4CF\uA4FE\uA4FF\uA60D-\uA60F\uA62C-\uA63F\uA673\uA67E\uA6F2-\uA716\uA720\uA721\uA789\uA78A\uA7C0\uA7C1\uA7CB-\uA7F4\uA828-\uA82B\uA82D-\uA83F\uA874-\uA87F\uA8C6-\uA8CF\uA8DA-\uA8DF\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA954-\uA95F\uA97D-\uA97F\uA9C1-\uA9CE\uA9DA-\uA9DF\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A-\uAA5F\uAA77-\uAA79\uAAC3-\uAADA\uAADE\uAADF\uAAF0\uAAF1\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F\uAB5B\uAB6A-\uAB6F\uABEB\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uD7FF\uE000-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB29\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBB2-\uFBD2\uFD3E-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFC-\uFDFF\uFE10-\uFE1F\uFE30-\uFE32\uFE35-\uFE4C\uFE50-\uFE6F\uFE75\uFEFD-\uFF0F\uFF1A-\uFF20\uFF3B-\uFF3E\uFF40\uFF5B-\uFF65\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFFF]|\uD800[\uDC0C\uDC27\uDC3B\uDC3E\uDC4E\uDC4F\uDC5E-\uDC7F\uDCFB-\uDD3F\uDD75-\uDDFC\uDDFE-\uDE7F\uDE9D-\uDE9F\uDED1-\uDEDF\uDEE1-\uDEFF\uDF20-\uDF2C\uDF4B-\uDF4F\uDF7B-\uDF7F\uDF9E\uDF9F\uDFC4-\uDFC7\uDFD0\uDFD6-\uDFFF]|\uD801[\uDC9E\uDC9F\uDCAA-\uDCAF\uDCD4-\uDCD7\uDCFC-\uDCFF\uDD28-\uDD2F\uDD64-\uDDFF\uDF37-\uDF3F\uDF56-\uDF5F\uDF68-\uDFFF]|\uD802[\uDC06\uDC07\uDC09\uDC36\uDC39-\uDC3B\uDC3D\uDC3E\uDC56-\uDC5F\uDC77-\uDC7F\uDC9F-\uDCDF\uDCF3\uDCF6-\uDCFF\uDD16-\uDD1F\uDD3A-\uDD7F\uDDB8-\uDDBD\uDDC0-\uDDFF\uDE04\uDE07-\uDE0B\uDE14\uDE18\uDE36\uDE37\uDE3B-\uDE3E\uDE40-\uDE5F\uDE7D-\uDE7F\uDE9D-\uDEBF\uDEC8\uDEE7-\uDEFF\uDF36-\uDF3F\uDF56-\uDF5F\uDF73-\uDF7F\uDF92-\uDFFF]|\uD803[\uDC49-\uDC7F\uDCB3-\uDCBF\uDCF3-\uDCFF\uDD28-\uDD2F\uDD3A-\uDE7F\uDEAA\uDEAD-\uDEAF\uDEB2-\uDEFF\uDF1D-\uDF26\uDF28-\uDF2F\uDF51-\uDFAF\uDFC5-\uDFDF\uDFF7-\uDFFF]|\uD804[\uDC47-\uDC65\uDC70-\uDC7E\uDCBB-\uDCCF\uDCE9-\uDCEF\uDCFA-\uDCFF\uDD35\uDD40-\uDD43\uDD48-\uDD4F\uDD74\uDD75\uDD77-\uDD7F\uDDC5-\uDDC8\uDDCD\uDDDB\uDDDD-\uDDFF\uDE12\uDE38-\uDE3D\uDE3F-\uDE7F\uDE87\uDE89\uDE8E\uDE9E\uDEA9-\uDEAF\uDEEB-\uDEEF\uDEFA-\uDEFF\uDF04\uDF0D\uDF0E\uDF11\uDF12\uDF29\uDF31\uDF34\uDF3A\uDF45\uDF46\uDF49\uDF4A\uDF4E\uDF4F\uDF51-\uDF56\uDF58-\uDF5C\uDF64\uDF65\uDF6D-\uDF6F\uDF75-\uDFFF]|\uD805[\uDC4B-\uDC4F\uDC5A-\uDC5D\uDC62-\uDC7F\uDCC6\uDCC8-\uDCCF\uDCDA-\uDD7F\uDDB6\uDDB7\uDDC1-\uDDD7\uDDDE-\uDDFF\uDE41-\uDE43\uDE45-\uDE4F\uDE5A-\uDE7F\uDEB9-\uDEBF\uDECA-\uDEFF\uDF1B\uDF1C\uDF2C-\uDF2F\uDF3A-\uDFFF]|\uD806[\uDC3B-\uDC9F\uDCEA-\uDCFE\uDD07\uDD08\uDD0A\uDD0B\uDD14\uDD17\uDD36\uDD39\uDD3A\uDD44-\uDD4F\uDD5A-\uDD9F\uDDA8\uDDA9\uDDD8\uDDD9\uDDE2\uDDE5-\uDDFF\uDE3F-\uDE46\uDE48-\uDE4F\uDE9A-\uDE9C\uDE9E-\uDEBF\uDEF9-\uDFFF]|\uD807[\uDC09\uDC37\uDC41-\uDC4F\uDC5A-\uDC71\uDC90\uDC91\uDCA8\uDCB7-\uDCFF\uDD07\uDD0A\uDD37-\uDD39\uDD3B\uDD3E\uDD48-\uDD4F\uDD5A-\uDD5F\uDD66\uDD69\uDD8F\uDD92\uDD99-\uDD9F\uDDAA-\uDEDF\uDEF7-\uDFAF\uDFB1-\uDFFF]|\uD808[\uDF9A-\uDFFF]|\uD809[\uDC6F-\uDC7F\uDD44-\uDFFF]|[\uD80A\uD80B\uD80E-\uD810\uD812-\uD819\uD824-\uD82B\uD82D\uD82E\uD830-\uD833\uD837\uD839\uD83D\uD83F\uD87B-\uD87D\uD87F\uD885-\uDB3F\uDB41-\uDBFF][\uDC00-\uDFFF]|\uD80D[\uDC2F-\uDFFF]|\uD811[\uDE47-\uDFFF]|\uD81A[\uDE39-\uDE3F\uDE5F\uDE6A-\uDECF\uDEEE\uDEEF\uDEF5-\uDEFF\uDF37-\uDF3F\uDF44-\uDF4F\uDF5A-\uDF62\uDF78-\uDF7C\uDF90-\uDFFF]|\uD81B[\uDC00-\uDE3F\uDE80-\uDEFF\uDF4B-\uDF4E\uDF88-\uDF8E\uDFA0-\uDFDF\uDFE2\uDFE5-\uDFEF\uDFF2-\uDFFF]|\uD821[\uDFF8-\uDFFF]|\uD823[\uDCD6-\uDCFF\uDD09-\uDFFF]|\uD82C[\uDD1F-\uDD4F\uDD53-\uDD63\uDD68-\uDD6F\uDEFC-\uDFFF]|\uD82F[\uDC6B-\uDC6F\uDC7D-\uDC7F\uDC89-\uDC8F\uDC9A-\uDC9C\uDC9F-\uDFFF]|\uD834[\uDC00-\uDD64\uDD6A-\uDD6C\uDD73-\uDD7A\uDD83\uDD84\uDD8C-\uDDA9\uDDAE-\uDE41\uDE45-\uDFFF]|\uD835[\uDC55\uDC9D\uDCA0\uDCA1\uDCA3\uDCA4\uDCA7\uDCA8\uDCAD\uDCBA\uDCBC\uDCC4\uDD06\uDD0B\uDD0C\uDD15\uDD1D\uDD3A\uDD3F\uDD45\uDD47-\uDD49\uDD51\uDEA6\uDEA7\uDEC1\uDEDB\uDEFB\uDF15\uDF35\uDF4F\uDF6F\uDF89\uDFA9\uDFC3\uDFCC\uDFCD]|\uD836[\uDC00-\uDDFF\uDE37-\uDE3A\uDE6D-\uDE74\uDE76-\uDE83\uDE85-\uDE9A\uDEA0\uDEB0-\uDFFF]|\uD838[\uDC07\uDC19\uDC1A\uDC22\uDC25\uDC2B-\uDCFF\uDD2D-\uDD2F\uDD3E\uDD3F\uDD4A-\uDD4D\uDD4F-\uDEBF\uDEFA-\uDFFF]|\uD83A[\uDCC5-\uDCCF\uDCD7-\uDCFF\uDD4C-\uDD4F\uDD5A-\uDFFF]|\uD83B[\uDC00-\uDDFF\uDE04\uDE20\uDE23\uDE25\uDE26\uDE28\uDE33\uDE38\uDE3A\uDE3C-\uDE41\uDE43-\uDE46\uDE48\uDE4A\uDE4C\uDE50\uDE53\uDE55\uDE56\uDE58\uDE5A\uDE5C\uDE5E\uDE60\uDE63\uDE65\uDE66\uDE6B\uDE73\uDE78\uDE7D\uDE7F\uDE8A\uDE9C-\uDEA0\uDEA4\uDEAA\uDEBC-\uDFFF]|\uD83C[\uDC00-\uDD2F\uDD4A-\uDD4F\uDD6A-\uDD6F\uDD8A-\uDFFF]|\uD83E[\uDC00-\uDFEF\uDFFA-\uDFFF]|\uD869[\uDEDE-\uDEFF]|\uD86D[\uDF35-\uDF3F]|\uD86E[\uDC1E\uDC1F]|\uD873[\uDEA2-\uDEAF]|\uD87A[\uDFE1-\uDFFF]|\uD87E[\uDE1E-\uDFFF]|\uD884[\uDF4B-\uDFFF]|\uDB40[\uDC00-\uDCFF\uDDF0-\uDFFF]/g; - - function slugify(heading: string): string { - const slugifiedHeading = heading.trim() - .toLowerCase() - .replace(githubSlugReplaceRegex, '') - .replace(/\s/g, '-'); // Replace whitespace with - - - return slugifiedHeading; - } - - // Copied from https://github.com/markedjs/marked-gfm-heading-id/blob/main/src/index.js - // Removed logic for handling duplicate header ids for now - - // unescape from marked helpers - const unescapeTest = /&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig; - function unescape(html: string) { - // explicitly match decimal, hex, and named HTML entities - return html.replace(unescapeTest, (_, n) => { - n = n.toLowerCase(); - if (n === 'colon') { return ':'; } - if (n.charAt(0) === '#') { - return n.charAt(1) === 'x' - ? String.fromCharCode(parseInt(n.substring(2), 16)) - : String.fromCharCode(+n.substring(1)); - } - return ''; - }); - } - - export function gfmHeadingId({ prefix = '', globalSlugs = false } = {}): marked.MarkedExtension { - return { - // hooks: { - // preprocess(src: string) { - // if (!globalSlugs) { - // resetHeadings(); - // } - // return src; - // }, - // }, - renderer: { - heading({ tokens, depth }) { - const text = this.parser.parseInline(tokens); - const raw = unescape(this.parser.parseInline(tokens, this.parser.textRenderer)) - .trim() - .replace(/<[!\/a-z].*?>/gi, ''); - const level = depth; - const id = `${prefix}${slugify(raw)}`; - // const heading = { level, text, id, raw }; - // headings.push(heading); - return `${text}\n`; - }, - }, - }; - } -} diff --git a/src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts b/src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts index 9403958b8f0..456dff11557 100644 --- a/src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts +++ b/src/vs/workbench/contrib/markdown/browser/markdownSettingRenderer.ts @@ -3,18 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from '../../../../nls.js'; -import { IPreferencesService, ISetting } from '../../../services/preferences/common/preferences.js'; -import { settingKeyToDisplayFormat } from '../../preferences/browser/settingsTreeModels.js'; -import { URI } from '../../../../base/common/uri.js'; -import { ConfigurationTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { ActionViewItem } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; import { IAction } from '../../../../base/common/actions.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; -import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; +import type { Tokens } from '../../../../base/common/marked/marked.js'; import { Schemas } from '../../../../base/common/network.js'; -import { Tokens } from '../../../../base/common/marked/marked.js'; +import { URI } from '../../../../base/common/uri.js'; +import * as nls from '../../../../nls.js'; +import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; +import { ConfigurationTarget, IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; +import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; +import { IPreferencesService, ISetting } from '../../../services/preferences/common/preferences.js'; +import { settingKeyToDisplayFormat } from '../../preferences/browser/settingsTreeModels.js'; export class SimpleSettingRenderer { private readonly codeSettingRegex: RegExp; diff --git a/src/vs/workbench/contrib/markdown/browser/markedGfmHeadingIdPlugin.ts b/src/vs/workbench/contrib/markdown/browser/markedGfmHeadingIdPlugin.ts new file mode 100644 index 00000000000..39ed0d95270 --- /dev/null +++ b/src/vs/workbench/contrib/markdown/browser/markedGfmHeadingIdPlugin.ts @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as marked from '../../../../base/common/marked/marked.js'; + +// Copied from https://github.com/Flet/github-slugger since we can't use esm yet. +// eslint-disable-next-line no-control-regex, no-misleading-character-class +const githubSlugReplaceRegex = /[\0-\x1F!-,\.\/:-@\[-\^`\{-\xA9\xAB-\xB4\xB6-\xB9\xBB-\xBF\xD7\xF7\u02C2-\u02C5\u02D2-\u02DF\u02E5-\u02EB\u02ED\u02EF-\u02FF\u0375\u0378\u0379\u037E\u0380-\u0385\u0387\u038B\u038D\u03A2\u03F6\u0482\u0530\u0557\u0558\u055A-\u055F\u0589-\u0590\u05BE\u05C0\u05C3\u05C6\u05C8-\u05CF\u05EB-\u05EE\u05F3-\u060F\u061B-\u061F\u066A-\u066D\u06D4\u06DD\u06DE\u06E9\u06FD\u06FE\u0700-\u070F\u074B\u074C\u07B2-\u07BF\u07F6-\u07F9\u07FB\u07FC\u07FE\u07FF\u082E-\u083F\u085C-\u085F\u086B-\u089F\u08B5\u08C8-\u08D2\u08E2\u0964\u0965\u0970\u0984\u098D\u098E\u0991\u0992\u09A9\u09B1\u09B3-\u09B5\u09BA\u09BB\u09C5\u09C6\u09C9\u09CA\u09CF-\u09D6\u09D8-\u09DB\u09DE\u09E4\u09E5\u09F2-\u09FB\u09FD\u09FF\u0A00\u0A04\u0A0B-\u0A0E\u0A11\u0A12\u0A29\u0A31\u0A34\u0A37\u0A3A\u0A3B\u0A3D\u0A43-\u0A46\u0A49\u0A4A\u0A4E-\u0A50\u0A52-\u0A58\u0A5D\u0A5F-\u0A65\u0A76-\u0A80\u0A84\u0A8E\u0A92\u0AA9\u0AB1\u0AB4\u0ABA\u0ABB\u0AC6\u0ACA\u0ACE\u0ACF\u0AD1-\u0ADF\u0AE4\u0AE5\u0AF0-\u0AF8\u0B00\u0B04\u0B0D\u0B0E\u0B11\u0B12\u0B29\u0B31\u0B34\u0B3A\u0B3B\u0B45\u0B46\u0B49\u0B4A\u0B4E-\u0B54\u0B58-\u0B5B\u0B5E\u0B64\u0B65\u0B70\u0B72-\u0B81\u0B84\u0B8B-\u0B8D\u0B91\u0B96-\u0B98\u0B9B\u0B9D\u0BA0-\u0BA2\u0BA5-\u0BA7\u0BAB-\u0BAD\u0BBA-\u0BBD\u0BC3-\u0BC5\u0BC9\u0BCE\u0BCF\u0BD1-\u0BD6\u0BD8-\u0BE5\u0BF0-\u0BFF\u0C0D\u0C11\u0C29\u0C3A-\u0C3C\u0C45\u0C49\u0C4E-\u0C54\u0C57\u0C5B-\u0C5F\u0C64\u0C65\u0C70-\u0C7F\u0C84\u0C8D\u0C91\u0CA9\u0CB4\u0CBA\u0CBB\u0CC5\u0CC9\u0CCE-\u0CD4\u0CD7-\u0CDD\u0CDF\u0CE4\u0CE5\u0CF0\u0CF3-\u0CFF\u0D0D\u0D11\u0D45\u0D49\u0D4F-\u0D53\u0D58-\u0D5E\u0D64\u0D65\u0D70-\u0D79\u0D80\u0D84\u0D97-\u0D99\u0DB2\u0DBC\u0DBE\u0DBF\u0DC7-\u0DC9\u0DCB-\u0DCE\u0DD5\u0DD7\u0DE0-\u0DE5\u0DF0\u0DF1\u0DF4-\u0E00\u0E3B-\u0E3F\u0E4F\u0E5A-\u0E80\u0E83\u0E85\u0E8B\u0EA4\u0EA6\u0EBE\u0EBF\u0EC5\u0EC7\u0ECE\u0ECF\u0EDA\u0EDB\u0EE0-\u0EFF\u0F01-\u0F17\u0F1A-\u0F1F\u0F2A-\u0F34\u0F36\u0F38\u0F3A-\u0F3D\u0F48\u0F6D-\u0F70\u0F85\u0F98\u0FBD-\u0FC5\u0FC7-\u0FFF\u104A-\u104F\u109E\u109F\u10C6\u10C8-\u10CC\u10CE\u10CF\u10FB\u1249\u124E\u124F\u1257\u1259\u125E\u125F\u1289\u128E\u128F\u12B1\u12B6\u12B7\u12BF\u12C1\u12C6\u12C7\u12D7\u1311\u1316\u1317\u135B\u135C\u1360-\u137F\u1390-\u139F\u13F6\u13F7\u13FE-\u1400\u166D\u166E\u1680\u169B-\u169F\u16EB-\u16ED\u16F9-\u16FF\u170D\u1715-\u171F\u1735-\u173F\u1754-\u175F\u176D\u1771\u1774-\u177F\u17D4-\u17D6\u17D8-\u17DB\u17DE\u17DF\u17EA-\u180A\u180E\u180F\u181A-\u181F\u1879-\u187F\u18AB-\u18AF\u18F6-\u18FF\u191F\u192C-\u192F\u193C-\u1945\u196E\u196F\u1975-\u197F\u19AC-\u19AF\u19CA-\u19CF\u19DA-\u19FF\u1A1C-\u1A1F\u1A5F\u1A7D\u1A7E\u1A8A-\u1A8F\u1A9A-\u1AA6\u1AA8-\u1AAF\u1AC1-\u1AFF\u1B4C-\u1B4F\u1B5A-\u1B6A\u1B74-\u1B7F\u1BF4-\u1BFF\u1C38-\u1C3F\u1C4A-\u1C4C\u1C7E\u1C7F\u1C89-\u1C8F\u1CBB\u1CBC\u1CC0-\u1CCF\u1CD3\u1CFB-\u1CFF\u1DFA\u1F16\u1F17\u1F1E\u1F1F\u1F46\u1F47\u1F4E\u1F4F\u1F58\u1F5A\u1F5C\u1F5E\u1F7E\u1F7F\u1FB5\u1FBD\u1FBF-\u1FC1\u1FC5\u1FCD-\u1FCF\u1FD4\u1FD5\u1FDC-\u1FDF\u1FED-\u1FF1\u1FF5\u1FFD-\u203E\u2041-\u2053\u2055-\u2070\u2072-\u207E\u2080-\u208F\u209D-\u20CF\u20F1-\u2101\u2103-\u2106\u2108\u2109\u2114\u2116-\u2118\u211E-\u2123\u2125\u2127\u2129\u212E\u213A\u213B\u2140-\u2144\u214A-\u214D\u214F-\u215F\u2189-\u24B5\u24EA-\u2BFF\u2C2F\u2C5F\u2CE5-\u2CEA\u2CF4-\u2CFF\u2D26\u2D28-\u2D2C\u2D2E\u2D2F\u2D68-\u2D6E\u2D70-\u2D7E\u2D97-\u2D9F\u2DA7\u2DAF\u2DB7\u2DBF\u2DC7\u2DCF\u2DD7\u2DDF\u2E00-\u2E2E\u2E30-\u3004\u3008-\u3020\u3030\u3036\u3037\u303D-\u3040\u3097\u3098\u309B\u309C\u30A0\u30FB\u3100-\u3104\u3130\u318F-\u319F\u31C0-\u31EF\u3200-\u33FF\u4DC0-\u4DFF\u9FFD-\u9FFF\uA48D-\uA4CF\uA4FE\uA4FF\uA60D-\uA60F\uA62C-\uA63F\uA673\uA67E\uA6F2-\uA716\uA720\uA721\uA789\uA78A\uA7C0\uA7C1\uA7CB-\uA7F4\uA828-\uA82B\uA82D-\uA83F\uA874-\uA87F\uA8C6-\uA8CF\uA8DA-\uA8DF\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA954-\uA95F\uA97D-\uA97F\uA9C1-\uA9CE\uA9DA-\uA9DF\uA9FF\uAA37-\uAA3F\uAA4E\uAA4F\uAA5A-\uAA5F\uAA77-\uAA79\uAAC3-\uAADA\uAADE\uAADF\uAAF0\uAAF1\uAAF7-\uAB00\uAB07\uAB08\uAB0F\uAB10\uAB17-\uAB1F\uAB27\uAB2F\uAB5B\uAB6A-\uAB6F\uABEB\uABEE\uABEF\uABFA-\uABFF\uD7A4-\uD7AF\uD7C7-\uD7CA\uD7FC-\uD7FF\uE000-\uF8FF\uFA6E\uFA6F\uFADA-\uFAFF\uFB07-\uFB12\uFB18-\uFB1C\uFB29\uFB37\uFB3D\uFB3F\uFB42\uFB45\uFBB2-\uFBD2\uFD3E-\uFD4F\uFD90\uFD91\uFDC8-\uFDEF\uFDFC-\uFDFF\uFE10-\uFE1F\uFE30-\uFE32\uFE35-\uFE4C\uFE50-\uFE6F\uFE75\uFEFD-\uFF0F\uFF1A-\uFF20\uFF3B-\uFF3E\uFF40\uFF5B-\uFF65\uFFBF-\uFFC1\uFFC8\uFFC9\uFFD0\uFFD1\uFFD8\uFFD9\uFFDD-\uFFFF]|\uD800[\uDC0C\uDC27\uDC3B\uDC3E\uDC4E\uDC4F\uDC5E-\uDC7F\uDCFB-\uDD3F\uDD75-\uDDFC\uDDFE-\uDE7F\uDE9D-\uDE9F\uDED1-\uDEDF\uDEE1-\uDEFF\uDF20-\uDF2C\uDF4B-\uDF4F\uDF7B-\uDF7F\uDF9E\uDF9F\uDFC4-\uDFC7\uDFD0\uDFD6-\uDFFF]|\uD801[\uDC9E\uDC9F\uDCAA-\uDCAF\uDCD4-\uDCD7\uDCFC-\uDCFF\uDD28-\uDD2F\uDD64-\uDDFF\uDF37-\uDF3F\uDF56-\uDF5F\uDF68-\uDFFF]|\uD802[\uDC06\uDC07\uDC09\uDC36\uDC39-\uDC3B\uDC3D\uDC3E\uDC56-\uDC5F\uDC77-\uDC7F\uDC9F-\uDCDF\uDCF3\uDCF6-\uDCFF\uDD16-\uDD1F\uDD3A-\uDD7F\uDDB8-\uDDBD\uDDC0-\uDDFF\uDE04\uDE07-\uDE0B\uDE14\uDE18\uDE36\uDE37\uDE3B-\uDE3E\uDE40-\uDE5F\uDE7D-\uDE7F\uDE9D-\uDEBF\uDEC8\uDEE7-\uDEFF\uDF36-\uDF3F\uDF56-\uDF5F\uDF73-\uDF7F\uDF92-\uDFFF]|\uD803[\uDC49-\uDC7F\uDCB3-\uDCBF\uDCF3-\uDCFF\uDD28-\uDD2F\uDD3A-\uDE7F\uDEAA\uDEAD-\uDEAF\uDEB2-\uDEFF\uDF1D-\uDF26\uDF28-\uDF2F\uDF51-\uDFAF\uDFC5-\uDFDF\uDFF7-\uDFFF]|\uD804[\uDC47-\uDC65\uDC70-\uDC7E\uDCBB-\uDCCF\uDCE9-\uDCEF\uDCFA-\uDCFF\uDD35\uDD40-\uDD43\uDD48-\uDD4F\uDD74\uDD75\uDD77-\uDD7F\uDDC5-\uDDC8\uDDCD\uDDDB\uDDDD-\uDDFF\uDE12\uDE38-\uDE3D\uDE3F-\uDE7F\uDE87\uDE89\uDE8E\uDE9E\uDEA9-\uDEAF\uDEEB-\uDEEF\uDEFA-\uDEFF\uDF04\uDF0D\uDF0E\uDF11\uDF12\uDF29\uDF31\uDF34\uDF3A\uDF45\uDF46\uDF49\uDF4A\uDF4E\uDF4F\uDF51-\uDF56\uDF58-\uDF5C\uDF64\uDF65\uDF6D-\uDF6F\uDF75-\uDFFF]|\uD805[\uDC4B-\uDC4F\uDC5A-\uDC5D\uDC62-\uDC7F\uDCC6\uDCC8-\uDCCF\uDCDA-\uDD7F\uDDB6\uDDB7\uDDC1-\uDDD7\uDDDE-\uDDFF\uDE41-\uDE43\uDE45-\uDE4F\uDE5A-\uDE7F\uDEB9-\uDEBF\uDECA-\uDEFF\uDF1B\uDF1C\uDF2C-\uDF2F\uDF3A-\uDFFF]|\uD806[\uDC3B-\uDC9F\uDCEA-\uDCFE\uDD07\uDD08\uDD0A\uDD0B\uDD14\uDD17\uDD36\uDD39\uDD3A\uDD44-\uDD4F\uDD5A-\uDD9F\uDDA8\uDDA9\uDDD8\uDDD9\uDDE2\uDDE5-\uDDFF\uDE3F-\uDE46\uDE48-\uDE4F\uDE9A-\uDE9C\uDE9E-\uDEBF\uDEF9-\uDFFF]|\uD807[\uDC09\uDC37\uDC41-\uDC4F\uDC5A-\uDC71\uDC90\uDC91\uDCA8\uDCB7-\uDCFF\uDD07\uDD0A\uDD37-\uDD39\uDD3B\uDD3E\uDD48-\uDD4F\uDD5A-\uDD5F\uDD66\uDD69\uDD8F\uDD92\uDD99-\uDD9F\uDDAA-\uDEDF\uDEF7-\uDFAF\uDFB1-\uDFFF]|\uD808[\uDF9A-\uDFFF]|\uD809[\uDC6F-\uDC7F\uDD44-\uDFFF]|[\uD80A\uD80B\uD80E-\uD810\uD812-\uD819\uD824-\uD82B\uD82D\uD82E\uD830-\uD833\uD837\uD839\uD83D\uD83F\uD87B-\uD87D\uD87F\uD885-\uDB3F\uDB41-\uDBFF][\uDC00-\uDFFF]|\uD80D[\uDC2F-\uDFFF]|\uD811[\uDE47-\uDFFF]|\uD81A[\uDE39-\uDE3F\uDE5F\uDE6A-\uDECF\uDEEE\uDEEF\uDEF5-\uDEFF\uDF37-\uDF3F\uDF44-\uDF4F\uDF5A-\uDF62\uDF78-\uDF7C\uDF90-\uDFFF]|\uD81B[\uDC00-\uDE3F\uDE80-\uDEFF\uDF4B-\uDF4E\uDF88-\uDF8E\uDFA0-\uDFDF\uDFE2\uDFE5-\uDFEF\uDFF2-\uDFFF]|\uD821[\uDFF8-\uDFFF]|\uD823[\uDCD6-\uDCFF\uDD09-\uDFFF]|\uD82C[\uDD1F-\uDD4F\uDD53-\uDD63\uDD68-\uDD6F\uDEFC-\uDFFF]|\uD82F[\uDC6B-\uDC6F\uDC7D-\uDC7F\uDC89-\uDC8F\uDC9A-\uDC9C\uDC9F-\uDFFF]|\uD834[\uDC00-\uDD64\uDD6A-\uDD6C\uDD73-\uDD7A\uDD83\uDD84\uDD8C-\uDDA9\uDDAE-\uDE41\uDE45-\uDFFF]|\uD835[\uDC55\uDC9D\uDCA0\uDCA1\uDCA3\uDCA4\uDCA7\uDCA8\uDCAD\uDCBA\uDCBC\uDCC4\uDD06\uDD0B\uDD0C\uDD15\uDD1D\uDD3A\uDD3F\uDD45\uDD47-\uDD49\uDD51\uDEA6\uDEA7\uDEC1\uDEDB\uDEFB\uDF15\uDF35\uDF4F\uDF6F\uDF89\uDFA9\uDFC3\uDFCC\uDFCD]|\uD836[\uDC00-\uDDFF\uDE37-\uDE3A\uDE6D-\uDE74\uDE76-\uDE83\uDE85-\uDE9A\uDEA0\uDEB0-\uDFFF]|\uD838[\uDC07\uDC19\uDC1A\uDC22\uDC25\uDC2B-\uDCFF\uDD2D-\uDD2F\uDD3E\uDD3F\uDD4A-\uDD4D\uDD4F-\uDEBF\uDEFA-\uDFFF]|\uD83A[\uDCC5-\uDCCF\uDCD7-\uDCFF\uDD4C-\uDD4F\uDD5A-\uDFFF]|\uD83B[\uDC00-\uDDFF\uDE04\uDE20\uDE23\uDE25\uDE26\uDE28\uDE33\uDE38\uDE3A\uDE3C-\uDE41\uDE43-\uDE46\uDE48\uDE4A\uDE4C\uDE50\uDE53\uDE55\uDE56\uDE58\uDE5A\uDE5C\uDE5E\uDE60\uDE63\uDE65\uDE66\uDE6B\uDE73\uDE78\uDE7D\uDE7F\uDE8A\uDE9C-\uDEA0\uDEA4\uDEAA\uDEBC-\uDFFF]|\uD83C[\uDC00-\uDD2F\uDD4A-\uDD4F\uDD6A-\uDD6F\uDD8A-\uDFFF]|\uD83E[\uDC00-\uDFEF\uDFFA-\uDFFF]|\uD869[\uDEDE-\uDEFF]|\uD86D[\uDF35-\uDF3F]|\uD86E[\uDC1E\uDC1F]|\uD873[\uDEA2-\uDEAF]|\uD87A[\uDFE1-\uDFFF]|\uD87E[\uDE1E-\uDFFF]|\uD884[\uDF4B-\uDFFF]|\uDB40[\uDC00-\uDCFF\uDDF0-\uDFFF]/g; + +function slugify(heading: string): string { + const slugifiedHeading = heading.trim() + .toLowerCase() + .replace(githubSlugReplaceRegex, '') + .replace(/\s/g, '-'); // Replace whitespace with - + + return slugifiedHeading; +} + +// Copied from https://github.com/markedjs/marked-gfm-heading-id/blob/main/src/index.js +// Removed logic for handling duplicate header ids for now + +// unescape from marked helpers +const unescapeTest = /&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig; +function unescape(html: string) { + // explicitly match decimal, hex, and named HTML entities + return html.replace(unescapeTest, (_, n) => { + n = n.toLowerCase(); + if (n === 'colon') { return ':'; } + if (n.charAt(0) === '#') { + return n.charAt(1) === 'x' + ? String.fromCharCode(parseInt(n.substring(2), 16)) + : String.fromCharCode(+n.substring(1)); + } + return ''; + }); +} + +export function markedGfmHeadingIdPlugin({ prefix = '', globalSlugs = false } = {}): marked.MarkedExtension { + return { + // hooks: { + // preprocess(src: string) { + // if (!globalSlugs) { + // resetHeadings(); + // } + // return src; + // }, + // }, + renderer: { + heading({ tokens, depth }) { + const text = this.parser.parseInline(tokens); + const raw = unescape(this.parser.parseInline(tokens, this.parser.textRenderer)) + .trim() + .replace(/<[!\/a-z].*?>/gi, ''); + const level = depth; + const id = `${prefix}${slugify(raw)}`; + // const heading = { level, text, id, raw }; + // headings.push(heading); + return `${text}\n`; + }, + }, + }; +} diff --git a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts index 25632f11e6b..225089a5908 100644 --- a/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts +++ b/src/vs/workbench/contrib/multiDiffEditor/browser/multiDiffEditorInput.ts @@ -11,8 +11,7 @@ import { Disposable, DisposableStore, IDisposable, IReference } from '../../../. import { parse } from '../../../../base/common/marshalling.js'; import { Schemas } from '../../../../base/common/network.js'; import { deepClone } from '../../../../base/common/objects.js'; -import { ObservableLazyPromise, autorun, derived, observableFromEvent, observableValue } from '../../../../base/common/observable.js'; -import { ValueWithChangeEventFromObservable, constObservable, mapObservableArrayCached, observableFromValueWithChangeEvent, recomputeInitiallyAndOnChange } from '../../../../base/common/observableInternal/utils.js'; +import { ObservableLazyPromise, ValueWithChangeEventFromObservable, autorun, constObservable, derived, mapObservableArrayCached, observableFromEvent, observableFromValueWithChangeEvent, observableValue, recomputeInitiallyAndOnChange } from '../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { isDefined, isObject } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; @@ -28,10 +27,10 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { IEditorConfiguration } from '../../../browser/parts/editor/textEditor.js'; import { DEFAULT_EDITOR_ASSOCIATION, EditorInputCapabilities, EditorInputWithOptions, GroupIdentifier, IEditorSerializer, IResourceMultiDiffEditorInput, IRevertOptions, ISaveOptions, IUntypedEditorInput } from '../../../common/editor.js'; import { EditorInput, IEditorCloseHandler } from '../../../common/editor/editorInput.js'; -import { MultiDiffEditorIcon } from './icons.contribution.js'; -import { IMultiDiffSourceResolverService, IResolvedMultiDiffSource, MultiDiffEditorItem } from './multiDiffSourceResolverService.js'; import { IEditorResolverService, RegisteredEditorPriority } from '../../../services/editor/common/editorResolverService.js'; import { ILanguageSupport, ITextFileEditorModel, ITextFileService } from '../../../services/textfile/common/textfiles.js'; +import { MultiDiffEditorIcon } from './icons.contribution.js'; +import { IMultiDiffSourceResolverService, IResolvedMultiDiffSource, MultiDiffEditorItem } from './multiDiffSourceResolverService.js'; export class MultiDiffEditorInput extends EditorInput implements ILanguageSupport { public static fromResourceMultiDiffEditorInput(input: IResourceMultiDiffEditorInput, instantiationService: IInstantiationService): MultiDiffEditorInput { diff --git a/src/vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts b/src/vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts index c907958ab68..cdd3aac6b29 100644 --- a/src/vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts +++ b/src/vs/workbench/contrib/multiDiffEditor/browser/scmMultiDiffSourceResolver.ts @@ -4,18 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable } from '../../../../base/common/lifecycle.js'; -import { observableFromEvent, waitForState } from '../../../../base/common/observable.js'; -import { ValueWithChangeEventFromObservable } from '../../../../base/common/observableInternal/utils.js'; +import { observableFromEvent, ValueWithChangeEventFromObservable, waitForState } from '../../../../base/common/observable.js'; import { URI, UriComponents } from '../../../../base/common/uri.js'; import { IMultiDiffEditorOptions } from '../../../../editor/browser/widget/multiDiffEditor/multiDiffEditorWidgetImpl.js'; import { localize2 } from '../../../../nls.js'; import { Action2 } from '../../../../platform/actions/common/actions.js'; import { ContextKeyValue } from '../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import { IMultiDiffSourceResolver, IMultiDiffSourceResolverService, IResolvedMultiDiffSource, MultiDiffEditorItem } from './multiDiffSourceResolverService.js'; -import { ISCMRepository, ISCMResourceGroup, ISCMService } from '../../scm/common/scm.js'; -import { IEditorService } from '../../../services/editor/common/editorService.js'; import { IActivityService, ProgressBadge } from '../../../services/activity/common/activity.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { ISCMRepository, ISCMResourceGroup, ISCMService } from '../../scm/common/scm.js'; +import { IMultiDiffSourceResolver, IMultiDiffSourceResolverService, IResolvedMultiDiffSource, MultiDiffEditorItem } from './multiDiffSourceResolverService.js'; export class ScmMultiDiffSourceResolver implements IMultiDiffSourceResolver { private static readonly _scheme = 'scm-multi-diff-source'; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesTree.ts b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesTree.ts index 80bb7aaa87c..9daccd6d5ba 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesTree.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesTree.ts @@ -10,9 +10,9 @@ import { ITreeNode, ITreeRenderer } from '../../../../../../base/browser/ui/tree import { FuzzyScore } from '../../../../../../base/common/filters.js'; import { DisposableStore } from '../../../../../../base/common/lifecycle.js'; import { localize } from '../../../../../../nls.js'; -import { IHoverService } from '../../../../../../platform/hover/browser/hover.js'; +import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { WorkbenchObjectTree } from '../../../../../../platform/list/browser/listService.js'; -import { renderExpressionValue } from '../../../../debug/browser/baseDebugView.js'; +import { DebugExpressionRenderer } from '../../../../debug/browser/debugExpressionRenderer.js'; import { INotebookVariableElement } from './notebookVariablesDataSource.js'; const $ = dom.$; @@ -40,15 +40,16 @@ export interface IVariableTemplateData { export class NotebookVariableRenderer implements ITreeRenderer { + private expressionRenderer: DebugExpressionRenderer; + static readonly ID = 'variableElement'; get templateId(): string { return NotebookVariableRenderer.ID; } - constructor( - @IHoverService private readonly _hoverService: IHoverService - ) { + constructor(@IInstantiationService instantiationService: IInstantiationService) { + this.expressionRenderer = instantiationService.createInstance(DebugExpressionRenderer); } renderTemplate(container: HTMLElement): IVariableTemplateData { @@ -66,10 +67,11 @@ export class NotebookVariableRenderer implements ITreeRenderer, index: number, templateData: IVariableTemplateData, height: number | undefined): void { diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesView.ts b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesView.ts index bd70b53a444..253488522d5 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesView.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookVariablesView.ts @@ -88,7 +88,7 @@ export class NotebookVariablesView extends ViewPane { 'notebookVariablesTree', container, new NotebookVariablesDelegate(), - [new NotebookVariableRenderer(this.hoverService)], + [this.instantiationService.createInstance(NotebookVariableRenderer)], this.dataSource, { accessibilityProvider: new NotebookVariableAccessibilityProvider(), diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts index 726ea7f950d..90839d12a98 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/outline/notebookOutline.ts @@ -521,7 +521,6 @@ export class NotebookCellOutline implements IOutline { private _breadcrumbsDataSource!: IBreadcrumbsDataSource; // view settings - private gotoShowCodeCellSymbols: boolean; private outlineShowCodeCellSymbols: boolean; // getters @@ -562,7 +561,6 @@ export class NotebookCellOutline implements IOutline { @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @INotebookExecutionStateService private readonly _notebookExecutionStateService: INotebookExecutionStateService, ) { - this.gotoShowCodeCellSymbols = this._configurationService.getValue(NotebookSetting.gotoSymbolsAllSymbols); this.outlineShowCodeCellSymbols = this._configurationService.getValue(NotebookSetting.outlineShowCodeCellSymbols); this.initializeOutline(); @@ -633,8 +631,7 @@ export class NotebookCellOutline implements IOutline { // recompute symbols when the configuration changes (recompute state - and therefore recompute active - is also called within compute symbols) this._disposables.add(this._configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(NotebookSetting.gotoSymbolsAllSymbols) || e.affectsConfiguration(NotebookSetting.outlineShowCodeCellSymbols)) { - this.gotoShowCodeCellSymbols = this._configurationService.getValue(NotebookSetting.gotoSymbolsAllSymbols); + if (e.affectsConfiguration(NotebookSetting.outlineShowCodeCellSymbols)) { this.outlineShowCodeCellSymbols = this._configurationService.getValue(NotebookSetting.outlineShowCodeCellSymbols); this.computeSymbols(); } @@ -700,13 +697,14 @@ export class NotebookCellOutline implements IOutline { } private async computeSymbols(cancelToken: CancellationToken = CancellationToken.None) { - if (this._target === OutlineTarget.QuickPick && this.gotoShowCodeCellSymbols) { - await this._outlineDataSourceReference?.object?.computeFullSymbols(cancelToken); - } else if (this._target === OutlineTarget.OutlinePane && this.outlineShowCodeCellSymbols) { + if (this._target === OutlineTarget.OutlinePane && this.outlineShowCodeCellSymbols) { // No need to wait for this, we want the outline to show up quickly. - void this._outlineDataSourceReference?.object?.computeFullSymbols(cancelToken); + void this.doComputeSymbols(cancelToken); } } + public async doComputeSymbols(cancelToken: CancellationToken): Promise { + await this._outlineDataSourceReference?.object?.computeFullSymbols(cancelToken); + } private async delayedComputeSymbols() { this.delayerRecomputeState.cancel(); this.delayerRecomputeActive.cancel(); @@ -814,7 +812,7 @@ export class NotebookOutlineCreator implements IOutlineCreator reg.dispose(); @@ -824,8 +822,14 @@ export class NotebookOutlineCreator implements IOutlineCreator | undefined> { - return this._instantiationService.createInstance(NotebookCellOutline, editor, target); + async createOutline(editor: INotebookEditorPane, target: OutlineTarget, cancelToken: CancellationToken): Promise | undefined> { + const outline = this._instantiationService.createInstance(NotebookCellOutline, editor, target); + if (target === OutlineTarget.QuickPick) { + // The quickpick creates the outline on demand + // so we need to ensure the symbols are pre-cached before the entries are syncronously requested + await outline.doComputeSymbols(cancelToken); + } + return outline; } } diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffCellEditorOptions.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffCellEditorOptions.ts index a14887cabe9..14c76fe0a75 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffCellEditorOptions.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffCellEditorOptions.ts @@ -10,15 +10,19 @@ import { IEditorOptions } from '../../../../../editor/common/config/editorOption * Do not leave at 12, when at 12 and we have whitespace and only one line, * then there's not enough space for the button `Show Whitespace Differences` */ -export const fixedEditorPaddingSingleLineCells = { +const fixedEditorPaddingSingleLineCells = { top: 24, bottom: 24 }; -export const fixedEditorPadding = { +const fixedEditorPadding = { top: 12, bottom: 12 }; +export function getEditorPadding(lineCount: number) { + return lineCount === 1 ? fixedEditorPaddingSingleLineCells : fixedEditorPadding; +} + export const fixedEditorOptions: IEditorOptions = { padding: fixedEditorPadding, scrollBeyondLastLine: false, diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts index 664df4fa56a..9c4043426b0 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffComponents.ts @@ -38,7 +38,7 @@ import { IConfigurationService } from '../../../../../platform/configuration/com import { IThemeService } from '../../../../../platform/theme/common/themeService.js'; import { WorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js'; import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js'; -import { fixedDiffEditorOptions, fixedEditorOptions, fixedEditorPadding, fixedEditorPaddingSingleLineCells } from './diffCellEditorOptions.js'; +import { fixedDiffEditorOptions, fixedEditorOptions, getEditorPadding } from './diffCellEditorOptions.js'; import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js'; import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js'; import { DiffEditorWidget } from '../../../../../editor/browser/widget/diffEditor/diffEditorWidget.js'; @@ -48,6 +48,7 @@ import { localize } from '../../../../../nls.js'; import { Emitter } from '../../../../../base/common/event.js'; import { ITextResourceConfigurationService } from '../../../../../editor/common/services/textResourceConfiguration.js'; import { getFormattedMetadataJSON } from '../../common/model/notebookCellTextModel.js'; +import { IDiffEditorOptions } from '../../../../../editor/common/config/editorOptions.js'; export function getOptimizedNestedCodeEditorWidgetOptions(): ICodeEditorWidgetOptions { return { @@ -102,9 +103,9 @@ class PropertyHeader extends Disposable { readonly notebookEditor: INotebookTextDiffEditor, readonly accessor: { updateInfoRendering: (renderOutput: boolean) => void; - checkIfModified: (cell: DiffElementCellViewModelBase) => false | { reason: string | undefined }; - getFoldingState: (cell: DiffElementCellViewModelBase) => PropertyFoldingState; - updateFoldingState: (cell: DiffElementCellViewModelBase, newState: PropertyFoldingState) => void; + checkIfModified: () => false | { reason: string | undefined }; + getFoldingState: () => PropertyFoldingState; + updateFoldingState: (newState: PropertyFoldingState) => void; unChangedLabel: string; changedLabel: string; prefix: string; @@ -141,9 +142,7 @@ class PropertyHeader extends Disposable { return undefined; } }, this.menuService, this.contextKeyService, this.contextMenuService, this.keybindingService, this.commandService, this.telemetryService)); - this._toolbar.context = { - cell: this.cell - }; + this._toolbar.context = this.cell; const scopedContextKeyService = this.contextKeyService.createScoped(cellToolbarContainer); this._register(scopedContextKeyService); @@ -165,8 +164,8 @@ class PropertyHeader extends Disposable { target === this._foldingIndicator || this._foldingIndicator.contains(target) || target === metadataStatus || metadataStatus.contains(target) ) { - const oldFoldingState = this.accessor.getFoldingState(this.cell); - this.accessor.updateFoldingState(this.cell, oldFoldingState === PropertyFoldingState.Expanded ? PropertyFoldingState.Collapsed : PropertyFoldingState.Expanded); + const oldFoldingState = this.accessor.getFoldingState(); + this.accessor.updateFoldingState(oldFoldingState === PropertyFoldingState.Expanded ? PropertyFoldingState.Collapsed : PropertyFoldingState.Expanded); this._updateFoldingIcon(); this.accessor.updateInfoRendering(this.cell.renderOutput); } @@ -179,7 +178,7 @@ class PropertyHeader extends Disposable { this.updateMenu(); this._updateFoldingIcon(); - const metadataChanged = this.accessor.checkIfModified(this.cell); + const metadataChanged = this.accessor.checkIfModified(); if (this._propertyChanged) { this._propertyChanged.set(!!metadataChanged); } @@ -199,7 +198,7 @@ class PropertyHeader extends Disposable { } private updateMenu() { - const metadataChanged = this.accessor.checkIfModified(this.cell); + const metadataChanged = this.accessor.checkIfModified(); if (metadataChanged) { const actions: IAction[] = []; createAndFillInActionBarActions(this._menu, { shouldForwardArgs: true }, actions); @@ -210,7 +209,7 @@ class PropertyHeader extends Disposable { } private _updateFoldingIcon() { - if (this.accessor.getFoldingState(this.cell) === PropertyFoldingState.Collapsed) { + if (this.accessor.getFoldingState() === PropertyFoldingState.Collapsed) { DOM.reset(this._foldingIndicator, renderIcon(collapsedIcon)); this._propertyExpanded?.set(false); } else { @@ -516,20 +515,36 @@ abstract class AbstractElementRenderer extends Disposable { modifiedEditor: getOptimizedNestedCodeEditorWidgetOptions() }); + if (this.cell.unchangedRegionsService.options.enabled) { + this._metadataEditor.updateOptions({ hideUnchangedRegions: this.cell.unchangedRegionsService.options }); + } + this._metadataEditorDisposeStore.add(this.cell.unchangedRegionsService.options.onDidChangeEnablement(() => { + if (this._metadataEditor) { + this._metadataEditor.updateOptions({ hideUnchangedRegions: this.cell.unchangedRegionsService.options }); + } + })); + + this.layout({ metadataHeight: true }); this._metadataEditorDisposeStore.add(this._metadataEditor); this._metadataEditorContainer?.classList.add('diff'); - const originalMetadataModel = await this.textModelService.createModelReference(CellUri.generateCellPropertyUri(this.cell.originalDocument.uri, this.cell.original.handle, Schemas.vscodeNotebookCellMetadata)); - const modifiedMetadataModel = await this.textModelService.createModelReference(CellUri.generateCellPropertyUri(this.cell.modifiedDocument.uri, this.cell.modified.handle, Schemas.vscodeNotebookCellMetadata)); - this._metadataEditor.setModel({ + const [originalMetadataModel, modifiedMetadataModel] = await Promise.all([ + this.textModelService.createModelReference(CellUri.generateCellPropertyUri(this.cell.originalDocument.uri, this.cell.original.handle, Schemas.vscodeNotebookCellMetadata)), + this.textModelService.createModelReference(CellUri.generateCellPropertyUri(this.cell.modifiedDocument.uri, this.cell.modified.handle, Schemas.vscodeNotebookCellMetadata)) + ]); + this._metadataEditorDisposeStore.add(originalMetadataModel); + this._metadataEditorDisposeStore.add(modifiedMetadataModel); + const vm = this._metadataEditor.createViewModel({ original: originalMetadataModel.object.textEditorModel, modified: modifiedMetadataModel.object.textEditorModel }); - - this._metadataEditorDisposeStore.add(originalMetadataModel); - this._metadataEditorDisposeStore.add(modifiedMetadataModel); + // Reduces flicker (compute this before setting the model) + // Else when the model is set, the height of the editor will be x, after diff is computed, then height will be y. + // & that results in flicker. + await vm.waitForDiff(); + this._metadataEditor.setModel(vm); this.cell.metadataHeight = this._metadataEditor.getContentHeight(); @@ -847,10 +862,8 @@ abstract class SingleSideDiffElement extends AbstractElementRenderer { this.cell.editorHeight = 0; return; } - - const lineCount = this.nestedCellViewModel.textModel.textBuffer.getLineCount(); const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; - const editorHeight = lineCount * lineHeight + fixedEditorPadding.top + fixedEditorPadding.bottom; + const editorHeight = this.cell.computeInputEditorHeight(lineHeight); this._editorContainer.style.height = `${editorHeight}px`; this._editorContainer.style.display = 'block'; @@ -888,9 +901,9 @@ abstract class SingleSideDiffElement extends AbstractElementRenderer { this.notebookEditor, { updateInfoRendering: () => renderSourceEditor(), - checkIfModified: (_) => ({ reason: undefined }), - getFoldingState: (cell) => cell.cellFoldingState, - updateFoldingState: (cell, state) => cell.cellFoldingState = state, + checkIfModified: () => ({ reason: undefined }), + getFoldingState: () => this.cell.cellFoldingState, + updateFoldingState: (state) => this.cell.cellFoldingState = state, unChangedLabel: 'Input', changedLabel: 'Input', prefix: 'input', @@ -955,14 +968,14 @@ abstract class SingleSideDiffElement extends AbstractElementRenderer { this.notebookEditor, { updateInfoRendering: this.updateMetadataRendering.bind(this), - checkIfModified: (cell) => { - return cell.checkMetadataIfModified(); + checkIfModified: () => { + return this.cell.checkMetadataIfModified(); }, - getFoldingState: (cell) => { - return cell.metadataFoldingState; + getFoldingState: () => { + return this.cell.metadataFoldingState; }, - updateFoldingState: (cell, state) => { - cell.metadataFoldingState = state; + updateFoldingState: (state) => { + this.cell.metadataFoldingState = state; }, unChangedLabel: 'Metadata', changedLabel: 'Metadata changed', @@ -991,14 +1004,14 @@ abstract class SingleSideDiffElement extends AbstractElementRenderer { this.notebookEditor, { updateInfoRendering: this.updateOutputRendering.bind(this), - checkIfModified: (cell) => { - return cell.checkIfOutputsModified(); + checkIfModified: () => { + return this.cell.checkIfOutputsModified(); }, - getFoldingState: (cell) => { - return cell.outputFoldingState; + getFoldingState: () => { + return this.cell.outputFoldingState; }, - updateFoldingState: (cell, state) => { - cell.outputFoldingState = state; + updateFoldingState: (state) => { + this.cell.outputFoldingState = state; }, unChangedLabel: 'Outputs', changedLabel: 'Outputs changed', @@ -1352,14 +1365,14 @@ export class ModifiedElement extends AbstractElementRenderer { this.notebookEditor, { updateInfoRendering: this.updateMetadataRendering.bind(this), - checkIfModified: (cell) => { - return cell.checkMetadataIfModified(); + checkIfModified: () => { + return this.cell.checkMetadataIfModified(); }, - getFoldingState: (cell) => { - return cell.metadataFoldingState; + getFoldingState: () => { + return this.cell.metadataFoldingState; }, - updateFoldingState: (cell, state) => { - cell.metadataFoldingState = state; + updateFoldingState: (state) => { + this.cell.metadataFoldingState = state; }, unChangedLabel: 'Metadata', changedLabel: 'Metadata changed', @@ -1406,14 +1419,14 @@ export class ModifiedElement extends AbstractElementRenderer { this.notebookEditor, { updateInfoRendering: this.updateOutputRendering.bind(this), - checkIfModified: (cell) => { - return cell.checkIfOutputsModified(); + checkIfModified: () => { + return this.cell.checkIfOutputsModified(); }, - getFoldingState: (cell) => { - return cell.outputFoldingState; + getFoldingState: () => { + return this.cell.outputFoldingState; }, - updateFoldingState: (cell, state) => { - cell.outputFoldingState = state; + updateFoldingState: (state) => { + this.cell.outputFoldingState = state; }, unChangedLabel: 'Outputs', changedLabel: 'Outputs changed', @@ -1585,7 +1598,7 @@ export class ModifiedElement extends AbstractElementRenderer { const lineCount = modifiedCell.textModel.textBuffer.getLineCount(); const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; - const editorHeight = this.cell.layoutInfo.editorHeight !== 0 ? this.cell.layoutInfo.editorHeight : (lineCount * lineHeight) + fixedEditorPadding.top + fixedEditorPadding.bottom; + const editorHeight = this.cell.layoutInfo.editorHeight !== 0 ? this.cell.layoutInfo.editorHeight : this.cell.computeInputEditorHeight(lineHeight); this._editorContainer.style.height = `${editorHeight}px`; this._editorContainer.style.display = 'block'; @@ -1603,11 +1616,17 @@ export class ModifiedElement extends AbstractElementRenderer { // E.g. assume we have a cell with 1 line and we add some whitespace, // Then diff editor displays the button `Show Whitespace Differences`, however with 12 paddings on the top, the // button can get cut off. - if (lineCount === 1) { - this._editor.updateOptions({ - padding: fixedEditorPaddingSingleLineCells - }); + const options: IDiffEditorOptions = { + padding: getEditorPadding(lineCount) + }; + if (this.cell.unchangedRegionsService.options.enabled) { + options.hideUnchangedRegions = this.cell.unchangedRegionsService.options; } + this._editor.updateOptions(options); + this._register(this.cell.unchangedRegionsService.options.onDidChangeEnablement(() => { + options.hideUnchangedRegions = this.cell.unchangedRegionsService.options; + this._editor?.updateOptions(options); + })); this._editor.layout({ width: this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN, height: editorHeight @@ -1627,11 +1646,11 @@ export class ModifiedElement extends AbstractElementRenderer { this.notebookEditor, { updateInfoRendering: () => renderSourceEditor(), - checkIfModified: (cell) => { - return cell.modified?.textModel.getTextBufferHash() !== cell.original?.textModel.getTextBufferHash() ? { reason: undefined } : false; + checkIfModified: () => { + return this.cell.modified?.textModel.getTextBufferHash() !== this.cell.original?.textModel.getTextBufferHash() ? { reason: undefined } : false; }, - getFoldingState: (cell) => cell.cellFoldingState, - updateFoldingState: (cell, state) => cell.cellFoldingState = state, + getFoldingState: () => this.cell.cellFoldingState, + updateFoldingState: (state) => this.cell.cellFoldingState = state, unChangedLabel: 'Input', changedLabel: 'Input changed', prefix: 'input', @@ -1652,9 +1671,7 @@ export class ModifiedElement extends AbstractElementRenderer { this._toolbar = this.templateData.toolbar; - this._toolbar.context = { - cell: this.cell - }; + this._toolbar.context = this.cell; const refreshToolbar = () => { const ignore = this.textConfigurationService.getValue(this.cell.modified.uri, 'diffEditor.ignoreTrimWhitespace'); @@ -1683,25 +1700,28 @@ export class ModifiedElement extends AbstractElementRenderer { } private async _initializeSourceDiffEditor() { - const originalCell = this.cell.original; - const modifiedCell = this.cell.modified; - - const originalRef = await this.textModelService.createModelReference(originalCell.uri); - const modifiedRef = await this.textModelService.createModelReference(modifiedCell.uri); - - if (this._isDisposed) { - return; - } - - const textModel = originalRef.object.textEditorModel; - const modifiedTextModel = modifiedRef.object.textEditorModel; + const [originalRef, modifiedRef] = await Promise.all([ + this.textModelService.createModelReference(this.cell.original.uri), + this.textModelService.createModelReference(this.cell.modified.uri)]); this._register(originalRef); this._register(modifiedRef); - this._editor!.setModel({ - original: textModel, - modified: modifiedTextModel, - }); + if (this._isDisposed) { + originalRef.dispose(); + modifiedRef.dispose(); + return; + } + + const vm = this._register(this._editor!.createViewModel({ + original: originalRef.object.textEditorModel, + modified: modifiedRef.object.textEditorModel, + })); + + // Reduces flicker (compute this before setting the model) + // Else when the model is set, the height of the editor will be x, after diff is computed, then height will be y. + // & that results in flicker. + await vm.waitForDiff(); + this._editor!.setModel(vm); const handleViewStateChange = () => { this._editorViewStateChanged = true; diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts index 16c1b24c0c0..da39e4f6d3c 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts @@ -10,7 +10,7 @@ import { URI } from '../../../../../base/common/uri.js'; import { DiffEditorWidget } from '../../../../../editor/browser/widget/diffEditor/diffEditorWidget.js'; import { FontInfo } from '../../../../../editor/common/config/fontInfo.js'; import * as editorCommon from '../../../../../editor/common/editorCommon.js'; -import { fixedEditorPadding } from './diffCellEditorOptions.js'; +import { getEditorPadding } from './diffCellEditorOptions.js'; import { DiffNestedCellViewModel } from './diffNestedCellViewModel.js'; import { NotebookDiffEditorEventDispatcher, NotebookDiffViewEventType } from './eventDispatcher.js'; import { CellDiffViewModelLayoutChangeEvent, DIFF_CELL_MARGIN, DiffSide, IDiffElementLayoutInfo } from './notebookDiffEditorBrowser.js'; @@ -18,9 +18,16 @@ import { CellLayoutState, IGenericCellViewModel } from '../notebookBrowser.js'; import { NotebookLayoutInfo } from '../notebookViewEvents.js'; import { getFormattedMetadataJSON, NotebookCellTextModel } from '../../common/model/notebookCellTextModel.js'; import { NotebookTextModel } from '../../common/model/notebookTextModel.js'; -import { ICellOutput, INotebookTextModel, IOutputDto, IOutputItemDto } from '../../common/notebookCommon.js'; +import { CellUri, ICellOutput, INotebookTextModel, IOutputDto, IOutputItemDto } from '../../common/notebookCommon.js'; import { INotebookService } from '../../common/notebookService.js'; import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { IUnchangedEditorRegionsService } from './unchangedEditorRegions.js'; +import { Schemas } from '../../../../../base/common/network.js'; + +// From `.monaco-editor .diff-hidden-lines .center` in src/vs/editor/browser/widget/diffEditor/style.css +export const HeightOfHiddenLinesRegionInDiffEditor = 24; + +export const DefaultLineHeight = 17; export enum PropertyFoldingState { Expanded, @@ -186,6 +193,10 @@ export abstract class DiffElementCellViewModelBase extends DiffElementViewModelB return this.configurationService.getValue('notebook.diff.ignoreOutputs') || !!(this.mainDocumentTextModel?.transientOptions.transientOutputs); } + private get ignoreMetadata() { + return this.configurationService.getValue('notebook.diff.ignoreMetadata'); + } + private _sourceEditorViewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null = null; private _outputEditorViewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null = null; private _metadataEditorViewState: editorCommon.ICodeEditorViewState | editorCommon.IDiffEditorViewState | null = null; @@ -204,7 +215,9 @@ export abstract class DiffElementCellViewModelBase extends DiffElementViewModelB fontInfo: FontInfo | undefined; }, notebookService: INotebookService, - private readonly configurationService: IConfigurationService + public readonly index: number, + private readonly configurationService: IConfigurationService, + public readonly unchangedRegionsService: IUnchangedEditorRegionsService ) { super(mainDocumentTextModel, editorEventDispatcher, initData); this.original = original ? this._register(new DiffNestedCellViewModel(original, notebookService)) : undefined; @@ -217,10 +230,10 @@ export abstract class DiffElementCellViewModelBase extends DiffElementViewModelB editorMargin: 0, metadataHeight: 0, cellStatusHeight, - metadataStatusHeight: 25, + metadataStatusHeight: this.ignoreMetadata ? 0 : 25, rawOutputHeight: 0, outputTotalHeight: 0, - outputStatusHeight: 25, + outputStatusHeight: this.ignoreOutputs ? 0 : 25, outputMetadataHeight: 0, bodyMargin: 32, totalHeight: 82 + cellStatusHeight + editorHeight, @@ -230,8 +243,6 @@ export abstract class DiffElementCellViewModelBase extends DiffElementViewModelB this.cellFoldingState = modified?.getTextBufferHash() !== original?.getTextBufferHash() ? PropertyFoldingState.Expanded : PropertyFoldingState.Collapsed; this.metadataFoldingState = PropertyFoldingState.Collapsed; this.outputFoldingState = PropertyFoldingState.Collapsed; - - this._register(this.editorEventDispatcher.onDidChangeLayout(e => this._layoutInfoEmitter.fire({ outerWidth: true }))); } layoutChange() { @@ -246,14 +257,14 @@ export abstract class DiffElementCellViewModelBase extends DiffElementViewModelB case 'insert': { const lineCount = this.modified!.textModel.textBuffer.getLineCount(); - const editorHeight = lineCount * lineHeight + fixedEditorPadding.top + fixedEditorPadding.bottom; + const editorHeight = lineCount * lineHeight + getEditorPadding(lineCount).top + getEditorPadding(lineCount).bottom; return editorHeight; } case 'delete': case 'modified': { const lineCount = this.original!.textModel.textBuffer.getLineCount(); - const editorHeight = lineCount * lineHeight + fixedEditorPadding.top + fixedEditorPadding.bottom; + const editorHeight = lineCount * lineHeight + getEditorPadding(lineCount).top + getEditorPadding(lineCount).bottom; return editorHeight; } } @@ -364,7 +375,7 @@ export abstract class DiffElementCellViewModelBase extends DiffElementViewModelB getHeight(lineHeight: number) { if (this._layoutInfo.layoutState === CellLayoutState.Uninitialized) { - const editorHeight = this.cellFoldingState === PropertyFoldingState.Collapsed ? 0 : this.estimateEditorHeight(lineHeight); + const editorHeight = this.cellFoldingState === PropertyFoldingState.Collapsed ? 0 : this.computeInputEditorHeight(lineHeight); return this._computeTotalHeight(editorHeight); } else { return this._layoutInfo.totalHeight; @@ -385,15 +396,9 @@ export abstract class DiffElementCellViewModelBase extends DiffElementViewModelB return totalHeight; } - private estimateEditorHeight(lineHeight: number | undefined = 20): number { - const hasScrolling = false; - const verticalScrollbarHeight = hasScrolling ? 12 : 0; // take zoom level into account - // const editorPadding = this.viewContext.notebookOptions.computeEditorPadding(this.internalMetadata); + public computeInputEditorHeight(lineHeight: number): number { const lineCount = Math.max(this.original?.textModel.textBuffer.getLineCount() ?? 1, this.modified?.textModel.textBuffer.getLineCount() ?? 1); - return lineCount * lineHeight - + 24 // Top padding - + 12 // Bottom padding - + verticalScrollbarHeight; + return lineCount * lineHeight + getEditorPadding(lineCount).top + getEditorPadding(lineCount).bottom; } private _getOutputTotalHeight(rawOutputHeight: number, metadataHeight: number) { @@ -474,6 +479,10 @@ export class SideBySideDiffElementViewModel extends DiffElementCellViewModelBase override readonly modified!: DiffNestedCellViewModel; override readonly type: 'unchanged' | 'modified'; + /** + * The height of the editor when the unchanged lines are collapsed. + */ + private editorHeightWithUnchangedLinesCollapsed?: number; constructor( mainDocumentTextModel: NotebookTextModel, readonly otherDocumentTextModel: NotebookTextModel, @@ -487,7 +496,9 @@ export class SideBySideDiffElementViewModel extends DiffElementCellViewModelBase fontInfo: FontInfo | undefined; }, notebookService: INotebookService, - configurationService: IConfigurationService + configurationService: IConfigurationService, + index: number, + unchangedRegionsService: IUnchangedEditorRegionsService ) { super( mainDocumentTextModel, @@ -497,7 +508,9 @@ export class SideBySideDiffElementViewModel extends DiffElementCellViewModelBase editorEventDispatcher, initData, notebookService, - configurationService); + index, + configurationService, + unchangedRegionsService); this.type = type; @@ -629,6 +642,40 @@ export class SideBySideDiffElementViewModel extends DiffElementCellViewModelBase return this.modified; } } + + public override computeInputEditorHeight(lineHeight: number): number { + if (this.type === 'modified' && + typeof this.editorHeightWithUnchangedLinesCollapsed === 'number' && + this.unchangedRegionsService.options.enabled && + this.checkIfInputModified()) { + return this.editorHeightWithUnchangedLinesCollapsed; + } + + return super.computeInputEditorHeight(lineHeight); + } + + private async computeInputEditorHeightWithUnchangedLinesHidden() { + if (this.checkIfInputModified()) { + this.editorHeightWithUnchangedLinesCollapsed = this._layoutInfo.editorHeight = await this.unchangedRegionsService.computeEditorHeight(this.original.uri, this.modified.uri); + } + } + + private async computeMetadataEditorHeightWithUnchangedLinesHidden() { + if (this.checkMetadataIfModified()) { + const originalMetadataUri = CellUri.generateCellPropertyUri(this.originalDocument.uri, this.original.handle, Schemas.vscodeNotebookCellMetadata); + const modifiedMetadataUri = CellUri.generateCellPropertyUri(this.modifiedDocument.uri, this.modified.handle, Schemas.vscodeNotebookCellMetadata); + this._layoutInfo.metadataHeight = await this.unchangedRegionsService.computeEditorHeight(originalMetadataUri, modifiedMetadataUri); + } + } + + public async computeEditorHeights() { + if (this.type === 'unchanged' || !this.unchangedRegionsService.options.enabled) { + return; + } + + await Promise.all([this.computeInputEditorHeightWithUnchangedLinesHidden(), this.computeMetadataEditorHeightWithUnchangedLinesHidden()]); + } + } export class SingleSideDiffElementViewModel extends DiffElementCellViewModelBase { @@ -667,9 +714,11 @@ export class SingleSideDiffElementViewModel extends DiffElementCellViewModelBase fontInfo: FontInfo | undefined; }, notebookService: INotebookService, - configurationService: IConfigurationService + configurationService: IConfigurationService, + unchangedRegionsService: IUnchangedEditorRegionsService, + index: number ) { - super(mainDocumentTextModel, original, modified, type, editorEventDispatcher, initData, notebookService, configurationService); + super(mainDocumentTextModel, original, modified, type, editorEventDispatcher, initData, notebookService, index, configurationService, unchangedRegionsService); this.type = type; this._register(this.cellViewModel.onDidChangeOutputLayout(() => { diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts index fe0f33e5e75..8e5666d8ecb 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts @@ -61,6 +61,30 @@ registerAction2(class extends Action2 { } }); +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'notebook.diff.cell.toggleCollapseUnchangedRegions', + title: localize2('notebook.diff.cell.toggleCollapseUnchangedRegions', 'Toggle Collapse Unchanged Regions'), + icon: Codicon.map, + toggled: ContextKeyExpr.has('config.diffEditor.hideUnchangedRegions.enabled'), + precondition: ActiveEditorContext.isEqualTo(NotebookTextDiffEditor.ID), + menu: { + id: MenuId.EditorTitle, + group: 'navigation', + when: ActiveEditorContext.isEqualTo(NotebookTextDiffEditor.ID), + }, + }); + } + + run(accessor: ServicesAccessor, ...args: unknown[]): void { + const configurationService = accessor.get(IConfigurationService); + const newValue = !configurationService.getValue('diffEditor.hideUnchangedRegions.enabled'); + configurationService.updateValue('diffEditor.hideUnchangedRegions.enabled', newValue); + } +}); + + registerAction2(class extends Action2 { constructor() { super({ @@ -332,29 +356,29 @@ registerAction2(class extends Action2 { } ); } - run(accessor: ServicesAccessor, context?: { cell: DiffElementCellViewModelBase }) { + run(accessor: ServicesAccessor, context?: DiffElementCellViewModelBase) { if (!context) { return; } - if (!(context.cell instanceof SideBySideDiffElementViewModel)) { + if (!(context instanceof SideBySideDiffElementViewModel)) { return; } - const original = context.cell.original; - const modified = context.cell.modified; + const original = context.original; + const modified = context.modified; - const modifiedCellIndex = context.cell.mainDocumentTextModel.cells.indexOf(modified.textModel); + const modifiedCellIndex = context.mainDocumentTextModel.cells.indexOf(modified.textModel); if (modifiedCellIndex === -1) { return; } const rawEdits: ICellEditOperation[] = [{ editType: CellEditType.Metadata, index: modifiedCellIndex, metadata: original.metadata }]; - if (context.cell.original.language && context.cell.modified.language !== context.cell.original.language) { - rawEdits.push({ editType: CellEditType.CellLanguage, index: modifiedCellIndex, language: context.cell.original.language }); + if (context.original.language && context.modified.language !== context.original.language) { + rawEdits.push({ editType: CellEditType.CellLanguage, index: modifiedCellIndex, language: context.original.language }); } - context.cell.modifiedDocument.applyEdits(rawEdits, true, undefined, () => undefined, undefined, true); + context.modifiedDocument.applyEdits(rawEdits, true, undefined, () => undefined, undefined, true); } }); @@ -372,12 +396,12 @@ registerAction2(class extends Action2 { // } // ); // } -// run(accessor: ServicesAccessor, context?: { cell: DiffElementViewModelBase }) { +// run(accessor: ServicesAccessor, context?: DiffElementViewModelBase) { // if (!context) { // return; // } -// context.cell.renderOutput = true; +// context.renderOutput = true; // } // }); @@ -397,12 +421,12 @@ registerAction2(class extends Action2 { } ); } - run(accessor: ServicesAccessor, context?: { cell: DiffElementCellViewModelBase }) { + run(accessor: ServicesAccessor, context?: DiffElementCellViewModelBase) { if (!context) { return; } - context.cell.renderOutput = !context.cell.renderOutput; + context.renderOutput = !context.renderOutput; } }); @@ -422,24 +446,24 @@ registerAction2(class extends Action2 { } ); } - run(accessor: ServicesAccessor, context?: { cell: DiffElementCellViewModelBase }) { + run(accessor: ServicesAccessor, context?: DiffElementCellViewModelBase) { if (!context) { return; } - if (!(context.cell instanceof SideBySideDiffElementViewModel)) { + if (!(context instanceof SideBySideDiffElementViewModel)) { return; } - const original = context.cell.original; - const modified = context.cell.modified; + const original = context.original; + const modified = context.modified; - const modifiedCellIndex = context.cell.mainDocumentTextModel.cells.indexOf(modified.textModel); + const modifiedCellIndex = context.mainDocumentTextModel.cells.indexOf(modified.textModel); if (modifiedCellIndex === -1) { return; } - context.cell.mainDocumentTextModel.applyEdits([{ + context.mainDocumentTextModel.applyEdits([{ editType: CellEditType.Output, index: modifiedCellIndex, outputs: original.outputs }], true, undefined, () => undefined, undefined, true); } @@ -464,8 +488,8 @@ registerAction2(class extends Action2 { } ); } - run(accessor: ServicesAccessor, context?: { cell: DiffElementCellViewModelBase }) { - const cell = context?.cell; + run(accessor: ServicesAccessor, context?: DiffElementCellViewModelBase) { + const cell = context; if (!cell?.modified) { return; } @@ -495,13 +519,13 @@ registerAction2(class extends Action2 { } ); } - run(accessor: ServicesAccessor, context?: { cell: DiffElementCellViewModelBase }) { + run(accessor: ServicesAccessor, context?: DiffElementCellViewModelBase) { if (!context) { return; } - const original = context.cell.original; - const modified = context.cell.modified; + const original = context.original; + const modified = context.modified; if (!original || !modified) { return; diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts index 79710bf55df..973b3ef0587 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffEditor.ts @@ -46,6 +46,10 @@ import { NotebookDiffOverviewRuler } from './notebookDiffOverviewRuler.js'; import { registerZIndex, ZIndex } from '../../../../../platform/layout/browser/zIndexRegistry.js'; import { NotebookDiffViewModel } from './notebookDiffViewModel.js'; import { INotebookService } from '../../common/notebookService.js'; +import { IEditorWorkerService } from '../../../../../editor/common/services/editorWorker.js'; +import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; +import { ITextResourceConfigurationService } from '../../../../../editor/common/services/textResourceConfiguration.js'; +import { UnchangedEditorRegionsService } from './unchangedEditorRegions.js'; const $ = DOM.$; @@ -150,6 +154,9 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD @ITelemetryService telemetryService: ITelemetryService, @IStorageService storageService: IStorageService, @INotebookService private readonly notebookService: INotebookService, + @IEditorWorkerService private readonly editorWorkerService: IEditorWorkerService, + @ITextModelService private readonly textModelResolverService: ITextModelService, + @ITextResourceConfigurationService private readonly textConfigurationService: ITextResourceConfigurationService ) { super(NotebookTextDiffEditor.ID, group, telemetryService, themeService, storageService); this._notebookOptions = instantiationService.createInstance(NotebookOptions, this.window, false, undefined); @@ -511,7 +518,8 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD })); if (this._model) { - const vm = this.notebookDiffViewModel = this._register(new NotebookDiffViewModel(this._model, this.notebookEditorWorkerService, this.configurationService, this._eventDispatcher!, this.notebookService, this.fontInfo)); + const unchangedEditorRegions = this._localStore.add(new UnchangedEditorRegionsService(this.configurationService, this.editorWorkerService, this.textModelResolverService, this.textConfigurationService, this.fontInfo.lineHeight)); + const vm = this.notebookDiffViewModel = this._register(new NotebookDiffViewModel(this._model, this.notebookEditorWorkerService, this.configurationService, this._eventDispatcher!, this.notebookService, unchangedEditorRegions, this.fontInfo, undefined)); this._localStore.add(this.notebookDiffViewModel.onDidChangeItems(e => { this._list.splice(e.start, e.deleteCount, e.elements); if (this.isOverviewRulerEnabled()) { @@ -609,11 +617,11 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD }, 10); } - private pendingLayouts = new WeakMap(); + private pendingLayouts = new WeakMap(); - layoutNotebookCell(cell: DiffElementCellViewModelBase, height: number) { - const relayout = (cell: DiffElementCellViewModelBase, height: number) => { + layoutNotebookCell(cell: IDiffElementViewModelBase, height: number) { + const relayout = (cell: IDiffElementViewModelBase, height: number) => { this._list.updateElementHeight2(cell, height); }; diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffViewModel.ts index 0d5c1f6fe1d..6c22cefa0ce 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffViewModel.ts @@ -20,6 +20,7 @@ import { NotebookTextModel } from '../../common/model/notebookTextModel.js'; import { CellUri, INotebookDiffEditorModel, INotebookDiffResult } from '../../common/notebookCommon.js'; import { INotebookService } from '../../common/notebookService.js'; import { INotebookEditorWorkerService } from '../../common/services/notebookWorkerService.js'; +import { IUnchangedEditorRegionsService } from './unchangedEditorRegions.js'; export class NotebookDiffViewModel extends Disposable implements INotebookDiffViewModel, IValueWithChangeEvent { private readonly placeholderAndRelatedCells = new Map(); @@ -76,6 +77,7 @@ export class NotebookDiffViewModel extends Disposable implements INotebookDiffVi private readonly configurationService: IConfigurationService, private readonly eventDispatcher: NotebookDiffEditorEventDispatcher, private readonly notebookService: INotebookService, + private readonly unchangedRegionsService: IUnchangedEditorRegionsService, private readonly fontInfo?: FontInfo, private readonly excludeUnchangedPlaceholder?: boolean, ) { @@ -134,7 +136,7 @@ export class NotebookDiffViewModel extends Disposable implements INotebookDiffVi if (isEqual(cellDiffInfo, this.originalCellViewModels, this.model)) { return; } else { - this.updateViewModels(cellDiffInfo); + await this.updateViewModels(cellDiffInfo); this.updateDiffEditorItems(); return { firstChangeIndex }; } @@ -192,8 +194,8 @@ export class NotebookDiffViewModel extends Disposable implements INotebookDiffVi this._onDidChange.fire(); } - private updateViewModels(cellDiffInfo: CellDiffInfo[]) { - const cellViewModels = createDiffViewModels(this.configurationService, this.model, this.eventDispatcher, cellDiffInfo, this.fontInfo, this.notebookService); + private async updateViewModels(cellDiffInfo: CellDiffInfo[]) { + const cellViewModels = await createDiffViewModels(this.configurationService, this.model, this.eventDispatcher, cellDiffInfo, this.fontInfo, this.notebookService, this.unchangedRegionsService); const oldLength = this._items.length; this.clear(); this._items.splice(0, oldLength); @@ -237,6 +239,8 @@ export class NotebookDiffViewModel extends Disposable implements INotebookDiffVi } }); + // Note, ensure all of the height calculations are done before firing the event. + // This is to ensure that the diff editor is not resized multiple times, thereby avoiding flickering. this._onDidChangeItems.fire({ start: 0, deleteCount: oldLength, elements: this._items }); } } @@ -391,7 +395,7 @@ function isEqual(cellDiffInfo: CellDiffInfo[], viewModels: DiffElementCellViewMo return true; } -function createDiffViewModels(configurationService: IConfigurationService, model: INotebookDiffEditorModel, eventDispatcher: NotebookDiffEditorEventDispatcher, computedCellDiffs: CellDiffInfo[], fontInfo: FontInfo | undefined, notebookService: INotebookService) { +async function createDiffViewModels(configurationService: IConfigurationService, model: INotebookDiffEditorModel, eventDispatcher: NotebookDiffEditorEventDispatcher, computedCellDiffs: CellDiffInfo[], fontInfo: FontInfo | undefined, notebookService: INotebookService, unchangedRegionsService: IUnchangedEditorRegionsService) { const originalModel = model.original.notebook; const modifiedModel = model.modified.notebook; const initData = { @@ -399,8 +403,7 @@ function createDiffViewModels(configurationService: IConfigurationService, model outputStatusHeight: configurationService.getValue('notebook.diff.ignoreOutputs') || !!(modifiedModel.transientOptions.transientOutputs) ? 0 : 25, fontInfo }; - - return computedCellDiffs.map(diff => { + return Promise.all(computedCellDiffs.map(async (diff) => { switch (diff.type) { case 'delete': { return new SingleSideDiffElementViewModel( @@ -412,7 +415,9 @@ function createDiffViewModels(configurationService: IConfigurationService, model eventDispatcher, initData, notebookService, - configurationService + configurationService, + unchangedRegionsService, + diff.originalCellIndex ); } case 'insert': { @@ -425,11 +430,13 @@ function createDiffViewModels(configurationService: IConfigurationService, model eventDispatcher, initData, notebookService, - configurationService + configurationService, + unchangedRegionsService, + diff.modifiedCellIndex ); } case 'modified': { - return new SideBySideDiffElementViewModel( + const viewModel = new SideBySideDiffElementViewModel( model.modified.notebook, model.original.notebook, originalModel.cells[diff.originalCellIndex], @@ -438,8 +445,15 @@ function createDiffViewModels(configurationService: IConfigurationService, model eventDispatcher, initData, notebookService, - configurationService + configurationService, + diff.originalCellIndex, + unchangedRegionsService ); + // Reduces flicker (compute this before setting the model) + // Else when the model is set, the height of the editor will be x, after diff is computed, then height will be y. + // & that results in flicker. + await viewModel.computeEditorHeights(); + return viewModel; } case 'unchanged': { return new SideBySideDiffElementViewModel( @@ -450,11 +464,13 @@ function createDiffViewModels(configurationService: IConfigurationService, model 'unchanged', eventDispatcher, initData, notebookService, - configurationService + configurationService, + diff.originalCellIndex, + unchangedRegionsService ); } } - }); + })); } function computeModifiedLCS(change: IDiffChange, originalModel: NotebookTextModel, modifiedModel: NotebookTextModel) { diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookMultiDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookMultiDiffEditor.ts index 7f6f35699d0..3df5bc39b5e 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookMultiDiffEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookMultiDiffEditor.ts @@ -39,6 +39,7 @@ import type { DocumentDiffItemViewModel, MultiDiffEditorViewModel } from '../../ import type { URI } from '../../../../../base/common/uri.js'; import { type IDiffElementViewModelBase } from './diffElementViewModel.js'; import { autorun, transaction } from '../../../../../base/common/observable.js'; +import { UnchangedEditorRegionsService } from './unchangedEditorRegions.js'; export class NotebookMultiTextDiffEditor extends EditorPane { private _multiDiffEditorWidget?: MultiDiffEditorWidget; @@ -111,7 +112,7 @@ export class NotebookMultiTextDiffEditor extends EditorPane { this._model = model; } const eventDispatcher = this.modelSpecificResources.add(new NotebookDiffEditorEventDispatcher()); - this.viewModel = this.modelSpecificResources.add(new NotebookDiffViewModel(model, this.notebookEditorWorkerService, this.configurationService, eventDispatcher, this.notebookService, undefined, true)); + this.viewModel = this.modelSpecificResources.add(new NotebookDiffViewModel(model, this.notebookEditorWorkerService, this.configurationService, eventDispatcher, this.notebookService, UnchangedEditorRegionsService.Empty, undefined, true)); await this.viewModel.computeDiff(this.modelSpecificResources.add(new CancellationTokenSource()).token); this.ctxHasUnchangedCells.set(this.viewModel.hasUnchangedCells); this.ctxHasUnchangedCells.set(this.viewModel.hasUnchangedCells); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/unchangedEditorRegions.ts b/src/vs/workbench/contrib/notebook/browser/diff/unchangedEditorRegions.ts new file mode 100644 index 00000000000..a365cbcb822 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/unchangedEditorRegions.ts @@ -0,0 +1,142 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from '../../../../../base/common/event.js'; +import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; +import { URI } from '../../../../../base/common/uri.js'; +import { UnchangedRegion } from '../../../../../editor/browser/widget/diffEditor/diffEditorViewModel.js'; +import { IEditorWorkerService } from '../../../../../editor/common/services/editorWorker.js'; +import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; +import { ITextResourceConfigurationService } from '../../../../../editor/common/services/textResourceConfiguration.js'; +import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; +import { getEditorPadding } from './diffCellEditorOptions.js'; +import { HeightOfHiddenLinesRegionInDiffEditor } from './diffElementViewModel.js'; + +export type UnchangedEditorRegionOptions = { + enabled: boolean; + contextLineCount: number; + minimumLineCount: number; + revealLineCount: number; + onDidChangeEnablement: Event; +}; + +export interface IUnchangedEditorRegionsService { + readonly options: Readonly; + + /** + * Given two URIs, compute the height of the editor with unchanged regions collapsed. + * @param originalUri + * @param modifiedUri + */ + computeEditorHeight(originalUri: URI, modifiedUri: URI): Promise; +} + +export class UnchangedEditorRegionsService extends Disposable implements IUnchangedEditorRegionsService { + public readonly options: Readonly; + constructor(configurationService: IConfigurationService, + private readonly editorWorkerService: IEditorWorkerService, + private readonly textModelResolverService: ITextModelService, + private readonly textConfigurationService: ITextResourceConfigurationService, + private readonly lineHeight: number + ) { + super(); + this.options = this._register(createHideUnchangedRegionOptions(configurationService)); + } + + public static Empty: IUnchangedEditorRegionsService = { + options: { + enabled: false, + contextLineCount: 0, + minimumLineCount: 0, + revealLineCount: 0, + onDidChangeEnablement: Event.None, + }, + computeEditorHeight: (_originalUri: URI, _modifiedUri: URI) => Promise.resolve(0) + }; + + public async computeEditorHeight( + originalUri: URI, + modifiedUri: URI) { + const { numberOfUnchangedRegions, numberOfVisibleLines } = await computeInputUnchangedLines(originalUri, modifiedUri, this.options, this.editorWorkerService, this.textModelResolverService, this.textConfigurationService); + const lineCount = numberOfVisibleLines; + const unchangeRegionsHeight = numberOfUnchangedRegions * HeightOfHiddenLinesRegionInDiffEditor; + // TODO: When we have a horizontal scrollbar, we need to add 12 to the height. + // Right now there's no way to determine if a horizontal scrollbar is visible in the editor. + return lineCount * this.lineHeight + getEditorPadding(lineCount).top + getEditorPadding(lineCount).bottom + unchangeRegionsHeight; + + } +} + +function createHideUnchangedRegionOptions(configurationService: IConfigurationService): UnchangedEditorRegionOptions & { dispose: () => void } { + const disposables = new DisposableStore(); + const unchangedRegionsEnablementEmitter = disposables.add(new Emitter()); + + const options = { + enabled: configurationService.getValue('diffEditor.hideUnchangedRegions.enabled'), + minimumLineCount: configurationService.getValue('diffEditor.hideUnchangedRegions.minimumLineCount'), + contextLineCount: configurationService.getValue('diffEditor.hideUnchangedRegions.contextLineCount'), + revealLineCount: configurationService.getValue('diffEditor.hideUnchangedRegions.revealLineCount'), + // We only care about enable/disablement. + // If user changes counters when a diff editor is open, we do not care, might as well ask user to reload. + // Simpler and almost never going to happen. + onDidChangeEnablement: unchangedRegionsEnablementEmitter.event.bind(unchangedRegionsEnablementEmitter), + dispose: () => disposables.dispose() + }; + + disposables.add(configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('diffEditor.hideUnchangedRegions.minimumLineCount')) { + options.minimumLineCount = configurationService.getValue('diffEditor.hideUnchangedRegions.minimumLineCount'); + } + if (e.affectsConfiguration('diffEditor.hideUnchangedRegions.contextLineCount')) { + options.contextLineCount = configurationService.getValue('diffEditor.hideUnchangedRegions.contextLineCount'); + } + if (e.affectsConfiguration('diffEditor.hideUnchangedRegions.revealLineCount')) { + options.revealLineCount = configurationService.getValue('diffEditor.hideUnchangedRegions.revealLineCount'); + } + if (e.affectsConfiguration('diffEditor.hideUnchangedRegions.enabled')) { + options.enabled = configurationService.getValue('diffEditor.hideUnchangedRegions.enabled'); + unchangedRegionsEnablementEmitter.fire(options.enabled); + } + + })); + + return options; +} + +async function computeInputUnchangedLines(originalUri: URI, + modifiedUri: URI, + unchangedRegionOptions: UnchangedEditorRegionOptions, + editorWorkerService: IEditorWorkerService, + textModelResolverService: ITextModelService, + textConfigurationService: ITextResourceConfigurationService +) { + // Ensure we have resolved the cell text models. + const [originalModel, modifiedModel] = await Promise.all([textModelResolverService.createModelReference(originalUri), textModelResolverService.createModelReference(modifiedUri)]); + + try { + const ignoreTrimWhitespace = textConfigurationService.getValue(originalUri, 'diffEditor.ignoreTrimWhitespace'); + const diff = await editorWorkerService.computeDiff(originalUri, modifiedUri, { + ignoreTrimWhitespace, + maxComputationTimeMs: 0, + computeMoves: false + }, 'advanced'); + const originalLineCount = originalModel.object.textEditorModel.getLineCount(); + const modifiedLineCount = modifiedModel.object.textEditorModel.getLineCount(); + const unchanged = diff ? UnchangedRegion.fromDiffs(diff.changes, + originalLineCount, + modifiedLineCount, + unchangedRegionOptions.minimumLineCount ?? 3, + unchangedRegionOptions.contextLineCount ?? 3) : []; + + const totalLines = Math.max(originalLineCount, modifiedLineCount); + const numberOfUnchangedRegions = unchanged.length; + const numberOfVisibleLines = totalLines - unchanged.reduce((prev, curr) => prev + curr.lineCount, 0); + return { numberOfUnchangedRegions, numberOfVisibleLines }; + } finally { + originalModel.dispose(); + modifiedModel.dispose(); + } +} + diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 8e4349b8b50..4b1e25a025b 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -127,6 +127,7 @@ import { DefaultFormatter } from '../../format/browser/formatActionsMultiple.js' import { NotebookMultiTextDiffEditor } from './diff/notebookMultiDiffEditor.js'; import { NotebookMultiDiffEditorInput } from './diff/notebookMultiDiffEditorInput.js'; import { getFormattedMetadataJSON } from '../common/model/notebookCellTextModel.js'; +import { INotebookOutlineEntryFactory, NotebookOutlineEntryFactory } from './viewModel/notebookOutlineEntryFactory.js'; /*--------------------------------------------------------------------------------------------- */ @@ -790,6 +791,7 @@ registerSingleton(INotebookRendererMessagingService, NotebookRendererMessagingSe registerSingleton(INotebookKeymapService, NotebookKeymapService, InstantiationType.Delayed); registerSingleton(INotebookLoggingService, NotebookLoggingService, InstantiationType.Delayed); registerSingleton(INotebookCellOutlineDataSourceFactory, NotebookCellOutlineDataSourceFactory, InstantiationType.Delayed); +registerSingleton(INotebookOutlineEntryFactory, NotebookOutlineEntryFactory, InstantiationType.Delayed); const schemas: IJSONSchemaMap = {}; function isConfigurationPropertySchema(x: IConfigurationPropertySchema | { [path: string]: IConfigurationPropertySchema }): x is IConfigurationPropertySchema { diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts index 735bdad7d94..e561cf6e626 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellOutput.ts @@ -19,8 +19,7 @@ import { IInstantiationService } from '../../../../../../platform/instantiation/ import { IOpenerService } from '../../../../../../platform/opener/common/opener.js'; import { IQuickInputService, IQuickPickItem } from '../../../../../../platform/quickinput/common/quickInput.js'; import { ThemeIcon } from '../../../../../../base/common/themables.js'; -import { ViewContainerLocation } from '../../../../../common/views.js'; -import { IExtensionsViewPaneContainer, VIEWLET_ID as EXTENSION_VIEWLET_ID } from '../../../../extensions/common/extensions.js'; +import { IExtensionsWorkbenchService } from '../../../../extensions/common/extensions.js'; import { ICellOutputViewModel, ICellViewModel, IInsetRenderOutput, INotebookEditorDelegate, JUPYTER_EXTENSION_ID, RenderOutputType } from '../../notebookBrowser.js'; import { mimetypeIcon } from '../../notebookIcons.js'; import { CellContentPart } from '../cellPart.js'; @@ -31,7 +30,6 @@ import { CellUri, IOrderedMimeType, NotebookCellExecutionState, NotebookCellOutp import { INotebookExecutionStateService } from '../../../common/notebookExecutionStateService.js'; import { INotebookKernel } from '../../../common/notebookKernelService.js'; import { INotebookService } from '../../../common/notebookService.js'; -import { IPaneCompositePartService } from '../../../../../services/panecomposite/browser/panecomposite.js'; import { COPY_OUTPUT_COMMAND_ID } from '../../controller/cellOutputActions.js'; import { TEXT_BASED_MIMETYPES } from '../../contrib/clipboard/cellOutputClipboard.js'; import { autorun, observableValue } from '../../../../../../base/common/observable.js'; @@ -80,7 +78,7 @@ class CellOutputElement extends Disposable { @IQuickInputService private readonly quickInputService: IQuickInputService, @IContextKeyService parentContextKeyService: IContextKeyService, @IMenuService private readonly menuService: IMenuService, - @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); @@ -430,9 +428,7 @@ class CellOutputElement extends Disposable { } private async _showJupyterExtension() { - const viewlet = await this.paneCompositeService.openPaneComposite(EXTENSION_VIEWLET_ID, ViewContainerLocation.Sidebar, true); - const view = viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer | undefined; - view?.search(`@id:${JUPYTER_EXTENSION_ID}`); + await this.extensionsWorkbenchService.openSearch(`@id:${JUPYTER_EXTENSION_ID}`); } private _generateRendererInfo(renderId: string): string { diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts index ed204686f09..f58ff0aa23f 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellStatusPart.ts @@ -105,7 +105,17 @@ export class CellEditorStatusBar extends CellContentPart { event: e }); } else { - if ((e.target as HTMLElement).classList.contains('cell-status-item-has-command')) { + const target = e.target; + let itemHasCommand = false; + if (target && DOM.isHTMLElement(target)) { + const targetElement = target; + if (targetElement.classList.contains('cell-status-item-has-command')) { + itemHasCommand = true; + } else if (targetElement.parentElement && targetElement.parentElement.classList.contains('cell-status-item-has-command')) { + itemHasCommand = true; + } + } + if (itemHasCommand) { this._onDidClick.fire({ type: ClickTargetType.ContributedCommandItem, event: e diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts index f44b10e4e84..8cdb4f05629 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/codeCell.ts @@ -3,36 +3,35 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from '../../../../../../nls.js'; import * as DOM from '../../../../../../base/browser/dom.js'; import { raceCancellation } from '../../../../../../base/common/async.js'; import { CancellationTokenSource } from '../../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; -import { ThemeIcon } from '../../../../../../base/common/themables.js'; import { Event } from '../../../../../../base/common/event.js'; import { Disposable, IDisposable, toDisposable } from '../../../../../../base/common/lifecycle.js'; import { clamp } from '../../../../../../base/common/numbers.js'; import * as strings from '../../../../../../base/common/strings.js'; +import { ThemeIcon } from '../../../../../../base/common/themables.js'; import { EditorOption } from '../../../../../../editor/common/config/editorOptions.js'; import { IDimension } from '../../../../../../editor/common/core/dimension.js'; import { ILanguageService } from '../../../../../../editor/common/languages/language.js'; import { tokenizeToStringSync } from '../../../../../../editor/common/languages/textToHtmlTokenizer.js'; import { IReadonlyTextBuffer, ITextModel } from '../../../../../../editor/common/model.js'; -import { localize } from '../../../../../../nls.js'; +import { CodeActionController } from '../../../../../../editor/contrib/codeAction/browser/codeActionController.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js'; import { IOpenerService } from '../../../../../../platform/opener/common/opener.js'; +import { INotebookExecutionStateService } from '../../../common/notebookExecutionStateService.js'; import { CellFocusMode, EXPAND_CELL_INPUT_COMMAND_ID, IActiveNotebookEditorDelegate } from '../../notebookBrowser.js'; +import { CodeCellViewModel, outputDisplayLimit } from '../../viewModel/codeCellViewModel.js'; import { CellPartsCollection } from '../cellPart.js'; +import { NotebookCellEditorPool } from '../notebookCellEditorPool.js'; +import { CodeCellRenderTemplate } from '../notebookRenderingCommon.js'; import { CellEditorOptions } from './cellEditorOptions.js'; import { CellOutputContainer } from './cellOutput.js'; import { CollapsedCodeCellExecutionIcon } from './codeCellExecutionIcon.js'; -import { CodeCellRenderTemplate } from '../notebookRenderingCommon.js'; -import { CodeCellViewModel, outputDisplayLimit } from '../../viewModel/codeCellViewModel.js'; -import { INotebookExecutionStateService } from '../../../common/notebookExecutionStateService.js'; -import { WordHighlighterContribution } from '../../../../../../editor/contrib/wordHighlighter/browser/wordHighlighter.js'; -import { CodeActionController } from '../../../../../../editor/contrib/codeAction/browser/codeActionController.js'; -import { NotebookCellEditorPool } from '../notebookCellEditorPool.js'; export class CodeCell extends Disposable { private _outputContainerRenderer: CellOutputContainer; @@ -354,13 +353,9 @@ export class CodeCell extends Disposable { })); this._register(this.templateData.editor.onDidBlurEditorWidget(() => { - WordHighlighterContribution.get(this.templateData.editor)?.stopHighlighting(); CodeActionController.get(this.templateData.editor)?.hideCodeActions(); CodeActionController.get(this.templateData.editor)?.hideLightBulbWidget(); })); - this._register(this.templateData.editor.onDidFocusEditorWidget(() => { - WordHighlighterContribution.get(this.templateData.editor)?.restoreViewState(true); - })); } private _reigsterModelListeners(model: ITextModel) { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSource.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSource.ts index c02dadb3196..57c4a840aa7 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSource.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineDataSource.ts @@ -11,12 +11,10 @@ import { IConfigurationService } from '../../../../../platform/configuration/com import { IMarkerService } from '../../../../../platform/markers/common/markers.js'; import { IActiveNotebookEditor, INotebookEditor } from '../notebookBrowser.js'; import { CellKind } from '../../common/notebookCommon.js'; -import { INotebookExecutionStateService } from '../../common/notebookExecutionStateService.js'; import { OutlineChangeEvent, OutlineConfigKeys } from '../../../../services/outline/browser/outline.js'; import { OutlineEntry } from './OutlineEntry.js'; -import { IOutlineModelService } from '../../../../../editor/contrib/documentSymbols/browser/outlineModel.js'; import { CancellationToken } from '../../../../../base/common/cancellation.js'; -import { NotebookOutlineEntryFactory } from './notebookOutlineEntryFactory.js'; +import { INotebookOutlineEntryFactory, NotebookOutlineEntryFactory } from './notebookOutlineEntryFactory.js'; export interface INotebookCellOutlineDataSource { readonly activeElement: OutlineEntry | undefined; @@ -34,16 +32,12 @@ export class NotebookCellOutlineDataSource implements INotebookCellOutlineDataSo private _entries: OutlineEntry[] = []; private _activeEntry?: OutlineEntry; - private readonly _outlineEntryFactory: NotebookOutlineEntryFactory; - constructor( private readonly _editor: INotebookEditor, - @INotebookExecutionStateService private readonly _notebookExecutionStateService: INotebookExecutionStateService, - @IOutlineModelService private readonly _outlineModelService: IOutlineModelService, @IMarkerService private readonly _markerService: IMarkerService, @IConfigurationService private readonly _configurationService: IConfigurationService, + @INotebookOutlineEntryFactory private readonly _outlineEntryFactory: NotebookOutlineEntryFactory ) { - this._outlineEntryFactory = new NotebookOutlineEntryFactory(this._notebookExecutionStateService); this.recomputeState(); } @@ -68,9 +62,9 @@ export class NotebookCellOutlineDataSource implements INotebookCellOutlineDataSo if (notebookCells) { const promises: Promise[] = []; // limit the number of cells so that we don't resolve an excessive amount of text models - for (const cell of notebookCells.slice(0, 100)) { + for (const cell of notebookCells.slice(0, 50)) { // gather all symbols asynchronously - promises.push(this._outlineEntryFactory.cacheSymbols(cell, this._outlineModelService, cancelToken)); + promises.push(this._outlineEntryFactory.cacheSymbols(cell, cancelToken)); } await Promise.allSettled(promises); } diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory.ts index 28dc9ac2227..6c708a2fa67 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookOutlineEntryFactory.ts @@ -14,6 +14,8 @@ import { CellKind } from '../../common/notebookCommon.js'; import { INotebookExecutionStateService } from '../../common/notebookExecutionStateService.js'; import { IRange } from '../../../../../editor/common/core/range.js'; import { SymbolKind } from '../../../../../editor/common/languages.js'; +import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js'; +import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; export const enum NotebookOutlineConstants { NonHeaderOutlineLevel = 7, @@ -41,12 +43,25 @@ function getMarkdownHeadersInCellFallbackToHtmlTags(fullContent: string) { return headers; } -export class NotebookOutlineEntryFactory { +export const INotebookOutlineEntryFactory = createDecorator('INotebookOutlineEntryFactory'); + +export interface INotebookOutlineEntryFactory { + readonly _serviceBrand: undefined; + + getOutlineEntries(cell: ICellViewModel, index: number): OutlineEntry[]; + cacheSymbols(cell: ICellViewModel, cancelToken: CancellationToken): Promise; +} + +export class NotebookOutlineEntryFactory implements INotebookOutlineEntryFactory { + + declare readonly _serviceBrand: undefined; private cellOutlineEntryCache: Record = {}; private readonly cachedMarkdownOutlineEntries = new WeakMap(); constructor( - private readonly executionStateService: INotebookExecutionStateService + @INotebookExecutionStateService private readonly executionStateService: INotebookExecutionStateService, + @IOutlineModelService private readonly outlineModelService: IOutlineModelService, + @ITextModelService private readonly textModelService: ITextModelService ) { } public getOutlineEntries(cell: ICellViewModel, index: number): OutlineEntry[] { @@ -80,16 +95,16 @@ export class NotebookOutlineEntryFactory { const exeState = !isMarkdown && this.executionStateService.getCellExecution(cell.uri); let preview = content.trim(); - if (!isMarkdown && cell.model.textModel) { - const cachedEntries = this.cellOutlineEntryCache[cell.model.textModel.id]; + if (!isMarkdown) { + const cached = this.cellOutlineEntryCache[cell.id]; // Gathering symbols from the model is an async operation, but this provider is syncronous. // So symbols need to be precached before this function is called to get the full list. - if (cachedEntries) { + if (cached) { // push code cell entry that is a parent of cached symbols, always necessary. filtering for quickpick done in that provider. entries.push(new OutlineEntry(index++, NotebookOutlineConstants.NonHeaderOutlineLevel, cell, preview, !!exeState, exeState ? exeState.isPaused : false)); - cachedEntries.forEach((cached) => { - entries.push(new OutlineEntry(index++, cached.level, cell, cached.name, false, false, cached.range, cached.kind)); + cached.forEach((entry) => { + entries.push(new OutlineEntry(index++, entry.level, cell, entry.name, false, false, entry.range, entry.kind)); }); } } @@ -106,11 +121,20 @@ export class NotebookOutlineEntryFactory { return entries; } - public async cacheSymbols(cell: ICellViewModel, outlineModelService: IOutlineModelService, cancelToken: CancellationToken) { - const textModel = await cell.resolveTextModel(); - const outlineModel = await outlineModelService.getOrCreate(textModel, cancelToken); - const entries = createOutlineEntries(outlineModel.getTopLevelSymbols(), 8); - this.cellOutlineEntryCache[textModel.id] = entries; + public async cacheSymbols(cell: ICellViewModel, cancelToken: CancellationToken) { + if (cell.cellKind === CellKind.Markup) { + return; + } + + const ref = await this.textModelService.createModelReference(cell.uri); + try { + const textModel = ref.object.textEditorModel; + const outlineModel = await this.outlineModelService.getOrCreate(textModel, cancelToken); + const entries = createOutlineEntries(outlineModel.getTopLevelSymbols(), 8); + this.cellOutlineEntryCache[cell.id] = entries; + } finally { + ref.dispose(); + } } } diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts index 9acc12688d7..f14a7619562 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts @@ -22,15 +22,13 @@ import { IProductService } from '../../../../../platform/product/common/productS import { ProgressLocation } from '../../../../../platform/progress/common/progress.js'; import { IQuickInputButton, IQuickInputService, IQuickPick, IQuickPickItem, QuickPickInput } from '../../../../../platform/quickinput/common/quickInput.js'; import { ThemeIcon } from '../../../../../base/common/themables.js'; -import { ViewContainerLocation } from '../../../../common/views.js'; -import { IExtension, IExtensionsViewPaneContainer, IExtensionsWorkbenchService, VIEWLET_ID as EXTENSION_VIEWLET_ID } from '../../../extensions/common/extensions.js'; +import { IExtension, IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js'; import { IActiveNotebookEditor, INotebookExtensionRecommendation, JUPYTER_EXTENSION_ID, KERNEL_RECOMMENDATIONS } from '../notebookBrowser.js'; import { NotebookEditorWidget } from '../notebookEditorWidget.js'; import { executingStateIcon, selectKernelIcon } from '../notebookIcons.js'; import { NotebookTextModel } from '../../common/model/notebookTextModel.js'; import { INotebookKernel, INotebookKernelHistoryService, INotebookKernelMatchResult, INotebookKernelService, ISourceAction } from '../../common/notebookKernelService.js'; import { IExtensionService } from '../../../../services/extensions/common/extensions.js'; -import { IPaneCompositePartService } from '../../../../services/panecomposite/browser/panecomposite.js'; import { URI } from '../../../../../base/common/uri.js'; import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; import { INotebookTextModel } from '../../common/notebookCommon.js'; @@ -105,7 +103,6 @@ abstract class KernelPickerStrategyBase implements IKernelPickerStrategy { protected readonly _quickInputService: IQuickInputService, protected readonly _labelService: ILabelService, protected readonly _logService: ILogService, - protected readonly _paneCompositePartService: IPaneCompositePartService, protected readonly _extensionWorkbenchService: IExtensionsWorkbenchService, protected readonly _extensionService: IExtensionService, protected readonly _commandService: ICommandService @@ -255,7 +252,6 @@ abstract class KernelPickerStrategyBase implements IKernelPickerStrategy { // actions if (isSearchMarketplacePick(pick)) { await this._showKernelExtension( - this._paneCompositePartService, this._extensionWorkbenchService, this._extensionService, editor.textModel.viewType, @@ -264,7 +260,6 @@ abstract class KernelPickerStrategyBase implements IKernelPickerStrategy { // suggestedExtension must be defined for this option to be shown, but still check to make TS happy } else if (isInstallExtensionPick(pick)) { await this._showKernelExtension( - this._paneCompositePartService, this._extensionWorkbenchService, this._extensionService, editor.textModel.viewType, @@ -284,7 +279,6 @@ abstract class KernelPickerStrategyBase implements IKernelPickerStrategy { } protected async _showKernelExtension( - paneCompositePartService: IPaneCompositePartService, extensionWorkbenchService: IExtensionsWorkbenchService, extensionService: IExtensionService, viewType: string, @@ -337,10 +331,8 @@ abstract class KernelPickerStrategyBase implements IKernelPickerStrategy { return; } - const viewlet = await paneCompositePartService.openPaneComposite(EXTENSION_VIEWLET_ID, ViewContainerLocation.Sidebar, true); - const view = viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer | undefined; const pascalCased = viewType.split(/[^a-z0-9]/ig).map(uppercaseFirstLetter).join(''); - view?.search(`@tag:notebookKernel${pascalCased}`); + await extensionWorkbenchService.openSearch(`@tag:notebookKernel${pascalCased}`); } private async _showInstallKernelExtensionRecommendation( @@ -441,7 +433,6 @@ export class KernelPickerMRUStrategy extends KernelPickerStrategyBase { @IQuickInputService _quickInputService: IQuickInputService, @ILabelService _labelService: ILabelService, @ILogService _logService: ILogService, - @IPaneCompositePartService _paneCompositePartService: IPaneCompositePartService, @IExtensionsWorkbenchService _extensionWorkbenchService: IExtensionsWorkbenchService, @IExtensionService _extensionService: IExtensionService, @ICommandService _commandService: ICommandService, @@ -455,7 +446,6 @@ export class KernelPickerMRUStrategy extends KernelPickerStrategyBase { _quickInputService, _labelService, _logService, - _paneCompositePartService, _extensionWorkbenchService, _extensionService, _commandService, @@ -617,7 +607,6 @@ export class KernelPickerMRUStrategy extends KernelPickerStrategyBase { } } else if (isSearchMarketplacePick(selectedKernelPickItem)) { await this._showKernelExtension( - this._paneCompositePartService, this._extensionWorkbenchService, this._extensionService, editor.textModel.viewType, @@ -626,7 +615,6 @@ export class KernelPickerMRUStrategy extends KernelPickerStrategyBase { return true; } else if (isInstallExtensionPick(selectedKernelPickItem)) { await this._showKernelExtension( - this._paneCompositePartService, this._extensionWorkbenchService, this._extensionService, editor.textModel.viewType, diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts index d7fd7e68189..6debc78a96d 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts @@ -11,8 +11,7 @@ import * as UUID from '../../../../../base/common/uuid.js'; import { Range } from '../../../../../editor/common/core/range.js'; import * as model from '../../../../../editor/common/model.js'; import { PieceTreeTextBuffer } from '../../../../../editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.js'; -import { PieceTreeTextBufferBuilder } from '../../../../../editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder.js'; -import { TextModel } from '../../../../../editor/common/model/textModel.js'; +import { createTextBuffer, TextModel } from '../../../../../editor/common/model/textModel.js'; import { PLAINTEXT_LANGUAGE_ID } from '../../../../../editor/common/languages/modesRegistry.js'; import { ILanguageService } from '../../../../../editor/common/languages/language.js'; import { NotebookCellOutputTextModel } from './notebookCellOutputTextModel.js'; @@ -113,12 +112,7 @@ export class NotebookCellTextModel extends Disposable implements ICell { return this._textBuffer; } - const builder = new PieceTreeTextBufferBuilder(); - builder.acceptChunk(this._source); - const bufferFactory = builder.finish(true); - const { textBuffer, disposable } = bufferFactory.create(model.DefaultEndOfLine.LF); - this._textBuffer = textBuffer; - this._register(disposable); + this._textBuffer = this._register(createTextBuffer(this._source, model.DefaultEndOfLine.LF).textBuffer); this._register(this._textBuffer.onDidChangeContent(() => { this._hash = null; diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 8a526b263ee..b84d74a275d 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -562,7 +562,7 @@ export interface INotebookContributionData { priority?: RegisteredEditorPriority; } -export namespace NotebookUri { +export namespace NotebookMetadataUri { export const scheme = Schemas.vscodeNotebookMetadata; export function generate(notebook: URI): URI { return generateMetadataUri(notebook); diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutline.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutline.test.ts index 428a991cb6f..81042265ee8 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutline.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutline.test.ts @@ -16,22 +16,26 @@ import { CellKind, IOutputDto, NotebookCellMetadata } from '../../../common/note import { IActiveNotebookEditor, INotebookEditorPane } from '../../../browser/notebookBrowser.js'; import { DisposableStore } from '../../../../../../base/common/lifecycle.js'; import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; -import { NotebookCellOutline } from '../../../browser/contrib/outline/notebookOutline.js'; +import { NotebookCellOutline, NotebookOutlineCreator } from '../../../browser/contrib/outline/notebookOutline.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/test/common/utils.js'; import { ILanguageFeaturesService } from '../../../../../../editor/common/services/languageFeatures.js'; import { LanguageFeaturesService } from '../../../../../../editor/common/services/languageFeaturesService.js'; import { IEditorPaneSelectionChangeEvent } from '../../../../../common/editor.js'; +import { CancellationToken } from '../../../../../../base/common/cancellation.js'; +import { INotebookOutlineEntryFactory, NotebookOutlineEntryFactory } from '../../../browser/viewModel/notebookOutlineEntryFactory.js'; suite('Notebook Outline', function () { let disposables: DisposableStore; let instantiationService: TestInstantiationService; + let symbolsCached: boolean; teardown(() => disposables.dispose()); ensureNoDisposablesAreLeakedInTestSuite(); setup(() => { + symbolsCached = false; disposables = new DisposableStore(); instantiationService = setupInstantiationService(disposables); instantiationService.set(IEditorService, new class extends mock() { }); @@ -46,27 +50,39 @@ suite('Notebook Outline', function () { }); - function withNotebookOutline(cells: [source: string, lang: string, kind: CellKind, output?: IOutputDto[], metadata?: NotebookCellMetadata][], callback: (outline: NotebookCellOutline, editor: IActiveNotebookEditor) => R): Promise { - return withTestNotebook(cells, (editor) => { + async function withNotebookOutline( + cells: [source: string, lang: string, kind: CellKind, output?: IOutputDto[], metadata?: NotebookCellMetadata][], + target: OutlineTarget, + callback: (outline: NotebookCellOutline, editor: IActiveNotebookEditor) => R, + ): Promise { + + return withTestNotebook(cells, async (editor) => { if (!editor.hasModel()) { assert.ok(false, 'MUST have active text editor'); } - const outline = instantiationService.createInstance(NotebookCellOutline, new class extends mock() { + const notebookEditorPane = new class extends mock() { override getControl() { return editor; } override onDidChangeModel: Event = Event.None; override onDidChangeSelection: Event = Event.None; - }, OutlineTarget.OutlinePane); + }; - disposables.add(outline); - return callback(outline, editor); + + const testOutlineEntryFactory = instantiationService.createInstance(NotebookOutlineEntryFactory) as any; + testOutlineEntryFactory.cacheSymbols = async () => { symbolsCached = true; }; + instantiationService.stub(INotebookOutlineEntryFactory, testOutlineEntryFactory); + + const outline = await instantiationService.createInstance(NotebookOutlineCreator).createOutline(notebookEditorPane, target, CancellationToken.None); + + disposables.add(outline!); + return callback(outline as NotebookCellOutline, editor); }); } test('basic', async function () { - await withNotebookOutline([], outline => { + await withNotebookOutline([], OutlineTarget.OutlinePane, outline => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements(), []); }); @@ -75,7 +91,7 @@ suite('Notebook Outline', function () { test('special characters in heading', async function () { await withNotebookOutline([ ['# Hellö & Hällo', 'md', CellKind.Markup] - ], outline => { + ], OutlineTarget.OutlinePane, outline => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 1); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[0].label, 'Hellö & Hällo'); @@ -83,7 +99,7 @@ suite('Notebook Outline', function () { await withNotebookOutline([ ['# bold', 'md', CellKind.Markup] - ], outline => { + ], OutlineTarget.OutlinePane, outline => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 1); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[0].label, 'bold'); @@ -93,7 +109,7 @@ suite('Notebook Outline', function () { test('Notebook falsely detects "empty cells"', async function () { await withNotebookOutline([ [' 的时代 ', 'md', CellKind.Markup] - ], outline => { + ], OutlineTarget.OutlinePane, outline => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 1); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[0].label, '的时代'); @@ -101,7 +117,7 @@ suite('Notebook Outline', function () { await withNotebookOutline([ [' ', 'md', CellKind.Markup] - ], outline => { + ], OutlineTarget.OutlinePane, outline => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 1); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[0].label, 'empty cell'); @@ -109,7 +125,7 @@ suite('Notebook Outline', function () { await withNotebookOutline([ ['+++++[]{}--)(0 ', 'md', CellKind.Markup] - ], outline => { + ], OutlineTarget.OutlinePane, outline => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 1); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[0].label, '+++++[]{}--)(0'); @@ -117,7 +133,7 @@ suite('Notebook Outline', function () { await withNotebookOutline([ ['+++++[]{}--)(0 Hello **&^ ', 'md', CellKind.Markup] - ], outline => { + ], OutlineTarget.OutlinePane, outline => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 1); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[0].label, '+++++[]{}--)(0 Hello **&^'); @@ -125,7 +141,7 @@ suite('Notebook Outline', function () { await withNotebookOutline([ ['!@#$\n Überschrïft', 'md', CellKind.Markup] - ], outline => { + ], OutlineTarget.OutlinePane, outline => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 1); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[0].label, '!@#$'); @@ -135,7 +151,7 @@ suite('Notebook Outline', function () { test('Heading text defines entry label', async function () { return await withNotebookOutline([ ['foo\n # h1', 'md', CellKind.Markup] - ], outline => { + ], OutlineTarget.OutlinePane, outline => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 1); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[0].label, 'h1'); @@ -145,7 +161,7 @@ suite('Notebook Outline', function () { test('Notebook outline ignores markdown headings #115200', async function () { await withNotebookOutline([ ['## h2 \n# h1', 'md', CellKind.Markup] - ], outline => { + ], OutlineTarget.OutlinePane, outline => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 2); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[0].label, 'h2'); @@ -155,11 +171,20 @@ suite('Notebook Outline', function () { await withNotebookOutline([ ['## h2', 'md', CellKind.Markup], ['# h1', 'md', CellKind.Markup] - ], outline => { + ], OutlineTarget.OutlinePane, outline => { assert.ok(outline instanceof NotebookCellOutline); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements().length, 2); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[0].label, 'h2'); assert.deepStrictEqual(outline.config.quickPickDataSource.getQuickPickElements()[1].label, 'h1'); }); }); + + test('Symbols for goto quickpick are pre-cached', async function () { + await withNotebookOutline([ + ['a = 1\nb = 2', 'python', CellKind.Code] + ], OutlineTarget.QuickPick, outline => { + assert.ok(outline instanceof NotebookCellOutline); + assert.strictEqual(symbolsCached, true); + }); + }); }); diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutlineViewProviders.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutlineViewProviders.test.ts index 513458476e6..a7f35bacea3 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutlineViewProviders.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookOutlineViewProviders.test.ts @@ -20,6 +20,8 @@ import { NotebookOutlineEntryFactory } from '../../../browser/viewModel/notebook import { OutlineEntry } from '../../../browser/viewModel/OutlineEntry.js'; import { INotebookExecutionStateService } from '../../../common/notebookExecutionStateService.js'; import { MockDocumentSymbol } from '../testNotebookEditor.js'; +import { IResolvedTextEditorModel, ITextModelService } from '../../../../../../editor/common/services/resolverService.js'; +import { URI } from '../../../../../../base/common/uri.js'; suite('Notebook Outline View Providers', function () { @@ -55,12 +57,27 @@ suite('Notebook Outline View Providers', function () { return 0; } }; + const textModelService = new class extends mock() { + override createModelReference(uri: URI) { + return Promise.resolve({ + object: { + textEditorModel: { + id: uri.toString(), + getVersionId() { return 1; } + } + }, + dispose() { } + } as IReference); + } + }; // #endregion // #region Helpers function createCodeCellViewModel(version: number = 1, source = '# code', textmodelId = 'textId') { return { + uri: { toString() { return textmodelId; } }, + id: textmodelId, textBuffer: { getLineCount() { return 0; } }, @@ -206,9 +223,9 @@ suite('Notebook Outline View Providers', function () { setSymbolsForTextModel([{ name: 'var3', range: {} }], '$3'); // Cache symbols - const entryFactory = new NotebookOutlineEntryFactory(executionService); + const entryFactory = new NotebookOutlineEntryFactory(executionService, outlineModelService, textModelService); for (const cell of cells) { - await entryFactory.cacheSymbols(cell, outlineModelService, CancellationToken.None); + await entryFactory.cacheSymbols(cell, CancellationToken.None); } // Generate raw outline @@ -249,9 +266,9 @@ suite('Notebook Outline View Providers', function () { setSymbolsForTextModel([{ name: 'var3', range: {} }], '$3'); // Cache symbols - const entryFactory = new NotebookOutlineEntryFactory(executionService); + const entryFactory = new NotebookOutlineEntryFactory(executionService, outlineModelService, textModelService); for (const cell of cells) { - await entryFactory.cacheSymbols(cell, outlineModelService, CancellationToken.None); + await entryFactory.cacheSymbols(cell, CancellationToken.None); } // Generate raw outline @@ -295,9 +312,9 @@ suite('Notebook Outline View Providers', function () { setSymbolsForTextModel([{ name: 'var3', range: {} }], '$3'); // Cache symbols - const entryFactory = new NotebookOutlineEntryFactory(executionService); + const entryFactory = new NotebookOutlineEntryFactory(executionService, outlineModelService, textModelService); for (const cell of cells) { - await entryFactory.cacheSymbols(cell, outlineModelService, CancellationToken.None); + await entryFactory.cacheSymbols(cell, CancellationToken.None); } // Generate raw outline @@ -338,9 +355,9 @@ suite('Notebook Outline View Providers', function () { setSymbolsForTextModel([{ name: 'var3', range: {} }], '$3'); // Cache symbols - const entryFactory = new NotebookOutlineEntryFactory(executionService); + const entryFactory = new NotebookOutlineEntryFactory(executionService, outlineModelService, textModelService); for (const cell of cells) { - await entryFactory.cacheSymbols(cell, outlineModelService, CancellationToken.None); + await entryFactory.cacheSymbols(cell, CancellationToken.None); } // Generate raw outline @@ -387,9 +404,9 @@ suite('Notebook Outline View Providers', function () { setSymbolsForTextModel([{ name: 'var3', range: {} }], '$3'); // Cache symbols - const entryFactory = new NotebookOutlineEntryFactory(executionService); + const entryFactory = new NotebookOutlineEntryFactory(executionService, outlineModelService, textModelService); for (const cell of cells) { - await entryFactory.cacheSymbols(cell, outlineModelService, CancellationToken.None); + await entryFactory.cacheSymbols(cell, CancellationToken.None); } // Generate raw outline @@ -446,9 +463,9 @@ suite('Notebook Outline View Providers', function () { setSymbolsForTextModel([{ name: 'var3', range: {}, kind: 12 }], '$3'); // Cache symbols - const entryFactory = new NotebookOutlineEntryFactory(executionService); + const entryFactory = new NotebookOutlineEntryFactory(executionService, outlineModelService, textModelService); for (const cell of cells) { - await entryFactory.cacheSymbols(cell, outlineModelService, CancellationToken.None); + await entryFactory.cacheSymbols(cell, CancellationToken.None); } // Generate raw outline @@ -499,9 +516,9 @@ suite('Notebook Outline View Providers', function () { setSymbolsForTextModel([{ name: 'var3', range: {}, kind: 12 }], '$3'); // Cache symbols - const entryFactory = new NotebookOutlineEntryFactory(executionService); + const entryFactory = new NotebookOutlineEntryFactory(executionService, outlineModelService, textModelService); for (const cell of cells) { - await entryFactory.cacheSymbols(cell, outlineModelService, CancellationToken.None); + await entryFactory.cacheSymbols(cell, CancellationToken.None); } // Generate raw outline @@ -552,9 +569,9 @@ suite('Notebook Outline View Providers', function () { setSymbolsForTextModel([{ name: 'var3', range: {}, kind: 12 }], '$3'); // Cache symbols - const entryFactory = new NotebookOutlineEntryFactory(executionService); + const entryFactory = new NotebookOutlineEntryFactory(executionService, outlineModelService, textModelService); for (const cell of cells) { - await entryFactory.cacheSymbols(cell, outlineModelService, CancellationToken.None); + await entryFactory.cacheSymbols(cell, CancellationToken.None); } // Generate raw outline @@ -608,9 +625,9 @@ suite('Notebook Outline View Providers', function () { setSymbolsForTextModel([{ name: 'var3', range: {}, kind: 12 }], '$3'); // Cache symbols - const entryFactory = new NotebookOutlineEntryFactory(executionService); + const entryFactory = new NotebookOutlineEntryFactory(executionService, outlineModelService, textModelService); for (const cell of cells) { - await entryFactory.cacheSymbols(cell, outlineModelService, CancellationToken.None); + await entryFactory.cacheSymbols(cell, CancellationToken.None); } // Generate raw outline @@ -659,9 +676,9 @@ suite('Notebook Outline View Providers', function () { setSymbolsForTextModel([{ name: 'var3', range: {}, kind: 12 }], '$3'); // Cache symbols - const entryFactory = new NotebookOutlineEntryFactory(executionService); + const entryFactory = new NotebookOutlineEntryFactory(executionService, outlineModelService, textModelService); for (const cell of cells) { - await entryFactory.cacheSymbols(cell, outlineModelService, CancellationToken.None); + await entryFactory.cacheSymbols(cell, CancellationToken.None); } // Generate raw outline diff --git a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookSymbols.test.ts b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookSymbols.test.ts index c7deb045301..1eaf85e79f5 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookSymbols.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/contrib/notebookSymbols.test.ts @@ -13,6 +13,9 @@ import { ICellViewModel } from '../../../browser/notebookBrowser.js'; import { NotebookOutlineEntryFactory } from '../../../browser/viewModel/notebookOutlineEntryFactory.js'; import { INotebookExecutionStateService } from '../../../common/notebookExecutionStateService.js'; import { MockDocumentSymbol } from '../testNotebookEditor.js'; +import { IResolvedTextEditorModel, ITextModelService } from '../../../../../../editor/common/services/resolverService.js'; +import { URI } from '../../../../../../base/common/uri.js'; +import { IReference } from '../../../../../../base/common/lifecycle.js'; suite('Notebook Symbols', function () { ensureNoDisposablesAreLeakedInTestSuite(); @@ -42,9 +45,24 @@ suite('Notebook Symbols', function () { return 0; } }; + const textModelService = new class extends mock() { + override createModelReference(uri: URI) { + return Promise.resolve({ + object: { + textEditorModel: { + id: uri.toString(), + getVersionId() { return 1; } + } + }, + dispose() { } + } as IReference); + } + }; function createCellViewModel(version: number = 1, textmodelId = 'textId') { return { + id: textmodelId, + uri: { toString() { return textmodelId; } }, textBuffer: { getLineCount() { return 0; } }, @@ -65,7 +83,7 @@ suite('Notebook Symbols', function () { test('Cell without symbols cache', function () { setSymbolsForTextModel([{ name: 'var', range: {} }]); - const entryFactory = new NotebookOutlineEntryFactory(executionService); + const entryFactory = new NotebookOutlineEntryFactory(executionService, outlineModelService, textModelService); const entries = entryFactory.getOutlineEntries(createCellViewModel(), 0); assert.equal(entries.length, 1, 'no entries created'); @@ -74,10 +92,10 @@ suite('Notebook Symbols', function () { test('Cell with simple symbols', async function () { setSymbolsForTextModel([{ name: 'var1', range: {} }, { name: 'var2', range: {} }]); - const entryFactory = new NotebookOutlineEntryFactory(executionService); + const entryFactory = new NotebookOutlineEntryFactory(executionService, outlineModelService, textModelService); const cell = createCellViewModel(); - await entryFactory.cacheSymbols(cell, outlineModelService, CancellationToken.None); + await entryFactory.cacheSymbols(cell, CancellationToken.None); const entries = entryFactory.getOutlineEntries(cell, 0); assert.equal(entries.length, 3, 'wrong number of outline entries'); @@ -96,10 +114,10 @@ suite('Notebook Symbols', function () { { name: 'root1', range: {}, children: [{ name: 'nested1', range: {} }, { name: 'nested2', range: {} }] }, { name: 'root2', range: {}, children: [{ name: 'nested1', range: {} }] } ]); - const entryFactory = new NotebookOutlineEntryFactory(executionService); + const entryFactory = new NotebookOutlineEntryFactory(executionService, outlineModelService, textModelService); const cell = createCellViewModel(); - await entryFactory.cacheSymbols(cell, outlineModelService, CancellationToken.None); + await entryFactory.cacheSymbols(cell, CancellationToken.None); const entries = entryFactory.getOutlineEntries(createCellViewModel(), 0); assert.equal(entries.length, 6, 'wrong number of outline entries'); @@ -119,12 +137,12 @@ suite('Notebook Symbols', function () { test('Multiple Cells with symbols', async function () { setSymbolsForTextModel([{ name: 'var1', range: {} }], '$1'); setSymbolsForTextModel([{ name: 'var2', range: {} }], '$2'); - const entryFactory = new NotebookOutlineEntryFactory(executionService); + const entryFactory = new NotebookOutlineEntryFactory(executionService, outlineModelService, textModelService); const cell1 = createCellViewModel(1, '$1'); const cell2 = createCellViewModel(1, '$2'); - await entryFactory.cacheSymbols(cell1, outlineModelService, CancellationToken.None); - await entryFactory.cacheSymbols(cell2, outlineModelService, CancellationToken.None); + await entryFactory.cacheSymbols(cell1, CancellationToken.None); + await entryFactory.cacheSymbols(cell2, CancellationToken.None); const entries1 = entryFactory.getOutlineEntries(createCellViewModel(1, '$1'), 0); const entries2 = entryFactory.getOutlineEntries(createCellViewModel(1, '$2'), 0); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts index e9d86215e56..39a5468f12f 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookDiff.test.ts @@ -20,6 +20,7 @@ import { CellKind, INotebookTextModel } from '../../common/notebookCommon.js'; import { INotebookService } from '../../common/notebookService.js'; import { INotebookEditorWorkerService } from '../../common/services/notebookWorkerService.js'; import { withTestNotebookDiffModel } from './testNotebookEditor.js'; +import { UnchangedEditorRegionsService } from '../../browser/diff/unchangedEditorRegions.js'; class CellSequence implements ISequence { @@ -87,7 +88,7 @@ suite('NotebookDiff', () => { modifiedLength: 1 }]); - diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), undefined)); + diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), UnchangedEditorRegionsService.Empty, undefined)); await diffViewModel.computeDiff(token); assert.strictEqual(diffViewModel.items.length, 1); @@ -116,7 +117,7 @@ suite('NotebookDiff', () => { modifiedLength: 1 }]); - diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), undefined)); + diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), UnchangedEditorRegionsService.Empty, undefined)); await diffViewModel.computeDiff(token); await verifyChangeEventIsNotFired(diffViewModel); @@ -146,7 +147,7 @@ suite('NotebookDiff', () => { modifiedLength: 1 }]); - diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), undefined)); + diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), UnchangedEditorRegionsService.Empty, undefined)); let eventArgs: INotebookDiffViewModelUpdateEvent | undefined = undefined; disposables.add(diffViewModel.onDidChangeItems(e => eventArgs = e)); await diffViewModel.computeDiff(token); @@ -195,7 +196,7 @@ suite('NotebookDiff', () => { modifiedLength: 1 }]); - diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), undefined)); + diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), UnchangedEditorRegionsService.Empty, undefined)); await diffViewModel.computeDiff(token); assert.strictEqual(diffViewModel.items.length, 1); @@ -234,7 +235,7 @@ suite('NotebookDiff', () => { modifiedLength: 1 }]); - diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), undefined)); + diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), UnchangedEditorRegionsService.Empty, undefined)); await diffViewModel.computeDiff(token); assert.strictEqual(diffViewModel.items.length, 1); @@ -257,7 +258,7 @@ suite('NotebookDiff', () => { const diff = new LcsDiff(new CellSequence(model.original.notebook), new CellSequence(model.modified.notebook)); diffResult = diff.ComputeDiff(false); - diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), undefined)); + diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), UnchangedEditorRegionsService.Empty, undefined)); let eventArgs: INotebookDiffViewModelUpdateEvent | undefined = undefined; disposables.add(diffViewModel.onDidChangeItems(e => eventArgs = e)); await diffViewModel.computeDiff(token); @@ -287,7 +288,7 @@ suite('NotebookDiff', () => { const diff = new LcsDiff(new CellSequence(model.original.notebook), new CellSequence(model.modified.notebook)); diffResult = diff.ComputeDiff(false); - diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), undefined)); + diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), UnchangedEditorRegionsService.Empty, undefined)); let eventArgs: INotebookDiffViewModelUpdateEvent | undefined = undefined; disposables.add(diffViewModel.onDidChangeItems(e => eventArgs = e)); await diffViewModel.computeDiff(token); @@ -325,7 +326,7 @@ suite('NotebookDiff', () => { quitEarly: false }; - diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), undefined)); + diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), UnchangedEditorRegionsService.Empty, undefined)); let eventArgs: INotebookDiffViewModelUpdateEvent | undefined = undefined; disposables.add(diffViewModel.onDidChangeItems(e => eventArgs = e)); const result = await diffViewModel.computeDiff(token); @@ -379,7 +380,7 @@ suite('NotebookDiff', () => { quitEarly: false }; - diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), undefined)); + diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), UnchangedEditorRegionsService.Empty, undefined)); let eventArgs: INotebookDiffViewModelUpdateEvent | undefined = undefined; disposables.add(diffViewModel.onDidChangeItems(e => eventArgs = e)); const result = await diffViewModel.computeDiff(token); @@ -441,7 +442,7 @@ suite('NotebookDiff', () => { quitEarly: false }; - diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), undefined)); + diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), UnchangedEditorRegionsService.Empty, undefined)); let eventArgs: INotebookDiffViewModelUpdateEvent | undefined = undefined; disposables.add(diffViewModel.onDidChangeItems(e => eventArgs = e)); await diffViewModel.computeDiff(token); @@ -611,7 +612,7 @@ suite('NotebookDiff', () => { const diff = new LcsDiff(new CellSequence(model.original.notebook), new CellSequence(model.modified.notebook)); diffResult = diff.ComputeDiff(false); - diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), undefined)); + diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), UnchangedEditorRegionsService.Empty, undefined)); await diffViewModel.computeDiff(token); assert.strictEqual(diffViewModel.items.length, 2); @@ -635,7 +636,7 @@ suite('NotebookDiff', () => { const diff = new LcsDiff(new CellSequence(model.original.notebook), new CellSequence(model.modified.notebook)); diffResult = diff.ComputeDiff(false); - diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), undefined)); + diffViewModel = disposables.add(new NotebookDiffViewModel(model, notebookEditorWorkerService, configurationService, eventDispatcher, accessor.get(INotebookService), UnchangedEditorRegionsService.Empty, undefined)); await diffViewModel.computeDiff(token); assert.strictEqual(diffViewModel.items.length, 2); diff --git a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts index d23c98e25a9..de21fbf708a 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/testNotebookEditor.ts @@ -67,6 +67,8 @@ import { mainWindow } from '../../../../../base/browser/window.js'; import { TestCodeEditorService } from '../../../../../editor/test/browser/editorTestServices.js'; import { INotebookCellOutlineDataSourceFactory, NotebookCellOutlineDataSourceFactory } from '../../browser/viewModel/notebookOutlineDataSourceFactory.js'; import { ILanguageDetectionService } from '../../../../services/languageDetection/common/languageDetectionWorkerService.js'; +import { INotebookOutlineEntryFactory, NotebookOutlineEntryFactory } from '../../browser/viewModel/notebookOutlineEntryFactory.js'; +import { IOutlineService } from '../../../../services/outline/browser/outline.js'; export class TestCell extends NotebookCellTextModel { constructor( @@ -199,7 +201,9 @@ export function setupInstantiationService(disposables: Pick() { override registerOutlineCreator() { return { dispose() { } }; } }); instantiationService.stub(INotebookCellOutlineDataSourceFactory, instantiationService.createInstance(NotebookCellOutlineDataSourceFactory)); + instantiationService.stub(INotebookOutlineEntryFactory, instantiationService.createInstance(NotebookOutlineEntryFactory)); instantiationService.stub(ILanguageDetectionService, new class MockLanguageDetectionService implements ILanguageDetectionService { _serviceBrand: undefined; diff --git a/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts b/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts index 56166ad831a..ad62b0a04d2 100644 --- a/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts +++ b/src/vs/workbench/contrib/relauncher/browser/relauncher.contribution.ts @@ -30,6 +30,7 @@ interface IConfiguration extends IWindowsConfiguration { workbench?: { enableExperiments?: boolean }; _extensionsGallery?: { enablePPE?: boolean }; accessibility?: { verbosity?: { debug?: boolean } }; + files?: { experimentalWatcherNext?: boolean }; } export class SettingsChangeRelauncher extends Disposable implements IWorkbenchContribution { @@ -46,7 +47,8 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo 'workbench.enableExperiments', '_extensionsGallery.enablePPE', 'security.restrictUNCAccess', - 'accessibility.verbosity.debug' + 'accessibility.verbosity.debug', + 'files.experimentalWatcherNext' ]; private readonly titleBarStyle = new ChangeObserver('string'); @@ -61,6 +63,7 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo private readonly enablePPEExtensionsGallery = new ChangeObserver('boolean'); private readonly restrictUNCAccess = new ChangeObserver('boolean'); private readonly accessibilityVerbosityDebug = new ChangeObserver('boolean'); + private readonly filesExperimentalWatcherNext = new ChangeObserver('boolean'); constructor( @IHostService private readonly hostService: IHostService, @@ -123,6 +126,9 @@ export class SettingsChangeRelauncher extends Disposable implements IWorkbenchCo // Debug accessibility verbosity processChanged(this.accessibilityVerbosityDebug.handleChange(config?.accessibility?.verbosity?.debug)); + + // File watcher next + processChanged(this.filesExperimentalWatcherNext.handleChange(config?.files?.experimentalWatcherNext)); } // Experiments diff --git a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts index bf781d717da..101650b7dbd 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts @@ -30,12 +30,10 @@ import { getCodiconAriaLabel } from '../../../../base/common/iconLabels.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { ReloadWindowAction } from '../../../browser/actions/windowActions.js'; import { EXTENSION_INSTALL_SKIP_WALKTHROUGH_CONTEXT, IExtensionGalleryService, IExtensionManagementService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; -import { IExtensionsViewPaneContainer, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID, VIEWLET_ID } from '../../extensions/common/extensions.js'; +import { IExtensionsWorkbenchService, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID } from '../../extensions/common/extensions.js'; import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; import { IMarkdownString, MarkdownString } from '../../../../base/common/htmlContent.js'; import { RemoteNameContext, VirtualWorkspaceContext } from '../../../common/contextkeys.js'; -import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js'; -import { ViewContainerLocation } from '../../../common/views.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../base/common/actions.js'; import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; @@ -227,13 +225,8 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr }); } run = (accessor: ServicesAccessor, input: string) => { - const paneCompositeService = accessor.get(IPaneCompositePartService); - return paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true).then(viewlet => { - if (viewlet) { - (viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer).search(`@recommended:remotes`); - viewlet.focus(); - } - }); + const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); + return extensionsWorkbenchService.openSearch(`@recommended:remotes`); }; })); } diff --git a/src/vs/workbench/contrib/request/common/request.contribution.ts b/src/vs/workbench/contrib/request/common/request.contribution.ts deleted file mode 100644 index cdc1d1d4865..00000000000 --- a/src/vs/workbench/contrib/request/common/request.contribution.ts +++ /dev/null @@ -1,37 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Event } from '../../../../base/common/event.js'; -import { localize2 } from '../../../../nls.js'; -import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; -import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; -import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import { ILoggerService } from '../../../../platform/log/common/log.js'; -import { Registry } from '../../../../platform/registry/common/platform.js'; -import { Extensions, IOutputChannelRegistry, IOutputService } from '../../../services/output/common/output.js'; - -registerAction2(class extends Action2 { - constructor() { - super({ - id: 'workbench.actions.showNetworkLog', - title: localize2('showNetworkLog', "Show Network Log"), - category: Categories.Developer, - f1: true, - }); - } - async run(servicesAccessor: ServicesAccessor): Promise { - const loggerService = servicesAccessor.get(ILoggerService); - const outputService = servicesAccessor.get(IOutputService); - for (const logger of loggerService.getRegisteredLoggers()) { - if (logger.id.startsWith('network-')) { - loggerService.setVisibility(logger.id, true); - } - } - if (!outputService.getChannelDescriptor('network-window')) { - await Event.toPromise(Event.filter(Registry.as(Extensions.OutputChannels).onDidRegisterChannel, channel => channel === 'network-window')); - } - outputService.showChannel('network-window'); - } -}); diff --git a/src/vs/workbench/contrib/scm/browser/activity.ts b/src/vs/workbench/contrib/scm/browser/activity.ts index faca874aa54..a5d9ca5adde 100644 --- a/src/vs/workbench/contrib/scm/browser/activity.ts +++ b/src/vs/workbench/contrib/scm/browser/activity.ts @@ -36,12 +36,12 @@ export class SCMActiveRepositoryController extends Disposable implements IWorkbe Event.any(this.scmService.onDidAddRepository, this.scmService.onDidRemoveRepository), () => this.scmService.repositories); - private readonly _activeRepositoryCurrentHistoryItemGroupName = derived(reader => { + private readonly _activeRepositoryHistoryItemRefName = derived(reader => { const repository = this.scmViewService.activeRepository.read(reader); const historyProvider = repository?.provider.historyProvider.read(reader); - const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup.read(reader); + const historyItemRef = historyProvider?.historyItemRef.read(reader); - return currentHistoryItemGroup?.name; + return historyItemRef?.name; }); private readonly _countBadgeRepositories = derived(this, reader => { @@ -109,9 +109,9 @@ export class SCMActiveRepositoryController extends Disposable implements IWorkbe this._register(autorun(reader => { const repository = this.scmViewService.activeRepository.read(reader); - const currentHistoryItemGroupName = this._activeRepositoryCurrentHistoryItemGroupName.read(reader); + const historyItemRefName = this._activeRepositoryHistoryItemRefName.read(reader); - this._updateActiveRepositoryContextKeys(repository?.provider.name, currentHistoryItemGroupName); + this._updateActiveRepositoryContextKeys(repository?.provider.name, historyItemRefName); })); } diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index f41adfd5435..5088f3878d9 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -139,10 +139,22 @@ height: 22px; } +.scm-view .monaco-list-row .history-item > .graph-container.current > .graph > circle:last-child { + fill: var(--vscode-sideBar-background); +} + +.scm-view .monaco-list-row:hover .history-item > .graph-container.current > .graph > circle:last-child { + fill: var(--vscode-list-hoverBackground); +} + .scm-view .monaco-list-row .history-item > .graph-container > .graph > circle { stroke: var(--vscode-sideBar-background); } +.scm-view .monaco-list-row:hover .history-item > .graph-container > .graph > circle { + stroke: var(--vscode-list-hoverBackground); +} + .scm-view .monaco-list-row .history-item > .label-container { display: flex; flex-shrink: 0; @@ -473,7 +485,11 @@ } .monaco-hover.history-item-hover p:last-child { - margin-bottom: 4px; + margin-bottom: 0; +} + +.monaco-hover.history-item-hover p:last-child span:not(.codicon) { + margin-bottom: 2px !important; } .monaco-hover.history-item-hover hr { @@ -485,26 +501,60 @@ margin: 4px 0; } -.monaco-hover.history-item-hover .markdown-hover .hover-contents:not(.code-hover-contents):not(.html-hover-contents) span:not(.codicon) { +.monaco-hover.history-item-hover span:not(.codicon) { margin-bottom: 0 !important; } +.monaco-hover.history-item-hover .hover-row.status-bar .action { + display: flex; + align-items: center; +} + +.monaco-hover.history-item-hover .hover-row.status-bar .action .codicon { + color: inherit; + font-size: 12px; +} + /* Graph */ +.pane-header .scm-graph-view-badge-container { + display: flex; + align-items: center; + min-width: fit-content; +} + +.pane-header .scm-graph-view-badge-container > .scm-graph-view-badge.monaco-count-badge.long { + background-color: var(--vscode-badge-background); + color: var(--vscode-badge-foreground); + border: 1px solid var(--vscode-contrastBorder); + margin-left: 6px; + padding: 2px 4px; +} + .monaco-workbench .part.auxiliarybar > .title > .title-actions .action-label.scm-graph-repository-picker { display: flex; } -.monaco-toolbar .action-label.scm-graph-repository-picker { +.monaco-toolbar .action-label.scm-graph-repository-picker, +.monaco-toolbar .action-label.scm-graph-history-item-picker { + color: var(--vscode-icon-foreground); align-items: center; font-weight: normal; line-height: 16px; } -.monaco-toolbar .action-label.scm-graph-repository-picker .codicon { +.monaco-toolbar .action-label.scm-graph-repository-picker .codicon, +.monaco-toolbar .action-label.scm-graph-history-item-picker .codicon { font-size: 14px; } +.monaco-toolbar .action-label.scm-graph-repository-picker > .name, +.monaco-toolbar .action-label.scm-graph-history-item-picker > .name { + max-width: 100px; + overflow: hidden; + text-overflow: ellipsis; +} + .scm-history-view .scm-provider .label-name { font-weight: bold; } @@ -558,7 +608,7 @@ .scm-history-view .history-item-load-more .history-item-placeholder.shimmer .monaco-icon-label-container { height: 18px; - background: var(--vscode-scm-historyItemDefaultLabelBackground); + background: var(--vscode-scmGraph-historyItemHoverDefaultLabelBackground); border-radius: 2px; opacity: 0.5; } diff --git a/src/vs/workbench/contrib/scm/browser/scmHistory.ts b/src/vs/workbench/contrib/scm/browser/scmHistory.ts index c51e6fbcdd8..b7c0dc13380 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistory.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistory.ts @@ -5,25 +5,24 @@ import { localize } from '../../../../nls.js'; import { deepClone } from '../../../../base/common/objects.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; -import { buttonForeground, foreground } from '../../../../platform/theme/common/colorRegistry.js'; -import { chartsBlue, chartsGreen, chartsOrange, chartsPurple, chartsRed, chartsYellow } from '../../../../platform/theme/common/colors/chartsColors.js'; +import { buttonForeground, chartsBlue, chartsPurple, foreground } from '../../../../platform/theme/common/colorRegistry.js'; import { asCssVariable, ColorIdentifier, registerColor, transparent } from '../../../../platform/theme/common/colorUtils.js'; -import { ISCMHistoryItem, ISCMHistoryItemGraphNode, ISCMHistoryItemViewModel } from '../common/history.js'; +import { ISCMHistoryItem, ISCMHistoryItemGraphNode, ISCMHistoryItemRef, ISCMHistoryItemViewModel } from '../common/history.js'; import { rot } from '../../../../base/common/numbers.js'; import { svgElem } from '../../../../base/browser/dom.js'; export const SWIMLANE_HEIGHT = 22; export const SWIMLANE_WIDTH = 11; -const CIRCLE_RADIUS = 4; const SWIMLANE_CURVE_RADIUS = 5; +const CIRCLE_RADIUS = 4; +const CIRCLE_STROKE_WIDTH = 2; /** - * History graph colors (local, remote, base) + * History item reference colors (local, remote, base) */ -export const historyItemGroupLocal = registerColor('scmGraph.historyItemGroupLocal', chartsBlue, localize('scmGraphHistoryItemGroupLocal', "Local history item group color.")); -export const historyItemGroupRemote = registerColor('scmGraph.historyItemGroupRemote', chartsPurple, localize('scmGraphHistoryItemGroupRemote', "Remote history item group color.")); -export const historyItemGroupBase = registerColor('scmGraph.historyItemGroupBase', chartsOrange, localize('scmGraphHistoryItemGroupBase', "Base history item group color.")); +export const historyItemRefColor = registerColor('scmGraph.historyItemRefColor', chartsBlue, localize('scmGraphHistoryItemRefColor', "History item reference color.")); +export const historyItemRemoteRefColor = registerColor('scmGraph.historyItemRemoteRefColor', chartsPurple, localize('scmGraphHistoryItemRemoteRefColor', "History item remote reference color.")); +export const historyItemBaseRefColor = registerColor('scmGraph.historyItemBaseRefColor', '#EA5C00', localize('scmGraphHistoryItemBaseRefColor', "History item base reference color.")); /** * History item hover color @@ -38,16 +37,18 @@ export const historyItemHoverDeletionsForeground = registerColor('scmGraph.histo * History graph color registry */ export const colorRegistry: ColorIdentifier[] = [ - registerColor('scmGraph.foreground1', chartsGreen, localize('scmGraphForeground1', "Source control graph foreground color (1).")), - registerColor('scmGraph.foreground2', chartsRed, localize('scmGraphForeground2', "Source control graph foreground color (2).")), - registerColor('scmGraph.foreground3', chartsYellow, localize('scmGraphForeground3', "Source control graph foreground color (3).")), + registerColor('scmGraph.foreground1', '#FFB000', localize('scmGraphForeground1', "Source control graph foreground color (1).")), + registerColor('scmGraph.foreground2', '#DC267F', localize('scmGraphForeground2', "Source control graph foreground color (2).")), + registerColor('scmGraph.foreground3', '#994F00', localize('scmGraphForeground3', "Source control graph foreground color (3).")), + registerColor('scmGraph.foreground4', '#40B0A6', localize('scmGraphForeground4', "Source control graph foreground color (4).")), + registerColor('scmGraph.foreground5', '#B66DFF', localize('scmGraphForeground5', "Source control graph foreground color (5).")), ]; -function getLabelColorIdentifier(historyItem: ISCMHistoryItem, colorMap: Map): ColorIdentifier | undefined { - for (const label of historyItem.labels ?? []) { - const colorIndex = colorMap.get(label.title); - if (colorIndex !== undefined) { - return colorIndex; +function getLabelColorIdentifier(historyItem: ISCMHistoryItem, colorMap: Map): ColorIdentifier | undefined { + for (const ref of historyItem.references ?? []) { + const colorIdentifier = colorMap.get(ref.id); + if (colorIdentifier !== undefined) { + return colorIdentifier; } } @@ -64,12 +65,16 @@ function createPath(colorIdentifier: string): SVGPathElement { return path; } -function drawCircle(index: number, radius: number, colorIdentifier: string): SVGCircleElement { +function drawCircle(index: number, radius: number, strokeWidth: number, colorIdentifier?: string): SVGCircleElement { const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); circle.setAttribute('cx', `${SWIMLANE_WIDTH * (index + 1)}`); circle.setAttribute('cy', `${SWIMLANE_WIDTH}`); circle.setAttribute('r', `${radius}`); - circle.style.fill = asCssVariable(colorIdentifier); + + circle.style.strokeWidth = `${strokeWidth}px`; + if (colorIdentifier) { + circle.style.fill = asCssVariable(colorIdentifier); + } return circle; } @@ -107,7 +112,7 @@ export function renderSCMHistoryItemGraph(historyItemViewModel: ISCMHistoryItemV // Circle color - use the output swimlane color if present, otherwise the input swimlane color const circleColor = circleIndex < outputSwimlanes.length ? outputSwimlanes[circleIndex].color : - circleIndex < inputSwimlanes.length ? inputSwimlanes[circleIndex].color : historyItemGroupLocal; + circleIndex < inputSwimlanes.length ? inputSwimlanes[circleIndex].color : historyItemRefColor; let outputSwimlaneIndex = 0; for (let index = 0; index < inputSwimlanes.length; index++) { @@ -205,24 +210,26 @@ export function renderSCMHistoryItemGraph(historyItemViewModel: ISCMHistoryItemV } // Draw * - if (historyItem.parentIds.length > 1) { - // Multi-parent node - const circleOuter = drawCircle(circleIndex, CIRCLE_RADIUS + 1, circleColor); - svg.append(circleOuter); - - const circleInner = drawCircle(circleIndex, CIRCLE_RADIUS - 1, circleColor); - svg.append(circleInner); - } else { + if (historyItemViewModel.isCurrent) { // HEAD - // TODO@lszomoru - implement a better way to determine if the commit is HEAD - if (historyItem.labels?.some(l => ThemeIcon.isThemeIcon(l.icon) && l.icon.id === 'target')) { - const outerCircle = drawCircle(circleIndex, CIRCLE_RADIUS + 2, circleColor); - svg.append(outerCircle); - } + const outerCircle = drawCircle(circleIndex, CIRCLE_RADIUS + 3, CIRCLE_STROKE_WIDTH, circleColor); + svg.append(outerCircle); - // Node - const circle = drawCircle(circleIndex, CIRCLE_RADIUS, circleColor); - svg.append(circle); + const innerCircle = drawCircle(circleIndex, CIRCLE_STROKE_WIDTH, CIRCLE_RADIUS); + svg.append(innerCircle); + } else { + if (historyItem.parentIds.length > 1) { + // Multi-parent node + const circleOuter = drawCircle(circleIndex, CIRCLE_RADIUS + 2, CIRCLE_STROKE_WIDTH, circleColor); + svg.append(circleOuter); + + const circleInner = drawCircle(circleIndex, CIRCLE_RADIUS - 1, CIRCLE_STROKE_WIDTH, circleColor); + svg.append(circleInner); + } else { + // Node + const circle = drawCircle(circleIndex, CIRCLE_RADIUS + 1, CIRCLE_STROKE_WIDTH, circleColor); + svg.append(circle); + } } // Set dimensions @@ -246,13 +253,18 @@ export function renderSCMHistoryGraphPlaceholder(columns: ISCMHistoryItemGraphNo return elements.root; } -export function toISCMHistoryItemViewModelArray(historyItems: ISCMHistoryItem[], colorMap = new Map()): ISCMHistoryItemViewModel[] { +export function toISCMHistoryItemViewModelArray( + historyItems: ISCMHistoryItem[], + colorMap = new Map(), + currentHistoryItemRef?: ISCMHistoryItemRef +): ISCMHistoryItemViewModel[] { let colorIndex = -1; const viewModels: ISCMHistoryItemViewModel[] = []; for (let index = 0; index < historyItems.length; index++) { const historyItem = historyItems[index]; + const isCurrent = historyItem.id === currentHistoryItemRef?.revision; const outputSwimlanesFromPreviousItem = viewModels.at(-1)?.outputSwimlanes ?? []; const inputSwimlanes = outputSwimlanesFromPreviousItem.map(i => deepClone(i)); const outputSwimlanes: ISCMHistoryItemGraphNode[] = []; @@ -302,11 +314,11 @@ export function toISCMHistoryItemViewModelArray(historyItems: ISCMHistoryItem[], }); } - // Add colors to labels - const labels = (historyItem.labels ?? []) - .map(label => { - let color = colorMap.get(label.title); - if (!color && colorMap.has('*')) { + // Add colors to references + const references = (historyItem.references ?? []) + .map(ref => { + let color = colorMap.get(ref.id); + if (colorMap.has(ref.id) && color === undefined) { // Find the history item in the input swimlanes const inputIndex = inputSwimlanes.findIndex(node => node.id === historyItem.id); @@ -315,17 +327,18 @@ export function toISCMHistoryItemViewModelArray(historyItems: ISCMHistoryItem[], // Circle color - use the output swimlane color if present, otherwise the input swimlane color color = circleIndex < outputSwimlanes.length ? outputSwimlanes[circleIndex].color : - circleIndex < inputSwimlanes.length ? inputSwimlanes[circleIndex].color : historyItemGroupLocal; + circleIndex < inputSwimlanes.length ? inputSwimlanes[circleIndex].color : historyItemRefColor; } - return { ...label, color }; + return { ...ref, color }; }); viewModels.push({ historyItem: { ...historyItem, - labels + references }, + isCurrent, inputSwimlanes, outputSwimlanes, }); diff --git a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts index 51916fe9657..5d03872692f 100644 --- a/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts +++ b/src/vs/workbench/contrib/scm/browser/scmHistoryViewPane.ts @@ -16,7 +16,7 @@ import { fromNow } from '../../../../base/common/date.js'; import { createMatches, FuzzyScore, IMatch } from '../../../../base/common/filters.js'; import { MarkdownString } from '../../../../base/common/htmlContent.js'; import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; -import { autorun, autorunWithStore, autorunWithStoreHandleChanges, derived, derivedOpts, IObservable, observableValue } from '../../../../base/common/observable.js'; +import { autorun, autorunWithStore, autorunWithStoreHandleChanges, derived, IObservable, observableValue, waitForState, constObservable, latestChangedValue, observableFromEvent, runOnChange, signalFromObservable } from '../../../../base/common/observable.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { localize } from '../../../../nls.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; @@ -32,9 +32,9 @@ import { asCssVariable, ColorIdentifier, foreground } from '../../../../platform import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { IViewPaneOptions, ViewAction, ViewPane, ViewPaneShowActions } from '../../../browser/parts/views/viewPane.js'; import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js'; -import { renderSCMHistoryItemGraph, historyItemGroupLocal, historyItemGroupRemote, historyItemGroupBase, toISCMHistoryItemViewModelArray, SWIMLANE_WIDTH, renderSCMHistoryGraphPlaceholder, historyItemHoverDeletionsForeground, historyItemHoverLabelForeground, historyItemHoverAdditionsForeground, historyItemHoverDefaultLabelForeground, historyItemHoverDefaultLabelBackground } from './scmHistory.js'; +import { renderSCMHistoryItemGraph, toISCMHistoryItemViewModelArray, SWIMLANE_WIDTH, renderSCMHistoryGraphPlaceholder, historyItemHoverDeletionsForeground, historyItemHoverLabelForeground, historyItemHoverAdditionsForeground, historyItemHoverDefaultLabelForeground, historyItemHoverDefaultLabelBackground } from './scmHistory.js'; import { isSCMHistoryItemLoadMoreTreeElement, isSCMHistoryItemViewModelTreeElement, isSCMRepository } from './util.js'; -import { ISCMHistoryItem, ISCMHistoryItemGroup, ISCMHistoryItemViewModel, SCMHistoryItemLoadMoreTreeElement, SCMHistoryItemViewModelTreeElement } from '../common/history.js'; +import { ISCMHistoryItem, ISCMHistoryItemRef, ISCMHistoryItemViewModel, ISCMHistoryProvider, SCMHistoryItemLoadMoreTreeElement, SCMHistoryItemViewModelTreeElement } from '../common/history.js'; import { HISTORY_VIEW_PANE_ID, ISCMProvider, ISCMRepository, ISCMService, ISCMViewService } from '../common/scm.js'; import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js'; import { stripIcons } from '../../../../base/common/iconLabels.js'; @@ -45,21 +45,23 @@ import { Sequencer, Throttler } from '../../../../base/common/async.js'; import { URI } from '../../../../base/common/uri.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { ActionRunner, IAction, IActionRunner } from '../../../../base/common/actions.js'; -import { tail } from '../../../../base/common/arrays.js'; +import { delta, groupBy, tail } from '../../../../base/common/arrays.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { IProgressService } from '../../../../platform/progress/common/progress.js'; -import { constObservable, derivedConstOnceDefined, latestChangedValue, observableFromEvent } from '../../../../base/common/observableInternal/utils.js'; import { ContextKeys } from './scmViewPane.js'; import { IActionViewItem } from '../../../../base/browser/ui/actionbar/actionbar.js'; import { IDropdownMenuActionViewItemOptions } from '../../../../base/browser/ui/dropdown/dropdownActionViewItem.js'; import { ActionViewItem } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; -import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../platform/quickinput/common/quickInput.js'; import { Event } from '../../../../base/common/event.js'; import { Iterable } from '../../../../base/common/iterator.js'; import { clamp } from '../../../../base/common/numbers.js'; import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js'; -import { structuralEquals } from '../../../../base/common/equals.js'; +import { compare } from '../../../../base/common/strings.js'; +import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; + +const PICK_REPOSITORY_ACTION_ID = 'workbench.scm.action.graph.pickRepository'; +const PICK_HISTORY_ITEM_REFS_ACTION_ID = 'workbench.scm.action.graph.pickHistoryItemRefs'; type TreeElement = SCMHistoryItemViewModelTreeElement | SCMHistoryItemLoadMoreTreeElement; @@ -71,7 +73,70 @@ class SCMRepositoryActionViewItem extends ActionViewItem { protected override updateLabel(): void { if (this.options.label && this.label) { this.label.classList.add('scm-graph-repository-picker'); - reset(this.label, ...renderLabelWithIcons(`$(repo) ${this._repository.provider.name}`)); + + const icon = $('.icon'); + icon.classList.add(...ThemeIcon.asClassNameArray(Codicon.repo)); + + const name = $('.name'); + name.textContent = this._repository.provider.name; + + + reset(this.label, icon, name); + } + } + + protected override getTooltip(): string | undefined { + return this._repository.provider.name; + } +} + +class SCMHistoryItemRefsActionViewItem extends ActionViewItem { + constructor( + private readonly _repository: ISCMRepository, + private readonly _historyItemsFilter: HistoryItemRefsFilter, + action: IAction, + options?: IDropdownMenuActionViewItemOptions + ) { + super(null, action, { ...options, icon: false, label: true }); + } + + protected override updateLabel(): void { + if (this.options.label && this.label) { + this.label.classList.add('scm-graph-history-item-picker'); + + const icon = $('.icon'); + icon.classList.add(...ThemeIcon.asClassNameArray(Codicon.gitBranch)); + + const name = $('.name'); + if (this._historyItemsFilter === 'all') { + name.textContent = localize('all', "All"); + } else if (this._historyItemsFilter === 'auto') { + name.textContent = localize('auto', "Auto"); + } else if (this._historyItemsFilter.length === 1) { + name.textContent = this._historyItemsFilter[0].name; + } else { + name.textContent = localize('items', "{0} Items", this._historyItemsFilter.length); + } + + reset(this.label, icon, name); + } + } + + protected override getTooltip(): string | undefined { + if (this._historyItemsFilter === 'all') { + return localize('allHistoryItemRefs', "All history item references"); + } else if (this._historyItemsFilter === 'auto') { + const historyProvider = this._repository.provider.historyProvider.get(); + + return [ + historyProvider?.historyItemRef.get()?.name, + historyProvider?.historyItemRemoteRef.get()?.name, + historyProvider?.historyItemBaseRef.get()?.name + ].filter(ref => !!ref).join(', '); + } else if (this._historyItemsFilter.length === 1) { + return this._historyItemsFilter[0].name; + } else { + return this._historyItemsFilter.map(ref => ref.name).join(', '); } } } @@ -79,7 +144,7 @@ class SCMRepositoryActionViewItem extends ActionViewItem { registerAction2(class extends ViewAction { constructor() { super({ - id: 'workbench.scm.action.repository', + id: PICK_REPOSITORY_ACTION_ID, title: '', viewId: HISTORY_VIEW_PANE_ID, f1: false, @@ -100,7 +165,29 @@ registerAction2(class extends ViewAction { registerAction2(class extends ViewAction { constructor() { super({ - id: 'workbench.scm.action.refreshGraph', + id: PICK_HISTORY_ITEM_REFS_ACTION_ID, + title: '', + icon: Codicon.gitBranch, + viewId: HISTORY_VIEW_PANE_ID, + f1: false, + menu: { + id: MenuId.SCMHistoryTitle, + group: 'navigation', + order: 0 + } + }); + } + + async runInView(_: ServicesAccessor, view: SCMHistoryViewPane): Promise { + view.pickHistoryItemRef(); + } +}); + + +registerAction2(class extends ViewAction { + constructor() { + super({ + id: 'workbench.scm.action.graph.refresh', title: localize('refreshGraph', "Refresh"), viewId: HISTORY_VIEW_PANE_ID, f1: false, @@ -121,7 +208,7 @@ registerAction2(class extends ViewAction { registerAction2(class extends Action2 { constructor() { super({ - id: 'workbench.scm.action.scm.viewChanges', + id: 'workbench.scm.action.graph.viewChanges', title: localize('viewChanges', "View Changes"), f1: false, menu: [ @@ -146,7 +233,7 @@ registerAction2(class extends Action2 { const historyProvider = provider.historyProvider.get(); if (historyItems.length > 1) { - const ancestor = await historyProvider?.resolveHistoryItemGroupCommonAncestor([historyItem.id, historyItemLast.id]); + const ancestor = await historyProvider?.resolveHistoryItemRefsCommonAncestor([historyItem.id, historyItemLast.id]); if (!ancestor || (ancestor !== historyItem.id && ancestor !== historyItemLast.id)) { return; } @@ -206,6 +293,7 @@ class HistoryItemRenderer implements ITreeRenderer { const labelConfig = this._badgesConfig.read(reader); templateData.labelContainer.textContent = ''; - const firstColoredLabel = historyItem.labels?.find(label => label.color); + const firstColoredRef = historyItem.references?.find(ref => ref.color); - for (const label of historyItem.labels ?? []) { - if (!label.color && labelConfig === 'filter') { + for (const ref of historyItem.references ?? []) { + if (!ref.color && labelConfig === 'filter') { continue; } - if (label.icon && ThemeIcon.isThemeIcon(label.icon)) { + if (ref.icon && ThemeIcon.isThemeIcon(ref.icon)) { const elements = h('div.label', { style: { - color: label.color ? asCssVariable(historyItemHoverLabelForeground) : asCssVariable(foreground), - backgroundColor: label.color ? asCssVariable(label.color) : asCssVariable(historyItemHoverDefaultLabelBackground) + color: ref.color ? asCssVariable(historyItemHoverLabelForeground) : asCssVariable(foreground), + backgroundColor: ref.color ? asCssVariable(ref.color) : asCssVariable(historyItemHoverDefaultLabelBackground) } }, [ h('div.icon@icon'), h('div.description@description') ]); - elements.icon.classList.add(...ThemeIcon.asClassNameArray(label.icon)); + elements.icon.classList.add(...ThemeIcon.asClassNameArray(ref.icon)); - elements.description.textContent = label.title; - elements.description.style.display = label === firstColoredLabel ? '' : 'none'; + elements.description.textContent = ref.name; + elements.description.style.display = ref === firstColoredRef ? '' : 'none'; append(templateData.labelContainer, elements.root); } @@ -278,12 +369,29 @@ class HistoryItemRenderer implements ITreeRenderer this._clipboardService.writeText(historyItem.id) + }, + { + commandId: 'workbench.scm.action.graph.copyHistoryItemMessage', + iconClass: 'codicon.codicon-copy', + label: localize('historyItemMessage', "Message"), + run: () => this._clipboardService.writeText(historyItem.message) + } + ]; + } + + private _getHoverContent(element: SCMHistoryItemViewModelTreeElement): IManagedHoverTooltipMarkdownString { const colorTheme = this._themeService.getColorTheme(); const historyItem = element.historyItemViewModel.historyItem; const markdown = new MarkdownString('', { isTrusted: true, supportThemeIcons: true }); - markdown.appendMarkdown(`$(git-commit) \`${historyItem.displayId ?? historyItem.id}\`\n\n`); + // markdown.appendMarkdown(`$(git-commit) \`${historyItem.displayId ?? historyItem.id}\`\n\n`); if (historyItem.author) { markdown.appendMarkdown(`$(account) **${historyItem.author}**`); @@ -320,22 +428,22 @@ class HistoryItemRenderer implements ITreeRenderer 0) { + if ((historyItem.references ?? []).length > 0) { markdown.appendMarkdown(`\n\n---\n\n`); - markdown.appendMarkdown((historyItem.labels ?? []).map(label => { - const labelIconId = ThemeIcon.isThemeIcon(label.icon) ? label.icon.id : ''; + markdown.appendMarkdown((historyItem.references ?? []).map(ref => { + const labelIconId = ThemeIcon.isThemeIcon(ref.icon) ? ref.icon.id : ''; - const labelBackgroundColor = label.color ? asCssVariable(label.color) : asCssVariable(historyItemHoverDefaultLabelBackground); - const labelForegroundColor = label.color ? asCssVariable(historyItemHoverLabelForeground) : asCssVariable(historyItemHoverDefaultLabelForeground); + const labelBackgroundColor = ref.color ? asCssVariable(ref.color) : asCssVariable(historyItemHoverDefaultLabelBackground); + const labelForegroundColor = ref.color ? asCssVariable(historyItemHoverLabelForeground) : asCssVariable(historyItemHoverDefaultLabelForeground); - return ` $(${labelIconId}) ${label.title} `; + return ` $(${labelIconId}) ${ref.name} `; }).join('  ')); } return { markdown, markdownNotSupportedFallback: historyItem.message }; } - private processMatches(historyItemViewModel: ISCMHistoryItemViewModel, filterData: LabelFuzzyScore | undefined): [IMatch[] | undefined, IMatch[] | undefined] { + private _processMatches(historyItemViewModel: ISCMHistoryItemViewModel, filterData: LabelFuzzyScore | undefined): [IMatch[] | undefined, IMatch[] | undefined] { if (!filterData) { return [undefined, undefined]; } @@ -510,8 +618,6 @@ class SCMHistoryTreeKeyboardNavigationLabelProvider implements IKeyboardNavigati } } -type HistoryItemState = { currentHistoryItemGroup: ISCMHistoryItemGroup; items: ISCMHistoryItem[]; loadMore: boolean }; - class SCMHistoryTreeDataSource extends Disposable implements IAsyncDataSource { async getChildren(inputOrElement: SCMHistoryViewModel | TreeElement): Promise> { @@ -543,6 +649,9 @@ class SCMHistoryTreeDataSource extends Disposable implements IAsyncDataSource(this, 'auto'); - - readonly historyItemGroupFilter = derived(reader => { - const filter = this._historyItemGroupFilter.read(reader); - if (Array.isArray(filter)) { - return filter; - } - - if (filter === 'all') { - return []; - } - - const repository = this.repository.get(); - const historyProvider = repository?.provider.historyProvider.get(); - const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup.get(); - - if (!currentHistoryItemGroup) { - return []; - } - - return [ - currentHistoryItemGroup.revision ?? currentHistoryItemGroup.id, - ...currentHistoryItemGroup.remote ? [currentHistoryItemGroup.remote.revision ?? currentHistoryItemGroup.remote.id] : [], - ...currentHistoryItemGroup.base ? [currentHistoryItemGroup.base.revision ?? currentHistoryItemGroup.base.id] : [], - ]; - }); + readonly historyItemsFilter = observableValue(this, 'auto'); private readonly _state = new Map(); @@ -652,24 +735,44 @@ class SCMHistoryViewModel extends Disposable { let state = this._state.get(repository); const historyProvider = repository.provider.historyProvider.get(); - const currentHistoryItemGroup = state?.currentHistoryItemGroup ?? historyProvider?.currentHistoryItemGroup.get(); - if (!historyProvider || !currentHistoryItemGroup) { + if (!historyProvider) { return []; } if (!state || state.loadMore) { const existingHistoryItems = state?.items ?? []; + let historyItemRefs = state?.historyItemRefs; + + if (!historyItemRefs) { + const historyItemsFilter = this.historyItemsFilter.get(); + + switch (historyItemsFilter) { + case 'all': + historyItemRefs = await historyProvider.provideHistoryItemRefs() ?? []; + break; + case 'auto': + historyItemRefs = [ + historyProvider.historyItemRef.get(), + historyProvider.historyItemRemoteRef.get(), + historyProvider.historyItemBaseRef.get(), + ].filter(ref => !!ref); + break; + default: + historyItemRefs = historyItemsFilter; + break; + } + } - const historyItemGroupIds = this.historyItemGroupFilter.get(); const limit = clamp(this._configurationService.getValue('scm.graph.pageSize'), 1, 1000); + const historyItemRefIds = historyItemRefs.map(ref => ref.revision ?? ref.id); const historyItems = await historyProvider.provideHistoryItems({ - historyItemGroupIds, limit, skip: existingHistoryItems.length + historyItemRefs: historyItemRefIds, limit, skip: existingHistoryItems.length }) ?? []; state = { - currentHistoryItemGroup, + historyItemRefs, items: [...existingHistoryItems, ...historyItems], loadMore: false }; @@ -678,9 +781,9 @@ class SCMHistoryViewModel extends Disposable { } // Create the color map - const colorMap = this._getGraphColorMap(currentHistoryItemGroup); + const colorMap = this._getGraphColorMap(state.historyItemRefs); - return toISCMHistoryItemViewModelArray(state.items, colorMap) + return toISCMHistoryItemViewModelArray(state.items, colorMap, historyProvider.historyItemRef.get()) .map(historyItemViewModel => ({ repository, historyItemViewModel, @@ -692,18 +795,38 @@ class SCMHistoryViewModel extends Disposable { this._selectedRepository.set(repository, undefined); } - private _getGraphColorMap(currentHistoryItemGroup: ISCMHistoryItemGroup): Map { - const colorMap = new Map([ - [currentHistoryItemGroup.name, historyItemGroupLocal] - ]); - if (currentHistoryItemGroup.remote) { - colorMap.set(currentHistoryItemGroup.remote.name, historyItemGroupRemote); + setHistoryItemsFilter(filter: 'all' | 'auto' | ISCMHistoryItemRef[]): void { + this.historyItemsFilter.set(filter, undefined); + } + + private _getGraphColorMap(historyItemRefs: ISCMHistoryItemRef[]): Map { + const repository = this.repository.get(); + const historyProvider = repository?.provider.historyProvider.get(); + const historyItemRef = historyProvider?.historyItemRef.get(); + const historyItemRemoteRef = historyProvider?.historyItemRemoteRef.get(); + const historyItemBaseRef = historyProvider?.historyItemBaseRef.get(); + + const colorMap = new Map(); + + if (historyItemRef) { + colorMap.set(historyItemRef.id, historyItemRef.color); + + if (historyItemRemoteRef) { + colorMap.set(historyItemRemoteRef.id, historyItemRemoteRef.color); + } + if (historyItemBaseRef) { + colorMap.set(historyItemBaseRef.id, historyItemBaseRef.color); + } } - if (currentHistoryItemGroup.base) { - colorMap.set(currentHistoryItemGroup.base.name, historyItemGroupBase); - } - if (this._historyItemGroupFilter.get() === 'all') { - colorMap.set('*', ''); + + // Add the remaining history item references to the color map + // if not already present. These history item references will + // be colored using the color of the history item to which they + // point to. + for (const ref of historyItemRefs) { + if (!colorMap.has(ref.id)) { + colorMap.set(ref.id, undefined); + } } return colorMap; @@ -715,6 +838,175 @@ class SCMHistoryViewModel extends Disposable { } } +type RepositoryQuickPickItem = IQuickPickItem & { repository: 'auto' | ISCMRepository }; + +class RepositoryPicker extends Disposable { + private readonly _autoQuickPickItem: RepositoryQuickPickItem = { + label: localize('auto', "Auto"), + description: localize('activeRepository', "Show the source control graph for the active repository"), + repository: 'auto' + }; + + constructor( + @IQuickInputService private readonly _quickInputService: IQuickInputService, + @ISCMViewService private readonly _scmViewService: ISCMViewService + ) { + super(); + } + + async pickRepository(): Promise { + const picks: (RepositoryQuickPickItem | IQuickPickSeparator)[] = [ + this._autoQuickPickItem, + { type: 'separator' }]; + + picks.push(...this._scmViewService.repositories.map(r => ({ + label: r.provider.name, + description: r.provider.rootUri?.fsPath, + iconClass: ThemeIcon.asClassName(Codicon.repo), + repository: r + }))); + + return this._quickInputService.pick(picks, { + placeHolder: localize('scmGraphRepository', "Select the repository to view, type to filter all repositories") + }); + } +} + +type HistoryItemRefQuickPickItem = IQuickPickItem & { historyItemRef: 'all' | 'auto' | ISCMHistoryItemRef }; + +class HistoryItemRefPicker extends Disposable { + private readonly _allQuickPickItem: HistoryItemRefQuickPickItem = { + id: 'all', + label: localize('all', "All"), + description: localize('allHistoryItemRefs', "All history item references"), + historyItemRef: 'all' + }; + + private readonly _autoQuickPickItem: HistoryItemRefQuickPickItem = { + id: 'auto', + label: localize('auto', "Auto"), + description: localize('currentHistoryItemRef', "Current history item reference(s)"), + historyItemRef: 'auto' + }; + + constructor( + private readonly _historyProvider: ISCMHistoryProvider, + private readonly _historyItemsFilter: 'all' | 'auto' | ISCMHistoryItemRef[], + @IQuickInputService private readonly _quickInputService: IQuickInputService, + ) { + super(); + } + + async pickHistoryItemRef(): Promise<'all' | 'auto' | ISCMHistoryItemRef[] | undefined> { + const quickPick = this._quickInputService.createQuickPick({ useSeparators: true }); + this._store.add(quickPick); + + quickPick.placeholder = localize('scmGraphHistoryItemRef', "Select one/more history item references to view, type to filter"); + quickPick.canSelectMany = true; + quickPick.hideCheckAll = true; + quickPick.busy = true; + quickPick.show(); + + const items = await this._createQuickPickItems(); + + // Set initial selection + let selectedItems: HistoryItemRefQuickPickItem[] = []; + if (this._historyItemsFilter === 'all') { + selectedItems.push(this._allQuickPickItem); + } else if (this._historyItemsFilter === 'auto') { + selectedItems.push(this._autoQuickPickItem); + } else { + let index = 0; + while (index < items.length) { + if (items[index].type === 'separator') { + index++; + continue; + } + + if (this._historyItemsFilter.some(ref => ref.id === items[index].id)) { + const item = items.splice(index, 1) as HistoryItemRefQuickPickItem[]; + selectedItems.push(...item); + } else { + index++; + } + } + + // Insert the selected items after `All` and `Auto` + items.splice(2, 0, { type: 'separator' }, ...selectedItems); + } + + quickPick.items = items; + quickPick.selectedItems = selectedItems; + quickPick.busy = false; + + return new Promise<'all' | 'auto' | ISCMHistoryItemRef[] | undefined>(resolve => { + this._store.add(quickPick.onDidChangeSelection(items => { + const { added } = delta(selectedItems, items, (a, b) => compare(a.id ?? '', b.id ?? '')); + if (added.length > 0) { + if (added[0].historyItemRef === 'all' || added[0].historyItemRef === 'auto') { + quickPick.selectedItems = [added[0]]; + } else { + // Remove 'all' and 'auto' items if present + quickPick.selectedItems = [...quickPick.selectedItems + .filter(i => i.historyItemRef !== 'all' && i.historyItemRef !== 'auto')]; + } + } + + selectedItems = [...quickPick.selectedItems]; + })); + + this._store.add(quickPick.onDidAccept(() => { + if (selectedItems.length === 0) { + resolve(undefined); + } else if (selectedItems.length === 1 && selectedItems[0].historyItemRef === 'all') { + resolve('all'); + } else if (selectedItems.length === 1 && selectedItems[0].historyItemRef === 'auto') { + resolve('auto'); + } else { + resolve(selectedItems.map(item => item.historyItemRef) as ISCMHistoryItemRef[]); + } + + quickPick.hide(); + })); + + this._store.add(quickPick.onDidHide(() => { + resolve(undefined); + this.dispose(); + })); + }); + } + + private async _createQuickPickItems(): Promise<(HistoryItemRefQuickPickItem | IQuickPickSeparator)[]> { + const picks: (HistoryItemRefQuickPickItem | IQuickPickSeparator)[] = [ + this._allQuickPickItem, this._autoQuickPickItem + ]; + + const historyItemRefs = await this._historyProvider.provideHistoryItemRefs() ?? []; + const historyItemRefsByCategory = groupBy(historyItemRefs, (a, b) => compare(a.category ?? '', b.category ?? '')); + + for (const refs of historyItemRefsByCategory) { + if (refs.length === 0) { + continue; + } + + picks.push({ type: 'separator', label: refs[0].category }); + + picks.push(...refs.map(ref => { + return { + id: ref.id, + label: ref.name, + description: ref.description, + iconClass: ThemeIcon.isThemeIcon(ref.icon) ? + ThemeIcon.asClassName(ref.icon) : undefined, + historyItemRef: ref + }; + })); + } + + return picks; + } +} + export class SCMHistoryViewPane extends ViewPane { private _treeContainer!: HTMLElement; @@ -722,7 +1014,9 @@ export class SCMHistoryViewPane extends ViewPane { private _treeViewModel!: SCMHistoryViewModel; private _treeDataSource!: SCMHistoryTreeDataSource; private _treeIdentityProvider!: SCMHistoryTreeIdentityProvider; - private _repositoryLoadMore = observableValue(this, false); + + private readonly _repositoryLoadMore = observableValue(this, false); + private readonly _repositoryOutdated = observableValue(this, false); private readonly _actionRunner: IActionRunner; private readonly _visibilityDisposables = new DisposableStore(); @@ -736,9 +1030,8 @@ export class SCMHistoryViewPane extends ViewPane { constructor( options: IViewPaneOptions, @ICommandService private readonly _commandService: ICommandService, - @ISCMViewService private readonly _scmViewService: ISCMViewService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, @IProgressService private readonly _progressService: IProgressService, - @IQuickInputService private readonly _quickInputService: IQuickInputService, @IConfigurationService configurationService: IConfigurationService, @IContextMenuService contextMenuService: IContextMenuService, @IKeybindingService keybindingService: IKeybindingService, @@ -764,9 +1057,20 @@ export class SCMHistoryViewPane extends ViewPane { this._register(this._updateChildrenThrottler); } - protected override layoutBody(height: number, width: number): void { - super.layoutBody(height, width); - this._tree.layout(height, width); + protected override renderHeaderTitle(container: HTMLElement): void { + super.renderHeaderTitle(container, this.title); + + const element = h('div.scm-graph-view-badge-container', [ + h('div.scm-graph-view-badge.monaco-count-badge.long@badge') + ]); + + element.badge.textContent = 'Outdated'; + container.appendChild(element.root); + + this._register(autorun(reader => { + const outdated = this._repositoryOutdated.read(reader); + element.root.style.display = outdated ? '' : 'none'; + })); } protected override renderBody(container: HTMLElement): void { @@ -777,111 +1081,122 @@ export class SCMHistoryViewPane extends ViewPane { this._createTree(this._treeContainer); - this.onDidChangeBodyVisibility(visible => { - if (visible) { - this._treeViewModel = this.instantiationService.createInstance(SCMHistoryViewModel); - this._visibilityDisposables.add(this._treeViewModel); + this.onDidChangeBodyVisibility(async visible => { + if (!visible) { + this._visibilityDisposables.clear(); + return; + } - const firstRepository = derivedConstOnceDefined(this, reader => { + // Create view model + this._treeViewModel = this.instantiationService.createInstance(SCMHistoryViewModel); + this._visibilityDisposables.add(this._treeViewModel); + + // Initial rendering + await this._progressService.withProgress({ location: this.id }, async () => { + const firstRepositoryInitialized = derived(this, reader => { const repository = this._treeViewModel.repository.read(reader); const historyProvider = repository?.provider.historyProvider.read(reader); - const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup.read(reader); + const historyItemRef = historyProvider?.historyItemRef.read(reader); - return currentHistoryItemGroup !== undefined ? repository : undefined; + return historyItemRef !== undefined ? true : undefined; }); - this._visibilityDisposables.add(autorunWithStore(async (reader, store) => { - const repository = firstRepository.read(reader); - if (!repository) { - return; - } + // Wait for first repository to be initialized + await waitForState(firstRepositoryInitialized); - this._treeOperationSequencer.queue(async () => { - await this._tree.setInput(this._treeViewModel); - this._tree.scrollTop = 0; - }); + // Set tree input + await this._treeOperationSequencer.queue(async () => { + await this._tree.setInput(this._treeViewModel); + this._tree.scrollTop = 0; + }); + }); - // Repository change - store.add( - autorunWithStoreHandleChanges<{ refresh: boolean }>({ - owner: this, - createEmptyChangeSummary: () => ({ refresh: false }), - handleChange(_, changeSummary) { - changeSummary.refresh = true; - return true; - }, - }, (reader, changeSummary, store) => { - const repository = this._treeViewModel.repository.read(reader); - const historyProvider = repository?.provider.historyProvider.read(reader); - if (!repository || !historyProvider) { + // Repository change + let isFirstRun = true; + this._visibilityDisposables.add(autorunWithStore((reader, store) => { + const repository = this._treeViewModel.repository.read(reader); + const historyProvider = repository?.provider.historyProvider.read(reader); + if (!repository || !historyProvider) { + return; + } + + // Update context + this._scmProviderCtx.set(repository.provider.contextValue); + + // Publish + const historyItemRemoteRefIdSignal = signalFromObservable(this, derived(reader => { + return historyProvider.historyItemRemoteRef.read(reader)?.id; + })); + + // Fetch, Push + const historyItemRemoteRefRevision = derived(reader => { + return historyProvider.historyItemRemoteRef.read(reader)?.revision; + }); + + // HistoryItemRefs changed + store.add( + autorunWithStoreHandleChanges<{ refresh: boolean | 'ifScrollTop' }>({ + owner: this, + createEmptyChangeSummary: () => ({ refresh: false }), + handleChange(context, changeSummary) { + changeSummary.refresh = context.didChange(historyItemRemoteRefRevision) ? 'ifScrollTop' : true; + return true; + }, + }, (reader, changeSummary) => { + historyItemRemoteRefIdSignal.read(reader); + const historyItemRefValue = historyProvider.historyItemRef.read(reader); + const historyItemRemoteRefRevisionValue = historyItemRemoteRefRevision.read(reader); + + // Commit, Checkout, Publish, Pull + if (changeSummary.refresh === true) { + this.refresh(); + return; + } + + if (changeSummary.refresh === 'ifScrollTop') { + // If the history item remote revision has changed, but it matches the history + // item revision, then it means that a Push operation was performed and it is + // safe to refresh the graph. + if (historyItemRefValue?.revision === historyItemRemoteRefRevisionValue) { + this.refresh(); return; } - // Update context - this._scmProviderCtx.set(repository.provider.contextValue); - - // Checkout, Commit, and Publish - const historyItemGroup = derivedOpts<{ id: string; revision?: string; remoteId?: string } | undefined>({ - owner: this, - equalsFn: structuralEquals - }, reader => { - const currentHistoryItemGroup = historyProvider.currentHistoryItemGroup.read(reader); - return currentHistoryItemGroup ? { - id: currentHistoryItemGroup.id, - revision: currentHistoryItemGroup.revision, - remoteId: currentHistoryItemGroup.remote?.id - } : undefined; - }); - - // Fetch, Push - const historyItemRemoteRevision = derived(reader => { - return historyProvider.currentHistoryItemGroup.read(reader)?.remote?.revision; - }); - - // HistoryItemGroup change - store.add( - autorunWithStoreHandleChanges<{ refresh: boolean | 'ifScrollTop' }>({ - owner: this, - createEmptyChangeSummary: () => ({ refresh: false }), - handleChange(context, changeSummary) { - changeSummary.refresh = context.didChange(historyItemRemoteRevision) ? 'ifScrollTop' : true; - return true; - }, - }, (reader, changeSummary) => { - if ((!historyItemGroup.read(reader) && !historyItemRemoteRevision.read(reader)) || changeSummary.refresh === false) { - return; - } - - if (changeSummary.refresh === true) { - this.refresh(); - return; - } - - if (changeSummary.refresh === 'ifScrollTop') { - // Remote revision changes can occur as a result of a user action (Fetch, Push) but - // it can also occur as a result of background action (Auto Fetch). If the tree is - // scrolled to the top, we can safely refresh the tree. - if (this._tree.scrollTop === 0) { - this.refresh(); - return; - } - - // Set the "OUTDATED" description - this.updateTitleDescription(localize('outdated', "OUTDATED")); - } - })); - - if (changeSummary.refresh) { + // If the history item remote revision has changed, but it does not matches the + // history item revision, then a Fetch operation was performed. This can be the + // result of a user action (Fetch) or a background action (Auto Fetch). If the + // tree is scrolled to the top, we can safely refresh the tree. + if (this._tree.scrollTop === 0) { this.refresh(); + return; } - })); + + // Show the "Outdated" badge on the view + this._repositoryOutdated.set(true, undefined); + } + })); + + // HistoryItemRefs filter changed + store.add(runOnChange(this._treeViewModel.historyItemsFilter, () => { + this.refresh(); })); - } else { - this._visibilityDisposables.clear(); - } + + // We skip refreshing the graph on the first execution of the autorun + // since the graph for the first repository is rendered when the tree + // input is set. + if (!isFirstRun) { + this.refresh(); + } + isFirstRun = false; + })); }); } + protected override layoutBody(height: number, width: number): void { + super.layoutBody(height, width); + this._tree.layout(height, width); + } + override getActionRunner(): IActionRunner | undefined { return this._actionRunner; } @@ -891,11 +1206,17 @@ export class SCMHistoryViewPane extends ViewPane { } override getActionViewItem(action: IAction, options?: IDropdownMenuActionViewItemOptions): IActionViewItem | undefined { - if (action.id === 'workbench.scm.action.repository') { + if (action.id === PICK_REPOSITORY_ACTION_ID) { const repository = this._treeViewModel?.repository.get(); if (repository) { return new SCMRepositoryActionViewItem(repository, action, options); } + } else if (action.id === PICK_HISTORY_ITEM_REFS_ACTION_ID) { + const repository = this._treeViewModel?.repository.get(); + const historyItemsFilter = this._treeViewModel?.historyItemsFilter.get(); + if (repository && historyItemsFilter) { + return new SCMHistoryItemRefsActionViewItem(repository, historyItemsFilter, action, options); + } } return super.getActionViewItem(action, options); @@ -905,38 +1226,36 @@ export class SCMHistoryViewPane extends ViewPane { await this._updateChildren(true); this.updateActions(); - this.updateTitleDescription(undefined); + this._repositoryOutdated.set(false, undefined); this._tree.scrollTop = 0; } async pickRepository(): Promise { - const picks: (IQuickPickItem & { repository: 'auto' | ISCMRepository } | IQuickPickSeparator)[] = [ - { - label: localize('auto', "Auto"), - description: localize('activeRepository', "Show the source control graph for the active repository"), - repository: 'auto' - }, - { - type: 'separator' - }, - ]; - - picks.push(...this._scmViewService.repositories.map(r => ({ - label: r.provider.name, - description: r.provider.rootUri?.fsPath, - iconClass: ThemeIcon.asClassName(Codicon.repo), - repository: r - }))); - - const result = await this._quickInputService.pick(picks, { - placeHolder: localize('scmGraphRepository', "Select the repository to view, type to filter all repositories") - }); + const picker = this._instantiationService.createInstance(RepositoryPicker); + const result = await picker.pickRepository(); if (result) { this._treeViewModel.setRepository(result.repository); } } + async pickHistoryItemRef(): Promise { + const repository = this._treeViewModel.repository.get(); + const historyProvider = repository?.provider.historyProvider.get(); + const historyItemsFilter = this._treeViewModel.historyItemsFilter.get(); + + if (!historyProvider) { + return; + } + + const picker = this._instantiationService.createInstance(HistoryItemRefPicker, historyProvider, historyItemsFilter); + const result = await picker.pickHistoryItemRef(); + + if (result) { + this._treeViewModel.setHistoryItemsFilter(result); + } + } + private _createTree(container: HTMLElement): void { this._treeIdentityProvider = new SCMHistoryTreeIdentityProvider(); diff --git a/src/vs/workbench/contrib/scm/browser/scmViewService.ts b/src/vs/workbench/contrib/scm/browser/scmViewService.ts index 7952dfbce0a..38c0119b7de 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewService.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewService.ts @@ -18,7 +18,7 @@ import { binarySearch } from '../../../../base/common/arrays.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { IExtensionService } from '../../../services/extensions/common/extensions.js'; -import { derivedObservableWithCache, latestChangedValue, observableFromEventOpts } from '../../../../base/common/observableInternal/utils.js'; +import { derivedObservableWithCache, latestChangedValue, observableFromEventOpts } from '../../../../base/common/observable.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { EditorResourceAccessor } from '../../../common/editor.js'; diff --git a/src/vs/workbench/contrib/scm/browser/workingSet.ts b/src/vs/workbench/contrib/scm/browser/workingSet.ts index 7dfd2c00bd5..1185f8de20a 100644 --- a/src/vs/workbench/contrib/scm/browser/workingSet.ts +++ b/src/vs/workbench/contrib/scm/browser/workingSet.ts @@ -63,17 +63,17 @@ export class SCMWorkingSetController extends Disposable implements IWorkbenchCon private _onDidAddRepository(repository: ISCMRepository): void { const disposables = new DisposableStore(); - const currentHistoryItemGroupId = derived(reader => { + const historyItemRefId = derived(reader => { const historyProvider = repository.provider.historyProvider.read(reader); - const currentHistoryItemGroup = historyProvider?.currentHistoryItemGroup.read(reader); + const historyItemRef = historyProvider?.historyItemRef.read(reader); - return currentHistoryItemGroup?.id; + return historyItemRef?.id; }); disposables.add(autorun(async reader => { - const historyItemGroupId = currentHistoryItemGroupId.read(reader); + const historyItemRefIdValue = historyItemRefId.read(reader); - if (!historyItemGroupId) { + if (!historyItemRefIdValue) { return; } @@ -81,20 +81,20 @@ export class SCMWorkingSetController extends Disposable implements IWorkbenchCon const repositoryWorkingSets = this._workingSets.get(providerKey); if (!repositoryWorkingSets) { - this._workingSets.set(providerKey, { currentHistoryItemGroupId: historyItemGroupId, editorWorkingSets: new Map() }); + this._workingSets.set(providerKey, { currentHistoryItemGroupId: historyItemRefIdValue, editorWorkingSets: new Map() }); return; } // Editors for the current working set are automatically restored - if (repositoryWorkingSets.currentHistoryItemGroupId === historyItemGroupId) { + if (repositoryWorkingSets.currentHistoryItemGroupId === historyItemRefIdValue) { return; } // Save the working set - this._saveWorkingSet(providerKey, historyItemGroupId, repositoryWorkingSets); + this._saveWorkingSet(providerKey, historyItemRefIdValue, repositoryWorkingSets); // Restore the working set - await this._restoreWorkingSet(providerKey, historyItemGroupId); + await this._restoreWorkingSet(providerKey, historyItemRefIdValue); })); this._repositoryDisposables.set(repository, disposables); diff --git a/src/vs/workbench/contrib/scm/common/history.ts b/src/vs/workbench/contrib/scm/common/history.ts index f5d4de3fa52..813e43f97cf 100644 --- a/src/vs/workbench/contrib/scm/common/history.ts +++ b/src/vs/workbench/contrib/scm/common/history.ts @@ -15,26 +15,22 @@ export interface ISCMHistoryProviderMenus { } export interface ISCMHistoryProvider { - readonly currentHistoryItemGroup: IObservable; + readonly historyItemRef: IObservable; + readonly historyItemRemoteRef: IObservable; + readonly historyItemBaseRef: IObservable; + readonly historyItemRefChanges: IObservable; + + provideHistoryItemRefs(): Promise; provideHistoryItems(options: ISCMHistoryOptions): Promise; provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined): Promise; - resolveHistoryItemGroupCommonAncestor(historyItemGroupIds: string[]): Promise; + resolveHistoryItemRefsCommonAncestor(historyItemRefs: string[]): Promise; } export interface ISCMHistoryOptions { - readonly cursor?: string; readonly skip?: number; readonly limit?: number | { id?: string }; - readonly historyItemGroupIds?: readonly string[]; -} - -export interface ISCMHistoryItemGroup { - readonly id: string; - readonly name: string; - readonly revision?: string; - readonly base?: Omit, 'remote'>; - readonly remote?: Omit, 'remote'>; + readonly historyItemRefs?: readonly string[]; } export interface ISCMHistoryItemStatistics { @@ -43,10 +39,20 @@ export interface ISCMHistoryItemStatistics { readonly deletions: number; } -export interface ISCMHistoryItemLabel { - readonly title: string; - readonly icon?: URI | { light: URI; dark: URI } | ThemeIcon; +export interface ISCMHistoryItemRef { + readonly id: string; + readonly name: string; + readonly revision?: string; + readonly category?: string; + readonly description?: string; readonly color?: ColorIdentifier; + readonly icon?: URI | { light: URI; dark: URI } | ThemeIcon; +} + +export interface ISCMHistoryItemRefsChangeEvent { + readonly added: readonly ISCMHistoryItemRef[]; + readonly removed: readonly ISCMHistoryItemRef[]; + readonly modified: readonly ISCMHistoryItemRef[]; } export interface ISCMHistoryItem { @@ -58,7 +64,7 @@ export interface ISCMHistoryItem { readonly author?: string; readonly timestamp?: number; readonly statistics?: ISCMHistoryItemStatistics; - readonly labels?: ISCMHistoryItemLabel[]; + readonly references?: ISCMHistoryItemRef[]; } export interface ISCMHistoryItemGraphNode { @@ -68,6 +74,7 @@ export interface ISCMHistoryItemGraphNode { export interface ISCMHistoryItemViewModel { readonly historyItem: ISCMHistoryItem; + readonly isCurrent: boolean; readonly inputSwimlanes: ISCMHistoryItemGraphNode[]; readonly outputSwimlanes: ISCMHistoryItemGraphNode[]; } diff --git a/src/vs/workbench/contrib/scm/test/browser/scmHistory.test.ts b/src/vs/workbench/contrib/scm/test/browser/scmHistory.test.ts index 9da7394ecd7..d322704bd79 100644 --- a/src/vs/workbench/contrib/scm/test/browser/scmHistory.test.ts +++ b/src/vs/workbench/contrib/scm/test/browser/scmHistory.test.ts @@ -6,11 +6,11 @@ import * as assert from 'assert'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { ColorIdentifier } from '../../../../../platform/theme/common/colorUtils.js'; -import { colorRegistry, historyItemGroupBase, historyItemGroupLocal, historyItemGroupRemote, toISCMHistoryItemViewModelArray } from '../../browser/scmHistory.js'; -import { ISCMHistoryItem, ISCMHistoryItemLabel } from '../../common/history.js'; +import { colorRegistry, historyItemBaseRefColor, historyItemRefColor, historyItemRemoteRefColor, toISCMHistoryItemViewModelArray } from '../../browser/scmHistory.js'; +import { ISCMHistoryItem, ISCMHistoryItemRef } from '../../common/history.js'; -function toSCMHistoryItem(id: string, parentIds: string[], labels?: ISCMHistoryItemLabel[]): ISCMHistoryItem { - return { id, parentIds, subject: '', message: '', labels } satisfies ISCMHistoryItem; +function toSCMHistoryItem(id: string, parentIds: string[], references?: ISCMHistoryItemRef[]): ISCMHistoryItem { + return { id, parentIds, subject: '', message: '', references } satisfies ISCMHistoryItem; } suite('toISCMHistoryItemViewModelArray', () => { @@ -517,18 +517,18 @@ suite('toISCMHistoryItemViewModelArray', () => { */ test('graph with color map', () => { const models = [ - toSCMHistoryItem('a', ['b'], [{ title: 'topic' }]), + toSCMHistoryItem('a', ['b'], [{ id: 'topic', name: 'topic' }]), toSCMHistoryItem('b', ['c']), - toSCMHistoryItem('c', ['d'], [{ title: 'origin/topic' }]), + toSCMHistoryItem('c', ['d'], [{ id: 'origin/topic', name: 'origin/topic' }]), toSCMHistoryItem('d', ['e']), toSCMHistoryItem('e', ['f', 'g']), - toSCMHistoryItem('g', ['h'], [{ title: 'origin/main' }]) + toSCMHistoryItem('g', ['h'], [{ id: 'origin/main', name: 'origin/main' }]) ]; const colorMap = new Map([ - ['topic', historyItemGroupLocal], - ['origin/topic', historyItemGroupRemote], - ['origin/main', historyItemGroupBase], + ['topic', historyItemRefColor], + ['origin/topic', historyItemRemoteRefColor], + ['origin/main', historyItemBaseRefColor], ]); const viewModels = toISCMHistoryItemViewModelArray(models, colorMap); @@ -540,57 +540,57 @@ suite('toISCMHistoryItemViewModelArray', () => { assert.strictEqual(viewModels[0].outputSwimlanes.length, 1); assert.strictEqual(viewModels[0].outputSwimlanes[0].id, 'b'); - assert.strictEqual(viewModels[0].outputSwimlanes[0].color, historyItemGroupLocal); + assert.strictEqual(viewModels[0].outputSwimlanes[0].color, historyItemRefColor); // node b assert.strictEqual(viewModels[1].inputSwimlanes.length, 1); assert.strictEqual(viewModels[1].inputSwimlanes[0].id, 'b'); - assert.strictEqual(viewModels[1].inputSwimlanes[0].color, historyItemGroupLocal); + assert.strictEqual(viewModels[1].inputSwimlanes[0].color, historyItemRefColor); assert.strictEqual(viewModels[1].outputSwimlanes.length, 1); assert.strictEqual(viewModels[1].outputSwimlanes[0].id, 'c'); - assert.strictEqual(viewModels[1].outputSwimlanes[0].color, historyItemGroupLocal); + assert.strictEqual(viewModels[1].outputSwimlanes[0].color, historyItemRefColor); // node c assert.strictEqual(viewModels[2].inputSwimlanes.length, 1); assert.strictEqual(viewModels[2].inputSwimlanes[0].id, 'c'); - assert.strictEqual(viewModels[2].inputSwimlanes[0].color, historyItemGroupLocal); + assert.strictEqual(viewModels[2].inputSwimlanes[0].color, historyItemRefColor); assert.strictEqual(viewModels[2].outputSwimlanes.length, 1); assert.strictEqual(viewModels[2].outputSwimlanes[0].id, 'd'); - assert.strictEqual(viewModels[2].outputSwimlanes[0].color, historyItemGroupRemote); + assert.strictEqual(viewModels[2].outputSwimlanes[0].color, historyItemRemoteRefColor); // node d assert.strictEqual(viewModels[3].inputSwimlanes.length, 1); assert.strictEqual(viewModels[3].inputSwimlanes[0].id, 'd'); - assert.strictEqual(viewModels[3].inputSwimlanes[0].color, historyItemGroupRemote); + assert.strictEqual(viewModels[3].inputSwimlanes[0].color, historyItemRemoteRefColor); assert.strictEqual(viewModels[3].outputSwimlanes.length, 1); assert.strictEqual(viewModels[3].outputSwimlanes[0].id, 'e'); - assert.strictEqual(viewModels[3].outputSwimlanes[0].color, historyItemGroupRemote); + assert.strictEqual(viewModels[3].outputSwimlanes[0].color, historyItemRemoteRefColor); // node e assert.strictEqual(viewModels[4].inputSwimlanes.length, 1); assert.strictEqual(viewModels[4].inputSwimlanes[0].id, 'e'); - assert.strictEqual(viewModels[4].inputSwimlanes[0].color, historyItemGroupRemote); + assert.strictEqual(viewModels[4].inputSwimlanes[0].color, historyItemRemoteRefColor); assert.strictEqual(viewModels[4].outputSwimlanes.length, 2); assert.strictEqual(viewModels[4].outputSwimlanes[0].id, 'f'); - assert.strictEqual(viewModels[4].outputSwimlanes[0].color, historyItemGroupRemote); + assert.strictEqual(viewModels[4].outputSwimlanes[0].color, historyItemRemoteRefColor); assert.strictEqual(viewModels[4].outputSwimlanes[1].id, 'g'); - assert.strictEqual(viewModels[4].outputSwimlanes[1].color, historyItemGroupBase); + assert.strictEqual(viewModels[4].outputSwimlanes[1].color, historyItemBaseRefColor); // node g assert.strictEqual(viewModels[5].inputSwimlanes.length, 2); assert.strictEqual(viewModels[5].inputSwimlanes[0].id, 'f'); - assert.strictEqual(viewModels[5].inputSwimlanes[0].color, historyItemGroupRemote); + assert.strictEqual(viewModels[5].inputSwimlanes[0].color, historyItemRemoteRefColor); assert.strictEqual(viewModels[5].inputSwimlanes[1].id, 'g'); - assert.strictEqual(viewModels[5].inputSwimlanes[1].color, historyItemGroupBase); + assert.strictEqual(viewModels[5].inputSwimlanes[1].color, historyItemBaseRefColor); assert.strictEqual(viewModels[5].outputSwimlanes.length, 2); assert.strictEqual(viewModels[5].outputSwimlanes[0].id, 'f'); - assert.strictEqual(viewModels[5].outputSwimlanes[0].color, historyItemGroupRemote); + assert.strictEqual(viewModels[5].outputSwimlanes[0].color, historyItemRemoteRefColor); assert.strictEqual(viewModels[5].outputSwimlanes[1].id, 'h'); - assert.strictEqual(viewModels[5].outputSwimlanes[1].color, historyItemGroupBase); + assert.strictEqual(viewModels[5].outputSwimlanes[1].color, historyItemBaseRefColor); }); }); diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts index a88fdd95cfb..0fd92493bc3 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatController.ts @@ -204,7 +204,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr this._chatWidget?.value.setValue(undefined); } - async acceptInput(): Promise { + async acceptInput(isVoiceInput?: boolean): Promise { assertType(this._chatWidget); if (!this._model.value) { await this.reveal(); @@ -222,7 +222,7 @@ export class TerminalChatController extends Disposable implements ITerminalContr const store = new DisposableStore(); this._requestActiveContextKey.set(true); let responseContent = ''; - const response = await this._chatWidget.value.inlineChatWidget.chatWidget.acceptInput(lastInput); + const response = await this._chatWidget.value.inlineChatWidget.chatWidget.acceptInput(lastInput, isVoiceInput); this._currentRequestId = response?.requestId; const responsePromise = new DeferredPromise(); try { diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts index 4dea30b5e12..2897cc88612 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalSuggestAddon.ts @@ -348,6 +348,11 @@ export class SuggestAddon extends Disposable implements ITerminalAddon, ISuggest return; } + // Only show the suggest widget if the terminal is focused + if (!dom.isAncestorOfActiveElement(terminal.element)) { + return; + } + let replacementIndex = 0; let replacementLength = this._promptInputModel.cursorIndex; diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.integrationTest.ts b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.integrationTest.ts index 01cc24341c9..119993e65cd 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.integrationTest.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/test/browser/terminalSuggestAddon.integrationTest.ts @@ -114,6 +114,8 @@ suite('Terminal Contrib Suggest Recordings', () => { xterm.loadAddon(shellIntegrationAddon); xterm.loadAddon(suggestAddon); + + xterm.focus(); }); for (const testCase of recordedTestCases) { diff --git a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts index c5de8a8adaa..5557570e892 100644 --- a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts +++ b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts @@ -38,8 +38,7 @@ import { widgetClose } from '../../../../platform/theme/common/iconRegistry.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { ViewAction } from '../../../browser/parts/views/viewPane.js'; import { FocusedViewContext } from '../../../common/contextkeys.js'; -import { ViewContainerLocation } from '../../../common/views.js'; -import { VIEWLET_ID as EXTENSIONS_VIEWLET_ID, IExtensionsViewPaneContainer } from '../../extensions/common/extensions.js'; +import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; import { TestExplorerTreeElement, TestItemTreeElement } from './explorerProjections/index.js'; import * as icons from './icons.js'; import { TestingExplorerView } from './testingExplorerView.js'; @@ -58,7 +57,6 @@ import { ITestingContinuousRunService } from '../common/testingContinuousRunServ import { ITestingPeekOpener } from '../common/testingPeekOpener.js'; import { isFailedState } from '../common/testingStates.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; -import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; const category = Categories.Test; @@ -1556,10 +1554,7 @@ export class SearchForTestExtension extends Action2 { } public async run(accessor: ServicesAccessor) { - const paneCompositeService = accessor.get(IPaneCompositePartService); - const viewlet = (await paneCompositeService.openPaneComposite(EXTENSIONS_VIEWLET_ID, ViewContainerLocation.Sidebar, true))?.getViewPaneContainer() as IExtensionsViewPaneContainer; - viewlet.search('@category:"testing"'); - viewlet.focus(); + accessor.get(IExtensionsWorkbenchService).openSearch('@category:"testing"'); } } diff --git a/src/vs/workbench/contrib/testing/browser/testing.contribution.ts b/src/vs/workbench/contrib/testing/browser/testing.contribution.ts index c7b89e49751..31b4073e7b4 100644 --- a/src/vs/workbench/contrib/testing/browser/testing.contribution.ts +++ b/src/vs/workbench/contrib/testing/browser/testing.contribution.ts @@ -235,5 +235,15 @@ CommandsRegistry.registerCommand({ } }); +CommandsRegistry.registerCommand({ + id: 'vscode.testing.getControllersWithTests', + handler: async (accessor: ServicesAccessor) => { + const testService = accessor.get(ITestService); + return [...testService.collection.rootItems] + .filter(r => r.children.size > 0) + .map(r => r.controllerId); + } +}); + Registry.as(ConfigurationExtensions.Configuration).registerConfiguration(testingConfiguration); diff --git a/src/vs/workbench/contrib/themes/browser/themes.contribution.ts b/src/vs/workbench/contrib/themes/browser/themes.contribution.ts index b7a983ccd40..93599087f54 100644 --- a/src/vs/workbench/contrib/themes/browser/themes.contribution.ts +++ b/src/vs/workbench/contrib/themes/browser/themes.contribution.ts @@ -10,7 +10,7 @@ import { equalsIgnoreCase } from '../../../../base/common/strings.js'; import { Registry } from '../../../../platform/registry/common/platform.js'; import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; import { IWorkbenchThemeService, IWorkbenchTheme, ThemeSettingTarget, IWorkbenchColorTheme, IWorkbenchFileIconTheme, IWorkbenchProductIconTheme, ThemeSettings, ThemeSettingDefaults } from '../../../services/themes/common/workbenchThemeService.js'; -import { VIEWLET_ID, IExtensionsViewPaneContainer } from '../../extensions/common/extensions.js'; +import { IExtensionsWorkbenchService } from '../../extensions/common/extensions.js'; import { IExtensionGalleryService, IExtensionManagementService, IGalleryExtension } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { IColorRegistry, Extensions as ColorRegistryExtensions } from '../../../../platform/theme/common/colorRegistry.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; @@ -20,8 +20,6 @@ import { colorThemeSchemaId } from '../../../services/themes/common/colorThemeSc import { isCancellationError, onUnexpectedError } from '../../../../base/common/errors.js'; import { IQuickInputButton, IQuickInputService, IQuickInputToggle, IQuickPick, IQuickPickItem, QuickPickInput } from '../../../../platform/quickinput/common/quickInput.js'; import { DEFAULT_PRODUCT_ICON_THEME_ID, ProductIconThemeData } from '../../../services/themes/browser/productIconThemeData.js'; -import { IPaneCompositePartService } from '../../../services/panecomposite/browser/panecomposite.js'; -import { ViewContainerLocation } from '../../../common/views.js'; import { ThrottledDelayer } from '../../../../base/common/async.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { ILogService } from '../../../../platform/log/common/log.js'; @@ -81,7 +79,7 @@ class MarketplaceThemesPicker { @IQuickInputService private readonly quickInputService: IQuickInputService, @ILogService private readonly logService: ILogService, @IProgressService private readonly progressService: IProgressService, - @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IDialogService private readonly dialogService: IDialogService ) { this._installedExtensions = extensionManagementService.getInstalled().then(installed => { @@ -197,9 +195,9 @@ class MarketplaceThemesPicker { if (isItem(e.item)) { const extensionId = e.item.theme?.extensionData?.extensionId; if (extensionId) { - openExtensionViewlet(this.paneCompositeService, `@id:${extensionId}`); + this.extensionsWorkbenchService.openSearch(`@id:${extensionId}`); } else { - openExtensionViewlet(this.paneCompositeService, `${this.marketplaceQuery} ${quickpick.value}`); + this.extensionsWorkbenchService.openSearch(`${this.marketplaceQuery} ${quickpick.value}`); } } })); @@ -248,7 +246,7 @@ class MarketplaceThemesPicker { } private async installExtension(galleryExtension: IGalleryExtension) { - openExtensionViewlet(this.paneCompositeService, `@id:${galleryExtension.identifier.id}`); + this.extensionsWorkbenchService.openSearch(`@id:${galleryExtension.identifier.id}`); const result = await this.dialogService.confirm({ message: localize('installExtension.confirm', "This will install extension '{0}' published by '{1}'. Do you want to continue?", galleryExtension.displayName, galleryExtension.publisherDisplayName), primaryButton: localize('installExtension.button.ok', "OK") @@ -303,7 +301,7 @@ class InstalledThemesPicker { private readonly getMarketplaceColorThemes: (publisher: string, name: string, version: string) => Promise, @IQuickInputService private readonly quickInputService: IQuickInputService, @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, - @IPaneCompositePartService private readonly paneCompositeService: IPaneCompositePartService, + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IExtensionResourceLoaderService private readonly extensionResourceLoaderService: IExtensionResourceLoaderService, @IInstantiationService private readonly instantiationService: IInstantiationService ) { @@ -361,7 +359,7 @@ class InstalledThemesPicker { const theme = quickpick.selectedItems[0]; if (!theme || theme.configureItem) { // 'pick in marketplace' entry if (!theme || theme.configureItem === ConfigureItem.EXTENSIONS_VIEW) { - openExtensionViewlet(this.paneCompositeService, `${this.options.marketplaceTag} ${quickpick.value}`); + this.extensionsWorkbenchService.openSearch(`${this.options.marketplaceTag} ${quickpick.value}`); } else if (theme.configureItem === ConfigureItem.BROWSE_GALLERY) { if (marketplaceThemePicker) { const res = await marketplaceThemePicker.openQuickPick(quickpick.value, currentTheme, selectTheme); @@ -389,9 +387,9 @@ class InstalledThemesPicker { if (isItem(e.item)) { const extensionId = e.item.theme?.extensionData?.extensionId; if (extensionId) { - openExtensionViewlet(this.paneCompositeService, `@id:${extensionId}`); + this.extensionsWorkbenchService.openSearch(`@id:${extensionId}`); } else { - openExtensionViewlet(this.paneCompositeService, `${this.options.marketplaceTag} ${quickpick.value}`); + this.extensionsWorkbenchService.openSearch(`${this.options.marketplaceTag} ${quickpick.value}`); } } })); @@ -606,14 +604,6 @@ function configurationEntry(label: string, configureItem: ConfigureItem): QuickP }; } -function openExtensionViewlet(paneCompositeService: IPaneCompositePartService, query: string) { - return paneCompositeService.openPaneComposite(VIEWLET_ID, ViewContainerLocation.Sidebar, true).then(viewlet => { - if (viewlet) { - (viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer).search(query); - viewlet.focus(); - } - }); -} interface ThemeItem extends IQuickPickItem { readonly id: string | undefined; readonly theme?: IWorkbenchTheme; diff --git a/src/vs/workbench/contrib/themes/test/node/colorRegistry.releaseTest.ts b/src/vs/workbench/contrib/themes/test/node/colorRegistry.releaseTest.ts index 91100d7138f..50a4b4b6837 100644 --- a/src/vs/workbench/contrib/themes/test/node/colorRegistry.releaseTest.ts +++ b/src/vs/workbench/contrib/themes/test/node/colorRegistry.releaseTest.ts @@ -15,7 +15,7 @@ import { RequestService } from '../../../../../platform/request/node/requestServ import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js'; // eslint-disable-next-line local/code-import-patterns import '../../../../workbench.desktop.main.js'; -import { NullLogger, NullLogService } from '../../../../../platform/log/common/log.js'; +import { NullLogService } from '../../../../../platform/log/common/log.js'; import { mock } from '../../../../../base/test/common/mock.js'; import { INativeEnvironmentService } from '../../../../../platform/environment/common/environment.js'; import { FileAccess } from '../../../../../base/common/network.js'; @@ -91,7 +91,7 @@ suite('Color Registry', function () { const docUrl = 'https://raw.githubusercontent.com/microsoft/vscode-docs/main/api/references/theme-color.md'; - const reqContext = await new RequestService(new NullLogger(), new TestConfigurationService(), environmentService, new NullLogService()).request({ url: docUrl }, CancellationToken.None); + const reqContext = await new RequestService(new TestConfigurationService(), environmentService, new NullLogService()).request({ url: docUrl }, CancellationToken.None); const content = (await asTextOrError(reqContext))!; const expression = /-\s*\`([\w\.]+)\`: (.*)/g; diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts b/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts index 485e749acf3..ddbe5e662c6 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/common/gettingStartedContent.ts @@ -184,7 +184,6 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ 'onSettingChanged:workbench.colorTheme', 'onCommand:workbench.action.selectTheme' ], - when: '!accessibilityModeEnabled', media: { type: 'markdown', path: 'theme_picker', } }, { @@ -352,9 +351,9 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ } }, { - id: 'SetupScreenReader', - title: localize('gettingStarted.setupScreenReader.title', "Get Started with VS Code using a Screen Reader"), - description: localize('gettingStarted.setupScreenReader.description', "Learn the tools and shortcuts that make VS Code accessible. Note that some actions are not actionable from within the context of the walkthrough."), + id: 'SetupAccessibility', + title: localize('gettingStarted.setupAccessibility.title', "Get Started with Accessibility Features"), + description: localize('gettingStarted.setupAccessibility.description', "Learn the tools and shortcuts that make VS Code accessible. Note that some actions are not actionable from within the context of the walkthrough."), isFeatured: true, icon: setupIcon, when: CONTEXT_ACCESSIBILITY_MODE_ENABLED.key, @@ -372,7 +371,7 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ }, { id: 'accessibleView', - title: localize('gettingStarted.accessibleView.title', "Use the accessible view to inspect content line by line, character by character"), + title: localize('gettingStarted.accessibleView.title', "Screen reader users can inspect content line by line, character by character in the accessible view."), description: localize('gettingStarted.accessibleView.description.interpolated', "The accessible view is available for the terminal, hovers, notifications, comments, notebook output, chat responses, inline completions, and debug console output.\n With focus in any of those features, it can be opened with the Open Accessible View command.\n{0}", Button(localize('openAccessibleView', "Open Accessible View"), 'command:editor.action.accessibleView')), media: { type: 'markdown', path: 'empty' @@ -381,7 +380,7 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ { id: 'verbositySettings', title: localize('gettingStarted.verbositySettings.title', "Control the verbosity of aria labels"), - description: localize('gettingStarted.verbositySettings.description.interpolated', "Verbosity settings exist for features around the workbench so that once a user is familiar with a feature, they can avoid hearing hints about how to operate it. For example, features for which an accessibility help dialog exists will indicate how to open the dialog until the verbosity setting for that feature has been disabled.\n These and other accessibility settings can be configured by running the Open Accessibility Settings command.\n{0}", Button(localize('openVerbositySettings', "Open Accessibility Settings"), 'command:workbench.action.openAccessibilitySettings')), + description: localize('gettingStarted.verbositySettings.description.interpolated', "Screen reader verbosity settings exist for features around the workbench so that once a user is familiar with a feature, they can avoid hearing hints about how to operate it. For example, features for which an accessibility help dialog exists will indicate how to open the dialog until the verbosity setting for that feature has been disabled.\n These and other accessibility settings can be configured by running the Open Accessibility Settings command.\n{0}", Button(localize('openVerbositySettings', "Open Accessibility Settings"), 'command:workbench.action.openAccessibilitySettings')), media: { type: 'markdown', path: 'empty' } @@ -425,6 +424,12 @@ export const walkthroughs: GettingStartedWalkthroughContent = [ media: { type: 'markdown', path: 'empty' } + }, + { + id: 'accessibilitySettings', + title: localize('gettingStarted.accessibilitySettings.title', "Configure accessibility settings"), + description: localize('gettingStarted.accessibilitySettings.description.interpolated', "Accessibility settings can be configured by running the Open Accessibility Settings command.\n{0}", Button(localize('openAccessibilitySettings', "Open Accessibility Settings"), 'command:workbench.action.openAccessibilitySettings')), + media: { type: 'markdown', path: 'empty' } } ] } diff --git a/src/vs/workbench/contrib/welcomeWalkthrough/browser/editor/vs_code_editor_walkthrough.ts b/src/vs/workbench/contrib/welcomeWalkthrough/browser/editor/vs_code_editor_walkthrough.ts index 90709c19de3..bdd30bf2171 100644 --- a/src/vs/workbench/contrib/welcomeWalkthrough/browser/editor/vs_code_editor_walkthrough.ts +++ b/src/vs/workbench/contrib/welcomeWalkthrough/browser/editor/vs_code_editor_walkthrough.ts @@ -11,7 +11,7 @@ export default function content(accessor: ServicesAccessor) { const isServerless = platform.isWeb && !accessor.get(IWorkbenchEnvironmentService).remoteAuthority; return ` ## Interactive Editor Playground -The core editor in VS Code is packed with features. This page highlights a number of them and lets you interactively try them out through the use of a number of embedded editors. For full details on the editor features for VS Code and more head over to our [documentation](command:workbench.action.openDocumentationUrl). +The core editor in VS Code is packed with features. This page highlights a number of them and lets you interactively try them out through the use of a number of embedded editors. For full details on the editor features for VS Code and more head over to our [documentation](https://code.visualstudio.com/docs). * [Multi-cursor Editing](#multi-cursor-editing) - block selection, select all occurrences, add additional cursors and more. * [IntelliSense](#intellisense) - get code assistance and parameter suggestions for your code and external modules. diff --git a/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughInput.ts b/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughInput.ts index 02562eadc8b..2e09781b796 100644 --- a/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughInput.ts +++ b/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughInput.ts @@ -3,18 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Dimension } from '../../../../base/browser/dom.js'; +import { DisposableStore, IReference } from '../../../../base/common/lifecycle.js'; +import * as marked from '../../../../base/common/marked/marked.js'; +import { Schemas } from '../../../../base/common/network.js'; +import { isEqual } from '../../../../base/common/resources.js'; +import { URI } from '../../../../base/common/uri.js'; +import { ITextEditorModel, ITextModelService } from '../../../../editor/common/services/resolverService.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { EditorInputCapabilities, IUntypedEditorInput } from '../../../common/editor.js'; import { EditorInput } from '../../../common/editor/editorInput.js'; import { EditorModel } from '../../../common/editor/editorModel.js'; -import { URI } from '../../../../base/common/uri.js'; -import { DisposableStore, IReference } from '../../../../base/common/lifecycle.js'; -import { ITextEditorModel, ITextModelService } from '../../../../editor/common/services/resolverService.js'; -import { marked, Tokens } from '../../../../base/common/marked/marked.js'; -import { isEqual } from '../../../../base/common/resources.js'; +import { markedGfmHeadingIdPlugin } from '../../markdown/browser/markedGfmHeadingIdPlugin.js'; import { moduleToContent } from '../common/walkThroughContentProvider.js'; -import { Dimension } from '../../../../base/browser/dom.js'; -import { EditorInputCapabilities, IUntypedEditorInput } from '../../../common/editor.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { Schemas } from '../../../../base/common/network.js'; class WalkThroughModel extends EditorModel { @@ -115,15 +116,16 @@ export class WalkThroughInput extends EditorInput { const snippets: Promise>[] = []; let i = 0; - const renderer = new marked.Renderer(); - renderer.code = ({ lang }: Tokens.Code) => { + const renderer = new marked.marked.Renderer(); + renderer.code = ({ lang }: marked.Tokens.Code) => { i++; const resource = this.options.resource.with({ scheme: Schemas.walkThroughSnippet, fragment: `${i}.${lang}` }); snippets.push(this.textModelResolverService.createModelReference(resource)); return `
`; }; - content = marked(content, { async: false, renderer }); + const m = new marked.Marked({ renderer }, markedGfmHeadingIdPlugin()); + content = m.parse(content, { async: false }); return Promise.all(snippets) .then(refs => new WalkThroughModel(content, refs)); }); diff --git a/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart.ts b/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart.ts index a65a480f5d0..27d04d09f01 100644 --- a/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart.ts +++ b/src/vs/workbench/contrib/welcomeWalkthrough/browser/walkThroughPart.ts @@ -287,7 +287,7 @@ export class WalkThroughPart extends EditorPane { const content = model.main; if (!input.resource.path.endsWith('.md')) { - safeInnerHtml(this.content, content); + safeInnerHtml(this.content, content, { ALLOW_UNKNOWN_PROTOCOLS: true }); this.updateSizeClasses(); this.decorateContent(); @@ -302,7 +302,7 @@ export class WalkThroughPart extends EditorPane { const innerContent = document.createElement('div'); innerContent.classList.add('walkThroughContent'); // only for markdown files const markdown = this.expandMacros(content); - safeInnerHtml(innerContent, markdown); + safeInnerHtml(innerContent, markdown, { ALLOW_UNKNOWN_PROTOCOLS: true }); this.content.appendChild(innerContent); model.snippets.forEach((snippet, i) => { diff --git a/src/vs/workbench/services/activity/common/activity.ts b/src/vs/workbench/services/activity/common/activity.ts index 7599f94db2f..be1239fc725 100644 --- a/src/vs/workbench/services/activity/common/activity.ts +++ b/src/vs/workbench/services/activity/common/activity.ts @@ -8,6 +8,11 @@ import { createDecorator } from '../../../../platform/instantiation/common/insta import { ThemeIcon } from '../../../../base/common/themables.js'; import { Event } from '../../../../base/common/event.js'; import { ViewContainer } from '../../../common/views.js'; +import { IColorTheme } from '../../../../platform/theme/common/themeService.js'; +import { Color } from '../../../../base/common/color.js'; +import { registerColor } from '../../../../platform/theme/common/colorUtils.js'; +import { localize } from '../../../../nls.js'; +import { Codicon } from '../../../../base/common/codicons.js'; export interface IActivity { readonly badge: IBadge; @@ -58,23 +63,36 @@ export interface IActivityService { export interface IBadge { getDescription(): string; + getColors(theme: IColorTheme): IBadgeStyles | undefined; +} + +export interface IBadgeStyles { + readonly badgeBackground: Color | undefined; + readonly badgeForeground: Color | undefined; + readonly badgeBorder: Color | undefined; } class BaseBadge implements IBadge { - constructor(readonly descriptorFn: (arg: any) => string) { - this.descriptorFn = descriptorFn; + constructor( + protected readonly descriptorFn: (arg: any) => string, + private readonly stylesFn: ((theme: IColorTheme) => IBadgeStyles | undefined) | undefined, + ) { } getDescription(): string { return this.descriptorFn(null); } + + getColors(theme: IColorTheme): IBadgeStyles | undefined { + return this.stylesFn?.(theme); + } } export class NumberBadge extends BaseBadge { constructor(readonly number: number, descriptorFn: (num: number) => string) { - super(descriptorFn); + super(descriptorFn, undefined); this.number = number; } @@ -85,9 +103,53 @@ export class NumberBadge extends BaseBadge { } export class IconBadge extends BaseBadge { - constructor(readonly icon: ThemeIcon, descriptorFn: () => string) { - super(descriptorFn); + constructor( + readonly icon: ThemeIcon, + descriptorFn: () => string, + stylesFn?: (theme: IColorTheme) => IBadgeStyles | undefined, + ) { + super(descriptorFn, stylesFn); } } -export class ProgressBadge extends BaseBadge { } +export class ProgressBadge extends BaseBadge { + constructor(descriptorFn: () => string) { + super(descriptorFn, undefined); + } +} + +export class WarningBadge extends IconBadge { + constructor(descriptorFn: () => string) { + super(Codicon.warning, descriptorFn, (theme: IColorTheme) => ({ + badgeBackground: theme.getColor(activityWarningBadgeBackground), + badgeForeground: theme.getColor(activityWarningBadgeForeground), + badgeBorder: undefined, + })); + } +} + +export class ErrorBadge extends IconBadge { + constructor(descriptorFn: () => string) { + super(Codicon.error, descriptorFn, (theme: IColorTheme) => ({ + badgeBackground: theme.getColor(activityErrorBadgeBackground), + badgeForeground: theme.getColor(activityErrorBadgeForeground), + badgeBorder: undefined, + })); + } +} + +const activityWarningBadgeForeground = registerColor('activityWarningBadge.foreground', + { dark: Color.black.lighten(0.2), light: Color.white, hcDark: null, hcLight: Color.black.lighten(0.2) }, + localize('activityWarningBadge.foreground', 'Foreground color of the warning activity badge')); + +const activityWarningBadgeBackground = registerColor('activityWarningBadge.background', + { dark: '#CCA700', light: '#BF8803', hcDark: null, hcLight: '#CCA700' }, + localize('activityWarningBadge.background', 'Background color of the warning activity badge')); + +const activityErrorBadgeForeground = registerColor('activityErrorBadge.foreground', + { dark: Color.black.lighten(0.2), light: Color.white, hcDark: null, hcLight: Color.black.lighten(0.2) }, + localize('activityErrorBadge.foreground', 'Foreground color of the error activity badge')); + +const activityErrorBadgeBackground = registerColor('activityErrorBadge.background', + { dark: '#F14C4C', light: '#E51400', hcDark: null, hcLight: '#F14C4C' }, + localize('activityErrorBadge.background', 'Background color of the error activity badge')); diff --git a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts index 99aefca9884..efdf60b8565 100644 --- a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts @@ -42,6 +42,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench protected readonly extensionsManager: ExtensionsManager; private readonly storageManager: StorageManager; + private extensionsDisabledByExtensionDependency: IExtension[] = []; constructor( @IStorageService storageService: IStorageService, @@ -71,6 +72,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench this.extensionsManager = this._register(instantiationService.createInstance(ExtensionsManager)); this.extensionsManager.whenInitialized().then(() => { if (!isDisposed) { + this._onDidChangeExtensions([], [], false); this._register(this.extensionsManager.onDidChangeExtensions(({ added, removed, isProfileSwitch }) => this._onDidChangeExtensions(added, removed, isProfileSwitch))); uninstallDisposable.dispose(); } @@ -161,6 +163,8 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench throw new Error(localize('cannot change enablement virtual workspace', "Cannot change enablement of {0} extension because it does not support virtual workspaces", extension.manifest.displayName || extension.identifier.id)); case EnablementState.DisabledByExtensionKind: throw new Error(localize('cannot change enablement extension kind', "Cannot change enablement of {0} extension because of its extension kind", extension.manifest.displayName || extension.identifier.id)); + case EnablementState.DisabledByInvalidExtension: + throw new Error(localize('cannot change invalid extension enablement', "Cannot change enablement of {0} extension because of it is invalid", extension.manifest.displayName || extension.identifier.id)); case EnablementState.DisabledByExtensionDependency: if (donotCheckDependencies) { break; @@ -334,8 +338,13 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench } enablementState = this._getUserEnablementState(extension.identifier); + const isEnabled = this.isEnabledEnablementState(enablementState); - if (this.extensionBisectService.isDisabledByBisect(extension)) { + if (isEnabled && !extension.isValid) { + enablementState = EnablementState.DisabledByInvalidExtension; + } + + else if (this.extensionBisectService.isDisabledByBisect(extension)) { enablementState = EnablementState.DisabledByEnvironment; } @@ -347,7 +356,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench enablementState = EnablementState.DisabledByVirtualWorkspace; } - else if (this.isEnabledEnablementState(enablementState) && this._isDisabledByWorkspaceTrust(extension, workspaceType)) { + else if (isEnabled && this._isDisabledByWorkspaceTrust(extension, workspaceType)) { enablementState = EnablementState.DisabledByTrustRequirement; } @@ -355,11 +364,11 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench enablementState = EnablementState.DisabledByExtensionKind; } - else if (this.isEnabledEnablementState(enablementState) && this._isDisabledByExtensionDependency(extension, extensions, workspaceType, computedEnablementStates)) { + else if (isEnabled && this._isDisabledByExtensionDependency(extension, extensions, workspaceType, computedEnablementStates)) { enablementState = EnablementState.DisabledByExtensionDependency; } - else if (!this.isEnabledEnablementState(enablementState) && this._isEnabledInEnv(extension)) { + else if (!isEnabled && this._isEnabledInEnv(extension)) { enablementState = EnablementState.EnabledByEnvironment; } @@ -626,9 +635,21 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench } private _onDidChangeExtensions(added: ReadonlyArray, removed: ReadonlyArray, isProfileSwitch: boolean): void { - const disabledExtensions = added.filter(e => !this.isEnabledEnablementState(this.getEnablementState(e))); - if (disabledExtensions.length) { - this._onEnablementChanged.fire(disabledExtensions); + const changedExtensions: IExtension[] = added.filter(e => !this.isEnabledEnablementState(this.getEnablementState(e))); + const existingExtensionsDisabledByExtensionDependency = this.extensionsDisabledByExtensionDependency; + this.extensionsDisabledByExtensionDependency = this.extensionsManager.extensions.filter(extension => this.getEnablementState(extension) === EnablementState.DisabledByExtensionDependency); + for (const extension of existingExtensionsDisabledByExtensionDependency) { + if (this.extensionsDisabledByExtensionDependency.every(e => !areSameExtensions(e.identifier, extension.identifier))) { + changedExtensions.push(extension); + } + } + for (const extension of this.extensionsDisabledByExtensionDependency) { + if (existingExtensionsDisabledByExtensionDependency.every(e => !areSameExtensions(e.identifier, extension.identifier))) { + changedExtensions.push(extension); + } + } + if (changedExtensions.length) { + this._onEnablementChanged.fire(changedExtensions); } if (!isProfileSwitch) { removed.forEach(({ identifier }) => this._reset(identifier)); @@ -714,6 +735,13 @@ class ExtensionsManager extends Disposable { private updateExtensions(added: IExtension[], identifiers: IExtensionIdentifier[], server: IExtensionManagementServer | undefined, isProfileSwitch: boolean): void { if (added.length) { + for (const extension of added) { + const extensionServer = this.extensionManagementServerService.getExtensionManagementServer(extension); + const index = this._extensions.findIndex(e => areSameExtensions(e.identifier, extension.identifier) && this.extensionManagementServerService.getExtensionManagementServer(e) === extensionServer); + if (index !== -1) { + this._extensions.splice(index, 1); + } + } this._extensions.push(...added); } const removed: IExtension[] = []; diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts index 07d4b25fbfc..32c82faa57d 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts @@ -94,6 +94,7 @@ export const enum EnablementState { DisabledByEnvironment, EnabledByEnvironment, DisabledByVirtualWorkspace, + DisabledByInvalidExtension, DisabledByExtensionDependency, DisabledGlobally, DisabledWorkspace, diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts index dea6f7238a2..1cd69fbf9f0 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts @@ -25,6 +25,7 @@ import { IUserDataProfileService } from '../../userDataProfile/common/userDataPr import { IRemoteUserDataProfilesService } from '../../userDataProfile/common/remoteUserDataProfiles.js'; import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uriIdentity.js'; import { areApiProposalsCompatible } from '../../../../platform/extensions/common/extensionValidator.js'; +import { isBoolean, isUndefined } from '../../../../base/common/types.js'; export class NativeRemoteExtensionManagementService extends RemoteExtensionManagementService { @@ -51,15 +52,19 @@ export class NativeRemoteExtensionManagementService extends RemoteExtensionManag return local; } - override async installFromGallery(extension: IGalleryExtension, installOptions?: InstallOptions): Promise { + override async installFromGallery(extension: IGalleryExtension, installOptions: InstallOptions = {}): Promise { + if (isUndefined(installOptions.donotVerifySignature)) { + const value = this.configurationService.getValue('extensions.verifySignature'); + installOptions.donotVerifySignature = isBoolean(value) ? !value : undefined; + } const local = await this.doInstallFromGallery(extension, installOptions); await this.installUIDependenciesAndPackedExtensions(local); return local; } - private async doInstallFromGallery(extension: IGalleryExtension, installOptions?: InstallOptions): Promise { + private async doInstallFromGallery(extension: IGalleryExtension, installOptions: InstallOptions): Promise { if (this.configurationService.getValue('remote.downloadExtensionsLocally')) { - return this.downloadAndInstall(extension, installOptions || {}); + return this.downloadAndInstall(extension, installOptions); } try { const clientTargetPlatform = await this.localExtensionManagementServer.extensionManagementService.getTargetPlatform(); @@ -73,7 +78,7 @@ export class NativeRemoteExtensionManagementService extends RemoteExtensionManag case ExtensionManagementErrorCode.Unknown: try { this.logService.error(`Error while installing '${extension.identifier.id}' extension in the remote server.`, toErrorMessage(error)); - return await this.downloadAndInstall(extension, installOptions || {}); + return await this.downloadAndInstall(extension, installOptions); } catch (e) { this.logService.error(e); throw e; 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 fd3d4210cb7..fbd716c5fcf 100644 --- a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts +++ b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; import * as sinon from 'sinon'; -import { IExtensionManagementService, DidUninstallExtensionEvent, ILocalExtension, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, DidUpdateExtensionMetadata } from '../../../../../platform/extensionManagement/common/extensionManagement.js'; +import { IExtensionManagementService, DidUninstallExtensionEvent, ILocalExtension, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, DidUpdateExtensionMetadata, InstallOperation } from '../../../../../platform/extensionManagement/common/extensionManagement.js'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IWorkbenchExtensionManagementService, ExtensionInstallLocation, IProfileAwareExtensionManagementService, DidChangeProfileEvent } from '../../common/extensionManagement.js'; import { ExtensionEnablementService } from '../../browser/extensionEnablementService.js'; import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; @@ -979,6 +979,53 @@ suite('ExtensionEnablementService Test', () => { assert.deepStrictEqual(testObject.getEnablementStates(installed), [EnablementState.DisabledGlobally, EnablementState.DisabledByExtensionDependency]); }); + test('test extension is not disabled when it has a missing dependency', async () => { + const target = aLocalExtension2('pub.b', { extensionDependencies: ['pub.a'] }); + installed.push(target); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); + await (testObject).waitUntilInitialized(); + + assert.strictEqual(testObject.getEnablementState(target), EnablementState.EnabledGlobally); + }); + + test('test extension is disabled by invalidity', async () => { + const target = aLocalExtension2('pub.b', {}, { isValid: false }); + assert.strictEqual(testObject.getEnablementState(target), EnablementState.DisabledByInvalidExtension); + }); + + test('test extension is disabled by dependency when it has a dependency that is invalid', async () => { + const target = aLocalExtension2('pub.b', { extensionDependencies: ['pub.a'] }); + installed.push(...[target, aLocalExtension2('pub.a', {}, { isValid: false })]); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); + await (testObject).waitUntilInitialized(); + + assert.strictEqual(testObject.getEnablementState(target), EnablementState.DisabledByExtensionDependency); + }); + + test('test extension is enabled when its dependency becomes valid', async () => { + const extension = aLocalExtension2('pub.b', { extensionDependencies: ['pub.a'] }); + installed.push(...[extension, aLocalExtension2('pub.a', {}, { isValid: false })]); + testObject = disposableStore.add(new TestExtensionEnablementService(instantiationService)); + await (testObject).waitUntilInitialized(); + + assert.strictEqual(testObject.getEnablementState(extension), EnablementState.DisabledByExtensionDependency); + + const target = sinon.spy(); + disposableStore.add(testObject.onEnablementChanged(target)); + + const validExtension = aLocalExtension2('pub.a'); + didInstallEvent.fire([{ + identifier: validExtension.identifier, + operation: InstallOperation.Install, + source: validExtension.location, + profileLocation: validExtension.location, + local: validExtension, + }]); + + assert.strictEqual(testObject.getEnablementState(extension), EnablementState.EnabledGlobally); + assert.deepStrictEqual((target.args[0][0][0]).identifier, { id: 'pub.b' }); + }); + test('test override workspace to trusted when getting extensions enablements', async () => { const extension = aLocalExtension2('pub.a', { main: 'main.js', capabilities: { untrustedWorkspaces: { supported: false, description: 'hello' } } }); instantiationService.stub(IWorkspaceTrustManagementService, >{ isWorkspaceTrusted() { return false; } }); @@ -1080,7 +1127,8 @@ function aLocalExtension2(id: string, manifest: Partial = {} location: URI.file(`pub.${name}`), galleryIdentifier: { id, uuid: undefined }, type: ExtensionType.User, - ...properties + ...properties, + isValid: properties.isValid ?? true, }; properties.isBuiltin = properties.type === ExtensionType.System; return Object.create({ manifest, ...properties }); diff --git a/src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.esm.html b/src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.esm.html index bdceb3fe7ed..53bc54bb5aa 100644 --- a/src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.esm.html +++ b/src/vs/workbench/services/extensions/worker/webWorkerExtensionHostIframe.esm.html @@ -4,7 +4,7 @@ @@ -100,13 +100,17 @@ const isESM = true; // ESM-uncomment-end + // In below blob code, we are using JSON.stringify to ensure the passed + // in values are not breaking our script. The values may contain string + // terminating characters (such as ' or "). + const blob = new Blob([[ `/*extensionHostWorker*/`, - `globalThis.MonacoEnvironment = { baseUrl: '${baseUrl}' };`, + `globalThis.MonacoEnvironment = { baseUrl: ${JSON.stringify(baseUrl)} };`, `globalThis._VSCODE_NLS_MESSAGES = ${JSON.stringify(nlsMessages)};`, `globalThis._VSCODE_NLS_LANGUAGE = ${JSON.stringify(nlsLanguage)};`, - `globalThis._VSCODE_FILE_ROOT = '${fileRoot}';`, - isESM ? `await import('${workerUrl}');` : `importScripts('${workerUrl}');`, + `globalThis._VSCODE_FILE_ROOT = ${JSON.stringify(fileRoot)};`, + isESM ? `await import(${JSON.stringify(workerUrl)});` : `importScripts(${JSON.stringify(workerUrl)});`, `/*extensionHostWorker*/` ].join('')], { type: 'application/javascript' }); diff --git a/src/vs/workbench/services/request/browser/requestService.ts b/src/vs/workbench/services/request/browser/requestService.ts index b2138eea0fa..afa0b0f4358 100644 --- a/src/vs/workbench/services/request/browser/requestService.ts +++ b/src/vs/workbench/services/request/browser/requestService.ts @@ -12,8 +12,7 @@ import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js import { CommandsRegistry } from '../../../../platform/commands/common/commands.js'; import { AbstractRequestService, AuthInfo, Credentials, IRequestService } from '../../../../platform/request/common/request.js'; import { request } from '../../../../base/parts/request/browser/request.js'; -import { ILoggerService } from '../../../../platform/log/common/log.js'; -import { localize } from '../../../../nls.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; export class BrowserRequestService extends AbstractRequestService implements IRequestService { @@ -22,12 +21,9 @@ export class BrowserRequestService extends AbstractRequestService implements IRe constructor( @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, @IConfigurationService private readonly configurationService: IConfigurationService, - @ILoggerService loggerService: ILoggerService, + @ILogService logService: ILogService, ) { - super(loggerService.createLogger('network-window', { - name: localize('network-window', "Network (Window)"), - hidden: true - })); + super(logService); } async request(options: IRequestOptions, token: CancellationToken): Promise { diff --git a/src/vs/workbench/services/request/electron-sandbox/requestService.ts b/src/vs/workbench/services/request/electron-sandbox/requestService.ts index 8e8ba4e4c9d..f0b2a760c56 100644 --- a/src/vs/workbench/services/request/electron-sandbox/requestService.ts +++ b/src/vs/workbench/services/request/electron-sandbox/requestService.ts @@ -10,8 +10,7 @@ import { INativeHostService } from '../../../../platform/native/common/native.js import { IRequestContext, IRequestOptions } from '../../../../base/parts/request/common/request.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { request } from '../../../../base/parts/request/browser/request.js'; -import { ILoggerService } from '../../../../platform/log/common/log.js'; -import { localize } from '../../../../nls.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; export class NativeRequestService extends AbstractRequestService implements IRequestService { @@ -20,12 +19,9 @@ export class NativeRequestService extends AbstractRequestService implements IReq constructor( @INativeHostService private readonly nativeHostService: INativeHostService, @IConfigurationService private readonly configurationService: IConfigurationService, - @ILoggerService loggerService: ILoggerService, + @ILogService logService: ILogService, ) { - super(loggerService.createLogger('network-window', { - name: localize('network-window', "Network (Window)"), - hidden: true - })); + super(logService); } async request(options: IRequestOptions, token: CancellationToken): Promise { diff --git a/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts b/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts index a20c8146c15..44b591daf4a 100644 --- a/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts +++ b/src/vs/workbench/services/treeSitter/browser/treeSitterTokenizationFeature.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// eslint-disable-next-line local/code-import-patterns import type { Parser } from '@vscode/tree-sitter-wasm'; import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable, DisposableMap, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index ffe569930b7..285fea2c50e 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -398,7 +398,4 @@ import './contrib/accountEntitlements/browser/accountsEntitlements.contribution. // Synchronized Scrolling import './contrib/scrollLocking/browser/scrollLocking.contribution.js'; -// Network -import './contrib/request/common/request.contribution.js'; - //#endregion diff --git a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts index a3ff6db82d1..1c500f53e69 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantAdditions.d.ts @@ -36,6 +36,11 @@ declare module 'vscode' { constructor(value: string | MarkdownString, vulnerabilities: ChatVulnerability[]); } + export class ChatResponseCodeblockUriPart { + value: Uri; + constructor(value: Uri); + } + /** * Displays a {@link Command command} as a button in the chat response. */ @@ -129,6 +134,18 @@ declare module 'vscode' { constructor(uri: Uri, range: Range); } + // Extended to add `SymbolInformation`. Would also be added to `constructor`. + export interface ChatResponseAnchorPart { + /** + * The target of this anchor. + * + * If this is a {@linkcode Uri} or {@linkcode Location}, this is rendered as a normal link. + * + * If this is a {@linkcode SymbolInformation}, this is rendered as a symbol link. + */ + value2: Uri | Location | SymbolInformation; + } + export interface ChatResponseStream { /** @@ -143,6 +160,7 @@ declare module 'vscode' { textEdit(target: Uri, edits: TextEdit | TextEdit[]): void; markdownWithVulnerabilities(value: string | MarkdownString, vulnerabilities: ChatVulnerability[]): void; + codeblockUri(uri: Uri): void; detectedParticipant(participant: string, command?: ChatCommand): void; push(part: ChatResponsePart | ChatResponseTextEditPart | ChatResponseDetectedParticipantPart | ChatResponseWarningPart | ChatResponseProgressPart2): void; @@ -230,6 +248,10 @@ declare module 'vscode' { export type ChatExtendedRequestHandler = (request: ChatRequest, context: ChatContext, response: ChatResponseStream, token: CancellationToken) => ProviderResult; + export interface ChatRequest { + toolInvocationToken: ChatParticipantToolToken; + } + export interface ChatResult { nextQuestion?: { prompt: string; diff --git a/src/vscode-dts/vscode.proposed.lmTools.d.ts b/src/vscode-dts/vscode.proposed.lmTools.d.ts index 3fff1acd8dd..e4b045a4a46 100644 --- a/src/vscode-dts/vscode.proposed.lmTools.d.ts +++ b/src/vscode-dts/vscode.proposed.lmTools.d.ts @@ -103,7 +103,11 @@ declare module 'vscode' { export function invokeTool(id: string, options: LanguageModelToolInvocationOptions, token: CancellationToken): Thenable; } + export type ChatParticipantToolToken = unknown; + export interface LanguageModelToolInvocationOptions { + toolInvocationToken: ChatParticipantToolToken | undefined; + /** * Parameters with which to invoke the tool. */ diff --git a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts index 39ee5f8281e..cc1e6b44635 100644 --- a/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts +++ b/src/vscode-dts/vscode.proposed.scmHistoryProvider.d.ts @@ -11,38 +11,32 @@ declare module 'vscode' { } export interface SourceControlHistoryProvider { - currentHistoryItemGroup?: SourceControlHistoryItemGroup; + readonly currentHistoryItemRef: SourceControlHistoryItemRef | undefined; + readonly currentHistoryItemRemoteRef: SourceControlHistoryItemRef | undefined; + readonly currentHistoryItemBaseRef: SourceControlHistoryItemRef | undefined; /** - * Fires when the current history item group changes after - * a user action (ex: commit, checkout, fetch, pull, push) + * Fires when the current history item refs (local, remote, base) + * change after a user action (ex: commit, checkout, fetch, pull, push) */ - onDidChangeCurrentHistoryItemGroup: Event; + onDidChangeCurrentHistoryItemRefs: Event; /** - * Fires when the history item groups change (ex: commit, push, fetch) + * Fires when history item refs change */ - // onDidChangeHistoryItemGroups: Event; + onDidChangeHistoryItemRefs: Event; + provideHistoryItemRefs(token: CancellationToken): ProviderResult; provideHistoryItems(options: SourceControlHistoryOptions, token: CancellationToken): ProviderResult; provideHistoryItemChanges(historyItemId: string, historyItemParentId: string | undefined, token: CancellationToken): ProviderResult; - resolveHistoryItemGroupCommonAncestor(historyItemGroupIds: string[], token: CancellationToken): ProviderResult; + resolveHistoryItemRefsCommonAncestor(historyItemRefs: string[], token: CancellationToken): ProviderResult; } export interface SourceControlHistoryOptions { - readonly cursor?: string; readonly skip?: number; readonly limit?: number | { id?: string }; - readonly historyItemGroupIds?: readonly string[]; - } - - export interface SourceControlHistoryItemGroup { - readonly id: string; - readonly name: string; - readonly revision?: string; - readonly base?: Omit, 'remote'>; - readonly remote?: Omit, 'remote'>; + readonly historyItemRefs?: readonly string[]; } export interface SourceControlHistoryItemStatistics { @@ -51,11 +45,6 @@ declare module 'vscode' { readonly deletions: number; } - export interface SourceControlHistoryItemLabel { - readonly title: string; - readonly icon?: Uri | { light: Uri; dark: Uri } | ThemeIcon; - } - export interface SourceControlHistoryItem { readonly id: string; readonly parentIds: string[]; @@ -64,7 +53,16 @@ declare module 'vscode' { readonly author?: string; readonly timestamp?: number; readonly statistics?: SourceControlHistoryItemStatistics; - readonly labels?: SourceControlHistoryItemLabel[]; + readonly references?: SourceControlHistoryItemRef[]; + } + + export interface SourceControlHistoryItemRef { + readonly id: string; + readonly name: string; + readonly description?: string; + readonly revision?: string; + readonly category?: string; + readonly icon?: Uri | { light: Uri; dark: Uri } | ThemeIcon; } export interface SourceControlHistoryItemChange { @@ -74,10 +72,9 @@ declare module 'vscode' { readonly renameUri: Uri | undefined; } - // export interface SourceControlHistoryChangeEvent { - // readonly added: Iterable; - // readonly removed: Iterable; - // readonly modified: Iterable; - // } - + export interface SourceControlHistoryItemRefsChangeEvent { + readonly added: readonly SourceControlHistoryItemRef[]; + readonly removed: readonly SourceControlHistoryItemRef[]; + readonly modified: readonly SourceControlHistoryItemRef[]; + } } diff --git a/test/automation/package-lock.json b/test/automation/package-lock.json index 1a59eaa65f1..0253b826daf 100644 --- a/test/automation/package-lock.json +++ b/test/automation/package-lock.json @@ -9,14 +9,12 @@ "version": "1.71.0", "license": "MIT", "dependencies": { - "mkdirp": "^1.0.4", "ncp": "^2.0.0", "tmp": "0.2.1", "tree-kill": "1.2.2", "vscode-uri": "3.0.2" }, "devDependencies": { - "@types/mkdirp": "^1.0.1", "@types/ncp": "2.0.1", "@types/node": "20.x", "@types/tmp": "0.2.2", @@ -25,15 +23,6 @@ "watch": "^1.0.2" } }, - "node_modules/@types/mkdirp": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-1.0.1.tgz", - "integrity": "sha512-HkGSK7CGAXncr8Qn/0VqNtExEE+PHMWb+qlR1faHMao7ng6P3tAaoWWBMdva0gL5h4zprjIO89GJOLXsMcDm1Q==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/ncp": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/ncp/-/ncp-2.0.1.tgz", @@ -596,17 +585,6 @@ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/test/automation/package.json b/test/automation/package.json index b9dbbec4bb8..4d5dbd02f63 100644 --- a/test/automation/package.json +++ b/test/automation/package.json @@ -18,14 +18,12 @@ "prepublishOnly": "npm run copy-package-version" }, "dependencies": { - "mkdirp": "^1.0.4", "ncp": "^2.0.0", "tmp": "0.2.1", "tree-kill": "1.2.2", "vscode-uri": "3.0.2" }, "devDependencies": { - "@types/mkdirp": "^1.0.1", "@types/ncp": "2.0.1", "@types/node": "20.x", "@types/tmp": "0.2.2", diff --git a/test/automation/src/electron.ts b/test/automation/src/electron.ts index da2087a8faa..8a9a73974f6 100644 --- a/test/automation/src/electron.ts +++ b/test/automation/src/electron.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { join } from 'path'; -import * as mkdirp from 'mkdirp'; +import * as fs from 'fs'; import { copyExtension } from './extensions'; import { URI } from 'vscode-uri'; import { measureAndLog } from './logger'; @@ -70,7 +70,7 @@ export async function resolveElectronConfiguration(options: LaunchOptions): Prom } args.push('--enable-proposed-api=vscode.vscode-test-resolver'); const remoteDataDir = `${userDataDir}-server`; - mkdirp.sync(remoteDataDir); + fs.mkdirSync(remoteDataDir, { recursive: true }); env['TESTRESOLVER_DATA_FOLDER'] = remoteDataDir; env['TESTRESOLVER_LOGS_FOLDER'] = join(logsPath, 'server'); diff --git a/test/automation/src/playwrightBrowser.ts b/test/automation/src/playwrightBrowser.ts index 0a98250767b..e36d070cdc9 100644 --- a/test/automation/src/playwrightBrowser.ts +++ b/test/automation/src/playwrightBrowser.ts @@ -6,7 +6,7 @@ import * as playwright from '@playwright/test'; import { ChildProcess, spawn } from 'child_process'; import { join } from 'path'; -import * as mkdirp from 'mkdirp'; +import * as fs from 'fs'; import { URI } from 'vscode-uri'; import { Logger, measureAndLog } from './logger'; import type { LaunchOptions } from './code'; @@ -35,7 +35,7 @@ async function launchServer(options: LaunchOptions) { const serverLogsPath = join(logsPath, 'server'); const codeServerPath = codePath ?? process.env.VSCODE_REMOTE_SERVER_PATH; const agentFolder = userDataDir; - await measureAndLog(() => mkdirp(agentFolder), `mkdirp(${agentFolder})`, logger); + await measureAndLog(() => fs.promises.mkdir(agentFolder, { recursive: true }), `mkdirp(${agentFolder})`, logger); const env = { VSCODE_REMOTE_SERVER_PATH: codeServerPath, diff --git a/test/integration/browser/package-lock.json b/test/integration/browser/package-lock.json index ec66b5554e9..bdec916fb4b 100644 --- a/test/integration/browser/package-lock.json +++ b/test/integration/browser/package-lock.json @@ -9,7 +9,6 @@ "version": "0.1.0", "license": "MIT", "devDependencies": { - "@types/mkdirp": "^1.0.1", "@types/node": "20.x", "@types/rimraf": "^2.0.4", "@types/tmp": "0.1.0", @@ -42,15 +41,6 @@ "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", "dev": true }, - "node_modules/@types/mkdirp": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-1.0.1.tgz", - "integrity": "sha512-HkGSK7CGAXncr8Qn/0VqNtExEE+PHMWb+qlR1faHMao7ng6P3tAaoWWBMdva0gL5h4zprjIO89GJOLXsMcDm1Q==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/node": { "version": "20.11.24", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", diff --git a/test/integration/browser/package.json b/test/integration/browser/package.json index e87c8669983..728d02763d1 100644 --- a/test/integration/browser/package.json +++ b/test/integration/browser/package.json @@ -7,7 +7,6 @@ "compile": "node ../../../node_modules/typescript/bin/tsc" }, "devDependencies": { - "@types/mkdirp": "^1.0.1", "@types/node": "20.x", "@types/rimraf": "^2.0.4", "@types/tmp": "0.1.0", diff --git a/test/monaco/package-lock.json b/test/monaco/package-lock.json index 7846640eea8..224680dd597 100644 --- a/test/monaco/package-lock.json +++ b/test/monaco/package-lock.json @@ -77,9 +77,9 @@ } }, "node_modules/pathval": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", - "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA= sha512-qZ181q3ICkag/+lv1X6frDUF84pqCm30qild3LGbD84n0AC75CYwnWsQRDlpz7zDkU5NVcmhHh4LjXK0goLYZA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", "dev": true, "engines": { "node": "*" diff --git a/test/smoke/package-lock.json b/test/smoke/package-lock.json index 4932002dbe8..af2f72d890a 100644 --- a/test/smoke/package-lock.json +++ b/test/smoke/package-lock.json @@ -9,13 +9,11 @@ "version": "0.1.0", "license": "MIT", "dependencies": { - "mkdirp": "^1.0.4", "ncp": "^2.0.0", "node-fetch": "^2.6.7", "rimraf": "3.0.2" }, "devDependencies": { - "@types/mkdirp": "^1.0.1", "@types/mocha": "^9.1.1", "@types/ncp": "2.0.1", "@types/node": "20.x", @@ -48,15 +46,6 @@ "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", "dev": true }, - "node_modules/@types/mkdirp": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-1.0.1.tgz", - "integrity": "sha512-HkGSK7CGAXncr8Qn/0VqNtExEE+PHMWb+qlR1faHMao7ng6P3tAaoWWBMdva0gL5h4zprjIO89GJOLXsMcDm1Q==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/mocha": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.1.tgz", @@ -582,17 +571,6 @@ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/ncp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", diff --git a/test/smoke/package.json b/test/smoke/package.json index 1b4d142a1e7..90da59a0f53 100644 --- a/test/smoke/package.json +++ b/test/smoke/package.json @@ -11,13 +11,11 @@ "mocha": "node ../node_modules/mocha/bin/mocha" }, "dependencies": { - "mkdirp": "^1.0.4", "ncp": "^2.0.0", "node-fetch": "^2.6.7", "rimraf": "3.0.2" }, "devDependencies": { - "@types/mkdirp": "^1.0.1", "@types/mocha": "^9.1.1", "@types/ncp": "2.0.1", "@types/node": "20.x", diff --git a/test/smoke/src/main.ts b/test/smoke/src/main.ts index 4df38db9d72..d8120ea0675 100644 --- a/test/smoke/src/main.ts +++ b/test/smoke/src/main.ts @@ -10,7 +10,6 @@ import * as path from 'path'; import * as os from 'os'; import * as minimist from 'minimist'; import * as rimraf from 'rimraf'; -import * as mkdirp from 'mkdirp'; import * as vscodetest from '@vscode/test-electron'; import fetch from 'node-fetch'; import { Quality, MultiLogger, Logger, ConsoleLogger, FileLogger, measureAndLog, getDevElectronPath, getBuildElectronPath, getBuildVersion } from '../../automation'; @@ -105,7 +104,7 @@ function createLogger(): Logger { // Prepare logs rot path fs.rmSync(logsRootPath, { recursive: true, force: true, maxRetries: 3 }); - mkdirp.sync(logsRootPath); + fs.mkdirSync(logsRootPath, { recursive: true }); // Always log to log file loggers.push(new FileLogger(path.join(logsRootPath, 'smoke-test-runner.log'))); @@ -123,7 +122,7 @@ const testDataPath = path.join(os.tmpdir(), 'vscsmoke'); if (fs.existsSync(testDataPath)) { rimraf.sync(testDataPath); } -mkdirp.sync(testDataPath); +fs.mkdirSync(testDataPath, { recursive: true }); process.once('exit', () => { try { rimraf.sync(testDataPath); @@ -135,7 +134,7 @@ process.once('exit', () => { const testRepoUrl = 'https://github.com/microsoft/vscode-smoketest-express'; const workspacePath = path.join(testDataPath, 'vscode-smoketest-express'); const extensionsPath = path.join(testDataPath, 'extensions-dir'); -mkdirp.sync(extensionsPath); +fs.mkdirSync(extensionsPath, { recursive: true }); function fail(errorMessage): void { logger.log(errorMessage); @@ -179,7 +178,7 @@ function parseQuality(): Quality { // if (!opts.web) { let testCodePath = opts.build; - let electronPath: string; + let electronPath: string | undefined; if (testCodePath) { electronPath = getBuildElectronPath(testCodePath);