diff --git a/.github/classifier.json b/.github/classifier.json index f39be53b8b9..dad8fe6cb76 100644 --- a/.github/classifier.json +++ b/.github/classifier.json @@ -78,13 +78,13 @@ "file-watcher": {"assign": ["bpasero"]}, "font-rendering": {"assign": []}, "formatting": {"assign": []}, + "getting-started": {"assign": ["bhavyaus"]}, "ghost-text": {"assign": ["hediet"]}, "git": {"assign": ["lszomoru"]}, "gpu": {"assign": ["deepak1556"]}, "grammar": {"assign": ["mjbvz"]}, "grid-view": {"assign": ["joaomoreno"]}, "html": {"assign": ["aeschli"]}, - "i18n": {"assign": []}, "icon-brand": {"assign": []}, "icons-product": {"assign": ["daviddossett"]}, "inlay-hints": {"assign": ["jrieken", "hediet"]}, diff --git a/cli/src/tunnels/dev_tunnels.rs b/cli/src/tunnels/dev_tunnels.rs index eb362ef8e5b..911bf547e2f 100644 --- a/cli/src/tunnels/dev_tunnels.rs +++ b/cli/src/tunnels/dev_tunnels.rs @@ -12,6 +12,7 @@ use crate::util::input::prompt_placeholder; use crate::{debug, info, log, spanf, trace, warning}; use async_trait::async_trait; use futures::TryFutureExt; +use lazy_static::lazy_static; use rand::prelude::IteratorRandom; use regex::Regex; use reqwest::StatusCode; @@ -121,7 +122,7 @@ impl AccessTokenProvider for LookupAccessTokenProvider { match tunnel_lookup { Ok(tunnel) => Ok(get_host_token_from_tunnel(&tunnel)), - Err(e) => Err(wrap(e, "failed to lookup tunnel")), + Err(e) => Err(wrap(e, "failed to lookup tunnel for host token")), } } } @@ -212,6 +213,14 @@ fn is_valid_name(name: &str) -> Result<(), InvalidTunnelName> { Ok(()) } +lazy_static! { + static ref HOST_TUNNEL_REQUEST_OPTIONS: TunnelRequestOptions = TunnelRequestOptions { + include_ports: true, + token_scopes: vec!["host".to_string()], + ..Default::default() + }; +} + /// Structure optionally passed into `start_existing_tunnel` to forward an existing tunnel. #[derive(Clone, Debug)] pub struct ExistingTunnel { @@ -260,6 +269,7 @@ impl DevTunnels { Ok(()) } + /// Renames the current tunnel to the new name. pub async fn rename_tunnel(&mut self, name: &str) -> Result<(), AnyError> { is_valid_name(name)?; @@ -269,7 +279,7 @@ impl DevTunnels { Some(t) => t, None => { debug!(self.log, "No code server tunnel found, creating new one"); - let (persisted, _) = self.create_tunnel(name).await?; + let (persisted, _) = self.create_tunnel(name, NO_REQUEST_OPTIONS).await?; self.launcher_tunnel.save(Some(persisted))?; return Ok(()); } @@ -282,7 +292,7 @@ impl DevTunnels { self.log.span("dev-tunnel.tag.get"), self.client.get_tunnel(&locator, NO_REQUEST_OPTIONS) ) - .map_err(|e| wrap(e, "failed to lookup tunnel"))?; + .map_err(|e| wrap(e, "failed to lookup original tunnel"))?; full_tunnel.tags = vec![name.to_string(), VSCODE_CLI_TUNNEL_TAG.to_string()]; spanf!( @@ -297,6 +307,70 @@ impl DevTunnels { Ok(()) } + /// Updates the name of the existing persisted tunnel to the new name. + /// Gracefully creates a new tunnel if the previous one was deleted. + async fn update_tunnel_name( + &mut self, + persisted: PersistedTunnel, + name: &str, + ) -> Result<(Tunnel, PersistedTunnel), AnyError> { + self.check_is_name_free(name).await?; + + debug!(self.log, "Tunnel name changed, applying updates..."); + + let (mut full_tunnel, mut persisted, is_new) = self + .get_or_create_tunnel(persisted, Some(name), NO_REQUEST_OPTIONS) + .await?; + if is_new { + return Ok((full_tunnel, persisted)); + } + + full_tunnel.tags = vec![name.to_string(), VSCODE_CLI_TUNNEL_TAG.to_string()]; + + let new_tunnel = spanf!( + self.log, + self.log.span("dev-tunnel.tag.update"), + self.client.update_tunnel(&full_tunnel, NO_REQUEST_OPTIONS) + ) + .map_err(|e| wrap(e, "failed to rename tunnel"))?; + + persisted.name = name.to_string(); + self.launcher_tunnel.save(Some(persisted.clone()))?; + + Ok((new_tunnel, persisted)) + } + + /// Gets the persisted tunnel from the service, or creates a new one. + /// If `create_with_new_name` is given, the new tunnel has that name + /// instead of the one previously persisted. + async fn get_or_create_tunnel( + &mut self, + persisted: PersistedTunnel, + create_with_new_name: Option<&str>, + options: &TunnelRequestOptions, + ) -> Result<(Tunnel, PersistedTunnel, /* is_new */ bool), AnyError> { + let tunnel_lookup = spanf!( + self.log, + self.log.span("dev-tunnel.tag.get"), + self.client.get_tunnel(&persisted.locator(), options) + ); + + match tunnel_lookup { + Ok(ft) => Ok((ft, persisted, false)), + Err(HttpError::ResponseError(e)) + if e.status_code == StatusCode::NOT_FOUND + || e.status_code == StatusCode::FORBIDDEN => + { + let (persisted, tunnel) = self + .create_tunnel(create_with_new_name.unwrap_or(&persisted.name), options) + .await?; + self.launcher_tunnel.save(Some(persisted.clone()))?; + Ok((tunnel, persisted, true)) + } + Err(e) => Err(wrap(e, "failed to lookup tunnel").into()), + } + } + /// Starts a new tunnel for the code server on the port. Unlike `start_new_tunnel`, /// this attempts to reuse or create a tunnel of a preferred name or of a generated friendly tunnel name. pub async fn start_new_launcher_tunnel( @@ -308,64 +382,23 @@ impl DevTunnels { Some(mut persisted) => { if let Some(name) = preferred_name { if persisted.name.ne(&name) { - self.check_is_name_free(&name).await?; - let mut full_tunnel = spanf!( - self.log, - self.log.span("dev-tunnel.tag.get"), - self.client - .get_tunnel(&persisted.locator(), NO_REQUEST_OPTIONS) - ) - .map_err(|e| wrap(e, "failed to lookup tunnel"))?; - - info!(self.log, "Updating name of existing tunnel"); - - full_tunnel.tags = - vec![name.to_string(), VSCODE_CLI_TUNNEL_TAG.to_string()]; - if spanf!( - self.log, - self.log.span("dev-tunnel.tag.update"), - self.client.update_tunnel(&full_tunnel, NO_REQUEST_OPTIONS) - ) - .is_ok() - { - persisted.name = name.to_string(); - self.launcher_tunnel.save(Some(persisted.clone()))?; - } + (_, persisted) = self.update_tunnel_name(persisted, &name).await?; } } - let tunnel_lookup = spanf!( - self.log, - self.log.span("dev-tunnel.tag.get"), - self.client.get_tunnel( - &persisted.locator(), - &TunnelRequestOptions { - include_ports: true, - token_scopes: vec!["host".to_string()], - ..Default::default() - } - ) - ); - - match tunnel_lookup { - Ok(ft) => (ft, persisted), - Err(HttpError::ResponseError(e)) - if e.status_code == StatusCode::NOT_FOUND - || e.status_code == StatusCode::FORBIDDEN => - { - let (persisted, tunnel) = self.create_tunnel(&persisted.name).await?; - self.launcher_tunnel.save(Some(persisted.clone()))?; - (tunnel, persisted) - } - Err(e) => return Err(AnyError::from(wrap(e, "failed to lookup tunnel"))), - } + let (tunnel, persisted, _) = self + .get_or_create_tunnel(persisted, None, &HOST_TUNNEL_REQUEST_OPTIONS) + .await?; + (tunnel, persisted) } None => { debug!(self.log, "No code server tunnel found, creating new one"); let name = self .get_name_for_tunnel(preferred_name, use_random_name) .await?; - let (persisted, full_tunnel) = self.create_tunnel(&name).await?; + let (persisted, full_tunnel) = self + .create_tunnel(&name, &HOST_TUNNEL_REQUEST_OPTIONS) + .await?; self.launcher_tunnel.save(Some(persisted.clone()))?; (full_tunnel, persisted) } @@ -419,7 +452,11 @@ impl DevTunnels { .await } - async fn create_tunnel(&mut self, name: &str) -> Result<(PersistedTunnel, Tunnel), AnyError> { + async fn create_tunnel( + &mut self, + name: &str, + options: &TunnelRequestOptions, + ) -> Result<(PersistedTunnel, Tunnel), AnyError> { info!(self.log, "Creating tunnel with the name: {}", name); let mut tried_recycle = false; @@ -433,7 +470,7 @@ impl DevTunnels { let result = spanf!( self.log, self.log.span("dev-tunnel.create"), - self.client.create_tunnel(&new_tunnel, NO_REQUEST_OPTIONS) + self.client.create_tunnel(&new_tunnel, options) ); match result { @@ -446,9 +483,9 @@ impl DevTunnels { } return Err(AnyError::from(TunnelCreationFailed( - name.to_string(), - "You've exceeded the 10 machine limit for the port fowarding service. Please remove other machines before trying to add this machine.".to_string(), - ))); + name.to_string(), + "You've exceeded the 10 machine limit for the port fowarding service. Please remove other machines before trying to add this machine.".to_string(), + ))); } Err(e) => { return Err(AnyError::from(TunnelCreationFailed( diff --git a/cli/src/util/prereqs.rs b/cli/src/util/prereqs.rs index 0070858ddeb..46bb0dc6dbf 100644 --- a/cli/src/util/prereqs.rs +++ b/cli/src/util/prereqs.rs @@ -17,6 +17,7 @@ use super::errors::AnyError; lazy_static! { static ref LDCONFIG_STDC_RE: Regex = Regex::new(r"libstdc\+\+.* => (.+)").unwrap(); static ref LDD_VERSION_RE: BinRegex = BinRegex::new(r"^ldd.*(.+)\.(.+)\s").unwrap(); + static ref GENERIC_VERSION_RE: Regex = Regex::new(r"^([0-9]+)\.([0-9]+)$").unwrap(); static ref LIBSTD_CXX_VERSION_RE: BinRegex = BinRegex::new(r"GLIBCXX_([0-9]+)\.([0-9]+)(?:\.([0-9]+))?").unwrap(); static ref MIN_CXX_VERSION: SimpleSemver = SimpleSemver::new(3, 4, 18); @@ -116,13 +117,23 @@ async fn check_musl_interpreter() -> Result<(), String> { #[allow(dead_code)] async fn check_glibc_version() -> Result<(), String> { - let ldd_version = capture_command("ldd", ["--version"]) - .await - .ok() - .and_then(|o| extract_ldd_version(&o.stdout)); + #[cfg(target_env = "gnu")] + let version = { + let v = unsafe { libc::gnu_get_libc_version() }; + let v = unsafe { std::ffi::CStr::from_ptr(v) }; + let v = v.to_str().unwrap(); + extract_generic_version(v) + }; + #[cfg(not(target_env = "gnu"))] + let version = { + capture_command("ldd", ["--version"]) + .await + .ok() + .and_then(|o| extract_ldd_version(&o.stdout)) + }; - if let Some(v) = ldd_version { - return if v.gte(&MIN_LDD_VERSION) { + if let Some(v) = version { + return if v >= *MIN_LDD_VERSION { Ok(()) } else { Err(format!( @@ -181,7 +192,7 @@ fn check_for_sufficient_glibcxx_versions(contents: Vec) -> Result<(), String }) .collect(); - if !all_versions.iter().any(|v| MIN_CXX_VERSION.gte(v)) { + if !all_versions.iter().any(|v| &*MIN_CXX_VERSION >= v) { return Err(format!( "find GLIBCXX >= 3.4.18 (but found {} instead) for GNU environments", all_versions @@ -195,6 +206,7 @@ fn check_for_sufficient_glibcxx_versions(contents: Vec) -> Result<(), String Ok(()) } +#[allow(dead_code)] fn extract_ldd_version(output: &[u8]) -> Option { LDD_VERSION_RE.captures(output).map(|m| SimpleSemver { major: m.get(1).map_or(0, |s| u32_from_bytes(s.as_bytes())), @@ -203,6 +215,15 @@ fn extract_ldd_version(output: &[u8]) -> Option { }) } +#[allow(dead_code)] +fn extract_generic_version(output: &str) -> Option { + GENERIC_VERSION_RE.captures(output).map(|m| SimpleSemver { + major: m.get(1).map_or(0, |s| s.as_str().parse().unwrap()), + minor: m.get(2).map_or(0, |s| s.as_str().parse().unwrap()), + patch: 0, + }) +} + fn extract_libstd_from_ldconfig(output: &[u8]) -> Option { String::from_utf8_lossy(output) .lines() @@ -215,13 +236,35 @@ fn u32_from_bytes(b: &[u8]) -> u32 { String::from_utf8_lossy(b).parse::().unwrap_or(0) } -#[derive(Debug, PartialEq)] +#[derive(Debug, Default, PartialEq, Eq)] struct SimpleSemver { major: u32, minor: u32, patch: u32, } +impl PartialOrd for SimpleSemver { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for SimpleSemver { + fn cmp(&self, other: &Self) -> Ordering { + let major = self.major.cmp(&other.major); + if major != Ordering::Equal { + return major; + } + + let minor = self.minor.cmp(&other.minor); + if minor != Ordering::Equal { + return minor; + } + + self.patch.cmp(&other.patch) + } +} + impl From<&SimpleSemver> for String { fn from(s: &SimpleSemver) -> Self { format!("v{}.{}.{}", s.major, s.minor, s.patch) @@ -243,18 +286,6 @@ impl SimpleSemver { patch, } } - - fn gte(&self, other: &SimpleSemver) -> bool { - match self.major.cmp(&other.major) { - Ordering::Greater => true, - Ordering::Less => false, - Ordering::Equal => match self.minor.cmp(&other.minor) { - Ordering::Greater => true, - Ordering::Less => false, - Ordering::Equal => self.patch >= other.patch, - }, - } - } } #[cfg(test)] @@ -284,13 +315,13 @@ mod tests { #[test] fn test_gte() { - assert!(SimpleSemver::new(1, 2, 3).gte(&SimpleSemver::new(1, 2, 3))); - assert!(SimpleSemver::new(1, 2, 3).gte(&SimpleSemver::new(0, 10, 10))); - assert!(SimpleSemver::new(1, 2, 3).gte(&SimpleSemver::new(1, 1, 10))); + assert!(SimpleSemver::new(1, 2, 3) >= SimpleSemver::new(1, 2, 3)); + assert!(SimpleSemver::new(1, 2, 3) >= SimpleSemver::new(0, 10, 10)); + assert!(SimpleSemver::new(1, 2, 3) >= SimpleSemver::new(1, 1, 10)); - assert!(!SimpleSemver::new(1, 2, 3).gte(&SimpleSemver::new(1, 2, 10))); - assert!(!SimpleSemver::new(1, 2, 3).gte(&SimpleSemver::new(1, 3, 1))); - assert!(!SimpleSemver::new(1, 2, 3).gte(&SimpleSemver::new(2, 2, 1))); + assert!(SimpleSemver::new(1, 2, 3) < SimpleSemver::new(1, 2, 10)); + assert!(SimpleSemver::new(1, 2, 3) < SimpleSemver::new(1, 3, 1)); + assert!(SimpleSemver::new(1, 2, 3) < SimpleSemver::new(2, 2, 1)); } #[test] diff --git a/cli/src/util/zipper.rs b/cli/src/util/zipper.rs index bfb7f4085b6..84eec040b0e 100644 --- a/cli/src/util/zipper.rs +++ b/cli/src/util/zipper.rs @@ -54,12 +54,7 @@ where let mut archive = zip::ZipArchive::new(file) .map_err(|e| wrap(e, format!("failed to open zip archive {}", path.display())))?; - let skip_segments_no = if should_skip_first_segment(&mut archive) { - 1 - } else { - 0 - }; - + let skip_segments_no = usize::from(should_skip_first_segment(&mut archive)); for i in 0..archive.len() { reporter.report_progress(i as u64, archive.len() as u64); let mut file = archive @@ -83,7 +78,7 @@ where } if let Some(p) = outpath.parent() { - fs::create_dir_all(&p) + fs::create_dir_all(p) .map_err(|e| wrap(e, format!("could not create dir for {}", outpath.display())))?; } @@ -138,7 +133,7 @@ fn apply_permissions(file: &ZipFile, outpath: &Path) -> Result<(), WrappedError> use std::os::unix::fs::PermissionsExt; if let Some(mode) = file.unix_mode() { - fs::set_permissions(&outpath, fs::Permissions::from_mode(mode)).map_err(|e| { + fs::set_permissions(outpath, fs::Permissions::from_mode(mode)).map_err(|e| { wrap( e, format!("error setting permissions on {}", outpath.display()), diff --git a/extensions/debug-server-ready/package.json b/extensions/debug-server-ready/package.json index 156280396c3..5bfccfb575e 100644 --- a/extensions/debug-server-ready/package.json +++ b/extensions/debug-server-ready/package.json @@ -40,7 +40,8 @@ "additionalProperties": false, "markdownDescription": "%debug.server.ready.serverReadyAction.description%", "default": { - "action": "openExternally" + "action": "openExternally", + "killOnServerStop": false }, "properties": { "action": { @@ -63,6 +64,11 @@ "type": "string", "markdownDescription": "%debug.server.ready.uriFormat.description%", "default": "http://localhost:%s" + }, + "killOnServerStop": { + "type": "boolean", + "markdownDescription": "%debug.server.ready.killOnServerStop.description%", + "default": false } } }, @@ -74,7 +80,8 @@ "action": "debugWithEdge", "pattern": "listening on port ([0-9]+)", "uriFormat": "http://localhost:%s", - "webRoot": "${workspaceFolder}" + "webRoot": "${workspaceFolder}", + "killOnServerStop": false }, "properties": { "action": { @@ -103,6 +110,11 @@ "type": "string", "markdownDescription": "%debug.server.ready.webRoot.description%", "default": "${workspaceFolder}" + }, + "killOnServerStop": { + "type": "boolean", + "markdownDescription": "%debug.server.ready.killOnServerStop.description%", + "default": false } } }, @@ -112,7 +124,8 @@ "markdownDescription": "%debug.server.ready.serverReadyAction.description%", "default": { "action": "startDebugging", - "name": "" + "name": "", + "killOnServerStop": false }, "required": [ "name" @@ -138,6 +151,11 @@ "type": "string", "markdownDescription": "%debug.server.ready.debugConfigName.description%", "default": "Launch Browser" + }, + "killOnServerStop": { + "type": "boolean", + "markdownDescription": "%debug.server.ready.killOnServerStop.description%", + "default": false } } } diff --git a/extensions/debug-server-ready/package.nls.json b/extensions/debug-server-ready/package.nls.json index c5212d7ee02..45169b0095b 100644 --- a/extensions/debug-server-ready/package.nls.json +++ b/extensions/debug-server-ready/package.nls.json @@ -10,5 +10,6 @@ "debug.server.ready.pattern.description": "Server is ready if this pattern appears on the debug console. The first capture group must include a URI or a port number.", "debug.server.ready.uriFormat.description": "A format string used when constructing the URI from a port number. The first '%s' is substituted with the port number.", "debug.server.ready.webRoot.description": "Value passed to the debug configuration for the 'Debugger for Chrome'.", + "debug.server.ready.killOnServerStop.description": "Stop the child session when the parent session stopped.", "debug.server.ready.debugConfigName.description": "Name of the launch configuration to run." } diff --git a/extensions/debug-server-ready/src/extension.ts b/extensions/debug-server-ready/src/extension.ts index 547125c7c03..592dec936eb 100644 --- a/extensions/debug-server-ready/src/extension.ts +++ b/extensions/debug-server-ready/src/extension.ts @@ -5,6 +5,7 @@ import * as vscode from 'vscode'; import * as util from 'util'; +import { randomUUID } from 'crypto'; const PATTERN = 'listening on.* (https?://\\S+|[0-9]+)'; // matches "listening on port 3000" or "Now listening on: https://localhost:5001" const URI_PORT_FORMAT = 'http://localhost:%s'; @@ -17,6 +18,7 @@ interface ServerReadyAction { uriFormat?: string; webRoot?: string; name?: string; + killOnServerStop?: boolean; } class Trigger { @@ -40,6 +42,7 @@ class ServerReadyDetector extends vscode.Disposable { private shellPid?: number; private regexp: RegExp; private disposables: vscode.Disposable[] = []; + private lateDisposables = new Set([]); static start(session: vscode.DebugSession): ServerReadyDetector | undefined { if (session.configuration.serverReadyAction) { @@ -109,6 +112,11 @@ class ServerReadyDetector extends vscode.Disposable { this.disposables = []; } + override dispose() { + this.lateDisposables.forEach(d => d.dispose()); + return super.dispose(); + } + detectPattern(s: string): boolean { if (!this.trigger.hasFired) { const matches = this.regexp.exec(s); @@ -153,25 +161,25 @@ class ServerReadyDetector extends vscode.Disposable { this.openExternalWithUri(session, uri); } - private openExternalWithUri(session: vscode.DebugSession, uri: string) { + private async openExternalWithUri(session: vscode.DebugSession, uri: string) { const args: ServerReadyAction = session.configuration.serverReadyAction; switch (args.action || 'openExternally') { case 'openExternally': - vscode.env.openExternal(vscode.Uri.parse(uri)); + await vscode.env.openExternal(vscode.Uri.parse(uri)); break; case 'debugWithChrome': - this.debugWithBrowser('pwa-chrome', session, uri); + await this.debugWithBrowser('pwa-chrome', session, uri); break; case 'debugWithEdge': - this.debugWithBrowser('pwa-msedge', session, uri); + await this.debugWithBrowser('pwa-msedge', session, uri); break; case 'startDebugging': - vscode.debug.startDebugging(session.workspaceFolder, args.name || 'unspecified'); + await this.startNamedDebugSession(session, args.name || 'unspecified'); break; default: @@ -180,13 +188,104 @@ class ServerReadyDetector extends vscode.Disposable { } } - private debugWithBrowser(type: string, session: vscode.DebugSession, uri: string) { + private async debugWithBrowser(type: string, session: vscode.DebugSession, uri: string) { + const args = session.configuration.serverReadyAction as ServerReadyAction; + if (!args.killOnServerStop) { + await this.startBrowserDebugSession(type, session, uri); + return; + } + + const trackerId = randomUUID(); + const cts = new vscode.CancellationTokenSource(); + const newSessionPromise = this.catchStartedDebugSession(session => session.configuration._debugServerReadySessionId === trackerId, cts.token); + + if (!await this.startBrowserDebugSession(type, session, uri, trackerId)) { + cts.cancel(); + cts.dispose(); + return; + } + + const createdSession = await newSessionPromise; + cts.dispose(); + + if (!createdSession) { + return; + } + + const stopListener = vscode.debug.onDidTerminateDebugSession(async (terminated) => { + if (terminated === session) { + stopListener.dispose(); + this.lateDisposables.delete(stopListener); + await vscode.debug.stopDebugging(createdSession); + } + }); + this.lateDisposables.add(stopListener); + } + + private startBrowserDebugSession(type: string, session: vscode.DebugSession, uri: string, trackerId?: string) { return vscode.debug.startDebugging(session.workspaceFolder, { type, name: 'Browser Debug', request: 'launch', url: uri, - webRoot: session.configuration.serverReadyAction.webRoot || WEB_ROOT + webRoot: session.configuration.serverReadyAction.webRoot || WEB_ROOT, + _debugServerReadySessionId: trackerId, + }); + } + + private async startNamedDebugSession(session: vscode.DebugSession, name: string) { + const args = session.configuration.serverReadyAction as ServerReadyAction; + if (!args.killOnServerStop) { + await vscode.debug.startDebugging(session.workspaceFolder, name); + return; + } + + const cts = new vscode.CancellationTokenSource(); + const newSessionPromise = this.catchStartedDebugSession(x => x.name === name, cts.token); + + if (!await vscode.debug.startDebugging(session.workspaceFolder, name)) { + cts.cancel(); + cts.dispose(); + return; + } + + const createdSession = await newSessionPromise; + cts.dispose(); + + if (!createdSession) { + return; + } + + const stopListener = vscode.debug.onDidTerminateDebugSession(async (terminated) => { + if (terminated === session) { + stopListener.dispose(); + this.lateDisposables.delete(stopListener); + await vscode.debug.stopDebugging(createdSession); + } + }); + this.lateDisposables.add(stopListener); + } + + private catchStartedDebugSession(predicate: (session: vscode.DebugSession) => boolean, cancellationToken: vscode.CancellationToken): Promise { + return new Promise(_resolve => { + const done = (value?: vscode.DebugSession) => { + listener.dispose(); + cancellationListener.dispose(); + this.lateDisposables.delete(listener); + this.lateDisposables.delete(cancellationListener); + _resolve(value); + }; + + const cancellationListener = cancellationToken.onCancellationRequested(done); + const listener = vscode.debug.onDidStartDebugSession(session => { + if (predicate(session)) { + done(session); + } + }); + + // In case the debug session of interest was never caught anyhow. + this.lateDisposables.add(listener); + this.lateDisposables.add(cancellationListener); }); } } diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 6477b95d4a1..67486d7b3b9 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -3017,6 +3017,7 @@ export class CommandCenter { const yes = l10n.t('Yes'); const result = await window.showWarningMessage( l10n.t('Are you sure you want to drop the stash: {0}?', stash.description), + { modal: true }, yes ); if (result !== yes) { @@ -3041,7 +3042,7 @@ export class CommandCenter { l10n.t('Are you sure you want to drop ALL stashes? There is 1 stash that will be subject to pruning, and MAY BE IMPOSSIBLE TO RECOVER.') : l10n.t('Are you sure you want to drop ALL stashes? There are {0} stashes that will be subject to pruning, and MAY BE IMPOSSIBLE TO RECOVER.', stashes.length); - const result = await window.showWarningMessage(question, yes); + const result = await window.showWarningMessage(question, { modal: true }, yes); if (result !== yes) { return; } diff --git a/extensions/github/src/pushErrorHandler.ts b/extensions/github/src/pushErrorHandler.ts index c337fc5960a..7c3da24e8e0 100644 --- a/extensions/github/src/pushErrorHandler.ts +++ b/extensions/github/src/pushErrorHandler.ts @@ -18,9 +18,9 @@ export function isInCodespaces(): boolean { async function handlePushError(repository: Repository, remote: Remote, refspec: string, owner: string, repo: string): Promise { const yes = l10n.t('Create Fork'); const no = l10n.t('No'); - const askFork = l10n.t('You don\'t have permissions to push to "{0}/{1}" on GitHub.Would you like to create a fork and push to it instead?', owner, repo); + const askFork = l10n.t('You don\'t have permissions to push to "{0}/{1}" on GitHub. Would you like to create a fork and push to it instead?', owner, repo); - const answer = await window.showInformationMessage(askFork, yes, no); + const answer = await window.showWarningMessage(askFork, { modal: true }, yes, no); if (answer !== yes) { return; } diff --git a/extensions/ipynb/src/notebookAttachmentCleaner.ts b/extensions/ipynb/src/notebookAttachmentCleaner.ts index a72761754df..c7af81492da 100644 --- a/extensions/ipynb/src/notebookAttachmentCleaner.ts +++ b/extensions/ipynb/src/notebookAttachmentCleaner.ts @@ -187,7 +187,7 @@ export class AttachmentCleaner implements vscode.CodeActionProvider { } } - if (!objectEquals(markdownAttachmentsInUse, cell.metadata.attachments)) { + if (cell.index > -1 && !objectEquals(markdownAttachmentsInUse, cell.metadata.attachments)) { const updateMetadata: { [key: string]: any } = deepClone(cell.metadata); updateMetadata.attachments = markdownAttachmentsInUse; const metadataEdit = vscode.NotebookEdit.updateCellMetadata(cell.index, updateMetadata); diff --git a/extensions/package.json b/extensions/package.json index b9739529518..7c28cece4f9 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -4,7 +4,7 @@ "license": "MIT", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "^4.9.2-rc" + "typescript": "^4.9.3" }, "scripts": { "postinstall": "node ./postinstall.mjs" diff --git a/extensions/typescript-language-features/src/extension.browser.ts b/extensions/typescript-language-features/src/extension.browser.ts index 21bdad79300..7b37e470b3e 100644 --- a/extensions/typescript-language-features/src/extension.browser.ts +++ b/extensions/typescript-language-features/src/extension.browser.ts @@ -58,7 +58,7 @@ export function activate( new TypeScriptVersion( TypeScriptVersionSource.Bundled, vscode.Uri.joinPath(context.extensionUri, 'dist/browser/typescript/tsserver.web.js').toString(), - API.fromSimpleString('4.8.2'))); + API.fromSimpleString('4.9.3'))); let experimentTelemetryReporter: IExperimentationTelemetryReporter | undefined; const packageInfo = getPackageInfo(context); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/interactiveWindow.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/interactiveWindow.test.ts index 181068b38f6..4f8a1211c2d 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/interactiveWindow.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/interactiveWindow.test.ts @@ -36,7 +36,7 @@ async function addCell(code: string, notebook: vscode.NotebookDocument) { async function addCellAndRun(code: string, notebook: vscode.NotebookDocument, i: number) { const cell = await addCell(code, notebook); const event = asPromise(vscode.workspace.onDidChangeNotebookDocument); - await vscode.commands.executeCommand('notebook.cell.execute', { start: i, end: i + 1 }); + await vscode.commands.executeCommand('notebook.cell.execute', { start: i, end: i + 1 }, notebook.uri); await event; assert.strictEqual(cell.outputs.length, 1, 'execute failed'); return cell; diff --git a/extensions/yarn.lock b/extensions/yarn.lock index 8cd7aaf26af..87d201a9541 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -175,10 +175,10 @@ node-gyp-build@^4.3.0: resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q== -typescript@^4.9.2-rc: - version "4.9.2-rc" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.2-rc.tgz#3525dbeb8458a8c98ce7d60724e4a9380d7b46e7" - integrity sha512-Ly9UUxJBfiiFjfegI1gsW9FI8Xhw1cuwRMBJ4wdYg+UXZR4VnZvD1OnBDj/iQ2U+tWbWEjYqJ5xx1Cwr4Vsa4w== +typescript@^4.9.3: + version "4.9.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.3.tgz#3aea307c1746b8c384435d8ac36b8a2e580d85db" + integrity sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA== vscode-grammar-updater@^1.1.0: version "1.1.0" diff --git a/package.json b/package.json index 6bbff98b9ef..c2529db54dd 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.74.0", - "distro": "d7139fbecd8c263fbe78f29e5cebc53f220e10b2", + "distro": "7153817c51ee07fd57e84a33afdaede63b3937f8", "author": { "name": "Microsoft Corporation" }, diff --git a/src/server-main.js b/src/server-main.js index 27ecbaed0d6..3dbbeaedf54 100644 --- a/src/server-main.js +++ b/src/server-main.js @@ -197,6 +197,7 @@ async function parsePort(host, strPort) { if (port !== undefined) { return port; } + // Remote-SSH extension relies on this exact port error message, treat as an API console.warn(`--port: Could not find free port in range: ${range.start} - ${range.end} (inclusive).`); process.exit(1); diff --git a/src/vs/base/browser/ui/actionbar/actionViewItems.ts b/src/vs/base/browser/ui/actionbar/actionViewItems.ts index a4a8f5204a3..7722ef6b3d8 100644 --- a/src/vs/base/browser/ui/actionbar/actionViewItems.ts +++ b/src/vs/base/browser/ui/actionbar/actionViewItems.ts @@ -12,6 +12,7 @@ import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; import { ICustomHover, setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; import { ISelectBoxOptions, ISelectOptionItem, SelectBox } from 'vs/base/browser/ui/selectBox/selectBox'; +import { IToggleStyles } from 'vs/base/browser/ui/toggle/toggle'; import { Action, ActionRunner, IAction, IActionChangeEvent, IActionRunner, Separator } from 'vs/base/common/actions'; import { Disposable } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; @@ -261,6 +262,7 @@ export interface IActionViewItemOptions extends IBaseActionViewItemOptions { icon?: boolean; label?: boolean; keybinding?: string | null; + toggleStyles?: IToggleStyles; } export class ActionViewItem extends BaseActionViewItem { @@ -270,7 +272,7 @@ export class ActionViewItem extends BaseActionViewItem { private cssClass?: string; - constructor(context: unknown, action: IAction, options: IActionViewItemOptions = {}) { + constructor(context: unknown, action: IAction, options: IActionViewItemOptions) { super(context, action, options); this.options = options; diff --git a/src/vs/base/browser/ui/button/button.css b/src/vs/base/browser/ui/button/button.css index 36beeb00478..dff0019cfee 100644 --- a/src/vs/base/browser/ui/button/button.css +++ b/src/vs/base/browser/ui/button/button.css @@ -69,6 +69,11 @@ .monaco-button-dropdown > .monaco-button.monaco-dropdown-button { border-left-width: 0 !important; + border-radius: 0 2px 2px 0; +} + +.monaco-button-dropdown > .monaco-button.monaco-text-button { + border-radius: 2px 0 0 2px; } .monaco-description-button { diff --git a/src/vs/base/browser/ui/button/button.ts b/src/vs/base/browser/ui/button/button.ts index 0d28e607144..45d213b4781 100644 --- a/src/vs/base/browser/ui/button/button.ts +++ b/src/vs/base/browser/ui/button/button.ts @@ -257,9 +257,10 @@ export class ButtonWithDropdown extends Disposable implements IButton { this.separatorContainer.style.borderTop = '1px solid ' + border; this.separatorContainer.style.borderBottom = '1px solid ' + border; } - this.separatorContainer.style.backgroundColor = options.buttonBackground ?? ''; - this.separator.style.backgroundColor = options.buttonSeparator ?? ''; + const buttonBackground = options.secondary ? options.buttonSecondaryBackground : options.buttonBackground; + this.separatorContainer.style.backgroundColor = buttonBackground ?? ''; + this.separator.style.backgroundColor = options.buttonSeparator ?? ''; this.dropdownButton = this._register(new Button(this.element, { ...options, title: false, supportIcons: true })); this.dropdownButton.element.title = localize("button dropdown more actions", 'More Actions...'); diff --git a/src/vs/base/browser/ui/dialog/dialog.ts b/src/vs/base/browser/ui/dialog/dialog.ts index 3ec1b02ca80..e4411793cd7 100644 --- a/src/vs/base/browser/ui/dialog/dialog.ts +++ b/src/vs/base/browser/ui/dialog/dialog.ts @@ -8,10 +8,9 @@ import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { ButtonBar, ButtonWithDescription, IButtonStyles } from 'vs/base/browser/ui/button/button'; import { ICheckboxStyles, Checkbox } from 'vs/base/browser/ui/toggle/toggle'; -import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; +import { IInputBoxStyles, InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { Action } from 'vs/base/common/actions'; import { Codicon } from 'vs/base/common/codicons'; -import { Color } from 'vs/base/common/color'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -39,6 +38,9 @@ export interface IDialogOptions { readonly disableCloseAction?: boolean; readonly disableDefaultAction?: boolean; readonly buttonStyles: IButtonStyles; + readonly checkboxStyles: ICheckboxStyles; + readonly inputBoxStyles: IInputBoxStyles; + readonly dialogStyles: IDialogStyles; } export interface IDialogResult { @@ -47,19 +49,15 @@ export interface IDialogResult { readonly values?: string[]; } -export interface IDialogStyles extends ICheckboxStyles { - readonly dialogForeground?: Color; - readonly dialogBackground?: Color; - readonly dialogShadow?: Color; - readonly dialogBorder?: Color; - readonly errorIconForeground?: Color; - readonly warningIconForeground?: Color; - readonly infoIconForeground?: Color; - readonly inputBackground?: Color; - readonly inputForeground?: Color; - readonly inputBorder?: Color; - readonly textLinkForeground?: Color; - +export interface IDialogStyles { + readonly dialogForeground: string | undefined; + readonly dialogBackground: string | undefined; + readonly dialogShadow: string | undefined; + readonly dialogBorder: string | undefined; + readonly errorIconForeground: string | undefined; + readonly warningIconForeground: string | undefined; + readonly infoIconForeground: string | undefined; + readonly textLinkForeground: string | undefined; } interface ButtonMapEntry { @@ -78,7 +76,6 @@ export class Dialog extends Disposable { private readonly checkbox: Checkbox | undefined; private readonly toolbarContainer: HTMLElement; private buttonBar: ButtonBar | undefined; - private styles: IDialogStyles | undefined; private focusToReturn: HTMLElement | undefined; private readonly inputs: InputBox[]; private readonly buttons: string[]; @@ -140,6 +137,7 @@ export class Dialog extends Disposable { const inputBox = this._register(new InputBox(inputRowElement, undefined, { placeholder: input.placeholder, type: input.type ?? 'text', + inputBoxStyles: options.inputBoxStyles })); if (input.value) { @@ -155,7 +153,9 @@ export class Dialog extends Disposable { if (this.options.checkboxLabel) { const checkboxRowElement = this.messageContainer.appendChild($('.dialog-checkbox-row')); - const checkbox = this.checkbox = this._register(new Checkbox(this.options.checkboxLabel, !!this.options.checkboxChecked)); + const checkbox = this.checkbox = this._register( + new Checkbox(this.options.checkboxLabel, !!this.options.checkboxChecked, options.checkboxStyles) + ); checkboxRowElement.appendChild(checkbox.domNode); @@ -166,6 +166,8 @@ export class Dialog extends Disposable { const toolbarRowElement = this.element.appendChild($('.dialog-toolbar-row')); this.toolbarContainer = toolbarRowElement.appendChild($('.dialog-toolbar')); + + this.applyStyles(); } private getIconAriaLabel(): string { @@ -391,7 +393,7 @@ export class Dialog extends Disposable { }); })); - actionBar.push(action, { icon: true, label: false, }); + actionBar.push(action, { icon: true, label: false }); } this.applyStyles(); @@ -416,60 +418,47 @@ export class Dialog extends Disposable { } private applyStyles() { - if (this.styles) { - const style = this.styles; + const style = this.options.dialogStyles; - const fgColor = style.dialogForeground; - const bgColor = style.dialogBackground; - const shadowColor = style.dialogShadow ? `0 0px 8px ${style.dialogShadow}` : ''; - const border = style.dialogBorder ? `1px solid ${style.dialogBorder}` : ''; - const linkFgColor = style.textLinkForeground; + const fgColor = style.dialogForeground; + const bgColor = style.dialogBackground; + const shadowColor = style.dialogShadow ? `0 0px 8px ${style.dialogShadow}` : ''; + const border = style.dialogBorder ? `1px solid ${style.dialogBorder}` : ''; + const linkFgColor = style.textLinkForeground; - this.shadowElement.style.boxShadow = shadowColor; + this.shadowElement.style.boxShadow = shadowColor; - this.element.style.color = fgColor?.toString() ?? ''; - this.element.style.backgroundColor = bgColor?.toString() ?? ''; - this.element.style.border = border; + this.element.style.color = fgColor?.toString() ?? ''; + this.element.style.backgroundColor = bgColor?.toString() ?? ''; + this.element.style.border = border; - this.checkbox?.style(style); + // TODO fix + // if (fgColor && bgColor) { + // const messageDetailColor = fgColor.transparent(.9); + // this.messageDetailElement.style.mixBlendMode = messageDetailColor.makeOpaque(bgColor).toString(); + // } - if (fgColor && bgColor) { - const messageDetailColor = fgColor.transparent(.9); - this.messageDetailElement.style.color = messageDetailColor.makeOpaque(bgColor).toString(); - } - - if (linkFgColor) { - for (const el of this.messageContainer.getElementsByTagName('a')) { - el.style.color = linkFgColor.toString(); - } - } - - let color; - switch (this.options.type) { - case 'error': - color = style.errorIconForeground; - break; - case 'warning': - color = style.warningIconForeground; - break; - default: - color = style.infoIconForeground; - break; - } - if (color) { - this.iconElement.style.color = color.toString(); - } - - for (const input of this.inputs) { - input.style(style); + if (linkFgColor) { + for (const el of this.messageContainer.getElementsByTagName('a')) { + el.style.color = linkFgColor; } } - } - style(style: IDialogStyles): void { - this.styles = style; - - this.applyStyles(); + let color; + switch (this.options.type) { + case 'error': + color = style.errorIconForeground; + break; + case 'warning': + color = style.warningIconForeground; + break; + default: + color = style.infoIconForeground; + break; + } + if (color) { + this.iconElement.style.color = color; + } } override dispose(): void { diff --git a/src/vs/base/browser/ui/findinput/findInput.ts b/src/vs/base/browser/ui/findinput/findInput.ts index d9809ddf156..39fd66a7251 100644 --- a/src/vs/base/browser/ui/findinput/findInput.ts +++ b/src/vs/base/browser/ui/findinput/findInput.ts @@ -11,7 +11,6 @@ import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview import { CaseSensitiveToggle, RegexToggle, WholeWordsToggle } from 'vs/base/browser/ui/findinput/findInputToggles'; import { HistoryInputBox, IInputBoxStyles, IInputValidator, IMessage as InputBoxMessage } from 'vs/base/browser/ui/inputbox/inputBox'; import { Widget } from 'vs/base/browser/ui/widget'; -import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; import 'vs/css!./findInput'; @@ -19,7 +18,7 @@ import * as nls from 'vs/nls'; import { DisposableStore } from 'vs/base/common/lifecycle'; -export interface IFindInputOptions extends IFindInputStyles { +export interface IFindInputOptions { readonly placeholder?: string; readonly width?: number; readonly validation?: IInputValidator; @@ -35,12 +34,8 @@ export interface IFindInputOptions extends IFindInputStyles { readonly history?: string[]; readonly additionalToggles?: Toggle[]; readonly showHistoryHint?: () => boolean; -} - -export interface IFindInputStyles extends IInputBoxStyles { - inputActiveOptionBorder?: Color; - inputActiveOptionForeground?: Color; - inputActiveOptionBackground?: Color; + readonly toggleStyles: IToggleStyles; + readonly inputBoxStyles: IInputBoxStyles; } const NLS_DEFAULT_LABEL = nls.localize('defaultLabel', "input"); @@ -57,23 +52,6 @@ export class FindInput extends Widget { private imeSessionInProgress = false; private additionalTogglesDisposables: DisposableStore = new DisposableStore(); - protected inputActiveOptionBorder?: Color; - protected inputActiveOptionForeground?: Color; - protected inputActiveOptionBackground?: Color; - protected inputBackground?: Color; - protected inputForeground?: Color; - protected inputBorder?: Color; - - protected inputValidationInfoBorder?: Color; - protected inputValidationInfoBackground?: Color; - protected inputValidationInfoForeground?: Color; - protected inputValidationWarningBorder?: Color; - protected inputValidationWarningBackground?: Color; - protected inputValidationWarningForeground?: Color; - protected inputValidationErrorBorder?: Color; - protected inputValidationErrorBackground?: Color; - protected inputValidationErrorForeground?: Color; - protected readonly controls: HTMLDivElement; protected readonly regex?: RegexToggle; protected readonly wholeWords?: WholeWordsToggle; @@ -110,23 +88,6 @@ export class FindInput extends Widget { this.label = options.label || NLS_DEFAULT_LABEL; this.showCommonFindToggles = !!options.showCommonFindToggles; - this.inputActiveOptionBorder = options.inputActiveOptionBorder; - this.inputActiveOptionForeground = options.inputActiveOptionForeground; - this.inputActiveOptionBackground = options.inputActiveOptionBackground; - this.inputBackground = options.inputBackground; - this.inputForeground = options.inputForeground; - this.inputBorder = options.inputBorder; - - this.inputValidationInfoBorder = options.inputValidationInfoBorder; - this.inputValidationInfoBackground = options.inputValidationInfoBackground; - this.inputValidationInfoForeground = options.inputValidationInfoForeground; - this.inputValidationWarningBorder = options.inputValidationWarningBorder; - this.inputValidationWarningBackground = options.inputValidationWarningBackground; - this.inputValidationWarningForeground = options.inputValidationWarningForeground; - this.inputValidationErrorBorder = options.inputValidationErrorBorder; - this.inputValidationErrorBackground = options.inputValidationErrorBackground; - this.inputValidationErrorForeground = options.inputValidationErrorForeground; - const appendCaseSensitiveLabel = options.appendCaseSensitiveLabel || ''; const appendWholeWordsLabel = options.appendWholeWordsLabel || ''; const appendRegexLabel = options.appendRegexLabel || ''; @@ -144,32 +105,19 @@ export class FindInput extends Widget { validationOptions: { validation: this.validation }, - inputBackground: this.inputBackground, - inputForeground: this.inputForeground, - inputBorder: this.inputBorder, - inputValidationInfoBackground: this.inputValidationInfoBackground, - inputValidationInfoForeground: this.inputValidationInfoForeground, - inputValidationInfoBorder: this.inputValidationInfoBorder, - inputValidationWarningBackground: this.inputValidationWarningBackground, - inputValidationWarningForeground: this.inputValidationWarningForeground, - inputValidationWarningBorder: this.inputValidationWarningBorder, - inputValidationErrorBackground: this.inputValidationErrorBackground, - inputValidationErrorForeground: this.inputValidationErrorForeground, - inputValidationErrorBorder: this.inputValidationErrorBorder, history, showHistoryHint: options.showHistoryHint, flexibleHeight, flexibleWidth, - flexibleMaxHeight + flexibleMaxHeight, + inputBoxStyles: options.inputBoxStyles, })); if (this.showCommonFindToggles) { this.regex = this._register(new RegexToggle({ appendTitle: appendRegexLabel, isChecked: false, - inputActiveOptionBorder: this.inputActiveOptionBorder, - inputActiveOptionForeground: this.inputActiveOptionForeground, - inputActiveOptionBackground: this.inputActiveOptionBackground + ...options.toggleStyles })); this._register(this.regex.onChange(viaKeyboard => { this._onDidOptionChange.fire(viaKeyboard); @@ -185,9 +133,7 @@ export class FindInput extends Widget { this.wholeWords = this._register(new WholeWordsToggle({ appendTitle: appendWholeWordsLabel, isChecked: false, - inputActiveOptionBorder: this.inputActiveOptionBorder, - inputActiveOptionForeground: this.inputActiveOptionForeground, - inputActiveOptionBackground: this.inputActiveOptionBackground + ...options.toggleStyles })); this._register(this.wholeWords.onChange(viaKeyboard => { this._onDidOptionChange.fire(viaKeyboard); @@ -200,9 +146,7 @@ export class FindInput extends Widget { this.caseSensitive = this._register(new CaseSensitiveToggle({ appendTitle: appendCaseSensitiveLabel, isChecked: false, - inputActiveOptionBorder: this.inputActiveOptionBorder, - inputActiveOptionForeground: this.inputActiveOptionForeground, - inputActiveOptionBackground: this.inputActiveOptionBackground + ...options.toggleStyles })); this._register(this.caseSensitive.onChange(viaKeyboard => { this._onDidOptionChange.fire(viaKeyboard); @@ -375,60 +319,6 @@ export class FindInput extends Widget { this.inputBox.addToHistory(); } - public style(styles: IFindInputStyles): void { - this.inputActiveOptionBorder = styles.inputActiveOptionBorder; - this.inputActiveOptionForeground = styles.inputActiveOptionForeground; - this.inputActiveOptionBackground = styles.inputActiveOptionBackground; - this.inputBackground = styles.inputBackground; - this.inputForeground = styles.inputForeground; - this.inputBorder = styles.inputBorder; - - this.inputValidationInfoBackground = styles.inputValidationInfoBackground; - this.inputValidationInfoForeground = styles.inputValidationInfoForeground; - this.inputValidationInfoBorder = styles.inputValidationInfoBorder; - this.inputValidationWarningBackground = styles.inputValidationWarningBackground; - this.inputValidationWarningForeground = styles.inputValidationWarningForeground; - this.inputValidationWarningBorder = styles.inputValidationWarningBorder; - this.inputValidationErrorBackground = styles.inputValidationErrorBackground; - this.inputValidationErrorForeground = styles.inputValidationErrorForeground; - this.inputValidationErrorBorder = styles.inputValidationErrorBorder; - - this.applyStyles(); - } - - protected applyStyles(): void { - if (this.domNode) { - const toggleStyles: IToggleStyles = { - inputActiveOptionBorder: this.inputActiveOptionBorder, - inputActiveOptionForeground: this.inputActiveOptionForeground, - inputActiveOptionBackground: this.inputActiveOptionBackground, - }; - this.regex?.style(toggleStyles); - this.wholeWords?.style(toggleStyles); - this.caseSensitive?.style(toggleStyles); - - for (const toggle of this.additionalToggles) { - toggle.style(toggleStyles); - } - - const inputBoxStyles: IInputBoxStyles = { - inputBackground: this.inputBackground, - inputForeground: this.inputForeground, - inputBorder: this.inputBorder, - inputValidationInfoBackground: this.inputValidationInfoBackground, - inputValidationInfoForeground: this.inputValidationInfoForeground, - inputValidationInfoBorder: this.inputValidationInfoBorder, - inputValidationWarningBackground: this.inputValidationWarningBackground, - inputValidationWarningForeground: this.inputValidationWarningForeground, - inputValidationWarningBorder: this.inputValidationWarningBorder, - inputValidationErrorBackground: this.inputValidationErrorBackground, - inputValidationErrorForeground: this.inputValidationErrorForeground, - inputValidationErrorBorder: this.inputValidationErrorBorder - }; - this.inputBox.style(inputBoxStyles); - } - } - public select(): void { this.inputBox.select(); } diff --git a/src/vs/base/browser/ui/findinput/findInputToggles.ts b/src/vs/base/browser/ui/findinput/findInputToggles.ts index 8f6f30dda44..591ab981577 100644 --- a/src/vs/base/browser/ui/findinput/findInputToggles.ts +++ b/src/vs/base/browser/ui/findinput/findInputToggles.ts @@ -5,15 +5,14 @@ import { Toggle } from 'vs/base/browser/ui/toggle/toggle'; import { Codicon } from 'vs/base/common/codicons'; -import { Color } from 'vs/base/common/color'; import * as nls from 'vs/nls'; export interface IFindInputToggleOpts { readonly appendTitle: string; readonly isChecked: boolean; - readonly inputActiveOptionBorder?: Color; - readonly inputActiveOptionForeground?: Color; - readonly inputActiveOptionBackground?: Color; + readonly inputActiveOptionBorder: string | undefined; + readonly inputActiveOptionForeground: string | undefined; + readonly inputActiveOptionBackground: string | undefined; } const NLS_CASE_SENSITIVE_TOGGLE_LABEL = nls.localize('caseDescription', "Match Case"); diff --git a/src/vs/base/browser/ui/findinput/replaceInput.ts b/src/vs/base/browser/ui/findinput/replaceInput.ts index cbfd7136436..a9b668c7c05 100644 --- a/src/vs/base/browser/ui/findinput/replaceInput.ts +++ b/src/vs/base/browser/ui/findinput/replaceInput.ts @@ -6,20 +6,19 @@ import * as dom from 'vs/base/browser/dom'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; -import { Toggle, IToggleStyles } from 'vs/base/browser/ui/toggle/toggle'; +import { IToggleStyles, Toggle } from 'vs/base/browser/ui/toggle/toggle'; import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; import { IFindInputToggleOpts } from 'vs/base/browser/ui/findinput/findInputToggles'; import { HistoryInputBox, IInputBoxStyles, IInputValidator, IMessage as InputBoxMessage } from 'vs/base/browser/ui/inputbox/inputBox'; import { Widget } from 'vs/base/browser/ui/widget'; import { Codicon } from 'vs/base/common/codicons'; -import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; import 'vs/css!./findInput'; import * as nls from 'vs/nls'; -export interface IReplaceInputOptions extends IReplaceInputStyles { +export interface IReplaceInputOptions { readonly placeholder?: string; readonly width?: number; readonly validation?: IInputValidator; @@ -31,12 +30,8 @@ export interface IReplaceInputOptions extends IReplaceInputStyles { readonly appendPreserveCaseLabel?: string; readonly history?: string[]; readonly showHistoryHint?: () => boolean; -} - -export interface IReplaceInputStyles extends IInputBoxStyles { - inputActiveOptionBorder?: Color; - inputActiveOptionForeground?: Color; - inputActiveOptionBackground?: Color; + readonly inputBoxStyles: IInputBoxStyles; + readonly toggleStyles: IToggleStyles; } const NLS_DEFAULT_LABEL = nls.localize('defaultLabel', "input"); @@ -66,23 +61,6 @@ export class ReplaceInput extends Widget { private label: string; private fixFocusOnOptionClickEnabled = true; - private inputActiveOptionBorder?: Color; - private inputActiveOptionForeground?: Color; - private inputActiveOptionBackground?: Color; - private inputBackground?: Color; - private inputForeground?: Color; - private inputBorder?: Color; - - private inputValidationInfoBorder?: Color; - private inputValidationInfoBackground?: Color; - private inputValidationInfoForeground?: Color; - private inputValidationWarningBorder?: Color; - private inputValidationWarningBackground?: Color; - private inputValidationWarningForeground?: Color; - private inputValidationErrorBorder?: Color; - private inputValidationErrorBackground?: Color; - private inputValidationErrorForeground?: Color; - private preserveCase: PreserveCaseToggle; private cachedOptionsWidth: number = 0; public domNode: HTMLElement; @@ -113,23 +91,6 @@ export class ReplaceInput extends Widget { this.validation = options.validation; this.label = options.label || NLS_DEFAULT_LABEL; - this.inputActiveOptionBorder = options.inputActiveOptionBorder; - this.inputActiveOptionForeground = options.inputActiveOptionForeground; - this.inputActiveOptionBackground = options.inputActiveOptionBackground; - this.inputBackground = options.inputBackground; - this.inputForeground = options.inputForeground; - this.inputBorder = options.inputBorder; - - this.inputValidationInfoBorder = options.inputValidationInfoBorder; - this.inputValidationInfoBackground = options.inputValidationInfoBackground; - this.inputValidationInfoForeground = options.inputValidationInfoForeground; - this.inputValidationWarningBorder = options.inputValidationWarningBorder; - this.inputValidationWarningBackground = options.inputValidationWarningBackground; - this.inputValidationWarningForeground = options.inputValidationWarningForeground; - this.inputValidationErrorBorder = options.inputValidationErrorBorder; - this.inputValidationErrorBackground = options.inputValidationErrorBackground; - this.inputValidationErrorForeground = options.inputValidationErrorForeground; - const appendPreserveCaseLabel = options.appendPreserveCaseLabel || ''; const history = options.history || []; const flexibleHeight = !!options.flexibleHeight; @@ -145,31 +106,18 @@ export class ReplaceInput extends Widget { validationOptions: { validation: this.validation }, - inputBackground: this.inputBackground, - inputForeground: this.inputForeground, - inputBorder: this.inputBorder, - inputValidationInfoBackground: this.inputValidationInfoBackground, - inputValidationInfoForeground: this.inputValidationInfoForeground, - inputValidationInfoBorder: this.inputValidationInfoBorder, - inputValidationWarningBackground: this.inputValidationWarningBackground, - inputValidationWarningForeground: this.inputValidationWarningForeground, - inputValidationWarningBorder: this.inputValidationWarningBorder, - inputValidationErrorBackground: this.inputValidationErrorBackground, - inputValidationErrorForeground: this.inputValidationErrorForeground, - inputValidationErrorBorder: this.inputValidationErrorBorder, history, showHistoryHint: options.showHistoryHint, flexibleHeight, flexibleWidth, - flexibleMaxHeight + flexibleMaxHeight, + inputBoxStyles: options.inputBoxStyles })); this.preserveCase = this._register(new PreserveCaseToggle({ appendTitle: appendPreserveCaseLabel, isChecked: false, - inputActiveOptionBorder: this.inputActiveOptionBorder, - inputActiveOptionForeground: this.inputActiveOptionForeground, - inputActiveOptionBackground: this.inputActiveOptionBackground, + ...options.toggleStyles })); this._register(this.preserveCase.onChange(viaKeyboard => { this._onDidOptionChange.fire(viaKeyboard); @@ -277,52 +225,7 @@ export class ReplaceInput extends Widget { this.inputBox.addToHistory(); } - public style(styles: IReplaceInputStyles): void { - this.inputActiveOptionBorder = styles.inputActiveOptionBorder; - this.inputActiveOptionForeground = styles.inputActiveOptionForeground; - this.inputActiveOptionBackground = styles.inputActiveOptionBackground; - this.inputBackground = styles.inputBackground; - this.inputForeground = styles.inputForeground; - this.inputBorder = styles.inputBorder; - - this.inputValidationInfoBackground = styles.inputValidationInfoBackground; - this.inputValidationInfoForeground = styles.inputValidationInfoForeground; - this.inputValidationInfoBorder = styles.inputValidationInfoBorder; - this.inputValidationWarningBackground = styles.inputValidationWarningBackground; - this.inputValidationWarningForeground = styles.inputValidationWarningForeground; - this.inputValidationWarningBorder = styles.inputValidationWarningBorder; - this.inputValidationErrorBackground = styles.inputValidationErrorBackground; - this.inputValidationErrorForeground = styles.inputValidationErrorForeground; - this.inputValidationErrorBorder = styles.inputValidationErrorBorder; - - this.applyStyles(); - } - protected applyStyles(): void { - if (this.domNode) { - const toggleStyles: IToggleStyles = { - inputActiveOptionBorder: this.inputActiveOptionBorder, - inputActiveOptionForeground: this.inputActiveOptionForeground, - inputActiveOptionBackground: this.inputActiveOptionBackground, - }; - this.preserveCase.style(toggleStyles); - - const inputBoxStyles: IInputBoxStyles = { - inputBackground: this.inputBackground, - inputForeground: this.inputForeground, - inputBorder: this.inputBorder, - inputValidationInfoBackground: this.inputValidationInfoBackground, - inputValidationInfoForeground: this.inputValidationInfoForeground, - inputValidationInfoBorder: this.inputValidationInfoBorder, - inputValidationWarningBackground: this.inputValidationWarningBackground, - inputValidationWarningForeground: this.inputValidationWarningForeground, - inputValidationWarningBorder: this.inputValidationWarningBorder, - inputValidationErrorBackground: this.inputValidationErrorBackground, - inputValidationErrorForeground: this.inputValidationErrorForeground, - inputValidationErrorBorder: this.inputValidationErrorBorder - }; - this.inputBox.style(inputBoxStyles); - } } public select(): void { diff --git a/src/vs/base/browser/ui/inputbox/inputBox.ts b/src/vs/base/browser/ui/inputbox/inputBox.ts index c3d50ef272c..53f918e163d 100644 --- a/src/vs/base/browser/ui/inputbox/inputBox.ts +++ b/src/vs/base/browser/ui/inputbox/inputBox.ts @@ -14,10 +14,8 @@ import { AnchorAlignment, IContextViewProvider } from 'vs/base/browser/ui/contex import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { Widget } from 'vs/base/browser/ui/widget'; import { IAction } from 'vs/base/common/actions'; -import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; import { HistoryNavigator } from 'vs/base/common/history'; -import { mixin } from 'vs/base/common/objects'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import 'vs/css!./inputBox'; import * as nls from 'vs/nls'; @@ -25,7 +23,7 @@ import * as nls from 'vs/nls'; const $ = dom.$; -export interface IInputOptions extends IInputBoxStyles { +export interface IInputOptions { readonly placeholder?: string; readonly showPlaceholderOnFocus?: boolean; readonly tooltip?: string; @@ -36,21 +34,22 @@ export interface IInputOptions extends IInputBoxStyles { readonly flexibleWidth?: boolean; readonly flexibleMaxHeight?: number; readonly actions?: ReadonlyArray; + readonly inputBoxStyles: IInputBoxStyles; } export interface IInputBoxStyles { - readonly inputBackground?: Color; - readonly inputForeground?: Color; - readonly inputBorder?: Color; - readonly inputValidationInfoBorder?: Color; - readonly inputValidationInfoBackground?: Color; - readonly inputValidationInfoForeground?: Color; - readonly inputValidationWarningBorder?: Color; - readonly inputValidationWarningBackground?: Color; - readonly inputValidationWarningForeground?: Color; - readonly inputValidationErrorBorder?: Color; - readonly inputValidationErrorBackground?: Color; - readonly inputValidationErrorForeground?: Color; + readonly inputBackground: string | undefined; + readonly inputForeground: string | undefined; + readonly inputBorder: string | undefined; + readonly inputValidationInfoBorder: string | undefined; + readonly inputValidationInfoBackground: string | undefined; + readonly inputValidationInfoForeground: string | undefined; + readonly inputValidationWarningBorder: string | undefined; + readonly inputValidationWarningBackground: string | undefined; + readonly inputValidationWarningForeground: string | undefined; + readonly inputValidationErrorBorder: string | undefined; + readonly inputValidationErrorBackground: string | undefined; + readonly inputValidationErrorForeground: string | undefined; } export interface IInputValidator { @@ -78,15 +77,19 @@ export interface IRange { end: number; } -const defaultOpts = { - inputBackground: Color.fromHex('#3C3C3C'), - inputForeground: Color.fromHex('#CCCCCC'), - inputValidationInfoBorder: Color.fromHex('#55AAFF'), - inputValidationInfoBackground: Color.fromHex('#063B49'), - inputValidationWarningBorder: Color.fromHex('#B89500'), - inputValidationWarningBackground: Color.fromHex('#352A05'), - inputValidationErrorBorder: Color.fromHex('#BE1100'), - inputValidationErrorBackground: Color.fromHex('#5A1D1D') +export const unthemedInboxStyles: IInputBoxStyles = { + inputBackground: '#3C3C3C', + inputForeground: '#CCCCCC', + inputValidationInfoBorder: '#55AAFF', + inputValidationInfoBackground: '#063B49', + inputValidationWarningBorder: '#B89500', + inputValidationWarningBackground: '#352A05', + inputValidationErrorBorder: '#BE1100', + inputValidationErrorBackground: '#5A1D1D', + inputBorder: undefined, + inputValidationErrorForeground: undefined, + inputValidationInfoForeground: undefined, + inputValidationWarningForeground: undefined }; export class InputBox extends Widget { @@ -94,7 +97,7 @@ export class InputBox extends Widget { element: HTMLElement; protected input: HTMLInputElement; private actionbar?: ActionBar; - private options: IInputOptions; + private readonly options: IInputOptions; private message: IMessage | null; protected placeholder: string; private tooltip: string; @@ -108,51 +111,23 @@ export class InputBox extends Widget { private maxHeight: number = Number.POSITIVE_INFINITY; private scrollableElement: ScrollableElement | undefined; - private inputBackground?: Color; - private inputForeground?: Color; - private inputBorder?: Color; - - private inputValidationInfoBorder?: Color; - private inputValidationInfoBackground?: Color; - private inputValidationInfoForeground?: Color; - private inputValidationWarningBorder?: Color; - private inputValidationWarningBackground?: Color; - private inputValidationWarningForeground?: Color; - private inputValidationErrorBorder?: Color; - private inputValidationErrorBackground?: Color; - private inputValidationErrorForeground?: Color; - private _onDidChange = this._register(new Emitter()); public readonly onDidChange: Event = this._onDidChange.event; private _onDidHeightChange = this._register(new Emitter()); public readonly onDidHeightChange: Event = this._onDidHeightChange.event; - constructor(container: HTMLElement, contextViewProvider: IContextViewProvider | undefined, options?: IInputOptions) { + constructor(container: HTMLElement, contextViewProvider: IContextViewProvider | undefined, options: IInputOptions) { super(); this.contextViewProvider = contextViewProvider; - this.options = options || Object.create(null); - mixin(this.options, defaultOpts, false); + this.options = options; + this.message = null; this.placeholder = this.options.placeholder || ''; this.tooltip = this.options.tooltip ?? (this.placeholder || ''); this.ariaLabel = this.options.ariaLabel || ''; - this.inputBackground = this.options.inputBackground; - this.inputForeground = this.options.inputForeground; - this.inputBorder = this.options.inputBorder; - - this.inputValidationInfoBorder = this.options.inputValidationInfoBorder; - this.inputValidationInfoBackground = this.options.inputValidationInfoBackground; - this.inputValidationInfoForeground = this.options.inputValidationInfoForeground; - this.inputValidationWarningBorder = this.options.inputValidationWarningBorder; - this.inputValidationWarningBackground = this.options.inputValidationWarningBackground; - this.inputValidationWarningForeground = this.options.inputValidationWarningForeground; - this.inputValidationErrorBorder = this.options.inputValidationErrorBorder; - this.inputValidationErrorBackground = this.options.inputValidationErrorBackground; - this.inputValidationErrorForeground = this.options.inputValidationErrorForeground; - if (this.options.validationOptions) { this.validation = this.options.validationOptions.validation; } @@ -436,11 +411,12 @@ export class InputBox extends Widget { return errorMsg?.type; } - public stylesForType(type: MessageType | undefined): { border: Color | undefined; background: Color | undefined; foreground: Color | undefined } { + public stylesForType(type: MessageType | undefined): { border: string | undefined; background: string | undefined; foreground: string | undefined } { + const styles = this.options.inputBoxStyles; switch (type) { - case MessageType.INFO: return { border: this.inputValidationInfoBorder, background: this.inputValidationInfoBackground, foreground: this.inputValidationInfoForeground }; - case MessageType.WARNING: return { border: this.inputValidationWarningBorder, background: this.inputValidationWarningBackground, foreground: this.inputValidationWarningForeground }; - default: return { border: this.inputValidationErrorBorder, background: this.inputValidationErrorBackground, foreground: this.inputValidationErrorForeground }; + case MessageType.INFO: return { border: styles.inputValidationInfoBorder, background: styles.inputValidationInfoBackground, foreground: styles.inputValidationInfoForeground }; + case MessageType.WARNING: return { border: styles.inputValidationWarningBorder, background: styles.inputValidationWarningBackground, foreground: styles.inputValidationWarningForeground }; + default: return { border: styles.inputValidationErrorBorder, background: styles.inputValidationErrorBackground, foreground: styles.inputValidationErrorForeground }; } } @@ -555,37 +531,22 @@ export class InputBox extends Widget { this.layout(); } - public style(styles: IInputBoxStyles): void { - this.inputBackground = styles.inputBackground; - this.inputForeground = styles.inputForeground; - this.inputBorder = styles.inputBorder; - - this.inputValidationInfoBackground = styles.inputValidationInfoBackground; - this.inputValidationInfoForeground = styles.inputValidationInfoForeground; - this.inputValidationInfoBorder = styles.inputValidationInfoBorder; - this.inputValidationWarningBackground = styles.inputValidationWarningBackground; - this.inputValidationWarningForeground = styles.inputValidationWarningForeground; - this.inputValidationWarningBorder = styles.inputValidationWarningBorder; - this.inputValidationErrorBackground = styles.inputValidationErrorBackground; - this.inputValidationErrorForeground = styles.inputValidationErrorForeground; - this.inputValidationErrorBorder = styles.inputValidationErrorBorder; - - this.applyStyles(); - } - protected applyStyles(): void { - const background = this.inputBackground ? this.inputBackground.toString() : ''; - const foreground = this.inputForeground ? this.inputForeground.toString() : ''; - const border = this.inputBorder ? this.inputBorder.toString() : ''; + const styles = this.options.inputBoxStyles; + + const background = styles.inputBackground ?? ''; + const foreground = styles.inputForeground ?? ''; + const border = styles.inputBorder ?? ''; this.element.style.backgroundColor = background; this.element.style.color = foreground; this.input.style.backgroundColor = 'inherit'; this.input.style.color = foreground; - this.element.style.borderWidth = border ? '1px' : ''; - this.element.style.borderStyle = border ? 'solid' : ''; - this.element.style.borderColor = border; + if (border) { + this.element.style.border = '1px solid ' + border; + } + } public layout(): void { diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index c5efc9adb7a..cbdc849f114 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -9,7 +9,6 @@ import { DomEmitter } from 'vs/base/browser/event'; import { IKeyboardEvent, StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { Gesture } from 'vs/base/browser/touch'; import { alert } from 'vs/base/browser/ui/aria/aria'; -import { IFindInputStyles } from 'vs/base/browser/ui/findinput/findInput'; import { CombinedSpliceable } from 'vs/base/browser/ui/list/splice'; import { ScrollableElementChangeOptions } from 'vs/base/browser/ui/scrollbar/scrollableElementOptions'; import { binarySearch, firstOrDefault, range } from 'vs/base/common/arrays'; @@ -963,7 +962,7 @@ export interface IListOptions extends IListOptionsUpdate { readonly initialSize?: Dimension; } -export interface IListStyles extends IFindInputStyles { +export interface IListStyles { listBackground?: Color; listFocusBackground?: Color; listFocusForeground?: Color; @@ -985,10 +984,6 @@ export interface IListStyles extends IFindInputStyles { listInactiveFocusOutline?: Color; listSelectionOutline?: Color; listHoverOutline?: Color; - listFilterWidgetBackground?: Color; - listFilterWidgetOutline?: Color; - listFilterWidgetNoMatchesOutline?: Color; - listFilterWidgetShadow?: Color; treeIndentGuidesStroke?: Color; tableColumnsBorder?: Color; tableOddRowsBackgroundColor?: Color; diff --git a/src/vs/base/browser/ui/toggle/toggle.ts b/src/vs/base/browser/ui/toggle/toggle.ts index b422d5a6d75..fb2083b9de1 100644 --- a/src/vs/base/browser/ui/toggle/toggle.ts +++ b/src/vs/base/browser/ui/toggle/toggle.ts @@ -8,7 +8,6 @@ import { BaseActionViewItem, IActionViewItemOptions } from 'vs/base/browser/ui/a import { Widget } from 'vs/base/browser/ui/widget'; import { IAction } from 'vs/base/common/actions'; import { Codicon, CSSIcon } from 'vs/base/common/codicons'; -import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; import 'vs/css!./toggle'; @@ -22,34 +21,38 @@ export interface IToggleOpts extends IToggleStyles { } export interface IToggleStyles { - inputActiveOptionBorder?: Color; - inputActiveOptionForeground?: Color; - inputActiveOptionBackground?: Color; + readonly inputActiveOptionBorder: string | undefined; + readonly inputActiveOptionForeground: string | undefined; + readonly inputActiveOptionBackground: string | undefined; } export interface ICheckboxStyles { - checkboxBackground?: Color; - checkboxBorder?: Color; - checkboxForeground?: Color; + readonly checkboxBackground: string | undefined; + readonly checkboxBorder: string | undefined; + readonly checkboxForeground: string | undefined; } -const defaultOpts = { - inputActiveOptionBorder: Color.fromHex('#007ACC00'), - inputActiveOptionForeground: Color.fromHex('#FFFFFF'), - inputActiveOptionBackground: Color.fromHex('#0E639C50') +export const unthemedToggleStyles = { + inputActiveOptionBorder: '#007ACC00', + inputActiveOptionForeground: '#FFFFFF', + inputActiveOptionBackground: '#0E639C50' }; export class ToggleActionViewItem extends BaseActionViewItem { protected readonly toggle: Toggle; - constructor(context: any, action: IAction, options: IActionViewItemOptions | undefined) { + constructor(context: any, action: IAction, options: IActionViewItemOptions) { super(context, action, options); + this.toggle = this._register(new Toggle({ actionClassName: this._action.class, isChecked: !!this._action.checked, title: (this.options).keybinding ? `${this._action.label} (${(this.options).keybinding})` : this._action.label, - notFocusable: true + notFocusable: true, + inputActiveOptionBackground: options.toggleStyles?.inputActiveOptionBackground, + inputActiveOptionBorder: options.toggleStyles?.inputActiveOptionBorder, + inputActiveOptionForeground: options.toggleStyles?.inputActiveOptionForeground, })); this._register(this.toggle.onChange(() => this._action.checked = !!this.toggle && this.toggle.checked)); } @@ -106,7 +109,7 @@ export class Toggle extends Widget { constructor(opts: IToggleOpts) { super(); - this._opts = { ...defaultOpts, ...opts }; + this._opts = opts; this._checked = this._opts.isChecked; const classes = ['monaco-custom-toggle']; @@ -191,24 +194,11 @@ export class Toggle extends Widget { return 2 /*margin left*/ + 2 /*border*/ + 2 /*padding*/ + 16 /* icon width */; } - style(styles: IToggleStyles): void { - if (styles.inputActiveOptionBorder) { - this._opts.inputActiveOptionBorder = styles.inputActiveOptionBorder; - } - if (styles.inputActiveOptionForeground) { - this._opts.inputActiveOptionForeground = styles.inputActiveOptionForeground; - } - if (styles.inputActiveOptionBackground) { - this._opts.inputActiveOptionBackground = styles.inputActiveOptionBackground; - } - this.applyStyles(); - } - protected applyStyles(): void { if (this.domNode) { - this.domNode.style.borderColor = this._checked && this._opts.inputActiveOptionBorder ? this._opts.inputActiveOptionBorder.toString() : ''; - this.domNode.style.color = this._checked && this._opts.inputActiveOptionForeground ? this._opts.inputActiveOptionForeground.toString() : 'inherit'; - this.domNode.style.backgroundColor = this._checked && this._opts.inputActiveOptionBackground ? this._opts.inputActiveOptionBackground.toString() : ''; + this.domNode.style.borderColor = (this._checked && this._opts.inputActiveOptionBorder) || ''; + this.domNode.style.color = (this._checked && this._opts.inputActiveOptionForeground) || 'inherit'; + this.domNode.style.backgroundColor = (this._checked && this._opts.inputActiveOptionBackground) || ''; } } @@ -232,18 +222,16 @@ export class Checkbox extends Widget { readonly domNode: HTMLElement; - constructor(private title: string, private isChecked: boolean) { + constructor(private title: string, private isChecked: boolean, styles: ICheckboxStyles) { super(); - this.checkbox = new Toggle({ title: this.title, isChecked: this.isChecked, icon: Codicon.check, actionClassName: 'monaco-checkbox' }); + this.checkbox = new Toggle({ title: this.title, isChecked: this.isChecked, icon: Codicon.check, actionClassName: 'monaco-checkbox', ...unthemedToggleStyles }); this.domNode = this.checkbox.domNode; - this.styles = {}; + this.styles = styles; - this.checkbox.onChange(() => { - this.applyStyles(); - }); + this.applyStyles(); } get checked(): boolean { @@ -252,8 +240,6 @@ export class Checkbox extends Widget { set checked(newIsChecked: boolean) { this.checkbox.checked = newIsChecked; - - this.applyStyles(); } focus(): void { @@ -264,15 +250,9 @@ export class Checkbox extends Widget { return this.domNode === document.activeElement; } - style(styles: ICheckboxStyles): void { - this.styles = styles; - - this.applyStyles(); - } - protected applyStyles(): void { - this.domNode.style.color = this.styles.checkboxForeground ? this.styles.checkboxForeground.toString() : ''; - this.domNode.style.backgroundColor = this.styles.checkboxBackground ? this.styles.checkboxBackground.toString() : ''; - this.domNode.style.borderColor = this.styles.checkboxBorder ? this.styles.checkboxBorder.toString() : ''; + this.domNode.style.color = this.styles.checkboxForeground || ''; + this.domNode.style.backgroundColor = this.styles.checkboxBackground || ''; + this.domNode.style.borderColor = this.styles.checkboxBorder || ''; } } diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index b9232d29cd6..20c3c4d2281 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -9,12 +9,12 @@ import { DomEmitter } from 'vs/base/browser/event'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; -import { FindInput, IFindInputStyles } from 'vs/base/browser/ui/findinput/findInput'; -import { IMessage, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; +import { FindInput } from 'vs/base/browser/ui/findinput/findInput'; +import { IInputBoxStyles, IMessage, MessageType, unthemedInboxStyles } from 'vs/base/browser/ui/inputbox/inputBox'; import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListContextMenuEvent, IListDragAndDrop, IListDragOverReaction, IListMouseEvent, IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; import { IListOptions, IListStyles, isButton, isInputElement, isMonacoEditor, List, MouseController, TypeNavigationMode } from 'vs/base/browser/ui/list/listWidget'; -import { Toggle } from 'vs/base/browser/ui/toggle/toggle'; +import { IToggleStyles, Toggle, unthemedToggleStyles } from 'vs/base/browser/ui/toggle/toggle'; import { getVisibleState, isFilterResult } from 'vs/base/browser/ui/tree/indexTreeModel'; import { ICollapseStateChangeEvent, ITreeContextMenuEvent, ITreeDragAndDrop, ITreeEvent, ITreeFilter, ITreeModel, ITreeModelSpliceEvent, ITreeMouseEvent, ITreeNavigator, ITreeNode, ITreeRenderer, TreeDragOverBubble, TreeError, TreeFilterResult, TreeMouseEventTarget, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; import { Action } from 'vs/base/common/actions'; @@ -22,7 +22,6 @@ import { distinct, equals, firstOrDefault, range } from 'vs/base/common/arrays'; import { disposableTimeout, timeout } from 'vs/base/common/async'; import { Codicon } from 'vs/base/common/codicons'; import { SetMap } from 'vs/base/common/collections'; -import { Color } from 'vs/base/common/color'; import { Emitter, Event, EventBufferer, Relay } from 'vs/base/common/event'; import { fuzzyScore, FuzzyScore } from 'vs/base/common/filters'; import { KeyCode } from 'vs/base/common/keyCodes'; @@ -644,27 +643,45 @@ class FindFilter implements ITreeFilter, IDi export interface ICaseSensitiveToggleOpts { readonly isChecked: boolean; - readonly inputActiveOptionBorder?: Color; - readonly inputActiveOptionForeground?: Color; - readonly inputActiveOptionBackground?: Color; + readonly inputActiveOptionBorder: string | undefined; + readonly inputActiveOptionForeground: string | undefined; + readonly inputActiveOptionBackground: string | undefined; } export class ModeToggle extends Toggle { - constructor(opts?: ICaseSensitiveToggleOpts) { + constructor(opts: ICaseSensitiveToggleOpts) { super({ icon: Codicon.listFilter, title: localize('filter', "Filter"), - isChecked: opts?.isChecked ?? false, - inputActiveOptionBorder: opts?.inputActiveOptionBorder, - inputActiveOptionForeground: opts?.inputActiveOptionForeground, - inputActiveOptionBackground: opts?.inputActiveOptionBackground + isChecked: opts.isChecked ?? false, + inputActiveOptionBorder: opts.inputActiveOptionBorder, + inputActiveOptionForeground: opts.inputActiveOptionForeground, + inputActiveOptionBackground: opts.inputActiveOptionBackground }); } } -export interface IFindWidgetStyles extends IFindInputStyles, IListStyles { } +export interface IFindWidgetStyles { + listFilterWidgetBackground: string | undefined; + listFilterWidgetOutline: string | undefined; + listFilterWidgetNoMatchesOutline: string | undefined; + listFilterWidgetShadow: string | undefined; + readonly toggleStyles: IToggleStyles; + readonly inputBoxStyles: IInputBoxStyles; +} -export interface IFindWidgetOpts extends IFindWidgetStyles { } +export interface IFindWidgetOptions { + readonly styles?: IFindWidgetStyles; +} + +const unthemedFindWidgetStyles: IFindWidgetStyles = { + inputBoxStyles: unthemedInboxStyles, + toggleStyles: unthemedToggleStyles, + listFilterWidgetBackground: undefined, + listFilterWidgetNoMatchesOutline: undefined, + listFilterWidgetOutline: undefined, + listFilterWidgetShadow: undefined +}; export enum TreeFindMode { Highlight, @@ -709,20 +726,32 @@ class FindWidget extends Disposable { private tree: AbstractTree, contextViewProvider: IContextViewProvider, mode: TreeFindMode, - options?: IFindWidgetOpts + options?: IFindWidgetOptions ) { super(); container.appendChild(this.elements.root); this._register(toDisposable(() => container.removeChild(this.elements.root))); - this.modeToggle = this._register(new ModeToggle({ ...options, isChecked: mode === TreeFindMode.Filter })); + const styles = options?.styles ?? unthemedFindWidgetStyles; + + if (styles.listFilterWidgetBackground) { + this.elements.root.style.backgroundColor = styles.listFilterWidgetBackground; + } + + if (styles.listFilterWidgetShadow) { + this.elements.root.style.boxShadow = `0 0 8px 2px ${styles.listFilterWidgetShadow}`; + } + + this.modeToggle = this._register(new ModeToggle({ ...styles.toggleStyles, isChecked: mode === TreeFindMode.Filter })); this.onDidChangeMode = Event.map(this.modeToggle.onChange, () => this.modeToggle.checked ? TreeFindMode.Filter : TreeFindMode.Highlight, this._store); this.findInput = this._register(new FindInput(this.elements.findInput, contextViewProvider, { label: localize('type to search', "Type to search"), additionalToggles: [this.modeToggle], - showCommonFindToggles: false + showCommonFindToggles: false, + inputBoxStyles: styles.inputBoxStyles, + toggleStyles: styles.toggleStyles })); this.actionbar = this._register(new ActionBar(this.elements.actionbar)); @@ -822,19 +851,6 @@ class FindWidget extends Disposable { })); this.onDidChangeValue = this.findInput.onDidChange; - this.style(options ?? {}); - } - - style(styles: IFindWidgetStyles): void { - this.findInput.style(styles); - - if (styles.listFilterWidgetBackground) { - this.elements.root.style.backgroundColor = styles.listFilterWidgetBackground.toString(); - } - - if (styles.listFilterWidgetShadow) { - this.elements.root.style.boxShadow = `0 0 8px 2px ${styles.listFilterWidgetShadow}`; - } } focus() { @@ -869,6 +885,8 @@ class FindWidget extends Disposable { } } +interface IFindControllerOptions extends IFindWidgetOptions { } + class FindController implements IDisposable { private _pattern = ''; @@ -894,7 +912,6 @@ class FindController implements IDisposable { } private widget: FindWidget | undefined; - private styles: IFindWidgetStyles | undefined; private width = 0; private readonly _onDidChangeMode = new Emitter(); @@ -914,7 +931,8 @@ class FindController implements IDisposable { model: ITreeModel, private view: List>, private filter: FindFilter, - private readonly contextViewProvider: IContextViewProvider + private readonly contextViewProvider: IContextViewProvider, + private readonly options: IFindControllerOptions = {} ) { this._mode = tree.options.defaultFindMode ?? TreeFindMode.Highlight; model.onDidSplice(this.onDidSpliceModel, this, this.disposables); @@ -928,7 +946,7 @@ class FindController implements IDisposable { } this.mode = this.tree.options.defaultFindMode ?? TreeFindMode.Highlight; - this.widget = new FindWidget(this.view.getHTMLElement(), this.tree, this.contextViewProvider, this.mode, this.styles); + this.widget = new FindWidget(this.view.getHTMLElement(), this.tree, this.contextViewProvider, this.mode, this.options); this.enabledDisposables.add(this.widget); this.widget.onDidChangeValue(this.onDidChangeValue, this, this.enabledDisposables); @@ -1020,11 +1038,6 @@ class FindController implements IDisposable { return !FuzzyScore.isDefault(node.filterData as any as FuzzyScore); } - style(styles: IFindWidgetStyles): void { - this.styles = styles; - this.widget?.style(styles); - } - layout(width: number): void { this.width = width; this.widget?.layout(width); @@ -1084,6 +1097,7 @@ export interface IAbstractTreeOptions extends IAbstractTr readonly dnd?: ITreeDragAndDrop; readonly additionalScrollHeight?: number; readonly findWidgetEnabled?: boolean; + readonly findWidgetStyles?: IFindWidgetStyles; } function dfs(node: ITreeNode, fn: (node: ITreeNode) => void): void { @@ -1252,11 +1266,10 @@ class TreeNodeListMouseController extends MouseController< } if (node.collapsible) { - const model = this.tree.model; // internal - const location = model.getNodeLocation(node); + const location = this.tree.getNodeLocation(node); const recursive = e.browserEvent.altKey; this.tree.setFocus([location]); - model.setCollapsed(location, undefined, recursive); + this.tree.toggleCollapsed(location, recursive); if (expandOnlyOnTwistieClick && onTwistie) { return; @@ -1375,7 +1388,7 @@ export abstract class AbstractTree implements IDisposable protected view: TreeNodeList; private renderers: TreeRenderer[]; - model: ITreeModel; // used in MouseController + protected model: ITreeModel; private focus: Trait; private selection: Trait; private anchor: Trait; @@ -1500,7 +1513,8 @@ export abstract class AbstractTree implements IDisposable } if ((_options.findWidgetEnabled ?? true) && _options.keyboardNavigationLabelProvider && _options.contextViewProvider) { - this.findController = new FindController(this, this.model, this.view, filter!, _options.contextViewProvider); + const opts = this.options.findWidgetStyles ? { styles: this.options.findWidgetStyles } : undefined; + this.findController = new FindController(this, this.model, this.view, filter!, _options.contextViewProvider, opts); this.focusNavigationFilter = node => this.findController!.shouldAllowFocus(node); this.onDidChangeFindOpenState = this.findController.onDidChangeOpenState; this.disposables.add(this.findController!); @@ -1630,7 +1644,6 @@ export abstract class AbstractTree implements IDisposable this.styleElement.textContent = content.join('\n'); - this.findController?.style(styles); this.view.style(styles); } @@ -1652,6 +1665,10 @@ export abstract class AbstractTree implements IDisposable return this.model.getNode(location); } + getNodeLocation(node: ITreeNode): TRef { + return this.model.getNodeLocation(node); + } + collapse(location: TRef, recursive: boolean = false): boolean { return this.model.setCollapsed(location, true, recursive); } diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index 63d1d9e0113..063e38474c8 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -268,8 +268,7 @@ function asObjectTreeOptions(options?: IAsyncDataTreeOpt typeof options.expandOnlyOnTwistieClick !== 'function' ? options.expandOnlyOnTwistieClick : ( e => (options.expandOnlyOnTwistieClick as ((e: T) => boolean))(e.element as T) ) - ), - additionalScrollHeight: options.additionalScrollHeight + ) }; } diff --git a/src/vs/base/browser/ui/tree/dataTree.ts b/src/vs/base/browser/ui/tree/dataTree.ts index 6dd8cc19275..06b8e2339ec 100644 --- a/src/vs/base/browser/ui/tree/dataTree.ts +++ b/src/vs/base/browser/ui/tree/dataTree.ts @@ -16,7 +16,7 @@ export interface IDataTreeOptions extends IAbstractTreeOp export class DataTree extends AbstractTree { - declare model: ObjectTreeModel; + protected declare model: ObjectTreeModel; private input: TInput | undefined; private identityProvider: IIdentityProvider | undefined; diff --git a/src/vs/base/browser/ui/tree/indexTree.ts b/src/vs/base/browser/ui/tree/indexTree.ts index 2e7c4d87688..cf86858ca51 100644 --- a/src/vs/base/browser/ui/tree/indexTree.ts +++ b/src/vs/base/browser/ui/tree/indexTree.ts @@ -14,7 +14,7 @@ export interface IIndexTreeOptions extends IAbstractTreeO export class IndexTree extends AbstractTree { - declare model: IndexTreeModel; + protected declare model: IndexTreeModel; constructor( user: string, diff --git a/src/vs/base/browser/ui/tree/objectTree.ts b/src/vs/base/browser/ui/tree/objectTree.ts index c1fab1e99d5..c6be2d0f4ff 100644 --- a/src/vs/base/browser/ui/tree/objectTree.ts +++ b/src/vs/base/browser/ui/tree/objectTree.ts @@ -45,7 +45,7 @@ export interface IObjectTreeViewState { export class ObjectTree, TFilterData = void> extends AbstractTree { - declare model: IObjectTreeModel; + protected declare model: IObjectTreeModel; override get onDidChangeCollapseState(): Event> { return this.model.onDidChangeCollapseState; } @@ -197,7 +197,7 @@ export interface ICompressibleObjectTreeOptionsUpdate extends IAbstractTreeOptio export class CompressibleObjectTree, TFilterData = void> extends ObjectTree implements ICompressedTreeNodeProvider { - declare model: CompressibleObjectTreeModel; + protected declare model: CompressibleObjectTreeModel; constructor( user: string, diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index 232375b7f10..910af8de46a 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -819,7 +819,7 @@ export class IntervalTimer implements IDisposable { } } -export class RunOnceScheduler { +export class RunOnceScheduler implements IDisposable { protected runner: ((...args: unknown[]) => void) | null; diff --git a/src/vs/base/common/marshallingIds.ts b/src/vs/base/common/marshallingIds.ts index 4387c42cc67..abd7698ed92 100644 --- a/src/vs/base/common/marshallingIds.ts +++ b/src/vs/base/common/marshallingIds.ts @@ -12,6 +12,7 @@ export const enum MarshalledId { ScmProvider, CommentController, CommentThread, + CommentThreadInstance, CommentThreadReply, CommentNode, CommentThreadNode, diff --git a/src/vs/base/common/types.ts b/src/vs/base/common/types.ts index 08063ae38d1..c35da047d1f 100644 --- a/src/vs/base/common/types.ts +++ b/src/vs/base/common/types.ts @@ -224,3 +224,11 @@ export type AddFirstParameterToFunctions; */ export type AtLeastOne }> = Partial & U[keyof U]; + + +/** + * A type that removed readonly-less from all properties of `T` + */ +export type Mutable = { + -readonly [P in keyof T]: T[P] +}; diff --git a/src/vs/base/parts/quickinput/browser/quickInput.ts b/src/vs/base/parts/quickinput/browser/quickInput.ts index f2f31a4cb6c..f0a8d484126 100644 --- a/src/vs/base/parts/quickinput/browser/quickInput.ts +++ b/src/vs/base/parts/quickinput/browser/quickInput.ts @@ -15,7 +15,7 @@ import { IKeybindingLabelStyles } from 'vs/base/browser/ui/keybindingLabel/keybi import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IListOptions, IListStyles, List } from 'vs/base/browser/ui/list/listWidget'; import { IProgressBarStyles, ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; -import { Toggle } from 'vs/base/browser/ui/toggle/toggle'; +import { IToggleStyles, Toggle } from 'vs/base/browser/ui/toggle/toggle'; import { Action } from 'vs/base/common/actions'; import { equals } from 'vs/base/common/arrays'; import { TimeoutTimer } from 'vs/base/common/async'; @@ -56,6 +56,7 @@ export interface IQuickInputOptions { export interface IQuickInputStyles { widget: IQuickInputWidgetStyles; inputBox: IInputBoxStyles; + toggle: IToggleStyles; countBadge: ICountBadgetyles; button: IButtonStyles; progressBar: IProgressBarStyles; @@ -1282,7 +1283,7 @@ export class QuickInputController extends Disposable { const extraContainer = dom.append(headerContainer, $('.quick-input-and-message')); const filterContainer = dom.append(extraContainer, $('.quick-input-filter')); - const inputBox = this._register(new QuickInputBox(filterContainer)); + const inputBox = this._register(new QuickInputBox(filterContainer, this.styles.inputBox, this.styles.toggle)); inputBox.setAttribute('aria-describedby', `${this.idPrefix}message`); const visibleCountContainer = dom.append(filterContainer, $('.quick-input-visible-count')); @@ -1823,7 +1824,6 @@ export class QuickInputController extends Disposable { this.ui.container.style.color = quickInputForeground ? quickInputForeground.toString() : ''; this.ui.container.style.border = contrastBorder ? `1px solid ${contrastBorder}` : ''; this.ui.container.style.boxShadow = widgetShadow ? `0 0 8px 2px ${widgetShadow}` : ''; - this.ui.inputBox.style(this.styles.inputBox); this.ui.count.style(this.styles.countBadge); this.ui.list.style(this.styles.list); diff --git a/src/vs/base/parts/quickinput/browser/quickInputBox.ts b/src/vs/base/parts/quickinput/browser/quickInputBox.ts index 1184a456574..f73ff7d739a 100644 --- a/src/vs/base/parts/quickinput/browser/quickInputBox.ts +++ b/src/vs/base/parts/quickinput/browser/quickInputBox.ts @@ -8,7 +8,7 @@ import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { FindInput } from 'vs/base/browser/ui/findinput/findInput'; import { IInputBoxStyles, IRange, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; -import { Toggle } from 'vs/base/browser/ui/toggle/toggle'; +import { IToggleStyles, Toggle } from 'vs/base/browser/ui/toggle/toggle'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import Severity from 'vs/base/common/severity'; import 'vs/css!./media/quickInput'; @@ -21,11 +21,13 @@ export class QuickInputBox extends Disposable { private findInput: FindInput; constructor( - private parent: HTMLElement + private parent: HTMLElement, + inputBoxStyles: IInputBoxStyles, + toggleStyles: IToggleStyles ) { super(); this.container = dom.append(this.parent, $('.quick-input-box')); - this.findInput = this._register(new FindInput(this.container, undefined, { label: '' })); + this.findInput = this._register(new FindInput(this.container, undefined, { label: '', inputBoxStyles, toggleStyles })); } onKeyDown = (handler: (event: StandardKeyboardEvent) => void): IDisposable => { @@ -127,8 +129,4 @@ export class QuickInputBox extends Disposable { layout(): void { this.findInput.inputBox.layout(); } - - style(styles: IInputBoxStyles): void { - this.findInput.style(styles); - } } diff --git a/src/vs/base/parts/quickinput/test/browser/quickinput.test.ts b/src/vs/base/parts/quickinput/test/browser/quickinput.test.ts index b408d42236c..f5c04572ec8 100644 --- a/src/vs/base/parts/quickinput/test/browser/quickinput.test.ts +++ b/src/vs/base/parts/quickinput/test/browser/quickinput.test.ts @@ -4,9 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; +import { unthemedInboxStyles } from 'vs/base/browser/ui/inputbox/inputBox'; import { unthemedButtonStyles } from 'vs/base/browser/ui/button/button'; import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IListOptions, List } from 'vs/base/browser/ui/list/listWidget'; +import { unthemedToggleStyles } from 'vs/base/browser/ui/toggle/toggle'; import { raceTimeout } from 'vs/base/common/async'; import { QuickInputController } from 'vs/base/parts/quickinput/browser/quickInput'; import { IQuickPick, IQuickPickItem } from 'vs/base/parts/quickinput/common/quickInput'; @@ -55,7 +57,8 @@ suite('QuickInput', () => { // https://github.com/microsoft/vscode/issues/147543 styles: { button: unthemedButtonStyles, countBadge: {}, - inputBox: {}, + inputBox: unthemedInboxStyles, + toggle: unthemedToggleStyles, keybindingLabel: {}, list: {}, progressBar: {}, diff --git a/src/vs/base/test/browser/ui/tree/dataTree.test.ts b/src/vs/base/test/browser/ui/tree/dataTree.test.ts index bc6ed5f3106..ab450c682e7 100644 --- a/src/vs/base/test/browser/ui/tree/dataTree.test.ts +++ b/src/vs/base/test/browser/ui/tree/dataTree.test.ts @@ -63,9 +63,7 @@ suite('DataTree', function () { } }; - tree = new DataTree('test', container, delegate, [renderer], dataSource, { - identityProvider - }); + tree = new DataTree('test', container, delegate, [renderer], dataSource, { identityProvider }); tree.layout(200); }); diff --git a/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts b/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts index 2fcf59e3ae0..f71559a35dd 100644 --- a/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts +++ b/src/vs/code/electron-sandbox/processExplorer/processExplorerMain.ts @@ -333,7 +333,7 @@ class ProcessExplorer { return 'header'; } - }, + } }); this.tree.setInput({ processes: { processRoots } }); diff --git a/src/vs/editor/browser/widget/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditorWidget.ts index 216e31ab49e..0cf378ee559 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget.ts @@ -1878,6 +1878,11 @@ function createDecoration(startLineNumber: number, startColumn: number, endLineN }; } +const enum DiffEditorLineClasses { + Insert = 'line-insert', + Delete = 'line-delete' +} + const DECORATIONS = { arrowRevertChange: ModelDecorationOptions.register({ @@ -1907,13 +1912,13 @@ const DECORATIONS = { lineInsert: ModelDecorationOptions.register({ description: 'diff-editor-line-insert', - className: 'line-insert', + className: DiffEditorLineClasses.Insert, marginClassName: 'gutter-insert', isWholeLine: true }), lineInsertWithSign: ModelDecorationOptions.register({ description: 'diff-editor-line-insert-with-sign', - className: 'line-insert', + className: DiffEditorLineClasses.Insert, linesDecorationsClassName: 'insert-sign ' + ThemeIcon.asClassName(diffInsertIcon), marginClassName: 'gutter-insert', isWholeLine: true @@ -1921,13 +1926,13 @@ const DECORATIONS = { lineDelete: ModelDecorationOptions.register({ description: 'diff-editor-line-delete', - className: 'line-delete', + className: DiffEditorLineClasses.Delete, marginClassName: 'gutter-delete', isWholeLine: true }), lineDeleteWithSign: ModelDecorationOptions.register({ description: 'diff-editor-line-delete-with-sign', - className: 'line-delete', + className: DiffEditorLineClasses.Delete, linesDecorationsClassName: 'delete-sign ' + ThemeIcon.asClassName(diffRemoveIcon), marginClassName: 'gutter-delete', isWholeLine: true diff --git a/src/vs/editor/browser/widget/diffReview.ts b/src/vs/editor/browser/widget/diffReview.ts index a615c329e05..12889bed475 100644 --- a/src/vs/editor/browser/widget/diffReview.ts +++ b/src/vs/editor/browser/widget/diffReview.ts @@ -33,6 +33,7 @@ import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { ILanguageIdCodec } from 'vs/editor/common/languages'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { ILineChange } from 'vs/editor/common/diff/smartLinesDiffComputer'; +import { AudioCue, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService'; const DIFF_LINES_PADDING = 3; @@ -66,6 +67,11 @@ class DiffEntry { } } +const enum DiffEditorLineClasses { + Insert = 'line-insert', + Delete = 'line-delete' +} + class Diff { readonly entries: DiffEntry[]; @@ -95,7 +101,8 @@ export class DiffReview extends Disposable { constructor( diffEditor: DiffEditorWidget, - @ILanguageService private readonly _languageService: ILanguageService + @ILanguageService private readonly _languageService: ILanguageService, + @IAudioCueService private readonly _audioCueService: IAudioCueService ) { super(); this._diffEditor = diffEditor; @@ -149,7 +156,7 @@ export class DiffReview extends Disposable { || e.equals(KeyMod.Alt | KeyCode.DownArrow) ) { e.preventDefault(); - this._goToRow(this._getNextRow()); + this._goToRow(this._getNextRow(), 'next'); } if ( @@ -158,7 +165,7 @@ export class DiffReview extends Disposable { || e.equals(KeyMod.Alt | KeyCode.UpArrow) ) { e.preventDefault(); - this._goToRow(this._getPrevRow()); + this._goToRow(this._getPrevRow(), 'previous'); } if ( @@ -215,7 +222,7 @@ export class DiffReview extends Disposable { this._isVisible = true; this._diffEditor.doLayout(); this._render(); - this._goToRow(this._getNextRow()); + this._goToRow(this._getPrevRow(), 'previous'); } public next(): void { @@ -250,7 +257,7 @@ export class DiffReview extends Disposable { this._isVisible = true; this._diffEditor.doLayout(); this._render(); - this._goToRow(this._getNextRow()); + this._goToRow(this._getNextRow(), 'next'); } private accept(): void { @@ -312,12 +319,18 @@ export class DiffReview extends Disposable { return null; } - private _goToRow(row: HTMLElement): void { - const prev = this._getCurrentFocusedRow(); + private _goToRow(row: HTMLElement, type?: 'next' | 'previous'): void { + const current = this._getCurrentFocusedRow(); row.tabIndex = 0; row.focus(); - if (prev && prev !== row) { - prev.tabIndex = -1; + if (current && current !== row) { + current.tabIndex = -1; + } + const element = !type ? current : type === 'next' ? current?.nextElementSibling : current?.previousElementSibling; + if (element?.classList.contains(DiffEditorLineClasses.Insert)) { + this._audioCueService.playAudioCue(AudioCue.diffLineInserted, true); + } else if (element?.classList.contains(DiffEditorLineClasses.Delete)) { + this._audioCueService.playAudioCue(AudioCue.diffLineDeleted, true); } this.scrollbar.scanDomNode(); } diff --git a/src/vs/editor/common/core/wordHelper.ts b/src/vs/editor/common/core/wordHelper.ts index cdb2d4b8bfb..862d94ccb06 100644 --- a/src/vs/editor/common/core/wordHelper.ts +++ b/src/vs/editor/common/core/wordHelper.ts @@ -61,7 +61,7 @@ export function ensureValidWordDefinition(wordDefinition?: RegExp | null): RegEx if (wordDefinition.multiline) { flags += 'm'; } - if ((wordDefinition as any).unicode) { + if (wordDefinition.unicode) { flags += 'u'; } result = new RegExp(wordDefinition.source, flags); diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 0d2d8f3576f..861ad5b5832 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -21,6 +21,7 @@ import { IModelContentChange, IModelContentChangedEvent, IModelDecorationsChange import { IGuidesTextModelPart } from 'vs/editor/common/textModelGuides'; import { ITokenizationTextModelPart } from 'vs/editor/common/tokenizationTextModelPart'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; +import { UndoRedoGroup } from 'vs/platform/undoRedo/common/undoRedo'; /** * Vertical Lane in the overview ruler of the editor. @@ -1023,6 +1024,10 @@ export interface ITextModel { * @return The cursor state returned by the `cursorStateComputer`. */ pushEditOperations(beforeCursorState: Selection[] | null, editOperations: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer): Selection[] | null; + /** + * @internal + */ + pushEditOperations(beforeCursorState: Selection[] | null, editOperations: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer, group?: UndoRedoGroup): Selection[] | null; /** * Change the end of line sequence. This is the preferred way of diff --git a/src/vs/editor/common/model/editStack.ts b/src/vs/editor/common/model/editStack.ts index c1fd64e880d..8c9f1c4d6e2 100644 --- a/src/vs/editor/common/model/editStack.ts +++ b/src/vs/editor/common/model/editStack.ts @@ -8,7 +8,7 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { Selection } from 'vs/editor/common/core/selection'; import { EndOfLineSequence, ICursorStateComputer, IValidEditOperation, ITextModel } from 'vs/editor/common/model'; import { TextModel } from 'vs/editor/common/model/textModel'; -import { IUndoRedoService, IResourceUndoRedoElement, UndoRedoElementType, IWorkspaceUndoRedoElement } from 'vs/platform/undoRedo/common/undoRedo'; +import { IUndoRedoService, IResourceUndoRedoElement, UndoRedoElementType, IWorkspaceUndoRedoElement, UndoRedoGroup } from 'vs/platform/undoRedo/common/undoRedo'; import { URI } from 'vs/base/common/uri'; import { TextChange, compressConsecutiveTextChanges } from 'vs/editor/common/core/textChange'; import * as buffer from 'vs/base/common/buffer'; @@ -408,24 +408,24 @@ export class EditStack { this._undoRedoService.removeElements(this._model.uri); } - private _getOrCreateEditStackElement(beforeCursorState: Selection[] | null): EditStackElement { + private _getOrCreateEditStackElement(beforeCursorState: Selection[] | null, group: UndoRedoGroup | undefined): EditStackElement { const lastElement = this._undoRedoService.getLastElement(this._model.uri); if (isEditStackElement(lastElement) && lastElement.canAppend(this._model)) { return lastElement; } const newElement = new SingleModelEditStackElement(nls.localize('edit', "Typing"), 'undoredo.textBufferEdit', this._model, beforeCursorState); - this._undoRedoService.pushElement(newElement); + this._undoRedoService.pushElement(newElement, group); return newElement; } public pushEOL(eol: EndOfLineSequence): void { - const editStackElement = this._getOrCreateEditStackElement(null); + const editStackElement = this._getOrCreateEditStackElement(null, undefined); this._model.setEOL(eol); editStackElement.append(this._model, [], getModelEOL(this._model), this._model.getAlternativeVersionId(), null); } - public pushEditOperation(beforeCursorState: Selection[] | null, editOperations: ISingleEditOperation[], cursorStateComputer: ICursorStateComputer | null): Selection[] | null { - const editStackElement = this._getOrCreateEditStackElement(beforeCursorState); + public pushEditOperation(beforeCursorState: Selection[] | null, editOperations: ISingleEditOperation[], cursorStateComputer: ICursorStateComputer | null, group?: UndoRedoGroup): Selection[] | null { + const editStackElement = this._getOrCreateEditStackElement(beforeCursorState, group); const inverseEditOperations = this._model.applyEdits(editOperations, true); const afterCursorState = EditStack._computeCursorState(cursorStateComputer, inverseEditOperations); const textChanges = inverseEditOperations.map((op, index) => ({ index: index, textChange: op.textChange })); diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index f22215997fb..9447ad43570 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -42,7 +42,7 @@ import { IModelContentChangedEvent, IModelDecorationsChangedEvent, IModelOptions import { IGuidesTextModelPart } from 'vs/editor/common/textModelGuides'; import { ITokenizationTextModelPart } from 'vs/editor/common/tokenizationTextModelPart'; import { IColorTheme, ThemeColor } from 'vs/platform/theme/common/themeService'; -import { IUndoRedoService, ResourceEditStackSnapshot } from 'vs/platform/undoRedo/common/undoRedo'; +import { IUndoRedoService, ResourceEditStackSnapshot, UndoRedoGroup } from 'vs/platform/undoRedo/common/undoRedo'; export function createTextBufferFactory(text: string): model.ITextBufferFactory { const builder = new PieceTreeTextBufferBuilder(); @@ -1242,18 +1242,18 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati return result; } - public pushEditOperations(beforeCursorState: Selection[] | null, editOperations: model.IIdentifiedSingleEditOperation[], cursorStateComputer: model.ICursorStateComputer | null): Selection[] | null { + public pushEditOperations(beforeCursorState: Selection[] | null, editOperations: model.IIdentifiedSingleEditOperation[], cursorStateComputer: model.ICursorStateComputer | null, group?: UndoRedoGroup): Selection[] | null { try { this._onDidChangeDecorations.beginDeferredEmit(); this._eventEmitter.beginDeferredEmit(); - return this._pushEditOperations(beforeCursorState, this._validateEditOperations(editOperations), cursorStateComputer); + return this._pushEditOperations(beforeCursorState, this._validateEditOperations(editOperations), cursorStateComputer, group); } finally { this._eventEmitter.endDeferredEmit(); this._onDidChangeDecorations.endDeferredEmit(); } } - private _pushEditOperations(beforeCursorState: Selection[] | null, editOperations: model.ValidAnnotatedEditOperation[], cursorStateComputer: model.ICursorStateComputer | null): Selection[] | null { + private _pushEditOperations(beforeCursorState: Selection[] | null, editOperations: model.ValidAnnotatedEditOperation[], cursorStateComputer: model.ICursorStateComputer | null, group?: UndoRedoGroup): Selection[] | null { if (this._options.trimAutoWhitespace && this._trimAutoWhitespaceLines) { // Go through each saved line number and insert a trim whitespace edit // if it is safe to do so (no conflicts with other edits). @@ -1340,7 +1340,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati if (this._initialUndoRedoSnapshot === null) { this._initialUndoRedoSnapshot = this._undoRedoService.createSnapshot(this.uri); } - return this._commandManager.pushEditOperation(beforeCursorState, editOperations, cursorStateComputer); + return this._commandManager.pushEditOperation(beforeCursorState, editOperations, cursorStateComputer, group); } _applyUndo(changes: TextChange[], eol: model.EndOfLineSequence, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): void { diff --git a/src/vs/editor/contrib/copyPaste/browser/copyPasteContribution.ts b/src/vs/editor/contrib/copyPaste/browser/copyPasteContribution.ts index 55c120d060f..ace4c5b0112 100644 --- a/src/vs/editor/contrib/copyPaste/browser/copyPasteContribution.ts +++ b/src/vs/editor/contrib/copyPaste/browser/copyPasteContribution.ts @@ -3,14 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { EditorContributionInstantiation, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { editorConfigurationBaseNode } from 'vs/editor/common/config/editorConfigurationSchema'; import { CopyPasteController } from 'vs/editor/contrib/copyPaste/browser/copyPasteController'; import * as nls from 'vs/nls'; import { ConfigurationScope, Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; -registerEditorContribution(CopyPasteController.ID, CopyPasteController); +registerEditorContribution(CopyPasteController.ID, CopyPasteController, EditorContributionInstantiation.Idle); Registry.as(Extensions.Configuration).registerConfiguration({ ...editorConfigurationBaseNode, diff --git a/src/vs/editor/contrib/dropIntoEditor/browser/dropIntoEditorContribution.ts b/src/vs/editor/contrib/dropIntoEditor/browser/dropIntoEditorContribution.ts index 84632383d27..191aac71112 100644 --- a/src/vs/editor/contrib/dropIntoEditor/browser/dropIntoEditorContribution.ts +++ b/src/vs/editor/contrib/dropIntoEditor/browser/dropIntoEditorContribution.ts @@ -12,7 +12,7 @@ import { relativePath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { addExternalEditorsDropData, toVSDataTransfer } from 'vs/editor/browser/dnd'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { EditorContributionInstantiation, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { IBulkEditService, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { IPosition } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; @@ -178,5 +178,5 @@ class DefaultOnDropProvider implements DocumentOnDropEditProvider { } -registerEditorContribution(DropIntoEditorController.ID, DropIntoEditorController); +registerEditorContribution(DropIntoEditorController.ID, DropIntoEditorController, EditorContributionInstantiation.Idle); diff --git a/src/vs/editor/contrib/find/browser/findController.ts b/src/vs/editor/contrib/find/browser/findController.ts index 3ca56c29d04..676fa5dcbaa 100644 --- a/src/vs/editor/contrib/find/browser/findController.ts +++ b/src/vs/editor/contrib/find/browser/findController.ts @@ -484,7 +484,7 @@ export class FindController extends CommonFindController implements IFindControl private _createFindWidget() { this._widget = this._register(new FindWidget(this._editor, this, this._state, this._contextViewService, this._keybindingService, this._contextKeyService, this._themeService, this._storageService, this._notificationService)); - this._findOptionsWidget = this._register(new FindOptionsWidget(this._editor, this._state, this._keybindingService, this._themeService)); + this._findOptionsWidget = this._register(new FindOptionsWidget(this._editor, this._state, this._keybindingService)); } saveViewState(): any { diff --git a/src/vs/editor/contrib/find/browser/findOptionsWidget.ts b/src/vs/editor/contrib/find/browser/findOptionsWidget.ts index 0246d79df8e..096af99392b 100644 --- a/src/vs/editor/contrib/find/browser/findOptionsWidget.ts +++ b/src/vs/editor/contrib/find/browser/findOptionsWidget.ts @@ -12,8 +12,7 @@ import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPosit import { FIND_IDS } from 'vs/editor/contrib/find/browser/findModel'; import { FindReplaceState } from 'vs/editor/contrib/find/browser/findState'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground } from 'vs/platform/theme/common/colorRegistry'; -import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; +import { asCssValue, inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground } from 'vs/platform/theme/common/colorRegistry'; export class FindOptionsWidget extends Widget implements IOverlayWidget { @@ -31,8 +30,7 @@ export class FindOptionsWidget extends Widget implements IOverlayWidget { constructor( editor: ICodeEditor, state: FindReplaceState, - keybindingService: IKeybindingService, - themeService: IThemeService + keybindingService: IKeybindingService ) { super(); @@ -48,16 +46,16 @@ export class FindOptionsWidget extends Widget implements IOverlayWidget { this._domNode.setAttribute('role', 'presentation'); this._domNode.setAttribute('aria-hidden', 'true'); - const inputActiveOptionBorderColor = themeService.getColorTheme().getColor(inputActiveOptionBorder); - const inputActiveOptionForegroundColor = themeService.getColorTheme().getColor(inputActiveOptionForeground); - const inputActiveOptionBackgroundColor = themeService.getColorTheme().getColor(inputActiveOptionBackground); + const toggleStyles = { + inputActiveOptionBorder: asCssValue(inputActiveOptionBorder), + inputActiveOptionForeground: asCssValue(inputActiveOptionForeground), + inputActiveOptionBackground: asCssValue(inputActiveOptionBackground), + }; this.caseSensitive = this._register(new CaseSensitiveToggle({ appendTitle: this._keybindingLabelFor(FIND_IDS.ToggleCaseSensitiveCommand), isChecked: this._state.matchCase, - inputActiveOptionBorder: inputActiveOptionBorderColor, - inputActiveOptionForeground: inputActiveOptionForegroundColor, - inputActiveOptionBackground: inputActiveOptionBackgroundColor + ...toggleStyles })); this._domNode.appendChild(this.caseSensitive.domNode); this._register(this.caseSensitive.onChange(() => { @@ -69,9 +67,7 @@ export class FindOptionsWidget extends Widget implements IOverlayWidget { this.wholeWords = this._register(new WholeWordsToggle({ appendTitle: this._keybindingLabelFor(FIND_IDS.ToggleWholeWordCommand), isChecked: this._state.wholeWord, - inputActiveOptionBorder: inputActiveOptionBorderColor, - inputActiveOptionForeground: inputActiveOptionForegroundColor, - inputActiveOptionBackground: inputActiveOptionBackgroundColor + ...toggleStyles })); this._domNode.appendChild(this.wholeWords.domNode); this._register(this.wholeWords.onChange(() => { @@ -83,9 +79,7 @@ export class FindOptionsWidget extends Widget implements IOverlayWidget { this.regex = this._register(new RegexToggle({ appendTitle: this._keybindingLabelFor(FIND_IDS.ToggleRegexCommand), isChecked: this._state.isRegex, - inputActiveOptionBorder: inputActiveOptionBorderColor, - inputActiveOptionForeground: inputActiveOptionForegroundColor, - inputActiveOptionBackground: inputActiveOptionBackgroundColor + ...toggleStyles })); this._domNode.appendChild(this.regex.domNode); this._register(this.regex.onChange(() => { @@ -117,9 +111,6 @@ export class FindOptionsWidget extends Widget implements IOverlayWidget { this._register(dom.addDisposableListener(this._domNode, dom.EventType.MOUSE_LEAVE, (e) => this._onMouseLeave())); this._register(dom.addDisposableListener(this._domNode, 'mouseover', (e) => this._onMouseOver())); - - this._applyTheme(themeService.getColorTheme()); - this._register(themeService.onDidColorThemeChange(this._applyTheme.bind(this))); } private _keybindingLabelFor(actionId: string): string { @@ -187,15 +178,4 @@ export class FindOptionsWidget extends Widget implements IOverlayWidget { this._isVisible = false; this._domNode.style.display = 'none'; } - - private _applyTheme(theme: IColorTheme) { - const inputStyles = { - inputActiveOptionBorder: theme.getColor(inputActiveOptionBorder), - inputActiveOptionForeground: theme.getColor(inputActiveOptionForeground), - inputActiveOptionBackground: theme.getColor(inputActiveOptionBackground) - }; - this.caseSensitive.style(inputStyles); - this.wholeWords.style(inputStyles); - this.regex.style(inputStyles); - } } diff --git a/src/vs/editor/contrib/find/browser/findWidget.ts b/src/vs/editor/contrib/find/browser/findWidget.ts index 72b7ad349e8..5d4b9f76f63 100644 --- a/src/vs/editor/contrib/find/browser/findWidget.ts +++ b/src/vs/editor/contrib/find/browser/findWidget.ts @@ -9,7 +9,7 @@ import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { alert as alertFn } from 'vs/base/browser/ui/aria/aria'; import { Toggle } from 'vs/base/browser/ui/toggle/toggle'; import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; -import { FindInput, IFindInputStyles } from 'vs/base/browser/ui/findinput/findInput'; +import { FindInput } from 'vs/base/browser/ui/findinput/findInput'; import { ReplaceInput } from 'vs/base/browser/ui/findinput/replaceInput'; import { IMessage as InputBoxMessage } from 'vs/base/browser/ui/inputbox/inputBox'; import { ISashEvent, IVerticalSashLayoutProvider, Orientation, Sash } from 'vs/base/browser/ui/sash/sash'; @@ -36,11 +36,12 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { contrastBorder, editorFindMatch, editorFindMatchBorder, editorFindMatchHighlight, editorFindMatchHighlightBorder, editorFindRangeHighlight, editorFindRangeHighlightBorder, editorWidgetBackground, editorWidgetBorder, editorWidgetForeground, editorWidgetResizeBorder, errorForeground, focusBorder, inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground, inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, inputValidationInfoBackground, inputValidationInfoBorder, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningBorder, inputValidationWarningForeground, toolbarHoverBackground, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; +import { asCssValue, contrastBorder, editorFindMatch, editorFindMatchBorder, editorFindMatchHighlight, editorFindMatchHighlightBorder, editorFindRangeHighlight, editorFindRangeHighlightBorder, editorWidgetBackground, editorWidgetBorder, editorWidgetForeground, editorWidgetResizeBorder, errorForeground, focusBorder, inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground, toolbarHoverBackground, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; import { registerIcon, widgetClose } from 'vs/platform/theme/common/iconRegistry'; -import { IColorTheme, IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { isHighContrast } from 'vs/platform/theme/common/theme'; import { assertIsDefined } from 'vs/base/common/types'; +import { defaultInputBoxStyles, defaultToggleStyles } from 'vs/platform/theme/browser/defaultStyles'; const findSelectionIcon = registerIcon('find-selection', Codicon.selection, nls.localize('findSelectionIcon', 'Icon for \'Find in Selection\' in the editor find widget.')); const findCollapsedIcon = registerIcon('find-collapsed', Codicon.chevronRight, nls.localize('findCollapsedIcon', 'Icon to indicate that the editor find widget is collapsed.')); @@ -258,9 +259,6 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL this._viewZone = new FindWidgetViewZone(0); // Put it before the first line then users can scroll beyond the first line. } - this._applyTheme(themeService.getColorTheme()); - this._register(themeService.onDidColorThemeChange(this._applyTheme.bind(this))); - this._register(this._codeEditor.onDidChangeModel(() => { if (!this._isVisible) { return; @@ -675,29 +673,6 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL }); } - private _applyTheme(theme: IColorTheme) { - const inputStyles: IFindInputStyles = { - inputActiveOptionBorder: theme.getColor(inputActiveOptionBorder), - inputActiveOptionBackground: theme.getColor(inputActiveOptionBackground), - inputActiveOptionForeground: theme.getColor(inputActiveOptionForeground), - inputBackground: theme.getColor(inputBackground), - inputForeground: theme.getColor(inputForeground), - inputBorder: theme.getColor(inputBorder), - inputValidationInfoBackground: theme.getColor(inputValidationInfoBackground), - inputValidationInfoForeground: theme.getColor(inputValidationInfoForeground), - inputValidationInfoBorder: theme.getColor(inputValidationInfoBorder), - inputValidationWarningBackground: theme.getColor(inputValidationWarningBackground), - inputValidationWarningForeground: theme.getColor(inputValidationWarningForeground), - inputValidationWarningBorder: theme.getColor(inputValidationWarningBorder), - inputValidationErrorBackground: theme.getColor(inputValidationErrorBackground), - inputValidationErrorForeground: theme.getColor(inputValidationErrorForeground), - inputValidationErrorBorder: theme.getColor(inputValidationErrorBorder), - }; - this._findInput.style(inputStyles); - this._replaceInput.style(inputStyles); - this._toggleSelectionFind.style(inputStyles); - } - private _tryUpdateWidgetWidth() { if (!this._isVisible) { return; @@ -981,7 +956,9 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL flexibleWidth, flexibleMaxHeight: 118, showCommonFindToggles: true, - showHistoryHint: () => showHistoryKeybindingHint(this._keybindingService) + showHistoryHint: () => showHistoryKeybindingHint(this._keybindingService), + inputBoxStyles: defaultInputBoxStyles, + toggleStyles: defaultToggleStyles }, this._contextKeyService)); this._findInput.setRegex(!!this._state.isRegex); this._findInput.setCaseSensitive(!!this._state.matchCase); @@ -1061,7 +1038,10 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL this._toggleSelectionFind = this._register(new Toggle({ icon: findSelectionIcon, title: NLS_TOGGLE_SELECTION_FIND_TITLE + this._keybindingLabelFor(FIND_IDS.ToggleSearchScopeCommand), - isChecked: false + isChecked: false, + inputActiveOptionBackground: asCssValue(inputActiveOptionBackground), + inputActiveOptionBorder: asCssValue(inputActiveOptionBorder), + inputActiveOptionForeground: asCssValue(inputActiveOptionForeground), })); this._register(this._toggleSelectionFind.onChange(() => { @@ -1121,7 +1101,9 @@ export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashL flexibleHeight, flexibleWidth, flexibleMaxHeight: 118, - showHistoryHint: () => showHistoryKeybindingHint(this._keybindingService) + showHistoryHint: () => showHistoryKeybindingHint(this._keybindingService), + inputBoxStyles: defaultInputBoxStyles, + toggleStyles: defaultToggleStyles }, this._contextKeyService, true)); this._replaceInput.setPreserveCase(!!this._state.preserveCase); this._register(this._replaceInput.onKeyDown((e) => this._onReplaceInputKeyDown(e))); diff --git a/src/vs/editor/contrib/linkedEditing/browser/linkedEditing.css b/src/vs/editor/contrib/linkedEditing/browser/linkedEditing.css new file mode 100644 index 00000000000..5e2ecdb5400 --- /dev/null +++ b/src/vs/editor/contrib/linkedEditing/browser/linkedEditing.css @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-editor .linked-editing-decoration { + background-color: var(--vscode-editor-linkedEditingBackground); + border-left-color: var(--vscode-editor-linkedEditingBackground); +} diff --git a/src/vs/editor/contrib/linkedEditing/browser/linkedEditing.ts b/src/vs/editor/contrib/linkedEditing/browser/linkedEditing.ts index 4cfa30dac1c..c5e20edfe5f 100644 --- a/src/vs/editor/contrib/linkedEditing/browser/linkedEditing.ts +++ b/src/vs/editor/contrib/linkedEditing/browser/linkedEditing.ts @@ -28,13 +28,13 @@ import { ILanguageConfigurationService } from 'vs/editor/common/languages/langua import * as nls from 'vs/nls'; import { ContextKeyExpr, IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { registerColor } from 'vs/platform/theme/common/colorRegistry'; -import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; +import { registerColor } from 'vs/platform/theme/common/colorRegistry'; import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { IFeatureDebounceInformation, ILanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; import { StopWatch } from 'vs/base/common/stopwatch'; +import 'vs/css!./linkedEditing'; export const CONTEXT_ONTYPE_RENAME_INPUT_VISIBLE = new RawContextKey('LinkedEditingInputVisible', false); @@ -461,12 +461,6 @@ function getLinkedEditingRanges(providers: LanguageFeatureRegistry { - const editorLinkedEditingBackgroundColor = theme.getColor(editorLinkedEditingBackground); - if (editorLinkedEditingBackgroundColor) { - collector.addRule(`.monaco-editor .${DECORATION_CLASS_NAME} { background: ${editorLinkedEditingBackgroundColor}; border-left-color: ${editorLinkedEditingBackgroundColor}; }`); - } -}); registerModelAndPositionCommand('_executeLinkedEditingProvider', (_accessor, model, position) => { const { linkedEditingRangeProvider } = _accessor.get(ILanguageFeaturesService); diff --git a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts index 8e9a237c5b3..9f15f355956 100644 --- a/src/vs/editor/standalone/browser/standaloneCodeEditor.ts +++ b/src/vs/editor/standalone/browser/standaloneCodeEditor.ts @@ -495,7 +495,7 @@ export class StandaloneDiffEditor extends DiffEditorWidget implements IStandalon @IConfigurationService configurationService: IConfigurationService, @IContextMenuService contextMenuService: IContextMenuService, @IEditorProgressService editorProgressService: IEditorProgressService, - @IClipboardService clipboardService: IClipboardService, + @IClipboardService clipboardService: IClipboardService ) { const options = { ..._options }; updateConfigurationService(configurationService, options, true); diff --git a/src/vs/platform/actionWidget/browser/actionList.ts b/src/vs/platform/actionWidget/browser/actionList.ts index 5dacc07d2f7..f040e6c997f 100644 --- a/src/vs/platform/actionWidget/browser/actionList.ts +++ b/src/vs/platform/actionWidget/browser/actionList.ts @@ -127,7 +127,7 @@ class ActionItemRenderer> implements IListR if (element.disabled) { data.container.title = element.label; } else if (actionTitle && previewTitle) { - data.container.title = localize({ key: 'label', comment: ['placeholders are keybindings, e.g "F2 to Apply, Shift+F2 to Preview"'] }, "{0} to Apply, {1} to Preview", actionTitle, previewTitle); + data.container.title = localize({ key: 'label', comment: ['placeholders are keybindings, e.g "F2 to apply, Shift+F2 to preview"'] }, "{0} to apply, {1} to preview", actionTitle, previewTitle); } else { data.container.title = ''; } diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 53460c60090..762b9754744 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -103,6 +103,7 @@ export class MenuId { static readonly SCMSourceControl = new MenuId('SCMSourceControl'); static readonly SCMTitle = new MenuId('SCMTitle'); static readonly SearchContext = new MenuId('SearchContext'); + static readonly SearchActionMenu = new MenuId('SearchActionContext'); static readonly StatusBarWindowIndicatorMenu = new MenuId('StatusBarWindowIndicatorMenu'); static readonly StatusBarRemoteIndicatorMenu = new MenuId('StatusBarRemoteIndicatorMenu'); static readonly StickyScrollContext = new MenuId('StickyScrollContext'); @@ -127,6 +128,7 @@ export class MenuId { static readonly ViewTitleContext = new MenuId('ViewTitleContext'); static readonly CommentThreadTitle = new MenuId('CommentThreadTitle'); static readonly CommentThreadActions = new MenuId('CommentThreadActions'); + static readonly CommentThreadAdditionalActions = new MenuId('CommentThreadAdditionalActions'); static readonly CommentThreadTitleContext = new MenuId('CommentThreadTitleContext'); static readonly CommentThreadCommentContext = new MenuId('CommentThreadCommentContext'); static readonly CommentTitle = new MenuId('CommentTitle'); diff --git a/src/vs/workbench/contrib/audioCues/browser/audioCueService.ts b/src/vs/platform/audioCues/browser/audioCueService.ts similarity index 86% rename from src/vs/workbench/contrib/audioCues/browser/audioCueService.ts rename to src/vs/platform/audioCues/browser/audioCueService.ts index 6aeb6d66f25..3508c387929 100644 --- a/src/vs/workbench/contrib/audioCues/browser/audioCueService.ts +++ b/src/vs/platform/audioCues/browser/audioCueService.ts @@ -17,7 +17,7 @@ export const IAudioCueService = createDecorator('audioCue'); export interface IAudioCueService { readonly _serviceBrand: undefined; - playAudioCue(cue: AudioCue): Promise; + playAudioCue(cue: AudioCue, allowManyInParallel?: boolean): Promise; playAudioCues(cues: AudioCue[]): Promise; isEnabled(cue: AudioCue): IObservable; @@ -39,9 +39,9 @@ export class AudioCueService extends Disposable implements IAudioCueService { super(); } - public async playAudioCue(cue: AudioCue): Promise { + public async playAudioCue(cue: AudioCue, allowManyInParallel = false): Promise { if (this.isEnabled(cue).get()) { - await this.playSound(cue.sound); + await this.playSound(cue.sound, allowManyInParallel); } } @@ -70,7 +70,7 @@ export class AudioCueService extends Disposable implements IAudioCueService { this.playingSounds.add(sound); const url = FileAccess.asBrowserUri( - `vs/workbench/contrib/audioCues/browser/media/${sound.fileName}` + `vs/platform/audioCues/browser/media/${sound.fileName}` ).toString(); const audio = new Audio(url); audio.volume = this.getVolumeInPercent() / 100; @@ -164,6 +164,8 @@ export class Sound { public static readonly taskCompleted = Sound.register({ fileName: 'taskCompleted.mp3' }); public static readonly taskFailed = Sound.register({ fileName: 'taskFailed.mp3' }); public static readonly terminalBell = Sound.register({ fileName: 'terminalBell.mp3' }); + public static readonly diffLineInserted = Sound.register({ fileName: 'diffLineInserted.mp3' }); + public static readonly diffLineDeleted = Sound.register({ fileName: 'diffLineDeleted.mp3' }); private constructor(public readonly fileName: string) { } } @@ -247,6 +249,30 @@ export class AudioCue { settingsKey: 'audioCues.terminalBell' }); + public static readonly notebookCellCompleted = AudioCue.register({ + name: localize('audioCues.notebookCellCompleted', 'Notebook Cell Completed'), + sound: Sound.taskCompleted, + settingsKey: 'audioCues.notebookCellCompleted' + }); + + public static readonly notebookCellFailed = AudioCue.register({ + name: localize('audioCues.notebookCellFailed', 'Notebook Cell Failed'), + sound: Sound.taskFailed, + settingsKey: 'audioCues.notebookCellFailed' + }); + + public static readonly diffLineInserted = AudioCue.register({ + name: localize('audioCues.diffLineInserted', 'Diff Line Inserted'), + sound: Sound.diffLineInserted, + settingsKey: 'audioCues.diffLineInserted' + }); + + public static readonly diffLineDeleted = AudioCue.register({ + name: localize('audioCues.diffLineDeleted', 'Diff Line Deleted'), + sound: Sound.diffLineDeleted, + settingsKey: 'audioCues.diffLineDeleted' + }); + private constructor( public readonly sound: Sound, public readonly name: string, diff --git a/src/vs/workbench/contrib/audioCues/browser/media/break.mp3 b/src/vs/platform/audioCues/browser/media/break.mp3 similarity index 100% rename from src/vs/workbench/contrib/audioCues/browser/media/break.mp3 rename to src/vs/platform/audioCues/browser/media/break.mp3 diff --git a/src/vs/platform/audioCues/browser/media/diffLineDeleted.mp3 b/src/vs/platform/audioCues/browser/media/diffLineDeleted.mp3 new file mode 100644 index 00000000000..fc7ec846611 Binary files /dev/null and b/src/vs/platform/audioCues/browser/media/diffLineDeleted.mp3 differ diff --git a/src/vs/platform/audioCues/browser/media/diffLineInserted.mp3 b/src/vs/platform/audioCues/browser/media/diffLineInserted.mp3 new file mode 100644 index 00000000000..5f3ede4ced7 Binary files /dev/null and b/src/vs/platform/audioCues/browser/media/diffLineInserted.mp3 differ diff --git a/src/vs/workbench/contrib/audioCues/browser/media/error.mp3 b/src/vs/platform/audioCues/browser/media/error.mp3 similarity index 100% rename from src/vs/workbench/contrib/audioCues/browser/media/error.mp3 rename to src/vs/platform/audioCues/browser/media/error.mp3 diff --git a/src/vs/workbench/contrib/audioCues/browser/media/foldedAreas.mp3 b/src/vs/platform/audioCues/browser/media/foldedAreas.mp3 similarity index 100% rename from src/vs/workbench/contrib/audioCues/browser/media/foldedAreas.mp3 rename to src/vs/platform/audioCues/browser/media/foldedAreas.mp3 diff --git a/src/vs/workbench/contrib/audioCues/browser/media/quickFixes.mp3 b/src/vs/platform/audioCues/browser/media/quickFixes.mp3 similarity index 100% rename from src/vs/workbench/contrib/audioCues/browser/media/quickFixes.mp3 rename to src/vs/platform/audioCues/browser/media/quickFixes.mp3 diff --git a/src/vs/workbench/contrib/audioCues/browser/media/taskCompleted.mp3 b/src/vs/platform/audioCues/browser/media/taskCompleted.mp3 similarity index 100% rename from src/vs/workbench/contrib/audioCues/browser/media/taskCompleted.mp3 rename to src/vs/platform/audioCues/browser/media/taskCompleted.mp3 diff --git a/src/vs/workbench/contrib/audioCues/browser/media/taskFailed.mp3 b/src/vs/platform/audioCues/browser/media/taskFailed.mp3 similarity index 100% rename from src/vs/workbench/contrib/audioCues/browser/media/taskFailed.mp3 rename to src/vs/platform/audioCues/browser/media/taskFailed.mp3 diff --git a/src/vs/workbench/contrib/audioCues/browser/media/terminalBell.mp3 b/src/vs/platform/audioCues/browser/media/terminalBell.mp3 similarity index 100% rename from src/vs/workbench/contrib/audioCues/browser/media/terminalBell.mp3 rename to src/vs/platform/audioCues/browser/media/terminalBell.mp3 diff --git a/src/vs/workbench/contrib/audioCues/browser/media/warning.mp3 b/src/vs/platform/audioCues/browser/media/warning.mp3 similarity index 100% rename from src/vs/workbench/contrib/audioCues/browser/media/warning.mp3 rename to src/vs/platform/audioCues/browser/media/warning.mp3 diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index 2ec49007e19..f41968d9b0e 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -69,9 +69,6 @@ export interface IEnvironmentService { editSessionId?: string; editSessionsLogResource: URI; - // remote tunnel - remoteTunnelLogResource: URI; - // --- extension development debugExtensionHost: IExtensionHostDebugParams; isExtensionDevelopment: boolean; diff --git a/src/vs/platform/environment/common/environmentService.ts b/src/vs/platform/environment/common/environmentService.ts index 1745ed81462..87401e16abe 100644 --- a/src/vs/platform/environment/common/environmentService.ts +++ b/src/vs/platform/environment/common/environmentService.ts @@ -85,9 +85,6 @@ export abstract class AbstractNativeEnvironmentService implements INativeEnviron @memoize get editSessionsLogResource(): URI { return URI.file(join(this.logsPath, 'editSessions.log')); } - @memoize - get remoteTunnelLogResource(): URI { return URI.file(join(this.logsPath, 'remoteTunnel.log')); } - @memoize get sync(): 'on' | 'off' | undefined { return this.args.sync; } diff --git a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts index 13dfbaa1a07..a333a32d3e6 100644 --- a/src/vs/platform/extensionManagement/common/extensionsScannerService.ts +++ b/src/vs/platform/extensionManagement/common/extensionsScannerService.ts @@ -36,6 +36,7 @@ import { IExtensionsProfileScannerService, IScannedProfileExtension } from 'vs/p import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls'; +import { ImplicitActivationEvents } from 'vs/platform/extensionManagement/common/implicitActivationEvents'; export type IScannedExtensionManifest = IRelaxedExtensionManifest & { __metadata?: Metadata }; @@ -577,6 +578,7 @@ class ExtensionsScanner extends Disposable { const identifier = metadata?.id ? { id, uuid: metadata.id } : { id }; const type = metadata?.isSystem ? ExtensionType.System : input.type; const isBuiltin = type === ExtensionType.System || !!metadata?.isBuiltin; + ImplicitActivationEvents.updateManifest(manifest); manifest = await this.translateManifest(input.location, manifest, ExtensionScannerInput.createNlsConfiguration(input)); const extension = { type, diff --git a/src/vs/platform/extensionManagement/common/implicitActivationEvents.ts b/src/vs/platform/extensionManagement/common/implicitActivationEvents.ts new file mode 100644 index 00000000000..8810abc6127 --- /dev/null +++ b/src/vs/platform/extensionManagement/common/implicitActivationEvents.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { onUnexpectedError } from 'vs/base/common/errors'; +import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; + +export interface IActivationEventsGenerator { + (contribution: T, result: { push(item: string): void }): void; +} + +export class ImplicitActivationEventsImpl { + + private readonly _generators = new Map>(); + + public register(extensionPointName: string, generator: IActivationEventsGenerator): void { + this._generators.set(extensionPointName, generator); + } + + public updateManifest(manifest: IExtensionManifest) { + if (!Array.isArray(manifest.activationEvents) || !manifest.contributes) { + return; + } + if (typeof manifest.main === 'undefined' && typeof manifest.browser === 'undefined') { + return; + } + + for (const extPointName in manifest.contributes) { + const generator = this._generators.get(extPointName); + if (!generator) { + // There's no generator for this extension point + continue; + } + const contrib = (manifest.contributes as any)[extPointName]; + const contribArr = Array.isArray(contrib) ? contrib : [contrib]; + try { + generator(contribArr, manifest.activationEvents); + } catch (err) { + onUnexpectedError(err); + } + } + } +} + +export const ImplicitActivationEvents: ImplicitActivationEventsImpl = new ImplicitActivationEventsImpl(); diff --git a/src/vs/platform/files/common/inMemoryFilesystemProvider.ts b/src/vs/platform/files/common/inMemoryFilesystemProvider.ts index 5ee8e5366b6..615948123ad 100644 --- a/src/vs/platform/files/common/inMemoryFilesystemProvider.ts +++ b/src/vs/platform/files/common/inMemoryFilesystemProvider.ts @@ -52,10 +52,20 @@ export type Entry = File | Directory; export class InMemoryFileSystemProvider extends Disposable implements IFileSystemProviderWithFileReadWriteCapability { - readonly capabilities: FileSystemProviderCapabilities = - FileSystemProviderCapabilities.FileReadWrite - | FileSystemProviderCapabilities.PathCaseSensitive; - readonly onDidChangeCapabilities: Event = Event.None; + private _onDidChangeCapabilities = this._register(new Emitter()); + readonly onDidChangeCapabilities = this._onDidChangeCapabilities.event; + + private _capabilities = FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.PathCaseSensitive; + get capabilities(): FileSystemProviderCapabilities { return this._capabilities; } + + setReadOnly(readonly: boolean) { + const isReadonly = !!(this._capabilities & FileSystemProviderCapabilities.Readonly); + if (readonly !== isReadonly) { + this._capabilities = readonly ? FileSystemProviderCapabilities.Readonly | FileSystemProviderCapabilities.PathCaseSensitive | FileSystemProviderCapabilities.FileReadWrite + : FileSystemProviderCapabilities.FileReadWrite | FileSystemProviderCapabilities.PathCaseSensitive; + this._onDidChangeCapabilities.fire(); + } + } root = new Directory(''); diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index 180b544463f..95467136e67 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -27,6 +27,7 @@ import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { createDecorator, IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { Registry } from 'vs/platform/registry/common/platform'; +import { defaultFindWidgetStyles } from 'vs/platform/theme/browser/defaultStyles'; import { attachListStyler, computeStyles, defaultListStyles, IColorMapping } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -1123,7 +1124,8 @@ function workbenchTreeDataPreamble(treeExpandMode) === 'doubleClick'), - contextViewProvider: contextViewService as IContextViewProvider + contextViewProvider: contextViewService as IContextViewProvider, + findWidgetStyles: defaultFindWidgetStyles } as TOptions }; } diff --git a/src/vs/platform/opener/browser/link.css b/src/vs/platform/opener/browser/link.css new file mode 100644 index 00000000000..f55549478e8 --- /dev/null +++ b/src/vs/platform/opener/browser/link.css @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-link { + color: var(--vscode-textLink-foreground); +} + +.monaco-link:hover { + color: var(--vscode-textLink-activeForeground); +} + diff --git a/src/vs/platform/opener/browser/link.ts b/src/vs/platform/opener/browser/link.ts index 86ad9f5f3ba..85d2bc75a7d 100644 --- a/src/vs/platform/opener/browser/link.ts +++ b/src/vs/platform/opener/browser/link.ts @@ -11,8 +11,6 @@ import { Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable } from 'vs/base/common/lifecycle'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; -import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; export interface ILinkDescriptor { readonly label: string | HTMLElement; @@ -119,15 +117,3 @@ export class Link extends Disposable { this.enabled = true; } } - -registerThemingParticipant((theme, collector) => { - const textLinkForegroundColor = theme.getColor(textLinkForeground); - if (textLinkForegroundColor) { - collector.addRule(`.monaco-link { color: ${textLinkForegroundColor}; }`); - } - - const textLinkActiveForegroundColor = theme.getColor(textLinkActiveForeground); - if (textLinkActiveForegroundColor) { - collector.addRule(`.monaco-link:hover { color: ${textLinkActiveForegroundColor}; }`); - } -}); diff --git a/src/vs/platform/policy/node/nativePolicyService.ts b/src/vs/platform/policy/node/nativePolicyService.ts index 39e2f126cfe..7b78806548f 100644 --- a/src/vs/platform/policy/node/nativePolicyService.ts +++ b/src/vs/platform/policy/node/nativePolicyService.ts @@ -23,7 +23,7 @@ export class NativePolicyService extends AbstractPolicyService implements IPolic } protected async _updatePolicyDefinitions(policyDefinitions: IStringDictionary): Promise { - this.logService.trace(`NativePolicyService#_updatePolicyDefinitions - Found ${policyDefinitions.length} policy definitions`); + this.logService.trace(`NativePolicyService#_updatePolicyDefinitions - Found ${Object.keys(policyDefinitions).length} policy definitions`); await this.throttler.queue(() => new Promise((c, e) => { try { @@ -39,7 +39,7 @@ export class NativePolicyService extends AbstractPolicyService implements IPolic } private _onDidPolicyChange(update: PolicyUpdate>): void { - this.logService.trace(`NativePolicyService#_onDidPolicyChange - Updated policy values: ${Object.keys(update).join(', ')}`); + this.logService.trace(`NativePolicyService#_onDidPolicyChange - Updated policy values: ${JSON.stringify(update)}`); for (const key in update) { const value = update[key] as any; diff --git a/src/vs/platform/quickinput/browser/quickInput.ts b/src/vs/platform/quickinput/browser/quickInput.ts index 6210a38dad6..372a4a4e971 100644 --- a/src/vs/platform/quickinput/browser/quickInput.ts +++ b/src/vs/platform/quickinput/browser/quickInput.ts @@ -16,8 +16,8 @@ import { IWorkbenchListOptions, WorkbenchList } from 'vs/platform/list/browser/l import { QuickAccessController } from 'vs/platform/quickinput/browser/quickAccess'; import { IQuickAccessController } from 'vs/platform/quickinput/common/quickAccess'; import { IInputBox, IInputOptions, IKeyMods, IPickOptions, IQuickInputButton, IQuickInputService, IQuickNavigateConfiguration, IQuickPick, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; -import { defaultButtonStyles, getProgressBarStyles } from 'vs/platform/theme/browser/defaultStyles'; -import { activeContrastBorder, badgeBackground, badgeForeground, contrastBorder, inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, inputValidationInfoBackground, inputValidationInfoBorder, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningBorder, inputValidationWarningForeground, keybindingLabelBackground, keybindingLabelBorder, keybindingLabelBottomBorder, keybindingLabelForeground, pickerGroupBorder, pickerGroupForeground, quickInputBackground, quickInputForeground, quickInputListFocusBackground, quickInputListFocusForeground, quickInputListFocusIconForeground, quickInputTitleBackground, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; +import { defaultButtonStyles, defaultInputBoxStyles, defaultKeybindingLabelStyles, defaultProgressBarStyles, defaultToggleStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { activeContrastBorder, badgeBackground, badgeForeground, contrastBorder, pickerGroupBorder, pickerGroupForeground, quickInputBackground, quickInputForeground, quickInputListFocusBackground, quickInputListFocusForeground, quickInputListFocusIconForeground, quickInputTitleBackground, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; import { computeStyles } from 'vs/platform/theme/common/styler'; import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; @@ -194,34 +194,16 @@ export class QuickInputService extends Themable implements IQuickInputService { widgetShadow }), }, - inputBox: computeStyles(this.theme, { - inputForeground, - inputBackground, - inputBorder, - inputValidationInfoBackground, - inputValidationInfoForeground, - inputValidationInfoBorder, - inputValidationWarningBackground, - inputValidationWarningForeground, - inputValidationWarningBorder, - inputValidationErrorBackground, - inputValidationErrorForeground, - inputValidationErrorBorder - }), + inputBox: defaultInputBoxStyles, + toggle: defaultToggleStyles, countBadge: computeStyles(this.theme, { badgeBackground, badgeForeground, badgeBorder: contrastBorder }), button: defaultButtonStyles, - progressBar: getProgressBarStyles(), // default uses progressBarBackground - keybindingLabel: computeStyles(this.theme, { - keybindingLabelBackground, - keybindingLabelForeground, - keybindingLabelBorder, - keybindingLabelBottomBorder, - keybindingLabelShadow: widgetShadow - }), + progressBar: defaultProgressBarStyles, // default uses progressBarBackground + keybindingLabel: defaultKeybindingLabelStyles, list: computeStyles(this.theme, { listBackground: quickInputBackground, // Look like focused when inactive. diff --git a/src/vs/platform/remote/common/remoteAuthorityResolver.ts b/src/vs/platform/remote/common/remoteAuthorityResolver.ts index 87ec7fead5e..607f53c8c9e 100644 --- a/src/vs/platform/remote/common/remoteAuthorityResolver.ts +++ b/src/vs/platform/remote/common/remoteAuthorityResolver.ts @@ -97,9 +97,7 @@ export class RemoteAuthorityResolverError extends ErrorNoTelemetry { // workaround when extending builtin objects and when compiling to ES5, see: // https://github.com/microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work - if (typeof (Object).setPrototypeOf === 'function') { - (Object).setPrototypeOf(this, RemoteAuthorityResolverError.prototype); - } + Object.setPrototypeOf(this, RemoteAuthorityResolverError.prototype); } } diff --git a/src/vs/platform/remoteTunnel/common/remoteTunnel.ts b/src/vs/platform/remoteTunnel/common/remoteTunnel.ts index 955572b5314..f92e5a0f563 100644 --- a/src/vs/platform/remoteTunnel/common/remoteTunnel.ts +++ b/src/vs/platform/remoteTunnel/common/remoteTunnel.ts @@ -6,7 +6,6 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Event } from 'vs/base/common/event'; - export interface IRemoteTunnelAccount { readonly authenticationProviderId: string; readonly token: string; @@ -61,3 +60,7 @@ export interface ConnectionInfo { export const CONFIGURATION_KEY_PREFIX = 'remote.tunnels.access'; export const CONFIGURATION_KEY_HOST_NAME = CONFIGURATION_KEY_PREFIX + '.hostNameOverride'; + +export const LOG_FILE_NAME = 'remoteTunnelService.log'; +export const LOGGER_NAME = 'remoteTunnelService'; +export const LOG_CHANNEL_ID = 'remoteTunnelServiceLog'; diff --git a/src/vs/platform/remoteTunnel/electron-browser/remoteTunnelService.ts b/src/vs/platform/remoteTunnel/electron-browser/remoteTunnelService.ts index d4a1c1360ff..20b8ec756ce 100644 --- a/src/vs/platform/remoteTunnel/electron-browser/remoteTunnelService.ts +++ b/src/vs/platform/remoteTunnel/electron-browser/remoteTunnelService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CONFIGURATION_KEY_HOST_NAME, ConnectionInfo, IRemoteTunnelAccount, IRemoteTunnelService, TunnelStates, TunnelStatus } from 'vs/platform/remoteTunnel/common/remoteTunnel'; +import { CONFIGURATION_KEY_HOST_NAME, ConnectionInfo, IRemoteTunnelAccount, IRemoteTunnelService, LOGGER_NAME, LOG_FILE_NAME, TunnelStates, TunnelStatus } from 'vs/platform/remoteTunnel/common/remoteTunnel'; import { Emitter } from 'vs/base/common/event'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { INativeEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -18,6 +18,7 @@ import { ISharedProcessLifecycleService } from 'vs/platform/lifecycle/electron-b import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { localize } from 'vs/nls'; import { hostname, homedir } from 'os'; +import { URI } from 'vs/base/common/uri'; type RemoteTunnelEnablementClassification = { owner: 'aeschli'; @@ -65,7 +66,8 @@ export class RemoteTunnelService extends Disposable implements IRemoteTunnelServ @IConfigurationService private readonly configurationService: IConfigurationService ) { super(); - this._logger = this._register(loggerService.createLogger(environmentService.remoteTunnelLogResource, { name: 'remoteTunnel' })); + const remoteTunnelLogResource = URI.file(join(environmentService.logsPath, LOG_FILE_NAME)); + this._logger = this._register(loggerService.createLogger(remoteTunnelLogResource, { name: LOGGER_NAME })); this._startTunnelProcessDelayer = new Delayer(100); this._register(sharedProcessLifecycleService.onWillShutdown(e => { diff --git a/src/vs/platform/telemetry/common/serverTelemetryService.ts b/src/vs/platform/telemetry/common/serverTelemetryService.ts index a034f4d81c0..dd9fd5359ed 100644 --- a/src/vs/platform/telemetry/common/serverTelemetryService.ts +++ b/src/vs/platform/telemetry/common/serverTelemetryService.ts @@ -30,15 +30,15 @@ export class ServerTelemetryService extends TelemetryService implements IServerT this._injectedTelemetryLevel = injectedTelemetryLevel; } - override publicLog(eventName: string, data?: ITelemetryData, anonymizeFilePaths?: boolean): Promise { + override publicLog(eventName: string, data?: ITelemetryData): Promise { if (this._injectedTelemetryLevel < TelemetryLevel.USAGE) { return Promise.resolve(undefined); } - return super.publicLog(eventName, data, anonymizeFilePaths); + return super.publicLog(eventName, data); } - override publicLog2> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck, anonymizeFilePaths?: boolean): Promise { - return this.publicLog(eventName, data as ITelemetryData | undefined, anonymizeFilePaths); + override publicLog2> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck): Promise { + return this.publicLog(eventName, data as ITelemetryData | undefined); } override publicLogError(errorEventName: string, data?: ITelemetryData): Promise { diff --git a/src/vs/platform/telemetry/common/telemetry.ts b/src/vs/platform/telemetry/common/telemetry.ts index f465690a677..e2817ecb35d 100644 --- a/src/vs/platform/telemetry/common/telemetry.ts +++ b/src/vs/platform/telemetry/common/telemetry.ts @@ -34,13 +34,13 @@ export interface ITelemetryService { /** * @deprecated Use publicLog2 and the typescript GDPR annotation where possible */ - publicLog(eventName: string, data?: ITelemetryData, anonymizeFilePaths?: boolean): Promise; + publicLog(eventName: string, data?: ITelemetryData): Promise; /** * Sends a telemetry event that has been privacy approved. * Do not call this unless you have been given approval. */ - publicLog2> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck, anonymizeFilePaths?: boolean): Promise; + publicLog2> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck): Promise; /** * @deprecated Use publicLogError2 and the typescript GDPR annotation where possible diff --git a/src/vs/platform/telemetry/common/telemetryService.ts b/src/vs/platform/telemetry/common/telemetryService.ts index fef48e29682..a6668775118 100644 --- a/src/vs/platform/telemetry/common/telemetryService.ts +++ b/src/vs/platform/telemetry/common/telemetryService.ts @@ -58,6 +58,10 @@ export class TelemetryService implements ITelemetryService { for (const piiPath of this._piiPaths) { this._cleanupPatterns.push(new RegExp(escapeRegExpCharacters(piiPath), 'gi')); + + if (piiPath.indexOf('\\') >= 0) { + this._cleanupPatterns.push(new RegExp(escapeRegExpCharacters(piiPath.replace(/\\/g, '/')), 'gi')); + } } this._updateTelemetryLevel(); @@ -102,7 +106,7 @@ export class TelemetryService implements ITelemetryService { this._disposables.dispose(); } - private _log(eventName: string, eventLevel: TelemetryLevel, data?: ITelemetryData, anonymizeFilePaths?: boolean): Promise { + private _log(eventName: string, eventLevel: TelemetryLevel, data?: ITelemetryData): Promise { // don't send events when the user is optout if (this.telemetryLevel.value < eventLevel) { return Promise.resolve(undefined); @@ -128,12 +132,12 @@ export class TelemetryService implements ITelemetryService { }); } - publicLog(eventName: string, data?: ITelemetryData, anonymizeFilePaths?: boolean): Promise { - return this._log(eventName, TelemetryLevel.USAGE, data, anonymizeFilePaths); + publicLog(eventName: string, data?: ITelemetryData): Promise { + return this._log(eventName, TelemetryLevel.USAGE, data); } - publicLog2> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck, anonymizeFilePaths?: boolean): Promise { - return this.publicLog(eventName, data as ITelemetryData, anonymizeFilePaths); + publicLog2> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck): Promise { + return this.publicLog(eventName, data as ITelemetryData); } publicLogError(errorEventName: string, data?: ITelemetryData): Promise { @@ -142,7 +146,7 @@ export class TelemetryService implements ITelemetryService { } // Send error event and anonymize paths - return this._log(errorEventName, TelemetryLevel.ERROR, data, true); + return this._log(errorEventName, TelemetryLevel.ERROR, data); } publicLogError2> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck): Promise { diff --git a/src/vs/platform/telemetry/common/telemetryUtils.ts b/src/vs/platform/telemetry/common/telemetryUtils.ts index 1b02638b6a5..88f56ebe955 100644 --- a/src/vs/platform/telemetry/common/telemetryUtils.ts +++ b/src/vs/platform/telemetry/common/telemetryUtils.ts @@ -15,6 +15,14 @@ import { verifyMicrosoftInternalDomain } from 'vs/platform/telemetry/common/comm import { ClassifiedEvent, IGDPRProperty, OmitMetadata, StrictPropertyCheck } from 'vs/platform/telemetry/common/gdprTypings'; import { ICustomEndpointTelemetryService, ITelemetryData, ITelemetryEndpoint, ITelemetryInfo, ITelemetryService, TelemetryConfiguration, TelemetryLevel, TELEMETRY_OLD_SETTING_ID, TELEMETRY_SETTING_ID } from 'vs/platform/telemetry/common/telemetry'; +/** + * A special class used to denoate a telemetry value which should not be clean. + * This is because that value is "Trusted" not to contain identifiable information such as paths + */ +export class TrustedTelemetryValue { + constructor(public readonly value: any) { } +} + export class NullTelemetryServiceShape implements ITelemetryService { declare readonly _serviceBrand: undefined; readonly sendErrorTelemetry = false; @@ -399,6 +407,12 @@ function removePropertiesWithPossibleUserInfo(property: string): string { */ export function cleanData(data: Record, cleanUpPatterns: RegExp[]): Record { return cloneAndChange(data, value => { + + // If it's a trusted value it means it's okay to skip cleaning so we don't clean it + if (value instanceof TrustedTelemetryValue) { + return value.value; + } + // We only know how to clean strings if (typeof value === 'string') { let updatedProperty = value; diff --git a/src/vs/platform/telemetry/test/browser/telemetryService.test.ts b/src/vs/platform/telemetry/test/browser/telemetryService.test.ts index 501ea625958..26f261d253e 100644 --- a/src/vs/platform/telemetry/test/browser/telemetryService.test.ts +++ b/src/vs/platform/telemetry/test/browser/telemetryService.test.ts @@ -224,8 +224,8 @@ suite('TelemetryService', () => { return Promise.all(this.promises); } - override publicLog(eventName: string, data?: ITelemetryData, anonymizeFilePaths?: boolean): Promise { - const p = super.publicLog(eventName, data, anonymizeFilePaths); + override publicLog(eventName: string, data?: ITelemetryData): Promise { + const p = super.publicLog(eventName, data); // publicLog is called from the ctor and therefore promises can be undefined this.promises = this.promises ?? []; this.promises.push(p); diff --git a/src/vs/platform/theme/browser/defaultStyles.ts b/src/vs/platform/theme/browser/defaultStyles.ts index 5220a7c0263..c3e30709514 100644 --- a/src/vs/platform/theme/browser/defaultStyles.ts +++ b/src/vs/platform/theme/browser/defaultStyles.ts @@ -4,62 +4,112 @@ *--------------------------------------------------------------------------------------------*/ import { IButtonStyles } from 'vs/base/browser/ui/button/button'; import { IKeybindingLabelStyles } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; -import { ColorIdentifier, keybindingLabelBackground, keybindingLabelBorder, keybindingLabelBottomBorder, keybindingLabelForeground, asCssValue, widgetShadow, buttonForeground, buttonSeparator, buttonBackground, buttonHoverBackground, buttonSecondaryForeground, buttonSecondaryBackground, buttonSecondaryHoverBackground, buttonBorder, progressBarBackground } from 'vs/platform/theme/common/colorRegistry'; +import { ColorIdentifier, keybindingLabelBackground, keybindingLabelBorder, keybindingLabelBottomBorder, keybindingLabelForeground, asCssValue, widgetShadow, buttonForeground, buttonSeparator, buttonBackground, buttonHoverBackground, buttonSecondaryForeground, buttonSecondaryBackground, buttonSecondaryHoverBackground, buttonBorder, progressBarBackground, inputActiveOptionBorder, inputActiveOptionForeground, inputActiveOptionBackground, editorWidgetBackground, editorWidgetForeground, contrastBorder, checkboxBorder, checkboxBackground, checkboxForeground, problemsErrorIconForeground, problemsWarningIconForeground, problemsInfoIconForeground, inputBackground, inputForeground, inputBorder, textLinkForeground, inputValidationInfoBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationWarningBorder, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationErrorBorder, inputValidationErrorBackground, inputValidationErrorForeground, listFilterWidgetBackground, listFilterWidgetNoMatchesOutline, listFilterWidgetOutline, listFilterWidgetShadow } from 'vs/platform/theme/common/colorRegistry'; import { IProgressBarStyles } from 'vs/base/browser/ui/progressbar/progressbar'; -import { IStyleOverrides } from 'vs/platform/theme/common/styler'; +import { ICheckboxStyles, IToggleStyles } from 'vs/base/browser/ui/toggle/toggle'; +import { IDialogStyles } from 'vs/base/browser/ui/dialog/dialog'; +import { IInputBoxStyles } from 'vs/base/browser/ui/inputbox/inputBox'; +import { IFindWidgetStyles } from 'vs/base/browser/ui/tree/abstractTree'; +export type IStyleOverride = { + [P in keyof T]?: ColorIdentifier; +}; -export interface IKeybindingLabelStyleOverrides extends IStyleOverrides { - keybindingLabelBackground?: ColorIdentifier; - keybindingLabelForeground?: ColorIdentifier; - keybindingLabelBorder?: ColorIdentifier; - keybindingLabelBottomBorder?: ColorIdentifier; - keybindingLabelShadow?: ColorIdentifier; -} +export const defaultKeybindingLabelStyles = getKeybindingLabelStyles({}); -export function getKeybindingLabelStyles(style?: IKeybindingLabelStyleOverrides): IKeybindingLabelStyles { +export function getKeybindingLabelStyles(override: IStyleOverride): IKeybindingLabelStyles { return { - keybindingLabelBackground: asCssValue(style?.keybindingLabelBackground || keybindingLabelBackground), - keybindingLabelForeground: asCssValue(style?.keybindingLabelForeground || keybindingLabelForeground), - keybindingLabelBorder: asCssValue(style?.keybindingLabelBorder || keybindingLabelBorder), - keybindingLabelBottomBorder: asCssValue(style?.keybindingLabelBottomBorder || keybindingLabelBottomBorder), - keybindingLabelShadow: asCssValue(style?.keybindingLabelShadow || widgetShadow) + keybindingLabelBackground: asCssValue(override.keybindingLabelBackground ?? keybindingLabelBackground), + keybindingLabelForeground: asCssValue(override.keybindingLabelForeground ?? keybindingLabelForeground), + keybindingLabelBorder: asCssValue(override.keybindingLabelBorder ?? keybindingLabelBorder), + keybindingLabelBottomBorder: asCssValue(override.keybindingLabelBottomBorder ?? keybindingLabelBottomBorder), + keybindingLabelShadow: asCssValue(override.keybindingLabelShadow ?? widgetShadow) }; } - -export interface IButtonStyleOverrides extends IStyleOverrides { - readonly buttonForeground?: ColorIdentifier; - readonly buttonSeparator?: ColorIdentifier; - readonly buttonBackground?: ColorIdentifier; - readonly buttonHoverBackground?: ColorIdentifier; - readonly buttonSecondaryForeground?: ColorIdentifier; - readonly buttonSecondaryBackground?: ColorIdentifier; - readonly buttonSecondaryHoverBackground?: ColorIdentifier; - readonly buttonBorder?: ColorIdentifier; -} - - export const defaultButtonStyles: IButtonStyles = getButtonStyles({}); -export function getButtonStyles(style: IButtonStyleOverrides): IButtonStyles { +export function getButtonStyles(override: IStyleOverride): IButtonStyles { return { - buttonForeground: asCssValue(style.buttonForeground || buttonForeground), - buttonSeparator: asCssValue(style.buttonSeparator || buttonSeparator), - buttonBackground: asCssValue(style.buttonBackground || buttonBackground), - buttonHoverBackground: asCssValue(style.buttonHoverBackground || buttonHoverBackground), - buttonSecondaryForeground: asCssValue(style.buttonSecondaryForeground || buttonSecondaryForeground), - buttonSecondaryBackground: asCssValue(style.buttonSecondaryBackground || buttonSecondaryBackground), - buttonSecondaryHoverBackground: asCssValue(style.buttonSecondaryHoverBackground || buttonSecondaryHoverBackground), - buttonBorder: asCssValue(style.buttonBorder || buttonBorder), + buttonForeground: asCssValue(override.buttonForeground ?? buttonForeground), + buttonSeparator: asCssValue(override.buttonSeparator ?? buttonSeparator), + buttonBackground: asCssValue(override.buttonBackground ?? buttonBackground), + buttonHoverBackground: asCssValue(override.buttonHoverBackground ?? buttonHoverBackground), + buttonSecondaryForeground: asCssValue(override.buttonSecondaryForeground ?? buttonSecondaryForeground), + buttonSecondaryBackground: asCssValue(override.buttonSecondaryBackground ?? buttonSecondaryBackground), + buttonSecondaryHoverBackground: asCssValue(override.buttonSecondaryHoverBackground ?? buttonSecondaryHoverBackground), + buttonBorder: asCssValue(override.buttonBorder ?? buttonBorder), }; } -export interface IProgressBarStyleOverrides extends IStyleOverrides { - progressBarBackground?: ColorIdentifier; -} +export const defaultProgressBarStyles: IProgressBarStyles = getProgressBarStyles({}); -export function getProgressBarStyles(style?: IProgressBarStyleOverrides): IProgressBarStyles { +export function getProgressBarStyles(override: IStyleOverride): IProgressBarStyles { return { - progressBarBackground: asCssValue(style?.progressBarBackground || progressBarBackground) + progressBarBackground: asCssValue(override.progressBarBackground ?? progressBarBackground) }; } + +export const defaultToggleStyles: IToggleStyles = getToggleStyles({}); + +export function getToggleStyles(override: IStyleOverride): IToggleStyles { + return { + inputActiveOptionBorder: asCssValue(override.inputActiveOptionBorder ?? inputActiveOptionBorder), + inputActiveOptionForeground: asCssValue(override.inputActiveOptionForeground ?? inputActiveOptionForeground), + inputActiveOptionBackground: asCssValue(override.inputActiveOptionBackground ?? inputActiveOptionBackground) + }; +} + +export const defaultCheckboxStyles: ICheckboxStyles = getCheckboxStyles({}); + +export function getCheckboxStyles(override: IStyleOverride): ICheckboxStyles { + return { + checkboxBackground: asCssValue(override.checkboxBackground ?? checkboxBackground), + checkboxBorder: asCssValue(override.checkboxBorder ?? checkboxBorder), + checkboxForeground: asCssValue(override.checkboxForeground ?? checkboxForeground) + }; +} + +export type IDialogStyleOverrides = IStyleOverride; + +export const defaultDialogStyles = getDialogStyle({}); + +export function getDialogStyle(override: IStyleOverride): IDialogStyles { + return { + dialogBackground: asCssValue(override.dialogBackground ?? editorWidgetBackground), + dialogForeground: asCssValue(override.dialogForeground ?? editorWidgetForeground), + dialogShadow: asCssValue(override.dialogShadow ?? widgetShadow), + dialogBorder: asCssValue(override.dialogBorder ?? contrastBorder), + errorIconForeground: asCssValue(override.errorIconForeground ?? problemsErrorIconForeground), + warningIconForeground: asCssValue(override.warningIconForeground ?? problemsWarningIconForeground), + infoIconForeground: asCssValue(override.infoIconForeground ?? problemsInfoIconForeground), + textLinkForeground: asCssValue(override.textLinkForeground ?? textLinkForeground) + }; +} + +export const defaultInputBoxStyles = getInputBoxStyle({}); + +export function getInputBoxStyle(override: IStyleOverride): IInputBoxStyles { + return { + inputBackground: asCssValue(override.inputBackground ?? inputBackground), + inputForeground: asCssValue(override.inputForeground ?? inputForeground), + inputBorder: asCssValue(override.inputBorder ?? inputBorder), + inputValidationInfoBorder: asCssValue(override.inputValidationInfoBorder ?? inputValidationInfoBorder), + inputValidationInfoBackground: asCssValue(override.inputValidationInfoBackground ?? inputValidationInfoBackground), + inputValidationInfoForeground: asCssValue(override.inputValidationInfoForeground ?? inputValidationInfoForeground), + inputValidationWarningBorder: asCssValue(override.inputValidationWarningBorder ?? inputValidationWarningBorder), + inputValidationWarningBackground: asCssValue(override.inputValidationWarningBackground ?? inputValidationWarningBackground), + inputValidationWarningForeground: asCssValue(override.inputValidationWarningForeground ?? inputValidationWarningForeground), + inputValidationErrorBorder: asCssValue(override.inputValidationErrorBorder ?? inputValidationErrorBorder), + inputValidationErrorBackground: asCssValue(override.inputValidationErrorBackground ?? inputValidationErrorBackground), + inputValidationErrorForeground: asCssValue(override.inputValidationErrorForeground ?? inputValidationErrorForeground) + }; +} + +export const defaultFindWidgetStyles: IFindWidgetStyles = { + listFilterWidgetBackground: asCssValue(listFilterWidgetBackground), + listFilterWidgetOutline: asCssValue(listFilterWidgetOutline), + listFilterWidgetNoMatchesOutline: asCssValue(listFilterWidgetNoMatchesOutline), + listFilterWidgetShadow: asCssValue(listFilterWidgetShadow), + inputBoxStyles: defaultInputBoxStyles, + toggleStyles: defaultToggleStyles +}; diff --git a/src/vs/platform/theme/common/colorRegistry.ts b/src/vs/platform/theme/common/colorRegistry.ts index e9ab3704a2f..203b79689b4 100644 --- a/src/vs/platform/theme/common/colorRegistry.ts +++ b/src/vs/platform/theme/common/colorRegistry.ts @@ -247,7 +247,7 @@ export const inputBorder = registerColor('input.border', { dark: null, light: nu export const inputActiveOptionBorder = registerColor('inputOption.activeBorder', { dark: '#007ACC00', light: '#007ACC00', hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('inputBoxActiveOptionBorder', "Border color of activated options in input fields.")); export const inputActiveOptionHoverBackground = registerColor('inputOption.hoverBackground', { dark: '#5a5d5e80', light: '#b8b8b850', hcDark: null, hcLight: null }, nls.localize('inputOption.hoverBackground', "Background color of activated options in input fields.")); export const inputActiveOptionBackground = registerColor('inputOption.activeBackground', { dark: transparent(focusBorder, 0.4), light: transparent(focusBorder, 0.2), hcDark: Color.transparent, hcLight: Color.transparent }, nls.localize('inputOption.activeBackground', "Background hover color of options in input fields.")); -export const inputActiveOptionForeground = registerColor('inputOption.activeForeground', { dark: Color.white, light: Color.black, hcDark: null, hcLight: foreground }, nls.localize('inputOption.activeForeground', "Foreground color of activated options in input fields.")); +export const inputActiveOptionForeground = registerColor('inputOption.activeForeground', { dark: Color.white, light: Color.black, hcDark: foreground, hcLight: foreground }, nls.localize('inputOption.activeForeground', "Foreground color of activated options in input fields.")); export const inputPlaceholderForeground = registerColor('input.placeholderForeground', { light: transparent(foreground, 0.5), dark: transparent(foreground, 0.5), hcDark: transparent(foreground, 0.7), hcLight: transparent(foreground, 0.7) }, nls.localize('inputPlaceholderForeground', "Input box foreground color for placeholder text.")); export const inputValidationInfoBackground = registerColor('inputValidation.infoBackground', { dark: '#063B49', light: '#D6ECF2', hcDark: Color.black, hcLight: Color.white }, nls.localize('inputValidationInfoBackground', "Input validation background color for information severity.")); @@ -268,7 +268,7 @@ export const selectBorder = registerColor('dropdown.border', { dark: selectBackg export const buttonForeground = registerColor('button.foreground', { dark: Color.white, light: Color.white, hcDark: Color.white, hcLight: Color.white }, nls.localize('buttonForeground', "Button foreground color.")); export const buttonSeparator = registerColor('button.separator', { dark: transparent(buttonForeground, .4), light: transparent(buttonForeground, .4), hcDark: transparent(buttonForeground, .4), hcLight: transparent(buttonForeground, .4) }, nls.localize('buttonSeparator', "Button separator color.")); export const buttonBackground = registerColor('button.background', { dark: '#0E639C', light: '#007ACC', hcDark: null, hcLight: '#0F4A85' }, nls.localize('buttonBackground', "Button background color.")); -export const buttonHoverBackground = registerColor('button.hoverBackground', { dark: lighten(buttonBackground, 0.2), light: darken(buttonBackground, 0.2), hcDark: null, hcLight: null }, nls.localize('buttonHoverBackground', "Button background color when hovering.")); +export const buttonHoverBackground = registerColor('button.hoverBackground', { dark: lighten(buttonBackground, 0.2), light: darken(buttonBackground, 0.2), hcDark: buttonBackground, hcLight: buttonBackground }, nls.localize('buttonHoverBackground', "Button background color when hovering.")); export const buttonBorder = registerColor('button.border', { dark: contrastBorder, light: contrastBorder, hcDark: contrastBorder, hcLight: contrastBorder }, nls.localize('buttonBorder', "Button border color.")); export const buttonSecondaryForeground = registerColor('button.secondaryForeground', { dark: Color.white, light: Color.white, hcDark: Color.white, hcLight: foreground }, nls.localize('buttonSecondaryForeground', "Secondary button foreground color.")); diff --git a/src/vs/platform/theme/common/styler.ts b/src/vs/platform/theme/common/styler.ts index d1c8fc7594c..503c803501c 100644 --- a/src/vs/platform/theme/common/styler.ts +++ b/src/vs/platform/theme/common/styler.ts @@ -6,7 +6,19 @@ import { Color } from 'vs/base/common/color'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IThemable, styleFn } from 'vs/base/common/styler'; -import { activeContrastBorder, badgeBackground, badgeForeground, breadcrumbsActiveSelectionForeground, breadcrumbsBackground, breadcrumbsFocusForeground, breadcrumbsForeground, buttonBackground, buttonBorder, buttonForeground, buttonHoverBackground, buttonSecondaryBackground, buttonSecondaryForeground, buttonSecondaryHoverBackground, ColorIdentifier, ColorTransform, ColorValue, contrastBorder, editorWidgetBackground, editorWidgetBorder, editorWidgetForeground, focusBorder, inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground, inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, inputValidationInfoBackground, inputValidationInfoBorder, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningBorder, inputValidationWarningForeground, listActiveSelectionBackground, listActiveSelectionForeground, listActiveSelectionIconForeground, listDropBackground, listFilterWidgetBackground, listFilterWidgetNoMatchesOutline, listFilterWidgetOutline, listFocusBackground, listFocusForeground, listFocusOutline, listHoverBackground, listHoverForeground, listInactiveFocusBackground, listInactiveFocusOutline, listInactiveSelectionBackground, listInactiveSelectionForeground, listInactiveSelectionIconForeground, menuBackground, menuBorder, menuForeground, menuSelectionBackground, menuSelectionBorder, menuSelectionForeground, menuSeparatorBackground, pickerGroupForeground, problemsErrorIconForeground, problemsInfoIconForeground, problemsWarningIconForeground, quickInputListFocusBackground, quickInputListFocusForeground, quickInputListFocusIconForeground, resolveColorValue, scrollbarShadow, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, selectBackground, selectBorder, selectForeground, selectListBackground, checkboxBackground, checkboxBorder, checkboxForeground, tableColumnsBorder, tableOddRowsBackgroundColor, textLinkForeground, treeIndentGuidesStroke, widgetShadow, listFocusAndSelectionOutline, listFilterWidgetShadow, buttonSeparator } from 'vs/platform/theme/common/colorRegistry'; +import { + activeContrastBorder, badgeBackground, badgeForeground, breadcrumbsActiveSelectionForeground, breadcrumbsBackground, breadcrumbsFocusForeground, breadcrumbsForeground, + ColorIdentifier, ColorTransform, ColorValue, contrastBorder, editorWidgetBorder, focusBorder, inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground, + inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, inputValidationInfoBackground, + inputValidationInfoBorder, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningBorder, inputValidationWarningForeground, + listActiveSelectionBackground, listActiveSelectionForeground, listActiveSelectionIconForeground, listDropBackground, + listFocusBackground, listFocusForeground, listFocusOutline, listHoverBackground, listHoverForeground, + listInactiveFocusBackground, listInactiveFocusOutline, listInactiveSelectionBackground, listInactiveSelectionForeground, listInactiveSelectionIconForeground, + menuBackground, menuBorder, menuForeground, menuSelectionBackground, menuSelectionBorder, menuSelectionForeground, menuSeparatorBackground, pickerGroupForeground, + quickInputListFocusBackground, quickInputListFocusForeground, quickInputListFocusIconForeground, resolveColorValue, scrollbarShadow, scrollbarSliderActiveBackground, + scrollbarSliderBackground, scrollbarSliderHoverBackground, selectBackground, selectBorder, selectForeground, selectListBackground, tableColumnsBorder, tableOddRowsBackgroundColor, + treeIndentGuidesStroke, widgetShadow, listFocusAndSelectionOutline +} from 'vs/platform/theme/common/colorRegistry'; import { isHighContrast } from 'vs/platform/theme/common/theme'; import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; @@ -50,20 +62,6 @@ export function attachStyler(themeService: IThemeServic return themeService.onDidColorThemeChange(applyStyles); } -export interface IToggleStyleOverrides extends IStyleOverrides { - inputActiveOptionBorderColor?: ColorIdentifier; - inputActiveOptionForegroundColor?: ColorIdentifier; - inputActiveOptionBackgroundColor?: ColorIdentifier; -} - -export function attachToggleStyler(widget: IThemable, themeService: IThemeService, style?: IToggleStyleOverrides): IDisposable { - return attachStyler(themeService, { - inputActiveOptionBorder: style?.inputActiveOptionBorderColor || inputActiveOptionBorder, - inputActiveOptionForeground: style?.inputActiveOptionForegroundColor || inputActiveOptionForeground, - inputActiveOptionBackground: style?.inputActiveOptionBackgroundColor || inputActiveOptionBackground - } as IToggleStyleOverrides, widget); -} - export interface IBadgeStyleOverrides extends IStyleOverrides { badgeBackground?: ColorIdentifier; badgeForeground?: ColorIdentifier; @@ -77,41 +75,6 @@ export function attachBadgeStyler(widget: IThemable, themeService: IThemeService } as IBadgeStyleOverrides, widget); } -export interface IInputBoxStyleOverrides extends IStyleOverrides { - inputBackground?: ColorIdentifier; - inputForeground?: ColorIdentifier; - inputBorder?: ColorIdentifier; - inputActiveOptionBorder?: ColorIdentifier; - inputActiveOptionForeground?: ColorIdentifier; - inputActiveOptionBackground?: ColorIdentifier; - inputValidationInfoBorder?: ColorIdentifier; - inputValidationInfoBackground?: ColorIdentifier; - inputValidationInfoForeground?: ColorIdentifier; - inputValidationWarningBorder?: ColorIdentifier; - inputValidationWarningBackground?: ColorIdentifier; - inputValidationWarningForeground?: ColorIdentifier; - inputValidationErrorBorder?: ColorIdentifier; - inputValidationErrorBackground?: ColorIdentifier; - inputValidationErrorForeground?: ColorIdentifier; -} - -export function attachInputBoxStyler(widget: IThemable, themeService: IThemeService, style?: IInputBoxStyleOverrides): IDisposable { - return attachStyler(themeService, { - inputBackground: style?.inputBackground || inputBackground, - inputForeground: style?.inputForeground || inputForeground, - inputBorder: style?.inputBorder || inputBorder, - inputValidationInfoBorder: style?.inputValidationInfoBorder || inputValidationInfoBorder, - inputValidationInfoBackground: style?.inputValidationInfoBackground || inputValidationInfoBackground, - inputValidationInfoForeground: style?.inputValidationInfoForeground || inputValidationInfoForeground, - inputValidationWarningBorder: style?.inputValidationWarningBorder || inputValidationWarningBorder, - inputValidationWarningBackground: style?.inputValidationWarningBackground || inputValidationWarningBackground, - inputValidationWarningForeground: style?.inputValidationWarningForeground || inputValidationWarningForeground, - inputValidationErrorBorder: style?.inputValidationErrorBorder || inputValidationErrorBorder, - inputValidationErrorBackground: style?.inputValidationErrorBackground || inputValidationErrorBackground, - inputValidationErrorForeground: style?.inputValidationErrorForeground || inputValidationErrorForeground - } as IInputBoxStyleOverrides, widget); -} - export interface ISelectBoxStyleOverrides extends IStyleOverrides, IListStyleOverrides { selectBackground?: ColorIdentifier; selectListBackground?: ColorIdentifier; @@ -140,26 +103,6 @@ export function attachSelectBoxStyler(widget: IThemable, themeService: IThemeSer } as ISelectBoxStyleOverrides, widget); } -export function attachFindReplaceInputBoxStyler(widget: IThemable, themeService: IThemeService, style?: IInputBoxStyleOverrides): IDisposable { - return attachStyler(themeService, { - inputBackground: style?.inputBackground || inputBackground, - inputForeground: style?.inputForeground || inputForeground, - inputBorder: style?.inputBorder || inputBorder, - inputActiveOptionBorder: style?.inputActiveOptionBorder || inputActiveOptionBorder, - inputActiveOptionForeground: style?.inputActiveOptionForeground || inputActiveOptionForeground, - inputActiveOptionBackground: style?.inputActiveOptionBackground || inputActiveOptionBackground, - inputValidationInfoBorder: style?.inputValidationInfoBorder || inputValidationInfoBorder, - inputValidationInfoBackground: style?.inputValidationInfoBackground || inputValidationInfoBackground, - inputValidationInfoForeground: style?.inputValidationInfoForeground || inputValidationInfoForeground, - inputValidationWarningBorder: style?.inputValidationWarningBorder || inputValidationWarningBorder, - inputValidationWarningBackground: style?.inputValidationWarningBackground || inputValidationWarningBackground, - inputValidationWarningForeground: style?.inputValidationWarningForeground || inputValidationWarningForeground, - inputValidationErrorBorder: style?.inputValidationErrorBorder || inputValidationErrorBorder, - inputValidationErrorBackground: style?.inputValidationErrorBackground || inputValidationErrorBackground, - inputValidationErrorForeground: style?.inputValidationErrorForeground || inputValidationErrorForeground - } as IInputBoxStyleOverrides, widget); -} - export interface IListStyleOverrides extends IStyleOverrides { listBackground?: ColorIdentifier; listFocusBackground?: ColorIdentifier; @@ -181,10 +124,6 @@ export interface IListStyleOverrides extends IStyleOverrides { listDropBackground?: ColorIdentifier; listSelectionOutline?: ColorIdentifier; listHoverOutline?: ColorIdentifier; - listFilterWidgetBackground?: ColorIdentifier; - listFilterWidgetOutline?: ColorIdentifier; - listFilterWidgetNoMatchesOutline?: ColorIdentifier; - listFilterWidgetShadow?: ColorIdentifier; treeIndentGuidesStroke?: ColorIdentifier; tableColumnsBorder?: ColorIdentifier; tableOddRowsBackgroundColor?: ColorIdentifier; @@ -214,10 +153,6 @@ export const defaultListStyles: IColorMapping = { listDropBackground, listSelectionOutline: activeContrastBorder, listHoverOutline: activeContrastBorder, - listFilterWidgetBackground, - listFilterWidgetOutline, - listFilterWidgetNoMatchesOutline, - listFilterWidgetShadow, treeIndentGuidesStroke, tableColumnsBorder, tableOddRowsBackgroundColor, @@ -292,60 +227,3 @@ export const defaultMenuStyles = { export function attachMenuStyler(widget: IThemable, themeService: IThemeService, style?: IMenuStyleOverrides): IDisposable { return attachStyler(themeService, { ...defaultMenuStyles, ...style }, widget); } - -interface IButtonStyleOverrides extends IStyleOverrides { - buttonForeground?: ColorIdentifier; - buttonSeparator?: ColorIdentifier; - buttonBackground?: ColorIdentifier; - buttonHoverBackground?: ColorIdentifier; - buttonSecondaryForeground?: ColorIdentifier; - buttonSecondaryBackground?: ColorIdentifier; - buttonSecondaryHoverBackground?: ColorIdentifier; - buttonBorder?: ColorIdentifier; -} - -export interface IDialogStyleOverrides extends IButtonStyleOverrides { - dialogForeground?: ColorIdentifier; - dialogBackground?: ColorIdentifier; - dialogShadow?: ColorIdentifier; - dialogBorder?: ColorIdentifier; - checkboxBorder?: ColorIdentifier; - checkboxBackground?: ColorIdentifier; - checkboxForeground?: ColorIdentifier; - errorIconForeground?: ColorIdentifier; - warningIconForeground?: ColorIdentifier; - infoIconForeground?: ColorIdentifier; - inputBackground?: ColorIdentifier; - inputForeground?: ColorIdentifier; - inputBorder?: ColorIdentifier; -} - -export const defaultDialogStyles = { - dialogBackground: editorWidgetBackground, - dialogForeground: editorWidgetForeground, - dialogShadow: widgetShadow, - dialogBorder: contrastBorder, - buttonForeground: buttonForeground, - buttonSeparator: buttonSeparator, - buttonBackground: buttonBackground, - buttonSecondaryBackground: buttonSecondaryBackground, - buttonSecondaryForeground: buttonSecondaryForeground, - buttonSecondaryHoverBackground: buttonSecondaryHoverBackground, - buttonHoverBackground: buttonHoverBackground, - buttonBorder: buttonBorder, - checkboxBorder: checkboxBorder, - checkboxBackground: checkboxBackground, - checkboxForeground: checkboxForeground, - errorIconForeground: problemsErrorIconForeground, - warningIconForeground: problemsWarningIconForeground, - infoIconForeground: problemsInfoIconForeground, - inputBackground: inputBackground, - inputForeground: inputForeground, - inputBorder: inputBorder, - textLinkForeground: textLinkForeground -}; - - -export function attachDialogStyler(widget: IThemable, themeService: IThemeService, style?: IDialogStyleOverrides): IDisposable { - return attachStyler(themeService, { ...defaultDialogStyles, ...style }, widget); -} diff --git a/src/vs/platform/userDataProfile/electron-sandbox/userDataProfileStorageService.ts b/src/vs/platform/userDataProfile/electron-sandbox/userDataProfileStorageService.ts index b0d9c5082fd..f000b71599d 100644 --- a/src/vs/platform/userDataProfile/electron-sandbox/userDataProfileStorageService.ts +++ b/src/vs/platform/userDataProfile/electron-sandbox/userDataProfileStorageService.ts @@ -12,6 +12,7 @@ import { AbstractUserDataProfileStorageService, IProfileStorageChanges, IUserDat import { isProfileUsingDefaultStorage, IStorageService } from 'vs/platform/storage/common/storage'; import { ApplicationStorageDatabaseClient, ProfileStorageDatabaseClient } from 'vs/platform/storage/common/storageIpc'; import { IUserDataProfile, IUserDataProfilesService, reviveProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; export class UserDataProfileStorageService extends AbstractUserDataProfileStorageService implements IUserDataProfileStorageService { @@ -50,3 +51,5 @@ export class UserDataProfileStorageService extends AbstractUserDataProfileStorag return isProfileUsingDefaultStorage(profile) ? new ApplicationStorageDatabaseClient(storageChannel) : new ProfileStorageDatabaseClient(storageChannel, profile); } } + +registerSingleton(IUserDataProfileStorageService, UserDataProfileStorageService, InstantiationType.Delayed); diff --git a/src/vs/platform/window/common/window.ts b/src/vs/platform/window/common/window.ts index 3a7393455ee..408e9f667da 100644 --- a/src/vs/platform/window/common/window.ts +++ b/src/vs/platform/window/common/window.ts @@ -14,6 +14,7 @@ import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { FileType } from 'vs/platform/files/common/files'; import { LogLevel } from 'vs/platform/log/common/log'; import { PolicyDefinition, PolicyValue } from 'vs/platform/policy/common/policy'; +import { IProductService } from 'vs/platform/product/common/productService'; import { IPartsSplash } from 'vs/platform/theme/common/themeService'; import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; import { IAnyWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace'; @@ -163,7 +164,7 @@ export function getTitleBarStyle(configurationService: IConfigurationService): ' return isLinux ? 'native' : 'custom'; // default to custom on all macOS and Windows } -export function useWindowControlsOverlay(configurationService: IConfigurationService): boolean { +export function useWindowControlsOverlay(configurationService: IConfigurationService, productService: IProductService): boolean { if (!isWindows || isWeb) { return false; // only supported on a desktop Windows instance } @@ -177,7 +178,9 @@ export function useWindowControlsOverlay(configurationService: IConfigurationSer return configuredUseWindowControlsOverlay; } - return false; // disable by default + // Default to true for Insider and Exploration to match with + // app.getPreferredSystemLanguages() only being available on those builds. + return productService.quality === 'insider' || productService.quality === 'exploration'; } export interface IPath extends IPathData { diff --git a/src/vs/platform/windows/electron-main/windowImpl.ts b/src/vs/platform/windows/electron-main/windowImpl.ts index 8efb68d9130..1f37bc6b114 100644 --- a/src/vs/platform/windows/electron-main/windowImpl.ts +++ b/src/vs/platform/windows/electron-main/windowImpl.ts @@ -274,7 +274,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { options.frame = false; } - if (useWindowControlsOverlay(this.configurationService)) { + if (useWindowControlsOverlay(this.configurationService, this.productService)) { // This logic will not perfectly guess the right colors // to use on initialization, but prefer to keep things @@ -305,7 +305,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { } // Update the window controls immediately based on cached values - if (useCustomTitleStyle && ((isWindows && useWindowControlsOverlay(this.configurationService)) || isMacintosh)) { + if (useCustomTitleStyle && ((isWindows && useWindowControlsOverlay(this.configurationService, this.productService)) || isMacintosh)) { const cachedWindowControlHeight = this.stateMainService.getItem((CodeWindow.windowControlHeightStateStorageKey)); if (cachedWindowControlHeight) { this.updateWindowControls({ height: cachedWindowControlHeight }); diff --git a/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts b/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts index 08a02f4e0f1..26badff38b9 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { isNonEmptyArray } from 'vs/base/common/arrays'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { combinedDisposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; @@ -15,7 +16,7 @@ import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/ext import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; import { INotebookCellExecution, INotebookExecutionStateService } from 'vs/workbench/contrib/notebook/common/notebookExecutionStateService'; -import { INotebookKernel, INotebookKernelChangeEvent, INotebookKernelDetectionTask, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { IKernelSourceActionProvider, INotebookKernel, INotebookKernelChangeEvent, INotebookKernelDetectionTask, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import { ExtHostContext, ExtHostNotebookKernelsShape, ICellExecuteUpdateDto, ICellExecutionCompleteDto, INotebookKernelDto2, MainContext, MainThreadNotebookKernelsShape } from '../common/extHost.protocol'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; @@ -113,6 +114,7 @@ export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape private readonly _kernels = new Map(); private readonly _kernelDetectionTasks = new Map(); + private readonly _kernelSourceActionProviders = new Map(); private readonly _proxy: ExtHostNotebookKernelsShape; private readonly _executions = new Map(); @@ -309,4 +311,25 @@ export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape this._kernelDetectionTasks.delete(handle); } } + + // --- notebook kernel source action provider + + async $addKernelSourceActionProvider(handle: number, notebookType: string): Promise { + const kernelSourceActionProvider: IKernelSourceActionProvider = { + viewType: notebookType, + provideKernelSourceActions: async () => { + return this._proxy.$provideKernelSourceActions(handle, CancellationToken.None); + } + }; + const registration = this._notebookKernelService.registerKernelSourceActionProvider(notebookType, kernelSourceActionProvider); + this._kernelSourceActionProviders.set(handle, [kernelSourceActionProvider, registration]); + } + + $removeKernelSourceActionProvider(handle: number): void { + const tuple = this._kernelSourceActionProviders.get(handle); + if (tuple) { + tuple[1].dispose(); + this._kernelSourceActionProviders.delete(handle); + } + } } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 28c7d9bbf69..48e90b6323c 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1156,6 +1156,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'notebookKernelSource'); return extHostNotebookKernels.createNotebookControllerDetectionTask(extension, notebookType); }, + registerKernelSourceActionProvider(notebookType: string, provider: vscode.NotebookKernelSourceActionProvider) { + checkProposedApiEnabled(extension, 'notebookKernelSource'); + return extHostNotebookKernels.registerKernelSourceActionProvider(extension, notebookType, provider); + }, onDidChangeNotebookCellExecutionState(listener, thisArgs?, disposables?) { checkProposedApiEnabled(extension, 'notebookCellExecutionState'); return extHostNotebookKernels.onDidChangeNotebookCellExecutionState(listener, thisArgs, disposables); @@ -1352,6 +1356,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I NotebookControllerAffinity: extHostTypes.NotebookControllerAffinity, NotebookControllerAffinity2: extHostTypes.NotebookControllerAffinity2, NotebookEdit: extHostTypes.NotebookEdit, + NotebookKernelSourceAction: extHostTypes.NotebookKernelSourceAction, PortAttributes: extHostTypes.PortAttributes, LinkedEditingRanges: extHostTypes.LinkedEditingRanges, TestResultState: extHostTypes.TestResultState, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 1d1637b7db2..00d6fcb0e6a 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1050,6 +1050,9 @@ export interface MainThreadNotebookKernelsShape extends IDisposable { $addKernelDetectionTask(handle: number, notebookType: string): Promise; $removeKernelDetectionTask(handle: number): void; + + $addKernelSourceActionProvider(handle: number, notebookType: string): Promise; + $removeKernelSourceActionProvider(handle: number): void; } export interface MainThreadNotebookRenderersShape extends IDisposable { @@ -2140,6 +2143,7 @@ export interface ExtHostNotebookKernelsShape { $cancelCells(handle: number, uri: UriComponents, handles: number[]): Promise; $acceptKernelMessageFromRenderer(handle: number, editorId: string, message: any): void; $cellExecutionChanged(uri: UriComponents, cellHandle: number, state: notebookCommon.NotebookCellExecutionState | undefined): void; + $provideKernelSourceActions(handle: number, token: CancellationToken): Promise; } export interface ExtHostInteractiveShape { diff --git a/src/vs/workbench/api/common/extHostComments.ts b/src/vs/workbench/api/common/extHostComments.ts index e66c60290f9..2e2f0e0835c 100644 --- a/src/vs/workbench/api/common/extHostComments.ts +++ b/src/vs/workbench/api/common/extHostComments.ts @@ -66,7 +66,7 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo } return commentThread.value; - } else if (arg && arg.$mid === MarshalledId.CommentThreadReply) { + } else if (arg && (arg.$mid === MarshalledId.CommentThreadReply || arg.$mid === MarshalledId.CommentThreadInstance)) { const commentController = this._commentControllers.get(arg.thread.commentControlHandle); if (!commentController) { @@ -79,6 +79,10 @@ export function createExtHostComments(mainContext: IMainContext, commands: ExtHo return arg; } + if (arg.$mid === MarshalledId.CommentThreadInstance) { + return commentThread.value; + } + return { thread: commentThread.value, text: arg.text diff --git a/src/vs/workbench/api/common/extHostNotebookKernels.ts b/src/vs/workbench/api/common/extHostNotebookKernels.ts index 3107aa0eb0c..a58f8cec0be 100644 --- a/src/vs/workbench/api/common/extHostNotebookKernels.ts +++ b/src/vs/workbench/api/common/extHostNotebookKernels.ts @@ -5,13 +5,14 @@ import { asArray } from 'vs/base/common/arrays'; import { DeferredPromise, timeout } from 'vs/base/common/async'; -import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; +import { Cache } from 'vs/workbench/api/common/cache'; import { ExtHostNotebookKernelsShape, ICellExecuteUpdateDto, IMainContext, INotebookKernelDto2, MainContext, MainThreadNotebookKernelsShape, NotebookOutputDto } from 'vs/workbench/api/common/extHost.protocol'; import { ApiCommand, ApiCommandArgument, ApiCommandResult, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; @@ -20,7 +21,7 @@ import { ExtHostCell } from 'vs/workbench/api/common/extHostNotebookDocument'; import * as extHostTypeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { NotebookCellExecutionState as ExtHostNotebookCellExecutionState, NotebookCellOutput, NotebookControllerAffinity2 } from 'vs/workbench/api/common/extHostTypes'; import { asWebviewUri } from 'vs/workbench/contrib/webview/common/webview'; -import { NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookKernelSourceAction, NotebookCellExecutionState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CellExecutionUpdateType } from 'vs/workbench/contrib/notebook/common/notebookExecutionService'; import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import { SerializableObjectWithBuffers } from 'vs/workbench/services/extensions/common/proxyIdentifier'; @@ -47,6 +48,10 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { private _kernelDetectionTask = new Map(); private _kernelDetectionTaskHandlePool: number = 0; + private _kernelSourceActionProviders = new Map(); + private _kernelSourceActionProviderHandlePool: number = 0; + private _kernelSourceActionProviderCache = new Cache('NotebookKernelSourceActionProviderCache'); + private readonly _kernelData = new Map(); private _handlePool: number = 0; @@ -292,6 +297,33 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { return detectionTask; } + registerKernelSourceActionProvider(extension: IExtensionDescription, viewType: string, provider: vscode.NotebookKernelSourceActionProvider) { + const handle = this._kernelSourceActionProviderHandlePool++; + const that = this; + + this._kernelSourceActionProviders.set(handle, provider); + this._logService.trace(`NotebookKernelSourceActionProvider[${handle}], CREATED by ${extension.identifier.value}`); + this._proxy.$addKernelSourceActionProvider(handle, viewType); + + return { + dispose: () => { + this._kernelSourceActionProviders.delete(handle); + that._proxy.$removeKernelSourceActionProvider(handle); + } + }; + } + + async $provideKernelSourceActions(handle: number, token: CancellationToken): Promise { + const provider = this._kernelSourceActionProviders.get(handle); + if (provider) { + const disposables = new DisposableStore(); + this._kernelSourceActionProviderCache.add([disposables]); + const ret = await provider.provideNotebookKernelSourceActions(token); + return (ret ?? []).map(item => extHostTypeConverters.NotebookKernelSourceAction.from(item, this._commands.converter, disposables)); + } + return []; + } + $acceptNotebookAssociation(handle: number, uri: UriComponents, value: boolean): void { const obj = this._kernelData.get(handle); if (obj) { diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index ff023d1fc3b..bc889cd13cf 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -1705,6 +1705,19 @@ export namespace NotebookStatusBarItem { } } +export namespace NotebookKernelSourceAction { + export function from(item: vscode.NotebookKernelSourceAction, commandsConverter: Command.ICommandsConverter, disposables: DisposableStore): notebooks.INotebookKernelSourceAction { + const command = typeof item.command === 'string' ? { title: '', command: item.command } : item.command; + + return { + command: commandsConverter.toInternal(command, disposables), + label: item.label, + description: item.description, + detail: item.detail + }; + } +} + export namespace NotebookDocumentContentOptions { export function from(options: vscode.NotebookDocumentContentOptions | undefined): notebooks.TransientOptions { return { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 8f20cc90728..f0458313274 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -525,9 +525,7 @@ export class RemoteAuthorityResolverError extends Error { // workaround when extending builtin objects and when compiling to ES5, see: // https://github.com/microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work - if (typeof (Object).setPrototypeOf === 'function') { - (Object).setPrototypeOf(this, RemoteAuthorityResolverError.prototype); - } + Object.setPrototypeOf(this, RemoteAuthorityResolverError.prototype); } } @@ -2964,9 +2962,7 @@ export class FileSystemError extends Error { // workaround when extending builtin objects and when compiling to ES5, see: // https://github.com/microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work - if (typeof (Object).setPrototypeOf === 'function') { - (Object).setPrototypeOf(this, FileSystemError.prototype); - } + Object.setPrototypeOf(this, FileSystemError.prototype); if (typeof Error.captureStackTrace === 'function' && typeof terminator === 'function') { // nice stack traces @@ -3627,6 +3623,15 @@ export class NotebookRendererScript { } } +export class NotebookKernelSourceAction { + description?: string; + detail?: string; + command?: vscode.Command; + constructor( + public label: string + ) { } +} + //#endregion //#region Timeline diff --git a/src/vs/workbench/api/common/extensionHostMain.ts b/src/vs/workbench/api/common/extensionHostMain.ts index 88bc7909828..623777165ba 100644 --- a/src/vs/workbench/api/common/extensionHostMain.ts +++ b/src/vs/workbench/api/common/extensionHostMain.ts @@ -11,7 +11,7 @@ import { IMessagePassingProtocol } from 'vs/base/parts/ipc/common/ipc'; import { MainContext, MainThreadConsoleShape } from 'vs/workbench/api/common/extHost.protocol'; import { IExtensionHostInitData } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { RPCProtocol } from 'vs/workbench/services/extensions/common/rpcProtocol'; -import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription, IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; import { getSingletonServiceDescriptors } from 'vs/platform/instantiation/common/extensions'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; @@ -22,6 +22,7 @@ import { IExtHostRpcService, ExtHostRpcService } from 'vs/workbench/api/common/e import { IURITransformerService, URITransformerService } from 'vs/workbench/api/common/extHostUriTransformerService'; import { IExtHostExtensionService, IHostUtils } from 'vs/workbench/api/common/extHostExtensionService'; import { IExtHostTelemetry } from 'vs/workbench/api/common/extHostTelemetry'; +import { Mutable } from 'vs/base/common/types'; export interface IExitFn { (code?: number): any; @@ -163,11 +164,11 @@ export class ExtensionHostMain { private static _transform(initData: IExtensionHostInitData, rpcProtocol: RPCProtocol): IExtensionHostInitData { initData.allExtensions.forEach((ext) => { - (ext).extensionLocation = URI.revive(rpcProtocol.transformIncomingURIs(ext.extensionLocation)); + (>ext).extensionLocation = URI.revive(rpcProtocol.transformIncomingURIs(ext.extensionLocation)); const browserNlsBundleUris: { [language: string]: URI } = {}; if (ext.browserNlsBundleUris) { Object.keys(ext.browserNlsBundleUris).forEach(lang => browserNlsBundleUris[lang] = URI.revive(rpcProtocol.transformIncomingURIs(ext.browserNlsBundleUris![lang]))); - (ext).browserNlsBundleUris = browserNlsBundleUris; + (>ext).browserNlsBundleUris = browserNlsBundleUris; } }); initData.environment.appRoot = URI.revive(rpcProtocol.transformIncomingURIs(initData.environment.appRoot)); diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index f1c508f2e79..2efbf6f249b 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -43,8 +43,6 @@ import { StringSHA1 } from 'vs/base/common/hash'; import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; import { GestureEvent } from 'vs/base/browser/touch'; import { IPaneCompositePart, IPaneCompositeSelectorPart } from 'vs/workbench/browser/parts/paneCompositePart'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { Extensions, IProfileStorageRegistry } from 'vs/workbench/services/userDataProfile/common/userDataProfileStorageRegistry'; import { IUserDataProfileService, PROFILES_TTILE } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; @@ -149,14 +147,6 @@ export class ActivitybarPart extends Part implements IPaneCompositeSelectorPart this.registerListeners(); - Registry.as(Extensions.ProfileStorageRegistry) - .registerKeys([{ - key: ActivitybarPart.PINNED_VIEW_CONTAINERS, - description: localize('pinned view containers', "Activity bar entries visibility customizations") - }, { - key: AccountsActivityActionViewItem.ACCOUNTS_VISIBILITY_PREFERENCE_KEY, - description: localize('accounts visibility key', "Accounts entry visibility customization in the activity bar.") - }]); } private createCompositeBar() { diff --git a/src/vs/workbench/browser/parts/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index 33abdb3e8f8..bdb4ab394e4 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -31,7 +31,7 @@ import { assertIsDefined, withNullAsUndefined } from 'vs/base/common/types'; import { createActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { AbstractProgressScope, ScopedProgressIndicator } from 'vs/workbench/services/progress/browser/progressIndicator'; import { WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; -import { getProgressBarStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { defaultProgressBarStyles } from 'vs/platform/theme/browser/defaultStyles'; export interface ICompositeTitleLabel { @@ -458,7 +458,7 @@ export abstract class CompositePart extends Part { override createContentArea(parent: HTMLElement): HTMLElement { const contentContainer = append(parent, $('.content')); - this.progressBar = this._register(new ProgressBar(contentContainer, getProgressBarStyles())); + this.progressBar = this._register(new ProgressBar(contentContainer, defaultProgressBarStyles)); this.progressBar.hide(); return contentContainer; diff --git a/src/vs/workbench/browser/parts/dialogs/dialog.web.contribution.ts b/src/vs/workbench/browser/parts/dialogs/dialog.web.contribution.ts index 4fa42a97578..299ac93afef 100644 --- a/src/vs/workbench/browser/parts/dialogs/dialog.web.contribution.ts +++ b/src/vs/workbench/browser/parts/dialogs/dialog.web.contribution.ts @@ -10,7 +10,6 @@ import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { ILogService } from 'vs/platform/log/common/log'; import { IProductService } from 'vs/platform/product/common/productService'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { IDialogsModel, IDialogViewItem } from 'vs/workbench/common/dialogs'; import { BrowserDialogHandler } from 'vs/workbench/browser/parts/dialogs/dialogHandler'; @@ -29,7 +28,6 @@ export class DialogHandlerContribution extends Disposable implements IWorkbenchC @IDialogService private dialogService: IDialogService, @ILogService logService: ILogService, @ILayoutService layoutService: ILayoutService, - @IThemeService themeService: IThemeService, @IKeybindingService keybindingService: IKeybindingService, @IInstantiationService instantiationService: IInstantiationService, @IProductService productService: IProductService, @@ -37,7 +35,7 @@ export class DialogHandlerContribution extends Disposable implements IWorkbenchC ) { super(); - this.impl = new BrowserDialogHandler(logService, layoutService, themeService, keybindingService, instantiationService, productService, clipboardService); + this.impl = new BrowserDialogHandler(logService, layoutService, keybindingService, instantiationService, productService, clipboardService); this.model = (this.dialogService as DialogService).model; diff --git a/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts b/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts index e7dc6440849..6a3768dd5f5 100644 --- a/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts +++ b/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts @@ -9,8 +9,6 @@ import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { ILogService } from 'vs/platform/log/common/log'; import Severity from 'vs/base/common/severity'; import { Dialog, IDialogResult } from 'vs/base/browser/ui/dialog/dialog'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { attachDialogStyler } from 'vs/platform/theme/common/styler'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { EventHelper } from 'vs/base/browser/dom'; @@ -20,7 +18,7 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService import { fromNow } from 'vs/base/common/date'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { MarkdownRenderer } from 'vs/editor/contrib/markdownRenderer/browser/markdownRenderer'; -import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { defaultButtonStyles, defaultCheckboxStyles, defaultDialogStyles, defaultInputBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; export class BrowserDialogHandler implements IDialogHandler { @@ -38,7 +36,6 @@ export class BrowserDialogHandler implements IDialogHandler { constructor( @ILogService private readonly logService: ILogService, @ILayoutService private readonly layoutService: ILayoutService, - @IThemeService private readonly themeService: IThemeService, @IKeybindingService private readonly keybindingService: IKeybindingService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IProductService private readonly productService: IProductService, @@ -119,11 +116,14 @@ export class BrowserDialogHandler implements IDialogHandler { checkboxLabel: checkbox?.label, checkboxChecked: checkbox?.checked, inputs, - buttonStyles: defaultButtonStyles - }); + buttonStyles: defaultButtonStyles, + checkboxStyles: defaultCheckboxStyles, + inputBoxStyles: defaultInputBoxStyles, + dialogStyles: defaultDialogStyles + } + ); dialogDisposables.add(dialog); - dialogDisposables.add(attachDialogStyler(dialog, this.themeService)); const result = await dialog.show(); dialogDisposables.dispose(); diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index 92542a8d03f..68c3d487a41 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -549,15 +549,16 @@ registerAction2(class FocusAndSelectBreadcrumbs extends Action2 { super({ id: 'breadcrumbs.focusAndSelect', title: { - value: localize('cmd.focus', "Focus Breadcrumbs"), - original: 'Focus Breadcrumbs' + value: localize('cmd.focusAndSelect', "Focus and Select Breadcrumbs"), + original: 'Focus and Select Breadcrumbs' }, precondition: BreadcrumbsControl.CK_BreadcrumbsVisible, keybinding: { weight: KeybindingWeight.WorkbenchContrib, primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Period, when: BreadcrumbsControl.CK_BreadcrumbsPossible, - } + }, + f1: true }); } run(accessor: ServicesAccessor, ...args: any[]): void { @@ -565,12 +566,26 @@ registerAction2(class FocusAndSelectBreadcrumbs extends Action2 { } }); -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'breadcrumbs.focus', - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Semicolon, - when: BreadcrumbsControl.CK_BreadcrumbsPossible, - handler: accessor => focusAndSelectHandler(accessor, false) +registerAction2(class FocusBreadcrumbs extends Action2 { + constructor() { + super({ + id: 'breadcrumbs.focus', + title: { + value: localize('cmd.focus', "Focus Breadcrumbs"), + original: 'Focus Breadcrumbs' + }, + precondition: BreadcrumbsControl.CK_BreadcrumbsVisible, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Semicolon, + when: BreadcrumbsControl.CK_BreadcrumbsPossible, + }, + f1: true + }); + } + run(accessor: ServicesAccessor, ...args: any[]): void { + focusAndSelectHandler(accessor, false); + } }); // this commands is only enabled when breadcrumbs are diff --git a/src/vs/workbench/browser/parts/editor/editor.contribution.ts b/src/vs/workbench/browser/parts/editor/editor.contribution.ts index fbe1d5a8853..d38d510d380 100644 --- a/src/vs/workbench/browser/parts/editor/editor.contribution.ts +++ b/src/vs/workbench/browser/parts/editor/editor.contribution.ts @@ -129,7 +129,6 @@ Registry.as(WorkbenchExtensions.Workbench).regi Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(DynamicEditorConfigurations, LifecyclePhase.Ready); registerEditorContribution(FloatingClickMenu.ID, FloatingClickMenu); - //#endregion //#region Quick Access diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 0c2c8f23796..10ce180feaa 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -52,7 +52,8 @@ import { URI } from 'vs/base/common/uri'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; import { isLinux, isMacintosh, isNative, isWindows } from 'vs/base/common/platform'; import { ILogService } from 'vs/platform/log/common/log'; -import { getProgressBarStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { TrustedTelemetryValue } from 'vs/platform/telemetry/common/telemetryUtils'; +import { defaultProgressBarStyles } from 'vs/platform/theme/browser/defaultStyles'; export class EditorGroupView extends Themable implements IEditorGroupView { @@ -182,7 +183,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this.element.appendChild(letterpressContainer); // Progress bar - this.progressBar = this._register(new ProgressBar(this.element, getProgressBarStyles())); + this.progressBar = this._register(new ProgressBar(this.element, defaultProgressBarStyles)); this.progressBar.hide(); // Scoped instantiation service @@ -642,7 +643,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Remove query parameters from the resource extension const queryStringLocation = resourceExt.indexOf('?'); resourceExt = queryStringLocation !== -1 ? resourceExt.substr(0, queryStringLocation) : resourceExt; - descriptor['resource'] = { mimeType: getMimeTypes(resource).join(', '), scheme: resource.scheme, ext: resourceExt, path: hash(path) }; + descriptor['resource'] = { mimeType: new TrustedTelemetryValue(getMimeTypes(resource).join(', ')), scheme: resource.scheme, ext: resourceExt, path: hash(path) }; /* __GDPR__FRAGMENT__ "EditorTelemetryDescriptor" : { diff --git a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts index 05dbe36b621..c32bbd3cf3a 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsViewer.ts @@ -25,7 +25,7 @@ import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdown import { DomEmitter } from 'vs/base/browser/event'; import { Gesture, EventType as GestureEventType } from 'vs/base/browser/touch'; import { Event } from 'vs/base/common/event'; -import { defaultButtonStyles, getProgressBarStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { defaultButtonStyles, defaultProgressBarStyles } from 'vs/platform/theme/browser/defaultStyles'; export class NotificationsListDelegate implements IListVirtualDelegate { @@ -257,7 +257,7 @@ export class NotificationRenderer implements IListRenderer impleme // Global Panel Actions this.globalActions = this._register(this.instantiationService.createInstance(CompositeMenuActions, partId === Parts.PANEL_PART ? MenuId.PanelTitle : MenuId.AuxiliaryBarTitle, undefined, undefined)); this._register(this.globalActions.onDidChange(() => this.updateGlobalToolbarActions())); - - Registry.as(Extensions.ProfileStorageRegistry) - .registerKeys([{ - key: this.pinnedPanelsKey, - description: localize('pinned view containers', "Panel entries visibility customizations") - }]); } protected abstract getActivityHoverOptions(): IActivityHoverOptions; diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts b/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts index 1f5f723f8f0..bf44fea999d 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarModel.ts @@ -8,9 +8,6 @@ import { isStatusbarEntryLocation, IStatusbarEntryLocation, StatusbarAlignment } import { hide, show, isAncestor } from 'vs/base/browser/dom'; import { IStorageService, StorageScope, IStorageValueChangeEvent, StorageTarget } from 'vs/platform/storage/common/storage'; import { Emitter } from 'vs/base/common/event'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { Extensions, IProfileStorageRegistry } from 'vs/workbench/services/userDataProfile/common/userDataProfileStorageRegistry'; -import { localize } from 'vs/nls'; export interface IStatusbarEntryPriority { @@ -68,11 +65,6 @@ export class StatusbarViewModel extends Disposable { this.restoreState(); this.registerListeners(); - Registry.as(Extensions.ProfileStorageRegistry) - .registerKeys([{ - key: StatusbarViewModel.HIDDEN_ENTRIES_KEY, - description: localize('statusbar.hidden', "Status bar entries visibility customizations"), - }]); } private restoreState(): void { diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 9a1786e1e08..faa3ba17b07 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -36,6 +36,7 @@ import { IHoverDelegate } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; import { MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; +import { IProductService } from 'vs/platform/product/common/productService'; export class TitlebarPart extends Part implements ITitleService { @@ -89,12 +90,13 @@ export class TitlebarPart extends Part implements ITitleService { @IConfigurationService protected readonly configurationService: IConfigurationService, @IBrowserWorkbenchEnvironmentService protected readonly environmentService: IBrowserWorkbenchEnvironmentService, @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IProductService protected readonly productService: IProductService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IHostService private readonly hostService: IHostService, - @IHoverService hoverService: IHoverService, + @IHoverService hoverService: IHoverService ) { super(Parts.TITLEBAR_PART, { hasTitle: false }, themeService, storageService, layoutService); this.windowTitle = this._register(instantiationService.createInstance(WindowTitle)); diff --git a/src/vs/workbench/browser/parts/views/checkbox.ts b/src/vs/workbench/browser/parts/views/checkbox.ts index 3b11fe0f2c6..5da59e75b1a 100644 --- a/src/vs/workbench/browser/parts/views/checkbox.ts +++ b/src/vs/workbench/browser/parts/views/checkbox.ts @@ -9,8 +9,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; -import { attachToggleStyler } from 'vs/platform/theme/common/styler'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { defaultToggleStyles } from 'vs/platform/theme/browser/defaultStyles'; import { ITreeItem, ITreeItemCheckboxState } from 'vs/workbench/common/views'; export class CheckboxStateHandler extends Disposable { @@ -32,7 +31,7 @@ export class TreeItemCheckbox extends Disposable { private readonly _onDidChangeState = new Emitter(); readonly onDidChangeState: Event = this._onDidChangeState.event; - constructor(container: HTMLElement, private checkboxStateHandler: CheckboxStateHandler, private themeService: IThemeService) { + constructor(container: HTMLElement, private checkboxStateHandler: CheckboxStateHandler) { super(); this.checkboxContainer = container; } @@ -54,7 +53,8 @@ export class TreeItemCheckbox extends Disposable { this.toggle = new Toggle({ isChecked: node.checkbox.isChecked, title: this.createCheckboxTitle(node.checkbox), - icon: node.checkbox.isChecked ? Codicon.check : undefined + icon: node.checkbox.isChecked ? Codicon.check : undefined, + ...defaultToggleStyles }); this.toggle.domNode.classList.add(TreeItemCheckbox.checkboxClass); @@ -70,7 +70,6 @@ export class TreeItemCheckbox extends Disposable { this._register(this.toggle.onChange(() => { this.setCheckbox(node); })); - this._register(attachToggleStyler(this.toggle, this.themeService)); } } diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 5f328015235..e38c4539c5b 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -1247,7 +1247,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer showHistoryKeybindingHint(this.keybindingService) + showHistoryHint: () => showHistoryKeybindingHint(this.keybindingService), + inputBoxStyles: defaultInputBoxStyles })); - this._register(attachInputBoxStyler(inputBox, this.themeService)); if (this.options.text) { inputBox.value = this.options.text; } diff --git a/src/vs/workbench/browser/parts/views/viewPane.ts b/src/vs/workbench/browser/parts/views/viewPane.ts index e61f0fdbb46..933adaf1f9c 100644 --- a/src/vs/workbench/browser/parts/views/viewPane.ts +++ b/src/vs/workbench/browser/parts/views/viewPane.ts @@ -44,7 +44,7 @@ import { WorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; import { FilterWidget, IFilterWidgetOptions } from 'vs/workbench/browser/parts/views/viewFilter'; import { BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { defaultButtonStyles, getProgressBarStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { defaultButtonStyles, defaultProgressBarStyles } from 'vs/platform/theme/browser/defaultStyles'; export interface IViewPaneOptions extends IPaneOptions { id: string; @@ -452,7 +452,7 @@ export abstract class ViewPane extends Pane implements IView { getProgressIndicator() { if (this.progressBar === undefined) { // Progress bar - this.progressBar = this._register(new ProgressBar(this.element, getProgressBarStyles())); + this.progressBar = this._register(new ProgressBar(this.element, defaultProgressBarStyles)); this.progressBar.hide(); } diff --git a/src/vs/workbench/contrib/audioCues/browser/audioCueDebuggerContribution.ts b/src/vs/workbench/contrib/audioCues/browser/audioCueDebuggerContribution.ts index 3b400d3fafc..84e1a657153 100644 --- a/src/vs/workbench/contrib/audioCues/browser/audioCueDebuggerContribution.ts +++ b/src/vs/workbench/contrib/audioCues/browser/audioCueDebuggerContribution.ts @@ -5,8 +5,8 @@ import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { autorunWithStore } from 'vs/base/common/observable'; +import { IAudioCueService, AudioCue } from 'vs/platform/audioCues/browser/audioCueService'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { AudioCue, IAudioCueService } from 'vs/workbench/contrib/audioCues/browser/audioCueService'; import { IDebugService, IDebugSession } from 'vs/workbench/contrib/debug/common/debug'; export class AudioCueLineDebuggerContribution diff --git a/src/vs/workbench/contrib/audioCues/browser/audioCueLineFeatureContribution.ts b/src/vs/workbench/contrib/audioCues/browser/audioCueLineFeatureContribution.ts index 490fab6c4c6..c3a895a3ecb 100644 --- a/src/vs/workbench/contrib/audioCues/browser/audioCueLineFeatureContribution.ts +++ b/src/vs/workbench/contrib/audioCues/browser/audioCueLineFeatureContribution.ts @@ -14,9 +14,9 @@ import { FoldingController } from 'vs/editor/contrib/folding/browser/folding'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITextModel } from 'vs/editor/common/model'; import { GhostTextController } from 'vs/editor/contrib/inlineCompletions/browser/ghostTextController'; -import { AudioCue, IAudioCueService } from 'vs/workbench/contrib/audioCues/browser/audioCueService'; import { CursorChangeReason } from 'vs/editor/common/cursorEvents'; import { autorun, autorunDelta, constObservable, debouncedObservable, derived, IObservable, observableFromEvent, observableFromPromise, wasEventTriggeredRecently } from 'vs/base/common/observable'; +import { AudioCue, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService'; export class AudioCueLineFeatureContribution extends Disposable diff --git a/src/vs/workbench/contrib/audioCues/browser/audioCues.contribution.ts b/src/vs/workbench/contrib/audioCues/browser/audioCues.contribution.ts index 2fc361426fd..78bf29148c2 100644 --- a/src/vs/workbench/contrib/audioCues/browser/audioCues.contribution.ts +++ b/src/vs/workbench/contrib/audioCues/browser/audioCues.contribution.ts @@ -3,17 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { ShowAudioCueHelp } from 'vs/workbench/contrib/audioCues/browser/commands'; import { localize } from 'vs/nls'; import { registerAction2 } from 'vs/platform/actions/common/actions'; import { Extensions as ConfigurationExtensions, IConfigurationPropertySchema, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { IAudioCueService, AudioCueService } from 'vs/platform/audioCues/browser/audioCueService'; import { AudioCueLineDebuggerContribution } from 'vs/workbench/contrib/audioCues/browser/audioCueDebuggerContribution'; import { AudioCueLineFeatureContribution } from 'vs/workbench/contrib/audioCues/browser/audioCueLineFeatureContribution'; -import { AudioCueService, IAudioCueService } from 'vs/workbench/contrib/audioCues/browser/audioCueService'; -import { ShowAudioCueHelp } from 'vs/workbench/contrib/audioCues/browser/commands'; -import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; registerSingleton(IAudioCueService, AudioCueService, InstantiationType.Delayed); @@ -87,6 +87,22 @@ Registry.as(ConfigurationExtensions.Configuration).regis 'description': localize('audioCues.terminalQuickFix', "Plays a sound when terminal Quick Fixes are available."), ...audioCueFeatureBase, }, + 'audioCues.diffLineInserted': { + 'description': localize('audioCues.diffLineInserted', "Plays a sound when the focus moves to an inserted line in diff review mode"), + ...audioCueFeatureBase, + }, + 'audioCues.diffLineDeleted': { + 'description': localize('audioCues.diffLineDeleted', "Plays a sound when the focus moves to a deleted line in diff review mode"), + ...audioCueFeatureBase, + }, + 'audioCues.notebookCellCompleted': { + 'description': localize('audioCues.notebookCellCompleted', "Plays a sound when a notebook cell execution is successfully completed."), + ...audioCueFeatureBase, + }, + 'audioCues.notebookCellFailed': { + 'description': localize('audioCues.notebookCellFailed', "Plays a sound when a notebook cell execution fails."), + ...audioCueFeatureBase, + }, } }); diff --git a/src/vs/workbench/contrib/audioCues/browser/commands.ts b/src/vs/workbench/contrib/audioCues/browser/commands.ts index ab6b124bfce..834c481cc5d 100644 --- a/src/vs/workbench/contrib/audioCues/browser/commands.ts +++ b/src/vs/workbench/contrib/audioCues/browser/commands.ts @@ -7,9 +7,9 @@ import { Codicon } from 'vs/base/common/codicons'; import { localize } from 'vs/nls'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { Action2 } from 'vs/platform/actions/common/actions'; +import { AudioCue, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -import { AudioCue, IAudioCueService } from 'vs/workbench/contrib/audioCues/browser/audioCueService'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; export class ShowAudioCueHelp extends Action2 { diff --git a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts index 8e0108c20df..ac7a2b01767 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts @@ -38,6 +38,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ResourceEdit } from 'vs/editor/browser/services/bulkEditService'; import { ButtonBar } from 'vs/base/browser/ui/button/button'; import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { Mutable } from 'vs/base/common/types'; const enum State { Data = 'data', @@ -314,9 +315,6 @@ export class BulkEditPane extends ViewPane { } private async _openElementAsEditor(e: IOpenEvent): Promise { - type Mutable = { - -readonly [P in keyof T]: T[P] - }; const options: Mutable = { ...e.editorOptions }; let fileElement: FileElement; diff --git a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts index 016548cb48f..923da26be3b 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindWidget.ts @@ -6,7 +6,7 @@ import 'vs/css!./simpleFindWidget'; import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; -import { FindInput, IFindInputStyles } from 'vs/base/browser/ui/findinput/findInput'; +import { FindInput } from 'vs/base/browser/ui/findinput/findInput'; import { Widget } from 'vs/base/browser/ui/widget'; import { Delayer } from 'vs/base/common/async'; import { KeyCode } from 'vs/base/common/keyCodes'; @@ -15,8 +15,6 @@ import { IMessage as InputBoxMessage } from 'vs/base/browser/ui/inputbox/inputBo import { SimpleButton, findPreviousMatchIcon, findNextMatchIcon, NLS_NO_RESULTS, NLS_MATCHES_LOCATION } from 'vs/editor/contrib/find/browser/findWidget'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { inputActiveOptionBorder, inputActiveOptionBackground, inputActiveOptionForeground, inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, inputValidationInfoBackground, inputValidationInfoBorder, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningBorder, inputValidationWarningForeground } from 'vs/platform/theme/common/colorRegistry'; -import { IColorTheme } from 'vs/platform/theme/common/themeService'; import { ContextScopedFindInput } from 'vs/platform/history/browser/contextScopedHistoryWidget'; import { widgetClose } from 'vs/platform/theme/common/iconRegistry'; import * as strings from 'vs/base/common/strings'; @@ -24,6 +22,7 @@ import { TerminalCommandId } from 'vs/workbench/contrib/terminal/common/terminal import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { showHistoryKeybindingHint } from 'vs/platform/history/browser/historyWidgetKeybindingHint'; import { alert as alertFn } from 'vs/base/browser/ui/aria/aria'; +import { defaultInputBoxStyles, defaultToggleStyles } from 'vs/platform/theme/browser/defaultStyles'; const NLS_FIND_INPUT_LABEL = nls.localize('label.find', "Find"); const NLS_FIND_INPUT_PLACEHOLDER = nls.localize('placeholder.find', "Find (\u21C5 for history)"); @@ -88,7 +87,9 @@ export abstract class SimpleFindWidget extends Widget { appendCaseSensitiveLabel: options.appendCaseSensitiveLabel && options.type === 'Terminal' ? this._getKeybinding(TerminalCommandId.ToggleFindCaseSensitive) : undefined, appendRegexLabel: options.appendRegexLabel && options.type === 'Terminal' ? this._getKeybinding(TerminalCommandId.ToggleFindRegex) : undefined, appendWholeWordsLabel: options.appendWholeWordsLabel && options.type === 'Terminal' ? this._getKeybinding(TerminalCommandId.ToggleFindWholeWord) : undefined, - showHistoryHint: () => showHistoryKeybindingHint(_keybindingService) + showHistoryHint: () => showHistoryKeybindingHint(_keybindingService), + inputBoxStyles: defaultInputBoxStyles, + toggleStyles: defaultToggleStyles }, contextKeyService)); // Find History with update delayer this._updateHistoryDelayer = new Delayer(500); @@ -209,27 +210,6 @@ export abstract class SimpleFindWidget extends Widget { return this._focusTracker; } - public updateTheme(theme: IColorTheme): void { - const inputStyles: IFindInputStyles = { - inputActiveOptionBorder: theme.getColor(inputActiveOptionBorder), - inputActiveOptionForeground: theme.getColor(inputActiveOptionForeground), - inputActiveOptionBackground: theme.getColor(inputActiveOptionBackground), - inputBackground: theme.getColor(inputBackground), - inputForeground: theme.getColor(inputForeground), - inputBorder: theme.getColor(inputBorder), - inputValidationInfoBackground: theme.getColor(inputValidationInfoBackground), - inputValidationInfoForeground: theme.getColor(inputValidationInfoForeground), - inputValidationInfoBorder: theme.getColor(inputValidationInfoBorder), - inputValidationWarningBackground: theme.getColor(inputValidationWarningBackground), - inputValidationWarningForeground: theme.getColor(inputValidationWarningForeground), - inputValidationWarningBorder: theme.getColor(inputValidationWarningBorder), - inputValidationErrorBackground: theme.getColor(inputValidationErrorBackground), - inputValidationErrorForeground: theme.getColor(inputValidationErrorForeground), - inputValidationErrorBorder: theme.getColor(inputValidationErrorBorder) - }; - this._findInput.style(inputStyles); - } - private _getKeybinding(actionId: string): string { const kb = this._keybindingService?.lookupKeybinding(actionId); if (!kb) { diff --git a/src/vs/workbench/contrib/comments/browser/commentFormActions.ts b/src/vs/workbench/contrib/comments/browser/commentFormActions.ts index 8e5bed5afac..bda8197f14e 100644 --- a/src/vs/workbench/contrib/comments/browser/commentFormActions.ts +++ b/src/vs/workbench/contrib/comments/browser/commentFormActions.ts @@ -17,21 +17,23 @@ export class CommentFormActions implements IDisposable { constructor( private container: HTMLElement, private actionHandler: (action: IAction) => void, + private readonly maxActions?: number ) { } - setActions(menu: IMenu) { + setActions(menu: IMenu, hasOnlySecondaryActions: boolean = false) { this._toDispose.clear(); this._buttonElements.forEach(b => b.remove()); const groups = menu.getActions({ shouldForwardArgs: true }); - let isPrimary: boolean = true; + let isPrimary: boolean = !hasOnlySecondaryActions; for (const group of groups) { const [, actions] = group; this._actions = actions; for (const action of actions) { const button = new Button(this.container, { secondary: !isPrimary, ...defaultButtonStyles }); + isPrimary = false; this._buttonElements.push(button.element); @@ -40,6 +42,10 @@ export class CommentFormActions implements IDisposable { button.enabled = action.enabled; button.label = action.label; + if ((this.maxActions !== undefined) && (this._buttonElements.length >= this.maxActions)) { + console.warn(`An extension has contributed more than the allowable number of actions to a comments menu.`); + return; + } } } } diff --git a/src/vs/workbench/contrib/comments/browser/commentMenus.ts b/src/vs/workbench/contrib/comments/browser/commentMenus.ts index dae9f44956f..479e5156b12 100644 --- a/src/vs/workbench/contrib/comments/browser/commentMenus.ts +++ b/src/vs/workbench/contrib/comments/browser/commentMenus.ts @@ -23,6 +23,10 @@ export class CommentMenus implements IDisposable { return this.getMenu(MenuId.CommentThreadActions, contextKeyService); } + getCommentThreadAdditionalActions(contextKeyService: IContextKeyService): IMenu { + return this.getMenu(MenuId.CommentThreadAdditionalActions, contextKeyService); + } + getCommentTitleActions(comment: Comment, contextKeyService: IContextKeyService): IMenu { return this.getMenu(MenuId.CommentTitle, contextKeyService); } diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadAdditionalActions.ts b/src/vs/workbench/contrib/comments/browser/commentThreadAdditionalActions.ts new file mode 100644 index 00000000000..34366676669 --- /dev/null +++ b/src/vs/workbench/contrib/comments/browser/commentThreadAdditionalActions.ts @@ -0,0 +1,94 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dom from 'vs/base/browser/dom'; + +import { IAction } from 'vs/base/common/actions'; +import { IMenu, SubmenuItemAction } from 'vs/platform/actions/common/actions'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { MarshalledId } from 'vs/base/common/marshallingIds'; +import { IRange } from 'vs/editor/common/core/range'; +import * as languages from 'vs/editor/common/languages'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { CommentFormActions } from 'vs/workbench/contrib/comments/browser/commentFormActions'; +import { CommentMenus } from 'vs/workbench/contrib/comments/browser/commentMenus'; +import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange'; + +export class CommentThreadAdditionalActions extends Disposable { + private _container: HTMLElement | null; + private _buttonBar: HTMLElement | null; + private _commentFormActions!: CommentFormActions; + + constructor( + container: HTMLElement, + private _commentThread: languages.CommentThread, + private _contextKeyService: IContextKeyService, + private _commentMenus: CommentMenus, + private _actionRunDelegate: (() => void) | null, + ) { + super(); + + this._container = dom.append(container, dom.$('.comment-additional-actions')); + dom.append(this._container, dom.$('.section-separator')); + + this._buttonBar = dom.append(this._container, dom.$('.button-bar')); + this._createAdditionalActions(this._buttonBar); + } + + private _showMenu() { + this._container?.classList.remove('hidden'); + } + + private _hideMenu() { + this._container?.classList.add('hidden'); + } + + private _enableDisableMenu(menu: IMenu) { + const groups = menu.getActions({ shouldForwardArgs: true }); + + // Show the menu if at least one action is enabled. + for (const group of groups) { + const [, actions] = group; + for (const action of actions) { + if (action.enabled) { + this._showMenu(); + return; + } + + for (const subAction of (action as SubmenuItemAction).actions ?? []) { + if (subAction.enabled) { + this._showMenu(); + return; + } + } + } + } + + this._hideMenu(); + } + + + private _createAdditionalActions(container: HTMLElement) { + const menu = this._commentMenus.getCommentThreadAdditionalActions(this._contextKeyService); + this._register(menu); + this._register(menu.onDidChange(() => { + this._commentFormActions.setActions(menu, /*hasOnlySecondaryActions*/ true); + this._enableDisableMenu(menu); + })); + + this._commentFormActions = new CommentFormActions(container, async (action: IAction) => { + this._actionRunDelegate?.(); + + action.run({ + thread: this._commentThread, + $mid: MarshalledId.CommentThreadInstance + }); + }, 4); + + this._register(this._commentFormActions); + this._commentFormActions.setActions(menu, /*hasOnlySecondaryActions*/ true); + this._enableDisableMenu(menu); + } +} diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index d7af5c13258..8c07cd661fe 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -17,6 +17,7 @@ import { CommentReply } from 'vs/workbench/contrib/comments/browser/commentReply import { ICommentService } from 'vs/workbench/contrib/comments/browser/commentService'; import { CommentThreadBody } from 'vs/workbench/contrib/comments/browser/commentThreadBody'; import { CommentThreadHeader } from 'vs/workbench/contrib/comments/browser/commentThreadHeader'; +import { CommentThreadAdditionalActions } from 'vs/workbench/contrib/comments/browser/commentThreadAdditionalActions'; import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys'; import { ICommentThreadWidget } from 'vs/workbench/contrib/comments/common/commentThreadWidget'; import { IColorTheme } from 'vs/platform/theme/common/themeService'; @@ -35,6 +36,7 @@ export class CommentThreadWidget extends private _header!: CommentThreadHeader; private _body!: CommentThreadBody; private _commentReply?: CommentReply; + private _additionalActions?: CommentThreadAdditionalActions; private _commentMenus: CommentMenus; private _commentThreadDisposables: IDisposable[] = []; private _threadIsEmpty: IContextKey; @@ -177,6 +179,7 @@ export class CommentThreadWidget extends if (this._commentThread.canReply) { this._createCommentForm(); } + this._createAdditionalActions(); this._register(this._body.onDidResize(dimension => { this._refresh(dimension); @@ -239,6 +242,19 @@ export class CommentThreadWidget extends this._register(this._commentReply); } + private _createAdditionalActions() { + this._additionalActions = this._scopedInstatiationService.createInstance( + CommentThreadAdditionalActions, + this._body.container, + this._commentThread, + this._contextKeyService, + this._commentMenus, + this._containerDelegate.actionRunner, + ); + + this._register(this._additionalActions); + } + getCommentCoords(commentUniqueId: number) { return this._body.getCommentCoords(commentUniqueId); } diff --git a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts index da5c81f2b62..021e56c0edb 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsEditorContribution.ts @@ -698,7 +698,7 @@ export class CommentController implements IEditorContribution { return; } - const matchedNewCommentThreadZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && (zoneWidget.commentThread as any).commentThreadHandle === -1 && Range.equalsRange(zoneWidget.commentThread.range, thread.range)); + const matchedNewCommentThreadZones = this._commentWidgets.filter(zoneWidget => zoneWidget.owner === e.owner && zoneWidget.commentThread.commentThreadHandle === -1 && Range.equalsRange(zoneWidget.commentThread.range, thread.range)); if (matchedNewCommentThreadZones.length) { matchedNewCommentThreadZones[0].update(thread); diff --git a/src/vs/workbench/contrib/comments/browser/media/review.css b/src/vs/workbench/contrib/comments/browser/media/review.css index d26c07a67c4..ef33e562093 100644 --- a/src/vs/workbench/contrib/comments/browser/media/review.css +++ b/src/vs/workbench/contrib/comments/browser/media/review.css @@ -246,6 +246,42 @@ word-wrap: break-word; } + +.review-widget .body .comment-additional-actions { + margin: 10px 20px; +} + +.review-widget .body .comment-additional-actions .section-separator { + border-top: 1px solid var(--vscode-menu-separatorBackground); + margin: 14px 0; +} + +.review-widget .body .comment-additional-actions .button-bar { + display: flex; + white-space: nowrap; +} + +.review-widget .body .comment-additional-actions .monaco-button, +.review-widget .body .comment-additional-actions .monaco-text-button, +.review-widget .body .comment-additional-actions .monaco-button-dropdown { + display: flex; + width: auto; +} + +.review-widget .body .comment-additional-actions .button-bar>.monaco-text-button, +.review-widget .body .comment-additional-actions .button-bar>.monaco-button-dropdown { + margin: 0 10px 0 0; +} + +.review-widget .body .comment-additional-actions .button-bar .monaco-text-button { + padding: 4px 10px; +} + + +.review-widget .body .comment-additional-actions .codicon-drop-down-button { + align-items: center; +} + .review-widget .body .comment-form.expand .review-thread-reply-button { display: none; } @@ -295,7 +331,7 @@ .review-widget .body .comment-form .form-actions, .review-widget .body .edit-container .form-actions { overflow: auto; - padding: 10px 0; + margin: 10px 0; } .review-widget .body .edit-textarea { diff --git a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts index 378180562ba..479332bd147 100644 --- a/src/vs/workbench/contrib/debug/browser/baseDebugView.ts +++ b/src/vs/workbench/contrib/debug/browser/baseDebugView.ts @@ -16,8 +16,7 @@ import { KeyCode } from 'vs/base/common/keyCodes'; import { DisposableStore, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { localize } from 'vs/nls'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { defaultInputBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; import { IDebugService, IExpression, IExpressionContainer } from 'vs/workbench/contrib/debug/common/debug'; import { Expression, ExpressionContainer, Variable } from 'vs/workbench/contrib/debug/common/debugModel'; @@ -150,7 +149,6 @@ export abstract class AbstractExpressionsRenderer implements ITreeRenderer { diff --git a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index f278ea8a78d..57702c0b841 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -37,7 +37,7 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { WorkbenchList } from 'vs/platform/list/browser/listService'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; +import { defaultInputBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { ViewAction, ViewPane } from 'vs/workbench/browser/parts/views/viewPane'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; @@ -63,8 +63,8 @@ function createCheckbox(disposables: IDisposable[]): HTMLInputElement { } const MAX_VISIBLE_BREAKPOINTS = 9; -export function getExpandedBodySize(model: IDebugModel, countLimit: number): number { - const length = model.getBreakpoints().length + model.getExceptionBreakpoints().length + model.getFunctionBreakpoints().length + model.getDataBreakpoints().length + model.getInstructionBreakpoints().length; +export function getExpandedBodySize(model: IDebugModel, sessionId: string | undefined, countLimit: number): number { + const length = model.getBreakpoints().length + model.getExceptionBreakpointsForSession(sessionId).length + model.getFunctionBreakpoints().length + model.getDataBreakpoints().length + model.getInstructionBreakpoints().length; return Math.min(countLimit, length) * 22; } type BreakpointItem = IBreakpoint | IFunctionBreakpoint | IDataBreakpoint | IExceptionBreakpoint | IInstructionBreakpoint; @@ -117,7 +117,7 @@ export class BreakpointsView extends ViewPane { this.breakpointSupportsCondition = CONTEXT_BREAKPOINT_SUPPORTS_CONDITION.bindTo(contextKeyService); this.breakpointInputFocused = CONTEXT_BREAKPOINT_INPUT_FOCUSED.bindTo(contextKeyService); this._register(this.debugService.getModel().onDidChangeBreakpoints(() => this.onBreakpointsChange())); - this._register(this.debugService.getViewModel().onDidFocusSession(() => this.updateBreakpointsHint())); + this._register(this.debugService.getViewModel().onDidFocusSession(() => this.onBreakpointsChange())); this._register(this.debugService.onDidChangeState(() => this.onStateChange())); this.hintDelayer = this._register(new RunOnceScheduler(() => this.updateBreakpointsHint(true), 4000)); } @@ -132,10 +132,10 @@ export class BreakpointsView extends ViewPane { this.list = this.instantiationService.createInstance(WorkbenchList, 'Breakpoints', container, delegate, [ this.instantiationService.createInstance(BreakpointsRenderer, this.menu, this.breakpointSupportsCondition, this.breakpointItemType), new ExceptionBreakpointsRenderer(this.menu, this.breakpointSupportsCondition, this.breakpointItemType, this.debugService), - new ExceptionBreakpointInputRenderer(this, this.debugService, this.contextViewService, this.themeService), + new ExceptionBreakpointInputRenderer(this, this.debugService, this.contextViewService), this.instantiationService.createInstance(FunctionBreakpointsRenderer, this.menu, this.breakpointSupportsCondition, this.breakpointItemType), this.instantiationService.createInstance(DataBreakpointsRenderer), - new FunctionBreakpointInputRenderer(this, this.debugService, this.contextViewService, this.themeService, this.labelService), + new FunctionBreakpointInputRenderer(this, this.debugService, this.contextViewService, this.labelService), this.instantiationService.createInstance(InstructionBreakpointsRenderer), ], { identityProvider: { getId: (element: IEnablement) => element.getId() }, @@ -273,8 +273,9 @@ export class BreakpointsView extends ViewPane { const containerModel = this.viewDescriptorService.getViewContainerModel(this.viewDescriptorService.getViewContainerByViewId(this.id)!)!; // Adjust expanded body size - this.minimumBodySize = this.orientation === Orientation.VERTICAL ? getExpandedBodySize(this.debugService.getModel(), MAX_VISIBLE_BREAKPOINTS) : 170; - this.maximumBodySize = this.orientation === Orientation.VERTICAL && containerModel.visibleViewDescriptors.length > 1 ? getExpandedBodySize(this.debugService.getModel(), Number.POSITIVE_INFINITY) : Number.POSITIVE_INFINITY; + const sessionId = this.debugService.getViewModel().focusedSession?.getId(); + this.minimumBodySize = this.orientation === Orientation.VERTICAL ? getExpandedBodySize(this.debugService.getModel(), sessionId, MAX_VISIBLE_BREAKPOINTS) : 170; + this.maximumBodySize = this.orientation === Orientation.VERTICAL && containerModel.visibleViewDescriptors.length > 1 ? getExpandedBodySize(this.debugService.getModel(), sessionId, Number.POSITIVE_INFINITY) : Number.POSITIVE_INFINITY; } private updateBreakpointsHint(delayed = false): void { @@ -363,7 +364,8 @@ export class BreakpointsView extends ViewPane { private get elements(): BreakpointItem[] { const model = this.debugService.getModel(); - const elements = (>model.getExceptionBreakpoints()).concat(model.getFunctionBreakpoints()).concat(model.getDataBreakpoints()).concat(model.getBreakpoints()).concat(model.getInstructionBreakpoints()); + const sessionId = this.debugService.getViewModel().focusedSession?.getId(); + const elements = (>model.getExceptionBreakpointsForSession(sessionId)).concat(model.getFunctionBreakpoints()).concat(model.getDataBreakpoints()).concat(model.getBreakpoints()).concat(model.getInstructionBreakpoints()); return elements as BreakpointItem[]; } @@ -808,7 +810,6 @@ class FunctionBreakpointInputRenderer implements IListRenderer { template.updating = true; @@ -926,7 +925,6 @@ class ExceptionBreakpointInputRenderer implements IListRenderer { this.view.breakpointInputFocused.set(false); diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 99327f8edd6..8dd9418a7ad 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -152,8 +152,12 @@ export class DebugService implements IDebugService { this.disposables.add(this.viewModel.onDidFocusStackFrame(() => { this.onStateChange(); })); - this.disposables.add(this.viewModel.onDidFocusSession(() => { + this.disposables.add(this.viewModel.onDidFocusSession((session: IDebugSession | undefined) => { this.onStateChange(); + + if (session) { + this.setExceptionBreakpointFallbackSession(session.getId()); + } })); this.disposables.add(Event.any(this.adapterManager.onDidRegisterDebugger, this.configurationManager.onDidSelectConfiguration)(() => { const debugUxValue = (this.state !== State.Inactive || (this.configurationManager.getAllConfigurations().length > 0 && this.adapterManager.hasEnabledDebuggers())) ? 'default' : 'simple'; @@ -715,6 +719,8 @@ export class DebugService implements IDebugService { } } } + + this.model.removeExceptionBreakpointsForSession(session.getId()); })); } @@ -1046,8 +1052,13 @@ export class DebugService implements IDebugService { await this.sendInstructionBreakpoints(); } - setExceptionBreakpoints(data: DebugProtocol.ExceptionBreakpointsFilter[]): void { - this.model.setExceptionBreakpoints(data); + setExceptionBreakpointFallbackSession(sessionId: string) { + this.model.setExceptionBreakpointFallbackSession(sessionId); + this.debugStorage.storeBreakpoints(this.model); + } + + setExceptionBreakpointsForSession(session: IDebugSession, data: DebugProtocol.ExceptionBreakpointsFilter[]): void { + this.model.setExceptionBreakpointsForSession(session.getId(), data); this.debugStorage.storeBreakpoints(this.model); } @@ -1121,9 +1132,8 @@ export class DebugService implements IDebugService { } private sendExceptionBreakpoints(session?: IDebugSession): Promise { - const enabledExceptionBps = this.model.getExceptionBreakpoints().filter(exb => exb.enabled); - return sendToOneOrAllSessions(this.model, session, async s => { + const enabledExceptionBps = this.model.getExceptionBreakpointsForSession(s.getId()).filter(exb => exb.enabled); if (s.capabilities.supportsConfigurationDoneRequest && (!s.capabilities.exceptionBreakpointFilters || s.capabilities.exceptionBreakpointFilters.length === 0)) { // Only call `setExceptionBreakpoints` as specified in dap protocol #90001 return; diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 6296138005b..4cd3e34e260 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -312,7 +312,7 @@ export class DebugSession implements IDebugSession { this.initialized = true; this._onDidChangeState.fire(); - this.debugService.setExceptionBreakpoints((this.raw && this.raw.capabilities.exceptionBreakpointFilters) || []); + this.debugService.setExceptionBreakpointsForSession(this, (this.raw && this.raw.capabilities.exceptionBreakpointFilters) || []); } catch (err) { this.initialized = true; this._onDidChangeState.fire(); diff --git a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts index 646cf34625b..49b0b302764 100644 --- a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts @@ -569,7 +569,7 @@ export class RawDebugSession implements IDisposable { args.suspendDebuggee = suspendDebuggee; } - this.send('disconnect', args, undefined, 2000); + await this.send('disconnect', args, undefined, 2000); } catch (e) { // Catch the potential 'disconnect' error - no need to show it to the user since the adapter is shutting down } finally { diff --git a/src/vs/workbench/contrib/debug/browser/replViewer.ts b/src/vs/workbench/contrib/debug/browser/replViewer.ts index 9c4adf953f3..a6e3d3361dc 100644 --- a/src/vs/workbench/contrib/debug/browser/replViewer.ts +++ b/src/vs/workbench/contrib/debug/browser/replViewer.ts @@ -232,9 +232,8 @@ export class ReplVariablesRenderer extends AbstractExpressionsRenderer { private readonly linkDetector: LinkDetector, @IDebugService debugService: IDebugService, @IContextViewService contextViewService: IContextViewService, - @IThemeService themeService: IThemeService, ) { - super(debugService, contextViewService, themeService); + super(debugService, contextViewService); } protected renderExpression(expression: IExpression, data: IExpressionTemplateData, highlights: IHighlight[]): void { diff --git a/src/vs/workbench/contrib/debug/browser/variablesView.ts b/src/vs/workbench/contrib/debug/browser/variablesView.ts index 15ee525c464..be1f0d135ad 100644 --- a/src/vs/workbench/contrib/debug/browser/variablesView.ts +++ b/src/vs/workbench/contrib/debug/browser/variablesView.ts @@ -402,9 +402,8 @@ export class VariablesRenderer extends AbstractExpressionsRenderer { @IContextKeyService private readonly contextKeyService: IContextKeyService, @IDebugService debugService: IDebugService, @IContextViewService contextViewService: IContextViewService, - @IThemeService themeService: IThemeService, ) { - super(debugService, contextViewService, themeService); + super(debugService, contextViewService); } get templateId(): string { diff --git a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts index 702eadc54e1..352cf44e602 100644 --- a/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts +++ b/src/vs/workbench/contrib/debug/browser/watchExpressionsView.ts @@ -272,9 +272,8 @@ class WatchExpressionsRenderer extends AbstractExpressionsRenderer { @IContextKeyService private readonly contextKeyService: IContextKeyService, @IDebugService debugService: IDebugService, @IContextViewService contextViewService: IContextViewService, - @IThemeService themeService: IThemeService, ) { - super(debugService, contextViewService, themeService); + super(debugService, contextViewService); } get templateId() { diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 36f868fdfe6..53cae1e8955 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -600,7 +600,18 @@ export interface IDebugModel extends ITreeElement { areBreakpointsActivated(): boolean; getFunctionBreakpoints(): ReadonlyArray; getDataBreakpoints(): ReadonlyArray; + + /** + * Returns list of all exception breakpoints. + */ getExceptionBreakpoints(): ReadonlyArray; + + /** + * Returns list of exception breakpoints for the given session + * @param sessionId Session id. If falsy, returns the breakpoints from the last set fallback session. + */ + getExceptionBreakpointsForSession(sessionId?: string): ReadonlyArray; + getInstructionBreakpoints(): ReadonlyArray; getWatchExpressions(): ReadonlyArray; @@ -1054,7 +1065,7 @@ export interface IDebugService { setExceptionBreakpointCondition(breakpoint: IExceptionBreakpoint, condition: string | undefined): Promise; - setExceptionBreakpoints(data: DebugProtocol.ExceptionBreakpointsFilter[]): void; + setExceptionBreakpointsForSession(session: IDebugSession, data: DebugProtocol.ExceptionBreakpointsFilter[]): void; /** * Sends all breakpoints to the passed session. diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index 1f50eb36368..812167ff8db 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -1056,6 +1056,8 @@ export class DataBreakpoint extends BaseBreakpoint implements IDataBreakpoint { export class ExceptionBreakpoint extends BaseBreakpoint implements IExceptionBreakpoint { + private supportedSessions: Set = new Set(); + constructor( public readonly filter: string, public readonly label: string, @@ -1063,7 +1065,8 @@ export class ExceptionBreakpoint extends BaseBreakpoint implements IExceptionBre public readonly supportsCondition: boolean, condition: string | undefined, public readonly description: string | undefined, - public readonly conditionDescription: string | undefined + public readonly conditionDescription: string | undefined, + private fallback: boolean = false ) { super(enabled, undefined, condition, undefined, generateUuid()); } @@ -1075,14 +1078,44 @@ export class ExceptionBreakpoint extends BaseBreakpoint implements IExceptionBre result.enabled = this.enabled; result.supportsCondition = this.supportsCondition; result.condition = this.condition; + result.fallback = this.fallback; return result; } + setSupportedSession(sessionId: string, supported: boolean): void { + if (supported) { + this.supportedSessions.add(sessionId); + } + else { + this.supportedSessions.delete(sessionId); + } + } + + /** + * Used to specify which breakpoints to show when no session is specified. + * Useful when no session is active and we want to show the exception breakpoints from the last session. + */ + setFallback(isFallback: boolean) { + this.fallback = isFallback; + } + get supported(): boolean { return true; } + /** + * Checks if the breakpoint is applicable for the specified session. + * If sessionId is undefined, returns true if this breakpoint is a fallback breakpoint. + */ + isSupportedSession(sessionId?: string): boolean { + return sessionId ? this.supportedSessions.has(sessionId) : this.fallback; + } + + matches(filter: DebugProtocol.ExceptionBreakpointsFilter) { + return this.filter === filter.filter && this.label === filter.label && this.supportsCondition === !!filter.supportsCondition && this.conditionDescription === filter.conditionDescription && this.description === filter.description; + } + override toString(): string { return this.label; } @@ -1340,26 +1373,45 @@ export class DebugModel implements IDebugModel { return this.exceptionBreakpoints; } + getExceptionBreakpointsForSession(sessionId?: string): IExceptionBreakpoint[] { + return this.exceptionBreakpoints.filter(ebp => ebp.isSupportedSession(sessionId)); + } + getInstructionBreakpoints(): IInstructionBreakpoint[] { return this.instructionBreakpoints; } - setExceptionBreakpoints(data: DebugProtocol.ExceptionBreakpointsFilter[]): void { + setExceptionBreakpointsForSession(sessionId: string, data: DebugProtocol.ExceptionBreakpointsFilter[]): void { if (data) { - if (this.exceptionBreakpoints.length === data.length && this.exceptionBreakpoints.every((exbp, i) => - exbp.filter === data[i].filter && exbp.label === data[i].label && exbp.supportsCondition === data[i].supportsCondition && exbp.conditionDescription === data[i].conditionDescription && exbp.description === data[i].description)) { - // No change - return; - } + let didChangeBreakpoints = false; + data.forEach(d => { + let ebp = this.exceptionBreakpoints.filter((exbp) => exbp.matches(d)).pop(); - this.exceptionBreakpoints = data.map(d => { - const ebp = this.exceptionBreakpoints.filter(ebp => ebp.filter === d.filter).pop(); - return new ExceptionBreakpoint(d.filter, d.label, ebp ? ebp.enabled : !!d.default, !!d.supportsCondition, ebp?.condition, d.description, d.conditionDescription); + if (!ebp) { + didChangeBreakpoints = true; + ebp = new ExceptionBreakpoint(d.filter, d.label, !!d.default, !!d.supportsCondition, undefined /* condition */, d.description, d.conditionDescription); + this.exceptionBreakpoints.push(ebp); + } + + ebp.setSupportedSession(sessionId, true); }); - this._onDidChangeBreakpoints.fire(undefined); + + if (didChangeBreakpoints) { + this._onDidChangeBreakpoints.fire(undefined); + } } } + removeExceptionBreakpointsForSession(sessionId: string): void { + this.exceptionBreakpoints.forEach(ebp => ebp.setSupportedSession(sessionId, false)); + } + + // Set last focused session as fallback session. + // This is done to keep track of the exception breakpoints to show when no session is active. + setExceptionBreakpointFallbackSession(sessionId: string): void { + this.exceptionBreakpoints.forEach(ebp => ebp.setFallback(ebp.isSupportedSession(sessionId))); + } + setExceptionBreakpointCondition(exceptionBreakpoint: IExceptionBreakpoint, condition: string | undefined): void { (exceptionBreakpoint as ExceptionBreakpoint).condition = condition; this._onDidChangeBreakpoints.fire(undefined); diff --git a/src/vs/workbench/contrib/debug/common/debugStorage.ts b/src/vs/workbench/contrib/debug/common/debugStorage.ts index 53ba25115bd..639fc19078b 100644 --- a/src/vs/workbench/contrib/debug/common/debugStorage.ts +++ b/src/vs/workbench/contrib/debug/common/debugStorage.ts @@ -59,7 +59,7 @@ export class DebugStorage { let result: ExceptionBreakpoint[] | undefined; try { result = JSON.parse(this.storageService.get(DEBUG_EXCEPTION_BREAKPOINTS_KEY, StorageScope.WORKSPACE, '[]')).map((exBreakpoint: any) => { - return new ExceptionBreakpoint(exBreakpoint.filter, exBreakpoint.label, exBreakpoint.enabled, exBreakpoint.supportsCondition, exBreakpoint.condition, exBreakpoint.description, exBreakpoint.conditionDescription); + return new ExceptionBreakpoint(exBreakpoint.filter, exBreakpoint.label, exBreakpoint.enabled, exBreakpoint.supportsCondition, exBreakpoint.condition, exBreakpoint.description, exBreakpoint.conditionDescription, !!exBreakpoint.fallback); }); } catch (e) { } diff --git a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts index dc1df27c873..c3d4af019fa 100644 --- a/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/breakpoints.test.ts @@ -100,10 +100,10 @@ suite('Debug - Breakpoints', () => { const modelUri1 = uri.file('/myfolder/my file first.js'); const modelUri2 = uri.file('/secondfolder/second/second file.js'); addBreakpointsAndCheckEvents(model, modelUri1, [{ lineNumber: 5, enabled: true }, { lineNumber: 10, enabled: false }]); - assert.strictEqual(getExpandedBodySize(model, 9), 44); + assert.strictEqual(getExpandedBodySize(model, undefined, 9), 44); addBreakpointsAndCheckEvents(model, modelUri2, [{ lineNumber: 1, enabled: true }, { lineNumber: 2, enabled: true }, { lineNumber: 3, enabled: false }]); - assert.strictEqual(getExpandedBodySize(model, 9), 110); + assert.strictEqual(getExpandedBodySize(model, undefined, 9), 110); assert.strictEqual(model.getBreakpoints().length, 5); assert.strictEqual(model.getBreakpoints({ uri: modelUri1 }).length, 2); @@ -137,7 +137,7 @@ suite('Debug - Breakpoints', () => { assert.strictEqual(bp.enabled, true); model.removeBreakpoints(model.getBreakpoints({ uri: modelUri1 })); - assert.strictEqual(getExpandedBodySize(model, 9), 66); + assert.strictEqual(getExpandedBodySize(model, undefined, 9), 66); assert.strictEqual(model.getBreakpoints().length, 3); }); @@ -213,22 +213,75 @@ suite('Debug - Breakpoints', () => { test('exception breakpoints', () => { let eventCount = 0; model.onDidChangeBreakpoints(() => eventCount++); - model.setExceptionBreakpoints([{ filter: 'uncaught', label: 'UNCAUGHT', default: true }]); + model.setExceptionBreakpointsForSession("session-id-1", [{ filter: 'uncaught', label: 'UNCAUGHT', default: true }]); assert.strictEqual(eventCount, 1); - let exceptionBreakpoints = model.getExceptionBreakpoints(); + let exceptionBreakpoints = model.getExceptionBreakpointsForSession("session-id-1"); assert.strictEqual(exceptionBreakpoints.length, 1); assert.strictEqual(exceptionBreakpoints[0].filter, 'uncaught'); assert.strictEqual(exceptionBreakpoints[0].enabled, true); - model.setExceptionBreakpoints([{ filter: 'uncaught', label: 'UNCAUGHT' }, { filter: 'caught', label: 'CAUGHT' }]); + model.setExceptionBreakpointsForSession("session-id-2", [{ filter: 'uncaught', label: 'UNCAUGHT' }, { filter: 'caught', label: 'CAUGHT' }]); assert.strictEqual(eventCount, 2); - exceptionBreakpoints = model.getExceptionBreakpoints(); + exceptionBreakpoints = model.getExceptionBreakpointsForSession("session-id-2"); assert.strictEqual(exceptionBreakpoints.length, 2); assert.strictEqual(exceptionBreakpoints[0].filter, 'uncaught'); assert.strictEqual(exceptionBreakpoints[0].enabled, true); assert.strictEqual(exceptionBreakpoints[1].filter, 'caught'); assert.strictEqual(exceptionBreakpoints[1].label, 'CAUGHT'); assert.strictEqual(exceptionBreakpoints[1].enabled, false); + + model.setExceptionBreakpointsForSession("session-id-3", [{ filter: 'all', label: 'ALL' }]); + assert.strictEqual(eventCount, 3); + assert.strictEqual(model.getExceptionBreakpointsForSession("session-id-3").length, 1); + exceptionBreakpoints = model.getExceptionBreakpoints(); + assert.strictEqual(exceptionBreakpoints[0].filter, 'uncaught'); + assert.strictEqual(exceptionBreakpoints[0].enabled, true); + assert.strictEqual(exceptionBreakpoints[1].filter, 'caught'); + assert.strictEqual(exceptionBreakpoints[1].label, 'CAUGHT'); + assert.strictEqual(exceptionBreakpoints[1].enabled, false); + assert.strictEqual(exceptionBreakpoints[2].filter, 'all'); + assert.strictEqual(exceptionBreakpoints[2].label, 'ALL'); + }); + + test('exception breakpoints multiple sessions', () => { + let eventCount = 0; + model.onDidChangeBreakpoints(() => eventCount++); + + model.setExceptionBreakpointsForSession("session-id-4", [{ filter: 'uncaught', label: 'UNCAUGHT', default: true }, { filter: 'caught', label: 'CAUGHT' }]); + model.setExceptionBreakpointFallbackSession("session-id-4"); + assert.strictEqual(eventCount, 1); + let exceptionBreakpointsForSession = model.getExceptionBreakpointsForSession("session-id-4"); + assert.strictEqual(exceptionBreakpointsForSession.length, 2); + assert.strictEqual(exceptionBreakpointsForSession[0].filter, 'uncaught'); + assert.strictEqual(exceptionBreakpointsForSession[1].filter, 'caught'); + + model.setExceptionBreakpointsForSession("session-id-5", [{ filter: 'all', label: 'ALL' }, { filter: 'caught', label: 'CAUGHT' }]); + assert.strictEqual(eventCount, 2); + exceptionBreakpointsForSession = model.getExceptionBreakpointsForSession("session-id-5"); + let exceptionBreakpointsForUndefined = model.getExceptionBreakpointsForSession(undefined); + assert.strictEqual(exceptionBreakpointsForSession.length, 2); + assert.strictEqual(exceptionBreakpointsForSession[0].filter, 'caught'); + assert.strictEqual(exceptionBreakpointsForSession[1].filter, 'all'); + assert.strictEqual(exceptionBreakpointsForUndefined.length, 2); + assert.strictEqual(exceptionBreakpointsForUndefined[0].filter, 'uncaught'); + assert.strictEqual(exceptionBreakpointsForUndefined[1].filter, 'caught'); + + model.removeExceptionBreakpointsForSession("session-id-4"); + assert.strictEqual(eventCount, 2); + exceptionBreakpointsForUndefined = model.getExceptionBreakpointsForSession(undefined); + assert.strictEqual(exceptionBreakpointsForUndefined.length, 2); + assert.strictEqual(exceptionBreakpointsForUndefined[0].filter, 'uncaught'); + assert.strictEqual(exceptionBreakpointsForUndefined[1].filter, 'caught'); + + model.setExceptionBreakpointFallbackSession("session-id-5"); + assert.strictEqual(eventCount, 2); + exceptionBreakpointsForUndefined = model.getExceptionBreakpointsForSession(undefined); + assert.strictEqual(exceptionBreakpointsForUndefined.length, 2); + assert.strictEqual(exceptionBreakpointsForUndefined[0].filter, 'caught'); + assert.strictEqual(exceptionBreakpointsForUndefined[1].filter, 'all'); + + const exceptionBreakpoints = model.getExceptionBreakpoints(); + assert.strictEqual(exceptionBreakpoints.length, 3); }); test('instruction breakpoints', () => { diff --git a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts index b60984f5735..fe909c8daaa 100644 --- a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts +++ b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts @@ -93,7 +93,7 @@ export class MockDebugService implements IDebugService { throw new Error('Method not implemented.'); } - setExceptionBreakpoints(data: DebugProtocol.ExceptionBreakpointsFilter[]): void { + setExceptionBreakpointsForSession(session: IDebugSession, data: DebugProtocol.ExceptionBreakpointsFilter[]): void { throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index e146eafbd1c..55c46a9095f 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -69,7 +69,7 @@ import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/b import { ViewContainerLocation } from 'vs/workbench/common/views'; import { IExtensionGalleryService, IGalleryExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; import * as semver from 'vs/base/common/semver/semver'; -import { getKeybindingLabelStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { defaultKeybindingLabelStyles } from 'vs/platform/theme/browser/defaultStyles'; class NavBar extends Disposable { @@ -1556,7 +1556,7 @@ export class ExtensionEditor extends EditorPane { const renderKeybinding = (keybinding: ResolvedKeybinding): HTMLElement => { const element = $(''); - const kbl = new KeybindingLabel(element, OS, getKeybindingLabelStyles()); + const kbl = new KeybindingLabel(element, OS, defaultKeybindingLabelStyles); kbl.set(keybinding); return element; }; diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 9cc0159b382..8e3d0624a18 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -23,7 +23,6 @@ import { IFilesConfiguration, UndoConfirmLevel } from 'vs/workbench/contrib/file import { dirname, joinPath, distinctParents } from 'vs/base/common/resources'; import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; import { localize } from 'vs/nls'; -import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; import { once } from 'vs/base/common/functional'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { equals, deepClone } from 'vs/base/common/objects'; @@ -61,6 +60,7 @@ import { WebFileSystemAccess } from 'vs/platform/files/browser/webFileSystemAcce import { IgnoreFile } from 'vs/workbench/services/search/common/ignoreFile'; import { ResourceSet } from 'vs/base/common/map'; import { TernarySearchTree } from 'vs/base/common/ternarySearchTree'; +import { defaultInputBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; export class ExplorerDelegate implements IListVirtualDelegate { @@ -467,9 +467,9 @@ export class FilesRenderer implements ICompressibleTreeRenderer { done(inputBox.isInputValid(), true); }), - label, - styler + label ]; return toDisposable(() => { diff --git a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts index 86fb1f94e9b..6b83ad251b3 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts @@ -11,7 +11,7 @@ import { workbenchInstantiationService, TestServiceAccessor, getLastResolvedFile import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IEditorFactoryRegistry, Verbosity, EditorExtensions, EditorInputCapabilities } from 'vs/workbench/common/editor'; import { EncodingMode, TextFileOperationError, TextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; -import { FileOperationResult, FileOperationError, NotModifiedSinceFileOperationError, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; +import { FileOperationResult, FileOperationError, NotModifiedSinceFileOperationError } from 'vs/platform/files/common/files'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { timeout } from 'vs/base/common/async'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; @@ -127,11 +127,10 @@ suite('Files - FileEditorInput', () => { test('reports as readonly with readonly file scheme', async function () { - class ReadonlyInMemoryFileSystemProvider extends InMemoryFileSystemProvider { - override readonly capabilities: FileSystemProviderCapabilities = FileSystemProviderCapabilities.Readonly; - } + const inMemoryFilesystemProvider = new InMemoryFileSystemProvider(); + inMemoryFilesystemProvider.setReadOnly(true); - const disposable = accessor.fileService.registerProvider('someTestingReadonlyScheme', new ReadonlyInMemoryFileSystemProvider()); + const disposable = accessor.fileService.registerProvider('someTestingReadonlyScheme', inMemoryFilesystemProvider); try { const input = createFileInput(toResource.call(this, '/foo/bar/file.js').with({ scheme: 'someTestingReadonlyScheme' })); diff --git a/src/vs/workbench/contrib/inlayHints/browser/inlayHintsAccessibilty.ts b/src/vs/workbench/contrib/inlayHints/browser/inlayHintsAccessibilty.ts index 1cd411eb22c..38ea286744c 100644 --- a/src/vs/workbench/contrib/inlayHints/browser/inlayHintsAccessibilty.ts +++ b/src/vs/workbench/contrib/inlayHints/browser/inlayHintsAccessibilty.ts @@ -15,11 +15,11 @@ import { InlayHintItem, asCommandLink } from 'vs/editor/contrib/inlayHints/brows import { InlayHintsController } from 'vs/editor/contrib/inlayHints/browser/inlayHintsController'; import { localize } from 'vs/nls'; import { registerAction2 } from 'vs/platform/actions/common/actions'; +import { AudioCue, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { Link } from 'vs/platform/opener/browser/link'; -import { AudioCue, IAudioCueService } from 'vs/workbench/contrib/audioCues/browser/audioCueService'; export class InlayHintsAccessibility implements IEditorContribution { diff --git a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.css b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.css new file mode 100644 index 00000000000..491aa3e8c17 --- /dev/null +++ b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.css @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.interactive-editor .input-cell-container:focus-within .input-editor-container .monaco-editor { + outline: solid 1px var(--vscode-notebook-focusedCellBorder); +} + +.interactive-editor .input-cell-container .input-editor-container .monaco-editor { + outline: solid 1px var(--vscode-notebook-inactiveFocusedCellBorder); +} + +.interactive-editor .input-cell-container .input-focus-indicator { + top: 8px; +} + +.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); +} diff --git a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts index 86efcc7fdc3..cae59e4ac5a 100644 --- a/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts +++ b/src/vs/workbench/contrib/interactive/browser/interactiveEditor.ts @@ -16,8 +16,8 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { editorBackground, editorForeground, resolveColorValue } from 'vs/platform/theme/common/colorRegistry'; -import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { editorForeground, resolveColorValue } from 'vs/platform/theme/common/colorRegistry'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { EditorPaneSelectionChangeReason, IEditorMemento, IEditorOpenContext, IEditorPaneSelectionChangeEvent } from 'vs/workbench/common/editor'; import { getSimpleEditorOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; @@ -25,7 +25,7 @@ import { InteractiveEditorInput } from 'vs/workbench/contrib/interactive/browser import { ICellViewModel, INotebookEditorOptions, INotebookEditorViewState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; import { IBorrowValue, INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService'; -import { cellEditorBackground, NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; +import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; import { GroupsOrder, IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ExecutionStateCellStatusBarContrib, TimerCellStatusBarContrib } from 'vs/workbench/contrib/notebook/browser/contrib/cellStatusBar/executionStatusBarItemController'; import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; @@ -60,6 +60,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { isEqual } from 'vs/base/common/resources'; import { NotebookFindContrib } from 'vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget'; import { INTERACTIVE_WINDOW_EDITOR_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import 'vs/css!./interactiveEditor'; const DECORATION_KEY = 'interactiveInputDecoration'; const INTERACTIVE_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'InteractiveEditorViewState'; @@ -683,25 +684,3 @@ export class InteractiveEditor extends EditorPane { }; } } - -registerThemingParticipant((theme, collector) => { - collector.addRule(` - .interactive-editor .input-cell-container:focus-within .input-editor-container .monaco-editor { - outline: solid 1px var(--vscode-notebook-focusedCellBorder); - } - .interactive-editor .input-cell-container .input-editor-container .monaco-editor { - outline: solid 1px var(--vscode-notebook-inactiveFocusedCellBorder); - } - .interactive-editor .input-cell-container .input-focus-indicator { - top: ${INPUT_CELL_VERTICAL_PADDING}px; - } - `); - - const editorBackgroundColor = theme.getColor(cellEditorBackground) ?? theme.getColor(editorBackground); - if (editorBackgroundColor) { - collector.addRule(`.interactive-editor .input-cell-container .monaco-editor-background, - .interactive-editor .input-cell-container .margin-view-overlays { - background: ${editorBackgroundColor}; - }`); - } -}); diff --git a/src/vs/workbench/contrib/logs/common/logConstants.ts b/src/vs/workbench/contrib/logs/common/logConstants.ts index 49139de524d..a9869d06fc7 100644 --- a/src/vs/workbench/contrib/logs/common/logConstants.ts +++ b/src/vs/workbench/contrib/logs/common/logConstants.ts @@ -10,7 +10,6 @@ export const telemetryLogChannelId = 'telemetryLog'; export const extensionTelemetryLogChannelId = 'extensionTelemetryLog'; export const userDataSyncLogChannelId = 'userDataSyncLog'; export const editSessionsLogChannelId = 'editSessionsSyncLog'; -export const remoteTunnelLogChannelId = 'remoteTunnelLog'; export const remoteServerLog = 'remoteServerLog'; export const remotePtyHostLog = 'remotePtyHostLog'; diff --git a/src/vs/workbench/contrib/logs/common/logs.contribution.ts b/src/vs/workbench/contrib/logs/common/logs.contribution.ts index ca6f046c7cf..07f420f6731 100644 --- a/src/vs/workbench/contrib/logs/common/logs.contribution.ts +++ b/src/vs/workbench/contrib/logs/common/logs.contribution.ts @@ -50,7 +50,6 @@ class LogOutputChannels extends Disposable implements IWorkbenchContribution { private registerCommonContributions(): void { this.registerLogChannel(Constants.userDataSyncLogChannelId, nls.localize('userDataSyncLog', "Settings Sync"), this.environmentService.userDataSyncLogResource); this.registerLogChannel(Constants.editSessionsLogChannelId, nls.localize('editSessionsLog', "Edit Sessions"), this.environmentService.editSessionsLogResource); - this.registerLogChannel(Constants.remoteTunnelLogChannelId, nls.localize('remoteTunnelLog', "Remote Tunnel"), this.environmentService.remoteTunnelLogResource); this.registerLogChannel(Constants.rendererLogChannelId, nls.localize('rendererLog', "Window"), this.environmentService.logFile); const registerTelemetryChannel = () => { diff --git a/src/vs/workbench/contrib/markers/browser/markersView.ts b/src/vs/workbench/contrib/markers/browser/markersView.ts index f54ff823334..1eea4afe508 100644 --- a/src/vs/workbench/contrib/markers/browser/markersView.ts +++ b/src/vs/workbench/contrib/markers/browser/markersView.ts @@ -790,6 +790,7 @@ export class MarkersView extends FilterViewPane implements IMarkersView { this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor!, menuId: MenuId.ProblemsPanelContext, + contextKeyService: this.widget.contextKeyService, getActions: () => this.getMenuActions(element), getActionViewItem: (action) => { const keybinding = this.keybindingService.lookupKeybinding(action.id); diff --git a/src/vs/workbench/contrib/markers/browser/markersViewActions.css b/src/vs/workbench/contrib/markers/browser/markersViewActions.css new file mode 100644 index 00000000000..83ca947571d --- /dev/null +++ b/src/vs/workbench/contrib/markers/browser/markersViewActions.css @@ -0,0 +1,10 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.markers-panel-action-filter > .markers-panel-filter-controls > .monaco-action-bar .action-label.markers-filters.checked { + border-color: var(--vscode-inputOption-activeBorder); + color: var(--vscode-inputOption-activeForeground); + background-color: var(--vscode-inputOption-activeBackground); +} diff --git a/src/vs/workbench/contrib/markers/browser/markersViewActions.ts b/src/vs/workbench/contrib/markers/browser/markersViewActions.ts index 65d23126b72..caf421e2931 100644 --- a/src/vs/workbench/contrib/markers/browser/markersViewActions.ts +++ b/src/vs/workbench/contrib/markers/browser/markersViewActions.ts @@ -7,15 +7,14 @@ import * as DOM from 'vs/base/browser/dom'; import { Action, IAction } from 'vs/base/common/actions'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import Messages from 'vs/workbench/contrib/markers/browser/messages'; -import { registerThemingParticipant, ICssStyleCollector, IColorTheme } from 'vs/platform/theme/common/themeService'; import { Disposable } from 'vs/base/common/lifecycle'; -import { inputActiveOptionBorder, inputActiveOptionBackground, inputActiveOptionForeground } from 'vs/platform/theme/common/colorRegistry'; import { Marker } from 'vs/workbench/contrib/markers/browser/markersModel'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { Event, Emitter } from 'vs/base/common/event'; import { Codicon } from 'vs/base/common/codicons'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { MarkersContextKeys } from 'vs/workbench/contrib/markers/common/markers'; +import 'vs/css!./markersViewActions'; export interface IMarkersFiltersChangeEvent { excludedFiles?: boolean; @@ -174,17 +173,3 @@ export class QuickFixActionViewItem extends ActionViewItem { } } -registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { - const inputActiveOptionBorderColor = theme.getColor(inputActiveOptionBorder); - if (inputActiveOptionBorderColor) { - collector.addRule(`.markers-panel-action-filter > .markers-panel-filter-controls > .monaco-action-bar .action-label.markers-filters.checked { border-color: ${inputActiveOptionBorderColor}; }`); - } - const inputActiveOptionForegroundColor = theme.getColor(inputActiveOptionForeground); - if (inputActiveOptionForegroundColor) { - collector.addRule(`.markers-panel-action-filter > .markers-panel-filter-controls > .monaco-action-bar .action-label.markers-filters.checked { color: ${inputActiveOptionForegroundColor}; }`); - } - const inputActiveOptionBackgroundColor = theme.getColor(inputActiveOptionBackground); - if (inputActiveOptionBackgroundColor) { - collector.addRule(`.markers-panel-action-filter > .markers-panel-filter-controls > .monaco-action-bar .action-label.markers-filters.checked { background-color: ${inputActiveOptionBackgroundColor}; }`); - } -}); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/editing.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/editing.ts index de2e9e2c6df..57775b6e7aa 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/model/editing.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/model/editing.ts @@ -5,7 +5,7 @@ import { equals } from 'vs/base/common/arrays'; import { Range } from 'vs/editor/common/core/range'; -import { ITextModel } from 'vs/editor/common/model'; +import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; import { LineRange } from './lineRange'; /** @@ -22,8 +22,8 @@ export class LineRangeEdit { return this.range.equals(other.range) && equals(this.newLines, other.newLines); } - public apply(model: ITextModel): void { - new LineEdits([this]).apply(model); + public toEdits(modelLineCount: number): IIdentifiedSingleEditOperation[] { + return new LineEdits([this]).toEdits(modelLineCount); } } @@ -41,30 +41,26 @@ export class RangeEdit { export class LineEdits { constructor(public readonly edits: readonly LineRangeEdit[]) { } - public apply(model: ITextModel): void { - model.pushEditOperations( - null, - this.edits.map((e) => { - if (e.range.endLineNumberExclusive <= model.getLineCount()) { - return { - range: new Range(e.range.startLineNumber, 1, e.range.endLineNumberExclusive, 1), - text: e.newLines.map(s => s + '\n').join(''), - }; - } - - if (e.range.startLineNumber === 1) { - return { - range: new Range(1, 1, model.getLineCount(), Number.MAX_SAFE_INTEGER), - text: e.newLines.join('\n'), - }; - } - + public toEdits(modelLineCount: number): IIdentifiedSingleEditOperation[] { + return this.edits.map((e) => { + if (e.range.endLineNumberExclusive <= modelLineCount) { return { - range: new Range(e.range.startLineNumber - 1, Number.MAX_SAFE_INTEGER, model.getLineCount(), Number.MAX_SAFE_INTEGER), - text: e.newLines.map(s => '\n' + s).join(''), + range: new Range(e.range.startLineNumber, 1, e.range.endLineNumberExclusive, 1), + text: e.newLines.map(s => s + '\n').join(''), }; - }), - () => null - ); + } + + if (e.range.startLineNumber === 1) { + return { + range: new Range(1, 1, modelLineCount, Number.MAX_SAFE_INTEGER), + text: e.newLines.join('\n'), + }; + } + + return { + range: new Range(e.range.startLineNumber - 1, Number.MAX_SAFE_INTEGER, modelLineCount, Number.MAX_SAFE_INTEGER), + text: e.newLines.map(s => '\n' + s).join(''), + }; + }); } } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts index e3bbbd7c81f..7d8f404b794 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/model/mergeEditorModel.ts @@ -6,10 +6,13 @@ import { CompareResult, equals } from 'vs/base/common/arrays'; import { BugIndicatingError } from 'vs/base/common/errors'; import { autorunHandleChanges, derived, IObservable, IReader, ISettableObservable, ITransaction, keepAlive, observableValue, transaction, waitForState } from 'vs/base/common/observable'; +import { URI } from 'vs/base/common/uri'; import { Range } from 'vs/editor/common/core/range'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { ITextModel } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/model'; +import { localize } from 'vs/nls'; +import { IResourceUndoRedoElement, IUndoRedoService, UndoRedoElementType, UndoRedoGroup } from 'vs/platform/undoRedo/common/undoRedo'; import { EditorModel } from 'vs/workbench/common/editor/editorModel'; import { IMergeDiffComputer } from 'vs/workbench/contrib/mergeEditor/browser/model/diffComputer'; import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRange'; @@ -58,6 +61,7 @@ export class MergeEditorModel extends EditorModel { public readonly telemetry: MergeEditorTelemetry, @IModelService private readonly modelService: IModelService, @ILanguageService private readonly languageService: ILanguageService, + @IUndoRedoService private readonly undoRedoService: IUndoRedoService, ) { super(); @@ -405,9 +409,9 @@ export class MergeEditorModel extends EditorModel { public setState( baseRange: ModifiedBaseRange, state: ModifiedBaseRangeState, - markInputAsHandled: boolean | InputNumber, - transaction: ITransaction, - pushStackElement: boolean = false + _markInputAsHandled: boolean | InputNumber, + tx: ITransaction, + _pushStackElement: boolean = false ): void { if (!this.isUpToDate.get()) { throw new BugIndicatingError('Cannot set state while updating'); @@ -421,29 +425,36 @@ export class MergeEditorModel extends EditorModel { const conflictingDiffs = this.resultTextModelDiffs.findTouchingDiffs( baseRange.baseRange ); + const group = new UndoRedoGroup(); if (conflictingDiffs) { - this.resultTextModelDiffs.removeDiffs(conflictingDiffs, transaction); + this.resultTextModelDiffs.removeDiffs(conflictingDiffs, tx, group); } const { edit, effectiveState } = baseRange.getEditForBase(state); - existingState.accepted.set(effectiveState, transaction); + existingState.accepted.set(effectiveState, tx); existingState.previousNonDiffingState = undefined; existingState.computedFromDiffing = false; + const input1Handled = existingState.handledInput1.get(); + const input2Handled = existingState.handledInput2.get(); + + if (!input1Handled || !input2Handled) { + this.undoRedoService.pushElement( + new MarkAsHandledUndoRedoElement(this.resultTextModel.uri, new WeakRef(this), new WeakRef(existingState), input1Handled, input2Handled), + group + ); + } + if (edit) { - if (pushStackElement) { - this.resultTextModel.pushStackElement(); - } - this.resultTextModelDiffs.applyEditRelativeToOriginal(edit, transaction); - if (pushStackElement) { - this.resultTextModel.pushStackElement(); - } + this.resultTextModel.pushStackElement(); + this.resultTextModelDiffs.applyEditRelativeToOriginal(edit, tx, group); + this.resultTextModel.pushStackElement(); } // always set conflict as handled - existingState.handledInput1.set(true, transaction); - existingState.handledInput2.set(true, transaction); + existingState.handledInput1.set(true, tx); + existingState.handledInput2.set(true, tx); } public resetDirtyConflictsToBase(): void { @@ -474,6 +485,42 @@ export class MergeEditorModel extends EditorModel { return; } + const dataRef = new WeakRef(ModifiedBaseRangeData); + const modelRef = new WeakRef(this); + + this.undoRedoService.pushElement({ + type: UndoRedoElementType.Resource, + resource: this.resultTextModel.uri, + code: 'setInputHandled', + label: localize('setInputHandled', "Set Input Handled"), + redo() { + const model = modelRef.deref(); + const data = dataRef.deref(); + if (model && !model.isDisposed() && data) { + transaction(tx => { + if (inputNumber === 1) { + state.handledInput1.set(handled, tx); + } else { + state.handledInput2.set(handled, tx); + } + }); + } + }, + undo() { + const model = modelRef.deref(); + const data = dataRef.deref(); + if (model && !model.isDisposed() && data) { + transaction(tx => { + if (inputNumber === 1) { + state.handledInput1.set(!handled, tx); + } else { + state.handledInput2.set(!handled, tx); + } + }); + } + }, + }); + if (inputNumber === 1) { state.handledInput1.set(handled, tx); } else { @@ -723,3 +770,43 @@ export const enum MergeEditorModelState { upToDate = 2, updating = 3, } + +class MarkAsHandledUndoRedoElement implements IResourceUndoRedoElement { + public readonly code = 'undoMarkAsHandled'; + public readonly label = localize('undoMarkAsHandled', 'Undo Mark As Handled'); + + public readonly type = UndoRedoElementType.Resource; + + constructor( + public readonly resource: URI, + private readonly mergeEditorModelRef: WeakRef, + private readonly stateRef: WeakRef, + private readonly input1Handled: boolean, + private readonly input2Handled: boolean, + ) { } + + public redo() { + const mergeEditorModel = this.mergeEditorModelRef.deref(); + if (!mergeEditorModel || mergeEditorModel.isDisposed()) { + return; + } + const state = this.stateRef.deref(); + if (!state) { return; } + transaction(tx => { + state.handledInput1.set(true, tx); + state.handledInput2.set(true, tx); + }); + } + public undo() { + const mergeEditorModel = this.mergeEditorModelRef.deref(); + if (!mergeEditorModel || mergeEditorModel.isDisposed()) { + return; + } + const state = this.stateRef.deref(); + if (!state) { return; } + transaction(tx => { + state.handledInput1.set(this.input1Handled, tx); + state.handledInput2.set(this.input2Handled, tx); + }); + } +} diff --git a/src/vs/workbench/contrib/mergeEditor/browser/model/textModelDiffs.ts b/src/vs/workbench/contrib/mergeEditor/browser/model/textModelDiffs.ts index b618ffd82bc..526ca3d4085 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/model/textModelDiffs.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/model/textModelDiffs.ts @@ -13,6 +13,7 @@ import { LineRange } from 'vs/workbench/contrib/mergeEditor/browser/model/lineRa import { ReentrancyBarrier } from 'vs/workbench/contrib/mergeEditor/browser/utils'; import { IMergeDiffComputer } from './diffComputer'; import { autorun, IObservable, IReader, ITransaction, observableSignal, observableValue, transaction } from 'vs/base/common/observable'; +import { UndoRedoGroup } from 'vs/platform/undoRedo/common/undoRedo'; export class TextModelDiffs extends Disposable { private recomputeCount = 0; @@ -120,7 +121,7 @@ export class TextModelDiffs extends Disposable { } } - public removeDiffs(diffToRemoves: DetailedLineRangeMapping[], transaction: ITransaction | undefined): void { + public removeDiffs(diffToRemoves: DetailedLineRangeMapping[], transaction: ITransaction | undefined, group?: UndoRedoGroup): void { this.ensureUpToDate(); diffToRemoves.sort(compareBy((d) => d.inputRange.startLineNumber, numberComparator)); @@ -137,7 +138,8 @@ export class TextModelDiffs extends Disposable { } this.barrier.runExclusivelyOrThrow(() => { - diffToRemove.getReverseLineEdit().apply(this.textModel); + const edits = diffToRemove.getReverseLineEdit().toEdits(this.textModel.getLineCount()); + this.textModel.pushEditOperations(null, edits, () => null, group); }); diffs = diffs.map((d) => @@ -153,7 +155,7 @@ export class TextModelDiffs extends Disposable { /** * Edit must be conflict free. */ - public applyEditRelativeToOriginal(edit: LineRangeEdit, transaction: ITransaction | undefined): void { + public applyEditRelativeToOriginal(edit: LineRangeEdit, transaction: ITransaction | undefined, group?: UndoRedoGroup): void { this.ensureUpToDate(); const editMapping = new DetailedLineRangeMapping( @@ -191,7 +193,8 @@ export class TextModelDiffs extends Disposable { } this.barrier.runExclusivelyOrThrow(() => { - new LineRangeEdit(edit.range.delta(delta), edit.newLines).apply(this.textModel); + const edits = new LineRangeEdit(edit.range.delta(delta), edit.newLines).toEdits(this.textModel.getLineCount()); + this.textModel.pushEditOperations(null, edits, () => null, group); }); this._diffs.set(newDiffs, transaction, TextModelDiffChangeReason.other); } diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView.ts index 604ddedb213..d362b430c44 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/editors/inputCodeEditorView.ts @@ -21,8 +21,7 @@ import { MenuId } from 'vs/platform/actions/common/actions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { attachToggleStyler } from 'vs/platform/theme/common/styler'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { defaultToggleStyles } from 'vs/platform/theme/browser/defaultStyles'; import { InputState, ModifiedBaseRange, ModifiedBaseRangeState } from 'vs/workbench/contrib/mergeEditor/browser/model/modifiedBaseRange'; import { applyObservableDecorations, setFields } from 'vs/workbench/contrib/mergeEditor/browser/utils'; import { handledConflictMinimapOverViewRulerColor, unhandledConflictMinimapOverViewRulerColor } from 'vs/workbench/contrib/mergeEditor/browser/view/colors'; @@ -38,7 +37,6 @@ export class InputCodeEditorView extends CodeEditorView { viewModel: IObservable, @IInstantiationService instantiationService: IInstantiationService, @IContextMenuService contextMenuService: IContextMenuService, - @IThemeService themeService: IThemeService, @IConfigurationService configurationService: IConfigurationService, ) { super(instantiationService, viewModel, configurationService); @@ -53,7 +51,7 @@ export class InputCodeEditorView extends CodeEditorView { getIntersectingGutterItems: (range, reader) => { return this.modifiedBaseRangeGutterItemInfos.read(reader); }, - createView: (item, target) => new MergeConflictGutterItemView(item, target, contextMenuService, themeService), + createView: (item, target) => new MergeConflictGutterItemView(item, target, contextMenuService), }) ); } @@ -375,7 +373,6 @@ export class MergeConflictGutterItemView extends Disposable implements IGutterIt item: ModifiedBaseRangeGutterItemModel, target: HTMLElement, contextMenuService: IContextMenuService, - themeService: IThemeService ) { super(); @@ -384,12 +381,11 @@ export class MergeConflictGutterItemView extends Disposable implements IGutterIt const checkBox = new Toggle({ isChecked: false, title: '', - icon: Codicon.check + icon: Codicon.check, + ...defaultToggleStyles }); checkBox.domNode.classList.add('accept-conflict-group'); - this._register(attachToggleStyler(checkBox, themeService)); - this._register( addDisposableListener(checkBox.domNode, EventType.MOUSE_DOWN, (e) => { const item = this.item.get(); diff --git a/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts b/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts index 69bdccfa99e..75a2bf57513 100644 --- a/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts +++ b/src/vs/workbench/contrib/mergeEditor/browser/view/viewModel.ts @@ -8,6 +8,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { derived, derivedObservableWithWritableCache, IObservable, IReader, ITransaction, observableFromEvent, observableValue, transaction } from 'vs/base/common/observable'; import { Range } from 'vs/editor/common/core/range'; import { ScrollType } from 'vs/editor/common/editorCommon'; +import { ITextModel } from 'vs/editor/common/model'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -24,6 +25,8 @@ export class MergeEditorViewModel extends Disposable { { range: ModifiedBaseRange | undefined; counter: number } >('manuallySetActiveModifiedBaseRange', { range: undefined, counter: 0 }); + private readonly attachedHistory = this._register(new AttachedHistory(this.model.resultTextModel)); + constructor( public readonly model: MergeEditorModel, public readonly inputCodeEditorView1: InputCodeEditorView, @@ -32,24 +35,53 @@ export class MergeEditorViewModel extends Disposable { public readonly baseCodeEditorView: IObservable, public readonly showNonConflictingChanges: IObservable, @IConfigurationService private readonly configurationService: IConfigurationService, - @INotificationService private readonly notificationService: INotificationService + @INotificationService private readonly notificationService: INotificationService, ) { super(); this._register(resultCodeEditorView.editor.onDidChangeModelContent(e => { - if (this.model.isApplyingEditInResult) { + if (this.model.isApplyingEditInResult || e.isRedoing || e.isUndoing) { return; } - transaction(tx => { - /** @description Mark conflicts touched by manual edits as handled */ - for (const change of e.changes) { - const rangeInBase = this.model.translateResultRangeToBase(Range.lift(change.range)); - const baseRanges = this.model.findModifiedBaseRangesInRange(new LineRange(rangeInBase.startLineNumber, rangeInBase.endLineNumber - rangeInBase.startLineNumber)); - if (baseRanges.length === 1) { - this.model.setHandled(baseRanges[0], true, tx); + + const baseRangeStates: ModifiedBaseRange[] = []; + + for (const change of e.changes) { + const rangeInBase = this.model.translateResultRangeToBase(Range.lift(change.range)); + const baseRanges = this.model.findModifiedBaseRangesInRange(new LineRange(rangeInBase.startLineNumber, rangeInBase.endLineNumber - rangeInBase.startLineNumber)); + if (baseRanges.length === 1) { + const isHandled = this.model.isHandled(baseRanges[0]).get(); + if (!isHandled) { + baseRangeStates.push(baseRanges[0]); } } - }); + } + + if (baseRangeStates.length === 0) { + return; + } + + const element = { + model: this.model, + redo() { + transaction(tx => { + /** @description Mark conflicts touched by manual edits as handled */ + for (const r of baseRangeStates) { + this.model.setHandled(r, true, tx); + } + }); + }, + undo() { + transaction(tx => { + /** @description Mark conflicts touched by manual edits as handled */ + for (const r of baseRangeStates) { + this.model.setHandled(r, false, tx); + } + }); + }, + }; + this.attachedHistory.pushAttachedHistoryElement(element); + element.redo(); })); } @@ -255,3 +287,56 @@ export class MergeEditorViewModel extends Disposable { }); } } + +class AttachedHistory extends Disposable { + private readonly attachedHistory: { element: IAttachedHistoryElement; altId: number }[] = []; + private previousAltId: number = this.model.getAlternativeVersionId(); + + constructor(private readonly model: ITextModel) { + super(); + + this._register(model.onDidChangeContent((e) => { + const currentAltId = model.getAlternativeVersionId(); + + if (e.isRedoing) { + for (const item of this.attachedHistory) { + if (this.previousAltId < item.altId && item.altId <= currentAltId) { + item.element.redo(); + } + } + } else if (e.isUndoing) { + for (let i = this.attachedHistory.length - 1; i >= 0; i--) { + const item = this.attachedHistory[i]; + if (currentAltId < item.altId && item.altId <= this.previousAltId) { + item.element.undo(); + } + } + + } else { + // The user destroyed the redo stack by performing a non redo/undo operation. + // Thus we also need to remove all history elements after the last version id. + while ( + this.attachedHistory.length > 0 + && this.attachedHistory[this.attachedHistory.length - 1]!.altId > this.previousAltId + ) { + this.attachedHistory.pop(); + } + } + + this.previousAltId = currentAltId; + })); + } + + /** + * Pushes an history item that is tied to the last text edit (or an extension of it). + * When the last text edit is undone/redone, so is is this history item. + */ + public pushAttachedHistoryElement(element: IAttachedHistoryElement): void { + this.attachedHistory.push({ altId: this.model.getAlternativeVersionId(), element }); + } +} + +interface IAttachedHistoryElement { + undo(): void; + redo(): void; +} diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts index 59d525ee6a0..e7455c46dce 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import { FindInput, IFindInputOptions, IFindInputStyles } from 'vs/base/browser/ui/findinput/findInput'; -import { IReplaceInputStyles, ReplaceInput } from 'vs/base/browser/ui/findinput/replaceInput'; +import { FindInput, IFindInputOptions } from 'vs/base/browser/ui/findinput/findInput'; +import { ReplaceInput } from 'vs/base/browser/ui/findinput/replaceInput'; import { IMessage as InputBoxMessage } from 'vs/base/browser/ui/inputbox/inputBox'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { Widget } from 'vs/base/browser/ui/widget'; @@ -18,9 +18,8 @@ import * as nls from 'vs/nls'; import { ContextScopedReplaceInput, registerAndCreateHistoryNavigationContext } from 'vs/platform/history/browser/contextScopedHistoryWidget'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground, inputBackground, inputBorder, inputForeground, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, inputValidationInfoBackground, inputValidationInfoBorder, inputValidationInfoForeground, inputValidationWarningBackground, inputValidationWarningBorder, inputValidationWarningForeground } from 'vs/platform/theme/common/colorRegistry'; import { registerIcon, widgetClose } from 'vs/platform/theme/common/iconRegistry'; -import { IColorTheme, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { parseReplaceString, ReplacePattern } from 'vs/editor/contrib/find/browser/replacePattern'; import { Codicon } from 'vs/base/common/codicons'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -36,7 +35,8 @@ import { NotebookFindFilters } from 'vs/workbench/contrib/notebook/browser/contr import { isSafari } from 'vs/base/common/platform'; import { ISashEvent, Orientation, Sash } from 'vs/base/browser/ui/sash/sash'; import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { getProgressBarStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { defaultInputBoxStyles, defaultProgressBarStyles, defaultToggleStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { IToggleStyles } from 'vs/base/browser/ui/toggle/toggle'; const NLS_FIND_INPUT_LABEL = nls.localize('label.find', "Find"); const NLS_FIND_INPUT_PLACEHOLDER = nls.localize('placeholder.find', "Find"); @@ -154,6 +154,7 @@ class NotebookFindInput extends FindInput { private _actionbar: ActionBar | null = null; private _filterChecked: boolean = false; private _filtersAction: IAction; + private _toggleStyles: IToggleStyles; constructor( readonly filters: NotebookFindFilters, @@ -166,6 +167,8 @@ class NotebookFindInput extends FindInput { ) { super(parent, contextViewProvider, options); + this._toggleStyles = options.toggleStyles; + this._register(registerAndCreateHistoryNavigationContext(contextKeyService, this.inputBox)); this._filtersAction = new Action('notebookFindFilterAction', NOTEBOOK_FIND_FILTERS, 'notebook-filters ' + ThemeIcon.asClassName(filterIcon)); this._filtersAction.checked = false; @@ -225,12 +228,12 @@ class NotebookFindInput extends FindInput { this.applyStyles(); } - protected override applyStyles(): void { - super.applyStyles(); + private applyStyles(): void { + const toggleStyles = this._toggleStyles; - this._filterButtonContainer.style.borderColor = this._filterChecked && this.inputActiveOptionBorder ? this.inputActiveOptionBorder.toString() : ''; - this._filterButtonContainer.style.color = this._filterChecked && this.inputActiveOptionForeground ? this.inputActiveOptionForeground.toString() : 'inherit'; - this._filterButtonContainer.style.backgroundColor = this._filterChecked && this.inputActiveOptionBackground ? this.inputActiveOptionBackground.toString() : ''; + this._filterButtonContainer.style.borderColor = (this._filterChecked && toggleStyles.inputActiveOptionBorder) || ''; + this._filterButtonContainer.style.color = (this._filterChecked && toggleStyles.inputActiveOptionForeground) || 'inherit'; + this._filterButtonContainer.style.backgroundColor = (this._filterChecked && toggleStyles.inputActiveOptionBackground) || ''; } getCellToolbarActions(menu: IMenu): { primary: IAction[]; secondary: IAction[] } { @@ -299,7 +302,7 @@ export abstract class SimpleFindReplaceWidget extends Widget { this._scopedContextKeyService = contextKeyService.createScoped(this._domNode); const progressContainer = dom.$('.find-replace-progress'); - this._progressBar = new ProgressBar(progressContainer, getProgressBarStyles()); + this._progressBar = new ProgressBar(progressContainer, defaultProgressBarStyles); this._domNode.appendChild(progressContainer); const isInteractiveWindow = contextKeyService.getContextKeyValue('notebookType') === 'interactive'; @@ -351,7 +354,9 @@ export abstract class SimpleFindReplaceWidget extends Widget { } }, flexibleWidth: true, - showCommonFindToggles: true + showCommonFindToggles: true, + inputBoxStyles: defaultInputBoxStyles, + toggleStyles: defaultToggleStyles } )); @@ -452,7 +457,9 @@ export abstract class SimpleFindReplaceWidget extends Widget { this._replaceInput = this._register(new ContextScopedReplaceInput(null, undefined, { label: NLS_REPLACE_INPUT_LABEL, placeholder: NLS_REPLACE_INPUT_PLACEHOLDER, - history: [] + history: [], + inputBoxStyles: defaultInputBoxStyles, + toggleStyles: defaultToggleStyles }, contextKeyService, false)); this._innerReplaceDomNode.appendChild(this._replaceInput.domNode); this._replaceInputFocusTracker = this._register(dom.trackFocus(this._replaceInput.domNode)); @@ -581,45 +588,6 @@ export abstract class SimpleFindReplaceWidget extends Widget { return this._focusTracker; } - public updateTheme(theme: IColorTheme): void { - const inputStyles: IFindInputStyles = { - inputActiveOptionBorder: theme.getColor(inputActiveOptionBorder), - inputActiveOptionForeground: theme.getColor(inputActiveOptionForeground), - inputActiveOptionBackground: theme.getColor(inputActiveOptionBackground), - inputBackground: theme.getColor(inputBackground), - inputForeground: theme.getColor(inputForeground), - inputBorder: theme.getColor(inputBorder), - inputValidationInfoBackground: theme.getColor(inputValidationInfoBackground), - inputValidationInfoForeground: theme.getColor(inputValidationInfoForeground), - inputValidationInfoBorder: theme.getColor(inputValidationInfoBorder), - inputValidationWarningBackground: theme.getColor(inputValidationWarningBackground), - inputValidationWarningForeground: theme.getColor(inputValidationWarningForeground), - inputValidationWarningBorder: theme.getColor(inputValidationWarningBorder), - inputValidationErrorBackground: theme.getColor(inputValidationErrorBackground), - inputValidationErrorForeground: theme.getColor(inputValidationErrorForeground), - inputValidationErrorBorder: theme.getColor(inputValidationErrorBorder), - }; - this._findInput.style(inputStyles); - const replaceStyles: IReplaceInputStyles = { - inputActiveOptionBorder: theme.getColor(inputActiveOptionBorder), - inputActiveOptionForeground: theme.getColor(inputActiveOptionForeground), - inputActiveOptionBackground: theme.getColor(inputActiveOptionBackground), - inputBackground: theme.getColor(inputBackground), - inputForeground: theme.getColor(inputForeground), - inputBorder: theme.getColor(inputBorder), - inputValidationInfoBackground: theme.getColor(inputValidationInfoBackground), - inputValidationInfoForeground: theme.getColor(inputValidationInfoForeground), - inputValidationInfoBorder: theme.getColor(inputValidationInfoBorder), - inputValidationWarningBackground: theme.getColor(inputValidationWarningBackground), - inputValidationWarningForeground: theme.getColor(inputValidationWarningForeground), - inputValidationWarningBorder: theme.getColor(inputValidationWarningBorder), - inputValidationErrorBackground: theme.getColor(inputValidationErrorBackground), - inputValidationErrorForeground: theme.getColor(inputValidationErrorForeground), - inputValidationErrorBorder: theme.getColor(inputValidationErrorBorder), - }; - this._replaceInput.style(replaceStyles); - } - private _onStateChanged(e: FindReplaceStateChangedEvent): void { this._updateButtons(); this._updateMatchesCount(); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget.ts b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget.ts index d422ae6e7eb..3589f801003 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget.ts @@ -21,7 +21,6 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; import { NotebookFindFilters } from 'vs/workbench/contrib/notebook/browser/contrib/find/findFilters'; import { FindModel } from 'vs/workbench/contrib/notebook/browser/contrib/find/findModel'; import { SimpleFindReplaceWidget } from 'vs/workbench/contrib/notebook/browser/contrib/find/notebookFindReplaceWidget'; @@ -81,7 +80,6 @@ class NotebookFindWidget extends SimpleFindReplaceWidget implements INotebookEdi _notebookEditor: INotebookEditor, @IContextViewService contextViewService: IContextViewService, @IContextKeyService contextKeyService: IContextKeyService, - @IThemeService themeService: IThemeService, @IConfigurationService configurationService: IConfigurationService, @IContextMenuService contextMenuService: IContextMenuService, @IMenuService menuService: IMenuService, @@ -93,10 +91,6 @@ class NotebookFindWidget extends SimpleFindReplaceWidget implements INotebookEdi DOM.append(this._notebookEditor.getDomNode(), this.getDomNode()); this._findWidgetFocused = KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED.bindTo(contextKeyService); this._register(this._findInput.onKeyDown((e) => this._onFindInputKeyDown(e))); - this.updateTheme(themeService.getColorTheme()); - this._register(themeService.onDidColorThemeChange(() => { - this.updateTheme(themeService.getColorTheme()); - })); this._register(this._state.onFindReplaceStateChange((e) => { this.onInputChanged(); diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 88009aa22df..1c428740fd4 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -46,7 +46,7 @@ import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema'; import { Event } from 'vs/base/common/event'; import { getFormattedMetadataJSON, getStreamOutputData } from 'vs/workbench/contrib/notebook/browser/diff/diffElementViewModel'; import { NotebookModelResolverServiceImpl } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl'; -import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernelHistoryService, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { NotebookKernelService } from 'vs/workbench/contrib/notebook/browser/services/notebookKernelServiceImpl'; import { IWorkingCopyIdentifier } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; @@ -105,6 +105,7 @@ import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeat import { NotebookInfo } from 'vs/editor/common/languageFeatureRegistry'; import { COMMENTEDITOR_DECORATION_KEY } from 'vs/workbench/contrib/comments/browser/commentReply'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { NotebookKernelHistoryService } from 'vs/workbench/contrib/notebook/browser/services/notebookKernelHistoryServiceImpl'; /*--------------------------------------------------------------------------------------------- */ @@ -703,6 +704,7 @@ registerSingleton(INotebookEditorModelResolverService, NotebookModelResolverServ registerSingleton(INotebookCellStatusBarService, NotebookCellStatusBarService, InstantiationType.Delayed); registerSingleton(INotebookEditorService, NotebookEditorWidgetService, InstantiationType.Delayed); registerSingleton(INotebookKernelService, NotebookKernelService, InstantiationType.Delayed); +registerSingleton(INotebookKernelHistoryService, NotebookKernelHistoryService, InstantiationType.Delayed); registerSingleton(INotebookExecutionService, NotebookExecutionService, InstantiationType.Delayed); registerSingleton(INotebookExecutionStateService, NotebookExecutionStateService, InstantiationType.Delayed); registerSingleton(INotebookRendererMessagingService, NotebookRendererMessagingService, InstantiationType.Delayed); @@ -910,11 +912,16 @@ configurationRegistry.registerConfiguration({ type: 'string', tags: ['notebookLayout'] }, - [NotebookSetting.kernelPickerMRU]: { - markdownDescription: nls.localize('notebook.kernelPickerMRU', "Controls whether the kernel picker should show the most recently used kernels."), - type: 'boolean', + [NotebookSetting.kernelPickerType]: { + markdownDescription: nls.localize('notebook.kernelPickerType', "Controls the type of kernel picker to use."), + type: 'string', + enum: ['all', 'mru'], + enumDescriptions: [ + nls.localize('notebook.kernelPickerType.all', "Show all kernels."), + nls.localize('notebook.kernelPickerType.mru', "Experiment: show recently used kernels."), + ], tags: ['notebookLayout'], - default: false + default: 'all' } } }); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookExtensionPoint.ts b/src/vs/workbench/contrib/notebook/browser/notebookExtensionPoint.ts index d720fa8bd9f..760f24f2c00 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookExtensionPoint.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookExtensionPoint.ts @@ -232,7 +232,14 @@ const notebookPreloadContribution: IJSONSchema = { export const notebooksExtensionPoint = ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'notebooks', - jsonSchema: notebookProviderContribution + jsonSchema: notebookProviderContribution, + activationEventsGenerator: (contribs: INotebookEditorContribution[], result: { push(item: string): void }) => { + for (const contrib of contribs) { + if (contrib.type) { + result.push(`onNotebookSerializer:${contrib.type}`); + } + } + } }); export const notebookRendererExtensionPoint = ExtensionsRegistry.registerExtensionPoint({ diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookExecutionStateServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookExecutionStateServiceImpl.ts index 7cf663d07f6..66eeeae13b9 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookExecutionStateServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookExecutionStateServiceImpl.ts @@ -9,6 +9,7 @@ import { ResourceMap } from 'vs/base/common/map'; import { isEqual } from 'vs/base/common/resources'; import { withNullAsUndefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; +import { AudioCue, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; @@ -36,6 +37,7 @@ export class NotebookExecutionStateService extends Disposable implements INotebo @IInstantiationService private readonly _instantiationService: IInstantiationService, @ILogService private readonly _logService: ILogService, @INotebookService private readonly _notebookService: INotebookService, + @IAudioCueService private readonly _audioCueService: IAudioCueService ) { super(); } @@ -104,8 +106,12 @@ export class NotebookExecutionStateService extends Disposable implements INotebo if (lastRunSuccess !== undefined) { if (lastRunSuccess) { + if (this._executions.size === 0) { + this._audioCueService.playAudioCue(AudioCue.notebookCellCompleted); + } this._clearLastFailedCell(notebookUri); } else { + this._audioCueService.playAudioCue(AudioCue.notebookCellFailed); this._setLastFailedCell(notebookUri, cellHandle); } } diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookKernelHistoryServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookKernelHistoryServiceImpl.ts new file mode 100644 index 00000000000..916c293254f --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookKernelHistoryServiceImpl.ts @@ -0,0 +1,149 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from 'vs/base/common/lifecycle'; +import { LinkedMap, Touch } from 'vs/base/common/map'; +import { localize } from 'vs/nls'; +import { Categories } from 'vs/platform/action/common/actionCommonCategories'; +import { Action2, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { INotebookKernel, INotebookKernelHistoryService, INotebookKernelService, INotebookTextModelLike } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; + +interface ISerializedKernelsListPerType { + entries: string[]; +} + +interface ISerializedKernelsList { + [viewType: string]: ISerializedKernelsListPerType; +} + +const MAX_KERNELS_IN_HISTORY = 5; + +export class NotebookKernelHistoryService extends Disposable implements INotebookKernelHistoryService { + declare _serviceBrand: undefined; + + private static STORAGE_KEY = 'notebook.kernelHistory'; + private _mostRecentKernelsMap: { [key: string]: LinkedMap } = {}; + + constructor(@IStorageService private readonly _storageService: IStorageService, + @INotebookKernelService private readonly _notebookKernelService: INotebookKernelService) { + super(); + + this._loadState(); + this._register(this._storageService.onWillSaveState(() => this._saveState())); + } + + getKernels(notebook: INotebookTextModelLike): { selected: INotebookKernel | undefined; all: INotebookKernel[] } { + const allAvailableKernels = this._notebookKernelService.getMatchingKernel(notebook); + const allKernels = allAvailableKernels.all; + const selectedKernel = allAvailableKernels.selected; + const suggested = (allAvailableKernels.suggestions.length === 1 ? allAvailableKernels.suggestions[0] : undefined) + ?? (allAvailableKernels.all.length === 1) ? allAvailableKernels.all[0] : undefined; + + const mostRecentKernelIds = this._mostRecentKernelsMap[notebook.viewType] ? [...this._mostRecentKernelsMap[notebook.viewType].values()] : []; + + const all = mostRecentKernelIds.map(kernelId => allKernels.find(kernel => kernel.id === kernelId)).filter(kernel => !!kernel) as INotebookKernel[]; + + return { + selected: selectedKernel ?? suggested, + all + }; + } + + addMostRecentKernel(kernel: INotebookKernel): void { + const key = kernel.id; + const viewType = kernel.viewType; + const recentKeynels = this._mostRecentKernelsMap[viewType] ?? new LinkedMap(); + + recentKeynels.set(key, key, Touch.AsOld); + + + if (recentKeynels.size > MAX_KERNELS_IN_HISTORY) { + const reserved = [...recentKeynels.entries()].slice(0, MAX_KERNELS_IN_HISTORY); + recentKeynels.fromJSON(reserved); + } + + this._mostRecentKernelsMap[viewType] = recentKeynels; + } + + private _saveState(): void { + let notEmpty = false; + for (const [_, kernels] of Object.entries(this._mostRecentKernelsMap)) { + notEmpty = notEmpty || kernels.size > 0; + } + + if (notEmpty) { + const serialized = this._serialize(); + this._storageService.store(NotebookKernelHistoryService.STORAGE_KEY, JSON.stringify(serialized), StorageScope.WORKSPACE, StorageTarget.MACHINE); + } else { + this._storageService.remove(NotebookKernelHistoryService.STORAGE_KEY, StorageScope.WORKSPACE); + } + } + + private _loadState(): void { + const serialized = this._storageService.get(NotebookKernelHistoryService.STORAGE_KEY, StorageScope.WORKSPACE); + if (serialized) { + try { + this._deserialize(JSON.parse(serialized)); + } catch (e) { + this._mostRecentKernelsMap = {}; + } + } else { + this._mostRecentKernelsMap = {}; + } + } + + private _serialize(): ISerializedKernelsList { + const result: ISerializedKernelsList = Object.create(null); + + for (const [viewType, kernels] of Object.entries(this._mostRecentKernelsMap)) { + result[viewType] = { + entries: [...kernels.values()] + }; + } + return result; + } + + private _deserialize(serialized: ISerializedKernelsList): void { + this._mostRecentKernelsMap = {}; + + for (const [viewType, kernels] of Object.entries(serialized)) { + const linkedMap = new LinkedMap(); + const mapValues: [string, string][] = []; + + for (const entry of kernels.entries) { + mapValues.push([entry, entry]); + } + + linkedMap.fromJSON(mapValues); + this._mostRecentKernelsMap[viewType] = linkedMap; + } + } + + _clear(): void { + this._mostRecentKernelsMap = {}; + this._saveState(); + } +} + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'notebook.clearNotebookKernelsMRUCache', + title: { + value: localize('workbench.notebook.clearNotebookKernelsMRUCache', "Clear Notebook Kernels MRU Cache"), + original: 'Clear Notebook Kernels MRU Cache' + }, + category: Categories.Developer, + f1: true + }); + } + + async run(accessor: ServicesAccessor): Promise { + const historyService = accessor.get(INotebookKernelHistoryService) as NotebookKernelHistoryService; + historyService._clear(); + } +}); diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookKernelServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookKernelServiceImpl.ts index 97b33b0a540..e6024820012 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookKernelServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookKernelServiceImpl.ts @@ -5,8 +5,8 @@ import { Event, Emitter } from 'vs/base/common/event'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { INotebookTextModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { INotebookKernel, ISelectedNotebooksChangeEvent, INotebookKernelMatchResult, INotebookKernelService, INotebookTextModelLike, ISourceAction, INotebookSourceActionChangeEvent, INotebookKernelDetectionTask } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { INotebookKernelSourceAction, INotebookTextModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookKernel, ISelectedNotebooksChangeEvent, INotebookKernelMatchResult, INotebookKernelService, INotebookTextModelLike, ISourceAction, INotebookSourceActionChangeEvent, INotebookKernelDetectionTask, IKernelSourceActionProvider } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { LRUCache, ResourceMap } from 'vs/base/common/map'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { URI } from 'vs/base/common/uri'; @@ -107,6 +107,7 @@ export class NotebookKernelService extends Disposable implements INotebookKernel private readonly _kernelSources = new Map(); private readonly _kernelDetectionTasks = new Map(); private readonly _onDidChangeKernelDetectionTasks = this._register(new Emitter()); + private readonly _kernelSourceActionProviders = new Map(); readonly onDidChangeSelectedNotebooks: Event = this._onDidChangeNotebookKernelBinding.event; readonly onDidAddKernel: Event = this._onDidAddKernel.event; @@ -366,4 +367,31 @@ export class NotebookKernelService extends Disposable implements INotebookKernel getKernelDetectionTasks(notebook: INotebookTextModelLike): INotebookKernelDetectionTask[] { return this._kernelDetectionTasks.get(notebook.viewType) ?? []; } + + registerKernelSourceActionProvider(viewType: string, provider: IKernelSourceActionProvider): IDisposable { + const providers = this._kernelSourceActionProviders.get(viewType) ?? []; + providers.push(provider); + this._kernelSourceActionProviders.set(viewType, providers); + + return toDisposable(() => { + const providers = this._kernelSourceActionProviders.get(viewType) ?? []; + const idx = providers.indexOf(provider); + if (idx >= 0) { + providers.splice(idx, 1); + this._kernelSourceActionProviders.set(viewType, providers); + } + }); + } + + /** + * Get kernel source actions from providers + */ + getKernelSourceActions2(notebook: INotebookTextModelLike): Promise { + const viewType = notebook.viewType; + const providers = this._kernelSourceActionProviders.get(viewType) ?? []; + const promises = providers.map(provider => provider.provideKernelSourceActions()); + return Promise.all(promises).then(actions => { + return actions.reduce((a, b) => a.concat(b), []); + }); + } } diff --git a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts index 4242f322651..c6f592fceb7 100644 --- a/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/services/notebookServiceImpl.ts @@ -606,20 +606,7 @@ export class NotebookService extends Disposable implements INotebookService { } await this._extensionService.whenInstalledExtensionsRegistered(); - - const info = this._notebookProviderInfoStore?.get(viewType); - const waitFor: Promise[] = [Event.toPromise(Event.filter(this.onAddViewType, () => { - return this._notebookProviders.has(viewType); - }))]; - - if (info && info.extension) { - const extensionManifest = await this._extensionService.getExtension(info.extension.value); - if (extensionManifest?.activationEvents && extensionManifest.activationEvents.indexOf(`onNotebook:${viewType}`) >= 0) { - waitFor.push(this._extensionService._activateById(info.extension, { startup: false, activationEvent: `onNotebook:${viewType}}`, extensionId: info.extension })); - } - } - - await Promise.race(waitFor); + await this._extensionService.activateByEvent(`onNotebookSerializer:${viewType}`); return this._notebookProviders.has(viewType); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellProgressBar.ts b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellProgressBar.ts index df45ee5073b..6b4ac184e7b 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellProgressBar.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/cellParts/cellProgressBar.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; -import { getProgressBarStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { defaultProgressBarStyles } from 'vs/platform/theme/browser/defaultStyles'; import { ICellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellViewModelStateChangeEvent } from 'vs/workbench/contrib/notebook/browser/notebookViewEvents'; import { CellContentPart } from 'vs/workbench/contrib/notebook/browser/view/cellPart'; @@ -21,10 +21,10 @@ export class CellProgressBar extends CellContentPart { @INotebookExecutionStateService private readonly _notebookExecutionStateService: INotebookExecutionStateService) { super(); - this._progressBar = this._register(new ProgressBar(editorContainer, getProgressBarStyles())); + this._progressBar = this._register(new ProgressBar(editorContainer, defaultProgressBarStyles)); this._progressBar.hide(); - this._collapsedProgressBar = this._register(new ProgressBar(collapsedInputContainer, getProgressBarStyles())); + this._collapsedProgressBar = this._register(new ProgressBar(collapsedInputContainer, defaultProgressBarStyles)); this._collapsedProgressBar.hide(); } diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts new file mode 100644 index 00000000000..f5379d218ce --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy.ts @@ -0,0 +1,771 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./notebookKernelActionViewItem'; +import { groupBy } from 'vs/base/common/arrays'; +import { createCancelablePromise } from 'vs/base/common/async'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Codicon } from 'vs/base/common/codicons'; +import { Event } from 'vs/base/common/event'; +import { compareIgnoreCase, uppercaseFirstLetter } from 'vs/base/common/strings'; +import { localize } from 'vs/nls'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { ProgressLocation } from 'vs/platform/progress/common/progress'; +import { IQuickInputService, IQuickPick, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; +import { ViewContainerLocation } from 'vs/workbench/common/views'; +import { IExtensionsViewPaneContainer, IExtensionsWorkbenchService, VIEWLET_ID as EXTENSION_VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; +import { getNotebookEditorFromEditorPane, INotebookEditor, INotebookExtensionRecommendation, JUPYTER_EXTENSION_ID, KERNEL_RECOMMENDATIONS } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { INotebookKernel, INotebookKernelHistoryService, INotebookKernelMatchResult, INotebookKernelService, ISourceAction } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; +import { Command } from 'vs/editor/common/languages'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { MarshalledId } from 'vs/base/common/marshallingIds'; +import { IAction } from 'vs/base/common/actions'; +import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { executingStateIcon, selectKernelIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; + +type KernelPick = IQuickPickItem & { kernel: INotebookKernel }; +function isKernelPick(item: QuickPickInput): item is KernelPick { + return 'kernel' in item; +} +type SourcePick = IQuickPickItem & { action: ISourceAction }; +function isSourcePick(item: QuickPickInput): item is SourcePick { + return 'action' in item; +} +type InstallExtensionPick = IQuickPickItem & { extensionId: string }; +function isInstallExtensionPick(item: QuickPickInput): item is InstallExtensionPick { + return item.id === 'installSuggested' && 'extensionId' in item; +} +type KernelSourceQuickPickItem = IQuickPickItem & { command: Command }; +type KernelQuickPickItem = IQuickPickItem | InstallExtensionPick | KernelPick | SourcePick | KernelSourceQuickPickItem; +const KERNEL_PICKER_UPDATE_DEBOUNCE = 200; + +export type KernelQuickPickContext = + { id: string; extension: string } | + { notebookEditorId: string } | + { id: string; extension: string; notebookEditorId: string } | + { ui?: boolean; notebookEditor?: NotebookEditorWidget }; + +export interface IKernelPickerStrategy { + showQuickPick(context?: KernelQuickPickContext): Promise; +} + +function getEditorFromContext(editorService: IEditorService, context?: KernelQuickPickContext): INotebookEditor | undefined { + let editor: INotebookEditor | undefined; + if (context !== undefined && 'notebookEditorId' in context) { + const editorId = context.notebookEditorId; + const matchingEditor = editorService.visibleEditorPanes.find((editorPane) => { + const notebookEditor = getNotebookEditorFromEditorPane(editorPane); + return notebookEditor?.getId() === editorId; + }); + editor = getNotebookEditorFromEditorPane(matchingEditor); + } else if (context !== undefined && 'notebookEditor' in context) { + editor = context?.notebookEditor; + } else { + editor = getNotebookEditorFromEditorPane(editorService.activeEditorPane); + } + + return editor; +} + +function toQuickPick(kernel: INotebookKernel, selected: INotebookKernel | undefined) { + const res = { + kernel, + picked: kernel.id === selected?.id, + label: kernel.label, + description: kernel.description, + detail: kernel.detail + }; + if (kernel.id === selected?.id) { + if (!res.description) { + res.description = localize('current1', "Currently Selected"); + } else { + res.description = localize('current2', "{0} - Currently Selected", res.description); + } + } + return res; +} + + +abstract class KernelPickerStrategyBase implements IKernelPickerStrategy { + constructor( + protected readonly _notebookKernelService: INotebookKernelService, + protected readonly _editorService: IEditorService, + protected readonly _productService: IProductService, + protected readonly _quickInputService: IQuickInputService, + protected readonly _labelService: ILabelService, + protected readonly _logService: ILogService, + protected readonly _paneCompositePartService: IPaneCompositePartService, + protected readonly _extensionWorkbenchService: IExtensionsWorkbenchService, + protected readonly _extensionService: IExtensionService, + protected readonly _commandService: ICommandService + ) { } + + async showQuickPick(context?: KernelQuickPickContext): Promise { + const editor = getEditorFromContext(this._editorService, context); + + if (!editor || !editor.hasModel()) { + return false; + } + let controllerId = context && 'id' in context ? context.id : undefined; + let extensionId = context && 'extension' in context ? context.extension : undefined; + + if (controllerId && (typeof controllerId !== 'string' || typeof extensionId !== 'string')) { + // validate context: id & extension MUST be strings + controllerId = undefined; + extensionId = undefined; + } + + const notebook = editor.textModel; + const scopedContextKeyService = editor.scopedContextKeyService; + const matchResult = this._getMatchingResult(notebook); + const { selected, all } = matchResult; + + if (selected && controllerId && selected.id === controllerId && ExtensionIdentifier.equals(selected.extension, extensionId)) { + // current kernel is wanted kernel -> done + return true; + } + + let newKernel: INotebookKernel | undefined; + if (controllerId) { + const wantedId = `${extensionId}/${controllerId}`; + for (const candidate of all) { + if (candidate.id === wantedId) { + newKernel = candidate; + break; + } + } + if (!newKernel) { + this._logService.warn(`wanted kernel DOES NOT EXIST, wanted: ${wantedId}, all: ${all.map(k => k.id)}`); + return false; + } + } + + if (newKernel) { + this._selecteKernel(notebook, newKernel); + return true; + } + + const quickPick = this._quickInputService.createQuickPick(); + const quickPickItems = this._getKernelPickerQuickPickItems(notebook, matchResult, this._notebookKernelService, scopedContextKeyService); + quickPick.items = quickPickItems; + quickPick.canSelectMany = false; + quickPick.placeholder = selected + ? localize('prompt.placeholder.change', "Change kernel for '{0}'", this._labelService.getUriLabel(notebook.uri, { relative: true })) + : localize('prompt.placeholder.select', "Select kernel for '{0}'", this._labelService.getUriLabel(notebook.uri, { relative: true })); + + quickPick.busy = this._notebookKernelService.getKernelDetectionTasks(notebook).length > 0; + + const kernelDetectionTaskListener = this._notebookKernelService.onDidChangeKernelDetectionTasks(() => { + quickPick.busy = this._notebookKernelService.getKernelDetectionTasks(notebook).length > 0; + }); + + // run extension recommendataion task if quickPickItems is empty + const extensionRecommendataionPromise = quickPickItems.length === 0 + ? createCancelablePromise(token => this._showInstallKernelExtensionRecommendation(notebook, quickPick, this._extensionWorkbenchService, token)) + : undefined; + + const kernelChangeEventListener = Event.debounce( + Event.any( + this._notebookKernelService.onDidChangeSourceActions, + this._notebookKernelService.onDidAddKernel, + this._notebookKernelService.onDidRemoveKernel, + this._notebookKernelService.onDidChangeNotebookAffinity + ), + (last, _current) => last, + KERNEL_PICKER_UPDATE_DEBOUNCE + )(async () => { + // reset quick pick progress + quickPick.busy = false; + extensionRecommendataionPromise?.cancel(); + + const currentActiveItems = quickPick.activeItems; + const matchResult = this._getMatchingResult(notebook); + const quickPickItems = this._getKernelPickerQuickPickItems(notebook, matchResult, this._notebookKernelService, scopedContextKeyService); + quickPick.keepScrollPosition = true; + + // recalcuate active items + const activeItems: KernelQuickPickItem[] = []; + for (const item of currentActiveItems) { + if (isKernelPick(item)) { + const kernelId = item.kernel.id; + const sameItem = quickPickItems.find(pi => isKernelPick(pi) && pi.kernel.id === kernelId) as KernelPick | undefined; + if (sameItem) { + activeItems.push(sameItem); + } + } else if (isSourcePick(item)) { + const sameItem = quickPickItems.find(pi => isSourcePick(pi) && pi.action.action.id === item.action.action.id) as SourcePick | undefined; + if (sameItem) { + activeItems.push(sameItem); + } + } + } + + quickPick.items = quickPickItems; + quickPick.activeItems = activeItems; + }, this); + + const pick = await new Promise((resolve, reject) => { + quickPick.onDidAccept(() => { + const item = quickPick.selectedItems[0]; + if (item) { + resolve(item); + } else { + reject(); + } + + quickPick.hide(); + }); + + quickPick.onDidHide(() => () => { + kernelDetectionTaskListener.dispose(); + kernelChangeEventListener.dispose(); + quickPick.dispose(); + reject(); + }); + quickPick.show(); + }); + + if (pick) { + return await this._handleQuickPick(notebook, pick, context); + } + + return false; + } + + protected _getMatchingResult(notebook: NotebookTextModel) { + return this._notebookKernelService.getMatchingKernel(notebook); + } + + protected abstract _getKernelPickerQuickPickItems( + notebookTextModel: NotebookTextModel, + matchResult: INotebookKernelMatchResult, + notebookKernelService: INotebookKernelService, + scopedContextKeyService: IContextKeyService + ): QuickPickInput[]; + + protected async _handleQuickPick(notebook: NotebookTextModel, pick: KernelQuickPickItem, context?: KernelQuickPickContext) { + if (isKernelPick(pick)) { + const newKernel = pick.kernel; + this._selecteKernel(notebook, newKernel); + return true; + } + + // actions + if (pick.id === 'install') { + await this._showKernelExtension( + this._paneCompositePartService, + this._extensionWorkbenchService, + this._extensionService, + notebook.viewType + ); + // suggestedExtension must be defined for this option to be shown, but still check to make TS happy + } else if (isInstallExtensionPick(pick)) { + await this._showKernelExtension( + this._paneCompositePartService, + this._extensionWorkbenchService, + this._extensionService, + notebook.viewType, + pick.extensionId, + this._productService.quality !== 'stable' + ); + } else if (isSourcePick(pick)) { + // selected explicilty, it should trigger the execution? + pick.action.runAction(); + } + + return true; + } + + protected _selecteKernel(notebook: NotebookTextModel, kernel: INotebookKernel) { + this._notebookKernelService.selectKernelForNotebook(kernel, notebook); + } + + private async _showKernelExtension( + paneCompositePartService: IPaneCompositePartService, + extensionWorkbenchService: IExtensionsWorkbenchService, + extensionService: IExtensionService, + viewType: string, + extId?: string, + isInsiders?: boolean + ) { + // If extension id is provided attempt to install the extension as the user has requested the suggested ones be installed + if (extId) { + const extension = (await extensionWorkbenchService.getExtensions([{ id: extId }], CancellationToken.None))[0]; + const canInstall = await extensionWorkbenchService.canInstall(extension); + // If we can install then install it, otherwise we will fall out into searching the viewlet + if (canInstall) { + await extensionWorkbenchService.install( + extension, + { + installPreReleaseVersion: isInsiders ?? false, + context: { skipWalkthrough: true } + }, + ProgressLocation.Notification + ); + await extensionService.activateByEvent(`onNotebook:${viewType}`); + return; + } + } + + const viewlet = await paneCompositePartService.openPaneComposite(EXTENSION_VIEWLET_ID, ViewContainerLocation.Sidebar, true); + const view = viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer | undefined; + const pascalCased = viewType.split(/[^a-z0-9]/ig).map(uppercaseFirstLetter).join(''); + view?.search(`@tag:notebookKernel${pascalCased}`); + } + + private async _showInstallKernelExtensionRecommendation( + notebookTextModel: NotebookTextModel, + quickPick: IQuickPick, + extensionWorkbenchService: IExtensionsWorkbenchService, + token: CancellationToken + ) { + quickPick.busy = true; + + const newQuickPickItems = await this._getKernelRecommendationsQuickPickItems(notebookTextModel, extensionWorkbenchService); + quickPick.busy = false; + + if (token.isCancellationRequested) { + return; + } + + if (newQuickPickItems && quickPick.items.length === 0) { + quickPick.items = newQuickPickItems; + } + } + + private async _getKernelRecommendationsQuickPickItems( + notebookTextModel: NotebookTextModel, + extensionWorkbenchService: IExtensionsWorkbenchService, + ): Promise[] | undefined> { + const quickPickItems: QuickPickInput[] = []; + + const language = this.getSuggestedLanguage(notebookTextModel); + const suggestedExtension: INotebookExtensionRecommendation | undefined = language ? this.getSuggestedKernelFromLanguage(notebookTextModel.viewType, language) : undefined; + if (suggestedExtension) { + await extensionWorkbenchService.queryLocal(); + const extension = extensionWorkbenchService.installed.find(e => e.identifier.id === suggestedExtension.extensionId); + + if (extension) { + // it's installed but might be detecting kernels + return undefined; + } + + // We have a suggested kernel, show an option to install it + quickPickItems.push({ + id: 'installSuggested', + description: suggestedExtension.displayName ?? suggestedExtension.extensionId, + label: `$(${Codicon.lightbulb.id}) ` + localize('installSuggestedKernel', 'Install suggested extensions'), + extensionId: suggestedExtension.extensionId + }); + } + // there is no kernel, show the install from marketplace + quickPickItems.push({ + id: 'install', + label: localize('searchForKernels', "Browse marketplace for kernel extensions"), + }); + + return quickPickItems; + } + + /** + * Examine the most common language in the notebook + * @param notebookTextModel The notebook text model + * @returns What the suggested language is for the notebook. Used for kernal installing + */ + private getSuggestedLanguage(notebookTextModel: NotebookTextModel): string | undefined { + const metaData = notebookTextModel.metadata; + let suggestedKernelLanguage: string | undefined = (metaData.custom as any)?.metadata?.language_info?.name; + // TODO how do we suggest multi language notebooks? + if (!suggestedKernelLanguage) { + const cellLanguages = notebookTextModel.cells.map(cell => cell.language).filter(language => language !== 'markdown'); + // Check if cell languages is all the same + if (cellLanguages.length > 1) { + const firstLanguage = cellLanguages[0]; + if (cellLanguages.every(language => language === firstLanguage)) { + suggestedKernelLanguage = firstLanguage; + } + } + } + return suggestedKernelLanguage; + } + + /** + * Given a language and notebook view type suggest a kernel for installation + * @param language The language to find a suggested kernel extension for + * @returns A recommednation object for the recommended extension, else undefined + */ + private getSuggestedKernelFromLanguage(viewType: string, language: string): INotebookExtensionRecommendation | undefined { + const recommendation = KERNEL_RECOMMENDATIONS.get(viewType)?.get(language); + return recommendation; + } +} + +export class KernelPickerFlatStrategy extends KernelPickerStrategyBase { + + constructor( + @INotebookKernelService _notebookKernelService: INotebookKernelService, + @IEditorService _editorService: IEditorService, + @IProductService _productService: IProductService, + @IQuickInputService _quickInputService: IQuickInputService, + @ILabelService _labelService: ILabelService, + @ILogService _logService: ILogService, + @IPaneCompositePartService _paneCompositePartService: IPaneCompositePartService, + @IExtensionsWorkbenchService _extensionWorkbenchService: IExtensionsWorkbenchService, + @IExtensionService _extensionService: IExtensionService, + @ICommandService _commandService: ICommandService + + ) { + super( + _notebookKernelService, + _editorService, + _productService, + _quickInputService, + _labelService, + _logService, + _paneCompositePartService, + _extensionWorkbenchService, + _extensionService, + _commandService, + ); + } + + protected _getKernelPickerQuickPickItems( + notebookTextModel: NotebookTextModel, + matchResult: INotebookKernelMatchResult, + notebookKernelService: INotebookKernelService, + scopedContextKeyService: IContextKeyService + ): QuickPickInput[] { + const { selected, all, suggestions, hidden } = matchResult; + + const quickPickItems: QuickPickInput[] = []; + if (all.length) { + // Always display suggested kernels on the top. + this._fillInSuggestions(quickPickItems, suggestions, selected); + + // Next display all of the kernels not marked as hidden grouped by categories or extensions. + // If we don't have a kind, always display those at the bottom. + const picks = all.filter(item => (!suggestions.includes(item) && !hidden.includes(item))).map(kernel => toQuickPick(kernel, selected)); + const kernelsPerCategory = groupBy(picks, (a, b) => compareIgnoreCase(a.kernel.kind || 'z', b.kernel.kind || 'z')); + kernelsPerCategory.forEach(items => { + quickPickItems.push({ + type: 'separator', + label: items[0].kernel.kind || localize('otherKernelKinds', "Other") + }); + quickPickItems.push(...items); + }); + } + + const sourceActions = notebookKernelService.getSourceActions(notebookTextModel, scopedContextKeyService); + if (sourceActions.length) { + quickPickItems.push({ + type: 'separator', + // label: localize('sourceActions', "") + }); + + sourceActions.forEach(sourceAction => { + const res = { + action: sourceAction, + picked: false, + label: sourceAction.action.label, + }; + + quickPickItems.push(res); + }); + } + + return quickPickItems; + } + + private _fillInSuggestions(quickPickItems: QuickPickInput[], suggestions: INotebookKernel[], selected: INotebookKernel | undefined) { + if (!suggestions.length) { + return; + } + + if (suggestions.length === 1 && suggestions[0].id === selected?.id) { + quickPickItems.push({ + type: 'separator', + label: localize('selectedKernels', "Selected") + }); + + // The title is already set to "Selected" so we don't need to set it again in description, thus passing in `undefined`. + quickPickItems.push(toQuickPick(suggestions[0], undefined)); + return; + } + + quickPickItems.push({ + type: 'separator', + label: localize('suggestedKernels', "Suggested") + }); + quickPickItems.push(...suggestions.map(kernel => toQuickPick(kernel, selected))); + } + + static updateKernelStatusAction(notebook: NotebookTextModel, action: IAction, notebookKernelService: INotebookKernelService, scopedContextKeyService?: IContextKeyService) { + const detectionTasks = notebookKernelService.getKernelDetectionTasks(notebook); + if (detectionTasks.length) { + action.enabled = true; + action.label = localize('kernels.detecting', "Detecting Kernels"); + action.class = ThemeIcon.asClassName(ThemeIcon.modify(executingStateIcon, 'spin')); + return; + } + + const runningActions = notebookKernelService.getRunningSourceActions(notebook); + + const updateActionFromSourceAction = (sourceAction: ISourceAction, running: boolean) => { + const sAction = sourceAction.action; + action.class = running ? ThemeIcon.asClassName(ThemeIcon.modify(executingStateIcon, 'spin')) : ThemeIcon.asClassName(selectKernelIcon); + action.label = sAction.label; + action.enabled = true; + }; + + if (runningActions.length) { + return updateActionFromSourceAction(runningActions[0] /** TODO handle multiple actions state */, true); + } + + const info = notebookKernelService.getMatchingKernel(notebook); + if (info.all.length === 0) { + action.enabled = true; + const sourceActions = notebookKernelService.getSourceActions(notebook, scopedContextKeyService); + if (sourceActions.length === 1) { + // exact one action + updateActionFromSourceAction(sourceActions[0], false); + } else if (sourceActions.filter(sourceAction => sourceAction.isPrimary).length === 1) { + // exact one primary action + updateActionFromSourceAction(sourceActions.filter(sourceAction => sourceAction.isPrimary)[0], false); + } else { + action.class = ThemeIcon.asClassName(selectKernelIcon); + action.label = localize('select', "Select Kernel"); + action.tooltip = ''; + } + return; + } + + action.enabled = true; + action.class = ThemeIcon.asClassName(selectKernelIcon); + const selectedOrSuggested = info.selected + ?? (info.suggestions.length === 1 ? info.suggestions[0] : undefined) + ?? (info.all.length === 1 ? info.all[0] : undefined); + if (selectedOrSuggested) { + // selected or suggested kernel + action.label = selectedOrSuggested.label; + action.tooltip = selectedOrSuggested.description ?? selectedOrSuggested.detail ?? ''; + if (!info.selected) { + // special UI for selected kernel? + } + } else { + // many kernels or no kernels + action.label = localize('select', "Select Kernel"); + action.tooltip = ''; + } + } +} + +export class KernelPickerMRUStrategy extends KernelPickerStrategyBase { + constructor( + @INotebookKernelService _notebookKernelService: INotebookKernelService, + @IEditorService _editorService: IEditorService, + @IProductService _productService: IProductService, + @IQuickInputService _quickInputService: IQuickInputService, + @ILabelService _labelService: ILabelService, + @ILogService _logService: ILogService, + @IPaneCompositePartService _paneCompositePartService: IPaneCompositePartService, + @IExtensionsWorkbenchService _extensionWorkbenchService: IExtensionsWorkbenchService, + @IExtensionService _extensionService: IExtensionService, + @ICommandService _commandService: ICommandService, + @INotebookKernelHistoryService private readonly _notebookKernelHistoryService: INotebookKernelHistoryService, + + ) { + super( + _notebookKernelService, + _editorService, + _productService, + _quickInputService, + _labelService, + _logService, + _paneCompositePartService, + _extensionWorkbenchService, + _extensionService, + _commandService, + ); + } + + protected _getKernelPickerQuickPickItems(notebookTextModel: NotebookTextModel, matchResult: INotebookKernelMatchResult, notebookKernelService: INotebookKernelService, scopedContextKeyService: IContextKeyService): QuickPickInput[] { + const quickPickItems: QuickPickInput[] = []; + let previousKind = ''; + + if (matchResult.selected) { + const kernelItem = toQuickPick(matchResult.selected, matchResult.selected); + const kind = matchResult.selected.kind || ''; + if (kind) { + previousKind = kind; + quickPickItems.push({ type: 'separator', label: kind }); + } + quickPickItems.push(kernelItem); + } + + matchResult.suggestions.filter(kernel => kernel.id !== matchResult.selected?.id).map(kernel => toQuickPick(kernel, matchResult.selected)) + .forEach(kernel => { + const kind = kernel.kernel.kind || ''; + if (kind && kind !== previousKind) { + previousKind = kind; + quickPickItems.push({ type: 'separator', label: kind }); + } + quickPickItems.push(kernel); + }); + + quickPickItems.push({ + type: 'separator' + }); + + // select another kernel quick pick + quickPickItems.push({ + id: 'selectAnother', + label: localize('selectAnotherKernel.more', "Select Another Kernel..."), + }); + + return quickPickItems; + } + + protected override _selecteKernel(notebook: NotebookTextModel, kernel: INotebookKernel): void { + super._selecteKernel(notebook, kernel); + this._notebookKernelHistoryService.addMostRecentKernel(kernel); + } + + protected override _getMatchingResult(notebook: NotebookTextModel): INotebookKernelMatchResult { + const { selected, all } = this._notebookKernelHistoryService.getKernels(notebook); + const matchingResult = this._notebookKernelService.getMatchingKernel(notebook); + return { + selected: selected, + all: matchingResult.all, + suggestions: all, + hidden: [] + }; + } + + protected override async _handleQuickPick(notebook: NotebookTextModel, pick: KernelQuickPickItem, context?: KernelQuickPickContext): Promise { + if (pick.id === 'selectAnother') { + return this.displaySelectAnotherQuickPick(notebook, context); + } + + return super._handleQuickPick(notebook, pick, context); + } + + private async displaySelectAnotherQuickPick(notebook: NotebookTextModel, context?: KernelQuickPickContext) { + const disposables = new DisposableStore(); + return new Promise(resolve => { + // select from kernel sources + const quickPick = this._quickInputService.createQuickPick(); + quickPick.title = localize('selectAnotherKernel', "Select Another Kernel"); + quickPick.busy = true; + quickPick.buttons = [this._quickInputService.backButton]; + quickPick.show(); + + const quickPickItems: QuickPickInput[] = []; + disposables.add(quickPick.onDidTriggerButton(button => { + if (button === this._quickInputService.backButton) { + quickPick.hide(); + resolve(this.showQuickPick(context)); + } + })); + disposables.add(quickPick.onDidAccept(async () => { + quickPick.hide(); + quickPick.dispose(); + if (quickPick.selectedItems) { + if ('command' in quickPick.selectedItems[0]) { + const selectedKernelId = await this._executeCommand(notebook, quickPick.selectedItems[0].command); + if (selectedKernelId) { + const { all } = await this._getMatchingResult(notebook); + const kernel = all.find(kernel => kernel.id === `ms-toolsai.jupyter/${selectedKernelId}`); + if (kernel) { + await this._selecteKernel(notebook, kernel); + resolve(true); + } + resolve(true); + } else { + return resolve(this.displaySelectAnotherQuickPick(notebook)); + } + } else if ('kernel' in quickPick.selectedItems[0]) { + await this._selecteKernel(notebook, quickPick.selectedItems[0].kernel); + resolve(true); + } + } + })); + this._notebookKernelService.getKernelSourceActions2(notebook).then(actions => { + quickPick.busy = false; + const matchResult = this._getMatchingResult(notebook); + const others = matchResult.all.filter(item => item.extension.value !== JUPYTER_EXTENSION_ID); + quickPickItems.push(...others.map(kernel => ({ + label: kernel.label, + detail: kernel.extension.value, + kernel + }))); + const validActions = actions.filter(action => action.command); + + quickPickItems.push(...validActions.map(action => { + return { + id: typeof action.command! === 'string' ? action.command! : action.command!.id, + label: action.label, + detail: action.detail, + description: action.description, + command: action.command + }; + })); + + quickPick.items = quickPickItems; + }); + }).finally(() => { + disposables.dispose(); + }); + } + + private async _executeCommand(notebook: NotebookTextModel, command: string | Command): Promise { + const id = typeof command === 'string' ? command : command.id; + const args = typeof command === 'string' ? [] : command.arguments ?? []; + + if (typeof command === 'string' || !command.arguments || !Array.isArray(command.arguments) || command.arguments.length === 0) { + args.unshift({ + uri: notebook.uri, + $mid: MarshalledId.NotebookActionContext + }); + } + + if (typeof command === 'string') { + return this._commandService.executeCommand(id); + } else { + return this._commandService.executeCommand(id, ...args); + } + } + + static updateKernelStatusAction(notebook: NotebookTextModel, action: IAction, notebookKernelService: INotebookKernelService) { + const detectionTasks = notebookKernelService.getKernelDetectionTasks(notebook); + if (detectionTasks.length) { + action.enabled = true; + action.label = localize('kernels.detecting', "Detecting Kernels"); + action.class = ThemeIcon.asClassName(ThemeIcon.modify(executingStateIcon, 'spin')); + return; + } + + const info = notebookKernelService.getMatchingKernel(notebook); + + if (info.selected) { + action.label = info.selected.label; + action.class = ThemeIcon.asClassName(selectKernelIcon); + action.tooltip = info.selected.description ?? info.selected.detail ?? ''; + } else { + action.label = localize('select', "Select Kernel"); + action.class = ThemeIcon.asClassName(selectKernelIcon); + action.tooltip = ''; + } + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelView.ts b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelView.ts index 1e3f67378b1..fee5eecb3d8 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelView.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewParts/notebookKernelView.ts @@ -6,457 +6,21 @@ import 'vs/css!./notebookKernelActionViewItem'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { Action, IAction } from 'vs/base/common/actions'; -import { groupBy } from 'vs/base/common/arrays'; -import { createCancelablePromise } from 'vs/base/common/async'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { Codicon } from 'vs/base/common/codicons'; import { Event } from 'vs/base/common/event'; -import { compareIgnoreCase, uppercaseFirstLetter } from 'vs/base/common/strings'; import { localize } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { ILabelService } from 'vs/platform/label/common/label'; -import { ILogService } from 'vs/platform/log/common/log'; -import { IProductService } from 'vs/platform/product/common/productService'; -import { ProgressLocation } from 'vs/platform/progress/common/progress'; -import { IQuickInputService, IQuickPick, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { ViewContainerLocation } from 'vs/workbench/common/views'; -import { IExtensionsViewPaneContainer, IExtensionsWorkbenchService, VIEWLET_ID as EXTENSION_VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { NOTEBOOK_ACTIONS_CATEGORY, SELECT_KERNEL_ID } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; -import { getNotebookEditorFromEditorPane, INotebookEditor, INotebookExtensionRecommendation, KERNEL_RECOMMENDATIONS } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; -import { executingStateIcon, selectKernelIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; +import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { selectKernelIcon } from 'vs/workbench/contrib/notebook/browser/notebookIcons'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT } from 'vs/workbench/contrib/notebook/common/notebookContextKeys'; -import { INotebookKernel, INotebookKernelMatchResult, INotebookKernelService, ISourceAction } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; +import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { KernelPickerFlatStrategy, KernelPickerMRUStrategy, KernelQuickPickContext } from 'vs/workbench/contrib/notebook/browser/viewParts/notebookKernelQuickPickStrategy'; -type KernelPick = IQuickPickItem & { kernel: INotebookKernel }; -function isKernelPick(item: QuickPickInput): item is KernelPick { - return 'kernel' in item; -} -type SourcePick = IQuickPickItem & { action: ISourceAction }; -function isSourcePick(item: QuickPickInput): item is SourcePick { - return 'action' in item; -} -type InstallExtensionPick = IQuickPickItem & { extensionId: string }; -function isInstallExtensionPick(item: QuickPickInput): item is InstallExtensionPick { - return item.id === 'installSuggested' && 'extensionId' in item; -} -type KernelQuickPickItem = IQuickPickItem | InstallExtensionPick | KernelPick | SourcePick; -const KERNEL_PICKER_UPDATE_DEBOUNCE = 200; - -type KernelQuickPickContext = - { id: string; extension: string } | - { notebookEditorId: string } | - { id: string; extension: string; notebookEditorId: string } | - { ui?: boolean; notebookEditor?: NotebookEditorWidget }; - -interface IKernelPickerStrategy { - showQuickPick(context?: KernelQuickPickContext): Promise; -} - -class KernelPickerFlatStrategy implements IKernelPickerStrategy { - constructor( - private readonly _notebookKernelService: INotebookKernelService, - private readonly _editorService: IEditorService, - private readonly _productService: IProductService, - private readonly _quickInputService: IQuickInputService, - private readonly _labelService: ILabelService, - private readonly _logService: ILogService, - private readonly _paneCompositePartService: IPaneCompositePartService, - private readonly _extensionWorkbenchService: IExtensionsWorkbenchService, - private readonly _extensionService: IExtensionService, - ) { } - async showQuickPick(context?: KernelQuickPickContext): Promise { - const editor = this._getEditorFromContext(context); - - if (!editor || !editor.hasModel()) { - return false; - } - let controllerId = context && 'id' in context ? context.id : undefined; - let extensionId = context && 'extension' in context ? context.extension : undefined; - - if (controllerId && (typeof controllerId !== 'string' || typeof extensionId !== 'string')) { - // validate context: id & extension MUST be strings - controllerId = undefined; - extensionId = undefined; - } - - const notebook = editor.textModel; - const scopedContextKeyService = editor.scopedContextKeyService; - const matchResult = this._notebookKernelService.getMatchingKernel(notebook); - const { selected, all } = matchResult; - - if (selected && controllerId && selected.id === controllerId && ExtensionIdentifier.equals(selected.extension, extensionId)) { - // current kernel is wanted kernel -> done - return true; - } - - let newKernel: INotebookKernel | undefined; - if (controllerId) { - const wantedId = `${extensionId}/${controllerId}`; - for (const candidate of all) { - if (candidate.id === wantedId) { - newKernel = candidate; - break; - } - } - if (!newKernel) { - this._logService.warn(`wanted kernel DOES NOT EXIST, wanted: ${wantedId}, all: ${all.map(k => k.id)}`); - return false; - } - } - - if (newKernel) { - this._notebookKernelService.selectKernelForNotebook(newKernel, notebook); - return true; - } - - const quickPick = this._quickInputService.createQuickPick(); - const quickPickItems = this._getKernelPickerQuickPickItems(notebook, matchResult, this._notebookKernelService, scopedContextKeyService); - quickPick.items = quickPickItems; - quickPick.canSelectMany = false; - quickPick.placeholder = selected - ? localize('prompt.placeholder.change', "Change kernel for '{0}'", this._labelService.getUriLabel(notebook.uri, { relative: true })) - : localize('prompt.placeholder.select', "Select kernel for '{0}'", this._labelService.getUriLabel(notebook.uri, { relative: true })); - - quickPick.busy = this._notebookKernelService.getKernelDetectionTasks(notebook).length > 0; - - const kernelDetectionTaskListener = this._notebookKernelService.onDidChangeKernelDetectionTasks(() => { - quickPick.busy = this._notebookKernelService.getKernelDetectionTasks(notebook).length > 0; - }); - - // run extension recommendataion task if quickPickItems is empty - const extensionRecommendataionPromise = quickPickItems.length === 0 - ? createCancelablePromise(token => this._showInstallKernelExtensionRecommendation(notebook, quickPick, this._extensionWorkbenchService, token)) - : undefined; - - const kernelChangeEventListener = Event.debounce( - Event.any( - this._notebookKernelService.onDidChangeSourceActions, - this._notebookKernelService.onDidAddKernel, - this._notebookKernelService.onDidRemoveKernel, - this._notebookKernelService.onDidChangeNotebookAffinity - ), - (last, _current) => last, - KERNEL_PICKER_UPDATE_DEBOUNCE - )(async () => { - // reset quick pick progress - quickPick.busy = false; - extensionRecommendataionPromise?.cancel(); - - const currentActiveItems = quickPick.activeItems; - const matchResult = this._notebookKernelService.getMatchingKernel(notebook); - const quickPickItems = this._getKernelPickerQuickPickItems(notebook, matchResult, this._notebookKernelService, scopedContextKeyService); - quickPick.keepScrollPosition = true; - - // recalcuate active items - const activeItems: KernelQuickPickItem[] = []; - for (const item of currentActiveItems) { - if (isKernelPick(item)) { - const kernelId = item.kernel.id; - const sameItem = quickPickItems.find(pi => isKernelPick(pi) && pi.kernel.id === kernelId) as KernelPick | undefined; - if (sameItem) { - activeItems.push(sameItem); - } - } else if (isSourcePick(item)) { - const sameItem = quickPickItems.find(pi => isSourcePick(pi) && pi.action.action.id === item.action.action.id) as SourcePick | undefined; - if (sameItem) { - activeItems.push(sameItem); - } - } - } - - quickPick.items = quickPickItems; - quickPick.activeItems = activeItems; - }, this); - - const pick = await new Promise((resolve, reject) => { - quickPick.onDidAccept(() => { - const item = quickPick.selectedItems[0]; - if (item) { - resolve(item); - } else { - reject(); - } - - quickPick.hide(); - }); - - quickPick.onDidHide(() => () => { - kernelDetectionTaskListener.dispose(); - kernelChangeEventListener.dispose(); - quickPick.dispose(); - reject(); - }); - quickPick.show(); - }); - - if (pick) { - if (isKernelPick(pick)) { - newKernel = pick.kernel; - this._notebookKernelService.selectKernelForNotebook(newKernel, notebook); - return true; - } - - // actions - if (pick.id === 'install') { - await this._showKernelExtension( - this._paneCompositePartService, - this._extensionWorkbenchService, - this._extensionService, - notebook.viewType - ); - // suggestedExtension must be defined for this option to be shown, but still check to make TS happy - } else if (isInstallExtensionPick(pick)) { - await this._showKernelExtension( - this._paneCompositePartService, - this._extensionWorkbenchService, - this._extensionService, - notebook.viewType, - pick.extensionId, - this._productService.quality !== 'stable' - ); - } else if (isSourcePick(pick)) { - // selected explicilty, it should trigger the execution? - pick.action.runAction(); - } - } - - return false; - } - - private _getEditorFromContext(context?: KernelQuickPickContext): INotebookEditor | undefined { - let editor: INotebookEditor | undefined; - if (context !== undefined && 'notebookEditorId' in context) { - const editorId = context.notebookEditorId; - const matchingEditor = this._editorService.visibleEditorPanes.find((editorPane) => { - const notebookEditor = getNotebookEditorFromEditorPane(editorPane); - return notebookEditor?.getId() === editorId; - }); - editor = getNotebookEditorFromEditorPane(matchingEditor); - } else if (context !== undefined && 'notebookEditor' in context) { - editor = context?.notebookEditor; - } else { - editor = getNotebookEditorFromEditorPane(this._editorService.activeEditorPane); - } - - return editor; - } - - private _getKernelPickerQuickPickItems( - notebookTextModel: NotebookTextModel, - matchResult: INotebookKernelMatchResult, - notebookKernelService: INotebookKernelService, - scopedContextKeyService: IContextKeyService - ): QuickPickInput[] { - const { selected, all, suggestions, hidden } = matchResult; - - const quickPickItems: QuickPickInput[] = []; - if (all.length) { - // Always display suggested kernels on the top. - this._fillInSuggestions(quickPickItems, suggestions, selected); - - // Next display all of the kernels not marked as hidden grouped by categories or extensions. - // If we don't have a kind, always display those at the bottom. - const picks = all.filter(item => (!suggestions.includes(item) && !hidden.includes(item))).map(kernel => this._toQuickPick(kernel, selected)); - const kernelsPerCategory = groupBy(picks, (a, b) => compareIgnoreCase(a.kernel.kind || 'z', b.kernel.kind || 'z')); - kernelsPerCategory.forEach(items => { - quickPickItems.push({ - type: 'separator', - label: items[0].kernel.kind || localize('otherKernelKinds', "Other") - }); - quickPickItems.push(...items); - }); - } - - const sourceActions = notebookKernelService.getSourceActions(notebookTextModel, scopedContextKeyService); - if (sourceActions.length) { - quickPickItems.push({ - type: 'separator', - // label: localize('sourceActions', "") - }); - - sourceActions.forEach(sourceAction => { - const res = { - action: sourceAction, - picked: false, - label: sourceAction.action.label, - }; - - quickPickItems.push(res); - }); - } - - return quickPickItems; - } - - private _toQuickPick(kernel: INotebookKernel, selected: INotebookKernel | undefined) { - const res = { - kernel, - picked: kernel.id === selected?.id, - label: kernel.label, - description: kernel.description, - detail: kernel.detail - }; - if (kernel.id === selected?.id) { - if (!res.description) { - res.description = localize('current1', "Currently Selected"); - } else { - res.description = localize('current2', "{0} - Currently Selected", res.description); - } - } - return res; - } - - private _fillInSuggestions(quickPickItems: QuickPickInput[], suggestions: INotebookKernel[], selected: INotebookKernel | undefined) { - if (!suggestions.length) { - return; - } - - if (suggestions.length === 1 && suggestions[0].id === selected?.id) { - quickPickItems.push({ - type: 'separator', - label: localize('selectedKernels', "Selected") - }); - - // The title is already set to "Selected" so we don't need to set it again in description, thus passing in `undefined`. - quickPickItems.push(this._toQuickPick(suggestions[0], undefined)); - return; - } - - quickPickItems.push({ - type: 'separator', - label: localize('suggestedKernels', "Suggested") - }); - quickPickItems.push(...suggestions.map(kernel => this._toQuickPick(kernel, selected))); - } - - private async _showInstallKernelExtensionRecommendation( - notebookTextModel: NotebookTextModel, - quickPick: IQuickPick, - extensionWorkbenchService: IExtensionsWorkbenchService, - token: CancellationToken - ) { - quickPick.busy = true; - - const newQuickPickItems = await this._getKernelRecommendationsQuickPickItems(notebookTextModel, extensionWorkbenchService); - quickPick.busy = false; - - if (token.isCancellationRequested) { - return; - } - - if (newQuickPickItems && quickPick.items.length === 0) { - quickPick.items = newQuickPickItems; - } - } - - private async _getKernelRecommendationsQuickPickItems( - notebookTextModel: NotebookTextModel, - extensionWorkbenchService: IExtensionsWorkbenchService, - ): Promise[] | undefined> { - const quickPickItems: QuickPickInput[] = []; - - const language = this.getSuggestedLanguage(notebookTextModel); - const suggestedExtension: INotebookExtensionRecommendation | undefined = language ? this.getSuggestedKernelFromLanguage(notebookTextModel.viewType, language) : undefined; - if (suggestedExtension) { - await extensionWorkbenchService.queryLocal(); - const extension = extensionWorkbenchService.installed.find(e => e.identifier.id === suggestedExtension.extensionId); - - if (extension) { - // it's installed but might be detecting kernels - return undefined; - } - - // We have a suggested kernel, show an option to install it - quickPickItems.push({ - id: 'installSuggested', - description: suggestedExtension.displayName ?? suggestedExtension.extensionId, - label: `$(${Codicon.lightbulb.id}) ` + localize('installSuggestedKernel', 'Install suggested extensions'), - extensionId: suggestedExtension.extensionId - }); - } - // there is no kernel, show the install from marketplace - quickPickItems.push({ - id: 'install', - label: localize('searchForKernels', "Browse marketplace for kernel extensions"), - }); - - return quickPickItems; - } - - /** - * Examine the most common language in the notebook - * @param notebookTextModel The notebook text model - * @returns What the suggested language is for the notebook. Used for kernal installing - */ - private getSuggestedLanguage(notebookTextModel: NotebookTextModel): string | undefined { - const metaData = notebookTextModel.metadata; - let suggestedKernelLanguage: string | undefined = (metaData.custom as any)?.metadata?.language_info?.name; - // TODO how do we suggest multi language notebooks? - if (!suggestedKernelLanguage) { - const cellLanguages = notebookTextModel.cells.map(cell => cell.language).filter(language => language !== 'markdown'); - // Check if cell languages is all the same - if (cellLanguages.length > 1) { - const firstLanguage = cellLanguages[0]; - if (cellLanguages.every(language => language === firstLanguage)) { - suggestedKernelLanguage = firstLanguage; - } - } - } - return suggestedKernelLanguage; - } - - /** - * Given a language and notebook view type suggest a kernel for installation - * @param language The language to find a suggested kernel extension for - * @returns A recommednation object for the recommended extension, else undefined - */ - private getSuggestedKernelFromLanguage(viewType: string, language: string): INotebookExtensionRecommendation | undefined { - const recommendation = KERNEL_RECOMMENDATIONS.get(viewType)?.get(language); - return recommendation; - } - - private async _showKernelExtension( - paneCompositePartService: IPaneCompositePartService, - extensionWorkbenchService: IExtensionsWorkbenchService, - extensionService: IExtensionService, - viewType: string, - extId?: string, - isInsiders?: boolean - ) { - // If extension id is provided attempt to install the extension as the user has requested the suggested ones be installed - if (extId) { - const extension = (await extensionWorkbenchService.getExtensions([{ id: extId }], CancellationToken.None))[0]; - const canInstall = await extensionWorkbenchService.canInstall(extension); - // If we can install then install it, otherwise we will fall out into searching the viewlet - if (canInstall) { - await extensionWorkbenchService.install( - extension, - { - installPreReleaseVersion: isInsiders ?? false, - context: { skipWalkthrough: true } - }, - ProgressLocation.Notification - ); - await extensionService.activateByEvent(`onNotebook:${viewType}`); - return; - } - } - - const viewlet = await paneCompositePartService.openPaneComposite(EXTENSION_VIEWLET_ID, ViewContainerLocation.Sidebar, true); - const view = viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer | undefined; - const pascalCased = viewType.split(/[^a-z0-9]/ig).map(uppercaseFirstLetter).join(''); - view?.search(`@tag:notebookKernel${pascalCased}`); - } -} registerAction2(class extends Action2 { constructor() { @@ -513,28 +77,18 @@ registerAction2(class extends Action2 { } async run(accessor: ServicesAccessor, context?: KernelQuickPickContext): Promise { - const notebookKernelService = accessor.get(INotebookKernelService); - const editorService = accessor.get(IEditorService); - const productService = accessor.get(IProductService); - const quickInputService = accessor.get(IQuickInputService); - const labelService = accessor.get(ILabelService); - const logService = accessor.get(ILogService); - const paneCompositeService = accessor.get(IPaneCompositePartService); - const extensionWorkbenchService = accessor.get(IExtensionsWorkbenchService); - const extensionHostService = accessor.get(IExtensionService); + const instantiationService = accessor.get(IInstantiationService); + const configurationService = accessor.get(IConfigurationService); - const strategy = new KernelPickerFlatStrategy( - notebookKernelService, - editorService, - productService, - quickInputService, - labelService, - logService, - paneCompositeService, - extensionWorkbenchService, - extensionHostService - ); - return await strategy.showQuickPick(context); + const kernelPickerType = configurationService.getValue<'all' | 'mru'>('notebook.kernelPicker.type'); + + if (kernelPickerType === 'mru') { + const strategy = instantiationService.createInstance(KernelPickerMRUStrategy); + return await strategy.showQuickPick(context); + } else { + const strategy = instantiationService.createInstance(KernelPickerFlatStrategy); + return await strategy.showQuickPick(context); + } } }); @@ -546,6 +100,7 @@ export class NotebooKernelActionViewItem extends ActionViewItem { actualAction: IAction, private readonly _editor: { onDidChangeModel: Event; textModel: NotebookTextModel | undefined; scopedContextKeyService?: IContextKeyService } | INotebookEditor, @INotebookKernelService private readonly _notebookKernelService: INotebookKernelService, + @IConfigurationService private readonly _configurationService: IConfigurationService, ) { super( undefined, @@ -584,76 +139,14 @@ export class NotebooKernelActionViewItem extends ActionViewItem { return; } - const detectionTasks = this._notebookKernelService.getKernelDetectionTasks(notebook); - if (detectionTasks.length) { - return this._updateActionFromDetectionTask(); + const kernelPickerType = this._configurationService.getValue<'all' | 'mru'>('notebook.kernelPicker.type'); + if (kernelPickerType === 'mru') { + KernelPickerMRUStrategy.updateKernelStatusAction(notebook, this._action, this._notebookKernelService); + } else { + KernelPickerFlatStrategy.updateKernelStatusAction(notebook, this._action, this._notebookKernelService, this._editor.scopedContextKeyService); } - const runningActions = this._notebookKernelService.getRunningSourceActions(notebook); - if (runningActions.length) { - return this._updateActionFromSourceAction(runningActions[0] /** TODO handle multiple actions state */, true); - } - - const info = this._notebookKernelService.getMatchingKernel(notebook); - if (info.all.length === 0) { - return this._updateActionsFromSourceActions(); - } - - this._updateActionFromKernelInfo(info); - } - - private _updateActionFromDetectionTask() { - this._action.enabled = true; - this._action.label = localize('kernels.detecting', "Detecting Kernels"); - this._action.class = ThemeIcon.asClassName(ThemeIcon.modify(executingStateIcon, 'spin')); - } - - private _updateActionFromSourceAction(sourceAction: ISourceAction, running: boolean) { - const action = sourceAction.action; - this.action.class = running ? ThemeIcon.asClassName(ThemeIcon.modify(executingStateIcon, 'spin')) : ThemeIcon.asClassName(selectKernelIcon); this.updateClass(); - this._action.label = action.label; - this._action.enabled = true; - } - - private _updateActionsFromSourceActions() { - this._action.enabled = true; - const sourceActions = this._editor.textModel ? this._notebookKernelService.getSourceActions(this._editor.textModel, this._editor.scopedContextKeyService) : []; - if (sourceActions.length === 1) { - // exact one action - this._updateActionFromSourceAction(sourceActions[0], false); - } else if (sourceActions.filter(sourceAction => sourceAction.isPrimary).length === 1) { - // exact one primary action - this._updateActionFromSourceAction(sourceActions.filter(sourceAction => sourceAction.isPrimary)[0], false); - } else { - this._action.class = ThemeIcon.asClassName(selectKernelIcon); - this._action.label = localize('select', "Select Kernel"); - this._action.tooltip = ''; - } - } - - private _updateActionFromKernelInfo(info: INotebookKernelMatchResult): void { - this._action.enabled = true; - this._action.class = ThemeIcon.asClassName(selectKernelIcon); - const selectedOrSuggested = info.selected - ?? (info.suggestions.length === 1 ? info.suggestions[0] : undefined) - ?? (info.all.length === 1 ? info.all[0] : undefined); - if (selectedOrSuggested) { - // selected or suggested kernel - this._action.label = this._generateKenrelLabel(selectedOrSuggested); - this._action.tooltip = selectedOrSuggested.description ?? selectedOrSuggested.detail ?? ''; - if (!info.selected) { - // special UI for selected kernel? - } - } else { - // many kernels or no kernels - this._action.label = localize('select', "Select Kernel"); - this._action.tooltip = ''; - } - } - - private _generateKenrelLabel(kernel: INotebookKernel) { - return kernel.label; } private _resetAction(): void { diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index c033b37ceb8..9cf0175219e 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -926,7 +926,7 @@ export const NotebookSetting = { outputLineHeight: 'notebook.outputLineHeight', outputFontSize: 'notebook.outputFontSize', outputFontFamily: 'notebook.outputFontFamily', - kernelPickerMRU: 'notebook.experimental.kernelPicker.mru' + kernelPickerType: 'notebook.kernelPicker.type' } as const; export const enum CellStatusbarAlignment { @@ -1051,3 +1051,10 @@ function formatStreamText(buffer: VSBuffer): VSBuffer { // Do the same thing jupyter is doing return VSBuffer.fromString(fixCarriageReturn(fixBackspace(textDecoder.decode(buffer.buffer)))); } + +export interface INotebookKernelSourceAction { + readonly label: string; + readonly description?: string; + readonly detail?: string; + readonly command?: string | Command; +} diff --git a/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts b/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts index a79f5505650..f881f9dc789 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts @@ -10,6 +10,7 @@ import { URI } from 'vs/base/common/uri'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { INotebookKernelSourceAction } from 'vs/workbench/contrib/notebook/common/notebookCommon'; export interface ISelectedNotebooksChangeEvent { notebook: URI; @@ -83,6 +84,11 @@ export interface INotebookSourceActionChangeEvent { notebook: URI; } +export interface IKernelSourceActionProvider { + readonly viewType: string; + provideKernelSourceActions(): Promise; +} + export interface INotebookTextModelLike { uri: URI; viewType: string } export const INotebookKernelService = createDecorator('INotebookKernelService'); @@ -129,5 +135,14 @@ export interface INotebookKernelService { readonly onDidChangeSourceActions: Event; getSourceActions(notebook: INotebookTextModelLike, contextKeyService: IContextKeyService | undefined): ISourceAction[]; getRunningSourceActions(notebook: INotebookTextModelLike): ISourceAction[]; + registerKernelSourceActionProvider(viewType: string, provider: IKernelSourceActionProvider): IDisposable; + getKernelSourceActions2(notebook: INotebookTextModelLike): Promise; //#endregion } + +export const INotebookKernelHistoryService = createDecorator('INotebookKernelHistoryService'); +export interface INotebookKernelHistoryService { + _serviceBrand: undefined; + getKernels(notebook: INotebookTextModelLike): { selected: INotebookKernel | undefined; all: INotebookKernel[] }; + addMostRecentKernel(kernel: INotebookKernel): void; +} diff --git a/src/vs/workbench/contrib/notebook/test/browser/notebookKernelHistory.test.ts b/src/vs/workbench/contrib/notebook/test/browser/notebookKernelHistory.test.ts new file mode 100644 index 00000000000..0c0bad3cf73 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/test/browser/notebookKernelHistory.test.ts @@ -0,0 +1,168 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { URI } from 'vs/base/common/uri'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { setupInstantiationService, withTestNotebook as _withTestNotebook } from 'vs/workbench/contrib/notebook/test/browser/testNotebookEditor'; +import { Emitter, Event } from 'vs/base/common/event'; +import { INotebookKernel, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; +import { NotebookKernelService } from 'vs/workbench/contrib/notebook/browser/services/notebookKernelServiceImpl'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { mock } from 'vs/base/test/common/mock'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; +import { IMenu, IMenuService } from 'vs/platform/actions/common/actions'; +import { NotebookKernelHistoryService } from 'vs/workbench/contrib/notebook/browser/services/notebookKernelHistoryServiceImpl'; +import { IStorageService, IWillSaveStateEvent, StorageScope } from 'vs/platform/storage/common/storage'; + +suite('NotebookKernelHistoryService', () => { + + let instantiationService: TestInstantiationService; + let kernelService: INotebookKernelService; + let disposables: DisposableStore; + + let onDidAddNotebookDocument: Emitter; + + setup(function () { + disposables = new DisposableStore(); + + onDidAddNotebookDocument = new Emitter(); + disposables.add(onDidAddNotebookDocument); + + instantiationService = setupInstantiationService(disposables); + instantiationService.stub(INotebookService, new class extends mock() { + override onDidAddNotebookDocument = onDidAddNotebookDocument.event; + override onWillRemoveNotebookDocument = Event.None; + override getNotebookTextModels() { return []; } + }); + instantiationService.stub(IMenuService, new class extends mock() { + override createMenu() { + return new class extends mock() { + override onDidChange = Event.None; + override getActions() { return []; } + override dispose() { } + }; + } + }); + kernelService = instantiationService.createInstance(NotebookKernelService); + instantiationService.set(INotebookKernelService, kernelService); + }); + + teardown(() => { + disposables.dispose(); + }); + + test('notebook kernel empty history', function () { + + const u1 = URI.parse('foo:///one'); + + const k1 = new TestNotebookKernel({ label: 'z', viewType: 'foo' }); + const k2 = new TestNotebookKernel({ label: 'a', viewType: 'foo' }); + + kernelService.registerKernel(k1); + kernelService.registerKernel(k2); + + instantiationService.stub(IStorageService, new class extends mock() { + override onWillSaveState: Event = Event.None; + override get(key: string, scope: StorageScope, fallbackValue: string): string; + override get(key: string, scope: StorageScope, fallbackValue?: string | undefined): string | undefined; + override get(key: unknown, scope: unknown, fallbackValue?: unknown): string | undefined { + if (key === 'notebook.kernelHistory') { + return JSON.stringify({ + 'foo': { + 'entries': [] + } + }); + } + + return undefined; + } + }); + + const kernelHistoryService = instantiationService.createInstance(NotebookKernelHistoryService); + + let info = kernelHistoryService.getKernels({ uri: u1, viewType: 'foo' }); + assert.equal(info.all.length, 0); + assert.ok(!info.selected); + + // update priorities for u1 notebook + kernelService.updateKernelNotebookAffinity(k2, u1, 2); + + info = kernelHistoryService.getKernels({ uri: u1, viewType: 'foo' }); + assert.equal(info.all.length, 0); + // suggested kernel should be visible + assert.deepStrictEqual(info.selected, k2); + }); + + test('notebook kernel history restore', function () { + + const u1 = URI.parse('foo:///one'); + + const k1 = new TestNotebookKernel({ label: 'z', viewType: 'foo' }); + const k2 = new TestNotebookKernel({ label: 'a', viewType: 'foo' }); + const k3 = new TestNotebookKernel({ label: 'b', viewType: 'foo' }); + + kernelService.registerKernel(k1); + kernelService.registerKernel(k2); + kernelService.registerKernel(k3); + + instantiationService.stub(IStorageService, new class extends mock() { + override onWillSaveState: Event = Event.None; + override get(key: string, scope: StorageScope, fallbackValue: string): string; + override get(key: string, scope: StorageScope, fallbackValue?: string | undefined): string | undefined; + override get(key: unknown, scope: unknown, fallbackValue?: unknown): string | undefined { + if (key === 'notebook.kernelHistory') { + return JSON.stringify({ + 'foo': { + 'entries': [ + k2.id + ] + } + }); + } + + return undefined; + } + }); + + const kernelHistoryService = instantiationService.createInstance(NotebookKernelHistoryService); + let info = kernelHistoryService.getKernels({ uri: u1, viewType: 'foo' }); + assert.equal(info.all.length, 1); + assert.deepStrictEqual(info.selected, undefined); + + kernelHistoryService.addMostRecentKernel(k3); + info = kernelHistoryService.getKernels({ uri: u1, viewType: 'foo' }); + assert.deepStrictEqual(info.all, [k3, k2]); + }); +}); + +class TestNotebookKernel implements INotebookKernel { + id: string = Math.random() + 'kernel'; + label: string = 'test-label'; + viewType = '*'; + onDidChange = Event.None; + extension: ExtensionIdentifier = new ExtensionIdentifier('test'); + localResourceRoot: URI = URI.file('/test'); + description?: string | undefined; + detail?: string | undefined; + preloadUris: URI[] = []; + preloadProvides: string[] = []; + supportedLanguages: string[] = []; + executeNotebookCellsRequest(): Promise { + throw new Error('Method not implemented.'); + } + cancelNotebookCellExecution(): Promise { + throw new Error('Method not implemented.'); + } + + constructor(opts?: { languages?: string[]; label?: string; viewType?: string }) { + this.supportedLanguages = opts?.languages ?? [PLAINTEXT_LANGUAGE_ID]; + this.label = opts?.label ?? this.label; + this.viewType = opts?.viewType ?? this.viewType; + } +} diff --git a/src/vs/workbench/contrib/outline/browser/outlinePane.ts b/src/vs/workbench/contrib/outline/browser/outlinePane.ts index 09e74bcd91d..644cc8bb85a 100644 --- a/src/vs/workbench/contrib/outline/browser/outlinePane.ts +++ b/src/vs/workbench/contrib/outline/browser/outlinePane.ts @@ -35,7 +35,7 @@ import { ITreeSorter } from 'vs/base/browser/ui/tree/tree'; import { AbstractTreeViewState, IAbstractTreeViewState, TreeFindMode } from 'vs/base/browser/ui/tree/abstractTree'; import { URI } from 'vs/base/common/uri'; import { ctxAllCollapsed, ctxFilterOnType, ctxFollowsCursor, ctxSortMode, IOutlinePane, OutlineSortOrder } from 'vs/workbench/contrib/outline/browser/outline'; -import { getProgressBarStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { defaultProgressBarStyles } from 'vs/platform/theme/browser/defaultStyles'; class OutlineTreeSorter implements ITreeSorter { @@ -136,7 +136,7 @@ export class OutlinePane extends ViewPane implements IOutlinePane { const progressContainer = dom.$('.outline-progress'); this._message = dom.$('.outline-message'); - this._progressBar = new ProgressBar(progressContainer, getProgressBarStyles()); + this._progressBar = new ProgressBar(progressContainer, defaultProgressBarStyles); this._treeContainer = dom.$('.outline-tree'); dom.append(container, progressContainer, this._message, this._treeContainer); diff --git a/src/vs/workbench/contrib/performance/browser/inputLatencyContrib.ts b/src/vs/workbench/contrib/performance/browser/inputLatencyContrib.ts index 4f85f56e7dd..32f3bbe3273 100644 --- a/src/vs/workbench/contrib/performance/browser/inputLatencyContrib.ts +++ b/src/vs/workbench/contrib/performance/browser/inputLatencyContrib.ts @@ -43,18 +43,32 @@ export class InputLatencyContrib extends Disposable implements IWorkbenchContrib return; } + type InputLatencyStatisticFragment = { + owner: 'tyriar'; + comment: 'Represents a set of statistics collected about input latencies'; + average: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The average time it took to execute.'; isMeasurement: true }; + max: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The maximum time it took to execute.'; isMeasurement: true }; + min: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The minimum time it took to execute.'; isMeasurement: true }; + }; + type PerformanceInputLatencyClassification = { owner: 'tyriar'; comment: 'This is a set of samples of the time (in milliseconds) that various events took when typing in the editor'; - keydown: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The min, max and average time it took for the keydown event to execute.' }; - input: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The min, max and average time it took for the input event to execute.' }; - render: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The min, max and average time it took for the render animation frame to execute.' }; - total: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The min, max and average input latency.' }; + keydown: InputLatencyStatisticFragment; + input: InputLatencyStatisticFragment; + render: InputLatencyStatisticFragment; + total: InputLatencyStatisticFragment; sampleCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The number of samples measured.' }; }; type PerformanceInputLatencyEvent = inputLatency.IInputLatencyMeasurements; - this._telemetryService.publicLog2('performance.inputLatency', measurements); + this._telemetryService.publicLog2('performance.inputLatency', { + keydown: measurements.keydown, + input: measurements.input, + render: measurements.render, + total: measurements.total, + sampleCount: measurements.sampleCount + }); } } diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts b/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts index 27abf3b0cbf..3f0c333f2eb 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingWidgets.ts @@ -20,7 +20,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from 'vs/editor/browser/editorBrowser'; -import { attachInputBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; +import { attachStylerCallback } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { editorWidgetBackground, editorWidgetForeground, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; import { ScrollType } from 'vs/editor/common/editorCommon'; @@ -28,7 +28,7 @@ import { SearchWidget, SearchOptions } from 'vs/workbench/contrib/preferences/br import { withNullAsUndefined } from 'vs/base/common/types'; import { Promises, timeout } from 'vs/base/common/async'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { getKeybindingLabelStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { defaultInputBoxStyles, defaultKeybindingLabelStyles } from 'vs/platform/theme/browser/defaultStyles'; export interface KeybindingsSearchOptions extends SearchOptions { recordEnter?: boolean; @@ -63,7 +63,6 @@ export class KeybindingsSearchWidget extends SearchWidget { @IKeybindingService keybindingService: IKeybindingService, ) { super(parent, options, contextViewService, instantiationService, themeService, contextKeyService, keybindingService); - this._register(attachInputBoxStyler(this.inputBox, themeService)); this._register(toDisposable(() => this.stopRecordingKeys())); this._firstPart = null; this._chordPart = null; @@ -204,7 +203,7 @@ export class DefineKeybindingWidget extends Widget { } })); - this._keybindingInputWidget = this._register(this.instantiationService.createInstance(KeybindingsSearchWidget, this._domNode.domNode, { ariaLabel: message, history: [] })); + this._keybindingInputWidget = this._register(this.instantiationService.createInstance(KeybindingsSearchWidget, this._domNode.domNode, { ariaLabel: message, history: [], inputBoxStyles: defaultInputBoxStyles })); this._keybindingInputWidget.startRecordingKeys(); this._register(this._keybindingInputWidget.onKeybinding(keybinding => this.onKeybinding(keybinding))); this._register(this._keybindingInputWidget.onEnter(() => this.hide())); @@ -277,12 +276,12 @@ export class DefineKeybindingWidget extends Widget { dom.clearNode(this._outputNode); dom.clearNode(this._showExistingKeybindingsNode); - const firstLabel = new KeybindingLabel(this._outputNode, OS, getKeybindingLabelStyles()); + const firstLabel = new KeybindingLabel(this._outputNode, OS, defaultKeybindingLabelStyles); firstLabel.set(withNullAsUndefined(this._firstPart)); if (this._chordPart) { this._outputNode.appendChild(document.createTextNode(nls.localize('defineKeybinding.chordsTo', "chord to"))); - const chordLabel = new KeybindingLabel(this._outputNode, OS, getKeybindingLabelStyles()); + const chordLabel = new KeybindingLabel(this._outputNode, OS, defaultKeybindingLabelStyles); chordLabel.set(this._chordPart); } diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts index a3a1187e91d..0e71587ff2a 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts @@ -35,36 +35,23 @@ import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions'; import { WorkbenchTable } from 'vs/platform/list/browser/listService'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { attachStylerCallback, attachInputBoxStyler, attachToggleStyler } from 'vs/platform/theme/common/styler'; +import { attachStylerCallback } from 'vs/platform/theme/common/styler'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; import { Emitter, Event } from 'vs/base/common/event'; import { MenuRegistry, MenuId, isIMenuItem } from 'vs/platform/actions/common/actions'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { WORKBENCH_BACKGROUND } from 'vs/workbench/common/theme'; -import { IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { IKeybindingItemEntry, IKeybindingsEditorPane } from 'vs/workbench/services/preferences/common/preferences'; import { keybindingsRecordKeysIcon, keybindingsSortIcon, keybindingsAddIcon, preferencesClearInputIcon, keybindingsEditIcon } from 'vs/workbench/contrib/preferences/browser/preferencesIcons'; import { ITableRenderer, ITableVirtualDelegate } from 'vs/base/browser/ui/table/table'; import { KeybindingsEditorInput } from 'vs/workbench/services/preferences/browser/keybindingsEditorInput'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; -import { getKeybindingLabelStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { defaultInputBoxStyles, defaultKeybindingLabelStyles, defaultToggleStyles } from 'vs/platform/theme/browser/defaultStyles'; const $ = DOM.$; -class ThemableToggleActionViewItem extends ToggleActionViewItem { - - constructor(context: any, action: IAction, options: IActionViewItemOptions, private readonly themeService: IThemeService) { - super(context, action, options); - } - - override render(container: HTMLElement): void { - super.render(container); - this._register(attachToggleStyler(this.toggle, this.themeService)); - } -} - export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorPane { static readonly ID: string = 'workbench.editor.keybindings'; @@ -325,6 +312,7 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP recordEnter: true, quoteRecordedKeys: true, history: this.getMemento(StorageScope.PROFILE, StorageTarget.USER)['searchHistory'] || [], + inputBoxStyles: defaultInputBoxStyles })); this._register(this.searchWidget.onDidChange(searchValue => { clearInputAction.enabled = !!searchValue; @@ -365,7 +353,7 @@ export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorP const toolBar = this._register(new ToolBar(this.actionsContainer, this.contextMenuService, { actionViewItemProvider: (action: IAction) => { if (action.id === this.sortByPrecedenceAction.id || action.id === this.recordKeysAction.id) { - return new ThemableToggleActionViewItem(null, action, { keybinding: this.keybindingsService.lookupKeybinding(action.id)?.getLabel() }, this.themeService); + return new ToggleActionViewItem(null, action, { keybinding: this.keybindingsService.lookupKeybinding(action.id)?.getLabel(), toggleStyles: defaultToggleStyles }); } return undefined; }, @@ -939,7 +927,7 @@ class KeybindingColumnRenderer implements ITableRenderer = disposables.add(new Emitter()); const onDidAccept: Event = _onDidAccept.event; diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css index 00cfefb81a0..61d9d5c5f95 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css @@ -344,6 +344,7 @@ overflow: hidden; text-overflow: ellipsis; display: inline-block; /* size to contents for hover to show context button */ + padding-bottom: 2px; /* so that focus outlines wrap around nicely for indicators, especially ones with codicons */ } .settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-modified-indicator { @@ -368,7 +369,7 @@ bottom: 23px; } -.settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-title > .misc-label { +.settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-title > .setting-indicators-container { font-style: italic; } @@ -379,8 +380,8 @@ opacity: 0.9; } -.settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-title > .misc-label .setting-indicator:hover { - text-decoration: underline; +.settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-title > .setting-indicators-container .setting-indicator { + padding-bottom: 2px; } .settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-title .codicon { @@ -483,12 +484,18 @@ margin: 0px; } +.settings-editor > .settings-body .settings-tree-container .setting-item-contents .edit-in-settings-button { + opacity: 0.9; +} + .settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-markdown a, +.settings-editor > .settings-body .settings-tree-container .setting-item-contents .edit-in-settings-button, .settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-markdown a > code { color: var(--vscode-textLink-foreground); } -.settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-markdown a:focus { +.settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-markdown a:focus, +.settings-editor > .settings-body .settings-tree-container .setting-item-contents .edit-in-settings-button:focus { outline: 1px solid -webkit-focus-ring-color; outline-offset: -1px; text-decoration: underline; @@ -497,12 +504,15 @@ .settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-markdown a:hover, .settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-markdown a:active, +.settings-editor > .settings-body .settings-tree-container .setting-item-contents .edit-in-settings-button:hover, +.settings-editor > .settings-body .settings-tree-container .setting-item-contents .edit-in-settings-button:active, .settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-markdown a:hover > code, .settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-markdown a:active > code { color: var(--vscode-textLink-activeForeground); } -.settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-markdown a:hover { +.settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-markdown a:hover, +.settings-editor > .settings-body .settings-tree-container .edit-in-settings-button:hover { cursor: pointer; text-decoration: underline; } @@ -569,14 +579,6 @@ width: 320px; } -.settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-value .edit-in-settings-button, -.settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-value .edit-in-settings-button:hover, -.settings-editor > .settings-body .settings-tree-container .setting-item-contents .setting-item-value .edit-in-settings-button:active { - text-align: left; - text-decoration: underline; - padding-left: 0px; -} - .settings-editor > .settings-body .settings-tree-container .setting-item-contents .monaco-select-box { width: initial; font: inherit; diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts index cab5ff5e1f1..47a1eb06a31 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesWidgets.ts @@ -29,7 +29,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ILabelService } from 'vs/platform/label/common/label'; import { badgeBackground, badgeForeground, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; -import { attachInputBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; +import { attachStylerCallback } from 'vs/platform/theme/common/styler'; import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { isWorkspaceFolder, IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { settingsEditIcon, settingsScopeDropDownIcon } from 'vs/workbench/contrib/preferences/browser/preferencesIcons'; @@ -452,8 +452,6 @@ export class SearchWidget extends Widget { protected createInputBox(parent: HTMLElement): HistoryInputBox { const showHistoryHint = () => showHistoryKeybindingHint(this.keybindingService); const box = this._register(new ContextScopedHistoryInputBox(parent, this.contextViewService, { ...this.options, showHistoryHint }, this.contextKeyService)); - this._register(attachInputBoxStyler(box, this.themeService)); - return box; } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts index fba13a6f9b4..761bfb04414 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditorSettingIndicators.ts @@ -4,12 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; -import { IHoverDelegate, IHoverDelegateOptions } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate'; -import { ICustomHover, ITooltipMarkdownString, IUpdatableHoverOptions, setupCustomHover } from 'vs/base/browser/ui/iconLabel/iconLabelHover'; +import { HoverPosition } from 'vs/base/browser/ui/hover/hoverWidget'; import { SimpleIconLabel } from 'vs/base/browser/ui/iconLabel/simpleIconLabel'; +import { RunOnceScheduler } from 'vs/base/common/async'; import { Emitter } from 'vs/base/common/event'; -import { IDisposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ILanguageService } from 'vs/editor/common/languages/language'; import { localize } from 'vs/nls'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -18,12 +21,12 @@ import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/use import { getIgnoredSettings } from 'vs/platform/userDataSync/common/settingsMerge'; import { getDefaultIgnoredSettings, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; import { SettingsTreeSettingElement } from 'vs/workbench/contrib/preferences/browser/settingsTreeModels'; -import { MODIFIED_INDICATOR_USE_INLINE_ONLY, POLICY_SETTING_TAG } from 'vs/workbench/contrib/preferences/common/preferences'; -import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; +import { POLICY_SETTING_TAG } from 'vs/workbench/contrib/preferences/common/preferences'; +import { IHoverOptions, IHoverService, IHoverWidget } from 'vs/workbench/services/hover/browser/hover'; const $ = DOM.$; -type ScopeString = 'workspace' | 'user' | 'remote'; +type ScopeString = 'workspace' | 'user' | 'remote' | 'default'; export interface ISettingOverrideClickEvent { scope: ScopeString; @@ -34,122 +37,158 @@ export interface ISettingOverrideClickEvent { interface SettingIndicator { element: HTMLElement; label: SimpleIconLabel; - hover: MutableDisposable; + disposables: DisposableStore; } /** * Renders the indicators next to a setting, such as "Also Modified In". */ export class SettingsTreeIndicatorsLabel implements IDisposable { - private indicatorsContainerElement: HTMLElement; - private hoverDelegate: IHoverDelegate; + private readonly indicatorsContainerElement: HTMLElement; - private workspaceTrustIndicator: SettingIndicator; - private scopeOverridesIndicator: SettingIndicator; - private syncIgnoredIndicator: SettingIndicator; - private defaultOverrideIndicator: SettingIndicator; + private readonly workspaceTrustIndicator: SettingIndicator; + private readonly scopeOverridesIndicator: SettingIndicator; + private readonly syncIgnoredIndicator: SettingIndicator; + private readonly defaultOverrideIndicator: SettingIndicator; + private readonly allIndicators: SettingIndicator[]; - private profilesEnabled: boolean; + private readonly profilesEnabled: boolean; constructor( container: HTMLElement, - @IConfigurationService configurationService: IConfigurationService, - @IHoverService hoverService: IHoverService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IHoverService private readonly hoverService: IHoverService, @IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService, @ILanguageService private readonly languageService: ILanguageService, @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, @ICommandService private readonly commandService: ICommandService) { - this.indicatorsContainerElement = DOM.append(container, $('.misc-label')); + this.indicatorsContainerElement = DOM.append(container, $('.setting-indicators-container')); this.indicatorsContainerElement.style.display = 'inline'; - this.hoverDelegate = { - showHover: (options: IHoverDelegateOptions, focus?: boolean) => { - return hoverService.showHover(options, focus); - }, - onDidHideHover: () => { }, - delay: configurationService.getValue('workbench.hover.delay'), - placement: 'element' - }; - this.profilesEnabled = this.userDataProfilesService.isEnabled(); this.workspaceTrustIndicator = this.createWorkspaceTrustIndicator(); this.scopeOverridesIndicator = this.createScopeOverridesIndicator(); this.syncIgnoredIndicator = this.createSyncIgnoredIndicator(); this.defaultOverrideIndicator = this.createDefaultOverrideIndicator(); + this.allIndicators = [this.workspaceTrustIndicator, this.scopeOverridesIndicator, this.syncIgnoredIndicator, this.defaultOverrideIndicator]; + } + + private defaultHoverOptions: Partial = { + hoverPosition: HoverPosition.BELOW, + showPointer: true, + compact: false + }; + + private addHoverDisposables(disposables: DisposableStore, element: HTMLElement, showHover: (focus: boolean) => IHoverWidget | undefined) { + disposables.clear(); + const scheduler: RunOnceScheduler = disposables.add(new RunOnceScheduler(() => { + const hover = showHover(false); + if (hover) { + disposables.add(hover); + } + }, this.configurationService.getValue('workbench.hover.delay'))); + disposables.add(DOM.addDisposableListener(element, DOM.EventType.MOUSE_OVER, () => { + if (!scheduler.isScheduled()) { + scheduler.schedule(); + } + })); + disposables.add(DOM.addDisposableListener(element, DOM.EventType.MOUSE_LEAVE, () => { + scheduler.cancel(); + })); + disposables.add(DOM.addDisposableListener(element, DOM.EventType.KEY_DOWN, (e) => { + const evt = new StandardKeyboardEvent(e); + if (evt.equals(KeyCode.Space) || evt.equals(KeyCode.Enter)) { + const hover = showHover(true); + if (hover) { + disposables.add(hover); + } + e.preventDefault(); + } + })); } private createWorkspaceTrustIndicator(): SettingIndicator { const workspaceTrustElement = $('span.setting-indicator.setting-item-workspace-trust'); + workspaceTrustElement.tabIndex = 0; const workspaceTrustLabel = new SimpleIconLabel(workspaceTrustElement); workspaceTrustLabel.text = '$(warning) ' + localize('workspaceUntrustedLabel', "Setting value not applied"); - const contentFallback = localize('trustLabel', "The setting value can only be applied in a trusted workspace."); - const contentMarkdownString = contentFallback + ` [${localize('manageWorkspaceTrust', "Manage Workspace Trust")}](manage-workspace-trust).`; - const content: ITooltipMarkdownString = { - markdown: { - value: contentMarkdownString, - isTrusted: false, - supportHtml: false - }, - markdownNotSupportedFallback: contentFallback + const content = localize('trustLabel', "The setting value can only be applied in a trusted workspace."); + const disposables = new DisposableStore(); + const showHover = (focus: boolean) => { + return this.hoverService.showHover({ + ...this.defaultHoverOptions, + content, + target: workspaceTrustElement, + actions: [{ + label: localize('manageWorkspaceTrust', "Manage Workspace Trust"), + commandId: 'workbench.trust.manage', + run: (target: HTMLElement) => { + this.commandService.executeCommand('workbench.trust.manage'); + } + }], + }, focus); }; - - const hover = new MutableDisposable(); - const options: IUpdatableHoverOptions = { - linkHandler: (url: string) => { - this.commandService.executeCommand('workbench.trust.manage'); - hover.value?.hide(); - } - }; - hover.value = setupCustomHover(this.hoverDelegate, workspaceTrustElement, content, options); + this.addHoverDisposables(disposables, workspaceTrustElement, showHover); return { element: workspaceTrustElement, label: workspaceTrustLabel, - hover + disposables }; } private createScopeOverridesIndicator(): SettingIndicator { // Don't add .setting-indicator class here, because it gets conditionally added later. const otherOverridesElement = $('span.setting-item-overrides'); + otherOverridesElement.tabIndex = 0; const otherOverridesLabel = new SimpleIconLabel(otherOverridesElement); return { element: otherOverridesElement, label: otherOverridesLabel, - hover: new MutableDisposable() + disposables: new DisposableStore() }; } private createSyncIgnoredIndicator(): SettingIndicator { const syncIgnoredElement = $('span.setting-indicator.setting-item-ignored'); + syncIgnoredElement.tabIndex = 0; const syncIgnoredLabel = new SimpleIconLabel(syncIgnoredElement); syncIgnoredLabel.text = localize('extensionSyncIgnoredLabel', 'Not synced'); const syncIgnoredHoverContent = localize('syncIgnoredTitle', "This setting is ignored during sync"); - const hover = new MutableDisposable(); - hover.value = setupCustomHover(this.hoverDelegate, syncIgnoredElement, syncIgnoredHoverContent); + const disposables = new DisposableStore(); + const showHover = (focus: boolean) => { + return this.hoverService.showHover({ + ...this.defaultHoverOptions, + content: syncIgnoredHoverContent, + target: syncIgnoredElement + }, focus); + }; + this.addHoverDisposables(disposables, syncIgnoredElement, showHover); + return { element: syncIgnoredElement, label: syncIgnoredLabel, - hover + disposables: new DisposableStore() }; } private createDefaultOverrideIndicator(): SettingIndicator { const defaultOverrideIndicator = $('span.setting-indicator.setting-item-default-overridden'); + defaultOverrideIndicator.tabIndex = 0; const defaultOverrideLabel = new SimpleIconLabel(defaultOverrideIndicator); defaultOverrideLabel.text = localize('defaultOverriddenLabel', "Default value changed"); return { element: defaultOverrideIndicator, label: defaultOverrideLabel, - hover: new MutableDisposable() + disposables: new DisposableStore() }; } private render() { - const indicatorsToShow = [this.workspaceTrustIndicator, this.scopeOverridesIndicator, this.syncIgnoredIndicator, this.defaultOverrideIndicator].filter(indicator => { + const indicatorsToShow = this.allIndicators.filter(indicator => { return indicator.element.style.display !== 'none'; }); @@ -193,7 +232,7 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { const indicators = [this.workspaceTrustIndicator, this.scopeOverridesIndicator, this.syncIgnoredIndicator, this.defaultOverrideIndicator]; for (const indicator of indicators) { - indicator.hover.dispose(); + indicator.disposables.dispose(); } } @@ -206,24 +245,22 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { this.scopeOverridesIndicator.element.classList.add('setting-indicator'); this.scopeOverridesIndicator.label.text = '$(warning) ' + localize('policyLabelText', "Setting value not applied"); - const contentFallback = localize('policyDescription', "This setting is managed by your organization and its applied value cannot be changed."); - const contentMarkdownString = contentFallback + ` [${localize('policyFilterLink', "View policy settings")}](policy-settings).`; - const content: ITooltipMarkdownString = { - markdown: { - value: contentMarkdownString, - isTrusted: false, - supportHtml: false - }, - markdownNotSupportedFallback: contentFallback + const content = localize('policyDescription', "This setting is managed by your organization and its applied value cannot be changed."); + const showHover = (focus: boolean) => { + return this.hoverService.showHover({ + ...this.defaultHoverOptions, + content, + actions: [{ + label: localize('policyFilterLink', "View policy settings"), + commandId: '_settings.action.viewPolicySettings', + run: (_) => { + onApplyFilter.fire(`@${POLICY_SETTING_TAG}`); + } + }], + target: this.scopeOverridesIndicator.element + }, focus); }; - const options: IUpdatableHoverOptions = { - linkHandler: _ => { - onApplyFilter.fire(`@${POLICY_SETTING_TAG}`); - this.scopeOverridesIndicator.hover.value?.hide(); - } - }; - this.scopeOverridesIndicator.hover.value = setupCustomHover(this.hoverDelegate, this.scopeOverridesIndicator.element, content, options); - + this.addHoverDisposables(this.scopeOverridesIndicator.disposables, this.scopeOverridesIndicator.element, showHover); } else if (this.profilesEnabled && element.matchesScope(ConfigurationTarget.APPLICATION, false)) { // If the setting is an application-scoped setting, there are no overrides so we can use this // indicator to display that information instead. @@ -234,45 +271,42 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { this.scopeOverridesIndicator.label.text = applicationSettingText; const content = localize('applicationSettingDescription', "The setting is not specific to the current profile, and will retain its value when switching profiles."); - this.scopeOverridesIndicator.hover.value = setupCustomHover(this.hoverDelegate, this.scopeOverridesIndicator.element, content); + const showHover = (focus: boolean) => { + return this.hoverService.showHover({ + ...this.defaultHoverOptions, + content, + target: this.scopeOverridesIndicator.element + }, focus); + }; + this.addHoverDisposables(this.scopeOverridesIndicator.disposables, this.scopeOverridesIndicator.element, showHover); } else if (element.overriddenScopeList.length || element.overriddenDefaultsLanguageList.length) { - if ((MODIFIED_INDICATOR_USE_INLINE_ONLY && element.overriddenScopeList.length) || - (element.overriddenScopeList.length === 1 && !element.overriddenDefaultsLanguageList.length)) { - // This branch renders some info inline! - // Render inline if we have the flag and there are scope overrides to render, - // or if there is only one scope override to render and no language overrides. + if (element.overriddenScopeList.length === 1 && !element.overriddenDefaultsLanguageList.length) { + // We can inline the override and show all the text in the label + // so that users don't have to wait for the hover to load + // just to click into the one override there is. this.scopeOverridesIndicator.element.style.display = 'inline'; this.scopeOverridesIndicator.element.classList.remove('setting-indicator'); - this.scopeOverridesIndicator.hover.value = undefined; + this.scopeOverridesIndicator.disposables.clear(); - // Just show all the text in the label. const prefaceText = element.isConfigured ? localize('alsoConfiguredIn', "Also modified in") : localize('configuredIn', "Modified in"); this.scopeOverridesIndicator.label.text = `${prefaceText} `; - for (let i = 0; i < element.overriddenScopeList.length; i++) { - const overriddenScope = element.overriddenScopeList[i]; - const view = DOM.append(this.scopeOverridesIndicator.element, $('a.modified-scope', undefined, this.getInlineScopeDisplayText(overriddenScope))); - if (i !== element.overriddenScopeList.length - 1) { - DOM.append(this.scopeOverridesIndicator.element, $('span.comma', undefined, ', ')); - } - elementDisposables.add( - DOM.addStandardDisposableListener(view, DOM.EventType.CLICK, (e: IMouseEvent) => { - const [scope, language] = overriddenScope.split(':'); - onDidClickOverrideElement.fire({ - settingKey: element.setting.key, - scope: scope as ScopeString, - language - }); - e.preventDefault(); - e.stopPropagation(); - })); - } - } else if (!MODIFIED_INDICATOR_USE_INLINE_ONLY) { - // Even if the check above fails, we want to - // show the text in a custom hover only if - // the feature flag isn't on. + const overriddenScope = element.overriddenScopeList[0]; + const view = DOM.append(this.scopeOverridesIndicator.element, $('a.modified-scope', undefined, this.getInlineScopeDisplayText(overriddenScope))); + elementDisposables.add( + DOM.addStandardDisposableListener(view, DOM.EventType.CLICK, (e: IMouseEvent) => { + const [scope, language] = overriddenScope.split(':'); + onDidClickOverrideElement.fire({ + settingKey: element.setting.key, + scope: scope as ScopeString, + language + }); + e.preventDefault(); + e.stopPropagation(); + })); + } else { this.scopeOverridesIndicator.element.style.display = 'inline'; this.scopeOverridesIndicator.element.classList.add('setting-indicator'); const scopeOverridesLabelText = element.isConfigured ? @@ -281,53 +315,48 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { this.scopeOverridesIndicator.label.text = scopeOverridesLabelText; let contentMarkdownString = ''; - let contentFallback = ''; if (element.overriddenScopeList.length) { const prefaceText = element.isConfigured ? localize('alsoModifiedInScopes', "The setting has also been modified in the following scopes:") : localize('modifiedInScopes', "The setting has been modified in the following scopes:"); contentMarkdownString = prefaceText; - contentFallback = prefaceText; for (const scope of element.overriddenScopeList) { const scopeDisplayText = this.getInlineScopeDisplayText(scope); contentMarkdownString += `\n- [${scopeDisplayText}](${encodeURIComponent(scope)} "${getAccessibleScopeDisplayText(scope, this.languageService)}")`; - contentFallback += `\n• ${scopeDisplayText}`; } } if (element.overriddenDefaultsLanguageList.length) { if (contentMarkdownString) { contentMarkdownString += `\n\n`; - contentFallback += `\n\n`; } const prefaceText = localize('hasDefaultOverridesForLanguages', "The following languages have default overrides:"); contentMarkdownString += prefaceText; - contentFallback += prefaceText; for (const language of element.overriddenDefaultsLanguageList) { const scopeDisplayText = this.languageService.getLanguageName(language); contentMarkdownString += `\n- [${scopeDisplayText}](${encodeURIComponent(`default:${language}`)} "${scopeDisplayText}")`; - contentFallback += `\n• ${scopeDisplayText}`; } } - const content: ITooltipMarkdownString = { - markdown: { - value: contentMarkdownString, - isTrusted: false, - supportHtml: false - }, - markdownNotSupportedFallback: contentFallback + const content: IMarkdownString = { + value: contentMarkdownString, + isTrusted: false, + supportHtml: false }; - const options: IUpdatableHoverOptions = { - linkHandler: (url: string) => { - const [scope, language] = decodeURIComponent(url).split(':'); - onDidClickOverrideElement.fire({ - settingKey: element.setting.key, - scope: scope as ScopeString, - language - }); - this.scopeOverridesIndicator.hover.value?.hide(); - } + const showHover = (focus: boolean) => { + return this.hoverService.showHover({ + ...this.defaultHoverOptions, + content, + linkHandler: (url: string) => { + const [scope, language] = decodeURIComponent(url).split(':'); + onDidClickOverrideElement.fire({ + settingKey: element.setting.key, + scope: scope as ScopeString, + language + }); + }, + target: this.scopeOverridesIndicator.element + }, focus); }; - this.scopeOverridesIndicator.hover.value = setupCustomHover(this.hoverDelegate, this.scopeOverridesIndicator.element, content, options); + this.addHoverDisposables(this.scopeOverridesIndicator.disposables, this.scopeOverridesIndicator.element, showHover); } } this.render(); @@ -338,9 +367,19 @@ export class SettingsTreeIndicatorsLabel implements IDisposable { const sourceToDisplay = getDefaultValueSourceToDisplay(element); if (sourceToDisplay !== undefined) { this.defaultOverrideIndicator.element.style.display = 'inline'; + this.defaultOverrideIndicator.disposables.clear(); const defaultOverrideHoverContent = localize('defaultOverriddenDetails', "Default setting value overridden by {0}", sourceToDisplay); - this.defaultOverrideIndicator.hover.value = setupCustomHover(this.hoverDelegate, this.defaultOverrideIndicator.element, defaultOverrideHoverContent); + const showHover = (focus: boolean) => { + return this.hoverService.showHover({ + content: defaultOverrideHoverContent, + target: this.defaultOverrideIndicator.element, + hoverPosition: HoverPosition.BELOW, + showPointer: true, + compact: false + }, focus); + }; + this.addHoverDisposables(this.defaultOverrideIndicator.disposables, this.defaultOverrideIndicator.element, showHover); } this.render(); } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index aca340635d4..6f37adb815b 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -8,7 +8,7 @@ import * as DOM from 'vs/base/browser/dom'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { alert as ariaAlert } from 'vs/base/browser/ui/aria/aria'; import { Button } from 'vs/base/browser/ui/button/button'; -import { Toggle } from 'vs/base/browser/ui/toggle/toggle'; +import { Toggle, unthemedToggleStyles } from 'vs/base/browser/ui/toggle/toggle'; import { IInputOptions, InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { CachedListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { DefaultStyleController, IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; @@ -18,8 +18,6 @@ import { IObjectTreeOptions } from 'vs/base/browser/ui/tree/objectTree'; import { ObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel'; import { ITreeFilter, ITreeModel, ITreeNode, ITreeRenderer, TreeFilterResult, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; import { Action, IAction, Separator } from 'vs/base/common/actions'; -import * as arrays from 'vs/base/common/arrays'; -import { Color } from 'vs/base/common/color'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; @@ -36,7 +34,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { editorBackground, foreground } from 'vs/platform/theme/common/colorRegistry'; -import { attachInputBoxStyler, attachSelectBoxStyler, attachStyler } from 'vs/platform/theme/common/styler'; +import { attachSelectBoxStyler, attachStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { getIgnoredSettings } from 'vs/platform/userDataSync/common/settingsMerge'; import { ITOCEntry } from 'vs/workbench/contrib/preferences/browser/settingsLayout'; @@ -64,7 +62,7 @@ import { getIndicatorsLabelAriaLabel, ISettingOverrideClickEvent, SettingsTreeIn import { ILanguageService } from 'vs/editor/common/languages/language'; import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; -import { defaultButtonStyles, getButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { defaultButtonStyles, getInputBoxStyle } from 'vs/platform/theme/browser/defaultStyles'; const $ = DOM.$; @@ -97,7 +95,7 @@ function areAllPropertiesDefined(properties: string[], itemsToDisplay: IObjectDa function getEnumOptionsFromSchema(schema: IJSONSchema): IObjectEnumOption[] { if (schema.anyOf) { - return arrays.flatten(schema.anyOf.map(getEnumOptionsFromSchema)); + return schema.anyOf.map(getEnumOptionsFromSchema).flat(); } const enumDescriptions = schema.enumDescriptions ?? []; @@ -425,8 +423,7 @@ export async function createTocTreeForExtensionSettings(extensionService: IExten extGroupTree.get(extensionId)!.children!.push(childEntry); }; const processGroupEntry = async (group: ISettingsGroup) => { - const flatSettings = arrays.flatten( - group.sections.map(section => section.settings)); + const flatSettings = group.sections.map(section => section.settings).flat(); const extensionId = group.extensionInfo!.id; const extension = await extensionService.getExtension(extensionId); @@ -509,7 +506,7 @@ function _resolveSettingsTree(tocData: ITOCEntry, allSettings: Set getMatchingSettings(allSettings, pattern, logService))); + settings = tocData.settings.map(pattern => getMatchingSettings(allSettings, pattern, logService)).flat(); } if (!children && !settings) { @@ -619,7 +616,7 @@ interface ISettingEnumItemTemplate extends ISettingItemTemplate { } interface ISettingComplexItemTemplate extends ISettingItemTemplate { - button: Button; + button: HTMLElement; validationErrorMessageElement: HTMLElement; } @@ -1051,17 +1048,9 @@ export class SettingComplexRenderer extends AbstractSettingRenderer implements I renderTemplate(container: HTMLElement): ISettingComplexItemTemplate { const common = this.renderCommonTemplate(null, container, 'complex'); - const openSettingsButton = new Button(common.controlElement, { - title: true, ...getButtonStyles({ - buttonBackground: Color.transparent.toString(), - buttonHoverBackground: Color.transparent.toString(), - buttonForeground: 'foreground' - }) - }); - common.toDispose.add(openSettingsButton); - - openSettingsButton.element.classList.add('edit-in-settings-button'); - openSettingsButton.element.classList.add(AbstractSettingRenderer.CONTROL_CLASS); + const openSettingsButton = DOM.append(common.controlElement, $('a.edit-in-settings-button')); + openSettingsButton.classList.add(AbstractSettingRenderer.CONTROL_CLASS); + openSettingsButton.role = 'button'; const validationErrorMessageElement = $('.setting-item-validation-message'); common.containerElement.appendChild(validationErrorMessageElement); @@ -1085,11 +1074,11 @@ export class SettingComplexRenderer extends AbstractSettingRenderer implements I const plainKey = getLanguageTagSettingPlainKey(dataElement.setting.key); const editLanguageSettingLabel = localize('editLanguageSettingLabel', "Edit settings for {0}", plainKey); const isLanguageTagSetting = dataElement.setting.isLanguageTagSetting; - template.button.label = isLanguageTagSetting + template.button.textContent = isLanguageTagSetting ? editLanguageSettingLabel : SettingComplexRenderer.EDIT_IN_JSON_LABEL; - template.elementDisposables.add(template.button.onDidClick(() => { + template.elementDisposables.add(DOM.addDisposableListener(template.button, DOM.EventType.CLICK, () => { if (isLanguageTagSetting) { this._onApplyFilter.fire(`@${LANGUAGE_SETTING_TAG}${plainKey}`); } else { @@ -1100,9 +1089,9 @@ export class SettingComplexRenderer extends AbstractSettingRenderer implements I this.renderValidations(dataElement, template); if (isLanguageTagSetting) { - template.button.element.setAttribute('aria-label', editLanguageSettingLabel); + template.button.setAttribute('aria-label', editLanguageSettingLabel); } else { - template.button.element.setAttribute('aria-label', `${SettingComplexRenderer.EDIT_IN_JSON_LABEL}: ${dataElement.setting.key}`); + template.button.setAttribute('aria-label', `${SettingComplexRenderer.EDIT_IN_JSON_LABEL}: ${dataElement.setting.key}`); } } @@ -1493,6 +1482,12 @@ export class SettingExcludeRenderer extends AbstractSettingRenderer implements I } } +const settingsInputBoxStyles = getInputBoxStyle({ + inputBackground: settingsTextInputBackground, + inputForeground: settingsTextInputForeground, + inputBorder: settingsTextInputBorder +}); + abstract class AbstractSettingTextRenderer extends AbstractSettingRenderer implements ITreeRenderer { private readonly MULTILINE_MAX_HEIGHT = 150; @@ -1503,15 +1498,11 @@ abstract class AbstractSettingTextRenderer extends AbstractSettingRenderer imple const inputBoxOptions: IInputOptions = { flexibleHeight: useMultiline, flexibleWidth: false, - flexibleMaxHeight: this.MULTILINE_MAX_HEIGHT + flexibleMaxHeight: this.MULTILINE_MAX_HEIGHT, + inputBoxStyles: settingsInputBoxStyles }; const inputBox = new InputBox(common.controlElement, this._contextViewService, inputBoxOptions); common.toDispose.add(inputBox); - common.toDispose.add(attachInputBoxStyler(inputBox, this._themeService, { - inputBackground: settingsTextInputBackground, - inputForeground: settingsTextInputForeground, - inputBorder: settingsTextInputBorder - })); common.toDispose.add( inputBox.onDidChange(e => { template.onChange?.(e); @@ -1708,6 +1699,12 @@ export class SettingEnumRenderer extends AbstractSettingRenderer implements ITre } } +const settingsNumberInputBoxStyles = getInputBoxStyle({ + inputBackground: settingsNumberInputBackground, + inputForeground: settingsNumberInputForeground, + inputBorder: settingsNumberInputBorder +}); + export class SettingNumberRenderer extends AbstractSettingRenderer implements ITreeRenderer { templateId = SETTINGS_NUMBER_TEMPLATE_ID; @@ -1715,13 +1712,8 @@ export class SettingNumberRenderer extends AbstractSettingRenderer implements IT const common = super.renderCommonTemplate(null, _container, 'number'); const validationErrorMessageElement = DOM.append(common.containerElement, $('.setting-item-validation-message')); - const inputBox = new InputBox(common.controlElement, this._contextViewService, { type: 'number' }); + const inputBox = new InputBox(common.controlElement, this._contextViewService, { type: 'number', inputBoxStyles: settingsNumberInputBoxStyles }); common.toDispose.add(inputBox); - common.toDispose.add(attachInputBoxStyler(inputBox, this._themeService, { - inputBackground: settingsNumberInputBackground, - inputForeground: settingsNumberInputForeground, - inputBorder: settingsNumberInputBorder - })); common.toDispose.add( inputBox.onDidChange(e => { template.onChange?.(e); @@ -1790,7 +1782,7 @@ export class SettingBoolRenderer extends AbstractSettingRenderer implements ITre const deprecationWarningElement = DOM.append(container, $('.setting-item-deprecation-message')); const toDispose = new DisposableStore(); - const checkbox = new Toggle({ icon: Codicon.check, actionClassName: 'setting-value-checkbox', isChecked: true, title: '', inputActiveOptionBorder: undefined }); + const checkbox = new Toggle({ icon: Codicon.check, actionClassName: 'setting-value-checkbox', isChecked: true, title: '', ...unthemedToggleStyles }); controlElement.appendChild(checkbox.domNode); toDispose.add(checkbox); toDispose.add(checkbox.onChange(() => { diff --git a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts index 08371f9de22..549ccf18d60 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts @@ -8,7 +8,7 @@ import * as DOM from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { Button } from 'vs/base/browser/ui/button/button'; -import { Toggle } from 'vs/base/browser/ui/toggle/toggle'; +import { Toggle, unthemedToggleStyles } from 'vs/base/browser/ui/toggle/toggle'; import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { SelectBox } from 'vs/base/browser/ui/selectBox/selectBox'; import { IAction } from 'vs/base/common/actions'; @@ -22,11 +22,11 @@ import { isDefined, isUndefinedOrNull } from 'vs/base/common/types'; import 'vs/css!./media/settingsWidgets'; import { localize } from 'vs/nls'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { attachInputBoxStyler, attachSelectBoxStyler } from 'vs/platform/theme/common/styler'; +import { attachSelectBoxStyler } from 'vs/platform/theme/common/styler'; import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { settingsDiscardIcon, settingsEditIcon, settingsRemoveIcon } from 'vs/workbench/contrib/preferences/browser/preferencesIcons'; import { settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground } from 'vs/workbench/contrib/preferences/common/settingsEditorColorRegistry'; -import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { defaultButtonStyles, getInputBoxStyle } from 'vs/platform/theme/browser/defaultStyles'; const $ = DOM.$; @@ -614,15 +614,15 @@ export class ListSettingWidget extends AbstractListSettingWidget let siblingInput: InputBox | undefined; if (!isUndefinedOrNull(item.sibling)) { siblingInput = new InputBox(rowElement, this.contextViewService, { - placeholder: this.getLocalizedStrings().siblingInputPlaceholder + placeholder: this.getLocalizedStrings().siblingInputPlaceholder, + inputBoxStyles: getInputBoxStyle({ + inputBackground: settingsTextInputBackground, + inputForeground: settingsTextInputForeground, + inputBorder: settingsTextInputBorder + }) }); siblingInput.element.classList.add('setting-list-siblingInput'); this.listDisposables.add(siblingInput); - this.listDisposables.add(attachInputBoxStyler(siblingInput, this.themeService, { - inputBackground: settingsTextInputBackground, - inputForeground: settingsTextInputForeground, - inputBorder: settingsTextInputBorder - })); siblingInput.value = item.sibling; this.listDisposables.add( @@ -688,15 +688,15 @@ export class ListSettingWidget extends AbstractListSettingWidget private renderInputBox(value: ObjectValue, rowElement: HTMLElement): InputBox { const valueInput = new InputBox(rowElement, this.contextViewService, { - placeholder: this.getLocalizedStrings().inputPlaceholder + placeholder: this.getLocalizedStrings().inputPlaceholder, + inputBoxStyles: getInputBoxStyle({ + inputBackground: settingsTextInputBackground, + inputForeground: settingsTextInputForeground, + inputBorder: settingsTextInputBorder + }) }); valueInput.element.classList.add('setting-list-valueInput'); - this.listDisposables.add(attachInputBoxStyler(valueInput, this.themeService, { - inputBackground: settingsTextInputBackground, - inputForeground: settingsTextInputForeground, - inputBorder: settingsTextInputBorder - })); this.listDisposables.add(valueInput); valueInput.value = value.data.toString(); @@ -1026,15 +1026,15 @@ export class ObjectSettingDropdownWidget extends AbstractListSettingWidget('forwardedPortsViewEnabled', false, nls.localize('tunnel.forwardedPortsViewEnabled', "Whether the Ports view is enabled.")); export const openPreviewEnabledContext = new RawContextKey('openPreviewEnabled', false); @@ -347,7 +346,6 @@ class ActionBarRenderer extends Disposable implements ITableRenderer { return done(inputBox.validate() !== MessageType.ERROR, true); - }), - styler + }) ]; return toDisposable(() => { @@ -840,7 +836,7 @@ export class TunnelPanel extends ViewPane { widgetContainer.classList.add('file-icon-themable-tree', 'show-file-icons'); const actionBarRenderer = new ActionBarRenderer(this.instantiationService, this.contextKeyService, - this.menuService, this.contextViewService, this.themeService, this.remoteExplorerService, this.commandService, + this.menuService, this.contextViewService, this.remoteExplorerService, this.commandService, this.configurationService, this.hoverService); const columns = [new IconColumn(), new PortColumn(), new LocalAddressColumn(), new RunningProcessColumn()]; if (this.tunnelService.canChangePrivacy) { diff --git a/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts b/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts index 1d0ffffb7e4..105dcf61c0e 100644 --- a/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts +++ b/src/vs/workbench/contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.ts @@ -6,7 +6,7 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { IProductService } from 'vs/platform/product/common/productService'; -import { CONFIGURATION_KEY_HOST_NAME, CONFIGURATION_KEY_PREFIX, ConnectionInfo, IRemoteTunnelService } from 'vs/platform/remoteTunnel/common/remoteTunnel'; +import { CONFIGURATION_KEY_HOST_NAME, CONFIGURATION_KEY_PREFIX, ConnectionInfo, IRemoteTunnelService, LOGGER_NAME, LOG_CHANNEL_ID, LOG_FILE_NAME } from 'vs/platform/remoteTunnel/common/remoteTunnel'; import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationService } from 'vs/workbench/services/authentication/common/authentication'; import { localize } from 'vs/nls'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -21,7 +21,7 @@ import { INativeEnvironmentService } from 'vs/platform/environment/common/enviro import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IStringDictionary } from 'vs/base/common/collections'; import { IQuickInputService, IQuickPickItem, IQuickPickSeparator, QuickPickItem } from 'vs/platform/quickinput/common/quickInput'; -import { IOutputService } from 'vs/workbench/services/output/common/output'; +import { IOutputService, registerLogChannel } from 'vs/workbench/services/output/common/output'; import { IFileService } from 'vs/platform/files/common/files'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { IProgress, IProgressService, IProgressStep, ProgressLocation } from 'vs/platform/progress/common/progress'; @@ -32,11 +32,11 @@ import { IPreferencesService } from 'vs/workbench/services/preferences/common/pr import { IOpenerService } from 'vs/platform/opener/common/opener'; import { Action } from 'vs/base/common/actions'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import * as Constants from 'vs/workbench/contrib/logs/common/logConstants'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { joinPath } from 'vs/base/common/resources'; +import { join } from 'vs/base/common/path'; export const REMOTE_TUNNEL_CATEGORY: ILocalizedString = { original: 'Remote Tunnels', @@ -106,7 +106,9 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo ) { super(); - this.logger = this._register(loggerService.createLogger(environmentService.remoteTunnelLogResource, { name: 'remoteTunnel' })); + const remoteTunnelServiceLogResource = URI.file(join(environmentService.logsPath, LOG_FILE_NAME)); + + this.logger = this._register(loggerService.createLogger(remoteTunnelServiceLogResource, { name: LOGGER_NAME })); this.connectionStateContext = REMOTE_TUNNEL_CONNECTION_STATE.bindTo(this.contextKeyService); @@ -153,6 +155,7 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo this.initialize(true); } + registerLogChannel(LOG_CHANNEL_ID, localize('remoteTunnelLog', "Remote Tunnel Service"), remoteTunnelServiceLogResource, fileService, logService); } private get existingSessionId() { @@ -542,7 +545,7 @@ export class RemoteTunnelWorkbenchContribution extends Disposable implements IWo async run(accessor: ServicesAccessor) { const outputService = accessor.get(IOutputService); - outputService.showChannel(Constants.remoteTunnelLogChannelId); + outputService.showChannel(LOG_CHANNEL_ID); } })); diff --git a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts index b0bc4bbf0d8..5ba9b1a64ae 100644 --- a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts @@ -253,8 +253,8 @@ class DirtyDiffWidget extends PeekViewWidget { protected override _fillHead(container: HTMLElement): void { super._fillHead(container, true); - const previous = this.instantiationService.createInstance(UIEditorAction, this.editor, new ShowPreviousChangeAction(), ThemeIcon.asClassName(gotoPreviousLocation)); - const next = this.instantiationService.createInstance(UIEditorAction, this.editor, new ShowNextChangeAction(), ThemeIcon.asClassName(gotoNextLocation)); + const previous = this.instantiationService.createInstance(UIEditorAction, this.editor, new ShowPreviousChangeAction(this.editor), ThemeIcon.asClassName(gotoPreviousLocation)); + const next = this.instantiationService.createInstance(UIEditorAction, this.editor, new ShowNextChangeAction(this.editor), ThemeIcon.asClassName(gotoNextLocation)); this._disposables.add(previous); this._disposables.add(next); @@ -363,7 +363,7 @@ class DirtyDiffWidget extends PeekViewWidget { export class ShowPreviousChangeAction extends EditorAction { - constructor() { + constructor(private readonly outerEditor?: ICodeEditor) { super({ id: 'editor.action.dirtydiff.previous', label: nls.localize('show previous change', "Show Previous Change"), @@ -373,8 +373,8 @@ export class ShowPreviousChangeAction extends EditorAction { }); } - run(accessor: ServicesAccessor, editor: ICodeEditor): void { - const outerEditor = getOuterEditorFromDiffEditor(accessor); + run(accessor: ServicesAccessor): void { + const outerEditor = this.outerEditor ?? getOuterEditorFromDiffEditor(accessor); if (!outerEditor) { return; @@ -397,7 +397,7 @@ registerEditorAction(ShowPreviousChangeAction); export class ShowNextChangeAction extends EditorAction { - constructor() { + constructor(private readonly outerEditor?: ICodeEditor) { super({ id: 'editor.action.dirtydiff.next', label: nls.localize('show next change', "Show Next Change"), @@ -407,8 +407,8 @@ export class ShowNextChangeAction extends EditorAction { }); } - run(accessor: ServicesAccessor, editor: ICodeEditor): void { - const outerEditor = getOuterEditorFromDiffEditor(accessor); + run(accessor: ServicesAccessor): void { + const outerEditor = this.outerEditor ?? getOuterEditorFromDiffEditor(accessor); if (!outerEditor) { return; @@ -460,7 +460,7 @@ export class GotoPreviousChangeAction extends EditorAction { }); } - run(accessor: ServicesAccessor, editor: ICodeEditor): void { + run(accessor: ServicesAccessor): void { const outerEditor = getOuterEditorFromDiffEditor(accessor); if (!outerEditor || !outerEditor.hasModel()) { @@ -502,7 +502,7 @@ export class GotoNextChangeAction extends EditorAction { }); } - run(accessor: ServicesAccessor, editor: ICodeEditor): void { + run(accessor: ServicesAccessor): void { const outerEditor = getOuterEditorFromDiffEditor(accessor); if (!outerEditor || !outerEditor.hasModel()) { diff --git a/src/vs/workbench/contrib/scm/browser/media/scm.css b/src/vs/workbench/contrib/scm/browser/media/scm.css index 6a08158a58a..37a9d5a75e2 100644 --- a/src/vs/workbench/contrib/scm/browser/media/scm.css +++ b/src/vs/workbench/contrib/scm/browser/media/scm.css @@ -287,13 +287,8 @@ padding: 0 4px; } -/* split commit button */ -.scm-view .button-container > .monaco-button-dropdown > .monaco-dropdown-button.codicon-drop-down-button { - border-radius: 0 2px 2px 0; -} .scm-view .button-container > .monaco-button-dropdown > .monaco-button.monaco-text-button { - border-radius: 2px 0 0 2px; min-width: 0; } diff --git a/src/vs/workbench/contrib/search/browser/patternInputWidget.ts b/src/vs/workbench/contrib/search/browser/patternInputWidget.ts index 30e8ca1e9ea..5d9e6fb31e2 100644 --- a/src/vs/workbench/contrib/search/browser/patternInputWidget.ts +++ b/src/vs/workbench/contrib/search/browser/patternInputWidget.ts @@ -12,15 +12,13 @@ import { Widget } from 'vs/base/browser/ui/widget'; import { Codicon } from 'vs/base/common/codicons'; import { Emitter, Event as CommonEvent } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; -import type { IThemable } from 'vs/base/common/styler'; import * as nls from 'vs/nls'; import { ContextScopedHistoryInputBox } from 'vs/platform/history/browser/contextScopedHistoryWidget'; import { showHistoryKeybindingHint } from 'vs/platform/history/browser/historyWidgetKeybindingHint'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { attachToggleStyler, attachInputBoxStyler } from 'vs/platform/theme/common/styler'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { defaultToggleStyles } from 'vs/platform/theme/browser/defaultStyles'; export interface IOptions { placeholder?: string; @@ -29,9 +27,10 @@ export interface IOptions { width?: number; ariaLabel?: string; history?: string[]; + inputBoxStyles: IInputBoxStyles; } -export class PatternInputWidget extends Widget implements IThemable { +export class PatternInputWidget extends Widget { static OPTION_CHANGE: string = 'optionChange'; @@ -48,8 +47,7 @@ export class PatternInputWidget extends Widget implements IThemable { private _onCancel = this._register(new Emitter()); onCancel: CommonEvent = this._onCancel.event; - constructor(parent: HTMLElement, private contextViewProvider: IContextViewProvider, options: IOptions = Object.create(null), - @IThemeService protected themeService: IThemeService, + constructor(parent: HTMLElement, private contextViewProvider: IContextViewProvider, options: IOptions, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IConfigurationService protected readonly configurationService: IConfigurationService, @IKeybindingService private readonly keybindingService: IKeybindingService, @@ -135,10 +133,6 @@ export class PatternInputWidget extends Widget implements IThemable { this.inputBox.showPreviousValue(); } - style(styles: IInputBoxStyles): void { - this.inputBox.style(styles); - } - private render(options: IOptions): void { this.domNode = document.createElement('div'); this.domNode.style.width = this.width + 'px'; @@ -153,9 +147,9 @@ export class PatternInputWidget extends Widget implements IThemable { validation: undefined }, history: options.history || [], - showHistoryHint: () => showHistoryKeybindingHint(this.keybindingService) + showHistoryHint: () => showHistoryKeybindingHint(this.keybindingService), + inputBoxStyles: options.inputBoxStyles }, this.contextKeyService); - this._register(attachInputBoxStyler(this.inputBox, this.themeService)); this._register(this.inputBox.onDidChange(() => this._onSubmit.fire(true))); this.inputFocusTracker = dom.trackFocus(this.inputBox.inputElement); @@ -190,13 +184,12 @@ export class IncludePatternInputWidget extends PatternInputWidget { private _onChangeSearchInEditorsBoxEmitter = this._register(new Emitter()); onChangeSearchInEditorsBox = this._onChangeSearchInEditorsBoxEmitter.event; - constructor(parent: HTMLElement, contextViewProvider: IContextViewProvider, options: IOptions = Object.create(null), - @IThemeService themeService: IThemeService, + constructor(parent: HTMLElement, contextViewProvider: IContextViewProvider, options: IOptions, @IContextKeyService contextKeyService: IContextKeyService, @IConfigurationService configurationService: IConfigurationService, @IKeybindingService keybindingService: IKeybindingService, ) { - super(parent, contextViewProvider, options, themeService, contextKeyService, configurationService, keybindingService); + super(parent, contextViewProvider, options, contextKeyService, configurationService, keybindingService); } private useSearchInEditorsBox!: Toggle; @@ -224,6 +217,7 @@ export class IncludePatternInputWidget extends PatternInputWidget { icon: Codicon.book, title: nls.localize('onlySearchInOpenEditors', "Search only in Open Editors"), isChecked: false, + ...defaultToggleStyles })); this._register(this.useSearchInEditorsBox.onChange(viaKeyboard => { this._onChangeSearchInEditorsBoxEmitter.fire(); @@ -231,7 +225,6 @@ export class IncludePatternInputWidget extends PatternInputWidget { this.inputBox.focus(); } })); - this._register(attachToggleStyler(this.useSearchInEditorsBox, this.themeService)); controlsDiv.appendChild(this.useSearchInEditorsBox.domNode); super.renderSubcontrols(controlsDiv); } @@ -242,13 +235,12 @@ export class ExcludePatternInputWidget extends PatternInputWidget { private _onChangeIgnoreBoxEmitter = this._register(new Emitter()); onChangeIgnoreBox = this._onChangeIgnoreBoxEmitter.event; - constructor(parent: HTMLElement, contextViewProvider: IContextViewProvider, options: IOptions = Object.create(null), - @IThemeService themeService: IThemeService, + constructor(parent: HTMLElement, contextViewProvider: IContextViewProvider, options: IOptions, @IContextKeyService contextKeyService: IContextKeyService, @IConfigurationService configurationService: IConfigurationService, @IKeybindingService keybindingService: IKeybindingService, ) { - super(parent, contextViewProvider, options, themeService, contextKeyService, configurationService, keybindingService); + super(parent, contextViewProvider, options, contextKeyService, configurationService, keybindingService); } private useExcludesAndIgnoreFilesBox!: Toggle; @@ -277,6 +269,7 @@ export class ExcludePatternInputWidget extends PatternInputWidget { actionClassName: 'useExcludesAndIgnoreFiles', title: nls.localize('useExcludesAndIgnoreFilesDescription', "Use Exclude Settings and Ignore Files"), isChecked: true, + ...defaultToggleStyles })); this._register(this.useExcludesAndIgnoreFilesBox.onChange(viaKeyboard => { this._onChangeIgnoreBoxEmitter.fire(); @@ -284,7 +277,6 @@ export class ExcludePatternInputWidget extends PatternInputWidget { this.inputBox.focus(); } })); - this._register(attachToggleStyler(this.useExcludesAndIgnoreFilesBox, this.themeService)); controlsDiv.appendChild(this.useExcludesAndIgnoreFilesBox.domNode); super.renderSubcontrols(controlsDiv); diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index f0f6746a40f..b6f74172133 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -3,58 +3,44 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { onUnexpectedError } from 'vs/base/common/errors'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import * as platform from 'vs/base/common/platform'; -import { dirname } from 'vs/base/common/resources'; -import { assertIsDefined, assertType } from 'vs/base/common/types'; -import { URI } from 'vs/base/common/uri'; -import { ToggleCaseSensitiveKeybinding, TogglePreserveCaseKeybinding, ToggleRegexKeybinding, ToggleWholeWordKeybinding } from 'vs/editor/contrib/find/browser/findModel'; import { AbstractGotoLineQuickAccessProvider } from 'vs/editor/contrib/quickAccess/browser/gotoLineQuickAccess'; import * as nls from 'vs/nls'; -import { Action2, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; -import { ICommandAction } from 'vs/platform/action/common/action'; -import { CommandsRegistry, ICommandHandler, ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ConfigurationScope, Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; -import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IFileService } from 'vs/platform/files/common/files'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { IListService, WorkbenchListFocusContextKey, WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listService'; import { Extensions as QuickAccessExtensions, IQuickAccessRegistry } from 'vs/platform/quickinput/common/quickAccess'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { defaultQuickAccessContextKeyValue } from 'vs/workbench/browser/quickaccess'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; -import { Extensions as ViewExtensions, IViewContainersRegistry, IViewDescriptor, IViewDescriptorService, IViewsRegistry, IViewsService, ViewContainerLocation } from 'vs/workbench/common/views'; +import { Extensions as ViewExtensions, IViewContainersRegistry, IViewDescriptor, IViewDescriptorService, IViewsRegistry, ViewContainerLocation } from 'vs/workbench/common/views'; import { GotoSymbolQuickAccessProvider } from 'vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess'; -import { ExplorerViewPaneContainer } from 'vs/workbench/contrib/files/browser/explorerViewlet'; -import { getMultiSelectedResources, IExplorerService } from 'vs/workbench/contrib/files/browser/files'; -import { ExplorerFolderContext, ExplorerRootContext, FilesExplorerFocusCondition, VIEWLET_ID as VIEWLET_ID_FILES } from 'vs/workbench/contrib/files/common/files'; import { AnythingQuickAccessProvider } from 'vs/workbench/contrib/search/browser/anythingQuickAccess'; import { registerContributions as replaceContributions } from 'vs/workbench/contrib/search/browser/replaceContributions'; -import { cancelSearch, clearHistoryCommand, clearSearchResults, CloseReplaceAction, collapseDeepestExpandedLevel, copyAllCommand, copyMatchCommand, copyPathCommand, expandAll, FindInFilesCommand, findOrReplaceInFiles, FocusNextInputAction, focusNextSearchResult, FocusPreviousInputAction, focusPreviousSearchResult, focusSearchListCommand, getMultiSelectedSearchResources, getSearchView, openSearchView, refreshSearch, RemoveAction, ReplaceAction, ReplaceAllAction, ReplaceAllInFolderAction, toggleCaseSensitiveCommand, togglePreserveCaseCommand, toggleRegexCommand, toggleWholeWordCommand } from 'vs/workbench/contrib/search/browser/searchActions'; -import { searchClearIcon, searchCollapseAllIcon, searchExpandAllIcon, searchRefreshIcon, searchStopIcon, searchShowAsTree, searchViewIcon, searchShowAsList } from 'vs/workbench/contrib/search/browser/searchIcons'; +import { searchViewIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; import { SearchView } from 'vs/workbench/contrib/search/browser/searchView'; import { registerContributions as searchWidgetContributions } from 'vs/workbench/contrib/search/browser/searchWidget'; import { SymbolsQuickAccessProvider } from 'vs/workbench/contrib/search/browser/symbolsQuickAccess'; -import * as Constants from 'vs/workbench/contrib/search/common/constants'; -import { resolveResourcesForSearchIncludes } from 'vs/workbench/services/search/common/queryBuilder'; -import { getWorkspaceSymbols, IWorkspaceSymbol, SearchStateKey, SearchUIState } from 'vs/workbench/contrib/search/common/search'; import { ISearchHistoryService, SearchHistoryService } from 'vs/workbench/contrib/search/common/searchHistoryService'; -import { FileMatch, FileMatchOrMatch, FolderMatch, FolderMatchWithResource, ISearchWorkbenchService, Match, RenderableMatch, SearchWorkbenchService } from 'vs/workbench/contrib/search/common/searchModel'; -import * as SearchEditorConstants from 'vs/workbench/contrib/searchEditor/browser/constants'; -import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditor'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ISearchWorkbenchService, SearchWorkbenchService } from 'vs/workbench/contrib/search/common/searchModel'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; -import { ISearchConfiguration, SearchSortOrder, SEARCH_EXCLUDE_CONFIG, VIEWLET_ID, ViewMode, VIEW_ID } from 'vs/workbench/services/search/common/search'; +import { SearchSortOrder, SEARCH_EXCLUDE_CONFIG, VIEWLET_ID, ViewMode, VIEW_ID } from 'vs/workbench/services/search/common/search'; import { Extensions, IConfigurationMigrationRegistry } from 'vs/workbench/common/configuration'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { assertType } from 'vs/base/common/types'; +import { getWorkspaceSymbols, IWorkspaceSymbol } from 'vs/workbench/contrib/search/common/search'; +import * as Constants from 'vs/workbench/contrib/search/common/constants'; + +import 'vs/workbench/contrib/search/browser/searchActionsCopy'; +import 'vs/workbench/contrib/search/browser/searchActionsFind'; +import 'vs/workbench/contrib/search/browser/searchActionsNav'; +import 'vs/workbench/contrib/search/browser/searchActionsRemoveReplace'; +import 'vs/workbench/contrib/search/browser/searchActionsSymbol'; +import 'vs/workbench/contrib/search/browser/searchActionsTopBar'; registerSingleton(ISearchWorkbenchService, SearchWorkbenchService, InstantiationType.Delayed); registerSingleton(ISearchHistoryService, SearchHistoryService, InstantiationType.Delayed); @@ -62,710 +48,6 @@ registerSingleton(ISearchHistoryService, SearchHistoryService, InstantiationType replaceContributions(); searchWidgetContributions(); -const category = { value: nls.localize('search', "Search"), original: 'Search' }; - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: Constants.ToggleQueryDetailsActionId, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.or(Constants.SearchViewFocusedKey, SearchEditorConstants.InSearchEditor), - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyJ, - handler: accessor => { - const contextService = accessor.get(IContextKeyService).getContext(document.activeElement); - if (contextService.getValue(SearchEditorConstants.InSearchEditor.serialize())) { - (accessor.get(IEditorService).activeEditorPane as SearchEditor).toggleQueryDetails(); - } else if (contextService.getValue(Constants.SearchViewFocusedKey.serialize())) { - const searchView = getSearchView(accessor.get(IViewsService)); - assertIsDefined(searchView).toggleQueryDetails(); - } - } -}); - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: Constants.FocusSearchFromResults, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.FirstMatchFocusKey), - primary: KeyMod.CtrlCmd | KeyCode.UpArrow, - handler: (accessor, args: any) => { - const searchView = getSearchView(accessor.get(IViewsService)); - searchView?.focusPreviousInputBox(); - } -}); - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: Constants.OpenMatch, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.FileMatchOrMatchFocusKey), - primary: KeyCode.Enter, - mac: { - primary: KeyCode.Enter, - secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow] - }, - handler: (accessor) => { - const searchView = getSearchView(accessor.get(IViewsService)); - if (searchView) { - const tree: WorkbenchCompressibleObjectTree = searchView.getControl(); - const viewer = searchView.getControl(); - const focus = tree.getFocus()[0]; - - if (focus instanceof FolderMatch) { - viewer.toggleCollapsed(focus); - } else { - searchView.open(tree.getFocus()[0], false, false, true); - } - } - } -}); - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: Constants.OpenMatchToSide, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.FileMatchOrMatchFocusKey), - primary: KeyMod.CtrlCmd | KeyCode.Enter, - mac: { - primary: KeyMod.WinCtrl | KeyCode.Enter - }, - handler: (accessor, args: any) => { - const searchView = getSearchView(accessor.get(IViewsService)); - if (searchView) { - const tree: WorkbenchCompressibleObjectTree = searchView.getControl(); - searchView.open(tree.getFocus()[0], false, true, true); - } - } -}); - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: Constants.RemoveActionId, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.FileMatchOrMatchFocusKey), - primary: KeyCode.Delete, - mac: { - primary: KeyMod.CtrlCmd | KeyCode.Backspace, - }, - handler: (accessor, args: any) => { - const searchView = getSearchView(accessor.get(IViewsService)); - if (searchView) { - const tree: WorkbenchCompressibleObjectTree = searchView.getControl(); - accessor.get(IInstantiationService).createInstance(RemoveAction, tree, tree.getFocus()[0]!).run(); - } - } -}); - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: Constants.ReplaceActionId, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, Constants.MatchFocusKey), - primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.Digit1, - handler: (accessor, args: any) => { - const searchView = getSearchView(accessor.get(IViewsService)); - if (searchView) { - const tree: WorkbenchCompressibleObjectTree = searchView.getControl(); - accessor.get(IInstantiationService).createInstance(ReplaceAction, tree, tree.getFocus()[0] as Match, searchView).run(); - } - } -}); - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: Constants.ReplaceAllInFileActionId, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, Constants.FileFocusKey), - primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.Digit1, - secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter], - handler: (accessor, args: any) => { - const searchView = getSearchView(accessor.get(IViewsService)); - if (searchView) { - const tree: WorkbenchCompressibleObjectTree = searchView.getControl(); - accessor.get(IInstantiationService).createInstance(ReplaceAllAction, searchView, tree.getFocus()[0] as FileMatch).run(); - } - } -}); - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: Constants.ReplaceAllInFolderActionId, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, Constants.FolderFocusKey), - primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.Digit1, - secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter], - handler: (accessor, args: any) => { - const searchView = getSearchView(accessor.get(IViewsService)); - if (searchView) { - const tree: WorkbenchCompressibleObjectTree = searchView.getControl(); - accessor.get(IInstantiationService).createInstance(ReplaceAllInFolderAction, tree, tree.getFocus()[0] as FolderMatch).run(); - } - } -}); - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: Constants.CloseReplaceWidgetActionId, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceInputBoxFocusedKey), - primary: KeyCode.Escape, - handler: (accessor, args: any) => { - accessor.get(IInstantiationService).createInstance(CloseReplaceAction, Constants.CloseReplaceWidgetActionId, '').run(); - } -}); - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: FocusNextInputAction.ID, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.or( - ContextKeyExpr.and(SearchEditorConstants.InSearchEditor, Constants.InputBoxFocusedKey), - ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.InputBoxFocusedKey)), - primary: KeyMod.CtrlCmd | KeyCode.DownArrow, - handler: (accessor, args: any) => { - accessor.get(IInstantiationService).createInstance(FocusNextInputAction, FocusNextInputAction.ID, '').run(); - } -}); - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: FocusPreviousInputAction.ID, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.or( - ContextKeyExpr.and(SearchEditorConstants.InSearchEditor, Constants.InputBoxFocusedKey), - ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.InputBoxFocusedKey, Constants.SearchInputBoxFocusedKey.toNegated())), - primary: KeyMod.CtrlCmd | KeyCode.UpArrow, - handler: (accessor, args: any) => { - accessor.get(IInstantiationService).createInstance(FocusPreviousInputAction, FocusPreviousInputAction.ID, '').run(); - } -}); - -const restrictSearchToFolderFromSearch: ICommandHandler = async (accessor, folderMatch?: FolderMatchWithResource) => { - return searchWithFolderCommand(accessor, false, true, undefined, folderMatch); -}; -const excludeFolderFromSearch: ICommandHandler = async (accessor, folderMatch?: FolderMatchWithResource) => { - return searchWithFolderCommand(accessor, false, false, undefined, folderMatch); -}; -const RESTRICT_SEARCH_TO_FOLDER_ID = 'search.restrictSearchToFolder'; -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: RESTRICT_SEARCH_TO_FOLDER_ID, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ResourceFolderFocusKey), - primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KeyF, - handler: restrictSearchToFolderFromSearch -}); - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: Constants.ExcludeFolderFromSearchId, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ResourceFolderFocusKey), - handler: excludeFolderFromSearch -}); - -MenuRegistry.appendMenuItem(MenuId.SearchContext, { - command: { - id: Constants.ReplaceActionId, - title: ReplaceAction.LABEL - }, - when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.MatchFocusKey), - group: 'search', - order: 1 -}); - -MenuRegistry.appendMenuItem(MenuId.SearchContext, { - command: { - id: Constants.ReplaceAllInFolderActionId, - title: ReplaceAllInFolderAction.LABEL - }, - when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.FolderFocusKey), - group: 'search', - order: 1 -}); - -MenuRegistry.appendMenuItem(MenuId.SearchContext, { - command: { - id: Constants.ReplaceAllInFileActionId, - title: ReplaceAllAction.LABEL - }, - when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.FileFocusKey), - group: 'search', - order: 1 -}); - -MenuRegistry.appendMenuItem(MenuId.SearchContext, { - command: { - id: Constants.RemoveActionId, - title: RemoveAction.LABEL - }, - when: Constants.FileMatchOrMatchFocusKey, - group: 'search', - order: 2 -}); - -MenuRegistry.appendMenuItem(MenuId.SearchContext, { - group: 'search', - order: 3, - command: { - id: RESTRICT_SEARCH_TO_FOLDER_ID, - title: nls.localize('restrictResultsToFolder', "Restrict Search to Folder") - }, - when: ContextKeyExpr.and(Constants.ResourceFolderFocusKey) -}); - -MenuRegistry.appendMenuItem(MenuId.SearchContext, { - group: 'search', - order: 4, - command: { - id: Constants.ExcludeFolderFromSearchId, - title: nls.localize('excludeFolderFromSearch', "Exclude Folder from Search") - }, - when: ContextKeyExpr.and(Constants.ResourceFolderFocusKey) -}); - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: Constants.CopyMatchCommandId, - weight: KeybindingWeight.WorkbenchContrib, - when: Constants.FileMatchOrMatchFocusKey, - primary: KeyMod.CtrlCmd | KeyCode.KeyC, - handler: copyMatchCommand -}); - -MenuRegistry.appendMenuItem(MenuId.SearchContext, { - command: { - id: Constants.CopyMatchCommandId, - title: nls.localize('copyMatchLabel', "Copy") - }, - when: Constants.FileMatchOrMatchFocusKey, - group: 'search_2', - order: 1 -}); - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: Constants.CopyPathCommandId, - weight: KeybindingWeight.WorkbenchContrib, - when: Constants.FileMatchOrFolderMatchWithResourceFocusKey, - primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyC, - win: { - primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KeyC - }, - handler: copyPathCommand -}); - -MenuRegistry.appendMenuItem(MenuId.SearchContext, { - command: { - id: Constants.CopyPathCommandId, - title: nls.localize('copyPathLabel', "Copy Path") - }, - when: Constants.FileMatchOrFolderMatchWithResourceFocusKey, - group: 'search_2', - order: 2 -}); - -MenuRegistry.appendMenuItem(MenuId.SearchContext, { - command: { - id: Constants.CopyAllCommandId, - title: nls.localize('copyAllLabel', "Copy All") - }, - when: Constants.HasSearchResults, - group: 'search_2', - order: 3 -}); - -CommandsRegistry.registerCommand({ - id: Constants.CopyAllCommandId, - handler: copyAllCommand -}); - -CommandsRegistry.registerCommand({ - id: Constants.ClearSearchHistoryCommandId, - handler: clearHistoryCommand -}); - -CommandsRegistry.registerCommand({ - id: Constants.RevealInSideBarForSearchResults, - handler: (accessor, args: any) => { - const paneCompositeService = accessor.get(IPaneCompositePartService); - const explorerService = accessor.get(IExplorerService); - const contextService = accessor.get(IWorkspaceContextService); - - const searchView = getSearchView(accessor.get(IViewsService)); - if (!searchView) { - return; - } - - let fileMatch: FileMatch; - if (!(args instanceof FileMatch)) { - args = searchView.getControl().getFocus()[0]; - } - if (args instanceof FileMatch) { - fileMatch = args; - } else { - return; - } - - paneCompositeService.openPaneComposite(VIEWLET_ID_FILES, ViewContainerLocation.Sidebar, false).then((viewlet) => { - if (!viewlet) { - return; - } - - const explorerViewContainer = viewlet.getViewPaneContainer() as ExplorerViewPaneContainer; - const uri = fileMatch.resource; - if (uri && contextService.isInsideWorkspace(uri)) { - const explorerView = explorerViewContainer.getExplorerView(); - explorerView.setExpanded(true); - explorerService.select(uri, true).then(() => explorerView.focus(), onUnexpectedError); - } - }); - } -}); - -registerAction2(class CancelSearchAction extends Action2 { - constructor() { - super({ - id: Constants.CancelSearchActionId, - title: { - value: nls.localize('CancelSearchAction.label', "Cancel Search"), - original: 'Cancel Search' - }, - icon: searchStopIcon, - category, - f1: true, - precondition: SearchStateKey.isEqualTo(SearchUIState.Idle).negate(), - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, WorkbenchListFocusContextKey), - primary: KeyCode.Escape, - }, - menu: [{ - id: MenuId.ViewTitle, - group: 'navigation', - order: 0, - when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), SearchStateKey.isEqualTo(SearchUIState.SlowSearch)), - }] - }); - } - run(accessor: ServicesAccessor, ...args: any[]) { - return cancelSearch(accessor); - } -}); - -registerAction2(class RefreshAction extends Action2 { - constructor() { - super({ - id: Constants.RefreshSearchResultsActionId, - title: { - value: nls.localize('RefreshAction.label', "Refresh"), - original: 'Refresh' - }, - icon: searchRefreshIcon, - precondition: Constants.ViewHasSearchPatternKey, - category, - f1: true, - menu: [{ - id: MenuId.ViewTitle, - group: 'navigation', - order: 0, - when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), SearchStateKey.isEqualTo(SearchUIState.SlowSearch).negate()), - }] - }); - } - run(accessor: ServicesAccessor, ...args: any[]) { - return refreshSearch(accessor); - } -}); - -registerAction2(class CollapseDeepestExpandedLevelAction extends Action2 { - constructor() { - super({ - id: Constants.CollapseSearchResultsActionId, - title: { - value: nls.localize('CollapseDeepestExpandedLevelAction.label', "Collapse All"), - original: 'Collapse All' - }, - category, - icon: searchCollapseAllIcon, - f1: true, - precondition: ContextKeyExpr.and(Constants.HasSearchResults, Constants.ViewHasSomeCollapsibleKey), - menu: [{ - id: MenuId.ViewTitle, - group: 'navigation', - order: 3, - when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), ContextKeyExpr.or(Constants.HasSearchResults.negate(), Constants.ViewHasSomeCollapsibleKey)), - }] - }); - } - run(accessor: ServicesAccessor, ...args: any[]) { - return collapseDeepestExpandedLevel(accessor); - } -}); - -registerAction2(class ExpandAllAction extends Action2 { - constructor() { - super({ - id: Constants.ExpandSearchResultsActionId, - title: { - value: nls.localize('ExpandAllAction.label', "Expand All"), - original: 'Expand All' - }, - category, - icon: searchExpandAllIcon, - f1: true, - precondition: ContextKeyExpr.and(Constants.HasSearchResults, Constants.ViewHasSomeCollapsibleKey.toNegated()), - menu: [{ - id: MenuId.ViewTitle, - group: 'navigation', - order: 3, - when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), Constants.HasSearchResults, Constants.ViewHasSomeCollapsibleKey.toNegated()), - }] - }); - } - run(accessor: ServicesAccessor, ...args: any[]) { - return expandAll(accessor); - } -}); - -registerAction2(class ClearSearchResultsAction extends Action2 { - constructor() { - super({ - id: Constants.ClearSearchResultsActionId, - title: { - value: nls.localize('ClearSearchResultsAction.label', "Clear Search Results"), - original: 'Clear Search Results' - }, - category, - icon: searchClearIcon, - f1: true, - precondition: ContextKeyExpr.or(Constants.HasSearchResults, Constants.ViewHasSearchPatternKey, Constants.ViewHasReplacePatternKey, Constants.ViewHasFilePatternKey), - menu: [{ - id: MenuId.ViewTitle, - group: 'navigation', - order: 1, - when: ContextKeyExpr.equals('view', VIEW_ID), - }] - }); - } - run(accessor: ServicesAccessor, ...args: any[]) { - return clearSearchResults(accessor); - } -}); - -registerAction2(class ViewAsTreeAction extends Action2 { - constructor() { - super({ - id: Constants.ViewAsTreeActionId, - title: { - value: nls.localize('ViewAsTreeAction.label', "View as Tree"), - original: 'View as Tree' - }, - category, - icon: searchShowAsList, - f1: true, - precondition: ContextKeyExpr.and(Constants.HasSearchResults, Constants.InTreeViewKey.toNegated()), - menu: [{ - id: MenuId.ViewTitle, - group: 'navigation', - order: 2, - when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), Constants.InTreeViewKey.toNegated()), - }] - }); - } - run(accessor: ServicesAccessor, ...args: any[]) { - const searchView = getSearchView(accessor.get(IViewsService)); - if (searchView) { - searchView.setTreeView(true); - } - } -}); - - -registerAction2(class ViewAsListAction extends Action2 { - constructor() { - super({ - id: Constants.ViewAsListActionId, - title: { - value: nls.localize('ViewAsListAction.label', "View as List"), - original: 'View as List' - }, - category, - icon: searchShowAsTree, - f1: true, - precondition: ContextKeyExpr.and(Constants.HasSearchResults, Constants.InTreeViewKey), - menu: [{ - id: MenuId.ViewTitle, - group: 'navigation', - order: 2, - when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), Constants.InTreeViewKey), - }] - }); - } - run(accessor: ServicesAccessor, ...args: any[]) { - const searchView = getSearchView(accessor.get(IViewsService)); - if (searchView) { - searchView.setTreeView(false); - } - } -}); - -const RevealInSideBarForSearchResultsCommand: ICommandAction = { - id: Constants.RevealInSideBarForSearchResults, - title: nls.localize('revealInSideBar', "Reveal in Explorer View") -}; - -MenuRegistry.appendMenuItem(MenuId.SearchContext, { - command: RevealInSideBarForSearchResultsCommand, - when: ContextKeyExpr.and(Constants.FileFocusKey, Constants.HasSearchResults), - group: 'search_3', - order: 1 -}); - -const ClearSearchHistoryCommand: ICommandAction = { - id: Constants.ClearSearchHistoryCommandId, - title: { value: nls.localize('clearSearchHistoryLabel', "Clear Search History"), original: 'Clear Search History' }, - category -}; -MenuRegistry.addCommand(ClearSearchHistoryCommand); - -CommandsRegistry.registerCommand({ - id: Constants.FocusSearchListCommandID, - handler: focusSearchListCommand -}); - -const FocusSearchListCommand: ICommandAction = { - id: Constants.FocusSearchListCommandID, - title: { value: nls.localize('focusSearchListCommandLabel', "Focus List"), original: 'Focus List' }, - category -}; -MenuRegistry.addCommand(FocusSearchListCommand); - -const searchInFolderFromExplorer: ICommandHandler = async (accessor, resource?: URI) => { - return searchWithFolderCommand(accessor, true, true, resource); -}; - -const searchWithFolderCommand: ICommandHandler = async (accessor, isFromExplorer: boolean, isIncludes: boolean, resource?: URI, folderMatch?: FolderMatchWithResource) => { - const listService = accessor.get(IListService); - const fileService = accessor.get(IFileService); - const viewsService = accessor.get(IViewsService); - const contextService = accessor.get(IWorkspaceContextService); - const commandService = accessor.get(ICommandService); - const searchConfig = accessor.get(IConfigurationService).getValue().search; - const mode = searchConfig.mode; - - let resources: URI[]; - - if (isFromExplorer) { - resources = getMultiSelectedResources(resource, listService, accessor.get(IEditorService), accessor.get(IExplorerService)); - } else { - const searchView = getSearchView(accessor.get(IViewsService)); - if (!searchView) { - return; - } - resources = getMultiSelectedSearchResources(searchView.getControl(), folderMatch, searchConfig); - } - - const resolvedResources = fileService.resolveAll(resources.map(resource => ({ resource }))).then(results => { - const folders: URI[] = []; - results.forEach(result => { - if (result.success && result.stat) { - folders.push(result.stat.isDirectory ? result.stat.resource : dirname(result.stat.resource)); - } - }); - return resolveResourcesForSearchIncludes(folders, contextService); - }); - - if (mode === 'view') { - const searchView = await openSearchView(viewsService, true); - if (resources && resources.length && searchView) { - if (isIncludes) { - searchView.searchInFolders(await resolvedResources); - } else { - searchView.searchOutsideOfFolders(await resolvedResources); - } - } - return undefined; - } else { - if (isIncludes) { - return commandService.executeCommand(SearchEditorConstants.OpenEditorCommandId, { - filesToInclude: (await resolvedResources).join(', '), - showIncludesExcludes: true, - location: mode === 'newEditor' ? 'new' : 'reuse', - }); - } - else { - return commandService.executeCommand(SearchEditorConstants.OpenEditorCommandId, { - filesToExclude: (await resolvedResources).join(', '), - showIncludesExcludes: true, - location: mode === 'newEditor' ? 'new' : 'reuse', - }); - } - } -}; - -const FIND_IN_FOLDER_EXPLORER_ID = 'filesExplorer.findInFolder'; -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: FIND_IN_FOLDER_EXPLORER_ID, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerFolderContext), - primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KeyF, - handler: searchInFolderFromExplorer -}); - -const FIND_IN_WORKSPACE_ID = 'filesExplorer.findInWorkspace'; -CommandsRegistry.registerCommand({ - id: FIND_IN_WORKSPACE_ID, - handler: async (accessor) => { - const searchConfig = accessor.get(IConfigurationService).getValue().search; - const mode = searchConfig.mode; - - if (mode === 'view') { - const searchView = await openSearchView(accessor.get(IViewsService), true); - searchView?.searchInFolders(); - } - else { - return accessor.get(ICommandService).executeCommand(SearchEditorConstants.OpenEditorCommandId, { - location: mode === 'newEditor' ? 'new' : 'reuse', - filesToInclude: '', - }); - } - } -}); - -MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { - group: '4_search', - order: 10, - command: { - id: FIND_IN_FOLDER_EXPLORER_ID, - title: nls.localize('findInFolder', "Find in Folder...") - }, - when: ContextKeyExpr.and(ExplorerFolderContext) -}); - -MenuRegistry.appendMenuItem(MenuId.ExplorerContext, { - group: '4_search', - order: 10, - command: { - id: FIND_IN_WORKSPACE_ID, - title: nls.localize('findInWorkspace', "Find in Workspace...") - }, - when: ContextKeyExpr.and(ExplorerRootContext, ExplorerFolderContext.toNegated()) -}); - -class ShowAllSymbolsAction extends Action2 { - - static readonly ID = 'workbench.action.showAllSymbols'; - static readonly LABEL = nls.localize('showTriggerActions', "Go to Symbol in Workspace..."); - static readonly ALL_SYMBOLS_PREFIX = '#'; - - constructor( - ) { - super({ - id: Constants.ShowAllSymbolsActionId, - title: { - value: nls.localize('showTriggerActions', "Go to Symbol in Workspace..."), - original: 'Go to Symbol in Workspace...' - }, - f1: true, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.CtrlCmd | KeyCode.KeyT - } - }); - } - - override async run(accessor: ServicesAccessor): Promise { - accessor.get(IQuickInputService).quickAccess.show(ShowAllSymbolsAction.ALL_SYMBOLS_PREFIX); - } -} - -registerAction2(ShowAllSymbolsAction); - const SEARCH_MODE_CONFIG = 'search.mode'; const viewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ @@ -799,7 +81,6 @@ const viewDescriptor: IViewDescriptor = { // Register search default location to sidebar Registry.as(ViewExtensions.ViewsRegistry).registerViews([viewDescriptor], viewContainer); - // Migrate search location setting to new model class RegisterSearchViewContribution implements IWorkbenchContribution { constructor( @@ -816,200 +97,6 @@ class RegisterSearchViewContribution implements IWorkbenchContribution { } Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(RegisterSearchViewContribution, LifecyclePhase.Starting); -// Find in Files by default is the same as View: Show Search, but can be configured to open a search editor instead with the `search.mode` binding -KeybindingsRegistry.registerCommandAndKeybindingRule({ - description: { - description: nls.localize('findInFiles.description', "Open a workspace search"), - args: [ - { - name: nls.localize('findInFiles.args', "A set of options for the search"), - schema: { - type: 'object', - properties: { - query: { 'type': 'string' }, - replace: { 'type': 'string' }, - preserveCase: { 'type': 'boolean' }, - triggerSearch: { 'type': 'boolean' }, - filesToInclude: { 'type': 'string' }, - filesToExclude: { 'type': 'string' }, - isRegex: { 'type': 'boolean' }, - isCaseSensitive: { 'type': 'boolean' }, - matchWholeWord: { 'type': 'boolean' }, - useExcludeSettingsAndIgnoreFiles: { 'type': 'boolean' }, - onlyOpenEditors: { 'type': 'boolean' }, - } - } - }, - ] - }, - id: Constants.FindInFilesActionId, - weight: KeybindingWeight.WorkbenchContrib, - when: null, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyF, - handler: FindInFilesCommand -}); -MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: Constants.FindInFilesActionId, title: { value: nls.localize('findInFiles', "Find in Files"), original: 'Find in Files' }, category } }); -MenuRegistry.appendMenuItem(MenuId.MenubarEditMenu, { - group: '4_find_global', - command: { - id: Constants.FindInFilesActionId, - title: nls.localize({ key: 'miFindInFiles', comment: ['&& denotes a mnemonic'] }, "Find &&in Files") - }, - order: 1 -}); - -registerAction2(class FocusNextSearchResultAction extends Action2 { - constructor() { - super({ - id: Constants.FocusNextSearchResultActionId, - title: { - value: nls.localize('FocusNextSearchResult.label', 'Focus Next Search Result'), - original: 'Focus Next Search Result' - }, - keybinding: [{ - primary: KeyCode.F4, - weight: KeybindingWeight.WorkbenchContrib, - }], - category: category.value, - - precondition: ContextKeyExpr.or(Constants.HasSearchResults, SearchEditorConstants.InSearchEditor), - }); - } - - override async run(accessor: ServicesAccessor): Promise { - return focusNextSearchResult(accessor); - } -}); - -registerAction2(class FocusPreviousSearchResultAction extends Action2 { - constructor() { - super({ - id: Constants.FocusPreviousSearchResultActionId, - title: { - value: nls.localize('FocusPreviousSearchResult.label', 'Search: Focus Previous Search Result'), - original: 'Search: Focus Previous Search Result' - }, - keybinding: [{ - primary: KeyMod.Shift | KeyCode.F4, - weight: KeybindingWeight.WorkbenchContrib, - }], - category: category.value, - - precondition: ContextKeyExpr.or(Constants.HasSearchResults, SearchEditorConstants.InSearchEditor), - }); - } - - override async run(accessor: ServicesAccessor): Promise { - return focusPreviousSearchResult(accessor); - } -}); - -registerAction2(class ReplaceInFilesAction extends Action2 { - constructor() { - super({ - id: Constants.ReplaceInFilesActionId, - title: { - value: nls.localize('replaceInFiles', 'Search: Replace in Files'), - original: 'Search: Replace in Files' - }, - keybinding: [{ - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyH, - weight: KeybindingWeight.WorkbenchContrib, - }], - category: category.value, - }); - } - - override async run(accessor: ServicesAccessor): Promise { - return findOrReplaceInFiles(accessor, true); - } -}); - -MenuRegistry.appendMenuItem(MenuId.MenubarEditMenu, { - group: '4_find_global', - command: { - id: Constants.ReplaceInFilesActionId, - title: nls.localize({ key: 'miReplaceInFiles', comment: ['&& denotes a mnemonic'] }, "Replace &&in Files") - }, - order: 2 -}); - -if (platform.isMacintosh) { - // Register this with a more restrictive `when` on mac to avoid conflict with "copy path" - KeybindingsRegistry.registerCommandAndKeybindingRule(Object.assign({ - id: Constants.ToggleCaseSensitiveCommandId, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(Constants.SearchViewFocusedKey, Constants.FileMatchOrFolderMatchFocusKey.toNegated()), - handler: toggleCaseSensitiveCommand - }, ToggleCaseSensitiveKeybinding)); -} else { - KeybindingsRegistry.registerCommandAndKeybindingRule(Object.assign({ - id: Constants.ToggleCaseSensitiveCommandId, - weight: KeybindingWeight.WorkbenchContrib, - when: Constants.SearchViewFocusedKey, - handler: toggleCaseSensitiveCommand - }, ToggleCaseSensitiveKeybinding)); -} - -KeybindingsRegistry.registerCommandAndKeybindingRule(Object.assign({ - id: Constants.ToggleWholeWordCommandId, - weight: KeybindingWeight.WorkbenchContrib, - when: Constants.SearchViewFocusedKey, - handler: toggleWholeWordCommand -}, ToggleWholeWordKeybinding)); - -KeybindingsRegistry.registerCommandAndKeybindingRule(Object.assign({ - id: Constants.ToggleRegexCommandId, - weight: KeybindingWeight.WorkbenchContrib, - when: Constants.SearchViewFocusedKey, - handler: toggleRegexCommand -}, ToggleRegexKeybinding)); - -KeybindingsRegistry.registerCommandAndKeybindingRule(Object.assign({ - id: Constants.TogglePreserveCaseId, - weight: KeybindingWeight.WorkbenchContrib, - when: Constants.SearchViewFocusedKey, - handler: togglePreserveCaseCommand -}, TogglePreserveCaseKeybinding)); - -KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: Constants.AddCursorsAtSearchResults, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.FileMatchOrMatchFocusKey), - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyL, - handler: (accessor, args: any) => { - const searchView = getSearchView(accessor.get(IViewsService)); - if (searchView) { - const tree: WorkbenchCompressibleObjectTree = searchView.getControl(); - searchView.openEditorWithMultiCursor(tree.getFocus()[0]); - } - } -}); -// --- Toggle Search On Type - -registerAction2(class ToggleSearchOnTypeAction extends Action2 { - private static readonly searchOnTypeKey = 'search.searchOnType'; - - constructor( - ) { - super({ - id: Constants.ToggleSearchOnTypeActionId, - title: { - value: nls.localize('toggleTabs', 'Toggle Search on Type'), - original: 'Toggle Search on Type' - }, - category: category.value, - }); - - } - - override async run(accessor: ServicesAccessor): Promise { - const configurationService = accessor.get(IConfigurationService); - const searchOnType = configurationService.getValue(ToggleSearchOnTypeAction.searchOnTypeKey); - return configurationService.updateValue(ToggleSearchOnTypeAction.searchOnTypeKey, !searchOnType); - } -}); - // Register Quick Access Handler const quickAccessRegistry = Registry.as(QuickAccessExtensions.Quickaccess); @@ -1026,7 +113,7 @@ quickAccessRegistry.registerQuickAccessProvider({ prefix: SymbolsQuickAccessProvider.PREFIX, placeholder: nls.localize('symbolsQuickAccessPlaceholder', "Type the name of a symbol to open."), contextKey: 'inWorkspaceSymbolsPicker', - helpEntries: [{ description: nls.localize('symbolsQuickAccess', "Go to Symbol in Workspace"), commandId: ShowAllSymbolsAction.ID }] + helpEntries: [{ description: nls.localize('symbolsQuickAccess', "Go to Symbol in Workspace"), commandId: Constants.ShowAllSymbolsActionId }] }); // Configuration @@ -1272,14 +359,3 @@ CommandsRegistry.registerCommand('_executeWorkspaceSymbolProvider', async functi const result = await getWorkspaceSymbols(query); return result.map(item => item.symbol); }); - -// Go to menu - -MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { - group: '3_global_nav', - command: { - id: Constants.ShowAllSymbolsActionId, - title: nls.localize({ key: 'miGotoSymbolInWorkspace', comment: ['&& denotes a mnemonic'] }, "Go to Symbol in &&Workspace...") - }, - order: 2 -}); diff --git a/src/vs/workbench/contrib/search/browser/searchActions.ts b/src/vs/workbench/contrib/search/browser/searchActions.ts deleted file mode 100644 index 87ccd82a90a..00000000000 --- a/src/vs/workbench/contrib/search/browser/searchActions.ts +++ /dev/null @@ -1,778 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as DOM from 'vs/base/browser/dom'; -import { ITreeNavigator } from 'vs/base/browser/ui/tree/tree'; -import { Action, IAction } from 'vs/base/common/actions'; -import { createKeybinding, ResolvedKeybinding } from 'vs/base/common/keybindings'; -import { isWindows, OS } from 'vs/base/common/platform'; -import * as nls from 'vs/nls'; -import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { ICommandHandler, ICommandService } from 'vs/platform/commands/common/commands'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { ILabelService } from 'vs/platform/label/common/label'; -import { getSelectionKeyboardEvent, WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listService'; -import { ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { IViewsService } from 'vs/workbench/common/views'; -import { searchRemoveIcon, searchReplaceAllIcon, searchReplaceIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; -import { SearchView } from 'vs/workbench/contrib/search/browser/searchView'; -import * as Constants from 'vs/workbench/contrib/search/common/constants'; -import { IReplaceService } from 'vs/workbench/contrib/search/common/replace'; -import { ISearchHistoryService } from 'vs/workbench/contrib/search/common/searchHistoryService'; -import { arrayContainsElementOrParent, FileMatch, FolderMatch, FolderMatchNoRoot, FolderMatchWithResource, FolderMatchWorkspaceRoot, Match, RenderableMatch, searchComparer, searchMatchComparer, SearchResult } from 'vs/workbench/contrib/search/common/searchModel'; -import { OpenEditorCommandId } from 'vs/workbench/contrib/searchEditor/browser/constants'; -import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditor'; -import { OpenSearchEditorArgs } from 'vs/workbench/contrib/searchEditor/browser/searchEditor.contribution'; -import { SearchEditorInput } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { ISearchConfiguration, ISearchConfigurationProperties, VIEW_ID } from 'vs/workbench/services/search/common/search'; -import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; -import { URI } from 'vs/base/common/uri'; - -export function isSearchViewFocused(viewsService: IViewsService): boolean { - const searchView = getSearchView(viewsService); - const activeElement = document.activeElement; - return !!(searchView && activeElement && DOM.isAncestor(activeElement, searchView.getContainer())); -} - -export function appendKeyBindingLabel(label: string, inputKeyBinding: number | ResolvedKeybinding | undefined, keyBindingService2: IKeybindingService): string { - if (typeof inputKeyBinding === 'number') { - const keybinding = createKeybinding(inputKeyBinding, OS); - if (keybinding) { - const resolvedKeybindings = keyBindingService2.resolveKeybinding(keybinding); - return doAppendKeyBindingLabel(label, resolvedKeybindings.length > 0 ? resolvedKeybindings[0] : undefined); - } - return doAppendKeyBindingLabel(label, undefined); - } else { - return doAppendKeyBindingLabel(label, inputKeyBinding); - } -} - -export function openSearchView(viewsService: IViewsService, focus?: boolean): Promise { - return viewsService.openView(VIEW_ID, focus).then(view => (view as SearchView ?? undefined)); -} - -export function getSearchView(viewsService: IViewsService): SearchView | undefined { - return viewsService.getActiveViewWithId(VIEW_ID) as SearchView ?? undefined; -} - -function doAppendKeyBindingLabel(label: string, keyBinding: ResolvedKeybinding | undefined): string { - return keyBinding ? label + ' (' + keyBinding.getLabel() + ')' : label; -} - -export const toggleCaseSensitiveCommand = (accessor: ServicesAccessor) => { - const searchView = getSearchView(accessor.get(IViewsService)); - searchView?.toggleCaseSensitive(); -}; - -export const toggleWholeWordCommand = (accessor: ServicesAccessor) => { - const searchView = getSearchView(accessor.get(IViewsService)); - searchView?.toggleWholeWords(); -}; - -export const toggleRegexCommand = (accessor: ServicesAccessor) => { - const searchView = getSearchView(accessor.get(IViewsService)); - searchView?.toggleRegex(); -}; - -export const togglePreserveCaseCommand = (accessor: ServicesAccessor) => { - const searchView = getSearchView(accessor.get(IViewsService)); - searchView?.togglePreserveCase(); -}; - -export class FocusNextInputAction extends Action { - - static readonly ID = 'search.focus.nextInputBox'; - - constructor(id: string, label: string, - @IViewsService private readonly viewsService: IViewsService, - @IEditorService private readonly editorService: IEditorService, - ) { - super(id, label); - } - - override async run(): Promise { - const input = this.editorService.activeEditor; - if (input instanceof SearchEditorInput) { - // cast as we cannot import SearchEditor as a value b/c cyclic dependency. - (this.editorService.activeEditorPane as SearchEditor).focusNextInput(); - } - - const searchView = getSearchView(this.viewsService); - searchView?.focusNextInputBox(); - } -} - -export class FocusPreviousInputAction extends Action { - - static readonly ID = 'search.focus.previousInputBox'; - - constructor(id: string, label: string, - @IViewsService private readonly viewsService: IViewsService, - @IEditorService private readonly editorService: IEditorService, - ) { - super(id, label); - } - - override async run(): Promise { - const input = this.editorService.activeEditor; - if (input instanceof SearchEditorInput) { - // cast as we cannot import SearchEditor as a value b/c cyclic dependency. - (this.editorService.activeEditorPane as SearchEditor).focusPrevInput(); - } - - const searchView = getSearchView(this.viewsService); - searchView?.focusPreviousInputBox(); - } -} - -export interface IFindInFilesArgs { - query?: string; - replace?: string; - preserveCase?: boolean; - triggerSearch?: boolean; - filesToInclude?: string; - filesToExclude?: string; - isRegex?: boolean; - isCaseSensitive?: boolean; - matchWholeWord?: boolean; - useExcludeSettingsAndIgnoreFiles?: boolean; - onlyOpenEditors?: boolean; -} - -export const FindInFilesCommand: ICommandHandler = (accessor, args: IFindInFilesArgs = {}) => { - const searchConfig = accessor.get(IConfigurationService).getValue().search; - const mode = searchConfig.mode; - if (mode === 'view') { - const viewsService = accessor.get(IViewsService); - openSearchView(viewsService, false).then(openedView => { - if (openedView) { - const searchAndReplaceWidget = openedView.searchAndReplaceWidget; - searchAndReplaceWidget.toggleReplace(typeof args.replace === 'string'); - let updatedText = false; - if (typeof args.query === 'string') { - openedView.setSearchParameters(args); - } else { - updatedText = openedView.updateTextFromFindWidgetOrSelection({ allowUnselectedWord: typeof args.replace !== 'string' }); - } - openedView.searchAndReplaceWidget.focus(undefined, updatedText, updatedText); - } - }); - } else { - const convertArgs = (args: IFindInFilesArgs): OpenSearchEditorArgs => ({ - location: mode === 'newEditor' ? 'new' : 'reuse', - query: args.query, - filesToInclude: args.filesToInclude, - filesToExclude: args.filesToExclude, - matchWholeWord: args.matchWholeWord, - isCaseSensitive: args.isCaseSensitive, - isRegexp: args.isRegex, - useExcludeSettingsAndIgnoreFiles: args.useExcludeSettingsAndIgnoreFiles, - onlyOpenEditors: args.onlyOpenEditors, - showIncludesExcludes: !!(args.filesToExclude || args.filesToExclude || !args.useExcludeSettingsAndIgnoreFiles), - }); - accessor.get(ICommandService).executeCommand(OpenEditorCommandId, convertArgs(args)); - } -}; - -export class CloseReplaceAction extends Action { - - constructor(id: string, label: string, - @IViewsService private readonly viewsService: IViewsService - ) { - super(id, label); - } - - override run(): Promise { - const searchView = getSearchView(this.viewsService); - if (searchView) { - searchView.searchAndReplaceWidget.toggleReplace(false); - searchView.searchAndReplaceWidget.focus(); - } - return Promise.resolve(null); - } -} - -export function expandAll(accessor: ServicesAccessor) { - const viewsService = accessor.get(IViewsService); - const searchView = getSearchView(viewsService); - if (searchView) { - const viewer = searchView.getControl(); - viewer.expandAll(); - } -} - -export function clearSearchResults(accessor: ServicesAccessor) { - const viewsService = accessor.get(IViewsService); - const searchView = getSearchView(viewsService); - searchView?.clearSearchResults(); -} - -export function cancelSearch(accessor: ServicesAccessor) { - const viewsService = accessor.get(IViewsService); - const searchView = getSearchView(viewsService); - searchView?.cancelSearch(); -} - -export function refreshSearch(accessor: ServicesAccessor) { - const viewsService = accessor.get(IViewsService); - const searchView = getSearchView(viewsService); - searchView?.triggerQueryChange({ preserveFocus: false }); -} - -export function collapseDeepestExpandedLevel(accessor: ServicesAccessor) { - - const viewsService = accessor.get(IViewsService); - const searchView = getSearchView(viewsService); - if (searchView) { - const viewer = searchView.getControl(); - - /** - * one level to collapse so collapse everything. If FolderMatch, check if there are visible grandchildren, - * i.e. if Matches are returned by the navigator, and if so, collapse to them, otherwise collapse all levels. - */ - const navigator = viewer.navigate(); - let node = navigator.first(); - let canCollapseFileMatchLevel = false; - let canCollapseFirstLevel = false; - - if (node instanceof FolderMatchWorkspaceRoot) { - while (node = navigator.next()) { - if (node instanceof Match) { - canCollapseFileMatchLevel = true; - break; - } - if (searchView.isTreeLayoutViewVisible && !canCollapseFirstLevel) { - let nodeToTest = node; - - if (node instanceof FolderMatch) { - nodeToTest = node.compressionStartParent ?? node; - } - - const immediateParent = nodeToTest.parent(); - - if (!(immediateParent instanceof FolderMatchWorkspaceRoot || immediateParent instanceof FolderMatchNoRoot || immediateParent instanceof SearchResult)) { - canCollapseFirstLevel = true; - } - } - } - } - - if (canCollapseFileMatchLevel) { - node = navigator.first(); - do { - if (node instanceof FileMatch) { - viewer.collapse(node); - } - } while (node = navigator.next()); - } else if (canCollapseFirstLevel) { - node = navigator.first(); - if (node) { - do { - - let nodeToTest = node; - - if (node instanceof FolderMatch) { - nodeToTest = node.compressionStartParent ?? node; - } - const immediateParent = nodeToTest.parent(); - - if (immediateParent instanceof FolderMatchWorkspaceRoot || immediateParent instanceof FolderMatchNoRoot) { - if (viewer.hasElement(node)) { - viewer.collapse(node, true); - } else { - viewer.collapseAll(); - } - } - } while (node = navigator.next()); - } - } else { - viewer.collapseAll(); - } - - const firstFocusParent = viewer.getFocus()[0]?.parent(); - - if (firstFocusParent && (firstFocusParent instanceof FolderMatch || firstFocusParent instanceof FileMatch) && - viewer.hasElement(firstFocusParent) && viewer.isCollapsed(firstFocusParent)) { - viewer.domFocus(); - viewer.focusFirst(); - viewer.setSelection(viewer.getFocus()); - } - } -} - -export async function focusNextSearchResult(accessor: ServicesAccessor): Promise { - const editorService = accessor.get(IEditorService); - const input = editorService.activeEditor; - if (input instanceof SearchEditorInput) { - // cast as we cannot import SearchEditor as a value b/c cyclic dependency. - return (editorService.activeEditorPane as SearchEditor).focusNextResult(); - } - - return openSearchView(accessor.get(IViewsService)).then(searchView => { - searchView?.selectNextMatch(); - }); -} - -export async function focusPreviousSearchResult(accessor: ServicesAccessor): Promise { - const editorService = accessor.get(IEditorService); - const input = editorService.activeEditor; - if (input instanceof SearchEditorInput) { - // cast as we cannot import SearchEditor as a value b/c cyclic dependency. - return (editorService.activeEditorPane as SearchEditor).focusPreviousResult(); - } - - return openSearchView(accessor.get(IViewsService)).then(searchView => { - searchView?.selectPreviousMatch(); - }); -} - -export async function findOrReplaceInFiles(accessor: ServicesAccessor, expandSearchReplaceWidget: boolean): Promise { - return openSearchView(accessor.get(IViewsService), false).then(openedView => { - if (openedView) { - const searchAndReplaceWidget = openedView.searchAndReplaceWidget; - searchAndReplaceWidget.toggleReplace(expandSearchReplaceWidget); - - const updatedText = openedView.updateTextFromFindWidgetOrSelection({ allowUnselectedWord: !expandSearchReplaceWidget }); - openedView.searchAndReplaceWidget.focus(undefined, updatedText, updatedText); - } - }); -} - - -class ReplaceActionRunner { - constructor( - private viewer: WorkbenchCompressibleObjectTree, - private viewlet: SearchView | undefined, - // Services - @IReplaceService private readonly replaceService: IReplaceService, - @IEditorService private readonly editorService: IEditorService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, - @IViewsService private readonly viewsService: IViewsService - ) { } - - async performReplace(element: RenderableMatch): Promise { - // since multiple elements can be selected, we need to check the type of the FolderMatch/FileMatch/Match before we perform the replace. - const opInfo = getElementsToOperateOnInfo(this.viewer, element, this.configurationService.getValue('search')); - const elementsToReplace = opInfo.elements; - let focusElement = this.viewer.getFocus()[0]; - - if (!focusElement || (focusElement && !arrayContainsElementOrParent(focusElement, elementsToReplace)) || (focusElement instanceof SearchResult)) { - focusElement = element; - } - - if (elementsToReplace.length === 0) { - return; - } - let nextFocusElement; - if (focusElement) { - nextFocusElement = getElementToFocusAfterRemoved(this.viewer, focusElement, elementsToReplace); - } - - const searchResult = getSearchView(this.viewsService)?.searchResult; - - if (searchResult) { - searchResult.batchReplace(elementsToReplace); - } - - if (focusElement) { - if (!nextFocusElement) { - nextFocusElement = getLastNodeFromSameType(this.viewer, focusElement); - } - - if (nextFocusElement) { - this.viewer.reveal(nextFocusElement); - this.viewer.setFocus([nextFocusElement], getSelectionKeyboardEvent()); - this.viewer.setSelection([nextFocusElement], getSelectionKeyboardEvent()); - - if (nextFocusElement instanceof Match) { - const useReplacePreview = this.configurationService.getValue().search.useReplacePreview; - if (!useReplacePreview || this.hasToOpenFile(nextFocusElement)) { - this.viewlet?.open(nextFocusElement, true); - } else { - this.replaceService.openReplacePreview(nextFocusElement, true); - } - } else if (nextFocusElement instanceof FileMatch) { - this.viewlet?.open(nextFocusElement, true); - } - } - - } - - this.viewer.domFocus(); - } - - private hasToOpenFile(currBottomElem: RenderableMatch): boolean { - if (!(currBottomElem instanceof Match)) { - return false; - } - const activeEditor = this.editorService.activeEditor; - const file = activeEditor?.resource; - if (file) { - return this.uriIdentityService.extUri.isEqual(file, currBottomElem.parent().resource); - } - return false; - } -} - -export class RemoveAction implements IAction { - - static readonly LABEL = nls.localize('RemoveAction.label', "Dismiss"); - - readonly id = Constants.RemoveActionId; - public class = ThemeIcon.asClassName(searchRemoveIcon); - public label: string; - public tooltip = ''; - public enabled = true; - - constructor( - private readonly viewer: WorkbenchCompressibleObjectTree, - private readonly element: RenderableMatch, - @IKeybindingService keyBindingService: IKeybindingService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IViewsService private readonly viewsService: IViewsService, - ) { - this.label = appendKeyBindingLabel(RemoveAction.LABEL, keyBindingService.lookupKeybinding(Constants.RemoveActionId), keyBindingService); - } - - run(): void { - const opInfo = getElementsToOperateOnInfo(this.viewer, this.element, this.configurationService.getValue('search')); - const elementsToRemove = opInfo.elements; - let focusElement = this.viewer.getFocus()[0]; - - if (elementsToRemove.length === 0) { - return; - } - - if (!focusElement || (focusElement instanceof SearchResult)) { - focusElement = this.element; - } - - let nextFocusElement; - if (opInfo.mustReselect && focusElement) { - nextFocusElement = getElementToFocusAfterRemoved(this.viewer, focusElement, elementsToRemove); - } - - const searchResult = getSearchView(this.viewsService)?.searchResult; - - if (searchResult) { - searchResult.batchRemove(elementsToRemove); - } - - if (opInfo.mustReselect && focusElement) { - if (!nextFocusElement) { - nextFocusElement = getLastNodeFromSameType(this.viewer, focusElement); - } - - if (nextFocusElement && !arrayContainsElementOrParent(nextFocusElement, elementsToRemove)) { - this.viewer.reveal(nextFocusElement); - this.viewer.setFocus([nextFocusElement], getSelectionKeyboardEvent()); - this.viewer.setSelection([nextFocusElement], getSelectionKeyboardEvent()); - } - } - - this.viewer.domFocus(); - return; - } -} - -export class ReplaceAction extends Action { - - static readonly LABEL = nls.localize('match.replace.label', "Replace"); - - static runQ = Promise.resolve(); - private replaceRunner: ReplaceActionRunner; - - constructor(viewer: WorkbenchCompressibleObjectTree, private element: Match, viewlet: SearchView, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IKeybindingService keyBindingService: IKeybindingService, - ) { - super(Constants.ReplaceActionId, appendKeyBindingLabel(ReplaceAction.LABEL, keyBindingService.lookupKeybinding(Constants.ReplaceActionId), keyBindingService), ThemeIcon.asClassName(searchReplaceIcon)); - this.replaceRunner = this.instantiationService.createInstance(ReplaceActionRunner, viewer, viewlet); - } - - override async run(): Promise { - return this.replaceRunner.performReplace(this.element); - } -} - -export class ReplaceAllAction extends Action { - - static readonly LABEL = nls.localize('file.replaceAll.label', "Replace All"); - private replaceRunner: ReplaceActionRunner; - constructor( - viewlet: SearchView, - private fileMatch: FileMatch, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IKeybindingService keyBindingService: IKeybindingService, - ) { - super(Constants.ReplaceAllInFileActionId, appendKeyBindingLabel(ReplaceAllAction.LABEL, keyBindingService.lookupKeybinding(Constants.ReplaceAllInFileActionId), keyBindingService), ThemeIcon.asClassName(searchReplaceAllIcon)); - this.replaceRunner = this.instantiationService.createInstance(ReplaceActionRunner, viewlet.getControl(), viewlet); - } - - override async run(): Promise { - return this.replaceRunner.performReplace(this.fileMatch); - } -} - -export class ReplaceAllInFolderAction extends Action { - - static readonly LABEL = nls.localize('file.replaceAll.label', "Replace All"); - private replaceRunner: ReplaceActionRunner; - - constructor(viewer: WorkbenchCompressibleObjectTree, private folderMatch: FolderMatch, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IKeybindingService keyBindingService: IKeybindingService - ) { - super(Constants.ReplaceAllInFolderActionId, appendKeyBindingLabel(ReplaceAllInFolderAction.LABEL, keyBindingService.lookupKeybinding(Constants.ReplaceAllInFolderActionId), keyBindingService), ThemeIcon.asClassName(searchReplaceAllIcon)); - this.replaceRunner = this.instantiationService.createInstance(ReplaceActionRunner, viewer, undefined); - } - - override run(): Promise { - return this.replaceRunner.performReplace(this.folderMatch); - } -} - -export const copyPathCommand: ICommandHandler = async (accessor, fileMatch: FileMatch | FolderMatchWithResource | undefined) => { - if (!fileMatch) { - const selection = getSelectedRow(accessor); - if (!(selection instanceof FileMatch || selection instanceof FolderMatchWithResource)) { - return; - } - - fileMatch = selection; - } - - const clipboardService = accessor.get(IClipboardService); - const labelService = accessor.get(ILabelService); - - const text = labelService.getUriLabel(fileMatch.resource, { noPrefix: true }); - await clipboardService.writeText(text); -}; - -function matchToString(match: Match, indent = 0): string { - const getFirstLinePrefix = () => `${match.range().startLineNumber},${match.range().startColumn}`; - const getOtherLinePrefix = (i: number) => match.range().startLineNumber + i + ''; - - const fullMatchLines = match.fullPreviewLines(); - const largestPrefixSize = fullMatchLines.reduce((largest, _, i) => { - const thisSize = i === 0 ? - getFirstLinePrefix().length : - getOtherLinePrefix(i).length; - - return Math.max(thisSize, largest); - }, 0); - - const formattedLines = fullMatchLines - .map((line, i) => { - const prefix = i === 0 ? - getFirstLinePrefix() : - getOtherLinePrefix(i); - - const paddingStr = ' '.repeat(largestPrefixSize - prefix.length); - const indentStr = ' '.repeat(indent); - return `${indentStr}${prefix}: ${paddingStr}${line}`; - }); - - return formattedLines.join('\n'); -} -function fileFolderMatchToString(match: FileMatch | FolderMatch | FolderMatchWithResource, labelService: ILabelService): { text: string; count: number } { - if (match instanceof FileMatch) { - return fileMatchToString(match, labelService); - } else { - return folderMatchToString(match, labelService); - } -} -const lineDelimiter = isWindows ? '\r\n' : '\n'; -function fileMatchToString(fileMatch: FileMatch, labelService: ILabelService): { text: string; count: number } { - const matchTextRows = fileMatch.matches() - .sort(searchMatchComparer) - .map(match => matchToString(match, 2)); - const uriString = labelService.getUriLabel(fileMatch.resource, { noPrefix: true }); - return { - text: `${uriString}${lineDelimiter}${matchTextRows.join(lineDelimiter)}`, - count: matchTextRows.length - }; -} - -function folderMatchToString(folderMatch: FolderMatchWithResource | FolderMatch, labelService: ILabelService): { text: string; count: number } { - const results: string[] = []; - let numMatches = 0; - - const matches = folderMatch.matches().sort(searchMatchComparer); - - matches.forEach(match => { - const result = fileFolderMatchToString(match, labelService); - numMatches += result.count; - results.push(result.text); - }); - - return { - text: results.join(lineDelimiter + lineDelimiter), - count: numMatches - }; -} - -export const copyMatchCommand: ICommandHandler = async (accessor, match: RenderableMatch | undefined) => { - if (!match) { - const selection = getSelectedRow(accessor); - if (!selection) { - return; - } - - match = selection; - } - - const clipboardService = accessor.get(IClipboardService); - const labelService = accessor.get(ILabelService); - - let text: string | undefined; - if (match instanceof Match) { - text = matchToString(match); - } else if (match instanceof FileMatch) { - text = fileMatchToString(match, labelService).text; - } else if (match instanceof FolderMatch) { - text = folderMatchToString(match, labelService).text; - } - - if (text) { - await clipboardService.writeText(text); - } -}; - -function allFolderMatchesToString(folderMatches: Array, labelService: ILabelService): string { - const folderResults: string[] = []; - folderMatches = folderMatches.sort(searchMatchComparer); - for (let i = 0; i < folderMatches.length; i++) { - const folderResult = folderMatchToString(folderMatches[i], labelService); - if (folderResult.count) { - folderResults.push(folderResult.text); - } - } - - return folderResults.join(lineDelimiter + lineDelimiter); -} - -function getSelectedRow(accessor: ServicesAccessor): RenderableMatch | undefined | null { - const viewsService = accessor.get(IViewsService); - const searchView = getSearchView(viewsService); - return searchView?.getControl().getSelection()[0]; -} - -export const copyAllCommand: ICommandHandler = async (accessor) => { - const viewsService = accessor.get(IViewsService); - const clipboardService = accessor.get(IClipboardService); - const labelService = accessor.get(ILabelService); - - const searchView = getSearchView(viewsService); - if (searchView) { - const root = searchView.searchResult; - - const text = allFolderMatchesToString(root.folderMatches(), labelService); - await clipboardService.writeText(text); - } -}; - -export const clearHistoryCommand: ICommandHandler = accessor => { - const searchHistoryService = accessor.get(ISearchHistoryService); - searchHistoryService.clearHistory(); -}; - -export const focusSearchListCommand: ICommandHandler = accessor => { - const viewsService = accessor.get(IViewsService); - openSearchView(viewsService).then(searchView => { - searchView?.moveFocusToResults(); - }); -}; - -export function getMultiSelectedSearchResources(viewer: WorkbenchCompressibleObjectTree, currElement: RenderableMatch | undefined, sortConfig: ISearchConfigurationProperties): URI[] { - return getElementsToOperateOnInfo(viewer, currElement, sortConfig).elements - .map((renderableMatch) => ((renderableMatch instanceof Match) ? null : renderableMatch.resource)) - .filter((renderableMatch): renderableMatch is URI => (renderableMatch !== null)); -} - -function getElementsToOperateOnInfo(viewer: WorkbenchCompressibleObjectTree, currElement: RenderableMatch | undefined, sortConfig: ISearchConfigurationProperties): { elements: RenderableMatch[]; mustReselect: boolean } { - let elements: RenderableMatch[] = viewer.getSelection().filter((x): x is RenderableMatch => x !== null).sort((a, b) => searchComparer(a, b, sortConfig.sortOrder)); - - const mustReselect = !currElement || elements.includes(currElement); // this indicates whether we need to re-focus/re-select on a remove. - - // if selection doesn't include multiple elements, just return current focus element. - if (currElement && !(elements.length > 1 && elements.includes(currElement))) { - elements = [currElement]; - } - - return { elements, mustReselect }; -} - - -function compareLevels(elem1: RenderableMatch, elem2: RenderableMatch) { - if (elem1 instanceof Match) { - if (elem2 instanceof Match) { - return 0; - } else { - return -1; - } - - } else if (elem1 instanceof FileMatch) { - if (elem2 instanceof Match) { - return 1; - } else if (elem2 instanceof FileMatch) { - return 0; - } else { - return -1; - } - - } else { - // FolderMatch - if (elem2 instanceof FolderMatch) { - return 0; - } else { - return 1; - } - } -} - -/** - * Returns element to focus after removing the given element - */ -export function getElementToFocusAfterRemoved(viewer: WorkbenchCompressibleObjectTree, element: RenderableMatch, elementsToRemove: RenderableMatch[]): RenderableMatch | undefined { - const navigator: ITreeNavigator = viewer.navigate(element); - if (element instanceof FolderMatch) { - while (!!navigator.next() && (!(navigator.current() instanceof FolderMatch) || arrayContainsElementOrParent(navigator.current(), elementsToRemove))) { } - } else if (element instanceof FileMatch) { - while (!!navigator.next() && (!(navigator.current() instanceof FileMatch) || arrayContainsElementOrParent(navigator.current(), elementsToRemove))) { - viewer.expand(navigator.current()); - } - } else { - while (navigator.next() && (!(navigator.current() instanceof Match) || arrayContainsElementOrParent(navigator.current(), elementsToRemove))) { - viewer.expand(navigator.current()); - } - } - return navigator.current(); -} - -/*** - * Finds the last element in the tree with the same type as `element` - */ -export function getLastNodeFromSameType(viewer: WorkbenchCompressibleObjectTree, element: RenderableMatch): RenderableMatch | undefined { - let lastElem: RenderableMatch | null = viewer.lastVisibleElement ?? null; - - while (lastElem) { - const compareVal = compareLevels(element, lastElem); - if (compareVal === -1) { - viewer.expand(lastElem); - lastElem = viewer.lastVisibleElement; - } else if (compareVal === 1) { - lastElem = viewer.getParentElement(lastElem); - } else { - return lastElem; - } - } - - return undefined; -} diff --git a/src/vs/workbench/contrib/search/browser/searchActionsBase.ts b/src/vs/workbench/contrib/search/browser/searchActionsBase.ts new file mode 100644 index 00000000000..932b714e3ce --- /dev/null +++ b/src/vs/workbench/contrib/search/browser/searchActionsBase.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from 'vs/base/browser/dom'; +import { createKeybinding, ResolvedKeybinding } from 'vs/base/common/keybindings'; +import { OS } from 'vs/base/common/platform'; +import * as nls from 'vs/nls'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listService'; +import { IViewsService } from 'vs/workbench/common/views'; +import { SearchView } from 'vs/workbench/contrib/search/browser/searchView'; +import { RenderableMatch, searchComparer } from 'vs/workbench/contrib/search/common/searchModel'; +import { ISearchConfigurationProperties, VIEW_ID } from 'vs/workbench/services/search/common/search'; + +export const category = { value: nls.localize('search', "Search"), original: 'Search' }; + +export function isSearchViewFocused(viewsService: IViewsService): boolean { + const searchView = getSearchView(viewsService); + const activeElement = document.activeElement; + return !!(searchView && activeElement && DOM.isAncestor(activeElement, searchView.getContainer())); +} + +export function appendKeyBindingLabel(label: string, inputKeyBinding: number | ResolvedKeybinding | undefined, keyBindingService2: IKeybindingService): string { + if (typeof inputKeyBinding === 'number') { + const keybinding = createKeybinding(inputKeyBinding, OS); + if (keybinding) { + const resolvedKeybindings = keyBindingService2.resolveKeybinding(keybinding); + return doAppendKeyBindingLabel(label, resolvedKeybindings.length > 0 ? resolvedKeybindings[0] : undefined); + } + return doAppendKeyBindingLabel(label, undefined); + } else { + return doAppendKeyBindingLabel(label, inputKeyBinding); + } +} + +export function getSearchView(viewsService: IViewsService): SearchView | undefined { + return viewsService.getActiveViewWithId(VIEW_ID) as SearchView; +} + +export function getElementsToOperateOnInfo(viewer: WorkbenchCompressibleObjectTree, currElement: RenderableMatch | undefined, sortConfig: ISearchConfigurationProperties): { elements: RenderableMatch[]; mustReselect: boolean } { + let elements: RenderableMatch[] = viewer.getSelection().filter((x): x is RenderableMatch => x !== null).sort((a, b) => searchComparer(a, b, sortConfig.sortOrder)); + + const mustReselect = !currElement || elements.includes(currElement); // this indicates whether we need to re-focus/re-select on a remove. + + // if selection doesn't include multiple elements, just return current focus element. + if (currElement && !(elements.length > 1 && elements.includes(currElement))) { + elements = [currElement]; + } + + return { elements, mustReselect }; +} + +export function openSearchView(viewsService: IViewsService, focus?: boolean): Promise { + return viewsService.openView(VIEW_ID, focus).then(view => (view as SearchView ?? undefined)); +} + +function doAppendKeyBindingLabel(label: string, keyBinding: ResolvedKeybinding | undefined): string { + return keyBinding ? label + ' (' + keyBinding.getLabel() + ')' : label; +} + diff --git a/src/vs/workbench/contrib/search/browser/searchActionsCopy.ts b/src/vs/workbench/contrib/search/browser/searchActionsCopy.ts new file mode 100644 index 00000000000..58db3b759d5 --- /dev/null +++ b/src/vs/workbench/contrib/search/browser/searchActionsCopy.ts @@ -0,0 +1,256 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import * as nls from 'vs/nls'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IViewsService } from 'vs/workbench/common/views'; +import * as Constants from 'vs/workbench/contrib/search/common/constants'; +import { FileMatch, FolderMatch, FolderMatchWithResource, Match, RenderableMatch, searchMatchComparer } from 'vs/workbench/contrib/search/common/searchModel'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { category, getSearchView } from 'vs/workbench/contrib/search/browser/searchActionsBase'; +import { isWindows } from 'vs/base/common/platform'; + +//#region Actions +registerAction2(class CopyMatchCommandAction extends Action2 { + + constructor( + ) { + super({ + id: Constants.CopyMatchCommandId, + title: { + value: nls.localize('copyMatchLabel', "Copy"), + original: 'Copy' + }, + category: category.value, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: Constants.FileMatchOrMatchFocusKey, + primary: KeyMod.CtrlCmd | KeyCode.KeyC, + }, + menu: [{ + id: MenuId.SearchContext, + when: Constants.FileMatchOrMatchFocusKey, + group: 'search_2', + order: 1 + }] + }); + + } + + override async run(accessor: ServicesAccessor, match: RenderableMatch | undefined): Promise { + await copyMatchCommand(accessor, match); + } +}); + +registerAction2(class CopyPathCommandAction extends Action2 { + + constructor( + ) { + super({ + id: Constants.CopyPathCommandId, + title: { + value: nls.localize('copyPathLabel', "Copy Path"), + original: 'Copy Path' + }, + category: category.value, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: Constants.FileMatchOrFolderMatchWithResourceFocusKey, + primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyC, + win: { + primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KeyC + }, + }, + menu: [{ + id: MenuId.SearchContext, + when: Constants.FileMatchOrFolderMatchWithResourceFocusKey, + group: 'search_2', + order: 2 + }] + }); + + } + + override async run(accessor: ServicesAccessor, fileMatch: FileMatch | FolderMatchWithResource | undefined): Promise { + await copyPathCommand(accessor, fileMatch); + } +}); + +registerAction2(class CopyAllCommandAction extends Action2 { + + constructor( + ) { + super({ + id: Constants.CopyAllCommandId, + title: { + value: nls.localize('copyAllLabel', "Copy All"), + original: 'Copy All' + }, + category: category.value, + menu: [{ + id: MenuId.SearchContext, + when: Constants.HasSearchResults, + group: 'search_2', + order: 3 + }] + }); + + } + + override async run(accessor: ServicesAccessor): Promise { + await copyAllCommand(accessor); + } +}); + +//#endregion + +//#region Helpers +export const lineDelimiter = isWindows ? '\r\n' : '\n'; + +async function copyPathCommand(accessor: ServicesAccessor, fileMatch: FileMatch | FolderMatchWithResource | undefined) { + if (!fileMatch) { + const selection = getSelectedRow(accessor); + if (!(selection instanceof FileMatch || selection instanceof FolderMatchWithResource)) { + return; + } + + fileMatch = selection; + } + + const clipboardService = accessor.get(IClipboardService); + const labelService = accessor.get(ILabelService); + + const text = labelService.getUriLabel(fileMatch.resource, { noPrefix: true }); + await clipboardService.writeText(text); +} + +async function copyMatchCommand(accessor: ServicesAccessor, match: RenderableMatch | undefined) { + if (!match) { + const selection = getSelectedRow(accessor); + if (!selection) { + return; + } + + match = selection; + } + + const clipboardService = accessor.get(IClipboardService); + const labelService = accessor.get(ILabelService); + + let text: string | undefined; + if (match instanceof Match) { + text = matchToString(match); + } else if (match instanceof FileMatch) { + text = fileMatchToString(match, labelService).text; + } else if (match instanceof FolderMatch) { + text = folderMatchToString(match, labelService).text; + } + + if (text) { + await clipboardService.writeText(text); + } +} + +async function copyAllCommand(accessor: ServicesAccessor) { + const viewsService = accessor.get(IViewsService); + const clipboardService = accessor.get(IClipboardService); + const labelService = accessor.get(ILabelService); + + const searchView = getSearchView(viewsService); + if (searchView) { + const root = searchView.searchResult; + + const text = allFolderMatchesToString(root.folderMatches(), labelService); + await clipboardService.writeText(text); + } +} + +function matchToString(match: Match, indent = 0): string { + const getFirstLinePrefix = () => `${match.range().startLineNumber},${match.range().startColumn}`; + const getOtherLinePrefix = (i: number) => match.range().startLineNumber + i + ''; + + const fullMatchLines = match.fullPreviewLines(); + const largestPrefixSize = fullMatchLines.reduce((largest, _, i) => { + const thisSize = i === 0 ? + getFirstLinePrefix().length : + getOtherLinePrefix(i).length; + + return Math.max(thisSize, largest); + }, 0); + + const formattedLines = fullMatchLines + .map((line, i) => { + const prefix = i === 0 ? + getFirstLinePrefix() : + getOtherLinePrefix(i); + + const paddingStr = ' '.repeat(largestPrefixSize - prefix.length); + const indentStr = ' '.repeat(indent); + return `${indentStr}${prefix}: ${paddingStr}${line}`; + }); + + return formattedLines.join('\n'); +} + +function fileFolderMatchToString(match: FileMatch | FolderMatch | FolderMatchWithResource, labelService: ILabelService): { text: string; count: number } { + if (match instanceof FileMatch) { + return fileMatchToString(match, labelService); + } else { + return folderMatchToString(match, labelService); + } +} + +function fileMatchToString(fileMatch: FileMatch, labelService: ILabelService): { text: string; count: number } { + const matchTextRows = fileMatch.matches() + .sort(searchMatchComparer) + .map(match => matchToString(match, 2)); + const uriString = labelService.getUriLabel(fileMatch.resource, { noPrefix: true }); + return { + text: `${uriString}${lineDelimiter}${matchTextRows.join(lineDelimiter)}`, + count: matchTextRows.length + }; +} + +function folderMatchToString(folderMatch: FolderMatchWithResource | FolderMatch, labelService: ILabelService): { text: string; count: number } { + const results: string[] = []; + let numMatches = 0; + + const matches = folderMatch.matches().sort(searchMatchComparer); + + matches.forEach(match => { + const result = fileFolderMatchToString(match, labelService); + numMatches += result.count; + results.push(result.text); + }); + + return { + text: results.join(lineDelimiter + lineDelimiter), + count: numMatches + }; +} + +function allFolderMatchesToString(folderMatches: Array, labelService: ILabelService): string { + const folderResults: string[] = []; + folderMatches = folderMatches.sort(searchMatchComparer); + for (let i = 0; i < folderMatches.length; i++) { + const folderResult = folderMatchToString(folderMatches[i], labelService); + if (folderResult.count) { + folderResults.push(folderResult.text); + } + } + + return folderResults.join(lineDelimiter + lineDelimiter); +} + +function getSelectedRow(accessor: ServicesAccessor): RenderableMatch | undefined | null { + const viewsService = accessor.get(IViewsService); + const searchView = getSearchView(viewsService); + return searchView?.getControl().getSelection()[0]; +} + +//#endregion diff --git a/src/vs/workbench/contrib/search/browser/searchActionsFind.ts b/src/vs/workbench/contrib/search/browser/searchActionsFind.ts new file mode 100644 index 00000000000..2d8cf8d9929 --- /dev/null +++ b/src/vs/workbench/contrib/search/browser/searchActionsFind.ts @@ -0,0 +1,384 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { dirname } from 'vs/base/common/resources'; +import * as nls from 'vs/nls'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IListService, WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listService'; +import { IViewsService, ViewContainerLocation } from 'vs/workbench/common/views'; +import * as Constants from 'vs/workbench/contrib/search/common/constants'; +import * as SearchEditorConstants from 'vs/workbench/contrib/searchEditor/browser/constants'; +import { FileMatch, FolderMatchWithResource, Match, RenderableMatch } from 'vs/workbench/contrib/search/common/searchModel'; +import { OpenSearchEditorArgs } from 'vs/workbench/contrib/searchEditor/browser/searchEditor.contribution'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ISearchConfiguration, ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search'; +import { URI } from 'vs/base/common/uri'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { resolveResourcesForSearchIncludes } from 'vs/workbench/services/search/common/queryBuilder'; +import { getMultiSelectedResources, IExplorerService } from 'vs/workbench/contrib/files/browser/files'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { ExplorerFolderContext, ExplorerRootContext, FilesExplorerFocusCondition, VIEWLET_ID as VIEWLET_ID_FILES } from 'vs/workbench/contrib/files/common/files'; +import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; +import { ExplorerViewPaneContainer } from 'vs/workbench/contrib/files/browser/explorerViewlet'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { category, getElementsToOperateOnInfo, getSearchView, openSearchView } from 'vs/workbench/contrib/search/browser/searchActionsBase'; + + +//#region Interfaces +export interface IFindInFilesArgs { + query?: string; + replace?: string; + preserveCase?: boolean; + triggerSearch?: boolean; + filesToInclude?: string; + filesToExclude?: string; + isRegex?: boolean; + isCaseSensitive?: boolean; + matchWholeWord?: boolean; + useExcludeSettingsAndIgnoreFiles?: boolean; + onlyOpenEditors?: boolean; +} +//#endregion + +registerAction2(class RestrictSearchToFolderAction extends Action2 { + constructor() { + super({ + id: Constants.RestrictSearchToFolderId, + title: { + value: nls.localize('restrictResultsToFolder', "Restrict Search to Folder"), + original: 'Restrict Search to Folder' + }, + category, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ResourceFolderFocusKey), + primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KeyF, + }, + menu: [ + { + id: MenuId.SearchContext, + group: 'search', + order: 3, + when: ContextKeyExpr.and(Constants.ResourceFolderFocusKey) + } + ] + }); + } + async run(accessor: ServicesAccessor, folderMatch?: FolderMatchWithResource) { + await searchWithFolderCommand(accessor, false, true, undefined, folderMatch); + } +}); + +registerAction2(class ExcludeFolderFromSearchAction extends Action2 { + constructor() { + super({ + id: Constants.ExcludeFolderFromSearchId, + title: { + value: nls.localize('excludeFolderFromSearch', "Exclude Folder from Search"), + original: 'Exclude Folder from Search' + }, + category, + menu: [ + { + id: MenuId.SearchContext, + group: 'search', + order: 4, + when: ContextKeyExpr.and(Constants.ResourceFolderFocusKey) + } + ] + }); + } + async run(accessor: ServicesAccessor, folderMatch?: FolderMatchWithResource) { + await searchWithFolderCommand(accessor, false, false, undefined, folderMatch); + } +}); + +registerAction2(class RevealInSideBarForSearchResultsAction extends Action2 { + + constructor( + ) { + super({ + id: Constants.RevealInSideBarForSearchResults, + title: { + value: nls.localize('revealInSideBar', "Reveal in Explorer View"), + original: 'Reveal in Explorer View' + }, + category: category, + menu: [{ + id: MenuId.SearchContext, + when: ContextKeyExpr.and(Constants.FileFocusKey, Constants.HasSearchResults), + group: 'search_3', + order: 1 + }] + }); + + } + + override async run(accessor: ServicesAccessor, args: any): Promise { + const paneCompositeService = accessor.get(IPaneCompositePartService); + const explorerService = accessor.get(IExplorerService); + const contextService = accessor.get(IWorkspaceContextService); + + const searchView = getSearchView(accessor.get(IViewsService)); + if (!searchView) { + return; + } + + let fileMatch: FileMatch; + if (!(args instanceof FileMatch)) { + args = searchView.getControl().getFocus()[0]; + } + if (args instanceof FileMatch) { + fileMatch = args; + } else { + return; + } + + paneCompositeService.openPaneComposite(VIEWLET_ID_FILES, ViewContainerLocation.Sidebar, false).then((viewlet) => { + if (!viewlet) { + return; + } + + const explorerViewContainer = viewlet.getViewPaneContainer() as ExplorerViewPaneContainer; + const uri = fileMatch.resource; + if (uri && contextService.isInsideWorkspace(uri)) { + const explorerView = explorerViewContainer.getExplorerView(); + explorerView.setExpanded(true); + explorerService.select(uri, true).then(() => explorerView.focus(), onUnexpectedError); + } + }); + } +}); + +// Find in Files by default is the same as View: Show Search, but can be configured to open a search editor instead with the `search.mode` binding +registerAction2(class FindInFilesAction extends Action2 { + + constructor( + ) { + super({ + id: Constants.FindInFilesActionId, + title: { + value: nls.localize('findInFiles', "Find in Files"), + mnemonicTitle: nls.localize({ key: 'miFindInFiles', comment: ['&& denotes a mnemonic'] }, "Find &&in Files"), + original: 'Find in Files' + }, + description: { + description: nls.localize('findInFiles.description', "Open a workspace search"), + args: [ + { + name: nls.localize('findInFiles.args', "A set of options for the search"), + schema: { + type: 'object', + properties: { + query: { 'type': 'string' }, + replace: { 'type': 'string' }, + preserveCase: { 'type': 'boolean' }, + triggerSearch: { 'type': 'boolean' }, + filesToInclude: { 'type': 'string' }, + filesToExclude: { 'type': 'string' }, + isRegex: { 'type': 'boolean' }, + isCaseSensitive: { 'type': 'boolean' }, + matchWholeWord: { 'type': 'boolean' }, + useExcludeSettingsAndIgnoreFiles: { 'type': 'boolean' }, + onlyOpenEditors: { 'type': 'boolean' }, + } + } + }, + ] + }, + category: category, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyF, + }, + menu: [{ + id: MenuId.MenubarEditMenu, + group: '4_find_global', + order: 1, + }], + f1: true + }); + + } + + override async run(accessor: ServicesAccessor, args: IFindInFilesArgs = {}): Promise { + findInFilesCommand(accessor, args); + } +}); + +registerAction2(class FindInFolderAction extends Action2 { + // from explorer + constructor() { + super({ + id: Constants.FindInFolderId, + title: { + value: nls.localize('findInFolder', "Find in Folder..."), + original: 'Find in Folder...' + }, + category, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerFolderContext), + primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KeyF, + }, + menu: [ + { + id: MenuId.ExplorerContext, + group: '4_search', + order: 10, + when: ContextKeyExpr.and(ExplorerFolderContext) + } + ] + }); + } + async run(accessor: ServicesAccessor, resource?: URI) { + await searchWithFolderCommand(accessor, true, true, resource); + } +}); + +registerAction2(class FindInWorkspaceAction extends Action2 { + // from explorer + constructor() { + super({ + id: Constants.FindInWorkspaceId, + title: { + value: nls.localize('findInWorkspace', "Find in Workspace..."), + original: 'Find in Workspace...' + }, + category, + menu: [ + { + id: MenuId.ExplorerContext, + group: '4_search', + order: 10, + when: ContextKeyExpr.and(ExplorerRootContext, ExplorerFolderContext.toNegated()) + + } + ] + }); + } + async run(accessor: ServicesAccessor) { + const searchConfig = accessor.get(IConfigurationService).getValue().search; + const mode = searchConfig.mode; + + if (mode === 'view') { + const searchView = await openSearchView(accessor.get(IViewsService), true); + searchView?.searchInFolders(); + } + else { + return accessor.get(ICommandService).executeCommand(SearchEditorConstants.OpenEditorCommandId, { + location: mode === 'newEditor' ? 'new' : 'reuse', + filesToInclude: '', + }); + } + } +}); + +//#region Helpers +async function searchWithFolderCommand(accessor: ServicesAccessor, isFromExplorer: boolean, isIncludes: boolean, resource?: URI, folderMatch?: FolderMatchWithResource) { + const listService = accessor.get(IListService); + const fileService = accessor.get(IFileService); + const viewsService = accessor.get(IViewsService); + const contextService = accessor.get(IWorkspaceContextService); + const commandService = accessor.get(ICommandService); + const searchConfig = accessor.get(IConfigurationService).getValue().search; + const mode = searchConfig.mode; + + let resources: URI[]; + + if (isFromExplorer) { + resources = getMultiSelectedResources(resource, listService, accessor.get(IEditorService), accessor.get(IExplorerService)); + } else { + const searchView = getSearchView(accessor.get(IViewsService)); + if (!searchView) { + return; + } + resources = getMultiSelectedSearchResources(searchView.getControl(), folderMatch, searchConfig); + } + + const resolvedResources = fileService.resolveAll(resources.map(resource => ({ resource }))).then(results => { + const folders: URI[] = []; + results.forEach(result => { + if (result.success && result.stat) { + folders.push(result.stat.isDirectory ? result.stat.resource : dirname(result.stat.resource)); + } + }); + return resolveResourcesForSearchIncludes(folders, contextService); + }); + + if (mode === 'view') { + const searchView = await openSearchView(viewsService, true); + if (resources && resources.length && searchView) { + if (isIncludes) { + searchView.searchInFolders(await resolvedResources); + } else { + searchView.searchOutsideOfFolders(await resolvedResources); + } + } + return undefined; + } else { + if (isIncludes) { + return commandService.executeCommand(SearchEditorConstants.OpenEditorCommandId, { + filesToInclude: (await resolvedResources).join(', '), + showIncludesExcludes: true, + location: mode === 'newEditor' ? 'new' : 'reuse', + }); + } + else { + return commandService.executeCommand(SearchEditorConstants.OpenEditorCommandId, { + filesToExclude: (await resolvedResources).join(', '), + showIncludesExcludes: true, + location: mode === 'newEditor' ? 'new' : 'reuse', + }); + } + } +} + +function getMultiSelectedSearchResources(viewer: WorkbenchCompressibleObjectTree, currElement: RenderableMatch | undefined, sortConfig: ISearchConfigurationProperties): URI[] { + return getElementsToOperateOnInfo(viewer, currElement, sortConfig).elements + .map((renderableMatch) => ((renderableMatch instanceof Match) ? null : renderableMatch.resource)) + .filter((renderableMatch): renderableMatch is URI => (renderableMatch !== null)); +} + +export function findInFilesCommand(accessor: ServicesAccessor, args: IFindInFilesArgs = {}) { + const searchConfig = accessor.get(IConfigurationService).getValue().search; + const mode = searchConfig.mode; + if (mode === 'view') { + const viewsService = accessor.get(IViewsService); + openSearchView(viewsService, false).then(openedView => { + if (openedView) { + const searchAndReplaceWidget = openedView.searchAndReplaceWidget; + searchAndReplaceWidget.toggleReplace(typeof args.replace === 'string'); + let updatedText = false; + if (typeof args.query === 'string') { + openedView.setSearchParameters(args); + } else { + updatedText = openedView.updateTextFromFindWidgetOrSelection({ allowUnselectedWord: typeof args.replace !== 'string' }); + } + openedView.searchAndReplaceWidget.focus(undefined, updatedText, updatedText); + } + }); + } else { + const convertArgs = (args: IFindInFilesArgs): OpenSearchEditorArgs => ({ + location: mode === 'newEditor' ? 'new' : 'reuse', + query: args.query, + filesToInclude: args.filesToInclude, + filesToExclude: args.filesToExclude, + matchWholeWord: args.matchWholeWord, + isCaseSensitive: args.isCaseSensitive, + isRegexp: args.isRegex, + useExcludeSettingsAndIgnoreFiles: args.useExcludeSettingsAndIgnoreFiles, + onlyOpenEditors: args.onlyOpenEditors, + showIncludesExcludes: !!(args.filesToExclude || args.filesToExclude || !args.useExcludeSettingsAndIgnoreFiles), + }); + accessor.get(ICommandService).executeCommand(SearchEditorConstants.OpenEditorCommandId, convertArgs(args)); + } +} +//#endregion diff --git a/src/vs/workbench/contrib/search/browser/searchActionsNav.ts b/src/vs/workbench/contrib/search/browser/searchActionsNav.ts new file mode 100644 index 00000000000..77a6b79b1f1 --- /dev/null +++ b/src/vs/workbench/contrib/search/browser/searchActionsNav.ts @@ -0,0 +1,534 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { isMacintosh } from 'vs/base/common/platform'; +import * as nls from 'vs/nls'; +import { ICommandHandler } from 'vs/platform/commands/common/commands'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listService'; +import { IViewsService } from 'vs/workbench/common/views'; +import * as Constants from 'vs/workbench/contrib/search/common/constants'; +import * as SearchEditorConstants from 'vs/workbench/contrib/searchEditor/browser/constants'; +import { FileMatchOrMatch, FolderMatch, RenderableMatch } from 'vs/workbench/contrib/search/common/searchModel'; +import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditor'; +import { SearchEditorInput } from 'vs/workbench/contrib/searchEditor/browser/searchEditorInput'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { assertIsDefined } from 'vs/base/common/types'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { ToggleCaseSensitiveKeybinding, TogglePreserveCaseKeybinding, ToggleRegexKeybinding, ToggleWholeWordKeybinding } from 'vs/editor/contrib/find/browser/findModel'; +import { category, getSearchView, openSearchView } from 'vs/workbench/contrib/search/browser/searchActionsBase'; + +//#region Actions: Changing Search Input Options +registerAction2(class ToggleQueryDetailsAction extends Action2 { + constructor() { + super({ + id: Constants.ToggleQueryDetailsActionId, + title: { + value: nls.localize('ToggleQueryDetailsAction.label', "Toggle Query Details"), + original: 'Toggle Query Details' + }, + category, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.or(Constants.SearchViewFocusedKey, SearchEditorConstants.InSearchEditor), + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyJ, + }, + }); + } + run(accessor: ServicesAccessor) { + const contextService = accessor.get(IContextKeyService).getContext(document.activeElement); + if (contextService.getValue(SearchEditorConstants.InSearchEditor.serialize())) { + (accessor.get(IEditorService).activeEditorPane as SearchEditor).toggleQueryDetails(); + } else if (contextService.getValue(Constants.SearchViewFocusedKey.serialize())) { + const searchView = getSearchView(accessor.get(IViewsService)); + assertIsDefined(searchView).toggleQueryDetails(); + } + } +}); + +registerAction2(class CloseReplaceAction extends Action2 { + constructor() { + super({ + id: Constants.CloseReplaceWidgetActionId, + title: { + value: nls.localize('CloseReplaceWidget.label', "Close Replace Widget"), + original: 'Close Replace Widget' + }, + category, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceInputBoxFocusedKey), + primary: KeyCode.Escape, + }, + }); + } + run(accessor: ServicesAccessor) { + + const searchView = getSearchView(accessor.get(IViewsService)); + if (searchView) { + searchView.searchAndReplaceWidget.toggleReplace(false); + searchView.searchAndReplaceWidget.focus(); + } + return Promise.resolve(null); + } +}); + +registerAction2(class ToggleCaseSensitiveCommandAction extends Action2 { + + constructor( + ) { + + super({ + id: Constants.ToggleCaseSensitiveCommandId, + title: { + value: nls.localize('ToggleCaseSensitiveCommandId.label', "Toggle Case Sensitive"), + original: 'Toggle Case Sensitive' + }, + category: category, + keybinding: Object.assign({ + weight: KeybindingWeight.WorkbenchContrib, + when: isMacintosh ? ContextKeyExpr.and(Constants.SearchViewFocusedKey, Constants.FileMatchOrFolderMatchFocusKey.toNegated()) : Constants.SearchViewFocusedKey, + }, ToggleCaseSensitiveKeybinding) + + }); + + } + + override async run(accessor: ServicesAccessor): Promise { + toggleCaseSensitiveCommand(accessor); + } +}); + +registerAction2(class ToggleWholeWordCommandAction extends Action2 { + constructor() { + super({ + id: Constants.ToggleWholeWordCommandId, + title: { + value: nls.localize('ToggleWholeWordCommandId.label', 'Toggle Whole Word'), + original: 'Toggle Whole Word' + }, + keybinding: Object.assign({ + weight: KeybindingWeight.WorkbenchContrib, + when: Constants.SearchViewFocusedKey, + }, ToggleWholeWordKeybinding), + category: category.value, + }); + } + + override async run(accessor: ServicesAccessor): Promise { + return toggleWholeWordCommand(accessor); + } +}); + +registerAction2(class ToggleRegexCommandAction extends Action2 { + constructor() { + super({ + id: Constants.ToggleRegexCommandId, + title: { + value: nls.localize('ToggleRegexCommandId.label', 'Toggle Regex'), + original: 'Toggle Regex' + }, + keybinding: Object.assign({ + weight: KeybindingWeight.WorkbenchContrib, + when: Constants.SearchViewFocusedKey, + }, ToggleRegexKeybinding), + category: category.value, + }); + } + + override async run(accessor: ServicesAccessor): Promise { + return toggleRegexCommand(accessor); + } +}); + +registerAction2(class TogglePreserveCaseAction extends Action2 { + constructor() { + super({ + id: Constants.TogglePreserveCaseId, + title: { + value: nls.localize('TogglePreserveCaseId.label', 'Toggle Preserve Case'), + original: 'Toggle Preserve Case' + }, + keybinding: Object.assign({ + weight: KeybindingWeight.WorkbenchContrib, + when: Constants.SearchViewFocusedKey, + }, TogglePreserveCaseKeybinding), + category: category.value, + }); + } + + override async run(accessor: ServicesAccessor): Promise { + return togglePreserveCaseCommand(accessor); + } +}); + +//#endregion +//#region Actions: Opening Matches +registerAction2(class OpenMatchAction extends Action2 { + constructor() { + super({ + id: Constants.OpenMatch, + title: { + value: nls.localize('OpenMatch.label', "Open Match"), + original: 'Open Match' + }, + category, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.FileMatchOrMatchFocusKey), + primary: KeyCode.Enter, + mac: { + primary: KeyCode.Enter, + secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow] + }, + }, + }); + } + run(accessor: ServicesAccessor) { + const searchView = getSearchView(accessor.get(IViewsService)); + if (searchView) { + const tree: WorkbenchCompressibleObjectTree = searchView.getControl(); + const viewer = searchView.getControl(); + const focus = tree.getFocus()[0]; + + if (focus instanceof FolderMatch) { + viewer.toggleCollapsed(focus); + } else { + searchView.open(tree.getFocus()[0], false, false, true); + } + } + } +}); + +registerAction2(class OpenMatchToSideAction extends Action2 { + constructor() { + super({ + id: Constants.OpenMatchToSide, + title: { + value: nls.localize('OpenMatchToSide.label', "Open Match To Side"), + original: 'Open Match To Side' + }, + category, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.FileMatchOrMatchFocusKey), + primary: KeyMod.CtrlCmd | KeyCode.Enter, + mac: { + primary: KeyMod.WinCtrl | KeyCode.Enter + }, + }, + }); + } + run(accessor: ServicesAccessor) { + const searchView = getSearchView(accessor.get(IViewsService)); + if (searchView) { + const tree: WorkbenchCompressibleObjectTree = searchView.getControl(); + searchView.open(tree.getFocus()[0], false, true, true); + } + } +}); + +registerAction2(class AddCursorsAtSearchResultsAction extends Action2 { + constructor() { + super({ + id: Constants.AddCursorsAtSearchResults, + title: { + value: nls.localize('AddCursorsAtSearchResults.label', 'Add Cursors at Search Results'), + original: 'Add Cursors at Search Results' + }, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.FileMatchOrMatchFocusKey), + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyL, + }, + category: category.value, + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const searchView = getSearchView(accessor.get(IViewsService)); + if (searchView) { + const tree: WorkbenchCompressibleObjectTree = searchView.getControl(); + searchView.openEditorWithMultiCursor(tree.getFocus()[0]); + } + } +}); + +//#endregion +//#region Actions: Toggling Focus +registerAction2(class FocusNextInputAction extends Action2 { + constructor() { + super({ + id: Constants.FocusNextInputActionId, + title: { + value: nls.localize('FocusNextInputAction.label', "Focus Next Input"), + original: 'Focus Next Input' + }, + category, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.or( + ContextKeyExpr.and(SearchEditorConstants.InSearchEditor, Constants.InputBoxFocusedKey), + ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.InputBoxFocusedKey)), + primary: KeyMod.CtrlCmd | KeyCode.DownArrow, + }, + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + const input = editorService.activeEditor; + if (input instanceof SearchEditorInput) { + // cast as we cannot import SearchEditor as a value b/c cyclic dependency. + (editorService.activeEditorPane as SearchEditor).focusNextInput(); + } + + const searchView = getSearchView(accessor.get(IViewsService)); + searchView?.focusNextInputBox(); + } +}); + +registerAction2(class FocusPreviousInputAction extends Action2 { + constructor() { + super({ + id: Constants.FocusPreviousInputActionId, + title: { + value: nls.localize('FocusPreviousInputAction.label', "Focus Previous Input"), + original: 'Focus Previous Input' + }, + category, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.or( + ContextKeyExpr.and(SearchEditorConstants.InSearchEditor, Constants.InputBoxFocusedKey), + ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.InputBoxFocusedKey, Constants.SearchInputBoxFocusedKey.toNegated())), + primary: KeyMod.CtrlCmd | KeyCode.UpArrow, + }, + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + const input = editorService.activeEditor; + if (input instanceof SearchEditorInput) { + // cast as we cannot import SearchEditor as a value b/c cyclic dependency. + (editorService.activeEditorPane as SearchEditor).focusPrevInput(); + } + + const searchView = getSearchView(accessor.get(IViewsService)); + searchView?.focusPreviousInputBox(); + } +}); + +registerAction2(class FocusSearchFromResultsAction extends Action2 { + constructor() { + super({ + id: Constants.FocusSearchFromResults, + title: { + value: nls.localize('FocusSearchFromResults.label', "Focus Search From Results"), + original: 'Focus Search From Results' + }, + category, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.FirstMatchFocusKey), + primary: KeyMod.CtrlCmd | KeyCode.UpArrow, + }, + }); + } + run(accessor: ServicesAccessor) { + const searchView = getSearchView(accessor.get(IViewsService)); + searchView?.focusPreviousInputBox(); + } +}); + +registerAction2(class ToggleSearchOnTypeAction extends Action2 { + private static readonly searchOnTypeKey = 'search.searchOnType'; + + constructor( + ) { + super({ + id: Constants.ToggleSearchOnTypeActionId, + title: { + value: nls.localize('toggleTabs', 'Toggle Search on Type'), + original: 'Toggle Search on Type' + }, + category: category.value, + }); + + } + + override async run(accessor: ServicesAccessor): Promise { + const configurationService = accessor.get(IConfigurationService); + const searchOnType = configurationService.getValue(ToggleSearchOnTypeAction.searchOnTypeKey); + return configurationService.updateValue(ToggleSearchOnTypeAction.searchOnTypeKey, !searchOnType); + } +}); + +registerAction2(class FocusSearchListCommandAction extends Action2 { + + constructor( + ) { + super({ + id: Constants.FocusSearchListCommandID, + title: { + value: nls.localize('focusSearchListCommandLabel', "Focus List"), + original: 'Focus List' + }, + category: category, + f1: true + }); + } + + override async run(accessor: ServicesAccessor): Promise { + focusSearchListCommand(accessor); + } +}); + +registerAction2(class FocusNextSearchResultAction extends Action2 { + constructor() { + super({ + id: Constants.FocusNextSearchResultActionId, + title: { + value: nls.localize('FocusNextSearchResult.label', 'Focus Next Search Result'), + original: 'Focus Next Search Result' + }, + keybinding: [{ + primary: KeyCode.F4, + weight: KeybindingWeight.WorkbenchContrib, + }], + category: category, + f1: true, + precondition: ContextKeyExpr.or(Constants.HasSearchResults, SearchEditorConstants.InSearchEditor), + }); + } + + override async run(accessor: ServicesAccessor): Promise { + return await focusNextSearchResult(accessor); + } +}); + +registerAction2(class FocusPreviousSearchResultAction extends Action2 { + constructor() { + super({ + id: Constants.FocusPreviousSearchResultActionId, + title: { + value: nls.localize('FocusPreviousSearchResult.label', 'Focus Previous Search Result'), + original: 'Focus Previous Search Result' + }, + keybinding: [{ + primary: KeyMod.Shift | KeyCode.F4, + weight: KeybindingWeight.WorkbenchContrib, + }], + category: category, + f1: true, + precondition: ContextKeyExpr.or(Constants.HasSearchResults, SearchEditorConstants.InSearchEditor), + }); + } + + override async run(accessor: ServicesAccessor): Promise { + return await focusPreviousSearchResult(accessor); + } +}); + +registerAction2(class ReplaceInFilesAction extends Action2 { + constructor() { + super({ + id: Constants.ReplaceInFilesActionId, + title: { + value: nls.localize('replaceInFiles', 'Replace in Files'), + original: 'Replace in Files' + }, + keybinding: [{ + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyH, + weight: KeybindingWeight.WorkbenchContrib, + }], + category: category, + f1: true, + menu: [{ + id: MenuId.MenubarEditMenu, + group: '4_find_global', + order: 2 + }], + }); + } + + override async run(accessor: ServicesAccessor): Promise { + return await findOrReplaceInFiles(accessor, true); + } +}); + +//#endregion + +//#region Helpers +function toggleCaseSensitiveCommand(accessor: ServicesAccessor) { + const searchView = getSearchView(accessor.get(IViewsService)); + searchView?.toggleCaseSensitive(); +} + +function toggleWholeWordCommand(accessor: ServicesAccessor) { + const searchView = getSearchView(accessor.get(IViewsService)); + searchView?.toggleWholeWords(); +} + +function toggleRegexCommand(accessor: ServicesAccessor) { + const searchView = getSearchView(accessor.get(IViewsService)); + searchView?.toggleRegex(); +} + +function togglePreserveCaseCommand(accessor: ServicesAccessor) { + const searchView = getSearchView(accessor.get(IViewsService)); + searchView?.togglePreserveCase(); +} + +const focusSearchListCommand: ICommandHandler = accessor => { + const viewsService = accessor.get(IViewsService); + openSearchView(viewsService).then(searchView => { + searchView?.moveFocusToResults(); + }); +}; + +async function focusNextSearchResult(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + const input = editorService.activeEditor; + if (input instanceof SearchEditorInput) { + // cast as we cannot import SearchEditor as a value b/c cyclic dependency. + return (editorService.activeEditorPane as SearchEditor).focusNextResult(); + } + + return openSearchView(accessor.get(IViewsService)).then(searchView => { + searchView?.selectNextMatch(); + }); +} + +async function focusPreviousSearchResult(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + const input = editorService.activeEditor; + if (input instanceof SearchEditorInput) { + // cast as we cannot import SearchEditor as a value b/c cyclic dependency. + return (editorService.activeEditorPane as SearchEditor).focusPreviousResult(); + } + + return openSearchView(accessor.get(IViewsService)).then(searchView => { + searchView?.selectPreviousMatch(); + }); +} + +async function findOrReplaceInFiles(accessor: ServicesAccessor, expandSearchReplaceWidget: boolean): Promise { + return openSearchView(accessor.get(IViewsService), false).then(openedView => { + if (openedView) { + const searchAndReplaceWidget = openedView.searchAndReplaceWidget; + searchAndReplaceWidget.toggleReplace(expandSearchReplaceWidget); + + const updatedText = openedView.updateTextFromFindWidgetOrSelection({ allowUnselectedWord: !expandSearchReplaceWidget }); + openedView.searchAndReplaceWidget.focus(undefined, updatedText, updatedText); + } + }); +} +//#endregion diff --git a/src/vs/workbench/contrib/search/browser/searchActionsRemoveReplace.ts b/src/vs/workbench/contrib/search/browser/searchActionsRemoveReplace.ts new file mode 100644 index 00000000000..857f868b810 --- /dev/null +++ b/src/vs/workbench/contrib/search/browser/searchActionsRemoveReplace.ts @@ -0,0 +1,408 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ITreeNavigator } from 'vs/base/browser/ui/tree/tree'; +import * as nls from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { getSelectionKeyboardEvent, WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listService'; +import { IViewsService } from 'vs/workbench/common/views'; +import { searchRemoveIcon, searchReplaceIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; +import { SearchView } from 'vs/workbench/contrib/search/browser/searchView'; +import * as Constants from 'vs/workbench/contrib/search/common/constants'; +import { IReplaceService } from 'vs/workbench/contrib/search/common/replace'; +import { arrayContainsElementOrParent, FileMatch, FolderMatch, Match, RenderableMatch, SearchResult } from 'vs/workbench/contrib/search/common/searchModel'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ISearchConfiguration, ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { category, getElementsToOperateOnInfo, getSearchView } from 'vs/workbench/contrib/search/browser/searchActionsBase'; + + +//#region Interfaces +export interface ISearchActionContext { + readonly viewer: WorkbenchCompressibleObjectTree; + readonly element: RenderableMatch; +} + + +export interface IFindInFilesArgs { + query?: string; + replace?: string; + preserveCase?: boolean; + triggerSearch?: boolean; + filesToInclude?: string; + filesToExclude?: string; + isRegex?: boolean; + isCaseSensitive?: boolean; + matchWholeWord?: boolean; + useExcludeSettingsAndIgnoreFiles?: boolean; + onlyOpenEditors?: boolean; +} + +//#endregion + +//#region Actions +registerAction2(class RemoveAction extends Action2 { + + constructor( + ) { + super({ + id: Constants.RemoveActionId, + title: { + value: nls.localize('RemoveAction.label', "Dismiss"), + original: 'Dismiss' + }, + category, + icon: searchRemoveIcon, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.FileMatchOrMatchFocusKey), + primary: KeyCode.Delete, + mac: { + primary: KeyMod.CtrlCmd | KeyCode.Backspace, + }, + }, + menu: [ + { + id: MenuId.SearchContext, + group: 'search', + order: 2, + }, + { + id: MenuId.SearchActionMenu, + group: 'inline', + order: 2, + }, + ] + }); + } + + run(accessor: ServicesAccessor, context: ISearchActionContext | undefined): void { + const viewsService = accessor.get(IViewsService); + const configurationService = accessor.get(IConfigurationService); + const searchView = getSearchView(viewsService); + + if (!searchView) { + return; + } + + let element = context?.element; + let viewer = context?.viewer; + if (!viewer) { + viewer = searchView.getControl(); + } + if (!element) { + element = viewer.getFocus()[0] ?? undefined; + } + + const opInfo = getElementsToOperateOnInfo(viewer, element, configurationService.getValue('search')); + const elementsToRemove = opInfo.elements; + let focusElement = viewer.getFocus()[0] ?? undefined; + + if (elementsToRemove.length === 0) { + return; + } + + if (!focusElement || (focusElement instanceof SearchResult)) { + focusElement = element; + } + + let nextFocusElement; + if (opInfo.mustReselect && focusElement) { + nextFocusElement = getElementToFocusAfterRemoved(viewer, focusElement, elementsToRemove); + } + + const searchResult = searchView.searchResult; + + if (searchResult) { + searchResult.batchRemove(elementsToRemove); + } + + if (opInfo.mustReselect && focusElement) { + if (!nextFocusElement) { + nextFocusElement = getLastNodeFromSameType(viewer, focusElement); + } + + if (nextFocusElement && !arrayContainsElementOrParent(nextFocusElement, elementsToRemove)) { + viewer.reveal(nextFocusElement); + viewer.setFocus([nextFocusElement], getSelectionKeyboardEvent()); + viewer.setSelection([nextFocusElement], getSelectionKeyboardEvent()); + } + } + + viewer.domFocus(); + return; + } +}); + +registerAction2(class ReplaceAction extends Action2 { + constructor( + ) { + super({ + id: Constants.ReplaceActionId, + title: { + value: nls.localize('match.replace.label', "Replace"), + original: 'Replace' + }, + category, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, Constants.MatchFocusKey), + primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.Digit1, + }, + icon: searchReplaceIcon, + menu: [ + { + id: MenuId.SearchContext, + when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.MatchFocusKey), + group: 'search', + order: 1 + }, + { + id: MenuId.SearchActionMenu, + when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.MatchFocusKey), + group: 'inline', + order: 1 + } + ] + }); + } + + override async run(accessor: ServicesAccessor, context: ISearchActionContext | undefined): Promise { + return performReplace(accessor, context); + } +}); + +registerAction2(class ReplaceAllAction extends Action2 { + + constructor( + ) { + super({ + id: Constants.ReplaceAllInFileActionId, + title: { + value: nls.localize('file.replaceAll.label', "Replace All"), + original: 'Replace All' + }, + category, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, Constants.FileFocusKey), + primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.Digit1, + secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter], + }, + icon: searchReplaceIcon, + menu: [ + { + id: MenuId.SearchContext, + when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.FileFocusKey), + group: 'search', + order: 1 + }, + { + id: MenuId.SearchActionMenu, + when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.FileFocusKey), + group: 'inline', + order: 1 + } + ] + }); + } + + override async run(accessor: ServicesAccessor, context: ISearchActionContext | undefined): Promise { + return performReplace(accessor, context); + } +}); + +registerAction2(class ReplaceAllInFolderAction extends Action2 { + constructor( + ) { + super({ + id: Constants.ReplaceAllInFolderActionId, + title: { + value: nls.localize('file.replaceAll.label', "Replace All"), + original: 'Replace All' + }, + category, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, Constants.ReplaceActiveKey, Constants.FolderFocusKey), + primary: KeyMod.Shift | KeyMod.CtrlCmd | KeyCode.Digit1, + secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter], + }, + icon: searchReplaceIcon, + menu: [ + { + id: MenuId.SearchContext, + when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.FolderFocusKey), + group: 'search', + order: 1 + }, + { + id: MenuId.SearchActionMenu, + when: ContextKeyExpr.and(Constants.ReplaceActiveKey, Constants.FolderFocusKey), + group: 'inline', + order: 1 + } + ] + }); + } + + override async run(accessor: ServicesAccessor, context: ISearchActionContext | undefined): Promise { + return performReplace(accessor, context); + } +}); + +//#endregion + +//#region Helpers + +function performReplace(accessor: ServicesAccessor, + context: ISearchActionContext | undefined): void { + const configurationService = accessor.get(IConfigurationService); + const viewsService = accessor.get(IViewsService); + + const viewlet: SearchView | undefined = getSearchView(viewsService); + const viewer: WorkbenchCompressibleObjectTree | undefined = context?.viewer ?? viewlet?.getControl(); + + if (!viewer) { + return; + } + const element: RenderableMatch | null = context?.element ?? viewer.getFocus()[0]; + + // since multiple elements can be selected, we need to check the type of the FolderMatch/FileMatch/Match before we perform the replace. + const opInfo = getElementsToOperateOnInfo(viewer, element ?? undefined, configurationService.getValue('search')); + const elementsToReplace = opInfo.elements; + let focusElement = viewer.getFocus()[0]; + + if (!focusElement || (focusElement && !arrayContainsElementOrParent(focusElement, elementsToReplace)) || (focusElement instanceof SearchResult)) { + focusElement = element; + } + + if (elementsToReplace.length === 0) { + return; + } + let nextFocusElement; + if (focusElement) { + nextFocusElement = getElementToFocusAfterRemoved(viewer, focusElement, elementsToReplace); + } + + const searchResult = viewlet?.searchResult; + + if (searchResult) { + searchResult.batchReplace(elementsToReplace); + } + + if (focusElement) { + if (!nextFocusElement) { + nextFocusElement = getLastNodeFromSameType(viewer, focusElement); + } + + if (nextFocusElement) { + viewer.reveal(nextFocusElement); + viewer.setFocus([nextFocusElement], getSelectionKeyboardEvent()); + viewer.setSelection([nextFocusElement], getSelectionKeyboardEvent()); + + if (nextFocusElement instanceof Match) { + const useReplacePreview = configurationService.getValue().search.useReplacePreview; + if (!useReplacePreview || hasToOpenFile(accessor, nextFocusElement)) { + viewlet?.open(nextFocusElement, true); + } else { + accessor.get(IReplaceService).openReplacePreview(nextFocusElement, true); + } + } else if (nextFocusElement instanceof FileMatch) { + viewlet?.open(nextFocusElement, true); + } + } + + } + + viewer.domFocus(); +} + +function hasToOpenFile(accessor: ServicesAccessor, currBottomElem: RenderableMatch): boolean { + if (!(currBottomElem instanceof Match)) { + return false; + } + const activeEditor = accessor.get(IEditorService).activeEditor; + const file = activeEditor?.resource; + if (file) { + return accessor.get(IUriIdentityService).extUri.isEqual(file, currBottomElem.parent().resource); + } + return false; +} + +function compareLevels(elem1: RenderableMatch, elem2: RenderableMatch) { + if (elem1 instanceof Match) { + if (elem2 instanceof Match) { + return 0; + } else { + return -1; + } + + } else if (elem1 instanceof FileMatch) { + if (elem2 instanceof Match) { + return 1; + } else if (elem2 instanceof FileMatch) { + return 0; + } else { + return -1; + } + + } else { + // FolderMatch + if (elem2 instanceof FolderMatch) { + return 0; + } else { + return 1; + } + } +} + +/** + * Returns element to focus after removing the given element + */ +export function getElementToFocusAfterRemoved(viewer: WorkbenchCompressibleObjectTree, element: RenderableMatch, elementsToRemove: RenderableMatch[]): RenderableMatch | undefined { + const navigator: ITreeNavigator = viewer.navigate(element); + if (element instanceof FolderMatch) { + while (!!navigator.next() && (!(navigator.current() instanceof FolderMatch) || arrayContainsElementOrParent(navigator.current(), elementsToRemove))) { } + } else if (element instanceof FileMatch) { + while (!!navigator.next() && (!(navigator.current() instanceof FileMatch) || arrayContainsElementOrParent(navigator.current(), elementsToRemove))) { + viewer.expand(navigator.current()); + } + } else { + while (navigator.next() && (!(navigator.current() instanceof Match) || arrayContainsElementOrParent(navigator.current(), elementsToRemove))) { + viewer.expand(navigator.current()); + } + } + return navigator.current(); +} + +/*** + * Finds the last element in the tree with the same type as `element` + */ +export function getLastNodeFromSameType(viewer: WorkbenchCompressibleObjectTree, element: RenderableMatch): RenderableMatch | undefined { + let lastElem: RenderableMatch | null = viewer.lastVisibleElement ?? null; + + while (lastElem) { + const compareVal = compareLevels(element, lastElem); + if (compareVal === -1) { + viewer.expand(lastElem); + lastElem = viewer.lastVisibleElement; + } else if (compareVal === 1) { + lastElem = viewer.getParentElement(lastElem); + } else { + return lastElem; + } + } + + return undefined; +} + +//#endregion + diff --git a/src/vs/workbench/contrib/search/browser/searchActionsSymbol.ts b/src/vs/workbench/contrib/search/browser/searchActionsSymbol.ts new file mode 100644 index 00000000000..e6dec6fab8a --- /dev/null +++ b/src/vs/workbench/contrib/search/browser/searchActionsSymbol.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import * as Constants from 'vs/workbench/contrib/search/common/constants'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; + +//#region Actions +registerAction2(class ShowAllSymbolsAction extends Action2 { + + static readonly ID = 'workbench.action.showAllSymbols'; + static readonly LABEL = nls.localize('showTriggerActions', "Go to Symbol in Workspace..."); + static readonly ALL_SYMBOLS_PREFIX = '#'; + + constructor( + ) { + super({ + id: Constants.ShowAllSymbolsActionId, + title: { + value: nls.localize('showTriggerActions', "Go to Symbol in Workspace..."), + original: 'Go to Symbol in Workspace...', + mnemonicTitle: nls.localize({ key: 'miGotoSymbolInWorkspace', comment: ['&& denotes a mnemonic'] }, "Go to Symbol in &&Workspace...") + }, + f1: true, + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyCode.KeyT + }, + menu: { + id: MenuId.MenubarGoMenu, + group: '3_global_nav', + order: 2 + } + }); + } + + override async run(accessor: ServicesAccessor): Promise { + accessor.get(IQuickInputService).quickAccess.show(ShowAllSymbolsAction.ALL_SYMBOLS_PREFIX); + } +}); + +//#endregion diff --git a/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts b/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts new file mode 100644 index 00000000000..9c9d8c92057 --- /dev/null +++ b/src/vs/workbench/contrib/search/browser/searchActionsTopBar.ts @@ -0,0 +1,348 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import { ICommandHandler } from 'vs/platform/commands/common/commands'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { WorkbenchListFocusContextKey } from 'vs/platform/list/browser/listService'; +import { IViewsService } from 'vs/workbench/common/views'; +import { searchClearIcon, searchCollapseAllIcon, searchExpandAllIcon, searchRefreshIcon, searchShowAsList, searchShowAsTree, searchStopIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; +import * as Constants from 'vs/workbench/contrib/search/common/constants'; +import { ISearchHistoryService } from 'vs/workbench/contrib/search/common/searchHistoryService'; +import { FileMatch, FolderMatch, FolderMatchNoRoot, FolderMatchWorkspaceRoot, Match, SearchResult } from 'vs/workbench/contrib/search/common/searchModel'; +import { VIEW_ID } from 'vs/workbench/services/search/common/search'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { SearchStateKey, SearchUIState } from 'vs/workbench/contrib/search/common/search'; +import { category, getSearchView } from 'vs/workbench/contrib/search/browser/searchActionsBase'; + +//#region Actions +registerAction2(class ClearSearchHistoryCommandAction extends Action2 { + + constructor( + ) { + super({ + id: Constants.ClearSearchHistoryCommandId, + title: { + value: nls.localize('clearSearchHistoryLabel', "Clear Search History"), + original: 'Clear Search History' + }, + category: category, + f1: true + }); + + } + + override async run(accessor: ServicesAccessor): Promise { + clearHistoryCommand(accessor); + } +}); + +registerAction2(class CancelSearchAction extends Action2 { + constructor() { + super({ + id: Constants.CancelSearchActionId, + title: { + value: nls.localize('CancelSearchAction.label', "Cancel Search"), + original: 'Cancel Search' + }, + icon: searchStopIcon, + category, + f1: true, + precondition: SearchStateKey.isEqualTo(SearchUIState.Idle).negate(), + keybinding: { + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(Constants.SearchViewVisibleKey, WorkbenchListFocusContextKey), + primary: KeyCode.Escape, + }, + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + order: 0, + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), SearchStateKey.isEqualTo(SearchUIState.SlowSearch)), + }] + }); + } + run(accessor: ServicesAccessor) { + return cancelSearch(accessor); + } +}); + +registerAction2(class RefreshAction extends Action2 { + constructor() { + super({ + id: Constants.RefreshSearchResultsActionId, + title: { + value: nls.localize('RefreshAction.label', "Refresh"), + original: 'Refresh' + }, + icon: searchRefreshIcon, + precondition: Constants.ViewHasSearchPatternKey, + category, + f1: true, + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + order: 0, + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), SearchStateKey.isEqualTo(SearchUIState.SlowSearch).negate()), + }] + }); + } + run(accessor: ServicesAccessor, ...args: any[]) { + return refreshSearch(accessor); + } +}); + +registerAction2(class CollapseDeepestExpandedLevelAction extends Action2 { + constructor() { + super({ + id: Constants.CollapseSearchResultsActionId, + title: { + value: nls.localize('CollapseDeepestExpandedLevelAction.label', "Collapse All"), + original: 'Collapse All' + }, + category, + icon: searchCollapseAllIcon, + f1: true, + precondition: ContextKeyExpr.and(Constants.HasSearchResults, Constants.ViewHasSomeCollapsibleKey), + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + order: 3, + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), ContextKeyExpr.or(Constants.HasSearchResults.negate(), Constants.ViewHasSomeCollapsibleKey)), + }] + }); + } + run(accessor: ServicesAccessor, ...args: any[]) { + return collapseDeepestExpandedLevel(accessor); + } +}); + +registerAction2(class ExpandAllAction extends Action2 { + constructor() { + super({ + id: Constants.ExpandSearchResultsActionId, + title: { + value: nls.localize('ExpandAllAction.label', "Expand All"), + original: 'Expand All' + }, + category, + icon: searchExpandAllIcon, + f1: true, + precondition: ContextKeyExpr.and(Constants.HasSearchResults, Constants.ViewHasSomeCollapsibleKey.toNegated()), + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + order: 3, + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), Constants.HasSearchResults, Constants.ViewHasSomeCollapsibleKey.toNegated()), + }] + }); + } + run(accessor: ServicesAccessor, ...args: any[]) { + return expandAll(accessor); + } +}); + +registerAction2(class ClearSearchResultsAction extends Action2 { + constructor() { + super({ + id: Constants.ClearSearchResultsActionId, + title: { + value: nls.localize('ClearSearchResultsAction.label', "Clear Search Results"), + original: 'Clear Search Results' + }, + category, + icon: searchClearIcon, + f1: true, + precondition: ContextKeyExpr.or(Constants.HasSearchResults, Constants.ViewHasSearchPatternKey, Constants.ViewHasReplacePatternKey, Constants.ViewHasFilePatternKey), + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + order: 1, + when: ContextKeyExpr.equals('view', VIEW_ID), + }] + }); + } + run(accessor: ServicesAccessor, ...args: any[]) { + return clearSearchResults(accessor); + } +}); + + +registerAction2(class ViewAsTreeAction extends Action2 { + constructor() { + super({ + id: Constants.ViewAsTreeActionId, + title: { + value: nls.localize('ViewAsTreeAction.label', "View as Tree"), + original: 'View as Tree' + }, + category, + icon: searchShowAsList, + f1: true, + precondition: ContextKeyExpr.and(Constants.HasSearchResults, Constants.InTreeViewKey.toNegated()), + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + order: 2, + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), Constants.InTreeViewKey.toNegated()), + }] + }); + } + run(accessor: ServicesAccessor, ...args: any[]) { + const searchView = getSearchView(accessor.get(IViewsService)); + if (searchView) { + searchView.setTreeView(true); + } + } +}); + +registerAction2(class ViewAsListAction extends Action2 { + constructor() { + super({ + id: Constants.ViewAsListActionId, + title: { + value: nls.localize('ViewAsListAction.label', "View as List"), + original: 'View as List' + }, + category, + icon: searchShowAsTree, + f1: true, + precondition: ContextKeyExpr.and(Constants.HasSearchResults, Constants.InTreeViewKey), + menu: [{ + id: MenuId.ViewTitle, + group: 'navigation', + order: 2, + when: ContextKeyExpr.and(ContextKeyExpr.equals('view', VIEW_ID), Constants.InTreeViewKey), + }] + }); + } + run(accessor: ServicesAccessor, ...args: any[]) { + const searchView = getSearchView(accessor.get(IViewsService)); + if (searchView) { + searchView.setTreeView(false); + } + } +}); + +//#endregion + +//#region Helpers +const clearHistoryCommand: ICommandHandler = accessor => { + const searchHistoryService = accessor.get(ISearchHistoryService); + searchHistoryService.clearHistory(); +}; + +function expandAll(accessor: ServicesAccessor) { + const viewsService = accessor.get(IViewsService); + const searchView = getSearchView(viewsService); + if (searchView) { + const viewer = searchView.getControl(); + viewer.expandAll(); + } +} + +function clearSearchResults(accessor: ServicesAccessor) { + const viewsService = accessor.get(IViewsService); + const searchView = getSearchView(viewsService); + searchView?.clearSearchResults(); +} + +function cancelSearch(accessor: ServicesAccessor) { + const viewsService = accessor.get(IViewsService); + const searchView = getSearchView(viewsService); + searchView?.cancelSearch(); +} + +function refreshSearch(accessor: ServicesAccessor) { + const viewsService = accessor.get(IViewsService); + const searchView = getSearchView(viewsService); + searchView?.triggerQueryChange({ preserveFocus: false }); +} + +function collapseDeepestExpandedLevel(accessor: ServicesAccessor) { + + const viewsService = accessor.get(IViewsService); + const searchView = getSearchView(viewsService); + if (searchView) { + const viewer = searchView.getControl(); + + /** + * one level to collapse so collapse everything. If FolderMatch, check if there are visible grandchildren, + * i.e. if Matches are returned by the navigator, and if so, collapse to them, otherwise collapse all levels. + */ + const navigator = viewer.navigate(); + let node = navigator.first(); + let canCollapseFileMatchLevel = false; + let canCollapseFirstLevel = false; + + if (node instanceof FolderMatchWorkspaceRoot) { + while (node = navigator.next()) { + if (node instanceof Match) { + canCollapseFileMatchLevel = true; + break; + } + if (searchView.isTreeLayoutViewVisible && !canCollapseFirstLevel) { + let nodeToTest = node; + + if (node instanceof FolderMatch) { + nodeToTest = node.compressionStartParent ?? node; + } + + const immediateParent = nodeToTest.parent(); + + if (!(immediateParent instanceof FolderMatchWorkspaceRoot || immediateParent instanceof FolderMatchNoRoot || immediateParent instanceof SearchResult)) { + canCollapseFirstLevel = true; + } + } + } + } + + if (canCollapseFileMatchLevel) { + node = navigator.first(); + do { + if (node instanceof FileMatch) { + viewer.collapse(node); + } + } while (node = navigator.next()); + } else if (canCollapseFirstLevel) { + node = navigator.first(); + if (node) { + do { + + let nodeToTest = node; + + if (node instanceof FolderMatch) { + nodeToTest = node.compressionStartParent ?? node; + } + const immediateParent = nodeToTest.parent(); + + if (immediateParent instanceof FolderMatchWorkspaceRoot || immediateParent instanceof FolderMatchNoRoot) { + if (viewer.hasElement(node)) { + viewer.collapse(node, true); + } else { + viewer.collapseAll(); + } + } + } while (node = navigator.next()); + } + } else { + viewer.collapseAll(); + } + + const firstFocusParent = viewer.getFocus()[0]?.parent(); + + if (firstFocusParent && (firstFocusParent instanceof FolderMatch || firstFocusParent instanceof FileMatch) && + viewer.hasElement(firstFocusParent) && viewer.isCollapsed(firstFocusParent)) { + viewer.domFocus(); + viewer.focusFirst(); + viewer.setSelection(viewer.getFocus()); + } + } +} + +//#endregion diff --git a/src/vs/workbench/contrib/search/browser/searchResultsView.ts b/src/vs/workbench/contrib/search/browser/searchResultsView.ts index 265e89bc965..d7a75dc5a3a 100644 --- a/src/vs/workbench/contrib/search/browser/searchResultsView.ts +++ b/src/vs/workbench/contrib/search/browser/searchResultsView.ts @@ -4,35 +4,38 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; -import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { ITreeNode } from 'vs/base/browser/ui/tree/tree'; -import { IAction } from 'vs/base/common/actions'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import * as paths from 'vs/base/common/path'; import * as nls from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { FileKind } from 'vs/platform/files/common/files'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; import { ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search'; import { attachBadgeStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IResourceLabel, ResourceLabels } from 'vs/workbench/browser/labels'; -import { RemoveAction, ReplaceAction, ReplaceAllAction, ReplaceAllInFolderAction } from 'vs/workbench/contrib/search/browser/searchActions'; import { SearchView } from 'vs/workbench/contrib/search/browser/searchView'; import { FileMatch, Match, RenderableMatch, SearchModel, FolderMatch, FolderMatchNoRoot, FolderMatchWorkspaceRoot } from 'vs/workbench/contrib/search/common/searchModel'; import { isEqual } from 'vs/base/common/resources'; import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { HiddenItemStrategy, MenuWorkbenchToolBar } from 'vs/platform/actions/browser/toolbar'; +import { ISearchActionContext } from 'vs/workbench/contrib/search/browser/searchActionsRemoveReplace'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { FileFocusKey, FolderFocusKey, MatchFocusKey } from 'vs/workbench/contrib/search/common/constants'; interface IFolderMatchTemplate { label: IResourceLabel; badge: CountBadge; - actions: ActionBar; + actions: MenuWorkbenchToolBar; disposables: DisposableStore; disposableActions: DisposableStore; } @@ -41,7 +44,7 @@ interface IFileMatchTemplate { el: HTMLElement; label: IResourceLabel; badge: CountBadge; - actions: ActionBar; + actions: MenuWorkbenchToolBar; disposables: DisposableStore; } @@ -52,7 +55,8 @@ interface IMatchTemplate { replace: HTMLElement; after: HTMLElement; lineNumber: HTMLElement; - actions: ActionBar; + actions: MenuWorkbenchToolBar; + disposables: DisposableStore; } export class SearchDelegate implements IListVirtualDelegate { @@ -82,13 +86,13 @@ export class FolderMatchRenderer extends Disposable implements ICompressibleTree readonly templateId = FolderMatchRenderer.TEMPLATE_ID; constructor( - private searchModel: SearchModel, private searchView: SearchView, private labels: ResourceLabels, - @IInstantiationService private readonly instantiationService: IInstantiationService, @IThemeService private readonly themeService: IThemeService, @IWorkspaceContextService protected contextService: IWorkspaceContextService, @ILabelService private readonly labelService: ILabelService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, ) { super(); } @@ -109,8 +113,6 @@ export class FolderMatchRenderer extends Disposable implements ICompressibleTree templateData.label.setLabel(nls.localize('searchFolderMatch.other.label', "Other files")); } - templateData.actions.clear(); - templateData.actions.context = folder; this.renderFolderDetails(folder, templateData); } @@ -123,12 +125,22 @@ export class FolderMatchRenderer extends Disposable implements ICompressibleTree const badge = new CountBadge(DOM.append(folderMatchElement, DOM.$('.badge'))); disposables.add(attachBadgeStyler(badge, this.themeService)); const actionBarContainer = DOM.append(folderMatchElement, DOM.$('.actionBarContainer')); - const actions = new ActionBar(actionBarContainer, { animated: false }); - disposables.add(actions); const disposableElements = new DisposableStore(); disposables.add(disposableElements); + const contextKeyService = this.contextKeyService.createOverlay([[FolderFocusKey.key, true]]); + const instantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyService])); + const actions = disposables.add(instantiationService.createInstance(MenuWorkbenchToolBar, actionBarContainer, MenuId.SearchActionMenu, { + menuOptions: { + shouldForwardArgs: true + }, + hiddenItemStrategy: HiddenItemStrategy.Ignore, + toolbarOptions: { + primaryGroup: g => /^inline/.test(g), + }, + })); + return { label, badge, @@ -151,7 +163,6 @@ export class FolderMatchRenderer extends Disposable implements ICompressibleTree } else { templateData.label.setLabel(nls.localize('searchFolderMatch.other.label', "Other files")); } - templateData.actions.clear(); this.renderFolderDetails(folderMatch, templateData); } @@ -172,16 +183,7 @@ export class FolderMatchRenderer extends Disposable implements ICompressibleTree templateData.badge.setCount(count); templateData.badge.setTitleFormat(count > 1 ? nls.localize('searchFileMatches', "{0} files found", count) : nls.localize('searchFileMatch', "{0} file found", count)); - const actions: IAction[] = []; - if (this.searchModel.isReplaceActive() && count > 0) { - const replaceAction = this.instantiationService.createInstance(ReplaceAllInFolderAction, this.searchView.getControl(), folder); - actions.push(replaceAction); - templateData.disposableActions.add(replaceAction); - } - const removeAction = this.instantiationService.createInstance(RemoveAction, this.searchView.getControl(), folder); - actions.push(removeAction); - - templateData.actions.push(actions, { icon: true, label: false }); + templateData.actions.context = { viewer: this.searchView.getControl(), element: folder }; } } @@ -191,13 +193,13 @@ export class FileMatchRenderer extends Disposable implements ICompressibleTreeRe readonly templateId = FileMatchRenderer.TEMPLATE_ID; constructor( - private searchModel: SearchModel, private searchView: SearchView, private labels: ResourceLabels, - @IInstantiationService private readonly instantiationService: IInstantiationService, @IThemeService private readonly themeService: IThemeService, @IWorkspaceContextService protected contextService: IWorkspaceContextService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IConfigurationService private readonly configurationService: IConfigurationService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, ) { super(); } @@ -214,15 +216,25 @@ export class FileMatchRenderer extends Disposable implements ICompressibleTreeRe const badge = new CountBadge(DOM.append(fileMatchElement, DOM.$('.badge'))); disposables.add(attachBadgeStyler(badge, this.themeService)); const actionBarContainer = DOM.append(fileMatchElement, DOM.$('.actionBarContainer')); - const actions = new ActionBar(actionBarContainer, { animated: false }); - disposables.add(actions); + + const contextKeyService = this.contextKeyService.createOverlay([[FileFocusKey.key, true]]); + const instantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyService])); + const actions = disposables.add(instantiationService.createInstance(MenuWorkbenchToolBar, actionBarContainer, MenuId.SearchActionMenu, { + menuOptions: { + shouldForwardArgs: true + }, + hiddenItemStrategy: HiddenItemStrategy.Ignore, + toolbarOptions: { + primaryGroup: g => /^inline/.test(g), + }, + })); return { el: fileMatchElement, label, badge, actions, - disposables + disposables, }; } @@ -236,14 +248,7 @@ export class FileMatchRenderer extends Disposable implements ICompressibleTreeRe templateData.badge.setCount(count); templateData.badge.setTitleFormat(count > 1 ? nls.localize('searchMatches', "{0} matches found", count) : nls.localize('searchMatch', "{0} match found", count)); - templateData.actions.clear(); - - const actions: IAction[] = []; - if (this.searchModel.isReplaceActive() && count > 0) { - actions.push(this.instantiationService.createInstance(ReplaceAllAction, this.searchView, fileMatch)); - } - actions.push(this.instantiationService.createInstance(RemoveAction, this.searchView.getControl(), fileMatch)); - templateData.actions.push(actions, { icon: true, label: false }); + templateData.actions.context = { viewer: this.searchView.getControl(), element: fileMatch }; } disposeElement(element: ITreeNode, index: number, templateData: IFileMatchTemplate): void { @@ -262,9 +267,10 @@ export class MatchRenderer extends Disposable implements ICompressibleTreeRender constructor( private searchModel: SearchModel, private searchView: SearchView, - @IInstantiationService private readonly instantiationService: IInstantiationService, @IWorkspaceContextService protected contextService: IWorkspaceContextService, @IConfigurationService private readonly configurationService: IConfigurationService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, ) { super(); } @@ -282,7 +288,20 @@ export class MatchRenderer extends Disposable implements ICompressibleTreeRender const after = DOM.append(parent, DOM.$('span')); const lineNumber = DOM.append(container, DOM.$('span.matchLineNum')); const actionBarContainer = DOM.append(container, DOM.$('span.actionBarContainer')); - const actions = new ActionBar(actionBarContainer, { animated: false }); + + const disposables = new DisposableStore(); + + const contextKeyService = this.contextKeyService.createOverlay([[MatchFocusKey.key, true]]); + const instantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyService])); + const actions = disposables.add(instantiationService.createInstance(MenuWorkbenchToolBar, actionBarContainer, MenuId.SearchActionMenu, { + menuOptions: { + shouldForwardArgs: true + }, + hiddenItemStrategy: HiddenItemStrategy.Ignore, + toolbarOptions: { + primaryGroup: g => /^inline/.test(g), + }, + })); return { parent, @@ -291,7 +310,8 @@ export class MatchRenderer extends Disposable implements ICompressibleTreeRender replace, after, lineNumber, - actions + actions, + disposables, }; } @@ -317,19 +337,12 @@ export class MatchRenderer extends Disposable implements ICompressibleTreeRender templateData.lineNumber.textContent = lineNumberStr + extraLinesStr; templateData.lineNumber.setAttribute('title', this.getMatchTitle(match, showLineNumbers)); - templateData.actions.clear(); - if (this.searchModel.isReplaceActive()) { - templateData.actions.push([this.instantiationService.createInstance(ReplaceAction, this.searchView.getControl(), match, this.searchView), this.instantiationService.createInstance(RemoveAction, this.searchView.getControl(), match)], { icon: true, label: false }); - } else { - templateData.actions.push([this.instantiationService.createInstance(RemoveAction, this.searchView.getControl(), match)], { icon: true, label: false }); - } - } + templateData.actions.context = { viewer: this.searchView.getControl(), element: match }; - disposeElement(element: ITreeNode, index: number, templateData: IMatchTemplate): void { } disposeTemplate(templateData: IMatchTemplate): void { - templateData.actions.dispose(); + templateData.disposables.dispose(); } private getMatchTitle(match: Match, showLineNumbers: boolean): string { diff --git a/src/vs/workbench/contrib/search/browser/searchView.ts b/src/vs/workbench/contrib/search/browser/searchView.ts index 30454a8e988..9f5a9f44590 100644 --- a/src/vs/workbench/contrib/search/browser/searchView.ts +++ b/src/vs/workbench/contrib/search/browser/searchView.ts @@ -48,6 +48,7 @@ import { IOpenerService, withSelection } from 'vs/platform/opener/common/opener' import { IProgress, IProgressService, IProgressStep } from 'vs/platform/progress/common/progress'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { defaultInputBoxStyles, defaultToggleStyles } from 'vs/platform/theme/browser/defaultStyles'; import { diffInserted, diffInsertedOutline, diffRemoved, diffRemovedOutline, editorFindMatchHighlight, editorFindMatchHighlightBorder, foreground, listActiveSelectionForeground, textLinkActiveForeground, textLinkForeground, toolbarActiveBackground, toolbarHoverBackground } from 'vs/platform/theme/common/colorRegistry'; import { isHighContrast } from 'vs/platform/theme/common/theme'; import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; @@ -62,11 +63,12 @@ import { IViewDescriptorService } from 'vs/workbench/common/views'; import { NotebookFindContrib } from 'vs/workbench/contrib/notebook/browser/contrib/find/notebookFindWidget'; import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; import { ExcludePatternInputWidget, IncludePatternInputWidget } from 'vs/workbench/contrib/search/browser/patternInputWidget'; -import { appendKeyBindingLabel, IFindInFilesArgs } from 'vs/workbench/contrib/search/browser/searchActions'; +import { appendKeyBindingLabel } from 'vs/workbench/contrib/search/browser/searchActionsBase'; +import { IFindInFilesArgs } from 'vs/workbench/contrib/search/browser/searchActionsFind'; import { searchDetailsIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; import { renderSearchMessage } from 'vs/workbench/contrib/search/browser/searchMessage'; import { FileMatchRenderer, FolderMatchRenderer, MatchRenderer, SearchAccessibilityProvider, SearchDelegate } from 'vs/workbench/contrib/search/browser/searchResultsView'; -import { ISearchWidgetOptions, SearchWidget } from 'vs/workbench/contrib/search/browser/searchWidget'; +import { SearchWidget } from 'vs/workbench/contrib/search/browser/searchWidget'; import * as Constants from 'vs/workbench/contrib/search/common/constants'; import { IReplaceService } from 'vs/workbench/contrib/search/common/replace'; import { getOutOfWorkspaceEditorResources, SearchStateKey, SearchUIState } from 'vs/workbench/contrib/search/common/search'; @@ -349,6 +351,7 @@ export class SearchView extends ViewPane { placeholder: nls.localize('placeholder.includes', "e.g. *.ts, src/**/include"), showPlaceholderOnFocus: true, history: patternIncludesHistory, + inputBoxStyles: defaultInputBoxStyles })); this.inputPatternIncludes.setValue(patternIncludes); @@ -368,6 +371,7 @@ export class SearchView extends ViewPane { placeholder: nls.localize('placeholder.excludes', "e.g. *.ts, src/**/exclude"), showPlaceholderOnFocus: true, history: patternExclusionsHistory, + inputBoxStyles: defaultInputBoxStyles })); this.inputPatternExcludes.setValue(patternExclusions); @@ -445,7 +449,7 @@ export class SearchView extends ViewPane { const showReplace = typeof this.viewletState['view.showReplace'] === 'boolean' ? this.viewletState['view.showReplace'] : true; const preserveCase = this.viewletState['query.preserveCase'] === true; - this.searchWidget = this._register(this.instantiationService.createInstance(SearchWidget, container, { + this.searchWidget = this._register(this.instantiationService.createInstance(SearchWidget, container, { value: contentPattern, replaceValue: replaceText, isRegex: isRegex, @@ -453,7 +457,9 @@ export class SearchView extends ViewPane { isWholeWords: isWholeWords, searchHistory: searchHistory, replaceHistory: replaceHistory, - preserveCase: preserveCase + preserveCase: preserveCase, + inputBoxStyles: defaultInputBoxStyles, + toggleStyles: defaultToggleStyles })); if (showReplace) { @@ -750,8 +756,8 @@ export class SearchView extends ViewPane { this.resultsElement, delegate, [ - this._register(this.instantiationService.createInstance(FolderMatchRenderer, this.viewModel, this, this.treeLabels)), - this._register(this.instantiationService.createInstance(FileMatchRenderer, this.viewModel, this, this.treeLabels)), + this._register(this.instantiationService.createInstance(FolderMatchRenderer, this, this.treeLabels)), + this._register(this.instantiationService.createInstance(FileMatchRenderer, this, this.treeLabels)), this._register(this.instantiationService.createInstance(MatchRenderer, this.viewModel, this)), ], { diff --git a/src/vs/workbench/contrib/search/browser/searchWidget.ts b/src/vs/workbench/contrib/search/browser/searchWidget.ts index f80bf8d2e57..46d48bb7f75 100644 --- a/src/vs/workbench/contrib/search/browser/searchWidget.ts +++ b/src/vs/workbench/contrib/search/browser/searchWidget.ts @@ -9,7 +9,7 @@ import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { Button, IButtonOptions } from 'vs/base/browser/ui/button/button'; import { FindInput, IFindInputOptions } from 'vs/base/browser/ui/findinput/findInput'; import { ReplaceInput } from 'vs/base/browser/ui/findinput/replaceInput'; -import { IMessage, InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; +import { IInputBoxStyles, IMessage, InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { Widget } from 'vs/base/browser/ui/widget'; import { Action } from 'vs/base/common/actions'; import { Delayer } from 'vs/base/common/async'; @@ -24,18 +24,18 @@ import { IContextViewService } from 'vs/platform/contextview/browser/contextView import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search'; -import { attachFindReplaceInputBoxStyler, attachInputBoxStyler } from 'vs/platform/theme/common/styler'; -import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { ContextScopedFindInput, ContextScopedReplaceInput } from 'vs/platform/history/browser/contextScopedHistoryWidget'; -import { appendKeyBindingLabel, isSearchViewFocused, getSearchView } from 'vs/workbench/contrib/search/browser/searchActions'; +import { appendKeyBindingLabel, isSearchViewFocused, getSearchView } from 'vs/workbench/contrib/search/browser/searchActionsBase'; import * as Constants from 'vs/workbench/contrib/search/common/constants'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { isMacintosh } from 'vs/base/common/platform'; -import { Toggle } from 'vs/base/browser/ui/toggle/toggle'; +import { IToggleStyles, Toggle } from 'vs/base/browser/ui/toggle/toggle'; import { IViewsService } from 'vs/workbench/common/views'; import { searchReplaceAllIcon, searchHideReplaceIcon, searchShowContextIcon, searchShowReplaceIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; import { ToggleSearchEditorContextLinesCommandId } from 'vs/workbench/contrib/searchEditor/browser/constants'; import { showHistoryKeybindingHint } from 'vs/platform/history/browser/historyWidgetKeybindingHint'; +import { defaultInputBoxStyles, defaultToggleStyles } from 'vs/platform/theme/browser/defaultStyles'; /** Specified in searchview.css */ export const SingleLineInputHeight = 24; @@ -51,6 +51,8 @@ export interface ISearchWidgetOptions { preserveCase?: boolean; _hideReplaceToggle?: boolean; // TODO: Search Editor's replace experience showContextToggle?: boolean; + inputBoxStyles: IInputBoxStyles; + toggleStyles: IToggleStyles; } class ReplaceAllAction extends Action { @@ -155,7 +157,6 @@ export class SearchWidget extends Widget { container: HTMLElement, options: ISearchWidgetOptions, @IContextViewService private readonly contextViewService: IContextViewService, - @IThemeService private readonly themeService: IThemeService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IKeybindingService private readonly keybindingService: IKeybindingService, @IClipboardService private readonly clipboardServce: IClipboardService, @@ -319,12 +320,13 @@ export class SearchWidget extends Widget { showHistoryHint: () => showHistoryKeybindingHint(this.keybindingService), flexibleHeight: true, flexibleMaxHeight: SearchWidget.INPUT_MAX_HEIGHT, - showCommonFindToggles: true + showCommonFindToggles: true, + inputBoxStyles: options.inputBoxStyles, + toggleStyles: options.toggleStyles }; const searchInputContainer = dom.append(parent, dom.$('.search-container.input-box')); this.searchInput = this._register(new ContextScopedFindInput(searchInputContainer, this.contextViewService, inputOptions, this.contextKeyService)); - this._register(attachFindReplaceInputBoxStyler(this.searchInput, this.themeService)); this.searchInput.onKeyDown((keyboardEvent: IKeyboardEvent) => this.onSearchInputKeyDown(keyboardEvent)); this.searchInput.setValue(options.value || ''); this.searchInput.setRegex(!!options.isRegex); @@ -363,12 +365,13 @@ export class SearchWidget extends Widget { this.showContextToggle = new Toggle({ isChecked: false, title: appendKeyBindingLabel(nls.localize('showContext', "Toggle Context Lines"), this.keybindingService.lookupKeybinding(ToggleSearchEditorContextLinesCommandId), this.keybindingService), - icon: searchShowContextIcon + icon: searchShowContextIcon, + ...defaultToggleStyles }); this._register(this.showContextToggle.onChange(() => this.onContextLinesChanged())); if (options.showContextToggle) { - this.contextLinesInput = new InputBox(searchInputContainer, this.contextViewService, { type: 'number' }); + this.contextLinesInput = new InputBox(searchInputContainer, this.contextViewService, { type: 'number', inputBoxStyles: defaultInputBoxStyles }); this.contextLinesInput.element.classList.add('context-lines-input'); this.contextLinesInput.value = '' + (this.configurationService.getValue('search').searchEditor.defaultNumberOfContextLines ?? 1); this._register(this.contextLinesInput.onDidChange((value: string) => { @@ -377,7 +380,6 @@ export class SearchWidget extends Widget { } this.onContextLinesChanged(); })); - this._register(attachInputBoxStyler(this.contextLinesInput, this.themeService)); dom.append(searchInputContainer, this.showContextToggle.domNode); } } @@ -413,7 +415,9 @@ export class SearchWidget extends Widget { history: options.replaceHistory, showHistoryHint: () => showHistoryKeybindingHint(this.keybindingService), flexibleHeight: true, - flexibleMaxHeight: SearchWidget.INPUT_MAX_HEIGHT + flexibleMaxHeight: SearchWidget.INPUT_MAX_HEIGHT, + inputBoxStyles: options.inputBoxStyles, + toggleStyles: options.toggleStyles }, this.contextKeyService, true)); this._register(this.replaceInput.onDidOptionChange(viaKeyboard => { @@ -422,7 +426,6 @@ export class SearchWidget extends Widget { } })); - this._register(attachFindReplaceInputBoxStyler(this.replaceInput, this.themeService)); this.replaceInput.onKeyDown((keyboardEvent) => this.onReplaceInputKeyDown(keyboardEvent)); this.replaceInput.setValue(options.replaceValue || ''); this._register(this.replaceInput.inputBox.onDidChange(() => this._onReplaceValueChanged.fire())); diff --git a/src/vs/workbench/contrib/search/common/constants.ts b/src/vs/workbench/contrib/search/common/constants.ts index 9e5df9fd94e..f0700ce6a99 100644 --- a/src/vs/workbench/contrib/search/common/constants.ts +++ b/src/vs/workbench/contrib/search/common/constants.ts @@ -42,6 +42,11 @@ export const ViewAsTreeActionId = 'search.action.viewAsTree'; export const ViewAsListActionId = 'search.action.viewAsList'; export const ToggleQueryDetailsActionId = 'workbench.action.search.toggleQueryDetails'; export const ExcludeFolderFromSearchId = 'search.action.excludeFromSearch'; +export const FocusNextInputActionId = 'search.focus.nextInputBox'; +export const FocusPreviousInputActionId = 'search.focus.previousInputBox'; +export const RestrictSearchToFolderId = 'search.action.restrictSearchToFolder'; +export const FindInFolderId = 'filesExplorer.findInFolder'; +export const FindInWorkspaceId = 'filesExplorer.findInWorkspace'; export const SearchViewVisibleKey = new RawContextKey('searchViewletVisible', true); export const SearchViewFocusedKey = new RawContextKey('searchViewletFocus', false); diff --git a/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts b/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts index f8103e8652c..783ee335c4c 100644 --- a/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts +++ b/src/vs/workbench/contrib/search/test/browser/searchActions.test.ts @@ -15,7 +15,7 @@ import { TestInstantiationService } from 'vs/platform/instantiation/test/common/ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding'; import { IFileMatch } from 'vs/workbench/services/search/common/search'; -import { getElementToFocusAfterRemoved, getLastNodeFromSameType } from 'vs/workbench/contrib/search/browser/searchActions'; +import { getElementToFocusAfterRemoved, getLastNodeFromSameType } from 'vs/workbench/contrib/search/browser/searchActionsRemoveReplace'; import { FileMatch, FileMatchOrMatch, FolderMatch, Match, SearchModel } from 'vs/workbench/contrib/search/common/searchModel'; import { MockObjectTree } from 'vs/workbench/contrib/search/test/browser/mockSearchTree'; import { IThemeService } from 'vs/platform/theme/common/themeService'; diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts index b395623dc08..5540e023a3d 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.contribution.ts @@ -23,7 +23,7 @@ import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchCo import { IEditorSerializer, IEditorFactoryRegistry, EditorExtensions, DEFAULT_EDITOR_ASSOCIATION } from 'vs/workbench/common/editor'; import { ActiveEditorContext } from 'vs/workbench/common/contextkeys'; import { IViewsService } from 'vs/workbench/common/views'; -import { getSearchView } from 'vs/workbench/contrib/search/browser/searchActions'; +import { getSearchView } from 'vs/workbench/contrib/search/browser/searchActionsBase'; import { searchNewEditorIcon, searchRefreshIcon } from 'vs/workbench/contrib/search/browser/searchIcons'; import * as SearchConstants from 'vs/workbench/contrib/search/common/constants'; import * as SearchEditorConstants from 'vs/workbench/contrib/searchEditor/browser/constants'; diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts index 9dbee9c59fb..cdea9d6e3c8 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts @@ -33,7 +33,6 @@ import { IEditorProgressService, LongRunningOperation } from 'vs/platform/progre import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { inputBorder, registerColor, searchEditorFindMatch, searchEditorFindMatchBorder } from 'vs/platform/theme/common/colorRegistry'; -import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; import { IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { AbstractTextCodeEditor } from 'vs/workbench/browser/parts/editor/textCodeEditor'; @@ -61,6 +60,7 @@ import { renderSearchMessage } from 'vs/workbench/contrib/search/browser/searchM import { EditorExtensionsRegistry, IEditorContributionDescription } from 'vs/editor/browser/editorExtensions'; import { UnusualLineTerminatorsDetector } from 'vs/editor/contrib/unusualLineTerminators/browser/unusualLineTerminators'; import { isHighContrast } from 'vs/platform/theme/common/theme'; +import { defaultToggleStyles, getInputBoxStyle } from 'vs/platform/theme/browser/defaultStyles'; const RESULT_LINE_REGEX = /^(\s+)(\d+)(: | )(\s*)(.*)$/; const FILE_LINE_REGEX = /^(\S.*):$/; @@ -143,7 +143,9 @@ export class SearchEditor extends AbstractTextCodeEditor private createQueryEditor(container: HTMLElement, scopedInstantiationService: IInstantiationService, inputBoxFocusedContextKey: IContextKey) { - this.queryEditorWidget = this._register(scopedInstantiationService.createInstance(SearchWidget, container, { _hideReplaceToggle: true, showContextToggle: true })); + const searchEditorInputboxStyles = getInputBoxStyle({ inputBorder: searchEditorTextInputBorder }); + + this.queryEditorWidget = this._register(scopedInstantiationService.createInstance(SearchWidget, container, { _hideReplaceToggle: true, showContextToggle: true, inputBoxStyles: searchEditorInputboxStyles, toggleStyles: defaultToggleStyles })); this._register(this.queryEditorWidget.onReplaceToggled(() => this.reLayout())); this._register(this.queryEditorWidget.onDidHeightChange(() => this.reLayout())); this._register(this.queryEditorWidget.onSearchSubmit(({ delay }) => this.triggerSearch({ delay }))); @@ -185,6 +187,7 @@ export class SearchEditor extends AbstractTextCodeEditor DOM.append(folderIncludesList, DOM.$('h4', undefined, filesToIncludeTitle)); this.inputPatternIncludes = this._register(scopedInstantiationService.createInstance(IncludePatternInputWidget, folderIncludesList, this.contextViewService, { ariaLabel: localize('label.includes', 'Search Include Patterns'), + inputBoxStyles: searchEditorInputboxStyles })); this.inputPatternIncludes.onSubmit(triggeredOnType => this.triggerSearch({ resetCursor: false, delay: triggeredOnType ? this.searchConfig.searchOnTypeDebouncePeriod : 0 })); this._register(this.inputPatternIncludes.onChangeSearchInEditorsBox(() => this.triggerSearch())); @@ -195,13 +198,11 @@ export class SearchEditor extends AbstractTextCodeEditor DOM.append(excludesList, DOM.$('h4', undefined, excludesTitle)); this.inputPatternExcludes = this._register(scopedInstantiationService.createInstance(ExcludePatternInputWidget, excludesList, this.contextViewService, { ariaLabel: localize('label.excludes', 'Search Exclude Patterns'), + inputBoxStyles: searchEditorInputboxStyles })); this.inputPatternExcludes.onSubmit(triggeredOnType => this.triggerSearch({ resetCursor: false, delay: triggeredOnType ? this.searchConfig.searchOnTypeDebouncePeriod : 0 })); this._register(this.inputPatternExcludes.onChangeIgnoreBox(() => this.triggerSearch())); - [this.queryEditorWidget.searchInput, this.inputPatternIncludes, this.inputPatternExcludes, this.queryEditorWidget.contextLinesInput].map(input => - this._register(attachInputBoxStyler(input, this.themeService, { inputBorder: searchEditorTextInputBorder }))); - // Messages this.messageBox = DOM.append(container, DOM.$('.messages.text-search-provider-messages')); diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts index e1d2e65956d..b5a7b7c9980 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts @@ -16,7 +16,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { EditorsOrder } from 'vs/workbench/common/editor'; import { IViewsService } from 'vs/workbench/common/views'; -import { getSearchView } from 'vs/workbench/contrib/search/browser/searchActions'; +import { getSearchView } from 'vs/workbench/contrib/search/browser/searchActionsBase'; import { SearchResult } from 'vs/workbench/contrib/search/common/searchModel'; import { SearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditor'; import { OpenSearchEditorArgs } from 'vs/workbench/contrib/searchEditor/browser/searchEditor.contribution'; diff --git a/src/vs/workbench/contrib/tasks/browser/taskTerminalStatus.ts b/src/vs/workbench/contrib/tasks/browser/taskTerminalStatus.ts index 762f9f6f578..7c88571ea9f 100644 --- a/src/vs/workbench/contrib/tasks/browser/taskTerminalStatus.ts +++ b/src/vs/workbench/contrib/tasks/browser/taskTerminalStatus.ts @@ -15,7 +15,7 @@ import { ITerminalStatus } from 'vs/workbench/contrib/terminal/browser/terminalS import { MarkerSeverity } from 'vs/platform/markers/common/markers'; import { spinningLoading } from 'vs/platform/theme/common/iconRegistry'; import { IMarker } from 'vs/platform/terminal/common/capabilities/capabilities'; -import { AudioCue, IAudioCueService } from 'vs/workbench/contrib/audioCues/browser/audioCueService'; +import { AudioCue, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService'; interface ITerminalData { terminal: ITerminalInstance; diff --git a/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts b/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts index 76f88556c89..1b29fa27657 100644 --- a/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts +++ b/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts @@ -46,7 +46,7 @@ import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/b import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { IAudioCueService } from 'vs/workbench/contrib/audioCues/browser/audioCueService'; +import { IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService'; interface IWorkspaceFolderConfigurationResult { workspaceFolder: IWorkspaceFolder; diff --git a/src/vs/workbench/contrib/tasks/test/browser/taskTerminalStatus.test.ts b/src/vs/workbench/contrib/tasks/test/browser/taskTerminalStatus.test.ts index 11239dc5fa2..f15422f2768 100644 --- a/src/vs/workbench/contrib/tasks/test/browser/taskTerminalStatus.test.ts +++ b/src/vs/workbench/contrib/tasks/test/browser/taskTerminalStatus.test.ts @@ -5,9 +5,9 @@ import { ok } from 'assert'; import { Emitter, Event } from 'vs/base/common/event'; +import { AudioCue, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; -import { AudioCue, IAudioCueService } from 'vs/workbench/contrib/audioCues/browser/audioCueService'; import { ACTIVE_TASK_STATUS, FAILED_TASK_STATUS, SUCCEEDED_TASK_STATUS, TaskTerminalStatus } from 'vs/workbench/contrib/tasks/browser/taskTerminalStatus'; import { AbstractProblemCollector } from 'vs/workbench/contrib/tasks/common/problemCollectors'; import { CommonTask, ITaskEvent, TaskEventKind, TaskRunType } from 'vs/workbench/contrib/tasks/common/tasks'; diff --git a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts index f44c01d97a7..f1ea431aa9b 100644 --- a/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts +++ b/src/vs/workbench/contrib/telemetry/browser/telemetry.contribution.ts @@ -15,7 +15,7 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { language } from 'vs/base/common/platform'; import { Disposable } from 'vs/base/common/lifecycle'; import ErrorTelemetry from 'vs/platform/telemetry/browser/errorTelemetry'; -import { configurationTelemetry } from 'vs/platform/telemetry/common/telemetryUtils'; +import { configurationTelemetry, TrustedTelemetryValue } from 'vs/platform/telemetry/common/telemetryUtils'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITextFileService, ITextFileSaveEvent, ITextFileResolveEvent } from 'vs/workbench/services/textfile/common/textfiles'; import { extname, basename, isEqual, isEqualOrParent } from 'vs/base/common/resources'; @@ -28,7 +28,7 @@ import { ViewContainerLocation } from 'vs/workbench/common/views'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; type TelemetryData = { - mimeType: string; + mimeType: TrustedTelemetryValue; ext: string; path: number; reason?: number; @@ -216,7 +216,7 @@ export class TelemetryContribution extends Disposable implements IWorkbenchContr const fileName = basename(resource); const path = resource.scheme === Schemas.file ? resource.fsPath : resource.path; const telemetryData = { - mimeType: getMimeTypes(resource).join(', '), + mimeType: new TrustedTelemetryValue(getMimeTypes(resource).join(', ')), ext, path: hash(path), reason, diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index bf9964148ed..d253aa2d8b5 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -33,7 +33,7 @@ import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspac import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands'; import { CLOSE_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; import { ResourceContextKey } from 'vs/workbench/common/contextkeys'; -import { FindInFilesCommand, IFindInFilesArgs } from 'vs/workbench/contrib/search/browser/searchActions'; +import { findInFilesCommand, IFindInFilesArgs } from 'vs/workbench/contrib/search/browser/searchActionsFind'; import { Direction, ICreateTerminalOptions, IInternalXtermTerminal, ITerminalEditorService, ITerminalGroupService, ITerminalInstance, ITerminalInstanceService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalQuickAccessProvider } from 'vs/workbench/contrib/terminal/browser/terminalQuickAccess'; import { IRemoteTerminalAttachTarget, ITerminalConfigHelper, ITerminalProfileService, TerminalCommandId, TERMINAL_ACTION_CATEGORY } from 'vs/workbench/contrib/terminal/common/terminal'; @@ -1590,7 +1590,7 @@ export function registerTerminalActions() { } run(accessor: ServicesAccessor) { const query = accessor.get(ITerminalService).activeInstance?.selection; - FindInFilesCommand(accessor, { query } as IFindInFilesArgs); + findInFilesCommand(accessor, { query } as IFindInFilesArgs); } }); registerAction2(class extends Action2 { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts b/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts index 81f970de0c4..025b2f28856 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalFindWidget.ts @@ -9,7 +9,7 @@ import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/c import { FindReplaceState } from 'vs/editor/contrib/find/browser/findState'; import { ITerminalInstance, IXtermTerminal } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; -import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { Event } from 'vs/base/common/event'; @@ -37,9 +37,7 @@ export class TerminalFindWidget extends SimpleFindWidget { this._findInputFocused = TerminalContextKeys.findInputFocus.bindTo(this._contextKeyService); this._findWidgetFocused = TerminalContextKeys.findFocus.bindTo(this._contextKeyService); this._findWidgetVisible = TerminalContextKeys.findVisible.bindTo(this._contextKeyService); - this.updateTheme(this._themeService.getColorTheme()); - this._register(this._themeService.onDidColorThemeChange((theme?: IColorTheme) => { - this.updateTheme(theme ?? this._themeService.getColorTheme()); + this._register(this._themeService.onDidColorThemeChange(() => { if (this.isVisible()) { this.find(true, true); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index b37755588d6..567354cdd11 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -55,7 +55,6 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; import { IViewDescriptorService, IViewsService, ViewContainerLocation } from 'vs/workbench/common/views'; -import { AudioCue, IAudioCueService } from 'vs/workbench/contrib/audioCues/browser/audioCueService'; import { TaskSettingId } from 'vs/workbench/contrib/tasks/common/tasks'; import { IDetectedLinks, TerminalLinkManager } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager'; import { TerminalLinkQuickpick } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkQuickpick'; @@ -89,6 +88,7 @@ import { IWorkbenchLayoutService, Position } from 'vs/workbench/services/layout/ import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import type { IMarker, ITerminalAddon, Terminal as XTermTerminal } from 'xterm'; +import { IAudioCueService, AudioCue } from 'vs/platform/audioCues/browser/audioCueService'; const enum Constants { /** diff --git a/src/vs/workbench/contrib/terminal/browser/terminalQuickFixBuiltinActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalQuickFixBuiltinActions.ts index 13b83090e85..01e75e84259 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalQuickFixBuiltinActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalQuickFixBuiltinActions.ts @@ -6,6 +6,7 @@ import { localize } from 'vs/nls'; import { IInternalOptions, ITerminalCommandMatchResult, TerminalQuickFixActionInternal } from 'vs/platform/terminal/common/terminal'; import { ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { TerminalQuickFixType } from 'vs/workbench/contrib/terminal/browser/widgets/terminalQuickFixMenuItems'; export const GitCommandLineRegex = /git/; export const GitPushCommandLineRegex = /git\s+push/; export const GitTwoDashesRegex = /error: did you mean `--(.+)` \(with two dashes\)\?/; @@ -40,7 +41,7 @@ export function gitSimilar(): IInternalOptions { if (fixedCommand) { actions.push({ id: 'Git Similar', - type: 'command', + type: TerminalQuickFixType.Command, terminalCommand: matchResult.commandLine.replace(/git\s+[^\s]+/, `git ${fixedCommand}`), addNewLine: true }); @@ -69,7 +70,7 @@ export function gitTwoDashes(): IInternalOptions { return; } return { - type: 'command', + type: TerminalQuickFixType.Command, id: 'Git Two Dashes', terminalCommand: matchResult.commandLine.replace(` -${problemArg}`, ` --${problemArg}`), addNewLine: true @@ -96,7 +97,7 @@ export function freePort(terminalInstance?: Partial): IIntern } const label = localize("terminal.freePort", "Free port {0}", port); return { - class: undefined, + class: TerminalQuickFixType.Port, tooltip: label, id: 'terminal.freePort', label, diff --git a/src/vs/workbench/contrib/terminal/browser/terminalRunRecentQuickPick.ts b/src/vs/workbench/contrib/terminal/browser/terminalRunRecentQuickPick.ts index e56f75730b1..57355534575 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalRunRecentQuickPick.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalRunRecentQuickPick.ts @@ -13,8 +13,8 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati import { IQuickInputButton, IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { ITerminalCommand, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities'; import { collapseTildePath } from 'vs/platform/terminal/common/terminalEnvironment'; -import { inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground } from 'vs/platform/theme/common/colorRegistry'; -import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { asCssValue, inputActiveOptionBackground, inputActiveOptionBorder, inputActiveOptionForeground } from 'vs/platform/theme/common/colorRegistry'; +import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { commandHistoryFuzzySearchIcon, commandHistoryOutputIcon, commandHistoryRemoveIcon } from 'vs/workbench/contrib/terminal/browser/terminalIcons'; import { getCommandHistory, getDirectoryHistory, getShellFileHistory } from 'vs/workbench/contrib/terminal/common/history'; @@ -43,7 +43,6 @@ export async function showRunRecentQuickPick( const instantiationService = accessor.get(IInstantiationService); const quickInputService = accessor.get(IQuickInputService); const storageService = accessor.get(IStorageService); - const themeService = accessor.get(IThemeService); const runRecentStorageKey = `${TerminalStorageKeys.PinnedRecentCommandsPrefix}.${instance.shellType}`; let placeholder: string; @@ -208,9 +207,9 @@ export async function showRunRecentQuickPick( title: 'Fuzzy search', icon: commandHistoryFuzzySearchIcon, isChecked: filterMode === 'fuzzy', - inputActiveOptionBorder: themeService.getColorTheme().getColor(inputActiveOptionBorder), - inputActiveOptionForeground: themeService.getColorTheme().getColor(inputActiveOptionForeground), - inputActiveOptionBackground: themeService.getColorTheme().getColor(inputActiveOptionBackground) + inputActiveOptionBorder: asCssValue(inputActiveOptionBorder), + inputActiveOptionForeground: asCssValue(inputActiveOptionForeground), + inputActiveOptionBackground: asCssValue(inputActiveOptionBackground) }); fuzzySearchToggle.onChange(() => { instantiationService.invokeFunction(showRunRecentQuickPick, instance, terminalInRunCommandPicker, type, fuzzySearchToggle.checked ? 'fuzzy' : 'contiguous', quickPick.value); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts index 82d9b1747e2..a47005a3d7f 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabsList.ts @@ -37,7 +37,6 @@ import { IEditableData } from 'vs/workbench/common/views'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; import { once } from 'vs/base/common/functional'; -import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; import { CodeDataTransfers, containsDragType } from 'vs/platform/dnd/browser/dnd'; @@ -47,6 +46,7 @@ import { IProcessDetails } from 'vs/platform/terminal/common/terminalProcess'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; import { getTerminalResourcesFromDragEvent, parseTerminalUri } from 'vs/workbench/contrib/terminal/browser/terminalUri'; import { getShellIntegrationTooltip } from 'vs/workbench/contrib/terminal/browser/terminalTooltip'; +import { defaultInputBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; const $ = DOM.$; @@ -394,9 +394,9 @@ class TerminalTabsRenderer implements IListRenderer { done(inputBox.isInputValid(), true); - }), - styler + }) ]; return toDisposable(() => { diff --git a/src/vs/workbench/contrib/terminal/browser/widgets/terminalQuickFixMenuItems.ts b/src/vs/workbench/contrib/terminal/browser/widgets/terminalQuickFixMenuItems.ts index 45761718426..32eca711af7 100644 --- a/src/vs/workbench/contrib/terminal/browser/widgets/terminalQuickFixMenuItems.ts +++ b/src/vs/workbench/contrib/terminal/browser/widgets/terminalQuickFixMenuItems.ts @@ -10,14 +10,22 @@ import { localize } from 'vs/nls'; import { ActionListItemKind, IListMenuItem } from 'vs/platform/actionWidget/browser/actionList'; import { IActionItem } from 'vs/platform/actionWidget/common/actionWidget'; +export const enum TerminalQuickFixType { + Command = 'command', + Opener = 'opener', + Port = 'port' +} + export class TerminalQuickFix implements IActionItem { action: IAction; + type: string; disabled?: boolean; title?: string; - constructor(action: IAction, title?: string, disabled?: boolean) { + constructor(action: IAction, type: string, title?: string, disabled?: boolean) { this.action = action; this.disabled = disabled; this.title = title; + this.type = type; } } @@ -28,7 +36,7 @@ export function toMenuItems(inputQuickFixes: readonly TerminalQuickFix[], showHe kind: ActionListItemKind.Header, group: { kind: CodeActionKind.QuickFix, - title: localize('codeAction.widget.id.quickfix', 'Quick Fix...') + title: localize('codeAction.widget.id.quickfix', 'Quick Fix') } }); for (const quickFix of showHeaders ? inputQuickFixes : inputQuickFixes.filter(i => !!i.action)) { @@ -50,13 +58,13 @@ export function toMenuItems(inputQuickFixes: readonly TerminalQuickFix[], showHe } function getQuickFixIcon(quickFix: TerminalQuickFix): { codicon: Codicon } { - switch (quickFix.action.id) { - case 'quickFix.opener': + switch (quickFix.type) { + case TerminalQuickFixType.Opener: // TODO: if it's a file link, use the open file icon return { codicon: Codicon.link }; - case 'quickFix.command': + case TerminalQuickFixType.Command: return { codicon: Codicon.run }; - case 'quickFix.freePort': + case TerminalQuickFixType.Port: return { codicon: Codicon.debugDisconnect }; } return { codicon: Codicon.lightBulb }; diff --git a/src/vs/workbench/contrib/terminal/browser/xterm/quickFixAddon.ts b/src/vs/workbench/contrib/terminal/browser/xterm/quickFixAddon.ts index b951f22a27b..24ff89a9536 100644 --- a/src/vs/workbench/contrib/terminal/browser/xterm/quickFixAddon.ts +++ b/src/vs/workbench/contrib/terminal/browser/xterm/quickFixAddon.ts @@ -11,8 +11,6 @@ import { asArray } from 'vs/base/common/arrays'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { AudioCue, IAudioCueService } from 'vs/workbench/contrib/audioCues/browser/audioCueService'; -import { ITerminalQuickFixProviderSelector, ITerminalQuickFixService } from 'vs/workbench/contrib/terminal/common/terminal'; import { DecorationSelector, updateLayout } from 'vs/workbench/contrib/terminal/browser/xterm/decorationStyles'; import { IDecoration, Terminal } from 'xterm'; // Importing types is safe in any layer @@ -23,12 +21,12 @@ import { ILogService } from 'vs/platform/log/common/log'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { ITerminalContributionService } from 'vs/workbench/contrib/terminal/common/terminalExtensionPoints'; -import { ITerminalQuickFixOptions, IResolvedExtensionOptions, IUnresolvedExtensionOptions, ITerminalCommandSelector, IInternalOptions, ITerminalQuickFixCommandAction, ITerminalQuickFixOpenerAction, ITerminalQuickFix } from 'vs/platform/terminal/common/terminal'; +import { AudioCue, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService'; import { IActionWidgetService } from 'vs/platform/actionWidget/browser/actionWidget'; import { ActionSet } from 'vs/platform/actionWidget/common/actionWidget'; -import { TerminalQuickFix, toMenuItems } from 'vs/workbench/contrib/terminal/browser/widgets/terminalQuickFixMenuItems'; -import { ICommandService } from 'vs/platform/commands/common/commands'; -import { previewSelectedActionCommand } from 'vs/platform/actionWidget/browser/actionList'; +import { TerminalQuickFix, TerminalQuickFixType, toMenuItems } from 'vs/workbench/contrib/terminal/browser/widgets/terminalQuickFixMenuItems'; +import { IInternalOptions, IResolvedExtensionOptions, ITerminalCommandSelector, ITerminalQuickFix, ITerminalQuickFixCommandAction, ITerminalQuickFixOpenerAction, ITerminalQuickFixOptions, IUnresolvedExtensionOptions } from 'vs/platform/terminal/common/terminal'; +import { ITerminalQuickFixProviderSelector, ITerminalQuickFixService } from 'vs/workbench/contrib/terminal/common/terminal'; const quickFixTelemetryTitle = 'terminal/quick-fix'; type QuickFixResultTelemetryEvent = { @@ -73,7 +71,6 @@ export class TerminalQuickFixAddon extends Disposable implements ITerminalAddon, constructor( private readonly _capabilities: ITerminalCapabilityStore, @ITerminalQuickFixService private readonly _quickFixService: ITerminalQuickFixService, - @ICommandService private readonly _commandService: ICommandService, @IConfigurationService private readonly _configurationService: IConfigurationService, @IAudioCueService private readonly _audioCueService: IAudioCueService, @IOpenerService private readonly _openerService: IOpenerService, @@ -261,8 +258,8 @@ export class TerminalQuickFixAddon extends Disposable implements ITerminalAddon, height: rect.height }; // TODO: What's documentation do? Need a vscode command? - const documentation = fixes.map(f => { return { id: f.id, title: f.id, tooltip: f.tooltip }; }); - const actions = fixes.map(f => new TerminalQuickFix(f, f.label)); + const documentation = fixes.map(f => { return { id: f.id, title: f.label, tooltip: f.tooltip }; }); + const actions = fixes.map(f => new TerminalQuickFix(f, f.class || TerminalQuickFixType.Command, f.label)); const actionSet = { // TODO: Documentation and actions are separate? documentation, @@ -277,13 +274,9 @@ export class TerminalQuickFixAddon extends Disposable implements ITerminalAddon, return; } const delegate = { - onSelect: async (fix: TerminalQuickFix, preview?: boolean) => { - if (preview) { - this._commandService.executeCommand(previewSelectedActionCommand); - } else { - fix.action?.run(); - this._actionWidgetService.hide(); - } + onSelect: async (fix: TerminalQuickFix) => { + fix.action?.run(); + this._actionWidgetService.hide(); }, onHide: () => { this._terminal?.focus(); @@ -349,13 +342,13 @@ export async function getQuickFixesForCommand( let action: IAction | undefined; if ('type' in quickFix) { switch (quickFix.type) { - case 'command': { + case TerminalQuickFixType.Command: { const fix = quickFix as ITerminalQuickFixCommandAction; const label = localize('quickFix.command', 'Run: {0}', fix.terminalCommand); action = { id: quickFix.id, label, - class: undefined, + class: quickFix.type, enabled: true, run: () => { onDidRequestRerunCommand?.fire({ @@ -369,13 +362,13 @@ export async function getQuickFixesForCommand( expectedCommands.push(fix.terminalCommand); break; } - case 'opener': { + case TerminalQuickFixType.Opener: { const fix = quickFix as ITerminalQuickFixOpenerAction; const label = localize('quickFix.opener', 'Open: {0}', fix.uri.toString()); action = { id: quickFix.id, label, - class: undefined, + class: quickFix.type, enabled: true, run: () => { openerService.open(fix.uri.path); diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 7304be08e3a..b276cb32519 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -8,7 +8,7 @@ import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { IProcessEnvironment, OperatingSystem } from 'vs/base/common/platform'; import { IExtensionPointDescriptor } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { IProcessDataEvent, IProcessReadyEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalLaunchError, ITerminalProfile, ITerminalProfileObject, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalIcon, TerminalLocationString, IProcessProperty, TitleEventSource, ProcessPropertyType, IFixedTerminalDimensions, IExtensionTerminalProfile, ICreateContributedTerminalProfileOptions, IProcessPropertyMap, ITerminalEnvironment, ITerminalProcessOptions, ITerminalQuickFixProvider, ITerminalCommandSelector, ITerminalOutputMatch } from 'vs/platform/terminal/common/terminal'; +import { IProcessDataEvent, IProcessReadyEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminalLaunchError, ITerminalProfile, ITerminalProfileObject, ITerminalsLayoutInfo, ITerminalsLayoutInfoById, TerminalIcon, TerminalLocationString, IProcessProperty, TitleEventSource, ProcessPropertyType, IFixedTerminalDimensions, IExtensionTerminalProfile, ICreateContributedTerminalProfileOptions, IProcessPropertyMap, ITerminalEnvironment, ITerminalProcessOptions, ITerminalContributions, ITerminalQuickFixProvider, ITerminalCommandSelector, ITerminalOutputMatch } from 'vs/platform/terminal/common/terminal'; import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; @@ -724,7 +724,7 @@ export const DEFAULT_COMMANDS_TO_SKIP_SHELL: string[] = [ 'workbench.action.toggleMaximizedPanel' ]; -export const terminalContributionsDescriptor: IExtensionPointDescriptor = { +export const terminalContributionsDescriptor: IExtensionPointDescriptor = { extensionPoint: 'terminal', defaultExtensionKind: ['workspace'], jsonSchema: { diff --git a/src/vs/workbench/contrib/terminal/test/browser/quickFixAddon.test.ts b/src/vs/workbench/contrib/terminal/test/browser/quickFixAddon.test.ts index 7dd21068e2d..898fc8b2be1 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/quickFixAddon.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/quickFixAddon.test.ts @@ -258,7 +258,7 @@ suite('QuickFixAddon', () => { Branch 'test22' set up to track remote branch 'test22' from 'origin'. `; const exitCode = 0; const actions = [{ - id: `quickFix.opener`, + id: 'Git Create Pr', enabled: true, label: 'Open: https://github.com/meganrogge/xterm.js/pull/new/test22', tooltip: 'Open: https://github.com/meganrogge/xterm.js/pull/new/test22', diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.css b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.css new file mode 100644 index 00000000000..0daecae72f2 --- /dev/null +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.css @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-editor .test-output-peek .test-output-peek-tree { + background-color: var(--vscode-peekViewResult-background); + color: var(--vscode-peekViewResult-lineForeground); +} + +.monaco-editor .test-output-peek .test-output-peek-tree .monaco-list:focus .monaco-list-rows > .monaco-list-row.selected:not(.highlighted) { + background-color: var(--vscode-peekViewResult-selectionBackground); + color: var(--vscode-peekViewResult-selectionForeground) !important; +} + +.monaco-editor .test-output-peek .test-output-peek-message-container a { + color: var(--vscode-textLink-foreground); +} + +.monaco-editor .test-output-peek .test-output-peek-message-container a :hover { + color: var(--vscode-textLink-activeForeground); +} diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 29009105d1a..c918579d28b 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -40,7 +40,7 @@ import { IEditorContribution, ScrollType } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; import { MarkdownRenderer } from 'vs/editor/contrib/markdownRenderer/browser/markdownRenderer'; -import { getOuterEditor, IPeekViewService, peekViewResultsBackground, peekViewResultsMatchForeground, peekViewResultsSelectionBackground, peekViewResultsSelectionForeground, peekViewTitleForeground, peekViewTitleInfoForeground, PeekViewWidget } from 'vs/editor/contrib/peekView/browser/peekView'; +import { getOuterEditor, IPeekViewService, peekViewTitleForeground, peekViewTitleInfoForeground, PeekViewWidget } from 'vs/editor/contrib/peekView/browser/peekView'; import { localize } from 'vs/nls'; import { createAndFillInActionBarActions, MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenuService, MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; @@ -53,8 +53,7 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { WorkbenchCompressibleObjectTree } from 'vs/platform/list/browser/listService'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; -import { IColorTheme, IThemeService, registerThemingParticipant, ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { IColorTheme, IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { Categories } from 'vs/platform/action/common/actionCommonCategories'; import { EditorModel } from 'vs/workbench/common/editor/editorModel'; import { flatTestItemDelimiter } from 'vs/workbench/contrib/testing/browser/explorerProjections/display'; @@ -77,6 +76,7 @@ import { ITestResultService, ResultChangeEvent } from 'vs/workbench/contrib/test import { ITestService } from 'vs/workbench/contrib/testing/common/testService'; import { IRichLocation, ITestErrorMessage, ITestItem, ITestMessage, ITestRunTask, ITestTaskState, TestMessageType, TestResultItem, TestResultState, TestRunProfileBitset } from 'vs/workbench/contrib/testing/common/testTypes'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import 'vs/css!./testingOutputPeek'; class TestDto { public readonly test: ITestItem; @@ -1621,35 +1621,6 @@ class TreeActionsProvider { } } -registerThemingParticipant((theme, collector) => { - const resultsBackground = theme.getColor(peekViewResultsBackground); - if (resultsBackground) { - collector.addRule(`.monaco-editor .test-output-peek .test-output-peek-tree { background-color: ${resultsBackground}; }`); - } - const resultsMatchForeground = theme.getColor(peekViewResultsMatchForeground); - if (resultsMatchForeground) { - collector.addRule(`.monaco-editor .test-output-peek .test-output-peek-tree { color: ${resultsMatchForeground}; }`); - } - const resultsSelectedBackground = theme.getColor(peekViewResultsSelectionBackground); - if (resultsSelectedBackground) { - collector.addRule(`.monaco-editor .test-output-peek .test-output-peek-tree .monaco-list:focus .monaco-list-rows > .monaco-list-row.selected:not(.highlighted) { background-color: ${resultsSelectedBackground}; }`); - } - const resultsSelectedForeground = theme.getColor(peekViewResultsSelectionForeground); - if (resultsSelectedForeground) { - collector.addRule(`.monaco-editor .test-output-peek .test-output-peek-tree .monaco-list:focus .monaco-list-rows > .monaco-list-row.selected:not(.highlighted) { color: ${resultsSelectedForeground} !important; }`); - } - - const textLinkForegroundColor = theme.getColor(textLinkForeground); - if (textLinkForegroundColor) { - collector.addRule(`.monaco-editor .test-output-peek .test-output-peek-message-container a { color: ${textLinkForegroundColor}; }`); - } - - const textLinkActiveForegroundColor = theme.getColor(textLinkActiveForeground); - if (textLinkActiveForegroundColor) { - collector.addRule(`.monaco-editor .test-output-peek .test-output-peek-message-container a :hover { color: ${textLinkActiveForegroundColor}; }`); - } -}); - const navWhen = ContextKeyExpr.and( EditorContextKeys.focus, TestingContextKeys.isPeekVisible, diff --git a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts index 546d3277dd6..d5b8b668319 100644 --- a/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts +++ b/src/vs/workbench/contrib/userDataProfile/browser/userDataProfile.ts @@ -19,18 +19,17 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { RenameProfileAction } from 'vs/workbench/contrib/userDataProfile/browser/userDataProfileActions'; import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { CURRENT_PROFILE_CONTEXT, HAS_PROFILES_CONTEXT, isUserDataProfileTemplate, IS_CURRENT_PROFILE_TRANSIENT_CONTEXT, IUserDataProfileImportExportService, IUserDataProfileManagementService, IUserDataProfileService, IUserDataProfileTemplate, ManageProfilesSubMenu, PROFILES_CATEGORY, PROFILES_ENABLEMENT_CONTEXT, PROFILES_TTILE, PROFILE_EXTENSION, PROFILE_FILTER } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { CURRENT_PROFILE_CONTEXT, HAS_PROFILES_CONTEXT, isUserDataProfileTemplate, IS_CURRENT_PROFILE_TRANSIENT_CONTEXT, IS_PROFILE_IMPORT_EXPORT_IN_PROGRESS_CONTEXT, IUserDataProfileImportExportService, IUserDataProfileManagementService, IUserDataProfileService, IUserDataProfileTemplate, ManageProfilesSubMenu, PROFILES_CATEGORY, PROFILES_ENABLEMENT_CONTEXT, PROFILES_TTILE, PROFILE_FILTER } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { charCount } from 'vs/base/common/strings'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { joinPath } from 'vs/base/common/resources'; import { Codicon } from 'vs/base/common/codicons'; import { IFileService } from 'vs/platform/files/common/files'; import { asJson, asText, IRequestService } from 'vs/platform/request/common/request'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { URI } from 'vs/base/common/uri'; export class UserDataProfilesWorkbenchContribution extends Disposable implements IWorkbenchContribution { @@ -262,6 +261,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements original: `Export (${that.userDataProfileService.currentProfile.name})...` }, category: PROFILES_CATEGORY, + precondition: IS_PROFILE_IMPORT_EXPORT_IN_PROGRESS_CONTEXT.toNegated(), menu: [ { id: ManageProfilesSubMenu, @@ -276,25 +276,8 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements } async run(accessor: ServicesAccessor) { - const textFileService = accessor.get(ITextFileService); - const fileDialogService = accessor.get(IFileDialogService); const userDataProfileImportExportService = accessor.get(IUserDataProfileImportExportService); - const notificationService = accessor.get(INotificationService); - - const profileLocation = await fileDialogService.showSaveDialog({ - title: localize('export profile dialog', "Save Profile"), - filters: PROFILE_FILTER, - defaultUri: joinPath(await fileDialogService.defaultFilePath(), `profile.${PROFILE_EXTENSION}`), - }); - - if (!profileLocation) { - return; - } - - const profile = await userDataProfileImportExportService.exportProfile({ skipComments: true }); - await textFileService.create([{ resource: profileLocation, value: JSON.stringify(profile), options: { overwrite: true } }]); - - notificationService.info(localize('export success', "{0}: Exported successfully.", PROFILES_CATEGORY.value)); + return userDataProfileImportExportService.exportProfile(); } })); disposables.add(MenuRegistry.appendMenuItem(MenuId.MenubarShare, { @@ -323,6 +306,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements }, category: PROFILES_CATEGORY, f1: true, + precondition: IS_PROFILE_IMPORT_EXPORT_IN_PROGRESS_CONTEXT.toNegated(), menu: [ { id: ManageProfilesSubMenu, @@ -358,11 +342,11 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements const disposables = new DisposableStore(); const quickPick = disposables.add(quickInputService.createQuickPick()); const updateQuickPickItems = (value?: string) => { - const selectFromFileItem: IQuickPickItem = { label: isSettingProfilesEnabled ? localize('select from file', "Select Profile template file") : localize('import from file', "Import from profile file") }; - quickPick.items = value ? [{ label: isSettingProfilesEnabled ? localize('select from url', "Create from template URL") : localize('import from url', "Import from URL"), description: quickPick.value }, selectFromFileItem] : [selectFromFileItem]; + const selectFromFileItem: IQuickPickItem = { label: localize('import from file', "Import from profile file") }; + quickPick.items = value ? [{ label: localize('import from url', "Import from URL"), description: quickPick.value }, selectFromFileItem] : [selectFromFileItem]; }; - quickPick.title = isSettingProfilesEnabled ? localize('create from profile template quick pick title', "Create from Profile Template") : localize('import profile quick pick title', "Import Settings from a Profile"); - quickPick.placeholder = isSettingProfilesEnabled ? localize('create from profile template placeholder', "Provide a template URL or Select a template file") : localize('import profile placeholder', "Provide profile URL or select profile file to import"); + quickPick.title = localize('import profile quick pick title', "Import Profile"); + quickPick.placeholder = localize('import profile placeholder', "Provide profile URL or select profile file to import"); quickPick.ignoreFocusOut = true; disposables.add(quickPick.onDidChangeValue(updateQuickPickItems)); updateQuickPickItems(); @@ -371,11 +355,14 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements disposables.add(quickPick.onDidAccept(async () => { try { quickPick.hide(); - const profile = quickPick.selectedItems[0].description ? await this.getProfileFromURL(quickPick.value, requestService) : await this.getProfileFromFileSystem(fileDialogService, fileService); - if (profile) { - if (isSettingProfilesEnabled) { + if (isSettingProfilesEnabled) { + const profile = quickPick.selectedItems[0].description ? URI.parse(quickPick.value) : await this.getProfileUriFromFileSystem(fileDialogService); + if (profile) { await userDataProfileImportExportService.importProfile(profile); - } else { + } + } else { + const profile = quickPick.selectedItems[0].description ? await this.getProfileFromURL(quickPick.value, requestService) : await this.getProfileFromFileSystem(fileDialogService, fileService); + if (profile) { await userDataProfileImportExportService.setProfile(profile); } } @@ -387,7 +374,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements quickPick.show(); } - private async getProfileFromFileSystem(fileDialogService: IFileDialogService, fileService: IFileService): Promise { + private async getProfileUriFromFileSystem(fileDialogService: IFileDialogService): Promise { const profileLocation = await fileDialogService.showOpenDialog({ canSelectFolders: false, canSelectFiles: true, @@ -398,7 +385,15 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements if (!profileLocation) { return null; } - const content = (await fileService.readFile(profileLocation[0])).value.toString(); + return profileLocation[0]; + } + + private async getProfileFromFileSystem(fileDialogService: IFileDialogService, fileService: IFileService): Promise { + const profileLocation = await this.getProfileUriFromFileSystem(fileDialogService); + if (!profileLocation) { + return null; + } + const content = (await fileService.readFile(profileLocation)).value.toString(); const parsed = JSON.parse(content); return isUserDataProfileTemplate(parsed) ? parsed : null; } diff --git a/src/vs/workbench/contrib/watermark/browser/watermark.ts b/src/vs/workbench/contrib/watermark/browser/watermark.ts index c2bc7aa63ef..30c71634a23 100644 --- a/src/vs/workbench/contrib/watermark/browser/watermark.ts +++ b/src/vs/workbench/contrib/watermark/browser/watermark.ts @@ -31,7 +31,7 @@ import { NEW_UNTITLED_FILE_COMMAND_ID } from 'vs/workbench/contrib/files/browser import { DEBUG_START_COMMAND_ID } from 'vs/workbench/contrib/debug/browser/debugCommands'; import { ContextKeyExpression, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey'; -import { getKeybindingLabelStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { defaultKeybindingLabelStyles } from 'vs/platform/theme/browser/defaultStyles'; const $ = dom.$; @@ -161,7 +161,7 @@ export class WatermarkContribution extends Disposable implements IWorkbenchContr const dt = dom.append(dl, $('dt')); dt.textContent = entry.text; const dd = dom.append(dl, $('dd')); - const keybinding = new KeybindingLabel(dd, OS, { renderUnboundKeybindings: true, ...getKeybindingLabelStyles() }); + const keybinding = new KeybindingLabel(dd, OS, { renderUnboundKeybindings: true, ...defaultKeybindingLabelStyles }); keybinding.set(this.keybindingService.lookupKeybinding(entry.id)); }); }; diff --git a/src/vs/workbench/contrib/webview/browser/overlayWebview.ts b/src/vs/workbench/contrib/webview/browser/overlayWebview.ts index 7bc380d9818..cf1e9a61f19 100644 --- a/src/vs/workbench/contrib/webview/browser/overlayWebview.ts +++ b/src/vs/workbench/contrib/webview/browser/overlayWebview.ts @@ -6,23 +6,20 @@ import { Dimension } from 'vs/base/browser/dom'; import { FastDomNode } from 'vs/base/browser/fastDomNode'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; -import { Emitter, Event } from 'vs/base/common/event'; +import { Emitter } from 'vs/base/common/event'; import { Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; -import { IWebviewService, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_ENABLED, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE, IWebview, WebviewContentOptions, IWebviewElement, WebviewExtensionDescription, WebviewMessageReceivedEvent, WebviewOptions, IOverlayWebview, WebviewInitInfo } from 'vs/workbench/contrib/webview/browser/webview'; +import { IOverlayWebview, IWebview, IWebviewElement, IWebviewService, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_ENABLED, KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE, WebviewContentOptions, WebviewExtensionDescription, WebviewInitInfo, WebviewMessageReceivedEvent, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview'; /** * Webview that is absolutely positioned over another element and that can creates and destroys an underlying webview as needed. */ export class OverlayWebview extends Disposable implements IOverlayWebview { - private readonly _onDidWheel = this._register(new Emitter()); - public readonly onDidWheel = this._onDidWheel.event; - private _isFirstLoad = true; private readonly _firstLoadPendingMessages = new Set<{ readonly message: any; readonly transfer?: readonly ArrayBuffer[]; readonly resolve: (value: boolean) => void }>(); private readonly _webview = this._register(new MutableDisposable()); @@ -91,10 +88,9 @@ export class OverlayWebview extends Disposable implements IOverlayWebview { super.dispose(); } - public get container(): HTMLElement { if (this._isDisposed) { - throw new Error(`DynamicWebviewEditorOverlay has been disposed`); + throw new Error(`OverlayWebview has been disposed`); } if (!this._container) { @@ -186,7 +182,7 @@ export class OverlayWebview extends Disposable implements IOverlayWebview { private _show() { if (this._isDisposed) { - throw new Error('Webview overlay is disposed'); + throw new Error('OverlayWebview is disposed'); } if (!this._webview.value) { @@ -294,28 +290,31 @@ export class OverlayWebview extends Disposable implements IOverlayWebview { } private readonly _onDidFocus = this._register(new Emitter()); - public readonly onDidFocus: Event = this._onDidFocus.event; + public readonly onDidFocus = this._onDidFocus.event; private readonly _onDidBlur = this._register(new Emitter()); - public readonly onDidBlur: Event = this._onDidBlur.event; + public readonly onDidBlur = this._onDidBlur.event; private readonly _onDidClickLink = this._register(new Emitter()); - public readonly onDidClickLink: Event = this._onDidClickLink.event; + public readonly onDidClickLink = this._onDidClickLink.event; private readonly _onDidReload = this._register(new Emitter()); public readonly onDidReload = this._onDidReload.event; - private readonly _onDidScroll = this._register(new Emitter<{ scrollYPercentage: number }>()); - public readonly onDidScroll: Event<{ scrollYPercentage: number }> = this._onDidScroll.event; + private readonly _onDidScroll = this._register(new Emitter<{ readonly scrollYPercentage: number }>()); + public readonly onDidScroll = this._onDidScroll.event; private readonly _onDidUpdateState = this._register(new Emitter()); - public readonly onDidUpdateState: Event = this._onDidUpdateState.event; + public readonly onDidUpdateState = this._onDidUpdateState.event; private readonly _onMessage = this._register(new Emitter()); public readonly onMessage = this._onMessage.event; private readonly _onMissingCsp = this._register(new Emitter()); - public readonly onMissingCsp: Event = this._onMissingCsp.event; + public readonly onMissingCsp = this._onMissingCsp.event; + + private readonly _onDidWheel = this._register(new Emitter()); + public readonly onDidWheel = this._onDidWheel.event; public async postMessage(message: any, transfer?: readonly ArrayBuffer[]): Promise { if (this._webview.value) { diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index 5f8f9e3ba2d..8634a43a6e3 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -350,7 +350,6 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD if (initInfo.options.enableFindWidget) { this._webviewFindWidget = this._register(instantiationService.createInstance(WebviewFindWidget, this)); - this.styledFindWidget(); } this._encodedWebviewOriginPromise.then(encodedWebviewOrigin => { @@ -668,13 +667,8 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD const screenReader = this._accessibilityService.isScreenReaderOptimized(); this._send('styles', { styles, activeTheme, themeId, themeLabel, reduceMotion, screenReader }); - - this.styledFindWidget(); } - private styledFindWidget() { - this._webviewFindWidget?.updateTheme(this.webviewThemeDataProvider.getTheme()); - } protected handleFocusChange(isFocused: boolean): void { this._focused = isFocused; diff --git a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts index 001d34a2fad..e6e015a1bce 100644 --- a/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcomeGettingStarted/browser/gettingStarted.ts @@ -68,7 +68,7 @@ import { restoreWalkthroughsConfigurationKey, RestoreWalkthroughsConfigurationVa import { GettingStartedDetailsRenderer } from 'vs/workbench/contrib/welcomeGettingStarted/browser/gettingStartedDetailsRenderer'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { renderLabelWithIcons } from 'vs/base/browser/ui/iconLabel/iconLabels'; -import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { defaultButtonStyles, defaultToggleStyles } from 'vs/platform/theme/browser/defaultStyles'; const SLIDE_TRANSITION_TIME_MS = 250; const configurationKey = 'workbench.startupEditor'; @@ -730,6 +730,7 @@ export class GettingStartedPage extends EditorPane { actionClassName: 'getting-started-checkbox', isChecked: this.configurationService.getValue(configurationKey) === 'welcomePage', title: localize('checkboxTitle', "When checked, this page will be shown on startup."), + ...defaultToggleStyles }); showOnStartupCheckbox.domNode.id = 'showOnStartup'; const showOnStartupLabel = $('label.caption', { for: 'showOnStartup' }, localize('welcomePage.showOnStartup', "Show welcome page on startup")); diff --git a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts index 6e17fd9d353..99d4f988f14 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts @@ -35,7 +35,7 @@ import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { buttonBackground, buttonSecondaryBackground, editorErrorForeground } from 'vs/platform/theme/common/colorRegistry'; import { ISingleFolderWorkspaceIdentifier, IWorkspaceContextService, toWorkspaceIdentifier, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { attachInputBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; +import { attachStylerCallback } from 'vs/platform/theme/common/styler'; import { IThemeService, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; @@ -55,7 +55,7 @@ import { hasDriveLetter, toSlashes } from 'vs/base/common/extpath'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IProductService } from 'vs/platform/product/common/productService'; import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; -import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { defaultButtonStyles, defaultInputBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; export const shieldIcon = registerIcon('workspace-trust-banner', Codicon.shield, localize('shieldIcon', 'Icon for workspace trust ion the banner.')); @@ -473,8 +473,7 @@ class TrustedUriPathColumnRenderer implements ITableRenderer this.table.validateUri(value, this.currentItem) - } + }, + inputBoxStyles: defaultInputBoxStyles }); const disposables = new DisposableStore(); - disposables.add(attachInputBoxStyler(pathInput, this.themeService)); - const renderDisposables = disposables.add(new DisposableStore()); return { diff --git a/src/vs/workbench/electron-sandbox/desktop.contribution.ts b/src/vs/workbench/electron-sandbox/desktop.contribution.ts index d2a4a62970b..125e0a8a3c0 100644 --- a/src/vs/workbench/electron-sandbox/desktop.contribution.ts +++ b/src/vs/workbench/electron-sandbox/desktop.contribution.ts @@ -207,7 +207,7 @@ import product from 'vs/platform/product/common/product'; }, 'window.experimental.windowControlsOverlay.enabled': { 'type': 'boolean', - 'default': false, + 'default': product.quality === 'insider' || product.quality === 'exploration', // switch back to true when app.getLocale() isn't used anymore. 'scope': ConfigurationScope.APPLICATION, 'description': localize('windowControlsOverlay', "Use window controls provided by the platform instead of our HTML-based window controls. Changes require a full restart to apply."), 'included': isWindows diff --git a/src/vs/workbench/electron-sandbox/parts/dialogs/dialog.contribution.ts b/src/vs/workbench/electron-sandbox/parts/dialogs/dialog.contribution.ts index 60eb43ec230..8a2558d6877 100644 --- a/src/vs/workbench/electron-sandbox/parts/dialogs/dialog.contribution.ts +++ b/src/vs/workbench/electron-sandbox/parts/dialogs/dialog.contribution.ts @@ -12,7 +12,6 @@ import { ILogService } from 'vs/platform/log/common/log'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { IProductService } from 'vs/platform/product/common/productService'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { IDialogsModel, IDialogViewItem } from 'vs/workbench/common/dialogs'; import { BrowserDialogHandler } from 'vs/workbench/browser/parts/dialogs/dialogHandler'; @@ -34,7 +33,6 @@ export class DialogHandlerContribution extends Disposable implements IWorkbenchC @IDialogService private dialogService: IDialogService, @ILogService logService: ILogService, @ILayoutService layoutService: ILayoutService, - @IThemeService themeService: IThemeService, @IKeybindingService keybindingService: IKeybindingService, @IInstantiationService instantiationService: IInstantiationService, @IProductService productService: IProductService, @@ -43,7 +41,7 @@ export class DialogHandlerContribution extends Disposable implements IWorkbenchC ) { super(); - this.browserImpl = new BrowserDialogHandler(logService, layoutService, themeService, keybindingService, instantiationService, productService, clipboardService); + this.browserImpl = new BrowserDialogHandler(logService, layoutService, keybindingService, instantiationService, productService, clipboardService); this.nativeImpl = new NativeDialogHandler(logService, nativeHostService, productService, clipboardService); this.model = (this.dialogService as DialogService).model; diff --git a/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts b/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts index 35899ccaa86..e4144ba1fec 100644 --- a/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/electron-sandbox/parts/titlebar/titlebarPart.ts @@ -22,6 +22,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { Codicon } from 'vs/base/common/codicons'; import { NativeMenubarControl } from 'vs/workbench/electron-sandbox/parts/titlebar/menubarControl'; import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; +import { IProductService } from 'vs/platform/product/common/productService'; export class TitlebarPart extends BrowserTitleBarPart { private maxRestoreControl: HTMLElement | undefined; @@ -58,6 +59,7 @@ export class TitlebarPart extends BrowserTitleBarPart { @IConfigurationService configurationService: IConfigurationService, @INativeWorkbenchEnvironmentService environmentService: INativeWorkbenchEnvironmentService, @IInstantiationService instantiationService: IInstantiationService, + @IProductService productService: IProductService, @IThemeService themeService: IThemeService, @IStorageService storageService: IStorageService, @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, @@ -66,7 +68,7 @@ export class TitlebarPart extends BrowserTitleBarPart { @INativeHostService private readonly nativeHostService: INativeHostService, @IHoverService hoverService: IHoverService, ) { - super(contextMenuService, configurationService, environmentService, instantiationService, themeService, storageService, layoutService, contextKeyService, hostService, hoverService); + super(contextMenuService, configurationService, environmentService, instantiationService, productService, themeService, storageService, layoutService, contextKeyService, hostService, hoverService); this.environmentService = environmentService; } @@ -216,7 +218,7 @@ export class TitlebarPart extends BrowserTitleBarPart { super.updateStyles(); // WCO styles only supported on Windows currently - if (useWindowControlsOverlay(this.configurationService)) { + if (useWindowControlsOverlay(this.configurationService, this.productService)) { if (!this.cachedWindowControlStyles || this.cachedWindowControlStyles.bgColor !== this.element.style.backgroundColor || this.cachedWindowControlStyles.fgColor !== this.element.style.color) { @@ -228,7 +230,7 @@ export class TitlebarPart extends BrowserTitleBarPart { override layout(width: number, height: number): void { super.layout(width, height); - if (useWindowControlsOverlay(this.configurationService) || + if (useWindowControlsOverlay(this.configurationService, this.productService) || (isMacintosh && isNative && getTitleBarStyle(this.configurationService) === 'custom')) { // When the user goes into full screen mode, the height of the title bar becomes 0. // Instead, set it back to the default titlebar height for Catalina users diff --git a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts index 4e2614f007c..fe81a6da6bc 100644 --- a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts +++ b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts @@ -159,6 +159,13 @@ const apiMenus: IAPIMenu[] = [ description: localize('commentThread.actions', "The contributed comment thread context menu, rendered as buttons below the comment editor"), supportsSubmenus: false }, + { + key: 'comments/commentThread/additionalActions', + id: MenuId.CommentThreadAdditionalActions, + description: localize('commentThread.actions', "The contributed comment thread context menu, rendered as buttons below the comment editor"), + supportsSubmenus: false, + proposed: 'contribCommentThreadAdditionalMenu' + }, { key: 'comments/commentThread/title/context', id: MenuId.CommentThreadTitleContext, @@ -623,7 +630,14 @@ const _commandRegistrations = new DisposableStore(); export const commandsExtensionPoint = ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'commands', - jsonSchema: schema.commandsContribution + jsonSchema: schema.commandsContribution, + activationEventsGenerator: (contribs: schema.IUserFriendlyCommand[], result: { push(item: string): void }) => { + for (const contrib of contribs) { + if (contrib.command) { + result.push(`onCommand:${contrib.command}`); + } + } + } }); commandsExtensionPoint.setHandler(extensions => { diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index 9447f7882f7..17b1d82de09 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -111,9 +111,6 @@ export class BrowserWorkbenchEnvironmentService implements IBrowserWorkbenchEnvi @memoize get editSessionsLogResource(): URI { return joinPath(this.logsHome, 'editSessions.log'); } - @memoize - get remoteTunnelLogResource(): URI { return joinPath(this.logsHome, 'remoteTunnel.log'); } - @memoize get sync(): 'on' | 'off' | undefined { return undefined; } diff --git a/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts index 7e797a7bc1d..21a1cd4c3dc 100644 --- a/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService.ts @@ -15,6 +15,7 @@ import { IExtensionResourceLoaderService } from 'vs/platform/extensionResourceLo import { IProductService } from 'vs/platform/product/common/productService'; import { ITranslations, localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls'; import { ILogService } from 'vs/platform/log/common/log'; +import { ImplicitActivationEvents } from 'vs/platform/extensionManagement/common/implicitActivationEvents'; interface IBundledExtension { extensionPath: string; @@ -74,6 +75,7 @@ export class BuiltinExtensionsScannerService implements IBuiltinExtensionsScanne } browserNlsBundleUris.en = uriIdentityService.extUri.resolvePath(builtinExtensionsServiceUrl!, e.browserNlsMetadataPath); } + ImplicitActivationEvents.updateManifest(e.packageJSON); return { identifier: { id }, location: uriIdentityService.extUri.joinPath(builtinExtensionsServiceUrl!, e.extensionPath), diff --git a/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts index 794aef50c5a..8f897e7bf78 100644 --- a/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/webExtensionsScannerService.ts @@ -43,6 +43,7 @@ import { IStringDictionary } from 'vs/base/common/collections'; import { IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; import { IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { ImplicitActivationEvents } from 'vs/platform/extensionManagement/common/implicitActivationEvents'; type GalleryExtensionInfo = { readonly id: string; preRelease?: boolean; migrateStorageFrom?: string }; type ExtensionInfo = { readonly id: string; preRelease: boolean }; @@ -662,6 +663,8 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten }; } + ImplicitActivationEvents.updateManifest(manifest); + const packageNLSUri = webExtension.packageNLSUris?.get(Language.value().toLowerCase()); if (packageNLSUri || webExtension.fallbackPackageNLSUri) { manifest = packageNLSUri diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService.ts index 6d2bf5f714b..5485b67400b 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/nativeExtensionManagementService.ts @@ -68,22 +68,25 @@ export class NativeExtensionManagementService extends ExtensionManagementChannel override async install(vsix: URI, options?: InstallVSIXOptions): Promise { const { location, cleanup } = await this.downloadVsix(vsix); try { - return await super.install(location, { ...options, profileLocation: this.userDataProfileService.currentProfile.extensionsResource }); + options = options?.profileLocation ? options : { ...options, profileLocation: this.userDataProfileService.currentProfile.extensionsResource }; + return await super.install(location, options); } finally { await cleanup(); } } override installFromGallery(extension: IGalleryExtension, installOptions?: InstallOptions): Promise { - return super.installFromGallery(extension, { ...installOptions, profileLocation: this.userDataProfileService.currentProfile.extensionsResource }); + installOptions = installOptions?.profileLocation ? installOptions : { ...installOptions, profileLocation: this.userDataProfileService.currentProfile.extensionsResource }; + return super.installFromGallery(extension, installOptions); } override uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise { - return super.uninstall(extension, { ...options, profileLocation: this.userDataProfileService.currentProfile.extensionsResource }); + options = options?.profileLocation ? options : { ...options, profileLocation: this.userDataProfileService.currentProfile.extensionsResource }; + return super.uninstall(extension, options); } - override getInstalled(type: ExtensionType | null = null): Promise { - return super.getInstalled(type, this.userDataProfileService.currentProfile.extensionsResource); + override getInstalled(type: ExtensionType | null = null, profileLocation: URI = this.userDataProfileService.currentProfile.extensionsResource): Promise { + return super.getInstalled(type, profileLocation); } private async downloadVsix(vsix: URI): Promise<{ location: URI; cleanup: () => Promise }> { diff --git a/src/vs/workbench/services/extensions/common/extensionHostManager.ts b/src/vs/workbench/services/extensions/common/extensionHostManager.ts index bfbc03d4f9c..6dd0f2be6df 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostManager.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostManager.ts @@ -165,7 +165,7 @@ class ExtensionHostManager extends Disposable implements IExtensionHostManager { if (err && err.stack) { failureTelemetryEvent.errorStack = err.stack; } - this._telemetryService.publicLog2('extensionHostStartup', failureTelemetryEvent, true); + this._telemetryService.publicLog2('extensionHostStartup', failureTelemetryEvent); return null; } diff --git a/src/vs/workbench/services/extensions/common/extensions.ts b/src/vs/workbench/services/extensions/common/extensions.ts index ef4924a0ef1..a524523645b 100644 --- a/src/vs/workbench/services/extensions/common/extensions.ts +++ b/src/vs/workbench/services/extensions/common/extensions.ts @@ -556,12 +556,6 @@ export interface IExtensionService { * @param env New properties for the remote extension host */ setRemoteEnvironment(env: { [key: string]: string | null }): Promise; - - /** - * Please do not use! - * (This is public such that the extension host process can coordinate with and call back in the IExtensionService) - */ - _activateById(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise; } export interface IInternalExtensionService { @@ -626,5 +620,4 @@ export class NullExtensionService implements IExtensionService { async setRemoteEnvironment(_env: { [key: string]: string | null }): Promise { } canAddExtension(): boolean { return false; } canRemoveExtension(): boolean { return false; } - _activateById(_extensionId: ExtensionIdentifier, _reason: ExtensionActivationReason): Promise { return Promise.resolve(); } } diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index a7c615b1eb7..d96e8590531 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -10,6 +10,7 @@ export const allApiProposals = Object.freeze({ codiconDecoration: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.codiconDecoration.d.ts', commentsResolvedState: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.commentsResolvedState.d.ts', contribCommentPeekContext: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentPeekContext.d.ts', + contribCommentThreadAdditionalMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribCommentThreadAdditionalMenu.d.ts', contribEditSessions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribEditSessions.d.ts', contribEditorContentMenu: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribEditorContentMenu.d.ts', contribLabelFormatterWorkspaceTooltip: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.contribLabelFormatterWorkspaceTooltip.d.ts', diff --git a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts index 9354ae6817d..9b21eb75568 100644 --- a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts +++ b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts @@ -15,6 +15,7 @@ import { ExtensionIdentifier, IExtensionDescription, EXTENSION_CATEGORIES } from import { ExtensionKind } from 'vs/platform/environment/common/environment'; import { allApiProposals } from 'vs/workbench/services/extensions/common/extensionsApiProposals'; import { productSchemaId } from 'vs/platform/product/common/productService'; +import { ImplicitActivationEvents, IActivationEventsGenerator } from 'vs/platform/extensionManagement/common/implicitActivationEvents'; const schemaRegistry = Registry.as(Extensions.JSONContribution); @@ -564,23 +565,33 @@ export const schema: IJSONSchema = { } }; -export interface IExtensionPointDescriptor { +export type toArray = T extends Array ? T : T[]; + +export interface IExtensionPointDescriptor { extensionPoint: string; deps?: IExtensionPoint[]; jsonSchema: IJSONSchema; defaultExtensionKind?: ExtensionKind[]; + /** + * A function which runs before the extension point has been validated and which + * can should collect automatic activation events from the contribution. + */ + activationEventsGenerator?: IActivationEventsGenerator>; } export class ExtensionsRegistryImpl { private readonly _extensionPoints = new Map>(); - public registerExtensionPoint(desc: IExtensionPointDescriptor): IExtensionPoint { + public registerExtensionPoint(desc: IExtensionPointDescriptor): IExtensionPoint { if (this._extensionPoints.has(desc.extensionPoint)) { throw new Error('Duplicate extension point: ' + desc.extensionPoint); } const result = new ExtensionPoint(desc.extensionPoint, desc.defaultExtensionKind); this._extensionPoints.set(desc.extensionPoint, result); + if (desc.activationEventsGenerator) { + ImplicitActivationEvents.register(desc.extensionPoint, desc.activationEventsGenerator); + } schema.properties!['contributes'].properties![desc.extensionPoint] = desc.jsonSchema; schemaRegistry.registerSchema(schemaId, schema); diff --git a/src/vs/workbench/services/progress/browser/progressService.ts b/src/vs/workbench/services/progress/browser/progressService.ts index 2e423fc7c1a..beb4c52bde1 100644 --- a/src/vs/workbench/services/progress/browser/progressService.ts +++ b/src/vs/workbench/services/progress/browser/progressService.ts @@ -17,8 +17,6 @@ import { Event, Emitter } from 'vs/base/common/event'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { Dialog } from 'vs/base/browser/ui/dialog/dialog'; -import { attachDialogStyler } from 'vs/platform/theme/common/styler'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { EventHelper } from 'vs/base/browser/dom'; @@ -26,7 +24,7 @@ import { parseLinkedText } from 'vs/base/common/linkedText'; import { IViewsService, IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { stripIcons } from 'vs/base/common/iconLabels'; -import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { defaultButtonStyles, defaultCheckboxStyles, defaultDialogStyles, defaultInputBoxStyles } from 'vs/platform/theme/browser/defaultStyles'; export class ProgressService extends Disposable implements IProgressService { @@ -40,7 +38,6 @@ export class ProgressService extends Disposable implements IProgressService { @INotificationService private readonly notificationService: INotificationService, @IStatusbarService private readonly statusbarService: IStatusbarService, @ILayoutService private readonly layoutService: ILayoutService, - @IThemeService private readonly themeService: IThemeService, @IKeybindingService private readonly keybindingService: IKeybindingService ) { super(); @@ -565,12 +562,14 @@ export class ProgressService extends Disposable implements IProgressService { } } }, - buttonStyles: defaultButtonStyles + buttonStyles: defaultButtonStyles, + checkboxStyles: defaultCheckboxStyles, + inputBoxStyles: defaultInputBoxStyles, + dialogStyles: defaultDialogStyles } ); disposables.add(dialog); - disposables.add(attachDialogStyler(dialog, this.themeService)); dialog.show().then(dialogResult => { onDidCancel?.(dialogResult.button); diff --git a/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts b/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts index 9f0841a6310..884d1d28ef7 100644 --- a/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts +++ b/src/vs/workbench/services/remote/common/abstractRemoteAgentService.ts @@ -20,6 +20,7 @@ import { ITelemetryData, TelemetryLevel } from 'vs/platform/telemetry/common/tel import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { IProductService } from 'vs/platform/product/common/productService'; import { URI } from 'vs/base/common/uri'; +import { ImplicitActivationEvents } from 'vs/platform/extensionManagement/common/implicitActivationEvents'; export abstract class AbstractRemoteAgentService extends Disposable implements IRemoteAgentService { @@ -85,14 +86,24 @@ export abstract class AbstractRemoteAgentService extends Disposable implements I scanExtensions(skipExtensions: ExtensionIdentifier[] = []): Promise { return this._withChannel( - (channel, connection) => RemoteExtensionEnvironmentChannelClient.scanExtensions(channel, connection.remoteAuthority, this._environmentService.extensionDevelopmentLocationURI, skipExtensions), + async (channel, connection) => { + const scannedExtensions = await RemoteExtensionEnvironmentChannelClient.scanExtensions(channel, connection.remoteAuthority, this._environmentService.extensionDevelopmentLocationURI, skipExtensions); + scannedExtensions.forEach((extension) => ImplicitActivationEvents.updateManifest(extension)); + return scannedExtensions; + }, [] ).then(undefined, () => []); } scanSingleExtension(extensionLocation: URI, isBuiltin: boolean): Promise { return this._withChannel( - (channel, connection) => RemoteExtensionEnvironmentChannelClient.scanSingleExtension(channel, connection.remoteAuthority, isBuiltin, extensionLocation), + async (channel, connection) => { + const scannedExtension = await RemoteExtensionEnvironmentChannelClient.scanSingleExtension(channel, connection.remoteAuthority, isBuiltin, extensionLocation); + if (scannedExtension !== null) { + ImplicitActivationEvents.updateManifest(scannedExtension); + } + return scannedExtension; + }, null ).then(undefined, () => null); } diff --git a/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts b/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts index 35bfae00a4d..a50a0bdab19 100644 --- a/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts +++ b/src/vs/workbench/services/remote/common/remoteAgentEnvironmentChannel.ts @@ -12,6 +12,7 @@ import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEn import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnostics'; import { ITelemetryData, TelemetryLevel } from 'vs/platform/telemetry/common/telemetry'; import { IExtensionHostExitInfo } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { Mutable } from 'vs/base/common/types'; export interface IGetEnvironmentDataArguments { remoteAuthority: string; @@ -118,7 +119,7 @@ export class RemoteExtensionEnvironmentChannelClient { const extension = await channel.call('scanSingleExtension', args); if (extension) { - (extension).extensionLocation = URI.revive(extension.extensionLocation); + (>extension).extensionLocation = URI.revive(extension.extensionLocation); } return extension; } diff --git a/src/vs/workbench/services/telemetry/browser/telemetryService.ts b/src/vs/workbench/services/telemetry/browser/telemetryService.ts index 6d5739aed97..80493e185ae 100644 --- a/src/vs/workbench/services/telemetry/browser/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/browser/telemetryService.ts @@ -87,12 +87,12 @@ export class TelemetryService extends Disposable implements ITelemetryService { return this.impl.telemetryLevel; } - publicLog(eventName: string, data?: ITelemetryData, anonymizeFilePaths?: boolean): Promise { - return this.impl.publicLog(eventName, data, anonymizeFilePaths); + publicLog(eventName: string, data?: ITelemetryData): Promise { + return this.impl.publicLog(eventName, data); } - publicLog2> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck, anonymizeFilePaths?: boolean) { - return this.publicLog(eventName, data as ITelemetryData, anonymizeFilePaths); + publicLog2> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck) { + return this.publicLog(eventName, data as ITelemetryData); } publicLogError(errorEventName: string, data?: ITelemetryData): Promise { diff --git a/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts b/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts index 342e4f61afb..9d19cd13d3b 100644 --- a/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts @@ -62,12 +62,12 @@ export class TelemetryService extends Disposable implements ITelemetryService { return this.impl.telemetryLevel; } - publicLog(eventName: string, data?: ITelemetryData, anonymizeFilePaths?: boolean): Promise { - return this.impl.publicLog(eventName, data, anonymizeFilePaths); + publicLog(eventName: string, data?: ITelemetryData): Promise { + return this.impl.publicLog(eventName, data); } - publicLog2> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck, anonymizeFilePaths?: boolean) { - return this.publicLog(eventName, data as ITelemetryData, anonymizeFilePaths); + publicLog2> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck) { + return this.publicLog(eventName, data as ITelemetryData); } publicLogError(errorEventName: string, data?: ITelemetryData): Promise { diff --git a/src/vs/workbench/services/themes/common/plistParser.ts b/src/vs/workbench/services/themes/common/plistParser.ts index d51ce513f66..a9c83450b0e 100644 --- a/src/vs/workbench/services/themes/common/plistParser.ts +++ b/src/vs/workbench/services/themes/common/plistParser.ts @@ -326,9 +326,9 @@ function _parse(content: string, filename: string | null, locationKeyName: strin function escapeVal(str: string): string { return str.replace(/&#([0-9]+);/g, function (_: string, m0: string) { - return (String).fromCodePoint(parseInt(m0, 10)); + return String.fromCodePoint(parseInt(m0, 10)); }).replace(/&#x([0-9a-f]+);/g, function (_: string, m0: string) { - return (String).fromCodePoint(parseInt(m0, 16)); + return String.fromCodePoint(parseInt(m0, 16)); }).replace(/&|<|>|"|'/g, function (_: string) { switch (_) { case '&': return '&'; diff --git a/src/vs/workbench/services/timer/browser/timerService.ts b/src/vs/workbench/services/timer/browser/timerService.ts index 2112d663b80..9b81da873fe 100644 --- a/src/vs/workbench/services/timer/browser/timerService.ts +++ b/src/vs/workbench/services/timer/browser/timerService.ts @@ -17,6 +17,7 @@ import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/la import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite'; import { ViewContainerLocation } from 'vs/workbench/common/views'; import { StopWatch } from 'vs/base/common/stopwatch'; +import { TrustedTelemetryValue } from 'vs/platform/telemetry/common/telemetryUtils'; /* __GDPR__FRAGMENT__ "IMemoryInfo" : { @@ -583,7 +584,7 @@ export abstract class AbstractTimerService implements ITimerService { // event and it is "normalized" to a relative timestamp where the first mark // defines the start - type Mark = { source: string; name: string; startTime: number }; + type Mark = { source: string; name: TrustedTelemetryValue; startTime: number }; type MarkClassification = { owner: 'jrieken'; comment: 'Information about a performance marker'; @@ -595,7 +596,7 @@ export abstract class AbstractTimerService implements ITimerService { for (const mark of marks) { this._telemetryService.publicLog2('startup.timer.mark', { source, - name: mark.name, + name: new TrustedTelemetryValue(mark.name), startTime: mark.startTime }); } diff --git a/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts b/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts new file mode 100644 index 00000000000..81e9272808f --- /dev/null +++ b/src/vs/workbench/services/userDataProfile/browser/extensionsResource.ts @@ -0,0 +1,224 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; +import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionEnablementService'; +import { IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, IGlobalExtensionEnablementService, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { ExtensionType } from 'vs/platform/extensions/common/extensions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IUserDataProfileStorageService } from 'vs/platform/userDataProfile/common/userDataProfileStorageService'; +import { ITreeItemCheckboxState, TreeItemCollapsibleState } from 'vs/workbench/common/views'; +import { IProfileResource, IProfileResourceChildTreeItem, IProfileResourceTreeItem } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; + +interface IProfileExtension { + identifier: IExtensionIdentifier; + displayName?: string; + preRelease?: boolean; + disabled?: boolean; +} + +export class ExtensionsResource implements IProfileResource { + + constructor( + @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, + @IUserDataProfileStorageService private readonly userDataProfileStorageService: IUserDataProfileStorageService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @ILogService private readonly logService: ILogService, + ) { + } + + async getContent(profile: IUserDataProfile, exclude?: string[]): Promise { + const extensions = await this.getLocalExtensions(profile); + return JSON.stringify(exclude?.length ? extensions.filter(e => !exclude.includes(e.identifier.id.toLowerCase())) : extensions); + } + + async apply(content: string, profile: IUserDataProfile): Promise { + return this.withProfileScopedServices(profile, async (extensionEnablementService) => { + const profileExtensions: IProfileExtension[] = await this.getProfileExtensions(content); + const installedExtensions = await this.extensionManagementService.getInstalled(undefined, profile.extensionsResource); + const extensionsToEnableOrDisable: { extension: ILocalExtension; enable: boolean }[] = []; + const extensionsToInstall: IProfileExtension[] = []; + for (const e of profileExtensions) { + const isDisabled = extensionEnablementService.getDisabledExtensions().some(disabledExtension => areSameExtensions(disabledExtension, e.identifier)); + const installedExtension = installedExtensions.find(installed => areSameExtensions(installed.identifier, e.identifier)); + if (!installedExtension || installedExtension.preRelease !== e.preRelease) { + extensionsToInstall.push(e); + } + if (installedExtension && isDisabled !== !!e.disabled) { + extensionsToEnableOrDisable.push({ extension: installedExtension, enable: !!e.disabled }); + } + } + const extensionsToUninstall: ILocalExtension[] = installedExtensions.filter(extension => extension.type === ExtensionType.User && !profileExtensions.some(({ identifier }) => areSameExtensions(identifier, extension.identifier))); + for (const { extension, enable } of extensionsToEnableOrDisable) { + if (enable) { + await extensionEnablementService.enableExtension(extension.identifier); + } else { + await extensionEnablementService.disableExtension(extension.identifier); + } + } + if (extensionsToInstall.length) { + const galleryExtensions = await this.extensionGalleryService.getExtensions(extensionsToInstall.map(e => ({ ...e.identifier, hasPreRelease: e.preRelease })), CancellationToken.None); + await Promise.all(extensionsToInstall.map(async e => { + const extension = galleryExtensions.find(galleryExtension => areSameExtensions(galleryExtension.identifier, e.identifier)); + if (!extension) { + return; + } + if (await this.extensionManagementService.canInstall(extension)) { + await this.extensionManagementService.installFromGallery(extension, { isMachineScoped: false, donotIncludePackAndDependencies: true, installPreReleaseVersion: e.preRelease, profileLocation: profile.extensionsResource } /* set isMachineScoped value to prevent install and sync dialog in web */); + } else { + this.logService.info(`Profile: Skipped installing extension because it cannot be installed.`, extension.displayName || extension.identifier.id); + } + })); + } + if (extensionsToUninstall.length) { + await Promise.all(extensionsToUninstall.map(e => this.extensionManagementService.uninstall(e))); + } + }); + } + + async getLocalExtensions(profile: IUserDataProfile): Promise { + return this.withProfileScopedServices(profile, async (extensionEnablementService) => { + const result: Array = []; + const installedExtensions = await this.extensionManagementService.getInstalled(undefined, profile.extensionsResource); + const disabledExtensions = extensionEnablementService.getDisabledExtensions(); + for (const extension of installedExtensions) { + const { identifier, preRelease } = extension; + const disabled = disabledExtensions.some(disabledExtension => areSameExtensions(disabledExtension, identifier)); + if (extension.type === ExtensionType.System && !disabled) { + // skip enabled system extensions + continue; + } + if (extension.type === ExtensionType.User) { + if (!extension.identifier.uuid) { + // skip user extensions without uuid + continue; + } + if (disabled && !extension.isBuiltin) { + // skip user disabled extensions + continue; + } + } + const profileExtension: IProfileExtension = { identifier, displayName: extension.manifest.displayName }; + if (disabled) { + profileExtension.disabled = true; + } + if (preRelease) { + profileExtension.preRelease = true; + } + result.push(profileExtension); + } + return result; + }); + } + + async getProfileExtensions(content: string): Promise { + return JSON.parse(content); + } + + private async withProfileScopedServices(profile: IUserDataProfile, fn: (extensionEnablementService: IGlobalExtensionEnablementService) => Promise): Promise { + return this.userDataProfileStorageService.withProfileScopedStorageService(profile, + async storageService => { + const disposables = new DisposableStore(); + const instantiationService = this.instantiationService.createChild(new ServiceCollection([IStorageService, storageService])); + const extensionEnablementService = disposables.add(instantiationService.createInstance(GlobalExtensionEnablementService)); + try { + return await fn(extensionEnablementService); + } finally { + disposables.dispose(); + } + }); + } +} + +export class ExtensionsResourceExportTreeItem implements IProfileResourceTreeItem { + + readonly handle = this.profile.extensionsResource.toString(); + readonly label = { label: localize('extensions', "Extensions") }; + readonly collapsibleState = TreeItemCollapsibleState.Expanded; + checkbox: ITreeItemCheckboxState = { isChecked: true }; + + private readonly excludedExtensions = new Set(); + + constructor( + private readonly profile: IUserDataProfile, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { } + + async getChildren(): Promise { + const extensions = await this.instantiationService.createInstance(ExtensionsResource).getLocalExtensions(this.profile); + const that = this; + return extensions.map(e => ({ + handle: e.identifier.id.toLowerCase(), + parent: this, + label: { label: e.displayName || e.identifier.id }, + description: e.disabled ? localize('disabled', "Disabled") : undefined, + collapsibleState: TreeItemCollapsibleState.None, + checkbox: { + get isChecked() { return !that.excludedExtensions.has(e.identifier.id.toLowerCase()); }, + set isChecked(value: boolean) { + if (value) { + that.excludedExtensions.delete(e.identifier.id.toLowerCase()); + } else { + that.excludedExtensions.add(e.identifier.id.toLowerCase()); + } + } + }, + command: { + id: 'extension.open', + title: '', + arguments: [e.identifier.id] + } + })); + } + + async hasContent(): Promise { + const extensions = await this.instantiationService.createInstance(ExtensionsResource).getLocalExtensions(this.profile); + return extensions.length > 0; + } + + async getContent(): Promise { + return this.instantiationService.createInstance(ExtensionsResource).getContent(this.profile, [...this.excludedExtensions.values()]); + } + +} + +export class ExtensionsResourceImportTreeItem implements IProfileResourceTreeItem { + + readonly handle = 'extensions'; + readonly label = { label: localize('extensions', "Extensions") }; + readonly collapsibleState = TreeItemCollapsibleState.Expanded; + + constructor( + private readonly content: string, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { } + + async getChildren(): Promise { + const extensions = await this.instantiationService.createInstance(ExtensionsResource).getProfileExtensions(this.content); + return extensions.map(e => ({ + handle: e.identifier.id.toLowerCase(), + parent: this, + label: { label: e.displayName || e.identifier.id }, + description: e.disabled ? localize('disabled', "Disabled") : undefined, + collapsibleState: TreeItemCollapsibleState.None, + command: { + id: 'extension.open', + title: '', + arguments: [e.identifier.id] + } + })); + } + +} + + diff --git a/src/vs/workbench/services/userDataProfile/browser/globalStateResource.ts b/src/vs/workbench/services/userDataProfile/browser/globalStateResource.ts new file mode 100644 index 00000000000..526a46ce468 --- /dev/null +++ b/src/vs/workbench/services/userDataProfile/browser/globalStateResource.ts @@ -0,0 +1,116 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IStringDictionary } from 'vs/base/common/collections'; +import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IUserDataProfileStorageService } from 'vs/platform/userDataProfile/common/userDataProfileStorageService'; +import { API_OPEN_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; +import { ITreeItemCheckboxState, TreeItemCollapsibleState } from 'vs/workbench/common/views'; +import { IProfileResource, IProfileResourceTreeItem } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; + +interface IGlobalState { + storage: IStringDictionary; +} + +export class GlobalStateResource implements IProfileResource { + + constructor( + @IStorageService private readonly storageService: IStorageService, + @IUserDataProfileStorageService private readonly userDataProfileStorageService: IUserDataProfileStorageService, + @ILogService private readonly logService: ILogService, + ) { + } + + async getContent(profile: IUserDataProfile): Promise { + const globalState = await this.getGlobalState(profile); + return JSON.stringify(globalState); + } + + async apply(content: string, profile: IUserDataProfile): Promise { + const globalState: IGlobalState = JSON.parse(content); + await this.writeGlobalState(globalState, profile); + } + + async getGlobalState(profile: IUserDataProfile): Promise { + const storage: IStringDictionary = {}; + const storageData = await this.userDataProfileStorageService.readStorageData(profile); + for (const [key, value] of storageData) { + if (value.value !== undefined && value.target === StorageTarget.USER) { + storage[key] = value.value; + } + } + return { storage }; + } + + private async writeGlobalState(globalState: IGlobalState, profile: IUserDataProfile): Promise { + const storageKeys = Object.keys(globalState.storage); + if (storageKeys.length) { + const updatedStorage = new Map(); + const nonProfileKeys = [ + // Do not include application scope user target keys because they also include default profile user target keys + ...this.storageService.keys(StorageScope.APPLICATION, StorageTarget.MACHINE), + ...this.storageService.keys(StorageScope.WORKSPACE, StorageTarget.USER), + ...this.storageService.keys(StorageScope.WORKSPACE, StorageTarget.MACHINE), + ]; + for (const key of storageKeys) { + if (nonProfileKeys.includes(key)) { + this.logService.info(`Profile: Ignoring global state key '${key}' because it is not a profile key.`); + } else { + updatedStorage.set(key, globalState.storage[key]); + } + } + await this.userDataProfileStorageService.updateStorageData(profile, updatedStorage, StorageTarget.USER); + } + } +} + +export class GlobalStateResourceExportTreeItem implements IProfileResourceTreeItem { + + readonly handle = this.profile.globalStorageHome.toString(); + readonly label = { label: localize('globalState', "UI State") }; + readonly collapsibleState = TreeItemCollapsibleState.None; + checkbox: ITreeItemCheckboxState = { isChecked: true }; + + constructor( + private readonly profile: IUserDataProfile, + @IInstantiationService private readonly instantiationService: IInstantiationService + ) { } + + async getChildren(): Promise { return undefined; } + + async hasContent(): Promise { + const globalState = await this.instantiationService.createInstance(GlobalStateResource).getGlobalState(this.profile); + return Object.keys(globalState.storage).length > 0; + } + + async getContent(): Promise { + return this.instantiationService.createInstance(GlobalStateResource).getContent(this.profile); + } + +} + +export class GlobalStateResourceImportTreeItem implements IProfileResourceTreeItem { + + readonly handle = 'globalState'; + readonly label = { label: localize('globalState', "UI State") }; + readonly collapsibleState = TreeItemCollapsibleState.None; + readonly command = { + id: API_OPEN_EDITOR_COMMAND_ID, + title: '', + arguments: [this.resource, undefined, undefined] + }; + + constructor(private readonly resource: URI) { } + + async getChildren(): Promise { return undefined; } +} + + + diff --git a/src/vs/workbench/services/userDataProfile/browser/keybindingsResource.ts b/src/vs/workbench/services/userDataProfile/browser/keybindingsResource.ts new file mode 100644 index 00000000000..a2707695411 --- /dev/null +++ b/src/vs/workbench/services/userDataProfile/browser/keybindingsResource.ts @@ -0,0 +1,93 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBuffer } from 'vs/base/common/buffer'; +import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IProfileResource, IProfileResourceTreeItem } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { platform, Platform } from 'vs/base/common/platform'; +import { ITreeItemCheckboxState, TreeItemCollapsibleState } from 'vs/workbench/common/views'; +import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { API_OPEN_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { localize } from 'vs/nls'; + +interface IKeybindingsResourceContent { + platform: Platform; + keybindings: string | null; +} + +export class KeybindingsResource implements IProfileResource { + + constructor( + @IFileService private readonly fileService: IFileService, + @ILogService private readonly logService: ILogService, + ) { + } + + async getContent(profile: IUserDataProfile): Promise { + const keybindingsContent = await this.getKeybindingsResourceContent(profile); + return JSON.stringify(keybindingsContent); + } + + async getKeybindingsResourceContent(profile: IUserDataProfile): Promise { + const keybindings = await this.getKeybindingsContent(profile); + return { keybindings, platform }; + } + + async apply(content: string, profile: IUserDataProfile): Promise { + const keybindingsContent: IKeybindingsResourceContent = JSON.parse(content); + if (keybindingsContent.keybindings === null) { + this.logService.info(`Profile: No keybindings to apply...`); + return; + } + await this.fileService.writeFile(profile.keybindingsResource, VSBuffer.fromString(keybindingsContent.keybindings)); + } + + private async getKeybindingsContent(profile: IUserDataProfile): Promise { + try { + const content = await this.fileService.readFile(profile.keybindingsResource); + return content.value.toString(); + } catch (error) { + // File not found + if (error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) { + return null; + } else { + throw error; + } + } + } + +} + +export class KeybindingsResourceTreeItem implements IProfileResourceTreeItem { + + readonly handle = this.profile.keybindingsResource.toString(); + readonly label = { label: localize('keybindings', "Keyboard Shortcuts") }; + readonly collapsibleState = TreeItemCollapsibleState.None; + checkbox: ITreeItemCheckboxState | undefined = { isChecked: true }; + readonly command = { + id: API_OPEN_EDITOR_COMMAND_ID, + title: '', + arguments: [this.profile.keybindingsResource, undefined, undefined] + }; + + constructor( + private readonly profile: IUserDataProfile, + @IInstantiationService private readonly instantiationService: IInstantiationService + ) { } + + async getChildren(): Promise { return undefined; } + + async hasContent(): Promise { + const keybindingsContent = await this.instantiationService.createInstance(KeybindingsResource).getKeybindingsResourceContent(this.profile); + return keybindingsContent.keybindings !== null; + } + + async getContent(): Promise { + return this.instantiationService.createInstance(KeybindingsResource).getContent(this.profile); + } + +} diff --git a/src/vs/workbench/services/userDataProfile/browser/settingsResource.ts b/src/vs/workbench/services/userDataProfile/browser/settingsResource.ts new file mode 100644 index 00000000000..cdb12f8f329 --- /dev/null +++ b/src/vs/workbench/services/userDataProfile/browser/settingsResource.ts @@ -0,0 +1,112 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBuffer } from 'vs/base/common/buffer'; +import { ConfigurationScope, Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; +import { ILogService } from 'vs/platform/log/common/log'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IProfileResource, IProfileResourceTreeItem } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { updateIgnoredSettings } from 'vs/platform/userDataSync/common/settingsMerge'; +import { IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync'; +import { ITreeItemCheckboxState, TreeItemCollapsibleState } from 'vs/workbench/common/views'; +import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { API_OPEN_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { localize } from 'vs/nls'; + +interface ISettingsContent { + settings: string | null; +} + +export class SettingsResource implements IProfileResource { + + constructor( + @IFileService private readonly fileService: IFileService, + @IUserDataSyncUtilService private readonly userDataSyncUtilService: IUserDataSyncUtilService, + @ILogService private readonly logService: ILogService, + ) { + } + + async getContent(profile: IUserDataProfile): Promise { + const settingsContent = await this.getSettingsContent(profile); + return JSON.stringify(settingsContent); + } + + async getSettingsContent(profile: IUserDataProfile): Promise { + const localContent = await this.getLocalFileContent(profile); + if (localContent === null) { + return { settings: null }; + } else { + const ignoredSettings = this.getIgnoredSettings(); + const formattingOptions = await this.userDataSyncUtilService.resolveFormattingOptions(profile.settingsResource); + const settings = updateIgnoredSettings(localContent || '{}', '{}', ignoredSettings, formattingOptions); + return { settings }; + } + } + + async apply(content: string, profile: IUserDataProfile): Promise { + const settingsContent: ISettingsContent = JSON.parse(content); + if (settingsContent.settings === null) { + this.logService.info(`Profile: No settings to apply...`); + return; + } + const localSettingsContent = await this.getLocalFileContent(profile); + const formattingOptions = await this.userDataSyncUtilService.resolveFormattingOptions(profile.settingsResource); + const contentToUpdate = updateIgnoredSettings(settingsContent.settings, localSettingsContent || '{}', this.getIgnoredSettings(), formattingOptions); + await this.fileService.writeFile(profile.settingsResource, VSBuffer.fromString(contentToUpdate)); + } + + private getIgnoredSettings(): string[] { + const allSettings = Registry.as(Extensions.Configuration).getConfigurationProperties(); + const ignoredSettings = Object.keys(allSettings).filter(key => allSettings[key]?.scope === ConfigurationScope.MACHINE || allSettings[key]?.scope === ConfigurationScope.MACHINE_OVERRIDABLE); + return ignoredSettings; + } + + private async getLocalFileContent(profile: IUserDataProfile): Promise { + try { + const content = await this.fileService.readFile(profile.settingsResource); + return content.value.toString(); + } catch (error) { + // File not found + if (error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) { + return null; + } else { + throw error; + } + } + } + +} + +export class SettingsResourceTreeItem implements IProfileResourceTreeItem { + + readonly handle = this.profile.settingsResource.toString(); + readonly label = { label: localize('settings', "Settings") }; + readonly collapsibleState = TreeItemCollapsibleState.None; + checkbox: ITreeItemCheckboxState | undefined = { isChecked: true }; + readonly command = { + id: API_OPEN_EDITOR_COMMAND_ID, + title: '', + arguments: [this.profile.settingsResource, undefined, undefined] + }; + + constructor( + private readonly profile: IUserDataProfile, + @IInstantiationService private readonly instantiationService: IInstantiationService + ) { } + + async getChildren(): Promise { return undefined; } + + async hasContent(): Promise { + const settingsContent = await this.instantiationService.createInstance(SettingsResource).getSettingsContent(this.profile); + return settingsContent.settings !== null; + } + + async getContent(): Promise { + return this.instantiationService.createInstance(SettingsResource).getContent(this.profile); + } + +} diff --git a/src/vs/workbench/services/userDataProfile/browser/snippetsResource.ts b/src/vs/workbench/services/userDataProfile/browser/snippetsResource.ts new file mode 100644 index 00000000000..2cca57fa599 --- /dev/null +++ b/src/vs/workbench/services/userDataProfile/browser/snippetsResource.ts @@ -0,0 +1,131 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBuffer } from 'vs/base/common/buffer'; +import { IStringDictionary } from 'vs/base/common/collections'; +import { ResourceSet } from 'vs/base/common/map'; +import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { FileOperationError, FileOperationResult, IFileService, IFileStat } from 'vs/platform/files/common/files'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { API_OPEN_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; +import { ITreeItemCheckboxState, TreeItemCollapsibleState } from 'vs/workbench/common/views'; +import { IProfileResource, IProfileResourceChildTreeItem, IProfileResourceTreeItem } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; + +interface ISnippetsContent { + snippets: IStringDictionary; +} + +export class SnippetsResource implements IProfileResource { + + constructor( + @IFileService private readonly fileService: IFileService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + ) { + } + + async getContent(profile: IUserDataProfile, excluded?: ResourceSet): Promise { + const snippets = await this.getSnippets(profile, excluded); + return JSON.stringify({ snippets }); + } + + async apply(content: string, profile: IUserDataProfile): Promise { + const snippetsContent: ISnippetsContent = JSON.parse(content); + for (const key in snippetsContent.snippets) { + const resource = this.uriIdentityService.extUri.joinPath(profile.snippetsHome, key); + await this.fileService.writeFile(resource, VSBuffer.fromString(snippetsContent.snippets[key])); + } + } + + private async getSnippets(profile: IUserDataProfile, excluded?: ResourceSet): Promise> { + const snippets: IStringDictionary = {}; + const snippetsResources = await this.getSnippetsResources(profile, excluded); + for (const resource of snippetsResources) { + const key = this.uriIdentityService.extUri.relativePath(profile.snippetsHome, resource)!; + const content = await this.fileService.readFile(resource); + snippets[key] = content.value.toString(); + } + return snippets; + } + + async getSnippetsResources(profile: IUserDataProfile, excluded?: ResourceSet): Promise { + const snippets: URI[] = []; + let stat: IFileStat; + try { + stat = await this.fileService.resolve(profile.snippetsHome); + } catch (e) { + // No snippets + if (e instanceof FileOperationError && e.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) { + return snippets; + } else { + throw e; + } + } + for (const { resource } of stat.children || []) { + if (excluded?.has(resource)) { + continue; + } + const extension = this.uriIdentityService.extUri.extname(resource); + if (extension === '.json' || extension === '.code-snippets') { + snippets.push(resource); + } + } + return snippets; + } +} + +export class SnippetsResourceTreeItem implements IProfileResourceTreeItem { + + readonly handle = this.profile.snippetsHome.toString(); + readonly label = { label: localize('snippets', "Snippets") }; + readonly collapsibleState = TreeItemCollapsibleState.Collapsed; + checkbox: ITreeItemCheckboxState | undefined = { isChecked: true }; + + private readonly excludedSnippets = new ResourceSet(); + + constructor( + private readonly profile: IUserDataProfile, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { } + + async getChildren(): Promise { + const snippetsResources = await this.instantiationService.createInstance(SnippetsResource).getSnippetsResources(this.profile); + const that = this; + return snippetsResources.map(resource => ({ + handle: resource.toString(), + parent: that, + resourceUri: resource, + collapsibleState: TreeItemCollapsibleState.None, + checkbox: that.checkbox ? { + get isChecked() { return !that.excludedSnippets.has(resource); }, + set isChecked(value: boolean) { + if (value) { + that.excludedSnippets.delete(resource); + } else { + that.excludedSnippets.add(resource); + } + } + } : undefined, + command: { + id: API_OPEN_EDITOR_COMMAND_ID, + title: '', + arguments: [resource, undefined, undefined] + } + })); + } + + async hasContent(): Promise { + const snippetsResources = await this.instantiationService.createInstance(SnippetsResource).getSnippetsResources(this.profile); + return snippetsResources.length > 0; + } + + async getContent(): Promise { + return this.instantiationService.createInstance(SnippetsResource).getContent(this.profile, this.excludedSnippets); + } + +} + diff --git a/src/vs/workbench/services/userDataProfile/browser/tasksResource.ts b/src/vs/workbench/services/userDataProfile/browser/tasksResource.ts new file mode 100644 index 00000000000..3929f989fc8 --- /dev/null +++ b/src/vs/workbench/services/userDataProfile/browser/tasksResource.ts @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBuffer } from 'vs/base/common/buffer'; +import { localize } from 'vs/nls'; +import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { API_OPEN_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; +import { ITreeItemCheckboxState, TreeItemCollapsibleState } from 'vs/workbench/common/views'; +import { IProfileResource, IProfileResourceTreeItem } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; + +interface ITasksResourceContent { + tasks: string | null; +} + +export class TasksResource implements IProfileResource { + + constructor( + @IFileService private readonly fileService: IFileService, + @ILogService private readonly logService: ILogService, + ) { + } + + async getContent(profile: IUserDataProfile): Promise { + const tasksContent = await this.getTasksResourceContent(profile); + return JSON.stringify(tasksContent); + } + + async getTasksResourceContent(profile: IUserDataProfile): Promise { + const tasksContent = await this.getTasksContent(profile); + return { tasks: tasksContent }; + } + + async apply(content: string, profile: IUserDataProfile): Promise { + const tasksContent: ITasksResourceContent = JSON.parse(content); + if (!tasksContent.tasks) { + this.logService.info(`Profile: No tasks to apply...`); + return; + } + await this.fileService.writeFile(profile.tasksResource, VSBuffer.fromString(tasksContent.tasks)); + } + + private async getTasksContent(profile: IUserDataProfile): Promise { + try { + const content = await this.fileService.readFile(profile.tasksResource); + return content.value.toString(); + } catch (error) { + // File not found + if (error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) { + return null; + } else { + throw error; + } + } + } + +} + +export class TasksResourceTreeItem implements IProfileResourceTreeItem { + + readonly handle = this.profile.tasksResource.toString(); + readonly label = { label: localize('tasks', "User Tasks") }; + readonly collapsibleState = TreeItemCollapsibleState.None; + checkbox: ITreeItemCheckboxState | undefined = { isChecked: true }; + readonly command = { + id: API_OPEN_EDITOR_COMMAND_ID, + title: '', + arguments: [this.profile.tasksResource, undefined, undefined] + }; + + constructor( + private readonly profile: IUserDataProfile, + @IInstantiationService private readonly instantiationService: IInstantiationService + ) { } + + async getChildren(): Promise { return undefined; } + + async hasContent(): Promise { + const tasksContent = await this.instantiationService.createInstance(TasksResource).getTasksResourceContent(this.profile); + return tasksContent.tasks !== null; + } + + async getContent(): Promise { + return this.instantiationService.createInstance(TasksResource).getContent(this.profile); + } + +} diff --git a/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts b/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts new file mode 100644 index 00000000000..2f2f0731ff3 --- /dev/null +++ b/src/vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService.ts @@ -0,0 +1,699 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { localize } from 'vs/nls'; +import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import * as DOM from 'vs/base/browser/dom'; +import { IUserDataProfileImportExportService, PROFILE_FILTER, PROFILE_EXTENSION, IUserDataProfileContentHandler, IS_PROFILE_IMPORT_EXPORT_IN_PROGRESS_CONTEXT, PROFILES_TTILE, defaultUserDataProfileIcon, IUserDataProfileService, IProfileResourceTreeItem, IProfileResourceChildTreeItem, PROFILES_CATEGORY, isUserDataProfileTemplate, IUserDataProfileManagementService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IFileService } from 'vs/platform/files/common/files'; +import { URI } from 'vs/base/common/uri'; +import { Extensions, ITreeItem, ITreeViewDataProvider, ITreeViewDescriptor, IViewContainersRegistry, IViewDescriptorService, IViewsRegistry, IViewsService, TreeItemCollapsibleState, ViewContainer, ViewContainerLocation } from 'vs/workbench/common/views'; +import { IUserDataProfile, IUserDataProfilesService, toUserDataProfile } from 'vs/platform/userDataProfile/common/userDataProfile'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ILogService } from 'vs/platform/log/common/log'; +import { TreeView, TreeViewPane } from 'vs/workbench/browser/parts/views/treeView'; +import { SettingsResource, SettingsResourceTreeItem } from 'vs/workbench/services/userDataProfile/browser/settingsResource'; +import { KeybindingsResource, KeybindingsResourceTreeItem } from 'vs/workbench/services/userDataProfile/browser/keybindingsResource'; +import { SnippetsResource, SnippetsResourceTreeItem } from 'vs/workbench/services/userDataProfile/browser/snippetsResource'; +import { TasksResource, TasksResourceTreeItem } from 'vs/workbench/services/userDataProfile/browser/tasksResource'; +import { ExtensionsResource, ExtensionsResourceExportTreeItem, ExtensionsResourceImportTreeItem } from 'vs/workbench/services/userDataProfile/browser/extensionsResource'; +import { GlobalStateResource, GlobalStateResourceExportTreeItem, GlobalStateResourceImportTreeItem } from 'vs/workbench/services/userDataProfile/browser/globalStateResource'; +import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; +import { Button } from 'vs/base/browser/ui/button/button'; +import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { defaultButtonStyles } from 'vs/platform/theme/browser/defaultStyles'; +import { generateUuid } from 'vs/base/common/uuid'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { EditorsOrder } from 'vs/workbench/common/editor'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { joinPath } from 'vs/base/common/resources'; + +interface IUserDataProfileTemplate { + readonly name: string; + readonly shortName?: string; + readonly settings?: string; + readonly keybindings?: string; + readonly tasks?: string; + readonly snippets?: string; + readonly globalState?: string; + readonly extensions?: string; +} + +export class UserDataProfileImportExportService extends Disposable implements IUserDataProfileImportExportService { + + readonly _serviceBrand: undefined; + + private profileContentHandlers = new Map(); + private readonly isProfileImportExportInProgressContextKey: IContextKey; + + private readonly viewContainer: ViewContainer; + private readonly fileUserDataProfileContentHandler: IUserDataProfileContentHandler; + + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, + @IViewsService private readonly viewsService: IViewsService, + @IEditorService private readonly editorService: IEditorService, + @IContextKeyService contextKeyService: IContextKeyService, + @IFileService private readonly fileService: IFileService, + @IUserDataProfileManagementService private readonly userDataProfileManagementService: IUserDataProfileManagementService, + @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, + @IExtensionService private readonly extensionService: IExtensionService, + @IQuickInputService private readonly quickInputService: IQuickInputService, + @INotificationService private readonly notificationService: INotificationService, + @IProgressService private readonly progressService: IProgressService, + @IDialogService private readonly dialogService: IDialogService, + @ILogService private readonly logService: ILogService, + ) { + super(); + this.registerProfileContentHandler(this.fileUserDataProfileContentHandler = instantiationService.createInstance(FileUserDataProfileContentHandler)); + this.isProfileImportExportInProgressContextKey = IS_PROFILE_IMPORT_EXPORT_IN_PROGRESS_CONTEXT.bindTo(contextKeyService); + + this.viewContainer = Registry.as(Extensions.ViewContainersRegistry).registerViewContainer( + { + id: 'userDataProfiles', + title: PROFILES_TTILE, + ctorDescriptor: new SyncDescriptor( + ViewPaneContainer, + ['userDataProfiles', { mergeViewWithContainerWhenSingleView: true }] + ), + icon: defaultUserDataProfileIcon, + hideIfEmpty: true, + }, ViewContainerLocation.Sidebar); + } + + registerProfileContentHandler(profileContentHandler: IUserDataProfileContentHandler): void { + if (this.profileContentHandlers.has(profileContentHandler.id)) { + throw new Error(`Profile content handler with id '${profileContentHandler.id}' already registered.`); + } + this.profileContentHandlers.set(profileContentHandler.id, profileContentHandler); + } + + async exportProfile(): Promise { + if (this.isProfileImportExportInProgressContextKey.get()) { + this.logService.warn('Profile import/export already in progress.'); + return; + } + + this.isProfileImportExportInProgressContextKey.set(true); + const disposables = new DisposableStore(); + + try { + disposables.add(toDisposable(() => this.isProfileImportExportInProgressContextKey.set(false))); + const userDataProfilesData = disposables.add(this.instantiationService.createInstance(UserDataProfileExportData, this.userDataProfileService.currentProfile)); + const exportProfile = await this.showProfilePreviewView(`workbench.views.profiles.export.preview`, localize('export profile preview', "Export"), userDataProfilesData); + if (exportProfile) { + const profileContent = await userDataProfilesData.getContent(); + const resource = await this.saveProfileContent(profileContent); + if (resource) { + this.notificationService.info(localize('export success', "{0}: Exported successfully.", PROFILES_CATEGORY.value)); + } + } + } finally { + disposables.dispose(); + } + } + + async importProfile(uri: URI): Promise { + if (this.isProfileImportExportInProgressContextKey.get()) { + this.logService.warn('Profile import/export already in progress.'); + return; + } + + this.isProfileImportExportInProgressContextKey.set(true); + const disposables = new DisposableStore(); + disposables.add(toDisposable(() => this.isProfileImportExportInProgressContextKey.set(false))); + + try { + const profileContent = await this.resolveProfileContent(uri); + if (profileContent === null) { + return; + } + const profileTemplate: IUserDataProfileTemplate = JSON.parse(profileContent); + if (!isUserDataProfileTemplate(profileTemplate)) { + this.notificationService.error('Invalid profile content.'); + return; + } + const userDataProfilesData = disposables.add(this.instantiationService.createInstance(UserDataProfileImportData, profileTemplate)); + const importProfile = await this.showProfilePreviewView(`workbench.views.profiles.import.preview`, localize('import profile preview', "Import"), userDataProfilesData); + if (!importProfile) { + return; + } + const profile = await this.getProfileToImport(profileTemplate); + if (!profile) { + return; + } + await this.progressService.withProgress({ + location: ProgressLocation.Notification, + title: localize('profiles.importing', "{0}: Importing...", PROFILES_CATEGORY.value), + }, async progress => { + if (profileTemplate.settings) { + await this.instantiationService.createInstance(SettingsResource).apply(profileTemplate.settings, profile); + } + if (profileTemplate.keybindings) { + await this.instantiationService.createInstance(KeybindingsResource).apply(profileTemplate.keybindings, profile); + } + if (profileTemplate.tasks) { + await this.instantiationService.createInstance(TasksResource).apply(profileTemplate.tasks, profile); + } + if (profileTemplate.snippets) { + await this.instantiationService.createInstance(SnippetsResource).apply(profileTemplate.snippets, profile); + } + if (profileTemplate.globalState) { + await this.instantiationService.createInstance(GlobalStateResource).apply(profileTemplate.globalState, profile); + } + if (profileTemplate.extensions) { + await this.instantiationService.createInstance(ExtensionsResource).apply(profileTemplate.extensions, profile); + } + await this.userDataProfileManagementService.switchProfile(profile); + }); + + this.notificationService.info(localize('imported profile', "{0}: Imported successfully.", PROFILES_CATEGORY.value)); + } finally { + disposables.dispose(); + } + } + + private async saveProfileContent(content: string): Promise { + const profileContentHandler = await this.pickProfileContentHandler(); + if (!profileContentHandler) { + return null; + } + const resource = await profileContentHandler.saveProfile(content); + return resource; + } + + private async resolveProfileContent(resource: URI): Promise { + if (await this.fileService.canHandleResource(resource)) { + return this.fileUserDataProfileContentHandler.readProfile(resource); + } + await this.extensionService.activateByEvent(`onProfile:import:${resource.authority}`); + const profileContentHandler = this.profileContentHandlers.get(resource.authority); + return profileContentHandler?.readProfile(resource) ?? null; + } + + private async pickProfileContentHandler(): Promise { + if (this.profileContentHandlers.size === 1) { + return this.profileContentHandlers.values().next().value; + } + await this.extensionService.activateByEvent('onProfile:export'); + return undefined; + } + + private async getProfileToImport(profileTemplate: IUserDataProfileTemplate): Promise { + const profile = this.userDataProfilesService.profiles.find(p => p.name === profileTemplate.name); + if (profile) { + const confirmation = await this.dialogService.confirm({ + type: 'info', + message: localize('profile already exists', "Profile with name '{0}' already exists. Do you want to overwrite it?", profileTemplate.name), + primaryButton: localize('overwrite', "Overwrite"), + secondaryButton: localize('create new', "Create New Profile"), + }); + if (confirmation.confirmed) { + return profile; + } + const name = await this.quickInputService.input({ + placeHolder: localize('name', "Profile name"), + title: localize('create new', "Create New Profile"), + validateInput: async (value: string) => { + if (this.userDataProfilesService.profiles.some(p => p.name === value)) { + return localize('profileExists', "Profile with name {0} already exists.", value); + } + return undefined; + } + }); + if (!name) { + return undefined; + } + return this.userDataProfilesService.createNamedProfile(name); + } else { + return this.userDataProfilesService.createNamedProfile(profileTemplate.name, { shortName: profileTemplate.shortName }); + } + } + + private async showProfilePreviewView(id: string, name: string, userDataProfilesData: UserDataProfileTreeViewData): Promise { + const disposables = new DisposableStore(); + const viewsRegistry = Registry.as(Extensions.ViewsRegistry); + const treeView = disposables.add(this.instantiationService.createInstance(TreeView, id, name)); + treeView.showRefreshAction = true; + let onConfirm: (() => void) | undefined, onCancel: (() => void) | undefined; + const exportPreviewConfirmPomise = new Promise((c, e) => { onConfirm = c; onCancel = e; }); + const descriptor: ITreeViewDescriptor = { + id, + name, + ctorDescriptor: new SyncDescriptor(UserDataProfileExportViewPane, [userDataProfilesData, name, onConfirm, onCancel]), + canToggleVisibility: false, + canMoveView: false, + treeView, + collapsed: false, + }; + + try { + viewsRegistry.registerViews([descriptor], this.viewContainer); + await this.viewsService.openView(id, true); + await exportPreviewConfirmPomise; + return true; + } catch { + return false; + } finally { + viewsRegistry.deregisterViews([descriptor], this.viewContainer); + disposables.dispose(); + this.closeAllImportExportPreviewEditors().then(null, onUnexpectedError); + } + } + + private async closeAllImportExportPreviewEditors(): Promise { + const editorsToColse = this.editorService.getEditors(EditorsOrder.SEQUENTIAL).filter(({ editor }) => editor.resource?.scheme === USER_DATA_PROFILE_IMPORT_EXPORT_PREVIEW_SCHEME); + if (editorsToColse.length) { + await this.editorService.closeEditors(editorsToColse); + } + } + + async setProfile(profile: IUserDataProfileTemplate): Promise { + await this.progressService.withProgress({ + location: ProgressLocation.Notification, + title: localize('profiles.applying', "{0}: Applying...", PROFILES_CATEGORY.value), + }, async progress => { + if (profile.settings) { + await this.instantiationService.createInstance(SettingsResource).apply(profile.settings, this.userDataProfileService.currentProfile); + } + if (profile.globalState) { + await this.instantiationService.createInstance(GlobalStateResource).apply(profile.globalState, this.userDataProfileService.currentProfile); + } + if (profile.extensions) { + await this.instantiationService.createInstance(ExtensionsResource).apply(profile.extensions, this.userDataProfileService.currentProfile); + } + }); + this.notificationService.info(localize('applied profile', "{0}: Applied successfully.", PROFILES_CATEGORY.value)); + } + +} + +class FileUserDataProfileContentHandler implements IUserDataProfileContentHandler { + + readonly id = 'file'; + readonly name = localize('file', "File"); + + constructor( + @IFileDialogService private readonly fileDialogService: IFileDialogService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @IFileService private readonly fileService: IFileService, + @ITextFileService private readonly textFileService: ITextFileService, + ) { } + + async saveProfile(content: string): Promise { + const profileLocation = await this.fileDialogService.showSaveDialog({ + title: localize('export profile dialog', "Save Profile"), + filters: PROFILE_FILTER, + defaultUri: this.uriIdentityService.extUri.joinPath(await this.fileDialogService.defaultFilePath(), `profile.${PROFILE_EXTENSION}`), + }); + if (!profileLocation) { + return null; + } + await this.textFileService.create([{ resource: profileLocation, value: content, options: { overwrite: true } }]); + return profileLocation; + } + + async readProfile(uri: URI): Promise { + return (await this.fileService.readFile(uri)).value.toString(); + } + + async selectProfile(): Promise { + const profileLocation = await this.fileDialogService.showOpenDialog({ + canSelectFolders: false, + canSelectFiles: true, + canSelectMany: false, + filters: PROFILE_FILTER, + title: localize('select profile', "Select Profile"), + }); + return profileLocation ? profileLocation[0] : null; + } + + +} + +class UserDataProfileExportViewPane extends TreeViewPane { + + private buttonsContainer!: HTMLElement; + private confirmButton!: Button; + private cancelButton!: Button; + private dimension: DOM.Dimension | undefined; + private totalTreeItemsCount: number = 0; + + constructor( + private readonly userDataProfileData: UserDataProfileTreeViewData, + private readonly confirmLabel: string, + private readonly onConfirm: () => void, + private readonly onCancel: () => void, + options: IViewletViewOptions, + @IKeybindingService keybindingService: IKeybindingService, + @IContextMenuService contextMenuService: IContextMenuService, + @IConfigurationService configurationService: IConfigurationService, + @IContextKeyService contextKeyService: IContextKeyService, + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, + @IInstantiationService instantiationService: IInstantiationService, + @IOpenerService openerService: IOpenerService, + @IThemeService themeService: IThemeService, + @ITelemetryService telemetryService: ITelemetryService, + @INotificationService notificationService: INotificationService, + ) { + super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService, notificationService); + } + + + protected override renderTreeView(container: HTMLElement): void { + this.treeView.dataProvider = this.userDataProfileData; + super.renderTreeView(DOM.append(container, DOM.$(''))); + this.createButtons(container); + this._register(this.treeView.onDidChangeCheckboxState(items => { + this.treeView.refresh(this.userDataProfileData.onDidChangeCheckboxState(items)); + this.updateConfirmButtonEnablement(); + })); + this.userDataProfileData.getExpandedItemsCount().then(count => { + this.totalTreeItemsCount = count; + if (this.dimension) { + this.layoutTreeView(this.dimension.height, this.dimension.width); + } + }); + } + + private createButtons(container: HTMLElement): void { + this.buttonsContainer = DOM.append(container, DOM.$('.manual-sync-buttons-container')); + + this.confirmButton = this._register(new Button(this.buttonsContainer, { ...defaultButtonStyles })); + this.confirmButton.label = this.confirmLabel; + this._register(this.confirmButton.onDidClick(() => this.onConfirm())); + + this.cancelButton = this._register(new Button(this.buttonsContainer, { secondary: true, ...defaultButtonStyles })); + this.cancelButton.label = localize('cancel', "Cancel"); + this._register(this.cancelButton.onDidClick(() => this.onCancel())); + } + + + protected override layoutTreeView(height: number, width: number): void { + this.dimension = new DOM.Dimension(width, height); + const buttonContainerHeight = 78; + this.buttonsContainer.style.height = `${buttonContainerHeight}px`; + this.buttonsContainer.style.width = `${width}px`; + + super.layoutTreeView(Math.min(height - buttonContainerHeight, 22 * (this.totalTreeItemsCount || 12)), width); + } + + private updateConfirmButtonEnablement(): void { + this.confirmButton.enabled = this.userDataProfileData.isEnabled(); + } + +} + +const USER_DATA_PROFILE_IMPORT_EXPORT_PREVIEW_SCHEME = 'userdataprofileexportpreview'; + +abstract class UserDataProfileTreeViewData extends Disposable implements ITreeViewDataProvider { + + async getExpandedItemsCount(): Promise { + const roots = await this.getRoots(); + const children = await Promise.all(roots.map(async root => { + if (root.collapsibleState === TreeItemCollapsibleState.Expanded) { + const children = await root.getChildren(); + return children ?? []; + } + return []; + })); + return roots.length + children.flat().length; + } + + private rootsPromise: Promise | undefined; + async getChildren(element?: ITreeItem): Promise { + if (element) { + return (element).getChildren(); + } else { + this.rootsPromise = undefined; + return this.getRoots(); + } + } + + private getRoots(): Promise { + if (!this.rootsPromise) { + this.rootsPromise = this.fetchRoots(); + } + return this.rootsPromise; + } + + abstract isEnabled(): boolean; + abstract onDidChangeCheckboxState(items: ITreeItem[]): ITreeItem[]; + protected abstract fetchRoots(): Promise; +} + +class UserDataProfileExportData extends UserDataProfileTreeViewData implements ITreeViewDataProvider { + + private settingsResourceTreeItem: SettingsResourceTreeItem | undefined; + private keybindingsResourceTreeItem: KeybindingsResourceTreeItem | undefined; + private tasksResourceTreeItem: TasksResourceTreeItem | undefined; + private snippetsResourceTreeItem: SnippetsResourceTreeItem | undefined; + private extensionsResourceTreeItem: ExtensionsResourceExportTreeItem | undefined; + private globalStateResourceTreeItem: GlobalStateResourceExportTreeItem | undefined; + + private readonly disposables = this._register(new DisposableStore()); + + constructor( + private readonly profile: IUserDataProfile, + @IFileService private readonly fileService: IFileService, + @IInstantiationService private readonly instantiationService: IInstantiationService + ) { + super(); + } + + onDidChangeCheckboxState(items: ITreeItem[]): ITreeItem[] { + const toRefresh: ITreeItem[] = []; + for (const item of items) { + if (item.children) { + for (const child of item.children) { + if (child.checkbox) { + child.checkbox.isChecked = !!item.checkbox?.isChecked; + } + } + toRefresh.push(item); + } else { + const parent = (item).parent; + if (item.checkbox?.isChecked && parent?.checkbox) { + parent.checkbox.isChecked = true; + toRefresh.push(parent); + } + } + } + return items; + } + + protected async fetchRoots(): Promise { + this.disposables.clear(); + this.disposables.add(this.fileService.registerProvider(USER_DATA_PROFILE_IMPORT_EXPORT_PREVIEW_SCHEME, this._register(new InMemoryFileSystemProvider()))); + const roots: IProfileResourceTreeItem[] = []; + const exportPreviewProfle = this.createExportPreviewProfile(this.profile); + + const settingsResource = this.instantiationService.createInstance(SettingsResource); + const settingsContent = await settingsResource.getContent(this.profile); + await settingsResource.apply(settingsContent, exportPreviewProfle); + this.settingsResourceTreeItem = this.instantiationService.createInstance(SettingsResourceTreeItem, exportPreviewProfle); + if (await this.settingsResourceTreeItem.hasContent()) { + roots.push(this.settingsResourceTreeItem); + } + + const keybindingsResource = this.instantiationService.createInstance(KeybindingsResource); + const keybindingsContent = await keybindingsResource.getContent(this.profile); + await keybindingsResource.apply(keybindingsContent, exportPreviewProfle); + this.keybindingsResourceTreeItem = this.instantiationService.createInstance(KeybindingsResourceTreeItem, exportPreviewProfle); + if (await this.keybindingsResourceTreeItem.hasContent()) { + roots.push(this.keybindingsResourceTreeItem); + } + + const tasksResource = this.instantiationService.createInstance(TasksResource); + const tasksContent = await tasksResource.getContent(this.profile); + await tasksResource.apply(tasksContent, exportPreviewProfle); + this.tasksResourceTreeItem = this.instantiationService.createInstance(TasksResourceTreeItem, exportPreviewProfle); + if (await this.tasksResourceTreeItem.hasContent()) { + roots.push(this.tasksResourceTreeItem); + } + + const snippetsResource = this.instantiationService.createInstance(SnippetsResource); + const snippetsContent = await snippetsResource.getContent(this.profile); + await snippetsResource.apply(snippetsContent, exportPreviewProfle); + this.snippetsResourceTreeItem = this.instantiationService.createInstance(SnippetsResourceTreeItem, exportPreviewProfle); + if (await this.snippetsResourceTreeItem.hasContent()) { + roots.push(this.snippetsResourceTreeItem); + } + + this.globalStateResourceTreeItem = this.instantiationService.createInstance(GlobalStateResourceExportTreeItem, exportPreviewProfle); + if (await this.globalStateResourceTreeItem.hasContent()) { + roots.push(this.globalStateResourceTreeItem); + } + + this.extensionsResourceTreeItem = this.instantiationService.createInstance(ExtensionsResourceExportTreeItem, exportPreviewProfle); + if (await this.extensionsResourceTreeItem.hasContent()) { + roots.push(this.extensionsResourceTreeItem); + } + + return roots; + } + + private createExportPreviewProfile(profile: IUserDataProfile): IUserDataProfile { + return { + id: profile.id, + name: profile.name, + location: profile.location, + isDefault: profile.isDefault, + shortName: profile.shortName, + globalStorageHome: profile.globalStorageHome, + settingsResource: profile.settingsResource.with({ scheme: USER_DATA_PROFILE_IMPORT_EXPORT_PREVIEW_SCHEME }), + keybindingsResource: profile.keybindingsResource.with({ scheme: USER_DATA_PROFILE_IMPORT_EXPORT_PREVIEW_SCHEME }), + tasksResource: profile.tasksResource.with({ scheme: USER_DATA_PROFILE_IMPORT_EXPORT_PREVIEW_SCHEME }), + snippetsHome: profile.snippetsHome.with({ scheme: USER_DATA_PROFILE_IMPORT_EXPORT_PREVIEW_SCHEME }), + extensionsResource: profile.extensionsResource, + useDefaultFlags: profile.useDefaultFlags, + isTransient: profile.isTransient + }; + } + + async getContent(): Promise { + const settings = this.settingsResourceTreeItem?.checkbox?.isChecked ? await this.settingsResourceTreeItem.getContent() : undefined; + const keybindings = this.keybindingsResourceTreeItem?.checkbox?.isChecked ? await this.keybindingsResourceTreeItem.getContent() : undefined; + const tasks = this.tasksResourceTreeItem?.checkbox?.isChecked ? await this.tasksResourceTreeItem.getContent() : undefined; + const snippets = this.snippetsResourceTreeItem?.checkbox?.isChecked ? await this.snippetsResourceTreeItem.getContent() : undefined; + const extensions = this.extensionsResourceTreeItem?.checkbox?.isChecked ? await this.extensionsResourceTreeItem.getContent() : undefined; + const globalState = this.globalStateResourceTreeItem?.checkbox?.isChecked ? await this.globalStateResourceTreeItem.getContent() : undefined; + const profile: IUserDataProfileTemplate = { + name: this.profile.name, + shortName: this.profile.shortName, + settings, + keybindings, + tasks, + snippets, + extensions, + globalState + }; + return JSON.stringify(profile); + } + + isEnabled(): boolean { + return !!this.settingsResourceTreeItem?.checkbox?.isChecked + || !!this.keybindingsResourceTreeItem?.checkbox?.isChecked + || !!this.tasksResourceTreeItem?.checkbox?.isChecked + || !!this.snippetsResourceTreeItem?.checkbox?.isChecked + || !!this.extensionsResourceTreeItem?.checkbox?.isChecked + || !!this.globalStateResourceTreeItem?.checkbox?.isChecked; + } + +} + +class UserDataProfileImportData extends UserDataProfileTreeViewData implements ITreeViewDataProvider { + + private settingsResourceTreeItem: SettingsResourceTreeItem | undefined; + private keybindingsResourceTreeItem: KeybindingsResourceTreeItem | undefined; + private tasksResourceTreeItem: TasksResourceTreeItem | undefined; + private snippetsResourceTreeItem: SnippetsResourceTreeItem | undefined; + private extensionsResourceTreeItem: ExtensionsResourceImportTreeItem | undefined; + private globalStateResourceTreeItem: GlobalStateResourceImportTreeItem | undefined; + + private readonly disposables = this._register(new DisposableStore()); + + constructor( + private readonly profile: IUserDataProfileTemplate, + @IFileService private readonly fileService: IFileService, + @IInstantiationService private readonly instantiationService: IInstantiationService + ) { + super(); + } + + onDidChangeCheckboxState(items: ITreeItem[]): ITreeItem[] { + return items; + } + + protected async fetchRoots(): Promise { + this.disposables.clear(); + + const inMemoryProvider = this._register(new InMemoryFileSystemProvider()); + this.disposables.add(this.fileService.registerProvider(USER_DATA_PROFILE_IMPORT_EXPORT_PREVIEW_SCHEME, inMemoryProvider)); + const roots: IProfileResourceTreeItem[] = []; + const importPreviewProfle = toUserDataProfile(generateUuid(), this.profile.name, URI.file('/root').with({ scheme: USER_DATA_PROFILE_IMPORT_EXPORT_PREVIEW_SCHEME })); + + this.settingsResourceTreeItem = undefined; + if (this.profile.settings) { + const settingsResource = this.instantiationService.createInstance(SettingsResource); + await settingsResource.apply(this.profile.settings, importPreviewProfle); + this.settingsResourceTreeItem = this.instantiationService.createInstance(SettingsResourceTreeItem, importPreviewProfle); + this.settingsResourceTreeItem.checkbox = undefined; + roots.push(this.settingsResourceTreeItem); + } + + this.keybindingsResourceTreeItem = undefined; + if (this.profile.keybindings) { + const keybindingsResource = this.instantiationService.createInstance(KeybindingsResource); + await keybindingsResource.apply(this.profile.keybindings, importPreviewProfle); + this.keybindingsResourceTreeItem = this.instantiationService.createInstance(KeybindingsResourceTreeItem, importPreviewProfle); + this.keybindingsResourceTreeItem.checkbox = undefined; + roots.push(this.keybindingsResourceTreeItem); + } + + this.tasksResourceTreeItem = undefined; + if (this.profile.tasks) { + const tasksResource = this.instantiationService.createInstance(TasksResource); + await tasksResource.apply(this.profile.tasks, importPreviewProfle); + this.tasksResourceTreeItem = this.instantiationService.createInstance(TasksResourceTreeItem, importPreviewProfle); + this.tasksResourceTreeItem.checkbox = undefined; + roots.push(this.tasksResourceTreeItem); + } + + this.snippetsResourceTreeItem = undefined; + if (this.profile.snippets) { + const snippetsResource = this.instantiationService.createInstance(SnippetsResource); + await snippetsResource.apply(this.profile.snippets, importPreviewProfle); + this.snippetsResourceTreeItem = this.instantiationService.createInstance(SnippetsResourceTreeItem, importPreviewProfle); + this.snippetsResourceTreeItem.checkbox = undefined; + roots.push(this.snippetsResourceTreeItem); + } + + this.globalStateResourceTreeItem = undefined; + if (this.profile.globalState) { + const globalStateResource = joinPath(importPreviewProfle.globalStorageHome, 'globalState.json'); + await this.fileService.writeFile(globalStateResource, VSBuffer.fromString(JSON.stringify(JSON.parse(this.profile.globalState), null, '\t'))); + this.globalStateResourceTreeItem = this.instantiationService.createInstance(GlobalStateResourceImportTreeItem, globalStateResource); + roots.push(this.globalStateResourceTreeItem); + } + + this.extensionsResourceTreeItem = undefined; + if (this.profile.extensions) { + this.extensionsResourceTreeItem = this.instantiationService.createInstance(ExtensionsResourceImportTreeItem, this.profile.extensions); + roots.push(this.extensionsResourceTreeItem); + } + + inMemoryProvider.setReadOnly(true); + + return roots; + } + + isEnabled(): boolean { + return true; + } + +} + +registerSingleton(IUserDataProfileImportExportService, UserDataProfileImportExportService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/userDataProfile/common/extensionsResource.ts b/src/vs/workbench/services/userDataProfile/common/extensionsResource.ts deleted file mode 100644 index 101a537d12e..00000000000 --- a/src/vs/workbench/services/userDataProfile/common/extensionsResource.ts +++ /dev/null @@ -1,102 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { CancellationToken } from 'vs/base/common/cancellation'; -import { IExtensionGalleryService, IExtensionIdentifier, IExtensionManagementService, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { ExtensionType } from 'vs/platform/extensions/common/extensions'; -import { ILogService } from 'vs/platform/log/common/log'; -import { EnablementState, IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { IProfileResource } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; - -interface IProfileExtension { - identifier: IExtensionIdentifier; - preRelease?: boolean; - disabled?: boolean; -} - -export class ExtensionsResource implements IProfileResource { - - constructor( - @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, - @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, - @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, - @ILogService private readonly logService: ILogService, - ) { - } - - async getContent(): Promise { - const extensions = await this.getLocalExtensions(); - return JSON.stringify(extensions); - } - - async apply(content: string): Promise { - const profileExtensions: IProfileExtension[] = JSON.parse(content); - const installedExtensions = await this.extensionManagementService.getInstalled(); - const extensionsToEnableOrDisable: { extension: ILocalExtension; enablementState: EnablementState }[] = []; - const extensionsToInstall: IProfileExtension[] = []; - for (const e of profileExtensions) { - const installedExtension = installedExtensions.find(installed => areSameExtensions(installed.identifier, e.identifier)); - if (!installedExtension || installedExtension.preRelease !== e.preRelease) { - extensionsToInstall.push(e); - } - if (installedExtension && this.extensionEnablementService.isEnabled(installedExtension) !== !e.disabled) { - extensionsToEnableOrDisable.push({ extension: installedExtension, enablementState: e.disabled ? EnablementState.DisabledGlobally : EnablementState.EnabledGlobally }); - } - } - const extensionsToUninstall: ILocalExtension[] = installedExtensions.filter(extension => extension.type === ExtensionType.User && !profileExtensions.some(({ identifier }) => areSameExtensions(identifier, extension.identifier))); - for (const { extension, enablementState } of extensionsToEnableOrDisable) { - this.logService.trace(`Profile: Updating extension enablement...`, extension.identifier.id); - await this.extensionEnablementService.setEnablement([extension], enablementState); - this.logService.info(`Profile: Updated extension enablement`, extension.identifier.id); - } - if (extensionsToInstall.length) { - const galleryExtensions = await this.extensionGalleryService.getExtensions(extensionsToInstall.map(e => ({ ...e.identifier, hasPreRelease: e.preRelease })), CancellationToken.None); - await Promise.all(extensionsToInstall.map(async e => { - const extension = galleryExtensions.find(galleryExtension => areSameExtensions(galleryExtension.identifier, e.identifier)); - if (!extension) { - return; - } - if (await this.extensionManagementService.canInstall(extension)) { - this.logService.trace(`Profile: Installing extension...`, e.identifier.id, extension.version); - await this.extensionManagementService.installFromGallery(extension, { isMachineScoped: false, donotIncludePackAndDependencies: true, installPreReleaseVersion: e.preRelease } /* set isMachineScoped value to prevent install and sync dialog in web */); - this.logService.info(`Profile: Installed extension.`, e.identifier.id, extension.version); - } else { - this.logService.info(`Profile: Skipped installing extension because it cannot be installed.`, extension.displayName || extension.identifier.id); - } - })); - } - if (extensionsToUninstall.length) { - await Promise.all(extensionsToUninstall.map(e => this.extensionManagementService.uninstall(e))); - } - } - - private async getLocalExtensions(): Promise { - const result: IProfileExtension[] = []; - const installedExtensions = await this.extensionManagementService.getInstalled(undefined); - for (const extension of installedExtensions) { - const { identifier, preRelease } = extension; - const enablementState = this.extensionEnablementService.getEnablementState(extension); - const disabled = !this.extensionEnablementService.isEnabledEnablementState(enablementState); - if (!disabled && extension.type === ExtensionType.System) { - // skip enabled system extensions - continue; - } - if (disabled && enablementState !== EnablementState.DisabledGlobally && enablementState !== EnablementState.DisabledWorkspace) { - //skip extensions that are not disabled by user - continue; - } - const profileExtension: IProfileExtension = { identifier }; - if (disabled) { - profileExtension.disabled = true; - } - if (preRelease) { - profileExtension.preRelease = true; - } - result.push(profileExtension); - } - return result; - } -} diff --git a/src/vs/workbench/services/userDataProfile/common/globalStateResource.ts b/src/vs/workbench/services/userDataProfile/common/globalStateResource.ts deleted file mode 100644 index d9f439a9ad6..00000000000 --- a/src/vs/workbench/services/userDataProfile/common/globalStateResource.ts +++ /dev/null @@ -1,64 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IStringDictionary } from 'vs/base/common/collections'; -import { ILogService } from 'vs/platform/log/common/log'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { IProfileResource } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; -import { Extensions, IProfileStorageRegistry } from 'vs/workbench/services/userDataProfile/common/userDataProfileStorageRegistry'; - -interface IGlobalState { - storage: IStringDictionary; -} - -export class GlobalStateResource implements IProfileResource { - - constructor( - @IStorageService private readonly storageService: IStorageService, - @ILogService private readonly logService: ILogService, - ) { - } - - async getContent(): Promise { - const globalState = await this.getLocalGlobalState(); - return JSON.stringify(globalState); - } - - async apply(content: string): Promise { - const globalState: IGlobalState = JSON.parse(content); - await this.writeLocalGlobalState(globalState); - } - - private async getLocalGlobalState(): Promise { - const storage: IStringDictionary = {}; - for (const { key } of Registry.as(Extensions.ProfileStorageRegistry).all) { - const value = this.storageService.get(key, StorageScope.PROFILE); - if (value) { - storage[key] = value; - } - } - return { storage }; - } - - private async writeLocalGlobalState(globalState: IGlobalState): Promise { - const profileKeys: string[] = Object.keys(globalState.storage); - const updatedStorage: IStringDictionary = globalState.storage; - for (const { key } of Registry.as(Extensions.ProfileStorageRegistry).all) { - if (!profileKeys.includes(key)) { - // Remove the key if it does not exist in the profile - updatedStorage[key] = undefined; - } - } - const updatedStorageKeys: string[] = Object.keys(updatedStorage); - if (updatedStorageKeys.length) { - this.logService.trace(`Profile: Updating global state...`); - for (const key of updatedStorageKeys) { - this.storageService.store(key, globalState.storage[key], StorageScope.PROFILE, StorageTarget.USER); - } - this.logService.info(`Profile: Updated global state`, updatedStorageKeys); - } - } -} diff --git a/src/vs/workbench/services/userDataProfile/common/settingsResource.ts b/src/vs/workbench/services/userDataProfile/common/settingsResource.ts deleted file mode 100644 index 88b1b25733e..00000000000 --- a/src/vs/workbench/services/userDataProfile/common/settingsResource.ts +++ /dev/null @@ -1,66 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { VSBuffer } from 'vs/base/common/buffer'; -import { ConfigurationScope, Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; -import { IFileService } from 'vs/platform/files/common/files'; -import { ILogService } from 'vs/platform/log/common/log'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { IUserDataProfileService, IProfileResource, ProfileCreationOptions } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; -import { removeComments, updateIgnoredSettings } from 'vs/platform/userDataSync/common/settingsMerge'; -import { IUserDataSyncUtilService } from 'vs/platform/userDataSync/common/userDataSync'; - -interface ISettingsContent { - settings: string; -} - -export class SettingsResource implements IProfileResource { - - constructor( - @IFileService private readonly fileService: IFileService, - @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, - @IUserDataSyncUtilService private readonly userDataSyncUtilService: IUserDataSyncUtilService, - @ILogService private readonly logService: ILogService, - ) { - } - - async getContent(options?: ProfileCreationOptions): Promise { - const ignoredSettings = this.getIgnoredSettings(); - const formattingOptions = await this.userDataSyncUtilService.resolveFormattingOptions(this.userDataProfileService.currentProfile.settingsResource); - const localContent = await this.getLocalFileContent(); - let settings = updateIgnoredSettings(localContent || '{}', '{}', ignoredSettings, formattingOptions); - if (options?.skipComments) { - settings = removeComments(settings, formattingOptions); - } - const settingsContent: ISettingsContent = { settings }; - return JSON.stringify(settingsContent); - } - - async apply(content: string): Promise { - const settingsContent: ISettingsContent = JSON.parse(content); - this.logService.trace(`Profile: Applying settings...`); - const localSettingsContent = await this.getLocalFileContent(); - const formattingOptions = await this.userDataSyncUtilService.resolveFormattingOptions(this.userDataProfileService.currentProfile.settingsResource); - const contentToUpdate = updateIgnoredSettings(settingsContent.settings, localSettingsContent || '{}', this.getIgnoredSettings(), formattingOptions); - await this.fileService.writeFile(this.userDataProfileService.currentProfile.settingsResource, VSBuffer.fromString(contentToUpdate)); - this.logService.info(`Profile: Applied settings`); - } - - private getIgnoredSettings(): string[] { - const allSettings = Registry.as(Extensions.Configuration).getConfigurationProperties(); - const ignoredSettings = Object.keys(allSettings).filter(key => allSettings[key]?.scope === ConfigurationScope.MACHINE || allSettings[key]?.scope === ConfigurationScope.MACHINE_OVERRIDABLE); - return ignoredSettings; - } - - private async getLocalFileContent(): Promise { - try { - const content = await this.fileService.readFile(this.userDataProfileService.currentProfile.settingsResource); - return content.value.toString(); - } catch (error) { - return null; - } - } - -} diff --git a/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts b/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts index 0109ce3c737..63b9032ad85 100644 --- a/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts +++ b/src/vs/workbench/services/userDataProfile/common/userDataProfile.ts @@ -10,6 +10,10 @@ import { MenuId } from 'vs/platform/actions/common/actions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IUserDataProfile, IUserDataProfileOptions, IUserDataProfileUpdateOptions } from 'vs/platform/userDataProfile/common/userDataProfile'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { URI } from 'vs/base/common/uri'; +import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +import { Codicon } from 'vs/base/common/codicons'; +import { ITreeItem } from 'vs/workbench/common/views'; export interface DidChangeUserDataProfileEvent { readonly preserveData: boolean; @@ -42,6 +46,9 @@ export interface IUserDataProfileManagementService { export interface IUserDataProfileTemplate { readonly settings?: string; + readonly keybindings?: string; + readonly tasks?: string; + readonly snippets?: string; readonly globalState?: string; readonly extensions?: string; } @@ -61,16 +68,36 @@ export const IUserDataProfileImportExportService = createDecorator; - importProfile(profile: IUserDataProfileTemplate): Promise; + registerProfileContentHandler(profileContentHandler: IUserDataProfileContentHandler): void; + + exportProfile(): Promise; + importProfile(uri: URI): Promise; setProfile(profile: IUserDataProfileTemplate): Promise; } export interface IProfileResource { - getContent(): Promise; - apply(content: string): Promise; + getContent(profile: IUserDataProfile): Promise; + apply(content: string, profile: IUserDataProfile): Promise; } +export interface IProfileResourceTreeItem extends ITreeItem { + getChildren(): Promise; +} + +export interface IProfileResourceChildTreeItem extends ITreeItem { + parent: IProfileResourceTreeItem; +} + +export interface IUserDataProfileContentHandler { + readonly id: string; + readonly name: string; + readonly description?: string; + saveProfile(content: string): Promise; + readProfile(uri: URI): Promise; +} + +export const defaultUserDataProfileIcon = registerIcon('defaultProfile-icon', Codicon.settings, localize('defaultProfileIcon', 'Icon for Default Profile.')); + export const ManageProfilesSubMenu = new MenuId('Profiles'); export const MANAGE_PROFILES_ACTION_ID = 'workbench.profiles.actions.manage'; export const PROFILES_TTILE = { value: localize('profiles', "Profiles"), original: 'Profiles' }; @@ -81,3 +108,4 @@ export const PROFILES_ENABLEMENT_CONTEXT = new RawContextKey('profiles. export const CURRENT_PROFILE_CONTEXT = new RawContextKey('currentProfile', ''); export const IS_CURRENT_PROFILE_TRANSIENT_CONTEXT = new RawContextKey('isCurrentProfileTransient', false); export const HAS_PROFILES_CONTEXT = new RawContextKey('hasProfiles', false); +export const IS_PROFILE_IMPORT_EXPORT_IN_PROGRESS_CONTEXT = new RawContextKey('isProfileImportExportInProgress', false); diff --git a/src/vs/workbench/services/userDataProfile/common/userDataProfileImportExportService.ts b/src/vs/workbench/services/userDataProfile/common/userDataProfileImportExportService.ts deleted file mode 100644 index bbeb2e00a12..00000000000 --- a/src/vs/workbench/services/userDataProfile/common/userDataProfileImportExportService.ts +++ /dev/null @@ -1,96 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { localize } from 'vs/nls'; -import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; -import { ExtensionsResource } from 'vs/workbench/services/userDataProfile/common/extensionsResource'; -import { GlobalStateResource } from 'vs/workbench/services/userDataProfile/common/globalStateResource'; -import { IUserDataProfileTemplate, IUserDataProfileImportExportService, PROFILES_CATEGORY, IUserDataProfileManagementService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; -import { SettingsResource } from 'vs/workbench/services/userDataProfile/common/settingsResource'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; - -export class UserDataProfileImportExportService implements IUserDataProfileImportExportService { - - readonly _serviceBrand: undefined; - - private readonly settingsResourceProfile: SettingsResource; - private readonly globalStateProfile: GlobalStateResource; - private readonly extensionsProfile: ExtensionsResource; - - constructor( - @IInstantiationService instantiationService: IInstantiationService, - @IProgressService private readonly progressService: IProgressService, - @INotificationService private readonly notificationService: INotificationService, - @IUserDataProfileManagementService private readonly userDataProfileManagementService: IUserDataProfileManagementService, - @IQuickInputService private readonly quickInputService: IQuickInputService, - ) { - this.settingsResourceProfile = instantiationService.createInstance(SettingsResource); - this.globalStateProfile = instantiationService.createInstance(GlobalStateResource); - this.extensionsProfile = instantiationService.createInstance(ExtensionsResource); - } - - async exportProfile(options?: { skipComments: boolean }): Promise { - const settings = await this.settingsResourceProfile.getContent(options); - const globalState = await this.globalStateProfile.getContent(); - const extensions = await this.extensionsProfile.getContent(); - return { - settings, - globalState, - extensions - }; - } - - async importProfile(profileTemplate: IUserDataProfileTemplate): Promise { - const name = await this.quickInputService.input({ - placeHolder: localize('name', "Profile name"), - title: localize('save profile as', "Create from Current Profile..."), - }); - if (!name) { - return undefined; - } - - await this.progressService.withProgress({ - location: ProgressLocation.Notification, - title: localize('profiles.importing', "{0}: Importing...", PROFILES_CATEGORY.value), - }, async progress => { - await this.userDataProfileManagementService.createAndEnterProfile(name); - if (profileTemplate.settings) { - await this.settingsResourceProfile.apply(profileTemplate.settings); - } - if (profileTemplate.globalState) { - await this.globalStateProfile.apply(profileTemplate.globalState); - } - if (profileTemplate.extensions) { - await this.extensionsProfile.apply(profileTemplate.extensions); - } - }); - - this.notificationService.info(localize('imported profile', "{0}: Imported successfully.", PROFILES_CATEGORY.value)); - } - - async setProfile(profile: IUserDataProfileTemplate): Promise { - await this.progressService.withProgress({ - location: ProgressLocation.Notification, - title: localize('profiles.applying', "{0}: Applying...", PROFILES_CATEGORY.value), - }, async progress => { - if (profile.settings) { - await this.settingsResourceProfile.apply(profile.settings); - } - if (profile.globalState) { - await this.globalStateProfile.apply(profile.globalState); - } - if (profile.extensions) { - await this.extensionsProfile.apply(profile.extensions); - } - }); - this.notificationService.info(localize('applied profile', "{0}: Applied successfully.", PROFILES_CATEGORY.value)); - } - -} - -registerSingleton(IUserDataProfileImportExportService, UserDataProfileImportExportService, InstantiationType.Delayed); diff --git a/src/vs/workbench/services/userDataProfile/common/userDataProfileService.ts b/src/vs/workbench/services/userDataProfile/common/userDataProfileService.ts index 37727ac08a3..9966a1cda99 100644 --- a/src/vs/workbench/services/userDataProfile/common/userDataProfileService.ts +++ b/src/vs/workbench/services/userDataProfile/common/userDataProfileService.ts @@ -4,15 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { Promises } from 'vs/base/common/async'; -import { Codicon } from 'vs/base/common/codicons'; import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; -import { localize } from 'vs/nls'; -import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; import { IUserDataProfile, IUserDataProfilesService } from 'vs/platform/userDataProfile/common/userDataProfile'; -import { DidChangeUserDataProfileEvent, IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; - -const defaultUserDataProfileIcon = registerIcon('defaultProfile-icon', Codicon.settings, localize('defaultProfileIcon', 'Icon for Default Profile.')); +import { defaultUserDataProfileIcon, DidChangeUserDataProfileEvent, IUserDataProfileService } from 'vs/workbench/services/userDataProfile/common/userDataProfile'; export class UserDataProfileService extends Disposable implements IUserDataProfileService { diff --git a/src/vs/workbench/services/userDataProfile/common/userDataProfileStorageRegistry.ts b/src/vs/workbench/services/userDataProfile/common/userDataProfileStorageRegistry.ts deleted file mode 100644 index 807b20e259b..00000000000 --- a/src/vs/workbench/services/userDataProfile/common/userDataProfileStorageRegistry.ts +++ /dev/null @@ -1,62 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Event, Emitter } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { Registry } from 'vs/platform/registry/common/platform'; - -export namespace Extensions { - export const ProfileStorageRegistry = 'workbench.registry.profile.storage'; -} - -export interface IProfileStorageKey { - readonly key: string; - readonly description?: string; -} - -/** - * A registry for storage keys used for profiles - */ -export interface IProfileStorageRegistry { - /** - * An event that is triggered when storage keys are registered. - */ - readonly onDidRegister: Event; - - /** - * All registered storage keys - */ - readonly all: IProfileStorageKey[]; - - /** - * Register profile storage keys - * - * @param keys keys to register - */ - registerKeys(keys: IProfileStorageKey[]): void; -} - -class ProfileStorageRegistryImpl extends Disposable implements IProfileStorageRegistry { - - private readonly _onDidRegister = this._register(new Emitter()); - readonly onDidRegister = this._onDidRegister.event; - - private readonly storageKeys = new Map(); - - get all(): IProfileStorageKey[] { - return [...this.storageKeys.values()].flat(); - } - - registerKeys(keys: IProfileStorageKey[]): void { - for (const key of keys) { - this.storageKeys.set(key.key, key); - } - this._onDidRegister.fire(keys); - } - -} - -Registry.add(Extensions.ProfileStorageRegistry, new ProfileStorageRegistryImpl()); - diff --git a/src/vs/workbench/services/views/browser/viewDescriptorService.ts b/src/vs/workbench/services/views/browser/viewDescriptorService.ts index 12750975fc0..19b35dc95f2 100644 --- a/src/vs/workbench/services/views/browser/viewDescriptorService.ts +++ b/src/vs/workbench/services/views/browser/viewDescriptorService.ts @@ -19,7 +19,6 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati import { getViewsStateStorageId, ViewContainerModel } from 'vs/workbench/services/views/common/viewContainerModel'; import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/actions'; import { localize } from 'vs/nls'; -import { Extensions, IProfileStorageRegistry } from 'vs/workbench/services/userDataProfile/common/userDataProfileStorageRegistry'; import { IStringDictionary } from 'vs/base/common/collections'; interface IViewsCustomizations { @@ -114,11 +113,6 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor this._register(this.extensionService.onDidRegisterExtensions(() => this.onDidRegisterExtensions())); - Registry.as(Extensions.ProfileStorageRegistry) - .registerKeys([{ - key: ViewDescriptorService.VIEWS_CUSTOMIZATIONS, - description: localize('views customizations', "Views Customizations"), - }]); } private migrateToViewsCustomizationsStorage(): void { diff --git a/src/vs/workbench/services/views/common/viewContainerModel.ts b/src/vs/workbench/services/views/common/viewContainerModel.ts index 82c767f520f..6b8593224a1 100644 --- a/src/vs/workbench/services/views/common/viewContainerModel.ts +++ b/src/vs/workbench/services/views/common/viewContainerModel.ts @@ -16,7 +16,6 @@ import { isUndefined, isUndefinedOrNull } from 'vs/base/common/types'; import { isEqual, joinPath } from 'vs/base/common/resources'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { IStringDictionary } from 'vs/base/common/collections'; -import { Extensions, IProfileStorageRegistry } from 'vs/workbench/services/userDataProfile/common/userDataProfileStorageRegistry'; import { localize } from 'vs/nls'; import { ILogger, ILoggerService } from 'vs/platform/log/common/log'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -128,11 +127,6 @@ class ViewDescriptorsState extends Disposable { this.state = this.initialize(); - Registry.as(Extensions.ProfileStorageRegistry) - .registerKeys([{ - key: this.globalViewsStateStorageId, - description: localize('globalViewsStateStorageId', "Views visibility customizations in {0} view container", viewContainerName), - }]); } set(id: string, state: IViewDescriptorState): void { diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 45eeada71ea..b36112b6b94 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -1351,10 +1351,11 @@ export class RemoteFileSystemProvider implements IFileSystemProvider { } export class TestInMemoryFileSystemProvider extends InMemoryFileSystemProvider implements IFileSystemProviderWithFileReadStreamCapability { - override readonly capabilities: FileSystemProviderCapabilities = - FileSystemProviderCapabilities.FileReadWrite - | FileSystemProviderCapabilities.PathCaseSensitive - | FileSystemProviderCapabilities.FileReadStream; + override get capabilities(): FileSystemProviderCapabilities { + return FileSystemProviderCapabilities.FileReadWrite + | FileSystemProviderCapabilities.PathCaseSensitive + | FileSystemProviderCapabilities.FileReadStream; + } readFileStream(resource: URI): ReadableStreamEvents { const BUFFER_SIZE = 64 * 1024; diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 1d03361f7cb..0298a6c9ee6 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -84,7 +84,7 @@ import 'vs/workbench/services/extensionRecommendations/common/extensionIgnoredRe import 'vs/workbench/services/extensionRecommendations/common/workspaceExtensionsConfig'; import 'vs/workbench/services/notification/common/notificationService'; import 'vs/workbench/services/userDataSync/common/userDataSyncUtil'; -import 'vs/workbench/services/userDataProfile/common/userDataProfileImportExportService'; +import 'vs/workbench/services/userDataProfile/browser/userDataProfileImportExportService'; import 'vs/workbench/services/userDataProfile/browser/userDataProfileManagement'; import 'vs/workbench/services/remote/common/remoteExplorerService'; import 'vs/workbench/services/workingCopy/common/workingCopyService'; diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index 3876bc2ec98..d097639f885 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -85,6 +85,7 @@ import 'vs/workbench/services/search/electron-sandbox/searchService'; import 'vs/workbench/services/workingCopy/electron-sandbox/workingCopyHistoryService'; import 'vs/workbench/services/userDataSync/browser/userDataSyncEnablementService'; import 'vs/workbench/services/extensions/electron-sandbox/sandboxExtensionService'; +import 'vs/platform/userDataProfile/electron-sandbox/userDataProfileStorageService'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IUserDataInitializationService, UserDataInitializationService } from 'vs/workbench/services/userData/browser/userDataInit'; diff --git a/src/vscode-dts/vscode.proposed.contribCommentThreadAdditionalMenu.d.ts b/src/vscode-dts/vscode.proposed.contribCommentThreadAdditionalMenu.d.ts new file mode 100644 index 00000000000..2e71d90a25a --- /dev/null +++ b/src/vscode-dts/vscode.proposed.contribCommentThreadAdditionalMenu.d.ts @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// empty placeholder for comment thread additional menus + +// https://github.com/microsoft/vscode/issues/163281 diff --git a/src/vscode-dts/vscode.proposed.notebookKernelSource.d.ts b/src/vscode-dts/vscode.proposed.notebookKernelSource.d.ts index 40d0d7de1b5..6820da8577b 100644 --- a/src/vscode-dts/vscode.proposed.notebookKernelSource.d.ts +++ b/src/vscode-dts/vscode.proposed.notebookKernelSource.d.ts @@ -11,10 +11,31 @@ declare module 'vscode' { dispose(): void; } + export class NotebookKernelSourceAction { + readonly label: string; + readonly description?: string; + readonly detail?: string; + readonly command: string | Command; + + constructor(label: string); + } + + export interface NotebookKernelSourceActionProvider { + /** + * Provide kernel source actions + */ + provideNotebookKernelSourceActions(token: CancellationToken): ProviderResult; + } + export namespace notebooks { /** * Create notebook controller detection task */ export function createNotebookControllerDetectionTask(notebookType: string): NotebookControllerDetectionTask; + + /** + * Register a notebook kernel source action provider + */ + export function registerKernelSourceActionProvider(notebookType: string, provider: NotebookKernelSourceActionProvider): Disposable; } }