Merge branch 'main' into joh/eslint-jsdoc

This commit is contained in:
Johannes
2023-09-01 10:42:07 +02:00
107 changed files with 1305 additions and 460 deletions
+1 -1
View File
@@ -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,
+2 -2
View File
@@ -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"
+27 -27
View File
@@ -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
+2 -2
View File
@@ -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": {
+3
View File
@@ -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
+16 -4
View File
@@ -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<i3
async move { Ok::<_, Infallible>(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<i3
Server::bind(&addr)
.serve(make_service_fn(|_| make_svc()))
.with_graceful_shutdown(async {
let _ = shutdown.wait().await;
})
.await
};
+18 -6
View File
@@ -14,7 +14,7 @@ use crate::tunnels::paths::{get_server_folder_name, SERVER_FOLDER_NAME};
use crate::update_service::{
unzip_downloaded_release, Platform, Release, TargetKind, UpdateService,
};
use crate::util::command::{capture_command, kill_tree};
use crate::util::command::{capture_command, capture_command_and_check_status, kill_tree};
use crate::util::errors::{wrap, AnyError, CodeError, ExtensionInstallFailed, WrappedError};
use crate::util::http::{self, BoxedHttp};
use crate::util::io::SilentCopyProgress;
@@ -416,11 +416,23 @@ impl<'a> 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(())
})
+8 -5
View File
@@ -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<Vec<Tunnel>, AnyError> {
async fn list_tunnels_with_tag(
&mut self,
tags: &[&'static str],
) -> Result<Vec<Tunnel>, 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<String, AnyError> {
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
+4 -4
View File
@@ -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"
}
]
@@ -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
+3 -3
View File
@@ -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'"
}
]
+1 -1
View File
@@ -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": [
@@ -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' : '');
+3 -1
View File
@@ -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;
}
@@ -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();
+3 -3
View File
@@ -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",
+2 -2
View File
@@ -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",
+1 -1
View File
@@ -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"
Binary file not shown.
+10 -11
View File
@@ -532,13 +532,16 @@ class TypeNavigationController<T> 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<T> implements IDisposable {
return;
}
if (this.isSelectionRangeChangeEvent(e)) {
return this.changeSelection(e);
}
if (this.isSelectionChangeEvent(e)) {
return this.changeSelection(e);
}
+4
View File
@@ -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;
}
+1
View File
@@ -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
+18
View File
@@ -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<T>(promise: Promise<T>): Event<T | undefined> {
const result = new Emitter<T | undefined>();
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.
*
+43 -1
View File
@@ -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<number>();
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<number>();
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<number>();
+40 -2
View File
@@ -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<Record<string, string>> {
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<string, string> {
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<string, string> = {};
// 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<string | undefined> {
@@ -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);
}
@@ -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<boolean>('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;
}
*/
@@ -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 => {
@@ -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);
};
}
}
+2 -2
View File
@@ -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;
@@ -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."),
}
}
+1 -1
View File
@@ -2799,7 +2799,7 @@ class EditorStickyScroll extends BaseEditorOption<EditorOption.stickyScroll, IEd
'editor.stickyScroll.scrollWithEditor': {
type: 'boolean',
default: defaults.scrollWithEditor,
description: nls.localize('editor.stickyScroll.scrollWithEditor', "When enabled it is possible to scroll the sticky scroll widget with the editor horizontal scrollbar.")
description: nls.localize('editor.stickyScroll.scrollWithEditor', "Enable scrolling of the sticky scroll widget with the editor's horizontal scrollbar.")
},
}
);
@@ -3,10 +3,11 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { compareBy, equals, findLastIndex, numberComparator, reverseOrder } from 'vs/base/common/arrays';
import { Comparator, CompareResult, compareBy, equals, findLastIndex, numberComparator, reverseOrder } from 'vs/base/common/arrays';
import { assertFn, checkAdjacentItems } from 'vs/base/common/assert';
import { CharCode } from 'vs/base/common/charCode';
import { SetMap } from 'vs/base/common/collections';
import { BugIndicatingError } from 'vs/base/common/errors';
import { LineRange } from 'vs/editor/common/core/lineRange';
import { OffsetRange } from 'vs/editor/common/core/offsetRange';
import { Position } from 'vs/editor/common/core/position';
@@ -302,7 +303,7 @@ export class AdvancedLinesDiffComputer implements ILinesDiffComputer {
if (moves.length === 0) {
return [];
}
const joinedMoves = [moves[0]];
let joinedMoves = [moves[0]];
for (let i = 1; i < moves.length; i++) {
const last = joinedMoves[joinedMoves.length - 1];
const current = moves[i];
@@ -324,6 +325,19 @@ export class AdvancedLinesDiffComputer implements ILinesDiffComputer {
joinedMoves.push(current);
}
// Ignore non moves
const originalChanges = MonotonousFinder.createOfSorted(changes, c => 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<TItem, TDomain> {
public static create<TItem, TDomain>(
items: TItem[],
itemToDomain: (item: TItem) => TDomain,
domainComparator: Comparator<TDomain>,
): MonotonousFinder<TItem, TDomain> {
items.sort((a, b) => domainComparator(itemToDomain(a), itemToDomain(b)));
return new MonotonousFinder(items, itemToDomain, domainComparator);
}
public static createOfSorted<TItem, TDomain>(
items: TItem[],
itemToDomain: (item: TItem) => TDomain,
domainComparator: Comparator<TDomain>,
): MonotonousFinder<TItem, TDomain> {
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<TDomain>,
) {
}
/**
* 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<T>(arr: T[], predicate: (item: T) => boolean): number {
@@ -865,7 +932,14 @@ function findLastIdxMonotonous<T>(arr: T[], predicate: (item: T) => boolean): nu
return i - 1;
}
export function findLastMonotonous<T>(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<T>(arr: T[], predicate: (item: T) => boolean): number {
@@ -882,6 +956,11 @@ function findFirstIdxMonotonous<T>(arr: T[], predicate: (item: T) => boolean): n
return i;
}
export function findFirstMonotonous<T>(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
@@ -1114,6 +1114,9 @@ export module StandaloneServices {
serviceCollection.set(IInstantiationService, instantiationService);
export function get<T>(serviceId: ServiceIdentifier<T>): T {
if (!initialized) {
initialize({});
}
const r = serviceCollection.get(serviceId);
if (!r) {
throw new Error('Missing service ' + serviceId);
@@ -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;
@@ -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 {
@@ -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,
@@ -47,7 +47,7 @@ export interface IQuickInputOptions {
renderers: IListRenderer<T, any>[],
options: IListOptions<T>,
): List<T>;
hoverDelegate: IHoverDelegate;
hoverDelegate?: IHoverDelegate;
styles: IQuickInputStyles;
}
@@ -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'));
@@ -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;
}
@@ -86,12 +86,6 @@ export class QuickInputService extends Themable implements IQuickInputService {
renderers: IListRenderer<T, any>[],
options: IWorkbenchListOptions<T>
) => this.instantiationService.createInstance(WorkbenchList, user, container, delegate, renderers, options) as List<T>,
hoverDelegate: {
showHover(options, focus) {
return undefined;
},
delay: 200
},
styles: this.computeStyles()
};
@@ -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
@@ -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 = <ICustomTreeViewDescriptor>{
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();
}
@@ -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);
@@ -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',
@@ -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
@@ -2264,6 +2264,11 @@ abstract class AbstractCreateEditorGroupAction extends Action2 {
override async run(accessor: ServicesAccessor): Promise<void> {
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 });
}
}
@@ -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 <body> element otherwise.
if (restoreFocus) {
locationView.focus();
}
return group;
}
@@ -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;
@@ -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;
}
}
@@ -93,7 +93,7 @@ export class NotificationsListDelegate implements IListVirtualDelegate<INotifica
if (isNonEmptyArray(notification.actions && notification.actions.secondary)) {
actions++; // secondary actions
}
this.offsetHelper.style.width = `${450 /* notifications container width */ - (10 /* padding */ + 26 /* severity icon */ + (actions * (24 + 8)) /* 24px (+8px padding) per action */ - 4 /* 4px less padding for last action */)}px`;
this.offsetHelper.style.width = `${450 /* notifications container width */ - (10 /* padding */ + 30 /* severity icon */ + (actions * 30) /* actions */ - (Math.max(actions - 1, 0) * 4) /* less padding for actions > 1 */)}px`;
// Render message into offset helper
const renderedMessage = NotificationMessageRenderer.render(notification.message);
@@ -533,7 +533,7 @@ const registry = Registry.as<IConfigurationRegistry>(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)."),
+1 -1
View File
@@ -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'] },
+2 -8
View File
@@ -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;
@@ -21,8 +21,8 @@ export const accessibleViewCurrentProviderId = new RawContextKey<string>('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,
@@ -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) });
}
@@ -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;
}
@@ -160,7 +160,7 @@ class AccessibleViewDisableHintAction extends Action2 {
primary: KeyMod.Alt | KeyCode.F6,
weight: KeybindingWeight.WorkbenchContrib
},
icon: Codicon.treeFilterClear,
icon: Codicon.bellSlash,
menu: [
commandPalette,
{
@@ -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<string>();
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');
}
@@ -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();
}
}
@@ -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
}
@@ -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(<IUntitledTextResourceEditorInput>{ 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);
@@ -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())
}
});
}
@@ -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<Ch
templateData.titleToolbar.context = element;
const isFiltered = !!(isResponseVM(element) && element.errorDetails?.responseIsFiltered);
CONTEXT_RESPONSE_FILTERED.bindTo(templateData.contextKeyService).set(isFiltered);
templateData.rowContainer.classList.toggle('interactive-request', isRequestVM(element));
templateData.rowContainer.classList.toggle('interactive-response', isResponseVM(element));
templateData.rowContainer.classList.toggle('interactive-welcome', isWelcomeVM(element));
templateData.rowContainer.classList.toggle('filtered-response', !!(isResponseVM(element) && element.errorDetails?.responseIsFiltered));
templateData.rowContainer.classList.toggle('filtered-response', isFiltered);
templateData.username.textContent = element.username;
if (element.avatarIconUri) {
@@ -449,9 +452,10 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
}
// Did this part's content change?
else if (!isInteractiveProgressTreeData(part) && !isInteractiveProgressTreeData(renderedPart) && !renderedPart.isFullyRendered) {
else if (!isInteractiveProgressTreeData(part) && !isInteractiveProgressTreeData(renderedPart)) {
const wordCountResult = this.getDataForProgressiveRender(element, part, renderedPart);
if (wordCountResult !== undefined) {
// Check if there are any new words to render
if (wordCountResult !== undefined && renderedPart.renderedWordCount !== wordCountResult?.actualWordCount) {
partsToRender[index] = {
renderedWordCount: wordCountResult.actualWordCount,
lastRenderTime: Date.now(),
@@ -535,6 +539,10 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
treeDisposables.add(tree.onDidChangeCollapseState(() => {
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<IChatResponsePro
}
renderElement(element: ITreeNode<IChatResponseProgressFileTreeData, void>, 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,
@@ -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<IDisposable> = this._register(new MutableDisposable<IDisposable>());
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();
@@ -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<boolean>;
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 <ITreeElement<ChatTreeItem>>{
@@ -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);
}
@@ -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);
@@ -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 {
@@ -8,6 +8,7 @@ import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
export const CONTEXT_RESPONSE_HAS_PROVIDER_ID = new RawContextKey<boolean>('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<string>('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<boolean>('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<boolean>('chatSessionRequestInProgress', false, { type: 'boolean', description: localize('interactiveSessionRequestInProgress', "True when the current request is still in progress.") });
export const CONTEXT_RESPONSE = new RawContextKey<boolean>('chatResponse', false, { type: 'boolean', description: localize('chatResponse', "The chat item is a response.") });
@@ -160,6 +160,7 @@ export class Response implements IResponse {
});
} else if (isCompleteInteractiveProgressTreeData(responsePart)) {
this._responseParts.push(responsePart);
this._updateRepr(quiet);
}
}
@@ -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 */
}
@@ -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<boolean>('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<boolean>('voiceChatInProgress', false, { type: 'boolean', description: localize('voiceChatInProgress', "True when voice recording from microphone is in progress.") });
const CONTEXT_VOICE_CHAT_GETTING_READY = new RawContextKey<boolean>('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<boolean>('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<boolean>('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<boolean>('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<boolean>('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<boolean>('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<unknown>;
readonly onDidCancelInput: Event<unknown>;
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<IVoiceChatSessionController | undefined>;
static create(accessor: ServicesAccessor, context: 'quick'): Promise<IVoiceChatSessionController | undefined>;
static create(accessor: ServicesAccessor, context: 'view'): Promise<IVoiceChatSessionController | undefined>;
static create(accessor: ServicesAccessor, context: 'focussed'): Promise<IVoiceChatSessionController | undefined>;
static async create(accessor: ServicesAccessor, context: 'inline' | 'quick' | 'view' | 'focussed'): Promise<IVoiceChatSessionController | undefined> {
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<IDisposable> {
async start(controller: IVoiceChatSessionController): Promise<void> {
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<string>) {
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<void> {
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<void> {
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<void> {
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<void> {
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);
}
}
@@ -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(
@@ -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'),
};
}
}
@@ -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');
}
@@ -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;
}
@@ -101,6 +101,7 @@ export class InlineChatController implements IEditorContribution {
private _messages = this._store.add(new Emitter<Message>());
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<StashedSession> = this._store.add(new MutableDisposable());
@@ -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));
}
@@ -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'
];
@@ -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 {
@@ -152,6 +152,7 @@ export interface IFocusNotebookCellOptions {
readonly focusEditorLine?: number;
readonly minimalScrolling?: boolean;
readonly outputId?: string;
readonly altOutputId?: string;
}
//#endregion
@@ -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;
@@ -1548,7 +1548,8 @@ export class BackLayerWebView<T extends ICommonCellInfo> extends Themable {
async copyImage(output: ICellOutputViewModel): Promise<void> {
this._sendMessageToWebview({
type: 'copyImage',
outputId: output.model.outputId
outputId: output.model.outputId,
altOutputId: output.model.alternativeOutputId
});
}
@@ -1608,7 +1609,7 @@ export class BackLayerWebView<T extends ICommonCellInfo> 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<T extends ICommonCellInfo> extends Themable {
this._sendMessageToWebview({
type: 'focus-output',
cellOrOutputId: cellOrOutputId,
alternateId: alternateId
});
}
@@ -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 {
@@ -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);
@@ -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);
});
});
@@ -789,7 +789,7 @@ Registry.as<IConfigurationRegistry>(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<IConfigurationRegistry>(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,
}
}
@@ -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<IPickerQuic
this.searchModel = this._instantiationService.createInstance(SearchModel);
}
override dispose(): void {
this.searchModel.dispose();
super.dispose();
}
override provide(picker: IQuickPick<IPickerQuickAccessItem>, 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<IWorkbenchEditorConfiguration>().workbench?.editor;
const searchConfig = this._configurationService.getValue<IWorkbenchSearchConfiguration>().search;
@@ -247,6 +260,9 @@ export class TextSearchQuickAccess extends PickerQuickAccessProvider<IPickerQuic
}
const matches = allMatches.syncResults;
const syncResult = this._getPicksFromMatches(matches, MAX_FILES_SHOWN);
if (syncResult.length > 0) {
this.searchModel.searchResult.toggleHighlights(true);
}
if (matches.length >= MAX_FILES_SHOWN) {
return syncResult;
@@ -254,9 +270,14 @@ export class TextSearchQuickAccess extends PickerQuickAccessProvider<IPickerQuic
return {
picks: syncResult,
additionalPicks: allMatches.asyncResults.then((asyncResults) => {
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;
})
};
}
@@ -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 {
@@ -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
@@ -590,6 +590,7 @@ export interface ITerminalInstance {
onDidBlur: Event<ITerminalInstance>;
onDidInputData: Event<ITerminalInstance>;
onDidChangeSelection: Event<ITerminalInstance>;
onDidRunText: Event<void>;
/**
* An event that fires when a terminal is dropped on this instance via drag and drop.
@@ -322,6 +322,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
readonly onRequestAddInstanceToGroup = this._onRequestAddInstanceToGroup.event;
private readonly _onDidChangeHasChildProcesses = this._register(new Emitter<boolean>());
readonly onDidChangeHasChildProcesses = this._onDidChangeHasChildProcesses.event;
private readonly _onDidRunText = this._register(new Emitter<void>());
readonly onDidRunText = this._onDidRunText.event;
constructor(
private readonly _terminalShellTypeContextKey: IContextKey<string>,
@@ -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<void> {
@@ -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<void> {
if (!this.raw.element || this._canvasAddon) {
return;
@@ -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."),
]
}
}
};
@@ -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);
@@ -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;
@@ -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');
}
@@ -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 {
@@ -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 {
@@ -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

Some files were not shown because too many files have changed in this diff Show More