diff --git a/.vscode/settings.json b/.vscode/settings.json index 5d2930c5a92..7eefe0c57f6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -36,6 +36,7 @@ "files.readonlyInclude": { "**/node_modules/**": true, "**/yarn.lock": true, + "**/Cargo.lock": true, "src/vs/workbench/workbench.web.main.css": true, "src/vs/workbench/workbench.desktop.main.css": true, "src/vs/workbench/workbench.desktop.main.nls.js": true, diff --git a/.yarnrc b/.yarnrc index 3af2059dbd7..4b56fb47de5 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,5 +1,5 @@ disturl "https://electronjs.org/headers" -target "22.3.14" -ms_build_id "21893604" +target "22.3.17" +ms_build_id "22432899" runtime "electron" build_from_source "true" diff --git a/build/checksums/electron.txt b/build/checksums/electron.txt index beb2b9730b0..d5265cab426 100644 --- a/build/checksums/electron.txt +++ b/build/checksums/electron.txt @@ -1,27 +1,27 @@ -3ba067c6f338f9a525c4b697e9cf8e3c3b3d9f6abfdfb11fba47e053da0f3496 *electron-v22.3.14-darwin-arm64-symbols.zip -c08bf19e11c006346b210585cf0803cd0b07107a362a2414cc185f6a228afbf2 *electron-v22.3.14-darwin-arm64.zip -72ced94e7230d3138dd84acbf38dc593d4a93ec796a3a478f99aa6974030d79c *electron-v22.3.14-darwin-x64-symbols.zip -77c1c96411326b00d3ef7c9f6af96a3b4c2fa2314196fce3374fcf734dd8dc67 *electron-v22.3.14-darwin-x64.zip -d847c59f3835749dcdd5376daefb3a5992f1ed5d7693f871328296e1388fb69d *electron-v22.3.14-linux-arm64-symbols.zip -95bb9ee160c60b50ff25b307fb8bc36bdb5297d43c6e366f0b835f36c4f327c9 *electron-v22.3.14-linux-arm64.zip -38a51d81f9ffe6e2ebf25844999fddbeb4edc63ae22136af1502db373bb024ab *electron-v22.3.14-linux-armv7l-symbols.zip -bf589c74f07fe11586ffcf8c122d34b91c5ced08d54532ee883d1e025b6d1b02 *electron-v22.3.14-linux-armv7l.zip -28d0eda61ea736375c549d0955f36b7d3e3c2019453ef83d793dae8b0d74f461 *electron-v22.3.14-linux-x64-symbols.zip -89b72e40fb8b9106deda3e6ffa30dd80beaa8f2e2a9d037b55c034a5a27a7b60 *electron-v22.3.14-linux-x64.zip -b9ba15fcf7c60cf57e95fae731bc0c336e131ed4fac91b4c59d50a28407ca0b0 *electron-v22.3.14-win32-arm64-pdb.zip -9f375d01feeb9e28f9c0913a4e22be900c0a7ff4e51449bb9b859ce1bd18f9f3 *electron-v22.3.14-win32-arm64-symbols.zip -17e354aca0683f79d79f7fa7ecfa8a4381b356d04fa45ec0aa85b5f048151c10 *electron-v22.3.14-win32-arm64.zip -900ca316ce939547ab62847c8833a78c1002a69b936be7e9af328a3518a7d379 *electron-v22.3.14-win32-ia32-pdb.zip -90af7a48b4e722a3436b6a8893540fb746d99b4832ca48c355a63fa0930f6446 *electron-v22.3.14-win32-ia32-symbols.zip -487d811c7cf3282f4c3a17b5ab7ab1fd71dbc585449d77da3a9bf052657ac4ad *electron-v22.3.14-win32-ia32.zip -41ce6c3d87c89f6b48aac74649657a120c28c78513908996dc20e57a640d4653 *electron-v22.3.14-win32-x64-pdb.zip -57b35bfa186b64a9dd1eb2bb85141bb998d0378bb20ac8038718b41d16deb978 *electron-v22.3.14-win32-x64-symbols.zip -f45eba3faa7e10fb1c6e5cf044dd42733a7c8cb455de57647b74e7510b0b94b6 *electron-v22.3.14-win32-x64.zip -16a75de6e3e4643589237e6e1c680c43b4e77fe04918bfbe4408775b7e616afc *ffmpeg-v22.3.14-darwin-arm64.zip -92db0c163c326d33a516ebfc56c7bd4faae9456f4238dde916c580b459b8dc8d *ffmpeg-v22.3.14-darwin-x64.zip -59d2e2b2f2cc515a86a4e0cfd1116d10a8b25a8d58d45bb04de3512e156c944b *ffmpeg-v22.3.14-linux-arm64.zip -b9d3b227bee17666d395ee7882ef477a733c3eeef3f1d9f2e3616d2d02eb3376 *ffmpeg-v22.3.14-linux-armv7l.zip -fa07ef910b23a4ef4b6761bc16d20c0e70ff0259325c4d523129e2d9c5084174 *ffmpeg-v22.3.14-linux-x64.zip -7f744b657ae7c26f80cae0f2771a00edd368350229b85118a246573987dd6ff1 *ffmpeg-v22.3.14-win32-arm64.zip -562e04d2cf1c970b6128d66d08dfe8d88a28e54adf599293eee2bd6c292fd16b *ffmpeg-v22.3.14-win32-ia32.zip -f69510384ef912fd9b4961f97357789a4a36e8df6ff382aeeab23fbb063def9a *ffmpeg-v22.3.14-win32-x64.zip +9ba54e68520fe94b15ab6224333f5e63538f78aa0356545130ac06e308b54a72 *electron-v22.3.17-darwin-arm64-symbols.zip +37aa86e637c1306aa0e7b382d3d3d23717ac4769114f00f5abd98a7b95a97a00 *electron-v22.3.17-darwin-arm64.zip +bfd7ffb2a4b2fa8f47a72ea35340c51b21bc3f0f6e85679e48eb30615110e7dd *electron-v22.3.17-darwin-x64-symbols.zip +87e063025bfd11b60cbb637ecf077dc52d53ba5ac76b828ab06a8c5a66b0b590 *electron-v22.3.17-darwin-x64.zip +1e885e3e10fc952e7c898b91fe0b327147d5ba2effa291a8dff63b84ed6f7f09 *electron-v22.3.17-linux-arm64-symbols.zip +d810449d93ddbe7ec81b3792dc4c7237c7ad52e03d15503316029afe95281ef6 *electron-v22.3.17-linux-arm64.zip +b1112192a53388754fc55018f1f5f116cd342d78cc2beedcd115f313c669975e *electron-v22.3.17-linux-armv7l-symbols.zip +647f375119b8611d9a6ec6b4837c246f2e02d6b5ac75eb21a7a0e8bd07717cc6 *electron-v22.3.17-linux-armv7l.zip +8d4c2743c8a8b4b48364bd27e05eb4e1d22b46ed93c978646fad37def2047722 *electron-v22.3.17-linux-x64-symbols.zip +030c540a88998112f8848f2ecc1429790bb90a19f1826eda0b4e89d2b6e2459c *electron-v22.3.17-linux-x64.zip +afa64d9f5523564d48a77c5e53f9ffdf9b9f144bd4ccf55577a8aa1532e7f5d8 *electron-v22.3.17-win32-arm64-pdb.zip +fd8dc02edf2b7120d9b79e06701001d192dc0007503961769bda3e680ad9cbc2 *electron-v22.3.17-win32-arm64-symbols.zip +80dc7fd40f47832757fb150b63cf3a6d14aad3d64b4891a7a3e2785bd4c98f2d *electron-v22.3.17-win32-arm64.zip +e30077316165162d8954e7af1bb0e3454587efb82c26a65269b0e5a0237a739e *electron-v22.3.17-win32-ia32-pdb.zip +feb05e2ee1555de1721b93613db52a4b42f3caa287b28b5e0918dee7d1d321ca *electron-v22.3.17-win32-ia32-symbols.zip +221e988045f9fd299bb00b27cdde31a7a6621a40cea3ad34efe584e988046766 *electron-v22.3.17-win32-ia32.zip +295c820c5f47ad02bdd5f23d5071ff551e14aa3b018a4daff4cb9b18694862a1 *electron-v22.3.17-win32-x64-pdb.zip +4fe6e8f71fe7ade774ff92a5f3dbb3d667185da3c4be10cf6ade5e2700e32cc6 *electron-v22.3.17-win32-x64-symbols.zip +9a3847ff2a2702a62b66a309368f7feced29294168155de6271d1409191441bf *electron-v22.3.17-win32-x64.zip +b1d258f2378b326d52ba1b4dbd55d49b8190e23ef00d3ccdec9407114242a8d8 *ffmpeg-v22.3.17-darwin-arm64.zip +7ad943f4bacff4379b751fd64db85adb68c2a0456c0fe4fe3d0eb2c50257bccf *ffmpeg-v22.3.17-darwin-x64.zip +59d2e2b2f2cc515a86a4e0cfd1116d10a8b25a8d58d45bb04de3512e156c944b *ffmpeg-v22.3.17-linux-arm64.zip +b9d3b227bee17666d395ee7882ef477a733c3eeef3f1d9f2e3616d2d02eb3376 *ffmpeg-v22.3.17-linux-armv7l.zip +fa07ef910b23a4ef4b6761bc16d20c0e70ff0259325c4d523129e2d9c5084174 *ffmpeg-v22.3.17-linux-x64.zip +a83e1314a9161ebf7ffae9c67bb9332f231f3b82e6ffb68de4704dd81489301c *ffmpeg-v22.3.17-win32-arm64.zip +725b025bbe8664733c5e5d6ee5d2baa36acabd415e0fbd21b056de968a1fe2cd *ffmpeg-v22.3.17-win32-ia32.zip +e4c76b9bb3826e8b56c720220cff0bd1fa1a34e69a26617cd3650bda452bff71 *ffmpeg-v22.3.17-win32-x64.zip diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index 67ec595ed30..bfcddd7d1b0 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -316,7 +316,6 @@ "--vscode-inlineChatInput-border", "--vscode-inlineChatInput-focusBorder", "--vscode-inlineChatInput-placeholderForeground", - "--vscode-inlineChatrDiff-removed", "--vscode-input-background", "--vscode-input-border", "--vscode-input-foreground", diff --git a/cgmanifest.json b/cgmanifest.json index d5669cdf185..c2812272b50 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -528,12 +528,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "4ade2a6fb65e4b723feb7c09a5df765e5006b378" + "commitHash": "047fdbf3ca1958e4e489f0e777bf4022540f5ec2" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "22.3.14" + "version": "22.3.17" }, { "component": { diff --git a/cli/src/bin/code/main.rs b/cli/src/bin/code/main.rs index d000998c7dc..62e4195c7e4 100644 --- a/cli/src/bin/code/main.rs +++ b/cli/src/bin/code/main.rs @@ -95,7 +95,9 @@ async fn main() -> Result<(), std::convert::Infallible> { args::VersionSubcommand::Show => version::show(context!()).await, }, - Some(args::Commands::CommandShell) => tunnels::command_shell(context!()).await, + Some(args::Commands::CommandShell(cs_args)) => { + tunnels::command_shell(context!(), cs_args).await + } Some(args::Commands::Tunnel(tunnel_args)) => match tunnel_args.subcommand { Some(args::TunnelSubcommand::Prune) => tunnels::prune(context!()).await, diff --git a/cli/src/commands/args.rs b/cli/src/commands/args.rs index a97e1fc3870..d34519d6810 100644 --- a/cli/src/commands/args.rs +++ b/cli/src/commands/args.rs @@ -174,7 +174,14 @@ pub enum Commands { /// Runs the control server on process stdin/stdout #[clap(hide = true)] - CommandShell, + CommandShell(CommandShellArgs), +} + +#[derive(Args, Debug, Clone)] +pub struct CommandShellArgs { + /// Listen on a socket instead of stdin/stdout. + #[clap(long)] + pub on_socket: bool, } #[derive(Args, Debug, Clone)] @@ -548,6 +555,7 @@ pub enum OutputFormat { #[derive(Args, Clone, Debug, Default)] pub struct ExistingTunnelArgs { /// Name you'd like to assign preexisting tunnel to use to connect the tunnel + /// Old option, new code sohuld just use `--name`. #[clap(long, hide = true)] pub tunnel_name: Option, diff --git a/cli/src/commands/tunnels.rs b/cli/src/commands/tunnels.rs index 24e349bb720..9831de6e426 100644 --- a/cli/src/commands/tunnels.rs +++ b/cli/src/commands/tunnels.rs @@ -5,6 +5,7 @@ use async_trait::async_trait; use base64::{engine::general_purpose as b64, Engine as _}; +use futures::{stream::FuturesUnordered, StreamExt}; use serde::Serialize; use sha2::{Digest, Sha256}; use std::{str::FromStr, time::Duration}; @@ -12,13 +13,14 @@ use sysinfo::Pid; use super::{ args::{ - AuthProvider, CliCore, ExistingTunnelArgs, TunnelRenameArgs, TunnelServeArgs, - TunnelServiceSubCommands, TunnelUserSubCommands, + AuthProvider, CliCore, CommandShellArgs, ExistingTunnelArgs, TunnelRenameArgs, + TunnelServeArgs, TunnelServiceSubCommands, TunnelUserSubCommands, }, CommandContext, }; use crate::{ + async_pipe::{get_socket_name, listen_socket_rw_stream, socket_stream_split}, auth::Auth, constants::{APPLICATION_NAME, TUNNEL_CLI_LOCK_NAME, TUNNEL_SERVICE_LOCK_NAME}, log, @@ -59,20 +61,31 @@ impl From for crate::auth::AuthProvider { } } -impl From for Option { - fn from(d: ExistingTunnelArgs) -> Option { - if let (Some(tunnel_id), Some(tunnel_name), Some(cluster), Some(host_token)) = - (d.tunnel_id, d.tunnel_name, d.cluster, d.host_token) - { +fn fulfill_existing_tunnel_args( + d: ExistingTunnelArgs, + name_arg: &Option, +) -> Option { + let tunnel_name = d.tunnel_name.or_else(|| name_arg.clone()); + + match (d.tunnel_id, d.cluster, d.host_token) { + (Some(tunnel_id), None, Some(host_token)) => { + let i = tunnel_id.find('.')?; Some(dev_tunnels::ExistingTunnel { - tunnel_id, + tunnel_id: tunnel_id[..i].to_string(), + cluster: tunnel_id[i + 1..].to_string(), tunnel_name, host_token, - cluster, }) - } else { - None } + + (Some(tunnel_id), Some(cluster), Some(host_token)) => Some(dev_tunnels::ExistingTunnel { + tunnel_id, + tunnel_name, + host_token, + cluster, + }), + + _ => None, } } @@ -109,23 +122,55 @@ impl ServiceContainer for TunnelServiceContainer { } } -pub async fn command_shell(ctx: CommandContext) -> Result { +pub async fn command_shell(ctx: CommandContext, args: CommandShellArgs) -> Result { let platform = PreReqChecker::new().verify().await?; - serve_stream( - tokio::io::stdin(), - tokio::io::stderr(), - ServeStreamParams { - log: ctx.log, - launcher_paths: ctx.paths, - platform, - requires_auth: true, - exit_barrier: ShutdownRequest::create_rx([ShutdownRequest::CtrlC]), - code_server_args: (&ctx.args).into(), - }, - ) - .await; + let mut params = ServeStreamParams { + log: ctx.log, + launcher_paths: ctx.paths, + platform, + requires_auth: true, + exit_barrier: ShutdownRequest::create_rx([ShutdownRequest::CtrlC]), + code_server_args: (&ctx.args).into(), + }; - Ok(0) + if !args.on_socket { + serve_stream(tokio::io::stdin(), tokio::io::stderr(), params).await; + return Ok(0); + } + + let socket = get_socket_name(); + let mut listener = listen_socket_rw_stream(&socket) + .await + .map_err(|e| wrap(e, "error listening on socket"))?; + + params + .log + .result(format!("Listening on {}", socket.display())); + + let mut servers = FuturesUnordered::new(); + + loop { + tokio::select! { + Some(_) = servers.next() => {}, + socket = listener.accept() => { + match socket { + Ok(s) => { + let (read, write) = socket_stream_split(s); + servers.push(serve_stream(read, write, params.clone())); + }, + Err(e) => { + error!(params.log, &format!("Error accepting connection: {}", e)); + return Ok(1); + } + } + }, + _ = params.exit_barrier.wait() => { + // wait for all servers to finish up: + while (servers.next().await).is_some() { } + return Ok(0); + } + } + } } pub async fn service( @@ -412,8 +457,10 @@ async fn serve_with_csa( let auth = Auth::new(&paths, log.clone()); let mut dt = dev_tunnels::DevTunnels::new(&log, auth, &paths); loop { - let tunnel = if let Some(d) = gateway_args.tunnel.clone().into() { - dt.start_existing_tunnel(d).await + let tunnel = if let Some(t) = + fulfill_existing_tunnel_args(gateway_args.tunnel.clone(), &gateway_args.name) + { + dt.start_existing_tunnel(t).await } else { dt.start_new_launcher_tunnel(gateway_args.name.as_deref(), gateway_args.random_name) .await diff --git a/cli/src/tunnels/control_server.rs b/cli/src/tunnels/control_server.rs index e0c1ec19fc2..8577f9668e9 100644 --- a/cli/src/tunnels/control_server.rs +++ b/cli/src/tunnels/control_server.rs @@ -233,6 +233,7 @@ pub async fn serve( } } +#[derive(Clone)] pub struct ServeStreamParams { pub log: log::Logger, pub launcher_paths: LauncherPaths, diff --git a/cli/src/tunnels/dev_tunnels.rs b/cli/src/tunnels/dev_tunnels.rs index c4ca9741b88..a04bdbcaf6d 100644 --- a/cli/src/tunnels/dev_tunnels.rs +++ b/cli/src/tunnels/dev_tunnels.rs @@ -229,7 +229,7 @@ lazy_static! { #[derive(Clone, Debug)] pub struct ExistingTunnel { /// Name you'd like to assign preexisting tunnel to use to connect to the VS Code Server - pub tunnel_name: String, + pub tunnel_name: Option, /// Token to authenticate and use preexisting tunnel pub host_token: String, @@ -393,7 +393,12 @@ impl DevTunnels { }; tunnel = self - .sync_tunnel_tags(&persisted.name, tunnel, &HOST_TUNNEL_REQUEST_OPTIONS) + .sync_tunnel_tags( + &self.client, + &persisted.name, + tunnel, + &HOST_TUNNEL_REQUEST_OPTIONS, + ) .await?; let locator = TunnelLocator::try_from(&tunnel).unwrap(); @@ -532,6 +537,7 @@ impl DevTunnels { /// other version tags. async fn sync_tunnel_tags( &self, + client: &TunnelManagementClient, name: &str, tunnel: Tunnel, options: &TunnelRequestOptions, @@ -558,7 +564,7 @@ impl DevTunnels { let result = spanf!( self.log, self.log.span("dev-tunnel.protocol-tag-update"), - self.client.update_tunnel(&tunnel_update, options) + client.update_tunnel(&tunnel_update, options) ); result.map_err(|e| wrap(e, "tunnel tag update failed").into()) @@ -639,6 +645,12 @@ impl DevTunnels { Ok(()) } + fn get_placeholder_name() -> String { + let mut n = clean_hostname_for_tunnel(&gethostname::gethostname().to_string_lossy()); + n.make_ascii_lowercase(); + n + } + async fn get_name_for_tunnel( &mut self, preferred_name: Option<&str>, @@ -670,10 +682,7 @@ impl DevTunnels { use_random_name = true; } - let mut placeholder_name = - clean_hostname_for_tunnel(&gethostname::gethostname().to_string_lossy()); - placeholder_name.make_ascii_lowercase(); - + let mut placeholder_name = Self::get_placeholder_name(); if !is_name_free(&placeholder_name) { for i in 2.. { let fixed_name = format!("{}{}", placeholder_name, i); @@ -715,7 +724,10 @@ impl DevTunnels { tunnel: ExistingTunnel, ) -> Result { let tunnel_details = PersistedTunnel { - name: tunnel.tunnel_name, + name: match tunnel.tunnel_name { + Some(n) => n, + None => Self::get_placeholder_name(), + }, id: tunnel.tunnel_id, cluster: tunnel.cluster, }; @@ -725,10 +737,23 @@ impl DevTunnels { tunnel.host_token.clone(), )); + let client = mgmt.into(); + self.sync_tunnel_tags( + &client, + &tunnel_details.name, + Tunnel { + cluster_id: Some(tunnel_details.cluster.clone()), + tunnel_id: Some(tunnel_details.id.clone()), + ..Default::default() + }, + &HOST_TUNNEL_REQUEST_OPTIONS, + ) + .await?; + self.start_tunnel( tunnel_details.locator(), &tunnel_details, - mgmt.into(), + client, StaticAccessTokenProvider::new(tunnel.host_token), ) .await diff --git a/extensions/github-authentication/extension-browser.webpack.config.js b/extensions/github-authentication/extension-browser.webpack.config.js index 37b207eb056..f109e203569 100644 --- a/extensions/github-authentication/extension-browser.webpack.config.js +++ b/extensions/github-authentication/extension-browser.webpack.config.js @@ -21,7 +21,8 @@ module.exports = withBrowserDefaults({ 'uuid': path.resolve(__dirname, 'node_modules/uuid/dist/esm-browser/index.js'), './node/authServer': path.resolve(__dirname, 'src/browser/authServer'), './node/crypto': path.resolve(__dirname, 'src/browser/crypto'), - './node/fetch': path.resolve(__dirname, 'src/browser/fetch') + './node/fetch': path.resolve(__dirname, 'src/browser/fetch'), + './node/buffer': path.resolve(__dirname, 'src/browser/buffer'), } } }); diff --git a/src/vscode-dts/vscode.proposed.quickPickItemIcon.d.ts b/extensions/github-authentication/src/browser/buffer.ts similarity index 56% rename from src/vscode-dts/vscode.proposed.quickPickItemIcon.d.ts rename to extensions/github-authentication/src/browser/buffer.ts index b53c8350117..7192f5f104a 100644 --- a/src/vscode-dts/vscode.proposed.quickPickItemIcon.d.ts +++ b/extensions/github-authentication/src/browser/buffer.ts @@ -3,15 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -declare module 'vscode' { - /** - * Represents an item that can be selected from - * a list of items. - */ - export interface QuickPickItem { - /** - * The icon path or {@link ThemeIcon} for the QuickPickItem. - */ - iconPath?: Uri | { light: Uri; dark: Uri } | ThemeIcon; - } +export function base64Encode(text: string): string { + return btoa(text); } diff --git a/extensions/github-authentication/src/common/logger.ts b/extensions/github-authentication/src/common/logger.ts index 84225bd707f..cf90c4176a9 100644 --- a/extensions/github-authentication/src/common/logger.ts +++ b/extensions/github-authentication/src/common/logger.ts @@ -26,4 +26,7 @@ export class Log { this.output.error(message); } + public warn(message: string): void { + this.output.warn(message); + } } diff --git a/extensions/github-authentication/src/flows.ts b/extensions/github-authentication/src/flows.ts index f3f9277bdc1..5bc9d095385 100644 --- a/extensions/github-authentication/src/flows.ts +++ b/extensions/github-authentication/src/flows.ts @@ -201,7 +201,8 @@ const allFlows: IFlow[] = [ supportsGitHubEnterpriseServer: false, supportsHostedGitHubEnterprise: true, supportsRemoteExtensionHost: true, - supportsWebWorkerExtensionHost: true, + // Web worker can't open a port to listen for the redirect + supportsWebWorkerExtensionHost: false, // exchanging a code for a token requires a client secret supportsNoClientSecret: false, supportsSupportedClients: true, diff --git a/extensions/github-authentication/src/github.ts b/extensions/github-authentication/src/github.ts index c710cbe4f2f..71aa17bd5cc 100644 --- a/extensions/github-authentication/src/github.ts +++ b/extensions/github-authentication/src/github.ts @@ -363,6 +363,7 @@ export class GitHubAuthenticationProvider implements vscode.AuthenticationProvid sessions.splice(sessionIndex, 1); await this.storeSessions(sessions); + await this._githubServer.logout(session); this._sessionChangeEmitter.fire({ added: [], removed: [session], changed: [] }); } else { diff --git a/extensions/github-authentication/src/githubServer.ts b/extensions/github-authentication/src/githubServer.ts index 7ac5cd8c577..0729c4c5077 100644 --- a/extensions/github-authentication/src/githubServer.ts +++ b/extensions/github-authentication/src/githubServer.ts @@ -12,6 +12,8 @@ import { crypto } from './node/crypto'; import { fetching } from './node/fetch'; import { ExtensionHost, GitHubTarget, getFlows } from './flows'; import { NETWORK_ERROR, USER_CANCELLATION_ERROR } from './common/errors'; +import { Config } from './config'; +import { base64Encode } from './node/buffer'; // This is the error message that we throw if the login was cancelled for any reason. Extensions // calling `getSession` can handle this error to know that the user cancelled the login. @@ -22,6 +24,7 @@ const REDIRECT_URL_INSIDERS = 'https://insiders.vscode.dev/redirect'; export interface IGitHubServer { login(scopes: string): Promise; + logout(session: vscode.AuthenticationSession): Promise; getUserInfo(token: string): Promise<{ id: string; accountName: string }>; sendAdditionalTelemetryInfo(session: vscode.AuthenticationSession): Promise; friendlyName: string; @@ -78,9 +81,14 @@ export class GitHubServer implements IGitHubServer { } // TODO@joaomoreno TODO@TylerLeonhardt + private _isNoCorsEnvironment: boolean | undefined; private async isNoCorsEnvironment(): Promise { + if (this._isNoCorsEnvironment !== undefined) { + return this._isNoCorsEnvironment; + } const uri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/dummy`)); - return (uri.scheme === 'https' && /^((insiders\.)?vscode|github)\./.test(uri.authority)) || (uri.scheme === 'http' && /^localhost/.test(uri.authority)); + this._isNoCorsEnvironment = (uri.scheme === 'https' && /^((insiders\.)?vscode|github)\./.test(uri.authority)) || (uri.scheme === 'http' && /^localhost/.test(uri.authority)); + return this._isNoCorsEnvironment; } public async login(scopes: string): Promise { @@ -144,6 +152,58 @@ export class GitHubServer implements IGitHubServer { throw new Error(userCancelled ? CANCELLATION_ERROR : 'No auth flow succeeded.'); } + public async logout(session: vscode.AuthenticationSession): Promise { + this._logger.trace(`Deleting session (${session.id}) from server...`); + + if (!Config.gitHubClientSecret) { + this._logger.warn('No client secret configured for GitHub authentication. The token has been deleted with best effort on this system, but we are unable to delete the token on server without the client secret.'); + return; + } + + // Only attempt to delete OAuth tokens. They are always prefixed with `gho_`. + // https://docs.github.com/en/rest/apps/oauth-applications#about-oauth-apps-and-oauth-authorizations-of-github-apps + if (!session.accessToken.startsWith('gho_')) { + this._logger.warn('The token being deleted is not an OAuth token. It has been deleted locally, but we cannot delete it on server.'); + return; + } + + if (!isSupportedTarget(this._type, this._ghesUri)) { + this._logger.trace('GitHub.com and GitHub hosted GitHub Enterprise are the only options that support deleting tokens on the server. Skipping.'); + return; + } + + const authHeader = 'Basic ' + base64Encode(`${Config.gitHubClientId}:${Config.gitHubClientSecret}`); + const uri = this.getServerUri(`/applications/${Config.gitHubClientId}/token`); + + try { + // Defined here: https://docs.github.com/en/rest/apps/oauth-applications?apiVersion=2022-11-28#delete-an-app-token + const result = await fetching(uri.toString(true), { + method: 'DELETE', + headers: { + Accept: 'application/vnd.github+json', + Authorization: authHeader, + 'X-GitHub-Api-Version': '2022-11-28', + 'User-Agent': `${vscode.env.appName} (${vscode.env.appHost})` + }, + body: JSON.stringify({ access_token: session.accessToken }), + }); + + if (result.status === 204) { + this._logger.trace(`Successfully deleted token from session (${session.id}) from server.`); + return; + } + + try { + const body = await result.text(); + throw new Error(body); + } catch (e) { + throw new Error(`${result.status} ${result.statusText}`); + } + } catch (e) { + this._logger.warn('Failed to delete token from server.' + e.message ?? e); + } + } + private getServerUri(path: string = '') { const apiUri = this.baseUri; // github.com and Hosted GitHub Enterprise instances diff --git a/extensions/github-authentication/src/node/buffer.ts b/extensions/github-authentication/src/node/buffer.ts new file mode 100644 index 00000000000..8e6208aa22a --- /dev/null +++ b/extensions/github-authentication/src/node/buffer.ts @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export function base64Encode(text: string): string { + return Buffer.from(text, 'binary').toString('base64'); +} diff --git a/extensions/github/src/links.ts b/extensions/github/src/links.ts index b270792404f..911f0e5376b 100644 --- a/extensions/github/src/links.ts +++ b/extensions/github/src/links.ts @@ -191,6 +191,8 @@ export function getVscodeDevHost(): string { } export async function ensurePublished(repository: Repository, file: vscode.Uri) { + await repository.status(); + if ((repository.state.HEAD?.type === RefType.Head || repository.state.HEAD?.type === RefType.Tag) // If HEAD is not published, make sure it is && !repository?.state.HEAD?.upstream diff --git a/extensions/markdown-language-features/notebook/index.ts b/extensions/markdown-language-features/notebook/index.ts index d52d6b6b12a..f050f5a3162 100644 --- a/extensions/markdown-language-features/notebook/index.ts +++ b/extensions/markdown-language-features/notebook/index.ts @@ -176,42 +176,39 @@ export const activate: ActivationFunction = (ctx) => { hr { border: 0; - height: 2px; - border-bottom: 2px solid; - } - - h2, h3, h4, h5, h6 { - font-weight: normal; + height: 1px; + border-bottom: 1px solid; } h1 { - font-size: 2.3em; + font-size: 2em; + margin-top: 0; + padding-bottom: 0.3em; + border-bottom-width: 1px; + border-bottom-style: solid; } h2 { - font-size: 2em; - } - - h3 { - font-size: 1.7em; - } - - h3 { font-size: 1.5em; + padding-bottom: 0.3em; + border-bottom-width: 1px; + border-bottom-style: solid; + } + + h3 { + font-size: 1.25em; } h4 { - font-size: 1.3em; + font-size: 1em; } h5 { - font-size: 1.2em; + font-size: 0.875em; } - h1, - h2, - h3 { - font-weight: normal; + h6 { + font-size: 0.85em; } div { @@ -229,12 +226,38 @@ export const activate: ActivationFunction = (ctx) => { } /* Removes bottom margin when only one item exists in markdown cell */ - #preview > *:only-child, - #preview > *:last-child { + #preview > *:not(h1):not(h2):only-child, + #preview > *:not(h1):not(h2):last-child { margin-bottom: 0; padding-bottom: 0; } + h1, + h2, + h3, + h4, + h5, + h6 { + font-weight: 600; + margin-top: 24px; + margin-bottom: 16px; + line-height: 1.25; + } + + .vscode-light h1, + .vscode-light h2, + .vscode-light hr, + .vscode-light td { + border-color: rgba(0, 0, 0, 0.18); + } + + .vscode-dark h1, + .vscode-dark h2, + .vscode-dark hr, + .vscode-dark td { + border-color: rgba(255, 255, 255, 0.18); + } + /* makes all markdown cells consistent */ div { min-height: var(--notebook-markdown-min-height); diff --git a/extensions/markdown-language-features/src/markdownEngine.ts b/extensions/markdown-language-features/src/markdownEngine.ts index 282cef1548d..cd999c44116 100644 --- a/extensions/markdown-language-features/src/markdownEngine.ts +++ b/extensions/markdown-language-features/src/markdownEngine.ts @@ -114,6 +114,7 @@ export class MarkdownItEngine implements IMdParser { _contributionProvider.onContributionsChanged(() => { // Markdown plugin contributions may have changed this._md = undefined; + this._tokenCache.clean(); }); } diff --git a/extensions/markdown-language-features/src/preview/preview.ts b/extensions/markdown-language-features/src/preview/preview.ts index de58e54784a..7ccbc625b47 100644 --- a/extensions/markdown-language-features/src/preview/preview.ts +++ b/extensions/markdown-language-features/src/preview/preview.ts @@ -95,7 +95,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { } this._register(_contributionProvider.onContributionsChanged(() => { - setTimeout(() => this.refresh(), 0); + setTimeout(() => this.refresh(true), 0); })); this._register(vscode.workspace.onDidChangeTextDocument(event => { diff --git a/extensions/notebook-renderers/src/index.ts b/extensions/notebook-renderers/src/index.ts index df674353633..090e9719420 100644 --- a/extensions/notebook-renderers/src/index.ts +++ b/extensions/notebook-renderers/src/index.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import type { ActivationFunction, OutputItem, RendererContext } from 'vscode-notebook-renderer'; -import { createOutputContent, scrollableClass } from './textHelper'; -import { HtmlRenderingHook, IDisposable, IRichRenderContext, JavaScriptRenderingHook, RenderOptions } from './rendererTypes'; +import { createOutputContent, appendOutput, scrollableClass } from './textHelper'; +import { HtmlRenderingHook, IDisposable, IRichRenderContext, JavaScriptRenderingHook, OutputWithAppend, RenderOptions } from './rendererTypes'; import { ttPolicy } from './htmlHelper'; function clearContainer(container: HTMLElement) { @@ -152,7 +152,7 @@ function renderError( outputInfo: OutputItem, outputElement: HTMLElement, ctx: IRichRenderContext, - trustHTML: boolean + trustHtml: boolean ): IDisposable { const disposableStore = createDisposableStore(); @@ -172,7 +172,7 @@ function renderError( outputElement.classList.add('traceback'); const outputScrolling = scrollingEnabled(outputInfo, ctx.settings); - const content = createOutputContent(outputInfo.id, [err.stack ?? ''], ctx.settings.lineLimit, outputScrolling, trustHTML); + const content = createOutputContent(outputInfo.id, err.stack ?? '', { linesLimit: ctx.settings.lineLimit, scrollable: outputScrolling, trustHtml }); const contentParent = document.createElement('div'); contentParent.classList.toggle('word-wrap', ctx.settings.outputWordWrap); disposableStore.push(ctx.onDidChangeSettings(e => { @@ -270,19 +270,13 @@ function scrollingEnabled(output: OutputItem, options: RenderOptions) { // div.output.output-stream <-- outputElement parameter // div.scrollable? tabindex="0" <-- contentParent // div output-item-id="{guid}" <-- content from outputItem parameter -function renderStream(outputInfo: OutputItem, outputElement: HTMLElement, error: boolean, ctx: IRichRenderContext): IDisposable { +function renderStream(outputInfo: OutputWithAppend, outputElement: HTMLElement, error: boolean, ctx: IRichRenderContext): IDisposable { const disposableStore = createDisposableStore(); const outputScrolling = scrollingEnabled(outputInfo, ctx.settings); + const outputOptions = { linesLimit: ctx.settings.lineLimit, scrollable: outputScrolling, trustHtml: false, error }; outputElement.classList.add('output-stream'); - const text = outputInfo.text(); - const newContent = createOutputContent(outputInfo.id, [text], ctx.settings.lineLimit, outputScrolling, false); - newContent.setAttribute('output-item-id', outputInfo.id); - if (error) { - newContent.classList.add('error'); - } - const scrollTop = outputScrolling ? findScrolledHeight(outputElement) : undefined; const previousOutputParent = getPreviousMatchingContentGroup(outputElement); @@ -290,9 +284,9 @@ function renderStream(outputInfo: OutputItem, outputElement: HTMLElement, error: if (previousOutputParent) { const existingContent = previousOutputParent.querySelector(`[output-item-id="${outputInfo.id}"]`) as HTMLElement | null; if (existingContent) { - existingContent.replaceWith(newContent); - + appendOutput(outputInfo, existingContent, outputOptions); } else { + const newContent = createOutputContent(outputInfo.id, outputInfo.text(), outputOptions); previousOutputParent.appendChild(newContent); } previousOutputParent.classList.toggle('scrollbar-visible', previousOutputParent.scrollHeight > previousOutputParent.clientHeight); @@ -301,12 +295,9 @@ function renderStream(outputInfo: OutputItem, outputElement: HTMLElement, error: const existingContent = outputElement.querySelector(`[output-item-id="${outputInfo.id}"]`) as HTMLElement | null; let contentParent = existingContent?.parentElement; if (existingContent && contentParent) { - existingContent.replaceWith(newContent); - while (newContent.nextSibling) { - // clear out any stale content if we had previously combined streaming outputs into this one - newContent.nextSibling.remove(); - } + appendOutput(outputInfo, existingContent, outputOptions); } else { + const newContent = createOutputContent(outputInfo.id, outputInfo.text(), outputOptions); contentParent = document.createElement('div'); contentParent.appendChild(newContent); while (outputElement.firstChild) { @@ -333,7 +324,7 @@ function renderText(outputInfo: OutputItem, outputElement: HTMLElement, ctx: IRi const text = outputInfo.text(); const outputScrolling = scrollingEnabled(outputInfo, ctx.settings); - const content = createOutputContent(outputInfo.id, [text], ctx.settings.lineLimit, outputScrolling, false); + const content = createOutputContent(outputInfo.id, text, { linesLimit: ctx.settings.lineLimit, scrollable: outputScrolling, trustHtml: false }); content.classList.add('output-plaintext'); if (ctx.settings.outputWordWrap) { content.classList.add('word-wrap'); diff --git a/extensions/notebook-renderers/src/rendererTypes.ts b/extensions/notebook-renderers/src/rendererTypes.ts index 9da94aeef5d..ded12bdcacc 100644 --- a/extensions/notebook-renderers/src/rendererTypes.ts +++ b/extensions/notebook-renderers/src/rendererTypes.ts @@ -35,3 +35,14 @@ export interface RenderOptions { } export type IRichRenderContext = RendererContext & { readonly settings: RenderOptions; readonly onDidChangeSettings: Event }; + +export type OutputElementOptions = { + linesLimit: number; + scrollable?: boolean; + error?: boolean; + trustHtml?: boolean; +}; + +export interface OutputWithAppend extends OutputItem { + appendedText?(): string | undefined; +} diff --git a/extensions/notebook-renderers/src/test/notebookRenderer.test.ts b/extensions/notebook-renderers/src/test/notebookRenderer.test.ts index e67d1d8ce26..0f747900377 100644 --- a/extensions/notebook-renderers/src/test/notebookRenderer.test.ts +++ b/extensions/notebook-renderers/src/test/notebookRenderer.test.ts @@ -5,8 +5,8 @@ import * as assert from 'assert'; import { activate } from '..'; -import { OutputItem, RendererApi } from 'vscode-notebook-renderer'; -import { IDisposable, IRichRenderContext, RenderOptions } from '../rendererTypes'; +import { RendererApi } from 'vscode-notebook-renderer'; +import { IDisposable, IRichRenderContext, OutputWithAppend, RenderOptions } from '../rendererTypes'; import { JSDOM } from "jsdom"; const dom = new JSDOM(); @@ -116,10 +116,13 @@ suite('Notebook builtin output renderer', () => { } } - function createOutputItem(text: string, mime: string, id: string = '123'): OutputItem { + function createOutputItem(text: string, mime: string, id: string = '123', appendedText?: string): OutputWithAppend { return { id: id, mime: mime, + appendedText() { + return appendedText; + }, text() { return text; }, @@ -177,9 +180,9 @@ suite('Notebook builtin output renderer', () => { assert.ok(renderer, 'Renderer not created'); const outputElement = new OutputHtml().getFirstOuputElement(); - const outputItem = createOutputItem('content', 'text/plain'); + const outputItem = createOutputItem('content', mimeType); await renderer!.renderOutputItem(outputItem, outputElement); - const outputItem2 = createOutputItem('replaced content', 'text/plain'); + const outputItem2 = createOutputItem('replaced content', mimeType); await renderer!.renderOutputItem(outputItem2, outputElement); const inserted = outputElement.firstChild as HTMLElement; @@ -189,6 +192,87 @@ suite('Notebook builtin output renderer', () => { }); + test('Append streaming output', async () => { + const context = createContext({ outputWordWrap: false, outputScrolling: true }); + const renderer = await activate(context); + assert.ok(renderer, 'Renderer not created'); + + const outputElement = new OutputHtml().getFirstOuputElement(); + const outputItem = createOutputItem('content', stdoutMimeType, '123', 'ignoredAppend'); + await renderer!.renderOutputItem(outputItem, outputElement); + const outputItem2 = createOutputItem('content\nappended', stdoutMimeType, '123', '\nappended'); + await renderer!.renderOutputItem(outputItem2, outputElement); + + const inserted = outputElement.firstChild as HTMLElement; + assert.ok(inserted.innerHTML.indexOf('>contentappendedcontentcontent { + const context = createContext({ outputScrolling: true }); + const renderer = await activate(context); + assert.ok(renderer, 'Renderer not created'); + + const outputHtml = new OutputHtml(); + const firstOutputElement = outputHtml.getFirstOuputElement(); + const outputItem1 = createOutputItem('first stream content', stdoutMimeType, '1'); + const outputItem2 = createOutputItem(JSON.stringify(error), errorMimeType, '2'); + const outputItem3 = createOutputItem('second stream content', stdoutMimeType, '3'); + await renderer!.renderOutputItem(outputItem1, firstOutputElement); + const secondOutputElement = outputHtml.appendOutputElement(); + await renderer!.renderOutputItem(outputItem2, secondOutputElement); + const thirdOutputElement = outputHtml.appendOutputElement(); + await renderer!.renderOutputItem(outputItem3, thirdOutputElement); + + const appendedItem1 = createOutputItem('', stdoutMimeType, '1', ' appended1'); + await renderer!.renderOutputItem(appendedItem1, firstOutputElement); + const appendedItem3 = createOutputItem('', stdoutMimeType, '3', ' appended3'); + await renderer!.renderOutputItem(appendedItem3, thirdOutputElement); + + assert.ok(firstOutputElement.innerHTML.indexOf('>first stream content') > -1, `Content was not added to output element: ${outputHtml.cellElement.innerHTML}`); + assert.ok(firstOutputElement.innerHTML.indexOf('appended1') > -1, `Content was not appended to output element: ${outputHtml.cellElement.innerHTML}`); + assert.ok(secondOutputElement.innerHTML.indexOf('>NameError -1, `Content was not added to output element: ${outputHtml.cellElement.innerHTML}`); + assert.ok(thirdOutputElement.innerHTML.indexOf('>second stream content') > -1, `Content was not added to output element: ${outputHtml.cellElement.innerHTML}`); + assert.ok(thirdOutputElement.innerHTML.indexOf('appended3') > -1, `Content was not appended to output element: ${outputHtml.cellElement.innerHTML}`); + }); + + test('Append large streaming outputs', async () => { + const context = createContext({ outputWordWrap: false, outputScrolling: true }); + const renderer = await activate(context); + assert.ok(renderer, 'Renderer not created'); + + const outputElement = new OutputHtml().getFirstOuputElement(); + const lotsOfLines = new Array(4998).fill('line').join('\n'); + const firstOuput = lotsOfLines + 'expected1'; + const outputItem = createOutputItem(firstOuput, stdoutMimeType, '123'); + await renderer!.renderOutputItem(outputItem, outputElement); + const appended = '\n' + lotsOfLines + 'expectedAppend'; + const outputItem2 = createOutputItem(firstOuput + appended, stdoutMimeType, '123', appended); + await renderer!.renderOutputItem(outputItem2, outputElement); + + const inserted = outputElement.firstChild as HTMLElement; + assert.ok(inserted.innerHTML.indexOf('expected1') !== -1, `Last bit of previous content should still exist`); + assert.ok(inserted.innerHTML.indexOf('expectedAppend') !== -1, `Content was not appended to output element`); + }); + + test('Streaming outputs larger than the line limit are truncated', async () => { + const context = createContext({ outputWordWrap: false, outputScrolling: true }); + const renderer = await activate(context); + assert.ok(renderer, 'Renderer not created'); + + const outputElement = new OutputHtml().getFirstOuputElement(); + const lotsOfLines = new Array(11000).fill('line').join('\n'); + const firstOuput = 'shouldBeTruncated' + lotsOfLines + 'expected1'; + const outputItem = createOutputItem(firstOuput, stdoutMimeType, '123'); + await renderer!.renderOutputItem(outputItem, outputElement); + + const inserted = outputElement.firstChild as HTMLElement; + assert.ok(inserted.innerHTML.indexOf('expected1') !== -1, `Last bit of content should exist`); + assert.ok(inserted.innerHTML.indexOf('shouldBeTruncated') === -1, `Beginning content should be truncated`); + }); + test(`Render with wordwrap and scrolling for error output`, async () => { const context = createContext({ outputWordWrap: true, outputScrolling: true }); const renderer = await activate(context); @@ -268,6 +352,29 @@ suite('Notebook builtin output renderer', () => { assert.ok(inserted.innerHTML.indexOf('>second stream content { + const context = createContext({ outputScrolling: true }); + const renderer = await activate(context); + assert.ok(renderer, 'Renderer not created'); + + const outputHtml = new OutputHtml(); + const outputElement = outputHtml.getFirstOuputElement(); + const outputItem1 = createOutputItem('first stream content', stdoutMimeType, '1'); + const outputItem2 = createOutputItem('second stream content', stdoutMimeType, '2'); + await renderer!.renderOutputItem(outputItem1, outputElement); + const secondOutput = outputHtml.appendOutputElement(); + await renderer!.renderOutputItem(outputItem2, secondOutput); + const appendingOutput = createOutputItem('', stdoutMimeType, '2', ' appended'); + await renderer!.renderOutputItem(appendingOutput, secondOutput); + + + const inserted = outputElement.firstChild as HTMLElement; + assert.ok(inserted, `nothing appended to output element: ${outputElement.innerHTML}`); + assert.ok(inserted.innerHTML.indexOf('>first stream content -1, `Content was not added to output element: ${outputHtml.cellElement.innerHTML}`); + assert.ok(inserted.innerHTML.indexOf('>second stream content') > -1, `Second content was not added to ouptut element: ${outputHtml.cellElement.innerHTML}`); + assert.ok(inserted.innerHTML.indexOf('appended') > -1, `Content was not appended to ouptut element: ${outputHtml.cellElement.innerHTML}`); + }); + test(`Streaming outputs interleaved with other mime types will produce separate outputs`, async () => { const context = createContext({ outputScrolling: false }); const renderer = await activate(context); diff --git a/extensions/notebook-renderers/src/textHelper.ts b/extensions/notebook-renderers/src/textHelper.ts index 8cc03fd543e..5cf0e24eb96 100644 --- a/extensions/notebook-renderers/src/textHelper.ts +++ b/extensions/notebook-renderers/src/textHelper.ts @@ -4,9 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { handleANSIOutput } from './ansi'; - +import { OutputElementOptions, OutputWithAppend } from './rendererTypes'; export const scrollableClass = 'scrollable'; +const softScrollableLineLimit = 5000; +const hardScrollableLineLimit = 8000; + /** * Output is Truncated. View as a [scrollable element] or open in a [text editor]. Adjust cell output [settings...] */ @@ -91,22 +94,70 @@ function truncatedArrayOfString(id: string, buffer: string[], linesLimit: number function scrollableArrayOfString(id: string, buffer: string[], trustHtml: boolean) { const element = document.createElement('div'); - if (buffer.length > 5000) { + if (buffer.length > softScrollableLineLimit) { element.appendChild(generateNestedViewAllElement(id)); } - element.appendChild(handleANSIOutput(buffer.slice(-5000).join('\n'), trustHtml)); + element.appendChild(handleANSIOutput(buffer.slice(-1 * softScrollableLineLimit).join('\n'), trustHtml)); return element; } -export function createOutputContent(id: string, outputs: string[], linesLimit: number, scrollable: boolean, trustHtml: boolean): HTMLElement { +const outputLengths: Record = {}; - const buffer = outputs.join('\n').split(/\r\n|\r|\n/g); - - if (scrollable) { - return scrollableArrayOfString(id, buffer, trustHtml); - } else { - return truncatedArrayOfString(id, buffer, linesLimit, trustHtml); +function appendScrollableOutput(element: HTMLElement, id: string, appended: string, trustHtml: boolean) { + if (!outputLengths[id]) { + outputLengths[id] = 0; } + + const buffer = appended.split(/\r\n|\r|\n/g); + const appendedLength = buffer.length + outputLengths[id]; + // Only append outputs up to the hard limit of lines, then replace it with the last softLimit number of lines + if (appendedLength > hardScrollableLineLimit) { + return false; + } + else { + element.appendChild(handleANSIOutput(buffer.join('\n'), trustHtml)); + outputLengths[id] = appendedLength; + } + return true; } + +export function createOutputContent(id: string, outputText: string, options: OutputElementOptions): HTMLElement { + const { linesLimit, error, scrollable, trustHtml } = options; + const buffer = outputText.split(/\r\n|\r|\n/g); + outputLengths[id] = outputLengths[id] = Math.min(buffer.length, softScrollableLineLimit); + + let outputElement: HTMLElement; + if (scrollable) { + outputElement = scrollableArrayOfString(id, buffer, !!trustHtml); + } else { + outputElement = truncatedArrayOfString(id, buffer, linesLimit, !!trustHtml); + } + + outputElement.setAttribute('output-item-id', id); + if (error) { + outputElement.classList.add('error'); + } + + return outputElement; +} + +export function appendOutput(outputInfo: OutputWithAppend, existingContent: HTMLElement, options: OutputElementOptions) { + const appendedText = outputInfo.appendedText?.(); + // appending output only supported for scrollable ouputs currently + if (appendedText && options.scrollable) { + if (appendScrollableOutput(existingContent, outputInfo.id, appendedText, false)) { + return; + } + } + + const newContent = createOutputContent(outputInfo.id, outputInfo.text(), options); + existingContent.replaceWith(newContent); + while (newContent.nextSibling) { + // clear out any stale content if we had previously combined streaming outputs into this one + newContent.nextSibling.remove(); + } + +} + diff --git a/extensions/typescript-language-features/package.nls.json b/extensions/typescript-language-features/package.nls.json index bd4dd6366ad..81260c405e1 100644 --- a/extensions/typescript-language-features/package.nls.json +++ b/extensions/typescript-language-features/package.nls.json @@ -152,7 +152,7 @@ "typescript.preferences.includePackageJsonAutoImports.auto": "Search dependencies based on estimated performance impact.", "typescript.preferences.includePackageJsonAutoImports.on": "Always search dependencies.", "typescript.preferences.includePackageJsonAutoImports.off": "Never search dependencies.", - "typescript.preferences.autoImportFileExcludePatterns": "Specify glob patterns of files to exclude from auto imports. Requires using TypeScript 4.8 or newer in the workspace.", + "typescript.preferences.autoImportFileExcludePatterns": "Specify glob patterns of files to exclude from auto imports. Relative paths are resolved relative to the workspace root. Patterns are evaluated using tsconfig.json [`exclude`](https://www.typescriptlang.org/tsconfig#exclude) semantics. Requires using TypeScript 4.8 or newer in the workspace.", "typescript.updateImportsOnFileMove.enabled": "Enable/disable automatic updating of import paths when you rename or move a file in VS Code.", "typescript.updateImportsOnFileMove.enabled.prompt": "Prompt on each rename.", "typescript.updateImportsOnFileMove.enabled.always": "Always update paths automatically.", diff --git a/extensions/typescript-language-features/src/languageFeatures/util/textRendering.ts b/extensions/typescript-language-features/src/languageFeatures/util/textRendering.ts index 2918a7de54c..fe26dd64029 100644 --- a/extensions/typescript-language-features/src/languageFeatures/util/textRendering.ts +++ b/extensions/typescript-language-features/src/languageFeatures/util/textRendering.ts @@ -188,10 +188,10 @@ function convertLinkTags( if (/^https?:/.test(text)) { const parts = text.split(' '); if (parts.length === 1) { - out.push(parts[0]); + out.push(`<${parts[0]}>`); } else if (parts.length > 1) { - const linkText = escapeMarkdownSyntaxTokensForCode(parts.slice(1).join(' ')); - out.push(`[${currentLink.linkcode ? '`' + linkText + '`' : linkText}](${parts[0]})`); + const linkText = parts.slice(1).join(' '); + out.push(`[${currentLink.linkcode ? '`' + escapeMarkdownSyntaxTokensForCode(linkText) + '`' : linkText}](${parts[0]})`); } } else { out.push(escapeMarkdownSyntaxTokensForCode(text)); diff --git a/extensions/typescript-language-features/src/typescriptServiceClient.ts b/extensions/typescript-language-features/src/typescriptServiceClient.ts index 984356f17b4..86c6bb8d9f1 100644 --- a/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -29,6 +29,7 @@ import { PluginManager, TypeScriptServerPlugin } from './tsServer/plugins'; import { TelemetryProperties, TelemetryReporter, VSCodeTelemetryReporter } from './logging/telemetry'; import Tracer from './logging/tracer'; import { ProjectType, inferredProjectCompilerOptions } from './tsconfig'; +import { Schemes } from './configuration/schemes'; export interface TsDiagnostics { @@ -762,6 +763,18 @@ export default class TypeScriptServiceClient extends Disposable implements IType return undefined; } + // For notebook cells, we need to use the notebook document to look up the workspace + if (resource.scheme === Schemes.notebookCell) { + for (const notebook of vscode.workspace.notebookDocuments) { + for (const cell of notebook.getCells()) { + if (cell.document.uri.toString() === resource.toString()) { + resource = notebook.uri; + break; + } + } + } + } + for (const root of roots.sort((a, b) => a.uri.fsPath.length - b.uri.fsPath.length)) { if (root.uri.scheme === resource.scheme && root.uri.authority === resource.authority) { if (resource.fsPath.startsWith(root.uri.fsPath + path.sep)) { @@ -770,7 +783,7 @@ export default class TypeScriptServiceClient extends Disposable implements IType } } - return undefined; + return vscode.workspace.getWorkspaceFolder(resource)?.uri; } public execute(command: keyof TypeScriptRequests, args: any, token: vscode.CancellationToken, config?: ExecConfig): Promise> { diff --git a/extensions/vscode-api-tests/package.json b/extensions/vscode-api-tests/package.json index 369927c00f2..b3e681dc25b 100644 --- a/extensions/vscode-api-tests/package.json +++ b/extensions/vscode-api-tests/package.json @@ -46,7 +46,6 @@ "treeItemCheckbox", "treeViewActiveItem", "treeViewReveal", - "testInvalidateResults", "workspaceTrust", "telemetry", "windowActivity", diff --git a/package.json b/package.json index a1961f94d9d..2708da44717 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.81.0", - "distro": "97db68abc58fce4ef0a70cdaa6255836e6a8d085", + "distro": "183ff774d6e9be194f3ef3014b582d92a3b1ce9d", "author": { "name": "Microsoft Corporation" }, @@ -70,7 +70,7 @@ "@parcel/watcher": "2.1.0", "@vscode/iconv-lite-umd": "0.7.0", "@vscode/policy-watcher": "^1.1.4", - "@vscode/proxy-agent": "^0.15.0", + "@vscode/proxy-agent": "^0.16.0", "@vscode/ripgrep": "^1.15.5", "@vscode/spdlog": "^0.13.10", "@vscode/sqlite3": "5.1.6-vscode", @@ -148,7 +148,7 @@ "cssnano": "^4.1.11", "debounce": "^1.0.0", "deemon": "^1.8.0", - "electron": "22.3.14", + "electron": "22.3.17", "eslint": "8.36.0", "eslint-plugin-header": "3.1.1", "eslint-plugin-jsdoc": "^39.3.2", @@ -210,7 +210,7 @@ "ts-loader": "^9.4.2", "ts-node": "^10.9.1", "tsec": "0.2.7", - "typescript": "^5.2.0-dev.20230712", + "typescript": "^5.2.0-dev.20230718", "typescript-formatter": "7.1.0", "underscore": "^1.12.1", "util": "^0.12.4", @@ -231,4 +231,4 @@ "optionalDependencies": { "windows-foreground-love": "0.5.0" } -} \ No newline at end of file +} diff --git a/remote/package.json b/remote/package.json index 9b92d007a47..e10bd518df4 100644 --- a/remote/package.json +++ b/remote/package.json @@ -7,7 +7,7 @@ "@microsoft/1ds-post-js": "^3.2.2", "@parcel/watcher": "2.1.0", "@vscode/iconv-lite-umd": "0.7.0", - "@vscode/proxy-agent": "^0.15.0", + "@vscode/proxy-agent": "^0.16.0", "@vscode/ripgrep": "^1.15.5", "@vscode/spdlog": "^0.13.10", "@vscode/vscode-languagedetection": "1.0.21", diff --git a/remote/yarn.lock b/remote/yarn.lock index 8084ab6e27a..7c74dbf3e57 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -48,27 +48,27 @@ node-addon-api "^3.2.1" node-gyp-build "^4.3.0" -"@tootallnate/once@1", "@tootallnate/once@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" - integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== +"@tootallnate/once@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-3.0.0.tgz#d52238c9052d746c9689523e650160e70786bc9a" + integrity sha512-OAdBVB7rlwvLD+DiecSAyVKzKVmSfXbouCyM5I6wHGi4MGXIyFqErg1IvyJ7PI1e+GYZuZh7cCHV/c4LA8SKMw== "@vscode/iconv-lite-umd@0.7.0": version "0.7.0" resolved "https://registry.yarnpkg.com/@vscode/iconv-lite-umd/-/iconv-lite-umd-0.7.0.tgz#d2f1e0664ee6036408f9743fee264ea0699b0e48" integrity sha512-bRRFxLfg5dtAyl5XyiVWz/ZBPahpOpPrNYnnHpOpUZvam4tKH35wdhP4Kj6PbM0+KdliOsPzbGWpkxcdpNB/sg== -"@vscode/proxy-agent@^0.15.0": - version "0.15.0" - resolved "https://registry.yarnpkg.com/@vscode/proxy-agent/-/proxy-agent-0.15.0.tgz#b8fb8b89180a71295a8f8682775f69ab1dcf6860" - integrity sha512-HpD4A9CUOwKbC6vLa0+MEsCo/qlgbue9U9s8Z7NzJDdf2YEGjUaYf9Mvj5T1LhJX20Hv1COvkGcc7zPhtIbgbA== +"@vscode/proxy-agent@^0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@vscode/proxy-agent/-/proxy-agent-0.16.0.tgz#32054387f7aaf26d1b5d53f553d53bfd8489eab8" + integrity sha512-b8yBHgdngDrP+9HPJtnPUJjPHd+zfEvOYoc8KioWJVs0rFVT2U77nFDVC70Mrrscf87ya2a/sPY32nTrwFfOQQ== dependencies: - "@tootallnate/once" "^1.1.2" - agent-base "^6.0.2" - debug "^4.3.1" - http-proxy-agent "^4.0.1" - https-proxy-agent "^5.0.0" - socks-proxy-agent "^5.0.0" + "@tootallnate/once" "^3.0.0" + agent-base "^7.0.1" + debug "^4.3.4" + http-proxy-agent "^7.0.0" + https-proxy-agent "^7.0.1" + socks-proxy-agent "^8.0.1" optionalDependencies: "@vscode/windows-ca-certs" "^0.3.1" @@ -120,7 +120,7 @@ agent-base@4: dependencies: es6-promisify "^5.0.0" -agent-base@6, agent-base@^6.0.2: +agent-base@6: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== @@ -134,6 +134,13 @@ agent-base@^4.3.0: dependencies: es6-promisify "^5.0.0" +agent-base@^7.0.1, agent-base@^7.0.2, agent-base@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.0.tgz#536802b76bc0b34aa50195eb2442276d613e3434" + integrity sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg== + dependencies: + debug "^4.3.4" + ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" @@ -237,10 +244,10 @@ debug@4: dependencies: ms "^2.1.1" -debug@^4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" - integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== +debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: ms "2.1.2" @@ -356,14 +363,13 @@ http-proxy-agent@^2.1.0: agent-base "4" debug "3.1.0" -http-proxy-agent@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" - integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== +http-proxy-agent@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz#e9096c5afd071a3fce56e6252bb321583c124673" + integrity sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ== dependencies: - "@tootallnate/once" "1" - agent-base "6" - debug "4" + agent-base "^7.1.0" + debug "^4.3.4" https-proxy-agent@^2.2.3: version "2.2.4" @@ -381,6 +387,14 @@ https-proxy-agent@^5.0.0: agent-base "6" debug "4" +https-proxy-agent@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.1.tgz#0277e28f13a07d45c663633841e20a40aaafe0ab" + integrity sha512-Eun8zV0kcYS1g19r78osiQLEFIRspRUDd9tIfBCTBPBeMieF/EsJNL8VI3xOIdYRDEkjQnqOYPsZ2DsWsVsFwQ== + dependencies: + agent-base "^7.0.2" + debug "4" + ieee754@^1.1.13: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" @@ -396,10 +410,10 @@ ini@~1.3.0: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== -ip@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" - integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= +ip@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da" + integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ== is-extglob@^2.1.1: version "2.1.1" @@ -691,27 +705,27 @@ simple-get@^4.0.0: once "^1.3.1" simple-concat "^1.0.0" -smart-buffer@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.1.0.tgz#91605c25d91652f4661ea69ccf45f1b331ca21ba" - integrity sha512-iVICrxOzCynf/SNaBQCw34eM9jROU/s5rzIhpOvzhzuYHfJR/DhZfDkXiZSgKXfgv26HT3Yni3AV/DGw0cGnnw== +smart-buffer@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== -socks-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-5.0.0.tgz#7c0f364e7b1cf4a7a437e71253bed72e9004be60" - integrity sha512-lEpa1zsWCChxiynk+lCycKuC502RxDWLKJZoIhnxrWNjLSDGYRFflHA1/228VkRcnv9TIb8w98derGbpKxJRgA== +socks-proxy-agent@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-8.0.1.tgz#ffc5859a66dac89b0c4dab90253b96705f3e7120" + integrity sha512-59EjPbbgg8U3x62hhKOFVAmySQUcfRQ4C7Q/D5sEHnZTQRrQlNKINks44DMR1gwXp0p4LaVIeccX2KHTTcHVqQ== dependencies: - agent-base "6" - debug "4" - socks "^2.3.3" + agent-base "^7.0.1" + debug "^4.3.4" + socks "^2.7.1" -socks@^2.3.3: - version "2.6.1" - resolved "https://registry.yarnpkg.com/socks/-/socks-2.6.1.tgz#989e6534a07cf337deb1b1c94aaa44296520d30e" - integrity sha512-kLQ9N5ucj8uIcxrDwjm0Jsqk06xdpBjGNQtpXy4Q8/QY2k+fY7nZH8CARy+hkbG+SGAovmzzuauCpBlb8FrnBA== +socks@^2.7.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.7.1.tgz#d8e651247178fde79c0663043e07240196857d55" + integrity sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ== dependencies: - ip "^1.1.5" - smart-buffer "^4.1.0" + ip "^2.0.0" + smart-buffer "^4.2.0" string-width@^1.0.1: version "1.0.2" diff --git a/scripts/playground-server.ts b/scripts/playground-server.ts index 9468087409f..1c2074ee191 100644 --- a/scripts/playground-server.ts +++ b/scripts/playground-server.ts @@ -280,7 +280,7 @@ function makeLoaderJsHotReloadable(loaderJsCode: string, fileChangesUrl: URL): s if (___globalModuleManager._modules2[moduleId]) { const srcUrl = ___globalModuleManager._config.moduleIdToPaths(data.moduleId); const newSrc = await (await fetch(srcUrl)).text(); - (new Function('define', newSrc))(function (deps, callback) { + (new Function('define', newSrc))(function (deps, callback) { // CodeQL [SM01632] This code is only executed during development (as part of the dev-only playground-server). It is required for the hot-reload functionality. const oldModule = ___globalModuleManager._modules2[moduleId]; delete ___globalModuleManager._modules2[moduleId]; diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index c4f9b728259..872263f2e9e 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -682,6 +682,7 @@ export namespace Event { } }; observable.addObserver(observer); + observable.reportChanges(); return { dispose() { observable.removeObserver(observer); diff --git a/src/vs/base/common/htmlContent.ts b/src/vs/base/common/htmlContent.ts index 9cefc0e56a4..10e25c9c9e3 100644 --- a/src/vs/base/common/htmlContent.ts +++ b/src/vs/base/common/htmlContent.ts @@ -60,7 +60,7 @@ export class MarkdownString implements IMarkdownString { this.value += escapeMarkdownSyntaxTokens(this.supportThemeIcons ? escapeIcons(value) : value) .replace(/([ \t]+)/g, (_match, g1) => ' '.repeat(g1.length)) .replace(/\>/gm, '\\>') - .replace(/\n/g, newlineStyle === MarkdownStringTextNewlineStyle.Break ? '\\\n' : '\n\n'); + .replace(/\n/g, newlineStyle === MarkdownStringTextNewlineStyle.Break ? '\\\n' : '\n\n'); // CodeQL [SM02383] The Markdown is fully sanitized after being rendered. return this; } diff --git a/src/vs/base/common/observableImpl/utils.ts b/src/vs/base/common/observableImpl/utils.ts index 5d9a2c568a8..b922cea1c95 100644 --- a/src/vs/base/common/observableImpl/utils.ts +++ b/src/vs/base/common/observableImpl/utils.ts @@ -278,6 +278,7 @@ export function wasEventTriggeredRecently(event: Event, timeoutMs: number, return observable; } +// TODO@hediet: Have `keepCacheAlive` and `recomputeOnChange` instead of forceRecompute /** * This ensures the observable is being observed. * Observed observables (such as {@link derived}s) can maintain a cache, as they receive invalidation events. diff --git a/src/vs/base/common/prefixTree.ts b/src/vs/base/common/prefixTree.ts new file mode 100644 index 00000000000..b4001390526 --- /dev/null +++ b/src/vs/base/common/prefixTree.ts @@ -0,0 +1,88 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +const unset = Symbol('unset'); + +/** + * A simple prefix tree implementation where a value is stored based on + * well-defined prefix segments. + */ +export class WellDefinedPrefixTree { + private readonly root = new Node(); + + /** Inserts a new value in the prefix tree. */ + insert(key: Iterable, value: V): void { + let node = this.root; + for (const part of key) { + if (!node.children) { + const next = new Node(); + node.children = new Map([[part, next]]); + node = next; + } else if (!node.children.has(part)) { + const next = new Node(); + node.children.set(part, next); + node = next; + } else { + node = node.children.get(part)!; + } + + } + + node.value = value; + } + + /** Gets a value from the tree. */ + find(key: Iterable): V | undefined { + let node = this.root; + for (const segment of key) { + const next = node.children?.get(segment); + if (!next) { + return undefined; + } + + node = next; + } + + return node.value === unset ? undefined : node.value; + } + + /** Gets whether the tree has the key, or a parent of the key, already inserted. */ + hasKeyOrParent(key: Iterable): boolean { + let node = this.root; + for (const segment of key) { + const next = node.children?.get(segment); + if (!next) { + return false; + } + if (next.value !== unset) { + return true; + } + + node = next; + } + + return false; + } + + /** Gets whether the tree has the given key or any children. */ + hasKeyOrChildren(key: Iterable): boolean { + let node = this.root; + for (const segment of key) { + const next = node.children?.get(segment); + if (!next) { + return false; + } + + node = next; + } + + return true; + } +} + +class Node { + public children?: Map>; + public value: T | typeof unset = unset; +} diff --git a/src/vs/base/test/common/observable.test.ts b/src/vs/base/test/common/observable.test.ts index 64d08a6c4d5..822becee547 100644 --- a/src/vs/base/test/common/observable.test.ts +++ b/src/vs/base/test/common/observable.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { Emitter } from 'vs/base/common/event'; +import { Emitter, Event } from 'vs/base/common/event'; import { ISettableObservable, autorun, derived, ITransaction, observableFromEvent, observableValue, transaction, keepAlive } from 'vs/base/common/observable'; import { BaseObservable, IObservable, IObserver } from 'vs/base/common/observableImpl/base'; @@ -962,6 +962,36 @@ suite('observables', () => { myObservable2.set(1, tx); }); }); + + test('bug: fromObservableLight doesnt subscribe', () => { + const log = new Log(); + const myObservable = new LoggingObservableValue('myObservable', 0, log); + + const myDerived = derived('myDerived', reader => { + const val = myObservable.read(reader); + log.log(`myDerived.computed(myObservable2: ${val})`); + return val % 10; + }); + + const e = Event.fromObservableLight(myDerived); + log.log('event created'); + e(() => { + log.log('event fired'); + }); + + myObservable.set(1, undefined); + + assert.deepStrictEqual(log.getAndClearEntries(), [ + 'event created', + 'myObservable.firstObserverAdded', + 'myObservable.get', + 'myDerived.computed(myObservable2: 0)', + 'myObservable.set (value 1)', + 'myObservable.get', + 'myDerived.computed(myObservable2: 1)', + 'event fired', + ]); + }); }); export class LoggingObserver implements IObserver { diff --git a/src/vs/base/test/common/prefixTree.test.ts b/src/vs/base/test/common/prefixTree.test.ts new file mode 100644 index 00000000000..3b1cb88ae86 --- /dev/null +++ b/src/vs/base/test/common/prefixTree.test.ts @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { WellDefinedPrefixTree } from 'vs/base/common/prefixTree'; +import * as assert from 'assert'; + +suite('WellDefinedPrefixTree', () => { + let tree: WellDefinedPrefixTree; + + setup(() => { + tree = new WellDefinedPrefixTree(); + }); + + test('find', () => { + const key1 = ['foo', 'bar']; + const key2 = ['foo', 'baz']; + tree.insert(key1, 42); + tree.insert(key2, 43); + assert.strictEqual(tree.find(key1), 42); + assert.strictEqual(tree.find(key2), 43); + assert.strictEqual(tree.find(['foo', 'baz', 'bop']), undefined); + assert.strictEqual(tree.find(['foo']), undefined); + }); + + test('hasParentOfKey', () => { + const key = ['foo', 'bar']; + tree.insert(key, 42); + + assert.strictEqual(tree.hasKeyOrParent(['foo', 'bar', 'baz']), true); + assert.strictEqual(tree.hasKeyOrParent(['foo', 'bar']), true); + assert.strictEqual(tree.hasKeyOrParent(['foo']), false); + assert.strictEqual(tree.hasKeyOrParent(['baz']), false); + }); + + + test('hasKeyOrChildren', () => { + const key = ['foo', 'bar']; + tree.insert(key, 42); + + assert.strictEqual(tree.hasKeyOrChildren([]), true); + assert.strictEqual(tree.hasKeyOrChildren(['foo']), true); + assert.strictEqual(tree.hasKeyOrChildren(['foo', 'bar']), true); + assert.strictEqual(tree.hasKeyOrChildren(['foo', 'bar', 'baz']), false); + }); +}); diff --git a/src/vs/base/worker/workerMain.ts b/src/vs/base/worker/workerMain.ts index 65df427d79e..ec18c5ca0f4 100644 --- a/src/vs/base/worker/workerMain.ts +++ b/src/vs/base/worker/workerMain.ts @@ -54,7 +54,7 @@ try { const func = ( trustedTypesPolicy - ? globalThis.eval(trustedTypesPolicy.createScript('', 'true')) + ? globalThis.eval(trustedTypesPolicy.createScript('', 'true')) // CodeQL [SM01632] fetch + eval is used on the web worker instead of importScripts if possible because importScripts is synchronous and we observed deadlocks on Safari : new Function('true') // CodeQL [SM01632] fetch + eval is used on the web worker instead of importScripts if possible because importScripts is synchronous and we observed deadlocks on Safari ); func.call(globalThis); diff --git a/src/vs/code/electron-main/auth.ts b/src/vs/code/electron-main/auth.ts index 9e56bcfa902..6a18eba4eb5 100644 --- a/src/vs/code/electron-main/auth.ts +++ b/src/vs/code/electron-main/auth.ts @@ -8,11 +8,14 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Event } from 'vs/base/common/event'; import { hash } from 'vs/base/common/hash'; import { Disposable } from 'vs/base/common/lifecycle'; +import { withNullAsUndefined } from 'vs/base/common/types'; import { generateUuid } from 'vs/base/common/uuid'; import { ICredentialsMainService } from 'vs/platform/credentials/common/credentials'; import { IEncryptionMainService } from 'vs/platform/encryption/common/encryptionService'; import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; +import { StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { IApplicationStorageMainService } from 'vs/platform/storage/electron-main/storageMainService'; import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows'; interface ElectronAuthenticationResponseDetails extends AuthenticationResponseDetails { @@ -56,7 +59,8 @@ enum ProxyAuthState { export class ProxyAuthHandler extends Disposable { - private readonly PROXY_CREDENTIALS_SERVICE_KEY = `${this.productService.urlProtocol}.proxy-credentials`; + private readonly OLD_PROXY_CREDENTIALS_SERVICE_KEY = `${this.productService.urlProtocol}.proxy-credentials`; + private readonly PROXY_CREDENTIALS_SERVICE_KEY = 'proxy-credentials://'; private pendingProxyResolve: Promise | undefined = undefined; @@ -69,6 +73,7 @@ export class ProxyAuthHandler extends Disposable { @IWindowsMainService private readonly windowsMainService: IWindowsMainService, @ICredentialsMainService private readonly credentialsService: ICredentialsMainService, @IEncryptionMainService private readonly encryptionMainService: IEncryptionMainService, + @IApplicationStorageMainService private readonly applicationStorageMainService: IApplicationStorageMainService, @IProductService private readonly productService: IProductService ) { super(); @@ -141,6 +146,34 @@ export class ProxyAuthHandler extends Disposable { return undefined; } + // TODO: remove this migration in a release or two. + private async getAndMigrateProxyCredentials(authInfoHash: string): Promise<{ storedUsername: string | undefined; storedPassword: string | undefined }> { + // Find any previously stored credentials + try { + let encryptedSerializedProxyCredentials = this.applicationStorageMainService.get(this.PROXY_CREDENTIALS_SERVICE_KEY + authInfoHash, StorageScope.APPLICATION); + let decryptedSerializedProxyCredentials: string | undefined; + if (!encryptedSerializedProxyCredentials) { + encryptedSerializedProxyCredentials = withNullAsUndefined(await this.credentialsService.getPassword(this.OLD_PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash)); + if (encryptedSerializedProxyCredentials) { + // re-encrypt to force new encryption algorithm to apply + decryptedSerializedProxyCredentials = await this.encryptionMainService.decrypt(encryptedSerializedProxyCredentials); + encryptedSerializedProxyCredentials = await this.encryptionMainService.encrypt(decryptedSerializedProxyCredentials); + this.applicationStorageMainService.store(this.PROXY_CREDENTIALS_SERVICE_KEY + authInfoHash, encryptedSerializedProxyCredentials, StorageScope.APPLICATION, StorageTarget.MACHINE); + // Remove it from the old location since it's in the new location. + await this.credentialsService.deletePassword(this.OLD_PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash); + } + } + if (encryptedSerializedProxyCredentials) { + const credentials: Credentials = JSON.parse(decryptedSerializedProxyCredentials ?? await this.encryptionMainService.decrypt(encryptedSerializedProxyCredentials)); + + return { storedUsername: credentials.username, storedPassword: credentials.password }; + } + } catch (error) { + this.logService.error(error); // handle errors by asking user for login via dialog + } + return { storedUsername: undefined, storedPassword: undefined }; + } + private async doResolveProxyCredentials(authInfo: AuthInfo): Promise { this.logService.trace('auth#doResolveProxyCredentials - enter', authInfo); @@ -149,21 +182,7 @@ export class ProxyAuthHandler extends Disposable { // given the properties of the auth request // (see https://github.com/microsoft/vscode/issues/109497) const authInfoHash = String(hash({ scheme: authInfo.scheme, host: authInfo.host, port: authInfo.port })); - - // Find any previously stored credentials - let storedUsername: string | undefined = undefined; - let storedPassword: string | undefined = undefined; - try { - const encryptedSerializedProxyCredentials = await this.credentialsService.getPassword(this.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash); - if (encryptedSerializedProxyCredentials) { - const credentials: Credentials = JSON.parse(await this.encryptionMainService.decrypt(encryptedSerializedProxyCredentials)); - - storedUsername = credentials.username; - storedPassword = credentials.password; - } - } catch (error) { - this.logService.error(error); // handle errors by asking user for login via dialog - } + const { storedUsername, storedPassword } = await this.getAndMigrateProxyCredentials(authInfoHash); // Reply with stored credentials unless we used them already. // In that case we need to show a login dialog again because @@ -212,9 +231,9 @@ export class ProxyAuthHandler extends Disposable { try { if (reply.remember) { const encryptedSerializedCredentials = await this.encryptionMainService.encrypt(JSON.stringify(credentials)); - await this.credentialsService.setPassword(this.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash, encryptedSerializedCredentials); + this.applicationStorageMainService.store(this.PROXY_CREDENTIALS_SERVICE_KEY + authInfoHash, encryptedSerializedCredentials, StorageScope.APPLICATION, StorageTarget.MACHINE); } else { - await this.credentialsService.deletePassword(this.PROXY_CREDENTIALS_SERVICE_KEY, authInfoHash); + this.applicationStorageMainService.remove(this.PROXY_CREDENTIALS_SERVICE_KEY + authInfoHash, StorageScope.APPLICATION); } } catch (error) { this.logService.error(error); // handle gracefully diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index 0065a8da62e..a003267c043 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -192,7 +192,8 @@ class CliMain extends Disposable { services.set(IUriIdentityService, new UriIdentityService(fileService)); // Request - services.set(IRequestService, new SyncDescriptor(RequestService, undefined, true)); + const requestService = new RequestService(configurationService, environmentService, logService, loggerService); + services.set(IRequestService, requestService); // Download Service services.set(IDownloadService, new SyncDescriptor(DownloadService, undefined, true)); @@ -212,7 +213,7 @@ class CliMain extends Disposable { const isInternal = isInternalTelemetry(productService, configurationService); if (supportsTelemetry(productService, environmentService)) { if (productService.aiConfig && productService.aiConfig.ariaKey) { - appenders.push(new OneDataSystemAppender(isInternal, 'monacoworkbench', null, productService.aiConfig.ariaKey)); + appenders.push(new OneDataSystemAppender(requestService, isInternal, 'monacoworkbench', null, productService.aiConfig.ariaKey)); } const config: ITelemetryServiceConfig = { diff --git a/src/vs/code/node/sharedProcess/sharedProcessMain.ts b/src/vs/code/node/sharedProcess/sharedProcessMain.ts index d8b6d791e74..f911b0da431 100644 --- a/src/vs/code/node/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/node/sharedProcess/sharedProcessMain.ts @@ -251,7 +251,8 @@ class SharedProcessMain extends Disposable { services.set(IUriIdentityService, uriIdentityService); // Request - services.set(IRequestService, new RequestChannelClient(mainProcessService.getChannel('request'))); + const requestService = new RequestChannelClient(mainProcessService.getChannel('request')); + services.set(IRequestService, requestService); // Checksum services.set(IChecksumService, new SyncDescriptor(ChecksumService, undefined, false /* proxied to other processes */)); @@ -279,7 +280,7 @@ class SharedProcessMain extends Disposable { const logAppender = new TelemetryLogAppender(logService, loggerService, environmentService, productService); appenders.push(logAppender); if (productService.aiConfig?.ariaKey) { - const collectorAppender = new OneDataSystemAppender(internalTelemetry, 'monacoworkbench', null, productService.aiConfig.ariaKey); + const collectorAppender = new OneDataSystemAppender(requestService, internalTelemetry, 'monacoworkbench', null, productService.aiConfig.ariaKey); this._register(toDisposable(() => collectorAppender.flush())); // Ensure the 1DS appender is disposed so that it flushes remaining data appenders.push(collectorAppender); } diff --git a/src/vs/editor/browser/controller/textAreaHandler.ts b/src/vs/editor/browser/controller/textAreaHandler.ts index bc95af4fe9e..94fa1a8bdb1 100644 --- a/src/vs/editor/browser/controller/textAreaHandler.ts +++ b/src/vs/editor/browser/controller/textAreaHandler.ts @@ -35,6 +35,7 @@ import { TokenizationRegistry } from 'vs/editor/common/languages'; import { ColorId, ITokenPresentation } from 'vs/editor/common/encodedTokenAttributes'; import { Color } from 'vs/base/common/color'; import { IME } from 'vs/base/common/ime'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; export interface IVisibleRangeProvider { visibleRangeForPosition(position: Position): HorizontalPosition | null; @@ -140,7 +141,12 @@ export class TextAreaHandler extends ViewPart { public readonly textAreaCover: FastDomNode; private readonly _textAreaInput: TextAreaInput; - constructor(context: ViewContext, viewController: ViewController, visibleRangeProvider: IVisibleRangeProvider) { + constructor( + context: ViewContext, + viewController: ViewController, + visibleRangeProvider: IVisibleRangeProvider, + @IKeybindingService private readonly _keybindingService: IKeybindingService + ) { super(context); this._viewController = viewController; @@ -553,7 +559,21 @@ export class TextAreaHandler extends ViewPart { private _getAriaLabel(options: IComputedEditorOptions): string { const accessibilitySupport = options.get(EditorOption.accessibilitySupport); if (accessibilitySupport === AccessibilitySupport.Disabled) { - return nls.localize('accessibilityOffAriaLabel', "The editor is not accessible at this time. Press {0} for options.", platform.isLinux ? 'Shift+Alt+F1' : 'Alt+F1'); + + const toggleKeybindingLabel = this._keybindingService.lookupKeybinding('editor.action.toggleScreenReaderAccessibilityMode')?.getAriaLabel(); + const runCommandKeybindingLabel = this._keybindingService.lookupKeybinding('workbench.action.showCommands')?.getAriaLabel(); + const keybindingEditorKeybindingLabel = this._keybindingService.lookupKeybinding('workbench.action.openGlobalKeybindings')?.getAriaLabel(); + const editorNotAccessibleMessage = nls.localize('accessibilityModeOff', "The editor is not accessible at this time."); + if (toggleKeybindingLabel) { + return nls.localize('accessibilityOffAriaLabel', "{0} To enable screen reader optimized mode, use {1}", editorNotAccessibleMessage, toggleKeybindingLabel); + } else if (runCommandKeybindingLabel) { + return nls.localize('accessibilityOffAriaLabelNoKb', "{0} To enable screen reader optimized mode, open the quick pick with {1} and run the command Toggle Screen Reader Accessibility Mode, which is currently not triggerable via keyboard.", editorNotAccessibleMessage, runCommandKeybindingLabel); + } else if (keybindingEditorKeybindingLabel) { + return nls.localize('accessibilityOffAriaLabelNoKbs', "{0} Please assign a keybinding for the command Toggle Screen Reader Accessibility Mode by accessing the keybindings editor with {1} and run it.", editorNotAccessibleMessage, keybindingEditorKeybindingLabel); + } else { + // SOS + return editorNotAccessibleMessage; + } } return options.get(EditorOption.ariaLabel); } diff --git a/src/vs/editor/browser/view.ts b/src/vs/editor/browser/view.ts index 82d1fb7a269..0e791b14305 100644 --- a/src/vs/editor/browser/view.ts +++ b/src/vs/editor/browser/view.ts @@ -54,6 +54,7 @@ import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { WhitespaceOverlay } from 'vs/editor/browser/viewParts/whitespace/whitespace'; import { GlyphMarginWidgets } from 'vs/editor/browser/viewParts/glyphMargin/glyphMargin'; import { GlyphMarginLane } from 'vs/editor/common/model'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; export interface IContentWidgetData { @@ -106,7 +107,8 @@ export class View extends ViewEventHandler { colorTheme: IColorTheme, model: IViewModel, userInputEvents: ViewUserInputEvents, - overflowWidgetsDomNode: HTMLElement | undefined + overflowWidgetsDomNode: HTMLElement | undefined, + @IInstantiationService private readonly _instantiationService: IInstantiationService ) { super(); this._selections = [new Selection(1, 1, 1, 1)]; @@ -123,7 +125,7 @@ export class View extends ViewEventHandler { this._viewParts = []; // Keyboard handler - this._textAreaHandler = new TextAreaHandler(this._context, viewController, this._createTextAreaHandlerHelper()); + this._textAreaHandler = this._instantiationService.createInstance(TextAreaHandler, this._context, viewController, this._createTextAreaHandlerHelper()); this._viewParts.push(this._textAreaHandler); // These two dom nodes must be constructed up front, since references are needed in the layout provider (scrolling & co.) diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index 0903e174700..f6389bf5410 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -73,6 +73,8 @@ export interface ICodeEditorWidgetOptions { /** * Contributions to instantiate. + * When provided, only the contributions included will be instantiated. + * To include the defaults, those must be provided as well via [...EditorExtensionsRegistry.getEditorContributions()] * Defaults to EditorExtensionsRegistry.getEditorContributions(). */ contributions?: IEditorContributionDescription[]; @@ -1851,7 +1853,8 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE this._themeService.getColorTheme(), viewModel, viewUserInputEvents, - this._overflowWidgetsDomNode + this._overflowWidgetsDomNode, + this._instantiationService ); return [view, true]; diff --git a/src/vs/editor/browser/widget/diffEditor.contribution.ts b/src/vs/editor/browser/widget/diffEditor.contribution.ts index 456dc90b293..2deb1fbc133 100644 --- a/src/vs/editor/browser/widget/diffEditor.contribution.ts +++ b/src/vs/editor/browser/widget/diffEditor.contribution.ts @@ -5,59 +5,67 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser'; -import { EditorAction, ServicesAccessor, registerEditorAction } from 'vs/editor/browser/editorExtensions'; +import { EditorAction2, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { localize } from 'vs/nls'; +import { ILocalizedString } from 'vs/platform/action/common/action'; +import { registerAction2 } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -export class DiffReviewNext extends EditorAction { + +const accessibleDiffViewerCategory: ILocalizedString = { + value: localize('accessibleDiffViewer', 'Accessible Diff Viewer'), + original: 'Accessible Diff Viewer', +}; + +export class DiffReviewNext extends EditorAction2 { public static id = 'editor.action.diffReview.next'; constructor() { super({ id: DiffReviewNext.id, - label: localize('editor.action.diffReview.next', "Go to Next Difference"), - alias: 'Go to Next Difference', + title: { value: localize('editor.action.diffReview.next', "Go to Next Difference"), original: 'Go to Next Difference' }, + category: accessibleDiffViewerCategory, precondition: ContextKeyExpr.has('isInDiffEditor'), - kbOpts: { - kbExpr: null, + keybinding: { primary: KeyCode.F7, weight: KeybindingWeight.EditorContrib - } + }, + f1: true, }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + public override runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor): void { const diffEditor = findFocusedDiffEditor(accessor); diffEditor?.diffReviewNext(); } } -export class DiffReviewPrev extends EditorAction { +export class DiffReviewPrev extends EditorAction2 { public static id = 'editor.action.diffReview.prev'; constructor() { super({ id: DiffReviewPrev.id, - label: localize('editor.action.diffReview.prev', "Go to Previous Difference"), - alias: 'Go to Previous Difference', + title: { value: localize('editor.action.diffReview.prev', "Go to Previous Difference"), original: 'Go to Previous Difference' }, + category: accessibleDiffViewerCategory, precondition: ContextKeyExpr.has('isInDiffEditor'), - kbOpts: { - kbExpr: null, + keybinding: { primary: KeyMod.Shift | KeyCode.F7, weight: KeybindingWeight.EditorContrib - } + }, + f1: true, }); } - public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + public override runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor): void { const diffEditor = findFocusedDiffEditor(accessor); diffEditor?.diffReviewPrev(); } } -function findFocusedDiffEditor(accessor: ServicesAccessor): IDiffEditor | null { +export function findFocusedDiffEditor(accessor: ServicesAccessor): IDiffEditor | null { const codeEditorService = accessor.get(ICodeEditorService); const diffEditors = codeEditorService.listDiffEditors(); const activeCodeEditor = codeEditorService.getFocusedCodeEditor() ?? codeEditorService.getActiveCodeEditor(); @@ -74,5 +82,5 @@ function findFocusedDiffEditor(accessor: ServicesAccessor): IDiffEditor | null { return null; } -registerEditorAction(DiffReviewNext); -registerEditorAction(DiffReviewPrev); +registerAction2(DiffReviewNext); +registerAction2(DiffReviewPrev); diff --git a/src/vs/editor/browser/widget/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditorWidget.ts index e21c1e9f3be..5166c3fe561 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget.ts @@ -299,6 +299,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE collapseUnchangedRegions: false, }, isInEmbeddedEditor: false, + onlyShowAccessibleDiffViewer: false, }); this.isEmbeddedDiffEditorKey = EditorContextKeys.isEmbeddedDiffEditor.bindTo(this._contextKeyService); @@ -1368,7 +1369,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._originalDomNode.style.width = splitPoint + 'px'; this._originalDomNode.style.left = '0px'; - this._modifiedDomNode.style.width = (width - splitPoint) + 'px'; + this._modifiedDomNode.style.width = (width - splitPoint - DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH) + 'px'; this._modifiedDomNode.style.left = splitPoint + 'px'; this._overviewDomElement.style.top = '0px'; @@ -2743,6 +2744,7 @@ function validateDiffEditorOptions(options: Readonly, defaul collapseUnchangedRegions: false, }, isInEmbeddedEditor: validateBooleanOption(options.isInEmbeddedEditor, defaults.isInEmbeddedEditor), + onlyShowAccessibleDiffViewer: false, }; } diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/accessibleDiffViewer.ts b/src/vs/editor/browser/widget/diffEditorWidget2/accessibleDiffViewer.ts new file mode 100644 index 00000000000..cc0f3a5a7a0 --- /dev/null +++ b/src/vs/editor/browser/widget/diffEditorWidget2/accessibleDiffViewer.ts @@ -0,0 +1,685 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { addDisposableListener, addStandardDisposableListener, reset } from 'vs/base/browser/dom'; +import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; +import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; +import { Action } from 'vs/base/common/actions'; +import { Codicon } from 'vs/base/common/codicons'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { IObservable, ITransaction, autorun, derived, keepAlive, observableValue, transaction } from 'vs/base/common/observable'; +import { autorunWithStore2 } from 'vs/base/common/observableImpl/autorun'; +import { subtransaction } from 'vs/base/common/observableImpl/base'; +import { derivedWithStore } from 'vs/base/common/observableImpl/derived'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { applyFontInfo } from 'vs/editor/browser/config/domFontInfo'; +import { DiffEditorEditors } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorEditors'; +import { applyStyle } from 'vs/editor/browser/widget/diffEditorWidget2/utils'; +import { DiffReview } from 'vs/editor/browser/widget/diffReview'; +import { EditorFontLigatures, EditorOption, IComputedEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { LineRange } from 'vs/editor/common/core/lineRange'; +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { LineRangeMapping, SimpleLineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer'; +import { ILanguageIdCodec } from 'vs/editor/common/languages'; +import { ILanguageService } from 'vs/editor/common/languages/language'; +import { ITextModel, TextModelResolvedOptions } from 'vs/editor/common/model'; +import { LineTokens } from 'vs/editor/common/tokens/lineTokens'; +import { RenderLineInput, renderViewLine2 } from 'vs/editor/common/viewLayout/viewLineRenderer'; +import { ViewLineRenderingData } from 'vs/editor/common/viewModel'; +import { localize } from 'vs/nls'; +import { AudioCue, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; + +const diffReviewInsertIcon = registerIcon('diff-review-insert', Codicon.add, localize('diffReviewInsertIcon', 'Icon for \'Insert\' in diff review.')); +const diffReviewRemoveIcon = registerIcon('diff-review-remove', Codicon.remove, localize('diffReviewRemoveIcon', 'Icon for \'Remove\' in diff review.')); +const diffReviewCloseIcon = registerIcon('diff-review-close', Codicon.close, localize('diffReviewCloseIcon', 'Icon for \'Close\' in diff review.')); + +export class AccessibleDiffViewer extends Disposable { + constructor( + private readonly _parentNode: HTMLElement, + private readonly _visible: IObservable, + private readonly _setVisible: (visible: boolean, tx: ITransaction | undefined) => void, + private readonly _canClose: IObservable, + private readonly _width: IObservable, + private readonly _height: IObservable, + private readonly _diffs: IObservable, + private readonly _editors: DiffEditorEditors, + @IInstantiationService private readonly _instantiationService: IInstantiationService, + ) { + super(); + this._register(keepAlive(this.model, true)); + } + + private readonly model = derivedWithStore('model', (reader, store) => { + const visible = this._visible.read(reader); + this._parentNode.style.visibility = visible ? 'visible' : 'hidden'; + if (!visible) { + return null; + } + const model = store.add(this._instantiationService.createInstance(ViewModel, this._diffs, this._editors, this._setVisible, this._canClose)); + const view = store.add(this._instantiationService.createInstance(View, this._parentNode, model, this._width, this._height, this._editors)); + return { + model, + view + }; + }); + + next(): void { + transaction(tx => { + const isVisible = this._visible.get(); + this._setVisible(true, tx); + if (isVisible) { + this.model.get()!.model.nextGroup(tx); + } + }); + } + + prev(): void { + transaction(tx => { + this._setVisible(true, tx); + this.model.get()!.model.previousGroup(tx); + }); + } + + close(): void { + transaction(tx => { + this._setVisible(false, tx); + }); + } +} + +class ViewModel extends Disposable { + private readonly _groups = observableValue('groups', []); + private readonly _currentGroupIdx = observableValue('currentGroupIdx', 0); + private readonly _currentElementIdx = observableValue('currentElementIdx', 0); + + public readonly groups: IObservable = this._groups; + public readonly currentGroup: IObservable + = this._currentGroupIdx.map((idx, r) => this._groups.read(r)[idx]); + public readonly currentGroupIndex: IObservable = this._currentGroupIdx; + + public readonly currentElement: IObservable + = this._currentElementIdx.map((idx, r) => this.currentGroup.read(r)?.lines[idx]); + + constructor( + private readonly _diffs: IObservable, + private readonly _editors: DiffEditorEditors, + private readonly _setVisible: (visible: boolean, tx: ITransaction | undefined) => void, + public readonly canClose: IObservable, + @IAudioCueService private readonly _audioCueService: IAudioCueService, + ) { + super(); + + this._register(autorun('update groups', reader => { + const diffs = this._diffs.read(reader); + if (!diffs) { + this._groups.set([], undefined); + return; + } + + const groups = computeViewElementGroups( + diffs, + this._editors.original.getModel()!.getLineCount(), + this._editors.modified.getModel()!.getLineCount() + ); + + transaction(tx => { + const p = this._editors.modified.getPosition(); + if (p) { + const nextGroup = groups.findIndex(g => p?.lineNumber < g.range.modified.endLineNumberExclusive); + if (nextGroup !== -1) { + this._currentGroupIdx.set(nextGroup, tx); + } + } + this._groups.set(groups, tx); + }); + })); + + this._register(autorun('play audio-cue for diff', reader => { + const currentViewItem = this.currentElement.read(reader); + if (currentViewItem?.type === LineType.Deleted) { + this._audioCueService.playAudioCue(AudioCue.diffLineDeleted); + } else if (currentViewItem?.type === LineType.Added) { + this._audioCueService.playAudioCue(AudioCue.diffLineInserted); + } + })); + + this._register(autorun('select lines in editor', reader => { + // This ensures editor commands (like revert/stage) work + const currentViewItem = this.currentElement.read(reader); + if (currentViewItem && currentViewItem.type !== LineType.Header) { + const lineNumber = currentViewItem.modifiedLineNumber ?? currentViewItem.diff.modifiedRange.startLineNumber; + this._editors.modified.setSelection(Range.fromPositions(new Position(lineNumber, 1))); + } + })); + } + + private _goToGroupDelta(delta: number, tx?: ITransaction): void { + const groups = this.groups.get(); + if (!groups || groups.length <= 1) { return; } + subtransaction(tx, tx => { + this._currentGroupIdx.set((this._currentGroupIdx.get() + groups.length + delta) % groups.length, tx); + this._currentElementIdx.set(0, tx); + }); + } + + nextGroup(tx?: ITransaction): void { this._goToGroupDelta(1, tx); } + previousGroup(tx?: ITransaction): void { this._goToGroupDelta(-1, tx); } + + private _goToLineDelta(delta: number): void { + const group = this.currentGroup.get(); + if (!group || group.lines.length <= 1) { return; } + transaction(tx => { + this._currentElementIdx.set((this._currentElementIdx.get() + group.lines.length + delta) % group.lines.length, tx); + }); + } + + goToNextLine(): void { this._goToLineDelta(1); } + goToPreviousLine(): void { this._goToLineDelta(-1); } + + goToLine(line: ViewElement): void { + const group = this.currentGroup.get(); + if (!group) { return; } + const idx = group.lines.indexOf(line); + if (idx === -1) { return; } + transaction(tx => { + this._currentElementIdx.set(idx, tx); + }); + } + + revealCurrentElementInEditor(): void { + this._setVisible(false, undefined); + + const curElem = this.currentElement.get(); + if (curElem) { + if (curElem.type === LineType.Deleted) { + this._editors.original.setSelection(Range.fromPositions(new Position(curElem.originalLineNumber, 1))); + this._editors.original.revealLine(curElem.originalLineNumber); + this._editors.original.focus(); + } else { + if (curElem.type !== LineType.Header) { + this._editors.modified.setSelection(Range.fromPositions(new Position(curElem.modifiedLineNumber, 1))); + this._editors.modified.revealLine(curElem.modifiedLineNumber); + } + this._editors.modified.focus(); + } + } + } + + close(): void { + this._setVisible(false, undefined); + this._editors.modified.focus(); + } +} + + +const viewElementGroupLineMargin = 3; + +function computeViewElementGroups(diffs: LineRangeMapping[], originalLineCount: number, modifiedLineCount: number): ViewElementGroup[] { + const result: ViewElementGroup[] = []; + + for (const g of group(diffs, (a, b) => (b.modifiedRange.startLineNumber - a.modifiedRange.endLineNumberExclusive < 2 * viewElementGroupLineMargin))) { + const viewElements: ViewElement[] = []; + viewElements.push(new HeaderViewElement()); + + const origFullRange = new LineRange( + Math.max(1, g[0].originalRange.startLineNumber - viewElementGroupLineMargin), + Math.min(g[g.length - 1].originalRange.endLineNumberExclusive + viewElementGroupLineMargin, originalLineCount + 1) + ); + const modifiedFullRange = new LineRange( + Math.max(1, g[0].modifiedRange.startLineNumber - viewElementGroupLineMargin), + Math.min(g[g.length - 1].modifiedRange.endLineNumberExclusive + viewElementGroupLineMargin, modifiedLineCount + 1) + ); + + forEachAdjacentItems(g, (a, b) => { + const origRange = new LineRange(a ? a.originalRange.endLineNumberExclusive : origFullRange.startLineNumber, b ? b.originalRange.startLineNumber : origFullRange.endLineNumberExclusive); + const modifiedRange = new LineRange(a ? a.modifiedRange.endLineNumberExclusive : modifiedFullRange.startLineNumber, b ? b.modifiedRange.startLineNumber : modifiedFullRange.endLineNumberExclusive); + + origRange.forEach(origLineNumber => { + viewElements.push(new UnchangedLineViewElement(origLineNumber, modifiedRange.startLineNumber + (origLineNumber - origRange.startLineNumber))); + }); + + if (b) { + b.originalRange.forEach(origLineNumber => { + viewElements.push(new DeletedLineViewElement(b, origLineNumber)); + }); + b.modifiedRange.forEach(modifiedLineNumber => { + viewElements.push(new AddedLineViewElement(b, modifiedLineNumber)); + }); + } + }); + + const modifiedRange = g[0].modifiedRange.join(g[g.length - 1].modifiedRange); + const originalRange = g[0].originalRange.join(g[g.length - 1].originalRange); + + result.push(new ViewElementGroup(new SimpleLineRangeMapping(modifiedRange, originalRange), viewElements)); + } + return result; +} + +enum LineType { + Header, + Unchanged, + Deleted, + Added, +} + +class ViewElementGroup { + constructor( + public readonly range: SimpleLineRangeMapping, + public readonly lines: readonly ViewElement[], + ) { } +} + +type ViewElement = HeaderViewElement | UnchangedLineViewElement | DeletedLineViewElement | AddedLineViewElement; + +class HeaderViewElement { + public readonly type = LineType.Header; +} + +class DeletedLineViewElement { + public readonly type = LineType.Deleted; + + public readonly modifiedLineNumber = undefined; + + constructor( + public readonly diff: LineRangeMapping, + public readonly originalLineNumber: number, + ) { + } +} + +class AddedLineViewElement { + public readonly type = LineType.Added; + + public readonly originalLineNumber = undefined; + + constructor( + public readonly diff: LineRangeMapping, + public readonly modifiedLineNumber: number, + ) { + } +} + +class UnchangedLineViewElement { + public readonly type = LineType.Unchanged; + constructor( + public readonly originalLineNumber: number, + public readonly modifiedLineNumber: number, + ) { + } +} + +class View extends Disposable { + public readonly domNode: HTMLElement; + private readonly _content: HTMLElement; + private readonly _scrollbar: DomScrollableElement; + private readonly _actionBar: ActionBar; + + constructor( + private readonly _element: HTMLElement, + private readonly _model: ViewModel, + private readonly _width: IObservable, + private readonly _height: IObservable, + private readonly _editors: DiffEditorEditors, + @ILanguageService private readonly _languageService: ILanguageService, + ) { + super(); + + this.domNode = this._element; + this.domNode.className = 'diff-review monaco-editor-background'; + + const actionBarContainer = document.createElement('div'); + actionBarContainer.className = 'diff-review-actions'; + this._actionBar = this._register(new ActionBar( + actionBarContainer + )); + this._register(autorun('update actions', reader => { + this._actionBar.clear(); + if (this._model.canClose.read(reader)) { + this._actionBar.push(new Action( + 'diffreview.close', + localize('label.close', "Close"), + 'close-diff-review ' + ThemeIcon.asClassName(diffReviewCloseIcon), + true, + async () => _model.close() + ), { label: false, icon: true }); + } + })); + + this._content = document.createElement('div'); + this._content.className = 'diff-review-content'; + this._content.setAttribute('role', 'code'); + this._scrollbar = this._register(new DomScrollableElement(this._content, {})); + reset(this.domNode, this._scrollbar.getDomNode(), actionBarContainer); + + this._register(toDisposable(() => { reset(this.domNode); })); + + this._register(applyStyle(this.domNode, { width: this._width, height: this._height })); + this._register(applyStyle(this._content, { width: this._width, height: this._height })); + + this._register(autorunWithStore2('render', (reader, store) => { + this._model.currentGroup.read(reader); + this._render(store); + })); + + // TODO@hediet use commands + this._register(addStandardDisposableListener(this.domNode, 'keydown', (e) => { + if ( + e.equals(KeyCode.DownArrow) + || e.equals(KeyMod.CtrlCmd | KeyCode.DownArrow) + || e.equals(KeyMod.Alt | KeyCode.DownArrow) + ) { + e.preventDefault(); + this._model.goToNextLine(); + } + + if ( + e.equals(KeyCode.UpArrow) + || e.equals(KeyMod.CtrlCmd | KeyCode.UpArrow) + || e.equals(KeyMod.Alt | KeyCode.UpArrow) + ) { + e.preventDefault(); + this._model.goToPreviousLine(); + } + + if ( + e.equals(KeyCode.Escape) + || e.equals(KeyMod.CtrlCmd | KeyCode.Escape) + || e.equals(KeyMod.Alt | KeyCode.Escape) + || e.equals(KeyMod.Shift | KeyCode.Escape) + ) { + e.preventDefault(); + this._model.close(); + } + + if ( + e.equals(KeyCode.Space) + || e.equals(KeyCode.Enter) + ) { + e.preventDefault(); + this._model.revealCurrentElementInEditor(); + } + })); + } + + private _render(store: DisposableStore): void { + const originalOptions = this._editors.original.getOptions(); + const modifiedOptions = this._editors.modified.getOptions(); + + const container = document.createElement('div'); + container.className = 'diff-review-table'; + container.setAttribute('role', 'list'); + container.setAttribute('aria-label', localize('ariaLabel', 'Accessible Diff Viewer. Use arrow up and down to navigate.')); + applyFontInfo(container, modifiedOptions.get(EditorOption.fontInfo)); + + reset(this._content, container); + + const originalModel = this._editors.original.getModel(); + const modifiedModel = this._editors.modified.getModel(); + if (!originalModel || !modifiedModel) { + return; + } + + const originalModelOpts = originalModel.getOptions(); + const modifiedModelOpts = modifiedModel.getOptions(); + + const lineHeight = modifiedOptions.get(EditorOption.lineHeight); + const group = this._model.currentGroup.get(); + for (const viewItem of group?.lines || []) { + if (!group) { + break; + } + let row: HTMLDivElement; + + if (viewItem.type === LineType.Header) { + + const header = document.createElement('div'); + header.className = 'diff-review-row'; + header.setAttribute('role', 'listitem'); + + const r = group.range; + const diffIndex = this._model.currentGroupIndex.get(); + const diffsLength = this._model.groups.get().length; + const getAriaLines = (lines: number) => + lines === 0 ? localize('no_lines_changed', "no lines changed") + : lines === 1 ? localize('one_line_changed', "1 line changed") + : localize('more_lines_changed', "{0} lines changed", lines); + + const originalChangedLinesCntAria = getAriaLines(r.original.length); + const modifiedChangedLinesCntAria = getAriaLines(r.modified.length); + header.setAttribute('aria-label', localize({ + key: 'header', + comment: [ + 'This is the ARIA label for a git diff header.', + 'A git diff header looks like this: @@ -154,12 +159,39 @@.', + 'That encodes that at original line 154 (which is now line 159), 12 lines were removed/changed with 39 lines.', + 'Variables 0 and 1 refer to the diff index out of total number of diffs.', + 'Variables 2 and 4 will be numbers (a line number).', + 'Variables 3 and 5 will be "no lines changed", "1 line changed" or "X lines changed", localized separately.' + ] + }, "Difference {0} of {1}: original line {2}, {3}, modified line {4}, {5}", + (diffIndex + 1), + diffsLength, + r.original.startLineNumber, + originalChangedLinesCntAria, + r.modified.startLineNumber, + modifiedChangedLinesCntAria + )); + + const cell = document.createElement('div'); + cell.className = 'diff-review-cell diff-review-summary'; + // e.g.: `1/10: @@ -504,7 +517,7 @@` + cell.appendChild(document.createTextNode(`${diffIndex + 1}/${diffsLength}: @@ -${r.original.startLineNumber},${r.original.length} +${r.modified.startLineNumber},${r.modified.length} @@`)); + header.appendChild(cell); + + row = header; + } else { + row = this._createRow(viewItem, lineHeight, + this._width.get(), originalOptions, originalModel, originalModelOpts, modifiedOptions, modifiedModel, modifiedModelOpts, + ); + } + + container.appendChild(row); + + const isSelectedObs = derived('isSelected', reader => this._model.currentElement.read(reader) === viewItem); + + store.add(autorun('update tab index', reader => { + const isSelected = isSelectedObs.read(reader); + row.tabIndex = isSelected ? 0 : -1; + if (isSelected) { + row.focus(); + } + })); + + store.add(addDisposableListener(row, 'focus', () => { + this._model.goToLine(viewItem); + })); + } + + this._scrollbar.scanDomNode(); + } + + private _createRow( + item: DeletedLineViewElement | AddedLineViewElement | UnchangedLineViewElement, + lineHeight: number, + width: number, + originalOptions: IComputedEditorOptions, originalModel: ITextModel, originalModelOpts: TextModelResolvedOptions, + modifiedOptions: IComputedEditorOptions, modifiedModel: ITextModel, modifiedModelOpts: TextModelResolvedOptions, + ): HTMLDivElement { + const originalLayoutInfo = originalOptions.get(EditorOption.layoutInfo); + const originalLineNumbersWidth = originalLayoutInfo.glyphMarginWidth + originalLayoutInfo.lineNumbersWidth; + + const modifiedLayoutInfo = modifiedOptions.get(EditorOption.layoutInfo); + const modifiedLineNumbersWidth = 10 + modifiedLayoutInfo.glyphMarginWidth + modifiedLayoutInfo.lineNumbersWidth; + + let rowClassName: string = 'diff-review-row'; + let lineNumbersExtraClassName: string = ''; + const spacerClassName: string = 'diff-review-spacer'; + let spacerIcon: ThemeIcon | null = null; + switch (item.type) { + case LineType.Added: + rowClassName = 'diff-review-row line-insert'; + lineNumbersExtraClassName = ' char-insert'; + spacerIcon = diffReviewInsertIcon; + break; + case LineType.Deleted: + rowClassName = 'diff-review-row line-delete'; + lineNumbersExtraClassName = ' char-delete'; + spacerIcon = diffReviewRemoveIcon; + break; + } + + const row = document.createElement('div'); + row.style.minWidth = width + 'px'; + row.className = rowClassName; + row.setAttribute('role', 'listitem'); + row.ariaLevel = ''; + + const cell = document.createElement('div'); + cell.className = 'diff-review-cell'; + cell.style.height = `${lineHeight}px`; + row.appendChild(cell); + + const originalLineNumber = document.createElement('span'); + originalLineNumber.style.width = (originalLineNumbersWidth + 'px'); + originalLineNumber.style.minWidth = (originalLineNumbersWidth + 'px'); + originalLineNumber.className = 'diff-review-line-number' + lineNumbersExtraClassName; + if (item.originalLineNumber !== undefined) { + originalLineNumber.appendChild(document.createTextNode(String(item.originalLineNumber))); + } else { + originalLineNumber.innerText = '\u00a0'; + } + cell.appendChild(originalLineNumber); + + const modifiedLineNumber = document.createElement('span'); + modifiedLineNumber.style.width = (modifiedLineNumbersWidth + 'px'); + modifiedLineNumber.style.minWidth = (modifiedLineNumbersWidth + 'px'); + modifiedLineNumber.style.paddingRight = '10px'; + modifiedLineNumber.className = 'diff-review-line-number' + lineNumbersExtraClassName; + if (item.modifiedLineNumber !== undefined) { + modifiedLineNumber.appendChild(document.createTextNode(String(item.modifiedLineNumber))); + } else { + modifiedLineNumber.innerText = '\u00a0'; + } + cell.appendChild(modifiedLineNumber); + + const spacer = document.createElement('span'); + spacer.className = spacerClassName; + + if (spacerIcon) { + const spacerCodicon = document.createElement('span'); + spacerCodicon.className = ThemeIcon.asClassName(spacerIcon); + spacerCodicon.innerText = '\u00a0\u00a0'; + spacer.appendChild(spacerCodicon); + } else { + spacer.innerText = '\u00a0\u00a0'; + } + cell.appendChild(spacer); + + let lineContent: string; + if (item.modifiedLineNumber !== undefined) { + let html: string | TrustedHTML = this._getLineHtml(modifiedModel, modifiedOptions, modifiedModelOpts.tabSize, item.modifiedLineNumber, this._languageService.languageIdCodec); + if (DiffReview._ttPolicy) { + html = DiffReview._ttPolicy.createHTML(html as string); + } + cell.insertAdjacentHTML('beforeend', html as string); + lineContent = modifiedModel.getLineContent(item.modifiedLineNumber); + } else { + let html: string | TrustedHTML = this._getLineHtml(originalModel, originalOptions, originalModelOpts.tabSize, item.originalLineNumber, this._languageService.languageIdCodec); + if (DiffReview._ttPolicy) { + html = DiffReview._ttPolicy.createHTML(html as string); + } + cell.insertAdjacentHTML('beforeend', html as string); + lineContent = originalModel.getLineContent(item.originalLineNumber); + } + + if (lineContent.length === 0) { + lineContent = localize('blankLine', "blank"); + } + + let ariaLabel: string = ''; + switch (item.type) { + case LineType.Unchanged: + if (item.originalLineNumber === item.modifiedLineNumber) { + ariaLabel = localize({ key: 'unchangedLine', comment: ['The placeholders are contents of the line and should not be translated.'] }, "{0} unchanged line {1}", lineContent, item.originalLineNumber); + } else { + ariaLabel = localize('equalLine', "{0} original line {1} modified line {2}", lineContent, item.originalLineNumber, item.modifiedLineNumber); + } + break; + case LineType.Added: + ariaLabel = localize('insertLine', "+ {0} modified line {1}", lineContent, item.modifiedLineNumber); + break; + case LineType.Deleted: + ariaLabel = localize('deleteLine', "- {0} original line {1}", lineContent, item.originalLineNumber); + break; + } + row.setAttribute('aria-label', ariaLabel); + + return row; + } + + private _getLineHtml(model: ITextModel, options: IComputedEditorOptions, tabSize: number, lineNumber: number, languageIdCodec: ILanguageIdCodec): string { + const lineContent = model.getLineContent(lineNumber); + const fontInfo = options.get(EditorOption.fontInfo); + const lineTokens = LineTokens.createEmpty(lineContent, languageIdCodec); + const isBasicASCII = ViewLineRenderingData.isBasicASCII(lineContent, model.mightContainNonBasicASCII()); + const containsRTL = ViewLineRenderingData.containsRTL(lineContent, isBasicASCII, model.mightContainRTL()); + const r = renderViewLine2(new RenderLineInput( + (fontInfo.isMonospace && !options.get(EditorOption.disableMonospaceOptimizations)), + fontInfo.canUseHalfwidthRightwardsArrow, + lineContent, + false, + isBasicASCII, + containsRTL, + 0, + lineTokens, + [], + tabSize, + 0, + fontInfo.spaceWidth, + fontInfo.middotWidth, + fontInfo.wsmiddotWidth, + options.get(EditorOption.stopRenderingLineAfter), + options.get(EditorOption.renderWhitespace), + options.get(EditorOption.renderControlCharacters), + options.get(EditorOption.fontLigatures) !== EditorFontLigatures.OFF, + null + )); + + return r.html; + } +} + +function forEachAdjacentItems(items: T[], callback: (item1: T | undefined, item2: T | undefined) => void) { + let last: T | undefined; + for (const item of items) { + callback(last, item); + last = item; + } + callback(last, undefined); +} + +function* group(items: Iterable, shouldBeGrouped: (item1: T, item2: T) => boolean): Iterable { + let currentGroup: T[] | undefined; + let last: T | undefined; + for (const item of items) { + if (last !== undefined && shouldBeGrouped(last, item)) { + currentGroup!.push(item); + } else { + if (currentGroup) { + yield currentGroup; + } + currentGroup = [item]; + } + last = item; + } + if (currentGroup) { + yield currentGroup; + } +} diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorDecorations.ts b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorDecorations.ts index 8a821c0b135..7e062b0ab09 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorDecorations.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorDecorations.ts @@ -42,13 +42,13 @@ export class DiffEditorDecorations extends Disposable { const originalDecorations: IModelDeltaDecoration[] = []; const modifiedDecorations: IModelDeltaDecoration[] = []; for (const m of diff.mappings) { - const fullRangeOriginal = LineRange.subtract(m.lineRangeMapping.originalRange, currentMove?.lineRangeMapping.originalRange) + const fullRangeOriginal = LineRange.subtract(m.lineRangeMapping.originalRange, currentMove?.lineRangeMapping.original) .map(i => i.toInclusiveRange()).filter(isDefined); for (const range of fullRangeOriginal) { originalDecorations.push({ range, options: renderIndicators ? diffLineDeleteDecorationBackgroundWithIndicator : diffLineDeleteDecorationBackground }); } - const fullRangeModified = LineRange.subtract(m.lineRangeMapping.modifiedRange, currentMove?.lineRangeMapping.modifiedRange) + const fullRangeModified = LineRange.subtract(m.lineRangeMapping.modifiedRange, currentMove?.lineRangeMapping.modified) .map(i => i.toInclusiveRange()).filter(isDefined); for (const range of fullRangeModified) { modifiedDecorations.push({ range, options: renderIndicators ? diffLineAddDecorationBackgroundWithIndicator : diffLineAddDecorationBackground }); @@ -64,8 +64,8 @@ export class DiffEditorDecorations extends Disposable { } else { for (const i of m.lineRangeMapping.innerChanges || []) { if (currentMove - && (currentMove.lineRangeMapping.originalRange.intersect(new LineRange(i.originalRange.startLineNumber, i.originalRange.endLineNumber)) - || currentMove.lineRangeMapping.modifiedRange.intersect(new LineRange(i.modifiedRange.startLineNumber, i.modifiedRange.endLineNumber)))) { + && (currentMove.lineRangeMapping.original.intersect(new LineRange(i.originalRange.startLineNumber, i.originalRange.endLineNumber)) + || currentMove.lineRangeMapping.modified.intersect(new LineRange(i.modifiedRange.startLineNumber, i.modifiedRange.endLineNumber)))) { continue; } @@ -104,7 +104,7 @@ export class DiffEditorDecorations extends Disposable { for (const m of diff.movedTexts) { originalDecorations.push({ - range: m.lineRangeMapping.originalRange.toInclusiveRange()!, options: { + range: m.lineRangeMapping.original.toInclusiveRange()!, options: { description: 'moved', blockClassName: 'movedOriginal', blockPadding: [MovedBlocksLinesPart.movedCodeBlockPadding, 0, MovedBlocksLinesPart.movedCodeBlockPadding, MovedBlocksLinesPart.movedCodeBlockPadding], @@ -112,7 +112,7 @@ export class DiffEditorDecorations extends Disposable { }); modifiedDecorations.push({ - range: m.lineRangeMapping.modifiedRange.toInclusiveRange()!, options: { + range: m.lineRangeMapping.modified.toInclusiveRange()!, options: { description: 'moved', blockClassName: 'movedModified', blockPadding: [4, 0, 4, 4], diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorEditors.ts b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorEditors.ts index a77d3c1727b..34ee3e06cdf 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorEditors.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorEditors.ts @@ -15,7 +15,7 @@ import { IContentSizeChangedEvent } from 'vs/editor/common/editorCommon'; import { localize } from 'vs/nls'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { DiffEditorOptions } from './diffEditorOptions'; -import { IObservable, IReader } from 'vs/base/common/observable'; +import { IReader } from 'vs/base/common/observable'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; export class DiffEditorEditors extends Disposable { @@ -31,7 +31,6 @@ export class DiffEditorEditors extends Disposable { private readonly _options: DiffEditorOptions, codeEditorWidgetOptions: IDiffCodeEditorWidgetOptions, private readonly _createInnerEditor: (instantiationService: IInstantiationService, container: HTMLElement, options: Readonly, editorWidgetOptions: ICodeEditorWidgetOptions) => CodeEditorWidget, - private readonly _modifiedReadOnlyOverride: IObservable, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IKeybindingService private readonly _keybindingService: IKeybindingService, ) { @@ -117,7 +116,6 @@ export class DiffEditorEditors extends Disposable { result.revealHorizontalRightPadding = EditorOptions.revealHorizontalRightPadding.defaultValue + OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH; result.scrollbar!.verticalHasArrows = false; result.extraEditorClassName = 'modified-in-monaco-diff-editor'; - result.readOnly = this._modifiedReadOnlyOverride.read(reader) || this._options.editorOptions.get().readOnly; return result; } @@ -161,6 +159,6 @@ export class DiffEditorEditors extends Disposable { } else if (ariaLabel) { return ariaLabel.replaceAll(ariaNavigationTip, ''); } - return undefined; + return ''; } } diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorOptions.ts b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorOptions.ts index fcec4e86f09..eed16b99472 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorOptions.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorOptions.ts @@ -46,6 +46,7 @@ export class DiffEditorOptions { public readonly accessibilityVerbose = derived('accessibilityVerbose', reader => this._options.read(reader).accessibilityVerbose); public readonly diffAlgorithm = derived('diffAlgorithm', reader => this._options.read(reader).diffAlgorithm); public readonly showEmptyDecorations = derived('showEmptyDecorations', reader => this._options.read(reader).experimental.showEmptyDecorations!); + public readonly onlyShowAccessibleDiffViewer = derived('onlyShowAccessibleDiffViewer', reader => this._options.read(reader).onlyShowAccessibleDiffViewer); public updateOptions(changedOptions: IDiffEditorOptions): void { const newDiffEditorOptions = validateDiffEditorOptions(changedOptions, this._options.get()); @@ -75,6 +76,7 @@ const diffEditorDefaultOptions: ValidDiffEditorBaseOptions = { showEmptyDecorations: true, }, isInEmbeddedEditor: false, + onlyShowAccessibleDiffViewer: false, }; function validateDiffEditorOptions(options: Readonly, defaults: ValidDiffEditorBaseOptions): ValidDiffEditorBaseOptions { @@ -99,5 +101,6 @@ function validateDiffEditorOptions(options: Readonly, defaul showEmptyDecorations: validateBooleanOption(options.experimental?.showEmptyDecorations, defaults.experimental.showEmptyDecorations!), }, isInEmbeddedEditor: validateBooleanOption(options.isInEmbeddedEditor, defaults.isInEmbeddedEditor), + onlyShowAccessibleDiffViewer: validateBooleanOption(options.onlyShowAccessibleDiffViewer, defaults.onlyShowAccessibleDiffViewer), }; } diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorViewModel.ts b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorViewModel.ts index f9d77296cea..6c9a8899466 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorViewModel.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorViewModel.ts @@ -117,7 +117,7 @@ export class DiffEditorViewModel extends Disposable implements IDiffEditorViewMo this._diff.set(DiffState.fromDiffResult(this._lastDiff!), tx); updateUnchangedRegions(result, tx); const currentSyncedMovedText = this.syncedMovedTexts.get(); - this.syncedMovedTexts.set(currentSyncedMovedText ? this._lastDiff!.moves.find(m => m.lineRangeMapping.modifiedRange.intersect(currentSyncedMovedText.lineRangeMapping.modifiedRange)) : undefined, tx); + this.syncedMovedTexts.set(currentSyncedMovedText ? this._lastDiff!.moves.find(m => m.lineRangeMapping.modified.intersect(currentSyncedMovedText.lineRangeMapping.modified)) : undefined, tx); }); } @@ -137,7 +137,7 @@ export class DiffEditorViewModel extends Disposable implements IDiffEditorViewMo this._diff.set(DiffState.fromDiffResult(this._lastDiff!), tx); updateUnchangedRegions(result, tx); const currentSyncedMovedText = this.syncedMovedTexts.get(); - this.syncedMovedTexts.set(currentSyncedMovedText ? this._lastDiff!.moves.find(m => m.lineRangeMapping.modifiedRange.intersect(currentSyncedMovedText.lineRangeMapping.modifiedRange)) : undefined, tx); + this.syncedMovedTexts.set(currentSyncedMovedText ? this._lastDiff!.moves.find(m => m.lineRangeMapping.modified.intersect(currentSyncedMovedText.lineRangeMapping.modified)) : undefined, tx); }); } @@ -183,7 +183,7 @@ export class DiffEditorViewModel extends Disposable implements IDiffEditorViewMo this._diff.set(state, tx); this._isDiffUpToDate.set(true, tx); const currentSyncedMovedText = this.syncedMovedTexts.get(); - this.syncedMovedTexts.set(currentSyncedMovedText ? this._lastDiff.moves.find(m => m.lineRangeMapping.modifiedRange.intersect(currentSyncedMovedText.lineRangeMapping.modifiedRange)) : undefined, tx); + this.syncedMovedTexts.set(currentSyncedMovedText ? this._lastDiff.moves.find(m => m.lineRangeMapping.modified.intersect(currentSyncedMovedText.lineRangeMapping.modified)) : undefined, tx); }); })); } @@ -443,9 +443,9 @@ function applyModifiedEdits(diff: IDocumentDiff, textEdits: TextEditInfo[], orig const changes = applyModifiedEditsToLineRangeMappings(diff.changes, textEdits, originalTextModel, modifiedTextModel); const moves = diff.moves.map(m => { - const newModifiedRange = applyEditToLineRange(m.lineRangeMapping.modifiedRange, textEdits); + const newModifiedRange = applyEditToLineRange(m.lineRangeMapping.modified, textEdits); return newModifiedRange ? new MovedText( - new SimpleLineRangeMapping(m.lineRangeMapping.originalRange, newModifiedRange), + new SimpleLineRangeMapping(m.lineRangeMapping.original, newModifiedRange), applyModifiedEditsToLineRangeMappings(m.changes, textEdits, originalTextModel, modifiedTextModel), ) : undefined; }).filter(isDefined); diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.ts b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.ts index 1dab8ec750e..339dc36a263 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.ts @@ -20,12 +20,11 @@ import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/wi import { IDiffCodeEditorWidgetOptions } from 'vs/editor/browser/widget/diffEditorWidget'; import { DiffEditorDecorations } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorDecorations'; import { DiffEditorSash } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorSash'; -import { DiffReview2 } from 'vs/editor/browser/widget/diffEditorWidget2/diffReview'; import { ViewZoneManager } from 'vs/editor/browser/widget/diffEditorWidget2/lineAlignment'; import { MovedBlocksLinesPart } from 'vs/editor/browser/widget/diffEditorWidget2/movedBlocksLines'; import { OverviewRulerPart } from 'vs/editor/browser/widget/diffEditorWidget2/overviewRulerPart'; import { UnchangedRangesFeature } from 'vs/editor/browser/widget/diffEditorWidget2/unchangedRanges'; -import { ObservableElementSizeObserver, applyStyle, readHotReloadableExport } from 'vs/editor/browser/widget/diffEditorWidget2/utils'; +import { CSSStyle, ObservableElementSizeObserver, applyStyle, readHotReloadableExport } from 'vs/editor/browser/widget/diffEditorWidget2/utils'; import { WorkerBasedDocumentDiffProvider } from 'vs/editor/browser/widget/workerBasedDocumentDiffProvider'; import { IDiffEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IDimension } from 'vs/editor/common/core/dimension'; @@ -42,12 +41,14 @@ import { DelegatingEditor } from './delegatingEditorImpl'; import { DiffEditorEditors } from './diffEditorEditors'; import { DiffEditorOptions } from './diffEditorOptions'; import { DiffEditorViewModel, DiffMapping, DiffState } from './diffEditorViewModel'; +import { AccessibleDiffViewer } from 'vs/editor/browser/widget/diffEditorWidget2/accessibleDiffViewer'; export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { private readonly elements = h('div.monaco-diff-editor.side-by-side', { style: { position: 'relative', height: '100%' } }, [ h('div.noModificationsOverlay@overlay', { style: { position: 'absolute', height: '100%', visibility: 'hidden', } }, [$('span', {}, 'No Changes')]), h('div.editor.original@original', { style: { position: 'absolute', height: '100%' } }), h('div.editor.modified@modified', { style: { position: 'absolute', height: '100%' } }), + h('div.accessibleDiffViewer@accessibleDiffViewer', { style: { position: 'absolute', height: '100%' } }), ]); private readonly _diffModel = this._register(disposableObservableValue('diffModel', undefined)); public readonly onDidChangeModel = Event.fromObservableLight(this._diffModel); @@ -65,7 +66,13 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { private unchangedRangesFeature!: UnchangedRangesFeature; - private readonly _reviewPane: DiffReview2; + private _accessibleDiffViewerShouldBeVisible = observableValue('accessibleDiffViewerShouldBeVisible', false); + private _accessibleDiffViewerVisible = derived('accessibleDiffViewerVisible', reader => + this._options.onlyShowAccessibleDiffViewer.read(reader) + ? true + : this._accessibleDiffViewerShouldBeVisible.read(reader) + ); + private _accessibleDiffViewer!: AccessibleDiffViewer; private readonly _options: DiffEditorOptions; private readonly _editors: DiffEditorEditors; @@ -96,15 +103,13 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { this._rootSizeObserver = this._register(new ObservableElementSizeObserver(this.elements.root, options.dimension)); this._rootSizeObserver.setAutomaticLayout(options.automaticLayout ?? false); - const reviewPaneObservable = observableValue('reviewPane', undefined); this._editors = this._register(this._instantiationService.createInstance( DiffEditorEditors, this.elements.original, this.elements.modified, this._options, codeEditorWidgetOptions, - (i, c, o, o2) => this._createInnerEditor(i, c, o, o2), - reviewPaneObservable.map((r, reader) => r?.isVisible.read(reader) ?? false), + (i, c, o, o2) => this._createInnerEditor(i, c, o, o2) )); this._sash = derivedWithStore('sash', (reader, store) => { @@ -158,10 +163,22 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { )); })); - this._reviewPane = this._register(this._instantiationService.createInstance(DiffReview2, this)); - this.elements.root.appendChild(this._reviewPane.domNode.domNode); - this.elements.root.appendChild(this._reviewPane.actionBarContainer.domNode); - reviewPaneObservable.set(this._reviewPane, undefined); + this._register(autorunWithStore2('_accessibleDiffViewer', (reader, store) => { + this._accessibleDiffViewer = store.add(this._register(this._instantiationService.createInstance( + readHotReloadableExport(AccessibleDiffViewer, reader), + this.elements.accessibleDiffViewer, + this._accessibleDiffViewerVisible, + (visible, tx) => this._accessibleDiffViewerShouldBeVisible.set(visible, tx), + this._options.onlyShowAccessibleDiffViewer.map(v => !v), + this._rootSizeObserver.width, + this._rootSizeObserver.height, + this._diffModel.map((m, r) => m?.diff.read(r)?.mappings.map(m => m.lineRangeMapping)), + this._editors, + ))); + })); + const visibility = this._accessibleDiffViewerVisible.map(v => v ? 'hidden' : 'visible'); + this._register(applyStyle(this.elements.modified, { visibility })); + this._register(applyStyle(this.elements.original, { visibility })); this._createDiffEditorContributions(); @@ -188,13 +205,13 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { this._register(this._editors.original.onDidChangeCursorPosition(e => { const m = this._diffModel.get(); if (!m) { return; } - const movedText = m.diff.get()!.movedTexts.find(m => m.lineRangeMapping.originalRange.contains(e.position.lineNumber)); + const movedText = m.diff.get()!.movedTexts.find(m => m.lineRangeMapping.original.contains(e.position.lineNumber)); m.syncedMovedTexts.set(movedText, undefined); })); this._register(this._editors.modified.onDidChangeCursorPosition(e => { const m = this._diffModel.get(); if (!m) { return; } - const movedText = m.diff.get()!.movedTexts.find(m => m.lineRangeMapping.modifiedRange.contains(e.position.lineNumber)); + const movedText = m.diff.get()!.movedTexts.find(m => m.lineRangeMapping.modified.contains(e.position.lineNumber)); m.syncedMovedTexts.set(movedText, undefined); })); @@ -220,6 +237,10 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { })); } + public getContentHeight() { + return this._editors.modified.getContentHeight(); + } + protected _createInnerEditor(instantiationService: IInstantiationService, container: HTMLElement, options: Readonly, editorWidgetOptions: ICodeEditorWidgetOptions): CodeEditorWidget { const editor = instantiationService.createInstance(CodeEditorWidget, container, options, editorWidgetOptions); return editor; @@ -231,20 +252,16 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { const sashLeft = this._sash.read(reader)?.sashLeft.read(reader); const originalWidth = sashLeft ?? Math.max(5, this._editors.original.getLayoutInfo().decorationsLeft); + const modifiedWidth = width - originalWidth - (this._options.renderOverviewRuler.read(reader) ? OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH : 0); this.elements.original.style.width = originalWidth + 'px'; this.elements.original.style.left = '0px'; - this.elements.modified.style.width = (width - originalWidth) + 'px'; + this.elements.modified.style.width = modifiedWidth + 'px'; this.elements.modified.style.left = originalWidth + 'px'; - this._editors.original.layout({ width: originalWidth, height: height }); - this._editors.modified.layout({ - width: width - originalWidth - - (this._options.renderOverviewRuler.read(reader) ? OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH : 0), - height - }); - this._reviewPane.layout(0, width, height); + this._editors.original.layout({ width: originalWidth, height }); + this._editors.modified.layout({ width: modifiedWidth, height }); return { modifiedEditor: this._editors.modified.getLayoutInfo(), @@ -317,7 +334,7 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { override setModel(model: IDiffEditorModel | null | IDiffEditorViewModel): void { if (!model && this._diffModel.get()) { // Transitioning from a model to no-model - this._reviewPane.hide(); + this._accessibleDiffViewer.close(); } const vm = model ? ('model' in model) ? model : this.createViewModel(model) : undefined; @@ -429,9 +446,9 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { }); } - diffReviewNext(): void { this._reviewPane.next(); } + diffReviewNext(): void { this._accessibleDiffViewer.next(); } - diffReviewPrev(): void { this._reviewPane.prev(); } + diffReviewPrev(): void { this._accessibleDiffViewer.prev(); } async waitForDiff(): Promise { const diffModel = this._diffModel.get(); diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/diffReview.ts b/src/vs/editor/browser/widget/diffEditorWidget2/diffReview.ts deleted file mode 100644 index 82359ffaef0..00000000000 --- a/src/vs/editor/browser/widget/diffEditorWidget2/diffReview.ts +++ /dev/null @@ -1,821 +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 { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; -import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; -import { Action } from 'vs/base/common/actions'; -import { Codicon } from 'vs/base/common/codicons'; -import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { IObservable, observableValue } from 'vs/base/common/observable'; -import { ThemeIcon } from 'vs/base/common/themables'; -import { Constants } from 'vs/base/common/uint'; -import { applyFontInfo } from 'vs/editor/browser/config/domFontInfo'; -import { DiffEditorWidget2 } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2'; -import { DiffReview } from 'vs/editor/browser/widget/diffReview'; -import { EditorFontLigatures, EditorOption, IComputedEditorOptions } from 'vs/editor/common/config/editorOptions'; -import { Position } from 'vs/editor/common/core/position'; -import { ILineChange } from 'vs/editor/common/diff/smartLinesDiffComputer'; -import { ScrollType } from 'vs/editor/common/editorCommon'; -import { ILanguageIdCodec } from 'vs/editor/common/languages'; -import { ILanguageService } from 'vs/editor/common/languages/language'; -import { ITextModel, TextModelResolvedOptions } from 'vs/editor/common/model'; -import { LineTokens } from 'vs/editor/common/tokens/lineTokens'; -import { RenderLineInput, renderViewLine2 as renderViewLine } from 'vs/editor/common/viewLayout/viewLineRenderer'; -import { ViewLineRenderingData } from 'vs/editor/common/viewModel'; -import * as nls from 'vs/nls'; -import { AudioCue, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; - -const DIFF_LINES_PADDING = 3; - -const enum DiffEntryType { - Equal = 0, - Insert = 1, - Delete = 2 -} - -class DiffEntry { - readonly originalLineStart: number; - readonly originalLineEnd: number; - readonly modifiedLineStart: number; - readonly modifiedLineEnd: number; - - constructor(originalLineStart: number, originalLineEnd: number, modifiedLineStart: number, modifiedLineEnd: number) { - this.originalLineStart = originalLineStart; - this.originalLineEnd = originalLineEnd; - this.modifiedLineStart = modifiedLineStart; - this.modifiedLineEnd = modifiedLineEnd; - } - - public getType(): DiffEntryType { - if (this.originalLineStart === 0) { - return DiffEntryType.Insert; - } - if (this.modifiedLineStart === 0) { - return DiffEntryType.Delete; - } - return DiffEntryType.Equal; - } -} - -const enum DiffEditorLineClasses { - Insert = 'line-insert', - Delete = 'line-delete' -} - -class Diff { - readonly entries: DiffEntry[]; - - constructor(entries: DiffEntry[]) { - this.entries = entries; - } -} - -const diffReviewInsertIcon = registerIcon('diff-review-insert', Codicon.add, nls.localize('diffReviewInsertIcon', 'Icon for \'Insert\' in diff review.')); -const diffReviewRemoveIcon = registerIcon('diff-review-remove', Codicon.remove, nls.localize('diffReviewRemoveIcon', 'Icon for \'Remove\' in diff review.')); -const diffReviewCloseIcon = registerIcon('diff-review-close', Codicon.close, nls.localize('diffReviewCloseIcon', 'Icon for \'Close\' in diff review.')); - -export class DiffReview2 extends Disposable { - - private static _ttPolicy = DiffReview._ttPolicy; // TODO inline once DiffReview is deprecated. - - private readonly _diffEditor: DiffEditorWidget2; - private get _isVisible() { return this._isVisibleObs.get(); } - private readonly _actionBar: ActionBar; - public readonly actionBarContainer: FastDomNode; - public readonly domNode: FastDomNode; - private readonly _content: FastDomNode; - private readonly scrollbar: DomScrollableElement; - private _diffs: Diff[]; - private _currentDiff: Diff | null; - - private readonly _isVisibleObs = observableValue('isVisible', false); - - public readonly isVisible: IObservable = this._isVisibleObs; - - constructor( - diffEditor: DiffEditorWidget2, - @ILanguageService private readonly _languageService: ILanguageService, - @IAudioCueService private readonly _audioCueService: IAudioCueService, - @IConfigurationService private readonly _configurationService: IConfigurationService - ) { - super(); - this._diffEditor = diffEditor; - - this.actionBarContainer = createFastDomNode(document.createElement('div')); - this.actionBarContainer.setClassName('diff-review-actions'); - this._actionBar = this._register(new ActionBar( - this.actionBarContainer.domNode - )); - - this._actionBar.push(new Action('diffreview.close', nls.localize('label.close', "Close"), 'close-diff-review ' + ThemeIcon.asClassName(diffReviewCloseIcon), true, async () => this.hide()), { label: false, icon: true }); - - this.domNode = createFastDomNode(document.createElement('div')); - this.domNode.setClassName('diff-review monaco-editor-background'); - - this._content = createFastDomNode(document.createElement('div')); - this._content.setClassName('diff-review-content'); - this._content.setAttribute('role', 'code'); - this.scrollbar = this._register(new DomScrollableElement(this._content.domNode, {})); - this.domNode.domNode.appendChild(this.scrollbar.getDomNode()); - - this._register(diffEditor.onDidUpdateDiff(() => { - if (!this._isVisible) { - return; - } - this._diffs = this._compute(); - this._render(); - })); - this._register(diffEditor.getModifiedEditor().onDidChangeCursorPosition(() => { - if (!this._isVisible) { - return; - } - this._render(); - })); - this._register(dom.addStandardDisposableListener(this.domNode.domNode, 'click', (e) => { - e.preventDefault(); - - const row = dom.findParentWithClass(e.target, 'diff-review-row'); - if (row) { - this._goToRow(row); - } - })); - this._register(dom.addStandardDisposableListener(this.domNode.domNode, 'keydown', (e) => { - if ( - e.equals(KeyCode.DownArrow) - || e.equals(KeyMod.CtrlCmd | KeyCode.DownArrow) - || e.equals(KeyMod.Alt | KeyCode.DownArrow) - ) { - e.preventDefault(); - this._goToRow(this._getNextRow(), 'next'); - } - - if ( - e.equals(KeyCode.UpArrow) - || e.equals(KeyMod.CtrlCmd | KeyCode.UpArrow) - || e.equals(KeyMod.Alt | KeyCode.UpArrow) - ) { - e.preventDefault(); - this._goToRow(this._getPrevRow(), 'previous'); - } - - if ( - e.equals(KeyCode.Escape) - || e.equals(KeyMod.CtrlCmd | KeyCode.Escape) - || e.equals(KeyMod.Alt | KeyCode.Escape) - || e.equals(KeyMod.Shift | KeyCode.Escape) - || e.equals(KeyCode.Space) - || e.equals(KeyCode.Enter) - ) { - e.preventDefault(); - this.accept(); - } - })); - this._register(this._configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('accessibility.verbosity.diffEditor')) { - this._diffEditor.updateOptions({ accessibilityVerbose: this._configurationService.getValue('accessibility.verbosity.diffEditor') }); - } - })); - this._diffs = []; - this._currentDiff = null; - } - - public prev(): void { - let index = 0; - - if (!this._isVisible) { - this._diffs = this._compute(); - } - - if (this._isVisible) { - let currentIndex = -1; - for (let i = 0, len = this._diffs.length; i < len; i++) { - if (this._diffs[i] === this._currentDiff) { - currentIndex = i; - break; - } - } - index = (this._diffs.length + currentIndex - 1); - } else { - index = this._findDiffIndex(this._diffEditor.getPosition()!); - } - - if (this._diffs.length === 0) { - // Nothing to do - return; - } - - index = index % this._diffs.length; - const entries = this._diffs[index].entries; - this._diffEditor.setPosition(new Position(entries[0].modifiedLineStart, 1)); - this._diffEditor.setSelection({ startColumn: 1, startLineNumber: entries[0].modifiedLineStart, endColumn: Constants.MAX_SAFE_SMALL_INTEGER, endLineNumber: entries[entries.length - 1].modifiedLineEnd }); - this._isVisibleObs.set(true, undefined); - this.layout(); - this._render(); - this._goToRow(this._getPrevRow(), 'previous'); - } - - public next(): void { - let index = 0; - - if (!this._isVisible) { - this._diffs = this._compute(); - } - - if (this._isVisible) { - let currentIndex = -1; - for (let i = 0, len = this._diffs.length; i < len; i++) { - if (this._diffs[i] === this._currentDiff) { - currentIndex = i; - break; - } - } - index = (currentIndex + 1); - } else { - index = this._findDiffIndex(this._diffEditor.getPosition()!); - } - - if (this._diffs.length === 0) { - // Nothing to do - return; - } - - index = index % this._diffs.length; - const entries = this._diffs[index].entries; - this._diffEditor.setPosition(new Position(entries[0].modifiedLineStart, 1)); - this._diffEditor.setSelection({ startColumn: 1, startLineNumber: entries[0].modifiedLineStart, endColumn: Constants.MAX_SAFE_SMALL_INTEGER, endLineNumber: entries[entries.length - 1].modifiedLineEnd }); - this._isVisibleObs.set(true, undefined); - this.layout(); - this._render(); - this._goToRow(this._getNextRow(), 'next'); - } - - private accept(): void { - let jumpToLineNumber = -1; - const current = this._getCurrentFocusedRow(); - if (current) { - const lineNumber = parseInt(current.getAttribute('data-line')!, 10); - if (!isNaN(lineNumber)) { - jumpToLineNumber = lineNumber; - } - } - this.hide(); - - if (jumpToLineNumber !== -1) { - this._diffEditor.setPosition(new Position(jumpToLineNumber, 1)); - this._diffEditor.revealPosition(new Position(jumpToLineNumber, 1), ScrollType.Immediate); - } - } - - public hide(): void { - this._isVisibleObs.set(false, undefined); - this._diffEditor.focus(); - this.layout(); - this._render(); - } - - private _getPrevRow(): HTMLElement { - const current = this._getCurrentFocusedRow(); - if (!current) { - return this._getFirstRow(); - } - if (current.previousElementSibling) { - return current.previousElementSibling; - } - return current; - } - - private _getNextRow(): HTMLElement { - const current = this._getCurrentFocusedRow(); - if (!current) { - return this._getFirstRow(); - } - if (current.nextElementSibling) { - return current.nextElementSibling; - } - return current; - } - - private _getFirstRow(): HTMLElement { - return this.domNode.domNode.querySelector('.diff-review-row'); - } - - private _getCurrentFocusedRow(): HTMLElement | null { - const result = document.activeElement; - if (result && /diff-review-row/.test(result.className)) { - return result; - } - return null; - } - - private _goToRow(row: HTMLElement, type?: 'next' | 'previous'): void { - const current = this._getCurrentFocusedRow(); - row.tabIndex = 0; - row.focus(); - if (current && current !== row) { - current.tabIndex = -1; - } - const element = !type ? current : type === 'next' ? current?.nextElementSibling : current?.previousElementSibling; - if (element?.classList.contains(DiffEditorLineClasses.Insert)) { - this._audioCueService.playAudioCue(AudioCue.diffLineInserted, true); - } else if (element?.classList.contains(DiffEditorLineClasses.Delete)) { - this._audioCueService.playAudioCue(AudioCue.diffLineDeleted, true); - } - this.scrollbar.scanDomNode(); - } - - private _width: number = 0; - private _top: number = 0; - private _height: number = 0; - - public layout(top: number = this._top, width: number = this._width, height: number = this._height): void { - this._width = width; - this._top = top; - this._height = height; - - this.domNode.setTop(top); - this.domNode.setWidth(width); - this.domNode.setHeight(height); - this._content.setHeight(height); - this._content.setWidth(width); - - if (this._isVisible) { - this.domNode.setDisplay('block'); - this.actionBarContainer.setAttribute('aria-hidden', 'false'); - this.actionBarContainer.setDisplay('block'); - } else { - this.domNode.setDisplay('none'); - this.actionBarContainer.setAttribute('aria-hidden', 'true'); - this.actionBarContainer.setDisplay('none'); - } - } - - private _compute(): Diff[] { - const lineChanges = this._diffEditor.getLineChanges(); - if (!lineChanges || lineChanges.length === 0) { - return []; - } - const originalModel = this._diffEditor.getOriginalEditor().getModel(); - const modifiedModel = this._diffEditor.getModifiedEditor().getModel(); - - if (!originalModel || !modifiedModel) { - return []; - } - - return DiffReview2._mergeAdjacent(lineChanges, originalModel.getLineCount(), modifiedModel.getLineCount()); - } - - private static _mergeAdjacent(lineChanges: ILineChange[], originalLineCount: number, modifiedLineCount: number): Diff[] { - if (!lineChanges || lineChanges.length === 0) { - return []; - } - - const diffs: Diff[] = []; - let diffsLength = 0; - - for (let i = 0, len = lineChanges.length; i < len; i++) { - const lineChange = lineChanges[i]; - - const originalStart = lineChange.originalStartLineNumber; - const originalEnd = lineChange.originalEndLineNumber; - const modifiedStart = lineChange.modifiedStartLineNumber; - const modifiedEnd = lineChange.modifiedEndLineNumber; - - const r: DiffEntry[] = []; - let rLength = 0; - - // Emit before anchors - { - const originalEqualAbove = (originalEnd === 0 ? originalStart : originalStart - 1); - const modifiedEqualAbove = (modifiedEnd === 0 ? modifiedStart : modifiedStart - 1); - - // Make sure we don't step into the previous diff - let minOriginal = 1; - let minModified = 1; - if (i > 0) { - const prevLineChange = lineChanges[i - 1]; - - if (prevLineChange.originalEndLineNumber === 0) { - minOriginal = prevLineChange.originalStartLineNumber + 1; - } else { - minOriginal = prevLineChange.originalEndLineNumber + 1; - } - - if (prevLineChange.modifiedEndLineNumber === 0) { - minModified = prevLineChange.modifiedStartLineNumber + 1; - } else { - minModified = prevLineChange.modifiedEndLineNumber + 1; - } - } - - let fromOriginal = originalEqualAbove - DIFF_LINES_PADDING + 1; - let fromModified = modifiedEqualAbove - DIFF_LINES_PADDING + 1; - if (fromOriginal < minOriginal) { - const delta = minOriginal - fromOriginal; - fromOriginal = fromOriginal + delta; - fromModified = fromModified + delta; - } - if (fromModified < minModified) { - const delta = minModified - fromModified; - fromOriginal = fromOriginal + delta; - fromModified = fromModified + delta; - } - - r[rLength++] = new DiffEntry( - fromOriginal, originalEqualAbove, - fromModified, modifiedEqualAbove - ); - } - - // Emit deleted lines - { - if (originalEnd !== 0) { - r[rLength++] = new DiffEntry(originalStart, originalEnd, 0, 0); - } - } - - // Emit inserted lines - { - if (modifiedEnd !== 0) { - r[rLength++] = new DiffEntry(0, 0, modifiedStart, modifiedEnd); - } - } - - // Emit after anchors - { - const originalEqualBelow = (originalEnd === 0 ? originalStart + 1 : originalEnd + 1); - const modifiedEqualBelow = (modifiedEnd === 0 ? modifiedStart + 1 : modifiedEnd + 1); - - // Make sure we don't step into the next diff - let maxOriginal = originalLineCount; - let maxModified = modifiedLineCount; - if (i + 1 < len) { - const nextLineChange = lineChanges[i + 1]; - - if (nextLineChange.originalEndLineNumber === 0) { - maxOriginal = nextLineChange.originalStartLineNumber; - } else { - maxOriginal = nextLineChange.originalStartLineNumber - 1; - } - - if (nextLineChange.modifiedEndLineNumber === 0) { - maxModified = nextLineChange.modifiedStartLineNumber; - } else { - maxModified = nextLineChange.modifiedStartLineNumber - 1; - } - } - - let toOriginal = originalEqualBelow + DIFF_LINES_PADDING - 1; - let toModified = modifiedEqualBelow + DIFF_LINES_PADDING - 1; - - if (toOriginal > maxOriginal) { - const delta = maxOriginal - toOriginal; - toOriginal = toOriginal + delta; - toModified = toModified + delta; - } - if (toModified > maxModified) { - const delta = maxModified - toModified; - toOriginal = toOriginal + delta; - toModified = toModified + delta; - } - - r[rLength++] = new DiffEntry( - originalEqualBelow, toOriginal, - modifiedEqualBelow, toModified, - ); - } - - diffs[diffsLength++] = new Diff(r); - } - - // Merge adjacent diffs - let curr: DiffEntry[] = diffs[0].entries; - const r: Diff[] = []; - let rLength = 0; - for (let i = 1, len = diffs.length; i < len; i++) { - const thisDiff = diffs[i].entries; - - const currLast = curr[curr.length - 1]; - const thisFirst = thisDiff[0]; - - if ( - currLast.getType() === DiffEntryType.Equal - && thisFirst.getType() === DiffEntryType.Equal - && thisFirst.originalLineStart <= currLast.originalLineEnd - ) { - // We are dealing with equal lines that overlap - - curr[curr.length - 1] = new DiffEntry( - currLast.originalLineStart, thisFirst.originalLineEnd, - currLast.modifiedLineStart, thisFirst.modifiedLineEnd - ); - curr = curr.concat(thisDiff.slice(1)); - continue; - } - - r[rLength++] = new Diff(curr); - curr = thisDiff; - } - r[rLength++] = new Diff(curr); - return r; - } - - private _findDiffIndex(pos: Position): number { - const lineNumber = pos.lineNumber; - for (let i = 0, len = this._diffs.length; i < len; i++) { - const diff = this._diffs[i].entries; - const lastModifiedLine = diff[diff.length - 1].modifiedLineEnd; - if (lineNumber <= lastModifiedLine) { - return i; - } - } - return 0; - } - - private _render(): void { - - const originalOptions = this._diffEditor.getOriginalEditor().getOptions(); - const modifiedOptions = this._diffEditor.getModifiedEditor().getOptions(); - - const originalModel = this._diffEditor.getOriginalEditor().getModel(); - const modifiedModel = this._diffEditor.getModifiedEditor().getModel(); - - const originalModelOpts = originalModel!.getOptions(); - const modifiedModelOpts = modifiedModel!.getOptions(); - - if (!this._isVisible || !originalModel || !modifiedModel) { - dom.clearNode(this._content.domNode); - this._currentDiff = null; - this.scrollbar.scanDomNode(); - return; - } - - const diffIndex = this._findDiffIndex(this._diffEditor.getPosition()!); - - if (this._diffs[diffIndex] === this._currentDiff) { - return; - } - this._currentDiff = this._diffs[diffIndex]; - - const diffs = this._diffs[diffIndex].entries; - const container = document.createElement('div'); - container.className = 'diff-review-table'; - container.setAttribute('role', 'list'); - container.setAttribute('aria-label', 'Difference review. Use "Stage | Unstage | Revert Selected Ranges" commands'); - applyFontInfo(container, modifiedOptions.get(EditorOption.fontInfo)); - - let minOriginalLine = 0; - let maxOriginalLine = 0; - let minModifiedLine = 0; - let maxModifiedLine = 0; - for (let i = 0, len = diffs.length; i < len; i++) { - const diffEntry = diffs[i]; - const originalLineStart = diffEntry.originalLineStart; - const originalLineEnd = diffEntry.originalLineEnd; - const modifiedLineStart = diffEntry.modifiedLineStart; - const modifiedLineEnd = diffEntry.modifiedLineEnd; - - if (originalLineStart !== 0 && ((minOriginalLine === 0 || originalLineStart < minOriginalLine))) { - minOriginalLine = originalLineStart; - } - if (originalLineEnd !== 0 && ((maxOriginalLine === 0 || originalLineEnd > maxOriginalLine))) { - maxOriginalLine = originalLineEnd; - } - if (modifiedLineStart !== 0 && ((minModifiedLine === 0 || modifiedLineStart < minModifiedLine))) { - minModifiedLine = modifiedLineStart; - } - if (modifiedLineEnd !== 0 && ((maxModifiedLine === 0 || modifiedLineEnd > maxModifiedLine))) { - maxModifiedLine = modifiedLineEnd; - } - } - - const header = document.createElement('div'); - header.className = 'diff-review-row'; - - const cell = document.createElement('div'); - cell.className = 'diff-review-cell diff-review-summary'; - const originalChangedLinesCnt = maxOriginalLine - minOriginalLine + 1; - const modifiedChangedLinesCnt = maxModifiedLine - minModifiedLine + 1; - cell.appendChild(document.createTextNode(`${diffIndex + 1}/${this._diffs.length}: @@ -${minOriginalLine},${originalChangedLinesCnt} +${minModifiedLine},${modifiedChangedLinesCnt} @@`)); - header.setAttribute('data-line', String(minModifiedLine)); - - const getAriaLines = (lines: number) => { - if (lines === 0) { - return nls.localize('no_lines_changed', "no lines changed"); - } else if (lines === 1) { - return nls.localize('one_line_changed', "1 line changed"); - } else { - return nls.localize('more_lines_changed', "{0} lines changed", lines); - } - }; - - const originalChangedLinesCntAria = getAriaLines(originalChangedLinesCnt); - const modifiedChangedLinesCntAria = getAriaLines(modifiedChangedLinesCnt); - header.setAttribute('aria-label', nls.localize({ - key: 'header', - comment: [ - 'This is the ARIA label for a git diff header.', - 'A git diff header looks like this: @@ -154,12 +159,39 @@.', - 'That encodes that at original line 154 (which is now line 159), 12 lines were removed/changed with 39 lines.', - 'Variables 0 and 1 refer to the diff index out of total number of diffs.', - 'Variables 2 and 4 will be numbers (a line number).', - 'Variables 3 and 5 will be "no lines changed", "1 line changed" or "X lines changed", localized separately.' - ] - }, "Difference {0} of {1}: original line {2}, {3}, modified line {4}, {5}", (diffIndex + 1), this._diffs.length, minOriginalLine, originalChangedLinesCntAria, minModifiedLine, modifiedChangedLinesCntAria)); - header.appendChild(cell); - - // @@ -504,7 +517,7 @@ - header.setAttribute('role', 'listitem'); - container.appendChild(header); - - const lineHeight = modifiedOptions.get(EditorOption.lineHeight); - let modLine = minModifiedLine; - for (let i = 0, len = diffs.length; i < len; i++) { - const diffEntry = diffs[i]; - DiffReview2._renderSection(container, diffEntry, modLine, lineHeight, this._width, originalOptions, originalModel, originalModelOpts, modifiedOptions, modifiedModel, modifiedModelOpts, this._languageService.languageIdCodec); - if (diffEntry.modifiedLineStart !== 0) { - modLine = diffEntry.modifiedLineEnd; - } - } - - dom.clearNode(this._content.domNode); - this._content.domNode.appendChild(container); - this.scrollbar.scanDomNode(); - } - - private static _renderSection( - dest: HTMLElement, diffEntry: DiffEntry, modLine: number, lineHeight: number, width: number, - originalOptions: IComputedEditorOptions, originalModel: ITextModel, originalModelOpts: TextModelResolvedOptions, - modifiedOptions: IComputedEditorOptions, modifiedModel: ITextModel, modifiedModelOpts: TextModelResolvedOptions, - languageIdCodec: ILanguageIdCodec - ): void { - - const type = diffEntry.getType(); - - let rowClassName: string = 'diff-review-row'; - let lineNumbersExtraClassName: string = ''; - const spacerClassName: string = 'diff-review-spacer'; - let spacerIcon: ThemeIcon | null = null; - switch (type) { - case DiffEntryType.Insert: - rowClassName = 'diff-review-row line-insert'; - lineNumbersExtraClassName = ' char-insert'; - spacerIcon = diffReviewInsertIcon; - break; - case DiffEntryType.Delete: - rowClassName = 'diff-review-row line-delete'; - lineNumbersExtraClassName = ' char-delete'; - spacerIcon = diffReviewRemoveIcon; - break; - } - - const originalLineStart = diffEntry.originalLineStart; - const originalLineEnd = diffEntry.originalLineEnd; - const modifiedLineStart = diffEntry.modifiedLineStart; - const modifiedLineEnd = diffEntry.modifiedLineEnd; - - const cnt = Math.max( - modifiedLineEnd - modifiedLineStart, - originalLineEnd - originalLineStart - ); - - const originalLayoutInfo = originalOptions.get(EditorOption.layoutInfo); - const originalLineNumbersWidth = originalLayoutInfo.glyphMarginWidth + originalLayoutInfo.lineNumbersWidth; - - const modifiedLayoutInfo = modifiedOptions.get(EditorOption.layoutInfo); - const modifiedLineNumbersWidth = 10 + modifiedLayoutInfo.glyphMarginWidth + modifiedLayoutInfo.lineNumbersWidth; - - for (let i = 0; i <= cnt; i++) { - const originalLine = (originalLineStart === 0 ? 0 : originalLineStart + i); - const modifiedLine = (modifiedLineStart === 0 ? 0 : modifiedLineStart + i); - - const row = document.createElement('div'); - row.style.minWidth = width + 'px'; - row.className = rowClassName; - row.setAttribute('role', 'listitem'); - if (modifiedLine !== 0) { - modLine = modifiedLine; - } - row.setAttribute('data-line', String(modLine)); - - const cell = document.createElement('div'); - cell.className = 'diff-review-cell'; - cell.style.height = `${lineHeight}px`; - row.appendChild(cell); - - const originalLineNumber = document.createElement('span'); - originalLineNumber.style.width = (originalLineNumbersWidth + 'px'); - originalLineNumber.style.minWidth = (originalLineNumbersWidth + 'px'); - originalLineNumber.className = 'diff-review-line-number' + lineNumbersExtraClassName; - if (originalLine !== 0) { - originalLineNumber.appendChild(document.createTextNode(String(originalLine))); - } else { - originalLineNumber.innerText = '\u00a0'; - } - cell.appendChild(originalLineNumber); - - const modifiedLineNumber = document.createElement('span'); - modifiedLineNumber.style.width = (modifiedLineNumbersWidth + 'px'); - modifiedLineNumber.style.minWidth = (modifiedLineNumbersWidth + 'px'); - modifiedLineNumber.style.paddingRight = '10px'; - modifiedLineNumber.className = 'diff-review-line-number' + lineNumbersExtraClassName; - if (modifiedLine !== 0) { - modifiedLineNumber.appendChild(document.createTextNode(String(modifiedLine))); - } else { - modifiedLineNumber.innerText = '\u00a0'; - } - cell.appendChild(modifiedLineNumber); - - const spacer = document.createElement('span'); - spacer.className = spacerClassName; - - if (spacerIcon) { - const spacerCodicon = document.createElement('span'); - spacerCodicon.className = ThemeIcon.asClassName(spacerIcon); - spacerCodicon.innerText = '\u00a0\u00a0'; - spacer.appendChild(spacerCodicon); - } else { - spacer.innerText = '\u00a0\u00a0'; - } - cell.appendChild(spacer); - - let lineContent: string; - if (modifiedLine !== 0) { - let html: string | TrustedHTML = this._renderLine(modifiedModel, modifiedOptions, modifiedModelOpts.tabSize, modifiedLine, languageIdCodec); - if (DiffReview2._ttPolicy) { - html = DiffReview2._ttPolicy.createHTML(html as string); - } - cell.insertAdjacentHTML('beforeend', html as string); - lineContent = modifiedModel.getLineContent(modifiedLine); - } else { - let html: string | TrustedHTML = this._renderLine(originalModel, originalOptions, originalModelOpts.tabSize, originalLine, languageIdCodec); - if (DiffReview2._ttPolicy) { - html = DiffReview2._ttPolicy.createHTML(html as string); - } - cell.insertAdjacentHTML('beforeend', html as string); - lineContent = originalModel.getLineContent(originalLine); - } - - if (lineContent.length === 0) { - lineContent = nls.localize('blankLine', "blank"); - } - - let ariaLabel: string = ''; - switch (type) { - case DiffEntryType.Equal: - if (originalLine === modifiedLine) { - ariaLabel = nls.localize({ key: 'unchangedLine', comment: ['The placeholders are contents of the line and should not be translated.'] }, "{0} unchanged line {1}", lineContent, originalLine); - } else { - ariaLabel = nls.localize('equalLine', "{0} original line {1} modified line {2}", lineContent, originalLine, modifiedLine); - } - break; - case DiffEntryType.Insert: - ariaLabel = nls.localize('insertLine', "+ {0} modified line {1}", lineContent, modifiedLine); - break; - case DiffEntryType.Delete: - ariaLabel = nls.localize('deleteLine', "- {0} original line {1}", lineContent, originalLine); - break; - } - row.setAttribute('aria-label', ariaLabel); - - dest.appendChild(row); - } - } - - private static _renderLine(model: ITextModel, options: IComputedEditorOptions, tabSize: number, lineNumber: number, languageIdCodec: ILanguageIdCodec): string { - const lineContent = model.getLineContent(lineNumber); - const fontInfo = options.get(EditorOption.fontInfo); - const lineTokens = LineTokens.createEmpty(lineContent, languageIdCodec); - const isBasicASCII = ViewLineRenderingData.isBasicASCII(lineContent, model.mightContainNonBasicASCII()); - const containsRTL = ViewLineRenderingData.containsRTL(lineContent, isBasicASCII, model.mightContainRTL()); - const r = renderViewLine(new RenderLineInput( - (fontInfo.isMonospace && !options.get(EditorOption.disableMonospaceOptimizations)), - fontInfo.canUseHalfwidthRightwardsArrow, - lineContent, - false, - isBasicASCII, - containsRTL, - 0, - lineTokens, - [], - tabSize, - 0, - fontInfo.spaceWidth, - fontInfo.middotWidth, - fontInfo.wsmiddotWidth, - options.get(EditorOption.stopRenderingLineAfter), - options.get(EditorOption.renderWhitespace), - options.get(EditorOption.renderControlCharacters), - options.get(EditorOption.fontLigatures) !== EditorFontLigatures.OFF, - null - )); - - return r.html; - } -} diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/lineAlignment.ts b/src/vs/editor/browser/widget/diffEditorWidget2/lineAlignment.ts index 507e6d2039c..9960732a03f 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget2/lineAlignment.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget2/lineAlignment.ts @@ -243,7 +243,7 @@ export class ViewZoneManager extends Disposable { } else { const delta = a.modifiedHeightInPx - a.originalHeightInPx; if (delta > 0) { - if (syncedMovedText?.lineRangeMapping.originalRange.contains(a.originalRange.endLineNumberExclusive - 1)) { + if (syncedMovedText?.lineRangeMapping.original.contains(a.originalRange.endLineNumberExclusive - 1)) { continue; } @@ -254,7 +254,7 @@ export class ViewZoneManager extends Disposable { showInHiddenAreas: true, }); } else { - if (syncedMovedText?.lineRangeMapping.modifiedRange.contains(a.modifiedRange.endLineNumberExclusive - 1)) { + if (syncedMovedText?.lineRangeMapping.modified.contains(a.modifiedRange.endLineNumberExclusive - 1)) { continue; } @@ -281,8 +281,8 @@ export class ViewZoneManager extends Disposable { } for (const a of alignmentsSyncedMovedText.read(reader) ?? []) { - if (!syncedMovedText?.lineRangeMapping.originalRange.intersect(a.originalRange) - && !syncedMovedText?.lineRangeMapping.modifiedRange.intersect(a.modifiedRange)) { + if (!syncedMovedText?.lineRangeMapping.original.intersect(a.originalRange) + && !syncedMovedText?.lineRangeMapping.modified.intersect(a.modifiedRange)) { // ignore unrelated alignments outside the synced moved text continue; } @@ -402,8 +402,8 @@ export class ViewZoneManager extends Disposable { let deltaOrigToMod = 0; if (m) { - const trueTopOriginal = this._editors.original.getTopForLineNumber(m.lineRangeMapping.originalRange.startLineNumber, true) - this._originalTopPadding.get(); - const trueTopModified = this._editors.modified.getTopForLineNumber(m.lineRangeMapping.modifiedRange.startLineNumber, true) - this._modifiedTopPadding.get(); + const trueTopOriginal = this._editors.original.getTopForLineNumber(m.lineRangeMapping.original.startLineNumber, true) - this._originalTopPadding.get(); + const trueTopModified = this._editors.modified.getTopForLineNumber(m.lineRangeMapping.modified.startLineNumber, true) - this._modifiedTopPadding.get(); deltaOrigToMod = trueTopModified - trueTopOriginal; } diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/movedBlocksLines.ts b/src/vs/editor/browser/widget/diffEditorWidget2/movedBlocksLines.ts index 4c85fd8afa6..88aed424b95 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget2/movedBlocksLines.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget2/movedBlocksLines.ts @@ -67,9 +67,9 @@ export class MovedBlocksLinesPart extends Disposable { return (t1 + t2) / 2; } - const start = computeLineStart(m.lineRangeMapping.originalRange, this._editors.original); + const start = computeLineStart(m.lineRangeMapping.original, this._editors.original); const startOffset = originalScrollTop.read(reader); - const end = computeLineStart(m.lineRangeMapping.modifiedRange, this._editors.modified); + const end = computeLineStart(m.lineRangeMapping.modified, this._editors.modified); const endOffset = modifiedScrollTop.read(reader); const top = start - startOffset; diff --git a/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts b/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts index 553470a8494..06c449a33ea 100644 --- a/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts +++ b/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts @@ -20,6 +20,7 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService import { IEditorProgressService } from 'vs/platform/progress/common/progress'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { DiffEditorWidget2 } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2'; export class EmbeddedCodeEditorWidget extends CodeEditorWidget { @@ -67,6 +68,9 @@ export class EmbeddedCodeEditorWidget extends CodeEditorWidget { } } +/** + * @deprecated Use EmbeddedDiffEditorWidget2 instead. + */ export class EmbeddedDiffEditorWidget extends DiffEditorWidget { private readonly _parentEditor: ICodeEditor; @@ -111,3 +115,46 @@ export class EmbeddedDiffEditorWidget extends DiffEditorWidget { super.updateOptions(this._overwriteOptions); } } + +/** + * TODO: Rename to EmbeddedDiffEditorWidget once EmbeddedDiffEditorWidget is removed. + */ +export class EmbeddedDiffEditorWidget2 extends DiffEditorWidget2 { + + private readonly _parentEditor: ICodeEditor; + private readonly _overwriteOptions: IDiffEditorOptions; + + constructor( + domElement: HTMLElement, + options: Readonly, + codeEditorWidgetOptions: IDiffCodeEditorWidgetOptions, + parentEditor: ICodeEditor, + @IContextKeyService contextKeyService: IContextKeyService, + @IInstantiationService instantiationService: IInstantiationService, + @ICodeEditorService codeEditorService: ICodeEditorService, + ) { + super(domElement, parentEditor.getRawOptions(), codeEditorWidgetOptions, contextKeyService, instantiationService, codeEditorService); + + this._parentEditor = parentEditor; + this._overwriteOptions = options; + + // Overwrite parent's options + super.updateOptions(this._overwriteOptions); + + this._register(parentEditor.onDidChangeConfiguration(e => this._onParentConfigurationChanged(e))); + } + + getParentEditor(): ICodeEditor { + return this._parentEditor; + } + + private _onParentConfigurationChanged(e: ConfigurationChangedEvent): void { + super.updateOptions(this._parentEditor.getRawOptions()); + super.updateOptions(this._overwriteOptions); + } + + override updateOptions(newOptions: IEditorOptions): void { + objects.mixin(this._overwriteOptions, newOptions, true); + super.updateOptions(this._overwriteOptions); + } +} diff --git a/src/vs/editor/common/config/editorConfigurationSchema.ts b/src/vs/editor/common/config/editorConfigurationSchema.ts index 70dd0b515db..e127ddc675e 100644 --- a/src/vs/editor/common/config/editorConfigurationSchema.ts +++ b/src/vs/editor/common/config/editorConfigurationSchema.ts @@ -216,6 +216,7 @@ const editorConfiguration: IConfigurationNode = { type: 'boolean', default: false, description: nls.localize('useVersion2', "Controls whether the diff editor uses the new or the old implementation."), + tags: ['experimental'], }, 'diffEditor.experimental.showEmptyDecorations': { type: 'boolean', diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 96dbb05c275..5f30ccd744d 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -827,6 +827,11 @@ export interface IDiffEditorBaseOptions { * Defaults to false */ isInEmbeddedEditor?: boolean; + + /** + * If the diff editor should only show the difference review mode. + */ + onlyShowAccessibleDiffViewer?: boolean; } /** diff --git a/src/vs/editor/common/core/lineRange.ts b/src/vs/editor/common/core/lineRange.ts index d39b5e1901e..c8779a2881a 100644 --- a/src/vs/editor/common/core/lineRange.ts +++ b/src/vs/editor/common/core/lineRange.ts @@ -219,6 +219,12 @@ export class LineRange { return result; } + public forEach(f: (lineNumber: number) => void): void { + for (let lineNumber = this.startLineNumber; lineNumber < this.endLineNumberExclusive; lineNumber++) { + f(lineNumber); + } + } + /** * @internal */ diff --git a/src/vs/editor/common/diff/algorithms/joinSequenceDiffs.ts b/src/vs/editor/common/diff/algorithms/joinSequenceDiffs.ts index 5afffbae370..0886c2da1e6 100644 --- a/src/vs/editor/common/diff/algorithms/joinSequenceDiffs.ts +++ b/src/vs/editor/common/diff/algorithms/joinSequenceDiffs.ts @@ -5,6 +5,7 @@ import { OffsetRange } from 'vs/editor/common/core/offsetRange'; import { ISequence, SequenceDiff } from 'vs/editor/common/diff/algorithms/diffAlgorithm'; +import { LinesSliceCharSequence } from 'vs/editor/common/diff/standardLinesDiffComputer'; export function optimizeSequenceDiffs(sequence1: ISequence, sequence2: ISequence, sequenceDiffs: SequenceDiff[]): SequenceDiff[] { let result = sequenceDiffs; @@ -32,6 +33,74 @@ export function smoothenSequenceDiffs(sequence1: ISequence, sequence2: ISequence return result; } +export function randomRandomMatches(sequence1: LinesSliceCharSequence, sequence2: LinesSliceCharSequence, sequenceDiffs: SequenceDiff[]): SequenceDiff[] { + let diffs = sequenceDiffs; + + let counter = 0; + let shouldRepeat: boolean; + do { + shouldRepeat = false; + + const result: SequenceDiff[] = [ + diffs[0] + ]; + + for (let i = 1; i < diffs.length; i++) { + const cur = diffs[i]; + const lastResult = result[result.length - 1]; + + function shouldJoinDiffs(before: SequenceDiff, after: SequenceDiff): boolean { + const unchangedRange = new OffsetRange(lastResult.seq1Range.endExclusive, cur.seq1Range.start); + + const unchangedLineCount = sequence1.countLinesIn(unchangedRange); + if (unchangedLineCount > 5 || unchangedRange.length > 500) { + return false; + } + + const unchangedText = sequence1.getText(unchangedRange).trim(); + if (unchangedText.length > 20 || unchangedText.split(/\r\n|\r|\n/).length > 1) { + return false; + } + + const beforeLineCount1 = sequence1.countLinesIn(before.seq1Range); + const beforeSeq1Length = before.seq1Range.length; + const beforeLineCount2 = sequence2.countLinesIn(before.seq2Range); + const beforeSeq2Length = before.seq2Range.length; + + const afterLineCount1 = sequence1.countLinesIn(after.seq1Range); + const afterSeq1Length = after.seq1Range.length; + const afterLineCount2 = sequence2.countLinesIn(after.seq2Range); + const afterSeq2Length = after.seq2Range.length; + + // TODO: Maybe a neural net can be used to derive the result from these numbers + + const max = 2 * 40 + 50; + function cap(v: number): number { + return Math.min(v, max); + } + + if (Math.pow(Math.pow(cap(beforeLineCount1 * 40 + beforeSeq1Length), 1.5) + Math.pow(cap(beforeLineCount2 * 40 + beforeSeq2Length), 1.5), 1.5) + + Math.pow(Math.pow(cap(afterLineCount1 * 40 + afterSeq1Length), 1.5) + Math.pow(cap(afterLineCount2 * 40 + afterSeq2Length), 1.5), 1.5) > ((max ** 1.5) ** 1.5) * 1.3) { + return true; + } + return false; + } + + const shouldJoin = shouldJoinDiffs(lastResult, cur); + if (shouldJoin) { + shouldRepeat = true; + result[result.length - 1] = result[result.length - 1].join(cur); + } else { + result.push(cur); + } + } + + diffs = result; + } while (counter++ < 10 && shouldRepeat); + + return diffs; +} + /** * This function fixes issues like this: * ``` diff --git a/src/vs/editor/common/diff/linesDiffComputer.ts b/src/vs/editor/common/diff/linesDiffComputer.ts index a096042419f..84505ab563a 100644 --- a/src/vs/editor/common/diff/linesDiffComputer.ts +++ b/src/vs/editor/common/diff/linesDiffComputer.ts @@ -140,19 +140,20 @@ export class RangeMapping { } } +// TODO@hediet: Make LineRangeMapping extend from this! export class SimpleLineRangeMapping { constructor( - public readonly originalRange: LineRange, - public readonly modifiedRange: LineRange, + public readonly original: LineRange, + public readonly modified: LineRange, ) { } public toString(): string { - return `{${this.originalRange.toString()}->${this.modifiedRange.toString()}}`; + return `{${this.original.toString()}->${this.modified.toString()}}`; } public flip(): SimpleLineRangeMapping { - return new SimpleLineRangeMapping(this.modifiedRange, this.originalRange); + return new SimpleLineRangeMapping(this.modified, this.original); } } diff --git a/src/vs/editor/common/diff/standardLinesDiffComputer.ts b/src/vs/editor/common/diff/standardLinesDiffComputer.ts index 51f62509b02..942308e0f09 100644 --- a/src/vs/editor/common/diff/standardLinesDiffComputer.ts +++ b/src/vs/editor/common/diff/standardLinesDiffComputer.ts @@ -11,7 +11,7 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { DateTimeout, ISequence, ITimeout, InfiniteTimeout, SequenceDiff } from 'vs/editor/common/diff/algorithms/diffAlgorithm'; import { DynamicProgrammingDiffing } from 'vs/editor/common/diff/algorithms/dynamicProgrammingDiffing'; -import { optimizeSequenceDiffs, smoothenSequenceDiffs } from 'vs/editor/common/diff/algorithms/joinSequenceDiffs'; +import { optimizeSequenceDiffs, randomRandomMatches, smoothenSequenceDiffs } from 'vs/editor/common/diff/algorithms/joinSequenceDiffs'; import { MyersDiffAlgorithm } from 'vs/editor/common/diff/algorithms/myersDiffAlgorithm'; import { ILinesDiffComputer, ILinesDiffComputerOptions, LineRangeMapping, LinesDiff, MovedText, RangeMapping, SimpleLineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer'; @@ -173,8 +173,8 @@ export class StandardLinesDiffComputer implements ILinesDiffComputer { } private refineDiff(originalLines: string[], modifiedLines: string[], diff: SequenceDiff, timeout: ITimeout, considerWhitespaceChanges: boolean): { mappings: RangeMapping[]; hitTimeout: boolean } { - const slice1 = new Slice(originalLines, diff.seq1Range, considerWhitespaceChanges); - const slice2 = new Slice(modifiedLines, diff.seq2Range, considerWhitespaceChanges); + const slice1 = new LinesSliceCharSequence(originalLines, diff.seq1Range, considerWhitespaceChanges); + const slice2 = new LinesSliceCharSequence(modifiedLines, diff.seq2Range, considerWhitespaceChanges); const diffResult = slice1.length + slice2.length < 500 ? this.dynamicProgrammingDiffing.compute(slice1, slice2, timeout) @@ -184,6 +184,7 @@ export class StandardLinesDiffComputer implements ILinesDiffComputer { diffs = optimizeSequenceDiffs(slice1, slice2, diffs); diffs = coverFullWords(slice1, slice2, diffs); diffs = smoothenSequenceDiffs(slice1, slice2, diffs); + diffs = randomRandomMatches(slice1, slice2, diffs); const result = diffs.map( (d) => @@ -202,7 +203,7 @@ export class StandardLinesDiffComputer implements ILinesDiffComputer { } } -function coverFullWords(sequence1: Slice, sequence2: Slice, sequenceDiffs: SequenceDiff[]): SequenceDiff[] { +function coverFullWords(sequence1: LinesSliceCharSequence, sequence2: LinesSliceCharSequence, sequenceDiffs: SequenceDiff[]): SequenceDiff[] { const additional: SequenceDiff[] = []; let lastModifiedWord: { added: number; deleted: number; count: number; s1Range: OffsetRange; s2Range: OffsetRange } | undefined = undefined; @@ -417,7 +418,7 @@ function getIndentation(str: string): number { return i; } -class Slice implements ISequence { +export class LinesSliceCharSequence implements ISequence { private readonly elements: number[] = []; private readonly firstCharOffsetByLineMinusOne: number[] = []; public readonly lineRange: OffsetRange; @@ -471,7 +472,11 @@ class Slice implements ISequence { } get text(): string { - return [...this.elements].map(e => String.fromCharCode(e)).join(''); + return this.getText(new OffsetRange(0, this.length)); + } + + getText(range: OffsetRange): string { + return this.elements.slice(range.start, range.endExclusive).map(e => String.fromCharCode(e)).join(''); } getElement(offset: number): number { @@ -559,6 +564,10 @@ class Slice implements ISequence { return new OffsetRange(start, end); } + + public countLinesIn(range: OffsetRange): number { + return this.translateOffset(range.endExclusive).lineNumber - this.translateOffset(range.start).lineNumber; + } } function isWordChar(charCode: number): boolean { diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index d589e8aa275..246d32bddd6 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -21,6 +21,7 @@ import { LanguageId } from 'vs/editor/common/encodedTokenAttributes'; import * as model from 'vs/editor/common/model'; import { TokenizationRegistry as TokenizationRegistryImpl } from 'vs/editor/common/tokenizationRegistry'; import { ContiguousMultilineTokens } from 'vs/editor/common/tokens/contiguousMultilineTokens'; +import { localize } from 'vs/nls'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IMarkerData } from 'vs/platform/markers/common/markers'; @@ -1142,6 +1143,45 @@ export const enum SymbolKind { TypeParameter = 25 } +/** + * @internal + */ +export const symbolKindNames: { [symbol: number]: string } = { + [SymbolKind.Array]: localize('Array', "array"), + [SymbolKind.Boolean]: localize('Boolean', "boolean"), + [SymbolKind.Class]: localize('Class', "class"), + [SymbolKind.Constant]: localize('Constant', "constant"), + [SymbolKind.Constructor]: localize('Constructor', "constructor"), + [SymbolKind.Enum]: localize('Enum', "enumeration"), + [SymbolKind.EnumMember]: localize('EnumMember', "enumeration member"), + [SymbolKind.Event]: localize('Event', "event"), + [SymbolKind.Field]: localize('Field', "field"), + [SymbolKind.File]: localize('File', "file"), + [SymbolKind.Function]: localize('Function', "function"), + [SymbolKind.Interface]: localize('Interface', "interface"), + [SymbolKind.Key]: localize('Key', "key"), + [SymbolKind.Method]: localize('Method', "method"), + [SymbolKind.Module]: localize('Module', "module"), + [SymbolKind.Namespace]: localize('Namespace', "namespace"), + [SymbolKind.Null]: localize('Null', "null"), + [SymbolKind.Number]: localize('Number', "number"), + [SymbolKind.Object]: localize('Object', "object"), + [SymbolKind.Operator]: localize('Operator', "operator"), + [SymbolKind.Package]: localize('Package', "package"), + [SymbolKind.Property]: localize('Property', "property"), + [SymbolKind.String]: localize('String', "string"), + [SymbolKind.Struct]: localize('Struct', "struct"), + [SymbolKind.TypeParameter]: localize('TypeParameter', "type parameter"), + [SymbolKind.Variable]: localize('Variable', "variable"), +}; + +/** + * @internal + */ +export function getAriaLabelForSymbol(symbolName: string, kind: SymbolKind): string { + return localize('symbolAriaLabel', '{0} ({1})', symbolName, symbolKindNames[kind]); +} + export const enum SymbolTag { Deprecated = 1, } diff --git a/src/vs/editor/common/services/editorSimpleWorker.ts b/src/vs/editor/common/services/editorSimpleWorker.ts index 2f51814946b..2c26721fd47 100644 --- a/src/vs/editor/common/services/editorSimpleWorker.ts +++ b/src/vs/editor/common/services/editorSimpleWorker.ts @@ -440,10 +440,10 @@ export class EditorSimpleWorker implements IRequestHandler, IDisposable { quitEarly: result.hitTimeout, changes: getLineChanges(result.changes), moves: result.moves.map(m => ([ - m.lineRangeMapping.originalRange.startLineNumber, - m.lineRangeMapping.originalRange.endLineNumberExclusive, - m.lineRangeMapping.modifiedRange.startLineNumber, - m.lineRangeMapping.modifiedRange.endLineNumberExclusive, + m.lineRangeMapping.original.startLineNumber, + m.lineRangeMapping.original.endLineNumberExclusive, + m.lineRangeMapping.modified.startLineNumber, + m.lineRangeMapping.modified.endLineNumberExclusive, getLineChanges(m.changes) ])), }; diff --git a/src/vs/editor/common/viewModel/modelLineProjection.ts b/src/vs/editor/common/viewModel/modelLineProjection.ts index 81be5db0a10..e66499cd98e 100644 --- a/src/vs/editor/common/viewModel/modelLineProjection.ts +++ b/src/vs/editor/common/viewModel/modelLineProjection.ts @@ -193,7 +193,7 @@ class ModelLineProjection implements IModelLineProjection { if (options.inlineClassName) { const offset = (outputLineIndex > 0 ? lineBreakData.wrappedTextIndentLength : 0); const start = offset + Math.max(injectedTextStartOffsetInInputWithInjections - lineStartOffsetInInputWithInjections, 0); - const end = offset + Math.min(injectedTextEndOffsetInInputWithInjections - lineStartOffsetInInputWithInjections, lineEndOffsetInInputWithInjections); + const end = offset + Math.min(injectedTextEndOffsetInInputWithInjections - lineStartOffsetInInputWithInjections, lineEndOffsetInInputWithInjections - lineStartOffsetInInputWithInjections); if (start !== end) { inlineDecorations.push(new SingleLineInlineDecoration(start, end, options.inlineClassName, options.inlineClassNameAffectsLetterSpacing!)); } diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionController.ts b/src/vs/editor/contrib/codeAction/browser/codeActionController.ts index 5b8d6576b14..14307f031e5 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionController.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionController.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* eslint-disable local/code-no-native-private */ - import { getDomNodePagePosition } from 'vs/base/browser/dom'; import { IAnchor } from 'vs/base/browser/ui/contextview/contextview'; import { IAction } from 'vs/base/common/actions'; diff --git a/src/vs/editor/contrib/codeAction/browser/codeActionModel.ts b/src/vs/editor/contrib/codeAction/browser/codeActionModel.ts index 19963bfe57f..b21d5f2dc5d 100644 --- a/src/vs/editor/contrib/codeAction/browser/codeActionModel.ts +++ b/src/vs/editor/contrib/codeAction/browser/codeActionModel.ts @@ -3,8 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -/* eslint-disable local/code-no-native-private */ - import { CancelablePromise, createCancelablePromise, TimeoutTimer } from 'vs/base/common/async'; import { isCancellationError } from 'vs/base/common/errors'; import { Emitter } from 'vs/base/common/event'; diff --git a/src/vs/editor/contrib/hover/browser/contentHover.ts b/src/vs/editor/contrib/hover/browser/contentHover.ts index 6989029fe4f..18e1b5f9d23 100644 --- a/src/vs/editor/contrib/hover/browser/contentHover.ts +++ b/src/vs/editor/contrib/hover/browser/contentHover.ts @@ -30,7 +30,7 @@ export class ContentHoverController extends Disposable { private readonly _participants: IEditorHoverParticipant[]; - private readonly _widget = this._register(this._instantiationService.createInstance(ContentHoverWidget, this._editor)); + private readonly _widget: ContentHoverWidget; getWidgetContent(): string | undefined { const node = this._widget.getDomNode(); @@ -52,6 +52,11 @@ export class ContentHoverController extends Disposable { ) { super(); + const minimumHeight = this._editor.getOption(EditorOption.lineHeight) + 8; + const minimumWidth = 4 / 3 * minimumHeight; + const minimumSize = new dom.Dimension(minimumWidth, minimumHeight); + this._widget = this._register(this._instantiationService.createInstance(ContentHoverWidget, this._editor, minimumSize)); + // Instantiate participants and sort them by `hoverOrdinal` which is relevant for rendering order. this._participants = []; for (const participant of HoverParticipantRegistry.getAll()) { @@ -490,9 +495,10 @@ export class ContentHoverWidget extends ResizableContentWidget { constructor( editor: ICodeEditor, + minimumSize: dom.Dimension, @IContextKeyService contextKeyService: IContextKeyService ) { - super(editor); + super(editor, minimumSize); this._hoverVisibleKey = EditorContextKeys.hoverVisible.bindTo(contextKeyService); this._hoverFocusedKey = EditorContextKeys.hoverFocused.bindTo(contextKeyService); diff --git a/src/vs/editor/contrib/hover/browser/resizableContentWidget.ts b/src/vs/editor/contrib/hover/browser/resizableContentWidget.ts index 69b72483fb3..0243d9e88bf 100644 --- a/src/vs/editor/contrib/hover/browser/resizableContentWidget.ts +++ b/src/vs/editor/contrib/hover/browser/resizableContentWidget.ts @@ -25,13 +25,13 @@ export abstract class ResizableContentWidget extends Disposable implements ICont constructor( protected readonly _editor: ICodeEditor, - initialSize: dom.IDimension = new dom.Dimension(10, 10) + minimumSize: dom.IDimension = new dom.Dimension(10, 10) ) { super(); this._resizableNode.domNode.style.position = 'absolute'; - this._resizableNode.minSize = new dom.Dimension(10, 10); + this._resizableNode.minSize = dom.Dimension.lift(minimumSize); + this._resizableNode.layout(minimumSize.height, minimumSize.width); this._resizableNode.enableSashes(true, true, true, true); - this._resizableNode.layout(initialSize.height, initialSize.width); this._register(this._resizableNode.onDidResize(e => { this._resize(new dom.Dimension(e.dimension.width, e.dimension.height)); if (e.done) { diff --git a/src/vs/editor/contrib/quickAccess/browser/gotoSymbolQuickAccess.ts b/src/vs/editor/contrib/quickAccess/browser/gotoSymbolQuickAccess.ts index d6c3feb2a31..77197c2f9d4 100644 --- a/src/vs/editor/contrib/quickAccess/browser/gotoSymbolQuickAccess.ts +++ b/src/vs/editor/contrib/quickAccess/browser/gotoSymbolQuickAccess.ts @@ -14,7 +14,7 @@ import { format, trim } from 'vs/base/common/strings'; import { IRange, Range } from 'vs/editor/common/core/range'; import { ScrollType } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; -import { DocumentSymbol, SymbolKind, SymbolKinds, SymbolTag } from 'vs/editor/common/languages'; +import { DocumentSymbol, SymbolKind, SymbolKinds, SymbolTag, getAriaLabelForSymbol } from 'vs/editor/common/languages'; import { IOutlineModelService } from 'vs/editor/contrib/documentSymbols/browser/outlineModel'; import { AbstractEditorNavigationQuickAccessProvider, IEditorNavigationQuickAccessOptions, IQuickAccessTextEditorContext } from 'vs/editor/contrib/quickAccess/browser/editorNavigationQuickAccess'; import { localize } from 'vs/nls'; @@ -316,7 +316,7 @@ export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEdit kind: symbol.kind, score: symbolScore, label: symbolLabelWithIcon, - ariaLabel: symbolLabel, + ariaLabel: getAriaLabelForSymbol(symbol.name, symbol.kind), description: containerLabel, highlights: deprecated ? undefined : { label: symbolMatches, diff --git a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts index 7c7260203bf..7b4a946f5f3 100644 --- a/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts +++ b/src/vs/editor/contrib/stickyScroll/browser/stickyScrollWidget.ts @@ -6,7 +6,6 @@ import * as dom from 'vs/base/browser/dom'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { createTrustedTypesPolicy } from 'vs/base/browser/trustedTypes'; -import { equals } from 'vs/base/common/arrays'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import 'vs/css!./stickyScroll'; import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from 'vs/editor/browser/editorBrowser'; @@ -22,10 +21,6 @@ export class StickyScrollWidgetState { readonly lineNumbers: number[], readonly lastLineRelativePosition: number ) { } - - public equals(other: StickyScrollWidgetState | undefined): boolean { - return !!other && this.lastLineRelativePosition === other.lastLineRelativePosition && equals(this.lineNumbers, other.lineNumbers); - } } const _ttPolicy = createTrustedTypesPolicy('stickyScrollViewLayer', { createHTML: value => value }); @@ -40,7 +35,6 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { private _lastLineRelativePosition: number = 0; private _hoverOnLine: number = -1; private _hoverOnColumn: number = -1; - private _state: StickyScrollWidgetState | undefined; constructor( private readonly _editor: ICodeEditor @@ -74,10 +68,6 @@ export class StickyScrollWidget extends Disposable implements IOverlayWidget { } setState(state: StickyScrollWidgetState): void { - if (state.equals(this._state)) { - return; - } - this._state = state; dom.clearNode(this._rootDomNode); this._disposableStore.clear(); this._lineNumbers.length = 0; diff --git a/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts b/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts index 9b77b25b72e..eea1de6f65b 100644 --- a/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts +++ b/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts @@ -429,6 +429,7 @@ export class UnicodeHighlighterHoverParticipant implements IEditorHoverParticipa } const result: MarkdownHover[] = []; + const existedReason = new Set(); let index = 300; for (const d of lineDecorations) { @@ -480,6 +481,11 @@ export class UnicodeHighlighterHoverParticipant implements IEditorHoverParticipa break; } + if (existedReason.has(reason)) { + continue; + } + existedReason.add(reason); + const adjustSettingsArgs: ShowExcludeOptionsArgs = { codePoint: codePoint, reason: highlightInfo.reason, diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index 0546f5fc282..f026f61f400 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -87,7 +87,7 @@ import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { IStorageService, InMemoryStorageService } from 'vs/platform/storage/common/storage'; import { DefaultConfiguration } from 'vs/platform/configuration/common/configurations'; import { WorkspaceEdit } from 'vs/editor/common/languages'; -import { AudioCue, AudioCueGroupId, IAudioCueService, Sound } from 'vs/platform/audioCues/browser/audioCueService'; +import { AudioCue, IAudioCueService, Sound } from 'vs/platform/audioCues/browser/audioCueService'; import { LogService } from 'vs/platform/log/common/logService'; import { getEditorFeatures } from 'vs/editor/common/editorFeatures'; import { onUnexpectedError } from 'vs/base/common/errors'; @@ -1058,8 +1058,6 @@ class StandaloneAudioService implements IAudioCueService { playAudioCueLoop(cue: AudioCue): IDisposable { return toDisposable(() => { }); } - playRandomAudioCue(groupId: AudioCueGroupId, allowManyInParallel?: boolean): void { - } } export interface IEditorOverrideServices { diff --git a/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts b/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts index 06de7665c02..530ae84b562 100644 --- a/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts +++ b/src/vs/editor/test/browser/viewModel/modelLineProjection.test.ts @@ -921,7 +921,7 @@ suite('SplitLinesCollection', () => { })), [ { inlineDecorations: [{ startOffset: 8, endOffset: 23 }] }, - { inlineDecorations: [{ startOffset: 4, endOffset: 42 }] }, + { inlineDecorations: [{ startOffset: 4, endOffset: 30 }] }, { inlineDecorations: [{ startOffset: 4, endOffset: 16 }] }, { inlineDecorations: undefined }, { inlineDecorations: undefined }, diff --git a/src/vs/editor/test/node/diffing/diffingFixture.test.ts b/src/vs/editor/test/node/diffing/diffingFixture.test.ts index 2cdcb08d472..59173290fc8 100644 --- a/src/vs/editor/test/node/diffing/diffingFixture.test.ts +++ b/src/vs/editor/test/node/diffing/diffingFixture.test.ts @@ -59,8 +59,8 @@ suite('diff fixtures', () => { modified: { content: secondContent, fileName: `./${secondFileName}` }, diffs: getDiffs(diff.changes), moves: diff.moves.map(v => ({ - originalRange: v.lineRangeMapping.originalRange.toString(), - modifiedRange: v.lineRangeMapping.modifiedRange.toString(), + originalRange: v.lineRangeMapping.original.toString(), + modifiedRange: v.lineRangeMapping.modified.toString(), changes: getDiffs(v.changes), })) }; diff --git a/src/vs/editor/test/node/diffing/fixtures/class-replacement/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/class-replacement/advanced.expected.diff.json index c6bc1333002..41e9321310a 100644 --- a/src/vs/editor/test/node/diffing/fixtures/class-replacement/advanced.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/class-replacement/advanced.expected.diff.json @@ -13,12 +13,8 @@ "modifiedRange": "[29,31)", "innerChanges": [ { - "originalRange": "[29,1 -> 33,1]", - "modifiedRange": "[29,1 -> 29,1]" - }, - { - "originalRange": "[33,14 -> 33,41]", - "modifiedRange": "[29,14 -> 30,54]" + "originalRange": "[29,1 -> 33,41]", + "modifiedRange": "[29,1 -> 30,54]" } ] }, @@ -47,16 +43,8 @@ "modifiedRange": "[36,37)", "innerChanges": [ { - "originalRange": "[41,9 -> 41,18]", - "modifiedRange": "[36,9 -> 36,44]" - }, - { - "originalRange": "[41,26 -> 42,34]", - "modifiedRange": "[36,52 -> 36,64]" - }, - { - "originalRange": "[43,1 -> 46,1]", - "modifiedRange": "[37,1 -> 37,1]" + "originalRange": "[41,9 -> 46,1]", + "modifiedRange": "[36,9 -> 37,1]" } ] }, @@ -65,12 +53,8 @@ "modifiedRange": "[39,40)", "innerChanges": [ { - "originalRange": "[48,9 -> 63,48]", - "modifiedRange": "[39,9 -> 39,43]" - }, - { - "originalRange": "[64,1 -> 72,1]", - "modifiedRange": "[40,1 -> 40,1]" + "originalRange": "[48,9 -> 72,1]", + "modifiedRange": "[39,9 -> 40,1]" } ] } diff --git a/src/vs/editor/test/node/diffing/fixtures/difficult-move/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/difficult-move/advanced.expected.diff.json index 6d0a4cf9977..b2155f7f625 100644 --- a/src/vs/editor/test/node/diffing/fixtures/difficult-move/advanced.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/difficult-move/advanced.expected.diff.json @@ -43,60 +43,8 @@ "modifiedRange": "[226,234)", "innerChanges": [ { - "originalRange": "[222,17 -> 222,19]", - "modifiedRange": "[226,17 -> 226,17]" - }, - { - "originalRange": "[223,4 -> 223,28]", - "modifiedRange": "[227,4 -> 227,37]" - }, - { - "originalRange": "[223,32 -> 223,49]", - "modifiedRange": "[227,41 -> 227,48]" - }, - { - "originalRange": "[223,54 -> 223,65]", - "modifiedRange": "[227,53 -> 227,62]" - }, - { - "originalRange": "[224,4 -> 224,29]", - "modifiedRange": "[228,4 -> 228,55]" - }, - { - "originalRange": "[224,43 -> 225,63]", - "modifiedRange": "[228,69 -> 228,108]" - }, - { - "originalRange": "[225,78 -> 226,8]", - "modifiedRange": "[228,123 -> 228,152]" - }, - { - "originalRange": "[226,22 -> 226,25]", - "modifiedRange": "[228,166 -> 228,169]" - }, - { - "originalRange": "[227,5 -> 227,93]", - "modifiedRange": "[229,5 -> 229,67]" - }, - { - "originalRange": "[228,5 -> 228,51]", - "modifiedRange": "[230,5 -> 230,30]" - }, - { - "originalRange": "[229,6 -> 229,143]", - "modifiedRange": "[231,6 -> 231,40]" - }, - { - "originalRange": "[230,5 -> 232,42]", - "modifiedRange": "[232,5 -> 232,19]" - }, - { - "originalRange": "[232,48 -> 232,98]", - "modifiedRange": "[232,25 -> 233,58]" - }, - { - "originalRange": "[233,1 -> 234,1]", - "modifiedRange": "[234,1 -> 234,1]" + "originalRange": "[222,17 -> 234,1]", + "modifiedRange": "[226,17 -> 234,1]" } ] } diff --git a/src/vs/editor/test/node/diffing/fixtures/method-splitting/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/method-splitting/advanced.expected.diff.json index 8bc08d08086..61e711a47e3 100644 --- a/src/vs/editor/test/node/diffing/fixtures/method-splitting/advanced.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/method-splitting/advanced.expected.diff.json @@ -33,16 +33,8 @@ "modifiedRange": "[7,9 -> 7,27]" }, { - "originalRange": "[7,61 -> 7,105]", - "modifiedRange": "[7,50 -> 7,85]" - }, - { - "originalRange": "[7,112 -> 8,10]", - "modifiedRange": "[7,92 -> 7,130]" - }, - { - "originalRange": "[8,15 -> 10,1]", - "modifiedRange": "[7,135 -> 8,1]" + "originalRange": "[7,61 -> 10,1]", + "modifiedRange": "[7,50 -> 8,1]" } ] }, diff --git a/src/vs/editor/test/node/diffing/fixtures/noise-1/1.tst b/src/vs/editor/test/node/diffing/fixtures/noise-1/1.tst new file mode 100644 index 00000000000..bd5594cabb4 --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/noise-1/1.tst @@ -0,0 +1,57 @@ +this._sash = derivedWithStore('sash', (reader, store) => { + const showSash = this._options.renderSideBySide.read(reader); + this.elements.root.classList.toggle('side-by-side', showSash); + if (!showSash) { return undefined; } + const result = store.add(new DiffEditorSash( + this._options, + this.elements.root, + { + height: this._rootSizeObserver.height, + width: this._rootSizeObserver.width.map((w, reader) => w - (this._options.renderOverviewRuler.read(reader) ? OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH : 0)), + } + )); + store.add(autorun('setBoundarySashes', reader => { + const boundarySashes = this._boundarySashes.read(reader); + if (boundarySashes) { + result.setBoundarySashes(boundarySashes); + } + })); + return result; +}); +this._register(keepAlive(this._sash, true)); + +this._register(autorunWithStore2('UnchangedRangesFeature', (reader, store) => { + this.unchangedRangesFeature = store.add(new (readHotReloadableExport(UnchangedRangesFeature, reader))(this._editors, this._diffModel, this._options)); +})); + +this._register(autorunWithStore2('DiffEditorDecorations', (reader, store) => { + store.add(new (readHotReloadableExport(DiffEditorDecorations, reader))(this._editors, this._diffModel, this._options)); +})); +this._register(autorunWithStore2('ViewZoneManager', (reader, store) => { + store.add(this._instantiationService.createInstance( + readHotReloadableExport(ViewZoneManager, reader), + this._editors, + this._diffModel, + this._options, + this, + () => this.unchangedRangesFeature.isUpdatingViewZones, + )); +})); + +this._register(autorunWithStore2('OverviewRulerPart', (reader, store) => { + store.add(this._instantiationService.createInstance(readHotReloadableExport(OverviewRulerPart, reader), this._editors, + this.elements.root, + this._diffModel, + this._rootSizeObserver.width, + this._rootSizeObserver.height, + this._layoutInfo.map(i => i.modifiedEditor), + this._options, + )); +})); + +this._reviewPane = this._register(this._instantiationService.createInstance(DiffReview2, this)); +this.elements.root.appendChild(this._reviewPane.domNode.domNode); +this.elements.root.appendChild(this._reviewPane.actionBarContainer.domNode); +reviewPaneObservable.set(this._reviewPane, undefined); + +this._createDiffEditorContributions(); diff --git a/src/vs/editor/test/node/diffing/fixtures/noise-1/2.tst b/src/vs/editor/test/node/diffing/fixtures/noise-1/2.tst new file mode 100644 index 00000000000..0432b64d75f --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/noise-1/2.tst @@ -0,0 +1,67 @@ +this._sash = derivedWithStore('sash', (reader, store) => { + const showSash = this._options.renderSideBySide.read(reader); + this.elements.root.classList.toggle('side-by-side', showSash); + if (!showSash) { return undefined; } + const result = store.add(new DiffEditorSash( + this._options, + this.elements.root, + { + height: this._rootSizeObserver.height, + width: this._rootSizeObserver.width.map((w, reader) => w - (this._options.renderOverviewRuler.read(reader) ? OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH : 0)), + } + )); + store.add(autorun('setBoundarySashes', reader => { + const boundarySashes = this._boundarySashes.read(reader); + if (boundarySashes) { + result.setBoundarySashes(boundarySashes); + } + })); + return result; +}); +this._register(keepAlive(this._sash, true)); + +this._register(autorunWithStore2('UnchangedRangesFeature', (reader, store) => { + this.unchangedRangesFeature = store.add(new (readHotReloadableExport(UnchangedRangesFeature, reader))(this._editors, this._diffModel, this._options)); +})); + +this._register(autorunWithStore2('DiffEditorDecorations', (reader, store) => { + store.add(new (readHotReloadableExport(DiffEditorDecorations, reader))(this._editors, this._diffModel, this._options)); +})); +this._register(autorunWithStore2('ViewZoneManager', (reader, store) => { + store.add(this._instantiationService.createInstance( + readHotReloadableExport(ViewZoneManager, reader), + this._editors, + this._diffModel, + this._options, + this, + () => this.unchangedRangesFeature.isUpdatingViewZones, + )); +})); + +this._register(autorunWithStore2('OverviewRulerPart', (reader, store) => { + store.add(this._instantiationService.createInstance(readHotReloadableExport(OverviewRulerPart, reader), this._editors, + this.elements.root, + this._diffModel, + this._rootSizeObserver.width, + this._rootSizeObserver.height, + this._layoutInfo.map(i => i.modifiedEditor), + this._options, + )); +})); + +this._register(autorunWithStore2('_accessibleDiffViewer', (reader, store) => { + this._accessibleDiffViewer = store.add(this._register(this._instantiationService.createInstance( + readHotReloadableExport(AccessibleDiffViewer, reader), + this.elements.accessibleDiffViewer, + this._accessibleDiffViewerVisible, + this._rootSizeObserver.width, + this._rootSizeObserver.height, + this._diffModel.map((m, r) => m?.diff.read(r)?.mappings.map(m => m.lineRangeMapping)), + this._editors, + ))); +})); +const visibility = this._accessibleDiffViewerVisible.map(v => v ? 'hidden' : 'visible'); +this._register(applyStyle(this.elements.modified, { visibility })); +this._register(applyStyle(this.elements.original, { visibility })); + +this._createDiffEditorContributions(); diff --git a/src/vs/editor/test/node/diffing/fixtures/noise-1/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/noise-1/advanced.expected.diff.json new file mode 100644 index 00000000000..5a679831447 --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/noise-1/advanced.expected.diff.json @@ -0,0 +1,30 @@ +{ + "original": { + "content": "this._sash = derivedWithStore('sash', (reader, store) => {\n\tconst showSash = this._options.renderSideBySide.read(reader);\n\tthis.elements.root.classList.toggle('side-by-side', showSash);\n\tif (!showSash) { return undefined; }\n\tconst result = store.add(new DiffEditorSash(\n\t\tthis._options,\n\t\tthis.elements.root,\n\t\t{\n\t\t\theight: this._rootSizeObserver.height,\n\t\t\twidth: this._rootSizeObserver.width.map((w, reader) => w - (this._options.renderOverviewRuler.read(reader) ? OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH : 0)),\n\t\t}\n\t));\n\tstore.add(autorun('setBoundarySashes', reader => {\n\t\tconst boundarySashes = this._boundarySashes.read(reader);\n\t\tif (boundarySashes) {\n\t\t\tresult.setBoundarySashes(boundarySashes);\n\t\t}\n\t}));\n\treturn result;\n});\nthis._register(keepAlive(this._sash, true));\n\nthis._register(autorunWithStore2('UnchangedRangesFeature', (reader, store) => {\n\tthis.unchangedRangesFeature = store.add(new (readHotReloadableExport(UnchangedRangesFeature, reader))(this._editors, this._diffModel, this._options));\n}));\n\nthis._register(autorunWithStore2('DiffEditorDecorations', (reader, store) => {\n\tstore.add(new (readHotReloadableExport(DiffEditorDecorations, reader))(this._editors, this._diffModel, this._options));\n}));\nthis._register(autorunWithStore2('ViewZoneManager', (reader, store) => {\n\tstore.add(this._instantiationService.createInstance(\n\t\treadHotReloadableExport(ViewZoneManager, reader),\n\t\tthis._editors,\n\t\tthis._diffModel,\n\t\tthis._options,\n\t\tthis,\n\t\t() => this.unchangedRangesFeature.isUpdatingViewZones,\n\t));\n}));\n\nthis._register(autorunWithStore2('OverviewRulerPart', (reader, store) => {\n\tstore.add(this._instantiationService.createInstance(readHotReloadableExport(OverviewRulerPart, reader), this._editors,\n\t\tthis.elements.root,\n\t\tthis._diffModel,\n\t\tthis._rootSizeObserver.width,\n\t\tthis._rootSizeObserver.height,\n\t\tthis._layoutInfo.map(i => i.modifiedEditor),\n\t\tthis._options,\n\t));\n}));\n\nthis._reviewPane = this._register(this._instantiationService.createInstance(DiffReview2, this));\nthis.elements.root.appendChild(this._reviewPane.domNode.domNode);\nthis.elements.root.appendChild(this._reviewPane.actionBarContainer.domNode);\nreviewPaneObservable.set(this._reviewPane, undefined);\n\nthis._createDiffEditorContributions();\n", + "fileName": "./1.tst" + }, + "modified": { + "content": "this._sash = derivedWithStore('sash', (reader, store) => {\n\tconst showSash = this._options.renderSideBySide.read(reader);\n\tthis.elements.root.classList.toggle('side-by-side', showSash);\n\tif (!showSash) { return undefined; }\n\tconst result = store.add(new DiffEditorSash(\n\t\tthis._options,\n\t\tthis.elements.root,\n\t\t{\n\t\t\theight: this._rootSizeObserver.height,\n\t\t\twidth: this._rootSizeObserver.width.map((w, reader) => w - (this._options.renderOverviewRuler.read(reader) ? OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH : 0)),\n\t\t}\n\t));\n\tstore.add(autorun('setBoundarySashes', reader => {\n\t\tconst boundarySashes = this._boundarySashes.read(reader);\n\t\tif (boundarySashes) {\n\t\t\tresult.setBoundarySashes(boundarySashes);\n\t\t}\n\t}));\n\treturn result;\n});\nthis._register(keepAlive(this._sash, true));\n\nthis._register(autorunWithStore2('UnchangedRangesFeature', (reader, store) => {\n\tthis.unchangedRangesFeature = store.add(new (readHotReloadableExport(UnchangedRangesFeature, reader))(this._editors, this._diffModel, this._options));\n}));\n\nthis._register(autorunWithStore2('DiffEditorDecorations', (reader, store) => {\n\tstore.add(new (readHotReloadableExport(DiffEditorDecorations, reader))(this._editors, this._diffModel, this._options));\n}));\nthis._register(autorunWithStore2('ViewZoneManager', (reader, store) => {\n\tstore.add(this._instantiationService.createInstance(\n\t\treadHotReloadableExport(ViewZoneManager, reader),\n\t\tthis._editors,\n\t\tthis._diffModel,\n\t\tthis._options,\n\t\tthis,\n\t\t() => this.unchangedRangesFeature.isUpdatingViewZones,\n\t));\n}));\n\nthis._register(autorunWithStore2('OverviewRulerPart', (reader, store) => {\n\tstore.add(this._instantiationService.createInstance(readHotReloadableExport(OverviewRulerPart, reader), this._editors,\n\t\tthis.elements.root,\n\t\tthis._diffModel,\n\t\tthis._rootSizeObserver.width,\n\t\tthis._rootSizeObserver.height,\n\t\tthis._layoutInfo.map(i => i.modifiedEditor),\n\t\tthis._options,\n\t));\n}));\n\nthis._register(autorunWithStore2('_accessibleDiffViewer', (reader, store) => {\n\tthis._accessibleDiffViewer = store.add(this._register(this._instantiationService.createInstance(\n\t\treadHotReloadableExport(AccessibleDiffViewer, reader),\n\t\tthis.elements.accessibleDiffViewer,\n\t\tthis._accessibleDiffViewerVisible,\n\t\tthis._rootSizeObserver.width,\n\t\tthis._rootSizeObserver.height,\n\t\tthis._diffModel.map((m, r) => m?.diff.read(r)?.mappings.map(m => m.lineRangeMapping)),\n\t\tthis._editors,\n\t)));\n}));\nconst visibility = this._accessibleDiffViewerVisible.map(v => v ? 'hidden' : 'visible');\nthis._register(applyStyle(this.elements.modified, { visibility }));\nthis._register(applyStyle(this.elements.original, { visibility }));\n\nthis._createDiffEditorContributions();\n", + "fileName": "./2.tst" + }, + "diffs": [ + { + "originalRange": "[52,56)", + "modifiedRange": "[52,66)", + "innerChanges": [ + { + "originalRange": "[52,7 -> 52,20]", + "modifiedRange": "[52,7 -> 53,2]" + }, + { + "originalRange": "[52,24 -> 52,24]", + "modifiedRange": "[53,6 -> 53,45]" + }, + { + "originalRange": "[52,77 -> 55,53]", + "modifiedRange": "[53,98 -> 65,66]" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/vs/editor/test/node/diffing/fixtures/noise-1/legacy.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/noise-1/legacy.expected.diff.json new file mode 100644 index 00000000000..65a39f37dbe --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/noise-1/legacy.expected.diff.json @@ -0,0 +1,70 @@ +{ + "original": { + "content": "this._sash = derivedWithStore('sash', (reader, store) => {\n\tconst showSash = this._options.renderSideBySide.read(reader);\n\tthis.elements.root.classList.toggle('side-by-side', showSash);\n\tif (!showSash) { return undefined; }\n\tconst result = store.add(new DiffEditorSash(\n\t\tthis._options,\n\t\tthis.elements.root,\n\t\t{\n\t\t\theight: this._rootSizeObserver.height,\n\t\t\twidth: this._rootSizeObserver.width.map((w, reader) => w - (this._options.renderOverviewRuler.read(reader) ? OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH : 0)),\n\t\t}\n\t));\n\tstore.add(autorun('setBoundarySashes', reader => {\n\t\tconst boundarySashes = this._boundarySashes.read(reader);\n\t\tif (boundarySashes) {\n\t\t\tresult.setBoundarySashes(boundarySashes);\n\t\t}\n\t}));\n\treturn result;\n});\nthis._register(keepAlive(this._sash, true));\n\nthis._register(autorunWithStore2('UnchangedRangesFeature', (reader, store) => {\n\tthis.unchangedRangesFeature = store.add(new (readHotReloadableExport(UnchangedRangesFeature, reader))(this._editors, this._diffModel, this._options));\n}));\n\nthis._register(autorunWithStore2('DiffEditorDecorations', (reader, store) => {\n\tstore.add(new (readHotReloadableExport(DiffEditorDecorations, reader))(this._editors, this._diffModel, this._options));\n}));\nthis._register(autorunWithStore2('ViewZoneManager', (reader, store) => {\n\tstore.add(this._instantiationService.createInstance(\n\t\treadHotReloadableExport(ViewZoneManager, reader),\n\t\tthis._editors,\n\t\tthis._diffModel,\n\t\tthis._options,\n\t\tthis,\n\t\t() => this.unchangedRangesFeature.isUpdatingViewZones,\n\t));\n}));\n\nthis._register(autorunWithStore2('OverviewRulerPart', (reader, store) => {\n\tstore.add(this._instantiationService.createInstance(readHotReloadableExport(OverviewRulerPart, reader), this._editors,\n\t\tthis.elements.root,\n\t\tthis._diffModel,\n\t\tthis._rootSizeObserver.width,\n\t\tthis._rootSizeObserver.height,\n\t\tthis._layoutInfo.map(i => i.modifiedEditor),\n\t\tthis._options,\n\t));\n}));\n\nthis._reviewPane = this._register(this._instantiationService.createInstance(DiffReview2, this));\nthis.elements.root.appendChild(this._reviewPane.domNode.domNode);\nthis.elements.root.appendChild(this._reviewPane.actionBarContainer.domNode);\nreviewPaneObservable.set(this._reviewPane, undefined);\n\nthis._createDiffEditorContributions();\n", + "fileName": "./1.tst" + }, + "modified": { + "content": "this._sash = derivedWithStore('sash', (reader, store) => {\n\tconst showSash = this._options.renderSideBySide.read(reader);\n\tthis.elements.root.classList.toggle('side-by-side', showSash);\n\tif (!showSash) { return undefined; }\n\tconst result = store.add(new DiffEditorSash(\n\t\tthis._options,\n\t\tthis.elements.root,\n\t\t{\n\t\t\theight: this._rootSizeObserver.height,\n\t\t\twidth: this._rootSizeObserver.width.map((w, reader) => w - (this._options.renderOverviewRuler.read(reader) ? OverviewRulerPart.ENTIRE_DIFF_OVERVIEW_WIDTH : 0)),\n\t\t}\n\t));\n\tstore.add(autorun('setBoundarySashes', reader => {\n\t\tconst boundarySashes = this._boundarySashes.read(reader);\n\t\tif (boundarySashes) {\n\t\t\tresult.setBoundarySashes(boundarySashes);\n\t\t}\n\t}));\n\treturn result;\n});\nthis._register(keepAlive(this._sash, true));\n\nthis._register(autorunWithStore2('UnchangedRangesFeature', (reader, store) => {\n\tthis.unchangedRangesFeature = store.add(new (readHotReloadableExport(UnchangedRangesFeature, reader))(this._editors, this._diffModel, this._options));\n}));\n\nthis._register(autorunWithStore2('DiffEditorDecorations', (reader, store) => {\n\tstore.add(new (readHotReloadableExport(DiffEditorDecorations, reader))(this._editors, this._diffModel, this._options));\n}));\nthis._register(autorunWithStore2('ViewZoneManager', (reader, store) => {\n\tstore.add(this._instantiationService.createInstance(\n\t\treadHotReloadableExport(ViewZoneManager, reader),\n\t\tthis._editors,\n\t\tthis._diffModel,\n\t\tthis._options,\n\t\tthis,\n\t\t() => this.unchangedRangesFeature.isUpdatingViewZones,\n\t));\n}));\n\nthis._register(autorunWithStore2('OverviewRulerPart', (reader, store) => {\n\tstore.add(this._instantiationService.createInstance(readHotReloadableExport(OverviewRulerPart, reader), this._editors,\n\t\tthis.elements.root,\n\t\tthis._diffModel,\n\t\tthis._rootSizeObserver.width,\n\t\tthis._rootSizeObserver.height,\n\t\tthis._layoutInfo.map(i => i.modifiedEditor),\n\t\tthis._options,\n\t));\n}));\n\nthis._register(autorunWithStore2('_accessibleDiffViewer', (reader, store) => {\n\tthis._accessibleDiffViewer = store.add(this._register(this._instantiationService.createInstance(\n\t\treadHotReloadableExport(AccessibleDiffViewer, reader),\n\t\tthis.elements.accessibleDiffViewer,\n\t\tthis._accessibleDiffViewerVisible,\n\t\tthis._rootSizeObserver.width,\n\t\tthis._rootSizeObserver.height,\n\t\tthis._diffModel.map((m, r) => m?.diff.read(r)?.mappings.map(m => m.lineRangeMapping)),\n\t\tthis._editors,\n\t)));\n}));\nconst visibility = this._accessibleDiffViewerVisible.map(v => v ? 'hidden' : 'visible');\nthis._register(applyStyle(this.elements.modified, { visibility }));\nthis._register(applyStyle(this.elements.original, { visibility }));\n\nthis._createDiffEditorContributions();\n", + "fileName": "./2.tst" + }, + "diffs": [ + { + "originalRange": "[52,56)", + "modifiedRange": "[52,66)", + "innerChanges": [ + { + "originalRange": "[52,9 -> 52,20]", + "modifiedRange": "[52,9 -> 53,41]" + }, + { + "originalRange": "[52,77 -> 52,77]", + "modifiedRange": "[53,98 -> 54,37]" + }, + { + "originalRange": "[52,81 -> 52,84]", + "modifiedRange": "[54,41 -> 54,42]" + }, + { + "originalRange": "[52,87 -> 53,1]", + "modifiedRange": "[54,45 -> 55,3]" + }, + { + "originalRange": "[53,15 -> 53,32]", + "modifiedRange": "[55,17 -> 56,3]" + }, + { + "originalRange": "[53,38 -> 53,41]", + "modifiedRange": "[56,9 -> 56,24]" + }, + { + "originalRange": "[53,44 -> 54,1]", + "modifiedRange": "[56,27 -> 59,3]" + }, + { + "originalRange": "[54,6 -> 54,20]", + "modifiedRange": "[59,8 -> 59,80]" + }, + { + "originalRange": "[54,23 -> 54,32]", + "modifiedRange": "[59,83 -> 63,20]" + }, + { + "originalRange": "[54,38 -> 54,41]", + "modifiedRange": "[63,26 -> 63,41]" + }, + { + "originalRange": "[54,44 -> 54,75]", + "modifiedRange": "[63,44 -> 63,111]" + }, + { + "originalRange": "[55,1 -> 55,26]", + "modifiedRange": "[64,1 -> 65,1]" + }, + { + "originalRange": "[55,34 -> 55,53]", + "modifiedRange": "[65,9 -> 65,66]" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/vs/editor/test/node/diffing/fixtures/noise-2/1.tst b/src/vs/editor/test/node/diffing/fixtures/noise-2/1.tst new file mode 100644 index 00000000000..684409b3bbb --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/noise-2/1.tst @@ -0,0 +1,91 @@ + +const maxPersistedSessions = 25; + +export class ChatService extends Disposable implements IChatService { + + private async _sendRequestAsync(model: ChatModel, provider: IChatProvider, message: string | IChatReplyFollowup, usedSlashCommand?: ISlashCommand): Promise { + const request = model.addRequest(message); + + const resolvedCommand = typeof message === 'string' && message.startsWith('/') ? await this.handleSlashCommand(model.sessionId, message) : message; + + let gotProgress = false; + const requestType = typeof message === 'string' ? + (message.startsWith('/') ? 'slashCommand' : 'string') : + 'followup'; + + const rawResponsePromise = createCancelablePromise(async token => { + const progressCallback = (progress: IChatProgress) => { + if (token.isCancellationRequested) { + return; + } + + gotProgress = true; + if ('content' in progress) { + this.trace('sendRequest', `Provider returned progress for session ${model.sessionId}, ${progress.content.length} chars`); + } else { + this.trace('sendRequest', `Provider returned id for session ${model.sessionId}, ${progress.requestId}`); + } + + model.acceptResponseProgress(request, progress); + }; + + const stopWatch = new StopWatch(false); + token.onCancellationRequested(() => { + this.trace('sendRequest', `Request for session ${model.sessionId} was cancelled`); + this.telemetryService.publicLog2('interactiveSessionProviderInvoked', { + providerId: provider.id, + timeToFirstProgress: -1, + // Normally timings happen inside the EH around the actual provider. For cancellation we can measure how long the user waited before cancelling + totalTime: stopWatch.elapsed(), + result: 'cancelled', + requestType, + slashCommand: usedSlashCommand?.command + }); + + model.cancelRequest(request); + }); + if (usedSlashCommand?.command) { + this._onDidSubmitSlashCommand.fire({ slashCommand: usedSlashCommand.command, sessionId: model.sessionId }); + } + let rawResponse = await provider.provideReply({ session: model.session!, message: resolvedCommand }, progressCallback, token); + if (token.isCancellationRequested) { + return; + } else { + if (!rawResponse) { + this.trace('sendRequest', `Provider returned no response for session ${model.sessionId}`); + rawResponse = { session: model.session!, errorDetails: { message: localize('emptyResponse', "Provider returned null response") } }; + } + + const result = rawResponse.errorDetails?.responseIsFiltered ? 'filtered' : + rawResponse.errorDetails && gotProgress ? 'errorWithOutput' : + rawResponse.errorDetails ? 'error' : + 'success'; + this.telemetryService.publicLog2('interactiveSessionProviderInvoked', { + providerId: provider.id, + timeToFirstProgress: rawResponse.timings?.firstProgress ?? 0, + totalTime: rawResponse.timings?.totalElapsed ?? 0, + result, + requestType, + slashCommand: usedSlashCommand?.command + }); + model.setResponse(request, rawResponse); + this.trace('sendRequest', `Provider returned response for session ${model.sessionId}`); + + // TODO refactor this or rethink the API https://github.com/microsoft/vscode-copilot/issues/593 + if (provider.provideFollowups) { + Promise.resolve(provider.provideFollowups(model.session!, CancellationToken.None)).then(followups => { + model.setFollowups(request, withNullAsUndefined(followups)); + model.completeResponse(request); + }); + } else { + model.completeResponse(request); + } + } + }); + this._pendingRequests.set(model.sessionId, rawResponsePromise); + rawResponsePromise.finally(() => { + this._pendingRequests.delete(model.sessionId); + }); + return rawResponsePromise; + } +} diff --git a/src/vs/editor/test/node/diffing/fixtures/noise-2/2.tst b/src/vs/editor/test/node/diffing/fixtures/noise-2/2.tst new file mode 100644 index 00000000000..b7778d336a9 --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/noise-2/2.tst @@ -0,0 +1,111 @@ + +const maxPersistedSessions = 25; + +export class ChatService extends Disposable implements IChatService { + + private async _sendRequestAsync(model: ChatModel, provider: IChatProvider, message: string | IChatReplyFollowup, usedSlashCommand?: ISlashCommand): Promise { + const request = model.addRequest(message); + + const resolvedCommand = typeof message === 'string' && message.startsWith('/') ? await this.handleSlashCommand(model.sessionId, message) : message; + + let gotProgress = false; + const requestType = typeof message === 'string' ? + (message.startsWith('/') ? 'slashCommand' : 'string') : + 'followup'; + + const rawResponsePromise = createCancelablePromise(async token => { + const progressCallback = (progress: IChatProgress) => { + if (token.isCancellationRequested) { + return; + } + + gotProgress = true; + if ('content' in progress) { + this.trace('sendRequest', `Provider returned progress for session ${model.sessionId}, ${progress.content.length} chars`); + } else { + this.trace('sendRequest', `Provider returned id for session ${model.sessionId}, ${progress.requestId}`); + } + + model.acceptResponseProgress(request, progress); + }; + + const stopWatch = new StopWatch(false); + token.onCancellationRequested(() => { + this.trace('sendRequest', `Request for session ${model.sessionId} was cancelled`); + this.telemetryService.publicLog2('interactiveSessionProviderInvoked', { + providerId: provider.id, + timeToFirstProgress: -1, + // Normally timings happen inside the EH around the actual provider. For cancellation we can measure how long the user waited before cancelling + totalTime: stopWatch.elapsed(), + result: 'cancelled', + requestType, + slashCommand: usedSlashCommand?.command + }); + + model.cancelRequest(request); + }); + if (usedSlashCommand?.command) { + this._onDidSubmitSlashCommand.fire({ slashCommand: usedSlashCommand.command, sessionId: model.sessionId }); + } + + let rawResponse: IChatResponse | null | undefined; + + if ((typeof resolvedCommand === 'string' && typeof message === 'string' && this.chatSlashCommandService.hasCommand(resolvedCommand))) { + // contributed slash commands + // TODO: spell this out in the UI + const history: IChatMessage[] = []; + for (const request of model.getRequests()) { + if (typeof request.message !== 'string' || !request.response) { + continue; + } + history.push({ role: ChatMessageRole.User, content: request.message }); + history.push({ role: ChatMessageRole.Assistant, content: request.response?.response.value }); + } + await this.chatSlashCommandService.executeCommand(resolvedCommand, message.substring(resolvedCommand.length + 1).trimStart(), new Progress(p => progressCallback(p)), history, token); + rawResponse = { session: model.session! }; + + } else { + rawResponse = await provider.provideReply({ session: model.session!, message: resolvedCommand }, progressCallback, token); + } + + if (token.isCancellationRequested) { + return; + } else { + if (!rawResponse) { + this.trace('sendRequest', `Provider returned no response for session ${model.sessionId}`); + rawResponse = { session: model.session!, errorDetails: { message: localize('emptyResponse', "Provider returned null response") } }; + } + + const result = rawResponse.errorDetails?.responseIsFiltered ? 'filtered' : + rawResponse.errorDetails && gotProgress ? 'errorWithOutput' : + rawResponse.errorDetails ? 'error' : + 'success'; + this.telemetryService.publicLog2('interactiveSessionProviderInvoked', { + providerId: provider.id, + timeToFirstProgress: rawResponse.timings?.firstProgress ?? 0, + totalTime: rawResponse.timings?.totalElapsed ?? 0, + result, + requestType, + slashCommand: usedSlashCommand?.command + }); + model.setResponse(request, rawResponse); + this.trace('sendRequest', `Provider returned response for session ${model.sessionId}`); + + // TODO refactor this or rethink the API https://github.com/microsoft/vscode-copilot/issues/593 + if (provider.provideFollowups) { + Promise.resolve(provider.provideFollowups(model.session!, CancellationToken.None)).then(followups => { + model.setFollowups(request, withNullAsUndefined(followups)); + model.completeResponse(request); + }); + } else { + model.completeResponse(request); + } + } + }); + this._pendingRequests.set(model.sessionId, rawResponsePromise); + rawResponsePromise.finally(() => { + this._pendingRequests.delete(model.sessionId); + }); + return rawResponsePromise; + } +} diff --git a/src/vs/editor/test/node/diffing/fixtures/noise-2/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/noise-2/advanced.expected.diff.json new file mode 100644 index 00000000000..a30c65cb9ee --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/noise-2/advanced.expected.diff.json @@ -0,0 +1,30 @@ +{ + "original": { + "content": "\nconst maxPersistedSessions = 25;\n\nexport class ChatService extends Disposable implements IChatService {\n\n\tprivate async _sendRequestAsync(model: ChatModel, provider: IChatProvider, message: string | IChatReplyFollowup, usedSlashCommand?: ISlashCommand): Promise {\n\t\tconst request = model.addRequest(message);\n\n\t\tconst resolvedCommand = typeof message === 'string' && message.startsWith('/') ? await this.handleSlashCommand(model.sessionId, message) : message;\n\n\t\tlet gotProgress = false;\n\t\tconst requestType = typeof message === 'string' ?\n\t\t\t(message.startsWith('/') ? 'slashCommand' : 'string') :\n\t\t\t'followup';\n\n\t\tconst rawResponsePromise = createCancelablePromise(async token => {\n\t\t\tconst progressCallback = (progress: IChatProgress) => {\n\t\t\t\tif (token.isCancellationRequested) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tgotProgress = true;\n\t\t\t\tif ('content' in progress) {\n\t\t\t\t\tthis.trace('sendRequest', `Provider returned progress for session ${model.sessionId}, ${progress.content.length} chars`);\n\t\t\t\t} else {\n\t\t\t\t\tthis.trace('sendRequest', `Provider returned id for session ${model.sessionId}, ${progress.requestId}`);\n\t\t\t\t}\n\n\t\t\t\tmodel.acceptResponseProgress(request, progress);\n\t\t\t};\n\n\t\t\tconst stopWatch = new StopWatch(false);\n\t\t\ttoken.onCancellationRequested(() => {\n\t\t\t\tthis.trace('sendRequest', `Request for session ${model.sessionId} was cancelled`);\n\t\t\t\tthis.telemetryService.publicLog2('interactiveSessionProviderInvoked', {\n\t\t\t\t\tproviderId: provider.id,\n\t\t\t\t\ttimeToFirstProgress: -1,\n\t\t\t\t\t// Normally timings happen inside the EH around the actual provider. For cancellation we can measure how long the user waited before cancelling\n\t\t\t\t\ttotalTime: stopWatch.elapsed(),\n\t\t\t\t\tresult: 'cancelled',\n\t\t\t\t\trequestType,\n\t\t\t\t\tslashCommand: usedSlashCommand?.command\n\t\t\t\t});\n\n\t\t\t\tmodel.cancelRequest(request);\n\t\t\t});\n\t\t\tif (usedSlashCommand?.command) {\n\t\t\t\tthis._onDidSubmitSlashCommand.fire({ slashCommand: usedSlashCommand.command, sessionId: model.sessionId });\n\t\t\t}\n\t\t\tlet rawResponse = await provider.provideReply({ session: model.session!, message: resolvedCommand }, progressCallback, token);\n\t\t\tif (token.isCancellationRequested) {\n\t\t\t\treturn;\n\t\t\t} else {\n\t\t\t\tif (!rawResponse) {\n\t\t\t\t\tthis.trace('sendRequest', `Provider returned no response for session ${model.sessionId}`);\n\t\t\t\t\trawResponse = { session: model.session!, errorDetails: { message: localize('emptyResponse', \"Provider returned null response\") } };\n\t\t\t\t}\n\n\t\t\t\tconst result = rawResponse.errorDetails?.responseIsFiltered ? 'filtered' :\n\t\t\t\t\trawResponse.errorDetails && gotProgress ? 'errorWithOutput' :\n\t\t\t\t\t\trawResponse.errorDetails ? 'error' :\n\t\t\t\t\t\t\t'success';\n\t\t\t\tthis.telemetryService.publicLog2('interactiveSessionProviderInvoked', {\n\t\t\t\t\tproviderId: provider.id,\n\t\t\t\t\ttimeToFirstProgress: rawResponse.timings?.firstProgress ?? 0,\n\t\t\t\t\ttotalTime: rawResponse.timings?.totalElapsed ?? 0,\n\t\t\t\t\tresult,\n\t\t\t\t\trequestType,\n\t\t\t\t\tslashCommand: usedSlashCommand?.command\n\t\t\t\t});\n\t\t\t\tmodel.setResponse(request, rawResponse);\n\t\t\t\tthis.trace('sendRequest', `Provider returned response for session ${model.sessionId}`);\n\n\t\t\t\t// TODO refactor this or rethink the API https://github.com/microsoft/vscode-copilot/issues/593\n\t\t\t\tif (provider.provideFollowups) {\n\t\t\t\t\tPromise.resolve(provider.provideFollowups(model.session!, CancellationToken.None)).then(followups => {\n\t\t\t\t\t\tmodel.setFollowups(request, withNullAsUndefined(followups));\n\t\t\t\t\t\tmodel.completeResponse(request);\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\tmodel.completeResponse(request);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\tthis._pendingRequests.set(model.sessionId, rawResponsePromise);\n\t\trawResponsePromise.finally(() => {\n\t\t\tthis._pendingRequests.delete(model.sessionId);\n\t\t});\n\t\treturn rawResponsePromise;\n\t}\n}\n", + "fileName": "./1.tst" + }, + "modified": { + "content": "\nconst maxPersistedSessions = 25;\n\nexport class ChatService extends Disposable implements IChatService {\n\n\tprivate async _sendRequestAsync(model: ChatModel, provider: IChatProvider, message: string | IChatReplyFollowup, usedSlashCommand?: ISlashCommand): Promise {\n\t\tconst request = model.addRequest(message);\n\n\t\tconst resolvedCommand = typeof message === 'string' && message.startsWith('/') ? await this.handleSlashCommand(model.sessionId, message) : message;\n\n\t\tlet gotProgress = false;\n\t\tconst requestType = typeof message === 'string' ?\n\t\t\t(message.startsWith('/') ? 'slashCommand' : 'string') :\n\t\t\t'followup';\n\n\t\tconst rawResponsePromise = createCancelablePromise(async token => {\n\t\t\tconst progressCallback = (progress: IChatProgress) => {\n\t\t\t\tif (token.isCancellationRequested) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tgotProgress = true;\n\t\t\t\tif ('content' in progress) {\n\t\t\t\t\tthis.trace('sendRequest', `Provider returned progress for session ${model.sessionId}, ${progress.content.length} chars`);\n\t\t\t\t} else {\n\t\t\t\t\tthis.trace('sendRequest', `Provider returned id for session ${model.sessionId}, ${progress.requestId}`);\n\t\t\t\t}\n\n\t\t\t\tmodel.acceptResponseProgress(request, progress);\n\t\t\t};\n\n\t\t\tconst stopWatch = new StopWatch(false);\n\t\t\ttoken.onCancellationRequested(() => {\n\t\t\t\tthis.trace('sendRequest', `Request for session ${model.sessionId} was cancelled`);\n\t\t\t\tthis.telemetryService.publicLog2('interactiveSessionProviderInvoked', {\n\t\t\t\t\tproviderId: provider.id,\n\t\t\t\t\ttimeToFirstProgress: -1,\n\t\t\t\t\t// Normally timings happen inside the EH around the actual provider. For cancellation we can measure how long the user waited before cancelling\n\t\t\t\t\ttotalTime: stopWatch.elapsed(),\n\t\t\t\t\tresult: 'cancelled',\n\t\t\t\t\trequestType,\n\t\t\t\t\tslashCommand: usedSlashCommand?.command\n\t\t\t\t});\n\n\t\t\t\tmodel.cancelRequest(request);\n\t\t\t});\n\t\t\tif (usedSlashCommand?.command) {\n\t\t\t\tthis._onDidSubmitSlashCommand.fire({ slashCommand: usedSlashCommand.command, sessionId: model.sessionId });\n\t\t\t}\n\n\t\t\tlet rawResponse: IChatResponse | null | undefined;\n\n\t\t\tif ((typeof resolvedCommand === 'string' && typeof message === 'string' && this.chatSlashCommandService.hasCommand(resolvedCommand))) {\n\t\t\t\t// contributed slash commands\n\t\t\t\t// TODO: spell this out in the UI\n\t\t\t\tconst history: IChatMessage[] = [];\n\t\t\t\tfor (const request of model.getRequests()) {\n\t\t\t\t\tif (typeof request.message !== 'string' || !request.response) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\thistory.push({ role: ChatMessageRole.User, content: request.message });\n\t\t\t\t\thistory.push({ role: ChatMessageRole.Assistant, content: request.response?.response.value });\n\t\t\t\t}\n\t\t\t\tawait this.chatSlashCommandService.executeCommand(resolvedCommand, message.substring(resolvedCommand.length + 1).trimStart(), new Progress(p => progressCallback(p)), history, token);\n\t\t\t\trawResponse = { session: model.session! };\n\n\t\t\t} else {\n\t\t\t\trawResponse = await provider.provideReply({ session: model.session!, message: resolvedCommand }, progressCallback, token);\n\t\t\t}\n\n\t\t\tif (token.isCancellationRequested) {\n\t\t\t\treturn;\n\t\t\t} else {\n\t\t\t\tif (!rawResponse) {\n\t\t\t\t\tthis.trace('sendRequest', `Provider returned no response for session ${model.sessionId}`);\n\t\t\t\t\trawResponse = { session: model.session!, errorDetails: { message: localize('emptyResponse', \"Provider returned null response\") } };\n\t\t\t\t}\n\n\t\t\t\tconst result = rawResponse.errorDetails?.responseIsFiltered ? 'filtered' :\n\t\t\t\t\trawResponse.errorDetails && gotProgress ? 'errorWithOutput' :\n\t\t\t\t\t\trawResponse.errorDetails ? 'error' :\n\t\t\t\t\t\t\t'success';\n\t\t\t\tthis.telemetryService.publicLog2('interactiveSessionProviderInvoked', {\n\t\t\t\t\tproviderId: provider.id,\n\t\t\t\t\ttimeToFirstProgress: rawResponse.timings?.firstProgress ?? 0,\n\t\t\t\t\ttotalTime: rawResponse.timings?.totalElapsed ?? 0,\n\t\t\t\t\tresult,\n\t\t\t\t\trequestType,\n\t\t\t\t\tslashCommand: usedSlashCommand?.command\n\t\t\t\t});\n\t\t\t\tmodel.setResponse(request, rawResponse);\n\t\t\t\tthis.trace('sendRequest', `Provider returned response for session ${model.sessionId}`);\n\n\t\t\t\t// TODO refactor this or rethink the API https://github.com/microsoft/vscode-copilot/issues/593\n\t\t\t\tif (provider.provideFollowups) {\n\t\t\t\t\tPromise.resolve(provider.provideFollowups(model.session!, CancellationToken.None)).then(followups => {\n\t\t\t\t\t\tmodel.setFollowups(request, withNullAsUndefined(followups));\n\t\t\t\t\t\tmodel.completeResponse(request);\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\tmodel.completeResponse(request);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\tthis._pendingRequests.set(model.sessionId, rawResponsePromise);\n\t\trawResponsePromise.finally(() => {\n\t\t\tthis._pendingRequests.delete(model.sessionId);\n\t\t});\n\t\treturn rawResponsePromise;\n\t}\n}\n", + "fileName": "./2.tst" + }, + "diffs": [ + { + "originalRange": "[50,51)", + "modifiedRange": "[50,71)", + "innerChanges": [ + { + "originalRange": "[50,1 -> 50,1]", + "modifiedRange": "[50,1 -> 51,1]" + }, + { + "originalRange": "[50,19 -> 50,27]", + "modifiedRange": "[51,19 -> 68,24]" + }, + { + "originalRange": "[51,1 -> 51,1]", + "modifiedRange": "[69,1 -> 71,1]" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/vs/editor/test/node/diffing/fixtures/noise-2/legacy.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/noise-2/legacy.expected.diff.json new file mode 100644 index 00000000000..1c4aecc9899 --- /dev/null +++ b/src/vs/editor/test/node/diffing/fixtures/noise-2/legacy.expected.diff.json @@ -0,0 +1,17 @@ +{ + "original": { + "content": "\nconst maxPersistedSessions = 25;\n\nexport class ChatService extends Disposable implements IChatService {\n\n\tprivate async _sendRequestAsync(model: ChatModel, provider: IChatProvider, message: string | IChatReplyFollowup, usedSlashCommand?: ISlashCommand): Promise {\n\t\tconst request = model.addRequest(message);\n\n\t\tconst resolvedCommand = typeof message === 'string' && message.startsWith('/') ? await this.handleSlashCommand(model.sessionId, message) : message;\n\n\t\tlet gotProgress = false;\n\t\tconst requestType = typeof message === 'string' ?\n\t\t\t(message.startsWith('/') ? 'slashCommand' : 'string') :\n\t\t\t'followup';\n\n\t\tconst rawResponsePromise = createCancelablePromise(async token => {\n\t\t\tconst progressCallback = (progress: IChatProgress) => {\n\t\t\t\tif (token.isCancellationRequested) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tgotProgress = true;\n\t\t\t\tif ('content' in progress) {\n\t\t\t\t\tthis.trace('sendRequest', `Provider returned progress for session ${model.sessionId}, ${progress.content.length} chars`);\n\t\t\t\t} else {\n\t\t\t\t\tthis.trace('sendRequest', `Provider returned id for session ${model.sessionId}, ${progress.requestId}`);\n\t\t\t\t}\n\n\t\t\t\tmodel.acceptResponseProgress(request, progress);\n\t\t\t};\n\n\t\t\tconst stopWatch = new StopWatch(false);\n\t\t\ttoken.onCancellationRequested(() => {\n\t\t\t\tthis.trace('sendRequest', `Request for session ${model.sessionId} was cancelled`);\n\t\t\t\tthis.telemetryService.publicLog2('interactiveSessionProviderInvoked', {\n\t\t\t\t\tproviderId: provider.id,\n\t\t\t\t\ttimeToFirstProgress: -1,\n\t\t\t\t\t// Normally timings happen inside the EH around the actual provider. For cancellation we can measure how long the user waited before cancelling\n\t\t\t\t\ttotalTime: stopWatch.elapsed(),\n\t\t\t\t\tresult: 'cancelled',\n\t\t\t\t\trequestType,\n\t\t\t\t\tslashCommand: usedSlashCommand?.command\n\t\t\t\t});\n\n\t\t\t\tmodel.cancelRequest(request);\n\t\t\t});\n\t\t\tif (usedSlashCommand?.command) {\n\t\t\t\tthis._onDidSubmitSlashCommand.fire({ slashCommand: usedSlashCommand.command, sessionId: model.sessionId });\n\t\t\t}\n\t\t\tlet rawResponse = await provider.provideReply({ session: model.session!, message: resolvedCommand }, progressCallback, token);\n\t\t\tif (token.isCancellationRequested) {\n\t\t\t\treturn;\n\t\t\t} else {\n\t\t\t\tif (!rawResponse) {\n\t\t\t\t\tthis.trace('sendRequest', `Provider returned no response for session ${model.sessionId}`);\n\t\t\t\t\trawResponse = { session: model.session!, errorDetails: { message: localize('emptyResponse', \"Provider returned null response\") } };\n\t\t\t\t}\n\n\t\t\t\tconst result = rawResponse.errorDetails?.responseIsFiltered ? 'filtered' :\n\t\t\t\t\trawResponse.errorDetails && gotProgress ? 'errorWithOutput' :\n\t\t\t\t\t\trawResponse.errorDetails ? 'error' :\n\t\t\t\t\t\t\t'success';\n\t\t\t\tthis.telemetryService.publicLog2('interactiveSessionProviderInvoked', {\n\t\t\t\t\tproviderId: provider.id,\n\t\t\t\t\ttimeToFirstProgress: rawResponse.timings?.firstProgress ?? 0,\n\t\t\t\t\ttotalTime: rawResponse.timings?.totalElapsed ?? 0,\n\t\t\t\t\tresult,\n\t\t\t\t\trequestType,\n\t\t\t\t\tslashCommand: usedSlashCommand?.command\n\t\t\t\t});\n\t\t\t\tmodel.setResponse(request, rawResponse);\n\t\t\t\tthis.trace('sendRequest', `Provider returned response for session ${model.sessionId}`);\n\n\t\t\t\t// TODO refactor this or rethink the API https://github.com/microsoft/vscode-copilot/issues/593\n\t\t\t\tif (provider.provideFollowups) {\n\t\t\t\t\tPromise.resolve(provider.provideFollowups(model.session!, CancellationToken.None)).then(followups => {\n\t\t\t\t\t\tmodel.setFollowups(request, withNullAsUndefined(followups));\n\t\t\t\t\t\tmodel.completeResponse(request);\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\tmodel.completeResponse(request);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\tthis._pendingRequests.set(model.sessionId, rawResponsePromise);\n\t\trawResponsePromise.finally(() => {\n\t\t\tthis._pendingRequests.delete(model.sessionId);\n\t\t});\n\t\treturn rawResponsePromise;\n\t}\n}\n", + "fileName": "./1.tst" + }, + "modified": { + "content": "\nconst maxPersistedSessions = 25;\n\nexport class ChatService extends Disposable implements IChatService {\n\n\tprivate async _sendRequestAsync(model: ChatModel, provider: IChatProvider, message: string | IChatReplyFollowup, usedSlashCommand?: ISlashCommand): Promise {\n\t\tconst request = model.addRequest(message);\n\n\t\tconst resolvedCommand = typeof message === 'string' && message.startsWith('/') ? await this.handleSlashCommand(model.sessionId, message) : message;\n\n\t\tlet gotProgress = false;\n\t\tconst requestType = typeof message === 'string' ?\n\t\t\t(message.startsWith('/') ? 'slashCommand' : 'string') :\n\t\t\t'followup';\n\n\t\tconst rawResponsePromise = createCancelablePromise(async token => {\n\t\t\tconst progressCallback = (progress: IChatProgress) => {\n\t\t\t\tif (token.isCancellationRequested) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tgotProgress = true;\n\t\t\t\tif ('content' in progress) {\n\t\t\t\t\tthis.trace('sendRequest', `Provider returned progress for session ${model.sessionId}, ${progress.content.length} chars`);\n\t\t\t\t} else {\n\t\t\t\t\tthis.trace('sendRequest', `Provider returned id for session ${model.sessionId}, ${progress.requestId}`);\n\t\t\t\t}\n\n\t\t\t\tmodel.acceptResponseProgress(request, progress);\n\t\t\t};\n\n\t\t\tconst stopWatch = new StopWatch(false);\n\t\t\ttoken.onCancellationRequested(() => {\n\t\t\t\tthis.trace('sendRequest', `Request for session ${model.sessionId} was cancelled`);\n\t\t\t\tthis.telemetryService.publicLog2('interactiveSessionProviderInvoked', {\n\t\t\t\t\tproviderId: provider.id,\n\t\t\t\t\ttimeToFirstProgress: -1,\n\t\t\t\t\t// Normally timings happen inside the EH around the actual provider. For cancellation we can measure how long the user waited before cancelling\n\t\t\t\t\ttotalTime: stopWatch.elapsed(),\n\t\t\t\t\tresult: 'cancelled',\n\t\t\t\t\trequestType,\n\t\t\t\t\tslashCommand: usedSlashCommand?.command\n\t\t\t\t});\n\n\t\t\t\tmodel.cancelRequest(request);\n\t\t\t});\n\t\t\tif (usedSlashCommand?.command) {\n\t\t\t\tthis._onDidSubmitSlashCommand.fire({ slashCommand: usedSlashCommand.command, sessionId: model.sessionId });\n\t\t\t}\n\n\t\t\tlet rawResponse: IChatResponse | null | undefined;\n\n\t\t\tif ((typeof resolvedCommand === 'string' && typeof message === 'string' && this.chatSlashCommandService.hasCommand(resolvedCommand))) {\n\t\t\t\t// contributed slash commands\n\t\t\t\t// TODO: spell this out in the UI\n\t\t\t\tconst history: IChatMessage[] = [];\n\t\t\t\tfor (const request of model.getRequests()) {\n\t\t\t\t\tif (typeof request.message !== 'string' || !request.response) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\thistory.push({ role: ChatMessageRole.User, content: request.message });\n\t\t\t\t\thistory.push({ role: ChatMessageRole.Assistant, content: request.response?.response.value });\n\t\t\t\t}\n\t\t\t\tawait this.chatSlashCommandService.executeCommand(resolvedCommand, message.substring(resolvedCommand.length + 1).trimStart(), new Progress(p => progressCallback(p)), history, token);\n\t\t\t\trawResponse = { session: model.session! };\n\n\t\t\t} else {\n\t\t\t\trawResponse = await provider.provideReply({ session: model.session!, message: resolvedCommand }, progressCallback, token);\n\t\t\t}\n\n\t\t\tif (token.isCancellationRequested) {\n\t\t\t\treturn;\n\t\t\t} else {\n\t\t\t\tif (!rawResponse) {\n\t\t\t\t\tthis.trace('sendRequest', `Provider returned no response for session ${model.sessionId}`);\n\t\t\t\t\trawResponse = { session: model.session!, errorDetails: { message: localize('emptyResponse', \"Provider returned null response\") } };\n\t\t\t\t}\n\n\t\t\t\tconst result = rawResponse.errorDetails?.responseIsFiltered ? 'filtered' :\n\t\t\t\t\trawResponse.errorDetails && gotProgress ? 'errorWithOutput' :\n\t\t\t\t\t\trawResponse.errorDetails ? 'error' :\n\t\t\t\t\t\t\t'success';\n\t\t\t\tthis.telemetryService.publicLog2('interactiveSessionProviderInvoked', {\n\t\t\t\t\tproviderId: provider.id,\n\t\t\t\t\ttimeToFirstProgress: rawResponse.timings?.firstProgress ?? 0,\n\t\t\t\t\ttotalTime: rawResponse.timings?.totalElapsed ?? 0,\n\t\t\t\t\tresult,\n\t\t\t\t\trequestType,\n\t\t\t\t\tslashCommand: usedSlashCommand?.command\n\t\t\t\t});\n\t\t\t\tmodel.setResponse(request, rawResponse);\n\t\t\t\tthis.trace('sendRequest', `Provider returned response for session ${model.sessionId}`);\n\n\t\t\t\t// TODO refactor this or rethink the API https://github.com/microsoft/vscode-copilot/issues/593\n\t\t\t\tif (provider.provideFollowups) {\n\t\t\t\t\tPromise.resolve(provider.provideFollowups(model.session!, CancellationToken.None)).then(followups => {\n\t\t\t\t\t\tmodel.setFollowups(request, withNullAsUndefined(followups));\n\t\t\t\t\t\tmodel.completeResponse(request);\n\t\t\t\t\t});\n\t\t\t\t} else {\n\t\t\t\t\tmodel.completeResponse(request);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\tthis._pendingRequests.set(model.sessionId, rawResponsePromise);\n\t\trawResponsePromise.finally(() => {\n\t\t\tthis._pendingRequests.delete(model.sessionId);\n\t\t});\n\t\treturn rawResponsePromise;\n\t}\n}\n", + "fileName": "./2.tst" + }, + "diffs": [ + { + "originalRange": "[50,51)", + "modifiedRange": "[50,71)", + "innerChanges": null + } + ] +} \ No newline at end of file diff --git a/src/vs/editor/test/node/diffing/fixtures/ts-class/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/ts-class/advanced.expected.diff.json index 85e7beaa072..92f5e20707f 100644 --- a/src/vs/editor/test/node/diffing/fixtures/ts-class/advanced.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/ts-class/advanced.expected.diff.json @@ -47,20 +47,8 @@ "modifiedRange": "[8,12)", "innerChanges": [ { - "originalRange": "[11,10 -> 11,14]", - "modifiedRange": "[8,10 -> 8,11]" - }, - { - "originalRange": "[12,4 -> 13,52]", - "modifiedRange": "[9,4 -> 9,12]" - }, - { - "originalRange": "[13,55 -> 19,67]", - "modifiedRange": "[9,15 -> 9,61]" - }, - { - "originalRange": "[20,17 -> 20,25]", - "modifiedRange": "[10,17 -> 11,51]" + "originalRange": "[11,10 -> 20,25]", + "modifiedRange": "[8,10 -> 11,51]" } ] }, diff --git a/src/vs/editor/test/node/diffing/fixtures/ts-confusing-2/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/ts-confusing-2/advanced.expected.diff.json index 07656313cdc..f639ee6bed4 100644 --- a/src/vs/editor/test/node/diffing/fixtures/ts-confusing-2/advanced.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/ts-confusing-2/advanced.expected.diff.json @@ -43,12 +43,8 @@ "modifiedRange": "[14,18)", "innerChanges": [ { - "originalRange": "[14,120 -> 14,154]", - "modifiedRange": "[14,120 -> 14,162]" - }, - { - "originalRange": "[14,165 -> 14,211]", - "modifiedRange": "[14,173 -> 17,1]" + "originalRange": "[14,120 -> 14,211]", + "modifiedRange": "[14,120 -> 17,1]" } ] }, @@ -65,12 +61,8 @@ "modifiedRange": "[21,8 -> 21,43]" }, { - "originalRange": "[17,71 -> 17,72]", - "modifiedRange": "[21,74 -> 22,1]" - }, - { - "originalRange": "[18,3 -> 23,4]", - "modifiedRange": "[23,3 -> 23,120]" + "originalRange": "[17,71 -> 23,4]", + "modifiedRange": "[21,74 -> 23,120]" } ] }, @@ -89,20 +81,8 @@ "modifiedRange": "[26,38)", "innerChanges": [ { - "originalRange": "[28,1 -> 28,1]", - "modifiedRange": "[26,1 -> 27,1]" - }, - { - "originalRange": "[28,15 -> 28,20]", - "modifiedRange": "[27,15 -> 27,16]" - }, - { - "originalRange": "[29,4 -> 29,24]", - "modifiedRange": "[28,4 -> 29,36]" - }, - { - "originalRange": "[30,4 -> 30,31]", - "modifiedRange": "[30,4 -> 37,9]" + "originalRange": "[28,1 -> 30,31]", + "modifiedRange": "[26,1 -> 37,9]" } ] } diff --git a/src/vs/editor/test/node/diffing/fixtures/ts-example1/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/ts-example1/advanced.expected.diff.json index cc5f77b5dc8..da419b4a59f 100644 --- a/src/vs/editor/test/node/diffing/fixtures/ts-example1/advanced.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/ts-example1/advanced.expected.diff.json @@ -31,16 +31,8 @@ "modifiedRange": "[17,80)", "innerChanges": [ { - "originalRange": "[13,10 -> 13,41]", - "modifiedRange": "[17,10 -> 35,18]" - }, - { - "originalRange": "[14,2 -> 14,8]", - "modifiedRange": "[36,2 -> 39,8]" - }, - { - "originalRange": "[14,13 -> 14,43]", - "modifiedRange": "[39,13 -> 79,2]" + "originalRange": "[13,10 -> 14,43]", + "modifiedRange": "[17,10 -> 79,2]" } ] } diff --git a/src/vs/editor/test/node/diffing/fixtures/ws-alignment/advanced.expected.diff.json b/src/vs/editor/test/node/diffing/fixtures/ws-alignment/advanced.expected.diff.json index 92c6e475761..85a9a2fe928 100644 --- a/src/vs/editor/test/node/diffing/fixtures/ws-alignment/advanced.expected.diff.json +++ b/src/vs/editor/test/node/diffing/fixtures/ws-alignment/advanced.expected.diff.json @@ -23,28 +23,8 @@ "modifiedRange": "[7,18)", "innerChanges": [ { - "originalRange": "[7,5 -> 7,43]", - "modifiedRange": "[7,5 -> 11,140]" - }, - { - "originalRange": "[8,5 -> 9,12]", - "modifiedRange": "[12,5 -> 12,131]" - }, - { - "originalRange": "[10,7 -> 10,22]", - "modifiedRange": "[13,7 -> 13,17]" - }, - { - "originalRange": "[10,30 -> 10,48]", - "modifiedRange": "[13,25 -> 13,118]" - }, - { - "originalRange": "[11,6 -> 11,13]", - "modifiedRange": "[14,6 -> 14,8]" - }, - { - "originalRange": "[12,5 -> 12,17]", - "modifiedRange": "[15,5 -> 16,7]" + "originalRange": "[7,5 -> 12,17]", + "modifiedRange": "[7,5 -> 16,7]" }, { "originalRange": "[13,6 -> 13,11]", @@ -53,4 +33,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/src/vs/loader.js b/src/vs/loader.js index cebfe6da858..c2d38dfca0e 100644 --- a/src/vs/loader.js +++ b/src/vs/loader.js @@ -22,60 +22,60 @@ const _amdLoaderGlobal = this; const _commonjsGlobal = typeof global === 'object' ? global : {}; var AMDLoader; (function (AMDLoader) { - AMDLoader.global = _amdLoaderGlobal; - class Environment { - get isWindows() { - this._detect(); - return this._isWindows; - } - get isNode() { - this._detect(); - return this._isNode; - } - get isElectronRenderer() { - this._detect(); - return this._isElectronRenderer; - } - get isWebWorker() { - this._detect(); - return this._isWebWorker; - } - get isElectronNodeIntegrationWebWorker() { - this._detect(); - return this._isElectronNodeIntegrationWebWorker; - } - constructor() { - this._detected = false; - this._isWindows = false; - this._isNode = false; - this._isElectronRenderer = false; - this._isWebWorker = false; - this._isElectronNodeIntegrationWebWorker = false; - } - _detect() { - if (this._detected) { - return; - } - this._detected = true; - this._isWindows = Environment._isWindows(); - this._isNode = (typeof module !== 'undefined' && !!module.exports); - this._isElectronRenderer = (typeof process !== 'undefined' && typeof process.versions !== 'undefined' && typeof process.versions.electron !== 'undefined' && process.type === 'renderer'); - this._isWebWorker = (typeof AMDLoader.global.importScripts === 'function'); - this._isElectronNodeIntegrationWebWorker = this._isWebWorker && (typeof process !== 'undefined' && typeof process.versions !== 'undefined' && typeof process.versions.electron !== 'undefined' && process.type === 'worker'); - } - static _isWindows() { - if (typeof navigator !== 'undefined') { - if (navigator.userAgent && navigator.userAgent.indexOf('Windows') >= 0) { - return true; - } - } - if (typeof process !== 'undefined') { - return (process.platform === 'win32'); - } - return false; - } - } - AMDLoader.Environment = Environment; + AMDLoader.global = _amdLoaderGlobal; + class Environment { + get isWindows() { + this._detect(); + return this._isWindows; + } + get isNode() { + this._detect(); + return this._isNode; + } + get isElectronRenderer() { + this._detect(); + return this._isElectronRenderer; + } + get isWebWorker() { + this._detect(); + return this._isWebWorker; + } + get isElectronNodeIntegrationWebWorker() { + this._detect(); + return this._isElectronNodeIntegrationWebWorker; + } + constructor() { + this._detected = false; + this._isWindows = false; + this._isNode = false; + this._isElectronRenderer = false; + this._isWebWorker = false; + this._isElectronNodeIntegrationWebWorker = false; + } + _detect() { + if (this._detected) { + return; + } + this._detected = true; + this._isWindows = Environment._isWindows(); + this._isNode = (typeof module !== 'undefined' && !!module.exports); + this._isElectronRenderer = (typeof process !== 'undefined' && typeof process.versions !== 'undefined' && typeof process.versions.electron !== 'undefined' && process.type === 'renderer'); + this._isWebWorker = (typeof AMDLoader.global.importScripts === 'function'); + this._isElectronNodeIntegrationWebWorker = this._isWebWorker && (typeof process !== 'undefined' && typeof process.versions !== 'undefined' && typeof process.versions.electron !== 'undefined' && process.type === 'worker'); + } + static _isWindows() { + if (typeof navigator !== 'undefined') { + if (navigator.userAgent && navigator.userAgent.indexOf('Windows') >= 0) { + return true; + } + } + if (typeof process !== 'undefined') { + return (process.platform === 'win32'); + } + return false; + } + } + AMDLoader.Environment = Environment; })(AMDLoader || (AMDLoader = {})); /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. @@ -83,36 +83,36 @@ var AMDLoader; *--------------------------------------------------------------------------------------------*/ var AMDLoader; (function (AMDLoader) { - class LoaderEvent { - constructor(type, detail, timestamp) { - this.type = type; - this.detail = detail; - this.timestamp = timestamp; - } - } - AMDLoader.LoaderEvent = LoaderEvent; - class LoaderEventRecorder { - constructor(loaderAvailableTimestamp) { - this._events = [new LoaderEvent(1 /* LoaderEventType.LoaderAvailable */, '', loaderAvailableTimestamp)]; - } - record(type, detail) { - this._events.push(new LoaderEvent(type, detail, AMDLoader.Utilities.getHighPerformanceTimestamp())); - } - getEvents() { - return this._events; - } - } - AMDLoader.LoaderEventRecorder = LoaderEventRecorder; - class NullLoaderEventRecorder { - record(type, detail) { - // Nothing to do - } - getEvents() { - return []; - } - } - NullLoaderEventRecorder.INSTANCE = new NullLoaderEventRecorder(); - AMDLoader.NullLoaderEventRecorder = NullLoaderEventRecorder; + class LoaderEvent { + constructor(type, detail, timestamp) { + this.type = type; + this.detail = detail; + this.timestamp = timestamp; + } + } + AMDLoader.LoaderEvent = LoaderEvent; + class LoaderEventRecorder { + constructor(loaderAvailableTimestamp) { + this._events = [new LoaderEvent(1 /* LoaderEventType.LoaderAvailable */, '', loaderAvailableTimestamp)]; + } + record(type, detail) { + this._events.push(new LoaderEvent(type, detail, AMDLoader.Utilities.getHighPerformanceTimestamp())); + } + getEvents() { + return this._events; + } + } + AMDLoader.LoaderEventRecorder = LoaderEventRecorder; + class NullLoaderEventRecorder { + record(type, detail) { + // Nothing to do + } + getEvents() { + return []; + } + } + NullLoaderEventRecorder.INSTANCE = new NullLoaderEventRecorder(); + AMDLoader.NullLoaderEventRecorder = NullLoaderEventRecorder; })(AMDLoader || (AMDLoader = {})); /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. @@ -120,99 +120,99 @@ var AMDLoader; *--------------------------------------------------------------------------------------------*/ var AMDLoader; (function (AMDLoader) { - class Utilities { - /** - * This method does not take care of / vs \ - */ - static fileUriToFilePath(isWindows, uri) { - uri = decodeURI(uri).replace(/%23/g, '#'); - if (isWindows) { - if (/^file:\/\/\//.test(uri)) { - // This is a URI without a hostname => return only the path segment - return uri.substr(8); - } - if (/^file:\/\//.test(uri)) { - return uri.substr(5); - } - } - else { - if (/^file:\/\//.test(uri)) { - return uri.substr(7); - } - } - // Not sure... - return uri; - } - static startsWith(haystack, needle) { - return haystack.length >= needle.length && haystack.substr(0, needle.length) === needle; - } - static endsWith(haystack, needle) { - return haystack.length >= needle.length && haystack.substr(haystack.length - needle.length) === needle; - } - // only check for "?" before "#" to ensure that there is a real Query-String - static containsQueryString(url) { - return /^[^\#]*\?/gi.test(url); - } - /** - * Does `url` start with http:// or https:// or file:// or / ? - */ - static isAbsolutePath(url) { - return /^((http:\/\/)|(https:\/\/)|(file:\/\/)|(\/))/.test(url); - } - static forEachProperty(obj, callback) { - if (obj) { - let key; - for (key in obj) { - if (obj.hasOwnProperty(key)) { - callback(key, obj[key]); - } - } - } - } - static isEmpty(obj) { - let isEmpty = true; - Utilities.forEachProperty(obj, () => { - isEmpty = false; - }); - return isEmpty; - } - static recursiveClone(obj) { - if (!obj || typeof obj !== 'object' || obj instanceof RegExp) { - return obj; - } - if (!Array.isArray(obj) && Object.getPrototypeOf(obj) !== Object.prototype) { - // only clone "simple" objects - return obj; - } - let result = Array.isArray(obj) ? [] : {}; - Utilities.forEachProperty(obj, (key, value) => { - if (value && typeof value === 'object') { - result[key] = Utilities.recursiveClone(value); - } - else { - result[key] = value; - } - }); - return result; - } - static generateAnonymousModule() { - return '===anonymous' + (Utilities.NEXT_ANONYMOUS_ID++) + '==='; - } - static isAnonymousModule(id) { - return Utilities.startsWith(id, '===anonymous'); - } - static getHighPerformanceTimestamp() { - if (!this.PERFORMANCE_NOW_PROBED) { - this.PERFORMANCE_NOW_PROBED = true; - this.HAS_PERFORMANCE_NOW = (AMDLoader.global.performance && typeof AMDLoader.global.performance.now === 'function'); - } - return (this.HAS_PERFORMANCE_NOW ? AMDLoader.global.performance.now() : Date.now()); - } - } - Utilities.NEXT_ANONYMOUS_ID = 1; - Utilities.PERFORMANCE_NOW_PROBED = false; - Utilities.HAS_PERFORMANCE_NOW = false; - AMDLoader.Utilities = Utilities; + class Utilities { + /** + * This method does not take care of / vs \ + */ + static fileUriToFilePath(isWindows, uri) { + uri = decodeURI(uri).replace(/%23/g, '#'); + if (isWindows) { + if (/^file:\/\/\//.test(uri)) { + // This is a URI without a hostname => return only the path segment + return uri.substr(8); + } + if (/^file:\/\//.test(uri)) { + return uri.substr(5); + } + } + else { + if (/^file:\/\//.test(uri)) { + return uri.substr(7); + } + } + // Not sure... + return uri; + } + static startsWith(haystack, needle) { + return haystack.length >= needle.length && haystack.substr(0, needle.length) === needle; + } + static endsWith(haystack, needle) { + return haystack.length >= needle.length && haystack.substr(haystack.length - needle.length) === needle; + } + // only check for "?" before "#" to ensure that there is a real Query-String + static containsQueryString(url) { + return /^[^\#]*\?/gi.test(url); + } + /** + * Does `url` start with http:// or https:// or file:// or / ? + */ + static isAbsolutePath(url) { + return /^((http:\/\/)|(https:\/\/)|(file:\/\/)|(\/))/.test(url); + } + static forEachProperty(obj, callback) { + if (obj) { + let key; + for (key in obj) { + if (obj.hasOwnProperty(key)) { + callback(key, obj[key]); + } + } + } + } + static isEmpty(obj) { + let isEmpty = true; + Utilities.forEachProperty(obj, () => { + isEmpty = false; + }); + return isEmpty; + } + static recursiveClone(obj) { + if (!obj || typeof obj !== 'object' || obj instanceof RegExp) { + return obj; + } + if (!Array.isArray(obj) && Object.getPrototypeOf(obj) !== Object.prototype) { + // only clone "simple" objects + return obj; + } + let result = Array.isArray(obj) ? [] : {}; + Utilities.forEachProperty(obj, (key, value) => { + if (value && typeof value === 'object') { + result[key] = Utilities.recursiveClone(value); + } + else { + result[key] = value; + } + }); + return result; + } + static generateAnonymousModule() { + return '===anonymous' + (Utilities.NEXT_ANONYMOUS_ID++) + '==='; + } + static isAnonymousModule(id) { + return Utilities.startsWith(id, '===anonymous'); + } + static getHighPerformanceTimestamp() { + if (!this.PERFORMANCE_NOW_PROBED) { + this.PERFORMANCE_NOW_PROBED = true; + this.HAS_PERFORMANCE_NOW = (AMDLoader.global.performance && typeof AMDLoader.global.performance.now === 'function'); + } + return (this.HAS_PERFORMANCE_NOW ? AMDLoader.global.performance.now() : Date.now()); + } + } + Utilities.NEXT_ANONYMOUS_ID = 1; + Utilities.PERFORMANCE_NOW_PROBED = false; + Utilities.HAS_PERFORMANCE_NOW = false; + AMDLoader.Utilities = Utilities; })(AMDLoader || (AMDLoader = {})); /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. @@ -220,318 +220,318 @@ var AMDLoader; *--------------------------------------------------------------------------------------------*/ var AMDLoader; (function (AMDLoader) { - function ensureError(err) { - if (err instanceof Error) { - return err; - } - const result = new Error(err.message || String(err) || 'Unknown Error'); - if (err.stack) { - result.stack = err.stack; - } - return result; - } - AMDLoader.ensureError = ensureError; - ; - class ConfigurationOptionsUtil { - /** - * Ensure configuration options make sense - */ - static validateConfigurationOptions(options) { - function defaultOnError(err) { - if (err.phase === 'loading') { - console.error('Loading "' + err.moduleId + '" failed'); - console.error(err); - console.error('Here are the modules that depend on it:'); - console.error(err.neededBy); - return; - } - if (err.phase === 'factory') { - console.error('The factory function of "' + err.moduleId + '" has thrown an exception'); - console.error(err); - console.error('Here are the modules that depend on it:'); - console.error(err.neededBy); - return; - } - } - options = options || {}; - if (typeof options.baseUrl !== 'string') { - options.baseUrl = ''; - } - if (typeof options.isBuild !== 'boolean') { - options.isBuild = false; - } - if (typeof options.paths !== 'object') { - options.paths = {}; - } - if (typeof options.config !== 'object') { - options.config = {}; - } - if (typeof options.catchError === 'undefined') { - options.catchError = false; - } - if (typeof options.recordStats === 'undefined') { - options.recordStats = false; - } - if (typeof options.urlArgs !== 'string') { - options.urlArgs = ''; - } - if (typeof options.onError !== 'function') { - options.onError = defaultOnError; - } - if (!Array.isArray(options.ignoreDuplicateModules)) { - options.ignoreDuplicateModules = []; - } - if (options.baseUrl.length > 0) { - if (!AMDLoader.Utilities.endsWith(options.baseUrl, '/')) { - options.baseUrl += '/'; - } - } - if (typeof options.cspNonce !== 'string') { - options.cspNonce = ''; - } - if (typeof options.preferScriptTags === 'undefined') { - options.preferScriptTags = false; - } - if (options.nodeCachedData && typeof options.nodeCachedData === 'object') { - if (typeof options.nodeCachedData.seed !== 'string') { - options.nodeCachedData.seed = 'seed'; - } - if (typeof options.nodeCachedData.writeDelay !== 'number' || options.nodeCachedData.writeDelay < 0) { - options.nodeCachedData.writeDelay = 1000 * 7; - } - if (!options.nodeCachedData.path || typeof options.nodeCachedData.path !== 'string') { - const err = ensureError(new Error('INVALID cached data configuration, \'path\' MUST be set')); - err.phase = 'configuration'; - options.onError(err); - options.nodeCachedData = undefined; - } - } - return options; - } - static mergeConfigurationOptions(overwrite = null, base = null) { - let result = AMDLoader.Utilities.recursiveClone(base || {}); - // Merge known properties and overwrite the unknown ones - AMDLoader.Utilities.forEachProperty(overwrite, (key, value) => { - if (key === 'ignoreDuplicateModules' && typeof result.ignoreDuplicateModules !== 'undefined') { - result.ignoreDuplicateModules = result.ignoreDuplicateModules.concat(value); - } - else if (key === 'paths' && typeof result.paths !== 'undefined') { - AMDLoader.Utilities.forEachProperty(value, (key2, value2) => result.paths[key2] = value2); - } - else if (key === 'config' && typeof result.config !== 'undefined') { - AMDLoader.Utilities.forEachProperty(value, (key2, value2) => result.config[key2] = value2); - } - else { - result[key] = AMDLoader.Utilities.recursiveClone(value); - } - }); - return ConfigurationOptionsUtil.validateConfigurationOptions(result); - } - } - AMDLoader.ConfigurationOptionsUtil = ConfigurationOptionsUtil; - class Configuration { - constructor(env, options) { - this._env = env; - this.options = ConfigurationOptionsUtil.mergeConfigurationOptions(options); - this._createIgnoreDuplicateModulesMap(); - this._createSortedPathsRules(); - if (this.options.baseUrl === '') { - if (this.options.nodeRequire && this.options.nodeRequire.main && this.options.nodeRequire.main.filename && this._env.isNode) { - let nodeMain = this.options.nodeRequire.main.filename; - let dirnameIndex = Math.max(nodeMain.lastIndexOf('/'), nodeMain.lastIndexOf('\\')); - this.options.baseUrl = nodeMain.substring(0, dirnameIndex + 1); - } - } - } - _createIgnoreDuplicateModulesMap() { - // Build a map out of the ignoreDuplicateModules array - this.ignoreDuplicateModulesMap = {}; - for (let i = 0; i < this.options.ignoreDuplicateModules.length; i++) { - this.ignoreDuplicateModulesMap[this.options.ignoreDuplicateModules[i]] = true; - } - } - _createSortedPathsRules() { - // Create an array our of the paths rules, sorted descending by length to - // result in a more specific -> less specific order - this.sortedPathsRules = []; - AMDLoader.Utilities.forEachProperty(this.options.paths, (from, to) => { - if (!Array.isArray(to)) { - this.sortedPathsRules.push({ - from: from, - to: [to] - }); - } - else { - this.sortedPathsRules.push({ - from: from, - to: to - }); - } - }); - this.sortedPathsRules.sort((a, b) => { - return b.from.length - a.from.length; - }); - } - /** - * Clone current configuration and overwrite options selectively. - * @param options The selective options to overwrite with. - * @result A new configuration - */ - cloneAndMerge(options) { - return new Configuration(this._env, ConfigurationOptionsUtil.mergeConfigurationOptions(options, this.options)); - } - /** - * Get current options bag. Useful for passing it forward to plugins. - */ - getOptionsLiteral() { - return this.options; - } - _applyPaths(moduleId) { - let pathRule; - for (let i = 0, len = this.sortedPathsRules.length; i < len; i++) { - pathRule = this.sortedPathsRules[i]; - if (AMDLoader.Utilities.startsWith(moduleId, pathRule.from)) { - let result = []; - for (let j = 0, lenJ = pathRule.to.length; j < lenJ; j++) { - result.push(pathRule.to[j] + moduleId.substr(pathRule.from.length)); - } - return result; - } - } - return [moduleId]; - } - _addUrlArgsToUrl(url) { - if (AMDLoader.Utilities.containsQueryString(url)) { - return url + '&' + this.options.urlArgs; - } - else { - return url + '?' + this.options.urlArgs; - } - } - _addUrlArgsIfNecessaryToUrl(url) { - if (this.options.urlArgs) { - return this._addUrlArgsToUrl(url); - } - return url; - } - _addUrlArgsIfNecessaryToUrls(urls) { - if (this.options.urlArgs) { - for (let i = 0, len = urls.length; i < len; i++) { - urls[i] = this._addUrlArgsToUrl(urls[i]); - } - } - return urls; - } - /** - * Transform a module id to a location. Appends .js to module ids - */ - moduleIdToPaths(moduleId) { - if (this._env.isNode) { - const isNodeModule = (this.options.amdModulesPattern instanceof RegExp - && !this.options.amdModulesPattern.test(moduleId)); - if (isNodeModule) { - // This is a node module... - if (this.isBuild()) { - // ...and we are at build time, drop it - return ['empty:']; - } - else { - // ...and at runtime we create a `shortcut`-path - return ['node|' + moduleId]; - } - } - } - let result = moduleId; - let results; - if (!AMDLoader.Utilities.endsWith(result, '.js') && !AMDLoader.Utilities.isAbsolutePath(result)) { - results = this._applyPaths(result); - for (let i = 0, len = results.length; i < len; i++) { - if (this.isBuild() && results[i] === 'empty:') { - continue; - } - if (!AMDLoader.Utilities.isAbsolutePath(results[i])) { - results[i] = this.options.baseUrl + results[i]; - } - if (!AMDLoader.Utilities.endsWith(results[i], '.js') && !AMDLoader.Utilities.containsQueryString(results[i])) { - results[i] = results[i] + '.js'; - } - } - } - else { - if (!AMDLoader.Utilities.endsWith(result, '.js') && !AMDLoader.Utilities.containsQueryString(result)) { - result = result + '.js'; - } - results = [result]; - } - return this._addUrlArgsIfNecessaryToUrls(results); - } - /** - * Transform a module id or url to a location. - */ - requireToUrl(url) { - let result = url; - if (!AMDLoader.Utilities.isAbsolutePath(result)) { - result = this._applyPaths(result)[0]; - if (!AMDLoader.Utilities.isAbsolutePath(result)) { - result = this.options.baseUrl + result; - } - } - return this._addUrlArgsIfNecessaryToUrl(result); - } - /** - * Flag to indicate if current execution is as part of a build. - */ - isBuild() { - return this.options.isBuild; - } - shouldInvokeFactory(strModuleId) { - if (!this.options.isBuild) { - // outside of a build, all factories should be invoked - return true; - } - // during a build, only explicitly marked or anonymous modules get their factories invoked - if (AMDLoader.Utilities.isAnonymousModule(strModuleId)) { - return true; - } - if (this.options.buildForceInvokeFactory && this.options.buildForceInvokeFactory[strModuleId]) { - return true; - } - return false; - } - /** - * Test if module `moduleId` is expected to be defined multiple times - */ - isDuplicateMessageIgnoredFor(moduleId) { - return this.ignoreDuplicateModulesMap.hasOwnProperty(moduleId); - } - /** - * Get the configuration settings for the provided module id - */ - getConfigForModule(moduleId) { - if (this.options.config) { - return this.options.config[moduleId]; - } - } - /** - * Should errors be caught when executing module factories? - */ - shouldCatchError() { - return this.options.catchError; - } - /** - * Should statistics be recorded? - */ - shouldRecordStats() { - return this.options.recordStats; - } - /** - * Forward an error to the error handler. - */ - onError(err) { - this.options.onError(err); - } - } - AMDLoader.Configuration = Configuration; + function ensureError(err) { + if (err instanceof Error) { + return err; + } + const result = new Error(err.message || String(err) || 'Unknown Error'); + if (err.stack) { + result.stack = err.stack; + } + return result; + } + AMDLoader.ensureError = ensureError; + ; + class ConfigurationOptionsUtil { + /** + * Ensure configuration options make sense + */ + static validateConfigurationOptions(options) { + function defaultOnError(err) { + if (err.phase === 'loading') { + console.error('Loading "' + err.moduleId + '" failed'); + console.error(err); + console.error('Here are the modules that depend on it:'); + console.error(err.neededBy); + return; + } + if (err.phase === 'factory') { + console.error('The factory function of "' + err.moduleId + '" has thrown an exception'); + console.error(err); + console.error('Here are the modules that depend on it:'); + console.error(err.neededBy); + return; + } + } + options = options || {}; + if (typeof options.baseUrl !== 'string') { + options.baseUrl = ''; + } + if (typeof options.isBuild !== 'boolean') { + options.isBuild = false; + } + if (typeof options.paths !== 'object') { + options.paths = {}; + } + if (typeof options.config !== 'object') { + options.config = {}; + } + if (typeof options.catchError === 'undefined') { + options.catchError = false; + } + if (typeof options.recordStats === 'undefined') { + options.recordStats = false; + } + if (typeof options.urlArgs !== 'string') { + options.urlArgs = ''; + } + if (typeof options.onError !== 'function') { + options.onError = defaultOnError; + } + if (!Array.isArray(options.ignoreDuplicateModules)) { + options.ignoreDuplicateModules = []; + } + if (options.baseUrl.length > 0) { + if (!AMDLoader.Utilities.endsWith(options.baseUrl, '/')) { + options.baseUrl += '/'; + } + } + if (typeof options.cspNonce !== 'string') { + options.cspNonce = ''; + } + if (typeof options.preferScriptTags === 'undefined') { + options.preferScriptTags = false; + } + if (options.nodeCachedData && typeof options.nodeCachedData === 'object') { + if (typeof options.nodeCachedData.seed !== 'string') { + options.nodeCachedData.seed = 'seed'; + } + if (typeof options.nodeCachedData.writeDelay !== 'number' || options.nodeCachedData.writeDelay < 0) { + options.nodeCachedData.writeDelay = 1000 * 7; + } + if (!options.nodeCachedData.path || typeof options.nodeCachedData.path !== 'string') { + const err = ensureError(new Error('INVALID cached data configuration, \'path\' MUST be set')); + err.phase = 'configuration'; + options.onError(err); + options.nodeCachedData = undefined; + } + } + return options; + } + static mergeConfigurationOptions(overwrite = null, base = null) { + let result = AMDLoader.Utilities.recursiveClone(base || {}); + // Merge known properties and overwrite the unknown ones + AMDLoader.Utilities.forEachProperty(overwrite, (key, value) => { + if (key === 'ignoreDuplicateModules' && typeof result.ignoreDuplicateModules !== 'undefined') { + result.ignoreDuplicateModules = result.ignoreDuplicateModules.concat(value); + } + else if (key === 'paths' && typeof result.paths !== 'undefined') { + AMDLoader.Utilities.forEachProperty(value, (key2, value2) => result.paths[key2] = value2); + } + else if (key === 'config' && typeof result.config !== 'undefined') { + AMDLoader.Utilities.forEachProperty(value, (key2, value2) => result.config[key2] = value2); + } + else { + result[key] = AMDLoader.Utilities.recursiveClone(value); + } + }); + return ConfigurationOptionsUtil.validateConfigurationOptions(result); + } + } + AMDLoader.ConfigurationOptionsUtil = ConfigurationOptionsUtil; + class Configuration { + constructor(env, options) { + this._env = env; + this.options = ConfigurationOptionsUtil.mergeConfigurationOptions(options); + this._createIgnoreDuplicateModulesMap(); + this._createSortedPathsRules(); + if (this.options.baseUrl === '') { + if (this.options.nodeRequire && this.options.nodeRequire.main && this.options.nodeRequire.main.filename && this._env.isNode) { + let nodeMain = this.options.nodeRequire.main.filename; + let dirnameIndex = Math.max(nodeMain.lastIndexOf('/'), nodeMain.lastIndexOf('\\')); + this.options.baseUrl = nodeMain.substring(0, dirnameIndex + 1); + } + } + } + _createIgnoreDuplicateModulesMap() { + // Build a map out of the ignoreDuplicateModules array + this.ignoreDuplicateModulesMap = {}; + for (let i = 0; i < this.options.ignoreDuplicateModules.length; i++) { + this.ignoreDuplicateModulesMap[this.options.ignoreDuplicateModules[i]] = true; + } + } + _createSortedPathsRules() { + // Create an array our of the paths rules, sorted descending by length to + // result in a more specific -> less specific order + this.sortedPathsRules = []; + AMDLoader.Utilities.forEachProperty(this.options.paths, (from, to) => { + if (!Array.isArray(to)) { + this.sortedPathsRules.push({ + from: from, + to: [to] + }); + } + else { + this.sortedPathsRules.push({ + from: from, + to: to + }); + } + }); + this.sortedPathsRules.sort((a, b) => { + return b.from.length - a.from.length; + }); + } + /** + * Clone current configuration and overwrite options selectively. + * @param options The selective options to overwrite with. + * @result A new configuration + */ + cloneAndMerge(options) { + return new Configuration(this._env, ConfigurationOptionsUtil.mergeConfigurationOptions(options, this.options)); + } + /** + * Get current options bag. Useful for passing it forward to plugins. + */ + getOptionsLiteral() { + return this.options; + } + _applyPaths(moduleId) { + let pathRule; + for (let i = 0, len = this.sortedPathsRules.length; i < len; i++) { + pathRule = this.sortedPathsRules[i]; + if (AMDLoader.Utilities.startsWith(moduleId, pathRule.from)) { + let result = []; + for (let j = 0, lenJ = pathRule.to.length; j < lenJ; j++) { + result.push(pathRule.to[j] + moduleId.substr(pathRule.from.length)); + } + return result; + } + } + return [moduleId]; + } + _addUrlArgsToUrl(url) { + if (AMDLoader.Utilities.containsQueryString(url)) { + return url + '&' + this.options.urlArgs; + } + else { + return url + '?' + this.options.urlArgs; + } + } + _addUrlArgsIfNecessaryToUrl(url) { + if (this.options.urlArgs) { + return this._addUrlArgsToUrl(url); + } + return url; + } + _addUrlArgsIfNecessaryToUrls(urls) { + if (this.options.urlArgs) { + for (let i = 0, len = urls.length; i < len; i++) { + urls[i] = this._addUrlArgsToUrl(urls[i]); + } + } + return urls; + } + /** + * Transform a module id to a location. Appends .js to module ids + */ + moduleIdToPaths(moduleId) { + if (this._env.isNode) { + const isNodeModule = (this.options.amdModulesPattern instanceof RegExp + && !this.options.amdModulesPattern.test(moduleId)); + if (isNodeModule) { + // This is a node module... + if (this.isBuild()) { + // ...and we are at build time, drop it + return ['empty:']; + } + else { + // ...and at runtime we create a `shortcut`-path + return ['node|' + moduleId]; + } + } + } + let result = moduleId; + let results; + if (!AMDLoader.Utilities.endsWith(result, '.js') && !AMDLoader.Utilities.isAbsolutePath(result)) { + results = this._applyPaths(result); + for (let i = 0, len = results.length; i < len; i++) { + if (this.isBuild() && results[i] === 'empty:') { + continue; + } + if (!AMDLoader.Utilities.isAbsolutePath(results[i])) { + results[i] = this.options.baseUrl + results[i]; + } + if (!AMDLoader.Utilities.endsWith(results[i], '.js') && !AMDLoader.Utilities.containsQueryString(results[i])) { + results[i] = results[i] + '.js'; + } + } + } + else { + if (!AMDLoader.Utilities.endsWith(result, '.js') && !AMDLoader.Utilities.containsQueryString(result)) { + result = result + '.js'; + } + results = [result]; + } + return this._addUrlArgsIfNecessaryToUrls(results); + } + /** + * Transform a module id or url to a location. + */ + requireToUrl(url) { + let result = url; + if (!AMDLoader.Utilities.isAbsolutePath(result)) { + result = this._applyPaths(result)[0]; + if (!AMDLoader.Utilities.isAbsolutePath(result)) { + result = this.options.baseUrl + result; + } + } + return this._addUrlArgsIfNecessaryToUrl(result); + } + /** + * Flag to indicate if current execution is as part of a build. + */ + isBuild() { + return this.options.isBuild; + } + shouldInvokeFactory(strModuleId) { + if (!this.options.isBuild) { + // outside of a build, all factories should be invoked + return true; + } + // during a build, only explicitly marked or anonymous modules get their factories invoked + if (AMDLoader.Utilities.isAnonymousModule(strModuleId)) { + return true; + } + if (this.options.buildForceInvokeFactory && this.options.buildForceInvokeFactory[strModuleId]) { + return true; + } + return false; + } + /** + * Test if module `moduleId` is expected to be defined multiple times + */ + isDuplicateMessageIgnoredFor(moduleId) { + return this.ignoreDuplicateModulesMap.hasOwnProperty(moduleId); + } + /** + * Get the configuration settings for the provided module id + */ + getConfigForModule(moduleId) { + if (this.options.config) { + return this.options.config[moduleId]; + } + } + /** + * Should errors be caught when executing module factories? + */ + shouldCatchError() { + return this.options.catchError; + } + /** + * Should statistics be recorded? + */ + shouldRecordStats() { + return this.options.recordStats; + } + /** + * Forward an error to the error handler. + */ + onError(err) { + this.options.onError(err); + } + } + AMDLoader.Configuration = Configuration; })(AMDLoader || (AMDLoader = {})); /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. @@ -539,503 +539,503 @@ var AMDLoader; *--------------------------------------------------------------------------------------------*/ var AMDLoader; (function (AMDLoader) { - /** - * Load `scriptSrc` only once (avoid multiple