diff --git a/build/azure-pipelines/cli/install-rust-posix.yml b/build/azure-pipelines/cli/install-rust-posix.yml index ee1e26f3505..d6022a44f7a 100644 --- a/build/azure-pipelines/cli/install-rust-posix.yml +++ b/build/azure-pipelines/cli/install-rust-posix.yml @@ -1,7 +1,7 @@ parameters: - name: channel type: string - default: 1.65.0 + default: 1.71.0 - name: targets default: [] type: object diff --git a/build/azure-pipelines/cli/install-rust-win32.yml b/build/azure-pipelines/cli/install-rust-win32.yml index 62ff0ce4ec5..373c41dd95f 100644 --- a/build/azure-pipelines/cli/install-rust-win32.yml +++ b/build/azure-pipelines/cli/install-rust-win32.yml @@ -1,7 +1,7 @@ parameters: - name: channel type: string - default: 1.65.0 + default: 1.71.0 - name: targets default: [] type: object diff --git a/cli/Cargo.lock b/cli/Cargo.lock index 8e6e477991f..fcbb3fee49f 100644 --- a/cli/Cargo.lock +++ b/cli/Cargo.lock @@ -183,17 +183,6 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3" -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi 0.1.19", - "libc", - "winapi", -] - [[package]] name = "autocfg" version = "1.1.0" @@ -301,11 +290,8 @@ checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" dependencies = [ "android-tzdata", "iana-time-zone", - "js-sys", "num-traits", "serde", - "time 0.1.44", - "wasm-bindgen", "winapi", ] @@ -356,7 +342,6 @@ name = "code-cli" version = "0.1.0" dependencies = [ "async-trait", - "atty", "base64 0.21.2", "bytes", "cfg-if", @@ -536,9 +521,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.78" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f39818dcfc97d45b03953c1292efc4e80954e1583c4aa770bac1383e2310a4" +checksum = "e88abab2f5abbe4c56e8f1fb431b784d710b709888f35755a160e62e33fe38e8" dependencies = [ "cc", "cxxbridge-flags", @@ -548,9 +533,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.78" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e580d70777c116df50c390d1211993f62d40302881e54d4b79727acb83d0199" +checksum = "5c0c11acd0e63bae27dcd2afced407063312771212b7a823b4fd72d633be30fb" dependencies = [ "cc", "codespan-reporting", @@ -558,24 +543,24 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn 1.0.103", + "syn 2.0.18", ] [[package]] name = "cxxbridge-flags" -version = "1.0.78" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56a46460b88d1cec95112c8c363f0e2c39afdb237f60583b0b36343bf627ea9c" +checksum = "8d3816ed957c008ccd4728485511e3d9aaf7db419aa321e3d2c5a2f3411e36c8" [[package]] name = "cxxbridge-macro" -version = "1.0.78" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "747b608fecf06b0d72d440f27acc99288207324b793be2c17991839f3d4995ea" +checksum = "a26acccf6f445af85ea056362561a24ef56cdc15fcc685f03aec50b9c702cb6d" dependencies = [ "proc-macro2", "quote", - "syn 1.0.103", + "syn 2.0.18", ] [[package]] @@ -1069,23 +1054,23 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.51" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5a6ef98976b22b3b7f2f3a806f858cb862044cfa66805aa3ad84cb3d3b785ed" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "winapi", + "windows", ] [[package]] name = "iana-time-zone-haiku" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fde6edd6cef363e9359ed3c98ba64590ba9eecba2293eb5a723ab32aee8926aa" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" dependencies = [ "cxx", "cxx-build", @@ -1234,9 +1219,9 @@ checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" [[package]] name = "link-cplusplus" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369" +checksum = "9d240c6f7e1ba3a28b0249f774e6a9dd0175054b52dfbb61b16eb8505c3785c9" dependencies = [ "cc", ] @@ -1996,9 +1981,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "scratch" -version = "1.0.2" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898" +checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152" [[package]] name = "secret-service" @@ -2271,9 +2256,9 @@ dependencies = [ [[package]] name = "termcolor" -version = "1.1.3" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" dependencies = [ "winapi-util", ] @@ -2298,17 +2283,6 @@ dependencies = [ "syn 2.0.18", ] -[[package]] -name = "time" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - [[package]] name = "time" version = "0.3.21" @@ -2649,12 +2623,6 @@ version = "0.9.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -2790,6 +2758,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.0", +] + [[package]] name = "windows-sys" version = "0.36.1" @@ -3100,7 +3077,7 @@ dependencies = [ "crc32fast", "crossbeam-utils", "flate2", - "time 0.3.21", + "time", ] [[package]] diff --git a/cli/Cargo.toml b/cli/Cargo.toml index d685d9efcbb..8aad5fea7b8 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -29,10 +29,9 @@ rmp-serde = "1.1.1" uuid = { version = "1.3.3", features = ["serde", "v4"] } dirs = "5.0.1" rand = "0.8.5" -atty = "0.2.14" opentelemetry = { version = "0.19.0", features = ["rt-tokio"] } serde_bytes = "0.11.9" -chrono = { version = "0.4.26", features = ["serde"] } +chrono = { version = "0.4.26", features = ["serde", "std", "clock"], default-features = false } gethostname = "0.4.3" libc = "0.2.144" tunnels = { git = "https://github.com/microsoft/dev-tunnels", rev = "2621784a9ad72aa39500372391332a14bad581a3", default-features = false, features = ["connections"] } diff --git a/cli/src/commands/tunnels.rs b/cli/src/commands/tunnels.rs index 95f9b12a3d4..85bfecc50a5 100644 --- a/cli/src/commands/tunnels.rs +++ b/cli/src/commands/tunnels.rs @@ -465,20 +465,17 @@ pub async fn forward( // #region singleton handler let auth = Auth::new(&ctx.paths, ctx.log.clone()); - println!("preauth {:?}", forward_args.login); if let (Some(p), Some(at)) = ( forward_args.login.provider.take(), forward_args.login.access_token.take(), ) { auth.login(Some(p.into()), Some(at)).await?; } - println!("auth done"); let mut tunnels = DevTunnels::new_port_forwarding(&ctx.log, auth, &ctx.paths); let tunnel = tunnels .start_new_launcher_tunnel(None, true, &forward_args.ports) .await?; - println!("made tunnel"); forwarding::server(ctx.log, tunnel, server, own_ports_rx, shutdown).await?; diff --git a/cli/src/constants.rs b/cli/src/constants.rs index 2dac5d43563..19ae4c82600 100644 --- a/cli/src/constants.rs +++ b/cli/src/constants.rs @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -use std::collections::HashMap; +use std::{collections::HashMap, io::IsTerminal}; use const_format::concatcp; use lazy_static::lazy_static; @@ -119,7 +119,7 @@ lazy_static! { option_env!("VSCODE_CLI_SERVER_NAME_MAP").and_then(|s| serde_json::from_str(s).unwrap()); /// Whether i/o interactions are allowed in the current CLI. - pub static ref IS_A_TTY: bool = atty::is(atty::Stream::Stdin); + pub static ref IS_A_TTY: bool = std::io::stdin().is_terminal(); /// Whether i/o interactions are allowed in the current CLI. pub static ref COLORS_ENABLED: bool = *IS_A_TTY && std::env::var(NO_COLOR_ENV).is_err(); diff --git a/cli/src/tunnels/code_server.rs b/cli/src/tunnels/code_server.rs index 7c044fc70cb..5bc5e39514a 100644 --- a/cli/src/tunnels/code_server.rs +++ b/cli/src/tunnels/code_server.rs @@ -541,6 +541,15 @@ impl<'a> ServerBuilder<'a> { debug!(self.logger, "Starting server with command... {:?}", cmd); + // On Windows spawning a code-server binary will run cmd.exe /c C:\path\to\code-server.cmd... + // This spawns a cmd.exe window for the user, which if they close will kill the code-server process + // and disconnect the tunnel. To prevent this, pass the CREATE_NO_WINDOW flag to the Command + // only on Windows. + // Original issue: https://github.com/microsoft/vscode/issues/184058 + // Partial fix: https://github.com/microsoft/vscode/pull/184621 + #[cfg(target_os = "windows")] + let cmd = cmd.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW); + let child = cmd .stderr(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) diff --git a/extensions/markdown-language-features/media/markdown.css b/extensions/markdown-language-features/media/markdown.css index 896531fdcc8..939eafc442b 100644 --- a/extensions/markdown-language-features/media/markdown.css +++ b/extensions/markdown-language-features/media/markdown.css @@ -200,10 +200,11 @@ table > tbody > tr + tr > td { } blockquote { - margin: 0 7px 0 5px; - padding: 0 16px 0 10px; + margin: 0; + padding: 2px 16px 0 10px; border-left-width: 5px; border-left-style: solid; + border-radius: 2px; } code { @@ -232,6 +233,7 @@ pre code { pre { background-color: var(--vscode-textCodeBlock-background); + border: 1px solid var(--vscode-widget-border); } .vscode-high-contrast h1 { diff --git a/extensions/theme-defaults/themes/dark_modern.json b/extensions/theme-defaults/themes/dark_modern.json index 3a8ba9a1ade..a4ff77a31e3 100644 --- a/extensions/theme-defaults/themes/dark_modern.json +++ b/extensions/theme-defaults/themes/dark_modern.json @@ -3,130 +3,123 @@ "name": "Default Dark Modern", "include": "./dark_plus.json", "colors": { - "activityBar.activeBorder": "#0078d4", + "activityBar.activeBorder": "#0078D4", "activityBar.background": "#181818", - "activityBar.border": "#ffffff15", - "activityBar.foreground": "#d7d7d7", - "activityBar.inactiveForeground": "#ffffff80", - "activityBarBadge.background": "#0078d4", - "activityBarBadge.foreground": "#ffffff", - "badge.background": "#4D4D4D", - "badge.foreground": "#f2f2f2", - "button.background": "#0078d4", - "button.border": "#ffffff12", - "button.foreground": "#ffffff", - "button.hoverBackground": "#0078d4e6", - "button.secondaryBackground": "#FFFFFF0F", - "button.secondaryForeground": "#cccccc", - "button.secondaryHoverBackground": "#ffffff15", + "activityBar.border": "#2B2B2B", + "activityBar.foreground": "#D7D7D7", + "activityBar.inactiveForeground": "#868686", + "activityBarBadge.background": "#0078D4", + "activityBarBadge.foreground": "#FFFFFF", + "badge.background": "#616161", + "badge.foreground": "#F8F8F8", + "button.background": "#0078D4", + "button.border": "#FFFFFF12", + "button.foreground": "#FFFFFF", + "button.hoverBackground": "#026EC1", + "button.secondaryBackground": "#1F1F1F", + "button.secondaryForeground": "#CCCCCC", + "button.secondaryHoverBackground": "#2B2B2B", "checkbox.background": "#313131", - "checkbox.border": "#ffffff1f", + "checkbox.border": "#3C3C3C", "debugToolBar.background": "#181818", - "descriptionForeground": "#8b949e", + "descriptionForeground": "#9D9D9D", "dropdown.background": "#313131", - "dropdown.border": "#ffffff1f", - "dropdown.foreground": "#cccccc", - "dropdown.listBackground": "#1f1f1f", - "editor.background": "#1f1f1f", - "editor.findMatchBackground": "#9e6a03", - "editor.foreground": "#cccccc", - "editorGroup.border": "#ffffff17", + "dropdown.border": "#3C3C3C", + "dropdown.foreground": "#CCCCCC", + "dropdown.listBackground": "#1F1F1F", + "editor.background": "#1F1F1F", + "editor.findMatchBackground": "#9E6A03", + "editor.foreground": "#CCCCCC", + "editorGroup.border": "#FFFFFF17", "editorGroupHeader.tabsBackground": "#181818", - "editorGroupHeader.tabsBorder": "#ffffff15", - "editorGutter.addedBackground": "#2ea043", - "editorGutter.deletedBackground": "#f85149", - "editorGutter.modifiedBackground": "#0078d4", - "editorLineNumber.activeForeground": "#cccccc", - "editorLineNumber.foreground": "#6e7681", + "editorGroupHeader.tabsBorder": "#2B2B2B", + "editorGutter.addedBackground": "#2EA043", + "editorGutter.deletedBackground": "#F85149", + "editorGutter.modifiedBackground": "#0078D4", + "editorLineNumber.activeForeground": "#CCCCCC", + "editorLineNumber.foreground": "#6E7681", "editorOverviewRuler.border": "#010409", - "editorWidget.background": "#1f1f1f", - "errorForeground": "#f85149", - "focusBorder": "#0078d4", - "foreground": "#cccccc", - "icon.foreground": "#cccccc", - "input.background": "#2a2a2a", - "input.border": "#ffffff1f", - "input.foreground": "#cccccc", - "input.placeholderForeground": "#ffffff79", - "inputOption.activeBackground": "#2489db82", - "inputOption.activeBorder": "#2488db", - "keybindingLabel.foreground": "#cccccc", - "list.activeSelectionBackground": "#323232", - "list.activeSelectionIconForeground": "#ffffff", - "list.activeSelectionForeground": "#ffffff", - "menu.background": "#1f1f1f", - "notificationCenterHeader.background": "#1f1f1f", - "notificationCenterHeader.foreground": "#cccccc", - "notifications.background": "#1f1f1f", - "notifications.border": "#ffffff15", - "notifications.foreground": "#cccccc", + "editorWidget.background": "#2B2B2B", + "errorForeground": "#F85149", + "focusBorder": "#0078D4", + "foreground": "#CCCCCC", + "icon.foreground": "#CCCCCC", + "input.background": "#2A2A2A", + "input.border": "#3C3C3C", + "input.foreground": "#CCCCCC", + "input.placeholderForeground": "#9D9D9D", + "inputOption.activeBackground": "#2489DB82", + "inputOption.activeBorder": "#2488DB", + "keybindingLabel.foreground": "#CCCCCC", + "menu.background": "#1F1F1F", + "notificationCenterHeader.background": "#1F1F1F", + "notificationCenterHeader.foreground": "#CCCCCC", + "notifications.background": "#1F1F1F", + "notifications.border": "#2B2B2B", + "notifications.foreground": "#CCCCCC", "panel.background": "#181818", - "panel.border": "#ffffff15", - "panelInput.border": "#ffffff15", - "panelTitle.activeBorder": "#0078d4", - "panelTitle.activeForeground": "#cccccc", - "panelTitle.inactiveForeground": "#8b949e", - "peekViewEditor.background": "#1f1f1f", - "peekViewEditor.matchHighlightBackground": "#bb800966", - "peekViewResult.background": "#1f1f1f", - "peekViewResult.matchHighlightBackground": "#bb800966", - "pickerGroup.border": "#ffffff15", - "pickerGroup.foreground": "#8b949e", - "progressBar.background": "#0078d4", - "quickInput.background": "#1f1f1f", - "quickInput.foreground": "#cccccc", - "scrollbar.shadow": "#484f5833", - "scrollbarSlider.activeBackground": "#6e768187", - "scrollbarSlider.background": "#6e768133", - "scrollbarSlider.hoverBackground": "#6e768145", + "panel.border": "#2B2B2B", + "panelInput.border": "#2B2B2B", + "panelTitle.activeBorder": "#0078D4", + "panelTitle.activeForeground": "#CCCCCC", + "panelTitle.inactiveForeground": "#9D9D9D", + "peekViewEditor.background": "#1F1F1F", + "peekViewEditor.matchHighlightBackground": "#BB800966", + "peekViewResult.background": "#1F1F1F", + "peekViewResult.matchHighlightBackground": "#BB800966", + "pickerGroup.border": "#2B2B2B", + "pickerGroup.foreground": "#8B949E", + "progressBar.background": "#0078D4", + "quickInput.background": "#2A2A2A", + "quickInput.foreground": "#CCCCCC", "settings.dropdownBackground": "#313131", - "settings.dropdownBorder": "#ffffff1f", - "settings.headerForeground": "#ffffff", - "settings.modifiedItemIndicator": "#bb800966", + "settings.dropdownBorder": "#3C3C3C", + "settings.headerForeground": "#FFFFFF", + "settings.modifiedItemIndicator": "#BB800966", "sideBar.background": "#181818", - "sideBar.border": "#ffffff15", - "sideBar.foreground": "#cccccc", + "sideBar.border": "#2B2B2B", + "sideBar.foreground": "#CCCCCC", "sideBarSectionHeader.background": "#181818", - "sideBarSectionHeader.border": "#ffffff15", - "sideBarSectionHeader.foreground": "#cccccc", - "sideBarTitle.foreground": "#cccccc", + "sideBarSectionHeader.border": "#2B2B2B", + "sideBarSectionHeader.foreground": "#CCCCCC", + "sideBarTitle.foreground": "#CCCCCC", "statusBar.background": "#181818", - "statusBar.border": "#ffffff15", - "statusBar.debuggingBackground": "#0078d4", - "statusBar.debuggingForeground": "#ffffff", - "statusBar.focusBorder": "#0078d4", - "statusBar.foreground": "#cccccc", - "statusBar.noFolderBackground": "#1f1f1f", - "statusBarItem.focusBorder": "#0078d4", - "statusBarItem.prominentBackground": "#6e768166", - "statusBarItem.remoteBackground": "#0078d4", - "statusBarItem.remoteForeground": "#ffffff", - "tab.activeBackground": "#1f1f1f", - "tab.activeBorder": "#1f1f1f", - "tab.activeBorderTop": "#0078d4", - "tab.activeForeground": "#ffffff", - "tab.border": "#ffffff15", - "tab.hoverBackground": "#1f1f1f", + "statusBar.border": "#2B2B2B", + "statusBar.debuggingBackground": "#0078D4", + "statusBar.debuggingForeground": "#FFFFFF", + "statusBar.focusBorder": "#0078D4", + "statusBar.foreground": "#CCCCCC", + "statusBar.noFolderBackground": "#1F1F1F", + "statusBarItem.focusBorder": "#0078D4", + "statusBarItem.prominentBackground": "#6E768166", + "statusBarItem.remoteBackground": "#0078D4", + "statusBarItem.remoteForeground": "#FFFFFF", + "tab.activeBackground": "#1F1F1F", + "tab.activeBorder": "#1F1F1F", + "tab.activeBorderTop": "#0078D4", + "tab.activeForeground": "#FFFFFF", + "tab.border": "#2B2B2B", + "tab.hoverBackground": "#1F1F1F", "tab.inactiveBackground": "#181818", - "tab.inactiveForeground": "#ffffff80", - "tab.unfocusedActiveBorder": "#1f1f1f", - "tab.unfocusedActiveBorderTop": "#ffffff15", - "tab.unfocusedHoverBackground": "#6e76811a", - "terminal.foreground": "#cccccc", - "terminal.tab.activeBorder": "#0078d4", - "textBlockQuote.background": "#010409", - "textBlockQuote.border": "#ffffff14", - "textCodeBlock.background": "#6e768166", + "tab.inactiveForeground": "#9D9D9D", + "tab.unfocusedActiveBorder": "#1F1F1F", + "tab.unfocusedActiveBorderTop": "#2B2B2B", + "tab.unfocusedHoverBackground": "#1F1F1F", + "terminal.foreground": "#CCCCCC", + "terminal.tab.activeBorder": "#0078D4", + "textBlockQuote.background": "#2B2B2B", + "textBlockQuote.border": "#616161", + "textCodeBlock.background": "#2B2B2B", "textLink.activeForeground": "#40A6FF", "textLink.foreground": "#40A6FF", - "textSeparator.foreground": "#21262d", + "textSeparator.foreground": "#21262D", "titleBar.activeBackground": "#181818", - "titleBar.activeForeground": "#cccccc", - "titleBar.border": "#ffffff15", - "titleBar.inactiveBackground": "#1f1f1f", - "titleBar.inactiveForeground": "#8b949e", - "welcomePage.tileBackground": "#ffffff0f", - "welcomePage.progress.foreground": "#0078d4", - "widget.border": "#ffffff15", + "titleBar.activeForeground": "#CCCCCC", + "titleBar.border": "#2B2B2B", + "titleBar.inactiveBackground": "#1F1F1F", + "titleBar.inactiveForeground": "#9D9D9D", + "welcomePage.tileBackground": "#2B2B2B", + "welcomePage.progress.foreground": "#0078D4", + "widget.border": "#333333", }, } diff --git a/extensions/theme-defaults/themes/light_modern.json b/extensions/theme-defaults/themes/light_modern.json index f5c07d9a858..2fb9a7aee79 100644 --- a/extensions/theme-defaults/themes/light_modern.json +++ b/extensions/theme-defaults/themes/light_modern.json @@ -4,141 +4,138 @@ "include": "./light_plus.json", "colors": { "activityBar.activeBorder": "#005FB8", - "activityBar.background": "#f8f8f8", - "activityBar.border": "#0000001a", - "activityBar.foreground": "#1f1f1f", - "activityBar.inactiveForeground": "#0000009e", + "activityBar.background": "#F8F8F8", + "activityBar.border": "#E5E5E5", + "activityBar.foreground": "#1F1F1F", + "activityBar.inactiveForeground": "#616161", "activityBarBadge.background": "#005FB8", - "activityBarBadge.foreground": "#ffffff", + "activityBarBadge.foreground": "#FFFFFF", "badge.background": "#CCCCCC", - "badge.foreground": "#3b3b3b", + "badge.foreground": "#3B3B3B", "button.background": "#005FB8", "button.border": "#0000001a", - "button.foreground": "#ffffff", - "button.hoverBackground": "#005FB8e6", - "button.secondaryBackground": "#0000001a", - "button.secondaryForeground": "#3b3b3b", - "button.secondaryHoverBackground": "#00000034", - "checkbox.background": "#f8f8f8", + "button.foreground": "#FFFFFF", + "button.hoverBackground": "#0258A8", + "button.secondaryBackground": "#E5E5E5", + "button.secondaryForeground": "#3B3B3B", + "button.secondaryHoverBackground": "#CBCBCB", + "checkbox.background": "#F8F8F8", "checkbox.border": "#CECECE", - "descriptionForeground": "#3b3b3b", - "dropdown.background": "#ffffff", + "descriptionForeground": "#3B3B3B", + "dropdown.background": "#FFFFFF", "dropdown.border": "#CECECE", - "dropdown.foreground": "#3b3b3b", - "dropdown.listBackground": "#ffffff", - "editor.background": "#ffffff", - "editor.foreground": "#3b3b3b", + "dropdown.foreground": "#3B3B3B", + "dropdown.listBackground": "#FFFFFF", + "editor.background": "#FFFFFF", + "editor.foreground": "#3B3B3B", "editor.inactiveSelectionBackground": "#E5EBF1", "editor.selectionHighlightBackground": "#ADD6FF80", - "editorGroup.border": "#0000001a", - "editorGroupHeader.tabsBackground": "#f8f8f8", - "editorGroupHeader.tabsBorder": "#0000001a", - "editorGutter.addedBackground": "#2ea043", - "editorGutter.deletedBackground": "#f85149", + "editorGroup.border": "#E5E5E5", + "editorGroupHeader.tabsBackground": "#F8F8F8", + "editorGroupHeader.tabsBorder": "#E5E5E5", + "editorGutter.addedBackground": "#2EA043", + "editorGutter.deletedBackground": "#F85149", "editorGutter.modifiedBackground": "#005FB8", "editorIndentGuide.background": "#D3D3D3", "editorLineNumber.activeForeground": "#171184", - "editorLineNumber.foreground": "#6e7681", - "editorOverviewRuler.border": "#0000001a", - "editorSuggestWidget.background": "#f8f8f8", - "editorWidget.background": "#ffffff", - "errorForeground": "#f85149", + "editorLineNumber.foreground": "#6E7681", + "editorOverviewRuler.border": "#E5E5E5", + "editorSuggestWidget.background": "#F8F8F8", + "editorWidget.background": "#F8F8F8", + "errorForeground": "#F85149", "focusBorder": "#005FB8", - "foreground": "#3b3b3b", - "icon.foreground": "#3b3b3b", - "input.background": "#ffffff", + "foreground": "#3B3B3B", + "icon.foreground": "#3B3B3B", + "input.background": "#FFFFFF", "input.border": "#CECECE", - "input.foreground": "#3b3b3b", - "input.placeholderForeground": "#00000079", - "inputOption.activeBackground": "#005fb841", + "input.foreground": "#3B3B3B", + "input.placeholderForeground": "#868686", + "inputOption.activeBackground": "#BED6ED", "inputOption.activeBorder": "#005FB8", "inputOption.activeForeground": "#000000", - "keybindingLabel.foreground": "#3b3b3b", - "list.activeSelectionBackground": "#e8e8e8", + "keybindingLabel.foreground": "#3B3B3B", + "list.activeSelectionBackground": "#E8E8E8", "list.activeSelectionForeground": "#000000", "list.activeSelectionIconForeground": "#000000", - "list.hoverBackground": "#f2f2f2", - "menu.border": "#D4D4D4", - "notebook.cellBorderColor": "#E8E8E8", - "notebook.selectedCellBackground": "#c8ddf150", - "notificationCenterHeader.background": "#ffffff", - "notificationCenterHeader.foreground": "#3b3b3b", - "notifications.background": "#ffffff", - "notifications.border": "#0000001a", - "notifications.foreground": "#3b3b3b", - "panel.background": "#f8f8f8", - "panel.border": "#0000001a", - "panelInput.border": "#0000001a", + "list.hoverBackground": "#F2F2F2", + "list.focusAndSelectionOutline": "#005FB8", + "menu.border": "#CECECE", + "notebook.cellBorderColor": "#E5E5E5", + "notebook.selectedCellBackground": "#C8DDF150", + "notificationCenterHeader.background": "#FFFFFF", + "notificationCenterHeader.foreground": "#3B3B3B", + "notifications.background": "#FFFFFF", + "notifications.border": "#E5E5E5", + "notifications.foreground": "#3B3B3B", + "panel.background": "#F8F8F8", + "panel.border": "#E5E5E5", + "panelInput.border": "#E5E5E5", "panelTitle.activeBorder": "#005FB8", - "panelTitle.activeForeground": "#3b3b3b", - "panelTitle.inactiveForeground": "#3b3b3b", - "peekViewEditor.matchHighlightBackground": "#bb800966", - "peekViewResult.background": "#ffffff", - "peekViewResult.matchHighlightBackground": "#bb800966", - "pickerGroup.border": "#0000001a", - "pickerGroup.foreground": "#8b949e", + "panelTitle.activeForeground": "#3B3B3B", + "panelTitle.inactiveForeground": "#3B3B3B", + "peekViewEditor.matchHighlightBackground": "#BB800966", + "peekViewResult.background": "#FFFFFF", + "peekViewResult.matchHighlightBackground": "#BB800966", + "pickerGroup.border": "#E5E5E5", + "pickerGroup.foreground": "#8B949E", "ports.iconRunningProcessForeground": "#369432", "progressBar.background": "#005FB8", - "quickInput.background": "#ffffff", - "quickInput.foreground": "#3b3b3b", - "scrollbar.shadow": "#484f5833", - "scrollbarSlider.activeBackground": "#6e768187", - "scrollbarSlider.background": "#6e768133", - "scrollbarSlider.hoverBackground": "#6e768145", + "quickInput.background": "#F8F8F8", + "quickInput.foreground": "#3B3B3B", "searchEditor.textInputBorder": "#CECECE", - "settings.dropdownBackground": "#ffffff", + "settings.dropdownBackground": "#FFFFFF", "settings.dropdownBorder": "#CECECE", - "settings.headerForeground": "#1f1f1f", - "settings.modifiedItemIndicator": "#bb800966", + "settings.headerForeground": "#1F1F1F", + "settings.modifiedItemIndicator": "#BB800966", "settings.numberInputBorder": "#CECECE", "settings.textInputBorder": "#CECECE", - "sideBar.background": "#f8f8f8", - "sideBar.border": "#0000001a", - "sideBar.foreground": "#3b3b3b", - "sideBarSectionHeader.background": "#f8f8f8", - "sideBarSectionHeader.border": "#0000001a", - "sideBarSectionHeader.foreground": "#3b3b3b", - "sideBarTitle.foreground": "#3b3b3b", - "statusBar.background": "#f8f8f8", - "statusBar.foreground": "#3b3b3b", - "statusBar.border": "#0000001a", - "statusBar.debuggingBackground": "#fd716c", + "sideBar.background": "#F8F8F8", + "sideBar.border": "#E5E5E5", + "sideBar.foreground": "#3B3B3B", + "sideBarSectionHeader.background": "#F8F8F8", + "sideBarSectionHeader.border": "#E5E5E5", + "sideBarSectionHeader.foreground": "#3B3B3B", + "sideBarTitle.foreground": "#3B3B3B", + "statusBar.background": "#F8F8F8", + "statusBar.foreground": "#3B3B3B", + "statusBar.border": "#E5E5E5", + "statusBar.debuggingBackground": "#FD716C", "statusBar.debuggingForeground": "#000000", "statusBar.focusBorder": "#005FB8", - "statusBar.noFolderBackground": "#f8f8f8", - "statusBarItem.errorBackground": "#c72e0f", + "statusBar.noFolderBackground": "#F8F8F8", + "statusBarItem.errorBackground": "#C72E0F", "statusBarItem.focusBorder": "#005FB8", - "statusBarItem.prominentBackground": "#6e768166", + "statusBarItem.prominentBackground": "#6E768166", "statusBarItem.remoteBackground": "#005FB8", - "statusBarItem.remoteForeground": "#ffffff", - "tab.activeBackground": "#ffffff", - "tab.activeBorder": "#f8f8f8", + "statusBarItem.remoteForeground": "#FFFFFF", + "tab.activeBackground": "#FFFFFF", + "tab.activeBorder": "#F8F8F8", "tab.activeBorderTop": "#005FB8", - "tab.activeForeground": "#3b3b3b", - "tab.border": "#0000001a", - "tab.hoverBackground": "#ffffff", - "tab.inactiveBackground": "#f8f8f8", - "tab.inactiveForeground": "#00000080", - "tab.lastPinnedBorder": "#3b3b3b30", - "tab.unfocusedActiveBorder": "#f8f8f8", - "tab.unfocusedActiveBorderTop": "#00000015", - "tab.unfocusedHoverBackground": "#6e76811a", + "tab.activeForeground": "#3B3B3B", + "tab.border": "#E5E5E5", + "tab.hoverBackground": "#FFFFFF", + "tab.inactiveBackground": "#F8F8F8", + "tab.inactiveForeground": "#868686", + "tab.lastPinnedBorder": "#D4D4D4", + "tab.unfocusedActiveBorder": "#F8F8F8", + "tab.unfocusedActiveBorderTop": "#E5E5E5", + "tab.unfocusedHoverBackground": "#F8F8F8", "terminalCursor.foreground": "#005FB8", - "terminal.foreground": "#3b3b3b", + "terminal.foreground": "#3B3B3B", "terminal.inactiveSelectionBackground": "#E5EBF1", - "terminal.tab.activeBorder": "#005fb8", - "textBlockQuote.background": "#f8f8f8", - "textBlockQuote.border": "#0000001a", - "textCodeBlock.background": "#f2f2f2", + "terminal.tab.activeBorder": "#005FB8", + "textBlockQuote.background": "#F8F8F8", + "textBlockQuote.border": "#E5E5E5", + "textCodeBlock.background": "#F8F8F8", "textLink.activeForeground": "#005FB8", "textLink.foreground": "#005FB8", - "textSeparator.foreground": "#21262d", - "titleBar.activeBackground": "#f8f8f8", - "titleBar.activeForeground": "#1e1e1e", - "titleBar.border": "#0000001a", - "titleBar.inactiveBackground": "#f8f8f8", - "titleBar.inactiveForeground": "#8b949e", - "welcomePage.tileBackground": "#f3f3f3", - "widget.border": "#0000001a", + "textSeparator.foreground": "#21262D", + "titleBar.activeBackground": "#F8F8F8", + "titleBar.activeForeground": "#1E1E1E", + "titleBar.border": "#E5E5E5", + "titleBar.inactiveBackground": "#F8F8F8", + "titleBar.inactiveForeground": "#8B949E", + "welcomePage.tileBackground": "#F3F3F3", + "widget.border": "#E5E5E5" }, } diff --git a/src/vs/base/common/arrays.ts b/src/vs/base/common/arrays.ts index 90261beed6d..a54c2c59f9c 100644 --- a/src/vs/base/common/arrays.ts +++ b/src/vs/base/common/arrays.ts @@ -707,6 +707,10 @@ export function tieBreakComparators(...comparators: Comparator[]): */ export const numberComparator: Comparator = (a, b) => a - b; +export function reverseOrder(comparator: Comparator): Comparator { + return (a, b) => -comparator(a, b); +} + /** * Returns the first item that is equal to or greater than every other item. */ diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/accessibleDiffViewer.ts b/src/vs/editor/browser/widget/diffEditorWidget2/accessibleDiffViewer.ts index 315ee1856ea..d66333e1aec 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget2/accessibleDiffViewer.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget2/accessibleDiffViewer.ts @@ -142,9 +142,9 @@ class ViewModel extends Disposable { /** @description play audio-cue for diff */ const currentViewItem = this.currentElement.read(reader); if (currentViewItem?.type === LineType.Deleted) { - this._audioCueService.playAudioCue(AudioCue.diffLineDeleted); + this._audioCueService.playAudioCue(AudioCue.diffLineDeleted, { source: 'accessibleDiffViewer.currentElementChanged' }); } else if (currentViewItem?.type === LineType.Added) { - this._audioCueService.playAudioCue(AudioCue.diffLineInserted); + this._audioCueService.playAudioCue(AudioCue.diffLineInserted, { source: 'accessibleDiffViewer.currentElementChanged' }); } })); diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.ts b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.ts index e2612415c32..ed02f28c188 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorWidget2.ts @@ -255,11 +255,11 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { if (e?.reason === CursorChangeReason.Explicit) { const diff = this._diffModel.get()?.diff.get()?.mappings.find(m => m.lineRangeMapping.modifiedRange.contains(e.position.lineNumber)); if (diff?.lineRangeMapping.modifiedRange.isEmpty) { - this._audioCueService.playAudioCue(AudioCue.diffLineDeleted); + this._audioCueService.playAudioCue(AudioCue.diffLineDeleted, { source: 'diffEditor.cursorPositionChanged' }); } else if (diff?.lineRangeMapping.originalRange.isEmpty) { - this._audioCueService.playAudioCue(AudioCue.diffLineInserted); + this._audioCueService.playAudioCue(AudioCue.diffLineInserted, { source: 'diffEditor.cursorPositionChanged' }); } else if (diff) { - this._audioCueService.playAudioCue(AudioCue.diffLineModified); + this._audioCueService.playAudioCue(AudioCue.diffLineModified, { source: 'diffEditor.cursorPositionChanged' }); } } })); @@ -460,11 +460,11 @@ export class DiffEditorWidget2 extends DelegatingEditor implements IDiffEditor { this._goTo(diff); if (diff.lineRangeMapping.modifiedRange.isEmpty) { - this._audioCueService.playAudioCue(AudioCue.diffLineDeleted); + this._audioCueService.playAudioCue(AudioCue.diffLineDeleted, { source: 'diffEditor.goToDiff' }); } else if (diff.lineRangeMapping.originalRange.isEmpty) { - this._audioCueService.playAudioCue(AudioCue.diffLineInserted); + this._audioCueService.playAudioCue(AudioCue.diffLineInserted, { source: 'diffEditor.goToDiff' }); } else if (diff) { - this._audioCueService.playAudioCue(AudioCue.diffLineModified); + this._audioCueService.playAudioCue(AudioCue.diffLineModified, { source: 'diffEditor.goToDiff' }); } } diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/lineAlignment.ts b/src/vs/editor/browser/widget/diffEditorWidget2/lineAlignment.ts index 9948632791e..4111988779b 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget2/lineAlignment.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget2/lineAlignment.ts @@ -286,7 +286,7 @@ export class ViewZoneManager extends Disposable { for (const a of alignmentsSyncedMovedText.read(reader) ?? []) { if (!syncedMovedText?.lineRangeMapping.original.intersect(a.originalRange) - && !syncedMovedText?.lineRangeMapping.modified.intersect(a.modifiedRange)) { + || !syncedMovedText?.lineRangeMapping.modified.intersect(a.modifiedRange)) { // ignore unrelated alignments outside the synced moved text continue; } diff --git a/src/vs/editor/browser/widget/diffNavigator.ts b/src/vs/editor/browser/widget/diffNavigator.ts index 0b221d7c7a4..0f34a1e5960 100644 --- a/src/vs/editor/browser/widget/diffNavigator.ts +++ b/src/vs/editor/browser/widget/diffNavigator.ts @@ -224,11 +224,11 @@ export class DiffNavigator extends Disposable implements IDiffNavigator { } const insertedOrModified = modifiedEditor.getLineDecorations(lineNumber).find(l => l.options.className === 'line-insert'); if (insertedOrModified) { - this._audioCueService.playAudioCue(AudioCue.diffLineModified, true); + this._audioCueService.playAudioCue(AudioCue.diffLineModified, { allowManyInParallel: true }); } else if (jumpToChange) { // The modified editor does not include deleted lines, but when // we are moved to the area where lines were deleted, play this cue - this._audioCueService.playAudioCue(AudioCue.diffLineDeleted, true); + this._audioCueService.playAudioCue(AudioCue.diffLineDeleted, { allowManyInParallel: true }); } else { return; } diff --git a/src/vs/editor/browser/widget/diffReview.ts b/src/vs/editor/browser/widget/diffReview.ts index 66ec0c678e1..ea813d85aa5 100644 --- a/src/vs/editor/browser/widget/diffReview.ts +++ b/src/vs/editor/browser/widget/diffReview.ts @@ -325,9 +325,9 @@ export class DiffReview extends Disposable { } const element = !type ? current : type === 'next' ? current?.nextElementSibling : current?.previousElementSibling; if (element?.classList.contains(DiffEditorLineClasses.Insert)) { - this._audioCueService.playAudioCue(AudioCue.diffLineInserted, true); + this._audioCueService.playAudioCue(AudioCue.diffLineInserted, { allowManyInParallel: true }); } else if (element?.classList.contains(DiffEditorLineClasses.Delete)) { - this._audioCueService.playAudioCue(AudioCue.diffLineDeleted, true); + this._audioCueService.playAudioCue(AudioCue.diffLineDeleted, { allowManyInParallel: true }); } this.scrollbar.scanDomNode(); } diff --git a/src/vs/editor/common/diff/standardLinesDiffComputer.ts b/src/vs/editor/common/diff/standardLinesDiffComputer.ts index 4fe0f30e746..79d20885d8a 100644 --- a/src/vs/editor/common/diff/standardLinesDiffComputer.ts +++ b/src/vs/editor/common/diff/standardLinesDiffComputer.ts @@ -3,8 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { compareBy, findLastIndex, numberComparator, reverseOrder } from 'vs/base/common/arrays'; import { assertFn, checkAdjacentItems } from 'vs/base/common/assert'; import { CharCode } from 'vs/base/common/charCode'; +import { SetMap } from 'vs/base/common/collections'; import { LineRange } from 'vs/editor/common/core/lineRange'; import { OffsetRange } from 'vs/editor/common/core/offsetRange'; import { Position } from 'vs/editor/common/core/position'; @@ -137,37 +139,9 @@ export class StandardLinesDiffComputer implements ILinesDiffComputer { const changes = lineRangeMappingFromRangeMappings(alignments, originalLines, modifiedLines); - const moves: MovedText[] = []; + let moves: MovedText[] = []; if (options.computeMoves) { - const deletions = changes - .filter(c => c.modifiedRange.isEmpty && c.originalRange.length >= 3) - .map(d => new LineRangeFragment(d.originalRange, originalLines)); - const insertions = new Set(changes - .filter(c => c.originalRange.isEmpty && c.modifiedRange.length >= 3) - .map(d => new LineRangeFragment(d.modifiedRange, modifiedLines))); - - for (const deletion of deletions) { - let highestSimilarity = -1; - let best: LineRangeFragment | undefined; - for (const insertion of insertions) { - const similarity = deletion.computeSimilarity(insertion); - if (similarity > highestSimilarity) { - highestSimilarity = similarity; - best = insertion; - } - } - - if (highestSimilarity > 0.90 && best) { - const moveChanges = this.refineDiff(originalLines, modifiedLines, new SequenceDiff( - new OffsetRange(deletion.range.startLineNumber - 1, deletion.range.endLineNumberExclusive - 1), - new OffsetRange(best.range.startLineNumber - 1, best.range.endLineNumberExclusive - 1), - ), timeout, considerWhitespaceChanges); - const mappings = lineRangeMappingFromRangeMappings(moveChanges.mappings, originalLines, modifiedLines, true); - - insertions.delete(best); - moves.push(new MovedText(new SimpleLineRangeMapping(deletion.range, best.range), mappings)); - } - } + moves = this.computeMoves(changes, originalLines, modifiedLines, srcDocLines, tgtDocLines, timeout, considerWhitespaceChanges); } // Make sure all ranges are valid @@ -202,6 +176,136 @@ export class StandardLinesDiffComputer implements ILinesDiffComputer { return new LinesDiff(changes, moves, hitTimeout); } + private computeMoves(changes: LineRangeMapping[], originalLines: string[], modifiedLines: string[], hashedOriginalLines: number[], hashedModifiedLines: number[], timeout: ITimeout, considerWhitespaceChanges: boolean): MovedText[] { + const moves: MovedText[] = []; + const deletions = changes + .filter(c => c.modifiedRange.isEmpty && c.originalRange.length >= 3) + .map(d => new LineRangeFragment(d.originalRange, originalLines, d)); + const insertions = new Set(changes + .filter(c => c.originalRange.isEmpty && c.modifiedRange.length >= 3) + .map(d => new LineRangeFragment(d.modifiedRange, modifiedLines, d))); + + const excludedChanges = new Set(); + + for (const deletion of deletions) { + let highestSimilarity = -1; + let best: LineRangeFragment | undefined; + for (const insertion of insertions) { + const similarity = deletion.computeSimilarity(insertion); + if (similarity > highestSimilarity) { + highestSimilarity = similarity; + best = insertion; + } + } + + if (highestSimilarity > 0.90 && best) { + const moveChanges = this.refineDiff(originalLines, modifiedLines, new SequenceDiff( + new OffsetRange(deletion.range.startLineNumber - 1, deletion.range.endLineNumberExclusive - 1), + new OffsetRange(best.range.startLineNumber - 1, best.range.endLineNumberExclusive - 1) + ), timeout, considerWhitespaceChanges); + const mappings = lineRangeMappingFromRangeMappings(moveChanges.mappings, originalLines, modifiedLines, true); + + insertions.delete(best); + moves.push(new MovedText(new SimpleLineRangeMapping(deletion.range, best.range), mappings)); + excludedChanges.add(deletion.source); + excludedChanges.add(best.source); + } + } + + const original3LineHashes = new SetMap(); + + for (const change of changes) { + if (excludedChanges.has(change)) { + continue; + } + + for (let i = change.originalRange.startLineNumber; i < change.originalRange.endLineNumberExclusive - 2; i++) { + const key = `${hashedOriginalLines[i - 1]}:${hashedOriginalLines[i + 1 - 1]}:${hashedOriginalLines[i + 2 - 1]}`; + original3LineHashes.add(key, { range: new LineRange(i, i + 3) }); + } + } + + interface PossibleMapping { + modifiedLineRange: LineRange; + originalLineRange: LineRange; + } + + const possibleMappings: PossibleMapping[] = []; + + changes.sort(compareBy(c => c.modifiedRange.startLineNumber, numberComparator)); + + for (const change of changes) { + if (excludedChanges.has(change)) { + continue; + } + + let lastMappings: PossibleMapping[] = []; + for (let i = change.modifiedRange.startLineNumber; i < change.modifiedRange.endLineNumberExclusive - 2; i++) { + const key = `${hashedModifiedLines[i - 1]}:${hashedModifiedLines[i + 1 - 1]}:${hashedModifiedLines[i + 2 - 1]}`; + + //const isWeakKey = (originalLines[i].trim().length + originalLines[i + 1].trim().length + originalLines[i + 2].trim().length < 20); + + const currentModifiedRange = new LineRange(i, i + 3); + + const nextMappings: PossibleMapping[] = []; + original3LineHashes.forEach(key, ({ range }) => { + for (const lastMapping of lastMappings) { + // does this match extend some last match? + if (lastMapping.originalLineRange.endLineNumberExclusive + 1 === range.endLineNumberExclusive && + lastMapping.modifiedLineRange.endLineNumberExclusive + 1 === currentModifiedRange.endLineNumberExclusive) { + lastMapping.originalLineRange = new LineRange(lastMapping.originalLineRange.startLineNumber, range.endLineNumberExclusive); + lastMapping.modifiedLineRange = new LineRange(lastMapping.modifiedLineRange.startLineNumber, currentModifiedRange.endLineNumberExclusive); + nextMappings.push(lastMapping); + return; + } + } + + const mapping: PossibleMapping = { + modifiedLineRange: currentModifiedRange, + originalLineRange: range, + }; + possibleMappings.push(mapping); + nextMappings.push(mapping); + }); + lastMappings = nextMappings; + } + } + + possibleMappings.sort(reverseOrder(compareBy(m => m.modifiedLineRange.length, numberComparator))); + + const modifiedSet = new LineRangeSet(); + const originalSet = new LineRangeSet(); + + for (const mapping of possibleMappings) { + + const diffOrigToMod = mapping.modifiedLineRange.startLineNumber - mapping.originalLineRange.startLineNumber; + const modifiedSections = modifiedSet.subtractFrom(mapping.modifiedLineRange); + const originalTranslatedSections = originalSet.subtractFrom(mapping.originalLineRange).map(r => r.delta(diffOrigToMod)); + + const modifiedIntersectedSections = intersectRanges(modifiedSections, originalTranslatedSections); + + for (const s of modifiedIntersectedSections) { + if (s.length < 3) { + continue; + } + const modifiedLineRange = s; + const originalLineRange = s.delta(-diffOrigToMod); + + const moveChanges = this.refineDiff(originalLines, modifiedLines, new SequenceDiff( + new OffsetRange(originalLineRange.startLineNumber - 1, originalLineRange.endLineNumberExclusive - 1), + new OffsetRange(modifiedLineRange.startLineNumber - 1, modifiedLineRange.endLineNumberExclusive - 1) + ), timeout, considerWhitespaceChanges); + const mappings = lineRangeMappingFromRangeMappings(moveChanges.mappings, originalLines, modifiedLines, true); + moves.push(new MovedText(new SimpleLineRangeMapping(originalLineRange, modifiedLineRange), mappings)); + + modifiedSet.addRange(modifiedLineRange); + originalSet.addRange(originalLineRange); + } + } + + return moves; + } + private refineDiff(originalLines: string[], modifiedLines: string[], diff: SequenceDiff, timeout: ITimeout, considerWhitespaceChanges: boolean): { mappings: RangeMapping[]; hitTimeout: boolean } { const slice1 = new LinesSliceCharSequence(originalLines, diff.seq1Range, considerWhitespaceChanges); const slice2 = new LinesSliceCharSequence(modifiedLines, diff.seq2Range, considerWhitespaceChanges); @@ -233,6 +337,100 @@ export class StandardLinesDiffComputer implements ILinesDiffComputer { } } +function intersectRanges(ranges1: LineRange[], ranges2: LineRange[]): LineRange[] { + const result: LineRange[] = []; + + let i1 = 0; + let i2 = 0; + while (i1 < ranges1.length && i2 < ranges2.length) { + const r1 = ranges1[i1]; + const r2 = ranges2[i2]; + + const i = r1.intersect(r2); + if (i && !i.isEmpty) { + result.push(i); + } + + if (r1.endLineNumberExclusive < r2.endLineNumberExclusive) { + i1++; + } else { + i2++; + } + } + + return result; +} + +// TODO make this fast +class LineRangeSet { + private readonly _normalizedRanges: LineRange[] = []; + + addRange(range: LineRange): void { + // Idea: Find joinRange such that: + // replaceRange = _normalizedRanges.replaceRange(joinRange, range.joinAll(joinRange.map(idx => this._normalizedRanges[idx]))) + + // idx of first element that touches range or that is after range + const joinRangeStartIdx = mapMinusOne(this._normalizedRanges.findIndex(r => r.endLineNumberExclusive >= range.startLineNumber), this._normalizedRanges.length); + // idx of element after { last element that touches range or that is before range } + const joinRangeEndIdxExclusive = findLastIndex(this._normalizedRanges, r => r.startLineNumber <= range.endLineNumberExclusive) + 1; + + if (joinRangeStartIdx === joinRangeEndIdxExclusive) { + // If there is no element that touches range, then joinRangeStartIdx === joinRangeEndIdxExclusive and that value is the index of the element after range + this._normalizedRanges.splice(joinRangeStartIdx, 0, range); + } else if (joinRangeStartIdx === joinRangeEndIdxExclusive - 1) { + // Else, there is an element that touches range and in this case it is both the first and last element. Thus we can replace it + const joinRange = this._normalizedRanges[joinRangeStartIdx]; + this._normalizedRanges[joinRangeStartIdx] = joinRange.join(range); + } else { + // First and last element are different - we need to replace the entire range + const joinRange = this._normalizedRanges[joinRangeStartIdx].join(this._normalizedRanges[joinRangeEndIdxExclusive - 1]).join(range); + this._normalizedRanges.splice(joinRangeStartIdx, joinRangeEndIdxExclusive - joinRangeStartIdx, joinRange); + } + } + + intersects(range: LineRange): boolean { + for (const r of this._normalizedRanges) { + if (r.intersectsStrict(range)) { + return true; + } + } + return false; + } + + /** + * Subtracts all ranges in this set from `range` and returns the result. + */ + subtractFrom(range: LineRange): LineRange[] { + // idx of first element that touches range or that is after range + const joinRangeStartIdx = mapMinusOne(this._normalizedRanges.findIndex(r => r.endLineNumberExclusive >= range.startLineNumber), this._normalizedRanges.length); + // idx of element after { last element that touches range or that is before range } + const joinRangeEndIdxExclusive = findLastIndex(this._normalizedRanges, r => r.startLineNumber <= range.endLineNumberExclusive) + 1; + + if (joinRangeStartIdx === joinRangeEndIdxExclusive) { + return [range]; + } + + const result: LineRange[] = []; + let startLineNumber = range.startLineNumber; + for (let i = joinRangeStartIdx; i < joinRangeEndIdxExclusive; i++) { + const r = this._normalizedRanges[i]; + if (r.startLineNumber > startLineNumber) { + result.push(new LineRange(startLineNumber, r.startLineNumber)); + } + startLineNumber = r.endLineNumberExclusive; + } + if (startLineNumber < range.endLineNumberExclusive) { + result.push(new LineRange(startLineNumber, range.endLineNumberExclusive)); + } + + return result; + } +} + +function mapMinusOne(idx: number, mapTo: number): number { + return idx === -1 ? mapTo : idx; +} + function coverFullWords(sequence1: LinesSliceCharSequence, sequence2: LinesSliceCharSequence, sequenceDiffs: SequenceDiff[]): SequenceDiff[] { const additional: SequenceDiff[] = []; @@ -676,6 +874,7 @@ class LineRangeFragment { constructor( public readonly range: LineRange, public readonly lines: string[], + public readonly source: LineRangeMapping, ) { let counter = 0; for (let i = range.startLineNumber - 1; i < range.endLineNumberExclusive - 1; i++) { diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index f026f61f400..e7297361013 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -1039,7 +1039,7 @@ class StandaloneContextMenuService extends ContextMenuService { class StandaloneAudioService implements IAudioCueService { _serviceBrand: undefined; - async playAudioCue(cue: AudioCue, allowManyInParallel?: boolean | undefined): Promise { + async playAudioCue(cue: AudioCue, options: {}): Promise { } async playAudioCues(cues: AudioCue[]): Promise { diff --git a/src/vs/platform/audioCues/browser/audioCueService.ts b/src/vs/platform/audioCues/browser/audioCueService.ts index a7b6d7507fd..bbf03de5613 100644 --- a/src/vs/platform/audioCues/browser/audioCueService.ts +++ b/src/vs/platform/audioCues/browser/audioCueService.ts @@ -11,13 +11,14 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { Event } from 'vs/base/common/event'; import { localize } from 'vs/nls'; import { observableFromEvent, derived } from 'vs/base/common/observable'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; export const IAudioCueService = createDecorator('audioCue'); export interface IAudioCueService { readonly _serviceBrand: undefined; - playAudioCue(cue: AudioCue, allowManyInParallel?: boolean): Promise; - playAudioCues(cues: AudioCue[]): Promise; + playAudioCue(cue: AudioCue, options?: IAudioCueOptions): Promise; + playAudioCues(cues: (AudioCue | { cue: AudioCue; source: string })[]): Promise; isEnabled(cue: AudioCue): boolean; onEnabledChanged(cue: AudioCue): Event; @@ -25,33 +26,72 @@ export interface IAudioCueService { playAudioCueLoop(cue: AudioCue, milliseconds: number): IDisposable; } +export interface IAudioCueOptions { + allowManyInParallel?: boolean; + source?: string; +} + export class AudioCueService extends Disposable implements IAudioCueService { readonly _serviceBrand: undefined; - sounds: Map = new Map(); + private readonly sounds: Map = new Map(); private readonly screenReaderAttached = observableFromEvent( this.accessibilityService.onDidChangeScreenReaderOptimized, () => /** @description accessibilityService.onDidChangeScreenReaderOptimized */ this.accessibilityService.isScreenReaderOptimized() ); + private readonly sentTelemetry = new Set(); constructor( @IConfigurationService private readonly configurationService: IConfigurationService, - @IAccessibilityService private readonly accessibilityService: IAccessibilityService + @IAccessibilityService private readonly accessibilityService: IAccessibilityService, + @ITelemetryService private readonly telemetryService: ITelemetryService, ) { super(); } - public async playAudioCue(cue: AudioCue, allowManyInParallel = false): Promise { + public async playAudioCue(cue: AudioCue, options: IAudioCueOptions = {}): Promise { if (this.isEnabled(cue)) { - await this.playSound(cue.sound.getSound(), allowManyInParallel); + this.sendAudioCueTelemetry(cue, options.source); + await this.playSound(cue.sound.getSound(), options.allowManyInParallel); } } - public async playAudioCues(cues: AudioCue[]): Promise { + public async playAudioCues(cues: (AudioCue | { cue: AudioCue; source: string })[]): Promise { + for (const cue of cues) { + this.sendAudioCueTelemetry('cue' in cue ? cue.cue : cue, 'source' in cue ? cue.source : undefined); + } + // Some audio cues might reuse sounds. Don't play the same sound twice. - const sounds = new Set(cues.filter(cue => this.isEnabled(cue)).map(cue => cue.sound.getSound())); + const sounds = new Set(cues.map(c => 'cue' in c ? c.cue : c).filter(cue => this.isEnabled(cue)).map(cue => cue.sound.getSound())); await Promise.all(Array.from(sounds).map(sound => this.playSound(sound, true))); } + private sendAudioCueTelemetry(cue: AudioCue, source: string | undefined): void { + const isScreenReaderOptimized = this.accessibilityService.isScreenReaderOptimized(); + const key = cue.name + (source ? `::${source}` : '') + (isScreenReaderOptimized ? '{screenReaderOptimized}' : ''); + // Only send once per user session + if (this.sentTelemetry.has(key) || this.getVolumeInPercent() === 0) { + return; + } + this.sentTelemetry.add(key); + + this.telemetryService.publicLog2<{ + audioCue: string; + source: string; + isScreenReaderOptimized: boolean; + }, { + owner: 'hediet'; + + audioCue: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The audio cue that was played.' }; + source: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The source that triggered the audio cue (e.g. "diffEditorNavigation").' }; + isScreenReaderOptimized: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user is using a screen reader' }; + + comment: 'This data is collected to understand how audio cues are used and if more audio cues should be added.'; + }>('audioCue.played', { + audioCue: cue.name, + source: source ?? '', + isScreenReaderOptimized, + }); + } private getVolumeInPercent(): number { const volume = this.configurationService.getValue('audioCues.volume'); @@ -92,7 +132,7 @@ export class AudioCueService extends Disposable implements IAudioCueService { let playing = true; const playSound = () => { if (playing) { - this.playAudioCue(cue, true).finally(() => { + this.playAudioCue(cue, { allowManyInParallel: true }).finally(() => { setTimeout(() => { if (playing) { playSound(); diff --git a/src/vs/platform/environment/common/environmentService.ts b/src/vs/platform/environment/common/environmentService.ts index 78eccd30b05..3d1adfa7619 100644 --- a/src/vs/platform/environment/common/environmentService.ts +++ b/src/vs/platform/environment/common/environmentService.ts @@ -236,6 +236,9 @@ export abstract class AbstractNativeEnvironmentService implements INativeEnviron @memoize get disableWorkspaceTrust(): boolean { return !!this.args['disable-workspace-trust']; } + @memoize + get disableKeytar(): boolean { return !!this.args['disable-keytar']; } + @memoize get policyFile(): URI | undefined { if (this.args['__enable-file-policy']) { diff --git a/src/vs/platform/environment/electron-main/environmentMainService.ts b/src/vs/platform/environment/electron-main/environmentMainService.ts index 1fd54c5997b..748ff075783 100644 --- a/src/vs/platform/environment/electron-main/environmentMainService.ts +++ b/src/vs/platform/environment/electron-main/environmentMainService.ts @@ -59,9 +59,6 @@ export class EnvironmentMainService extends NativeEnvironmentService implements @memoize get disableUpdates(): boolean { return !!this.args['disable-updates']; } - @memoize - get disableKeytar(): boolean { return !!this.args['disable-keytar']; } - @memoize get crossOriginIsolated(): boolean { return !!this.args['enable-coi']; } diff --git a/src/vs/platform/secrets/common/secrets.ts b/src/vs/platform/secrets/common/secrets.ts index 095457b46d3..87d2653f6ef 100644 --- a/src/vs/platform/secrets/common/secrets.ts +++ b/src/vs/platform/secrets/common/secrets.ts @@ -10,6 +10,7 @@ import { IStorageService, InMemoryStorageService, StorageScope, StorageTarget } import { Emitter, Event } from 'vs/base/common/event'; import { ILogService } from 'vs/platform/log/common/log'; import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Lazy } from 'vs/base/common/lazy'; export const ISecretStorageService = createDecorator('secretStorageService'); @@ -39,28 +40,24 @@ export class BaseSecretStorageService implements ISecretStorageService { private readonly _onDidChangeValueDisposable = new DisposableStore(); - protected resolvedStorageService = this.initialize(); - constructor( private readonly _useInMemoryStorage: boolean, @IStorageService private _storageService: IStorageService, @IEncryptionService protected _encryptionService: IEncryptionService, - @ILogService protected readonly _logService: ILogService + @ILogService protected readonly _logService: ILogService, ) { } + /** + * @Note initialize must be called first so that this can be resolved properly + * otherwise it will return 'unknown'. + */ get type() { return this._type; } - private onDidChangeValue(key: string): void { - if (!key.startsWith(this._storagePrefix)) { - return; - } - - const secretKey = key.slice(this._storagePrefix.length); - - this._logService.trace(`[SecretStorageService] Notifying change in value for secret: ${secretKey}`); - this.onDidChangeSecretEmitter.fire(secretKey); + private _lazyStorageService: Lazy> = new Lazy(() => this.initialize()); + protected get resolvedStorageService() { + return this._lazyStorageService.value; } get(key: string): Promise { @@ -127,6 +124,7 @@ export class BaseSecretStorageService implements ISecretStorageService { private async initialize(): Promise { let storageService; if (!this._useInMemoryStorage && await this._encryptionService.isEncryptionAvailable()) { + this._logService.trace(`[SecretStorageService] Encryption is available, using persisted storage`); this._type = 'persisted'; storageService = this._storageService; } else { @@ -147,7 +145,18 @@ export class BaseSecretStorageService implements ISecretStorageService { } protected reinitialize(): void { - this.resolvedStorageService = this.initialize(); + this._lazyStorageService = new Lazy(() => this.initialize()); + } + + private onDidChangeValue(key: string): void { + if (!key.startsWith(this._storagePrefix)) { + return; + } + + const secretKey = key.slice(this._storagePrefix.length); + + this._logService.trace(`[SecretStorageService] Notifying change in value for secret: ${secretKey}`); + this.onDidChangeSecretEmitter.fire(secretKey); } private getKey(key: string): string { diff --git a/src/vs/platform/secrets/test/common/secrets.test.ts b/src/vs/platform/secrets/test/common/secrets.test.ts index 11734973de0..c22701d7cd6 100644 --- a/src/vs/platform/secrets/test/common/secrets.test.ts +++ b/src/vs/platform/secrets/test/common/secrets.test.ts @@ -5,7 +5,6 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; -import { timeout } from 'vs/base/common/async'; import { IEncryptionService, KnownStorageProvider } from 'vs/platform/encryption/common/encryptionService'; import { NullLogService } from 'vs/platform/log/common/log'; import { BaseSecretStorageService } from 'vs/platform/secrets/common/secrets'; @@ -67,8 +66,10 @@ suite('secrets', () => { }); test('type', async () => { - // allow initialization to complete - await timeout(0); + assert.strictEqual(service.type, 'unknown'); + // trigger lazy initialization + await service.set('my-secret', 'my-secret-value'); + assert.strictEqual(service.type, 'in-memory'); }); @@ -122,8 +123,10 @@ suite('secrets', () => { }); test('type', async () => { - // allow initialization to complete - await timeout(0); + assert.strictEqual(service.type, 'unknown'); + // trigger lazy initialization + await service.set('my-secret', 'my-secret-value'); + assert.strictEqual(service.type, 'persisted'); }); @@ -177,8 +180,10 @@ suite('secrets', () => { }); test('type', async () => { - // allow initialization to complete - await timeout(0); + assert.strictEqual(service.type, 'unknown'); + // trigger lazy initialization + await service.set('my-secret', 'my-secret-value'); + assert.strictEqual(service.type, 'in-memory'); }); diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts index 70b39c12b83..dc4d9bcb84b 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts @@ -41,6 +41,8 @@ import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/c import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { ILogService } from 'vs/platform/log/common/log'; import { ISecretStorageService } from 'vs/platform/secrets/common/secrets'; +import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { runWhenIdle } from 'vs/base/common/async'; export class ViewContainerActivityAction extends ActivityAction { @@ -239,6 +241,7 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem { colors: (theme: IColorTheme) => ICompositeBarColors, activityHoverOptions: IActivityHoverOptions, @IThemeService themeService: IThemeService, + @ILifecycleService private readonly lifecycleService: ILifecycleService, @IHoverService hoverService: IHoverService, @IContextMenuService contextMenuService: IContextMenuService, @IMenuService menuService: IMenuService, @@ -285,6 +288,16 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem { // This function exists to ensure that the accounts are added for auth providers that had already been registered // before the menu was created. private async initialize(): Promise { + // Resolving the menu doesn't need to happen immediately, so we can wait until after the workbench has been restored + // and only run this when the system is idle. + await this.lifecycleService.when(LifecyclePhase.Restored); + const disposable = this._register(runWhenIdle(async () => { + await this.doInitialize(); + disposable.dispose(); + })); + } + + private async doInitialize(): Promise { const providerIds = this.authenticationService.getProviderIds(); const results = await Promise.allSettled(providerIds.map(providerId => this.addAccountsFromProvider(providerId))); diff --git a/src/vs/workbench/browser/parts/editor/editorCommands.ts b/src/vs/workbench/browser/parts/editor/editorCommands.ts index d7fd4b9af44..5f33c17c54f 100644 --- a/src/vs/workbench/browser/parts/editor/editorCommands.ts +++ b/src/vs/workbench/browser/parts/editor/editorCommands.ts @@ -1499,15 +1499,7 @@ export function getMultiSelectedEditorContexts(editorContext: IEditorCommandsCon if (focus) { const selection: Array = list.getSelectedElements().filter(onlyEditorGroupAndEditor); - // Only respect selection if it contains focused element - if (selection?.some(s => { - if (isEditorGroup(s)) { - return s.id === focus.groupId; - } - - const group = editorGroupService.getGroup(s.groupId); - return s.groupId === focus.groupId && (group ? group.getIndexOfEditor(s.editor) : -1) === focus.editorIndex; - })) { + if (selection.length > 0) { return selection.map(elementToContext); } diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index 85d78ce153d..976563aaafa 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -27,9 +27,11 @@ import { CodeActionController } from 'vs/editor/contrib/codeAction/browser/codeA import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { AccessibilityVerbositySettingId, accessibilityHelpIsShown, accessibleViewIsShown } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; -import { Action2, MenuId } from 'vs/platform/actions/common/actions'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; - +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { IPickerQuickAccessItem } from 'vs/platform/quickinput/browser/pickerQuickAccess'; +import { marked } from 'vs/base/common/marked/marked'; const enum DIMENSIONS { MAX_WIDTH = 600 @@ -42,6 +44,10 @@ export interface IAccessibleContentProvider { onKeyDown?(e: IKeyboardEvent): void; previous?(): void; next?(): void; + /** + * When the language is markdown, this is provided by default. + */ + getSymbols?(): IAccessibleViewSymbol[]; options: IAccessibleViewOptions; } @@ -52,6 +58,7 @@ export interface IAccessibleViewService { show(provider: IAccessibleContentProvider): void; next(): void; previous(): void; + goToSymbol(): void; /** * If the setting is enabled, provides the open accessible view hint as a localized string. * @param verbositySettingKey The setting key for the verbosity of the feature @@ -128,15 +135,21 @@ class AccessibleView extends Disposable { })); } - show(provider: IAccessibleContentProvider): void { + show(provider?: IAccessibleContentProvider, symbol?: IAccessibleViewSymbol): void { + if (!provider) { + provider = this._currentProvider; + } + if (!provider) { + return; + } const delegate: IContextViewDelegate = { getAnchor: () => { return { x: (window.innerWidth / 2) - ((Math.min(this._layoutService.dimension.width * 0.62 /* golden cut */, DIMENSIONS.MAX_WIDTH)) / 2), y: this._layoutService.offset.quickPickTop }; }, render: (container) => { container.classList.add('accessible-view-container'); - return this._render(provider, container); + return this._render(provider!, container); }, onHide: () => { - if (provider.options.type === AccessibleViewType.Help) { + if (provider!.options.type === AccessibleViewType.Help) { this._accessiblityHelpIsShown.reset(); } else { this._accessibleViewIsShown.reset(); @@ -150,6 +163,9 @@ class AccessibleView extends Disposable { } else { this._accessibleViewIsShown.set(true); } + if (symbol && this._currentProvider) { + this.showSymbol(this._currentProvider, symbol); + } this._currentProvider = provider; } @@ -167,6 +183,51 @@ class AccessibleView extends Disposable { this._currentProvider.next?.(); } + goToSymbol(): void { + this._instantiationService.createInstance(AccessibleViewSymbolQuickPick, this).show(this._currentProvider!); + } + + getSymbols(): IAccessibleViewSymbol[] | undefined { + if (!this._currentProvider) { + return; + } + const tokens = this._currentProvider.options.language && this._currentProvider.options.language !== 'markdown' ? this._currentProvider.getSymbols?.() : marked.lexer(this._currentProvider.provideContent()); + if (!tokens) { + return; + } + const symbols: IAccessibleViewSymbol[] = []; + for (const token of tokens) { + let label: string | undefined = undefined; + if ('type' in token) { + switch (token.type) { + case 'heading': + case 'paragraph': + case 'code': + label = token.text; + break; + case 'list': + label = token.items?.map(i => i.text).join(', '); + break; + } + } else { + label = token.label; + } + if (label) { + symbols.push({ info: label, label: localize('symbolLabel', "({0}) {1}", token.type, label), ariaLabel: localize('symbolLabelAria', "({0}) {1}", token.type, label) }); + } + } + return symbols; + } + + showSymbol(provider: IAccessibleContentProvider, symbol: IAccessibleViewSymbol): void { + const index = provider.provideContent().split('\n').findIndex(line => line.includes(symbol.info.split('\n')[0])) ?? -1; + if (index >= 0) { + this.show(provider); + this._editorWidget.revealLine(index + 1); + this._editorWidget.setSelection({ startLineNumber: index + 1, startColumn: 1, endLineNumber: index + 1, endColumn: 1 }); + } + } + private _render(provider: IAccessibleContentProvider, container: HTMLElement): IDisposable { this._currentProvider = provider; const settingKey = `accessibility.verbosity.${provider.verbositySettingKey}`; @@ -286,6 +347,9 @@ export class AccessibleViewService extends Disposable implements IAccessibleView previous(): void { this._accessibleView?.previous(); } + goToSymbol(): void { + this._accessibleView?.goToSymbol(); + } getOpenAriaHint(verbositySettingKey: AccessibilityVerbositySettingId): string | null { if (!this._configurationService.getValue(verbositySettingKey)) { return null; @@ -300,7 +364,7 @@ class AccessibleViewNextAction extends Action2 { static id: 'editor.action.accessibleViewNext'; constructor() { super({ - id: AccessibleViewNextAction.id, + id: 'editor.action.accessibleViewNext', precondition: accessibleViewIsShown, keybinding: { primary: KeyMod.Alt | KeyCode.BracketRight, @@ -318,13 +382,38 @@ class AccessibleViewNextAction extends Action2 { accessor.get(IAccessibleViewService).next(); } } +registerAction2(AccessibleViewNextAction); +class AccessibleViewGoToSymbolAction extends Action2 { + static id: 'editor.action.accessibleViewGoToSymbol'; + constructor() { + super({ + id: 'editor.action.accessibleViewGoToSymbol', + precondition: accessibleViewIsShown, + keybinding: { + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyO, + weight: KeybindingWeight.WorkbenchContrib + 10 + }, + menu: [{ + id: MenuId.CommandPalette, + group: '', + order: 1 + }], + title: localize('editor.action.accessibleViewGoToSymbol', "Go To Symbol in Accessible View") + }); + } + run(accessor: ServicesAccessor, ...args: unknown[]): void { + accessor.get(IAccessibleViewService).goToSymbol(); + } +} +registerAction2(AccessibleViewGoToSymbolAction); + class AccessibleViewPreviousAction extends Action2 { static id: 'editor.action.accessibleViewPrevious'; constructor() { super({ - id: AccessibleViewPreviousAction.id, + id: 'editor.action.accessibleViewPrevious', precondition: accessibleViewIsShown, keybinding: { primary: KeyMod.Alt | KeyCode.BracketLeft, @@ -339,10 +428,10 @@ class AccessibleViewPreviousAction extends Action2 { }); } run(accessor: ServicesAccessor, ...args: unknown[]): void { - accessor.get(IAccessibleViewService).next(); + accessor.get(IAccessibleViewService).previous(); } } - +registerAction2(AccessibleViewPreviousAction); function registerCommand(command: T): T { command.register(); @@ -388,3 +477,33 @@ export const AccessibleViewAction = registerCommand(new MultiCommand({ }], })); + +class AccessibleViewSymbolQuickPick { + constructor(private _accessibleView: AccessibleView, @IQuickInputService private readonly _quickInputService: IQuickInputService) { + + } + show(provider: IAccessibleContentProvider): void { + const quickPick = this._quickInputService.createQuickPick(); + const picks = []; + const symbols = this._accessibleView.getSymbols(); + if (!symbols) { + return; + } + for (const symbol of symbols) { + picks.push({ + label: symbol.label, + ariaLabel: symbol.ariaLabel + }); + } + quickPick.canSelectMany = false; + quickPick.items = symbols; + quickPick.show(); + quickPick.onDidAccept(() => { + this._accessibleView.showSymbol(provider, quickPick.selectedItems[0]); + }); + } +} + +interface IAccessibleViewSymbol extends IPickerQuickAccessItem { + info: string; +} diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatQuickInputActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatQuickInputActions.ts index e03b39894f3..0ea789e3602 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatQuickInputActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatQuickInputActions.ts @@ -3,12 +3,263 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { registerAction2 } from 'vs/platform/actions/common/actions'; -import { AskQuickQuestionAction } from 'vs/workbench/contrib/chat/browser/actions/quickQuestionActions/quickQuestionAction'; - -import 'vs/workbench/contrib/chat/browser/actions/quickQuestionActions/singleQuickQuestionAction'; -import 'vs/workbench/contrib/chat/browser/actions/quickQuestionActions/multipleByScrollQuickQuestionAction'; +import * as dom from 'vs/base/browser/dom'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Codicon } from 'vs/base/common/codicons'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { localize } from 'vs/nls'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ContextKeyExpr, IContextKeyService, IScopedContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { IQuickInputService, IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { editorBackground, editorForeground } from 'vs/platform/theme/common/colorRegistry'; +import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; +import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; +import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatViewOptions } from 'vs/workbench/contrib/chat/browser/chatViewPane'; +import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; +import { CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { ChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; +export const ASK_QUICK_QUESTION_ACTION_ID = 'chat.action.askQuickQuestion'; export function registerChatQuickQuestionActions() { registerAction2(AskQuickQuestionAction); } + +class AskQuickQuestionAction extends Action2 { + private _currentTimer: any | undefined; + private _input: IQuickPick | undefined; + private _currentChat: QuickChat | undefined; + + constructor() { + super({ + id: ASK_QUICK_QUESTION_ACTION_ID, + title: { value: localize('chat', "Chat"), original: 'Chat' }, + precondition: CONTEXT_PROVIDER_EXISTS, + icon: Codicon.commentDiscussion, + f1: false, + category: CHAT_CATEGORY, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyI, + linux: { + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyCode.KeyI + } + }, + menu: { + id: MenuId.LayoutControlMenu, + group: '0_workbench_toggles', + when: ContextKeyExpr.notEquals('config.chat.experimental.defaultMode', 'chatView'), + order: 0 + } + }); + } + + override run(accessor: ServicesAccessor, query: string): void { + const quickInputService = accessor.get(IQuickInputService); + const chatService = accessor.get(IChatService); + const instantiationService = accessor.get(IInstantiationService); + + // First things first, clear the existing timer that will dispose the session + clearTimeout(this._currentTimer); + this._currentTimer = undefined; + + // If the input is already shown, hide it. This provides a toggle behavior of the quick pick + if (this._input !== undefined) { + this._input.hide(); + return; + } + + // Check if any providers are available. If not, show nothing + // This shouldn't be needed because of the precondition, but just in case + const providerInfo = chatService.getProviderInfos()[0]; + if (!providerInfo) { + return; + } + + const disposableStore = new DisposableStore(); + + //#region Setup quick pick + + this._input = quickInputService.createQuickPick(); + disposableStore.add(this._input); + this._input.hideInput = true; + + + const containerSession = dom.$('.interactive-session'); + this._input.widget = containerSession; + + this._currentChat ??= instantiationService.createInstance(QuickChat, { + providerId: providerInfo.id, + }); + // show needs to come before the current chat rendering + this._input.show(); + this._currentChat.render(containerSession); + + const clearButton = { + iconClass: ThemeIcon.asClassName(Codicon.clearAll), + tooltip: localize('clear', "Clear"), + }; + this._input.buttons = [ + clearButton, + { + iconClass: ThemeIcon.asClassName(Codicon.commentDiscussion), + tooltip: localize('openInChat', "Open In Chat View"), + } + ]; + this._input.title = providerInfo.displayName; + + disposableStore.add(this._input.onDidHide(() => { + disposableStore.dispose(); + this._input = undefined; + this._currentTimer = setTimeout(() => { + this._currentChat?.dispose(); + this._currentChat = undefined; + }, 1000 * 30); // 30 seconds + })); + + //#endregion + + disposableStore.add(this._input.onDidAccept(() => { + this._currentChat?.acceptInput(); + })); + + disposableStore.add(this._input.onDidTriggerButton((e) => { + if (e === clearButton) { + this._currentChat?.clear(); + } else { + this._currentChat?.openChatView(); + } + })); + + this._currentChat.focus(); + + if (query) { + this._currentChat.setValue(query); + this._currentChat.acceptInput(); + } + } +} + +class QuickChat extends Disposable { + private widget!: ChatWidget; + private model: ChatModel | undefined; + private _currentQuery: string | undefined; + + private _scopedContextKeyService!: IScopedContextKeyService; + get scopedContextKeyService() { + return this._scopedContextKeyService; + } + + constructor( + private readonly _options: IChatViewOptions, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IChatService private readonly chatService: IChatService, + @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService + ) { + super(); + } + + clear() { + this.model?.dispose(); + this.model = undefined; + this.updateModel(); + this.widget.inputEditor.setValue(''); + } + + focus(): void { + if (this.widget) { + this.widget.focusInput(); + } + } + + render(parent: HTMLElement): void { + this._scopedContextKeyService?.dispose(); + this._scopedContextKeyService = this._register(this.contextKeyService.createScoped(parent)); + const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])); + this.widget?.dispose(); + this.widget = this._register( + scopedInstantiationService.createInstance( + ChatWidget, + { resource: true, renderInputOnTop: true }, + { + listForeground: editorForeground, + listBackground: editorBackground, + inputEditorBackground: SIDE_BAR_BACKGROUND, + resultEditorBackground: SIDE_BAR_BACKGROUND + })); + this.widget.render(parent); + this.widget.setVisible(true); + this.widget.setDynamicChatTreeItemLayout(2, 600); + this.updateModel(); + if (this._currentQuery) { + this.widget.inputEditor.setSelection({ + startLineNumber: 1, + startColumn: 1, + endLineNumber: 1, + endColumn: this._currentQuery.length + 1 + }); + } + + this.registerListeners(); + } + + private registerListeners(): void { + this._register(this.widget.inputEditor.onDidChangeModelContent((e) => { + this._currentQuery = this.widget.inputEditor.getValue(); + })); + } + + async acceptInput(): Promise { + if (this.widget.inputEditor.getValue().trim() === '/clear') { + this.clear(); + } else { + await this.widget.acceptInput(); + } + } + + async openChatView(): Promise { + const widget = await this._chatWidgetService.revealViewForProvider(this._options.providerId); + if (!widget?.viewModel || !this.model) { + return; + } + + for (const request of this.model.getRequests()) { + if (request.response?.response.value || request.response?.errorDetails) { + this.chatService.addCompleteRequest(widget.viewModel.sessionId, + request.message as string, + { + message: request.response.response.value, + errorDetails: request.response.errorDetails + }); + } else if (request.message) { + + } + } + + const value = this.widget.inputEditor.getValue(); + if (value) { + widget.inputEditor.setValue(value); + } + widget.focusInput(); + } + + setValue(value: string): void { + this.widget.inputEditor.setValue(value); + } + + private updateModel(): void { + this.model ??= this.chatService.startSession(this._options.providerId, CancellationToken.None); + if (!this.model) { + throw new Error('Could not start chat session'); + } + + this.widget.setModel(this.model, { inputValue: this._currentQuery }); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/actions/quickQuestionActions/multipleByScrollQuickQuestionAction.ts b/src/vs/workbench/contrib/chat/browser/actions/quickQuestionActions/multipleByScrollQuickQuestionAction.ts deleted file mode 100644 index b7357eb1366..00000000000 --- a/src/vs/workbench/contrib/chat/browser/actions/quickQuestionActions/multipleByScrollQuickQuestionAction.ts +++ /dev/null @@ -1,280 +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 { CancellationToken } from 'vs/base/common/cancellation'; -import { Codicon } from 'vs/base/common/codicons'; -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { ThemeIcon } from 'vs/base/common/themables'; -import { localize } from 'vs/nls'; -import { IContextKeyService, IScopedContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { IQuickInputService, IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -import { editorForeground, editorBackground } from 'vs/platform/theme/common/colorRegistry'; -import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; -import { AskQuickQuestionAction, IQuickQuestionMode, QuickQuestionMode } from 'vs/workbench/contrib/chat/browser/actions/quickQuestionActions/quickQuestionAction'; -import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; -import { IChatViewOptions } from 'vs/workbench/contrib/chat/browser/chatViewPane'; -import { ChatWidget } from 'vs/workbench/contrib/chat/browser/chatWidget'; -import { ChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; - -interface IChatQuickQuestionModeOptions { - renderInputOnTop: boolean; - useDynamicMessageLayout: boolean; -} - -class BaseChatQuickQuestionMode implements IQuickQuestionMode { - private _currentTimer: any | undefined; - private _input: IQuickPick | undefined; - private _currentChat: QuickChat | undefined; - - constructor( - private readonly _options: IChatQuickQuestionModeOptions - ) { } - - run(accessor: ServicesAccessor, query: string): void { - const quickInputService = accessor.get(IQuickInputService); - const chatService = accessor.get(IChatService); - const instantiationService = accessor.get(IInstantiationService); - - // First things first, clear the existing timer that will dispose the session - clearTimeout(this._currentTimer); - this._currentTimer = undefined; - - // If the input is already shown, hide it. This provides a toggle behavior of the quick pick - if (this._input !== undefined) { - this._input.hide(); - return; - } - - // Check if any providers are available. If not, show nothing - // This shouldn't be needed because of the precondition, but just in case - const providerInfo = chatService.getProviderInfos()[0]; - if (!providerInfo) { - return; - } - - const disposableStore = new DisposableStore(); - - //#region Setup quick pick - - this._input = quickInputService.createQuickPick(); - disposableStore.add(this._input); - this._input.hideInput = true; - - - const containerSession = dom.$('.interactive-session'); - this._input.widget = containerSession; - - this._currentChat ??= instantiationService.createInstance(QuickChat, { - providerId: providerInfo.id, - ...this._options - }); - // show needs to come before the current chat rendering - this._input.show(); - this._currentChat.render(containerSession); - - const clearButton = { - iconClass: ThemeIcon.asClassName(Codicon.clearAll), - tooltip: localize('clear', "Clear"), - }; - this._input.buttons = [ - clearButton, - { - iconClass: ThemeIcon.asClassName(Codicon.commentDiscussion), - tooltip: localize('openInChat', "Open In Chat View"), - } - ]; - this._input.title = providerInfo.displayName; - - disposableStore.add(this._input.onDidHide(() => { - disposableStore.dispose(); - this._input = undefined; - this._currentTimer = setTimeout(() => { - this._currentChat?.dispose(); - this._currentChat = undefined; - }, 1000 * 30); // 30 seconds - })); - - //#endregion - - disposableStore.add(this._input.onDidAccept(() => { - this._currentChat?.acceptInput(); - })); - - disposableStore.add(this._input.onDidTriggerButton((e) => { - if (e === clearButton) { - this._currentChat?.clear(); - } else { - this._currentChat?.openChatView(); - } - })); - - this._currentChat.layout(); - this._currentChat.focus(); - - if (query) { - this._currentChat.setValue(query); - this._currentChat.acceptInput(); - } - } -} - -class QuickChat extends Disposable { - private widget!: ChatWidget; - private model: ChatModel | undefined; - private _currentQuery: string | undefined; - - private _scopedContextKeyService!: IScopedContextKeyService; - get scopedContextKeyService() { - return this._scopedContextKeyService; - } - - private _currentParentElement?: HTMLElement; - - constructor( - private readonly _options: IChatViewOptions & IChatQuickQuestionModeOptions, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IChatService private readonly chatService: IChatService, - @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService - ) { - super(); - } - - clear() { - this.model?.dispose(); - this.model = undefined; - this.updateModel(); - this.widget.inputEditor.setValue(''); - } - - focus(): void { - if (this.widget) { - this.widget.focusInput(); - } - } - - render(parent: HTMLElement): void { - this._currentParentElement = parent; - this._scopedContextKeyService?.dispose(); - this._scopedContextKeyService = this._register(this.contextKeyService.createScoped(parent)); - const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])); - this.widget?.dispose(); - this.widget = this._register( - scopedInstantiationService.createInstance( - ChatWidget, - { resource: true, renderInputOnTop: this._options.renderInputOnTop }, - { - listForeground: editorForeground, - listBackground: editorBackground, - inputEditorBackground: SIDE_BAR_BACKGROUND, - resultEditorBackground: SIDE_BAR_BACKGROUND - })); - this.widget.render(parent); - this.widget.setVisible(true); - if (this._options.useDynamicMessageLayout) { - this.widget.setDynamicChatTreeItemLayout(2, 600); - } - this.updateModel(); - if (this._currentQuery) { - this.widget.inputEditor.setSelection({ - startLineNumber: 1, - startColumn: 1, - endLineNumber: 1, - endColumn: this._currentQuery.length + 1 - }); - } - - this.registerListeners(); - } - - private registerListeners(): void { - this._register(dom.addDisposableListener(parent, dom.EventType.RESIZE, () => this.layout())); - this._register(this.widget.inputEditor.onDidChangeModelContent((e) => { - this._currentQuery = this.widget.inputEditor.getValue(); - })); - } - - async acceptInput(): Promise { - if (this.widget.inputEditor.getValue().trim() === '/clear') { - this.clear(); - } else { - await this.widget.acceptInput(); - } - this.layout(); - } - - async openChatView(): Promise { - const widget = await this._chatWidgetService.revealViewForProvider(this._options.providerId); - if (!widget?.viewModel || !this.model) { - return; - } - - for (const request of this.model.getRequests()) { - if (request.response?.response.value || request.response?.errorDetails) { - this.chatService.addCompleteRequest(widget.viewModel.sessionId, - request.message as string, - { - message: request.response.response.value, - errorDetails: request.response.errorDetails - }); - } else if (request.message) { - - } - } - - const value = this.widget.inputEditor.getValue(); - if (value) { - widget.inputEditor.setValue(value); - } - widget.focusInput(); - } - - setValue(value: string): void { - this.widget.inputEditor.setValue(value); - } - - layout(): void { - if (!this._options.useDynamicMessageLayout && this._currentParentElement) { - this.widget.layout(500, this._currentParentElement.offsetWidth); - } - } - - private updateModel(): void { - this.model ??= this.chatService.startSession(this._options.providerId, CancellationToken.None); - if (!this.model) { - throw new Error('Could not start chat session'); - } - - this.widget.setModel(this.model, { inputValue: this._currentQuery }); - } -} - -AskQuickQuestionAction.registerMode( - QuickQuestionMode.InputOnTopChat, - class InputOnTopChatQuickQuestionMode extends BaseChatQuickQuestionMode { - constructor() { - super({ - renderInputOnTop: true, - useDynamicMessageLayout: true - }); - } - } -); - -AskQuickQuestionAction.registerMode( - QuickQuestionMode.InputOnBottomChat, - class InputOnBottomChatQuickQuestionMode extends BaseChatQuickQuestionMode { - constructor() { - super({ - renderInputOnTop: false, - useDynamicMessageLayout: false - }); - } - } -); diff --git a/src/vs/workbench/contrib/chat/browser/actions/quickQuestionActions/quickQuestionAction.ts b/src/vs/workbench/contrib/chat/browser/actions/quickQuestionActions/quickQuestionAction.ts deleted file mode 100644 index a76eb9f6636..00000000000 --- a/src/vs/workbench/contrib/chat/browser/actions/quickQuestionActions/quickQuestionAction.ts +++ /dev/null @@ -1,71 +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 { Codicon } from 'vs/base/common/codicons'; -import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import { Lazy } from 'vs/base/common/lazy'; -import { localize } from 'vs/nls'; -import { Action2, MenuId } from 'vs/platform/actions/common/actions'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; -import { CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys'; - -export const ASK_QUICK_QUESTION_ACTION_ID = 'chat.action.askQuickQuestion'; - -export const enum QuickQuestionMode { - SingleQuestion = 'singleQuestion', - InputOnTopChat = 'inputOnTopChat', - InputOnBottomChat = 'inputOnBottomChat', -} - -export interface IQuickQuestionMode { - run(accessor: ServicesAccessor, query: string): void; -} - -// TODO: This should be registered per chat-provider probably. -export class AskQuickQuestionAction extends Action2 { - - private static readonly modeRegistry: Map> = new Map(); - static registerMode(mode: QuickQuestionMode, modeAction: { new(): IQuickQuestionMode }) { - AskQuickQuestionAction.modeRegistry.set(mode, new Lazy(() => new modeAction())); - } - - constructor() { - super({ - id: ASK_QUICK_QUESTION_ACTION_ID, - title: { value: localize('chat', "Chat"), original: 'Chat' }, - precondition: CONTEXT_PROVIDER_EXISTS, - icon: Codicon.commentDiscussion, - f1: false, - category: CHAT_CATEGORY, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyI, - linux: { - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyCode.KeyI - } - }, - menu: { - id: MenuId.LayoutControlMenu, - group: '0_workbench_toggles', - when: ContextKeyExpr.notEquals('config.chat.experimental.defaultMode', 'chatView'), - order: 0 - } - }); - } - - override run(accessor: ServicesAccessor, query: string): void { - const configurationService = accessor.get(IConfigurationService); - - const mode = configurationService.getValue('chat.experimental.quickQuestion.mode'); - const modeAction = AskQuickQuestionAction.modeRegistry.get(mode); - if (modeAction) { - modeAction.value.run(accessor, query); - } - } -} diff --git a/src/vs/workbench/contrib/chat/browser/actions/quickQuestionActions/singleQuickQuestionAction.ts b/src/vs/workbench/contrib/chat/browser/actions/quickQuestionActions/singleQuickQuestionAction.ts deleted file mode 100644 index 606a67c79f0..00000000000 --- a/src/vs/workbench/contrib/chat/browser/actions/quickQuestionActions/singleQuickQuestionAction.ts +++ /dev/null @@ -1,261 +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 { Toggle } from 'vs/base/browser/ui/toggle/toggle'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { Codicon } from 'vs/base/common/codicons'; -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; -import { localize } from 'vs/nls'; -import { Emitter, Event } from 'vs/base/common/event'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IQuickInputService, IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -import { asCssVariable, editorBackground, foreground, inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground } from 'vs/platform/theme/common/colorRegistry'; -import { ChatListItemRenderer } from 'vs/workbench/contrib/chat/browser/chatListRenderer'; -import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; -import { ChatModel } from 'vs/workbench/contrib/chat/common/chatModel'; -import { IChatReplyFollowup, IChatService } from 'vs/workbench/contrib/chat/common/chatService'; -import { ChatViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel'; -import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; -import { AskQuickQuestionAction, IQuickQuestionMode, QuickQuestionMode } from 'vs/workbench/contrib/chat/browser/actions/quickQuestionActions/quickQuestionAction'; - -class AskSingleQuickQuestionMode implements IQuickQuestionMode { - - private _currentSession: InteractiveQuickPickSession | undefined; - private _currentQuery: string | undefined; - private _lastAcceptedQuery: string | undefined; - private _currentTimer: any | undefined; - private _input: IQuickPick | undefined; - - run(accessor: ServicesAccessor, query: string): void { - const quickInputService = accessor.get(IQuickInputService); - const chatService = accessor.get(IChatService); - const instantiationService = accessor.get(IInstantiationService); - - // First things first, clear the existing timer that will dispose the session - clearTimeout(this._currentTimer); - this._currentTimer = undefined; - - // If the input is already shown, hide it. This provides a toggle behavior of the quick pick - if (this._input !== undefined) { - this._input.hide(); - return; - } - - // Check if any providers are available. If not, show nothing - const providerInfo = chatService.getProviderInfos()[0]; - if (!providerInfo) { - return; - } - - const disposableStore = new DisposableStore(); - - //#region Setup quick pick - - this._input = quickInputService.createQuickPick(); - disposableStore.add(this._input); - this._input.placeholder = localize('askabot', "Ask {0} a question...", providerInfo.displayName); - - // Setup toggle that will be used to open the chat view - const openInChat = new Toggle({ - title: 'Open In Chat View', - icon: Codicon.commentDiscussion, - isChecked: false, - inputActiveOptionBorder: asCssVariable(inputActiveOptionBorder), - inputActiveOptionForeground: asCssVariable(inputActiveOptionForeground), - inputActiveOptionBackground: asCssVariable(inputActiveOptionBackground) - }); - disposableStore.add(openInChat); - disposableStore.add(openInChat.onChange(async () => { - await this._currentSession?.openInChat(this._lastAcceptedQuery ?? this._input!.value); - this._currentQuery = undefined; - this._lastAcceptedQuery = undefined; - this._currentSession?.dispose(); - this._currentSession = undefined; - })); - this._input.toggles = [openInChat]; - - // Setup the widget that will be used to render the chat response - const containerList = dom.$('.interactive-list'); - const containerSession = dom.$('.interactive-session', undefined, containerList); - containerList.style.position = 'relative'; - this._input.widget = containerSession; - - //#endregion - - //#region quick pick events - - disposableStore.add(this._input.onDidChangeValue((value) => { - if (value !== this._currentQuery) { - this._currentQuery = value; - } - })); - disposableStore.add(this._input.onDidHide(() => { - disposableStore.dispose(); - this._input = undefined; - this._currentTimer = setTimeout(() => { - this._currentQuery = undefined; - this._lastAcceptedQuery = undefined; - this._currentSession?.dispose(); - this._currentSession = undefined; - }, 1000 * 30); // 30 seconds - })); - disposableStore.add(this._input.onDidAccept(async () => { - await this._currentSession?.accept(this._input!.value); - this._lastAcceptedQuery = this._input!.value; - })); - - //#endregion - - // If we were given a query (via executeCommand), then clear past state - if (query) { - this._currentSession?.dispose(); - this._currentSession = undefined; - } - this._currentSession ??= instantiationService.createInstance(InteractiveQuickPickSession); - this._input.show(); - // This line must come after showing the input so the offsetWidth is correct - this._currentSession.createList(containerList, containerList.offsetWidth); - - disposableStore.add(this._currentSession.onDidClickFollowup(async e => { - this._input!.focusOnInput(); - this._input!.value = e.message; - await this._currentSession?.accept(e.message); - })); - - // If we were given a query (via executeCommand), then accept it - if (query) { - this._input.value = query; - this._input.valueSelection = [0, this._input.value.length]; - this._currentQuery = query; - this._currentSession.accept(query); - } else if (this._currentQuery) { - this._input.value = this._currentQuery; - this._input.valueSelection = [0, this._input.value.length]; - } - } -} - -class InteractiveQuickPickSession extends Disposable { - - private _model: ChatModel; - private _viewModel: ChatViewModel; - - private readonly _onDidClickFollowup: Emitter = this._register(new Emitter()); - onDidClickFollowup: Event = this._onDidClickFollowup.event; - - private _listDisposable: DisposableStore | undefined; - - constructor( - @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IChatService private readonly _chatService: IChatService, - @IChatWidgetService private readonly _chatWidgetService: IChatWidgetService - ) { - super(); - - const providerInfo = _chatService.getProviderInfos()[0]; - this._model = this._register(_chatService.startSession(providerInfo.id, CancellationToken.None)!); - this._viewModel = this._register(new ChatViewModel(this._model, _instantiationService)); - } - - createList(container: HTMLElement, offsetWidth: number) { - this._listDisposable?.dispose(); - this._listDisposable = new DisposableStore(); - const options = this._listDisposable.add(this._instantiationService.createInstance(ChatEditorOptions, 'quickpick-interactive', foreground, editorBackground, editorBackground)); - const list = this._listDisposable.add(this._instantiationService.createInstance( - ChatListItemRenderer, - options, - { - getListLength: () => { - return 1; - }, - getSlashCommands() { - return []; - }, - } - )); - - const template = list.renderTemplate(container); - list.layout(offsetWidth); - this._listDisposable.add(this._viewModel.onDidChange(() => { - const items = this._viewModel.getItems(); - const node = { - element: items[items.length - 1], - children: [], - collapsed: false, - collapsible: false, - depth: 0, - filterData: undefined, - visible: true, - visibleChildIndex: 0, - visibleChildrenCount: 1, - }; - list.disposeElement(node, 0, template); - list.renderElement(node, 0, template); - })); - - if (this._viewModel.getItems().length) { - const items = this._viewModel.getItems(); - const node = { - element: items[items.length - 1], - children: [], - collapsed: false, - collapsible: false, - depth: 0, - filterData: undefined, - visible: true, - visibleChildIndex: 0, - visibleChildrenCount: 1, - }; - list.disposeElement(node, 0, template); - list.renderElement(node, 0, template); - } - - this._listDisposable.add(list.onDidClickFollowup(e => { - this._onDidClickFollowup.fire(e); - })); - } - - get providerId() { - return this._model.providerId; - } - - get sessionId() { - return this._model.sessionId; - } - - async accept(query: string) { - await this._model.waitForInitialization(); - const requests = this._model.getRequests(); - const lastRequest = requests[requests.length - 1]; - if (lastRequest?.message && lastRequest?.message === query) { - return; - } - if (this._model.requestInProgress) { - this._chatService.cancelCurrentRequestForSession(this.sessionId); - } - await this._chatService.sendRequest(this.sessionId, query); - } - - async openInChat(value: string) { - const widget = await this._chatWidgetService.revealViewForProvider(this.providerId); - if (!widget?.viewModel) { - return; - } - - const requests = this._model.getRequests().reverse(); - const response = requests.find(r => r.response?.response.value !== undefined); - const message = response?.response?.response.value; - if (message) { - this._chatService.addCompleteRequest(widget.viewModel.sessionId, value, { message }); - } else if (value) { - this._chatService.sendRequest(widget.viewModel.sessionId, value); - } - widget.focusInput(); - } -} - -AskQuickQuestionAction.registerMode(QuickQuestionMode.SingleQuestion, AskSingleQuickQuestionMode); diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 3f65d4f3b38..bb72115ff3a 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -42,9 +42,10 @@ import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { CONTEXT_IN_CHAT_SESSION } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { ChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chatAccessibilityService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { QuickQuestionMode } from 'vs/workbench/contrib/chat/browser/actions/quickQuestionActions/quickQuestionAction'; import { alertFocusChange } from 'vs/workbench/contrib/accessibility/browser/accessibility.contribution'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; +import { ChatWelcomeMessageModel } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -90,13 +91,6 @@ configurationRegistry.registerConfiguration({ ], description: nls.localize('interactiveSession.defaultMode', "Controls the default mode of the chat experience."), default: 'chatView' - }, - 'chat.experimental.quickQuestion.mode': { - type: 'string', - tags: ['experimental'], - enum: [QuickQuestionMode.SingleQuestion, QuickQuestionMode.InputOnTopChat, QuickQuestionMode.InputOnBottomChat], - description: nls.localize('interactiveSession.quickQuestion.mode', "Controls the mode of quick question chat experience."), - default: QuickQuestionMode.InputOnTopChat, } } }); @@ -171,8 +165,19 @@ class ChatAccessibleViewContribution extends Disposable { } widget.focus(focusedItem); - - const responseContent = isResponseVM(focusedItem) ? focusedItem.response.value : undefined; + const isWelcome = focusedItem instanceof ChatWelcomeMessageModel; + let responseContent = isResponseVM(focusedItem) ? focusedItem.response.value : undefined; + if (isWelcome) { + const welcomeReplyContents = []; + for (const content of focusedItem.content) { + if (Array.isArray(content)) { + welcomeReplyContents.push(...content.map(m => m.message)); + } else { + welcomeReplyContents.push((content as IMarkdownString).value); + } + } + responseContent = welcomeReplyContents.join('\n'); + } if (!responseContent) { return false; } @@ -182,7 +187,7 @@ class ChatAccessibleViewContribution extends Disposable { accessibleViewService.show({ verbositySettingKey: AccessibilityVerbositySettingId.Chat, - provideContent(): string { return responseContent; }, + provideContent(): string { return responseContent!; }, onClose() { verifiedWidget.reveal(focusedItem); if (chatInputFocused) { diff --git a/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts b/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts index 026abbda6f0..71a9a649c0c 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAccessibilityService.ts @@ -30,7 +30,7 @@ export class ChatAccessibilityService extends Disposable implements IChatAccessi }, CHAT_RESPONSE_PENDING_ALLOWANCE_MS)); } acceptRequest(): void { - this._audioCueService.playAudioCue(AudioCue.chatRequestSent, true); + this._audioCueService.playAudioCue(AudioCue.chatRequestSent, { allowManyInParallel: true }); this._runOnceScheduler.schedule(); } acceptResponse(response?: IChatResponseViewModel | string): void { @@ -42,7 +42,7 @@ export class ChatAccessibilityService extends Disposable implements IChatAccessi if (this._lastResponse === responseContent) { return; } - this._audioCueService.playAudioCue(AudioCue.chatResponseReceived, true); + this._audioCueService.playAudioCue(AudioCue.chatResponseReceived, { allowManyInParallel: true }); this._hasReceivedRequest = false; if (!response) { return; diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 1eedf35a974..110d886d76d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -555,6 +555,17 @@ export class ChatAccessibilityProvider implements IListAccessibilityProvider followup.title).join(', ')); + } const codeBlockCount = marked.lexer(element.response.value).filter(token => token.type === 'code')?.length ?? 0; switch (codeBlockCount) { case 0: @@ -567,7 +578,7 @@ export class ChatAccessibilityProvider implements IListAccessibilityProvider(TerminalContextKeys.accessibleBufferFocus.key) === true) { + this._commandService.executeCommand(TerminalCommandId.FocusAccessibleBuffer); + } else { + this._instance.focus(); + } this.dispose(); } options: IAccessibleViewOptions = { @@ -42,7 +49,9 @@ export class TerminalAccessibleContentProvider extends Disposable implements IAc _xterm: Pick & { raw: Terminal }, @IInstantiationService _instantiationService: IInstantiationService, @IKeybindingService private readonly _keybindingService: IKeybindingService, - @IAccessibilityService private readonly _accessibilityService: IAccessibilityService + @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, + @IContextKeyService private readonly _contextKeyService: IContextKeyService, + @ICommandService private readonly _commandService: ICommandService ) { super(); this._hasShellIntegration = _xterm.shellIntegration.status === ShellIntegrationStatus.VSCode; diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBuffer.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBuffer.ts index 27ffd2db8cb..18a05b903cf 100644 --- a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBuffer.ts +++ b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleBuffer.ts @@ -147,7 +147,7 @@ export class AccessibleBufferWidget extends TerminalAccessibleWidget { return; } if (activeItem.exitCode) { - this._audioCueService.playAudioCue(AudioCue.error, true); + this._audioCueService.playAudioCue(AudioCue.error, { allowManyInParallel: true, source: 'accessibleBufferWidget' }); } this.editorWidget.revealLine(activeItem.lineNumber, 0); }); diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index ebd4e12e109..5080d93ece2 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -489,9 +489,11 @@ class ResultSummaryView extends Disposable { const { results } = this.resultService; const { count, root, status, duration, rerun } = this.elements; if (!results.length) { - this.container.removeChild(root); this.container.innerText = localize('noResults', 'No test results yet.'); - this.elementsWereAttached = false; + if (this.elementsWereAttached) { + this.container.removeChild(root); + this.elementsWereAttached = false; + } return; } diff --git a/src/vs/workbench/services/editor/browser/editorResolverService.ts b/src/vs/workbench/services/editor/browser/editorResolverService.ts index 18b9a9559e7..f30f71bd5bc 100644 --- a/src/vs/workbench/services/editor/browser/editorResolverService.ts +++ b/src/vs/workbench/services/editor/browser/editorResolverService.ts @@ -255,7 +255,9 @@ export class EditorResolverService extends Disposable implements IEditorResolver getAssociationsForResource(resource: URI): EditorAssociations { const associations = this.getAllUserAssociations(); - const matchingAssociations = associations.filter(association => association.filenamePattern && globMatchesResource(association.filenamePattern, resource)); + let matchingAssociations = associations.filter(association => association.filenamePattern && globMatchesResource(association.filenamePattern, resource)); + // Sort matching associations based on glob length as a longer glob will be more specific + matchingAssociations = matchingAssociations.sort((a, b) => (b.filenamePattern?.length ?? 0) - (a.filenamePattern?.length ?? 0)); const allEditors: RegisteredEditors = this._registeredEditors; // Ensure that the settings are valid editors return matchingAssociations.filter(association => allEditors.find(c => c.editorInfo.id === association.viewType)); diff --git a/src/vs/workbench/services/secrets/electron-sandbox/secretStorageService.ts b/src/vs/workbench/services/secrets/electron-sandbox/secretStorageService.ts index b22b1cedca6..70ae34239f7 100644 --- a/src/vs/workbench/services/secrets/electron-sandbox/secretStorageService.ts +++ b/src/vs/workbench/services/secrets/electron-sandbox/secretStorageService.ts @@ -43,7 +43,7 @@ export class NativeSecretStorageService extends BaseSecretStorageService { this._sequencer.queue(key, async () => { await this.resolvedStorageService; - if (this.type !== 'persisted') { + if (this.type !== 'persisted' && !this._environmentService.disableKeytar) { this._logService.trace('[NativeSecretStorageService] Notifying user that secrets are not being stored on disk.'); await this.notifyOfNoEncryptionOnce(); } diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index 8e9c1cef485..39a2cd09720 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -15980,7 +15980,7 @@ declare module 'vscode' { * @param id The unique identifier of the provider. * @param label The human-readable name of the provider. * @param provider The authentication provider provider. - * @params options Additional options for the provider. + * @param options Additional options for the provider. * @return A {@link Disposable} that unregisters this provider when being disposed. */ export function registerAuthenticationProvider(id: string, label: string, provider: AuthenticationProvider, options?: AuthenticationProviderOptions): Disposable;