diff --git a/.vscode/notebooks/my-endgame.github-issues b/.vscode/notebooks/my-endgame.github-issues index 96c780e5a6f..6978af0b6f0 100644 --- a/.vscode/notebooks/my-endgame.github-issues +++ b/.vscode/notebooks/my-endgame.github-issues @@ -177,7 +177,7 @@ { "kind": 2, "language": "github-issues", - "value": "$REPOS $MILESTONE -$MINE is:issue label:bug label:verification-steps-needed" + "value": "$REPOS $MILESTONE -$MINE is:issue label:bug label:verification-steps-needed -label:verified" }, { "kind": 1, diff --git a/.yarnrc b/.yarnrc index 8f658afd4a9..fff0be195f2 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,5 +1,5 @@ disturl "https://electronjs.org/headers" -target "25.5.0" -ms_build_id "23084831" +target "25.8.0" +ms_build_id "23503258" runtime "electron" build_from_source "true" diff --git a/build/checksums/electron.txt b/build/checksums/electron.txt index f5c22bc1c23..9c46b2ad8ef 100644 --- a/build/checksums/electron.txt +++ b/build/checksums/electron.txt @@ -1,27 +1,27 @@ -c3fb8cb4804143eb25fce55a179a6f2df8c215ed709104ec235c96e357b20f42 *electron-v25.5.0-darwin-arm64-symbols.zip -e6d2a09348d4fe7c9fbd92bf796489a95e625642f0f1ce96169212554cfa6841 *electron-v25.5.0-darwin-arm64.zip -15c28e613dfee0f7e46a296bb4aed64e17f9644d7ef19129aeb6480b8da230ab *electron-v25.5.0-darwin-x64-symbols.zip -a5c5c0b621daf8242258c89edb2387fbdf1c69125c984f8564d89b87927373e3 *electron-v25.5.0-darwin-x64.zip -69d60a69b7f692b9069cdf9e518bcbaca9cec561f40199cc87196929936a34ce *electron-v25.5.0-linux-arm64-symbols.zip -adec2ba09faf5f8d8af8997c8bed43c7712eba5546db1e7d06f8357bf4613921 *electron-v25.5.0-linux-arm64.zip -0fe7a5d152c9c401671e02ebcd9da34ab9bb0c28598ede3657077a79f8b3e70a *electron-v25.5.0-linux-armv7l-symbols.zip -eccb66e4a308a0bd2d90474894370e3d687e32f78672e6b5077c1822c7bc526b *electron-v25.5.0-linux-armv7l.zip -82bd9bc9e66f8ae802fe48a51b8d7e2fb599403e9715fa4b859190200a7376b1 *electron-v25.5.0-linux-x64-symbols.zip -485cbeb206fccfb4ed42f694100eebb80c9db6639b3537a95823c4fcb7f210cd *electron-v25.5.0-linux-x64.zip -ed2c2a7da571b53bcb336b9a2a024753a272df82ece45df83df888df51bf1912 *electron-v25.5.0-win32-arm64-pdb.zip -c316f6364e9b4cd61e19d4763c96abda7247a2c31ae7d30e81219c9a7754f11a *electron-v25.5.0-win32-arm64-symbols.zip -582ccbfc5a85a093f5639ee476bb5fef18c2d25dab4a60e5f5cb47e64c99f7fb *electron-v25.5.0-win32-arm64.zip -5776e650c23e3847b0c52d850f61c57a84a4fa30943c3cc82197250112911b5b *electron-v25.5.0-win32-ia32-pdb.zip -730b429ad2c4cae0fe3ba9600a6ee86c79dada77976bac1c75ca2404993495bc *electron-v25.5.0-win32-ia32-symbols.zip -7e6e68aa33a89c0d647575b06daf415a2401feb170ebb9cd795e221af321f751 *electron-v25.5.0-win32-ia32.zip -0792665fda9255b340a829cdd24601887a2ca8f04cc49f9a6d4557db0c0cd2f2 *electron-v25.5.0-win32-x64-pdb.zip -dc2459546951f8418e866857e9111dd83a0789d401906b2200c2f8dd59d9146b *electron-v25.5.0-win32-x64-symbols.zip -9bf7980fbc024ba77ea8ab3e2d32088a5f69bf32506a7d2db72ede17028abdf4 *electron-v25.5.0-win32-x64.zip -5b1ea601b737842eacf88d7456c4d14e697822753e9a08ede889cce9e20bafb2 *ffmpeg-v25.5.0-darwin-arm64.zip -37bf5c75edefc0b6735b44d0b5b06fdd427179a8501f6e93df9840283cdb4a95 *ffmpeg-v25.5.0-darwin-x64.zip -bd52d57ff97fb56ac01a3482af905d04f0d4e9c13c53858c6d9f99957eca82da *ffmpeg-v25.5.0-linux-arm64.zip -9b3d09177fa1e63e2a6beecfa70aeec30aeb5c1873ff21128a68051c4e23f95d *ffmpeg-v25.5.0-linux-armv7l.zip -edc7b1c9f1a0733f109a2c0375a4e40c5bfe0bf28b7f06dcc76e1ada0aa2f125 *ffmpeg-v25.5.0-linux-x64.zip -2e28767b3570ea247869a20988cddd23af710eb994d6099404f123390cedeba6 *ffmpeg-v25.5.0-win32-arm64.zip -715568eefd7267573a30186ade3de901587baeb1f013200d8ae50b35941b613f *ffmpeg-v25.5.0-win32-ia32.zip -29876504452aaa505f696642178968e24b8dc8cc4b055071e6f0f3f073088acc *ffmpeg-v25.5.0-win32-x64.zip +88cafda8394985e59d3d84cb4a6692ad04d8e32db9ecd6429e748e41526ddad7 *electron-v25.8.0-darwin-arm64-symbols.zip +6e33d3b8041561722ed41777e055a8c15d3f4e61b67367b2618918bcf0cfea76 *electron-v25.8.0-darwin-arm64.zip +438ac9915e062a239fb6d2595323c4783d2c820efc9cbcf3d2c1253d0e057e83 *electron-v25.8.0-darwin-x64-symbols.zip +798907d2a66bc79202c8213c61e7fd147ae2a8c31c485d814950b11d43bbbba8 *electron-v25.8.0-darwin-x64.zip +3243f3764319cff6c942d9f90a86323c36ec05ec51ef01e782c4e9a7194187e1 *electron-v25.8.0-linux-arm64-symbols.zip +f24f858b76bf8a2e18419f62e0f891712b2fa541089123e9caa8d5cd67fc3276 *electron-v25.8.0-linux-arm64.zip +dc3ff0489a0ebeda56d06b31eeae75dd7321a52bb601069c4475c56462b4814a *electron-v25.8.0-linux-armv7l-symbols.zip +3b7a0c3899f828a5cf30043b73992e90231400b90c1afa700a44f892a55e326b *electron-v25.8.0-linux-armv7l.zip +44803b2487406eca8fff9cec405e9e50bd92a911808dfaaa523b9ef52a0e72d8 *electron-v25.8.0-linux-x64-symbols.zip +d54fb2df0ad7318240220aa26327171ed1e891fb296f3c27c58b8b487c4df8eb *electron-v25.8.0-linux-x64.zip +bf7be6c0c8d0df06f0ce22e16c97aea823415d7f5cbf0ffdadf65d75feaf3cd8 *electron-v25.8.0-win32-arm64-pdb.zip +5d91757660b44bf30907f9c2b52225ade4d127d0fe48dc83dec134cc06c949f0 *electron-v25.8.0-win32-arm64-symbols.zip +d1e6f30a8d8c7aed28d08ddf915d79de6b16b3a0a7c84c45fd3cc0d47f2b7f53 *electron-v25.8.0-win32-arm64.zip +e389fef61c14ea0eefad91a9725aa0afd4dbdc982f7b30aba97bd9c2871c2061 *electron-v25.8.0-win32-ia32-pdb.zip +374d6c8897f97fab04e990ecf928e05f643ae33801546bf7d39bf4045b9d8b52 *electron-v25.8.0-win32-ia32-symbols.zip +73fc3382202b70dcaf7928f09a791662de82c701b8f403ed72cc5aa9b1401593 *electron-v25.8.0-win32-ia32.zip +010d248bd2e77585e1fa977e58b016659566de5a91c1e6845c85a7e6e1851bb9 *electron-v25.8.0-win32-x64-pdb.zip +72adb74fd92edff35c177c3c5d96765f230bc7adb8af11b30d5122b9e54c26e1 *electron-v25.8.0-win32-x64-symbols.zip +0051d0f241aedc6cdab4751c60f48758936122796f06c9e3033c7710a531686c *electron-v25.8.0-win32-x64.zip +2956915642c45eb0099228368d0af50e891e4c10014fa4d3d3bcfb135fbb89a7 *ffmpeg-v25.8.0-darwin-arm64.zip +099ee69d44f8ac3802cdd612895f279f7adb043a5b9c9d123479b0f96514a44c *ffmpeg-v25.8.0-darwin-x64.zip +bd52d57ff97fb56ac01a3482af905d04f0d4e9c13c53858c6d9f99957eca82da *ffmpeg-v25.8.0-linux-arm64.zip +9b3d09177fa1e63e2a6beecfa70aeec30aeb5c1873ff21128a68051c4e23f95d *ffmpeg-v25.8.0-linux-armv7l.zip +edc7b1c9f1a0733f109a2c0375a4e40c5bfe0bf28b7f06dcc76e1ada0aa2f125 *ffmpeg-v25.8.0-linux-x64.zip +a58e9480dab981ff973749e9d1e08936b2dd63a4b7f9523c030b1833387a4eb5 *ffmpeg-v25.8.0-win32-arm64.zip +6866b23a4d561c0322aeb7690aae646718c54398739946e352bf80d0dd721bfd *ffmpeg-v25.8.0-win32-ia32.zip +7b906df4ad6252881cf1e58619285b624f74d593379fbc6728e238b852d6abad *ffmpeg-v25.8.0-win32-x64.zip diff --git a/cgmanifest.json b/cgmanifest.json index 574ec75d24d..6b2d2bfff44 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -528,12 +528,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "34be316c404e84cdd967fa0e10fceeb6424eed90" + "commitHash": "84d7f7f071ae11637d4a41b95536410293672750" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "25.5.0" + "version": "25.8.0" }, { "component": { diff --git a/cli/src/commands/args.rs b/cli/src/commands/args.rs index bfa1c6f2da4..cbc33fcb071 100644 --- a/cli/src/commands/args.rs +++ b/cli/src/commands/args.rs @@ -52,6 +52,7 @@ const VERSION: &str = concatcp!(NUMBER_IN_VERSION, " (commit ", COMMIT_IN_VERSIO #[clap( help_template = INTEGRATED_TEMPLATE, long_about = None, + name = constants::APPLICATION_NAME, version = VERSION, )] pub struct IntegratedCli { @@ -84,6 +85,7 @@ pub struct CliCore { help_template = STANDALONE_TEMPLATE, long_about = None, version = VERSION, + name = constants::APPLICATION_NAME, )] pub struct StandaloneCli { #[clap(flatten)] @@ -173,6 +175,7 @@ pub enum Commands { Version(VersionArgs), /// Runs a local web version of VS Code. + #[clap(about = concatcp!("Runs a local web version of ", constants::PRODUCT_NAME_LONG))] ServeWeb(ServeWebArgs), /// Runs the control server on process stdin/stdout diff --git a/cli/src/commands/serve_web.rs b/cli/src/commands/serve_web.rs index 8d37427dd33..2181c6339a4 100644 --- a/cli/src/commands/serve_web.rs +++ b/cli/src/commands/serve_web.rs @@ -25,6 +25,7 @@ use crate::download_cache::DownloadCache; use crate::log; use crate::options::Quality; use crate::state::{LauncherPaths, PersistedState}; +use crate::tunnels::shutdown_signal::ShutdownRequest; use crate::update_service::{ unzip_downloaded_release, Platform, Release, TargetKind, UpdateService, }; @@ -98,12 +99,20 @@ pub async fn serve_web(ctx: CommandContext, mut args: ServeWebArgs) -> Result(service) } }; + let mut shutdown = ShutdownRequest::create_rx([ShutdownRequest::CtrlC]); let r = if let Some(s) = args.socket_path { - let socket = listen_socket_rw_stream(&PathBuf::from(&s)).await?; - ctx.log.result(format!("Web UI available on {}", s)); - Server::builder(socket.into_pollable()) + let s = PathBuf::from(&s); + let socket = listen_socket_rw_stream(&s).await?; + ctx.log + .result(format!("Web UI available on {}", s.display())); + let r = Server::builder(socket.into_pollable()) .serve(make_service_fn(|_| make_svc())) - .await + .with_graceful_shutdown(async { + let _ = shutdown.wait().await; + }) + .await; + let _ = std::fs::remove_file(&s); // cleanup + r } else { let addr: SocketAddr = match &args.host { Some(h) => { @@ -120,6 +129,9 @@ pub async fn serve_web(ctx: CommandContext, mut args: ServeWebArgs) -> Result ServerBuilder<'a> { ) .await?; - unzip_downloaded_release( - &archive_path, - &target_dir.join(SERVER_FOLDER_NAME), - SilentCopyProgress(), - )?; + let server_dir = target_dir.join(SERVER_FOLDER_NAME); + unzip_downloaded_release(&archive_path, &server_dir, SilentCopyProgress())?; + + let output = capture_command_and_check_status( + server_dir + .join("bin") + .join(self.server_params.release.quality.server_entrypoint()), + &["--version"], + ) + .await + .map_err(|e| wrap(e, "error checking server integrity"))?; + + trace!( + self.logger, + "Server integrity verified, version: {}", + String::from_utf8_lossy(&output.stdout).replace('\n', " / ") + ); Ok(()) }) diff --git a/cli/src/tunnels/dev_tunnels.rs b/cli/src/tunnels/dev_tunnels.rs index b77f6da5f2e..e7b84ed4112 100644 --- a/cli/src/tunnels/dev_tunnels.rs +++ b/cli/src/tunnels/dev_tunnels.rs @@ -212,6 +212,7 @@ impl ActiveTunnel { const VSCODE_CLI_TUNNEL_TAG: &str = "vscode-server-launcher"; const VSCODE_CLI_FORWARDING_TAG: &str = "vscode-port-forward"; +const OWNED_TUNNEL_TAGS: &[&str] = &[VSCODE_CLI_TUNNEL_TAG, VSCODE_CLI_FORWARDING_TAG]; const MAX_TUNNEL_NAME_LENGTH: usize = 20; fn get_host_token_from_tunnel(tunnel: &Tunnel) -> String { @@ -635,7 +636,7 @@ impl DevTunnels { "Tunnel limit hit, trying to recycle an old tunnel" ); - let existing_tunnels = self.list_all_server_tunnels().await?; + let existing_tunnels = self.list_tunnels_with_tag(OWNED_TUNNEL_TAGS).await?; let recyclable = existing_tunnels .iter() @@ -667,13 +668,15 @@ impl DevTunnels { } } - async fn list_all_server_tunnels(&mut self) -> Result, AnyError> { + async fn list_tunnels_with_tag( + &mut self, + tags: &[&'static str], + ) -> Result, AnyError> { let tunnels = spanf!( self.log, self.log.span("dev-tunnel.listall"), self.client.list_all_tunnels(&TunnelRequestOptions { - tags: vec![self.tag.to_string()], - require_all_tags: true, + tags: tags.iter().map(|t| t.to_string()).collect(), ..Default::default() }) ) @@ -711,7 +714,7 @@ impl DevTunnels { preferred_name: Option<&str>, mut use_random_name: bool, ) -> Result { - let existing_tunnels = self.list_all_server_tunnels().await?; + let existing_tunnels = self.list_tunnels_with_tag(&[self.tag]).await?; let is_name_free = |n: &str| { !existing_tunnels.iter().any(|v| { v.status diff --git a/extensions/git/package.json b/extensions/git/package.json index 105491916ca..37212410bfe 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -2950,13 +2950,13 @@ { "view": "scm", "contents": "%view.workbench.scm.folder%", - "when": "config.git.enabled && !git.missing && git.state == initialized && workbenchState == folder && scmRepositoryCount == 0 && git.parentRepositoryCount == 0 && git.unsafeRepositoryCount == 0 && git.closedRepositoryCount == 0 && remoteName != 'codespaces'", + "when": "config.git.enabled && !git.missing && git.state == initialized && workbenchState == folder && scm.providerCount == 0 && git.parentRepositoryCount == 0 && git.unsafeRepositoryCount == 0 && git.closedRepositoryCount == 0 && remoteName != 'codespaces'", "group": "5_scm@1" }, { "view": "scm", "contents": "%view.workbench.scm.workspace%", - "when": "config.git.enabled && !git.missing && git.state == initialized && workbenchState == workspace && workspaceFolderCount != 0 && scmRepositoryCount == 0 && git.parentRepositoryCount == 0 && git.unsafeRepositoryCount == 0 && git.closedRepositoryCount == 0 && remoteName != 'codespaces'", + "when": "config.git.enabled && !git.missing && git.state == initialized && workbenchState == workspace && workspaceFolderCount != 0 && scm.providerCount == 0 && git.parentRepositoryCount == 0 && git.unsafeRepositoryCount == 0 && git.closedRepositoryCount == 0 && remoteName != 'codespaces'", "group": "5_scm@1" }, { @@ -2992,13 +2992,13 @@ { "view": "explorer", "contents": "%view.workbench.cloneRepository%", - "when": "config.git.enabled && git.state == initialized && scmRepositoryCount == 0", + "when": "config.git.enabled && git.state == initialized && scm.providerCount == 0", "group": "5_scm@1" }, { "view": "explorer", "contents": "%view.workbench.learnMore%", - "when": "config.git.enabled && git.state == initialized && scmRepositoryCount == 0", + "when": "config.git.enabled && git.state == initialized && scm.providerCount == 0", "group": "5_scm@10" } ] diff --git a/extensions/github-authentication/src/flows.ts b/extensions/github-authentication/src/flows.ts index 5bc9d095385..1e988d92d30 100644 --- a/extensions/github-authentication/src/flows.ts +++ b/extensions/github-authentication/src/flows.ts @@ -200,7 +200,9 @@ const allFlows: IFlow[] = [ // other flows that work well. supportsGitHubEnterpriseServer: false, supportsHostedGitHubEnterprise: true, - supportsRemoteExtensionHost: true, + // Opening a port on the remote side can't be open in the browser on + // the client side so this flow won't work in remote extension hosts + supportsRemoteExtensionHost: false, // Web worker can't open a port to listen for the redirect supportsWebWorkerExtensionHost: false, // exchanging a code for a token requires a client secret diff --git a/extensions/ipynb/package.json b/extensions/ipynb/package.json index f5e25ac3695..25d08040183 100644 --- a/extensions/ipynb/package.json +++ b/extensions/ipynb/package.json @@ -58,8 +58,8 @@ "title": "%cleanInvalidImageAttachment.title%" }, { - "command": "notebook.cellOutput.copyToClipboard", - "title": "%copyOutputToClipboard.title%" + "command": "notebook.cellOutput.copy", + "title": "%copyCellOutput.title%" } ], "notebooks": [ @@ -106,7 +106,7 @@ ], "webview/context": [ { - "command": "notebook.cellOutput.copyToClipboard", + "command": "notebook.cellOutput.copy", "when": "webviewId == 'notebook.output' && webviewSection == 'image'" } ] diff --git a/extensions/ipynb/package.nls.json b/extensions/ipynb/package.nls.json index 45aa2aa03e8..1f281c32dd3 100644 --- a/extensions/ipynb/package.nls.json +++ b/extensions/ipynb/package.nls.json @@ -6,7 +6,7 @@ "newUntitledIpynb.shortTitle": "Jupyter Notebook", "openIpynbInNotebookEditor.title": "Open IPYNB File In Notebook Editor", "cleanInvalidImageAttachment.title": "Clean Invalid Image Attachment Reference", - "copyOutputToClipboard.title": "Copy Output to Clipboard", + "copyCellOutput.title": "Copy Output", "markdownAttachmentRenderer.displayName": { "message": "Markdown-It ipynb Cell Attachment renderer", "comment": [ diff --git a/extensions/tunnel-forwarding/src/extension.ts b/extensions/tunnel-forwarding/src/extension.ts index 3dd6fdfef19..83789934df5 100644 --- a/extensions/tunnel-forwarding/src/extension.ts +++ b/extensions/tunnel-forwarding/src/extension.ts @@ -25,7 +25,7 @@ const cliPath = process.env.VSCODE_FORWARDING_IS_DEV ? path.join(__dirname, '../../../cli/target/debug/code') : path.join( vscode.env.appRoot, - process.platform === 'win32' ? '../../bin' : 'bin', + process.platform === 'darwin' ? 'bin' : '../../bin', vscode.env.appQuality === 'stable' ? 'code-tunnel' : 'code-tunnel-insiders', ) + (process.platform === 'win32' ? '.exe' : ''); diff --git a/extensions/tunnel-forwarding/src/split.ts b/extensions/tunnel-forwarding/src/split.ts index 6e9d7474604..33ad055ac67 100644 --- a/extensions/tunnel-forwarding/src/split.ts +++ b/extensions/tunnel-forwarding/src/split.ts @@ -9,6 +9,8 @@ export const splitNewLines = () => new StreamSplitter('\n'.charCodeAt(0)); /** * Copied and simplified from src\vs\base\node\nodeStreams.ts + * + * Exception: does not include the split character in the output. */ export class StreamSplitter extends Transform { private buffer: Buffer | undefined; @@ -31,7 +33,7 @@ export class StreamSplitter extends Transform { break; } - this.push(this.buffer.subarray(offset, index + 1)); + this.push(this.buffer.subarray(offset, index)); offset = index + 1; } diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts index 6e0c8e59404..e69eecff5d1 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts @@ -977,6 +977,22 @@ suite('vscode API - workspace', () => { assert.strictEqual(document.getText(), expected); }); + + test('[Bug] Failed to create new test file when in an untitled file #1261', async function () { + const uri = vscode.Uri.parse('untitled:Untitled-5.test'); + const contents = `Hello Test File ${uri.toString()}`; + const we = new vscode.WorkspaceEdit(); + we.createFile(uri, { ignoreIfExists: true }); + we.replace(uri, new vscode.Range(0, 0, 0, 0), contents); + + const success = await vscode.workspace.applyEdit(we); + + assert.ok(success); + + const doc = await vscode.workspace.openTextDocument(uri); + assert.strictEqual(doc.getText(), contents); + }); + test('Should send a single FileWillRenameEvent instead of separate events when moving multiple files at once#111867, 1/3', async function () { const file1 = await createRandomFile(); diff --git a/package.json b/package.json index 99b6034460c..1f473f744fc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", - "version": "1.82.0", - "distro": "49cc0fbc0a8e222bcce2a7c3bf62e0d23f20d258", + "version": "1.83.0", + "distro": "0a5805caff2d59440704a3bf75eebaa509be862f", "author": { "name": "Microsoft Corporation" }, @@ -150,7 +150,7 @@ "cssnano": "^4.1.11", "debounce": "^1.0.0", "deemon": "^1.8.0", - "electron": "25.5.0", + "electron": "25.8.0", "eslint": "8.36.0", "eslint-plugin-header": "3.1.1", "eslint-plugin-jsdoc": "^46.5.0", diff --git a/product.json b/product.json index 2ae3ee8f0f8..acb24971c39 100644 --- a/product.json +++ b/product.json @@ -52,8 +52,8 @@ }, { "name": "ms-vscode.js-debug", - "version": "1.81.0", - "sha256": "6d1c7ee89881afd65e8fee47445b6a1c5fb345bf30e2bdf70cd2fdd8d1ff6dec", + "version": "1.82.0", + "sha256": "4fba41b4b764c3f5a6591d6d9a5bdc59b417f2d799071c889c2b54163f256282", "repo": "https://github.com/microsoft/vscode-js-debug", "metadata": { "id": "25629058-ddac-4e17-abba-74678e126c5d", diff --git a/remote/.yarnrc b/remote/.yarnrc index c4421581246..26dc815d0f8 100644 --- a/remote/.yarnrc +++ b/remote/.yarnrc @@ -1,5 +1,5 @@ disturl "https://nodejs.org/dist" target "18.15.0" -ms_build_id "223745" +ms_build_id "229541" runtime "node" build_from_source "true" diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf index b5623b0a55c..91105610d11 100644 Binary files a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf and b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf differ diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index ee900068b4b..262d3ca9e2e 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -532,13 +532,16 @@ class TypeNavigationController implements IDisposable { const prefix = matchesPrefix(word, labelStr); const fuzzy = matchesFuzzy2(word, labelStr); - // ensures that when fuzzy matching, it doesn't clash with prefix matching (1 input vs 1+ should be prefix and fuzzy respecitvely) - const fuzzyScore = fuzzy ? fuzzy[0].end - fuzzy[0].start : 0; - if (prefix || fuzzyScore > 1) { - this.previouslyFocused = start; - this.list.setFocus([index]); - this.list.reveal(index); - return; + if (fuzzy) { + const fuzzyScore = fuzzy[0].end - fuzzy[0].start; + + // ensures that when fuzzy matching, doesn't clash with prefix matching (1 input vs 1+ should be prefix and fuzzy respecitvely). Also makes sure that exact matches are prioritized. + if (prefix || (fuzzyScore > 1 && fuzzy.length === 1)) { + this.previouslyFocused = start; + this.list.setFocus([index]); + this.list.reveal(index); + return; + } } } } else { @@ -729,10 +732,6 @@ export class MouseController implements IDisposable { return; } - if (this.isSelectionRangeChangeEvent(e)) { - return this.changeSelection(e); - } - if (this.isSelectionChangeEvent(e)) { return this.changeSelection(e); } diff --git a/src/vs/base/common/arrays.ts b/src/vs/base/common/arrays.ts index 22c2859f5e9..94966dc1b6f 100644 --- a/src/vs/base/common/arrays.ts +++ b/src/vs/base/common/arrays.ts @@ -666,6 +666,10 @@ export namespace CompareResult { return result < 0; } + export function isLessThanOrEqual(result: CompareResult): boolean { + return result <= 0; + } + export function isGreaterThan(result: CompareResult): boolean { return result > 0; } diff --git a/src/vs/base/common/codicons.ts b/src/vs/base/common/codicons.ts index 48dd3abfb72..0c5fd5a01e8 100644 --- a/src/vs/base/common/codicons.ts +++ b/src/vs/base/common/codicons.ts @@ -567,6 +567,7 @@ export const Codicon = { send: register('send', 0xec0f), sparkle: register('sparkle', 0xec10), insert: register('insert', 0xec11), + mic: register('mic', 0xec12), // derived icons, that could become separate icons diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index 52a4b2d20f6..c26c0271f39 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -557,6 +557,24 @@ export namespace Event { return new Promise(resolve => once(event)(resolve)); } + /** + * Creates an event out of a promise that fires once when the promise is + * resolved with the result of the promise or `undefined`. + */ + export function fromPromise(promise: Promise): Event { + const result = new Emitter(); + + promise.then(res => { + result.fire(res); + }, () => { + result.fire(undefined); + }).finally(() => { + result.dispose(); + }); + + return result.event; + } + /** * Adds a listener to an event and calls the listener immediately with undefined as the event object. * diff --git a/src/vs/base/test/common/event.test.ts b/src/vs/base/test/common/event.test.ts index 8f14d7d47cd..30320370f2a 100644 --- a/src/vs/base/test/common/event.test.ts +++ b/src/vs/base/test/common/event.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import { stub } from 'sinon'; -import { timeout } from 'vs/base/common/async'; +import { DeferredPromise, timeout } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { errorHandler, setUnexpectedErrorHandler } from 'vs/base/common/errors'; import { AsyncEmitter, DebounceEmitter, DynamicListEventMultiplexer, Emitter, Event, EventBufferer, EventMultiplexer, IWaitUntil, MicrotaskEmitter, PauseableEmitter, Relay, createEventDeliveryQueue } from 'vs/base/common/event'; @@ -1158,6 +1158,48 @@ suite('Event utils', () => { listener.dispose(); // should not crash }); + suite('fromPromise', () => { + + test('not yet resolved', async function () { + return new Promise(resolve => { + let promise = new DeferredPromise(); + + Event.fromPromise(promise.p)(e => { + assert.strictEqual(e, 1); + + promise = new DeferredPromise(); + + Event.fromPromise(promise.p)(() => { + resolve(); + }); + + promise.error(undefined); + }); + + promise.complete(1); + }); + }); + + test('already resolved', async function () { + return new Promise(resolve => { + let promise = new DeferredPromise(); + promise.complete(1); + + Event.fromPromise(promise.p)(e => { + assert.strictEqual(e, 1); + + promise = new DeferredPromise(); + promise.error(undefined); + + Event.fromPromise(promise.p)(() => { + resolve(); + }); + }); + + }); + }); + }); + suite('Relay', () => { test('should input work', () => { const e1 = new Emitter(); diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts index 029cf8b10e7..962e2f4c314 100644 --- a/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts @@ -17,6 +17,7 @@ import product from 'vs/platform/product/common/product'; import { ISecretStorageProvider } from 'vs/platform/secrets/common/secrets'; import { isFolderToOpen, isWorkspaceToOpen } from 'vs/platform/window/common/window'; import type { IWorkbenchConstructionOptions } from 'vs/workbench/browser/web.api'; +import { AuthenticationSessionInfo } from 'vs/workbench/services/authentication/browser/authenticationService'; import type { IWorkspace, IWorkspaceProvider } from 'vs/workbench/services/host/browser/browserHostService'; import type { IURLCallbackProvider } from 'vs/workbench/services/url/browser/urlService'; import { create } from 'vs/workbench/workbench.web.main'; @@ -176,11 +177,13 @@ export class LocalStorageSecretStorageProvider implements ISecretStorageProvider ) { } private async load(): Promise> { + const record = this.loadAuthSessionFromElement(); // Get the secrets from localStorage const encrypted = window.localStorage.getItem(this._storageKey); if (encrypted) { try { - return JSON.parse(await this.crypto.unseal(encrypted)); + const decrypted = JSON.parse(await this.crypto.unseal(encrypted)); + return { ...record, ...decrypted }; } catch (err) { // TODO: send telemetry console.error('Failed to decrypt secrets from localStorage', err); @@ -188,7 +191,42 @@ export class LocalStorageSecretStorageProvider implements ISecretStorageProvider } } - return {}; + return record; + } + + private loadAuthSessionFromElement(): Record { + let authSessionInfo: (AuthenticationSessionInfo & { scopes: string[][] }) | undefined; + const authSessionElement = document.getElementById('vscode-workbench-auth-session'); + const authSessionElementAttribute = authSessionElement ? authSessionElement.getAttribute('data-settings') : undefined; + if (authSessionElementAttribute) { + try { + authSessionInfo = JSON.parse(authSessionElementAttribute); + } catch (error) { /* Invalid session is passed. Ignore. */ } + } + + if (!authSessionInfo) { + return {}; + } + + const record: Record = {}; + + // Settings Sync Entry + record[`${product.urlProtocol}.loginAccount`] = JSON.stringify(authSessionInfo); + + // Auth extension Entry + if (authSessionInfo.providerId !== 'github') { + console.error(`Unexpected auth provider: ${authSessionInfo.providerId}. Expected 'github'.`); + return record; + } + + const authAccount = JSON.stringify({ extensionId: 'vscode.github-authentication', key: 'github.auth' }); + record[authAccount] = JSON.stringify(authSessionInfo.scopes.map(scopes => ({ + id: authSessionInfo!.id, + scopes, + accessToken: authSessionInfo!.accessToken + }))); + + return record; } async get(key: string): Promise { diff --git a/src/vs/editor/browser/config/elementSizeObserver.ts b/src/vs/editor/browser/config/elementSizeObserver.ts index 3529822e209..f6b344f1872 100644 --- a/src/vs/editor/browser/config/elementSizeObserver.ts +++ b/src/vs/editor/browser/config/elementSizeObserver.ts @@ -41,12 +41,42 @@ export class ElementSizeObserver extends Disposable { public startObserving(): void { if (!this._resizeObserver && this._referenceDomElement) { - this._resizeObserver = new ResizeObserver((entries) => { - if (entries && entries[0] && entries[0].contentRect) { - this.observe({ width: entries[0].contentRect.width, height: entries[0].contentRect.height }); + // We want to react to the resize observer only once per animation frame + // The first time the resize observer fires, we will react to it immediately. + // Otherwise we will postpone to the next animation frame. + // We'll use `observeContentRect` to store the content rect we received. + + let observeContentRect: DOMRectReadOnly | null = null; + const observeNow = () => { + if (observeContentRect) { + this.observe({ width: observeContentRect.width, height: observeContentRect.height }); } else { this.observe(); } + }; + + let shouldObserve = false; + let alreadyObservedThisAnimationFrame = false; + + const update = () => { + if (shouldObserve && !alreadyObservedThisAnimationFrame) { + try { + shouldObserve = false; + alreadyObservedThisAnimationFrame = true; + observeNow(); + } finally { + requestAnimationFrame(() => { + alreadyObservedThisAnimationFrame = false; + update(); + }); + } + } + }; + + this._resizeObserver = new ResizeObserver((entries) => { + observeContentRect = (entries && entries[0] && entries[0].contentRect ? entries[0].contentRect : null); + shouldObserve = true; + update(); }); this._resizeObserver.observe(this._referenceDomElement); } diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorViewModel.ts b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorViewModel.ts index aff79983e40..860afa96659 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorViewModel.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget2/diffEditorViewModel.ts @@ -4,22 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import { RunOnceScheduler } from 'vs/base/common/async'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IObservable, IReader, ISettableObservable, ITransaction, autorunWithStore, derived, observableSignal, observableSignalFromEvent, observableValue, transaction, waitForState } from 'vs/base/common/observable'; -import { isDefined } from 'vs/base/common/types'; +import { readHotReloadableExport } from 'vs/editor/browser/widget/diffEditorWidget2/utils'; import { ISerializedLineRange, LineRange } from 'vs/editor/common/core/lineRange'; -import { Range } from 'vs/editor/common/core/range'; +import { AdvancedLinesDiffComputer } from 'vs/editor/common/diff/advancedLinesDiffComputer'; import { IDocumentDiff, IDocumentDiffProvider } from 'vs/editor/common/diff/documentDiffProvider'; -import { LineRangeMapping, MovedText, RangeMapping, SimpleLineRangeMapping } from 'vs/editor/common/diff/linesDiffComputer'; -import { AdvancedLinesDiffComputer, lineRangeMappingFromRangeMappings } from 'vs/editor/common/diff/advancedLinesDiffComputer'; +import { LineRangeMapping, MovedText } from 'vs/editor/common/diff/linesDiffComputer'; import { IDiffEditorModel, IDiffEditorViewModel } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; import { TextEditInfo } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/beforeEditPositionMapper'; import { combineTextEditInfos } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/combineTextEditInfos'; -import { lengthAdd, lengthDiffNonNegative, lengthGetLineCount, lengthOfRange, lengthToPosition, lengthZero, positionToLength } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/length'; import { DiffEditorOptions } from './diffEditorOptions'; -import { readHotReloadableExport } from 'vs/editor/browser/widget/diffEditorWidget2/utils'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; export class DiffEditorViewModel extends Disposable implements IDiffEditorViewModel { private readonly _isDiffUpToDate = observableValue('isDiffUpToDate', false); @@ -469,6 +466,9 @@ export class UnchangedRegion { } function applyOriginalEdits(diff: IDocumentDiff, textEdits: TextEditInfo[], originalTextModel: ITextModel, modifiedTextModel: ITextModel): IDocumentDiff | undefined { + return undefined; + /* + TODO@hediet if (textEdits.length === 0) { return diff; } @@ -478,9 +478,9 @@ function applyOriginalEdits(diff: IDocumentDiff, textEdits: TextEditInfo[], orig if (!diff3) { return undefined; } - return flip(diff3); + return flip(diff3);*/ } - +/* function flip(diff: IDocumentDiff): IDocumentDiff { return { changes: diff.changes.map(c => c.flip()), @@ -489,8 +489,11 @@ function flip(diff: IDocumentDiff): IDocumentDiff { quitEarly: diff.quitEarly, }; } - +*/ function applyModifiedEdits(diff: IDocumentDiff, textEdits: TextEditInfo[], originalTextModel: ITextModel, modifiedTextModel: ITextModel): IDocumentDiff | undefined { + return undefined; + /* + TODO@hediet if (textEdits.length === 0) { return diff; } @@ -514,9 +517,9 @@ function applyModifiedEdits(diff: IDocumentDiff, textEdits: TextEditInfo[], orig quitEarly: false, changes, moves, - }; + };*/ } - +/* function applyEditToLineRange(range: LineRange, textEdits: TextEditInfo[]): LineRange | undefined { let rangeStartLineNumber = range.startLineNumber; let rangeEndLineNumberEx = range.endLineNumberExclusive; @@ -583,3 +586,4 @@ function applyModifiedEditsToLineRangeMappings(changes: readonly LineRangeMappin ); return newChanges; } +*/ diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/movedBlocksLines.ts b/src/vs/editor/browser/widget/diffEditorWidget2/movedBlocksLines.ts index 27a9f8f46fb..8a618685996 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget2/movedBlocksLines.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget2/movedBlocksLines.ts @@ -9,7 +9,7 @@ import { Action } from 'vs/base/common/actions'; import { booleanComparator, compareBy, findMaxIdxBy, numberComparator, tieBreakComparators } from 'vs/base/common/arrays'; import { Codicon } from 'vs/base/common/codicons'; import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IObservable, autorun, autorunWithStore, constObservable, derived, derivedWithStore, keepAlive, observableFromEvent, observableSignalFromEvent, observableValue } from 'vs/base/common/observable'; +import { IObservable, autorun, autorunHandleChanges, autorunWithStore, constObservable, derived, derivedWithStore, keepAlive, observableFromEvent, observableSignalFromEvent, observableValue } from 'vs/base/common/observable'; import { ThemeIcon } from 'vs/base/common/themables'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { DiffEditorEditors } from 'vs/editor/browser/widget/diffEditorWidget2/diffEditorEditors'; @@ -82,19 +82,50 @@ export class MovedBlocksLinesPart extends Disposable { } })); - this._register(this._editors.original.onDidChangeCursorPosition(e => { - const m = this._diffModel.get(); - if (!m) { return; } - const movedText = m.diff.get()!.movedTexts.find(m => m.lineRangeMapping.original.contains(e.position.lineNumber)); - if (movedText !== m.movedTextToCompare.get()) { - m.movedTextToCompare.set(undefined, undefined); + const originalCursorPosition = observableFromEvent(this._editors.original.onDidChangeCursorPosition, () => this._editors.original.getPosition()); + const modifiedCursorPosition = observableFromEvent(this._editors.modified.onDidChangeCursorPosition, () => this._editors.modified.getPosition()); + const originalHasFocus = observableSignalFromEvent( + 'original.onDidFocusEditorWidget', + e => this._editors.original.onDidFocusEditorWidget(() => setTimeout(() => e(undefined), 0)) + ); + const modifiedHasFocus = observableSignalFromEvent( + 'modified.onDidFocusEditorWidget', + e => this._editors.modified.onDidFocusEditorWidget(() => setTimeout(() => e(undefined), 0)) + ); + + let lastChangedEditor: 'original' | 'modified' = 'modified'; + + this._register(autorunHandleChanges({ + createEmptyChangeSummary: () => undefined, + handleChange: (ctx, summary) => { + if (ctx.didChange(originalHasFocus)) { lastChangedEditor = 'original'; } + if (ctx.didChange(modifiedHasFocus)) { lastChangedEditor = 'modified'; } + return true; } - m.setActiveMovedText(movedText); - })); - this._register(this._editors.modified.onDidChangeCursorPosition(e => { - const m = this._diffModel.get(); + }, reader => { + originalHasFocus.read(reader); + modifiedHasFocus.read(reader); + + const m = this._diffModel.read(reader); if (!m) { return; } - const movedText = m.diff.get()!.movedTexts.find(m => m.lineRangeMapping.modified.contains(e.position.lineNumber)); + const diff = m.diff.read(reader); + + let movedText: MovedText | undefined = undefined; + + if (diff && lastChangedEditor === 'original') { + const originalPos = originalCursorPosition.read(reader); + if (originalPos) { + movedText = diff.movedTexts.find(m => m.lineRangeMapping.original.contains(originalPos.lineNumber)); + } + } + + if (diff && lastChangedEditor === 'modified') { + const modifiedPos = modifiedCursorPosition.read(reader); + if (modifiedPos) { + movedText = diff.movedTexts.find(m => m.lineRangeMapping.modified.contains(modifiedPos.lineNumber)); + } + } + if (movedText !== m.movedTextToCompare.get()) { m.movedTextToCompare.set(undefined, undefined); } @@ -324,7 +355,7 @@ class MovedBlockOverlayWidget extends ViewZoneOverlayWidget { true, () => { this._editor.focus(); - this._diffModel.movedTextToCompare.set(this._diffModel.movedTextToCompare.get() ? undefined : this._move, undefined); + this._diffModel.movedTextToCompare.set(this._diffModel.movedTextToCompare.get() === _move ? undefined : this._move, undefined); }, ); this._register(autorun(reader => { diff --git a/src/vs/editor/browser/widget/diffEditorWidget2/unchangedRanges.ts b/src/vs/editor/browser/widget/diffEditorWidget2/unchangedRanges.ts index 5b5c4ecc2a9..a61dc81803b 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget2/unchangedRanges.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget2/unchangedRanges.ts @@ -94,7 +94,7 @@ export class UnchangedRangesFeature extends Disposable { const d = derived(reader => /** @description hiddenOriginalRangeStart */ r.getHiddenOriginalRange(reader).startLineNumber - 1); const origVz = new PlaceholderViewZone(d, 24); origViewZones.push(origVz); - store.add(new CollapsedCodeOverlayWidget(this._editors.original, origVz, r, r.originalRange, !sideBySide, modifiedOutlineSource, l => this._diffModel.get()!.ensureOriginalLineIsVisible(l, undefined), this._options)); + store.add(new CollapsedCodeOverlayWidget(this._editors.original, origVz, r, r.originalRange, !sideBySide, modifiedOutlineSource, l => this._diffModel.get()!.ensureModifiedLineIsVisible(l, undefined), this._options)); } { const d = derived(reader => /** @description hiddenModifiedRangeStart */ r.getHiddenModifiedRange(reader).startLineNumber - 1); @@ -250,7 +250,7 @@ class CollapsedCodeOverlayWidget extends ViewZoneOverlayWidget { private readonly _nodes = h('div.diff-hidden-lines', [ h('div.top@top', { title: localize('diff.hiddenLines.top', 'Click or drag to show more above') }), h('div.center@content', { style: { display: 'flex' } }, [ - h('div@first', { style: { display: 'flex', justifyContent: 'center', alignItems: 'center' } }, + h('div@first', { style: { display: 'flex', justifyContent: 'center', alignItems: 'center', flexShrink: '0' } }, [$('a', { title: localize('showAll', 'Show all'), role: 'button', onclick: () => { this.showAll(); } }, ...renderLabelWithIcons('$(unfold)'))] ), h('div@others', { style: { display: 'flex', justifyContent: 'center', alignItems: 'center' } }), @@ -265,7 +265,7 @@ class CollapsedCodeOverlayWidget extends ViewZoneOverlayWidget { private readonly _unchangedRegionRange: LineRange, private readonly hide: boolean, private readonly _modifiedOutlineSource: OutlineSource, - private readonly _revealHiddenLine: (lineNumber: number) => void, + private readonly _revealModifiedHiddenLine: (lineNumber: number) => void, private readonly _options: DiffEditorOptions, ) { const root = h('div.diff-hidden-lines-widget'); @@ -365,7 +365,7 @@ class CollapsedCodeOverlayWidget extends ViewZoneOverlayWidget { const children: HTMLElement[] = []; if (!this.hide) { const lineCount = _unchangedRegion.getHiddenModifiedRange(reader).length; - const linesHiddenText = localize('hiddenLines', '{0} Hidden Lines', lineCount); + const linesHiddenText = localize('hiddenLines', '{0} hidden lines', lineCount); const span = $('span', { title: localize('diff.hiddenLines.expandAll', 'Double click to unfold') }, linesHiddenText); span.addEventListener('dblclick', e => { if (e.button !== 0) { return; } @@ -396,7 +396,7 @@ class CollapsedCodeOverlayWidget extends ViewZoneOverlayWidget { ]).root; children.push(divItem); divItem.onclick = () => { - this._revealHiddenLine(item.startLineNumber); + this._revealModifiedHiddenLine(item.startLineNumber); }; } } diff --git a/src/vs/editor/common/config/diffEditor.ts b/src/vs/editor/common/config/diffEditor.ts index 2f2bc06f2d2..2a62c479848 100644 --- a/src/vs/editor/common/config/diffEditor.ts +++ b/src/vs/editor/common/config/diffEditor.ts @@ -5,7 +5,7 @@ import { ValidDiffEditorBaseOptions } from 'vs/editor/common/config/editorOptions'; -export const diffEditorDefaultOptions: ValidDiffEditorBaseOptions = { +export const diffEditorDefaultOptions = { enableSplitViewResizing: true, splitViewDefaultRatio: 0.5, renderSideBySide: true, @@ -34,4 +34,4 @@ export const diffEditorDefaultOptions: ValidDiffEditorBaseOptions = { onlyShowAccessibleDiffViewer: false, renderSideBySideInlineBreakpoint: 900, useInlineViewWhenSpaceIsLimited: true, -}; +} satisfies ValidDiffEditorBaseOptions; diff --git a/src/vs/editor/common/config/editorConfigurationSchema.ts b/src/vs/editor/common/config/editorConfigurationSchema.ts index 845adcd40ac..684ec85c33b 100644 --- a/src/vs/editor/common/config/editorConfigurationSchema.ts +++ b/src/vs/editor/common/config/editorConfigurationSchema.ts @@ -151,53 +151,53 @@ const editorConfiguration: IConfigurationNode = { }, 'diffEditor.maxComputationTime': { type: 'number', - default: 5000, + default: diffEditorDefaultOptions.maxComputationTime, description: nls.localize('maxComputationTime', "Timeout in milliseconds after which diff computation is cancelled. Use 0 for no timeout.") }, 'diffEditor.maxFileSize': { type: 'number', - default: 50, + default: diffEditorDefaultOptions.maxFileSize, description: nls.localize('maxFileSize', "Maximum file size in MB for which to compute diffs. Use 0 for no limit.") }, 'diffEditor.renderSideBySide': { type: 'boolean', - default: true, + default: diffEditorDefaultOptions.renderSideBySide, description: nls.localize('sideBySide', "Controls whether the diff editor shows the diff side by side or inline.") }, 'diffEditor.renderSideBySideInlineBreakpoint': { type: 'number', - default: true, + default: diffEditorDefaultOptions.renderSideBySideInlineBreakpoint, description: nls.localize('renderSideBySideInlineBreakpoint', "If the diff editor width is smaller than this value, the inline view is used.") }, 'diffEditor.useInlineViewWhenSpaceIsLimited': { type: 'boolean', - default: true, + default: diffEditorDefaultOptions.useInlineViewWhenSpaceIsLimited, description: nls.localize('useInlineViewWhenSpaceIsLimited', "If enabled and the editor width is too small, the inline view is used.") }, 'diffEditor.renderMarginRevertIcon': { type: 'boolean', - default: true, + default: diffEditorDefaultOptions.renderMarginRevertIcon, description: nls.localize('renderMarginRevertIcon', "When enabled, the diff editor shows arrows in its glyph margin to revert changes.") }, 'diffEditor.ignoreTrimWhitespace': { type: 'boolean', - default: true, + default: diffEditorDefaultOptions.ignoreTrimWhitespace, description: nls.localize('ignoreTrimWhitespace', "When enabled, the diff editor ignores changes in leading or trailing whitespace.") }, 'diffEditor.renderIndicators': { type: 'boolean', - default: true, + default: diffEditorDefaultOptions.renderIndicators, description: nls.localize('renderIndicators', "Controls whether the diff editor shows +/- indicators for added/removed changes.") }, 'diffEditor.codeLens': { type: 'boolean', - default: false, + default: diffEditorDefaultOptions.diffCodeLens, description: nls.localize('codeLens', "Controls whether the editor shows CodeLens.") }, 'diffEditor.wordWrap': { type: 'string', enum: ['off', 'on', 'inherit'], - default: 'inherit', + default: diffEditorDefaultOptions.diffWordWrap, markdownEnumDescriptions: [ nls.localize('wordWrap.off', "Lines will never wrap."), nls.localize('wordWrap.on', "Lines will wrap at the viewport width."), @@ -239,7 +239,7 @@ const editorConfiguration: IConfigurationNode = { }, 'diffEditor.experimental.showMoves': { type: 'boolean', - default: false, + default: diffEditorDefaultOptions.experimental.showMoves, markdownDescription: nls.localize('showMoves', "Controls whether the diff editor should show detected code moves. Only works when {0} is set.", '`#diffEditor.experimental.useVersion2#`') }, 'diffEditor.experimental.useVersion2': { @@ -250,7 +250,7 @@ const editorConfiguration: IConfigurationNode = { }, 'diffEditor.experimental.showEmptyDecorations': { type: 'boolean', - default: true, + default: diffEditorDefaultOptions.experimental.showEmptyDecorations, description: nls.localize('showEmptyDecorations', "Controls whether the diff editor shows empty decorations to see where characters got inserted or deleted."), } } diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index f338f7b0817..35979acc29f 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -2799,7 +2799,7 @@ class EditorStickyScroll extends BaseEditorOption c.originalRange.endLineNumberExclusive, numberComparator); + joinedMoves = joinedMoves.filter(m => { + const diffBeforeOriginalMove = originalChanges.findLastItemBeforeOrEqual(m.original.startLineNumber) + || new LineRangeMapping(new LineRange(1, 1), new LineRange(1, 1), []); + + const modifiedDistToPrevDiff = m.modified.startLineNumber - diffBeforeOriginalMove.modifiedRange.endLineNumberExclusive; + const originalDistToPrevDiff = m.original.startLineNumber - diffBeforeOriginalMove.originalRange.endLineNumberExclusive; + + const differentDistances = modifiedDistToPrevDiff !== originalDistToPrevDiff; + return differentDistances; + }); + const fullMoves = joinedMoves.map(m => { const moveChanges = this.refineDiff(originalLines, modifiedLines, new SequenceDiff( m.original.toOffsetRange(), @@ -366,6 +380,60 @@ export class AdvancedLinesDiffComputer implements ILinesDiffComputer { } } +class MonotonousFinder { + public static create( + items: TItem[], + itemToDomain: (item: TItem) => TDomain, + domainComparator: Comparator, + ): MonotonousFinder { + items.sort((a, b) => domainComparator(itemToDomain(a), itemToDomain(b))); + return new MonotonousFinder(items, itemToDomain, domainComparator); + } + + public static createOfSorted( + items: TItem[], + itemToDomain: (item: TItem) => TDomain, + domainComparator: Comparator, + ): MonotonousFinder { + return new MonotonousFinder(items, itemToDomain, domainComparator); + } + + private _currentIdx = 0; // All values with index lower than this are smaller than or equal to _lastValue and vice versa. + private _lastValue: TDomain | undefined = undefined; // Represents a smallest value. + private _hasLastValue = false; + + private constructor( + private readonly _items: TItem[], + private readonly _itemToDomain: (item: TItem) => TDomain, + private readonly _domainComparator: Comparator, + ) { + } + + /** + * Assumes the values are monotonously increasing. + */ + findLastItemBeforeOrEqual(value: TDomain): TItem | undefined { + if (this._hasLastValue && CompareResult.isLessThan(this._domainComparator(value, this._lastValue!))) { + // Values must be monotonously increasing + throw new BugIndicatingError(); + } + this._lastValue = value; + this._hasLastValue = true; + + while ( + this._currentIdx < this._items.length + && CompareResult.isLessThanOrEqual(this._domainComparator( + this._itemToDomain(this._items[this._currentIdx]), + value + )) + ) { + this._currentIdx++; + } + + return this._currentIdx === 0 ? undefined : this._items[this._currentIdx - 1]; + } +} + function intersectRanges(ranges1: LineRange[], ranges2: LineRange[]): LineRange[] { const result: LineRange[] = []; @@ -839,16 +907,15 @@ export class LinesSliceCharSequence implements ISequence { } public extendToFullLines(range: OffsetRange): OffsetRange { - const firstIdx = findLastIdxMonotonous(this.firstCharOffsetByLineMinusOne, x => x <= range.start); - const lastIdx = findFirstIdxMonotonous(this.firstCharOffsetByLineMinusOne, x => range.endExclusive <= x); - - const start = firstIdx === -1 ? 0 : this.firstCharOffsetByLineMinusOne[firstIdx]; - const end = lastIdx === this.firstCharOffsetByLineMinusOne.length ? this.elements.length : this.firstCharOffsetByLineMinusOne[lastIdx]; + const start = findLastMonotonous(this.firstCharOffsetByLineMinusOne, x => x <= range.start) ?? 0; + const end = findFirstMonotonous(this.firstCharOffsetByLineMinusOne, x => range.endExclusive <= x) ?? this.elements.length; return new OffsetRange(start, end); } } /** + * `arr.map(predicate)` must be like `[true, ..., true, false, ..., false]`! + * * @returns -1 if predicate is false for all items */ function findLastIdxMonotonous(arr: T[], predicate: (item: T) => boolean): number { @@ -865,7 +932,14 @@ function findLastIdxMonotonous(arr: T[], predicate: (item: T) => boolean): nu return i - 1; } +export function findLastMonotonous(arr: T[], predicate: (item: T) => boolean): T | undefined { + const idx = findLastIdxMonotonous(arr, predicate); + return idx === -1 ? undefined : arr[idx]; +} + /** + * `arr.map(predicate)` must be like `[false, ..., false, true, ..., true]`! + * * @returns arr.length if predicate is false for all items */ function findFirstIdxMonotonous(arr: T[], predicate: (item: T) => boolean): number { @@ -882,6 +956,11 @@ function findFirstIdxMonotonous(arr: T[], predicate: (item: T) => boolean): n return i; } +export function findFirstMonotonous(arr: T[], predicate: (item: T) => boolean): T | undefined { + const idx = findFirstIdxMonotonous(arr, predicate); + return idx === arr.length ? undefined : arr[idx]; +} + function isWordChar(charCode: number): boolean { return charCode >= CharCode.a && charCode <= CharCode.z || charCode >= CharCode.A && charCode <= CharCode.Z diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index e7297361013..01f2c6987ac 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -1114,6 +1114,9 @@ export module StandaloneServices { serviceCollection.set(IInstantiationService, instantiationService); export function get(serviceId: ServiceIdentifier): T { + if (!initialized) { + initialize({}); + } const r = serviceCollection.get(serviceId); if (!r) { throw new Error('Missing service ' + serviceId); diff --git a/src/vs/platform/environment/electron-main/environmentMainService.ts b/src/vs/platform/environment/electron-main/environmentMainService.ts index 748ff075783..aab03b3130f 100644 --- a/src/vs/platform/environment/electron-main/environmentMainService.ts +++ b/src/vs/platform/environment/electron-main/environmentMainService.ts @@ -6,6 +6,7 @@ import { memoize } from 'vs/base/common/decorators'; import { join } from 'vs/base/common/path'; import { isLinux } from 'vs/base/common/platform'; +import { URI } from 'vs/base/common/uri'; import { createStaticIPCHandle } from 'vs/base/parts/ipc/node/ipc.net'; import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { NativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; @@ -68,6 +69,9 @@ export class EnvironmentMainService extends NativeEnvironmentService implements @memoize get useCodeCache(): boolean { return !!this.codeCachePath; } + @memoize + override get userRoamingDataHome(): URI { return this.appSettingsHome; } + unsetSnapExportedVariables() { if (!isLinux) { return; diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 677149f937b..9b8ba9a1b42 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -152,12 +152,13 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi throw new Error(nls.localize('incompatible', "Unable to install extension '{0}' as it is not compatible with VS Code '{1}'.", extensionId, this.productService.version)); } - const result = await this.installExtensions([{ manifest, extension: location, options }]); - if (result[0]?.local) { - return result[0]?.local; + const results = await this.installExtensions([{ manifest, extension: location, options }]); + const result = results.find(({ identifier }) => areSameExtensions(identifier, { id: extensionId })); + if (result?.local) { + return result.local; } - if (result[0]?.error) { - throw result[0].error; + if (result?.error) { + throw result.error; } throw toExtensionManagementError(new Error(`Unknown error while installing extension ${extensionId}`)); } finally { diff --git a/src/vs/platform/history/browser/contextScopedHistoryWidget.ts b/src/vs/platform/history/browser/contextScopedHistoryWidget.ts index 0278e0c0112..53b9397651d 100644 --- a/src/vs/platform/history/browser/contextScopedHistoryWidget.ts +++ b/src/vs/platform/history/browser/contextScopedHistoryWidget.ts @@ -113,6 +113,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ when: ContextKeyExpr.and( ContextKeyExpr.has(HistoryNavigationWidgetFocusContext), ContextKeyExpr.equals(HistoryNavigationBackwardsEnablementContext, true), + ContextKeyExpr.not('isComposing'), historyNavigationVisible.isEqualTo(false), ), primary: KeyCode.UpArrow, @@ -128,6 +129,7 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ when: ContextKeyExpr.and( ContextKeyExpr.has(HistoryNavigationWidgetFocusContext), ContextKeyExpr.equals(HistoryNavigationForwardsEnablementContext, true), + ContextKeyExpr.not('isComposing'), historyNavigationVisible.isEqualTo(false), ), primary: KeyCode.DownArrow, diff --git a/src/vs/platform/quickinput/browser/quickInput.ts b/src/vs/platform/quickinput/browser/quickInput.ts index 173bbc88096..4258cd0640a 100644 --- a/src/vs/platform/quickinput/browser/quickInput.ts +++ b/src/vs/platform/quickinput/browser/quickInput.ts @@ -47,7 +47,7 @@ export interface IQuickInputOptions { renderers: IListRenderer[], options: IListOptions, ): List; - hoverDelegate: IHoverDelegate; + hoverDelegate?: IHoverDelegate; styles: IQuickInputStyles; } diff --git a/src/vs/platform/quickinput/browser/quickInputController.ts b/src/vs/platform/quickinput/browser/quickInputController.ts index 85780828ce1..932eec180e1 100644 --- a/src/vs/platform/quickinput/browser/quickInputController.ts +++ b/src/vs/platform/quickinput/browser/quickInputController.ts @@ -83,12 +83,13 @@ export class QuickInputController extends Disposable { const titleBar = dom.append(container, $('.quick-input-titlebar')); - const leftActionBar = this._register(new ActionBar(titleBar)); + const actionBarOption = this.options.hoverDelegate ? { hoverDelegate: this.options.hoverDelegate } : undefined; + const leftActionBar = this._register(new ActionBar(titleBar, actionBarOption)); leftActionBar.domNode.classList.add('quick-input-left-action-bar'); const title = dom.append(titleBar, $('.quick-input-title')); - const rightActionBar = this._register(new ActionBar(titleBar)); + const rightActionBar = this._register(new ActionBar(titleBar, actionBarOption)); rightActionBar.domNode.classList.add('quick-input-right-action-bar'); const headerContainer = dom.append(container, $('.quick-input-header')); diff --git a/src/vs/platform/quickinput/browser/quickInputList.ts b/src/vs/platform/quickinput/browser/quickInputList.ts index e5914bfef84..bb8dc4c1429 100644 --- a/src/vs/platform/quickinput/browser/quickInputList.ts +++ b/src/vs/platform/quickinput/browser/quickInputList.ts @@ -541,46 +541,49 @@ export class QuickInputList { } })); - const delayer = new ThrottledDelayer(options.hoverDelegate.delay); - // onMouseOver triggers every time a new element has been moused over - // even if it's on the same list item. - this.disposables.push(this.list.onMouseOver(async e => { - // If we hover over an anchor element, we don't want to show the hover because - // the anchor may have a tooltip that we want to show instead. - if (e.browserEvent.target instanceof HTMLAnchorElement) { - delayer.cancel(); - return; - } - if ( - // anchors are an exception as called out above so we skip them here - !(e.browserEvent.relatedTarget instanceof HTMLAnchorElement) && - // check if the mouse is still over the same element - dom.isAncestor(e.browserEvent.relatedTarget as Node, e.element?.element as Node) - ) { - return; - } - try { - await delayer.trigger(async () => { - if (e.element) { - this.showHover(e.element); - } - }); - } catch (e) { - // Ignore cancellation errors due to mouse out - if (!isCancellationError(e)) { - throw e; + if (options.hoverDelegate) { + const delayer = new ThrottledDelayer(options.hoverDelegate.delay); + // onMouseOver triggers every time a new element has been moused over + // even if it's on the same list item. + this.disposables.push(this.list.onMouseOver(async e => { + // If we hover over an anchor element, we don't want to show the hover because + // the anchor may have a tooltip that we want to show instead. + if (e.browserEvent.target instanceof HTMLAnchorElement) { + delayer.cancel(); + return; } - } - })); - this.disposables.push(this.list.onMouseOut(e => { - // onMouseOut triggers every time a new element has been moused over - // even if it's on the same list item. We only want one event, so we - // check if the mouse is still over the same element. - if (dom.isAncestor(e.browserEvent.relatedTarget as Node, e.element?.element as Node)) { - return; - } - delayer.cancel(); - })); + if ( + // anchors are an exception as called out above so we skip them here + !(e.browserEvent.relatedTarget instanceof HTMLAnchorElement) && + // check if the mouse is still over the same element + dom.isAncestor(e.browserEvent.relatedTarget as Node, e.element?.element as Node) + ) { + return; + } + try { + await delayer.trigger(async () => { + if (e.element) { + this.showHover(e.element); + } + }); + } catch (e) { + // Ignore cancellation errors due to mouse out + if (!isCancellationError(e)) { + throw e; + } + } + })); + this.disposables.push(this.list.onMouseOut(e => { + // onMouseOut triggers every time a new element has been moused over + // even if it's on the same list item. We only want one event, so we + // check if the mouse is still over the same element. + if (dom.isAncestor(e.browserEvent.relatedTarget as Node, e.element?.element as Node)) { + return; + } + delayer.cancel(); + })); + this.disposables.push(delayer); + } this.disposables.push(this._listElementChecked.event(_ => this.fireCheckedEvents())); this.disposables.push( this._onChangedAllVisibleChecked, @@ -590,8 +593,7 @@ export class QuickInputList { this._onButtonTriggered, this._onSeparatorButtonTriggered, this._onLeave, - this._onKeyDown, - delayer + this._onKeyDown ); } @@ -839,10 +841,14 @@ export class QuickInputList { * @param element The element to show the hover for */ private showHover(element: IListElement): void { + if (this.options.hoverDelegate === undefined) { + return; + } if (this._lastHover && !this._lastHover.isDisposed) { this.options.hoverDelegate.onDidHideHover?.(); this._lastHover?.dispose(); } + if (!element.element || !element.saneTooltip) { return; } diff --git a/src/vs/platform/quickinput/browser/quickInputService.ts b/src/vs/platform/quickinput/browser/quickInputService.ts index c4d20832264..924e6b83ef6 100644 --- a/src/vs/platform/quickinput/browser/quickInputService.ts +++ b/src/vs/platform/quickinput/browser/quickInputService.ts @@ -86,12 +86,6 @@ export class QuickInputService extends Themable implements IQuickInputService { renderers: IListRenderer[], options: IWorkbenchListOptions ) => this.instantiationService.createInstance(WorkbenchList, user, container, delegate, renderers, options) as List, - hoverDelegate: { - showHover(options, focus) { - return undefined; - }, - delay: 200 - }, styles: this.computeStyles() }; diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index 235d8efdbe8..e7e9dbe82e2 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -114,6 +114,7 @@ export const enum TerminalSettingId { EnableImages = 'terminal.integrated.enableImages', SmoothScrolling = 'terminal.integrated.smoothScrolling', IgnoreBracketedPasteMode = 'terminal.integrated.ignoreBracketedPasteMode', + FocusAfterRun = 'terminal.integrated.focusAfterRun', // Debug settings that are hidden from user diff --git a/src/vs/workbench/api/browser/viewsExtensionPoint.ts b/src/vs/workbench/api/browser/viewsExtensionPoint.ts index 5fc7d5dc63d..f97885e8e02 100644 --- a/src/vs/workbench/api/browser/viewsExtensionPoint.ts +++ b/src/vs/workbench/api/browser/viewsExtensionPoint.ts @@ -18,7 +18,7 @@ import { Extensions as ViewletExtensions, PaneCompositeRegistry } from 'vs/workb import { CustomTreeView, RawCustomTreeViewContextKey, TreeViewPane } from 'vs/workbench/browser/parts/views/treeView'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; -import { Extensions as ViewContainerExtensions, ICustomTreeViewDescriptor, ICustomViewDescriptor, IViewContainersRegistry, IViewDescriptor, IViewsRegistry, ResolvableTreeItem, ViewContainer, ViewContainerLocation } from 'vs/workbench/common/views'; +import { Extensions as ViewContainerExtensions, ICustomViewDescriptor, IViewContainersRegistry, IViewDescriptor, IViewsRegistry, ResolvableTreeItem, ViewContainer, ViewContainerLocation } from 'vs/workbench/common/views'; import { VIEWLET_ID as DEBUG } from 'vs/workbench/contrib/debug/common/debug'; import { VIEWLET_ID as EXPLORER } from 'vs/workbench/contrib/files/common/files'; import { VIEWLET_ID as REMOTE } from 'vs/workbench/contrib/remote/browser/remoteExplorer'; @@ -530,14 +530,14 @@ class ViewsExtensionHandler implements IWorkbenchContribution { } } - const viewDescriptor = { + const viewDescriptor: ICustomViewDescriptor = { type: type, ctorDescriptor: type === ViewType.Tree ? new SyncDescriptor(TreeViewPane) : new SyncDescriptor(WebviewViewPane), id: item.id, name: item.name, when: ContextKeyExpr.deserialize(item.when), containerIcon: icon || viewContainer?.icon, - containerTitle: item.contextualTitle || viewContainer?.title, + containerTitle: item.contextualTitle || (viewContainer && (typeof viewContainer.title === 'string' ? viewContainer.title : viewContainer.title.value)), canToggleVisibility: true, canMoveView: viewContainer?.id !== REMOTE, treeView: type === ViewType.Tree ? this.instantiationService.createInstance(CustomTreeView, item.id, item.name, extension.description.identifier.value) : undefined, @@ -587,7 +587,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution { if (removedViews.length) { this.viewsRegistry.deregisterViews(removedViews, viewContainer); for (const view of removedViews) { - const anyView = view as ICustomTreeViewDescriptor; + const anyView = view as ICustomViewDescriptor; if (anyView.treeView) { anyView.treeView.dispose(); } diff --git a/src/vs/workbench/api/common/extHostTreeViews.ts b/src/vs/workbench/api/common/extHostTreeViews.ts index 3f33b7d7b62..844b0b63eae 100644 --- a/src/vs/workbench/api/common/extHostTreeViews.ts +++ b/src/vs/workbench/api/common/extHostTreeViews.ts @@ -246,6 +246,9 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { $setVisible(treeViewId: string, isVisible: boolean): void { const treeView = this.treeViews.get(treeViewId); if (!treeView) { + if (!isVisible) { + return; + } throw new NoTreeViewError(treeViewId); } treeView.setVisible(isVisible); diff --git a/src/vs/workbench/api/node/extensionHostProcess.ts b/src/vs/workbench/api/node/extensionHostProcess.ts index 860800e3f84..bb3cbfbca7b 100644 --- a/src/vs/workbench/api/node/extensionHostProcess.ts +++ b/src/vs/workbench/api/node/extensionHostProcess.ts @@ -6,7 +6,6 @@ import * as nativeWatchdog from 'native-watchdog'; import * as net from 'net'; import * as minimist from 'minimist'; -import * as dns from 'dns'; import * as performance from 'vs/base/common/performance'; import type { MessagePortMain } from 'vs/base/parts/sandbox/node/electronTypes'; import { isCancellationError, isSigPipeError, onUnexpectedError } from 'vs/base/common/errors'; @@ -47,15 +46,6 @@ interface ParsedExtHostArgs { } })(); -// TODO(deepak1556): Remove this once -// https://github.com/electron/electron/pull/39376 -// is available. The following API call is needed to get our -// remote integration tests to pass. -(function configureDnsResultOrder() { - // Refs https://github.com/microsoft/vscode/issues/189805 - dns.setDefaultResultOrder('ipv4first'); -})(); - const args = minimist(process.argv.slice(2), { boolean: [ 'transformURIs', diff --git a/src/vs/workbench/browser/actions/developerActions.ts b/src/vs/workbench/browser/actions/developerActions.ts index 91297a82197..48c3f08a46e 100644 --- a/src/vs/workbench/browser/actions/developerActions.ts +++ b/src/vs/workbench/browser/actions/developerActions.ts @@ -94,6 +94,7 @@ class InspectContextKeysAction extends Action2 { interface IScreencastKeyboardOptions { readonly showKeys?: boolean; + readonly showKeybindings?: boolean; readonly showCommands?: boolean; readonly showCommandGroups?: boolean; readonly showSingleEditorCursorMoves?: boolean; @@ -315,7 +316,7 @@ class ToggleScreencastModeAction extends Action2 { append(keyboardMarker, $('span.title', {}, `${commandAndGroupLabel} `)); } - if (options.showKeys ?? true) { + if ((options.showKeys ?? true) || (command && (options.showKeybindings ?? true))) { // Fix label for arrow keys keyLabel = keyLabel?.replace('UpArrow', '↑') ?.replace('DownArrow', '↓') @@ -435,6 +436,11 @@ configurationRegistry.registerConfiguration({ default: true, description: localize('screencastMode.keyboardOptions.showKeys', "Show raw keys.") }, + 'showKeybindings': { + type: 'boolean', + default: true, + description: localize('screencastMode.keyboardOptions.showKeybindings', "Show keyboard shortcuts.") + }, 'showCommands': { type: 'boolean', default: true, @@ -453,6 +459,7 @@ configurationRegistry.registerConfiguration({ }, default: { 'showKeys': true, + 'showKeybindings': true, 'showCommands': true, 'showCommandGroups': false, 'showSingleEditorCursorMoves': true diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index e3e5c11bfa6..e13b41e522f 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -2264,6 +2264,11 @@ abstract class AbstractCreateEditorGroupAction extends Action2 { override async run(accessor: ServicesAccessor): Promise { const editorGroupService = accessor.get(IEditorGroupsService); + // We intentionally do not want the new group to be focussed so that + // a user can have keyboard focus e.g. in a tree/list, open a new + // editor group that is active and then arrow-up/down in the tree/list + // to pick an editor to open in that group + editorGroupService.addGroup(editorGroupService.activeGroup, this.direction, { activate: true }); } } diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index 8b733caa867..c424f439a4f 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -515,21 +515,12 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro addGroup(location: IEditorGroupView | GroupIdentifier, direction: GroupDirection, options?: IAddGroupOptions): IEditorGroupView { const locationView = this.assertGroupView(location); - const restoreFocus = this.shouldRestoreFocus(locationView.element); - const group = this.doAddGroup(locationView, direction); if (options?.activate) { this.doSetGroupActive(group); } - // Restore focus if we had it previously after completing the grid - // operation. That operation might cause reparenting of grid views - // which moves focus to the element otherwise. - if (restoreFocus) { - locationView.focus(); - } - return group; } diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index 3272b3e4839..5d5d132b05d 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -2077,6 +2077,10 @@ registerThemingParticipant((theme, collector) => { outline-offset: -5px; } + .monaco-workbench .part.editor > .content .editor-group-container.active > .title .tabs-container > .tab.active:focus { + outline-style: dashed; + } + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active { outline: 1px dotted; outline-offset: -5px; diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts index 1f3ec683798..52910366b5f 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts @@ -68,9 +68,19 @@ export function getNotificationFromContext(listService: IListService, context?: const list = listService.lastFocusedList; if (list instanceof WorkbenchList) { - const focusedElement = list.getFocusedElements()[0]; - if (isNotificationViewItem(focusedElement)) { - return focusedElement; + let element = list.getFocusedElements()[0]; + if (!isNotificationViewItem(element)) { + if (list.isDOMFocused()) { + // the notification list might have received focus + // via keyboard and might not have a focussed element. + // in that case just return the first element + // https://github.com/microsoft/vscode/issues/191705 + element = list.element(0); + } + } + + if (isNotificationViewItem(element)) { + return element; } } diff --git a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts index 0be6eaf64c0..cb9d9b7ce0e 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts @@ -93,7 +93,7 @@ export class NotificationsListDelegate implements IListVirtualDelegate 1 */)}px`; // Render message into offset helper const renderedMessage = NotificationMessageRenderer.render(notification.message); diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index e41769f836d..a0aa8d51b7b 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -533,7 +533,7 @@ const registry = Registry.as(ConfigurationExtensions.Con // Window - let windowTitleDescription = localize('windowTitle', "Controls the window title based on the active editor. Variables are substituted based on the context:"); + let windowTitleDescription = localize('windowTitle', "Controls the window title based on the current context such as the opened workspace or active editor. Variables are substituted based on the context:"); windowTitleDescription += '\n- ' + [ localize('activeEditorShort', "`${activeEditorShort}`: the file name (e.g. myFile.txt)."), localize('activeEditorMedium', "`${activeEditorMedium}`: the path of the file relative to the workspace folder (e.g. myFolder/myFileFolder/myFile.txt)."), diff --git a/src/vs/workbench/browser/workbench.ts b/src/vs/workbench/browser/workbench.ts index 28863e8e434..8fae627d8f6 100644 --- a/src/vs/workbench/browser/workbench.ts +++ b/src/vs/workbench/browser/workbench.ts @@ -342,7 +342,7 @@ export class Workbench extends Layout { // Create Parts for (const { id, role, classes, options } of [ - { id: Parts.TITLEBAR_PART, role: 'contentinfo', classes: ['titlebar'] }, + { id: Parts.TITLEBAR_PART, role: 'none', classes: ['titlebar'] }, { id: Parts.BANNER_PART, role: 'banner', classes: ['banner'] }, { id: Parts.ACTIVITYBAR_PART, role: 'none', classes: ['activitybar', this.getSideBarPosition() === Position.LEFT ? 'left' : 'right'] }, // Use role 'none' for some parts to make screen readers less chatty #114892 { id: Parts.SIDEBAR_PART, role: 'none', classes: ['sidebar', this.getSideBarPosition() === Position.LEFT ? 'left' : 'right'] }, diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index 95f89542805..07578cb75b6 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -300,18 +300,12 @@ export interface IViewDescriptor { readonly openCommandActionDescriptor?: OpenCommandActionDescriptor; } -export interface ICustomTreeViewDescriptor extends ITreeViewDescriptor { +export interface ICustomViewDescriptor extends IViewDescriptor { readonly extensionId: ExtensionIdentifier; readonly originalContainerId: string; + readonly treeView?: ITreeView; } -export interface ICustomWebviewViewDescriptor extends IViewDescriptor { - readonly extensionId: ExtensionIdentifier; - readonly originalContainerId: string; -} - -export type ICustomViewDescriptor = ICustomTreeViewDescriptor | ICustomWebviewViewDescriptor; - export interface IViewDescriptorRef { viewDescriptor: IViewDescriptor; index: number; diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts index 2cd52bc8311..384572c7402 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts @@ -21,8 +21,8 @@ export const accessibleViewCurrentProviderId = new RawContextKey('access * were better to live under workbench for discoverability. */ export const enum AccessibilityWorkbenchSettingId { - ViewDimUnfocusedEnabled = 'workbench.view.dimUnfocused.enabled', - ViewDimUnfocusedOpacity = 'workbench.view.dimUnfocused.opacity' + DimUnfocusedEnabled = 'accessibility.dimUnfocused.enabled', + DimUnfocusedOpacity = 'accessibility.dimUnfocused.opacity' } export const enum ViewDimUnfocusedOpacityProperties { @@ -42,7 +42,7 @@ export const enum AccessibilityVerbositySettingId { Editor = 'accessibility.verbosity.editor', Hover = 'accessibility.verbosity.hover', Notification = 'accessibility.verbosity.notification', - EditorUntitledHint = 'accessibility.verbosity.editor.untitledHint' + EditorUntitledHint = 'accessibility.verbosity.untitledHint' } export const enum AccessibleViewProviderId { @@ -107,7 +107,7 @@ const configuration: IConfigurationNode = { ...baseProperty }, [AccessibilityVerbositySettingId.EditorUntitledHint]: { - description: localize('verbosity.editor.untitledhint', 'Provide information about relevant actions in an untitled text editor.'), + description: localize('verbosity.untitledhint', 'Provide information about relevant actions in an untitled text editor.'), ...baseProperty } } @@ -120,15 +120,15 @@ export function registerAccessibilityConfiguration() { registry.registerConfiguration({ ...workbenchConfigurationNodeBase, properties: { - [AccessibilityWorkbenchSettingId.ViewDimUnfocusedEnabled]: { - description: localize('dimUnfocusedEnabled', 'Whether to dim unfocused editors and terminals, making the focused view more obvious.'), + [AccessibilityWorkbenchSettingId.DimUnfocusedEnabled]: { + description: localize('dimUnfocusedEnabled', 'Whether to dim unfocused editors and terminals, which makes it more clear where typed input will go to. This works with the majority of editors with the notable exceptions of those that utilize iframes like notebooks and extension webview editors.'), type: 'boolean', default: false, tags: ['accessibility'], scope: ConfigurationScope.APPLICATION, }, - [AccessibilityWorkbenchSettingId.ViewDimUnfocusedOpacity]: { - description: localize('dimUnfocusedOpacity', 'The opacity fraction (0.2 to 1.0) to use for unfocused editors and terminals. This will only take effect when {0} is enabled.', `\`#${AccessibilityWorkbenchSettingId.ViewDimUnfocusedEnabled}#\``), + [AccessibilityWorkbenchSettingId.DimUnfocusedOpacity]: { + markdownDescription: localize('dimUnfocusedOpacity', 'The opacity fraction (0.2 to 1.0) to use for unfocused editors and terminals. This will only take effect when {0} is enabled.', `\`#${AccessibilityWorkbenchSettingId.DimUnfocusedEnabled}#\``), type: 'number', minimum: ViewDimUnfocusedOpacityProperties.Minimum, maximum: ViewDimUnfocusedOpacityProperties.Maximum, diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityContributions.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityContributions.ts index 720720b02df..90544aabea6 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityContributions.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityContributions.ts @@ -258,6 +258,10 @@ function getActionsFromNotification(notification: INotificationViewItem): IActio }; } } + const manageExtension = actions?.find(a => a.label.includes('Manage Extension')); + if (manageExtension) { + manageExtension.class = ThemeIcon.asClassName(Codicon.gear); + } if (actions) { actions.push({ id: 'clearNotification', label: localize('clearNotification', "Clear Notification"), tooltip: localize('clearNotification', "Clear Notification"), run: () => notification.close(), enabled: true, class: ThemeIcon.asClassName(Codicon.clearAll) }); } diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index 42e95d10c93..1f82f5c1e9b 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -506,9 +506,9 @@ class AccessibleView extends Disposable { let hint = ''; const disableKeybinding = this._keybindingService.lookupKeybinding(AccessibilityCommandId.DisableVerbosityHint, this._contextKeyService)?.getAriaLabel(); if (disableKeybinding) { - hint = localize('acessibleViewDisableHint', "Disable the aria label hint to open this ({0}).\n", disableKeybinding); + hint = localize('acessibleViewDisableHint', "Disable accessibility verbosity for this feature ({0}). This will disable the hint to open the accessible view for example.\n", disableKeybinding); } else { - hint = localize('accessibleViewDisableHintNoKb', "Add a keybinding for the command Disable Accessible View Hint to disable this hint.\n"); + hint = localize('accessibleViewDisableHintNoKb', "Add a keybinding for the command Disable Accessible View Hint, which disables accessibility verbosity for this feature.\n"); } return hint; } diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts index 21547678e51..0721b08a570 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleViewActions.ts @@ -160,7 +160,7 @@ class AccessibleViewDisableHintAction extends Action2 { primary: KeyMod.Alt | KeyCode.F6, weight: KeybindingWeight.WorkbenchContrib }, - icon: Codicon.treeFilterClear, + icon: Codicon.bellSlash, menu: [ commandPalette, { diff --git a/src/vs/workbench/contrib/accessibility/browser/unfocusedViewDimmingContribution.ts b/src/vs/workbench/contrib/accessibility/browser/unfocusedViewDimmingContribution.ts index 1dc2b4b3605..0d9c1bec943 100644 --- a/src/vs/workbench/contrib/accessibility/browser/unfocusedViewDimmingContribution.ts +++ b/src/vs/workbench/contrib/accessibility/browser/unfocusedViewDimmingContribution.ts @@ -21,21 +21,23 @@ export class UnfocusedViewDimmingContribution extends Disposable implements IWor this._register(toDisposable(() => this._removeStyleElement())); this._register(Event.runAndSubscribe(configurationService.onDidChangeConfiguration, e => { - if (e && !e.affectsConfiguration(AccessibilityWorkbenchSettingId.ViewDimUnfocusedEnabled) && !e.affectsConfiguration(AccessibilityWorkbenchSettingId.ViewDimUnfocusedOpacity)) { + if (e && !e.affectsConfiguration(AccessibilityWorkbenchSettingId.DimUnfocusedEnabled) && !e.affectsConfiguration(AccessibilityWorkbenchSettingId.DimUnfocusedOpacity)) { return; } let cssTextContent = ''; - const enabled = ensureBoolean(configurationService.getValue(AccessibilityWorkbenchSettingId.ViewDimUnfocusedEnabled), false); + const enabled = ensureBoolean(configurationService.getValue(AccessibilityWorkbenchSettingId.DimUnfocusedEnabled), false); if (enabled) { const opacity = clamp( - ensureNumber(configurationService.getValue(AccessibilityWorkbenchSettingId.ViewDimUnfocusedOpacity), ViewDimUnfocusedOpacityProperties.Default), + ensureNumber(configurationService.getValue(AccessibilityWorkbenchSettingId.DimUnfocusedOpacity), ViewDimUnfocusedOpacityProperties.Default), ViewDimUnfocusedOpacityProperties.Minimum, ViewDimUnfocusedOpacityProperties.Maximum ); if (opacity !== 1) { + // These filter rules are more specific than may be expected as the `filter` + // rule can cause problems if it's used inside the element like on editor hovers const rules = new Set(); const filterRule = `filter: opacity(${opacity});`; // Terminal tabs @@ -44,8 +46,18 @@ export class UnfocusedViewDimmingContribution extends Disposable implements IWor rules.add(`.monaco-workbench .pane-body.integrated-terminal .terminal-wrapper:not(:focus-within) { ${filterRule} }`); // Text editors rules.add(`.monaco-workbench .editor-instance:not(:focus-within) .monaco-editor { ${filterRule} }`); + // Breadcrumbs + rules.add(`.monaco-workbench .editor-instance:not(:focus-within) .tabs-breadcrumbs { ${filterRule} }`); // Terminal editors rules.add(`.monaco-workbench .editor-instance:not(:focus-within) .terminal-wrapper { ${filterRule} }`); + // Settings editor + rules.add(`.monaco-workbench .editor-instance:not(:focus-within) .settings-editor { ${filterRule} }`); + // Keybindings editor + rules.add(`.monaco-workbench .editor-instance:not(:focus-within) .keybindings-editor { ${filterRule} }`); + // Editor placeholder (error case) + rules.add(`.monaco-workbench .editor-instance:not(:focus-within) .monaco-editor-pane-placeholder { ${filterRule} }`); + // Welcome editor + rules.add(`.monaco-workbench .editor-instance:not(:focus-within) .gettingStartedContainer { ${filterRule} }`); cssTextContent = [...rules].join('\n'); } diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts index 1258f618970..888c67f46a9 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts @@ -8,7 +8,7 @@ import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { LinkedList } from 'vs/base/common/linkedList'; import { ResourceMap, ResourceSet } from 'vs/base/common/map'; import { URI } from 'vs/base/common/uri'; -import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IBulkEditOptions, IBulkEditPreviewHandler, IBulkEditResult, IBulkEditService, ResourceEdit, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { WorkspaceEdit } from 'vs/editor/common/languages'; @@ -197,6 +197,8 @@ export class BulkEditService implements IBulkEditService { const candidate = this._editorService.activeTextEditorControl; if (isCodeEditor(candidate)) { codeEditor = candidate; + } else if (isDiffEditor(candidate)) { + codeEditor = candidate.getModifiedEditor(); } } diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts index c21f9e7f6c6..9f895d8b8a7 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts @@ -18,6 +18,7 @@ import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { tail } from 'vs/base/common/arrays'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { Schemas } from 'vs/base/common/network'; interface IFileOperation { uris: URI[]; @@ -173,6 +174,9 @@ class CreateOperation implements IFileOperation { const undoes: DeleteEdit[] = []; for (const edit of this._edits) { + if (edit.newUri.scheme === Schemas.untitled) { + continue; // ignore, will be handled by a later edit + } if (edit.options.overwrite === undefined && edit.options.ignoreIfExists && await this._fileService.exists(edit.newUri)) { continue; // not overwriting, but ignoring, and the target file exists } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index 3761e137dc5..8439a5d14fd 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -11,8 +11,10 @@ import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { IBulkEditService, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { Range } from 'vs/editor/common/core/range'; +import { RelatedContextItem, WorkspaceEdit } from 'vs/editor/common/languages'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { ITextModel } from 'vs/editor/common/model'; +import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { CopyAction } from 'vs/editor/contrib/clipboard/browser/clipboard'; import { localize } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; @@ -31,8 +33,6 @@ import { CellKind, NOTEBOOK_EDITOR_ID } from 'vs/workbench/contrib/notebook/comm import { ITerminalEditorService, ITerminalGroupService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; -import { WorkspaceEdit, RelatedContextItem } from 'vs/editor/common/languages'; export interface IChatCodeBlockActionContext { code: string; @@ -92,6 +92,11 @@ export function registerChatCodeBlockActions() { return; } + if (context.element.errorDetails?.responseIsFiltered) { + // When run from command palette + return; + } + const clipboardService = accessor.get(IClipboardService); clipboardService.writeText(context.code); @@ -183,6 +188,11 @@ export function registerChatCodeBlockActions() { const editorService = accessor.get(IEditorService); const textFileService = accessor.get(ITextFileService); + if (context.element.errorDetails?.responseIsFiltered) { + // When run from command palette + return; + } + if (editorService.activeEditorPane?.getId() === NOTEBOOK_EDITOR_ID) { return this.handleNotebookEditor(accessor, editorService.activeEditorPane.getControl() as INotebookEditor, context); } @@ -312,6 +322,11 @@ export function registerChatCodeBlockActions() { } override async runWithContext(accessor: ServicesAccessor, context: IChatCodeBlockActionContext) { + if (context.element.errorDetails?.responseIsFiltered) { + // When run from command palette + return; + } + const editorService = accessor.get(IEditorService); const chatService = accessor.get(IChatService); editorService.openEditor({ contents: context.code, languageId: context.languageId, resource: undefined }); @@ -358,6 +373,11 @@ export function registerChatCodeBlockActions() { } override async runWithContext(accessor: ServicesAccessor, context: IChatCodeBlockActionContext) { + if (context.element.errorDetails?.responseIsFiltered) { + // When run from command palette + return; + } + const chatService = accessor.get(IChatService); const terminalService = accessor.get(ITerminalService); const editorService = accessor.get(IEditorService); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts index 19bd054e7b8..6d7a399b006 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts @@ -15,7 +15,7 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat'; -import { CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatService, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { isRequestVM, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; @@ -119,7 +119,7 @@ export function registerChatTitleActions() { id: MenuId.ChatMessageTitle, group: 'navigation', isHiddenByDefault: true, - when: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, CONTEXT_RESPONSE) + when: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, CONTEXT_RESPONSE, CONTEXT_RESPONSE_FILTERED.negate()) } }); } diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 1208e6134c3..ebb17cc541f 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -61,7 +61,7 @@ import { IChatCodeBlockActionContext } from 'vs/workbench/contrib/chat/browser/a import { ChatTreeItem, IChatCodeBlockInfo, IChatFileTreeInfo } from 'vs/workbench/contrib/chat/browser/chat'; import { ChatFollowups } from 'vs/workbench/contrib/chat/browser/chatFollowups'; import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions'; -import { CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_HAS_PROVIDER_ID, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; +import { CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_HAS_PROVIDER_ID, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys'; import { IChatReplyFollowup, IChatResponseProgressFileTreeData, IChatService, ISlashCommand, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatResponseMarkdownRenderData, IChatResponseRenderData, IChatResponseViewModel, IChatWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel'; import { IWordCountResult, getNWords } from 'vs/workbench/contrib/chat/common/chatWordCounter'; @@ -268,10 +268,13 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { this._onDidChangeItemHeight.fire({ element, height: templateData.rowContainer.offsetHeight }); })); + treeDisposables.add(tree.onContextMenu((e) => { + e.browserEvent.preventDefault(); + e.browserEvent.stopPropagation(); + })); tree.setInput(data).then(() => { if (!ref.isStale()) { @@ -969,6 +977,12 @@ class CodeBlockPart extends Disposable implements IChatResultCodeBlockPart { element: data.element, languageId: vscodeLanguageId }; + + if (isResponseVM(data.element) && data.element.errorDetails?.responseIsFiltered) { + dom.hide(this.toolbar.getElement()); + } else { + dom.show(this.toolbar.getElement()); + } } private fixCodeText(text: string, languageId: string): string { @@ -1190,7 +1204,8 @@ class ChatListTreeRenderer implements ICompressibleTreeRenderer, index: number, templateData: IChatListTreeRendererTemplate, height: number | undefined): void { templateData.label.element.style.display = 'flex'; - if (!element.children.length) { + const hasExtension = /\.[^/.]+$/.test(element.element.label); + if (!element.children.length && hasExtension) { templateData.label.setFile(element.element.uri, { fileKind: FileKind.FILE, hidePath: true, diff --git a/src/vs/workbench/contrib/chat/browser/chatQuick.ts b/src/vs/workbench/contrib/chat/browser/chatQuick.ts index 03420bc79b4..5c41a1a6f7e 100644 --- a/src/vs/workbench/contrib/chat/browser/chatQuick.ts +++ b/src/vs/workbench/contrib/chat/browser/chatQuick.ts @@ -5,9 +5,10 @@ import * as dom from 'vs/base/browser/dom'; import { Orientation, Sash } from 'vs/base/browser/ui/sash/sash'; +import { disposableTimeout } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; @@ -59,8 +60,9 @@ export class QuickChatService extends Disposable implements IQuickChatService { this.open(providerId, query); } } + open(providerId?: string, query?: string | undefined): void { - if (this.focused) { + if (this._input) { return this.focus(); } @@ -91,10 +93,13 @@ export class QuickChatService extends Disposable implements IQuickChatService { // show needs to come after the quickpick is shown this._currentChat.render(this._container); + } else { + this._currentChat.show(); } disposableStore.add(this._input.onDidHide(() => { disposableStore.dispose(); + this._currentChat!.hide(); this._input = undefined; this._onDidClose.fire(); })); @@ -128,6 +133,8 @@ class QuickChat extends Disposable { private sash!: Sash; private model: ChatModel | undefined; private _currentQuery: string | undefined; + private maintainScrollTimer: MutableDisposable = this._register(new MutableDisposable()); + private _deferUpdatingDynamicLayout: boolean = false; constructor( private readonly _options: IChatViewOptions, @@ -162,6 +169,30 @@ class QuickChat extends Disposable { } } + hide(): void { + this.widget.setVisible(false); + // Maintain scroll position for a short time so that if the user re-shows the chat + // the same scroll position will be used. + this.maintainScrollTimer.value = disposableTimeout(() => { + // At this point, clear this mutable disposable which will be our signal that + // the timer has expired and we should stop maintaining scroll position + this.maintainScrollTimer.clear(); + }, 30 * 1000); // 30 seconds + } + + show(): void { + this.widget.setVisible(true); + // If the mutable disposable is set, then we are keeping the existing scroll position + // so we should not update the layout. + if (this._deferUpdatingDynamicLayout) { + this._deferUpdatingDynamicLayout = false; + this.widget.updateDynamicChatTreeItemLayout(2, this.maxHeight); + } + if (!this.maintainScrollTimer.value) { + this.widget.layoutDynamicChatTreeItemMode(); + } + } + render(parent: HTMLElement): void { if (this.widget) { throw new Error('Cannot render quick chat twice'); @@ -196,7 +227,14 @@ class QuickChat extends Disposable { private registerListeners(parent: HTMLElement): void { this._register(this.layoutService.onDidLayout(() => { - this.widget.updateDynamicChatTreeItemLayout(2, this.maxHeight); + if (this.widget.visible) { + this.widget.updateDynamicChatTreeItemLayout(2, this.maxHeight); + } else { + // If the chat is not visible, then we should defer updating the layout + // because it relies on offsetHeight which only works correctly + // when the chat is visible. + this._deferUpdatingDynamicLayout = true; + } })); this._register(this.widget.inputEditor.onDidChangeModelContent((e) => { this._currentQuery = this.widget.inputEditor.getValue(); diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 7e65ed72d20..91c15635f2e 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -77,9 +77,12 @@ export class ChatWidget extends Disposable implements IChatWidget { private container!: HTMLElement; private bodyDimension: dom.Dimension | undefined; - private visible = false; private visibleChangeCount = 0; private requestInProgress: IContextKey; + private _visible = false; + public get visible() { + return this._visible; + } private previousTreeScrollHeight: number = 0; @@ -213,8 +216,8 @@ export class ChatWidget extends Disposable implements IChatWidget { this._onDidClear.fire(); } - private onDidChangeItems() { - if (this.tree && this.visible) { + private onDidChangeItems(skipDynamicLayout?: boolean) { + if (this.tree && this._visible) { const treeItems = (this.viewModel?.getItems() ?? []) .map(item => { return >{ @@ -239,7 +242,7 @@ export class ChatWidget extends Disposable implements IChatWidget { } }); - if (this._dynamicMessageLayoutData) { + if (!skipDynamicLayout && this._dynamicMessageLayoutData) { this.layoutDynamicChatTreeItemMode(); } @@ -261,7 +264,7 @@ export class ChatWidget extends Disposable implements IChatWidget { } setVisible(visible: boolean): void { - this.visible = visible; + this._visible = visible; this.visibleChangeCount++; this.renderer.setVisible(visible); @@ -269,8 +272,8 @@ export class ChatWidget extends Disposable implements IChatWidget { this._register(disposableTimeout(() => { // Progressive rendering paused while hidden, so start it up again. // Do it after a timeout because the container is not visible yet (it should be but offsetHeight returns 0 here) - if (this.visible) { - this.onDidChangeItems(); + if (this._visible) { + this.onDidChangeItems(true); } }, 0)); } @@ -540,6 +543,11 @@ export class ChatWidget extends Disposable implements IChatWidget { const mutableDisposable = this._register(new MutableDisposable()); this._register(this.tree.onDidScroll((e) => { + // TODO@TylerLeonhardt this should probably just be disposed when this is disabled + // and then set up again when it is enabled again + if (!this._dynamicMessageLayoutData?.enabled) { + return; + } mutableDisposable.value = dom.scheduleAtNextAnimationFrame(() => { if (!e.scrollTopChanged || e.heightChanged || e.scrollHeightChanged) { return; @@ -593,7 +601,9 @@ export class ChatWidget extends Disposable implements IChatWidget { if (!this.viewModel || !this._dynamicMessageLayoutData?.enabled) { return; } - const inputHeight = this.inputPart.layout(this._dynamicMessageLayoutData!.maxHeight, this.container.offsetWidth); + + const width = this.bodyDimension?.width ?? this.container.offsetWidth; + const inputHeight = this.inputPart.layout(this._dynamicMessageLayoutData!.maxHeight, width); const totalMessages = this.viewModel.getItems(); // grab the last N messages @@ -610,10 +620,10 @@ export class ChatWidget extends Disposable implements IChatWidget { inputHeight + listHeight + (totalMessages.length > 2 ? 18 : 0), this._dynamicMessageLayoutData!.maxHeight ), - this.container.offsetWidth + width ); - if (needsRerender) { + if (needsRerender || !listHeight) { // TODO: figure out a better place to reveal the last element revealLastElement(this.tree); } diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts index 8be40c833c1..d4ff01aec83 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from 'vs/base/common/cancellation'; +import { Iterable } from 'vs/base/common/iterator'; import { Disposable } from 'vs/base/common/lifecycle'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { Position } from 'vs/editor/common/core/position'; @@ -44,6 +45,7 @@ class InputEditorDecorations extends Disposable { @ICodeEditorService private readonly codeEditorService: ICodeEditorService, @IThemeService private readonly themeService: IThemeService, @IChatService private readonly chatService: IChatService, + @IChatVariablesService private readonly chatVariablesService: IChatVariablesService, ) { super(); @@ -175,21 +177,22 @@ class InputEditorDecorations extends Disposable { this.widget.inputEditor.setDecorationsByType(decorationDescription, slashCommandTextDecorationType, []); } - // const variables = this.chatVariablesService.getVariables(); + const variables = this.chatVariablesService.getVariables(); const variableReg = /(^|\s)@(\w+)(:\d+)?(?=(\s|$))/ig; let match: RegExpMatchArray | null; const varDecorations: IDecorationOptions[] = []; while (match = variableReg.exec(inputValue)) { - // const candidate = match[2]; - // if (Iterable.find(variables, v => v.name === candidate)) - varDecorations.push({ - range: { - startLineNumber: 1, - endLineNumber: 1, - startColumn: match.index! + match[1].length + 1, - endColumn: match.index! + match[0].length + 1 - } - }); + const varName = match[2]; + if (Iterable.find(variables, v => v.name === varName)) { + varDecorations.push({ + range: { + startLineNumber: 1, + endLineNumber: 1, + startColumn: match.index! + match[1].length + 1, + endColumn: match.index! + match[0].length + 1 + } + }); + } } this.widget.inputEditor.setDecorationsByType(decorationDescription, variableTextDecorationType, varDecorations); diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 5c36c297345..eb1c33b9fa6 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -238,7 +238,7 @@ .interactive-session .interactive-input-and-side-toolbar { display: flex; - gap: 6px; + gap: 4px; align-items: center; } @@ -427,6 +427,7 @@ .quick-input-widget .interactive-session .interactive-input-and-execute-toolbar { margin: 0; border-radius: 2px; + padding: 0 4px 0 6px; } .quick-input-widget .interactive-list { diff --git a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index 7509c369304..631f0af7d00 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -8,6 +8,7 @@ import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; export const CONTEXT_RESPONSE_HAS_PROVIDER_ID = new RawContextKey('chatSessionResponseHasProviderId', false, { type: 'boolean', description: localize('interactiveSessionResponseHasProviderId', "True when the provider has assigned an id to this response.") }); export const CONTEXT_RESPONSE_VOTE = new RawContextKey('chatSessionResponseVote', '', { type: 'string', description: localize('interactiveSessionResponseVote', "When the response has been voted up, is set to 'up'. When voted down, is set to 'down'. Otherwise an empty string.") }); +export const CONTEXT_RESPONSE_FILTERED = new RawContextKey('chatSessionResponseFiltered', false, { type: 'boolean', description: localize('chatResponseFiltered', "True when the chat response was filtered out by the server.") }); export const CONTEXT_CHAT_REQUEST_IN_PROGRESS = new RawContextKey('chatSessionRequestInProgress', false, { type: 'boolean', description: localize('interactiveSessionRequestInProgress', "True when the current request is still in progress.") }); export const CONTEXT_RESPONSE = new RawContextKey('chatResponse', false, { type: 'boolean', description: localize('chatResponse', "The chat item is a response.") }); diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 23fec163a79..5f51abedb9a 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -160,6 +160,7 @@ export class Response implements IResponse { }); } else if (isCompleteInteractiveProgressTreeData(responsePart)) { this._responseParts.push(responsePart); + this._updateRepr(quiet); } } diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/actions/media/voiceChatActions.css b/src/vs/workbench/contrib/chat/electron-sandbox/actions/media/voiceChatActions.css new file mode 100644 index 00000000000..b081c2c8d2f --- /dev/null +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/media/voiceChatActions.css @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-workbench .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled):hover, +.monaco-workbench .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled):hover { + animation: none; /* stop the running voice recording animation for showing another codicon to stop */ +} + +.monaco-workbench .interactive-input-part .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled):hover::before, +.monaco-workbench .inline-chat .monaco-action-bar .action-label.codicon-loading.codicon-modifier-spin:not(.disabled):hover::before { + content: "\ead7"; /* use `debug-stop` icon unicode for hovering over running voice recording */ +} diff --git a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts index 8773eff46d5..b244992f104 100644 --- a/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts +++ b/src/vs/workbench/contrib/chat/electron-sandbox/actions/voiceChatActions.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import 'vs/css!./media/voiceChatActions'; import { Event } from 'vs/base/common/event'; import { firstOrDefault } from 'vs/base/common/arrays'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Codicon } from 'vs/base/common/codicons'; -import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { equalsIgnoreCase } from 'vs/base/common/strings'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { localize } from 'vs/nls'; @@ -16,7 +17,7 @@ import { ContextKeyExpr, IContextKeyService, RawContextKey } from 'vs/platform/c import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { spinningLoading } from 'vs/platform/theme/common/iconRegistry'; import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions'; -import { IChatWidgetService, IQuickChatService } from 'vs/workbench/contrib/chat/browser/chat'; +import { IChatWidget, IChatWidgetService, IQuickChatService } from 'vs/workbench/contrib/chat/browser/chat'; import { IChatService } from 'vs/workbench/contrib/chat/common/chatService'; import { IWorkbenchVoiceRecognitionService } from 'vs/workbench/services/voiceRecognition/electron-sandbox/workbenchVoiceRecognitionService'; import { MENU_INLINE_CHAT_WIDGET } from 'vs/workbench/contrib/inlineChat/common/inlineChat'; @@ -24,58 +25,197 @@ import { CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatCo import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { getCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { isExecuteActionContext } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { process } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import product from 'vs/platform/product/common/product'; import { ActiveEditorContext } from 'vs/workbench/common/contextkeys'; +import { IViewsService } from 'vs/workbench/common/views'; +import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { isExecuteActionContext } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions'; +import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; -const CONTEXT_VOICE_CHAT_GETTING_READY = new RawContextKey('voiceChatGettingReady', false, { type: 'boolean', description: localize('voiceChatGettingReady', "True when getting ready for receiving voice input from the microphone.") }); -const CONTEXT_VOICE_CHAT_IN_PROGRESS = new RawContextKey('voiceChatInProgress', false, { type: 'boolean', description: localize('voiceChatInProgress', "True when voice recording from microphone is in progress.") }); +const CONTEXT_VOICE_CHAT_GETTING_READY = new RawContextKey('voiceChatGettingReady', false, { type: 'boolean', description: localize('voiceChatGettingReady', "True when getting ready for receiving voice input from the microphone for voice chat.") }); +const CONTEXT_VOICE_CHAT_IN_PROGRESS = new RawContextKey('voiceChatInProgress', false, { type: 'boolean', description: localize('voiceChatInProgress', "True when voice recording from microphone is in progress for voice chat.") }); + +const CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS = new RawContextKey('quickVoiceChatInProgress', false, { type: 'boolean', description: localize('quickVoiceChatInProgress', "True when voice recording from microphone is in progress for quick chat.") }); +const CONTEXT_INLINE_VOICE_CHAT_IN_PROGRESS = new RawContextKey('inlineVoiceChatInProgress', false, { type: 'boolean', description: localize('inlineVoiceChatInProgress', "True when voice recording from microphone is in progress for inline chat.") }); +const CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS = new RawContextKey('voiceChatInViewInProgress', false, { type: 'boolean', description: localize('voiceChatInViewInProgress', "True when voice recording from microphone is in progress in the chat view.") }); +const CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS = new RawContextKey('voiceChatInEditorInProgress', false, { type: 'boolean', description: localize('voiceChatInEditorInProgress', "True when voice recording from microphone is in progress in the chat editor.") }); + +type VoiceChatSessionContext = 'inline' | 'quick' | 'view' | 'editor'; interface IVoiceChatSessionController { readonly onDidAcceptInput: Event; + readonly onDidCancelInput: Event; + + readonly context: VoiceChatSessionContext; focusInput(): void; acceptInput(): void; updateInput(text: string): void; } -function getController(controller: InlineChatController): IVoiceChatSessionController; -function getController(context: unknown): IVoiceChatSessionController | undefined; -function getController(context: unknown): IVoiceChatSessionController | undefined { - if (context instanceof InlineChatController) { +class VoiceChatSessionControllerFactory { + + static create(accessor: ServicesAccessor, context: 'inline'): Promise; + static create(accessor: ServicesAccessor, context: 'quick'): Promise; + static create(accessor: ServicesAccessor, context: 'view'): Promise; + static create(accessor: ServicesAccessor, context: 'focussed'): Promise; + static async create(accessor: ServicesAccessor, context: 'inline' | 'quick' | 'view' | 'focussed'): Promise { + const chatWidgetService = accessor.get(IChatWidgetService); + const chatService = accessor.get(IChatService); + const viewsService = accessor.get(IViewsService); + const chatContributionService = accessor.get(IChatContributionService); + const editorService = accessor.get(IEditorService); + const quickChatService = accessor.get(IQuickChatService); + const layoutService = accessor.get(IWorkbenchLayoutService); + + // Currently Focussed Context + if (context === 'focussed') { + + // Try with the chat widget service, which currently + // only supports the chat view and quick chat + // https://github.com/microsoft/vscode/issues/191191 + const chatInput = chatWidgetService.lastFocusedWidget; + if (chatInput?.hasInputFocus()) { + // Unfortunately there does not seem to be a better way + // to figure out if the chat widget is in a part or picker + if ( + layoutService.hasFocus(Parts.SIDEBAR_PART) || + layoutService.hasFocus(Parts.PANEL_PART) || + layoutService.hasFocus(Parts.AUXILIARYBAR_PART) + ) { + return VoiceChatSessionControllerFactory.doCreateForChatView(chatInput, viewsService, chatContributionService); + } + + if (layoutService.hasFocus(Parts.EDITOR_PART)) { + return VoiceChatSessionControllerFactory.doCreateForChatEditor(chatInput, viewsService, chatContributionService); + } + + return VoiceChatSessionControllerFactory.doCreateForQuickChat(chatInput, quickChatService); + } + + // Try with the inline chat + const activeCodeEditor = getCodeEditor(editorService.activeTextEditorControl); + if (activeCodeEditor) { + const inlineChat = InlineChatController.get(activeCodeEditor); + if (inlineChat?.hasFocus()) { + return VoiceChatSessionControllerFactory.doCreateForInlineChat(inlineChat); + } + } + } + + // View Chat + if (context === 'view') { + const provider = firstOrDefault(chatService.getProviderInfos()); + if (provider) { + const chatView = await chatWidgetService.revealViewForProvider(provider.id); + if (chatView) { + return VoiceChatSessionControllerFactory.doCreateForChatView(chatView, viewsService, chatContributionService); + } + } + } + + // Inline Chat + if (context === 'inline') { + const activeCodeEditor = getCodeEditor(editorService.activeTextEditorControl); + if (activeCodeEditor) { + const inlineChat = InlineChatController.get(activeCodeEditor); + if (inlineChat) { + return VoiceChatSessionControllerFactory.doCreateForInlineChat(inlineChat); + } + } + } + + // Quick Chat + if (context === 'quick') { + quickChatService.open(); + + const quickChat = chatWidgetService.lastFocusedWidget; + if (quickChat) { + return VoiceChatSessionControllerFactory.doCreateForQuickChat(quickChat, quickChatService); + } + } + + return undefined; + } + + private static doCreateForChatView(chatView: IChatWidget, viewsService: IViewsService, chatContributionService: IChatContributionService): IVoiceChatSessionController { + return VoiceChatSessionControllerFactory.doCreateForChatViewOrEditor('view', chatView, viewsService, chatContributionService); + } + + private static doCreateForChatEditor(chatView: IChatWidget, viewsService: IViewsService, chatContributionService: IChatContributionService): IVoiceChatSessionController { + return VoiceChatSessionControllerFactory.doCreateForChatViewOrEditor('editor', chatView, viewsService, chatContributionService); + } + + private static doCreateForChatViewOrEditor(context: 'view' | 'editor', chatView: IChatWidget, viewsService: IViewsService, chatContributionService: IChatContributionService): IVoiceChatSessionController { return { - onDidAcceptInput: context.onDidAcceptInput, - focusInput: () => context.focus(), - acceptInput: () => context.acceptInput(), - updateInput: text => context.updateInput(text) + context, + onDidAcceptInput: chatView.onDidAcceptInput, + // TODO@bpasero cancellation needs to work better for chat editors that are not view bound + onDidCancelInput: Event.filter(viewsService.onDidChangeViewVisibility, e => e.id === chatContributionService.getViewIdForProvider(chatView.providerId)), + focusInput: () => chatView.focusInput(), + acceptInput: () => chatView.acceptInput(), + updateInput: text => chatView.updateInput(text) }; } - if (isExecuteActionContext(context)) { - return context.widget; + private static doCreateForQuickChat(quickChat: IChatWidget, quickChatService: IQuickChatService): IVoiceChatSessionController { + return { + context: 'quick', + onDidAcceptInput: quickChat.onDidAcceptInput, + onDidCancelInput: quickChatService.onDidClose, + focusInput: () => quickChat.focusInput(), + acceptInput: () => quickChat.acceptInput(), + updateInput: text => quickChat.updateInput(text) + }; } - return undefined; + private static doCreateForInlineChat(inlineChat: InlineChatController,): IVoiceChatSessionController { + const inlineChatSession = inlineChat.run(); + + return { + context: 'inline', + onDidAcceptInput: inlineChat.onDidAcceptInput, + onDidCancelInput: Event.any( + inlineChat.onDidCancelInput, + Event.fromPromise(inlineChatSession) + ), + focusInput: () => inlineChat.focus(), + acceptInput: () => inlineChat.acceptInput(), + updateInput: text => inlineChat.updateInput(text) + }; + } } -class VoiceChatSession { +interface ActiveVoiceChatSession { + readonly controller: IVoiceChatSessionController; + readonly disposables: DisposableStore; +} - private static instance: VoiceChatSession | undefined = undefined; - static getInstance(instantiationService: IInstantiationService): VoiceChatSession { - if (!VoiceChatSession.instance) { - VoiceChatSession.instance = instantiationService.createInstance(VoiceChatSession); +class VoiceChatSessions { + + private static instance: VoiceChatSessions | undefined = undefined; + static getInstance(instantiationService: IInstantiationService): VoiceChatSessions { + if (!VoiceChatSessions.instance) { + VoiceChatSessions.instance = instantiationService.createInstance(VoiceChatSessions); } - return VoiceChatSession.instance; + return VoiceChatSessions.instance; } private voiceChatInProgressKey = CONTEXT_VOICE_CHAT_IN_PROGRESS.bindTo(this.contextKeyService); private voiceChatGettingReadyKey = CONTEXT_VOICE_CHAT_GETTING_READY.bindTo(this.contextKeyService); - private currentVoiceChatSession: DisposableStore | undefined = undefined; + private quickVoiceChatInProgressKey = CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS.bindTo(this.contextKeyService); + private inlineVoiceChatInProgressKey = CONTEXT_INLINE_VOICE_CHAT_IN_PROGRESS.bindTo(this.contextKeyService); + private voiceChatInViewInProgressKey = CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS.bindTo(this.contextKeyService); + private voiceChatInEditorInProgressKey = CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS.bindTo(this.contextKeyService); + + private currentVoiceChatSession: ActiveVoiceChatSession | undefined = undefined; private voiceChatSessionIds = 0; constructor( @@ -83,33 +223,60 @@ class VoiceChatSession { @IWorkbenchVoiceRecognitionService private readonly voiceRecognitionService: IWorkbenchVoiceRecognitionService ) { } - async start(context: IVoiceChatSessionController): Promise { + async start(controller: IVoiceChatSessionController): Promise { this.stop(); - this.voiceChatGettingReadyKey.set(true); - this.currentVoiceChatSession = new DisposableStore(); + const voiceChatSessionId = ++this.voiceChatSessionIds; + this.currentVoiceChatSession = { + controller, + disposables: new DisposableStore() + }; const cts = new CancellationTokenSource(); - this.currentVoiceChatSession.add(toDisposable(() => cts.dispose(true))); + this.currentVoiceChatSession.disposables.add(toDisposable(() => cts.dispose(true))); - context.focusInput(); + this.currentVoiceChatSession.disposables.add(controller.onDidAcceptInput(() => this.stop(voiceChatSessionId, controller.context))); + this.currentVoiceChatSession.disposables.add(controller.onDidCancelInput(() => this.stop(voiceChatSessionId, controller.context))); + + controller.updateInput(''); + controller.focusInput(); + + this.voiceChatGettingReadyKey.set(true); const onDidTranscribe = await this.voiceRecognitionService.transcribe(cts.token, { - onDidCancel: () => this.stop(voiceChatSessionId) + onDidCancel: () => this.stop(voiceChatSessionId, controller.context) }); - if (cts.token.isCancellationRequested) { - return Disposable.None; - } - const voiceChatSessionId = ++this.voiceChatSessionIds; + if (cts.token.isCancellationRequested) { + return; + } this.voiceChatGettingReadyKey.set(false); this.voiceChatInProgressKey.set(true); + switch (controller.context) { + case 'inline': + this.inlineVoiceChatInProgressKey.set(true); + break; + case 'quick': + this.quickVoiceChatInProgressKey.set(true); + break; + case 'view': + this.voiceChatInViewInProgressKey.set(true); + break; + case 'editor': + this.voiceChatInEditorInProgressKey.set(true); + break; + } + + this.registerTranscriptionListener(this.currentVoiceChatSession, onDidTranscribe); + } + + private registerTranscriptionListener(session: ActiveVoiceChatSession, onDidTranscribe: Event) { let lastText: string | undefined = undefined; let lastTextSimilarCount = 0; - this.currentVoiceChatSession.add(onDidTranscribe(text => { + session.disposables.add(onDidTranscribe(text => { if (!text && lastText) { text = lastText; } @@ -123,17 +290,12 @@ class VoiceChatSession { } if (lastTextSimilarCount >= 2) { - context.acceptInput(); + session.controller.acceptInput(); } else { - context.updateInput(text); + session.controller.updateInput(text); } - } })); - - this.currentVoiceChatSession.add(context.onDidAcceptInput(() => this.stop(voiceChatSessionId))); - - return toDisposable(() => this.stop(voiceChatSessionId)); } private isSimilarTranscription(textA: string, textB: string): boolean { @@ -150,20 +312,36 @@ class VoiceChatSession { ); } - stop(voiceChatSessionId = this.voiceChatSessionIds): void { - if (!this.currentVoiceChatSession) { + stop(voiceChatSessionId = this.voiceChatSessionIds, context?: VoiceChatSessionContext): void { + if ( + !this.currentVoiceChatSession || + this.voiceChatSessionIds !== voiceChatSessionId || + (context && this.currentVoiceChatSession.controller.context !== context) + ) { return; } - if (this.voiceChatSessionIds !== voiceChatSessionId) { - return; - } - - this.currentVoiceChatSession.dispose(); + this.currentVoiceChatSession.disposables.dispose(); this.currentVoiceChatSession = undefined; this.voiceChatGettingReadyKey.set(false); this.voiceChatInProgressKey.set(false); + + this.quickVoiceChatInProgressKey.set(false); + this.inlineVoiceChatInProgressKey.set(false); + this.voiceChatInViewInProgressKey.set(false); + this.voiceChatInEditorInProgressKey.set(false); + } + + accept(voiceChatSessionId = this.voiceChatSessionIds): void { + if ( + !this.currentVoiceChatSession || + this.voiceChatSessionIds !== voiceChatSessionId + ) { + return; + } + + this.currentVoiceChatSession.controller.acceptInput(); } } @@ -185,16 +363,11 @@ class VoiceChatInChatViewAction extends Action2 { } async run(accessor: ServicesAccessor): Promise { - const chatWidgetService = accessor.get(IChatWidgetService); - const chatService = accessor.get(IChatService); const instantiationService = accessor.get(IInstantiationService); - const provider = firstOrDefault(chatService.getProviderInfos()); - if (provider) { - const controller = await chatWidgetService.revealViewForProvider(provider.id); - if (controller) { - VoiceChatSession.getInstance(instantiationService).start(controller); - } + const controller = await VoiceChatSessionControllerFactory.create(accessor, 'view'); + if (controller) { + VoiceChatSessions.getInstance(instantiationService).start(controller); } } } @@ -217,23 +390,12 @@ class InlineVoiceChatAction extends Action2 { } async run(accessor: ServicesAccessor): Promise { - const editorService = accessor.get(IEditorService); const instantiationService = accessor.get(IInstantiationService); - const activeCodeEditor = getCodeEditor(editorService.activeTextEditorControl); - if (!activeCodeEditor) { - return; + const controller = await VoiceChatSessionControllerFactory.create(accessor, 'inline'); + if (controller) { + VoiceChatSessions.getInstance(instantiationService).start(controller); } - - const controller = InlineChatController.get(activeCodeEditor); - if (!controller) { - return; - } - - const inlineChatSession = controller.run(); - - const disposable = await VoiceChatSession.getInstance(instantiationService).start(getController(controller)); - inlineChatSession.finally(() => disposable.dispose()); } } @@ -255,16 +417,11 @@ class QuickVoiceChatAction extends Action2 { } async run(accessor: ServicesAccessor): Promise { - const quickChatService = accessor.get(IQuickChatService); - const chatWidgetService = accessor.get(IChatWidgetService); const instantiationService = accessor.get(IInstantiationService); - quickChatService.open(); - - const controller = chatWidgetService.lastFocusedWidget; + const controller = await VoiceChatSessionControllerFactory.create(accessor, 'quick'); if (controller) { - const disposable = await VoiceChatSession.getInstance(instantiationService).start(controller); - Event.once(quickChatService.onDidClose)(() => disposable.dispose()); + VoiceChatSessions.getInstance(instantiationService).start(controller); } } } @@ -280,16 +437,16 @@ class StartVoiceChatAction extends Action2 { value: localize('workbench.action.chat.startVoiceChat', "Start Voice Chat"), original: 'Start Voice Chat' }, - icon: Codicon.record, + icon: Codicon.mic, precondition: CONTEXT_VOICE_CHAT_GETTING_READY.negate(), menu: [{ id: MenuId.ChatExecute, - when: CONTEXT_VOICE_CHAT_IN_PROGRESS.negate(), + when: ContextKeyExpr.and(CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS.negate(), CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS.negate(), CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS.negate()), group: 'navigation', order: -1 }, { id: MENU_INLINE_CHAT_WIDGET, - when: CONTEXT_VOICE_CHAT_IN_PROGRESS.negate(), + when: CONTEXT_INLINE_VOICE_CHAT_IN_PROGRESS.negate(), group: 'main', order: -1 }] @@ -297,45 +454,27 @@ class StartVoiceChatAction extends Action2 { } async run(accessor: ServicesAccessor, context: unknown): Promise { - const editorService = accessor.get(IEditorService); - const chatWidgetService = accessor.get(IChatWidgetService); const instantiationService = accessor.get(IInstantiationService); const commandService = accessor.get(ICommandService); - let controller = getController(context); - if (!controller) { - - // Without a controller, this action potentially executed from - // a global keybinding, and thus we have to find the chat - // input that is currently focussed, or have a fallback - - // 1.) a chat input widget has focus - if (chatWidgetService.lastFocusedWidget?.hasInputFocus()) { - controller = chatWidgetService.lastFocusedWidget; - } - - // 2.) a inline chat input widget has focus - if (!controller) { - const activeCodeEditor = getCodeEditor(editorService.activeTextEditorControl); - if (activeCodeEditor) { - const chatInput = InlineChatController.get(activeCodeEditor); - if (chatInput?.hasFocus()) { - controller = getController(chatInput); - } - } - } - - // 3.) open a quick chat view - if (!controller) { - return commandService.executeCommand(QuickVoiceChatAction.ID); - } + if (isExecuteActionContext(context)) { + // if we already get a context when the action is executed + // from a toolbar within the chat widget, then make sure + // to move focus into the input field so that the controller + // is properly retrieved + // TODO@bpasero this will actually not work if the button + // is clicked from the inline editor while focus is in a + // chat input field in a view or picker + context.widget.focusInput(); } - if (!controller) { - return; + const controller = await VoiceChatSessionControllerFactory.create(accessor, 'focussed'); + if (controller) { + VoiceChatSessions.getInstance(instantiationService).start(controller); + } else { + // fallback to Quick Voice Chat command + commandService.executeCommand(QuickVoiceChatAction.ID); } - - VoiceChatSession.getInstance(instantiationService).start(controller); } } @@ -352,16 +491,141 @@ class StopVoiceChatAction extends Action2 { }, category: CHAT_CATEGORY, f1: true, - precondition: CONTEXT_VOICE_CHAT_IN_PROGRESS, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib + 100, + when: CONTEXT_VOICE_CHAT_IN_PROGRESS, + primary: KeyCode.Escape + }, + precondition: CONTEXT_VOICE_CHAT_IN_PROGRESS + }); + } + + run(accessor: ServicesAccessor): void { + VoiceChatSessions.getInstance(accessor.get(IInstantiationService)).stop(); + } +} + +class StopVoiceChatInChatViewAction extends Action2 { + + static readonly ID = 'workbench.action.chat.stopVoiceChatInChatView'; + + constructor() { + super({ + id: StopVoiceChatInChatViewAction.ID, + title: { + value: localize('workbench.action.chat.stopVoiceChatInChatView.label', "Stop Voice Chat (Chat View)"), + original: 'Stop Voice Chat (Chat View)' + }, + category: CHAT_CATEGORY, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib + 100, + when: CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS, + primary: KeyCode.Escape + }, + precondition: CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS, icon: spinningLoading, menu: [{ id: MenuId.ChatExecute, - when: CONTEXT_VOICE_CHAT_IN_PROGRESS, + when: CONTEXT_VOICE_CHAT_IN_VIEW_IN_PROGRESS, group: 'navigation', order: -1 - }, { + }] + }); + } + + run(accessor: ServicesAccessor): void { + VoiceChatSessions.getInstance(accessor.get(IInstantiationService)).stop(undefined, 'view'); + } +} + +class StopVoiceChatInChatEditorAction extends Action2 { + + static readonly ID = 'workbench.action.chat.stopVoiceChatInChatEditor'; + + constructor() { + super({ + id: StopVoiceChatInChatEditorAction.ID, + title: { + value: localize('workbench.action.chat.stopVoiceChatInChatEditor.label', "Stop Voice Chat (Chat Editor)"), + original: 'Stop Voice Chat (Chat Editor)' + }, + category: CHAT_CATEGORY, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib + 100, + when: CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS, + primary: KeyCode.Escape + }, + precondition: CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS, + icon: spinningLoading, + menu: [{ + id: MenuId.ChatExecute, + when: CONTEXT_VOICE_CHAT_IN_EDITOR_IN_PROGRESS, + group: 'navigation', + order: -1 + }] + }); + } + + run(accessor: ServicesAccessor): void { + VoiceChatSessions.getInstance(accessor.get(IInstantiationService)).stop(undefined, 'editor'); + } +} + +class StopQuickVoiceChatAction extends Action2 { + + static readonly ID = 'workbench.action.chat.stopQuickVoiceChat'; + + constructor() { + super({ + id: StopQuickVoiceChatAction.ID, + title: { + value: localize('workbench.action.chat.stopQuickVoiceChat.label', "Stop Voice Chat (Quick Chat)"), + original: 'Stop Voice Chat (Quick Chat)' + }, + category: CHAT_CATEGORY, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib + 100, + when: CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS, + primary: KeyCode.Escape + }, + precondition: CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS, + icon: spinningLoading, + menu: [{ + id: MenuId.ChatExecute, + when: CONTEXT_QUICK_VOICE_CHAT_IN_PROGRESS, + group: 'navigation', + order: -1 + }] + }); + } + + run(accessor: ServicesAccessor): void { + VoiceChatSessions.getInstance(accessor.get(IInstantiationService)).stop(undefined, 'quick'); + } +} + +class StopInlineVoiceChatAction extends Action2 { + + static readonly ID = 'workbench.action.chat.stopInlineVoiceChat'; + + constructor() { + super({ + id: StopInlineVoiceChatAction.ID, + title: { + value: localize('workbench.action.chat.stopInlineVoiceChat.label', "Stop Voice Chat (Inline Editor)"), + original: 'Stop Voice Chat (Inline Editor)' + }, + category: CHAT_CATEGORY, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib + 100, + when: CONTEXT_INLINE_VOICE_CHAT_IN_PROGRESS, + primary: KeyCode.Escape + }, + precondition: CONTEXT_INLINE_VOICE_CHAT_IN_PROGRESS, + icon: spinningLoading, + menu: [{ id: MENU_INLINE_CHAT_WIDGET, - when: CONTEXT_VOICE_CHAT_IN_PROGRESS, + when: CONTEXT_INLINE_VOICE_CHAT_IN_PROGRESS, group: 'main', order: -1 }] @@ -369,7 +633,29 @@ class StopVoiceChatAction extends Action2 { } run(accessor: ServicesAccessor): void { - VoiceChatSession.getInstance(accessor.get(IInstantiationService)).stop(); + VoiceChatSessions.getInstance(accessor.get(IInstantiationService)).stop(undefined, 'inline'); + } +} + +class StopVoiceChatAndSubmitAction extends Action2 { + + static readonly ID = 'workbench.action.chat.stopVoiceChatAndSubmit'; + + constructor() { + super({ + id: StopVoiceChatAndSubmitAction.ID, + title: { + value: localize('workbench.action.chat.stopAndAcceptVoiceChat.label', "Stop Voice Chat and Submit"), + original: 'Stop Voice Chat and Submit' + }, + category: CHAT_CATEGORY, + f1: true, + precondition: CONTEXT_VOICE_CHAT_IN_PROGRESS + }); + } + + run(accessor: ServicesAccessor): void { + VoiceChatSessions.getInstance(accessor.get(IInstantiationService)).accept(); } } @@ -381,5 +667,11 @@ export function registerVoiceChatActions() { registerAction2(StartVoiceChatAction); registerAction2(StopVoiceChatAction); + registerAction2(StopVoiceChatAndSubmitAction); + + registerAction2(StopVoiceChatInChatViewAction); + registerAction2(StopVoiceChatInChatEditorAction); + registerAction2(StopQuickVoiceChatAction); + registerAction2(StopInlineVoiceChatAction); } } diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts index 1f00b285fa2..74590df5c4c 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadZoneWidget.ts @@ -129,7 +129,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget @IContextKeyService contextKeyService: IContextKeyService, @IConfigurationService private readonly configurationService: IConfigurationService ) { - super(editor, { keepEditorSelection: true }); + super(editor, { keepEditorSelection: true, isAccessible: true }); this._contextKeyService = contextKeyService.createScoped(this.domNode); this._scopedInstantiationService = instantiationService.createChild(new ServiceCollection( diff --git a/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts b/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts index c8ec74a476f..f087642f26b 100644 --- a/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts +++ b/src/vs/workbench/contrib/comments/browser/simpleCommentEditor.ts @@ -104,7 +104,8 @@ export class SimpleCommentEditor extends CodeEditorWidget { enabled: false }, autoClosingBrackets: configurationService.getValue('editor.autoClosingBrackets'), - quickSuggestions: false + quickSuggestions: false, + accessibilitySupport: configurationService.getValue<'auto' | 'off' | 'on'>('editor.accessibilitySupport'), }; } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index 0cb4cdeab43..16a37dc27c9 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -855,10 +855,12 @@ export class ExtensionEditor extends EditorPane { extensionPackReadme.style.maxWidth = '882px'; const extensionPack = append(extensionPackReadme, $('div', { class: 'extension-pack' })); - if (manifest.extensionPack!.length < 3) { + if (manifest.extensionPack!.length <= 3) { extensionPackReadme.classList.add('one-row'); - } else if (manifest.extensionPack!.length < 5) { + } else if (manifest.extensionPack!.length <= 6) { extensionPackReadme.classList.add('two-rows'); + } else if (manifest.extensionPack!.length <= 9) { + extensionPackReadme.classList.add('three-rows'); } else { extensionPackReadme.classList.add('more-rows'); } diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css index 575e6870b54..174b332538b 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css @@ -517,6 +517,10 @@ height: 224px; } +.extension-editor > .body > .content > .details > .readme-container > .extension-pack-readme.three-rows > .extension-pack { + height: 306px; +} + .extension-editor > .body > .content > .details > .readme-container > .extension-pack-readme.more-rows > .extension-pack { height: 326px; } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts index 023fee9cfd2..1d8cacb2985 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts @@ -101,6 +101,7 @@ export class InlineChatController implements IEditorContribution { private _messages = this._store.add(new Emitter()); readonly onDidAcceptInput = Event.filter(this._messages.event, m => m === Message.ACCEPT_INPUT, this._store); + readonly onDidCancelInput = Event.filter(this._messages.event, m => m === Message.CANCEL_INPUT || m === Message.CANCEL_SESSION, this._store); private readonly _sessionStore: DisposableStore = this._store.add(new DisposableStore()); private readonly _stashedSession: MutableDisposable = this._store.add(new MutableDisposable()); diff --git a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.css b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.css index 491aa3e8c17..d43c6a6e98b 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.css +++ b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.css @@ -17,5 +17,5 @@ .interactive-editor .input-cell-container .monaco-editor-background, .interactive-editor .input-cell-container .margin-view-overlays { - background-color: var(--vscode-notebook-cellEditorBackground, --vscode-editor-background); + background-color: var(--vscode-notebook-cellEditorBackground, var(--vscode-editor-background)); } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/cellOutputClipboard.ts b/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/cellOutputClipboard.ts index 576ded3d1f6..a544461cc28 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/cellOutputClipboard.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/clipboard/cellOutputClipboard.ts @@ -61,5 +61,7 @@ export const TEXT_BASED_MIMETYPES = [ 'application/x.notebook.stream', 'application/vnd.code.notebook.stderr', 'application/x.notebook.stderr', - 'text/plain' + 'text/plain', + 'text/markdown', + 'application/json' ]; diff --git a/src/vs/workbench/contrib/notebook/browser/controller/cellOutputActions.ts b/src/vs/workbench/contrib/notebook/browser/controller/cellOutputActions.ts index 956222d7a36..1f73db4342a 100644 --- a/src/vs/workbench/contrib/notebook/browser/controller/cellOutputActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/controller/cellOutputActions.ts @@ -17,13 +17,13 @@ import { ICellOutputViewModel, ICellViewModel, INotebookEditor, getNotebookEdito import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; -export const COPY_OUTPUT_COMMAND_ID = 'notebook.cellOutput.copyToClipboard'; +export const COPY_OUTPUT_COMMAND_ID = 'notebook.cellOutput.copy'; registerAction2(class CopyCellOutputAction extends Action2 { constructor() { super({ id: COPY_OUTPUT_COMMAND_ID, - title: localize('notebookActions.copyOutput', "Copy Output to Clipboard"), + title: localize('notebookActions.copyOutput', "Copy Output"), menu: { id: MenuId.NotebookOutputToolbar, when: NOTEBOOK_CELL_HAS_OUTPUTS @@ -55,7 +55,7 @@ registerAction2(class CopyCellOutputAction extends Action2 { const mimeType = outputViewModel.pickedMimeType?.mimeType; if (mimeType?.startsWith('image/')) { - const focusOptions = { skipReveal: true, outputId: outputViewModel.model.outputId }; + const focusOptions = { skipReveal: true, outputId: outputViewModel.model.outputId, altOutputId: outputViewModel.model.alternativeOutputId }; await notebookEditor.focusNotebookCell(outputViewModel.cellViewModel as ICellViewModel, 'output', focusOptions); notebookEditor.copyOutputImage(outputViewModel); } else { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 4f1d98b86bb..158b98692fe 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -152,6 +152,7 @@ export interface IFocusNotebookCellOptions { readonly focusEditorLine?: number; readonly minimalScrolling?: boolean; readonly outputId?: string; + readonly altOutputId?: string; } //#endregion diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index ba8ae7da1db..3830664287b 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -2350,7 +2350,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditorD } const focusElementId = options?.outputId ?? cell.id; - this._webview.focusOutput(focusElementId, this._webviewFocused); + this._webview.focusOutput(focusElementId, options?.altOutputId, this._webviewFocused); cell.updateEditState(CellEditState.Preview, 'focusNotebookCell'); cell.focusMode = CellFocusMode.Output; diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index b2359655e6d..b89185074f7 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -1548,7 +1548,8 @@ export class BackLayerWebView extends Themable { async copyImage(output: ICellOutputViewModel): Promise { this._sendMessageToWebview({ type: 'copyImage', - outputId: output.model.outputId + outputId: output.model.outputId, + altOutputId: output.model.alternativeOutputId }); } @@ -1608,7 +1609,7 @@ export class BackLayerWebView extends Themable { this.webview?.focus(); } - focusOutput(cellOrOutputId: string, viewFocused: boolean) { + focusOutput(cellOrOutputId: string, alternateId: string | undefined, viewFocused: boolean) { if (this._disposed) { return; } @@ -1620,6 +1621,7 @@ export class BackLayerWebView extends Themable { this._sendMessageToWebview({ type: 'focus-output', cellOrOutputId: cellOrOutputId, + alternateId: alternateId }); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts index e74e173ae33..b46964be307 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewMessages.ts @@ -269,11 +269,13 @@ export interface IShowOutputMessage { export interface ICopyImageMessage { readonly type: 'copyImage'; readonly outputId: string; + readonly altOutputId: string; } export interface IFocusOutputMessage { readonly type: 'focus-output'; readonly cellOrOutputId: string; + readonly alternateId?: string; } export interface IAckOutputHeight { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index 4715e64aa08..5197ae5372d 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -474,8 +474,9 @@ async function webviewPreloads(ctx: PreloadContext) { }); }; - function focusFirstFocusableOrContainerInOutput(cellOrOutputId: string) { - const cellOutputContainer = document.getElementById(cellOrOutputId); + function focusFirstFocusableOrContainerInOutput(cellOrOutputId: string, alternateId?: string) { + const cellOutputContainer = document.getElementById(cellOrOutputId) ?? + alternateId ? document.getElementById(alternateId!) : undefined; if (cellOutputContainer) { if (cellOutputContainer.contains(document.activeElement)) { return; @@ -1362,17 +1363,18 @@ async function webviewPreloads(ctx: PreloadContext) { }); }; - const copyOutputImage = async (outputId: string, retries = 5) => { + const copyOutputImage = async (outputId: string, altOutputId: string, retries = 5) => { if (!document.hasFocus() && retries > 0) { // copyImage can be called from outside of the webview, which means this function may be running whilst the webview is gaining focus. // Since navigator.clipboard.write requires the document to be focused, we need to wait for focus. // We cannot use a listener, as there is a high chance the focus is gained during the setup of the listener resulting in us missing it. - setTimeout(() => { copyOutputImage(outputId, retries - 1); }, 20); + setTimeout(() => { copyOutputImage(outputId, altOutputId, retries - 1); }, 20); return; } try { - const image = document.getElementById(outputId)?.querySelector('img'); + const image = document.getElementById(outputId)?.querySelector('img') + ?? document.getElementById(altOutputId)?.querySelector('img'); if (image) { await navigator.clipboard.write([new ClipboardItem({ 'image/png': new Promise((resolve) => { @@ -1500,9 +1502,7 @@ async function webviewPreloads(ctx: PreloadContext) { break; } case 'copyImage': { - - await copyOutputImage(event.data.outputId); - + await copyOutputImage(event.data.outputId, event.data.altOutputId); break; } case 'ack-dimension': { @@ -1524,7 +1524,7 @@ async function webviewPreloads(ctx: PreloadContext) { break; } case 'focus-output': - focusFirstFocusableOrContainerInOutput(event.data.cellOrOutputId); + focusFirstFocusableOrContainerInOutput(event.data.cellOrOutputId, event.data.alternateId); break; case 'decorations': { let outputContainer = document.getElementById(event.data.cellId); diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookStickyScroll.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookStickyScroll.test.ts index 708b8b7aea8..99cb8117b61 100644 --- a/src/vs/workbench/contrib/notebook/test/browser/notebookStickyScroll.test.ts +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookStickyScroll.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { isWeb } from 'vs/base/common/platform'; import { Event } from 'vs/base/common/event'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { mock } from 'vs/base/test/common/mock'; @@ -19,7 +20,7 @@ import { createNotebookCellList, setupInstantiationService, withTestNotebook } f import { OutlineTarget } from 'vs/workbench/services/outline/browser/outline'; -suite('NotebookEditorStickyScroll', () => { +(isWeb ? suite.skip : suite)('NotebookEditorStickyScroll', () => { let disposables: DisposableStore; let instantiationService: TestInstantiationService; @@ -92,7 +93,6 @@ suite('NotebookEditorStickyScroll', () => { const notebookOutlineEntries = getOutline(editor).entries; const resultingMap = nbStickyTestHelper(domNode, editor, cellList, notebookOutlineEntries); - await assertSnapshot(resultingMap); }); }); diff --git a/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts b/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts index c71388d51fb..145c7c81a1e 100644 --- a/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts +++ b/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts @@ -789,7 +789,7 @@ Registry.as(ConfigurationExtensions.Configuration).regis [CONFIGURATION_KEY_HOST_NAME]: { description: localize('remoteTunnelAccess.machineName', "The name under which the remote tunnel access is registered. If not set, the host name is used."), type: 'string', - scope: ConfigurationScope.MACHINE, + scope: ConfigurationScope.APPLICATION, pattern: '^(\\w[\\w-]*)?$', patternErrorMessage: localize('remoteTunnelAccess.machineNameRegex', "The name must only consist of letters, numbers, underscore and dash. It must not start with a dash."), maxLength: 20, @@ -798,7 +798,7 @@ Registry.as(ConfigurationExtensions.Configuration).regis [CONFIGURATION_KEY_PREVENT_SLEEP]: { description: localize('remoteTunnelAccess.preventSleep', "Prevent the computer from sleeping when remote tunnel access is turned on."), type: 'boolean', - scope: ConfigurationScope.MACHINE, + scope: ConfigurationScope.APPLICATION, default: false, } } diff --git a/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts b/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts index 6324613fbfd..14b1636c0b8 100644 --- a/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts +++ b/src/vs/workbench/contrib/search/browser/quickTextSearch/textSearchQuickAccess.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from 'vs/base/common/cancellation'; import { IMatch } from 'vs/base/common/filters'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { basenameOrAuthority, dirname } from 'vs/base/common/resources'; import { ThemeIcon } from 'vs/base/common/themables'; import { IRange, Range } from 'vs/editor/common/core/range'; @@ -15,8 +15,8 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ILabelService } from 'vs/platform/label/common/label'; import { WorkbenchCompressibleObjectTree, getSelectionKeyboardEvent } from 'vs/platform/list/browser/listService'; import { FastAndSlowPicks, IPickerQuickAccessItem, PickerQuickAccessProvider, Picks } from 'vs/platform/quickinput/browser/pickerQuickAccess'; -import { DefaultQuickAccessFilterValue } from 'vs/platform/quickinput/common/quickAccess'; -import { IKeyMods, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; +import { DefaultQuickAccessFilterValue, IQuickAccessProviderRunOptions } from 'vs/platform/quickinput/common/quickAccess'; +import { IKeyMods, IQuickPick, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor'; import { IViewsService } from 'vs/workbench/common/views'; @@ -75,6 +75,19 @@ export class TextSearchQuickAccess extends PickerQuickAccessProvider, token: CancellationToken, runOptions?: IQuickAccessProviderRunOptions): IDisposable { + const disposables = new DisposableStore(); + disposables.add(super.provide(picker, token, runOptions)); + disposables.add(picker.onDidHide(() => this.searchModel.searchResult.toggleHighlights(false))); + disposables.add(picker.onDidAccept(() => this.searchModel.searchResult.toggleHighlights(false))); + return disposables; + } + private get configuration() { const editorConfig = this._configurationService.getValue().workbench?.editor; const searchConfig = this._configurationService.getValue().search; @@ -247,6 +260,9 @@ export class TextSearchQuickAccess extends PickerQuickAccessProvider 0) { + this.searchModel.searchResult.toggleHighlights(true); + } if (matches.length >= MAX_FILES_SHOWN) { return syncResult; @@ -254,9 +270,14 @@ export class TextSearchQuickAccess extends PickerQuickAccessProvider { - return this._getPicksFromMatches(asyncResults, MAX_FILES_SHOWN - matches.length); - }) + additionalPicks: allMatches.asyncResults + .then(asyncResults => this._getPicksFromMatches(asyncResults, MAX_FILES_SHOWN - matches.length)) + .then(picks => { + if (picks.length > 0) { + this.searchModel.searchResult.toggleHighlights(true); + } + return picks; + }) }; } diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index b87d1347816..dd150169376 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -1894,7 +1894,6 @@ export class SearchView extends ViewPane { pinned, selection, revealIfVisible: true, - indexedCellOptions: element instanceof MatchInNotebook ? { index: element.cellIndex, selection: element.range() } : undefined, }; try { diff --git a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-rc.zsh b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-rc.zsh index 09718cfbab2..cc2cb83e0d2 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-rc.zsh +++ b/src/vs/workbench/contrib/terminal/browser/media/shellIntegration-rc.zsh @@ -51,7 +51,7 @@ if [ -n "${VSCODE_ENV_PREPEND:-}" ]; then IFS=':' read -rA ADDR <<< "$VSCODE_ENV_PREPEND" for ITEM in "${ADDR[@]}"; do VARNAME="$(echo ${ITEM%%=*})" - export $VARNAME="$(echo -e {ITEM#*=})${(P)VARNAME}" + export $VARNAME="$(echo -e ${ITEM#*=})${(P)VARNAME}" done unset VSCODE_ENV_PREPEND fi @@ -59,7 +59,7 @@ if [ -n "${VSCODE_ENV_APPEND:-}" ]; then IFS=':' read -rA ADDR <<< "$VSCODE_ENV_APPEND" for ITEM in "${ADDR[@]}"; do VARNAME="$(echo ${ITEM%%=*})" - export $VARNAME="${(P)VARNAME}$(echo -e {ITEM#*=})" + export $VARNAME="${(P)VARNAME}$(echo -e ${ITEM#*=})" done unset VSCODE_ENV_APPEND fi diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 28b600107aa..4092072afd7 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -590,6 +590,7 @@ export interface ITerminalInstance { onDidBlur: Event; onDidInputData: Event; onDidChangeSelection: Event; + onDidRunText: Event; /** * An event that fires when a terminal is dropped on this instance via drag and drop. diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 3262f52d621..645bcb88540 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -322,6 +322,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { readonly onRequestAddInstanceToGroup = this._onRequestAddInstanceToGroup.event; private readonly _onDidChangeHasChildProcesses = this._register(new Emitter()); readonly onDidChangeHasChildProcesses = this._onDidChangeHasChildProcesses.event; + private readonly _onDidRunText = this._register(new Emitter()); + readonly onDidRunText = this._onDidRunText.event; constructor( private readonly _terminalShellTypeContextKey: IContextKey, @@ -974,6 +976,13 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { return false; } + // Prevent default when shift+tab is being sent to the terminal to avoid it bubbling up + // and changing focus https://github.com/microsoft/vscode/issues/188329 + if (event.key === 'Tab' && event.shiftKey) { + event.preventDefault(); + return true; + } + // Always have alt+F4 skip the terminal on Windows and allow it to be handled by the // system if (isWindows && event.altKey && event.key === 'F4' && !event.ctrlKey) { @@ -1255,6 +1264,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._onDidInputData.fire(this); this.xterm?.suggestController?.handleNonXtermData(text); this.xterm?.scrollToBottom(); + this._onDidRunText.fire(); } async sendPath(originalPath: string | URI, addNewLine: boolean): Promise { diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts index fcf7a078831..12aaa0b0bb6 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/xtermTerminal.ts @@ -120,6 +120,7 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, ID readonly raw: RawXtermTerminal; private _core: IXtermCore; private static _suggestedRendererType: 'canvas' | 'dom' | undefined = undefined; + private static _checkedWebglCompatible = false; private _attached?: { container: HTMLElement; options: IXtermAttachToElementOptions }; private _isPhysicalMouseWheel = MouseWheelClassifier.INSTANCE.isPhysicalMouseWheel(); @@ -677,6 +678,25 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, ID if (!this.raw.element || this._webglAddon) { return; } + + // Check if the the WebGL renderer is compatible with xterm.js: + // - https://github.com/microsoft/vscode/issues/190195 + // - https://github.com/xtermjs/xterm.js/issues/4665 + // - https://bugs.chromium.org/p/chromium/issues/detail?id=1476475 + if (!XtermTerminal._checkedWebglCompatible) { + XtermTerminal._checkedWebglCompatible = true; + const checkCanvas = document.createElement('canvas'); + const checkGl = checkCanvas.getContext('webgl2'); + const debugInfo = checkGl?.getExtension('WEBGL_debug_renderer_info'); + if (checkGl && debugInfo) { + const renderer = checkGl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL); + if (renderer.startsWith('ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero)')) { + this._disableWebglForThisSession(); + return; + } + } + } + const Addon = await this._getWebglAddonConstructor(); this._webglAddon = new Addon(); this._disposeOfCanvasRenderer(); @@ -701,12 +721,16 @@ export class XtermTerminal extends DisposableStore implements IXtermTerminal, ID if (!neverMeasureRenderTime && this._configHelper.config.gpuAcceleration !== 'off') { this._measureRenderTime(); } - XtermTerminal._suggestedRendererType = 'canvas'; - this._disposeOfWebglRenderer(); - this._enableCanvasRenderer(); + this._disableWebglForThisSession(); } } + private _disableWebglForThisSession() { + XtermTerminal._suggestedRendererType = 'canvas'; + this._disposeOfWebglRenderer(); + this._enableCanvasRenderer(); + } + private async _enableCanvasRenderer(): Promise { if (!this.raw.element || this._canvasAddon) { return; diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index 8a8803cd23f..dde5c45fd5c 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -609,6 +609,17 @@ const terminalConfiguration: IConfigurationNode = { type: 'boolean', default: false }, + [TerminalSettingId.FocusAfterRun]: { + markdownDescription: localize('terminal.integrated.focusAfterRun', "Controls whether the terminal, accessible buffer, or neither will be focused after `Terminal: Run Selected Text In Active Terminal` has been run."), + enum: ['terminal', 'accessible-buffer', 'none'], + default: 'none', + tags: ['accessibility'], + markdownEnumDescriptions: [ + localize('terminal.integrated.focusAfterRun.terminal', "Always focus the terminal."), + localize('terminal.integrated.focusAfterRun.accessible-buffer', "Always focus the accessible buffer."), + localize('terminal.integrated.focusAfterRun.none', "Do nothing."), + ] + } } }; diff --git a/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts b/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts index ab689e76e87..204505dd1e6 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/xterm/xtermTerminal.test.ts @@ -256,7 +256,9 @@ suite('XtermTerminal', () => { }); suite('renderers', () => { - test('should re-evaluate gpu acceleration auto when the setting is changed', async () => { + // This is skipped until the webgl renderer bug is fixed in Chromium + // https://bugs.chromium.org/p/chromium/issues/detail?id=1476475 + test.skip('should re-evaluate gpu acceleration auto when the setting is changed', async () => { // Check initial state strictEqual(TestWebglAddon.isEnabled, false); diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts index f404c684fbd..682d14ccde1 100644 --- a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts +++ b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminal.accessibility.contribution.ts @@ -7,11 +7,12 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from 'vs/platform/accessibility/common/accessibility'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -import { terminalTabFocusModeContextKey } from 'vs/platform/terminal/common/terminal'; +import { TerminalSettingId, terminalTabFocusModeContextKey } from 'vs/platform/terminal/common/terminal'; import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; import { AccessibilityHelpAction } from 'vs/workbench/contrib/accessibility/browser/accessibleViewActions'; import { ITerminalContribution, ITerminalInstance, ITerminalService, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; @@ -59,9 +60,18 @@ class AccessibleBufferContribution extends DisposableStore implements ITerminalC private readonly _instance: ITerminalInstance, processManager: ITerminalProcessManager, widgetManager: TerminalWidgetManager, - @IInstantiationService private readonly _instantiationService: IInstantiationService + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @IConfigurationService configurationService: IConfigurationService ) { super(); + this.add(_instance.onDidRunText(() => { + const focusAfterRun = configurationService.getValue(TerminalSettingId.FocusAfterRun); + if (focusAfterRun === 'terminal') { + _instance.focus(true); + } else if (focusAfterRun === 'accessible-buffer') { + this.show(); + } + })); } layout(xterm: IXtermTerminal & { raw: Terminal }): void { this._xterm = xterm; diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibilityHelp.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibilityHelp.ts index 206e04d47cc..340427fd528 100644 --- a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibilityHelp.ts @@ -11,7 +11,7 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { ShellIntegrationStatus, WindowsShellType } from 'vs/platform/terminal/common/terminal'; +import { ShellIntegrationStatus, TerminalSettingId, WindowsShellType } from 'vs/platform/terminal/common/terminal'; import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration'; import { AccessibleViewType, IAccessibleContentProvider, IAccessibleViewOptions } from 'vs/workbench/contrib/accessibility/browser/accessibleView'; import { ITerminalInstance, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; @@ -60,7 +60,14 @@ export class TerminalAccessibleContentProvider extends Disposable implements IAc const kb = this._keybindingService.lookupKeybindings(commandId); // Run recent command has multiple keybindings. lookupKeybinding just returns the first one regardless of the when context. // Thus, we have to check if accessibility mode is enabled to determine which keybinding to use. - return this._accessibilityService.isScreenReaderOptimized() ? format(msg, kb[1].getAriaLabel()) : format(msg, kb[0].getAriaLabel()); + const isScreenReaderOptimized = this._accessibilityService.isScreenReaderOptimized(); + if (isScreenReaderOptimized && kb[1]) { + format(msg, kb[1].getAriaLabel()); + } else if (kb[0]) { + format(msg, kb[0].getAriaLabel()); + } else { + return format(noKbMsg, commandId); + } } const kb = this._keybindingService.lookupKeybinding(commandId, this._contextKeyService)?.getAriaLabel(); return !kb ? format(noKbMsg, commandId) : format(msg, kb); @@ -86,6 +93,7 @@ export class TerminalAccessibleContentProvider extends Disposable implements IAc } content.push(this._descriptionForCommand(TerminalCommandId.OpenDetectedLink, localize('openDetectedLink', 'The Open Detected Link ({0}) command enables screen readers to easily open links found in the terminal.'), localize('openDetectedLinkNoKb', 'The Open Detected Link command enables screen readers to easily open links found in the terminal and is currently not triggerable by a keybinding.'))); content.push(this._descriptionForCommand(TerminalCommandId.NewWithProfile, localize('newWithProfile', 'The Create New Terminal (With Profile) ({0}) command allows for easy terminal creation using a specific profile.'), localize('newWithProfileNoKb', 'The Create New Terminal (With Profile) command allows for easy terminal creation using a specific profile and is currently not triggerable by a keybinding.'))); + content.push(localize('focusAfterRun', 'Configure what gets focused after running selected text in the terminal with `{0}`.', TerminalSettingId.FocusAfterRun)); content.push(localize('accessibilitySettings', 'Access accessibility settings such as `terminal.integrated.tabFocusMode` via the Preferences: Open Accessibility Settings command.')); return content.join('\n\n'); } diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleWidget.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleWidget.ts index 6b43d58d553..448cd3da0e4 100644 --- a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleWidget.ts +++ b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/terminalAccessibleWidget.ts @@ -118,17 +118,13 @@ export abstract class TerminalAccessibleWidget extends DisposableStore { // On escape, hide the accessible buffer and force focus onto the terminal this.hide(true); break; - case KeyCode.Tab: - // On tab or shift+tab, hide the accessible buffer and perform the default tab - // behavior - this.hide(); - break; } })); this.add(this._editorWidget.onDidFocusEditorText(async () => { this._terminalService.setActiveInstance(this._instance as ITerminalInstance); this._xtermElement.classList.add(ClassName.Hide); })); + this.add(this._editorWidget.onDidBlurEditorText(async () => this.hide())); } registerListeners(): void { diff --git a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/textAreaSyncAddon.ts b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/textAreaSyncAddon.ts index 9916a6f60d0..33d4b52e956 100644 --- a/src/vs/workbench/contrib/terminalContrib/accessibility/browser/textAreaSyncAddon.ts +++ b/src/vs/workbench/contrib/terminalContrib/accessibility/browser/textAreaSyncAddon.ts @@ -100,7 +100,7 @@ export class TextAreaSyncAddon extends Disposable implements ITerminalAddon { this._logService.debug(`TextAreaSyncAddon#updateCommandAndCursor: no line`); return; } - if (!!currentCommand.commandStartX) { + if (currentCommand.commandStartX !== undefined) { this._currentCommand = commandLine.substring(currentCommand.commandStartX); this._cursorX = buffer.cursorX - currentCommand.commandStartX; } else { diff --git a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkParsing.ts b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkParsing.ts index 1e8b5bec80f..0c1a3dd16a0 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkParsing.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/browser/terminalLinkParsing.ts @@ -63,9 +63,11 @@ function generateLinkSuffixRegex(eolOnly: boolean) { // The comments in the regex below use real strings/numbers for better readability, here's // the legend: - // - Path = foo - // - Row = 339 - // - Col = 12 + // - Path = foo + // - Row = 339 + // - Col = 12 + // - RowEnd = 341 + // - ColEnd = 14 // // These all support single quote ' in the place of " and [] in the place of () const lineAndColumnRegexClauses = [ @@ -78,7 +80,9 @@ function generateLinkSuffixRegex(eolOnly: boolean) { // "foo",339 // "foo",339:12 // "foo",339.12 - `(?::| |['"],)${r()}([:.]${c()})?` + eolSuffix, + // "foo",339.12-14 + // "foo",339.12-341.14 + `(?::| |['"],)${r()}([:.]${c()}(?:-(?:${re()}\.)?${ce()})?)?` + eolSuffix, // The quotes below are optional [#171652] // "foo", line 339 [#40468] // "foo", line 339, col 12 diff --git a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkParsing.test.ts b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkParsing.test.ts index c200c575208..453b1f95ec4 100644 --- a/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkParsing.test.ts +++ b/src/vs/workbench/contrib/terminalContrib/links/test/browser/terminalLinkParsing.test.ts @@ -47,6 +47,8 @@ const testLinks: ITestLink[] = [ { link: 'foo 339', prefix: undefined, suffix: ' 339', hasRow: true, hasCol: false }, { link: 'foo 339:12', prefix: undefined, suffix: ' 339:12', hasRow: true, hasCol: true }, { link: 'foo 339.12', prefix: undefined, suffix: ' 339.12', hasRow: true, hasCol: true }, + { link: 'foo 339.12-14', prefix: undefined, suffix: ' 339.12-14', hasRow: true, hasCol: true, hasRowEnd: false, hasColEnd: true }, + { link: 'foo 339.12-341.14', prefix: undefined, suffix: ' 339.12-341.14', hasRow: true, hasCol: true, hasRowEnd: true, hasColEnd: true }, // Double quotes { link: '"foo",339', prefix: '"', suffix: '",339', hasRow: true, hasCol: false }, diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts index a6da647dbcc..1e57d47d68a 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.contribution.ts @@ -66,10 +66,8 @@ registerAction2(class extends Action2 { // Try first to select the walkthrough on an active welcome page with no selected walkthrough for (const group of editorGroupsService.groups) { if (group.activeEditor instanceof GettingStartedInput) { - if (!group.activeEditor.selectedCategory) { - (group.activeEditorPane as GettingStartedPage).makeCategoryVisibleWhenAvailable(selectedCategory, selectedStep); - return; - } + (group.activeEditorPane as GettingStartedPage).makeCategoryVisibleWhenAvailable(selectedCategory, selectedStep); + return; } } @@ -106,7 +104,10 @@ registerAction2(class extends Action2 { editorService.openEditor({ resource: GettingStartedInput.RESOURCE, options: { selectedCategory: selectedCategory, selectedStep: selectedStep, preserveFocus: toSide ?? false } + }).then((editor) => { + (editor as GettingStartedPage)?.makeCategoryVisibleWhenAvailable(selectedCategory, selectedStep); }); + } } else { editorService.openEditor({ resource: GettingStartedInput.RESOURCE }); diff --git a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts index 077b248bc0f..864a19e1ce4 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts @@ -311,13 +311,12 @@ export class WorkspaceTrustUXHandler extends Disposable implements IWorkbenchCon this._register(this.workspaceTrustRequestService.onDidInitiateWorkspaceTrustRequestOnStartup(async () => { let titleString: string | undefined; - let checkboxString: string | undefined; let learnMoreString: string | undefined; let trustOption: string | undefined; let dontTrustOption: string | undefined; - if (await this.isAiGeneratedWorkspace() && this.productService.aiGeneratedWorkspaceTrust) { + const isAiGeneratedWorkspace = await this.isAiGeneratedWorkspace(); + if (isAiGeneratedWorkspace && this.productService.aiGeneratedWorkspaceTrust) { titleString = this.productService.aiGeneratedWorkspaceTrust.title; - checkboxString = this.productService.aiGeneratedWorkspaceTrust.checkboxText; learnMoreString = this.productService.aiGeneratedWorkspaceTrust.startupTrustRequestLearnMore; trustOption = this.productService.aiGeneratedWorkspaceTrust.trustOption; dontTrustOption = this.productService.aiGeneratedWorkspaceTrust.dontTrustOption; @@ -333,9 +332,9 @@ export class WorkspaceTrustUXHandler extends Disposable implements IWorkbenchCon const workspaceIdentifier = toWorkspaceIdentifier(this.workspaceContextService.getWorkspace()); const isSingleFolderWorkspace = isSingleFolderWorkspaceIdentifier(workspaceIdentifier); const isEmptyWindow = isEmptyWorkspaceIdentifier(workspaceIdentifier); - if (this.workspaceTrustManagementService.canSetParentFolderTrust()) { + if (!isAiGeneratedWorkspace && this.workspaceTrustManagementService.canSetParentFolderTrust()) { const name = basename(uriDirname((workspaceIdentifier as ISingleFolderWorkspaceIdentifier).uri)); - checkboxText = checkboxString ?? localize('checkboxString', "Trust the authors of all files in the parent folder '{0}'", name); + checkboxText = localize('checkboxString', "Trust the authors of all files in the parent folder '{0}'", name); } // Show Workspace Trust Start Dialog diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementChannelClient.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementChannelClient.ts index 305332829c9..c0815fcd50f 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementChannelClient.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementChannelClient.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ILocalExtension, IGalleryExtension, InstallOptions, InstallVSIXOptions, UninstallOptions, Metadata, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ILocalExtension, IGalleryExtension, InstallOptions, InstallVSIXOptions, UninstallOptions, Metadata, DidUninstallExtensionEvent, InstallExtensionEvent, InstallExtensionResult, UninstallExtensionEvent, InstallExtensionInfo } from 'vs/platform/extensionManagement/common/extensionManagement'; import { URI } from 'vs/base/common/uri'; import { ExtensionIdentifier, ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { ExtensionManagementChannelClient as BaseExtensionManagementChannelClient, ExtensionEventResult } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; @@ -76,6 +76,14 @@ export abstract class ProfileAwareExtensionManagementChannelClient extends BaseE return super.installFromGallery(extension, installOptions); } + override async installGalleryExtensions(extensions: InstallExtensionInfo[]): Promise { + const infos: InstallExtensionInfo[] = []; + for (const extension of extensions) { + infos.push({ ...extension, options: { ...extension.options, profileLocation: extension.options?.profileLocation ? (await this.getProfileLocation(extension.options?.profileLocation)) : undefined } }); + } + return super.installGalleryExtensions(infos); + } + override async uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise { options = { ...options, profileLocation: await this.getProfileLocation(options?.profileLocation) }; return super.uninstall(extension, options); diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index 5a146bee7bb..4f5c61daaa7 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -11457,6 +11457,9 @@ declare module 'vscode' { clear(): void; } + /** + * A collection of mutations that an extension can apply to a process environment. Applies to all scopes. + */ export interface GlobalEnvironmentVariableCollection extends EnvironmentVariableCollection { /** * Gets scope-specific environment variable collection for the extension. This enables alterations to @@ -11471,6 +11474,8 @@ declare module 'vscode' { * If a scope parameter is omitted, collection applicable to all relevant scopes for that parameter is * returned. For instance, if the 'workspaceFolder' parameter is not specified, the collection that applies * across all workspace folders will be returned. + * + * @return Environment variable collection for the passed in scope. */ getScoped(scope: EnvironmentVariableScope): EnvironmentVariableCollection; } diff --git a/test/smoke/src/areas/extensions/extensions.test.ts b/test/smoke/src/areas/extensions/extensions.test.ts index 7a4875bcd7f..a8120cb12bf 100644 --- a/test/smoke/src/areas/extensions/extensions.test.ts +++ b/test/smoke/src/areas/extensions/extensions.test.ts @@ -7,7 +7,7 @@ import { Application, Logger } from '../../../../automation'; import { installAllHandlers } from '../../utils'; export function setup(logger: Logger) { - describe('Extensions', () => { + describe.skip('Extensions', () => { // Shared before/after handling installAllHandlers(logger); diff --git a/yarn.lock b/yarn.lock index e651e4a891d..e2a9ec02d7b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3597,10 +3597,10 @@ electron-to-chromium@^1.4.202: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.207.tgz#9c3310ebace2952903d05dcaba8abe3a4ed44c01" integrity sha512-piH7MJDJp4rJCduWbVvmUd59AUne1AFBJ8JaRQvk0KzNTSUnZrVXHCZc+eg+CGE4OujkcLJznhGKD6tuAshj5Q== -electron@25.5.0: - version "25.5.0" - resolved "https://registry.yarnpkg.com/electron/-/electron-25.5.0.tgz#6465d49c0731424e3e48776628c35771697caf11" - integrity sha512-w1DNj1LuAk0Vaas1rQ0pAkTe2gZ5YG75J27mC2m88y0G6Do5b5YoFDaF84fOGQHeQ4j8tC5LngSgWhbwmqDlrw== +electron@25.8.0: + version "25.8.0" + resolved "https://registry.yarnpkg.com/electron/-/electron-25.8.0.tgz#60c84f1f256924ac5a0aff13276b901b0c43767a" + integrity sha512-T3kC1a/3ntSaYMCVVfUUc9v7myPzi6J2GP0Ad/CyfWKDPp054dGyKxb2EEjKnxQQ7wfjsT1JTEdBG04x6ekVBw== dependencies: "@electron/get" "^2.0.0" "@types/node" "^18.11.18"