diff --git a/.eslintignore b/.eslintignore index af5f9a4940b..ae7798cc04e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -13,6 +13,9 @@ **/extensions/notebook-renderers/renderer-out/index.js **/extensions/simple-browser/media/index.js **/extensions/typescript-language-features/test-workspace/** +**/extensions/typescript-language-features/extension.webpack.config.js +**/extensions/typescript-language-features/extension-browser.webpack.config.js +**/extensions/typescript-language-features/web/** **/extensions/vscode-api-tests/testWorkspace/** **/extensions/vscode-api-tests/testWorkspace2/** **/fixtures/** diff --git a/build/azure-pipelines/cli/prepare.js b/build/azure-pipelines/cli/prepare.js index b0cb2e6152a..e46cae42e3d 100644 --- a/build/azure-pipelines/cli/prepare.js +++ b/build/azure-pipelines/cli/prepare.js @@ -42,6 +42,9 @@ const setLauncherEnvironmentVars = () => { ['VSCODE_CLI_VERSION', packageJson.version], ['VSCODE_CLI_UPDATE_ENDPOINT', product.updateUrl], ['VSCODE_CLI_QUALITY', product.quality], + ['VSCODE_CLI_NAME_SHORT', product.nameShort], + ['VSCODE_CLI_NAME_LONG', product.nameLong], + ['VSCODE_CLI_APPLICATION_NAME', product.applicationName], ['VSCODE_CLI_COMMIT', commit], [ 'VSCODE_CLI_WIN32_APP_IDS', diff --git a/build/azure-pipelines/cli/prepare.ts b/build/azure-pipelines/cli/prepare.ts index 53f33d08568..a9c172fa017 100644 --- a/build/azure-pipelines/cli/prepare.ts +++ b/build/azure-pipelines/cli/prepare.ts @@ -46,6 +46,9 @@ const setLauncherEnvironmentVars = () => { ['VSCODE_CLI_VERSION', packageJson.version], ['VSCODE_CLI_UPDATE_ENDPOINT', product.updateUrl], ['VSCODE_CLI_QUALITY', product.quality], + ['VSCODE_CLI_NAME_SHORT', product.nameShort], + ['VSCODE_CLI_NAME_LONG', product.nameLong], + ['VSCODE_CLI_APPLICATION_NAME', product.applicationName], ['VSCODE_CLI_COMMIT', commit], [ 'VSCODE_CLI_WIN32_APP_IDS', diff --git a/build/lib/extensions.js b/build/lib/extensions.js index ed38a475c61..1f763386e2e 100644 --- a/build/lib/extensions.js +++ b/build/lib/extensions.js @@ -104,7 +104,7 @@ function fromLocalWebpack(extensionPath, webpackConfigFileName) { // check for a webpack configuration files, then invoke webpack // and merge its output with the files stream. const webpackConfigLocations = glob.sync(path.join(extensionPath, '**', webpackConfigFileName), { ignore: ['**/node_modules'] }); - const webpackStreams = webpackConfigLocations.map(webpackConfigPath => { + const webpackStreams = webpackConfigLocations.flatMap(webpackConfigPath => { const webpackDone = (err, stats) => { fancyLog(`Bundled extension: ${ansiColors.yellow(path.join(path.basename(extensionPath), path.relative(extensionPath, webpackConfigPath)))}...`); if (err) { @@ -118,27 +118,30 @@ function fromLocalWebpack(extensionPath, webpackConfigFileName) { result.emit('error', compilation.warnings.join('\n')); } }; - const webpackConfig = { - ...require(webpackConfigPath), - ...{ mode: 'production' } - }; - const relativeOutputPath = path.relative(extensionPath, webpackConfig.output.path); - return webpackGulp(webpackConfig, webpack, webpackDone) - .pipe(es.through(function (data) { - data.stat = data.stat || {}; - data.base = extensionPath; - this.emit('data', data); - })) - .pipe(es.through(function (data) { - // source map handling: - // * rewrite sourceMappingURL - // * save to disk so that upload-task picks this up - const contents = data.contents.toString('utf8'); - data.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, function (_m, g1) { - return `\n//# sourceMappingURL=${sourceMappingURLBase}/extensions/${path.basename(extensionPath)}/${relativeOutputPath}/${g1}`; - }), 'utf8'); - this.emit('data', data); - })); + const exportedConfig = require(webpackConfigPath); + return (Array.isArray(exportedConfig) ? exportedConfig : [exportedConfig]).map(config => { + const webpackConfig = { + ...config, + ...{ mode: 'production' } + }; + const relativeOutputPath = path.relative(extensionPath, webpackConfig.output.path); + return webpackGulp(webpackConfig, webpack, webpackDone) + .pipe(es.through(function (data) { + data.stat = data.stat || {}; + data.base = extensionPath; + this.emit('data', data); + })) + .pipe(es.through(function (data) { + // source map handling: + // * rewrite sourceMappingURL + // * save to disk so that upload-task picks this up + const contents = data.contents.toString('utf8'); + data.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, function (_m, g1) { + return `\n//# sourceMappingURL=${sourceMappingURLBase}/extensions/${path.basename(extensionPath)}/${relativeOutputPath}/${g1}`; + }), 'utf8'); + this.emit('data', data); + })); + }); }); es.merge(...webpackStreams, es.readArray(files)) // .pipe(es.through(function (data) { diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index 71513ae1f58..56a47071b5a 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -121,7 +121,7 @@ function fromLocalWebpack(extensionPath: string, webpackConfigFileName: string): { ignore: ['**/node_modules'] } )); - const webpackStreams = webpackConfigLocations.map(webpackConfigPath => { + const webpackStreams = webpackConfigLocations.flatMap(webpackConfigPath => { const webpackDone = (err: any, stats: any) => { fancyLog(`Bundled extension: ${ansiColors.yellow(path.join(path.basename(extensionPath), path.relative(extensionPath, webpackConfigPath)))}...`); @@ -137,29 +137,32 @@ function fromLocalWebpack(extensionPath: string, webpackConfigFileName: string): } }; - const webpackConfig = { - ...require(webpackConfigPath), - ...{ mode: 'production' } - }; - const relativeOutputPath = path.relative(extensionPath, webpackConfig.output.path); + const exportedConfig = require(webpackConfigPath); + return (Array.isArray(exportedConfig) ? exportedConfig : [exportedConfig]).map(config => { + const webpackConfig = { + ...config, + ...{ mode: 'production' } + }; + const relativeOutputPath = path.relative(extensionPath, webpackConfig.output.path); - return webpackGulp(webpackConfig, webpack, webpackDone) - .pipe(es.through(function (data) { - data.stat = data.stat || {}; - data.base = extensionPath; - this.emit('data', data); - })) - .pipe(es.through(function (data: File) { - // source map handling: - // * rewrite sourceMappingURL - // * save to disk so that upload-task picks this up - const contents = (data.contents).toString('utf8'); - data.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, function (_m, g1) { - return `\n//# sourceMappingURL=${sourceMappingURLBase}/extensions/${path.basename(extensionPath)}/${relativeOutputPath}/${g1}`; - }), 'utf8'); + return webpackGulp(webpackConfig, webpack, webpackDone) + .pipe(es.through(function (data) { + data.stat = data.stat || {}; + data.base = extensionPath; + this.emit('data', data); + })) + .pipe(es.through(function (data: File) { + // source map handling: + // * rewrite sourceMappingURL + // * save to disk so that upload-task picks this up + const contents = (data.contents).toString('utf8'); + data.contents = Buffer.from(contents.replace(/\n\/\/# sourceMappingURL=(.*)$/gm, function (_m, g1) { + return `\n//# sourceMappingURL=${sourceMappingURLBase}/extensions/${path.basename(extensionPath)}/${relativeOutputPath}/${g1}`; + }), 'utf8'); - this.emit('data', data); - })); + this.emit('data', data); + })); + }); }); es.merge(...webpackStreams, es.readArray(files)) @@ -506,7 +509,7 @@ export async function webpackExtensions(taskName: string, isWatch: boolean, webp for (const { configPath, outputRoot } of webpackConfigLocations) { const configOrFnOrArray = require(configPath); - function addConfig(configOrFnOrArray: webpack.Configuration | ((env: unknown,args: unknown) => webpack.Configuration) | webpack.Configuration[]) { + function addConfig(configOrFnOrArray: webpack.Configuration | ((env: unknown, args: unknown) => webpack.Configuration) | webpack.Configuration[]) { for (const configOrFn of Array.isArray(configOrFnOrArray) ? configOrFnOrArray : [configOrFnOrArray]) { const config = typeof configOrFn === 'function' ? configOrFn({}, {}) : configOrFn; if (outputRoot) { diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 4ee1f5aae98..57fba7fbe05 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -26,6 +26,17 @@ dependencies = [ "libc", ] +[[package]] +name = "async-broadcast" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d26004fe83b2d1cd3a97609b21e39f9a31535822210fe83205d2ce48866ea61" +dependencies = [ + "event-listener", + "futures-core", + "parking_lot", +] + [[package]] name = "async-io" version = "1.9.0" @@ -47,10 +58,21 @@ dependencies = [ ] [[package]] -name = "async-trait" -version = "0.1.57" +name = "async-recursion" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f" +checksum = "2cda8f4bcc10624c4e85bc66b3f452cca98cfa5ca002dc83a16aad2367641bea" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e805d94e6b5001b651426cf4cd446b1ab5f319d27bab5c644f61de0a804360c" dependencies = [ "proc-macro2", "quote", @@ -241,6 +263,7 @@ dependencies = [ "uuid", "windows-service", "winreg", + "zbus 3.4.0", "zip", ] @@ -503,7 +526,17 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83c8d82922337cd23a15f88b70d8e4ef5f11da38dd7cdb55e84dd5de99695da0" dependencies = [ - "enumflags2_derive", + "enumflags2_derive 0.6.4", + "serde", +] + +[[package]] +name = "enumflags2" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e75d4cd21b95383444831539909fbb14b9dc3fdceb2a6f5d36577329a1f55ccb" +dependencies = [ + "enumflags2_derive 0.7.4", "serde", ] @@ -518,6 +551,17 @@ dependencies = [ "syn", ] +[[package]] +name = "enumflags2_derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58dc3c5e468259f19f2d46304a6b28f1c3d034442e14b322d2b850e36f6d5ae" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "err-derive" version = "0.3.1" @@ -532,6 +576,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "fastrand" version = "1.8.0" @@ -610,9 +660,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050" +checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed" dependencies = [ "futures-core", "futures-sink", @@ -620,9 +670,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf" +checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" [[package]] name = "futures-executor" @@ -637,9 +687,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68" +checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb" [[package]] name = "futures-lite" @@ -658,9 +708,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17" +checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d" dependencies = [ "proc-macro2", "quote", @@ -669,21 +719,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56" +checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9" [[package]] name = "futures-task" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1" +checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" [[package]] name = "futures-util" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90" +checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" dependencies = [ "futures-channel", "futures-core", @@ -779,6 +829,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hex-literal" version = "0.3.4" @@ -1102,6 +1158,20 @@ dependencies = [ "memoffset", ] +[[package]] +name = "nix" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e322c04a9e3440c327fca7b6c8a63e6890a32fa2ad689db972425f07e0d22abb" +dependencies = [ + "autocfg", + "bitflags", + "cfg-if", + "libc", + "memoffset", + "pin-utils", +] + [[package]] name = "ntapi" version = "0.3.7" @@ -1362,6 +1432,16 @@ dependencies = [ "tokio-stream", ] +[[package]] +name = "ordered-stream" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "034ce384018b245e8d8424bbe90577fbd91a533be74107e465e3474eb2285eef" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "os_str_bytes" version = "6.3.0" @@ -1830,10 +1910,10 @@ dependencies = [ "openssl", "rand 0.8.5", "serde", - "zbus", - "zbus_macros", - "zvariant", - "zvariant_derive", + "zbus 1.9.3", + "zbus_macros 1.9.3", + "zvariant 2.10.0", + "zvariant_derive 2.10.0", ] [[package]] @@ -2009,9 +2089,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fcd952facd492f9be3ef0d0b7032a6e442ee9b361d4acc2b1d0c4aaa5f613a1" +checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d" dependencies = [ "proc-macro2", "quote", @@ -2245,9 +2325,21 @@ checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tracing-core" version = "0.1.30" @@ -2312,6 +2404,16 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +[[package]] +name = "uds_windows" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce65604324d3cce9b966701489fbd0cf318cb1f7bd9dd07ac9a4ee6fb791930d" +dependencies = [ + "tempfile", + "winapi", +] + [[package]] name = "unicode-bidi" version = "0.3.8" @@ -2632,18 +2734,54 @@ dependencies = [ "async-io", "byteorder", "derivative", - "enumflags2", + "enumflags2 0.6.4", "fastrand", "futures", "nb-connect", - "nix", + "nix 0.22.3", "once_cell", "polling", "scoped-tls", "serde", "serde_repr", - "zbus_macros", - "zvariant", + "zbus_macros 1.9.3", + "zvariant 2.10.0", +] + +[[package]] +name = "zbus" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78a0b85c5608c27d2306d67e955b9c6e23a42d824205c85038a7afbe19c0ae22" +dependencies = [ + "async-broadcast", + "async-recursion", + "async-trait", + "byteorder", + "derivative", + "dirs 4.0.0", + "enumflags2 0.7.5", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "lazy_static", + "nix 0.25.0", + "once_cell", + "ordered-stream", + "rand 0.8.5", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tokio", + "tracing", + "uds_windows", + "winapi", + "zbus_macros 3.4.0", + "zbus_names", + "zvariant 3.7.1", ] [[package]] @@ -2658,6 +2796,30 @@ dependencies = [ "syn", ] +[[package]] +name = "zbus_macros" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b18018648e7e10ed856809befe7309002b87b2b12d5b282cb5040d7974b58677" +dependencies = [ + "proc-macro-crate 1.2.1", + "proc-macro2", + "quote", + "regex", + "syn", +] + +[[package]] +name = "zbus_names" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a408fd8a352695690f53906dc7fd036be924ec51ea5e05666ff42685ed0af5" +dependencies = [ + "serde", + "static_assertions", + "zvariant 3.7.1", +] + [[package]] name = "zeroize" version = "1.3.0" @@ -2684,11 +2846,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a68c7b55f2074489b7e8e07d2d0a6ee6b4f233867a653c664d8020ba53692525" dependencies = [ "byteorder", - "enumflags2", + "enumflags2 0.6.4", "libc", "serde", "static_assertions", - "zvariant_derive", + "zvariant_derive 2.10.0", +] + +[[package]] +name = "zvariant" +version = "3.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b794fb7f59af4105697b0449ba31731ee5dbb3e773a17dbdf3d36206ea1b1644" +dependencies = [ + "byteorder", + "enumflags2 0.7.5", + "libc", + "serde", + "static_assertions", + "zvariant_derive 3.7.1", ] [[package]] @@ -2702,3 +2878,15 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zvariant_derive" +version = "3.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd58d4b6c8e26d3dd2149c8c40c6613ef6451b9885ff1296d1ac86c388351a54" +dependencies = [ + "proc-macro-crate 1.2.1", + "proc-macro2", + "quote", + "syn", +] diff --git a/cli/Cargo.toml b/cli/Cargo.toml index dccd4a32c42..ff7e5c071b6 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -53,6 +53,7 @@ winreg = "0.10" [target.'cfg(target_os = "linux")'.dependencies] tar = { version = "0.4" } +zbus = { version = "3.4", default-features = false, features = ["tokio"] } [patch.crates-io] russh = { git = "https://github.com/microsoft/vscode-russh", branch = "main" } diff --git a/cli/src/commands/tunnels.rs b/cli/src/commands/tunnels.rs index 0dea7761d93..25a4601b52a 100644 --- a/cli/src/commands/tunnels.rs +++ b/cli/src/commands/tunnels.rs @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ use async_trait::async_trait; -use std::str::FromStr; use std::fmt; +use std::str::FromStr; use sysinfo::{Pid, SystemExt}; use tokio::sync::mpsc; use tokio::time::{sleep, Duration}; @@ -112,37 +112,45 @@ pub async fn service( ctx: CommandContext, service_args: TunnelServiceSubCommands, ) -> Result { - let manager = create_service_manager(ctx.log.clone()); + let manager = create_service_manager(ctx.log.clone(), &ctx.paths); match service_args { TunnelServiceSubCommands::Install => { // ensure logged in, otherwise subsequent serving will fail + println!("authing"); Auth::new(&ctx.paths, ctx.log.clone()) .get_credential() .await?; // likewise for license consent + println!("consent"); legal::require_consent(&ctx.paths, false)?; let current_exe = std::env::current_exe().map_err(|e| wrap(e, "could not get current exe"))?; - manager.register( - current_exe, - &[ - "--cli-data-dir", - ctx.paths.root().as_os_str().to_string_lossy().as_ref(), - "tunnel", - "service", - "internal-run", - ], - )?; + println!("calling register"); + manager + .register( + current_exe, + &[ + "--verbose", + "--cli-data-dir", + ctx.paths.root().as_os_str().to_string_lossy().as_ref(), + "tunnel", + "service", + "internal-run", + ], + ) + .await?; ctx.log.result("Service successfully installed! You can use `code tunnel service log` to monitor it, and `code tunnel service uninstall` to remove it."); } TunnelServiceSubCommands::Uninstall => { - manager.unregister()?; + manager.unregister().await?; } TunnelServiceSubCommands::InternalRun => { - manager.run(ctx.paths.clone(), TunnelServiceContainer::new(ctx.args))?; + manager + .run(ctx.paths.clone(), TunnelServiceContainer::new(ctx.args)) + .await?; } } diff --git a/cli/src/constants.rs b/cli/src/constants.rs index ab412fb860f..69f4fcc5d8c 100644 --- a/cli/src/constants.rs +++ b/cli/src/constants.rs @@ -28,10 +28,12 @@ pub const VSCODE_CLI_UPDATE_ENDPOINT: Option<&'static str> = pub const TUNNEL_SERVICE_USER_AGENT_ENV_VAR: &str = "TUNNEL_SERVICE_USER_AGENT"; +const MAYBE_APPLICATION_NAME: Option<&'static str> = option_env!("VSCODE_CLI_APPLICATION_NAME"); +const MAYBE_PRODUCT_NAME_LONG: Option<&'static str> = option_env!("VSCODE_CLI_NAME_LONG"); // JSON map of quality names to arrays of app IDs used for them, for example, `{"stable":["ABC123"]}` -const VSCODE_CLI_WIN32_APP_IDS: Option<&'static str> = option_env!("VSCODE_CLI_WIN32_APP_IDS"); +const MAYBE_CLI_WIN32_APP_IDS: Option<&'static str> = option_env!("VSCODE_CLI_WIN32_APP_IDS"); // JSON map of quality names to download URIs -const VSCODE_CLI_QUALITY_DOWNLOAD_URIS: Option<&'static str> = +const MAYBE_CLI_QUALITY_DOWNLOAD_URIS: Option<&'static str> = option_env!("VSCODE_CLI_QUALITY_DOWNLOAD_URIS"); pub fn get_default_user_agent() -> String { @@ -48,7 +50,10 @@ lazy_static! { _ => get_default_user_agent(), }; pub static ref WIN32_APP_IDS: Option>> = - VSCODE_CLI_WIN32_APP_IDS.and_then(|s| serde_json::from_str(s).unwrap()); + MAYBE_CLI_WIN32_APP_IDS.and_then(|s| serde_json::from_str(s).unwrap()); pub static ref QUALITY_DOWNLOAD_URIS: Option> = - VSCODE_CLI_QUALITY_DOWNLOAD_URIS.and_then(|s| serde_json::from_str(s).unwrap()); + MAYBE_CLI_QUALITY_DOWNLOAD_URIS.and_then(|s| serde_json::from_str(s).unwrap()); + pub static ref PRODUCT_NAME_LONG: &'static str = + MAYBE_PRODUCT_NAME_LONG.unwrap_or("Code - OSS"); + pub static ref APPLICATION_NAME: &'static str = MAYBE_APPLICATION_NAME.unwrap_or("code"); } diff --git a/cli/src/tunnels.rs b/cli/src/tunnels.rs index 638432b040d..e451dfb6621 100644 --- a/cli/src/tunnels.rs +++ b/cli/src/tunnels.rs @@ -17,6 +17,8 @@ mod protocol; #[cfg_attr(windows, path = "tunnels/server_bridge_windows.rs")] mod server_bridge; mod service; +#[cfg(target_os = "linux")] +mod service_linux; #[cfg(target_os = "windows")] mod service_windows; diff --git a/cli/src/tunnels/service.rs b/cli/src/tunnels/service.rs index 8085f3b069a..f66fd5d6b9f 100644 --- a/cli/src/tunnels/service.rs +++ b/cli/src/tunnels/service.rs @@ -25,58 +25,75 @@ pub trait ServiceContainer: Send { ) -> Result<(), AnyError>; } +#[async_trait] pub trait ServiceManager { /// Registers the current executable as a service to run with the given set /// of arguments. - fn register(&self, exe: PathBuf, args: &[&str]) -> Result<(), AnyError>; + async fn register(&self, exe: PathBuf, args: &[&str]) -> Result<(), AnyError>; /// Runs the service using the given handle. The executable *must not* take /// any action which may fail prior to calling this to ensure service /// states may update. - fn run( - &self, + async fn run( + self, launcher_paths: LauncherPaths, handle: impl 'static + ServiceContainer, ) -> Result<(), AnyError>; /// Unregisters the current executable as a service. - fn unregister(&self) -> Result<(), AnyError>; + async fn unregister(&self) -> Result<(), AnyError>; } #[cfg(target_os = "windows")] pub type ServiceManagerImpl = super::service_windows::WindowsService; -#[cfg(not(target_os = "windows"))] +#[cfg(target_os = "linux")] +pub type ServiceManagerImpl = super::service_linux::SystemdService; + +#[cfg(not(any(target_os = "windows", target_os = "linux")))] pub type ServiceManagerImpl = UnimplementedServiceManager; #[allow(unreachable_code)] -pub fn create_service_manager(log: log::Logger) -> ServiceManagerImpl { - ServiceManagerImpl::new(log) +#[allow(unused_variables)] +pub fn create_service_manager(log: log::Logger, paths: &LauncherPaths) -> ServiceManagerImpl { + #[cfg(target_os = "windows")] + { + super::service_windows::WindowsService::new(log) + } + #[cfg(target_os = "linux")] + { + super::service_linux::SystemdService::new(log, paths.clone()) + } + #[cfg(not(any(target_os = "windows", target_os = "linux")))] + { + UnimplementedServiceManager::new() + } } pub struct UnimplementedServiceManager(); #[allow(dead_code)] impl UnimplementedServiceManager { - fn new(_log: log::Logger) -> Self { + fn new() -> Self { Self() } } +#[async_trait] impl ServiceManager for UnimplementedServiceManager { - fn register(&self, _exe: PathBuf, _args: &[&str]) -> Result<(), AnyError> { + async fn register(&self, _exe: PathBuf, _args: &[&str]) -> Result<(), AnyError> { unimplemented!("Service management is not supported on this platform"); } - fn run( - &self, + async fn run( + self, _launcher_paths: LauncherPaths, _handle: impl 'static + ServiceContainer, ) -> Result<(), AnyError> { unimplemented!("Service management is not supported on this platform"); } - fn unregister(&self) -> Result<(), AnyError> { + async fn unregister(&self) -> Result<(), AnyError> { unimplemented!("Service management is not supported on this platform"); } } diff --git a/cli/src/tunnels/service_linux.rs b/cli/src/tunnels/service_linux.rs new file mode 100644 index 00000000000..e4f131d6e37 --- /dev/null +++ b/cli/src/tunnels/service_linux.rs @@ -0,0 +1,211 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +use std::{ + fs::File, + io::{self, Write}, + path::PathBuf, +}; + +use async_trait::async_trait; +use tokio::sync::mpsc; +use zbus::{dbus_proxy, zvariant, Connection}; + +use crate::{ + commands::tunnels::ShutdownSignal, + constants::{APPLICATION_NAME, PRODUCT_NAME_LONG}, + log, + state::LauncherPaths, + util::errors::{wrap, AnyError}, +}; + +use super::ServiceManager; + +pub struct SystemdService { + log: log::Logger, + service_file: PathBuf, +} + +impl SystemdService { + pub fn new(log: log::Logger, paths: LauncherPaths) -> Self { + Self { + log, + service_file: paths.root().join(SystemdService::service_name_string()), + } + } +} + +impl SystemdService { + async fn connect() -> Result { + let connection = Connection::session() + .await + .map_err(|e| wrap(e, "error creating dbus session"))?; + Ok(connection) + } + + async fn proxy(connection: &Connection) -> Result, AnyError> { + let proxy = SystemdManagerDbusProxy::new(connection) + .await + .map_err(|e| { + wrap( + e, + "error connecting to systemd, you may need to re-run with sudo:", + ) + })?; + + Ok(proxy) + } + + fn service_path_string(&self) -> String { + self.service_file.as_os_str().to_string_lossy().to_string() + } + + fn service_name_string() -> String { + format!("{}-tunnel.service", &*APPLICATION_NAME) + } +} + +#[async_trait] +impl ServiceManager for SystemdService { + async fn register( + &self, + exe: std::path::PathBuf, + args: &[&str], + ) -> Result<(), crate::util::errors::AnyError> { + let connection = SystemdService::connect().await?; + let proxy = SystemdService::proxy(&connection).await?; + + write_systemd_service_file(&self.service_file, exe, args) + .map_err(|e| wrap(e, "error creating service file"))?; + + proxy + .link_unit_files( + vec![self.service_path_string()], + /* 'runtime only'= */ false, + /* replace existing = */ true, + ) + .await + .map_err(|e| wrap(e, "error registering service"))?; + + info!(self.log, "Successfully registered service..."); + + proxy + .start_unit(SystemdService::service_name_string(), "replace".to_string()) + .await + .map_err(|e| wrap(e, "error starting service"))?; + + info!(self.log, "Tunnel service successfully started"); + + Ok(()) + } + + async fn run( + self, + launcher_paths: crate::state::LauncherPaths, + mut handle: impl 'static + super::ServiceContainer, + ) -> Result<(), crate::util::errors::AnyError> { + let (tx, rx) = mpsc::channel::(1); + tokio::spawn(async move { + tokio::signal::ctrl_c().await.ok(); + tx.send(ShutdownSignal::CtrlC).await.ok(); + }); + + handle.run_service(self.log, launcher_paths, rx).await + } + + async fn unregister(&self) -> Result<(), crate::util::errors::AnyError> { + let connection = SystemdService::connect().await?; + let proxy = SystemdService::proxy(&connection).await?; + + proxy + .stop_unit(SystemdService::service_name_string(), "replace".to_string()) + .await + .map_err(|e| wrap(e, "error unregistering service"))?; + + info!(self.log, "Successfully stopped service..."); + + proxy + .disable_unit_files( + vec![SystemdService::service_name_string()], + /* 'runtime only'= */ false, + ) + .await + .map_err(|e| wrap(e, "error unregistering service"))?; + + info!(self.log, "Tunnel service uninstalled"); + + Ok(()) + } +} + +fn write_systemd_service_file( + path: &PathBuf, + exe: std::path::PathBuf, + args: &[&str], +) -> io::Result<()> { + let mut f = File::create(path)?; + write!( + &mut f, + "[Unit]\n\ + Description={} Tunnel\n\ + After=network.target\n\ + StartLimitIntervalSec=0\n\ + \n\ + [Service]\n\ + Type=simple\n\ + Restart=always\n\ + RestartSec=10\n\ + ExecStart={} \"{}\"\n\ + \n\ + [Install]\n\ + WantedBy=multi-user.target\n\ + ", + &*PRODUCT_NAME_LONG, + exe.into_os_string().to_string_lossy(), + args.join("\" \"") + )?; + Ok(()) +} + +/// Minimal implementation of systemd types for the services we need. The full +/// definition can be found on any systemd machine with the command: +/// +/// gdbus introspect --system --dest org.freedesktop.systemd1 --object-path /org/freedesktop/systemd1 +/// +/// See docs here: https://www.freedesktop.org/software/systemd/man/org.freedesktop.systemd1.html +#[dbus_proxy( + interface = "org.freedesktop.systemd1.Manager", + gen_blocking = false, + default_service = "org.freedesktop.systemd1", + default_path = "/org/freedesktop/systemd1" +)] +trait SystemdManagerDbus { + #[dbus_proxy(name = "EnableUnitFiles")] + fn enable_unit_files( + &self, + files: Vec, + runtime: bool, + force: bool, + ) -> zbus::Result<(bool, Vec<(String, String, String)>)>; + + fn link_unit_files( + &self, + files: Vec, + runtime: bool, + force: bool, + ) -> zbus::Result>; + + fn disable_unit_files( + &self, + files: Vec, + runtime: bool, + ) -> zbus::Result>; + + #[dbus_proxy(name = "StartUnit")] + fn start_unit(&self, name: String, mode: String) -> zbus::Result; + + #[dbus_proxy(name = "StopUnit")] + fn stop_unit(&self, name: String, mode: String) -> zbus::Result; +} diff --git a/cli/src/tunnels/service_windows.rs b/cli/src/tunnels/service_windows.rs index 3d011dfa22b..9e743e89141 100644 --- a/cli/src/tunnels/service_windows.rs +++ b/cli/src/tunnels/service_windows.rs @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +use async_trait::async_trait; use dialoguer::{theme::ColorfulTheme, Input, Password}; use lazy_static::lazy_static; use std::{ffi::OsString, sync::Mutex, thread, time::Duration}; @@ -44,8 +45,9 @@ impl WindowsService { } } +#[async_trait] impl CliServiceManager for WindowsService { - fn register(&self, exe: std::path::PathBuf, args: &[&str]) -> Result<(), AnyError> { + async fn register(&self, exe: std::path::PathBuf, args: &[&str]) -> Result<(), AnyError> { let service_manager = ServiceManager::local_computer( None::<&str>, ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE, @@ -118,8 +120,8 @@ impl CliServiceManager for WindowsService { } #[allow(unused_must_use)] // triggers incorrectly on `define_windows_service!` - fn run( - &self, + async fn run( + self, launcher_paths: LauncherPaths, handle: impl 'static + ServiceContainer, ) -> Result<(), AnyError> { @@ -149,7 +151,7 @@ impl CliServiceManager for WindowsService { .map_err(|e| wrap(e, "error starting service dispatcher").into()) } - fn unregister(&self) -> Result<(), AnyError> { + async fn unregister(&self) -> Result<(), AnyError> { let service_manager = ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::CONNECT) .map_err(|e| wrap(e, "error getting service manager"))?; @@ -214,7 +216,9 @@ fn service_main(_arguments: Vec) -> Result<(), AnyError> { match control_event { ServiceControl::Interrogate => ServiceControlHandlerResult::NoError, ServiceControl::Stop => { - shutdown_tx.take().and_then(|tx| tx.blocking_send(ShutdownSignal::ServiceStopped).ok()); + shutdown_tx + .take() + .and_then(|tx| tx.blocking_send(ShutdownSignal::ServiceStopped).ok()); ServiceControlHandlerResult::NoError } _ => ServiceControlHandlerResult::NotImplemented, diff --git a/extensions/markdown-language-features/server/package.json b/extensions/markdown-language-features/server/package.json index a226de6c552..5c88971877d 100644 --- a/extensions/markdown-language-features/server/package.json +++ b/extensions/markdown-language-features/server/package.json @@ -14,11 +14,11 @@ "out/**/*.js" ], "dependencies": { + "@vscode/l10n": "^0.0.10", "vscode-languageserver": "^8.0.2", "vscode-languageserver-textdocument": "^1.0.5", "vscode-languageserver-types": "^3.17.1", "vscode-markdown-languageservice": "^0.2.0", - "vscode-nls": "^5.2.0", "vscode-uri": "^3.0.3" }, "devDependencies": { diff --git a/extensions/markdown-language-features/server/src/server.ts b/extensions/markdown-language-features/server/src/server.ts index a9dbfe940f3..3711832b1b6 100644 --- a/extensions/markdown-language-features/server/src/server.ts +++ b/extensions/markdown-language-features/server/src/server.ts @@ -7,7 +7,6 @@ import { CancellationToken, Connection, InitializeParams, InitializeResult, Note import { TextDocument } from 'vscode-languageserver-textdocument'; import * as lsp from 'vscode-languageserver-types'; import * as md from 'vscode-markdown-languageservice'; -import * as nls from 'vscode-nls'; import { URI } from 'vscode-uri'; import { getLsConfiguration, LsConfiguration } from './config'; import { ConfigurationManager } from './configuration'; @@ -16,8 +15,7 @@ import { LogFunctionLogger } from './logging'; import * as protocol from './protocol'; import { IDisposable } from './util/dispose'; import { VsCodeClientWorkspace } from './workspace'; - -const localize = nls.loadMessageBundle(); +import * as l10n from '@vscode/l10n'; interface MdServerInitializationOptions extends LsConfiguration { } @@ -204,7 +202,7 @@ export async function startServer(connection: Connection, serverConfig: { if (params.context.only?.some(kind => kind === 'source' || kind.startsWith('source.'))) { const action: lsp.CodeAction = { - title: localize('organizeLinkDefAction.title', "Organize link definitions"), + title: l10n.t("Organize link definitions"), kind: organizeLinkDefKind, data: { uri: document.uri } }; diff --git a/extensions/markdown-language-features/server/yarn.lock b/extensions/markdown-language-features/server/yarn.lock index 11e2e969f7e..fd29d323881 100644 --- a/extensions/markdown-language-features/server/yarn.lock +++ b/extensions/markdown-language-features/server/yarn.lock @@ -7,6 +7,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.59.tgz#823f238b9063ccc3b3b7f13186f143a57926c4f6" integrity sha512-6u+36Dj3aDzhfBVUf/mfmc92OEdzQ2kx2jcXGdigfl70E/neV21ZHE6UCz4MDzTRcVqGAM27fk+DLXvyDsn3Jw== +"@vscode/l10n@^0.0.10": + version "0.0.10" + resolved "https://registry.yarnpkg.com/@vscode/l10n/-/l10n-0.0.10.tgz#9c513107c690c0dd16e3ec61e453743de15ebdb0" + integrity sha512-E1OCmDcDWa0Ya7vtSjp/XfHFGqYJfh+YPC1RkATU71fTac+j1JjCcB3qwSzmlKAighx2WxhLlfhS0RwAN++PFQ== + picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" @@ -53,7 +58,7 @@ vscode-markdown-languageservice@^0.2.0: vscode-nls "^5.0.1" vscode-uri "^3.0.3" -vscode-nls@^5.0.1, vscode-nls@^5.2.0: +vscode-nls@^5.0.1: version "5.2.0" resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-5.2.0.tgz#3cb6893dd9bd695244d8a024bdf746eea665cc3f" integrity sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng== diff --git a/extensions/package.json b/extensions/package.json index ab0a515dacf..b9739529518 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -10,8 +10,8 @@ "postinstall": "node ./postinstall.mjs" }, "devDependencies": { - "@parcel/watcher": "2.0.5", - "esbuild": "^0.11.12", + "@parcel/watcher": "2.0.7", + "esbuild": "^0.15.14", "vscode-grammar-updater": "^1.1.0" } } diff --git a/extensions/typescript-language-features/.eslintrc.js b/extensions/typescript-language-features/.eslintrc.js new file mode 100644 index 00000000000..e910c2f2510 --- /dev/null +++ b/extensions/typescript-language-features/.eslintrc.js @@ -0,0 +1,10 @@ +module.exports = { + "parserOptions": { + "tsconfigRootDir": __dirname, + "project": "./tsconfig.json" + }, + "rules": { + "@typescript-eslint/prefer-optional-chain": "warn", + "@typescript-eslint/prefer-readonly": "warn" + } +}; diff --git a/extensions/typescript-language-features/.eslintrc.json b/extensions/typescript-language-features/.eslintrc.json deleted file mode 100644 index f7c6c1b495b..00000000000 --- a/extensions/typescript-language-features/.eslintrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "rules": { - "@typescript-eslint/prefer-optional-chain": "warn" - } -} diff --git a/extensions/typescript-language-features/extension-browser.webpack.config.js b/extensions/typescript-language-features/extension-browser.webpack.config.js index 9cbf6903725..c931b906965 100644 --- a/extensions/typescript-language-features/extension-browser.webpack.config.js +++ b/extensions/typescript-language-features/extension-browser.webpack.config.js @@ -63,7 +63,10 @@ module.exports = [withBrowserDefaults({ entry: { 'typescript/tsserver.web': './web/webServer.ts' }, - ignoreWarnings: [/Critical dependency: the request of a dependency is an expression/], + module: { + exprContextCritical: false, + }, + ignoreWarnings: [/Critical dependency: the request of a dependency is an expression/], output: { // all output goes into `dist`. // packaging depends on that and this must always be like it diff --git a/extensions/typescript-language-features/package.json b/extensions/typescript-language-features/package.json index 5fd6f84c662..2ddaf3d6a1c 100644 --- a/extensions/typescript-language-features/package.json +++ b/extensions/typescript-language-features/package.json @@ -36,7 +36,7 @@ ], "dependencies": { "@vscode/extension-telemetry": "0.7.1-preview", - "jsonc-parser": "^2.2.1", + "jsonc-parser": "^3.2.0", "semver": "5.5.1", "vscode-tas-client": "^0.1.63", "vscode-uri": "^3.0.3" diff --git a/extensions/typescript-language-features/src/experimentTelemetryReporter.ts b/extensions/typescript-language-features/src/experimentTelemetryReporter.ts index d7561300761..ddd643fbc88 100644 --- a/extensions/typescript-language-features/src/experimentTelemetryReporter.ts +++ b/extensions/typescript-language-features/src/experimentTelemetryReporter.ts @@ -16,10 +16,11 @@ export interface IExperimentationTelemetryReporter extends tas.IExperimentationT * but will only do so when passed to an {@link ExperimentationService}. */ -export class ExperimentationTelemetryReporter - implements IExperimentationTelemetryReporter { +export class ExperimentationTelemetryReporter implements IExperimentationTelemetryReporter { + private _sharedProperties: Record = {}; - private _reporter: VsCodeTelemetryReporter; + private readonly _reporter: VsCodeTelemetryReporter; + constructor(reporter: VsCodeTelemetryReporter) { this._reporter = reporter; } diff --git a/extensions/typescript-language-features/src/experimentationService.ts b/extensions/typescript-language-features/src/experimentationService.ts index 1f3b1fbd73b..86d1fd42b55 100644 --- a/extensions/typescript-language-features/src/experimentationService.ts +++ b/extensions/typescript-language-features/src/experimentationService.ts @@ -13,8 +13,8 @@ interface ExperimentTypes { } export class ExperimentationService { - private _experimentationServicePromise: Promise; - private _telemetryReporter: IExperimentationTelemetryReporter; + private readonly _experimentationServicePromise: Promise; + private readonly _telemetryReporter: IExperimentationTelemetryReporter; constructor(telemetryReporter: IExperimentationTelemetryReporter, id: string, version: string, globalState: vscode.Memento) { this._telemetryReporter = telemetryReporter; diff --git a/extensions/typescript-language-features/src/languageFeatures/codeLens/baseCodeLensProvider.ts b/extensions/typescript-language-features/src/languageFeatures/codeLens/baseCodeLensProvider.ts index 74377b29f74..9605eb01693 100644 --- a/extensions/typescript-language-features/src/languageFeatures/codeLens/baseCodeLensProvider.ts +++ b/extensions/typescript-language-features/src/languageFeatures/codeLens/baseCodeLensProvider.ts @@ -36,7 +36,7 @@ export abstract class TypeScriptBaseCodeLensProvider implements vscode.CodeLensP public constructor( protected client: ITypeScriptServiceClient, - private cachedResponse: CachedResponse + private readonly cachedResponse: CachedResponse ) { } diff --git a/extensions/typescript-language-features/src/languageFeatures/documentSymbol.ts b/extensions/typescript-language-features/src/languageFeatures/documentSymbol.ts index 0a17b83678b..1513229529d 100644 --- a/extensions/typescript-language-features/src/languageFeatures/documentSymbol.ts +++ b/extensions/typescript-language-features/src/languageFeatures/documentSymbol.ts @@ -37,7 +37,7 @@ class TypeScriptDocumentSymbolProvider implements vscode.DocumentSymbolProvider public constructor( private readonly client: ITypeScriptServiceClient, - private cachedResponse: CachedResponse, + private readonly cachedResponse: CachedResponse, ) { } public async provideDocumentSymbols(document: vscode.TextDocument, token: vscode.CancellationToken): Promise { diff --git a/extensions/typescript-language-features/src/languageFeatures/fixAll.ts b/extensions/typescript-language-features/src/languageFeatures/fixAll.ts index 1fad8f4eb61..fa64f520ad8 100644 --- a/extensions/typescript-language-features/src/languageFeatures/fixAll.ts +++ b/extensions/typescript-language-features/src/languageFeatures/fixAll.ts @@ -185,7 +185,7 @@ class SourceAddMissingImports extends SourceAction { class TypeScriptAutoFixProvider implements vscode.CodeActionProvider { - private static kindProviders = [ + private static readonly kindProviders = [ SourceFixAll, SourceRemoveUnused, SourceAddMissingImports, diff --git a/extensions/typescript-language-features/src/protocol.d.ts b/extensions/typescript-language-features/src/protocol.d.ts index 1aec9e082ed..38345971fc8 100644 --- a/extensions/typescript-language-features/src/protocol.d.ts +++ b/extensions/typescript-language-features/src/protocol.d.ts @@ -1,3 +1,7 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ import * as ts from 'typescript/lib/tsserverlibrary'; export = ts.server.protocol; diff --git a/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts b/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts index 5fdbbb6d2f7..302fe2bbb30 100644 --- a/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts +++ b/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts @@ -34,9 +34,9 @@ export class WorkerServerProcess implements TsServerProcess { ]); } - private _onDataHandlers = new Set<(data: Proto.Response) => void>(); - private _onErrorHandlers = new Set<(err: Error) => void>(); - private _onExitHandlers = new Set<(code: number | null, signal: string | null) => void>(); + private readonly _onDataHandlers = new Set<(data: Proto.Response) => void>(); + private readonly _onErrorHandlers = new Set<(err: Error) => void>(); + private readonly _onExitHandlers = new Set<(code: number | null, signal: string | null) => void>(); public constructor( private readonly worker: Worker, diff --git a/extensions/typescript-language-features/src/typescriptServiceClient.ts b/extensions/typescript-language-features/src/typescriptServiceClient.ts index 012942c3c55..99b263365ec 100644 --- a/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -99,9 +99,9 @@ export default class TypeScriptServiceClient extends Disposable implements IType private readonly workspaceState: vscode.Memento; - private _onReady?: { promise: Promise; resolve: () => void; reject: () => void }; + private readonly _onReady?: { promise: Promise; resolve: () => void; reject: () => void }; private _configuration: TypeScriptServiceConfiguration; - private pluginPathsProvider: TypeScriptPluginPathsProvider; + private readonly pluginPathsProvider: TypeScriptPluginPathsProvider; private readonly _versionManager: TypeScriptVersionManager; private readonly logger = new Logger(); diff --git a/extensions/typescript-language-features/src/ui/jsNodeWalkthrough.electron.ts b/extensions/typescript-language-features/src/ui/jsNodeWalkthrough.electron.ts index 854c2ff3c33..a1a6bd2ac2d 100644 --- a/extensions/typescript-language-features/src/ui/jsNodeWalkthrough.electron.ts +++ b/extensions/typescript-language-features/src/ui/jsNodeWalkthrough.electron.ts @@ -50,7 +50,9 @@ export class CreateNewJSFileCommand { public static readonly id = 'javascript-walkthrough.commands.createJsFile'; public readonly id = CreateNewJSFileCommand.id; - constructor(private walkthroughState: JsWalkthroughState) { } + constructor( + private readonly walkthroughState: JsWalkthroughState + ) { } public execute() { createNewJSFile(this.walkthroughState); @@ -61,7 +63,9 @@ export class DebugJsFileCommand { public static readonly id = 'javascript-walkthrough.commands.debugJsFile'; public readonly id = DebugJsFileCommand.id; - constructor(private walkthroughState: JsWalkthroughState) { } + constructor( + private readonly walkthroughState: JsWalkthroughState + ) { } public execute() { debugJsFile(this.walkthroughState); diff --git a/extensions/typescript-language-features/yarn.lock b/extensions/typescript-language-features/yarn.lock index ad0e264a688..c765571b9fa 100644 --- a/extensions/typescript-language-features/yarn.lock +++ b/extensions/typescript-language-features/yarn.lock @@ -320,10 +320,10 @@ form-data@^4.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" -jsonc-parser@^2.2.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-2.3.1.tgz#59549150b133f2efacca48fe9ce1ec0659af2342" - integrity sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg== +jsonc-parser@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76" + integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w== mime-db@1.52.0: version "1.52.0" diff --git a/extensions/yarn.lock b/extensions/yarn.lock index 9d6fa08317e..8cd7aaf26af 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -2,10 +2,20 @@ # yarn lockfile v1 -"@parcel/watcher@2.0.5": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.0.5.tgz#f913a54e1601b0aac972803829b0eece48de215b" - integrity sha512-x0hUbjv891omnkcHD7ZOhiyyUqUUR6MNjq89JhEI3BxppeKWAm6NPQsqqRrAkCJBogdT/o/My21sXtTI9rJIsw== +"@esbuild/android-arm@0.15.14": + version "0.15.14" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.15.14.tgz#5d0027f920eeeac313c01fd6ecb8af50c306a466" + integrity sha512-+Rb20XXxRGisNu2WmNKk+scpanb7nL5yhuI1KR9wQFiC43ddPj/V1fmNyzlFC9bKiG4mYzxW7egtoHVcynr+OA== + +"@esbuild/linux-loong64@0.15.14": + version "0.15.14" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.15.14.tgz#1221684955c44385f8af34f7240088b7dc08d19d" + integrity sha512-eQi9rosGNVQFJyJWV0HCA5WZae/qWIQME7s8/j8DMvnylfBv62Pbu+zJ2eUDqNf2O4u3WB+OEXyfkpBoe194sg== + +"@parcel/watcher@2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.0.7.tgz#c95fe1370e8c6237cb9729c9c075264acc7e21a5" + integrity sha512-gc3hoS6e+2XdIQ4HHljDB1l0Yx2EWh/sBBtCEFNKGSMlwASWeAQsOY/fPbxOBcZ/pg0jBh4Ga+4xHlZc4faAEQ== dependencies: node-addon-api "^3.2.1" node-gyp-build "^4.3.0" @@ -22,10 +32,133 @@ cson-parser@^4.0.9: dependencies: coffeescript "1.12.7" -esbuild@^0.11.12: - version "0.11.23" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.11.23.tgz#c42534f632e165120671d64db67883634333b4b8" - integrity sha512-iaiZZ9vUF5wJV8ob1tl+5aJTrwDczlvGP0JoMmnpC2B0ppiMCu8n8gmy5ZTGl5bcG081XBVn+U+jP+mPFm5T5Q== +esbuild-android-64@0.15.14: + version "0.15.14" + resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.15.14.tgz#114e55b0d58fb7b45d7fa3d93516bd13fc8869cc" + integrity sha512-HuilVIb4rk9abT4U6bcFdU35UHOzcWVGLSjEmC58OVr96q5UiRqzDtWjPlCMugjhgUGKEs8Zf4ueIvYbOStbIg== + +esbuild-android-arm64@0.15.14: + version "0.15.14" + resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.15.14.tgz#8541f38a9aacf88e574fb13f5ad4ca51a04c12bb" + integrity sha512-/QnxRVxsR2Vtf3XottAHj7hENAMW2wCs6S+OZcAbc/8nlhbAL/bCQRCVD78VtI5mdwqWkVi3wMqM94kScQCgqg== + +esbuild-darwin-64@0.15.14: + version "0.15.14" + resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.15.14.tgz#b40b334db81ff1e3677a6712b23761748a157c57" + integrity sha512-ToNuf1uifu8hhwWvoZJGCdLIX/1zpo8cOGnT0XAhDQXiKOKYaotVNx7pOVB1f+wHoWwTLInrOmh3EmA7Fd+8Vg== + +esbuild-darwin-arm64@0.15.14: + version "0.15.14" + resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.14.tgz#44b5c1477bb7bdb852dd905e906f68765e2828bc" + integrity sha512-KgGP+y77GszfYJgceO0Wi/PiRtYo5y2Xo9rhBUpxTPaBgWDJ14gqYN0+NMbu+qC2fykxXaipHxN4Scaj9tUS1A== + +esbuild-freebsd-64@0.15.14: + version "0.15.14" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.14.tgz#8c57315d238690f34b6ed0c94e5cfc04c858247a" + integrity sha512-xr0E2n5lyWw3uFSwwUXHc0EcaBDtsal/iIfLioflHdhAe10KSctV978Te7YsfnsMKzcoGeS366+tqbCXdqDHQA== + +esbuild-freebsd-arm64@0.15.14: + version "0.15.14" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.14.tgz#2e92acca09258daa849e635565f52469266f0b7b" + integrity sha512-8XH96sOQ4b1LhMlO10eEWOjEngmZ2oyw3pW4o8kvBcpF6pULr56eeYVP5radtgw54g3T8nKHDHYEI5AItvskZg== + +esbuild-linux-32@0.15.14: + version "0.15.14" + resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.15.14.tgz#ca5ed3e9dff82df486ddde362d7e00775a597dfd" + integrity sha512-6ssnvwaTAi8AzKN8By2V0nS+WF5jTP7SfuK6sStGnDP7MCJo/4zHgM9oE1eQTS2jPmo3D673rckuCzRlig+HMA== + +esbuild-linux-64@0.15.14: + version "0.15.14" + resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.15.14.tgz#42952e1d08a299d5f573c567639fb37b033befbf" + integrity sha512-ONySx3U0wAJOJuxGUlXBWxVKFVpWv88JEv0NZ6NlHknmDd1yCbf4AEdClSgLrqKQDXYywmw4gYDvdLsS6z0hcw== + +esbuild-linux-arm64@0.15.14: + version "0.15.14" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.14.tgz#0c0d788099703327ec0ae70758cb2639ef6c5d88" + integrity sha512-kle2Ov6a1e5AjlHlMQl1e+c4myGTeggrRzArQFmWp6O6JoqqB9hT+B28EW4tjFWgV/NxUq46pWYpgaWXsXRPAg== + +esbuild-linux-arm@0.15.14: + version "0.15.14" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.15.14.tgz#751a5ca5042cd60f669b07c3bcec3dd6c4f8151c" + integrity sha512-D2LImAIV3QzL7lHURyCHBkycVFbKwkDb1XEUWan+2fb4qfW7qAeUtul7ZIcIwFKZgPcl+6gKZmvLgPSj26RQ2Q== + +esbuild-linux-mips64le@0.15.14: + version "0.15.14" + resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.14.tgz#da8ac35f2704de0b52bf53a99c12f604fbe9b916" + integrity sha512-FVdMYIzOLXUq+OE7XYKesuEAqZhmAIV6qOoYahvUp93oXy0MOVTP370ECbPfGXXUdlvc0TNgkJa3YhEwyZ6MRA== + +esbuild-linux-ppc64le@0.15.14: + version "0.15.14" + resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.14.tgz#a315b5016917429080c3d32e03319f1ff876ac55" + integrity sha512-2NzH+iuzMDA+jjtPjuIz/OhRDf8tzbQ1tRZJI//aT25o1HKc0reMMXxKIYq/8nSHXiJSnYV4ODzTiv45s+h73w== + +esbuild-linux-riscv64@0.15.14: + version "0.15.14" + resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.14.tgz#9f2e0a935e5086d398fc19c7ff5d217bfefe3e12" + integrity sha512-VqxvutZNlQxmUNS7Ac+aczttLEoHBJ9e3OYGqnULrfipRvG97qLrAv9EUY9iSrRKBqeEbSvS9bSfstZqwz0T4Q== + +esbuild-linux-s390x@0.15.14: + version "0.15.14" + resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.14.tgz#53108112faff5a4e1bad17f7b0b0ffa1df4b7efb" + integrity sha512-+KVHEUshX5n6VP6Vp/AKv9fZIl5kr2ph8EUFmQUJnDpHwcfTSn2AQgYYm0HTBR2Mr4d0Wlr0FxF/Cs5pbFgiOw== + +esbuild-netbsd-64@0.15.14: + version "0.15.14" + resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.14.tgz#5330efc41fe4f1c2bab5462bcfe7a4ffce7ba00a" + integrity sha512-6D/dr17piEgevIm1xJfZP2SjB9Z+g8ERhNnBdlZPBWZl+KSPUKLGF13AbvC+nzGh8IxOH2TyTIdRMvKMP0nEzQ== + +esbuild-openbsd-64@0.15.14: + version "0.15.14" + resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.14.tgz#ee64944d863e937611fc31adf349e9bb4f5f7eac" + integrity sha512-rREQBIlMibBetgr2E9Lywt2Qxv2ZdpmYahR4IUlAQ1Efv/A5gYdO0/VIN3iowDbCNTLxp0bb57Vf0LFcffD6kA== + +esbuild-sunos-64@0.15.14: + version "0.15.14" + resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.15.14.tgz#29b0b20de6fe6ef50f9fbe533ec20dc4b595f9aa" + integrity sha512-DNVjSp/BY4IfwtdUAvWGIDaIjJXY5KI4uD82+15v6k/w7px9dnaDaJJ2R6Mu+KCgr5oklmFc0KjBjh311Gxl9Q== + +esbuild-windows-32@0.15.14: + version "0.15.14" + resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.15.14.tgz#05e9b159d664809f7a4a8a68ed048d193457b27d" + integrity sha512-pHBWrcA+/oLgvViuG9FO3kNPO635gkoVrRQwe6ZY1S0jdET07xe2toUvQoJQ8KT3/OkxqUasIty5hpuKFLD+eg== + +esbuild-windows-64@0.15.14: + version "0.15.14" + resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.15.14.tgz#d5ae086728ab30b72969e40ed0a7a0d9082f2cdd" + integrity sha512-CszIGQVk/P8FOS5UgAH4hKc9zOaFo69fe+k1rqgBHx3CSK3Opyk5lwYriIamaWOVjBt7IwEP6NALz+tkVWdFog== + +esbuild-windows-arm64@0.15.14: + version "0.15.14" + resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.14.tgz#8eb50ab9a0ecaf058593fbad17502749306f801d" + integrity sha512-KW9W4psdZceaS9A7Jsgl4WialOznSURvqX/oHZk3gOP7KbjtHLSsnmSvNdzagGJfxbAe30UVGXRe8q8nDsOSQw== + +esbuild@^0.15.14: + version "0.15.14" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.15.14.tgz#09202b811f1710363d5088a3401a351351c79875" + integrity sha512-pJN8j42fvWLFWwSMG4luuupl2Me7mxciUOsMegKvwCmhEbJ2covUdFnihxm0FMIBV+cbwbtMoHgMCCI+pj1btQ== + optionalDependencies: + "@esbuild/android-arm" "0.15.14" + "@esbuild/linux-loong64" "0.15.14" + esbuild-android-64 "0.15.14" + esbuild-android-arm64 "0.15.14" + esbuild-darwin-64 "0.15.14" + esbuild-darwin-arm64 "0.15.14" + esbuild-freebsd-64 "0.15.14" + esbuild-freebsd-arm64 "0.15.14" + esbuild-linux-32 "0.15.14" + esbuild-linux-64 "0.15.14" + esbuild-linux-arm "0.15.14" + esbuild-linux-arm64 "0.15.14" + esbuild-linux-mips64le "0.15.14" + esbuild-linux-ppc64le "0.15.14" + esbuild-linux-riscv64 "0.15.14" + esbuild-linux-s390x "0.15.14" + esbuild-netbsd-64 "0.15.14" + esbuild-openbsd-64 "0.15.14" + esbuild-sunos-64 "0.15.14" + esbuild-windows-32 "0.15.14" + esbuild-windows-64 "0.15.14" + esbuild-windows-arm64 "0.15.14" fast-plist@0.1.2: version "0.1.2" diff --git a/package.json b/package.json index b2a39f3b540..bc9c5b72f46 100644 --- a/package.json +++ b/package.json @@ -140,7 +140,7 @@ "css-loader": "^3.6.0", "cssnano": "^4.1.11", "debounce": "^1.0.0", - "deemon": "^1.7.2", + "deemon": "^1.8.0", "electron": "19.1.3", "eslint": "8.7.0", "eslint-plugin-header": "3.1.1", diff --git a/src/vs/base/browser/ui/button/button.ts b/src/vs/base/browser/ui/button/button.ts index 5c750408f71..0d28e607144 100644 --- a/src/vs/base/browser/ui/button/button.ts +++ b/src/vs/base/browser/ui/button/button.ts @@ -23,20 +23,19 @@ export interface IButtonOptions extends IButtonStyles { readonly secondary?: boolean; } -export type CSSValueString = string; export interface IButtonStyles { - readonly buttonBackground?: CSSValueString; - readonly buttonHoverBackground?: CSSValueString; - readonly buttonForeground?: CSSValueString; - readonly buttonSeparator?: CSSValueString; - readonly buttonSecondaryBackground?: CSSValueString; - readonly buttonSecondaryHoverBackground?: CSSValueString; - readonly buttonSecondaryForeground?: CSSValueString; - readonly buttonBorder?: CSSValueString; + readonly buttonBackground: string | undefined; + readonly buttonHoverBackground: string | undefined; + readonly buttonForeground: string | undefined; + readonly buttonSeparator: string | undefined; + readonly buttonSecondaryBackground: string | undefined; + readonly buttonSecondaryHoverBackground: string | undefined; + readonly buttonSecondaryForeground: string | undefined; + readonly buttonBorder: string | undefined; } -export const defaultOptions: IButtonStyles = { +export const unthemedButtonStyles: IButtonStyles = { buttonBackground: '#0E639C', buttonHoverBackground: '#006BB3', buttonSeparator: Color.white.toString(), @@ -255,16 +254,11 @@ export class ButtonWithDropdown extends Disposable implements IButton { // Separator styles const border = options.buttonBorder; if (border) { - this.separatorContainer.style.borderTopWidth = '1px'; - this.separatorContainer.style.borderTopStyle = 'solid'; - this.separatorContainer.style.borderTopColor = border; - - this.separatorContainer.style.borderBottomWidth = '1px'; - this.separatorContainer.style.borderBottomStyle = 'solid'; - this.separatorContainer.style.borderBottomColor = border; + this.separatorContainer.style.borderTop = '1px solid ' + border; + this.separatorContainer.style.borderBottom = '1px solid ' + border; } - this.separatorContainer.style.backgroundColor = options.buttonBackground?.toString() ?? ''; - this.separator.style.backgroundColor = options.buttonSeparator?.toString() ?? ''; + this.separatorContainer.style.backgroundColor = options.buttonBackground ?? ''; + this.separator.style.backgroundColor = options.buttonSeparator ?? ''; this.dropdownButton = this._register(new Button(this.element, { ...options, title: false, supportIcons: true })); diff --git a/src/vs/base/browser/ui/dialog/dialog.ts b/src/vs/base/browser/ui/dialog/dialog.ts index f9263782278..3ec1b02ca80 100644 --- a/src/vs/base/browser/ui/dialog/dialog.ts +++ b/src/vs/base/browser/ui/dialog/dialog.ts @@ -38,6 +38,7 @@ export interface IDialogOptions { readonly buttonDetails?: string[]; readonly disableCloseAction?: boolean; readonly disableDefaultAction?: boolean; + readonly buttonStyles: IButtonStyles; } export interface IDialogResult { @@ -46,7 +47,7 @@ export interface IDialogResult { readonly values?: string[]; } -export interface IDialogStyles extends IButtonStyles, ICheckboxStyles { +export interface IDialogStyles extends ICheckboxStyles { readonly dialogForeground?: Color; readonly dialogBackground?: Color; readonly dialogShadow?: Color; @@ -81,8 +82,9 @@ export class Dialog extends Disposable { private focusToReturn: HTMLElement | undefined; private readonly inputs: InputBox[]; private readonly buttons: string[]; + private readonly buttonStyles: IButtonStyles; - constructor(private container: HTMLElement, private message: string, buttons: string[] | undefined, private options: IDialogOptions) { + constructor(private container: HTMLElement, private message: string, buttons: string[] | undefined, private readonly options: IDialogOptions) { super(); this.modalElement = this.container.appendChild($(`.monaco-dialog-modal-block.dimmed`)); @@ -92,6 +94,8 @@ export class Dialog extends Disposable { this.element.tabIndex = -1; hide(this.element); + this.buttonStyles = options.buttonStyles; + if (Array.isArray(buttons) && buttons.length > 0) { this.buttons = buttons; } else if (!this.options.disableDefaultAction) { @@ -202,7 +206,7 @@ export class Dialog extends Disposable { // Handle button clicks buttonMap.forEach((entry, index) => { const primary = buttonMap[index].index === 0; - const button = this.options.buttonDetails ? this._register(buttonBar.addButtonWithDescription({ title: true, secondary: !primary })) : this._register(buttonBar.addButton({ title: true, secondary: !primary })); + const button = this.options.buttonDetails ? this._register(buttonBar.addButtonWithDescription({ title: true, secondary: !primary, ...this.buttonStyles })) : this._register(buttonBar.addButton({ title: true, secondary: !primary, ...this.buttonStyles })); button.label = mnemonicButtonLabel(buttonMap[index].label, true); if (button instanceof ButtonWithDescription) { button.description = this.options.buttonDetails![buttonMap[index].index]; diff --git a/src/vs/base/common/diff/diff.ts b/src/vs/base/common/diff/diff.ts index f97efa3baa3..0a938b65864 100644 --- a/src/vs/base/common/diff/diff.ts +++ b/src/vs/base/common/diff/diff.ts @@ -69,7 +69,7 @@ export interface IDiffResult { // The code below has been ported from a C# implementation in VS // -export class Debug { +class Debug { public static Assert(condition: boolean, message: string): void { if (!condition) { @@ -78,7 +78,7 @@ export class Debug { } } -export class MyArray { +class MyArray { /** * Copies a range of elements from an Array starting at the specified source index and pastes * them to another Array starting at the specified destination index. The length and the indexes diff --git a/src/vs/base/parts/quickinput/test/browser/quickinput.test.ts b/src/vs/base/parts/quickinput/test/browser/quickinput.test.ts index dc99756b291..322b6f48b80 100644 --- a/src/vs/base/parts/quickinput/test/browser/quickinput.test.ts +++ b/src/vs/base/parts/quickinput/test/browser/quickinput.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { unthemedButtonStyles } from 'vs/base/browser/ui/button/button'; import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IListOptions, List } from 'vs/base/browser/ui/list/listWidget'; import { raceTimeout } from 'vs/base/common/async'; @@ -52,7 +53,7 @@ suite('QuickInput', () => { // https://github.com/microsoft/vscode/issues/147543 options: IListOptions, ) => new List(user, container, delegate, renderers, options), styles: { - button: {}, + button: unthemedButtonStyles, countBadge: {}, inputBox: {}, keybindingLabel: {}, diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 8c904e25308..da9ec060539 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -80,8 +80,8 @@ import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyn import { UserDataSyncChannel } from 'vs/platform/userDataSync/common/userDataSyncServiceIpc'; import { UserDataSyncStoreManagementService, UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; import { UserDataAutoSyncService } from 'vs/platform/userDataSync/electron-sandbox/userDataAutoSyncService'; -import { UserDataSyncProfilesStorageService } from 'vs/platform/userDataSync/electron-sandbox/userDataSyncProfilesStorageService'; -import { IUserDataSyncProfilesStorageService } from 'vs/platform/userDataSync/common/userDataSyncProfilesStorageService'; +import { UserDataProfileStorageService } from 'vs/platform/userDataProfile/electron-sandbox/userDataProfileStorageService'; +import { IUserDataProfileStorageService } from 'vs/platform/userDataProfile/common/userDataProfileStorageService'; import { ActiveWindowManager } from 'vs/platform/windows/node/windowTracker'; import { ISignService } from 'vs/platform/sign/common/sign'; import { SignService } from 'vs/platform/sign/node/signService'; @@ -361,7 +361,7 @@ class SharedProcessMain extends Disposable { services.set(IUserDataSyncBackupStoreService, new SyncDescriptor(UserDataSyncBackupStoreService, undefined, false /* Eagerly cleans up old backups */)); services.set(IUserDataSyncEnablementService, new SyncDescriptor(UserDataSyncEnablementService, undefined, true)); services.set(IUserDataSyncService, new SyncDescriptor(UserDataSyncService, undefined, false /* Initializes the Sync State */)); - services.set(IUserDataSyncProfilesStorageService, new SyncDescriptor(UserDataSyncProfilesStorageService, undefined, true)); + services.set(IUserDataProfileStorageService, new SyncDescriptor(UserDataProfileStorageService, undefined, true)); services.set(IUserDataSyncResourceProviderService, new SyncDescriptor(UserDataSyncResourceProviderService, undefined, true)); // Terminal diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 63d0b154039..be68de2916f 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -107,7 +107,7 @@ import { ExtensionsProfileScannerService, IExtensionsProfileScannerService } fro import { IExtensionsScannerService } from 'vs/platform/extensionManagement/common/extensionsScannerService'; import { ExtensionsScannerService } from 'vs/platform/extensionManagement/node/extensionsScannerService'; import { UserDataTransientProfilesHandler } from 'vs/platform/userDataProfile/electron-main/userDataTransientProfilesHandler'; -import { ProfileStorageChangesListenerChannel } from 'vs/platform/userDataSync/electron-main/userDataSyncProfilesStorageIpc'; +import { ProfileStorageChangesListenerChannel } from 'vs/platform/userDataProfile/electron-main/userDataProfileStorageIpc'; import { Promises, RunOnceScheduler, runWhenIdle } from 'vs/base/common/async'; /** diff --git a/src/vs/code/electron-sandbox/issue/issueReporterMain.ts b/src/vs/code/electron-sandbox/issue/issueReporterMain.ts index 442e4c4d2f7..8936cda23bd 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporterMain.ts +++ b/src/vs/code/electron-sandbox/issue/issueReporterMain.ts @@ -7,7 +7,7 @@ import 'vs/css!./media/issueReporter'; import 'vs/base/browser/ui/codicons/codiconStyles'; // make sure codicon css is loaded import { localize } from 'vs/nls'; import { $, reset, safeInnerHtml, windowOpenNoOpener } from 'vs/base/browser/dom'; -import { Button, defaultOptions } from 'vs/base/browser/ui/button/button'; +import { Button, unthemedButtonStyles } from 'vs/base/browser/ui/button/button'; import { renderIcon } from 'vs/base/browser/ui/iconLabel/iconLabels'; import { Delayer } from 'vs/base/common/async'; import { Codicon } from 'vs/base/common/codicons'; @@ -88,7 +88,7 @@ export class IssueReporter extends Disposable { const issueReporterElement = this.getElementById('issue-reporter'); if (issueReporterElement) { - this.previewButton = new Button(issueReporterElement, defaultOptions); + this.previewButton = new Button(issueReporterElement, unthemedButtonStyles); this.updatePreviewButtonState(); } diff --git a/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.css b/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.css index 02def4da31f..774ffef273d 100644 --- a/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.css +++ b/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.css @@ -23,3 +23,11 @@ .monaco-editor .margin-view-overlays .line-numbers.lh-odd { margin-top: 1px; } + +.monaco-editor .line-numbers { + color: var(--vscode-editorLineNumber-foreground); +} + +.monaco-editor .line-numbers.active-line-number { + color: var(--vscode-editorLineNumber-activeForeground); +} diff --git a/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts b/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts index 3f5d8862f26..6b0f1277a2e 100644 --- a/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts +++ b/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.ts @@ -8,11 +8,9 @@ import * as platform from 'vs/base/common/platform'; import { DynamicViewOverlay } from 'vs/editor/browser/view/dynamicViewOverlay'; import { RenderLineNumbersType, EditorOption } from 'vs/editor/common/config/editorOptions'; import { Position } from 'vs/editor/common/core/position'; -import { editorActiveLineNumber, editorLineNumbers } from 'vs/editor/common/core/editorColorRegistry'; import { RenderingContext } from 'vs/editor/browser/view/renderingContext'; import { ViewContext } from 'vs/editor/common/viewModel/viewContext'; import * as viewEvents from 'vs/editor/common/viewEvents'; -import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; export class LineNumbersOverlay extends DynamicViewOverlay { @@ -192,16 +190,3 @@ export class LineNumbersOverlay extends DynamicViewOverlay { return this._renderResult[lineIndex]; } } - -// theming - -registerThemingParticipant((theme, collector) => { - const lineNumbers = theme.getColor(editorLineNumbers); - if (lineNumbers) { - collector.addRule(`.monaco-editor .line-numbers { color: ${lineNumbers}; }`); - } - const activeLineNumber = theme.getColor(editorActiveLineNumber); - if (activeLineNumber) { - collector.addRule(`.monaco-editor .line-numbers.active-line-number { color: ${activeLineNumber}; }`); - } -}); diff --git a/src/vs/editor/browser/viewParts/viewZones/viewZones.ts b/src/vs/editor/browser/viewParts/viewZones/viewZones.ts index 71f50e4796a..60ce7277ff2 100644 --- a/src/vs/editor/browser/viewParts/viewZones/viewZones.ts +++ b/src/vs/editor/browser/viewParts/viewZones/viewZones.ts @@ -14,7 +14,7 @@ import * as viewEvents from 'vs/editor/common/viewEvents'; import { IEditorWhitespace, IViewWhitespaceViewportData, IWhitespaceChangeAccessor } from 'vs/editor/common/viewModel'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; -export interface IMyViewZone { +interface IMyViewZone { whitespaceId: string; delegate: IViewZone; isInHiddenArea: boolean; diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/ast.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/ast.ts index e8dfd6f8f9f..cbabd157db0 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/ast.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/ast.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { BugIndicatingError } from 'vs/base/common/errors'; import { CursorColumns } from 'vs/editor/common/core/cursorColumns'; import { BracketKind } from 'vs/editor/common/languages/supports/languageBracketsConfiguration'; import { ITextModel } from 'vs/editor/common/model'; @@ -125,7 +126,7 @@ export class PairAstNode extends BaseAstNode { * Avoid using this property, it allocates an array! */ public get children() { - const result = new Array(); + const result: AstNode[] = []; result.push(this.openingBracket); if (this.child) { result.push(this.child); @@ -295,10 +296,19 @@ export abstract class ListAstNode extends BaseAstNode { return false; } + if (this.childrenLength === 0) { + // Don't reuse empty lists. + return false; + } + let lastChild: ListAstNode = this; - let lastLength: number; - while (lastChild.kind === AstNodeKind.List && (lastLength = lastChild.childrenLength) > 0) { - lastChild = lastChild.getChild(lastLength! - 1) as ListAstNode; + while (lastChild.kind === AstNodeKind.List) { + const lastLength = lastChild.childrenLength; + if (lastLength === 0) { + // Empty lists should never be contained in other lists. + throw new BugIndicatingError(); + } + lastChild = lastChild.getChild(lastLength - 1) as ListAstNode; } return lastChild.canBeReused(openBracketIds); @@ -324,7 +334,7 @@ export abstract class ListAstNode extends BaseAstNode { } public flattenLists(): ListAstNode { - const items = new Array(); + const items: AstNode[] = []; for (const c of this.children) { const normalized = c.flattenLists(); if (normalized.kind === AstNodeKind.List) { diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper.ts index 7d9e19c05d8..162509bed20 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper.ts @@ -26,7 +26,6 @@ export class BeforeEditPositionMapper { */ constructor( edits: readonly TextEditInfo[], - private readonly documentLength: Length, ) { this.edits = edits.map(edit => TextEditInfoCache.from(edit)); } @@ -41,12 +40,16 @@ export class BeforeEditPositionMapper { /** * @param offset Must be equal to or greater than the last offset this method has been called with. + * Returns null if there is no edit anymore. */ - getDistanceToNextChange(offset: Length): Length { + getDistanceToNextChange(offset: Length): Length | null { this.adjustNextEdit(offset); const nextEdit = this.edits[this.nextEditIdx]; - const nextChangeOffset = nextEdit ? this.translateOldToCur(nextEdit.offsetObj) : this.documentLength; + const nextChangeOffset = nextEdit ? this.translateOldToCur(nextEdit.offsetObj) : null; + if (nextChangeOffset === null) { + return null; + } return lengthDiffNonNegative(offset, nextChangeOffset); } diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/bracketPairsTree.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/bracketPairsTree.ts index 62d82fdd711..1a85bec9ec6 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/bracketPairsTree.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/bracketPairsTree.ts @@ -21,6 +21,7 @@ import { FastTokenizer, TextBufferTokenizer } from './tokenizer'; import { BackgroundTokenizationState } from 'vs/editor/common/tokenizationTextModelPart'; import { Position } from 'vs/editor/common/core/position'; import { CallbackIterable } from 'vs/base/common/arrays'; +import { combineTextEditInfos } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/combineTextEditInfos'; export class BracketPairsTree extends Disposable { private readonly didChangeEmitter = new Emitter(); @@ -45,6 +46,8 @@ export class BracketPairsTree extends Disposable { } public readonly onDidChange = this.didChangeEmitter.event; + private queuedTextEditsForInitialAstWithoutTokens: TextEditInfo[] = []; + private queuedTextEdits: TextEditInfo[] = []; public constructor( private readonly textModel: TextModel, @@ -90,7 +93,9 @@ export class BracketPairsTree extends Disposable { toLength(r.toLineNumber - r.fromLineNumber + 1, 0) ) ); - this.astWithTokens = this.parseDocumentFromTextBuffer(edits, this.astWithTokens, false); + + this.handleEdits(edits, true); + if (!this.initialAstWithoutTokens) { this.didChangeEmitter.fire(); } @@ -106,14 +111,34 @@ export class BracketPairsTree extends Disposable { ); }).reverse(); - this.astWithTokens = this.parseDocumentFromTextBuffer(edits, this.astWithTokens, false); - if (this.initialAstWithoutTokens) { - this.initialAstWithoutTokens = this.parseDocumentFromTextBuffer(edits, this.initialAstWithoutTokens, false); + this.handleEdits(edits, false); + } + + private handleEdits(edits: TextEditInfo[], tokenChange: boolean): void { + // Lazily queue the edits and only apply them when the tree is accessed. + const result = combineTextEditInfos(this.queuedTextEdits, edits); + + this.queuedTextEdits = result; + if (this.initialAstWithoutTokens && !tokenChange) { + this.queuedTextEditsForInitialAstWithoutTokens = combineTextEditInfos(this.queuedTextEditsForInitialAstWithoutTokens, edits); } } //#endregion + private flushQueue() { + if (this.queuedTextEdits.length > 0) { + this.astWithTokens = this.parseDocumentFromTextBuffer(this.queuedTextEdits, this.astWithTokens, false); + this.queuedTextEdits = []; + } + if (this.queuedTextEditsForInitialAstWithoutTokens.length > 0) { + if (this.initialAstWithoutTokens) { + this.initialAstWithoutTokens = this.parseDocumentFromTextBuffer(this.queuedTextEditsForInitialAstWithoutTokens, this.initialAstWithoutTokens, false); + } + this.queuedTextEditsForInitialAstWithoutTokens = []; + } + } + /** * @pure (only if isPure = true) */ @@ -127,6 +152,8 @@ export class BracketPairsTree extends Disposable { } public getBracketsInRange(range: Range): CallbackIterable { + this.flushQueue(); + const startOffset = toLength(range.startLineNumber - 1, range.startColumn - 1); const endOffset = toLength(range.endLineNumber - 1, range.endColumn - 1); return new CallbackIterable(cb => { @@ -136,6 +163,8 @@ export class BracketPairsTree extends Disposable { } public getBracketPairsInRange(range: Range, includeMinIndentation: boolean): CallbackIterable { + this.flushQueue(); + const startLength = positionToLength(range.getStartPosition()); const endLength = positionToLength(range.getEndPosition()); @@ -147,11 +176,15 @@ export class BracketPairsTree extends Disposable { } public getFirstBracketAfter(position: Position): IFoundBracket | null { + this.flushQueue(); + const node = this.initialAstWithoutTokens || this.astWithTokens!; return getFirstBracketAfter(node, lengthZero, node.length, positionToLength(position)); } public getFirstBracketBefore(position: Position): IFoundBracket | null { + this.flushQueue(); + const node = this.initialAstWithoutTokens || this.astWithTokens!; return getFirstBracketBefore(node, lengthZero, node.length, positionToLength(position)); } diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/combineTextEditInfos.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/combineTextEditInfos.ts new file mode 100644 index 00000000000..218ff36b6a3 --- /dev/null +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/combineTextEditInfos.ts @@ -0,0 +1,161 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ArrayQueue } from 'vs/base/common/arrays'; +import { TextEditInfo } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper'; +import { Length, lengthAdd, lengthDiffNonNegative, lengthEquals, lengthIsZero, lengthLessThanEqual, lengthZero, sumLengths } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length'; + +export function combineTextEditInfos(textEditInfoFirst: TextEditInfo[], textEditInfoSecond: TextEditInfo[]): TextEditInfo[] { + if (textEditInfoFirst.length === 0) { + return textEditInfoSecond; + } + + // s0: State before any edits + const firstMap = new ArrayQueue(toTextMap(textEditInfoFirst)); + // s1: State after first edit, but before second edit + const secondMap = toTextMap(textEditInfoSecond); + // s2: State after both edits + + // If set, we are in an edit + let remainingS0Length: Length | undefined = undefined; + let remainingS1Length: Length = lengthZero; + + /** + * @param s1Length Use undefined for length "infinity" + */ + function readPartialS0Map(s1Length: Length | undefined): TextMapping[] { + const result: TextMapping[] = []; + + while (true) { + if ((remainingS0Length !== undefined && !lengthIsZero(remainingS0Length)) || !lengthIsZero(remainingS1Length)) { + let readS1Length: Length; + if (s1Length !== undefined && lengthLessThanEqual(s1Length, remainingS1Length)) { + // remaining satisfies request + readS1Length = s1Length; + remainingS1Length = lengthDiffNonNegative(s1Length, remainingS1Length); + s1Length = lengthZero; + } else { + // Read all of remaining, potentially even more + readS1Length = remainingS1Length; + if (s1Length !== undefined) { + s1Length = lengthDiffNonNegative(remainingS1Length, s1Length); + } + remainingS1Length = lengthZero; + } + + if (remainingS0Length === undefined) { + // unchanged area + result.push({ + oldLength: readS1Length, + newLength: undefined + }); + } else { + // We eagerly consume all of the old length, even if + // we are in an edit and only consume it partially. + result.push({ + oldLength: remainingS0Length, + newLength: readS1Length + }); + remainingS0Length = lengthZero; + } + } + + if (s1Length !== undefined && lengthIsZero(s1Length)) { + break; + } + + const item = firstMap.dequeue(); + if (!item) { + if (s1Length !== undefined) { + result.push({ + oldLength: s1Length, + newLength: undefined, + }); + } + break; + } + if (item.newLength === undefined) { + remainingS1Length = item.oldLength; + remainingS0Length = undefined; + } else { + remainingS0Length = item.oldLength; + remainingS1Length = item.newLength; + } + } + + return result; + } + + const result: TextEditInfo[] = []; + + function push(startOffset: Length, endOffset: Length, newLength: Length) { + if (result.length > 0 && lengthEquals(result[result.length - 1].endOffset, startOffset)) { + const lastResult = result[result.length - 1]; + result[result.length - 1] = new TextEditInfo(lastResult.startOffset, endOffset, lengthAdd(lastResult.newLength, newLength)); + } else { + result.push({ startOffset, endOffset, newLength }); + } + } + + let s0offset = lengthZero; + for (const s2 of secondMap) { + const s0ToS1Map = readPartialS0Map(s2.oldLength); + if (s2.newLength !== undefined) { + // This is an edit + const s0Length = sumLengths(s0ToS1Map, s => s.oldLength); + const s0EndOffset = lengthAdd(s0offset, s0Length); + push(s0offset, s0EndOffset, s2.newLength); + s0offset = s0EndOffset; + } else { + // We are in an unchanged area + for (const s1 of s0ToS1Map) { + const s0startOffset = s0offset; + s0offset = lengthAdd(s0offset, s1.oldLength); + + if (s1.newLength !== undefined) { + push(s0startOffset, s0offset, s1.newLength); + } + } + } + } + + const s0ToS1Map = readPartialS0Map(undefined); + for (const s1 of s0ToS1Map) { + const s0startOffset = s0offset; + s0offset = lengthAdd(s0offset, s1.oldLength); + + if (s1.newLength !== undefined) { + push(s0startOffset, s0offset, s1.newLength); + } + } + + return result; +} + +interface TextMapping { + oldLength: Length; + + /** + * If set, this mapping represents an edit. + * If not set, this mapping represents an unchanged region (for which the new length equals the old length). + */ + newLength?: Length; +} + +function toTextMap(textEditInfos: TextEditInfo[]): TextMapping[] { + const result: TextMapping[] = []; + let lastOffset = lengthZero; + for (const textEditInfo of textEditInfos) { + const spaceLength = lengthDiffNonNegative(lastOffset, textEditInfo.startOffset); + if (!lengthIsZero(spaceLength)) { + result.push({ oldLength: spaceLength }); + } + + const oldLength = lengthDiffNonNegative(textEditInfo.startOffset, textEditInfo.endOffset); + result.push({ oldLength, newLength: textEditInfo.newLength }); + lastOffset = textEditInfo.endOffset; + } + return result; +} diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/concat23Trees.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/concat23Trees.ts index 98ae08e64c3..aab40426436 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/concat23Trees.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/concat23Trees.ts @@ -108,7 +108,7 @@ function concat(node1: AstNode, node2: AstNode): AstNode { function append(list: ListAstNode, nodeToAppend: AstNode): AstNode { list = list.toMutable() as ListAstNode; let curNode: AstNode = list; - const parents = new Array(); + const parents: ListAstNode[] = []; let nodeToAppendOfCorrectHeight: AstNode | undefined; while (true) { // assert nodeToInsert.listHeight <= curNode.listHeight @@ -157,7 +157,7 @@ function append(list: ListAstNode, nodeToAppend: AstNode): AstNode { function prepend(list: ListAstNode, nodeToAppend: AstNode): AstNode { list = list.toMutable() as ListAstNode; let curNode: AstNode = list; - const parents = new Array(); + const parents: ListAstNode[] = []; // assert nodeToInsert.listHeight <= curNode.listHeight while (nodeToAppend.listHeight !== curNode.listHeight) { // assert 0 <= nodeToInsert.listHeight < curNode.listHeight diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length.ts index ef87ef77d68..0b72d9dbf31 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length.ts @@ -100,10 +100,12 @@ export function lengthIsZero(length: Length): boolean { /* * We have 52 bits available in a JS number. * We use the upper 26 bits to store the line and the lower 26 bits to store the column. - * - * Set boolean to `true` when debugging, so that debugging is easier. */ -const factor = /* is debug: */ false ? 100000 : 2 ** 26; +///* +const factor = 2 ** 26; +/*/ +const factor = 1000000; +// */ export function toLength(lineCount: number, columnCount: number): Length { // llllllllllllllllllllllllllcccccccccccccccccccccccccc (52 bits) @@ -138,9 +140,17 @@ export function lengthGetColumnCountIfZeroLineCount(length: Length): number { // [10 lines, 5 cols] + [20 lines, 3 cols] = [30 lines, 3 cols] export function lengthAdd(length1: Length, length2: Length): Length; export function lengthAdd(l1: any, l2: any): Length { - return ((l2 < factor) - ? (l1 + l2) // l2 is the amount of columns (zero line count). Keep the column count from l1. - : (l1 - (l1 % factor) + l2)); // l1 - (l1 % factor) equals toLength(l1.lineCount, 0) + let r = l1 + l2; + if (l2 >= factor) { r = r - (l1 % factor); } + return r; +} + +export function sumLengths(items: readonly T[], lengthFn: (item: T) => Length): Length { + return items.reduce((a, b) => lengthAdd(a, lengthFn(b)), lengthZero); +} + +export function lengthEquals(length1: Length, length2: Length): boolean { + return length1 === length2; } /** diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/parser.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/parser.ts index fb4edcfb866..8758dfb3ea0 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/parser.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/parser.ts @@ -53,7 +53,7 @@ class Parser { } this.oldNodeReader = oldNode ? new NodeReader(oldNode) : undefined; - this.positionMapper = new BeforeEditPositionMapper(edits, tokenizer.length); + this.positionMapper = new BeforeEditPositionMapper(edits); } parseDocument(): AstNode { @@ -71,19 +71,24 @@ class Parser { private parseList( openedBracketIds: SmallImmutableSet, ): AstNode | null { - const items = new Array(); + const items: AstNode[] = []; while (true) { - const token = this.tokenizer.peek(); - if ( - !token || - (token.kind === TokenKind.ClosingBracket && - token.bracketIds.intersects(openedBracketIds)) - ) { - break; + let child = this.tryReadChildFromCache(openedBracketIds); + + if (!child) { + const token = this.tokenizer.peek(); + if ( + !token || + (token.kind === TokenKind.ClosingBracket && + token.bracketIds.intersects(openedBracketIds)) + ) { + break; + } + + child = this.parseChild(openedBracketIds); } - const child = this.parseChild(openedBracketIds); if (child.kind === AstNodeKind.List && child.childrenLength === 0) { continue; } @@ -96,14 +101,14 @@ class Parser { return result; } - private parseChild( - openedBracketIds: SmallImmutableSet, - ): AstNode { + private tryReadChildFromCache(openedBracketIds: SmallImmutableSet): AstNode | undefined { if (this.oldNodeReader) { const maxCacheableLength = this.positionMapper.getDistanceToNextChange(this.tokenizer.offset); - if (!lengthIsZero(maxCacheableLength)) { + if (maxCacheableLength === null || !lengthIsZero(maxCacheableLength)) { const cachedNode = this.oldNodeReader.readLongestNodeAt(this.positionMapper.getOffsetBeforeChange(this.tokenizer.offset), curNode => { - if (!lengthLessThan(curNode.length, maxCacheableLength)) { + // The edit could extend the ending token, thus we cannot re-use nodes that touch the edit. + // If there is no edit anymore, we can re-use the node in any case. + if (maxCacheableLength !== null && !lengthLessThan(curNode.length, maxCacheableLength)) { // Either the node contains edited text or touches edited text. // In the latter case, brackets might have been extended (`end` -> `ending`), so even touching nodes cannot be reused. return false; @@ -119,7 +124,12 @@ class Parser { } } } + return undefined; + } + private parseChild( + openedBracketIds: SmallImmutableSet, + ): AstNode { this._itemsConstructed++; const token = this.tokenizer.read()!; diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/smallImmutableSet.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/smallImmutableSet.ts index a0d4261e9f7..c324c97f0a7 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/smallImmutableSet.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/smallImmutableSet.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -const emptyArr = new Array(); +const emptyArr: number[] = []; /** * Represents an immutable set that works best for a small number of elements (less than 32). @@ -86,7 +86,7 @@ export class SmallImmutableSet { } // This can be optimized, but it's not a common case - const newItems = new Array(); + const newItems: number[] = []; for (let i = 0; i < Math.max(this.additionalItems.length, other.additionalItems.length); i++) { const item1 = this.additionalItems[i] || 0; const item2 = other.additionalItems[i] || 0; diff --git a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/tokenizer.ts b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/tokenizer.ts index 7cfe9631770..c5d8898ded7 100644 --- a/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/tokenizer.ts +++ b/src/vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/tokenizer.ts @@ -81,7 +81,7 @@ export class TextBufferTokenizer implements Tokenizer { } get length() { - return toLength(this.textBufferLineCount, this.textBufferLastLineLength); + return toLength(this.textBufferLineCount - 1, this.textBufferLastLineLength); } getText() { @@ -143,7 +143,9 @@ class NonPeekableTextBufferTokenizer { // We must not jump into a token! if (lineIdx === this.lineIdx) { this.lineCharOffset = column; - this.lineTokenOffset = this.lineCharOffset === 0 ? 0 : this.lineTokens!.findTokenIndexAtOffset(this.lineCharOffset); + if (this.line !== null) { + this.lineTokenOffset = this.lineCharOffset === 0 ? 0 : this.lineTokens!.findTokenIndexAtOffset(this.lineCharOffset); + } } else { this.lineIdx = lineIdx; this.lineCharOffset = column; @@ -291,7 +293,7 @@ export class FastTokenizer implements Tokenizer { let lastTokenEndOffset = 0; let lastTokenEndLine = 0; - const smallTextTokens0Line = new Array(); + const smallTextTokens0Line: Token[] = []; for (let i = 0; i < 60; i++) { smallTextTokens0Line.push( new Token( @@ -301,7 +303,7 @@ export class FastTokenizer implements Tokenizer { ); } - const smallTextTokens1Line = new Array(); + const smallTextTokens1Line: Token[] = []; for (let i = 0; i < 60; i++) { smallTextTokens1Line.push( new Token( diff --git a/src/vs/editor/contrib/codeAction/browser/codeAction.ts b/src/vs/editor/contrib/codeAction/browser/codeAction.ts index 87eda860c58..1ab20efe339 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeAction.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeAction.ts @@ -33,9 +33,6 @@ export const sourceActionCommandId = 'editor.action.sourceAction'; export const organizeImportsCommandId = 'editor.action.organizeImports'; export const fixAllCommandId = 'editor.action.fixAll'; -export const acceptSelectedCodeActionCommand = 'acceptSelectedCodeAction'; -export const previewSelectedCodeActionCommand = 'previewSelectedCodeAction'; - class ManagedCodeActionSet extends Disposable implements CodeActionSet { private static codeActionsPreferredComparator(a: languages.CodeAction, b: languages.CodeAction): number { diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts b/src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts index 0b345e4ecb8..57ca5c49f8d 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionCommands.ts @@ -16,12 +16,10 @@ import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { CodeActionTriggerType } from 'vs/editor/common/languages'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; -import { acceptSelectedCodeActionCommand, applyCodeAction, ApplyCodeActionReason, codeActionCommandId, fixAllCommandId, organizeImportsCommandId, previewSelectedCodeActionCommand, refactorCommandId, refactorPreviewCommandId, sourceActionCommandId } from 'vs/editor/contrib/codeAction/browser/codeAction'; +import { applyCodeAction, ApplyCodeActionReason, codeActionCommandId, fixAllCommandId, organizeImportsCommandId, refactorCommandId, refactorPreviewCommandId, sourceActionCommandId } from 'vs/editor/contrib/codeAction/browser/codeAction'; import { CodeActionUi } from 'vs/editor/contrib/codeAction/browser/codeActionUi'; -import { CodeActionWidget, Context } from 'vs/editor/contrib/codeAction/browser/codeActionWidget'; import { MessageController } from 'vs/editor/contrib/message/browser/messageController'; import * as nls from 'vs/nls'; -import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -102,7 +100,7 @@ export class CodeActionController extends Disposable implements IEditorContribut @IContextKeyService contextKeyService: IContextKeyService, @IEditorProgressService progressService: IEditorProgressService, @IInstantiationService private readonly _instantiationService: IInstantiationService, - @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, + @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService ) { super(); @@ -416,117 +414,3 @@ export class AutoFixAction extends EditorAction { CodeActionAutoApply.IfSingle, undefined, CodeActionTriggerSource.AutoFix); } } - -const weight = KeybindingWeight.EditorContrib + 1000; - -registerAction2(class extends Action2 { - constructor() { - super({ - id: 'hideCodeActionWidget', - title: { - value: nls.localize('hideCodeActionWidget.title', "Hide code action widget"), - original: 'Hide code action widget' - }, - precondition: Context.Visible, - keybinding: { - weight, - primary: KeyCode.Escape, - secondary: [KeyMod.Shift | KeyCode.Escape] - }, - }); - } - - run(): void { - CodeActionWidget.INSTANCE?.hide(); - } -}); - -registerAction2(class extends Action2 { - constructor() { - super({ - id: 'selectPrevCodeAction', - title: { - value: nls.localize('selectPrevCodeAction.title', "Select previous code action"), - original: 'Select previous code action' - }, - precondition: Context.Visible, - keybinding: { - weight, - primary: KeyCode.UpArrow, - secondary: [KeyMod.CtrlCmd | KeyCode.UpArrow], - mac: { primary: KeyCode.UpArrow, secondary: [KeyMod.CtrlCmd | KeyCode.UpArrow, KeyMod.WinCtrl | KeyCode.KeyP] }, - } - }); - } - - run(): void { - CodeActionWidget.INSTANCE?.focusPrevious(); - } -}); - -registerAction2(class extends Action2 { - constructor() { - super({ - id: 'selectNextCodeAction', - title: { - value: nls.localize('selectNextCodeAction.title', "Select next code action"), - original: 'Select next code action' - }, - precondition: Context.Visible, - keybinding: { - weight, - primary: KeyCode.DownArrow, - secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow], - mac: { primary: KeyCode.DownArrow, secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow, KeyMod.WinCtrl | KeyCode.KeyN] } - } - }); - } - - run(): void { - CodeActionWidget.INSTANCE?.focusNext(); - } -}); - -registerAction2(class extends Action2 { - constructor() { - super({ - id: acceptSelectedCodeActionCommand, - title: { - value: nls.localize('acceptSelected.title', "Accept selected code action"), - original: 'Accept selected code action' - }, - precondition: Context.Visible, - keybinding: { - weight, - primary: KeyCode.Enter, - secondary: [KeyMod.CtrlCmd | KeyCode.Period], - } - }); - } - - run(): void { - CodeActionWidget.INSTANCE?.acceptSelected(); - } -}); - -registerAction2(class extends Action2 { - constructor() { - super({ - id: previewSelectedCodeActionCommand, - title: { - value: nls.localize('previewSelected.title', "Preview selected code action"), - original: 'Preview selected code action' - }, - precondition: Context.Visible, - keybinding: { - weight, - primary: KeyMod.CtrlCmd | KeyCode.Enter, - } - }); - } - - run(): void { - CodeActionWidget.INSTANCE?.acceptSelected({ preview: true }); - } -}); - diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionKeybindingResolver.ts b/src/vs/editor/contrib/codeAction/browser/codeActionKeybindingResolver.ts index a44ba40983b..67240672131 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionKeybindingResolver.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionKeybindingResolver.ts @@ -8,6 +8,7 @@ import { Lazy } from 'vs/base/common/lazy'; import { CodeAction } from 'vs/editor/common/languages'; import { codeActionCommandId, fixAllCommandId, organizeImportsCommandId, refactorCommandId, sourceActionCommandId } from 'vs/editor/contrib/codeAction/browser/codeAction'; import { CodeActionAutoApply, CodeActionCommandArgs, CodeActionKind } from 'vs/editor/contrib/codeAction/common/types'; +import { IActionKeybindingResolver } from 'vs/platform/actionWidget/common/actionWidget'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; interface ResolveCodeActionKeybinding { @@ -16,7 +17,7 @@ interface ResolveCodeActionKeybinding { readonly resolvedKeybinding: ResolvedKeybinding; } -export class CodeActionKeybindingResolver { +export class CodeActionKeybindingResolver implements IActionKeybindingResolver { private static readonly codeActionCommands: readonly string[] = [ refactorCommandId, codeActionCommandId, diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionMenuItems.ts b/src/vs/editor/contrib/codeAction/browser/codeActionMenuItems.ts new file mode 100644 index 00000000000..8e3bad65bcf --- /dev/null +++ b/src/vs/editor/contrib/codeAction/browser/codeActionMenuItems.ts @@ -0,0 +1,68 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/base/browser/ui/codicons/codiconStyles'; // The codicon symbol styles are defined here and must be loaded +import { Codicon } from 'vs/base/common/codicons'; +import { ActionListItemKind, IListMenuItem } from 'vs/platform/actionWidget/browser/actionWidget'; +import { CodeActionItem, CodeActionKind } from 'vs/editor/contrib/codeAction/common/types'; +import 'vs/editor/contrib/symbolIcons/browser/symbolIcons'; // The codicon symbol colors are defined here and must be loaded to get colors +import { localize } from 'vs/nls'; + +export interface ActionGroup { + readonly kind: CodeActionKind; + readonly title: string; + readonly icon?: { readonly codicon: Codicon; readonly color?: string }; +} + +const uncategorizedCodeActionGroup = Object.freeze({ kind: CodeActionKind.Empty, title: localize('codeAction.widget.id.more', 'More Actions...') }); + +const codeActionGroups = Object.freeze([ + { kind: CodeActionKind.QuickFix, title: localize('codeAction.widget.id.quickfix', 'Quick Fix...') }, + { kind: CodeActionKind.RefactorExtract, title: localize('codeAction.widget.id.extract', 'Extract...'), icon: { codicon: Codicon.wrench } }, + { kind: CodeActionKind.RefactorInline, title: localize('codeAction.widget.id.inline', 'Inline...'), icon: { codicon: Codicon.wrench } }, + { kind: CodeActionKind.RefactorRewrite, title: localize('codeAction.widget.id.convert', 'Rewrite...'), icon: { codicon: Codicon.wrench } }, + { kind: CodeActionKind.RefactorMove, title: localize('codeAction.widget.id.move', 'Move...'), icon: { codicon: Codicon.wrench } }, + { kind: CodeActionKind.SurroundWith, title: localize('codeAction.widget.id.surround', 'Surround With...'), icon: { codicon: Codicon.symbolSnippet } }, + { kind: CodeActionKind.Source, title: localize('codeAction.widget.id.source', 'Source Action...'), icon: { codicon: Codicon.symbolFile } }, + uncategorizedCodeActionGroup, +]); + +export function toMenuItems(inputCodeActions: readonly CodeActionItem[], showHeaders: boolean): IListMenuItem[] { + if (!showHeaders) { + return inputCodeActions.map((action): IListMenuItem => { + return { + kind: ActionListItemKind.Action, + item: action, + group: uncategorizedCodeActionGroup, + disabled: !!action.action.disabled, + label: action.action.disabled || action.action.title + }; + }); + } + + // Group code actions + const menuEntries = codeActionGroups.map(group => ({ group, actions: [] as CodeActionItem[] })); + + for (const action of inputCodeActions) { + const kind = action.action.kind ? new CodeActionKind(action.action.kind) : CodeActionKind.None; + for (const menuEntry of menuEntries) { + if (menuEntry.group.kind.contains(kind)) { + menuEntry.actions.push(action); + break; + } + } + } + + const allMenuItems: IListMenuItem[] = []; + for (const menuEntry of menuEntries) { + if (menuEntry.actions.length) { + allMenuItems.push({ kind: ActionListItemKind.Header, group: menuEntry.group }); + for (const action of menuEntry.actions) { + allMenuItems.push({ kind: ActionListItemKind.Action, item: action, group: menuEntry.group, label: action.action.title, disabled: !!action.action.disabled }); + } + } + } + return allMenuItems; +} diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionUi.ts b/src/vs/editor/contrib/codeAction/browser/codeActionUi.ts index 5f14580fba5..384ea9b7dbc 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionUi.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionUi.ts @@ -12,14 +12,14 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { ScrollType } from 'vs/editor/common/editorCommon'; import { CodeActionTriggerType } from 'vs/editor/common/languages'; +import { IActionShowOptions, IActionWidgetService, IRenderDelegate } from 'vs/platform/actionWidget/browser/actionWidget'; import { MessageController } from 'vs/editor/contrib/message/browser/messageController'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { CodeActionAutoApply, CodeActionItem, CodeActionSet, CodeActionTrigger } from '../common/types'; import { CodeActionsState } from './codeActionModel'; -import { CodeActionShowOptions, CodeActionWidget } from './codeActionWidget'; import { LightBulbWidget } from './lightBulbWidget'; +import { toMenuItems } from 'vs/editor/contrib/codeAction/browser/codeActionMenuItems'; export class CodeActionUi extends Disposable { private readonly _lightBulbWidget: Lazy; @@ -35,19 +35,19 @@ export class CodeActionUi extends Disposable { applyCodeAction: (action: CodeActionItem, regtriggerAfterApply: boolean, preview: boolean) => Promise; }, @IConfigurationService private readonly _configurationService: IConfigurationService, - @IContextKeyService private readonly _contextKeyService: IContextKeyService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IInstantiationService readonly instantiationService: IInstantiationService, + @IActionWidgetService private readonly _actionWidgetService: IActionWidgetService ) { super(); this._lightBulbWidget = new Lazy(() => { - const widget = this._register(_instantiationService.createInstance(LightBulbWidget, this._editor, quickFixActionId, preferredFixActionId)); + const widget = this._register(instantiationService.createInstance(LightBulbWidget, this._editor, quickFixActionId, preferredFixActionId)); this._register(widget.onClick(e => this.showCodeActionList(e.trigger, e.actions, e, { includeDisabledActions: false, fromLightbulb: true, showHeaders: this.shouldShowHeaders() }))); return widget; }); - this._register(this._editor.onDidLayoutChange(() => CodeActionWidget.INSTANCE?.hide())); + this._register(this._editor.onDidLayoutChange(() => this._actionWidgetService.hide())); } override dispose() { @@ -115,7 +115,7 @@ export class CodeActionUi extends Disposable { this.showCodeActionList(newState.trigger, actions, this.toCoords(newState.position), { includeDisabledActions, fromLightbulb: false, showHeaders: this.shouldShowHeaders() }); } else { // auto magically triggered - if (CodeActionWidget.INSTANCE?.isVisible) { + if (this._actionWidgetService.isVisible) { // TODO: Figure out if we should update the showing menu? actions.dispose(); } else { @@ -152,7 +152,7 @@ export class CodeActionUi extends Disposable { return undefined; } - public async showCodeActionList(trigger: CodeActionTrigger, actions: CodeActionSet, at: IAnchor | IPosition, options: CodeActionShowOptions): Promise { + public async showCodeActionList(trigger: CodeActionTrigger, actions: CodeActionSet, at: IAnchor | IPosition, options: IActionShowOptions): Promise { const editorDom = this._editor.getDomNode(); if (!editorDom) { return; @@ -160,14 +160,23 @@ export class CodeActionUi extends Disposable { const anchor = Position.isIPosition(at) ? this.toCoords(at) : at; - CodeActionWidget.getOrCreateInstance(this._instantiationService).show(trigger, actions, anchor, editorDom, { ...options, showHeaders: this.shouldShowHeaders() }, { - onSelectCodeAction: async (action, trigger, options) => { - this.delegate.applyCodeAction(action, /* retrigger */ true, Boolean(options.preview || trigger.preview)); + const delegate: IRenderDelegate = { + onSelect: async (action: CodeActionItem, preview?: boolean) => { + this.delegate.applyCodeAction(action, /* retrigger */ true, !!preview ? preview : false); + this._actionWidgetService.hide(); }, onHide: () => { this._editor?.focus(); - }, - }, this._contextKeyService); + } + }; + this._actionWidgetService.show( + 'codeActionWidget', + toMenuItems, + delegate, + actions, + anchor, + editorDom, + { ...options, showHeaders: this.shouldShowHeaders() }); } private toCoords(position: IPosition): IAnchor { diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionWidget.ts b/src/vs/editor/contrib/codeAction/browser/codeActionWidget.ts deleted file mode 100644 index 1fcf39e20bd..00000000000 --- a/src/vs/editor/contrib/codeAction/browser/codeActionWidget.ts +++ /dev/null @@ -1,577 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as dom from 'vs/base/browser/dom'; -import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import 'vs/base/browser/ui/codicons/codiconStyles'; // The codicon symbol styles are defined here and must be loaded -import { IAnchor } from 'vs/base/browser/ui/contextview/contextview'; -import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; -import { IListEvent, IListMouseEvent, IListRenderer } from 'vs/base/browser/ui/list/list'; -import { List } from 'vs/base/browser/ui/list/listWidget'; -import { IAction } from 'vs/base/common/actions'; -import { Codicon } from 'vs/base/common/codicons'; -import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; -import { OS } from 'vs/base/common/platform'; -import 'vs/css!./codeActionWidget'; -import { acceptSelectedCodeActionCommand, previewSelectedCodeActionCommand } from 'vs/editor/contrib/codeAction/browser/codeAction'; -import { CodeActionItem, CodeActionKind, CodeActionSet, CodeActionTrigger, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/common/types'; -import 'vs/editor/contrib/symbolIcons/browser/symbolIcons'; // The codicon symbol colors are defined here and must be loaded to get colors -import { localize } from 'vs/nls'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { CodeActionKeybindingResolver } from './codeActionKeybindingResolver'; - -export const Context = { - Visible: new RawContextKey('codeActionMenuVisible', false, localize('codeActionMenuVisible', "Whether the code action list widget is visible")) -}; - -interface CodeActionWidgetDelegate { - onSelectCodeAction(action: CodeActionItem, trigger: CodeActionTrigger, options: { readonly preview: boolean }): Promise; - onHide(cancelled: boolean): void; -} - -export interface CodeActionShowOptions { - readonly includeDisabledActions: boolean; - readonly fromLightbulb?: boolean; - readonly showHeaders?: boolean; -} - -enum CodeActionListItemKind { - CodeAction = 'action', - Header = 'header' -} - -interface CodeActionListItemCodeAction { - readonly kind: CodeActionListItemKind.CodeAction; - readonly action: CodeActionItem; - readonly group: CodeActionGroup; -} - -interface CodeActionListItemHeader { - readonly kind: CodeActionListItemKind.Header; - readonly group: CodeActionGroup; -} - -type ICodeActionMenuItem = CodeActionListItemCodeAction | CodeActionListItemHeader; - -interface ICodeActionMenuTemplateData { - readonly container: HTMLElement; - readonly icon: HTMLElement; - readonly text: HTMLElement; - readonly keybinding: KeybindingLabel; -} - -function stripNewlines(str: string): string { - return str.replace(/\r\n|\r|\n/g, ' '); -} - -interface CodeActionGroup { - readonly kind: CodeActionKind; - readonly title: string; - readonly icon?: { readonly codicon: Codicon; readonly color?: string }; -} - -const uncategorizedCodeActionGroup = Object.freeze({ kind: CodeActionKind.Empty, title: localize('codeAction.widget.id.more', 'More Actions...') }); - -const codeActionGroups = Object.freeze([ - { kind: CodeActionKind.QuickFix, title: localize('codeAction.widget.id.quickfix', 'Quick Fix...') }, - { kind: CodeActionKind.RefactorExtract, title: localize('codeAction.widget.id.extract', 'Extract...'), icon: { codicon: Codicon.wrench } }, - { kind: CodeActionKind.RefactorInline, title: localize('codeAction.widget.id.inline', 'Inline...'), icon: { codicon: Codicon.wrench } }, - { kind: CodeActionKind.RefactorRewrite, title: localize('codeAction.widget.id.convert', 'Rewrite...'), icon: { codicon: Codicon.wrench } }, - { kind: CodeActionKind.RefactorMove, title: localize('codeAction.widget.id.move', 'Move...'), icon: { codicon: Codicon.wrench } }, - { kind: CodeActionKind.SurroundWith, title: localize('codeAction.widget.id.surround', 'Surround With...'), icon: { codicon: Codicon.symbolSnippet } }, - { kind: CodeActionKind.Source, title: localize('codeAction.widget.id.source', 'Source Action...'), icon: { codicon: Codicon.symbolFile } }, - uncategorizedCodeActionGroup, -]); - -class CodeActionItemRenderer implements IListRenderer { - constructor( - private readonly keybindingResolver: CodeActionKeybindingResolver, - @IKeybindingService private readonly keybindingService: IKeybindingService, - ) { } - - get templateId(): string { return CodeActionListItemKind.CodeAction; } - - renderTemplate(container: HTMLElement): ICodeActionMenuTemplateData { - container.classList.add('code-action'); - - const icon = document.createElement('div'); - icon.className = 'icon'; - container.append(icon); - - const text = document.createElement('span'); - text.className = 'title'; - container.append(text); - - const keybinding = new KeybindingLabel(container, OS); - - return { container, icon, text, keybinding }; - } - - renderElement(element: CodeActionListItemCodeAction, _index: number, data: ICodeActionMenuTemplateData): void { - if (element.group.icon) { - data.icon.className = element.group.icon.codicon.classNames; - data.icon.style.color = element.group.icon.color ?? ''; - } else { - data.icon.className = Codicon.lightBulb.classNames; - data.icon.style.color = 'var(--vscode-editorLightBulb-foreground)'; - } - - data.text.textContent = stripNewlines(element.action.action.title); - - const binding = this.keybindingResolver.getResolver()(element.action.action); - data.keybinding.set(binding); - if (!binding) { - dom.hide(data.keybinding.element); - } else { - dom.show(data.keybinding.element); - } - - if (element.action.action.disabled) { - data.container.title = element.action.action.disabled; - data.container.classList.add('option-disabled'); - } else { - data.container.title = localize({ key: 'label', comment: ['placeholders are keybindings, e.g "F2 to Apply, Shift+F2 to Preview"'] }, "{0} to Apply, {1} to Preview", this.keybindingService.lookupKeybinding(acceptSelectedCodeActionCommand)?.getLabel(), this.keybindingService.lookupKeybinding(previewSelectedCodeActionCommand)?.getLabel()); - data.container.classList.remove('option-disabled'); - } - } - - disposeTemplate(_templateData: ICodeActionMenuTemplateData): void { - // noop - } -} - -interface HeaderTemplateData { - readonly container: HTMLElement; - readonly text: HTMLElement; -} - -class HeaderRenderer implements IListRenderer { - - get templateId(): string { return CodeActionListItemKind.Header; } - - renderTemplate(container: HTMLElement): HeaderTemplateData { - container.classList.add('group-header'); - - const text = document.createElement('span'); - container.append(text); - - return { container, text }; - } - - renderElement(element: CodeActionListItemHeader, _index: number, templateData: HeaderTemplateData): void { - templateData.text.textContent = element.group.title; - } - - disposeTemplate(_templateData: HeaderTemplateData): void { - // noop - } -} - -const previewSelectedEventType = 'previewSelectedCodeAction'; -class CodeActionList extends Disposable { - - private readonly codeActionLineHeight = 24; - private readonly headerLineHeight = 26; - - public readonly domNode: HTMLElement; - - private readonly list: List; - - private readonly allMenuItems: ICodeActionMenuItem[]; - - constructor( - codeActions: readonly CodeActionItem[], - showHeaders: boolean, - private readonly onDidSelect: (action: CodeActionItem, options: { readonly preview: boolean }) => void, - @IKeybindingService keybindingService: IKeybindingService, - ) { - super(); - - this.domNode = document.createElement('div'); - this.domNode.classList.add('codeActionList'); - - this.list = this._register(new List('codeActionWidget', this.domNode, { - getHeight: element => element.kind === CodeActionListItemKind.Header ? this.headerLineHeight : this.codeActionLineHeight, - getTemplateId: element => element.kind, - }, [ - new CodeActionItemRenderer(new CodeActionKeybindingResolver(keybindingService), keybindingService), - new HeaderRenderer(), - ], { - keyboardSupport: false, - accessibilityProvider: { - getAriaLabel: element => { - if (element.kind === CodeActionListItemKind.CodeAction) { - let label = stripNewlines(element.action.action.title); - if (element.action.action.disabled) { - label = localize({ key: 'customCodeActionWidget.labels', comment: ['Code action labels for accessibility.'] }, "{0}, Disabled Reason: {1}", label, element.action.action.disabled); - } - return label; - } - return null; - }, - getWidgetAriaLabel: () => localize({ key: 'customCodeActionWidget', comment: ['A Code Action Option'] }, "Code Action Widget"), - getRole: () => 'option', - getWidgetRole: () => 'code-action-widget' - } - })); - - this._register(this.list.onMouseClick(e => this.onListClick(e))); - this._register(this.list.onMouseOver(e => this.onListHover(e))); - this._register(this.list.onDidChangeFocus(() => this.list.domFocus())); - this._register(this.list.onDidChangeSelection(e => this.onListSelection(e))); - - this.allMenuItems = this.toMenuItems(codeActions, showHeaders); - this.list.splice(0, this.list.length, this.allMenuItems); - - this.focusNext(); - } - - public layout(minWidth: number): number { - // Updating list height, depending on how many separators and headers there are. - const numHeaders = this.allMenuItems.filter(item => item.kind === CodeActionListItemKind.Header).length; - const height = this.allMenuItems.length * this.codeActionLineHeight; - const heightWithHeaders = height + numHeaders * this.headerLineHeight - numHeaders * this.codeActionLineHeight; - this.list.layout(heightWithHeaders); - - // For finding width dynamically (not using resize observer) - const itemWidths: number[] = this.allMenuItems.map((_, index): number => { - const element = document.getElementById(this.list.getElementID(index)); - if (element) { - element.style.width = 'auto'; - const width = element.getBoundingClientRect().width; - element.style.width = ''; - return width; - } - return 0; - }); - - // resize observer - can be used in the future since list widget supports dynamic height but not width - const width = Math.max(...itemWidths, minWidth); - this.list.layout(heightWithHeaders, width); - - this.domNode.style.height = `${heightWithHeaders}px`; - - this.list.domFocus(); - return width; - } - - public focusPrevious() { - this.list.focusPrevious(1, true, undefined, element => element.kind === CodeActionListItemKind.CodeAction && !element.action.action.disabled); - } - - public focusNext() { - this.list.focusNext(1, true, undefined, element => element.kind === CodeActionListItemKind.CodeAction && !element.action.action.disabled); - } - - public acceptSelected(options?: { readonly preview?: boolean }) { - const focused = this.list.getFocus(); - if (focused.length === 0) { - return; - } - - const focusIndex = focused[0]; - const element = this.list.element(focusIndex); - if (element.kind !== CodeActionListItemKind.CodeAction || element.action.action.disabled) { - return; - } - - const event = new UIEvent(options?.preview ? previewSelectedEventType : 'acceptSelectedCodeAction'); - this.list.setSelection([focusIndex], event); - } - - private onListSelection(e: IListEvent): void { - if (!e.elements.length) { - return; - } - - const element = e.elements[0]; - if (element.kind === CodeActionListItemKind.CodeAction && !element.action.action.disabled) { - this.onDidSelect(element.action, { preview: e.browserEvent?.type === previewSelectedEventType }); - } else { - this.list.setSelection([]); - } - } - - private onListHover(e: IListMouseEvent): void { - this.list.setFocus(typeof e.index === 'number' ? [e.index] : []); - } - - private onListClick(e: IListMouseEvent): void { - if (e.element && e.element.kind === CodeActionListItemKind.CodeAction && e.element.action.action.disabled) { - this.list.setFocus([]); - } - } - - private toMenuItems(inputCodeActions: readonly CodeActionItem[], showHeaders: boolean): ICodeActionMenuItem[] { - if (!showHeaders) { - return inputCodeActions.map((action): ICodeActionMenuItem => ({ kind: CodeActionListItemKind.CodeAction, action, group: uncategorizedCodeActionGroup })); - } - - // Group code actions - const menuEntries = codeActionGroups.map(group => ({ group, actions: [] as CodeActionItem[] })); - - for (const action of inputCodeActions) { - const kind = action.action.kind ? new CodeActionKind(action.action.kind) : CodeActionKind.None; - for (const menuEntry of menuEntries) { - if (menuEntry.group.kind.contains(kind)) { - menuEntry.actions.push(action); - break; - } - } - } - - const allMenuItems: ICodeActionMenuItem[] = []; - for (const menuEntry of menuEntries) { - if (menuEntry.actions.length) { - allMenuItems.push({ kind: CodeActionListItemKind.Header, group: menuEntry.group }); - for (const action of menuEntry.actions) { - allMenuItems.push({ kind: CodeActionListItemKind.CodeAction, action, group: menuEntry.group }); - } - } - } - - return allMenuItems; - } -} - -// TODO: Take a look at user storage for this so it is preserved across windows and on reload. -let showDisabled = false; - -export class CodeActionWidget extends Disposable { - - private static _instance?: CodeActionWidget; - - public static get INSTANCE(): CodeActionWidget | undefined { return this._instance; } - - public static getOrCreateInstance(instantiationService: IInstantiationService): CodeActionWidget { - if (!this._instance) { - this._instance = instantiationService.createInstance(CodeActionWidget); - } - return this._instance; - } - - private readonly codeActionList = this._register(new MutableDisposable()); - - private currentShowingContext?: { - readonly options: CodeActionShowOptions; - readonly trigger: CodeActionTrigger; - readonly anchor: IAnchor; - readonly container: HTMLElement | undefined; - readonly codeActions: CodeActionSet; - readonly delegate: CodeActionWidgetDelegate; - readonly contextKeyService: IContextKeyService; - }; - - constructor( - @ICommandService private readonly _commandService: ICommandService, - @IContextViewService private readonly _contextViewService: IContextViewService, - @IKeybindingService private readonly _keybindingService: IKeybindingService, - @ITelemetryService private readonly _telemetryService: ITelemetryService, - ) { - super(); - } - - get isVisible(): boolean { - return !!this.currentShowingContext; - } - - public async show(trigger: CodeActionTrigger, codeActions: CodeActionSet, anchor: IAnchor, container: HTMLElement | undefined, options: CodeActionShowOptions, delegate: CodeActionWidgetDelegate, contextKeyService: IContextKeyService): Promise { - this.currentShowingContext = undefined; - const visibleContext = Context.Visible.bindTo(contextKeyService); - - const actionsToShow = options.includeDisabledActions && (showDisabled || codeActions.validActions.length === 0) ? codeActions.allActions : codeActions.validActions; - if (!actionsToShow.length) { - visibleContext.reset(); - return; - } - - this.currentShowingContext = { trigger, codeActions, anchor, container, delegate, options, contextKeyService }; - - this._contextViewService.showContextView({ - getAnchor: () => anchor, - render: (container: HTMLElement) => { - visibleContext.set(true); - return this.renderWidget(container, trigger, codeActions, options, actionsToShow, delegate); - }, - onHide: (didCancel: boolean) => { - visibleContext.reset(); - return this.onWidgetClosed(trigger, options, codeActions, didCancel, delegate); - }, - }, container, false); - } - - public focusPrevious() { - this.codeActionList.value?.focusPrevious(); - } - - public focusNext() { - this.codeActionList.value?.focusNext(); - } - - public acceptSelected(options?: { readonly preview?: boolean }) { - this.codeActionList.value?.acceptSelected(options); - } - - public hide() { - this.codeActionList.clear(); - this._contextViewService.hideContextView(); - } - - private renderWidget(element: HTMLElement, trigger: CodeActionTrigger, codeActions: CodeActionSet, options: CodeActionShowOptions, showingCodeActions: readonly CodeActionItem[], delegate: CodeActionWidgetDelegate): IDisposable { - const renderDisposables = new DisposableStore(); - - const widget = document.createElement('div'); - widget.classList.add('codeActionWidget'); - element.appendChild(widget); - - this.codeActionList.value = new CodeActionList( - showingCodeActions, - options.showHeaders ?? true, - (action, options) => { - this.hide(); - delegate.onSelectCodeAction(action, trigger, options); - }, - this._keybindingService); - - widget.appendChild(this.codeActionList.value.domNode); - - // Invisible div to block mouse interaction in the rest of the UI - const menuBlock = document.createElement('div'); - const block = element.appendChild(menuBlock); - block.classList.add('context-view-block'); - block.style.position = 'fixed'; - block.style.cursor = 'initial'; - block.style.left = '0'; - block.style.top = '0'; - block.style.width = '100%'; - block.style.height = '100%'; - block.style.zIndex = '-1'; - renderDisposables.add(dom.addDisposableListener(block, dom.EventType.MOUSE_DOWN, e => e.stopPropagation())); - - // Invisible div to block mouse interaction with the menu - const pointerBlockDiv = document.createElement('div'); - const pointerBlock = element.appendChild(pointerBlockDiv); - pointerBlock.classList.add('context-view-pointerBlock'); - pointerBlock.style.position = 'fixed'; - pointerBlock.style.cursor = 'initial'; - pointerBlock.style.left = '0'; - pointerBlock.style.top = '0'; - pointerBlock.style.width = '100%'; - pointerBlock.style.height = '100%'; - pointerBlock.style.zIndex = '2'; - - // Removes block on click INSIDE widget or ANY mouse movement - renderDisposables.add(dom.addDisposableListener(pointerBlock, dom.EventType.POINTER_MOVE, () => pointerBlock.remove())); - renderDisposables.add(dom.addDisposableListener(pointerBlock, dom.EventType.MOUSE_DOWN, () => pointerBlock.remove())); - - // Action bar - let actionBarWidth = 0; - if (!options.fromLightbulb) { - const actionBar = this.createActionBar(codeActions, options); - if (actionBar) { - widget.appendChild(actionBar.getContainer().parentElement!); - renderDisposables.add(actionBar); - actionBarWidth = actionBar.getContainer().offsetWidth; - } - } - - const width = this.codeActionList.value.layout(actionBarWidth); - widget.style.width = `${width}px`; - - const focusTracker = renderDisposables.add(dom.trackFocus(element)); - renderDisposables.add(focusTracker.onDidBlur(() => this.hide())); - - return renderDisposables; - } - - /** - * Toggles whether the disabled actions in the code action widget are visible or not. - */ - private toggleShowDisabled(newShowDisabled: boolean): void { - const previousCtx = this.currentShowingContext; - - this.hide(); - - showDisabled = newShowDisabled; - - if (previousCtx) { - this.show(previousCtx.trigger, previousCtx.codeActions, previousCtx.anchor, previousCtx.container, previousCtx.options, previousCtx.delegate, previousCtx.contextKeyService); - } - } - - private onWidgetClosed(trigger: CodeActionTrigger, options: CodeActionShowOptions, codeActions: CodeActionSet, cancelled: boolean, delegate: CodeActionWidgetDelegate): void { - type ApplyCodeActionEvent = { - codeActionFrom: CodeActionTriggerSource; - validCodeActions: number; - cancelled: boolean; - }; - - type ApplyCodeEventClassification = { - codeActionFrom: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The kind of action used to opened the code action.' }; - validCodeActions: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The total number of valid actions that are highlighted and can be used.' }; - cancelled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The indicator if the menu was selected or cancelled.' }; - owner: 'mjbvz'; - comment: 'Event used to gain insights into how code actions are being triggered'; - }; - - this._telemetryService.publicLog2('codeAction.applyCodeAction', { - codeActionFrom: options.fromLightbulb ? CodeActionTriggerSource.Lightbulb : trigger.triggerAction, - validCodeActions: codeActions.validActions.length, - cancelled: cancelled, - }); - - this.currentShowingContext = undefined; - - delegate.onHide(cancelled); - } - - private createActionBar(codeActions: CodeActionSet, options: CodeActionShowOptions): ActionBar | undefined { - const actions = this.getActionBarActions(codeActions, options); - if (!actions.length) { - return undefined; - } - - const container = dom.$('.codeActionWidget-action-bar'); - const actionBar = new ActionBar(container); - actionBar.push(actions, { icon: false, label: true }); - return actionBar; - } - - private getActionBarActions(codeActions: CodeActionSet, options: CodeActionShowOptions): IAction[] { - const actions = codeActions.documentation.map((command): IAction => ({ - id: command.id, - label: command.title, - tooltip: command.tooltip ?? '', - class: undefined, - enabled: true, - run: () => this._commandService.executeCommand(command.id, ...(command.arguments ?? [])), - })); - - if (options.includeDisabledActions && codeActions.validActions.length > 0 && codeActions.allActions.length !== codeActions.validActions.length) { - actions.push(showDisabled ? { - id: 'hideMoreCodeActions', - label: localize('hideMoreCodeActions', 'Hide Disabled'), - enabled: true, - tooltip: '', - class: undefined, - run: () => this.toggleShowDisabled(false) - } : { - id: 'showMoreCodeActions', - label: localize('showMoreCodeActions', 'Show Disabled'), - enabled: true, - tooltip: '', - class: undefined, - run: () => this.toggleShowDisabled(true) - }); - } - - return actions; - } -} diff --git a/src/vs/editor/contrib/codeAction/common/types.ts b/src/vs/editor/contrib/codeAction/common/types.ts index 4fc2c763fab..05e239b8d4f 100644 --- a/src/vs/editor/contrib/codeAction/common/types.ts +++ b/src/vs/editor/contrib/codeAction/common/types.ts @@ -5,9 +5,9 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; -import { IDisposable } from 'vs/base/common/lifecycle'; import { Position } from 'vs/editor/common/core/position'; import * as languages from 'vs/editor/common/languages'; +import { ActionSet, IActionItem } from 'vs/platform/actionWidget/common/actionWidget'; export class CodeActionKind { private static readonly sep = '.'; @@ -188,12 +188,13 @@ export class CodeActionCommandArgs { ) { } } -export class CodeActionItem { +export class CodeActionItem implements IActionItem { constructor( public readonly action: languages.CodeAction, public readonly provider: languages.CodeActionProvider | undefined, - ) { } + ) { + } async resolve(token: CancellationToken): Promise { if (this.provider?.resolveCodeAction && !this.action.edit) { @@ -211,11 +212,7 @@ export class CodeActionItem { } } -export interface CodeActionSet extends IDisposable { +export interface CodeActionSet extends ActionSet { readonly validActions: readonly CodeActionItem[]; readonly allActions: readonly CodeActionItem[]; - readonly hasAutoFix: boolean; - - readonly documentation: readonly languages.Command[]; } - diff --git a/src/vs/editor/contrib/find/browser/findOptionsWidget.css b/src/vs/editor/contrib/find/browser/findOptionsWidget.css new file mode 100644 index 00000000000..2188fa9fa9d --- /dev/null +++ b/src/vs/editor/contrib/find/browser/findOptionsWidget.css @@ -0,0 +1,11 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-editor .findOptionsWidget { + background-color: var(--vscode-editorWidget-background); + color: var(--vscode-editorWidget-foreground); + box-shadow: 0 0 8px 2px var(--vscode-widget-shadow); + border: 2px solid var(--vscode-contrastBorder); +} diff --git a/src/vs/editor/contrib/find/browser/findOptionsWidget.ts b/src/vs/editor/contrib/find/browser/findOptionsWidget.ts index 7169c362279..0246d79df8e 100644 --- a/src/vs/editor/contrib/find/browser/findOptionsWidget.ts +++ b/src/vs/editor/contrib/find/browser/findOptionsWidget.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; +import 'vs/css!./findOptionsWidget'; import { CaseSensitiveToggle, RegexToggle, WholeWordsToggle } from 'vs/base/browser/ui/findinput/findInputToggles'; import { Widget } from 'vs/base/browser/ui/widget'; import { RunOnceScheduler } from 'vs/base/common/async'; @@ -11,8 +12,8 @@ import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPosit import { FIND_IDS } from 'vs/editor/contrib/find/browser/findModel'; import { FindReplaceState } from 'vs/editor/contrib/find/browser/findState'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { contrastBorder, editorWidgetBackground, editorWidgetForeground, inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; -import { IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground } from 'vs/platform/theme/common/colorRegistry'; +import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; export class FindOptionsWidget extends Widget implements IOverlayWidget { @@ -198,27 +199,3 @@ export class FindOptionsWidget extends Widget implements IOverlayWidget { this.regex.style(inputStyles); } } - - -registerThemingParticipant((theme, collector) => { - const widgetBackground = theme.getColor(editorWidgetBackground); - if (widgetBackground) { - collector.addRule(`.monaco-editor .findOptionsWidget { background-color: ${widgetBackground}; }`); - } - - const widgetForeground = theme.getColor(editorWidgetForeground); - if (widgetForeground) { - collector.addRule(`.monaco-editor .findOptionsWidget { color: ${widgetForeground}; }`); - } - - - const widgetShadowColor = theme.getColor(widgetShadow); - if (widgetShadowColor) { - collector.addRule(`.monaco-editor .findOptionsWidget { box-shadow: 0 0 8px 2px ${widgetShadowColor}; }`); - } - - const hcBorder = theme.getColor(contrastBorder); - if (hcBorder) { - collector.addRule(`.monaco-editor .findOptionsWidget { border: 2px solid ${hcBorder}; }`); - } -}); diff --git a/src/vs/editor/contrib/suggest/browser/suggestOvertypingCapturer.ts b/src/vs/editor/contrib/suggest/browser/suggestOvertypingCapturer.ts index 4a24f8807c1..a8f60b54fa9 100644 --- a/src/vs/editor/contrib/suggest/browser/suggestOvertypingCapturer.ts +++ b/src/vs/editor/contrib/suggest/browser/suggestOvertypingCapturer.ts @@ -13,15 +13,12 @@ export class OvertypingCapturer implements IDisposable { private readonly _disposables = new DisposableStore(); private _lastOvertyped: { value: string; multiline: boolean }[] = []; - private _empty: boolean = true; + private _locked: boolean = false; constructor(editor: ICodeEditor, suggestModel: SuggestModel) { this._disposables.add(editor.onWillType(() => { - if (!this._empty) { - return; - } - if (!editor.hasModel()) { + if (this._locked || !editor.hasModel()) { return; } @@ -37,6 +34,9 @@ export class OvertypingCapturer implements IDisposable { } } if (!willOvertype) { + if (this._lastOvertyped.length !== 0) { + this._lastOvertyped.length = 0; + } return; } @@ -50,18 +50,19 @@ export class OvertypingCapturer implements IDisposable { } this._lastOvertyped[i] = { value: model.getValueInRange(selection), multiline: selection.startLineNumber !== selection.endLineNumber }; } - this._empty = false; + })); + + this._disposables.add(suggestModel.onDidTrigger(e => { + this._locked = true; })); this._disposables.add(suggestModel.onDidCancel(e => { - if (!this._empty && !e.retrigger) { - this._empty = true; - } + this._locked = false; })); } getLastOvertypedInfo(idx: number): { value: string; multiline: boolean } | undefined { - if (!this._empty && idx >= 0 && idx < this._lastOvertyped.length) { + if (idx >= 0 && idx < this._lastOvertyped.length) { return this._lastOvertyped[idx]; } return undefined; diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/beforeEditPositionMapper.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/beforeEditPositionMapper.test.ts index 2fc81e3e176..609866a8bf5 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/beforeEditPositionMapper.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/beforeEditPositionMapper.test.ts @@ -27,8 +27,8 @@ suite('Bracket Pair Colorizer - BeforeEditPositionMapper', () => { '0 0 0 0 0 0 0 0 0 0 ', // the old line numbers '0 1 2 3 4 5 7 8 9 10 ', // the old columns - '0 0 0 0 0 0 0 0 0 0 ', // line count until next change - '4 3 2 1 0 0 3 2 1 0 ', // column count until next change + '0 0 0 0 0 0 ∞ ∞ ∞ ∞ ', // line count until next change + '4 3 2 1 0 0 ∞ ∞ ∞ ∞ ', // column count until next change ] ); }); @@ -50,8 +50,8 @@ suite('Bracket Pair Colorizer - BeforeEditPositionMapper', () => { '0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ', '0 1 2 3 4 5 4 5 6 7 6 7 8 9 10 ', - '0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ', - '2 1 0 0 0 0 2 1 0 0 4 3 2 1 0 ', + '0 0 0 0 0 0 0 0 0 0 ∞ ∞ ∞ ∞ ∞ ', + '2 1 0 0 0 0 2 1 0 0 ∞ ∞ ∞ ∞ ∞ ', ] ); }); @@ -75,16 +75,16 @@ suite('Bracket Pair Colorizer - BeforeEditPositionMapper', () => { '0 0 0 0 0 1 1 1 1 1 1 1 1 ', '0 1 2 3 4 3 4 5 6 7 8 9 10 ', - '0 0 0 0 0 1 1 1 1 1 1 1 1 ', - '3 2 1 0 0 10 10 10 10 10 10 10 10 ', + "0 0 0 0 0 ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ", + '3 2 1 0 0 ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ', // ------------------ '⁰ ¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ', '2 2 2 2 2 2 2 2 2 2 2 ', '0 1 2 3 4 5 6 7 8 9 10 ', - '0 0 0 0 0 0 0 0 0 0 0 ', - '10 9 8 7 6 5 4 3 2 1 0 ', + '∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ', + '∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ', ] ); }); @@ -109,16 +109,16 @@ suite('Bracket Pair Colorizer - BeforeEditPositionMapper', () => { '0 0 0 0 0 1 1 1 1 1 1 1 1 1 ', '0 1 2 3 4 0 1 2 3 4 5 7 8 9 ', - '0 0 0 0 0 0 0 0 0 0 0 1 1 1 ', - '3 2 1 0 0 5 4 3 2 1 0 10 10 10 ', + '0 0 0 0 0 0 0 0 0 0 0 ∞ ∞ ∞ ', + '3 2 1 0 0 5 4 3 2 1 0 ∞ ∞ ∞ ', // ------------------ '⁰ ¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ', '2 2 2 2 2 2 2 2 2 2 2 ', '0 1 2 3 4 5 6 7 8 9 10 ', - '0 0 0 0 0 0 0 0 0 0 0 ', - '10 9 8 7 6 5 4 3 2 1 0 ', + '∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ', + '∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ', ] ); }); @@ -144,8 +144,8 @@ suite('Bracket Pair Colorizer - BeforeEditPositionMapper', () => { '0 0 0 0 0 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 ', '0 1 2 3 4 0 1 2 3 4 5 7 8 4 5 6 7 8 9 10 ', - '0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ', - '3 2 1 0 0 5 4 3 2 1 0 1 0 6 5 4 3 2 1 0 ', + '0 0 0 0 0 0 0 0 0 0 0 0 0 ∞ ∞ ∞ ∞ ∞ ∞ ∞ ', + '3 2 1 0 0 5 4 3 2 1 0 1 0 ∞ ∞ ∞ ∞ ∞ ∞ ∞ ', ] ); }); @@ -175,8 +175,8 @@ suite('Bracket Pair Colorizer - BeforeEditPositionMapper', () => { '1 0 0 0 0 0 ', '0 5 6 7 8 9 ', - '0 0 0 0 0 0 ', - '0 4 3 2 1 0 ', + '0 ∞ ∞ ∞ ∞ ∞ ', + '0 ∞ ∞ ∞ ∞ ∞ ', ] ); }); @@ -215,8 +215,8 @@ suite('Bracket Pair Colorizer - BeforeEditPositionMapper', () => { '1 0 0 ', '0 8 9 ', - '0 0 0 ', - '0 1 0 ', + '0 ∞ ∞ ', + '0 ∞ ∞ ', ] ); }); @@ -247,16 +247,16 @@ suite('Bracket Pair Colorizer - BeforeEditPositionMapper', () => { '1 1 1 1 1 1 1 1 1 1 1 1 ', '0 1 2 1 2 3 4 5 6 7 8 9 ', - '0 0 0 1 1 1 1 1 1 1 1 1 ', - '0 0 0 10 10 10 10 10 10 10 10 10 ', + '0 0 0 ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ', + '0 0 0 ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ', // ------------------ '⁰ ¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ', '2 2 2 2 2 2 2 2 2 2 2 ', '0 1 2 3 4 5 6 7 8 9 10 ', - '0 0 0 0 0 0 0 0 0 0 0 ', - '10 9 8 7 6 5 4 3 2 1 0 ', + '∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ', + '∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ', ] ); }); @@ -306,8 +306,8 @@ suite('Bracket Pair Colorizer - BeforeEditPositionMapper', () => { '2 2 2 2 2 2 2 2 ', '0 4 5 6 7 8 9 10 ', - '0 0 0 0 0 0 0 0 ', - '0 6 5 4 3 2 1 0 ', + '0 ∞ ∞ ∞ ∞ ∞ ∞ ∞ ', + '0 ∞ ∞ ∞ ∞ ∞ ∞ ∞ ', ] ); }); @@ -320,7 +320,7 @@ function compute(inputArr: string[], edits: TextEdit[]): string[] { range: Range.fromPositions(lengthToPosition(e.startOffset), lengthToPosition(e.endOffset)) })))); - const mapper = new BeforeEditPositionMapper(edits, lengthOfString(newLines.join('\n'))); + const mapper = new BeforeEditPositionMapper(edits); const result = new Array(); @@ -342,9 +342,15 @@ function compute(inputArr: string[], edits: TextEdit[]): string[] { lineLine += rightPad('' + beforeObj.lineCount, 3); colLine += rightPad('' + beforeObj.columnCount, 3); - const dist = lengthToObj(mapper.getDistanceToNextChange(toLength(lineIdx, colIdx))); - lineDist += rightPad('' + dist.lineCount, 3); - colDist += rightPad('' + dist.columnCount, 3); + const distLen = mapper.getDistanceToNextChange(toLength(lineIdx, colIdx)); + if (distLen === null) { + lineDist += '∞ '; + colDist += '∞ '; + } else { + const dist = lengthToObj(distLen); + lineDist += rightPad('' + dist.lineCount, 3); + colDist += rightPad('' + dist.columnCount, 3); + } } result.push(lineStr); diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test.ts new file mode 100644 index 00000000000..7e81dbfbcf4 --- /dev/null +++ b/src/vs/editor/test/common/model/bracketPairColorizer/combineTextEditInfos.test.ts @@ -0,0 +1,145 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert = require('assert'); +import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; +import { Range } from 'vs/editor/common/core/range'; +import { TextEditInfo } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper'; +import { combineTextEditInfos } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/combineTextEditInfos'; +import { lengthAdd, lengthToObj, lengthToPosition, positionToLength, toLength } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length'; +import { TextModel } from 'vs/editor/common/model/textModel'; +import { createTextModel } from 'vs/editor/test/common/testTextModel'; + +suite('combineTextEditInfos', () => { + for (let seed = 0; seed < 50; seed++) { + test('test' + seed, () => { + runTest(seed); + }); + } +}); + +function runTest(seed: number) { + const rng = new MersenneTwister(seed); + + const str = 'abcde\nfghij\nklmno\npqrst\n'; + const textModelS0 = createTextModel(str); + + const edits1 = getRandomEditInfos(textModelS0, rng.nextIntRange(1, 4), rng); + const textModelS1 = createTextModel(textModelS0.getValue()); + textModelS1.applyEdits(edits1.map(e => toEdit(e))); + + const edits2 = getRandomEditInfos(textModelS1, rng.nextIntRange(1, 4), rng); + const textModelS2 = createTextModel(textModelS1.getValue()); + textModelS2.applyEdits(edits2.map(e => toEdit(e))); + + const combinedEdits = combineTextEditInfos(edits1, edits2); + for (const edit of combinedEdits) { + const range = Range.fromPositions(lengthToPosition(edit.startOffset), lengthToPosition(lengthAdd(edit.startOffset, edit.newLength))); + const value = textModelS2.getValueInRange(range); + if (!value.match(/^(L|C|\n)*$/)) { + throw new Error('Invalid edit: ' + value); + } + textModelS2.applyEdits([{ + range, + text: textModelS0.getValueInRange(Range.fromPositions(lengthToPosition(edit.startOffset), lengthToPosition(edit.endOffset))), + }]); + } + + assert.deepStrictEqual(textModelS2.getValue(), textModelS0.getValue()); + + textModelS0.dispose(); + textModelS1.dispose(); + textModelS2.dispose(); +} + +function getRandomEditInfos(textModel: TextModel, count: number, rng: MersenneTwister): TextEditInfo[] { + const edits: TextEditInfo[] = []; + let i = 0; + for (let j = 0; j < count; j++) { + edits.push(getRandomEdit(textModel, i, rng)); + i = textModel.getOffsetAt(lengthToPosition(edits[j].endOffset)); + } + return edits; +} + +function getRandomEdit(textModel: TextModel, rangeOffsetStart: number, rng: MersenneTwister): TextEditInfo { + const textModelLength = textModel.getValueLength(); + const offsetStart = rng.nextIntRange(rangeOffsetStart, textModelLength); + const offsetEnd = rng.nextIntRange(offsetStart, textModelLength); + + const lineCount = rng.nextIntRange(0, 3); + const columnCount = rng.nextIntRange(0, 5); + + return { + startOffset: positionToLength(textModel.getPositionAt(offsetStart)), + endOffset: positionToLength(textModel.getPositionAt(offsetEnd)), + newLength: toLength(lineCount, columnCount) + }; +} + +function toEdit(editInfo: TextEditInfo): ISingleEditOperation { + const l = lengthToObj(editInfo.newLength); + let text = ''; + + for (let i = 0; i < l.lineCount; i++) { + text += 'LLL\n'; + } + for (let i = 0; i < l.columnCount; i++) { + text += 'C'; + } + + return { + range: Range.fromPositions( + lengthToPosition(editInfo.startOffset), + lengthToPosition(editInfo.endOffset) + ), + text + }; +} + +// Generated by copilot +class MersenneTwister { + private readonly mt = new Array(624); + private index = 0; + + constructor(seed: number) { + this.mt[0] = seed >>> 0; + for (let i = 1; i < 624; i++) { + const s = this.mt[i - 1] ^ (this.mt[i - 1] >>> 30); + this.mt[i] = (((((s & 0xffff0000) >>> 16) * 0x6c078965) << 16) + (s & 0x0000ffff) * 0x6c078965 + i) >>> 0; + } + } + + public nextInt() { + if (this.index === 0) { + this.generateNumbers(); + } + + let y = this.mt[this.index]; + y = y ^ (y >>> 11); + y = y ^ ((y << 7) & 0x9d2c5680); + y = y ^ ((y << 15) & 0xefc60000); + y = y ^ (y >>> 18); + + this.index = (this.index + 1) % 624; + + return y >>> 0; + } + + public nextIntRange(start: number, endExclusive: number) { + const range = endExclusive - start; + return Math.floor(this.nextInt() / (0x100000000 / range)) + start; + } + + private generateNumbers() { + for (let i = 0; i < 624; i++) { + const y = (this.mt[i] & 0x80000000) + (this.mt[(i + 1) % 624] & 0x7fffffff); + this.mt[i] = this.mt[(i + 397) % 624] ^ (y >>> 1); + if ((y % 2) !== 0) { + this.mt[i] = this.mt[i] ^ 0x9908b0df; + } + } + } +} diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/getBracketPairsInRange.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/getBracketPairsInRange.test.ts index c938b730a46..2b05427a4ba 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/getBracketPairsInRange.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/getBracketPairsInRange.test.ts @@ -11,6 +11,10 @@ import { BracketPairInfo } from 'vs/editor/common/textModelBracketPairs'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { createModelServices, instantiateTextModel } from 'vs/editor/test/common/testTextModel'; import { TextModel } from 'vs/editor/common/model/textModel'; +import { TokenInfo, TokenizedDocument } from 'vs/editor/test/common/model/bracketPairColorizer/tokenizer.test'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { StandardTokenType } from 'vs/editor/common/encodedTokenAttributes'; +import { TokenizationRegistry } from 'vs/editor/common/languages'; suite('Bracket Pair Colorizer - getBracketPairsInRange', () => { @@ -18,6 +22,16 @@ suite('Bracket Pair Colorizer - getBracketPairsInRange', () => { const languageId = 'testLanguage'; const instantiationService = createModelServices(store); const languageConfigurationService = instantiationService.get(ILanguageConfigurationService); + const languageService = instantiationService.get(ILanguageService); + store.add(languageService.registerLanguage({ + id: languageId, + })); + + const encodedMode1 = languageService.languageIdCodec.encodeLanguageId(languageId); + const document = new TokenizedDocument([ + new TokenInfo(text, encodedMode1, StandardTokenType.Other, true) + ]); + store.add(TokenizationRegistry.register(languageId, document.getTokenizationSupport())); store.add(languageConfigurationService.register(languageId, { colorizedBracketPairs: [ @@ -26,13 +40,15 @@ suite('Bracket Pair Colorizer - getBracketPairsInRange', () => { ['(', ')'], ] })); - return store.add(instantiateTextModel(instantiationService, text, languageId)); + const textModel = store.add(instantiateTextModel(instantiationService, text, languageId)); + return textModel; } test('Basic 1', () => { disposeOnReturn(store => { const doc = new AnnotatedDocument(`{ ( [] ¹ ) [ ² { } ] () } []`); const model = createTextModelWithColorizedBracketPairs(store, doc.text); + model.tokenization.getLineTokens(1).getLanguageId(0); assert.deepStrictEqual( model.bracketPairs .getBracketPairsInRange(doc.range(1, 2)) diff --git a/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts b/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts index d1800b09f0d..ca5535c0eb5 100644 --- a/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts +++ b/src/vs/editor/test/common/model/bracketPairColorizer/tokenizer.test.ts @@ -124,7 +124,7 @@ function tokenToObj(token: Token, offset: Length, model: TextModel, keyProvider: }; } -class TokenizedDocument { +export class TokenizedDocument { private readonly tokensByLine: readonly TokenInfo[][]; constructor(tokens: TokenInfo[]) { const tokensByLine = new Array(); @@ -189,7 +189,7 @@ class TokenizedDocument { } } -class TokenInfo { +export class TokenInfo { constructor( public readonly text: string, public readonly languageId: LanguageId, diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionWidget.css b/src/vs/platform/actionWidget/browser/actionWidget.css similarity index 59% rename from src/vs/editor/contrib/codeAction/browser/codeActionWidget.css rename to src/vs/platform/actionWidget/browser/actionWidget.css index c788bfe5d1e..0c376cfd20e 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionWidget.css +++ b/src/vs/platform/actionWidget/browser/actionWidget.css @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.codeActionWidget { +.action-widget { font-size: 13px; border-radius: 0; min-width: 160px; @@ -16,23 +16,43 @@ color: var(--vscode-editorWidget-foreground); } -.codeActionWidget .monaco-list { +.context-view-block { + position: fixed; + cursor: initial; + left: 0; + top: 0; + width: 100%; + height: 100%; + z-index: -1; +} + +.context-view-pointerBlock { + position: fixed; + cursor: initial; + left: 0; + top: 0; + width: 100%; + height: 100%; + z-index: 2; +} + +.action-widget .monaco-list { user-select: none; -webkit-user-select: none; border: none !important; border-width: 0 !important; } -.codeActionWidget .monaco-list .monaco-scrollable-element .monaco-list-rows { +.action-widget .monaco-list .monaco-scrollable-element .monaco-list-rows { height: 100% !important; } -.codeActionWidget .monaco-list .monaco-scrollable-element { +.action-widget .monaco-list .monaco-scrollable-element { overflow: visible; } /** Styles for each row in the list element **/ -.codeActionWidget .monaco-list .monaco-list-row { +.action-widget .monaco-list .monaco-list-row { padding: 0 10px; white-space: nowrap; cursor: pointer; @@ -40,23 +60,23 @@ width: 100%; } -.codeActionWidget .monaco-list .monaco-list-row.code-action.focused:not(.option-disabled) { +.action-widget .monaco-list .monaco-list-row.action.focused:not(.option-disabled) { background-color: var(--vscode-quickInputList-focusBackground); color: var(--vscode-quickInputList-focusForeground); outline: 1px solid var(--vscode-menu-selectionBorder, transparent); outline-offset: -1px; } -.codeActionWidget .monaco-list-row.group-header { +.action-widget .monaco-list-row.group-header { color: var(--vscode-pickerGroup-foreground); font-weight: 600; } -.codeActionWidget .monaco-list .group-header, -.codeActionWidget .monaco-list .option-disabled, -.codeActionWidget .monaco-list .option-disabled:before, -.codeActionWidget .monaco-list .option-disabled .focused, -.codeActionWidget .monaco-list .option-disabled .focused:before { +.action-widget .monaco-list .group-header, +.action-widget .monaco-list .option-disabled, +.action-widget .monaco-list .option-disabled:before, +.action-widget .monaco-list .option-disabled .focused, +.action-widget .monaco-list .option-disabled .focused:before { cursor: default !important; -webkit-touch-callout: none; -webkit-user-select: none; @@ -68,25 +88,25 @@ outline: 0 solid !important; } -.codeActionWidget .monaco-list-row.code-action { +.action-widget .monaco-list-row.action { display: flex; gap: 6px; align-items: center; } -.codeActionWidget .monaco-list-row.code-action.option-disabled { +.action-widget .monaco-list-row.action.option-disabled { color: var(--vscode-disabledForeground); } -.codeActionWidget .monaco-list-row.code-action.option-disabled .codicon { +.action-widget .monaco-list-row.action.option-disabled .codicon { opacity: 0.4; } -.codeActionWidget .monaco-list-row.code-action:not(.option-disabled) .codicon { +.action-widget .monaco-list-row.action:not(.option-disabled) .codicon { color: inherit; } -.codeActionWidget .monaco-list-row.code-action .title { +.action-widget .monaco-list-row.action .title { flex: 1; overflow: hidden; text-overflow: ellipsis; @@ -94,23 +114,23 @@ /* Action bar */ -.codeActionWidget .codeActionWidget-action-bar { +.action-widget .action-widget-action-bar { margin-top: 4px; background-color: var(--vscode-editorHoverWidget-statusBarBackground); border-top: 1px solid var(--vscode-editorHoverWidget-border); } -.codeActionWidget .codeActionWidget-action-bar::before { +.action-widget .action-widget-action-bar::before { display: block; content: ""; width: 100%; } -.codeActionWidget .codeActionWidget-action-bar .actions-container { +.action-widget .action-widget-action-bar .actions-container { padding: 0 8px; } -.codeActionWidget-action-bar .action-label { +.action-widget-action-bar .action-label { color: var(--vscode-textLink-activeForeground); font-size: 12px; line-height: 22px; @@ -118,11 +138,11 @@ pointer-events: all; } -.codeActionWidget-action-bar .action-item { +.action-widget-action-bar .action-item { margin-right: 16px; pointer-events: none; } -.codeActionWidget-action-bar .action-label:hover { +.action-widget-action-bar .action-label:hover { background-color: transparent !important; } diff --git a/src/vs/platform/actionWidget/browser/actionWidget.ts b/src/vs/platform/actionWidget/browser/actionWidget.ts new file mode 100644 index 00000000000..996a586bebb --- /dev/null +++ b/src/vs/platform/actionWidget/browser/actionWidget.ts @@ -0,0 +1,619 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import * as dom from 'vs/base/browser/dom'; +import { IAnchor } from 'vs/base/browser/ui/contextview/contextview'; +import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; +import { IListEvent, IListMouseEvent, IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { List } from 'vs/base/browser/ui/list/listWidget'; +import { IAction } from 'vs/base/common/actions'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { OS } from 'vs/base/common/platform'; +import { localize } from 'vs/nls'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { Codicon } from 'vs/base/common/codicons'; +import 'vs/css!./actionWidget'; +import { createDecorator, IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { ActionSet, IActionItem, IActionKeybindingResolver } from 'vs/platform/actionWidget/common/actionWidget'; + +export const acceptSelectedActionCommand = 'acceptSelectedCodeAction'; +export const previewSelectedActionCommand = 'previewSelectedCodeAction'; + +export const ActionWidgetContextKeys = { + Visible: new RawContextKey('actionWidgetVisible', false, localize('actionWidgetVisible', "Whether the action widget list is visible")) +}; +export interface IRenderDelegate { + onHide(didCancel?: boolean): void; + onSelect(action: IActionItem, preview?: boolean): Promise; +} + +export interface IActionShowOptions { + readonly includeDisabledActions: boolean; + readonly fromLightbulb?: boolean; + readonly showHeaders?: boolean; +} + +export interface IListMenuItem { + item?: T; + kind: ActionListItemKind; + group?: { kind?: any; icon?: { codicon: Codicon; color?: string }; title: string }; + disabled?: boolean; + label?: string; +} + +export interface IActionList extends IDisposable { + hide(didCancel?: boolean): void; + focusPrevious(): void; + focusNext(): void; + layout(minWidth: number): void; + acceptSelected(preview?: boolean): void; + readonly domNode: HTMLElement; +} + +export interface IActionMenuTemplateData { + readonly container: HTMLElement; + readonly icon: HTMLElement; + readonly text: HTMLElement; + readonly keybinding: KeybindingLabel; +} + +export const enum ActionListItemKind { + Action = 'action', + Header = 'header' +} + +interface IHeaderTemplateData { + readonly container: HTMLElement; + readonly text: HTMLElement; +} + +export class HeaderRenderer> implements IListRenderer { + + get templateId(): string { return ActionListItemKind.Header; } + + renderTemplate(container: HTMLElement): IHeaderTemplateData { + container.classList.add('group-header'); + + const text = document.createElement('span'); + container.append(text); + + return { container, text }; + } + + renderElement(element: IListMenuItem, _index: number, templateData: IHeaderTemplateData): void { + if (!element.group) { + return; + } + templateData.text.textContent = element.group?.title; + } + + disposeTemplate(_templateData: IHeaderTemplateData): void { + // noop + } +} + + +export class ActionItemRenderer> implements IListRenderer { + + get templateId(): string { return 'action'; } + + constructor(private readonly _keybindingResolver: IActionKeybindingResolver | undefined, @IKeybindingService private readonly _keybindingService: IKeybindingService) { + } + + renderTemplate(container: HTMLElement): IActionMenuTemplateData { + container.classList.add(this.templateId); + + const icon = document.createElement('div'); + icon.className = 'icon'; + container.append(icon); + + const text = document.createElement('span'); + text.className = 'title'; + container.append(text); + + const keybinding = new KeybindingLabel(container, OS); + + return { container, icon, text, keybinding }; + } + + renderElement(element: T, _index: number, data: IActionMenuTemplateData): void { + if (element.group?.icon) { + data.icon.className = element.group.icon.codicon.classNames; + data.icon.style.color = element.group.icon.color ?? ''; + } else { + data.icon.className = Codicon.lightBulb.classNames; + data.icon.style.color = 'var(--vscode-editorLightBulb-foreground)'; + } + if (!element.item || !element.label) { + return; + } + data.text.textContent = stripNewlines(element.label); + const binding = this._keybindingResolver?.getResolver()(element.item); + if (binding) { + data.keybinding.set(binding); + } + + if (!binding) { + dom.hide(data.keybinding.element); + } else { + dom.show(data.keybinding.element); + } + const actionTitle = this._keybindingService.lookupKeybinding(acceptSelectedActionCommand)?.getLabel(); + const previewTitle = this._keybindingService.lookupKeybinding(previewSelectedActionCommand)?.getLabel(); + data.container.classList.toggle('option-disabled', element.disabled); + if (element.disabled) { + data.container.title = element.label; + } else if (actionTitle && previewTitle) { + data.container.title = localize({ key: 'label', comment: ['placeholders are keybindings, e.g "F2 to Apply, Shift+F2 to Preview"'] }, "{0} to Apply, {1} to Preview", actionTitle, previewTitle); + } else { + data.container.title = ''; + } + } + disposeTemplate(_templateData: IActionMenuTemplateData): void { + // noop + } +} + +export const IActionWidgetService = createDecorator('actionWidgetService'); +export interface IActionWidgetService { + readonly _serviceBrand: undefined; + show(user: string, toMenuItems: (inputQuickFixes: readonly any[], showHeaders: boolean) => IListMenuItem[], delegate: IRenderDelegate, actions: ActionSet, anchor: IAnchor, container: HTMLElement | undefined, options: IActionShowOptions): Promise; + hide(): void; + isVisible: boolean; + acceptSelected(preview?: boolean): void; + focusPrevious(): void; + focusNext(): void; +} + +export class ActionWidgetService extends Disposable implements IActionWidgetService { + declare readonly _serviceBrand: undefined; + get isVisible() { return ActionWidgetContextKeys.Visible.getValue(this._contextKeyService) || false; } + private _showDisabled = false; + private _currentShowingContext?: { + readonly user: string; + readonly toMenuItems: (inputItems: readonly any[], showHeaders: boolean) => IListMenuItem[]; + readonly options: IActionShowOptions; + readonly anchor: IAnchor; + readonly container: HTMLElement | undefined; + readonly actions: ActionSet; + readonly delegate: IRenderDelegate; + readonly resolver?: IActionKeybindingResolver; + }; + private _list = this._register(new MutableDisposable>()); + constructor(@ICommandService readonly _commandService: ICommandService, + @IContextViewService readonly contextViewService: IContextViewService, + @IKeybindingService readonly keybindingService: IKeybindingService, + @ITelemetryService readonly _telemetryService: ITelemetryService, + @IContextKeyService readonly _contextKeyService: IContextKeyService, + @IInstantiationService readonly _instantiationService: IInstantiationService) { + super(); + + } + + async show(user: string, toMenuItems: (inputQuickFixes: readonly IActionItem[], showHeaders: boolean) => IListMenuItem[], delegate: IRenderDelegate, actions: ActionSet, anchor: IAnchor, container: HTMLElement | undefined, options: IActionShowOptions, resolver?: IActionKeybindingResolver): Promise { + this._currentShowingContext = undefined; + const visibleContext = ActionWidgetContextKeys.Visible.bindTo(this._contextKeyService); + const list = this._instantiationService.createInstance(ActionList, user, actions.allActions, true, delegate, resolver, toMenuItems); + + const actionsToShow = options.includeDisabledActions && (this._showDisabled || actions.validActions.length === 0) ? actions.allActions : actions.validActions; + if (!actionsToShow.length) { + visibleContext.reset(); + return; + } + + this._currentShowingContext = { user, toMenuItems, delegate, actions, anchor, container, options, resolver }; + + this.contextViewService.showContextView({ + getAnchor: () => anchor, + render: (container: HTMLElement) => { + visibleContext.set(true); + return this._renderWidget(container, list, actions, options); + }, + onHide: (didCancel) => { + visibleContext.reset(); + return this._onWidgetClosed(didCancel); + }, + }, container, false); + } + + acceptSelected(preview?: boolean) { + this._list.value?.acceptSelected(preview); + } + + focusPrevious() { + this._list?.value?.focusPrevious(); + } + + focusNext() { + this._list?.value?.focusNext(); + } + + hide() { + this._list.value?.hide(); + this._list.clear(); + } + + clear() { + this._list.clear(); + } + + private _renderWidget(element: HTMLElement, list: ActionList, actions: ActionSet, options: IActionShowOptions): IDisposable { + const widget = document.createElement('div'); + widget.classList.add('action-widget'); + element.appendChild(widget); + + this._list.value = list; + if (this._list.value) { + widget.appendChild(this._list.value.domNode); + } else { + throw new Error('List has no value'); + } + const renderDisposables = new DisposableStore(); + + // Invisible div to block mouse interaction in the rest of the UI + const menuBlock = document.createElement('div'); + const block = element.appendChild(menuBlock); + block.classList.add('context-view-block'); + renderDisposables.add(dom.addDisposableListener(block, dom.EventType.MOUSE_DOWN, e => e.stopPropagation())); + + // Invisible div to block mouse interaction with the menu + const pointerBlockDiv = document.createElement('div'); + const pointerBlock = element.appendChild(pointerBlockDiv); + pointerBlock.classList.add('context-view-pointerBlock'); + + // Removes block on click INSIDE widget or ANY mouse movement + renderDisposables.add(dom.addDisposableListener(pointerBlock, dom.EventType.POINTER_MOVE, () => pointerBlock.remove())); + renderDisposables.add(dom.addDisposableListener(pointerBlock, dom.EventType.MOUSE_DOWN, () => pointerBlock.remove())); + + // Action bar + let actionBarWidth = 0; + if (!options.fromLightbulb) { + const actionBar = this._createActionBar('.action-widget-action-bar', actions, options); + if (actionBar) { + widget.appendChild(actionBar.getContainer().parentElement!); + renderDisposables.add(actionBar); + actionBarWidth = actionBar.getContainer().offsetWidth; + } + } + + const width = this._list.value?.layout(actionBarWidth); + widget.style.width = `${width}px`; + + const focusTracker = renderDisposables.add(dom.trackFocus(element)); + renderDisposables.add(focusTracker.onDidBlur(() => this.hide())); + + return renderDisposables; + } + + private _createActionBar(className: string, inputActions: ActionSet, options: IActionShowOptions): ActionBar | undefined { + const actions = this._getActionBarActions(inputActions, options); + if (!actions.length) { + return undefined; + } + + const container = dom.$(className); + const actionBar = new ActionBar(container); + actionBar.push(actions, { icon: false, label: true }); + return actionBar; + } + private _getActionBarActions(actions: ActionSet, options: IActionShowOptions): IAction[] { + const resultActions = actions.documentation.map((command): IAction => ({ + id: command.id, + label: command.title, + tooltip: command.tooltip ?? '', + class: undefined, + enabled: true, + run: () => this._commandService.executeCommand(command.id, ...(command.commandArguments ?? [])), + })); + + if (options.includeDisabledActions && actions.validActions.length > 0 && actions.allActions.length !== actions.validActions.length) { + resultActions.push(this._showDisabled ? { + id: 'hideMoreActions', + label: localize('hideMoreActions', 'Hide Disabled'), + enabled: true, + tooltip: '', + class: undefined, + run: () => this._toggleShowDisabled(false) + } : { + id: 'showMoreActions', + label: localize('showMoreActions', 'Show Disabled'), + enabled: true, + tooltip: '', + class: undefined, + run: () => this._toggleShowDisabled(true) + }); + } + return resultActions; + } + /** + * Toggles whether the disabled actions in the action widget are visible or not. + */ + private _toggleShowDisabled(newShowDisabled: boolean): void { + const previousCtx = this._currentShowingContext; + + this.hide(); + + this._showDisabled = newShowDisabled; + + if (previousCtx) { + this.show(previousCtx.user, previousCtx.toMenuItems, previousCtx.delegate, previousCtx.actions, previousCtx.anchor, previousCtx.container, previousCtx.options, previousCtx.resolver); + } + } + + private _onWidgetClosed(didCancel?: boolean): void { + this._currentShowingContext = undefined; + this._list.value?.hide(didCancel); + } + +} +registerSingleton(IActionWidgetService, ActionWidgetService, InstantiationType.Delayed); + +export class ActionList extends Disposable implements IActionList { + + readonly domNode: HTMLElement; + private readonly _list: List>; + + private readonly _actionLineHeight = 24; + private readonly _headerLineHeight = 26; + + private readonly _allMenuItems: IListMenuItem[]; + + private focusCondition(element: IListMenuItem): boolean { + return !element.disabled && element.kind === ActionListItemKind.Action; + } + + constructor( + user: string, + items: readonly T[], + showHeaders: boolean, + private readonly _delegate: IRenderDelegate, + resolver: IActionKeybindingResolver | undefined, + toMenuItems: (inputActions: readonly T[], showHeaders: boolean) => IListMenuItem[], + @IContextViewService private readonly _contextViewService: IContextViewService, + @IKeybindingService private readonly _keybindingService: IKeybindingService + ) { + super(); + this.domNode = document.createElement('div'); + this.domNode.classList.add('actionList'); + const virtualDelegate: IListVirtualDelegate> = { + getHeight: element => element.kind === 'header' ? this._headerLineHeight : this._actionLineHeight, + getTemplateId: element => element.kind + }; + this._list = new List(user, this.domNode, virtualDelegate, [new ActionItemRenderer>(resolver, this._keybindingService), new HeaderRenderer()], { + keyboardSupport: true, + accessibilityProvider: { + getAriaLabel: element => { + if (element.kind === 'action') { + let label = element.label ? stripNewlines(element?.label) : ''; + if (element.disabled) { + label = localize({ key: 'customQuickFixWidget.labels', comment: [`Action widget labels for accessibility.`] }, "{0}, Disabled Reason: {1}", label, element.disabled); + } + return label; + } + return null; + }, + getWidgetAriaLabel: () => localize({ key: 'customQuickFixWidget', comment: [`An action widget option`] }, "Action Widget"), + getRole: () => 'option', + getWidgetRole: () => user + }, + }); + + this._register(this._list.onMouseClick(e => this.onListClick(e))); + this._register(this._list.onMouseOver(e => this.onListHover(e))); + this._register(this._list.onDidChangeFocus(() => this._list.domFocus())); + this._register(this._list.onDidChangeSelection(e => this.onListSelection(e))); + + this._allMenuItems = toMenuItems(items, showHeaders); + this._list.splice(0, this._list.length, this._allMenuItems); + this.focusNext(); + } + + hide(didCancel?: boolean): void { + this._delegate.onHide(didCancel); + this._contextViewService.hideContextView(); + } + + layout(minWidth: number): number { + // Updating list height, depending on how many separators and headers there are. + const numHeaders = this._allMenuItems.filter(item => item.kind === 'header').length; + const height = this._allMenuItems.length * this._actionLineHeight; + const heightWithHeaders = height + numHeaders * this._headerLineHeight - numHeaders * this._actionLineHeight; + this._list.layout(heightWithHeaders); + + // For finding width dynamically (not using resize observer) + const itemWidths: number[] = this._allMenuItems.map((_, index): number => { + const element = document.getElementById(this._list.getElementID(index)); + if (element) { + element.style.width = 'auto'; + const width = element.getBoundingClientRect().width; + element.style.width = ''; + return width; + } + return 0; + }); + + // resize observer - can be used in the future since list widget supports dynamic height but not width + const width = Math.max(...itemWidths, minWidth); + this._list.layout(heightWithHeaders, width); + + this.domNode.style.height = `${heightWithHeaders}px`; + + this._list.domFocus(); + return width; + } + + focusPrevious() { + this._list.focusPrevious(1, true, undefined, this.focusCondition); + } + + focusNext() { + this._list.focusNext(1, true, undefined, this.focusCondition); + } + + acceptSelected(preview?: boolean) { + const focused = this._list.getFocus(); + if (focused.length === 0) { + return; + } + + const focusIndex = focused[0]; + const element = this._list.element(focusIndex); + if (!this.focusCondition(element)) { + return; + } + + const event = new UIEvent(preview ? 'previewSelectedCodeAction' : 'acceptSelectedCodeAction'); + this._list.setSelection([focusIndex], event); + } + + private onListSelection(e: IListEvent>): void { + if (!e.elements.length) { + return; + } + + const element = e.elements[0]; + if (element.item && this.focusCondition(element)) { + this._delegate.onSelect(element.item, e.browserEvent?.type === 'previewSelectedEventType'); + } else { + this._list.setSelection([]); + } + } + + private onListHover(e: IListMouseEvent>): void { + this._list.setFocus(typeof e.index === 'number' ? [e.index] : []); + } + + private onListClick(e: IListMouseEvent>): void { + if (e.element && this.focusCondition(e.element)) { + this._list.setFocus([]); + } + } +} + +export function stripNewlines(str: string): string { + return str.replace(/\r\n|\r|\n/g, ' '); +} + +const weight = KeybindingWeight.EditorContrib + 1000; + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'hideCodeActionWidget', + title: { + value: localize('hideCodeActionWidget.title', "Hide action widget"), + original: 'Hide action widget' + }, + precondition: ActionWidgetContextKeys.Visible, + keybinding: { + weight, + primary: KeyCode.Escape, + secondary: [KeyMod.Shift | KeyCode.Escape] + }, + }); + } + + run(accessor: ServicesAccessor): void { + accessor.get(IActionWidgetService).hide(); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'selectPrevCodeAction', + title: { + value: localize('selectPrevCodeAction.title', "Select previous action"), + original: 'Select previous action' + }, + precondition: ActionWidgetContextKeys.Visible, + keybinding: { + weight, + primary: KeyCode.UpArrow, + secondary: [KeyMod.CtrlCmd | KeyCode.UpArrow], + mac: { primary: KeyCode.UpArrow, secondary: [KeyMod.CtrlCmd | KeyCode.UpArrow, KeyMod.WinCtrl | KeyCode.KeyP] }, + } + }); + } + + run(accessor: ServicesAccessor): void { + accessor.get(IActionWidgetService).focusPrevious(); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'selectNextCodeAction', + title: { + value: localize('selectNextCodeAction.title', "Select next action"), + original: 'Select next action' + }, + precondition: ActionWidgetContextKeys.Visible, + keybinding: { + weight, + primary: KeyCode.DownArrow, + secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow], + mac: { primary: KeyCode.DownArrow, secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow, KeyMod.WinCtrl | KeyCode.KeyN] } + } + }); + } + + run(accessor: ServicesAccessor): void { + accessor.get(IActionWidgetService).focusNext(); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: acceptSelectedActionCommand, + title: { + value: localize('acceptSelected.title', "Accept selected action"), + original: 'Accept selected action' + }, + precondition: ActionWidgetContextKeys.Visible, + keybinding: { + weight, + primary: KeyCode.Enter, + secondary: [KeyMod.CtrlCmd | KeyCode.Period], + } + }); + } + + run(accessor: ServicesAccessor): void { + accessor.get(IActionWidgetService).acceptSelected(); + } +}); + +registerAction2(class extends Action2 { + constructor() { + super({ + id: previewSelectedActionCommand, + title: { + value: localize('previewSelected.title', "Preview selected action"), + original: 'Preview selected action' + }, + precondition: ActionWidgetContextKeys.Visible, + keybinding: { + weight, + primary: KeyMod.CtrlCmd | KeyCode.Enter, + } + }); + } + + run(accessor: ServicesAccessor): void { + accessor.get(IActionWidgetService).acceptSelected(true); + } +}); + diff --git a/src/vs/platform/actionWidget/common/actionWidget.ts b/src/vs/platform/actionWidget/common/actionWidget.ts new file mode 100644 index 00000000000..30301708742 --- /dev/null +++ b/src/vs/platform/actionWidget/common/actionWidget.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. + *--------------------------------------------------------------------------------------------*/ + +import { ResolvedKeybinding } from 'vs/base/common/keybindings'; +import { IDisposable } from 'vs/base/common/lifecycle'; + +export interface ActionSet extends IDisposable { + readonly validActions: readonly T[]; + readonly allActions: readonly T[]; + readonly hasAutoFix: boolean; + + readonly documentation: readonly { + id: string; + title: string; + tooltip?: string; + commandArguments?: any[]; + }[]; +} + +export interface IActionItem { + // TODO: Use generics + action: any; +} + +export interface IActionKeybindingResolver { + getResolver(): (action: any) => ResolvedKeybinding | undefined; +} diff --git a/src/vs/platform/dnd/browser/dnd.ts b/src/vs/platform/dnd/browser/dnd.ts index e836a18e4b3..9ff128373be 100644 --- a/src/vs/platform/dnd/browser/dnd.ts +++ b/src/vs/platform/dnd/browser/dnd.ts @@ -8,6 +8,7 @@ import { DragMouseEvent } from 'vs/base/browser/mouseEvent'; import { coalesce } from 'vs/base/common/arrays'; import { DeferredPromise } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; +import { ResourceMap } from 'vs/base/common/map'; import { parse } from 'vs/base/common/marshalling'; import { Schemas } from 'vs/base/common/network'; import { isWeb } from 'vs/base/common/platform'; @@ -121,7 +122,22 @@ export function extractEditorsDropData(e: DragEvent): Array(); + for (const editor of editors) { + if (!editor.resource) { + coalescedEditors.push(editor); + } else if (!seen.has(editor.resource)) { + coalescedEditors.push(editor); + seen.set(editor.resource, true); + } + } + + return coalescedEditors; } export async function extractEditorsAndFilesDropData(accessor: ServicesAccessor, e: DragEvent): Promise> { diff --git a/src/vs/platform/userDataSync/common/userDataSyncProfilesStorageService.ts b/src/vs/platform/userDataProfile/common/userDataProfileStorageService.ts similarity index 94% rename from src/vs/platform/userDataSync/common/userDataSyncProfilesStorageService.ts rename to src/vs/platform/userDataProfile/common/userDataProfileStorageService.ts index d90616fef82..b5a25d3b1df 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncProfilesStorageService.ts +++ b/src/vs/platform/userDataProfile/common/userDataProfileStorageService.ts @@ -25,8 +25,8 @@ export interface IStorageValue { readonly target: StorageTarget; } -export const IUserDataSyncProfilesStorageService = createDecorator('IUserDataSyncProfilesStorageService'); -export interface IUserDataSyncProfilesStorageService { +export const IUserDataProfileStorageService = createDecorator('IUserDataProfileStorageService'); +export interface IUserDataProfileStorageService { readonly _serviceBrand: undefined; /** @@ -54,7 +54,7 @@ export interface IUserDataSyncProfilesStorageService { withProfileScopedStorageService(profile: IUserDataProfile, fn: (storageService: IStorageService) => Promise): Promise; } -export abstract class AbstractUserDataSyncProfilesStorageService extends Disposable implements IUserDataSyncProfilesStorageService { +export abstract class AbstractUserDataProfileStorageService extends Disposable implements IUserDataProfileStorageService { _serviceBrand: undefined; diff --git a/src/vs/platform/userDataSync/electron-main/userDataSyncProfilesStorageIpc.ts b/src/vs/platform/userDataProfile/electron-main/userDataProfileStorageIpc.ts similarity index 98% rename from src/vs/platform/userDataSync/electron-main/userDataSyncProfilesStorageIpc.ts rename to src/vs/platform/userDataProfile/electron-main/userDataProfileStorageIpc.ts index 28d292e1515..e3ac3124a10 100644 --- a/src/vs/platform/userDataSync/electron-main/userDataSyncProfilesStorageIpc.ts +++ b/src/vs/platform/userDataProfile/electron-main/userDataProfileStorageIpc.ts @@ -7,7 +7,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { IServerChannel } from 'vs/base/parts/ipc/common/ipc'; import { ILogService } from 'vs/platform/log/common/log'; -import { IProfileStorageChanges, IProfileStorageValueChanges } from 'vs/platform/userDataSync/common/userDataSyncProfilesStorageService'; +import { IProfileStorageChanges, IProfileStorageValueChanges } from 'vs/platform/userDataProfile/common/userDataProfileStorageService'; import { loadKeyTargets, StorageScope, TARGET_KEY } from 'vs/platform/storage/common/storage'; import { IBaseSerializableStorageRequest } from 'vs/platform/storage/common/storageIpc'; import { IStorageMain } from 'vs/platform/storage/electron-main/storageMain'; diff --git a/src/vs/platform/userDataSync/electron-sandbox/userDataSyncProfilesStorageService.ts b/src/vs/platform/userDataProfile/electron-sandbox/userDataProfileStorageService.ts similarity index 89% rename from src/vs/platform/userDataSync/electron-sandbox/userDataSyncProfilesStorageService.ts rename to src/vs/platform/userDataProfile/electron-sandbox/userDataProfileStorageService.ts index 1d0679a115b..b0d9c5082fd 100644 --- a/src/vs/platform/userDataSync/electron-sandbox/userDataSyncProfilesStorageService.ts +++ b/src/vs/platform/userDataProfile/electron-sandbox/userDataProfileStorageService.ts @@ -8,12 +8,12 @@ import { MutableDisposable } from 'vs/base/common/lifecycle'; import { IStorageDatabase } from 'vs/base/parts/storage/common/storage'; import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/services'; import { ILogService } from 'vs/platform/log/common/log'; -import { AbstractUserDataSyncProfilesStorageService, IProfileStorageChanges, IUserDataSyncProfilesStorageService } from 'vs/platform/userDataSync/common/userDataSyncProfilesStorageService'; +import { AbstractUserDataProfileStorageService, IProfileStorageChanges, IUserDataProfileStorageService } from 'vs/platform/userDataProfile/common/userDataProfileStorageService'; import { isProfileUsingDefaultStorage, IStorageService } from 'vs/platform/storage/common/storage'; import { ApplicationStorageDatabaseClient, ProfileStorageDatabaseClient } from 'vs/platform/storage/common/storageIpc'; import { IUserDataProfile, IUserDataProfilesService, reviveProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; -export class UserDataSyncProfilesStorageService extends AbstractUserDataSyncProfilesStorageService implements IUserDataSyncProfilesStorageService { +export class UserDataProfileStorageService extends AbstractUserDataProfileStorageService implements IUserDataProfileStorageService { private readonly _onDidChange: Emitter; readonly onDidChange: Event; diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncProfilesStorageService.test.ts b/src/vs/platform/userDataProfile/test/common/userDataProfileStorageService.test.ts similarity index 90% rename from src/vs/platform/userDataSync/test/common/userDataSyncProfilesStorageService.test.ts rename to src/vs/platform/userDataProfile/test/common/userDataProfileStorageService.test.ts index a282d1e2d84..76075183f30 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncProfilesStorageService.test.ts +++ b/src/vs/platform/userDataProfile/test/common/userDataProfileStorageService.test.ts @@ -8,7 +8,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { InMemoryStorageDatabase, IStorageItemsChangeEvent, IUpdateRequest, Storage } from 'vs/base/parts/storage/common/storage'; -import { AbstractUserDataSyncProfilesStorageService, IUserDataSyncProfilesStorageService } from 'vs/platform/userDataSync/common/userDataSyncProfilesStorageService'; +import { AbstractUserDataProfileStorageService, IUserDataProfileStorageService } from 'vs/platform/userDataProfile/common/userDataProfileStorageService'; import { InMemoryStorageService, loadKeyTargets, StorageTarget, TARGET_KEY } from 'vs/platform/storage/common/storage'; import { IUserDataProfile, toUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; import { runWithFakedTimers } from 'vs/base/test/common/timeTravelScheduler'; @@ -26,7 +26,7 @@ class TestStorageDatabase extends InMemoryStorageDatabase { } } -export class TestUserDataSyncProfilesStorageService extends AbstractUserDataSyncProfilesStorageService implements IUserDataSyncProfilesStorageService { +export class TestUserDataProfileStorageService extends AbstractUserDataProfileStorageService implements IUserDataProfileStorageService { readonly onDidChange = Event.None; private databases = new Map(); @@ -46,11 +46,11 @@ suite('ProfileStorageService', () => { const disposables = new DisposableStore(); const profile = toUserDataProfile('test', 'test', URI.file('foo')); - let testObject: TestUserDataSyncProfilesStorageService; + let testObject: TestUserDataProfileStorageService; let storage: Storage; setup(async () => { - testObject = disposables.add(new TestUserDataSyncProfilesStorageService(new InMemoryStorageService())); + testObject = disposables.add(new TestUserDataProfileStorageService(new InMemoryStorageService())); storage = new Storage(await testObject.createStorageDatabase(profile)); await storage.init(); }); diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index c40c3a42a88..a98bbeff949 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -31,7 +31,7 @@ import { AbstractInitializer, AbstractSynchroniser, getSyncResourceLogLabel, IAc import { IMergeResult as IExtensionMergeResult, merge } from 'vs/platform/userDataSync/common/extensionsMerge'; import { IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions'; import { Change, IRemoteUserData, ISyncData, ISyncExtension, ISyncExtensionWithVersion, IUserDataSyncBackupStoreService, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, SyncResource, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync'; -import { IUserDataSyncProfilesStorageService } from 'vs/platform/userDataSync/common/userDataSyncProfilesStorageService'; +import { IUserDataProfileStorageService } from 'vs/platform/userDataProfile/common/userDataProfileStorageService'; type IExtensionResourceMergeResult = IAcceptResult & IExtensionMergeResult; @@ -128,7 +128,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse @ITelemetryService telemetryService: ITelemetryService, @IExtensionStorageService extensionStorageService: IExtensionStorageService, @IUriIdentityService uriIdentityService: IUriIdentityService, - @IUserDataSyncProfilesStorageService userDataSyncProfilesStorageService: IUserDataSyncProfilesStorageService, + @IUserDataProfileStorageService userDataProfileStorageService: IUserDataProfileStorageService, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super({ syncResource: SyncResource.Extensions, profile }, collection, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService, uriIdentityService); @@ -137,7 +137,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse Event.any( Event.filter(this.extensionManagementService.onDidInstallExtensions, (e => e.some(({ local }) => !!local))), Event.filter(this.extensionManagementService.onDidUninstallExtension, (e => !e.error)), - Event.filter(userDataSyncProfilesStorageService.onDidChange, e => e.valueChanges.some(({ profile, changes }) => this.syncResource.profile.id === profile.id && changes.some(change => change.key === DISABLED_EXTENSIONS_STORAGE_PATH))), + Event.filter(userDataProfileStorageService.onDidChange, e => e.valueChanges.some(({ profile, changes }) => this.syncResource.profile.id === profile.id && changes.some(change => change.key === DISABLED_EXTENSIONS_STORAGE_PATH))), extensionStorageService.onDidChangeExtensionStorageToSync)(() => this.triggerLocalChange())); } @@ -341,7 +341,7 @@ export class LocalExtensionsProvider { constructor( @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, - @IUserDataSyncProfilesStorageService private readonly userDataSyncProfilesStorageService: IUserDataSyncProfilesStorageService, + @IUserDataProfileStorageService private readonly userDataProfileStorageService: IUserDataProfileStorageService, @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, @IIgnoredExtensionsManagementService private readonly ignoredExtensionsManagementService: IIgnoredExtensionsManagementService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -510,7 +510,7 @@ export class LocalExtensionsProvider { } private async withProfileScopedServices(profile: IUserDataProfile, fn: (extensionEnablementService: IGlobalExtensionEnablementService, extensionStorageService: IExtensionStorageService) => Promise): Promise { - return this.userDataSyncProfilesStorageService.withProfileScopedStorageService(profile, + return this.userDataProfileStorageService.withProfileScopedStorageService(profile, async storageService => { const disposables = new DisposableStore(); const instantiationService = this.instantiationService.createChild(new ServiceCollection([IStorageService, storageService])); diff --git a/src/vs/platform/userDataSync/common/globalStateSync.ts b/src/vs/platform/userDataSync/common/globalStateSync.ts index ba15b967540..332918659bc 100644 --- a/src/vs/platform/userDataSync/common/globalStateSync.ts +++ b/src/vs/platform/userDataSync/common/globalStateSync.ts @@ -28,7 +28,7 @@ import { merge } from 'vs/platform/userDataSync/common/globalStateMerge'; import { ALL_SYNC_RESOURCES, Change, createSyncHeaders, getEnablementKey, IGlobalState, IRemoteUserData, IStorageValue, ISyncData, IUserData, IUserDataSyncBackupStoreService, IUserDataSynchroniser, IUserDataSyncLogService, IUserDataSyncEnablementService, IUserDataSyncStoreService, SyncResource, SYNC_SERVICE_URL_TYPE, UserDataSyncError, UserDataSyncErrorCode, UserDataSyncStoreType, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncStoreClient } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; -import { IUserDataSyncProfilesStorageService } from 'vs/platform/userDataSync/common/userDataSyncProfilesStorageService'; +import { IUserDataProfileStorageService } from 'vs/platform/userDataProfile/common/userDataProfileStorageService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; const argvStoragePrefx = 'globalState.argv.'; @@ -80,7 +80,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs constructor( profile: IUserDataProfile, collection: string | undefined, - @IUserDataSyncProfilesStorageService private readonly userDataSyncProfilesStorageService: IUserDataSyncProfilesStorageService, + @IUserDataProfileStorageService private readonly userDataProfileStorageService: IUserDataProfileStorageService, @IFileService fileService: IFileService, @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, @IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, @@ -100,7 +100,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs Event.any( /* Locale change */ Event.filter(fileService.onDidFilesChange, e => e.contains(this.environmentService.argvResource)), - Event.filter(userDataSyncProfilesStorageService.onDidChange, e => { + Event.filter(userDataProfileStorageService.onDidChange, e => { /* StorageTarget has changed in profile storage */ if (e.targetChanges.some(profile => this.syncResource.profile.id === profile.id)) { return true; @@ -282,7 +282,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs } private async getStorageKeys(lastSyncGlobalState: IGlobalState | null): Promise { - const storageData = await this.userDataSyncProfilesStorageService.readStorageData(this.syncResource.profile); + const storageData = await this.userDataProfileStorageService.readStorageData(this.syncResource.profile); const user: string[] = [], machine: string[] = []; for (const [key, value] of storageData) { if (value.target === StorageTarget.USER) { @@ -309,7 +309,7 @@ export class LocalGlobalStateProvider { constructor( @IFileService private readonly fileService: IFileService, @IEnvironmentService private readonly environmentService: IEnvironmentService, - @IUserDataSyncProfilesStorageService private readonly userDataSyncProfilesStorageService: IUserDataSyncProfilesStorageService, + @IUserDataProfileStorageService private readonly userDataProfileStorageService: IUserDataProfileStorageService, @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService ) { } @@ -324,7 +324,7 @@ export class LocalGlobalStateProvider { } } } - const storageData = await this.userDataSyncProfilesStorageService.readStorageData(profile); + const storageData = await this.userDataProfileStorageService.readStorageData(profile); for (const [key, value] of storageData) { if (value.value && value.target === StorageTarget.USER) { storage[key] = { version: 1, value: value.value }; @@ -349,7 +349,7 @@ export class LocalGlobalStateProvider { const syncResourceLogLabel = getSyncResourceLogLabel(SyncResource.GlobalState, profile); const argv: IStringDictionary = {}; const updatedStorage = new Map(); - const storageData = await this.userDataSyncProfilesStorageService.readStorageData(profile); + const storageData = await this.userDataProfileStorageService.readStorageData(profile); const handleUpdatedStorage = (keys: string[], storage?: IStringDictionary): void => { for (const key of keys) { if (key.startsWith(argvStoragePrefx)) { @@ -389,7 +389,7 @@ export class LocalGlobalStateProvider { if (updatedStorage.size) { this.logService.trace(`${syncResourceLogLabel}: Updating global state...`); - await this.userDataSyncProfilesStorageService.updateStorageData(profile, updatedStorage, StorageTarget.USER); + await this.userDataProfileStorageService.updateStorageData(profile, updatedStorage, StorageTarget.USER); this.logService.info(`${syncResourceLogLabel}: Updated global state`, [...updatedStorage.keys()]); } } diff --git a/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts b/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts index 110991b2d86..b7f6c141b02 100644 --- a/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts @@ -13,7 +13,7 @@ import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storag import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { GlobalStateSynchroniser } from 'vs/platform/userDataSync/common/globalStateSync'; import { IGlobalState, ISyncData, IUserDataSyncStoreService, SyncResource, SyncStatus } from 'vs/platform/userDataSync/common/userDataSync'; -import { IUserDataSyncProfilesStorageService } from 'vs/platform/userDataSync/common/userDataSyncProfilesStorageService'; +import { IUserDataProfileStorageService } from 'vs/platform/userDataProfile/common/userDataProfileStorageService'; import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient'; @@ -214,7 +214,7 @@ suite('GlobalStateSync', () => { await testClient.sync(); const syncedProfile = testClient.instantiationService.get(IUserDataProfilesService).profiles.find(p => p.id === profile.id)!; - const profileStorage = await testClient.instantiationService.get(IUserDataSyncProfilesStorageService).readStorageData(syncedProfile); + const profileStorage = await testClient.instantiationService.get(IUserDataProfileStorageService).readStorageData(syncedProfile); assert.strictEqual(profileStorage.get('a')?.value, 'value1'); assert.strictEqual(await readLocale(testClient), 'en'); @@ -241,7 +241,7 @@ suite('GlobalStateSync', () => { } async function updateUserStorageForProfile(key: string, value: string, profile: IUserDataProfile, client: UserDataSyncClient): Promise { - const storageService = client.instantiationService.get(IUserDataSyncProfilesStorageService); + const storageService = client.instantiationService.get(IUserDataProfileStorageService); const data = new Map(); data.set(key, value); await storageService.updateStorageData(profile, data, StorageTarget.USER); diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index dbbccba5373..7626dd2527e 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -43,8 +43,8 @@ import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyn import { UserDataSyncStoreManagementService, UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; import { InMemoryUserDataProfilesService, IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { NullPolicyService } from 'vs/platform/policy/common/policy'; -import { IUserDataSyncProfilesStorageService } from 'vs/platform/userDataSync/common/userDataSyncProfilesStorageService'; -import { TestUserDataSyncProfilesStorageService } from 'vs/platform/userDataSync/test/common/userDataSyncProfilesStorageService.test'; +import { IUserDataProfileStorageService } from 'vs/platform/userDataProfile/common/userDataProfileStorageService'; +import { TestUserDataProfileStorageService } from 'vs/platform/userDataProfile/test/common/userDataProfileStorageService.test'; export class UserDataSyncClient extends Disposable { @@ -94,7 +94,7 @@ export class UserDataSyncClient extends Disposable { const storageService = new TestStorageService(userDataProfilesService.defaultProfile); this.instantiationService.stub(IStorageService, this._register(storageService)); - this.instantiationService.stub(IUserDataSyncProfilesStorageService, this._register(new TestUserDataSyncProfilesStorageService(storageService))); + this.instantiationService.stub(IUserDataProfileStorageService, this._register(new TestUserDataProfileStorageService(storageService))); const configurationService = this._register(new ConfigurationService(userDataProfilesService.defaultProfile.settingsResource, fileService, new NullPolicyService(), logService)); await configurationService.initialize(); diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index c2f05b3598d..c71afbc4937 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -746,7 +746,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostCustomEditors.registerCustomEditorProvider(extension, viewType, provider, options); }, registerFileDecorationProvider(provider: vscode.FileDecorationProvider) { - return extHostDecorations.registerFileDecorationProvider(provider, extension.identifier); + return extHostDecorations.registerFileDecorationProvider(provider, extension); }, registerUriHandler(handler: vscode.UriHandler) { return extHostUrls.registerUriHandler(extension.identifier, handler); @@ -1257,6 +1257,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ExternalUriOpenerPriority: extHostTypes.ExternalUriOpenerPriority, FileChangeType: extHostTypes.FileChangeType, FileDecoration: extHostTypes.FileDecoration, + FileDecoration2: extHostTypes.FileDecoration, FileSystemError: extHostTypes.FileSystemError, FileType: files.FileType, FilePermission: files.FilePermission, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 23296caaca8..5f7f081e93c 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1947,7 +1947,7 @@ export interface DecorationRequest { readonly uri: UriComponents; } -export type DecorationData = [boolean, string, string, ThemeColor]; +export type DecorationData = [boolean, string, string | ThemeIcon, ThemeColor]; export type DecorationReply = { [id: number]: DecorationData }; export interface ExtHostDecorationsShape { diff --git a/src/vs/workbench/api/common/extHostDecorations.ts b/src/vs/workbench/api/common/extHostDecorations.ts index 8641121c9b1..12dcb1072a5 100644 --- a/src/vs/workbench/api/common/extHostDecorations.ts +++ b/src/vs/workbench/api/common/extHostDecorations.ts @@ -8,17 +8,18 @@ import { URI } from 'vs/base/common/uri'; import { MainContext, ExtHostDecorationsShape, MainThreadDecorationsShape, DecorationData, DecorationRequest, DecorationReply } from 'vs/workbench/api/common/extHost.protocol'; import { Disposable, FileDecoration } from 'vs/workbench/api/common/extHostTypes'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { ILogService } from 'vs/platform/log/common/log'; import { asArray, groupBy } from 'vs/base/common/arrays'; import { compare, count } from 'vs/base/common/strings'; import { dirname } from 'vs/base/common/path'; +import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; interface ProviderData { provider: vscode.FileDecorationProvider; - extensionId: ExtensionIdentifier; + extensionDescription: IExtensionDescription; } export class ExtHostDecorations implements ExtHostDecorationsShape { @@ -37,10 +38,10 @@ export class ExtHostDecorations implements ExtHostDecorationsShape { this._proxy = extHostRpc.getProxy(MainContext.MainThreadDecorations); } - registerFileDecorationProvider(provider: vscode.FileDecorationProvider, extensionId: ExtensionIdentifier): vscode.Disposable { + registerFileDecorationProvider(provider: vscode.FileDecorationProvider, extensionDescription: IExtensionDescription): vscode.Disposable { const handle = ExtHostDecorations._handlePool++; - this._provider.set(handle, { provider, extensionId }); - this._proxy.$registerDecorationProvider(handle, extensionId.value); + this._provider.set(handle, { provider, extensionDescription }); + this._proxy.$registerDecorationProvider(handle, extensionDescription.identifier.value); const listener = provider.onDidChangeFileDecorations && provider.onDidChangeFileDecorations(e => { if (!e) { @@ -55,7 +56,7 @@ export class ExtHostDecorations implements ExtHostDecorationsShape { // too many resources per event. pick one resource per folder, starting // with parent folders - this._logService.warn('[Decorations] CAPPING events from decorations provider', extensionId.value, array.length); + this._logService.warn('[Decorations] CAPPING events from decorations provider', extensionDescription.identifier.value, array.length); const mapped = array.map(uri => ({ uri, rank: count(uri.path, '/') })); const groups = groupBy(mapped, (a, b) => a.rank - b.rank || compare(a.uri.path, b.uri.path)); const picked: URI[] = []; @@ -89,7 +90,7 @@ export class ExtHostDecorations implements ExtHostDecorationsShape { } const result: DecorationReply = Object.create(null); - const { provider, extensionId } = this._provider.get(handle)!; + const { provider, extensionDescription: extensionId } = this._provider.get(handle)!; await Promise.all(requests.map(async request => { try { @@ -100,9 +101,12 @@ export class ExtHostDecorations implements ExtHostDecorationsShape { } try { FileDecoration.validate(data); + if (data.badge && typeof data.badge !== 'string') { + checkProposedApiEnabled(extensionId, 'codiconDecoration'); + } result[id] = [data.propagate, data.tooltip, data.badge, data.color]; } catch (e) { - this._logService.warn(`INVALID decoration from extension '${extensionId.value}': ${e}`); + this._logService.warn(`INVALID decoration from extension '${extensionId}': ${e}`); } } catch (err) { this._logService.error(err); diff --git a/src/vs/workbench/api/common/extHostLocalizationService.ts b/src/vs/workbench/api/common/extHostLocalizationService.ts index 611add6ffce..ea8ee4b770b 100644 --- a/src/vs/workbench/api/common/extHostLocalizationService.ts +++ b/src/vs/workbench/api/common/extHostLocalizationService.ts @@ -46,7 +46,7 @@ export class ExtHostLocalizationService implements ExtHostLocalizationShape { if (!str) { this.logService.warn(`Using default string since no string found in i18n bundle that has the key: ${key}`); } - return format2(str ?? key, (args ?? {})); + return format2(str ?? message, (args ?? {})); } getBundle(extensionId: string): { [key: string]: string } | undefined { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 00052e4f221..7e86810b3f2 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2659,6 +2659,14 @@ export class ThemeIcon { this.id = id; this.color = color; } + + static isThemeIcon(thing: any) { + if (typeof thing.id !== 'string') { + console.log('INVALID ThemeIcon, invalid id', thing.id); + return false; + } + return true; + } } ThemeIcon.File = new ThemeIcon('file'); ThemeIcon.Folder = new ThemeIcon('folder'); @@ -3283,7 +3291,7 @@ export enum ExtensionKind { export class FileDecoration { static validate(d: FileDecoration): boolean { - if (d.badge) { + if (typeof d.badge === 'string') { let len = nextCharLength(d.badge, 0); if (len < d.badge.length) { len += nextCharLength(d.badge, len); @@ -3291,6 +3299,10 @@ export class FileDecoration { if (d.badge.length > len) { throw new Error(`The 'badge'-property must be undefined or a short character`); } + } else if (d.badge) { + if (!ThemeIcon.isThemeIcon(d.badge)) { + throw new Error(`The 'badge'-property is not a valid ThemeIcon`); + } } if (!d.color && !d.badge && !d.tooltip) { throw new Error(`The decoration is empty`); @@ -3298,12 +3310,12 @@ export class FileDecoration { return true; } - badge?: string; + badge?: string | vscode.ThemeIcon; tooltip?: string; color?: vscode.ThemeColor; propagate?: boolean; - constructor(badge?: string, tooltip?: string, color?: ThemeColor) { + constructor(badge?: string | ThemeIcon, tooltip?: string, color?: ThemeColor) { this.badge = badge; this.tooltip = tooltip; this.color = color; diff --git a/src/vs/workbench/api/test/browser/extHostDecorations.test.ts b/src/vs/workbench/api/test/browser/extHostDecorations.test.ts index ee0b37ae01e..9666ee3e640 100644 --- a/src/vs/workbench/api/test/browser/extHostDecorations.test.ts +++ b/src/vs/workbench/api/test/browser/extHostDecorations.test.ts @@ -52,7 +52,7 @@ suite('ExtHostDecorations', function () { calledA = true; return new Promise(() => { }); } - }, nullExtensionDescription.identifier); + }, nullExtensionDescription); // always returns extHostDecorations.registerFileDecorationProvider({ @@ -61,7 +61,7 @@ suite('ExtHostDecorations', function () { calledB = true; return new Promise(resolve => resolve({ badge: 'H', tooltip: 'Hello' })); } - }, nullExtensionDescription.identifier); + }, nullExtensionDescription); const requests = [...providers.values()].map((handle, idx) => { diff --git a/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts b/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts index ec4ff95eef9..e7dc6440849 100644 --- a/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts +++ b/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts @@ -20,6 +20,7 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService import { fromNow } from 'vs/base/common/date'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { MarkdownRenderer } from 'vs/editor/contrib/markdownRenderer/browser/markdownRenderer'; +import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; export class BrowserDialogHandler implements IDialogHandler { @@ -117,7 +118,8 @@ export class BrowserDialogHandler implements IDialogHandler { buttonDetails: customOptions?.buttonDetails, checkboxLabel: checkbox?.label, checkboxChecked: checkbox?.checked, - inputs + inputs, + buttonStyles: defaultButtonStyles }); dialogDisposables.add(dialog); diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index afcd72c8aeb..1f54e4ec3e7 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -222,67 +222,69 @@ class State { update(update: StateDelta): StateChange { const change = new StateChange(); - if (update.type === 'selectionStatus') { - if (this._selectionStatus !== update.selectionStatus) { - this._selectionStatus = update.selectionStatus; - change.selectionStatus = true; - } - } + switch (update.type) { + case 'selectionStatus': + if (this._selectionStatus !== update.selectionStatus) { + this._selectionStatus = update.selectionStatus; + change.selectionStatus = true; + } + break; - if (update.type === 'indentation') { - if (this._indentation !== update.indentation) { - this._indentation = update.indentation; - change.indentation = true; - } - } + case 'indentation': + if (this._indentation !== update.indentation) { + this._indentation = update.indentation; + change.indentation = true; + } + break; - if (update.type === 'languageId') { - if (this._languageId !== update.languageId) { - this._languageId = update.languageId; - change.languageId = true; - } - } + case 'languageId': + if (this._languageId !== update.languageId) { + this._languageId = update.languageId; + change.languageId = true; + } + break; - if (update.type === 'encoding') { - if (this._encoding !== update.encoding) { - this._encoding = update.encoding; - change.encoding = true; - } - } + case 'encoding': + if (this._encoding !== update.encoding) { + this._encoding = update.encoding; + change.encoding = true; + } + break; - if (update.type === 'EOL') { - if (this._EOL !== update.EOL) { - this._EOL = update.EOL; - change.EOL = true; - } - } + case 'EOL': + if (this._EOL !== update.EOL) { + this._EOL = update.EOL; + change.EOL = true; + } + break; - if (update.type === 'tabFocusMode') { - if (this._tabFocusMode !== update.tabFocusMode) { - this._tabFocusMode = update.tabFocusMode; - change.tabFocusMode = true; - } - } + case 'tabFocusMode': + if (this._tabFocusMode !== update.tabFocusMode) { + this._tabFocusMode = update.tabFocusMode; + change.tabFocusMode = true; + } + break; - if (update.type === 'columnSelectionMode') { - if (this._columnSelectionMode !== update.columnSelectionMode) { - this._columnSelectionMode = update.columnSelectionMode; - change.columnSelectionMode = true; - } - } + case 'columnSelectionMode': + if (this._columnSelectionMode !== update.columnSelectionMode) { + this._columnSelectionMode = update.columnSelectionMode; + change.columnSelectionMode = true; + } + break; - if (update.type === 'screenReaderMode') { - if (this._screenReaderMode !== update.screenReaderMode) { - this._screenReaderMode = update.screenReaderMode; - change.screenReaderMode = true; - } - } + case 'screenReaderMode': + if (this._screenReaderMode !== update.screenReaderMode) { + this._screenReaderMode = update.screenReaderMode; + change.screenReaderMode = true; + } + break; - if (update.type === 'metadata') { - if (this._metadata !== update.metadata) { - this._metadata = update.metadata; - change.metadata = true; - } + case 'metadata': + if (this._metadata !== update.metadata) { + this._metadata = update.metadata; + change.metadata = true; + } + break; } return change; diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts index 33aae4de46f..8e0108c20df 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts @@ -37,6 +37,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ResourceEdit } from 'vs/editor/browser/services/bulkEditService'; import { ButtonBar } from 'vs/base/browser/ui/button/button'; +import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; const enum State { Data = 'data', @@ -150,11 +151,11 @@ export class BulkEditPane extends ViewPane { const buttonBar = new ButtonBar(buttonsContainer); this._disposables.add(buttonBar); - const btnConfirm = buttonBar.addButton({ supportIcons: true }); + const btnConfirm = buttonBar.addButton({ supportIcons: true, ...defaultButtonStyles }); btnConfirm.label = localize('ok', 'Apply'); btnConfirm.onDidClick(() => this.accept(), this, this._disposables); - const btnCancel = buttonBar.addButton({ /* secondary: true */ }); + const btnCancel = buttonBar.addButton(defaultButtonStyles /*{ secondary: true } */); btnCancel.label = localize('cancel', 'Discard'); btnCancel.onDidClick(() => this.discard(), this, this._disposables); diff --git a/src/vs/workbench/contrib/comments/browser/commentsView.ts b/src/vs/workbench/contrib/comments/browser/commentsView.ts index 858a940e5ce..b4b6e467613 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsView.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsView.ts @@ -36,6 +36,9 @@ import { CommentsFilters, CommentsFiltersChangeEvent } from 'vs/workbench/contri import { Memento, MementoObject } from 'vs/workbench/common/memento'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { FilterOptions } from 'vs/workbench/contrib/comments/browser/commentsFilterOptions'; +import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; +import { CommentThreadState } from 'vs/editor/common/languages'; +import { IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; const CONTEXT_KEY_HAS_COMMENTS = new RawContextKey('commentsView.hasComments', false); const VIEW_STORAGE_ID = 'commentsViewState'; @@ -47,9 +50,11 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { private messageBoxContainer!: HTMLElement; private commentsModel!: CommentsModel; private totalComments: number = 0; + private totalUnresolved = 0; private readonly hasCommentsContextKey: IContextKey; private readonly filter: Filter; readonly filters: CommentsFilters; + private readonly activity = this._register(new MutableDisposable()); private currentHeight = 0; private currentWidth = 0; @@ -73,6 +78,7 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { @ICommentService private readonly commentService: ICommentService, @ITelemetryService telemetryService: ITelemetryService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @IActivityService readonly activityService: IActivityService, @IStorageService readonly storageService: IStorageService ) { const stateMemento = new Memento(VIEW_STORAGE_ID, storageService); @@ -97,15 +103,6 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { }, this.contextKeyService)); this.filter = new Filter(new FilterOptions(this.filterWidget.getFilterText(), this.filters.showResolved, this.filters.showUnresolved)); - this._register(this.commentService.onDidSetAllCommentThreads(e => { - this.totalComments += e.commentThreads.length; - })); - - this._register(this.commentService.onDidUpdateCommentThreads(e => { - this.totalComments += e.added.length; - this.totalComments -= e.removed.length; - })); - this._register(this.filters.onDidChange((event: CommentsFiltersChangeEvent) => { if (event.showResolved || event.showUnresolved) { this.updateFilter(); @@ -114,6 +111,16 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { this._register(this.filterWidget.onDidChangeFilterText(() => this.updateFilter())); } + private updateBadge(unresolved: number) { + if (unresolved === this.totalUnresolved) { + return; + } + + this.totalUnresolved = unresolved; + const message = nls.localize('totalUnresolvedComments', '{0} Unresolved Comments', this.totalUnresolved); + this.activity.value = this.activityService.showViewActivity(this.id, { badge: new NumberBadge(this.totalUnresolved, () => message) }); + } + override saveState(): void { this.viewState['filter'] = this.filterWidget.getFilterText(); this.viewState['filterHistory'] = this.filterWidget.getHistory(); @@ -389,11 +396,36 @@ export class CommentsPanel extends FilterViewPane implements ICommentsView { private onAllCommentsChanged(e: IWorkspaceCommentThreadsEvent): void { this.commentsModel.setCommentThreads(e.ownerId, e.commentThreads); + + this.totalComments += e.commentThreads.length; + + let unresolved = 0; + for (const thread of e.commentThreads) { + if (thread.state === CommentThreadState.Unresolved) { + unresolved++; + } + } + this.updateBadge(unresolved); + this.refresh(); } private onCommentsUpdated(e: ICommentThreadChangedEvent): void { const didUpdate = this.commentsModel.updateCommentThreads(e); + + this.totalComments += e.added.length; + this.totalComments -= e.removed.length; + + let unresolved = 0; + for (const resource of this.commentsModel.resourceCommentThreads) { + for (const thread of resource.commentThreads) { + if (thread.threadState === CommentThreadState.Unresolved) { + unresolved++; + } + } + } + this.updateBadge(unresolved); + if (didUpdate) { this.refresh(); } diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts b/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts index a1fd2b83f39..c6908e0a7f1 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessions.contribution.ts @@ -131,7 +131,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo this.shouldShowViewsContext = EDIT_SESSIONS_SHOW_VIEW.bindTo(this.contextKeyService); this._register(this.fileService.registerProvider(EditSessionsFileSystemProvider.SCHEMA, new EditSessionsFileSystemProvider(this.editSessionsStorageService))); - this.lifecycleService.onWillShutdown((e) => e.join(this.autoStoreEditSession(), { id: 'autoStoreEditSession', label: localize('autoStoreEditSession', 'Storing current edit session...') })); + this.lifecycleService.onWillShutdown((e) => e.join(this.autoStoreEditSession(), { id: 'autoStoreWorkingChanges', label: localize('autoStoreWorkingChanges', 'Storing current working changes...') })); this._register(this.editSessionsStorageService.onDidSignIn(() => this.updateAccountsMenuBadge())); this._register(this.editSessionsStorageService.onDidSignOut(() => this.updateAccountsMenuBadge())); } @@ -149,10 +149,10 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo const shouldAutoResumeOnReload = this.configurationService.getValue('workbench.editSessions.autoResume') === 'onReload'; if (this.environmentService.editSessionId !== undefined) { - this.logService.info(`Resuming edit session, reason: found editSessionId ${this.environmentService.editSessionId} in environment service...`); + this.logService.info(`Resuming cloud changes, reason: found editSessionId ${this.environmentService.editSessionId} in environment service...`); await this.resumeEditSession(this.environmentService.editSessionId).finally(() => this.environmentService.editSessionId = undefined); } else if (shouldAutoResumeOnReload && this.editSessionsStorageService.isSignedIn) { - this.logService.info('Resuming edit session, reason: edit sessions enabled...'); + this.logService.info('Resuming cloud changes, reason: cloud changes enabled...'); // Attempt to resume edit session based on edit workspace identifier // Note: at this point if the user is not signed into edit sessions, // we don't want them to be prompted to sign in and should just return early @@ -203,7 +203,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo return this.accountsMenuBadgeDisposable.clear(); } - const badge = new NumberBadge(1, () => localize('check for pending edit sessions', 'Check for pending edit sessions')); + const badge = new NumberBadge(1, () => localize('check for pending cloud changes', 'Check for pending cloud changes')); this.accountsMenuBadgeDisposable.value = this.activityService.showAccountsActivity({ badge, priority: 1 }); } @@ -212,7 +212,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo await this.progressService.withProgress({ location: ProgressLocation.Window, type: 'syncing', - title: localize('store edit session', 'Storing edit session...') + title: localize('store working changes', 'Storing working changes...') }, async () => this.storeEditSession(false)); } } @@ -264,7 +264,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo constructor() { super({ id: 'workbench.editSessions.actions.showEditSessions', - title: { value: localize('show edit session', "Show Edit Sessions"), original: 'Show Edit Sessions' }, + title: { value: localize('show cloud changes', "Show Cloud Changes"), original: 'Show Cloud Changes' }, category: EDIT_SESSION_SYNC_CATEGORY, f1: true }); @@ -311,7 +311,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo ref = await that.progressService.withProgress({ location: ProgressLocation.Notification, type: 'syncing', - title: localize('store your edit session', 'Storing your edit session...') + title: localize('store your working changes', 'Storing your working changes...') }, async () => that.storeEditSession(false)); } @@ -335,7 +335,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo that.logService.info(`Opening ${uri.toString()}`); await that.openerService.open(uri, { openExternal: true }); } else if (ref === undefined && shouldStoreEditSession) { - that.logService.warn(`Failed to store edit session when invoking ${continueWorkingOnCommand.id}.`); + that.logService.warn(`Failed to store working changes when invoking ${continueWorkingOnCommand.id}.`); } } })); @@ -347,7 +347,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo constructor() { super({ id: 'workbench.editSessions.actions.resumeLatest', - title: { value: localize('resume latest.v2', "Resume Latest Edit Session"), original: 'Resume Latest Edit Session' }, + title: { value: localize('resume latest cloud changes', "Resume Latest Changes from Cloud"), original: 'Resume Latest Changes from Cloud' }, category: EDIT_SESSION_SYNC_CATEGORY, f1: true, }); @@ -373,7 +373,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo constructor() { super({ id: 'workbench.editSessions.actions.storeCurrent', - title: { value: localize('store current.v2', "Store Current Edit Session"), original: 'Store Current Edit Session' }, + title: { value: localize('store working changes in cloud', "Store Working Changes in Cloud"), original: 'Store Working Changes in Cloud' }, category: EDIT_SESSION_SYNC_CATEGORY, f1: true, }); @@ -382,7 +382,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo async run(accessor: ServicesAccessor): Promise { await that.progressService.withProgress({ location: ProgressLocation.Notification, - title: localize('storing edit session', 'Storing edit session...') + title: localize('storing working changes', 'Storing working changes...') }, async () => { type StoreEvent = {}; type StoreClassification = { @@ -403,7 +403,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo return; } - this.logService.info(ref !== undefined ? `Resuming edit session with ref ${ref}...` : 'Resuming edit session...'); + this.logService.info(ref !== undefined ? `Resuming changes from cloud with ref ${ref}...` : 'Resuming changes from cloud...'); if (silent && !(await this.editSessionsStorageService.initialize(false, true))) { return; @@ -412,18 +412,18 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo const data = await this.editSessionsStorageService.read(ref); if (!data) { if (ref === undefined && !silent) { - this.notificationService.info(localize('no edit session', 'There are no edit sessions to resume.')); + this.notificationService.info(localize('no cloud changes', 'There are no changes to resume from the cloud.')); } else if (ref !== undefined) { - this.notificationService.warn(localize('no edit session content for ref', 'Could not resume edit session contents for ID {0}.', ref)); + this.notificationService.warn(localize('no cloud changes for ref', 'Could not resume changes from the cloud for ID {0}.', ref)); } - this.logService.info(ref !== undefined ? `Aborting resuming edit session as no edit session content is available to be applied from ref ${ref}.` : `Aborting resuming edit session as no edit session content is available to be applied`); + this.logService.info(ref !== undefined ? `Aborting resuming changes from cloud as no edit session content is available to be applied from ref ${ref}.` : `Aborting resuming edit session as no edit session content is available to be applied`); return; } const editSession = data.editSession; ref = data.ref; if (editSession.version > EditSessionSchemaVersion) { - this.notificationService.error(localize('client too old', "Please upgrade to a newer version of {0} to resume this edit session.", this.productService.nameLong)); + this.notificationService.error(localize('client too old', "Please upgrade to a newer version of {0} to resume your working changes from the cloud.", this.productService.nameLong)); return; } @@ -442,8 +442,8 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo const result = await this.dialogService.show( Severity.Warning, conflictingChanges.length > 1 ? - localize('resume edit session warning many', 'Resuming your edit session will overwrite the following {0} files. Do you want to proceed?', conflictingChanges.length) : - localize('resume edit session warning 1', 'Resuming your edit session will overwrite {0}. Do you want to proceed?', basename(conflictingChanges[0].uri)), + localize('resume edit session warning many', 'Resuming your working changes from the cloud will overwrite the following {0} files. Do you want to proceed?', conflictingChanges.length) : + localize('resume edit session warning 1', 'Resuming your working changes from the cloud will overwrite {0}. Do you want to proceed?', basename(conflictingChanges[0].uri)), [cancel, yes], { detail: conflictingChanges.length > 1 ? getFileNamesMessage(conflictingChanges.map((c) => c.uri)) : undefined, @@ -468,7 +468,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo this.logService.info(`Deleted edit session with ref ${ref}.`); } catch (ex) { this.logService.error('Failed to resume edit session, reason: ', (ex as Error).toString()); - this.notificationService.error(localize('resume failed', "Failed to resume your edit session.")); + this.notificationService.error(localize('resume failed', "Failed to resume your working changes from the cloud.")); } } @@ -504,7 +504,7 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo // Surface partially matching edit session this.notificationService.prompt( Severity.Info, - localize('editSessionPartialMatch', 'You have a pending edit session for this workspace. Would you like to resume it?'), + localize('editSessionPartialMatch', 'You have pending working changes in the cloud for this workspace. Would you like to resume them?'), [{ label: localize('resume', 'Resume'), run: () => this.resumeEditSession(ref, false, true) }] ); } else { @@ -618,9 +618,9 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo } if (!hasEdits) { - this.logService.info('Skipping storing edit session as there are no edits to store.'); + this.logService.info('Skipped storing working changes in the cloud as there are no edits to store.'); if (fromStoreCommand) { - this.notificationService.info(localize('no edits to store', 'Skipped storing edit session as there are no edits to store.')); + this.notificationService.info(localize('no working changes to store', 'Skipped storing working changes in the cloud as there are no edits to store.')); } return undefined; } @@ -646,11 +646,11 @@ export class EditSessionsContribution extends Disposable implements IWorkbenchCo case UserDataSyncErrorCode.TooLarge: // Uploading a payload can fail due to server size limits this.telemetryService.publicLog2('editSessions.upload.failed', { reason: 'TooLarge' }); - this.notificationService.error(localize('payload too large', 'Your edit session exceeds the size limit and cannot be stored.')); + this.notificationService.error(localize('payload too large', 'Your working changes exceed the size limit and cannot be stored.')); break; default: this.telemetryService.publicLog2('editSessions.upload.failed', { reason: 'unknown' }); - this.notificationService.error(localize('payload failed', 'Your edit session cannot be stored.')); + this.notificationService.error(localize('payload failed', 'Your working changes cannot be stored.')); break; } } diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts b/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts index 84c9fc8b93c..1fe2147d551 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessionsStorageService.ts @@ -295,7 +295,7 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes private async getAccountPreference(fromContinueOn: boolean): Promise { const quickpick = this.quickInputService.createQuickPick(); quickpick.ok = false; - quickpick.placeholder = localize('choose account placeholder', "Select an account to turn on Edit Sessions"); + quickpick.placeholder = localize('choose account placeholder', "Select an account to store your working changes in the cloud"); quickpick.ignoreFocusOut = true; quickpick.items = await this.createQuickpickItems(fromContinueOn); @@ -459,7 +459,7 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes constructor() { super({ id: 'workbench.editSessions.actions.signIn', - title: localize('sign in', 'Turn on Edit Sessions...'), + title: localize('sign in', 'Turn on Cloud Changes...'), category: EDIT_SESSION_SYNC_CATEGORY, precondition: ContextKeyExpr.equals(EDIT_SESSIONS_SIGNED_IN_KEY, false), menu: [{ @@ -485,7 +485,7 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes constructor() { super({ id: 'workbench.editSessions.actions.resetAuth', - title: localize('reset auth.v3', 'Turn off Edit Sessions...'), + title: localize('reset auth.v3', 'Turn off Cloud Changes...'), category: EDIT_SESSION_SYNC_CATEGORY, precondition: ContextKeyExpr.equals(EDIT_SESSIONS_SIGNED_IN_KEY, true), menu: [{ @@ -502,8 +502,8 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes async run() { const result = await that.dialogService.confirm({ type: 'info', - message: localize('sign out of edit sessions clear data prompt.v2', 'Do you want to turn off Edit Sessions?'), - checkbox: { label: localize('delete all edit sessions.v2', 'Delete all stored data from the cloud.') }, + message: localize('sign out of cloud changes clear data prompt', 'Do you want to disable storing working changes in the cloud?'), + checkbox: { label: localize('delete all cloud changes', 'Delete all stored data from the cloud.') }, primaryButton: localize('clear data confirm', 'Yes'), }); if (result.confirmed) { diff --git a/src/vs/workbench/contrib/editSessions/browser/editSessionsViews.ts b/src/vs/workbench/contrib/editSessions/browser/editSessionsViews.ts index fbe5bed691c..9862af319a8 100644 --- a/src/vs/workbench/contrib/editSessions/browser/editSessionsViews.ts +++ b/src/vs/workbench/contrib/editSessions/browser/editSessionsViews.ts @@ -140,7 +140,7 @@ export class EditSessionsDataViews extends Disposable { constructor() { super({ id: 'workbench.editSessions.actions.deleteAll', - title: localize('workbench.editSessions.actions.deleteAll', "Delete All Edit Sessions"), + title: localize('workbench.editSessions.actions.deleteAll', "Delete All Working Changes from Cloud"), icon: Codicon.trash, menu: { id: MenuId.ViewTitle, @@ -153,7 +153,7 @@ export class EditSessionsDataViews extends Disposable { const dialogService = accessor.get(IDialogService); const editSessionStorageService = accessor.get(IEditSessionsStorageService); const result = await dialogService.confirm({ - message: localize('confirm delete all', 'Are you sure you want to permanently delete all edit sessions? You cannot undo this action.'), + message: localize('confirm delete all', 'Are you sure you want to permanently delete all stored changes from the cloud? You cannot undo this action.'), type: 'warning', title: EDIT_SESSIONS_TITLE }); diff --git a/src/vs/workbench/contrib/editSessions/common/editSessions.ts b/src/vs/workbench/contrib/editSessions/common/editSessions.ts index 99e5c8a16cc..24612aedd88 100644 --- a/src/vs/workbench/contrib/editSessions/common/editSessions.ts +++ b/src/vs/workbench/contrib/editSessions/common/editSessions.ts @@ -15,8 +15,8 @@ import { IResourceRefHandle } from 'vs/platform/userDataSync/common/userDataSync import { Event } from 'vs/base/common/event'; export const EDIT_SESSION_SYNC_CATEGORY: ILocalizedString = { - original: 'Edit Sessions', - value: localize('session sync', 'Edit Sessions') + original: 'Cloud Changes', + value: localize('cloud changes', 'Cloud Changes') }; export const IEditSessionsStorageService = createDecorator('IEditSessionsStorageService'); @@ -82,9 +82,9 @@ export const EDIT_SESSIONS_SIGNED_IN = new RawContextKey(EDIT_SESSIONS_ export const EDIT_SESSIONS_CONTAINER_ID = 'workbench.view.editSessions'; export const EDIT_SESSIONS_DATA_VIEW_ID = 'workbench.views.editSessions.data'; -export const EDIT_SESSIONS_TITLE = localize('edit sessions', 'Edit Sessions'); +export const EDIT_SESSIONS_TITLE = localize('cloud changes', 'Cloud Changes'); -export const EDIT_SESSIONS_VIEW_ICON = registerIcon('edit-sessions-view-icon', Codicon.cloudDownload, localize('editSessionViewIcon', 'View icon of the edit sessions view.')); +export const EDIT_SESSIONS_VIEW_ICON = registerIcon('edit-sessions-view-icon', Codicon.cloudDownload, localize('editSessionViewIcon', 'View icon of the cloud changes view.')); export const EDIT_SESSIONS_SHOW_VIEW = new RawContextKey('editSessionsShowView', false); diff --git a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts index 0f065457db4..b2dd9f62125 100644 --- a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts +++ b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts @@ -34,7 +34,7 @@ import { CancelablePromise, createCancelablePromise, Delayer } from 'vs/base/com import { IModelService } from 'vs/editor/common/services/model'; import { Range } from 'vs/editor/common/core/range'; import { applyCodeAction, ApplyCodeActionReason, getCodeActions } from 'vs/editor/contrib/codeAction/browser/codeAction'; -import { CodeActionKind, CodeActionTriggerSource, CodeActionSet } from 'vs/editor/contrib/codeAction/common/types'; +import { CodeActionKind, CodeActionSet, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/common/types'; import { ITextModel } from 'vs/editor/common/model'; import { IEditorService, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { SeverityIcon } from 'vs/platform/severityIcon/common/severityIcon'; diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts index 3829f745842..8bdb3976df4 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts @@ -442,14 +442,9 @@ export class MergeEditorModel extends EditorModel { } } - if (markInputAsHandled !== false) { - if (markInputAsHandled === true || markInputAsHandled === 1) { - existingState.handledInput1.set(true, transaction); - } - if (markInputAsHandled === true || markInputAsHandled === 2) { - existingState.handledInput2.set(true, transaction); - } - } + // always set conflict as handled + existingState.handledInput1.set(true, transaction); + existingState.handledInput2.set(true, transaction); } public resetDirtyConflictsToBase(): void { diff --git a/src/vs/workbench/contrib/search/browser/searchWidget.ts b/src/vs/workbench/contrib/search/browser/searchWidget.ts index a475f5ec796..f80bf8d2e57 100644 --- a/src/vs/workbench/contrib/search/browser/searchWidget.ts +++ b/src/vs/workbench/contrib/search/browser/searchWidget.ts @@ -292,7 +292,11 @@ export class SearchWidget extends Widget { buttonBackground: undefined, buttonBorder: undefined, buttonForeground: undefined, - buttonHoverBackground: undefined + buttonHoverBackground: undefined, + buttonSecondaryBackground: undefined, + buttonSecondaryForeground: undefined, + buttonSecondaryHoverBackground: undefined, + buttonSeparator: undefined }; this.toggleReplaceButton = this._register(new Button(parent, opts)); this.toggleReplaceButton.element.setAttribute('aria-expanded', 'false'); diff --git a/src/vs/workbench/contrib/terminal/browser/widgets/terminalQuickFixMenuItems.ts b/src/vs/workbench/contrib/terminal/browser/widgets/terminalQuickFixMenuItems.ts new file mode 100644 index 00000000000..9cb088a6a72 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/widgets/terminalQuickFixMenuItems.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 { IAction } from 'vs/base/common/actions'; +import { Codicon } from 'vs/base/common/codicons'; +import { CodeActionKind } from 'vs/editor/contrib/codeAction/common/types'; +import { localize } from 'vs/nls'; +import { IListMenuItem, ActionListItemKind } from 'vs/platform/actionWidget/browser/actionWidget'; +import { IActionItem } from 'vs/platform/actionWidget/common/actionWidget'; + +export class TerminalQuickFix implements IActionItem { + action: IAction; + disabled?: boolean; + title?: string; + constructor(action: IAction, title?: string, disabled?: boolean) { + this.action = action; + this.disabled = disabled; + this.title = title; + } +} + + +export function toMenuItems(inputQuickFixes: readonly TerminalQuickFix[], showHeaders: boolean): IListMenuItem[] { + const menuItems: IListMenuItem[] = []; + menuItems.push({ + kind: ActionListItemKind.Header, + group: { + kind: CodeActionKind.QuickFix, + title: localize('codeAction.widget.id.quickfix', 'Quick Fix...') + } + }); + for (const quickFix of showHeaders ? inputQuickFixes : inputQuickFixes.filter(i => !!i.action)) { + if (!quickFix.disabled && quickFix.action) { + menuItems.push({ + kind: ActionListItemKind.Action, + item: quickFix, + group: { + kind: CodeActionKind.QuickFix, + icon: getQuickFixIcon(quickFix), + title: quickFix.action.label + }, + disabled: false, + label: quickFix.title + }); + } + } + return menuItems; +} + +function getQuickFixIcon(quickFix: TerminalQuickFix): { codicon: Codicon } { + switch (quickFix.action.id) { + case 'quickFix.opener': + // TODO: if it's a file link, use the open file icon + return { codicon: Codicon.link }; + case 'quickFix.command': + return { codicon: Codicon.run }; + case 'quickFix.freePort': + return { codicon: Codicon.debugDisconnect }; + } + return { codicon: Codicon.lightBulb }; +} diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/quickFixAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/quickFixAddon.ts index 8ba6f79dba2..92d11cd95a5 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/quickFixAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/quickFixAddon.ts @@ -10,13 +10,10 @@ import { IAction } from 'vs/base/common/actions'; import { asArray } from 'vs/base/common/arrays'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { AudioCue, IAudioCueService } from 'vs/workbench/contrib/audioCues/browser/audioCueService'; -import { DecorationSelector, TerminalDecorationHoverManager, updateLayout } from 'vs/workbench/contrib/terminal/browser/xterm/decorationStyles'; -import { ITerminalQuickFixProviderSelector, ITerminalQuickFixService, TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ITerminalQuickFixProviderSelector, ITerminalQuickFixService } from 'vs/workbench/contrib/terminal/common/terminal'; +import { DecorationSelector, updateLayout } from 'vs/workbench/contrib/terminal/browser/xterm/decorationStyles'; import { IDecoration, Terminal } from 'xterm'; // Importing types is safe in any layer // eslint-disable-next-line local/code-import-patterns @@ -27,6 +24,10 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { ITerminalContributionService } from 'vs/workbench/contrib/terminal/common/terminalExtensionPoints'; import { ITerminalQuickFixOptions, IResolvedExtensionOptions, IUnresolvedExtensionOptions, ITerminalCommandSelector, ITerminalCommandMatchResult, TerminalQuickFixActionExtension, IInternalOptions } from 'vs/platform/terminal/common/terminal'; +import { IActionWidgetService, previewSelectedActionCommand } from 'vs/platform/actionWidget/browser/actionWidget'; +import { ActionSet } from 'vs/platform/actionWidget/common/actionWidget'; +import { TerminalQuickFix, toMenuItems } from 'vs/workbench/contrib/terminal/browser/widgets/terminalQuickFixMenuItems'; +import { ICommandService } from 'vs/platform/commands/common/commands'; const quickFixTelemetryTitle = 'terminal/quick-fix'; type QuickFixResultTelemetryEvent = { @@ -67,8 +68,6 @@ export class TerminalQuickFixAddon extends Disposable implements ITerminalAddon, private _decoration: IDecoration | undefined; - private readonly _terminalDecorationHoverService: TerminalDecorationHoverManager; - private _fixesShown: boolean = false; private _expectedCommands: string[] | undefined; private _fixId: string | undefined; @@ -76,16 +75,15 @@ export class TerminalQuickFixAddon extends Disposable implements ITerminalAddon, constructor( private readonly _capabilities: ITerminalCapabilityStore, @ITerminalQuickFixService private readonly _quickFixService: ITerminalQuickFixService, - @IContextMenuService private readonly _contextMenuService: IContextMenuService, + @ICommandService private readonly _commandService: ICommandService, @IConfigurationService private readonly _configurationService: IConfigurationService, - @IInstantiationService instantiationService: IInstantiationService, @IAudioCueService private readonly _audioCueService: IAudioCueService, - @IKeybindingService private readonly _keybindingService: IKeybindingService, @IOpenerService private readonly _openerService: IOpenerService, @ITelemetryService private readonly _telemetryService: ITelemetryService, @ILogService private readonly _logService: ILogService, @IExtensionService private readonly _extensionService: IExtensionService, - @ITerminalContributionService private readonly _terminalContributionService: ITerminalContributionService + @ITerminalContributionService private readonly _terminalContributionService: ITerminalContributionService, + @IActionWidgetService private readonly _actionWidgetService: IActionWidgetService ) { super(); const commandDetectionCapability = this._capabilities.get(TerminalCapability.CommandDetection); @@ -98,7 +96,6 @@ export class TerminalQuickFixAddon extends Disposable implements ITerminalAddon, } }); } - this._terminalDecorationHoverService = instantiationService.createInstance(TerminalDecorationHoverManager); for (const commandSelector of this._terminalContributionService.quickFixes) { this.registerCommandSelector(commandSelector); } @@ -240,8 +237,6 @@ export class TerminalQuickFixAddon extends Disposable implements ITerminalAddon, return; } this._decoration = decoration; - const kb = this._keybindingService.lookupKeybinding(TerminalCommandId.QuickFix); - const hoverLabel = kb ? localize('terminalQuickFixWithKb', "Show Quick Fixes ({0})", kb.getLabel()) : ''; const fixes = this._quickFixes; if (!fixes) { decoration.dispose(); @@ -255,9 +250,49 @@ export class TerminalQuickFixAddon extends Disposable implements ITerminalAddon, updateLayout(this._configurationService, e); this._audioCueService.playAudioCue(AudioCue.terminalQuickFix); this._register(dom.addDisposableListener(e, dom.EventType.CLICK, () => { - this._contextMenuService.showContextMenu({ getAnchor: () => e, getActions: () => fixes, autoSelectFirstItem: true }); + const rect = e.getBoundingClientRect(); + const anchor = { + x: rect.x, + y: rect.y, + width: rect.width, + height: rect.height + }; + // TODO: What's documentation do? Need a vscode command? + const documentation = fixes.map(f => { return { id: f.id, title: f.label, tooltip: f.tooltip }; }); + const actions = fixes.map(f => new TerminalQuickFix(f, f.label)); + const actionSet = { + // TODO: Documentation and actions are separate? + documentation, + allActions: actions, + hasAutoFix: false, + validActions: actions, + dispose: () => { } + } as ActionSet; + + const parentElement = e.parentElement?.parentElement?.parentElement?.parentElement; + if (!parentElement) { + return; + } + const delegate = { + onSelect: async (fix: TerminalQuickFix, preview?: boolean) => { + if (preview) { + this._commandService.executeCommand(previewSelectedActionCommand); + } else { + fix.action?.run(); + this._actionWidgetService.hide(); + } + }, + onHide: () => { + this._terminal?.focus(); + }, + }; + this._actionWidgetService.show('quickFixWidget', toMenuItems, delegate, actionSet, anchor, parentElement, + { + showHeaders: true, + includeDisabledActions: false, + fromLightbulb: true + }); })); - this._register(this._terminalDecorationHoverService.createHover(e, undefined, hoverLabel)); }); } } diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index cb8cde452b4..5f8f9e3ba2d 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -34,21 +34,10 @@ import { loadLocalResource, WebviewResourceResponse } from 'vs/workbench/contrib import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing'; import { areWebviewContentOptionsEqual, IWebview, WebviewContentOptions, WebviewExtensionDescription, WebviewInitInfo, WebviewMessageReceivedEvent, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview'; import { WebviewFindDelegate, WebviewFindWidget } from 'vs/workbench/contrib/webview/browser/webviewFindWidget'; -import { FromWebviewMessage, ToWebviewMessage } from 'vs/workbench/contrib/webview/browser/webviewMessages'; +import { FromWebviewMessage, KeyEvent, ToWebviewMessage } from 'vs/workbench/contrib/webview/browser/webviewMessages'; import { decodeAuthority, webviewGenericCspSource, webviewRootResourceAuthority } from 'vs/workbench/contrib/webview/common/webview'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -interface IKeydownEvent { - key: string; - keyCode: number; - code: string; - shiftKey: boolean; - altKey: boolean; - ctrlKey: boolean; - metaKey: boolean; - repeat: boolean; -} - interface WebviewContent { readonly html: string; readonly options: WebviewContentOptions; @@ -77,8 +66,8 @@ namespace WebviewState { } interface WebviewActionContext { - webview?: string; - [key: string]: unknown; + readonly webview?: string; + readonly [key: string]: unknown; } const webviewIdContext = 'webviewId'; @@ -696,7 +685,7 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD } } - private handleKeyEvent(type: 'keydown' | 'keyup', event: IKeydownEvent) { + private handleKeyEvent(type: 'keydown' | 'keyup', event: KeyEvent) { // Create a fake KeyboardEvent from the data provided const emulatedKeyboardEvent = new KeyboardEvent(type, event); // Force override the target diff --git a/src/vs/workbench/services/decorations/browser/decorationsService.ts b/src/vs/workbench/services/decorations/browser/decorationsService.ts index 5670058741f..4502eeeff06 100644 --- a/src/vs/workbench/services/decorations/browser/decorationsService.ts +++ b/src/vs/workbench/services/decorations/browser/decorationsService.ts @@ -136,7 +136,7 @@ class DecorationRule { createCSSRule( `.${this.iconBadgeClassName}::after`, `content: '${definition.fontCharacter}'; - color: ${getColor(color)}; + color: ${icon.color ? getColor(icon.color.id) : getColor(color)}; font-family: ${asCSSPropertyValue(definition.font?.id ?? 'codicon')}; font-size: 16px; margin-right: 14px; diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 5abce9f9b74..e24334df648 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -7,6 +7,7 @@ export const allApiProposals = Object.freeze({ authSession: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authSession.d.ts', + codiconDecoration: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.codiconDecoration.d.ts', commentsResolvedState: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentsResolvedState.d.ts', contribCommentPeekContext: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentPeekContext.d.ts', contribEditSessions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribEditSessions.d.ts', diff --git a/src/vs/workbench/services/progress/browser/progressService.ts b/src/vs/workbench/services/progress/browser/progressService.ts index c3a0416f735..2e423fc7c1a 100644 --- a/src/vs/workbench/services/progress/browser/progressService.ts +++ b/src/vs/workbench/services/progress/browser/progressService.ts @@ -26,6 +26,7 @@ import { parseLinkedText } from 'vs/base/common/linkedText'; import { IViewsService, IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { stripIcons } from 'vs/base/common/iconLabels'; +import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; export class ProgressService extends Disposable implements IProgressService { @@ -563,7 +564,8 @@ export class ProgressService extends Disposable implements IProgressService { EventHelper.stop(event, true); } } - } + }, + buttonStyles: defaultButtonStyles } ); diff --git a/src/vs/workbench/services/userDataSync/browser/userDataSyncProfilesStorageService.ts b/src/vs/workbench/services/userDataProfile/browser/userDataProfileStorageService.ts similarity index 85% rename from src/vs/workbench/services/userDataSync/browser/userDataSyncProfilesStorageService.ts rename to src/vs/workbench/services/userDataProfile/browser/userDataProfileStorageService.ts index 7cec5ba8d7d..8fe279fa9dd 100644 --- a/src/vs/workbench/services/userDataSync/browser/userDataSyncProfilesStorageService.ts +++ b/src/vs/workbench/services/userDataProfile/browser/userDataProfileStorageService.ts @@ -7,13 +7,13 @@ import { Emitter, Event } from 'vs/base/common/event'; import { IStorageDatabase } from 'vs/base/parts/storage/common/storage'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; -import { AbstractUserDataSyncProfilesStorageService, IProfileStorageChanges, IUserDataSyncProfilesStorageService } from 'vs/platform/userDataSync/common/userDataSyncProfilesStorageService'; +import { AbstractUserDataProfileStorageService, IProfileStorageChanges, IUserDataProfileStorageService } from 'vs/platform/userDataProfile/common/userDataProfileStorageService'; import { isProfileUsingDefaultStorage, IStorageService, IStorageValueChangeEvent, StorageScope } from 'vs/platform/storage/common/storage'; import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; import { IndexedDBStorageDatabase } from 'vs/workbench/services/storage/browser/storageService'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; -export class UserDataSyncProfilesStorageService extends AbstractUserDataSyncProfilesStorageService implements IUserDataSyncProfilesStorageService { +export class UserDataProfileStorageService extends AbstractUserDataProfileStorageService implements IUserDataProfileStorageService { private readonly _onDidChange = this._register(new Emitter()); readonly onDidChange: Event = this._onDidChange.event; @@ -45,4 +45,4 @@ export class UserDataSyncProfilesStorageService extends AbstractUserDataSyncProf } } -registerSingleton(IUserDataSyncProfilesStorageService, UserDataSyncProfilesStorageService, InstantiationType.Delayed); +registerSingleton(IUserDataProfileStorageService, UserDataProfileStorageService, InstantiationType.Delayed); diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index 39abd114941..d3710e7515d 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -61,7 +61,7 @@ import 'vs/workbench/services/tunnel/browser/tunnelService'; import 'vs/workbench/services/files/browser/elevatedFileService'; import 'vs/workbench/services/workingCopy/browser/workingCopyHistoryService'; import 'vs/workbench/services/userDataSync/browser/webUserDataSyncEnablementService'; -import 'vs/workbench/services/userDataSync/browser/userDataSyncProfilesStorageService'; +import 'vs/workbench/services/userDataProfile/browser/userDataProfileStorageService'; import 'vs/workbench/services/configurationResolver/browser/configurationResolverService'; import 'vs/platform/extensionResourceLoader/browser/extensionResourceLoaderService'; diff --git a/src/vscode-dts/vscode.proposed.codiconDecoration.d.ts b/src/vscode-dts/vscode.proposed.codiconDecoration.d.ts new file mode 100644 index 00000000000..2a0fc4578ba --- /dev/null +++ b/src/vscode-dts/vscode.proposed.codiconDecoration.d.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/135591 @alexr00 + + // export interface FileDecorationProvider { + // provideFileDecoration(uri: Uri, token: CancellationToken): ProviderResult; + // } + + /** + * A file decoration represents metadata that can be rendered with a file. + */ + export class FileDecoration2 { + /** + * A very short string that represents this decoration. + */ + badge?: string | ThemeIcon; + + /** + * A human-readable tooltip for this decoration. + */ + tooltip?: string; + + /** + * The color of this decoration. + */ + color?: ThemeColor; + + /** + * A flag expressing that this decoration should be + * propagated to its parents. + */ + propagate?: boolean; + + /** + * Creates a new decoration. + * + * @param badge A letter that represents the decoration. + * @param tooltip The tooltip of the decoration. + * @param color The color of the decoration. + */ + constructor(badge?: string | ThemeIcon, tooltip?: string, color?: ThemeColor); + } +} diff --git a/yarn.lock b/yarn.lock index d0f163f7bcb..0eef274f3c3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3584,10 +3584,10 @@ decompress@^4.2.1: pify "^2.3.0" strip-dirs "^2.0.0" -deemon@^1.7.2: - version "1.7.2" - resolved "https://registry.yarnpkg.com/deemon/-/deemon-1.7.2.tgz#8b04aff88bf5752ba6dfea4242758b98ec6fd57c" - integrity sha512-KxR0zGNLbeNZyXLLrAiRtj5f3gpXn85UEgrCWD49IlPzSPQg9iFR/uQnEKQzvt9kP5latEHQkM+SNcnJOBfP3Q== +deemon@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/deemon/-/deemon-1.8.0.tgz#7b9498905634a89bfe6db11b5edf779e2fac5248" + integrity sha512-qcuSMls/W5DdoEKKAF0PiNQrc8+tItFjvszfjNm1YqNv1p5wwEt+6qILA9sws6eM81nmNwD38ducqlgIXzQlsQ== dependencies: bl "^4.0.2" tree-kill "^1.2.2"