mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-06 08:07:49 +00:00
Merge remote-tracking branch 'origin/main' into feat/maven-dependency-analyzer
# Conflicts: # src/platform/locales/text/zh.json
This commit is contained in:
Generated
+30
-30
@@ -3962,7 +3962,7 @@ checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||
|
||||
[[package]]
|
||||
name = "ide-agent"
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
dependencies = [
|
||||
"ide-core",
|
||||
"ide-jsonrpc",
|
||||
@@ -3980,7 +3980,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-analysis"
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
dependencies = [
|
||||
"ast-grep-core",
|
||||
"ide-core",
|
||||
@@ -3994,7 +3994,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-build"
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
dependencies = [
|
||||
"dirs 6.0.0",
|
||||
"dunce",
|
||||
@@ -4009,11 +4009,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-clipboard"
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
|
||||
[[package]]
|
||||
name = "ide-core"
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"serde",
|
||||
@@ -4025,7 +4025,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-coverage"
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
dependencies = [
|
||||
"ide-core",
|
||||
"ide-run",
|
||||
@@ -4039,7 +4039,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-dap"
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
dependencies = [
|
||||
"ide-core",
|
||||
"ide-jsonrpc",
|
||||
@@ -4056,7 +4056,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-db"
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
dependencies = [
|
||||
"dirs 6.0.0",
|
||||
"futures",
|
||||
@@ -4076,7 +4076,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-docker"
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
dependencies = [
|
||||
"ide-core",
|
||||
"libc",
|
||||
@@ -4090,7 +4090,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-github"
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
dependencies = [
|
||||
"ide-core",
|
||||
"ide-vcs",
|
||||
@@ -4104,7 +4104,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-http"
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
dependencies = [
|
||||
"ide-core",
|
||||
"parking_lot",
|
||||
@@ -4122,7 +4122,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-index"
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"criterion",
|
||||
@@ -4144,7 +4144,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-jsonrpc"
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"ide-core",
|
||||
@@ -4159,7 +4159,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-language"
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"criterion",
|
||||
@@ -4212,7 +4212,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-lsp"
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"globset",
|
||||
@@ -4235,7 +4235,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-plugin"
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
dependencies = [
|
||||
"ide-core",
|
||||
"ide-language",
|
||||
@@ -4253,7 +4253,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-profiler"
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
dependencies = [
|
||||
"ide-core",
|
||||
"ide-run",
|
||||
@@ -4266,7 +4266,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-run"
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"ide-core",
|
||||
@@ -4286,7 +4286,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-sdk"
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"ide-core",
|
||||
@@ -4305,7 +4305,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-search"
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
dependencies = [
|
||||
"criterion",
|
||||
"globset",
|
||||
@@ -4325,7 +4325,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-spell"
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
dependencies = [
|
||||
"ide-core",
|
||||
"parking_lot",
|
||||
@@ -4338,7 +4338,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-syntax"
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
dependencies = [
|
||||
"ide-core",
|
||||
"ide-language",
|
||||
@@ -4353,7 +4353,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-terminal"
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
dependencies = [
|
||||
"ide-core",
|
||||
"libc",
|
||||
@@ -4366,7 +4366,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-text"
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
dependencies = [
|
||||
"criterion",
|
||||
"ide-core",
|
||||
@@ -4378,7 +4378,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-tools"
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"hex",
|
||||
@@ -4398,7 +4398,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-vcs"
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"gix",
|
||||
@@ -4415,7 +4415,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-vfs"
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
dependencies = [
|
||||
"chardetng",
|
||||
"dunce",
|
||||
@@ -4436,7 +4436,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-web"
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
dependencies = [
|
||||
"ide-core",
|
||||
"open",
|
||||
@@ -4452,7 +4452,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ide-workspace"
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"dunce",
|
||||
@@ -5877,7 +5877,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openidea"
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
dependencies = [
|
||||
"dirs 6.0.0",
|
||||
"dunce",
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ resolver = "3"
|
||||
members = ["crates/*", "src-tauri"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
edition = "2024"
|
||||
rust-version = "1.96"
|
||||
license = "Apache-2.0"
|
||||
|
||||
@@ -100,6 +100,17 @@ c → cpp, php_only), node names adapted to the bundled grammar versions
|
||||
definitions re-kinded as `field` / `method`, and missing scopes, definitions and
|
||||
references added; each file's header notes its changes.
|
||||
|
||||
## Theme plugin palettes
|
||||
|
||||
The sample theme plugins port MIT-licensed palettes; each package's `LICENSE`
|
||||
holds the full notice and source revisions:
|
||||
|
||||
- `plugins/theme-solarized/`: [Solarized](https://github.com/altercation/solarized),
|
||||
Copyright (c) 2011 Ethan Schoonover.
|
||||
- `plugins/theme-one/`: Atom [One Light](https://github.com/atom/one-light-syntax) /
|
||||
[One Dark](https://github.com/atom/one-dark-syntax) syntax themes, Copyright (c) 2016
|
||||
GitHub Inc., and their UI themes, Copyright (c) 2014 GitHub Inc.
|
||||
|
||||
---
|
||||
|
||||
Apache License
|
||||
|
||||
@@ -5,7 +5,9 @@ use ide_core::event::ServerStatus;
|
||||
use ide_core::{CancellationToken, DocId, IdeEvent, LanguageId, Pos};
|
||||
use ide_jsonrpc::{Handler, Peer};
|
||||
use ide_language::{LanguageConfig, LanguageRegistry, LanguageServerConfig};
|
||||
use ide_lsp::{LaunchError, Launcher, LspManager, ProcessLauncher};
|
||||
#[cfg(unix)]
|
||||
use ide_lsp::{LaunchError, Launcher};
|
||||
use ide_lsp::{LspManager, ProcessLauncher};
|
||||
use parking_lot::Mutex;
|
||||
use serde_json::{Value, json};
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -29,14 +29,18 @@ pub use types::*;
|
||||
|
||||
use ide_core::{IdeError, IdeResult};
|
||||
use ide_language::LanguageConfig;
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use parking_lot::{Condvar, Mutex, RwLock};
|
||||
use runtime::{CallEffects, CallError, PluginEngine, Registrations, Runtime, RuntimeSetup};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
const STATE_FILE: &str = "state.json";
|
||||
/// Top-level files copied with a package on install besides those the manifest references.
|
||||
const LICENSE_FILES: &[&str] =
|
||||
&["LICENSE", "LICENSE.md", "LICENSE.txt", "NOTICE", "NOTICE.md", "NOTICE.txt", "COPYING"];
|
||||
/// Dev plugins live here, relative to a project root.
|
||||
pub const DEV_PLUGINS_DIR: &str = ".openidea/plugins";
|
||||
|
||||
@@ -52,6 +56,10 @@ struct PersistedState {
|
||||
struct ThemeRef {
|
||||
plugin: String,
|
||||
theme: String,
|
||||
/// The theme's window frame colour as the UI resolved it (`#rrggbb[aa]`), so
|
||||
/// new windows open in it before the plugin is loaded.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
frame: Option<String>,
|
||||
}
|
||||
|
||||
struct Meta {
|
||||
@@ -95,6 +103,8 @@ pub struct PluginHost {
|
||||
active_files: Arc<Mutex<HashMap<String, String>>>,
|
||||
/// Serializes loading / unloading so concurrent windows can't race.
|
||||
lifecycle: Mutex<()>,
|
||||
/// Set once the first [`load_all`](Self::load_all) has finished.
|
||||
initial_load: (Mutex<bool>, Condvar),
|
||||
}
|
||||
|
||||
impl PluginHost {
|
||||
@@ -114,6 +124,7 @@ impl PluginHost {
|
||||
state: Mutex::new(state),
|
||||
active_files: Arc::default(),
|
||||
lifecycle: Mutex::new(()),
|
||||
initial_load: (Mutex::new(false), Condvar::new()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -129,9 +140,24 @@ impl PluginHost {
|
||||
self.load_dir(&dir, PluginSource::Installed, None);
|
||||
}
|
||||
}
|
||||
let (done, cv) = &self.initial_load;
|
||||
*done.lock() = true;
|
||||
cv.notify_all();
|
||||
self.emit(None, PluginEvent::CommandsChanged);
|
||||
}
|
||||
|
||||
/// Waits (at most `timeout`) for the first [`load_all`](Self::load_all), so a
|
||||
/// listing made at startup already holds the installed plugins and the
|
||||
/// selected theme. Returns whether loading has finished.
|
||||
pub fn wait_initial_load(&self, timeout: Duration) -> bool {
|
||||
let (done, cv) = &self.initial_load;
|
||||
let mut done = done.lock();
|
||||
if !*done {
|
||||
cv.wait_while_for(&mut done, |d| !*d, timeout);
|
||||
}
|
||||
*done
|
||||
}
|
||||
|
||||
/// Loads new dev plugins from `<root>/.openidea/plugins/` and unloads vanished ones.
|
||||
pub fn sync_project(&self, root: &Path) {
|
||||
let _guard = self.lifecycle.lock();
|
||||
@@ -251,6 +277,11 @@ impl PluginHost {
|
||||
}
|
||||
std::fs::copy(src.join(file), to)?;
|
||||
}
|
||||
for name in LICENSE_FILES {
|
||||
if src.join(name).is_file() {
|
||||
std::fs::copy(src.join(name), staging.join(name))?;
|
||||
}
|
||||
}
|
||||
if target.exists() {
|
||||
std::fs::remove_dir_all(&target)?;
|
||||
}
|
||||
@@ -345,13 +376,45 @@ impl PluginHost {
|
||||
}
|
||||
}
|
||||
self.state.lock().theme =
|
||||
selection.map(|(plugin, theme)| ThemeRef { plugin: plugin.into(), theme: theme.into() });
|
||||
selection.map(|(plugin, theme)| ThemeRef { plugin: plugin.into(), theme: theme.into(), frame: None });
|
||||
self.save_state();
|
||||
let active = self.active_theme();
|
||||
self.emit(None, PluginEvent::ThemeChanged { theme: active.clone() });
|
||||
Ok(active)
|
||||
}
|
||||
|
||||
/// Records the selected theme's resolved window frame colour (`#rgb`,
|
||||
/// `#rrggbb` or `#rrggbbaa`); ignored without a selected theme.
|
||||
pub fn set_theme_frame(&self, frame: Option<&str>) -> IdeResult<()> {
|
||||
if let Some(f) = frame
|
||||
&& !is_hex_color(f)
|
||||
{
|
||||
return Err(IdeError::invalid(format!("not a hex colour: {f:?}")));
|
||||
}
|
||||
let changed = match self.state.lock().theme.as_mut() {
|
||||
Some(t) if t.frame.as_deref() != frame => {
|
||||
t.frame = frame.map(str::to_owned);
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
if changed {
|
||||
self.save_state();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Frame colour for a new window: the recorded one of the selected theme,
|
||||
/// unless its plugin is disabled. Available before plugins are loaded.
|
||||
pub fn startup_frame(&self) -> Option<String> {
|
||||
let state = self.state.lock();
|
||||
let t = state.theme.as_ref()?;
|
||||
if state.disabled.contains(&t.plugin) {
|
||||
return None;
|
||||
}
|
||||
t.frame.clone()
|
||||
}
|
||||
|
||||
/// The selected plugin theme, when its plugin is enabled and running.
|
||||
pub fn active_theme(&self) -> Option<ActiveTheme> {
|
||||
let sel = self.state.lock().theme.clone()?;
|
||||
@@ -568,6 +631,10 @@ impl Drop for PluginHost {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_hex_color(s: &str) -> bool {
|
||||
s.strip_prefix('#').is_some_and(|h| matches!(h.len(), 3 | 6 | 8) && h.bytes().all(|b| b.is_ascii_hexdigit()))
|
||||
}
|
||||
|
||||
/// Plugin packages found in `dirs` (first occurrence of an id wins). Invalid
|
||||
/// manifests are skipped; packages with missing files carry a `problem`.
|
||||
pub fn scan_catalog(dirs: &[PathBuf]) -> Vec<CatalogPlugin> {
|
||||
|
||||
@@ -217,10 +217,11 @@ fn runtime_language_and_theme_registration() {
|
||||
#[test]
|
||||
fn contribution_only_plugins() {
|
||||
let f = fixture(Limits::default());
|
||||
let theme = f.host.install_from_dir(&sample("theme-solarized-dark")).unwrap();
|
||||
let theme = f.host.install_from_dir(&sample("theme-solarized")).unwrap();
|
||||
assert_eq!(theme.status, PluginStatus::Static);
|
||||
assert!(!theme.has_code);
|
||||
assert_eq!(theme.themes[0].id, "solarized-dark");
|
||||
let ids: Vec<&str> = theme.themes.iter().map(|t| t.id.as_str()).collect();
|
||||
assert_eq!(ids, ["solarized-light", "solarized-dark"]);
|
||||
let lang = f.host.install_from_dir(&sample("language-toy")).unwrap();
|
||||
assert_eq!(lang.status, PluginStatus::Static);
|
||||
assert_eq!(lang.languages[0].id, "toy");
|
||||
@@ -228,13 +229,14 @@ fn contribution_only_plugins() {
|
||||
assert!(f.run("language-toy", "x", None).unwrap_err().contains("no code"));
|
||||
|
||||
f.take_events();
|
||||
let active = f.host.set_theme(Some(("theme-solarized-dark", "solarized-dark"))).unwrap().unwrap();
|
||||
let active = f.host.set_theme(Some(("theme-solarized", "solarized-dark"))).unwrap().unwrap();
|
||||
assert_eq!(active.theme.tokens["--bg-app"], "#002b36");
|
||||
assert_eq!(active.theme.tokens["--syn-string"], "#2aa198");
|
||||
assert!(f.take_events().iter().any(|(_, e)| matches!(e, PluginEvent::ThemeChanged { theme: Some(_) })));
|
||||
assert_eq!(f.host.list(None).active_theme, Some(active));
|
||||
|
||||
// Disabling the plugin withdraws its theme and language.
|
||||
f.host.set_enabled("theme-solarized-dark", false).unwrap();
|
||||
f.host.set_enabled("theme-solarized", false).unwrap();
|
||||
assert!(f.host.active_theme().is_none());
|
||||
assert!(f.take_events().iter().any(|(_, e)| *e == PluginEvent::ThemeChanged { theme: None }));
|
||||
f.host.set_enabled("language-toy", false).unwrap();
|
||||
@@ -260,6 +262,49 @@ fn theme_template_installs_with_its_editor_scheme() {
|
||||
assert!(tokens.len() > 150);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundled_theme_plugins_install_with_their_licence() {
|
||||
let f = fixture(Limits::default());
|
||||
for (pkg, themes) in
|
||||
[("theme-solarized", ["solarized-light", "solarized-dark"]), ("theme-one", ["one-light", "one-dark"])]
|
||||
{
|
||||
let info = f.host.install_from_dir(&sample(pkg)).unwrap();
|
||||
assert_eq!(info.status, PluginStatus::Static, "{pkg}: {:?}", info.error);
|
||||
assert!(info.authors.iter().any(|a| a.starts_with("bghtyu")));
|
||||
let license = std::fs::read_to_string(f.plugins_dir.join(pkg).join("LICENSE")).unwrap();
|
||||
assert!(license.contains("MIT License"));
|
||||
for (theme, kind) in themes.iter().zip([ThemeKind::Light, ThemeKind::Dark]) {
|
||||
let active = f.host.set_theme(Some((pkg, theme))).unwrap().unwrap();
|
||||
assert_eq!(active.theme.kind, kind);
|
||||
let tokens = &active.theme.tokens;
|
||||
assert!(["--bg-frame", "--syn-keyword", "--ansi-red"].iter().all(|t| tokens.contains_key(*t)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn theme_frame_follows_the_selection() {
|
||||
let f = fixture(Limits::default());
|
||||
f.host.install_from_dir(&sample("theme-one")).unwrap();
|
||||
// Nothing to attach it to without a selected theme.
|
||||
f.host.set_theme_frame(Some("#21252b")).unwrap();
|
||||
assert_eq!(f.host.startup_frame(), None);
|
||||
f.host.set_theme(Some(("theme-one", "one-dark"))).unwrap();
|
||||
assert!(f.host.set_theme_frame(Some("hsl(220, 13%, 15%)")).is_err());
|
||||
f.host.set_theme_frame(Some("#21252b")).unwrap();
|
||||
assert_eq!(f.host.startup_frame().as_deref(), Some("#21252b"));
|
||||
f.host.set_enabled("theme-one", false).unwrap();
|
||||
assert_eq!(f.host.startup_frame(), None);
|
||||
f.host.set_enabled("theme-one", true).unwrap();
|
||||
assert_eq!(f.host.startup_frame().as_deref(), Some("#21252b"));
|
||||
// A new selection drops the old theme's colour until the UI reports the new one.
|
||||
f.host.set_theme(Some(("theme-one", "one-light"))).unwrap();
|
||||
assert_eq!(f.host.startup_frame(), None);
|
||||
f.host.set_theme_frame(Some("#eaeaeb")).unwrap();
|
||||
f.host.set_theme(None).unwrap();
|
||||
assert_eq!(f.host.startup_frame(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn theme_with_broken_editor_scheme_fails_to_load() {
|
||||
let f = fixture(Limits::default());
|
||||
@@ -282,12 +327,17 @@ fn theme_with_broken_editor_scheme_fails_to_load() {
|
||||
fn state_persists_across_restarts() {
|
||||
let f = fixture(Limits::default());
|
||||
f.host.install_from_dir(&sample("hello")).unwrap();
|
||||
f.host.install_from_dir(&sample("theme-solarized-dark")).unwrap();
|
||||
f.host.install_from_dir(&sample("theme-solarized")).unwrap();
|
||||
f.host.set_enabled("hello", false).unwrap();
|
||||
f.host.set_theme(Some(("theme-solarized-dark", "solarized-dark"))).unwrap();
|
||||
f.host.set_theme(Some(("theme-solarized", "solarized-dark"))).unwrap();
|
||||
f.host.set_theme_frame(Some("#073642")).unwrap();
|
||||
|
||||
let again = PluginHost::new(f.plugins_dir.clone(), Arc::new(|_: Option<&str>, _| {}), Limits::default()).unwrap();
|
||||
// Known before the plugins are loaded: new windows open in the theme's frame colour.
|
||||
assert_eq!(again.startup_frame().as_deref(), Some("#073642"));
|
||||
assert!(!again.wait_initial_load(Duration::from_millis(1)));
|
||||
again.load_all();
|
||||
assert!(again.wait_initial_load(Duration::ZERO));
|
||||
let list = again.list(None);
|
||||
let hello = list.plugins.iter().find(|p| p.id == "hello").unwrap();
|
||||
assert!(!hello.enabled);
|
||||
@@ -388,14 +438,14 @@ fn catalog_lists_sample_packages() {
|
||||
|
||||
let catalog = scan_catalog(&[repo().join("plugins"), tmp.path().to_path_buf()]);
|
||||
let ids: Vec<&str> = catalog.iter().map(|c| c.id.as_str()).collect();
|
||||
for id in ["hello", "theme-solarized-dark", "language-toy", "x"] {
|
||||
for id in ["hello", "theme-solarized", "theme-one", "language-toy", "x"] {
|
||||
assert!(ids.contains(&id), "{id} missing from {ids:?}");
|
||||
}
|
||||
let hello = catalog.iter().find(|c| c.id == "hello").unwrap();
|
||||
assert!(hello.has_code && hello.problem.is_none() && hello.change_notes.is_some());
|
||||
assert_eq!(hello.commands.len(), 2);
|
||||
let theme = catalog.iter().find(|c| c.id == "theme-solarized-dark").unwrap();
|
||||
assert_eq!((theme.theme_count, theme.has_code), (1, false));
|
||||
let theme = catalog.iter().find(|c| c.id == "theme-solarized").unwrap();
|
||||
assert_eq!((theme.theme_count, theme.has_code), (2, false));
|
||||
let x = catalog.iter().find(|c| c.id == "x").unwrap();
|
||||
assert!(x.problem.as_deref().unwrap().contains("plugin.wasm"));
|
||||
}
|
||||
|
||||
+28
-2
@@ -16,6 +16,31 @@ my-theme/
|
||||
└─ my-theme.scheme.json # editor colours, optional (IntelliJ: .icls)
|
||||
```
|
||||
|
||||
## Ready-made theme plugins
|
||||
|
||||
The repository's [`plugins/`](../../plugins) folder holds two complete theme
|
||||
plugins, each with a light and a dark theme and an editor colour scheme
|
||||
(syntax, editor and terminal ANSI colours):
|
||||
|
||||
| Package | Themes | Palette |
|
||||
|---|---|---|
|
||||
| [`plugins/theme-solarized`](../../plugins/theme-solarized) | Solarized Light, Solarized Dark | [Solarized](https://github.com/altercation/solarized) by Ethan Schoonover |
|
||||
| [`plugins/theme-one`](../../plugins/theme-one) | One Light, One Dark | Atom [One Light](https://github.com/atom/one-light-syntax) / [One Dark](https://github.com/atom/one-dark-syntax) by GitHub |
|
||||
|
||||
Both palettes are MIT-licensed; each package carries the notice in its
|
||||
`LICENSE`, which is installed with it. To use one:
|
||||
|
||||
1. *Settings › Plugins › ⚙ › Install Plugin from Disk…* and pick the package
|
||||
folder (`plugins/theme-solarized` or `plugins/theme-one` of a checkout or a
|
||||
downloaded copy). Development builds also list both on the *Available*
|
||||
tab, where *Install* does the same.
|
||||
2. Select the plugin and press *Apply* next to a theme.
|
||||
|
||||
The IDE remembers the theme: the next start-up opens its windows, splash and
|
||||
workbench in the theme's colours before the plugin host has loaded.
|
||||
|
||||
## Writing a theme plugin
|
||||
|
||||
The quickest way to start is to copy [`templates/theme-plugin`](../../templates/theme-plugin).
|
||||
It contains *Harbor Night*, a complete example dark theme that sets every
|
||||
themeable token. The rest of this page covers the file format, the token
|
||||
@@ -282,8 +307,9 @@ A `fontStyle: "italic"` on comments maps to `"--syn-comment-style": "italic"`.
|
||||
- Text should keep at least WCAG AA contrast (4.5:1) on its surfaces. The
|
||||
built-in themes are checked by `src/features/settings/__tests__/contrast.test.ts`.
|
||||
- Repository checks (for changes to this repo): `pnpm vitest run scripts/theme`
|
||||
validates the template against tokens.css and keeps this reference current,
|
||||
and `cargo test -p ide-plugin` loads the template through the plugin host.
|
||||
validates the template and the theme plugins in `plugins/` against
|
||||
tokens.css and keeps this reference current, and `cargo test -p ide-plugin`
|
||||
loads them through the plugin host.
|
||||
|
||||
## Token reference
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "openidea",
|
||||
"private": true,
|
||||
"description": "An open-source IDE inspired by IntelliJ IDEA",
|
||||
"version": "0.1.16",
|
||||
"version": "0.1.17",
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
+2
-1
@@ -11,7 +11,8 @@ code at all.
|
||||
plugins/
|
||||
├─ build.sh # builds Rust plugins into components
|
||||
├─ hello/ # Rust plugin: two commands
|
||||
├─ theme-solarized-dark/ # manifest-only: a theme
|
||||
├─ theme-solarized/ # manifest-only: Solarized Light / Dark themes
|
||||
├─ theme-one/ # manifest-only: One Light / Dark themes
|
||||
└─ language-toy/ # manifest-only: a language definition
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
The One Light and One Dark palettes are adapted from GitHub's Atom One themes
|
||||
(styles/colors.less, styles/syntax-variables.less, styles/ui-variables.less):
|
||||
|
||||
- https://github.com/atom/one-light-syntax (revision d84579027410c576086dfca14d934c4bd74b0438)
|
||||
- https://github.com/atom/one-dark-syntax (revision 9c96f4454362267ac45322063e193ccf9d2debb1)
|
||||
- https://github.com/atom/one-light-ui (revision a27bce35088ecd323e44f5c2510bcf2d4e9efcf8)
|
||||
- https://github.com/atom/one-dark-ui (revision 18c2143d41e17b337fff6dc7024562c4d999be17)
|
||||
|
||||
The exact HSL definitions are kept. Atom defines no terminal palette, so the
|
||||
ANSI slots reuse One's syntax hues. Adapted to OpenIDEA's design tokens by bghtyu.
|
||||
|
||||
## one-light-syntax, one-dark-syntax
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016 GitHub Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
## one-light-ui, one-dark-ui
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2014 GitHub Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Contribution-only theme plugin: no code, just theme and editor scheme files.
|
||||
id = "theme-one"
|
||||
name = "One Themes"
|
||||
version = "1.0.0"
|
||||
description = "One Light and One Dark: the Atom One palettes by GitHub for the UI, the editor and the terminal."
|
||||
authors = ["bghtyu <3931591+bghtyu@users.noreply.github.com>", "GitHub Inc. (Atom One palettes)"]
|
||||
homepage = "https://github.com/atom/one-dark-syntax"
|
||||
change-notes = "1.0.0 — One Light and One Dark: UI, editor colour scheme and terminal ANSI colours."
|
||||
openidea-api = "0.1"
|
||||
|
||||
[[contributes.themes]]
|
||||
path = "themes/one-light.theme.json"
|
||||
editor-scheme = "themes/one-light.scheme.json"
|
||||
|
||||
[[contributes.themes]]
|
||||
path = "themes/one-dark.theme.json"
|
||||
editor-scheme = "themes/one-dark.scheme.json"
|
||||
@@ -0,0 +1,103 @@
|
||||
// Atom One palette by GitHub Inc. (https://github.com/atom/one-dark-syntax and friends),
|
||||
// MIT licence, see LICENSE. Adapted to OpenIDEA's tokens by bghtyu.
|
||||
// Editor colour scheme: syntax, editor surfaces and terminal ANSI colours.
|
||||
{
|
||||
"name": "One Dark",
|
||||
"tokens": {
|
||||
// Syntax
|
||||
"--syn-keyword": "hsl(286, 60%, 67%)",
|
||||
"--syn-string": "hsl(95, 38%, 62%)",
|
||||
"--syn-type": "hsl(39, 67%, 69%)",
|
||||
"--syn-function": "hsl(207, 82%, 66%)",
|
||||
"--syn-function-call": "hsl(207, 82%, 66%)",
|
||||
"--syn-comment": "hsl(220, 10%, 40%)",
|
||||
"--syn-comment-style": "italic",
|
||||
"--syn-doc-comment": "hsl(220, 10%, 40%)",
|
||||
"--syn-docstring": "hsl(95, 38%, 62%)",
|
||||
"--syn-annotation": "hsl(29, 54%, 61%)",
|
||||
"--syn-decorator": "hsl(29, 54%, 61%)",
|
||||
"--syn-number": "hsl(29, 54%, 61%)",
|
||||
"--syn-punct": "hsl(220, 14%, 71%)",
|
||||
"--syn-variable": "hsl(355, 65%, 65%)",
|
||||
"--syn-param": "hsl(220, 14%, 71%)",
|
||||
"--syn-property": "hsl(220, 14%, 71%)",
|
||||
"--syn-constant": "hsl(29, 54%, 61%)",
|
||||
"--syn-builtin": "hsl(187, 47%, 55%)",
|
||||
"--syn-self": "hsl(355, 65%, 65%)",
|
||||
"--syn-keyword-arg": "hsl(355, 65%, 65%)",
|
||||
"--syn-predefined": "hsl(187, 47%, 55%)",
|
||||
"--syn-escape": "hsl(187, 47%, 55%)",
|
||||
"--syn-regexp": "hsl(187, 47%, 55%)",
|
||||
"--syn-tag": "hsl(355, 65%, 65%)",
|
||||
"--syn-attribute": "hsl(29, 54%, 61%)",
|
||||
"--syn-todo": "hsl(286, 60%, 67%)",
|
||||
"--syn-link": "hsl(207, 82%, 66%)",
|
||||
"--syn-markup-heading": "hsl(207, 82%, 66%)",
|
||||
"--syn-markup-code": "hsl(95, 38%, 62%)",
|
||||
"--syn-invalid": "hsl(355, 65%, 65%)",
|
||||
"--syn-inline-value": "hsl(220, 9%, 55%)",
|
||||
"--syn-rainbow-1": "hsl(187, 47%, 55%)",
|
||||
"--syn-rainbow-2": "hsl(29, 54%, 61%)",
|
||||
"--syn-rainbow-3": "hsl(95, 38%, 62%)",
|
||||
"--syn-rainbow-4": "hsl(286, 60%, 67%)",
|
||||
"--syn-rainbow-5": "hsl(207, 82%, 66%)",
|
||||
"--syn-bracket-1": "hsl(39, 67%, 69%)",
|
||||
"--syn-bracket-2": "hsl(286, 60%, 67%)",
|
||||
"--syn-bracket-3": "hsl(207, 82%, 66%)",
|
||||
|
||||
// Editor
|
||||
"--fg-editor": "hsl(220, 14%, 71%)",
|
||||
"--caret": "hsl(220, 100%, 66%)",
|
||||
"--fg-line-number": "hsl(220, 9%, 55%)",
|
||||
"--fg-line-number-current": "hsl(220, 14%, 71%)",
|
||||
"--border-gutter": "hsl(220, 13%, 10.8%)",
|
||||
"--border-indent-guide": "hsl(220, 13%, 10.8%)",
|
||||
"--border-indent-guide-active": "hsl(220, 100%, 66%)",
|
||||
"--border-right-margin": "hsl(220, 13%, 10.8%)",
|
||||
"--bg-editor-selection": "hsl(220, 13%, 28%)",
|
||||
"--bg-editor-selection-inactive": "hsl(220, 13%, 28%)",
|
||||
"--bg-text-search-result": "color-mix(in srgb, hsl(39, 67%, 69%) 22%, hsl(220, 13%, 18%))",
|
||||
"--bg-text-search-result-current": "color-mix(in srgb, hsl(39, 67%, 69%) 36%, hsl(220, 13%, 18%))",
|
||||
"--bg-matched-brace": "hsl(220, 13%, 28%)",
|
||||
"--matched-brace-weight": "var(--fw-code-bold)",
|
||||
"--bg-matched-tag": "hsl(220, 13%, 28%)",
|
||||
"--bg-fold-placeholder": "hsl(220, 13%, 15%)",
|
||||
"--fg-fold-placeholder": "hsl(220, 14%, 71%)",
|
||||
"--fg-fold-marker": "hsl(220, 9%, 55%)",
|
||||
"--fg-whitespace": "hsl(220, 9%, 55%)",
|
||||
"--fg-unused": "hsl(220, 9%, 55%)",
|
||||
"--bg-identifier-under-caret": "hsl(220, 13%, 28%)",
|
||||
"--bg-identifier-under-caret-write": "color-mix(in srgb, hsl(355, 65%, 65%) 12%, hsl(220, 13%, 18%))",
|
||||
"--fg-inlay": "hsl(220, 9%, 55%)",
|
||||
"--bg-inlay": "hsl(220, 13%, 15%)",
|
||||
"--wave-error": "hsl(355, 65%, 65%)",
|
||||
"--wave-warning": "hsl(39, 67%, 69%)",
|
||||
"--editor-error-decoration": "underline wavy var(--wave-error)",
|
||||
"--editor-warning-decoration": "underline wavy var(--wave-warning)",
|
||||
"--editor-warning-background": "color-mix(in srgb, hsl(39, 67%, 69%) 12%, hsl(220, 13%, 18%))",
|
||||
"--bg-breakpoint-line": "color-mix(in srgb, hsl(355, 65%, 65%) 12%, hsl(220, 13%, 18%))",
|
||||
"--fg-completion-match": "hsl(207, 82%, 66%)",
|
||||
"--bg-editor-inline": "hsl(220, 13%, 15%)",
|
||||
"--bg-editor-banner": "color-mix(in srgb, hsl(207, 82%, 66%) 12%, hsl(220, 13%, 18%))",
|
||||
"--fg-weak-warning": "hsl(220, 9%, 55%)",
|
||||
"--fg-bulb": "hsl(39, 67%, 69%)",
|
||||
|
||||
// ANSI palette
|
||||
"--ansi-black": "hsl(220, 13%, 15%)",
|
||||
"--ansi-red": "hsl(355, 65%, 65%)",
|
||||
"--ansi-green": "hsl(95, 38%, 62%)",
|
||||
"--ansi-yellow": "hsl(39, 67%, 69%)",
|
||||
"--ansi-blue": "hsl(207, 82%, 66%)",
|
||||
"--ansi-magenta": "hsl(286, 60%, 67%)",
|
||||
"--ansi-cyan": "hsl(187, 47%, 55%)",
|
||||
"--ansi-white": "hsl(220, 14%, 71%)",
|
||||
"--ansi-bright-black": "hsl(220, 9%, 55%)",
|
||||
"--ansi-bright-red": "hsl(355, 65%, 65%)",
|
||||
"--ansi-bright-green": "hsl(95, 38%, 62%)",
|
||||
"--ansi-bright-yellow": "hsl(39, 67%, 69%)",
|
||||
"--ansi-bright-blue": "hsl(207, 82%, 66%)",
|
||||
"--ansi-bright-magenta": "hsl(286, 60%, 67%)",
|
||||
"--ansi-bright-cyan": "hsl(187, 47%, 55%)",
|
||||
"--ansi-bright-white": "#ffffff"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
// Atom One palette by GitHub Inc. (https://github.com/atom/one-dark-syntax and friends),
|
||||
// MIT licence, see LICENSE. Adapted to OpenIDEA's tokens by bghtyu.
|
||||
// UI theme; the editor colour scheme is in one-dark.scheme.json.
|
||||
{
|
||||
"id": "one-dark",
|
||||
"name": "One Dark",
|
||||
"kind": "dark",
|
||||
"tokens": {
|
||||
// Surfaces
|
||||
"--bg-app": "hsl(220, 13%, 18%)",
|
||||
"--bg-elev": "hsl(220, 13%, 15%)",
|
||||
"--bg-popup": "hsl(220, 13%, 16.5%)",
|
||||
"--bg-hover": "hsl(220, 13%, 22%)",
|
||||
"--bg-selected": "hsl(220, 13%, 28%)",
|
||||
"--bg-selected-strong": "hsl(220, 13%, 28%)",
|
||||
"--bg-current-line": "color-mix(in srgb, hsl(220, 100%, 80%) 4%, hsl(220, 13%, 18%))",
|
||||
"--bg-debug-line": "color-mix(in srgb, hsl(207, 82%, 66%) 12%, hsl(220, 13%, 18%))",
|
||||
"--bg-backdrop": "hsl(220, 13%, 15%)",
|
||||
"--bg-input": "hsl(220, 13%, 18%)",
|
||||
|
||||
// Lines
|
||||
"--border": "hsl(220, 13%, 10.8%)",
|
||||
"--border-popup": "hsl(220, 13%, 10.8%)",
|
||||
"--border-strong": "hsl(220, 9%, 55%)",
|
||||
"--border-window": "hsl(220, 13%, 10.8%)",
|
||||
"--control-off": "hsl(220, 9%, 55%)",
|
||||
|
||||
// Text
|
||||
"--fg-strong": "hsl(220, 14%, 71%)",
|
||||
"--fg-emphasis": "hsl(220, 14%, 71%)",
|
||||
"--fg": "hsl(220, 14%, 71%)",
|
||||
"--fg-secondary": "hsl(220, 9%, 55%)",
|
||||
"--fg-muted": "hsl(220, 9%, 55%)",
|
||||
"--fg-subtle": "hsl(220, 9%, 55%)",
|
||||
"--fg-faint": "hsl(220, 9%, 55%)",
|
||||
"--fg-gutter": "hsl(220, 9%, 55%)",
|
||||
"--fg-icon": "hsl(220, 14%, 71%)",
|
||||
|
||||
// Accent
|
||||
"--accent": "hsl(220, 100%, 66%)",
|
||||
"--link": "hsl(207, 82%, 66%)",
|
||||
"--link-hover": "hsl(187, 47%, 55%)",
|
||||
|
||||
// Charts
|
||||
"--chart-series": "hsl(355, 65%, 65%)",
|
||||
|
||||
// Status
|
||||
"--danger": "hsl(355, 65%, 65%)",
|
||||
"--warning": "hsl(39, 67%, 69%)",
|
||||
"--success": "hsl(95, 38%, 62%)",
|
||||
"--db-connected": "hsl(95, 38%, 62%)",
|
||||
"--db-connected-ring": "hsl(95, 38%, 62%)",
|
||||
"--info": "hsl(207, 82%, 66%)",
|
||||
|
||||
// Vcs
|
||||
"--vcs-modified": "hsl(207, 82%, 66%)",
|
||||
"--vcs-added": "hsl(95, 38%, 62%)",
|
||||
"--vcs-deleted": "hsl(220, 9%, 55%)",
|
||||
"--vcs-untracked": "hsl(355, 65%, 65%)",
|
||||
"--vcs-ignored": "hsl(39, 67%, 69%)",
|
||||
"--vcs-conflict": "hsl(355, 65%, 65%)",
|
||||
"--gutter-modified": "hsl(207, 82%, 66%)",
|
||||
"--gutter-added": "hsl(95, 38%, 62%)",
|
||||
"--gutter-deleted": "hsl(355, 65%, 65%)",
|
||||
|
||||
// Shadows and window controls
|
||||
"--shadow-popup": "0 4px 16px #00000024",
|
||||
"--shadow-dialog": "0 10px 36px #00000033",
|
||||
"--traffic-close": "#ff5f57",
|
||||
"--traffic-close-border": "#e0443e",
|
||||
"--traffic-close-pressed": "#bf4942",
|
||||
"--traffic-glyph": "#4d0000",
|
||||
"--traffic-off": "hsl(220, 13%, 22%)",
|
||||
"--traffic-off-border": "hsl(220, 13%, 10.8%)",
|
||||
|
||||
// Additive
|
||||
"--bg-tooltip": "hsl(220, 13%, 16.5%)",
|
||||
"--fg-tooltip": "hsl(220, 14%, 71%)",
|
||||
"--border-tooltip": "hsl(220, 13%, 10.8%)",
|
||||
"--bg-control": "hsl(220, 13%, 18%)",
|
||||
"--bg-control-hover": "hsl(220, 13%, 22%)",
|
||||
"--bg-active": "hsl(220, 13%, 22%)",
|
||||
"--bg-scrim": "color-mix(in srgb, hsl(220, 13%, 18%) 68%, transparent)",
|
||||
"--fg-on-accent": "#000000",
|
||||
"--shadow-tooltip": "0 4px 16px #00000024",
|
||||
"--shadow-balloon": "0 10px 36px #00000033",
|
||||
|
||||
// Islands contract
|
||||
"--bg-frame": "hsl(220, 13%, 15%)",
|
||||
"--bg-island": "hsl(220, 13%, 18%)",
|
||||
"--bg-stripe-hover": "hsl(220, 13%, 22%)",
|
||||
"--bg-stripe-selected": "hsl(220, 13%, 28%)",
|
||||
"--fg-stripe-selected-active": "#000000",
|
||||
"--fg-stripe-label": "hsl(220, 14%, 71%)",
|
||||
"--bg-tab-selected": "hsl(220, 13%, 28%)",
|
||||
"--border-tab-selected": "hsl(220, 100%, 66%)",
|
||||
"--bg-tab-selected-inactive": "hsl(220, 13%, 15%)",
|
||||
"--border-tab-selected-inactive": "hsl(220, 13%, 10.8%)",
|
||||
"--bg-tab-hover": "hsl(220, 13%, 22%)",
|
||||
"--border-editor-tabs": "hsl(220, 13%, 10.8%)",
|
||||
"--bg-tree-selected": "hsl(220, 13%, 28%)",
|
||||
"--bg-tree-selected-inactive": "hsl(220, 13%, 22%)",
|
||||
"--bg-tree-hover": "hsl(220, 13%, 22%)",
|
||||
"--bg-toolbar-hover": "hsl(220, 13%, 22%)",
|
||||
"--bg-toolbar-pressed": "hsl(220, 13%, 28%)",
|
||||
"--bg-toolbar-selected": "hsl(220, 13%, 28%)",
|
||||
"--bg-toolbar-selected-hover": "hsl(220, 13%, 28%)",
|
||||
"--bg-button-primary-hover": "color-mix(in srgb, var(--accent) 86%, hsl(220, 14%, 71%))",
|
||||
"--fg-button-primary": "#000000",
|
||||
"--bg-button": "hsl(220, 13%, 18%)",
|
||||
"--border-button": "hsl(220, 13%, 10.8%)",
|
||||
"--bg-field": "hsl(220, 13%, 18%)",
|
||||
"--bg-checkbox": "hsl(220, 13%, 18%)",
|
||||
"--border-checkbox": "hsl(220, 9%, 55%)",
|
||||
"--bg-toggle-on": "hsl(95, 38%, 62%)",
|
||||
"--fg-toggle-on": "#000000",
|
||||
"--bg-toggle-off": "hsl(220, 13%, 15%)",
|
||||
"--fg-toggle-off": "hsl(220, 14%, 71%)",
|
||||
"--border-toggle": "hsl(220, 9%, 55%)",
|
||||
"--bg-toggle-knob": "hsl(220, 9%, 55%)",
|
||||
"--focus-ring-soft": "hsl(220, 100%, 66%)",
|
||||
"--scrollbar-thumb": "color-mix(in srgb, hsl(220, 14%, 71%) 24%, transparent)",
|
||||
"--scrollbar-thumb-hover": "color-mix(in srgb, hsl(220, 14%, 71%) 42%, transparent)",
|
||||
"--bg-search-match": "color-mix(in srgb, hsl(39, 67%, 69%) 22%, hsl(220, 13%, 18%))",
|
||||
"--bg-search-match-current": "color-mix(in srgb, hsl(39, 67%, 69%) 36%, hsl(220, 13%, 18%))",
|
||||
"--fg-search-match": "hsl(220, 14%, 71%)",
|
||||
"--bg-dialog": "hsl(220, 13%, 18%)",
|
||||
"--bg-popup-inline": "hsl(220, 13%, 15%)",
|
||||
"--bg-popup-advertiser": "hsl(220, 13%, 15%)",
|
||||
"--bg-feedback-info": "color-mix(in srgb, hsl(207, 82%, 66%) 12%, hsl(220, 13%, 18%))",
|
||||
"--border-feedback-info": "color-mix(in srgb, hsl(207, 82%, 66%) 55%, hsl(220, 13%, 18%))",
|
||||
"--bg-feedback-error": "color-mix(in srgb, hsl(355, 65%, 65%) 12%, hsl(220, 13%, 18%))",
|
||||
"--border-feedback-error": "color-mix(in srgb, hsl(355, 65%, 65%) 55%, hsl(220, 13%, 18%))",
|
||||
"--bg-feedback-warning": "color-mix(in srgb, hsl(39, 67%, 69%) 12%, hsl(220, 13%, 18%))",
|
||||
"--border-feedback-warning": "color-mix(in srgb, hsl(39, 67%, 69%) 55%, hsl(220, 13%, 18%))",
|
||||
"--bg-feedback-success": "color-mix(in srgb, hsl(95, 38%, 62%) 12%, hsl(220, 13%, 18%))",
|
||||
"--border-feedback-success": "color-mix(in srgb, hsl(95, 38%, 62%) 55%, hsl(220, 13%, 18%))",
|
||||
"--bg-counter": "hsl(220, 100%, 66%)",
|
||||
"--fg-counter": "#000000",
|
||||
"--bg-progress-track": "hsl(220, 13%, 22%)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// Atom One palette by GitHub Inc. (https://github.com/atom/one-dark-syntax and friends),
|
||||
// MIT licence, see LICENSE. Adapted to OpenIDEA's tokens by bghtyu.
|
||||
// Editor colour scheme: syntax, editor surfaces and terminal ANSI colours.
|
||||
{
|
||||
"name": "One Light",
|
||||
"tokens": {
|
||||
// Syntax
|
||||
"--syn-keyword": "hsl(301, 63%, 40%)",
|
||||
"--syn-string": "hsl(119, 34%, 47%)",
|
||||
"--syn-type": "hsl(41, 99%, 38%)",
|
||||
"--syn-function": "hsl(221, 87%, 60%)",
|
||||
"--syn-function-call": "hsl(221, 87%, 60%)",
|
||||
"--syn-comment": "hsl(230, 4%, 64%)",
|
||||
"--syn-comment-style": "italic",
|
||||
"--syn-doc-comment": "hsl(230, 4%, 64%)",
|
||||
"--syn-docstring": "hsl(119, 34%, 47%)",
|
||||
"--syn-annotation": "hsl(41, 99%, 30%)",
|
||||
"--syn-decorator": "hsl(41, 99%, 30%)",
|
||||
"--syn-number": "hsl(41, 99%, 30%)",
|
||||
"--syn-punct": "hsl(230, 8%, 24%)",
|
||||
"--syn-variable": "hsl(5, 74%, 59%)",
|
||||
"--syn-param": "hsl(230, 8%, 24%)",
|
||||
"--syn-property": "hsl(230, 8%, 24%)",
|
||||
"--syn-constant": "hsl(41, 99%, 30%)",
|
||||
"--syn-builtin": "hsl(198, 99%, 37%)",
|
||||
"--syn-self": "hsl(5, 74%, 59%)",
|
||||
"--syn-keyword-arg": "hsl(5, 74%, 59%)",
|
||||
"--syn-predefined": "hsl(198, 99%, 37%)",
|
||||
"--syn-escape": "hsl(198, 99%, 37%)",
|
||||
"--syn-regexp": "hsl(198, 99%, 37%)",
|
||||
"--syn-tag": "hsl(5, 74%, 59%)",
|
||||
"--syn-attribute": "hsl(41, 99%, 30%)",
|
||||
"--syn-todo": "hsl(301, 63%, 40%)",
|
||||
"--syn-link": "hsl(221, 87%, 60%)",
|
||||
"--syn-markup-heading": "hsl(221, 87%, 60%)",
|
||||
"--syn-markup-code": "hsl(119, 34%, 47%)",
|
||||
"--syn-invalid": "hsl(5, 74%, 59%)",
|
||||
"--syn-inline-value": "hsl(230, 6%, 44%)",
|
||||
"--syn-rainbow-1": "hsl(198, 99%, 37%)",
|
||||
"--syn-rainbow-2": "hsl(41, 99%, 30%)",
|
||||
"--syn-rainbow-3": "hsl(119, 34%, 47%)",
|
||||
"--syn-rainbow-4": "hsl(301, 63%, 40%)",
|
||||
"--syn-rainbow-5": "hsl(221, 87%, 60%)",
|
||||
"--syn-bracket-1": "hsl(41, 99%, 38%)",
|
||||
"--syn-bracket-2": "hsl(301, 63%, 40%)",
|
||||
"--syn-bracket-3": "hsl(221, 87%, 60%)",
|
||||
|
||||
// Editor
|
||||
"--fg-editor": "hsl(230, 8%, 24%)",
|
||||
"--caret": "hsl(230, 100%, 66%)",
|
||||
"--fg-line-number": "hsl(230, 6%, 44%)",
|
||||
"--fg-line-number-current": "hsl(230, 8%, 24%)",
|
||||
"--border-gutter": "hsl(230, 1%, 86%)",
|
||||
"--border-indent-guide": "hsl(230, 1%, 86%)",
|
||||
"--border-indent-guide-active": "hsl(230, 100%, 66%)",
|
||||
"--border-right-margin": "hsl(230, 1%, 86%)",
|
||||
"--bg-editor-selection": "hsl(230, 1%, 90%)",
|
||||
"--bg-editor-selection-inactive": "hsl(230, 1%, 90%)",
|
||||
"--bg-text-search-result": "color-mix(in srgb, hsl(41, 99%, 38%) 22%, hsl(230, 1%, 98%))",
|
||||
"--bg-text-search-result-current": "color-mix(in srgb, hsl(41, 99%, 38%) 36%, hsl(230, 1%, 98%))",
|
||||
"--bg-matched-brace": "hsl(230, 1%, 90%)",
|
||||
"--matched-brace-weight": "var(--fw-code-bold)",
|
||||
"--bg-matched-tag": "hsl(230, 1%, 90%)",
|
||||
"--bg-fold-placeholder": "hsl(230, 1%, 92%)",
|
||||
"--fg-fold-placeholder": "hsl(230, 8%, 24%)",
|
||||
"--fg-fold-marker": "hsl(230, 6%, 44%)",
|
||||
"--fg-whitespace": "hsl(230, 6%, 44%)",
|
||||
"--fg-unused": "hsl(230, 6%, 44%)",
|
||||
"--bg-identifier-under-caret": "hsl(230, 1%, 90%)",
|
||||
"--bg-identifier-under-caret-write": "color-mix(in srgb, hsl(5, 74%, 59%) 12%, hsl(230, 1%, 98%))",
|
||||
"--fg-inlay": "hsl(230, 6%, 44%)",
|
||||
"--bg-inlay": "hsl(230, 1%, 92%)",
|
||||
"--wave-error": "hsl(5, 74%, 59%)",
|
||||
"--wave-warning": "hsl(41, 99%, 38%)",
|
||||
"--editor-error-decoration": "underline wavy var(--wave-error)",
|
||||
"--editor-warning-decoration": "underline wavy var(--wave-warning)",
|
||||
"--editor-warning-background": "color-mix(in srgb, hsl(41, 99%, 38%) 12%, hsl(230, 1%, 98%))",
|
||||
"--bg-breakpoint-line": "color-mix(in srgb, hsl(5, 74%, 59%) 12%, hsl(230, 1%, 98%))",
|
||||
"--fg-completion-match": "hsl(221, 87%, 60%)",
|
||||
"--bg-editor-inline": "hsl(230, 1%, 92%)",
|
||||
"--bg-editor-banner": "color-mix(in srgb, hsl(221, 87%, 60%) 12%, hsl(230, 1%, 98%))",
|
||||
"--fg-weak-warning": "hsl(230, 6%, 44%)",
|
||||
"--fg-bulb": "hsl(41, 99%, 38%)",
|
||||
|
||||
// ANSI palette
|
||||
"--ansi-black": "hsl(230, 8%, 24%)",
|
||||
"--ansi-red": "hsl(5, 74%, 59%)",
|
||||
"--ansi-green": "hsl(119, 34%, 47%)",
|
||||
"--ansi-yellow": "hsl(41, 99%, 38%)",
|
||||
"--ansi-blue": "hsl(221, 87%, 60%)",
|
||||
"--ansi-magenta": "hsl(301, 63%, 40%)",
|
||||
"--ansi-cyan": "hsl(198, 99%, 37%)",
|
||||
"--ansi-white": "hsl(230, 6%, 44%)",
|
||||
"--ansi-bright-black": "hsl(230, 6%, 44%)",
|
||||
"--ansi-bright-red": "hsl(5, 74%, 59%)",
|
||||
"--ansi-bright-green": "hsl(119, 34%, 47%)",
|
||||
"--ansi-bright-yellow": "hsl(41, 99%, 38%)",
|
||||
"--ansi-bright-blue": "hsl(221, 87%, 60%)",
|
||||
"--ansi-bright-magenta": "hsl(301, 63%, 40%)",
|
||||
"--ansi-bright-cyan": "hsl(198, 99%, 37%)",
|
||||
"--ansi-bright-white": "hsl(230, 8%, 24%)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
// Atom One palette by GitHub Inc. (https://github.com/atom/one-dark-syntax and friends),
|
||||
// MIT licence, see LICENSE. Adapted to OpenIDEA's tokens by bghtyu.
|
||||
// UI theme; the editor colour scheme is in one-light.scheme.json.
|
||||
{
|
||||
"id": "one-light",
|
||||
"name": "One Light",
|
||||
"kind": "light",
|
||||
"tokens": {
|
||||
// Surfaces
|
||||
"--bg-app": "hsl(230, 1%, 98%)",
|
||||
"--bg-elev": "hsl(230, 1%, 92%)",
|
||||
"--bg-popup": "hsl(230, 1%, 95%)",
|
||||
"--bg-hover": "hsl(230, 1%, 90%)",
|
||||
"--bg-selected": "hsl(230, 1%, 90%)",
|
||||
"--bg-selected-strong": "hsl(230, 1%, 90%)",
|
||||
"--bg-current-line": "color-mix(in srgb, hsl(230, 8%, 24%) 5%, hsl(230, 1%, 98%))",
|
||||
"--bg-debug-line": "color-mix(in srgb, hsl(221, 87%, 60%) 12%, hsl(230, 1%, 98%))",
|
||||
"--bg-backdrop": "hsl(230, 1%, 92%)",
|
||||
"--bg-input": "hsl(230, 1%, 98%)",
|
||||
|
||||
// Lines
|
||||
"--border": "hsl(230, 1%, 86%)",
|
||||
"--border-popup": "hsl(230, 1%, 86%)",
|
||||
"--border-strong": "hsl(230, 6%, 44%)",
|
||||
"--border-window": "hsl(230, 1%, 86%)",
|
||||
"--control-off": "hsl(230, 6%, 44%)",
|
||||
|
||||
// Text
|
||||
"--fg-strong": "hsl(230, 8%, 24%)",
|
||||
"--fg-emphasis": "hsl(230, 8%, 24%)",
|
||||
"--fg": "hsl(230, 8%, 24%)",
|
||||
"--fg-secondary": "hsl(230, 6%, 44%)",
|
||||
"--fg-muted": "hsl(230, 6%, 44%)",
|
||||
"--fg-subtle": "hsl(230, 6%, 44%)",
|
||||
"--fg-faint": "hsl(230, 6%, 44%)",
|
||||
"--fg-gutter": "hsl(230, 6%, 44%)",
|
||||
"--fg-icon": "hsl(230, 8%, 24%)",
|
||||
|
||||
// Accent
|
||||
"--accent": "hsl(230, 100%, 66%)",
|
||||
"--link": "hsl(221, 87%, 60%)",
|
||||
"--link-hover": "hsl(198, 99%, 37%)",
|
||||
|
||||
// Charts
|
||||
"--chart-series": "hsl(5, 74%, 59%)",
|
||||
|
||||
// Status
|
||||
"--danger": "hsl(5, 74%, 59%)",
|
||||
"--warning": "hsl(41, 99%, 38%)",
|
||||
"--success": "hsl(119, 34%, 47%)",
|
||||
"--db-connected": "hsl(119, 34%, 47%)",
|
||||
"--db-connected-ring": "hsl(119, 34%, 47%)",
|
||||
"--info": "hsl(221, 87%, 60%)",
|
||||
|
||||
// Vcs
|
||||
"--vcs-modified": "hsl(221, 87%, 60%)",
|
||||
"--vcs-added": "hsl(119, 34%, 47%)",
|
||||
"--vcs-deleted": "hsl(230, 6%, 44%)",
|
||||
"--vcs-untracked": "hsl(5, 74%, 59%)",
|
||||
"--vcs-ignored": "hsl(41, 99%, 38%)",
|
||||
"--vcs-conflict": "hsl(5, 74%, 59%)",
|
||||
"--gutter-modified": "hsl(221, 87%, 60%)",
|
||||
"--gutter-added": "hsl(119, 34%, 47%)",
|
||||
"--gutter-deleted": "hsl(5, 74%, 59%)",
|
||||
|
||||
// Shadows and window controls
|
||||
"--shadow-popup": "0 4px 16px #00000024",
|
||||
"--shadow-dialog": "0 10px 36px #00000033",
|
||||
"--traffic-close": "#ff5f57",
|
||||
"--traffic-close-border": "#e0443e",
|
||||
"--traffic-close-pressed": "#bf4942",
|
||||
"--traffic-glyph": "#4d0000",
|
||||
"--traffic-off": "hsl(230, 1%, 90%)",
|
||||
"--traffic-off-border": "hsl(230, 1%, 86%)",
|
||||
|
||||
// Additive
|
||||
"--bg-tooltip": "hsl(230, 1%, 95%)",
|
||||
"--fg-tooltip": "hsl(230, 8%, 24%)",
|
||||
"--border-tooltip": "hsl(230, 1%, 86%)",
|
||||
"--bg-control": "hsl(230, 1%, 98%)",
|
||||
"--bg-control-hover": "hsl(230, 1%, 90%)",
|
||||
"--bg-active": "hsl(230, 1%, 90%)",
|
||||
"--bg-scrim": "color-mix(in srgb, hsl(230, 1%, 98%) 68%, transparent)",
|
||||
"--fg-on-accent": "#ffffff",
|
||||
"--shadow-tooltip": "0 4px 16px #00000024",
|
||||
"--shadow-balloon": "0 10px 36px #00000033",
|
||||
|
||||
// Islands contract
|
||||
"--bg-frame": "hsl(230, 1%, 92%)",
|
||||
"--bg-island": "hsl(230, 1%, 98%)",
|
||||
"--bg-stripe-hover": "hsl(230, 1%, 90%)",
|
||||
"--bg-stripe-selected": "hsl(230, 1%, 90%)",
|
||||
"--fg-stripe-selected-active": "#ffffff",
|
||||
"--fg-stripe-label": "hsl(230, 8%, 24%)",
|
||||
"--bg-tab-selected": "hsl(230, 1%, 90%)",
|
||||
"--border-tab-selected": "hsl(230, 100%, 66%)",
|
||||
"--bg-tab-selected-inactive": "hsl(230, 1%, 92%)",
|
||||
"--border-tab-selected-inactive": "hsl(230, 1%, 86%)",
|
||||
"--bg-tab-hover": "hsl(230, 1%, 90%)",
|
||||
"--border-editor-tabs": "hsl(230, 1%, 86%)",
|
||||
"--bg-tree-selected": "hsl(230, 1%, 90%)",
|
||||
"--bg-tree-selected-inactive": "hsl(230, 1%, 90%)",
|
||||
"--bg-tree-hover": "hsl(230, 1%, 90%)",
|
||||
"--bg-toolbar-hover": "hsl(230, 1%, 90%)",
|
||||
"--bg-toolbar-pressed": "hsl(230, 1%, 90%)",
|
||||
"--bg-toolbar-selected": "hsl(230, 1%, 90%)",
|
||||
"--bg-toolbar-selected-hover": "hsl(230, 1%, 90%)",
|
||||
"--bg-button-primary-hover": "color-mix(in srgb, var(--accent) 86%, hsl(230, 8%, 24%))",
|
||||
"--fg-button-primary": "#ffffff",
|
||||
"--bg-button": "hsl(230, 1%, 98%)",
|
||||
"--border-button": "hsl(230, 1%, 86%)",
|
||||
"--bg-field": "hsl(230, 1%, 98%)",
|
||||
"--bg-checkbox": "hsl(230, 1%, 98%)",
|
||||
"--border-checkbox": "hsl(230, 6%, 44%)",
|
||||
"--bg-toggle-on": "hsl(119, 34%, 47%)",
|
||||
"--fg-toggle-on": "#ffffff",
|
||||
"--bg-toggle-off": "hsl(230, 1%, 92%)",
|
||||
"--fg-toggle-off": "hsl(230, 8%, 24%)",
|
||||
"--border-toggle": "hsl(230, 6%, 44%)",
|
||||
"--bg-toggle-knob": "hsl(230, 6%, 44%)",
|
||||
"--focus-ring-soft": "hsl(230, 100%, 66%)",
|
||||
"--scrollbar-thumb": "color-mix(in srgb, hsl(230, 8%, 24%) 24%, transparent)",
|
||||
"--scrollbar-thumb-hover": "color-mix(in srgb, hsl(230, 8%, 24%) 42%, transparent)",
|
||||
"--bg-search-match": "color-mix(in srgb, hsl(41, 99%, 38%) 22%, hsl(230, 1%, 98%))",
|
||||
"--bg-search-match-current": "color-mix(in srgb, hsl(41, 99%, 38%) 36%, hsl(230, 1%, 98%))",
|
||||
"--fg-search-match": "hsl(230, 8%, 24%)",
|
||||
"--bg-dialog": "hsl(230, 1%, 98%)",
|
||||
"--bg-popup-inline": "hsl(230, 1%, 92%)",
|
||||
"--bg-popup-advertiser": "hsl(230, 1%, 92%)",
|
||||
"--bg-feedback-info": "color-mix(in srgb, hsl(221, 87%, 60%) 12%, hsl(230, 1%, 98%))",
|
||||
"--border-feedback-info": "color-mix(in srgb, hsl(221, 87%, 60%) 55%, hsl(230, 1%, 98%))",
|
||||
"--bg-feedback-error": "color-mix(in srgb, hsl(5, 74%, 59%) 12%, hsl(230, 1%, 98%))",
|
||||
"--border-feedback-error": "color-mix(in srgb, hsl(5, 74%, 59%) 55%, hsl(230, 1%, 98%))",
|
||||
"--bg-feedback-warning": "color-mix(in srgb, hsl(41, 99%, 38%) 12%, hsl(230, 1%, 98%))",
|
||||
"--border-feedback-warning": "color-mix(in srgb, hsl(41, 99%, 38%) 55%, hsl(230, 1%, 98%))",
|
||||
"--bg-feedback-success": "color-mix(in srgb, hsl(119, 34%, 47%) 12%, hsl(230, 1%, 98%))",
|
||||
"--border-feedback-success": "color-mix(in srgb, hsl(119, 34%, 47%) 55%, hsl(230, 1%, 98%))",
|
||||
"--bg-counter": "hsl(230, 100%, 66%)",
|
||||
"--fg-counter": "#ffffff",
|
||||
"--bg-progress-track": "hsl(230, 1%, 90%)"
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
# Contribution-only plugin: no code, just a theme file.
|
||||
id = "theme-solarized-dark"
|
||||
name = "Solarized Dark Theme"
|
||||
version = "1.0.0"
|
||||
description = "Ethan Schoonover's Solarized palette, dark variant."
|
||||
authors = ["OpenIDEA contributors"]
|
||||
change-notes = "1.0.0 — Solarized Dark UI and syntax colours."
|
||||
openidea-api = "0.1"
|
||||
|
||||
[[contributes.themes]]
|
||||
path = "themes/solarized-dark.json"
|
||||
@@ -1,64 +0,0 @@
|
||||
{
|
||||
"id": "solarized-dark",
|
||||
"name": "Solarized Dark",
|
||||
"kind": "dark",
|
||||
"tokens": {
|
||||
"--bg-app": "#002b36",
|
||||
"--bg-elev": "#03313c",
|
||||
"--bg-popup": "#073642",
|
||||
"--bg-hover": "#0a3d4a",
|
||||
"--bg-selected": "#0d4554",
|
||||
"--bg-selected-strong": "#104d5d",
|
||||
"--bg-current-line": "#04323d",
|
||||
"--bg-debug-line": "#0b3a50",
|
||||
"--bg-backdrop": "#00212a",
|
||||
"--bg-tooltip": "#073642",
|
||||
"--bg-control": "#073642",
|
||||
"--bg-control-hover": "#0a3d4a",
|
||||
"--bg-active": "#104d5d",
|
||||
"--border": "#06323d",
|
||||
"--border-popup": "#0a3d4a",
|
||||
"--border-strong": "#124b59",
|
||||
"--border-window": "#0a3d4a",
|
||||
"--control-off": "#2c5662",
|
||||
"--fg-strong": "#eee8d5",
|
||||
"--fg-emphasis": "#d9d4c1",
|
||||
"--fg": "#93a1a1",
|
||||
"--fg-secondary": "#839496",
|
||||
"--fg-muted": "#839496",
|
||||
"--fg-subtle": "#657b83",
|
||||
"--fg-faint": "#586e75",
|
||||
"--fg-gutter": "#4c636b",
|
||||
"--fg-tooltip": "#eee8d5",
|
||||
"--fg-on-accent": "#002b36",
|
||||
"--accent": "#268bd2",
|
||||
"--link": "#268bd2",
|
||||
"--link-hover": "#5fa9e0",
|
||||
"--danger": "#dc322f",
|
||||
"--warning": "#b58900",
|
||||
"--success": "#859900",
|
||||
"--info": "#268bd2",
|
||||
"--vcs-modified": "#b58900",
|
||||
"--vcs-added": "#859900",
|
||||
"--vcs-deleted": "#657b83",
|
||||
"--vcs-untracked": "#cb4b16",
|
||||
"--vcs-ignored": "#586e75",
|
||||
"--vcs-conflict": "#d33682",
|
||||
"--gutter-modified": "#268bd2",
|
||||
"--gutter-added": "#859900",
|
||||
"--gutter-deleted": "#dc322f",
|
||||
"--syn-keyword": "#859900",
|
||||
"--syn-string": "#2aa198",
|
||||
"--syn-type": "#b58900",
|
||||
"--syn-function": "#268bd2",
|
||||
"--syn-comment": "#586e75",
|
||||
"--syn-annotation": "#6c71c4",
|
||||
"--syn-number": "#d33682",
|
||||
"--syn-punct": "#839496",
|
||||
"--syn-variable": "#93a1a1",
|
||||
"--syn-property": "#93a1a1",
|
||||
"--syn-constant": "#cb4b16",
|
||||
"--syn-invalid": "#dc322f",
|
||||
"--syn-inline-value": "#657b83"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
The Solarized palette is adapted from Ethan Schoonover's Solarized,
|
||||
https://github.com/altercation/solarized (revision 62f656a02f93c5190a8753159e34b385588d5ff3):
|
||||
the sixteen sRGB colours of its README, the semantic roles of
|
||||
vim-colors-solarized/colors/solarized.vim and its iTerm2 ANSI slots.
|
||||
Adapted to OpenIDEA's design tokens by bghtyu.
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2011 Ethan Schoonover
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Contribution-only theme plugin: no code, just theme and editor scheme files.
|
||||
id = "theme-solarized"
|
||||
name = "Solarized Themes"
|
||||
version = "1.0.0"
|
||||
description = "Solarized Light and Solarized Dark: Ethan Schoonover's Solarized palette for the UI, the editor and the terminal."
|
||||
authors = ["bghtyu <3931591+bghtyu@users.noreply.github.com>", "Ethan Schoonover (Solarized palette)"]
|
||||
homepage = "https://github.com/altercation/solarized"
|
||||
change-notes = "1.0.0 — Solarized Light and Solarized Dark: UI, editor colour scheme and terminal ANSI colours."
|
||||
openidea-api = "0.1"
|
||||
|
||||
[[contributes.themes]]
|
||||
path = "themes/solarized-light.theme.json"
|
||||
editor-scheme = "themes/solarized-light.scheme.json"
|
||||
|
||||
[[contributes.themes]]
|
||||
path = "themes/solarized-dark.theme.json"
|
||||
editor-scheme = "themes/solarized-dark.scheme.json"
|
||||
@@ -0,0 +1,103 @@
|
||||
// Solarized palette by Ethan Schoonover (https://github.com/altercation/solarized),
|
||||
// MIT licence, see LICENSE. Adapted to OpenIDEA's tokens by bghtyu.
|
||||
// Editor colour scheme: syntax, editor surfaces and terminal ANSI colours.
|
||||
{
|
||||
"name": "Solarized Dark",
|
||||
"tokens": {
|
||||
// Syntax
|
||||
"--syn-keyword": "#859900",
|
||||
"--syn-string": "#2aa198",
|
||||
"--syn-type": "#b58900",
|
||||
"--syn-function": "#268bd2",
|
||||
"--syn-function-call": "#268bd2",
|
||||
"--syn-comment": "#586e75",
|
||||
"--syn-comment-style": "italic",
|
||||
"--syn-doc-comment": "#586e75",
|
||||
"--syn-docstring": "#2aa198",
|
||||
"--syn-annotation": "#cb4b16",
|
||||
"--syn-decorator": "#cb4b16",
|
||||
"--syn-number": "#2aa198",
|
||||
"--syn-punct": "#839496",
|
||||
"--syn-variable": "#268bd2",
|
||||
"--syn-param": "#839496",
|
||||
"--syn-property": "#268bd2",
|
||||
"--syn-constant": "#2aa198",
|
||||
"--syn-builtin": "#2aa198",
|
||||
"--syn-self": "#dc322f",
|
||||
"--syn-keyword-arg": "#dc322f",
|
||||
"--syn-predefined": "#2aa198",
|
||||
"--syn-escape": "#dc322f",
|
||||
"--syn-regexp": "#2aa198",
|
||||
"--syn-tag": "#268bd2",
|
||||
"--syn-attribute": "#cb4b16",
|
||||
"--syn-todo": "#d33682",
|
||||
"--syn-link": "#268bd2",
|
||||
"--syn-markup-heading": "#268bd2",
|
||||
"--syn-markup-code": "#2aa198",
|
||||
"--syn-invalid": "#dc322f",
|
||||
"--syn-inline-value": "#839496",
|
||||
"--syn-rainbow-1": "#2aa198",
|
||||
"--syn-rainbow-2": "#cb4b16",
|
||||
"--syn-rainbow-3": "#859900",
|
||||
"--syn-rainbow-4": "#d33682",
|
||||
"--syn-rainbow-5": "#268bd2",
|
||||
"--syn-bracket-1": "#b58900",
|
||||
"--syn-bracket-2": "#d33682",
|
||||
"--syn-bracket-3": "#268bd2",
|
||||
|
||||
// Editor
|
||||
"--fg-editor": "#839496",
|
||||
"--caret": "#268bd2",
|
||||
"--fg-line-number": "#839496",
|
||||
"--fg-line-number-current": "#839496",
|
||||
"--border-gutter": "#073642",
|
||||
"--border-indent-guide": "#073642",
|
||||
"--border-indent-guide-active": "#268bd2",
|
||||
"--border-right-margin": "#073642",
|
||||
"--bg-editor-selection": "#073642",
|
||||
"--bg-editor-selection-inactive": "#073642",
|
||||
"--bg-text-search-result": "color-mix(in srgb, #b58900 22%, #002b36)",
|
||||
"--bg-text-search-result-current": "color-mix(in srgb, #b58900 36%, #002b36)",
|
||||
"--bg-matched-brace": "#073642",
|
||||
"--matched-brace-weight": "var(--fw-code-bold)",
|
||||
"--bg-matched-tag": "#073642",
|
||||
"--bg-fold-placeholder": "#073642",
|
||||
"--fg-fold-placeholder": "#839496",
|
||||
"--fg-fold-marker": "#839496",
|
||||
"--fg-whitespace": "#839496",
|
||||
"--fg-unused": "#839496",
|
||||
"--bg-identifier-under-caret": "#073642",
|
||||
"--bg-identifier-under-caret-write": "color-mix(in srgb, #dc322f 12%, #002b36)",
|
||||
"--fg-inlay": "#839496",
|
||||
"--bg-inlay": "#073642",
|
||||
"--wave-error": "#dc322f",
|
||||
"--wave-warning": "#b58900",
|
||||
"--editor-error-decoration": "underline wavy var(--wave-error)",
|
||||
"--editor-warning-decoration": "underline wavy var(--wave-warning)",
|
||||
"--editor-warning-background": "color-mix(in srgb, #b58900 12%, #002b36)",
|
||||
"--bg-breakpoint-line": "color-mix(in srgb, #dc322f 12%, #002b36)",
|
||||
"--fg-completion-match": "#268bd2",
|
||||
"--bg-editor-inline": "#073642",
|
||||
"--bg-editor-banner": "color-mix(in srgb, #268bd2 12%, #002b36)",
|
||||
"--fg-weak-warning": "#839496",
|
||||
"--fg-bulb": "#b58900",
|
||||
|
||||
// ANSI palette
|
||||
"--ansi-black": "#073642",
|
||||
"--ansi-red": "#dc322f",
|
||||
"--ansi-green": "#859900",
|
||||
"--ansi-yellow": "#b58900",
|
||||
"--ansi-blue": "#268bd2",
|
||||
"--ansi-magenta": "#d33682",
|
||||
"--ansi-cyan": "#2aa198",
|
||||
"--ansi-white": "#eee8d5",
|
||||
"--ansi-bright-black": "#002b36",
|
||||
"--ansi-bright-red": "#cb4b16",
|
||||
"--ansi-bright-green": "#586e75",
|
||||
"--ansi-bright-yellow": "#657b83",
|
||||
"--ansi-bright-blue": "#839496",
|
||||
"--ansi-bright-magenta": "#6c71c4",
|
||||
"--ansi-bright-cyan": "#93a1a1",
|
||||
"--ansi-bright-white": "#fdf6e3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
// Solarized palette by Ethan Schoonover (https://github.com/altercation/solarized),
|
||||
// MIT licence, see LICENSE. Adapted to OpenIDEA's tokens by bghtyu.
|
||||
// UI theme; the editor colour scheme is in solarized-dark.scheme.json.
|
||||
{
|
||||
"id": "solarized-dark",
|
||||
"name": "Solarized Dark",
|
||||
"kind": "dark",
|
||||
"tokens": {
|
||||
// Surfaces
|
||||
"--bg-app": "#002b36",
|
||||
"--bg-elev": "#073642",
|
||||
"--bg-popup": "#073642",
|
||||
"--bg-hover": "#073642",
|
||||
"--bg-selected": "#073642",
|
||||
"--bg-selected-strong": "#073642",
|
||||
"--bg-current-line": "#073642",
|
||||
"--bg-debug-line": "color-mix(in srgb, #268bd2 12%, #002b36)",
|
||||
"--bg-backdrop": "#073642",
|
||||
"--bg-input": "#002b36",
|
||||
|
||||
// Lines
|
||||
"--border": "#073642",
|
||||
"--border-popup": "#073642",
|
||||
"--border-strong": "#839496",
|
||||
"--border-window": "#073642",
|
||||
"--control-off": "#839496",
|
||||
|
||||
// Text
|
||||
"--fg-strong": "#839496",
|
||||
"--fg-emphasis": "#839496",
|
||||
"--fg": "#839496",
|
||||
"--fg-secondary": "#839496",
|
||||
"--fg-muted": "#839496",
|
||||
"--fg-subtle": "#839496",
|
||||
"--fg-faint": "#839496",
|
||||
"--fg-gutter": "#839496",
|
||||
"--fg-icon": "#839496",
|
||||
|
||||
// Accent
|
||||
"--accent": "#268bd2",
|
||||
"--link": "#268bd2",
|
||||
"--link-hover": "#2aa198",
|
||||
|
||||
// Charts
|
||||
"--chart-series": "#dc322f",
|
||||
|
||||
// Status
|
||||
"--danger": "#dc322f",
|
||||
"--warning": "#b58900",
|
||||
"--success": "#859900",
|
||||
"--db-connected": "#859900",
|
||||
"--db-connected-ring": "#859900",
|
||||
"--info": "#268bd2",
|
||||
|
||||
// Vcs
|
||||
"--vcs-modified": "#268bd2",
|
||||
"--vcs-added": "#859900",
|
||||
"--vcs-deleted": "#839496",
|
||||
"--vcs-untracked": "#dc322f",
|
||||
"--vcs-ignored": "#b58900",
|
||||
"--vcs-conflict": "#dc322f",
|
||||
"--gutter-modified": "#268bd2",
|
||||
"--gutter-added": "#859900",
|
||||
"--gutter-deleted": "#dc322f",
|
||||
|
||||
// Shadows and window controls
|
||||
"--shadow-popup": "0 4px 16px #00000024",
|
||||
"--shadow-dialog": "0 10px 36px #00000033",
|
||||
"--traffic-close": "#ff5f57",
|
||||
"--traffic-close-border": "#e0443e",
|
||||
"--traffic-close-pressed": "#bf4942",
|
||||
"--traffic-glyph": "#4d0000",
|
||||
"--traffic-off": "#073642",
|
||||
"--traffic-off-border": "#073642",
|
||||
|
||||
// Additive
|
||||
"--bg-tooltip": "#073642",
|
||||
"--fg-tooltip": "#839496",
|
||||
"--border-tooltip": "#073642",
|
||||
"--bg-control": "#002b36",
|
||||
"--bg-control-hover": "#073642",
|
||||
"--bg-active": "#073642",
|
||||
"--bg-scrim": "color-mix(in srgb, #002b36 68%, transparent)",
|
||||
"--fg-on-accent": "#002b36",
|
||||
"--shadow-tooltip": "0 4px 16px #00000024",
|
||||
"--shadow-balloon": "0 10px 36px #00000033",
|
||||
|
||||
// Islands contract
|
||||
"--bg-frame": "#073642",
|
||||
"--bg-island": "#002b36",
|
||||
"--bg-stripe-hover": "#073642",
|
||||
"--bg-stripe-selected": "#073642",
|
||||
"--fg-stripe-selected-active": "#002b36",
|
||||
"--fg-stripe-label": "#839496",
|
||||
"--bg-tab-selected": "#073642",
|
||||
"--border-tab-selected": "#268bd2",
|
||||
"--bg-tab-selected-inactive": "#073642",
|
||||
"--border-tab-selected-inactive": "#073642",
|
||||
"--bg-tab-hover": "#073642",
|
||||
"--border-editor-tabs": "#073642",
|
||||
"--bg-tree-selected": "#073642",
|
||||
"--bg-tree-selected-inactive": "#073642",
|
||||
"--bg-tree-hover": "#073642",
|
||||
"--bg-toolbar-hover": "#073642",
|
||||
"--bg-toolbar-pressed": "#073642",
|
||||
"--bg-toolbar-selected": "#073642",
|
||||
"--bg-toolbar-selected-hover": "#073642",
|
||||
"--bg-button-primary-hover": "color-mix(in srgb, var(--accent) 86%, #839496)",
|
||||
"--fg-button-primary": "#002b36",
|
||||
"--bg-button": "#002b36",
|
||||
"--border-button": "#073642",
|
||||
"--bg-field": "#002b36",
|
||||
"--bg-checkbox": "#002b36",
|
||||
"--border-checkbox": "#839496",
|
||||
"--bg-toggle-on": "#859900",
|
||||
"--fg-toggle-on": "#002b36",
|
||||
"--bg-toggle-off": "#073642",
|
||||
"--fg-toggle-off": "#839496",
|
||||
"--border-toggle": "#839496",
|
||||
"--bg-toggle-knob": "#839496",
|
||||
"--focus-ring-soft": "#268bd2",
|
||||
"--scrollbar-thumb": "color-mix(in srgb, #839496 24%, transparent)",
|
||||
"--scrollbar-thumb-hover": "color-mix(in srgb, #839496 42%, transparent)",
|
||||
"--bg-search-match": "color-mix(in srgb, #b58900 22%, #002b36)",
|
||||
"--bg-search-match-current": "color-mix(in srgb, #b58900 36%, #002b36)",
|
||||
"--fg-search-match": "#839496",
|
||||
"--bg-dialog": "#002b36",
|
||||
"--bg-popup-inline": "#073642",
|
||||
"--bg-popup-advertiser": "#073642",
|
||||
"--bg-feedback-info": "color-mix(in srgb, #268bd2 12%, #002b36)",
|
||||
"--border-feedback-info": "color-mix(in srgb, #268bd2 55%, #002b36)",
|
||||
"--bg-feedback-error": "color-mix(in srgb, #dc322f 12%, #002b36)",
|
||||
"--border-feedback-error": "color-mix(in srgb, #dc322f 55%, #002b36)",
|
||||
"--bg-feedback-warning": "color-mix(in srgb, #b58900 12%, #002b36)",
|
||||
"--border-feedback-warning": "color-mix(in srgb, #b58900 55%, #002b36)",
|
||||
"--bg-feedback-success": "color-mix(in srgb, #859900 12%, #002b36)",
|
||||
"--border-feedback-success": "color-mix(in srgb, #859900 55%, #002b36)",
|
||||
"--bg-counter": "#268bd2",
|
||||
"--fg-counter": "#002b36",
|
||||
"--bg-progress-track": "#073642"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// Solarized palette by Ethan Schoonover (https://github.com/altercation/solarized),
|
||||
// MIT licence, see LICENSE. Adapted to OpenIDEA's tokens by bghtyu.
|
||||
// Editor colour scheme: syntax, editor surfaces and terminal ANSI colours.
|
||||
{
|
||||
"name": "Solarized Light",
|
||||
"tokens": {
|
||||
// Syntax
|
||||
"--syn-keyword": "#859900",
|
||||
"--syn-string": "#2aa198",
|
||||
"--syn-type": "#b58900",
|
||||
"--syn-function": "#268bd2",
|
||||
"--syn-function-call": "#268bd2",
|
||||
"--syn-comment": "#93a1a1",
|
||||
"--syn-comment-style": "italic",
|
||||
"--syn-doc-comment": "#93a1a1",
|
||||
"--syn-docstring": "#2aa198",
|
||||
"--syn-annotation": "#cb4b16",
|
||||
"--syn-decorator": "#cb4b16",
|
||||
"--syn-number": "#2aa198",
|
||||
"--syn-punct": "#657b83",
|
||||
"--syn-variable": "#268bd2",
|
||||
"--syn-param": "#657b83",
|
||||
"--syn-property": "#268bd2",
|
||||
"--syn-constant": "#2aa198",
|
||||
"--syn-builtin": "#2aa198",
|
||||
"--syn-self": "#dc322f",
|
||||
"--syn-keyword-arg": "#dc322f",
|
||||
"--syn-predefined": "#2aa198",
|
||||
"--syn-escape": "#dc322f",
|
||||
"--syn-regexp": "#2aa198",
|
||||
"--syn-tag": "#268bd2",
|
||||
"--syn-attribute": "#cb4b16",
|
||||
"--syn-todo": "#d33682",
|
||||
"--syn-link": "#268bd2",
|
||||
"--syn-markup-heading": "#268bd2",
|
||||
"--syn-markup-code": "#2aa198",
|
||||
"--syn-invalid": "#dc322f",
|
||||
"--syn-inline-value": "#657b83",
|
||||
"--syn-rainbow-1": "#2aa198",
|
||||
"--syn-rainbow-2": "#cb4b16",
|
||||
"--syn-rainbow-3": "#859900",
|
||||
"--syn-rainbow-4": "#d33682",
|
||||
"--syn-rainbow-5": "#268bd2",
|
||||
"--syn-bracket-1": "#b58900",
|
||||
"--syn-bracket-2": "#d33682",
|
||||
"--syn-bracket-3": "#268bd2",
|
||||
|
||||
// Editor
|
||||
"--fg-editor": "#657b83",
|
||||
"--caret": "#268bd2",
|
||||
"--fg-line-number": "#657b83",
|
||||
"--fg-line-number-current": "#657b83",
|
||||
"--border-gutter": "#eee8d5",
|
||||
"--border-indent-guide": "#eee8d5",
|
||||
"--border-indent-guide-active": "#268bd2",
|
||||
"--border-right-margin": "#eee8d5",
|
||||
"--bg-editor-selection": "#eee8d5",
|
||||
"--bg-editor-selection-inactive": "#eee8d5",
|
||||
"--bg-text-search-result": "color-mix(in srgb, #b58900 22%, #fdf6e3)",
|
||||
"--bg-text-search-result-current": "color-mix(in srgb, #b58900 36%, #fdf6e3)",
|
||||
"--bg-matched-brace": "#eee8d5",
|
||||
"--matched-brace-weight": "var(--fw-code-bold)",
|
||||
"--bg-matched-tag": "#eee8d5",
|
||||
"--bg-fold-placeholder": "#eee8d5",
|
||||
"--fg-fold-placeholder": "#657b83",
|
||||
"--fg-fold-marker": "#657b83",
|
||||
"--fg-whitespace": "#657b83",
|
||||
"--fg-unused": "#657b83",
|
||||
"--bg-identifier-under-caret": "#eee8d5",
|
||||
"--bg-identifier-under-caret-write": "color-mix(in srgb, #dc322f 12%, #fdf6e3)",
|
||||
"--fg-inlay": "#657b83",
|
||||
"--bg-inlay": "#eee8d5",
|
||||
"--wave-error": "#dc322f",
|
||||
"--wave-warning": "#b58900",
|
||||
"--editor-error-decoration": "underline wavy var(--wave-error)",
|
||||
"--editor-warning-decoration": "underline wavy var(--wave-warning)",
|
||||
"--editor-warning-background": "color-mix(in srgb, #b58900 12%, #fdf6e3)",
|
||||
"--bg-breakpoint-line": "color-mix(in srgb, #dc322f 12%, #fdf6e3)",
|
||||
"--fg-completion-match": "#268bd2",
|
||||
"--bg-editor-inline": "#eee8d5",
|
||||
"--bg-editor-banner": "color-mix(in srgb, #268bd2 12%, #fdf6e3)",
|
||||
"--fg-weak-warning": "#657b83",
|
||||
"--fg-bulb": "#b58900",
|
||||
|
||||
// ANSI palette
|
||||
"--ansi-black": "#073642",
|
||||
"--ansi-red": "#dc322f",
|
||||
"--ansi-green": "#859900",
|
||||
"--ansi-yellow": "#b58900",
|
||||
"--ansi-blue": "#268bd2",
|
||||
"--ansi-magenta": "#d33682",
|
||||
"--ansi-cyan": "#2aa198",
|
||||
"--ansi-white": "#eee8d5",
|
||||
"--ansi-bright-black": "#002b36",
|
||||
"--ansi-bright-red": "#cb4b16",
|
||||
"--ansi-bright-green": "#586e75",
|
||||
"--ansi-bright-yellow": "#657b83",
|
||||
"--ansi-bright-blue": "#839496",
|
||||
"--ansi-bright-magenta": "#6c71c4",
|
||||
"--ansi-bright-cyan": "#93a1a1",
|
||||
"--ansi-bright-white": "#fdf6e3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
// Solarized palette by Ethan Schoonover (https://github.com/altercation/solarized),
|
||||
// MIT licence, see LICENSE. Adapted to OpenIDEA's tokens by bghtyu.
|
||||
// UI theme; the editor colour scheme is in solarized-light.scheme.json.
|
||||
{
|
||||
"id": "solarized-light",
|
||||
"name": "Solarized Light",
|
||||
"kind": "light",
|
||||
"tokens": {
|
||||
// Surfaces
|
||||
"--bg-app": "#fdf6e3",
|
||||
"--bg-elev": "#eee8d5",
|
||||
"--bg-popup": "#fdf6e3",
|
||||
"--bg-hover": "#eee8d5",
|
||||
"--bg-selected": "#eee8d5",
|
||||
"--bg-selected-strong": "#eee8d5",
|
||||
"--bg-current-line": "#eee8d5",
|
||||
"--bg-debug-line": "color-mix(in srgb, #268bd2 12%, #fdf6e3)",
|
||||
"--bg-backdrop": "#eee8d5",
|
||||
"--bg-input": "#fdf6e3",
|
||||
|
||||
// Lines
|
||||
"--border": "#eee8d5",
|
||||
"--border-popup": "#eee8d5",
|
||||
"--border-strong": "#657b83",
|
||||
"--border-window": "#eee8d5",
|
||||
"--control-off": "#657b83",
|
||||
|
||||
// Text
|
||||
"--fg-strong": "#657b83",
|
||||
"--fg-emphasis": "#657b83",
|
||||
"--fg": "#657b83",
|
||||
"--fg-secondary": "#657b83",
|
||||
"--fg-muted": "#657b83",
|
||||
"--fg-subtle": "#657b83",
|
||||
"--fg-faint": "#657b83",
|
||||
"--fg-gutter": "#657b83",
|
||||
"--fg-icon": "#657b83",
|
||||
|
||||
// Accent
|
||||
"--accent": "#268bd2",
|
||||
"--link": "#268bd2",
|
||||
"--link-hover": "#2aa198",
|
||||
|
||||
// Charts
|
||||
"--chart-series": "#dc322f",
|
||||
|
||||
// Status
|
||||
"--danger": "#dc322f",
|
||||
"--warning": "#b58900",
|
||||
"--success": "#859900",
|
||||
"--db-connected": "#859900",
|
||||
"--db-connected-ring": "#859900",
|
||||
"--info": "#268bd2",
|
||||
|
||||
// Vcs
|
||||
"--vcs-modified": "#268bd2",
|
||||
"--vcs-added": "#859900",
|
||||
"--vcs-deleted": "#657b83",
|
||||
"--vcs-untracked": "#dc322f",
|
||||
"--vcs-ignored": "#b58900",
|
||||
"--vcs-conflict": "#dc322f",
|
||||
"--gutter-modified": "#268bd2",
|
||||
"--gutter-added": "#859900",
|
||||
"--gutter-deleted": "#dc322f",
|
||||
|
||||
// Shadows and window controls
|
||||
"--shadow-popup": "0 4px 16px #00000024",
|
||||
"--shadow-dialog": "0 10px 36px #00000033",
|
||||
"--traffic-close": "#ff5f57",
|
||||
"--traffic-close-border": "#e0443e",
|
||||
"--traffic-close-pressed": "#bf4942",
|
||||
"--traffic-glyph": "#4d0000",
|
||||
"--traffic-off": "#eee8d5",
|
||||
"--traffic-off-border": "#eee8d5",
|
||||
|
||||
// Additive
|
||||
"--bg-tooltip": "#fdf6e3",
|
||||
"--fg-tooltip": "#657b83",
|
||||
"--border-tooltip": "#eee8d5",
|
||||
"--bg-control": "#fdf6e3",
|
||||
"--bg-control-hover": "#eee8d5",
|
||||
"--bg-active": "#eee8d5",
|
||||
"--bg-scrim": "color-mix(in srgb, #fdf6e3 68%, transparent)",
|
||||
"--fg-on-accent": "#002b36",
|
||||
"--shadow-tooltip": "0 4px 16px #00000024",
|
||||
"--shadow-balloon": "0 10px 36px #00000033",
|
||||
|
||||
// Islands contract
|
||||
"--bg-frame": "#eee8d5",
|
||||
"--bg-island": "#fdf6e3",
|
||||
"--bg-stripe-hover": "#eee8d5",
|
||||
"--bg-stripe-selected": "#eee8d5",
|
||||
"--fg-stripe-selected-active": "#002b36",
|
||||
"--fg-stripe-label": "#657b83",
|
||||
"--bg-tab-selected": "#eee8d5",
|
||||
"--border-tab-selected": "#268bd2",
|
||||
"--bg-tab-selected-inactive": "#eee8d5",
|
||||
"--border-tab-selected-inactive": "#eee8d5",
|
||||
"--bg-tab-hover": "#eee8d5",
|
||||
"--border-editor-tabs": "#eee8d5",
|
||||
"--bg-tree-selected": "#eee8d5",
|
||||
"--bg-tree-selected-inactive": "#eee8d5",
|
||||
"--bg-tree-hover": "#eee8d5",
|
||||
"--bg-toolbar-hover": "#eee8d5",
|
||||
"--bg-toolbar-pressed": "#eee8d5",
|
||||
"--bg-toolbar-selected": "#eee8d5",
|
||||
"--bg-toolbar-selected-hover": "#eee8d5",
|
||||
"--bg-button-primary-hover": "color-mix(in srgb, var(--accent) 86%, #657b83)",
|
||||
"--fg-button-primary": "#002b36",
|
||||
"--bg-button": "#fdf6e3",
|
||||
"--border-button": "#eee8d5",
|
||||
"--bg-field": "#fdf6e3",
|
||||
"--bg-checkbox": "#fdf6e3",
|
||||
"--border-checkbox": "#657b83",
|
||||
"--bg-toggle-on": "#859900",
|
||||
"--fg-toggle-on": "#002b36",
|
||||
"--bg-toggle-off": "#eee8d5",
|
||||
"--fg-toggle-off": "#657b83",
|
||||
"--border-toggle": "#657b83",
|
||||
"--bg-toggle-knob": "#657b83",
|
||||
"--focus-ring-soft": "#268bd2",
|
||||
"--scrollbar-thumb": "color-mix(in srgb, #657b83 24%, transparent)",
|
||||
"--scrollbar-thumb-hover": "color-mix(in srgb, #657b83 42%, transparent)",
|
||||
"--bg-search-match": "color-mix(in srgb, #b58900 22%, #fdf6e3)",
|
||||
"--bg-search-match-current": "color-mix(in srgb, #b58900 36%, #fdf6e3)",
|
||||
"--fg-search-match": "#657b83",
|
||||
"--bg-dialog": "#fdf6e3",
|
||||
"--bg-popup-inline": "#eee8d5",
|
||||
"--bg-popup-advertiser": "#eee8d5",
|
||||
"--bg-feedback-info": "color-mix(in srgb, #268bd2 12%, #fdf6e3)",
|
||||
"--border-feedback-info": "color-mix(in srgb, #268bd2 55%, #fdf6e3)",
|
||||
"--bg-feedback-error": "color-mix(in srgb, #dc322f 12%, #fdf6e3)",
|
||||
"--border-feedback-error": "color-mix(in srgb, #dc322f 55%, #fdf6e3)",
|
||||
"--bg-feedback-warning": "color-mix(in srgb, #b58900 12%, #fdf6e3)",
|
||||
"--border-feedback-warning": "color-mix(in srgb, #b58900 55%, #fdf6e3)",
|
||||
"--bg-feedback-success": "color-mix(in srgb, #859900 12%, #fdf6e3)",
|
||||
"--border-feedback-success": "color-mix(in srgb, #859900 55%, #fdf6e3)",
|
||||
"--bg-counter": "#268bd2",
|
||||
"--fg-counter": "#002b36",
|
||||
"--bg-progress-track": "#eee8d5"
|
||||
}
|
||||
}
|
||||
+25
-2
@@ -3,9 +3,32 @@
|
||||
// so a new window starts in it instead of flashing the light default.
|
||||
// An external file because the CSP forbids inline scripts.
|
||||
try {
|
||||
const root = document.documentElement;
|
||||
const addStyle = (id, css) => {
|
||||
const style = document.createElement("style");
|
||||
style.id = id;
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
};
|
||||
const last = JSON.parse(localStorage.getItem("openidea.theme") ?? "null");
|
||||
if (last?.theme) document.documentElement.dataset.theme = last.theme;
|
||||
if (last?.contrast) document.documentElement.dataset.contrast = last.contrast;
|
||||
if (last?.theme) root.dataset.theme = last.theme;
|
||||
if (last?.contrast) root.dataset.contrast = last.contrast;
|
||||
// A plugin theme's stylesheet (plugins/bridge.ts), so the workbench doesn't
|
||||
// show the built-in theme until the plugin host answers.
|
||||
const plugin = JSON.parse(localStorage.getItem("openidea.pluginTheme") ?? "null");
|
||||
if (typeof plugin?.id === "string" && typeof plugin.css === "string" && !/[<\\]|url\(/i.test(plugin.css)) {
|
||||
addStyle("openidea-plugin-theme", plugin.css);
|
||||
root.dataset.pluginTheme = plugin.id;
|
||||
root.dataset.pluginThemeKind = root.dataset.theme = plugin.kind === "light" ? "light" : "dark";
|
||||
delete root.dataset.contrast;
|
||||
}
|
||||
// The splash's frame, text and accent colours, resolved by theme.ts (splash.css).
|
||||
const colors = [
|
||||
["--boot-frame", last?.frame],
|
||||
["--boot-fg", last?.fg],
|
||||
["--boot-accent", last?.accent],
|
||||
].filter(([, v]) => typeof v === "string" && /^#[0-9a-f]{3,8}$/i.test(v));
|
||||
if (colors.length) addStyle("openidea-boot-colors", `:root { ${colors.map(([k, v]) => `${k}: ${v};`).join(" ")} }`);
|
||||
} catch {
|
||||
/* storage unavailable */
|
||||
}
|
||||
|
||||
+10
-7
@@ -1,11 +1,14 @@
|
||||
/* Linked, not inlined in index.html: release builds hash inline <style> blocks into the CSP,
|
||||
which voids style-src 'unsafe-inline' and blocks the editor's runtime-injected styles. */
|
||||
/* Start-up splash over the page until the window's launch project is open (platform/splash.ts).
|
||||
Tokens aren't loaded yet, so the colours mirror --bg-frame / --fg-muted / --accent. */
|
||||
Until tokens load, the colours are the last theme's --bg-frame / --fg-muted / --accent that
|
||||
boot.js restored as --boot-*, else these built-in ones. */
|
||||
/* Not on html: the canvas then takes body's (themed) background, which a fast resize shows before relayout. */
|
||||
body, .splash { background: #e9eaee; }
|
||||
:root[data-theme="dark"] :is(body, .splash) { background: #26282c; }
|
||||
:root:is([data-theme="dark-hc"], [data-theme="dark"][data-contrast="high"]) :is(body, .splash) { background: #1a1b1e; }
|
||||
body, .splash { background: var(--bg-frame, var(--boot-frame, #e9eaee)); }
|
||||
:root[data-theme="dark"] :is(body, .splash) { background: var(--bg-frame, var(--boot-frame, #26282c)); }
|
||||
:root:is([data-theme="dark-hc"], [data-theme="dark"][data-contrast="high"]) :is(body, .splash) {
|
||||
background: var(--bg-frame, var(--boot-frame, #1a1b1e));
|
||||
}
|
||||
.splash {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
@@ -17,7 +20,7 @@ body, .splash { background: #e9eaee; }
|
||||
gap: 14px;
|
||||
padding-bottom: 38px;
|
||||
box-sizing: border-box;
|
||||
color: #6c707e;
|
||||
color: var(--fg-muted, var(--boot-fg, #6c707e));
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
@@ -25,7 +28,7 @@ body, .splash { background: #e9eaee; }
|
||||
transition: opacity 160ms ease-out;
|
||||
}
|
||||
.splash.splash-out { opacity: 0; pointer-events: none; }
|
||||
:root[data-theme^="dark"] .splash { color: #8c8f95; }
|
||||
:root[data-theme^="dark"] .splash { color: var(--fg-muted, var(--boot-fg, #8c8f95)); }
|
||||
.splash-logo { width: 56px; height: 56px; }
|
||||
/* 15px Inter: 2048 units per em, the line box from ascender to descender. */
|
||||
.splash-wordmark { width: 73.2px; height: 18.15px; }
|
||||
@@ -34,7 +37,7 @@ body, .splash { background: #e9eaee; }
|
||||
.splash-bar > i {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background: #3871e1;
|
||||
background: var(--accent, var(--boot-accent, #3871e1));
|
||||
transform-origin: left;
|
||||
animation: splash-fill 4s cubic-bezier(0.1, 0.7, 0.3, 1) both;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
convertIcls,
|
||||
@@ -77,6 +77,73 @@ describe("theme plugin template", () => {
|
||||
});
|
||||
});
|
||||
|
||||
/** Theme packages in plugins/: every theme they contribute, with its editor scheme. */
|
||||
function pluginThemes() {
|
||||
return readdirSync("plugins", { withFileTypes: true })
|
||||
.filter((d) => d.isDirectory() && existsSync(`plugins/${d.name}/plugin.toml`))
|
||||
.flatMap((d) => {
|
||||
const dir = `plugins/${d.name}`;
|
||||
const manifest = read(`${dir}/plugin.toml`);
|
||||
return manifest
|
||||
.split("[[contributes.themes]]")
|
||||
.slice(1)
|
||||
.map((entry) => ({
|
||||
dir,
|
||||
manifest,
|
||||
path: /^path\s*=\s*"([^"]+)"/m.exec(entry)?.[1],
|
||||
scheme: /^editor-scheme\s*=\s*"([^"]+)"/m.exec(entry)?.[1],
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
describe("theme plugins in plugins/", () => {
|
||||
const themes = pluginThemes();
|
||||
|
||||
it("include Solarized and One, light and dark", () => {
|
||||
expect(themes.map((t) => `${t.dir}/${t.path}`)).toEqual(
|
||||
expect.arrayContaining([
|
||||
"plugins/theme-solarized/themes/solarized-light.theme.json",
|
||||
"plugins/theme-solarized/themes/solarized-dark.theme.json",
|
||||
"plugins/theme-one/themes/one-light.theme.json",
|
||||
"plugins/theme-one/themes/one-dark.theme.json",
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it.each(themes.map((t) => [`${t.dir}/${t.path}`, t] as const))(
|
||||
"%s sets every UI token and its scheme every editor token",
|
||||
(_name, t) => {
|
||||
// A failure here means tokens.css gained (or lost) a themeable token: add it to the theme too.
|
||||
expect(t.path && t.scheme, "theme and editor-scheme paths").toBeTruthy();
|
||||
expect(t.manifest).not.toMatch(/^wasm\s*=/m);
|
||||
const ui = JSON.parse(stripJsonComments(read(`${t.dir}/${t.path}`))) as {
|
||||
id: string;
|
||||
name: string;
|
||||
kind: string;
|
||||
tokens: Record<string, string>;
|
||||
};
|
||||
const ed = JSON.parse(stripJsonComments(read(`${t.dir}/${t.scheme}`))) as { tokens: Record<string, string> };
|
||||
expect(ui.id).toMatch(/^[a-z0-9][a-z0-9._-]*$/);
|
||||
expect(ui.name.trim()).not.toBe("");
|
||||
expect(["light", "dark"]).toContain(ui.kind);
|
||||
expect(sorted(Object.keys(ui.tokens))).toEqual(sorted(sets.ui));
|
||||
expect(sorted(Object.keys(ed.tokens))).toEqual(sorted(sets.scheme));
|
||||
for (const [k, v] of Object.entries({ ...ui.tokens, ...ed.tokens })) {
|
||||
expect(/[;{}<>\\\n]|url\(|image\(/i.test(v), `${k}: ${v}`).toBe(false);
|
||||
// var() may only point at tokens the IDE defines.
|
||||
for (const [, ref] of v.matchAll(/var\((--[a-z0-9-]+)/g)) expect(css, `${k} → ${ref}`).toContain(`${ref}:`);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("ship the licence of the palettes they port", () => {
|
||||
for (const dir of new Set(themes.map((t) => t.dir))) {
|
||||
if (dir === "plugins/theme-solarized" || dir === "plugins/theme-one")
|
||||
expect(read(`${dir}/LICENSE`), dir).toContain("MIT License");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("IntelliJ theme import", () => {
|
||||
const themeJson = `{
|
||||
// comment
|
||||
|
||||
@@ -332,6 +332,7 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
|
||||
plugin::plugin_run_command,
|
||||
plugin::plugin_set_active_file,
|
||||
plugin::plugin_set_theme,
|
||||
plugin::plugin_set_theme_frame,
|
||||
perf::app_metrics,
|
||||
perf::clear_caches,
|
||||
analysis::inspect_code,
|
||||
|
||||
@@ -11,9 +11,13 @@ use parking_lot::Mutex;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use std::time::Duration;
|
||||
use tauri::ipc::Channel;
|
||||
use tauri::{AppHandle, Manager, State, Window};
|
||||
|
||||
/// How long `plugin_list` waits for the startup load before answering with what it has.
|
||||
const INITIAL_LOAD_WAIT: Duration = Duration::from_secs(5);
|
||||
|
||||
/// Window label → its event channel.
|
||||
static SUBSCRIBERS: LazyLock<Mutex<HashMap<String, Channel<PluginEvent>>>> = LazyLock::new(Default::default);
|
||||
|
||||
@@ -62,6 +66,8 @@ pub async fn plugin_list(window: Window, state: State<'_, AppState>) -> IdeResul
|
||||
let host = state.plugins.clone();
|
||||
let root = state.project(window.label()).ok().map(|p| p.root.clone());
|
||||
blocking(move || {
|
||||
// A window opened at startup would otherwise see no plugins and drop the selected theme.
|
||||
host.wait_initial_load(INITIAL_LOAD_WAIT);
|
||||
if let Some(root) = &root {
|
||||
host.sync_project(root);
|
||||
}
|
||||
@@ -156,3 +162,11 @@ pub fn plugin_set_theme(
|
||||
_ => state.plugins.set_theme(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// The UI reports the selected plugin theme's resolved frame colour (`#rrggbb[aa]`),
|
||||
/// which new windows open in before their page loads.
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn plugin_set_theme_frame(state: State<'_, AppState>, color: Option<String>) -> IdeResult<()> {
|
||||
state.plugins.set_theme_frame(color.as_deref())
|
||||
}
|
||||
|
||||
@@ -230,11 +230,21 @@ pub fn frame_color(appearance: &AppearanceSettings, os_dark: bool) -> Color {
|
||||
}
|
||||
}
|
||||
|
||||
/// A `#rgb` / `#rrggbb` / `#rrggbbaa` colour sent by the UI (Tauri's own parser).
|
||||
fn hex_color(hex: &str) -> Option<Color> {
|
||||
serde_json::from_value(serde_json::Value::String(hex.to_owned())).ok()
|
||||
}
|
||||
|
||||
/// Builds a project window painted in the saved theme's frame colour, so it
|
||||
/// doesn't flash the light default before its page (and splash) loads.
|
||||
fn build_project_window(app: &AppHandle, mut config: WindowConfig) -> IdeResult<WebviewWindow> {
|
||||
let appearance = app.try_state::<AppState>().map(|s| s.global_settings.effective().appearance);
|
||||
if let Some(a) = &appearance {
|
||||
let state = app.try_state::<AppState>();
|
||||
let appearance = state.as_ref().map(|s| s.global_settings.effective().appearance);
|
||||
// A plugin theme replaces the built-in one in either OS mode; the UI records its resolved frame.
|
||||
let plugin_frame = state.as_ref().and_then(|s| s.plugins.startup_frame()).and_then(|c| hex_color(&c));
|
||||
if let Some(color) = plugin_frame {
|
||||
config.background_color = Some(color);
|
||||
} else if let Some(a) = &appearance {
|
||||
config.background_color = Some(frame_color(a, false));
|
||||
}
|
||||
let window = WebviewWindowBuilder::from_config(app, &config)
|
||||
@@ -242,7 +252,7 @@ fn build_project_window(app: &AppHandle, mut config: WindowConfig) -> IdeResult<
|
||||
.map_err(|e| IdeError::internal(format!("cannot create window: {e}")))?;
|
||||
apply_zoom(app, &window);
|
||||
// "Sync with OS" needs the OS mode, known once the window exists.
|
||||
if let Some(a) = appearance.filter(|a| a.sync_with_os || a.theme == "system")
|
||||
if let Some(a) = appearance.filter(|a| plugin_frame.is_none() && (a.sync_with_os || a.theme == "system"))
|
||||
&& window.theme().is_ok_and(|t| t == Theme::Dark)
|
||||
{
|
||||
let _ = window.set_background_color(Some(frame_color(&a, true)));
|
||||
@@ -418,7 +428,7 @@ pub fn open_settings_window(
|
||||
config.fullscreen = false;
|
||||
config.drag_drop_enabled = false;
|
||||
// macOS shows a child window as soon as it is attached, before the page loads: paint the theme's frame colour.
|
||||
if let Some(color) = background.and_then(|c| serde_json::from_value(serde_json::Value::String(c)).ok()) {
|
||||
if let Some(color) = background.as_deref().and_then(hex_color) {
|
||||
config.background_color = Some(color);
|
||||
}
|
||||
windows.set_launch(&label, Launch { project: None, settings_page: page, new_projects });
|
||||
@@ -465,6 +475,19 @@ mod tests {
|
||||
assert_eq!(frame_color(&a, true), Color(0x26, 0x28, 0x2c, 0xff));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_colours_from_the_ui_parse_as_hex() {
|
||||
for (hex, expected) in [
|
||||
("#eaeaeb", Color(0xea, 0xea, 0xeb, 0xff)),
|
||||
("#21252b", Color(0x21, 0x25, 0x2b, 0xff)),
|
||||
("#8040bfcc", Color(0x80, 0x40, 0xbf, 0xcc)),
|
||||
("#fff", Color(0xff, 0xff, 0xff, 0xff)),
|
||||
] {
|
||||
assert_eq!(hex_color(hex), Some(expected), "{hex}");
|
||||
}
|
||||
assert_eq!(hex_color("hsl(220, 13%, 15%)"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn labels_sort_main_first_then_numerically() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "OpenIDEA",
|
||||
"version": "0.1.16",
|
||||
"version": "0.1.17",
|
||||
"identifier": "ai.l0ng.openidea",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
|
||||
@@ -698,6 +698,11 @@ export const commands = {
|
||||
pluginId: string,
|
||||
theme: ThemeDef,
|
||||
} | null, IdeError>(__TAURI_INVOKE("plugin_set_theme", { pluginId, themeId })),
|
||||
/**
|
||||
* The UI reports the selected plugin theme's resolved frame colour (`#rrggbb[aa]`),
|
||||
* which new windows open in before their page loads.
|
||||
*/
|
||||
pluginSetThemeFrame: (color: string | null) => typedError<null, IdeError>(__TAURI_INVOKE("plugin_set_theme_frame", { color })),
|
||||
/**
|
||||
* Snapshot of process memory and subsystem sizes, summed over all projects.
|
||||
* Async: the process walk runs on the blocking pool, not the main thread.
|
||||
|
||||
@@ -26,7 +26,10 @@ export function installColorScheme(): () => void {
|
||||
if (s.settings !== prev.settings) apply();
|
||||
});
|
||||
const observer = typeof MutationObserver !== "undefined" ? new MutationObserver(apply) : null;
|
||||
observer?.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme", "data-contrast"] });
|
||||
observer?.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["data-theme", "data-contrast", "data-plugin-theme"],
|
||||
});
|
||||
return () => {
|
||||
unsubscribe();
|
||||
observer?.disconnect();
|
||||
|
||||
@@ -144,7 +144,7 @@ describe("store", () => {
|
||||
|
||||
it("selects and clears a plugin theme", async () => {
|
||||
await refreshPlugins();
|
||||
await selectPluginTheme("theme-solarized-dark", "solarized-dark");
|
||||
await selectPluginTheme("theme-solarized", "solarized-dark");
|
||||
expect(usePlugins.getState().activeTheme?.theme.id).toBe("solarized-dark");
|
||||
await selectPluginTheme(null, null);
|
||||
expect(usePlugins.getState().activeTheme).toBeNull();
|
||||
@@ -238,7 +238,7 @@ describe("PluginsPage", () => {
|
||||
await waitFor(() => expect(screen.getByRole("checkbox", { name: "Enable Hello" })).toBeInTheDocument());
|
||||
expect(screen.getByRole("button", { name: "Run Hello: Insert Date" })).toBeDisabled();
|
||||
|
||||
await user.click(within(list).getByText("Solarized Dark Theme"));
|
||||
await user.click(within(list).getByText("Solarized Themes"));
|
||||
await user.click(screen.getByRole("button", { name: "Apply theme Solarized Dark" }));
|
||||
await waitFor(() => expect(screen.getByRole("button", { name: "Use Default Theme" })).toBeInTheDocument());
|
||||
|
||||
@@ -309,7 +309,7 @@ describe("catalog helpers", () => {
|
||||
expect(catalogState(entry, [plugin({ id: "hello", version: "0.0.9" })])).toBe("update");
|
||||
expect(catalogState(entry, [plugin({ id: "hello", version: "0.1.0" })])).toBe("installed");
|
||||
expect(catalogState(entry, [plugin({ id: "hello", source: "dev" })])).toBe("dev");
|
||||
expect(filterCatalog(sampleCatalog(), "solarized").map((c) => c.id)).toEqual(["theme-solarized-dark"]);
|
||||
expect(filterCatalog(sampleCatalog(), "solarized").map((c) => c.id)).toEqual(["theme-solarized"]);
|
||||
});
|
||||
|
||||
it("disables and re-enables all downloaded plugins, leaving dev plugins alone", async () => {
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ActiveTheme } from "@/bindings";
|
||||
import { STARTUP_THEME_KEY, syncWindowBackground } from "@/workbench/theme";
|
||||
import { createMemoryPluginsBackend, setPluginsBackend, type PluginsBackend } from "../backend";
|
||||
import {
|
||||
applyPluginTheme,
|
||||
PLUGIN_THEME_CACHE_KEY,
|
||||
PLUGIN_THEME_STYLE_ID,
|
||||
trackPluginTheme,
|
||||
type CachedPluginTheme,
|
||||
} from "../bridge";
|
||||
import { usePlugins } from "../store";
|
||||
|
||||
const BOOT_JS = readFileSync("public/boot.js", "utf8");
|
||||
/** Runs public/boot.js as the next window would, before any app code. */
|
||||
const boot = () => new Function(BOOT_JS)();
|
||||
|
||||
const ONE_DARK: ActiveTheme = {
|
||||
pluginId: "theme-one",
|
||||
theme: {
|
||||
id: "one-dark",
|
||||
name: "One Dark",
|
||||
kind: "dark",
|
||||
tokens: {
|
||||
"--bg-frame": "hsl(220, 13%, 15%)",
|
||||
"--fg-muted": "hsl(220, 9%, 55%)",
|
||||
"--accent": "hsl(220, 100%, 66%)",
|
||||
},
|
||||
},
|
||||
};
|
||||
const SOLARIZED_LIGHT: ActiveTheme = {
|
||||
pluginId: "theme-solarized",
|
||||
theme: { id: "solarized-light", name: "Solarized Light", kind: "light", tokens: { "--bg-frame": "#eee8d5" } },
|
||||
};
|
||||
|
||||
/** Computed style as a browser would resolve the active plugin theme. */
|
||||
function computedColors(values: Record<string, string>) {
|
||||
return vi.spyOn(globalThis, "getComputedStyle").mockImplementation(
|
||||
() =>
|
||||
({
|
||||
getPropertyValue: (name: string) => values[name] ?? "",
|
||||
color: "",
|
||||
}) as unknown as CSSStyleDeclaration,
|
||||
);
|
||||
}
|
||||
|
||||
function resetRoot() {
|
||||
const root = document.documentElement;
|
||||
for (const key of Object.keys(root.dataset)) delete root.dataset[key];
|
||||
document.getElementById(PLUGIN_THEME_STYLE_ID)?.remove();
|
||||
document.getElementById("openidea-boot-colors")?.remove();
|
||||
}
|
||||
|
||||
let backend: PluginsBackend;
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
resetRoot();
|
||||
backend = createMemoryPluginsBackend();
|
||||
setPluginsBackend(backend);
|
||||
usePlugins.setState({ plugins: [], activeTheme: null, loaded: false, error: null });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
applyPluginTheme(null);
|
||||
setPluginsBackend(null);
|
||||
localStorage.clear();
|
||||
resetRoot();
|
||||
});
|
||||
|
||||
describe("plugin theme at start-up", () => {
|
||||
it("restores the last plugin theme and splash colours before the app loads", () => {
|
||||
const style = computedColors({ "--bg-frame": "#21252b", "--fg-muted": "#828997", "--accent": "#5c8aff" });
|
||||
const stop = trackPluginTheme();
|
||||
usePlugins.setState({ loaded: true, activeTheme: ONE_DARK });
|
||||
syncWindowBackground();
|
||||
stop();
|
||||
style.mockRestore();
|
||||
resetRoot();
|
||||
|
||||
boot();
|
||||
const root = document.documentElement;
|
||||
expect(root.dataset.pluginTheme).toBe("theme-one/one-dark");
|
||||
expect(root.dataset.pluginThemeKind).toBe("dark");
|
||||
expect(root.dataset.theme).toBe("dark");
|
||||
expect(document.getElementById(PLUGIN_THEME_STYLE_ID)?.textContent).toContain("--bg-frame: hsl(220, 13%, 15%);");
|
||||
expect(document.getElementById("openidea-boot-colors")?.textContent).toBe(
|
||||
":root { --boot-frame: #21252b; --boot-fg: #828997; --boot-accent: #5c8aff; }",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the restored theme until the plugin list arrives, then follows it", () => {
|
||||
const cached: CachedPluginTheme = {
|
||||
id: "theme-one/one-dark",
|
||||
kind: "dark",
|
||||
css: ':root[data-plugin-theme]:not([data-plugin-theme=""]) {\n --bg-frame: #21252b;\n}\n',
|
||||
};
|
||||
localStorage.setItem(PLUGIN_THEME_CACHE_KEY, JSON.stringify(cached));
|
||||
boot();
|
||||
const root = document.documentElement;
|
||||
const stop = trackPluginTheme();
|
||||
// Nothing listed yet: the boot theme stays.
|
||||
expect(root.dataset.pluginTheme).toBe("theme-one/one-dark");
|
||||
expect(document.getElementById(PLUGIN_THEME_STYLE_ID)).not.toBeNull();
|
||||
// A failed listing doesn't drop it either.
|
||||
usePlugins.setState({ loaded: true, error: "backend unavailable" });
|
||||
expect(root.dataset.pluginTheme).toBe("theme-one/one-dark");
|
||||
|
||||
usePlugins.setState({ error: null, activeTheme: SOLARIZED_LIGHT });
|
||||
expect(root.dataset.pluginTheme).toBe("theme-solarized/solarized-light");
|
||||
expect(root.dataset.theme).toBe("light");
|
||||
expect(JSON.parse(localStorage.getItem(PLUGIN_THEME_CACHE_KEY)!)).toMatchObject({
|
||||
id: "theme-solarized/solarized-light",
|
||||
kind: "light",
|
||||
});
|
||||
|
||||
// The plugin was disabled or uninstalled meanwhile.
|
||||
usePlugins.setState({ activeTheme: null });
|
||||
expect(root.dataset.pluginTheme).toBeUndefined();
|
||||
expect(document.getElementById(PLUGIN_THEME_STYLE_ID)).toBeNull();
|
||||
expect(localStorage.getItem(PLUGIN_THEME_CACHE_KEY)).toBeNull();
|
||||
stop();
|
||||
});
|
||||
|
||||
it("ignores a cached stylesheet that could load resources", () => {
|
||||
localStorage.setItem(
|
||||
PLUGIN_THEME_CACHE_KEY,
|
||||
JSON.stringify({ id: "p/t", kind: "dark", css: ":root { --bg-app: url(https://example.com/x) }" }),
|
||||
);
|
||||
localStorage.setItem(STARTUP_THEME_KEY, JSON.stringify({ theme: "light", frame: "red; color: blue" }));
|
||||
boot();
|
||||
expect(document.documentElement.dataset.pluginTheme).toBeUndefined();
|
||||
expect(document.documentElement.dataset.theme).toBe("light");
|
||||
expect(document.getElementById(PLUGIN_THEME_STYLE_ID)).toBeNull();
|
||||
expect(document.getElementById("openidea-boot-colors")).toBeNull();
|
||||
});
|
||||
|
||||
it("reports the resolved frame colour once per selected theme", () => {
|
||||
const setFrame = vi.spyOn(backend, "setThemeFrame");
|
||||
computedColors({ "--bg-frame": "rgb(33, 37, 43)" });
|
||||
const stop = trackPluginTheme();
|
||||
usePlugins.setState({ loaded: true, activeTheme: ONE_DARK });
|
||||
usePlugins.setState({ activeTheme: { ...ONE_DARK } });
|
||||
expect(setFrame.mock.calls).toEqual([["#21252b"]]);
|
||||
// Selecting it again after the default theme reports it again: the backend forgot it.
|
||||
usePlugins.setState({ activeTheme: null });
|
||||
usePlugins.setState({ activeTheme: ONE_DARK });
|
||||
expect(setFrame).toHaveBeenCalledTimes(2);
|
||||
stop();
|
||||
});
|
||||
});
|
||||
@@ -18,6 +18,8 @@ export interface PluginsBackend {
|
||||
runCommand(pluginId: string, commandId: string, arg: string | null): Promise<string | null>;
|
||||
setActiveFile(path: string | null): Promise<void>;
|
||||
setTheme(pluginId: string | null, themeId: string | null): Promise<ActiveTheme | null>;
|
||||
/** The selected theme's resolved frame colour (`#rrggbb[aa]`), which new windows open in. */
|
||||
setThemeFrame(color: string | null): Promise<void>;
|
||||
/** Folder picker; `null` when cancelled. */
|
||||
pickFolder(): Promise<string | null>;
|
||||
/** Subscribes to backend plugin events (this window only). */
|
||||
@@ -40,6 +42,9 @@ export const tauriPluginsBackend: PluginsBackend = {
|
||||
await commands.pluginSetActiveFile(path);
|
||||
},
|
||||
setTheme: (pluginId, themeId) => call(commands.pluginSetTheme(pluginId, themeId)),
|
||||
setThemeFrame: async (color) => {
|
||||
await call(commands.pluginSetThemeFrame(color));
|
||||
},
|
||||
pickFolder: async () => {
|
||||
const { open } = await import("@tauri-apps/plugin-dialog");
|
||||
const picked = await open({ directory: true, multiple: false, title: "Install Plugin from Disk" });
|
||||
@@ -71,21 +76,38 @@ function ensureSubscribed() {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Browser simulation: the bundled sample plugins, with `hello` implemented in JS.
|
||||
|
||||
const SOLARIZED: ActiveTheme["theme"] = {
|
||||
id: "solarized-dark",
|
||||
name: "Solarized Dark",
|
||||
kind: "dark",
|
||||
tokens: {
|
||||
"--bg-app": "#002b36",
|
||||
"--bg-elev": "#03313c",
|
||||
"--bg-popup": "#073642",
|
||||
"--fg": "#93a1a1",
|
||||
"--fg-strong": "#eee8d5",
|
||||
"--accent": "#268bd2",
|
||||
"--syn-keyword": "#859900",
|
||||
"--syn-string": "#2aa198",
|
||||
const SOLARIZED: ActiveTheme["theme"][] = [
|
||||
{
|
||||
id: "solarized-light",
|
||||
name: "Solarized Light",
|
||||
kind: "light",
|
||||
tokens: {
|
||||
"--bg-app": "#fdf6e3",
|
||||
"--bg-island": "#fdf6e3",
|
||||
"--bg-frame": "#eee8d5",
|
||||
"--bg-elev": "#eee8d5",
|
||||
"--fg": "#657b83",
|
||||
"--accent": "#268bd2",
|
||||
"--syn-keyword": "#859900",
|
||||
"--syn-string": "#2aa198",
|
||||
},
|
||||
},
|
||||
};
|
||||
{
|
||||
id: "solarized-dark",
|
||||
name: "Solarized Dark",
|
||||
kind: "dark",
|
||||
tokens: {
|
||||
"--bg-app": "#002b36",
|
||||
"--bg-island": "#002b36",
|
||||
"--bg-frame": "#073642",
|
||||
"--bg-elev": "#073642",
|
||||
"--fg": "#839496",
|
||||
"--accent": "#268bd2",
|
||||
"--syn-keyword": "#859900",
|
||||
"--syn-string": "#2aa198",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function samplePlugins(): PluginInfo[] {
|
||||
const base = {
|
||||
@@ -118,14 +140,14 @@ function samplePlugins(): PluginInfo[] {
|
||||
},
|
||||
{
|
||||
...base,
|
||||
id: "theme-solarized-dark",
|
||||
name: "Solarized Dark Theme",
|
||||
id: "theme-solarized",
|
||||
name: "Solarized Themes",
|
||||
version: "1.0.0",
|
||||
description: "Ethan Schoonover's Solarized palette, dark variant.",
|
||||
path: "~/Library/Application Support/ai.l0ng.openidea/plugins/theme-solarized-dark",
|
||||
description: "Solarized Light and Solarized Dark: Ethan Schoonover's Solarized palette.",
|
||||
path: "~/Library/Application Support/ai.l0ng.openidea/plugins/theme-solarized",
|
||||
status: "static",
|
||||
hasCode: false,
|
||||
themes: [{ id: "solarized-dark", name: "Solarized Dark", kind: "dark" }],
|
||||
themes: SOLARIZED.map(({ id, name, kind }) => ({ id, name, kind })),
|
||||
},
|
||||
{
|
||||
...base,
|
||||
@@ -179,8 +201,9 @@ export function createMemoryPluginsBackend(
|
||||
const activeTheme = (): ActiveTheme | null => {
|
||||
if (!theme) return null;
|
||||
const p = plugins.find((x) => x.id === theme?.pluginId);
|
||||
if (!p?.enabled || theme.themeId !== SOLARIZED.id) return null;
|
||||
return { pluginId: p.id, theme: SOLARIZED };
|
||||
const t = SOLARIZED.find((x) => x.id === theme?.themeId);
|
||||
if (!p?.enabled || !t) return null;
|
||||
return { pluginId: p.id, theme: t };
|
||||
};
|
||||
return {
|
||||
list: async () => ({ plugins: plugins.map((p) => ({ ...p })), activeTheme: activeTheme() }),
|
||||
@@ -237,6 +260,7 @@ export function createMemoryPluginsBackend(
|
||||
emit({ type: "themeChanged", theme: t });
|
||||
return t;
|
||||
},
|
||||
setThemeFrame: async () => {},
|
||||
pickFolder: async () => null,
|
||||
onEvent: (handler) => {
|
||||
listeners.add(handler);
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { ActiveTheme, PluginEvent, PluginInfo } from "@/bindings";
|
||||
import { activeEditor, useEditorRegistry } from "@/editor/api";
|
||||
import { registerAction, type Action } from "@/platform/actions";
|
||||
import { notifications } from "@/workbench/contributions";
|
||||
import { applyThemeAttributes, currentThemeId, syncWindowBackground } from "@/workbench/theme";
|
||||
import { applyThemeAttributes, currentThemeId, syncWindowBackground, windowBackgroundHex } from "@/workbench/theme";
|
||||
import { pluginsBackend } from "./backend";
|
||||
import { refreshPlugins, runPluginCommand, usePlugins } from "./store";
|
||||
|
||||
@@ -105,10 +105,67 @@ export function applyPluginTheme(theme: ActiveTheme | null, root: HTMLElement =
|
||||
if (had || theme) syncWindowBackground();
|
||||
}
|
||||
|
||||
/** localStorage key read by public/boot.js: the selected theme's stylesheet, applied before the app loads. */
|
||||
export const PLUGIN_THEME_CACHE_KEY = "openidea.pluginTheme";
|
||||
|
||||
export interface CachedPluginTheme {
|
||||
/** `data-plugin-theme`: `<pluginId>/<themeId>`. */
|
||||
id: string;
|
||||
kind: "light" | "dark";
|
||||
css: string;
|
||||
}
|
||||
|
||||
function cachePluginTheme(theme: ActiveTheme | null) {
|
||||
try {
|
||||
if (!theme) {
|
||||
localStorage.removeItem(PLUGIN_THEME_CACHE_KEY);
|
||||
return;
|
||||
}
|
||||
const cached: CachedPluginTheme = {
|
||||
id: `${theme.pluginId}/${theme.theme.id}`,
|
||||
kind: theme.theme.kind === "light" ? "light" : "dark",
|
||||
css: pluginThemeCss(theme.theme),
|
||||
};
|
||||
localStorage.setItem(PLUGIN_THEME_CACHE_KEY, JSON.stringify(cached));
|
||||
} catch {
|
||||
/* storage unavailable */
|
||||
}
|
||||
}
|
||||
|
||||
let reportedFrame: string | undefined;
|
||||
|
||||
/**
|
||||
* Tells the backend the theme's resolved frame colour, which new windows open
|
||||
* in. Selecting a theme clears it there, so it is sent once per selection.
|
||||
*/
|
||||
function reportThemeFrame(theme: ActiveTheme | null) {
|
||||
const frame = theme ? windowBackgroundHex() : null;
|
||||
if (!theme || !frame) {
|
||||
reportedFrame = undefined;
|
||||
return;
|
||||
}
|
||||
const key = `${theme.pluginId}/${theme.theme.id}|${frame}`;
|
||||
if (key === reportedFrame) return;
|
||||
reportedFrame = key;
|
||||
void pluginsBackend()
|
||||
.setThemeFrame(frame)
|
||||
.catch(() => (reportedFrame = undefined));
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the selected plugin theme and keeps it for the next start-up. Until
|
||||
* the first plugin list arrives, the theme public/boot.js restored stays.
|
||||
*/
|
||||
export function trackPluginTheme(): () => void {
|
||||
applyPluginTheme(usePlugins.getState().activeTheme);
|
||||
const sync = (s: ReturnType<typeof usePlugins.getState>) => {
|
||||
if (!s.activeTheme && (!s.loaded || s.error)) return;
|
||||
applyPluginTheme(s.activeTheme);
|
||||
cachePluginTheme(s.activeTheme);
|
||||
reportThemeFrame(s.activeTheme);
|
||||
};
|
||||
sync(usePlugins.getState());
|
||||
const unsubscribe = usePlugins.subscribe((s, prev) => {
|
||||
if (s.activeTheme !== prev.activeTheme) applyPluginTheme(s.activeTheme);
|
||||
if (s.activeTheme !== prev.activeTheme || s.loaded !== prev.loaded) sync(s);
|
||||
});
|
||||
return () => {
|
||||
unsubscribe();
|
||||
|
||||
@@ -19,6 +19,7 @@ export const OPTION_LABELS: Array<[keyof ViewOptions, string]> = [
|
||||
["autoscrollToSource", "Autoscroll to Source"],
|
||||
["autoscrollFromSource", "Autoscroll from Source"],
|
||||
["singleClickOpen", "Open Files with Single Click"],
|
||||
["singleClickExpand", "Expand Directories with Single Click"],
|
||||
["previewTab", "Enable Preview Tab"],
|
||||
];
|
||||
|
||||
|
||||
@@ -109,6 +109,38 @@ describe("ProjectView", () => {
|
||||
act(() => setViewOption("previewTab", false));
|
||||
});
|
||||
|
||||
it("expands and collapses folders on single click only when enabled", async () => {
|
||||
const { row, rows } = setup();
|
||||
await waitFor(() => expect(rows()).toContain("README.md"));
|
||||
const src = () => row("src").querySelector("[data-project-path]")!;
|
||||
const click = (detail = 1, init: MouseEventInit = {}) => {
|
||||
fireEvent.mouseDown(row("src"), { detail, ...init });
|
||||
fireEvent.click(src(), { detail, ...init });
|
||||
};
|
||||
click();
|
||||
expect(rows()).not.toContain("App.kt");
|
||||
|
||||
act(() => setViewOption("singleClickExpand", true));
|
||||
click();
|
||||
await waitFor(() => expect(rows()).toContain("App.kt"));
|
||||
click();
|
||||
expect(rows()).not.toContain("App.kt");
|
||||
|
||||
// A double click toggles once: the second click and the dblclick are ignored.
|
||||
click(1);
|
||||
click(2);
|
||||
fireEvent.doubleClick(src());
|
||||
await waitFor(() => expect(rows()).toContain("App.kt"));
|
||||
|
||||
// Modifier clicks only change the selection.
|
||||
fireEvent.mouseDown(row("README.md"));
|
||||
click(1, { metaKey: true });
|
||||
expect(useProjectTree.getState().selected).toEqual(new Set(["/w/acme/README.md", "/w/acme/src"]));
|
||||
click(1, { shiftKey: true });
|
||||
expect(rows()).toContain("App.kt");
|
||||
act(() => setViewOption("singleClickExpand", false));
|
||||
});
|
||||
|
||||
it("cancels a new file when its name field loses focus", async () => {
|
||||
const { rows } = setup();
|
||||
await waitFor(() => expect(rows()).toContain("README.md"));
|
||||
|
||||
@@ -72,6 +72,7 @@ import {
|
||||
selectModel,
|
||||
setDropTarget,
|
||||
setExpanded,
|
||||
toggleExpanded,
|
||||
setSelection,
|
||||
useProjectTree,
|
||||
type Editing,
|
||||
@@ -221,6 +222,7 @@ interface RowProps {
|
||||
detail: string | null;
|
||||
onEditDone: () => void;
|
||||
onClick: (node: ProjectNode, e: ReactMouseEvent) => void;
|
||||
onDoubleClick: (node: ProjectNode, e: ReactMouseEvent) => void;
|
||||
onPointerDown: (node: ProjectNode, e: ReactPointerEvent) => void;
|
||||
}
|
||||
|
||||
@@ -233,6 +235,7 @@ const Row = memo(function Row({
|
||||
detail,
|
||||
onEditDone,
|
||||
onClick,
|
||||
onDoubleClick,
|
||||
onPointerDown,
|
||||
}: RowProps) {
|
||||
const deco = useFileDecoration(node.path);
|
||||
@@ -252,6 +255,7 @@ const Row = memo(function Row({
|
||||
data-dir={node.dir || undefined}
|
||||
title={deco.tooltip}
|
||||
onClick={(e) => onClick(node, e)}
|
||||
onDoubleClick={(e) => onDoubleClick(node, e)}
|
||||
onPointerDown={(e) => onPointerDown(node, e)}
|
||||
>
|
||||
<Icon name={icon} size={16} className={clsx(styles.icon, node.dir && styles.folderIcon, dimIcon && styles.faded)} />
|
||||
@@ -408,17 +412,30 @@ export function ProjectView() {
|
||||
|
||||
const focusTree = useCallback(() => requestAnimationFrame(() => handle.current?.focus()), []);
|
||||
|
||||
// A pointer drag that ends on the row it started from still clicks it.
|
||||
const dragged = useRef(false);
|
||||
|
||||
const onRowClick = useCallback((node: ProjectNode, e: ReactMouseEvent) => {
|
||||
if (e.button !== 0 || e.shiftKey || e.metaKey || e.ctrlKey || e.altKey || e.detail > 1) return;
|
||||
if (node.dir || node.placeholder) return;
|
||||
if (node.placeholder || dragged.current) return;
|
||||
const { options: o, editing: ed } = useProjectTree.getState();
|
||||
if (ed) return;
|
||||
if (node.dir) {
|
||||
if (o.singleClickExpand) toggleExpanded(node);
|
||||
return;
|
||||
}
|
||||
if (o.singleClickOpen) void openFile(node.path, { focus: false });
|
||||
else if (o.previewTab) void openFile(node.path, { preview: true, focus: false });
|
||||
}, []);
|
||||
|
||||
// Pointer-driven drag & drop (HTML5 DnD is unreliable inside the Tauri webview).
|
||||
// The single click already toggled the folder; double-click must not toggle it back.
|
||||
const onRowDoubleClick = useCallback((node: ProjectNode, e: ReactMouseEvent) => {
|
||||
if (node.dir && !node.placeholder && useProjectTree.getState().options.singleClickExpand) e.stopPropagation();
|
||||
}, []);
|
||||
|
||||
const onRowPointerDown = useCallback((node: ProjectNode, e: ReactPointerEvent) => {
|
||||
if (e.button === 0) dragged.current = false;
|
||||
if (e.button !== 0 || node.root || node.placeholder || node.readOnly || useProjectTree.getState().editing) return;
|
||||
const before = useProjectTree.getState().selected;
|
||||
const lead = useProjectTree.getState().lead;
|
||||
@@ -430,6 +447,7 @@ export function ProjectView() {
|
||||
if (!active) {
|
||||
if (Math.hypot(ev.clientX - startX, ev.clientY - startY) < DRAG_THRESHOLD) return;
|
||||
active = true;
|
||||
dragged.current = true;
|
||||
const m = selectModel(useProjectTree.getState());
|
||||
const keys = before.has(node.key) ? [...before] : [node.key];
|
||||
if (before.has(node.key)) setSelection(before, lead);
|
||||
@@ -577,10 +595,11 @@ export function ProjectView() {
|
||||
detail={node.root && !node.icon ? rootDetail : (node.detail ?? null)}
|
||||
onEditDone={focusTree}
|
||||
onClick={onRowClick}
|
||||
onDoubleClick={onRowDoubleClick}
|
||||
onPointerDown={onRowPointerDown}
|
||||
/>
|
||||
),
|
||||
[editing, cutPaths, dropTarget, rootDetail, focusTree, onRowClick, onRowPointerDown],
|
||||
[editing, cutPaths, dropTarget, rootDetail, focusTree, onRowClick, onRowDoubleClick, onRowPointerDown],
|
||||
);
|
||||
|
||||
if (!root) return null;
|
||||
|
||||
@@ -153,6 +153,7 @@ export function registerProjectActions(): () => void {
|
||||
title: `Project View: ${label}`,
|
||||
category: "View",
|
||||
keywords: ["project", "tree", "toggle"],
|
||||
toggled: () => useProjectTree.getState().options[key],
|
||||
run: () => setViewOption(key, !useProjectTree.getState().options[key]),
|
||||
}));
|
||||
|
||||
|
||||
@@ -245,7 +245,7 @@ export async function runFileRefactoring(
|
||||
} catch (e) {
|
||||
notifications().notify({
|
||||
level: "error",
|
||||
title: `Cannot ${label.toLowerCase()} ${moves.length === 1 ? nameOf(moves[0]!.from) : `${moves.length} items`}`,
|
||||
title: `${label === "Move" ? "Cannot move" : "Cannot rename"} ${moves.length === 1 ? nameOf(moves[0]!.from) : `${moves.length} items`}`,
|
||||
body: e instanceof Error ? e.message : String(e),
|
||||
});
|
||||
return "failed";
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
revealPath,
|
||||
setExpanded,
|
||||
setSelection,
|
||||
setViewOption,
|
||||
toggleExpanded,
|
||||
useProjectTree,
|
||||
} from "./store";
|
||||
import { beginNew, commitEditing, copySelection, paste, transfer } from "./operations";
|
||||
@@ -173,3 +175,27 @@ describe("file operations", () => {
|
||||
expect(fs.nodes.has("/p/docs/guide.md")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("single-click expansion", () => {
|
||||
it("toggles a folder, opening every folder of a compacted chain", async () => {
|
||||
await loadDir(ROOT);
|
||||
await loadDir("/p/src");
|
||||
await vi.waitFor(() => expect(childLabels(ROOT)).toContain("src/main/kotlin"));
|
||||
const chain = currentModel().byKey.get("/p/src")!;
|
||||
toggleExpanded(chain);
|
||||
expect([...useProjectTree.getState().expanded]).toEqual(
|
||||
expect.arrayContaining(["/p/src", "/p/src/main", "/p/src/main/kotlin"]),
|
||||
);
|
||||
toggleExpanded(chain);
|
||||
expect(useProjectTree.getState().expanded.has("/p/src")).toBe(false);
|
||||
});
|
||||
|
||||
it("is off by default and its option persists across reloads", async () => {
|
||||
expect(useProjectTree.getState().options.singleClickExpand).toBe(false);
|
||||
setViewOption("singleClickExpand", true);
|
||||
vi.resetModules();
|
||||
const fresh = await import("./store");
|
||||
expect(fresh.useProjectTree.getState().options.singleClickExpand).toBe(true);
|
||||
setViewOption("singleClickExpand", false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
planRefresh,
|
||||
visibleEntries,
|
||||
type Placeholder,
|
||||
type ProjectNode,
|
||||
type TreeModel,
|
||||
type TreeOptions,
|
||||
} from "./model";
|
||||
@@ -38,6 +39,8 @@ export interface ViewOptions extends TreeOptions {
|
||||
autoscrollToSource: boolean;
|
||||
/** Single click opens files (as regular tabs). */
|
||||
singleClickOpen: boolean;
|
||||
/** Single click on a folder row expands / collapses it. */
|
||||
singleClickExpand: boolean;
|
||||
/** Single click shows files in the preview tab (IDEA's "Enable Preview Tab"). */
|
||||
previewTab: boolean;
|
||||
}
|
||||
@@ -47,6 +50,7 @@ export const DEFAULT_VIEW_OPTIONS: ViewOptions = {
|
||||
autoscrollFromSource: false,
|
||||
autoscrollToSource: false,
|
||||
singleClickOpen: false,
|
||||
singleClickExpand: false,
|
||||
// IDEA's "Enable Preview Tab" is off by default.
|
||||
previewTab: false,
|
||||
};
|
||||
@@ -297,6 +301,17 @@ export function setExpanded(expanded: ReadonlySet<string>) {
|
||||
persistExpanded();
|
||||
}
|
||||
|
||||
/** Expands or collapses a folder node; expanding a compacted chain opens every folder in it. */
|
||||
export function toggleExpanded(node: Pick<ProjectNode, "key" | "chain">) {
|
||||
const next = new Set(get().expanded);
|
||||
if (next.has(node.key)) next.delete(node.key);
|
||||
else {
|
||||
next.add(node.key);
|
||||
for (const p of node.chain) next.add(keyLike(node.key, p));
|
||||
}
|
||||
setExpanded(next);
|
||||
}
|
||||
|
||||
export function setSelection(selected: ReadonlySet<string>, lead: string | null) {
|
||||
set({ selected, lead });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { act, fireEvent, render, screen, within } from "@testing-library/react";
|
||||
import type { Settings } from "@/platform/ipc";
|
||||
import { commands, type Settings } from "@/platform/ipc";
|
||||
|
||||
const settingsSet = vi.fn();
|
||||
const settingsGet = vi.fn();
|
||||
@@ -282,3 +282,29 @@ describe("Settings dialog", () => {
|
||||
expect(within(dialog).getByLabelText("UI font size")).toHaveValue("13");
|
||||
});
|
||||
});
|
||||
|
||||
describe("native Settings window background", () => {
|
||||
it.each([
|
||||
["a hex frame", "#26282c", "", "#26282c"],
|
||||
["an hsl() frame", "hsl(220, 13%, 15%)", "rgb(33, 37, 43)", "#21252b"],
|
||||
[
|
||||
"a translucent color-mix() frame",
|
||||
"color-mix(in srgb, #8040bf 80%, transparent)",
|
||||
"color(srgb 0.5 0.25 0.75 / 0.8)",
|
||||
"#8040bfcc",
|
||||
],
|
||||
])("sends %s as hex, the colour format the window command parses", async (_name, token, resolved, hex) => {
|
||||
const native = vi.spyOn(commands, "windowOpenSettings").mockResolvedValue({ status: "ok", data: null });
|
||||
const computed = vi
|
||||
.spyOn(globalThis, "getComputedStyle")
|
||||
.mockReturnValue({ getPropertyValue: () => token, color: resolved } as unknown as CSSStyleDeclaration);
|
||||
try {
|
||||
await executeAction("file.settings", "appearance");
|
||||
expect(native).toHaveBeenCalledWith("appearance", false, hex);
|
||||
expect(useSettingsDialog.getState().open).toBe(false);
|
||||
} finally {
|
||||
computed.mockRestore();
|
||||
native.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ import { call, commands, isTauri } from "@/platform/ipc";
|
||||
import { isPopupOpen, lazyComponent } from "@/ui";
|
||||
import { loadSettings, useProject } from "@/platform/project";
|
||||
import { provideSettingsSearchIndex, registerOverlay } from "@/workbench/contributions";
|
||||
import { windowBackgroundHex } from "@/workbench/theme";
|
||||
import { LazySettingsDialog, loadBuiltinPages, unloadBuiltinPages } from "./dialog/LazySettingsDialog";
|
||||
import {
|
||||
closeSettings,
|
||||
@@ -85,8 +86,7 @@ function settingsActions(open: (page?: string, newProjects?: boolean) => void):
|
||||
async function showSettings(page?: string, newProjects = false) {
|
||||
if (isTauri()) {
|
||||
try {
|
||||
const frame = getComputedStyle(document.documentElement).getPropertyValue("--bg-frame").trim();
|
||||
await call(commands.windowOpenSettings(page ?? null, newProjects, frame || null));
|
||||
await call(commands.windowOpenSettings(page ?? null, newProjects, windowBackgroundHex()));
|
||||
return;
|
||||
} catch (e) {
|
||||
console.warn("settings window unavailable, using the dialog", e);
|
||||
|
||||
@@ -195,7 +195,11 @@ export function activate(): () => void {
|
||||
terminalRuntime()?.applyThemeToAll();
|
||||
terminalRuntime()?.applyThemeToEmbedded();
|
||||
});
|
||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme", "style"] });
|
||||
// A plugin theme switch keeps data-theme when both themes share a light / dark base.
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["data-theme", "data-contrast", "data-plugin-theme", "style"],
|
||||
});
|
||||
disposers.push(() => observer.disconnect());
|
||||
}
|
||||
|
||||
|
||||
@@ -7,36 +7,9 @@
|
||||
import type { ITheme, Terminal } from "@xterm/xterm";
|
||||
import { fontWeightOf } from "@/platform/editorSettings";
|
||||
import { BUNDLED_MONO_FAMILY, fontsLoaded, monoFontFamily, monoStack } from "@/platform/fonts";
|
||||
import { parseColor, type Rgba } from "@/platform/cssColor";
|
||||
|
||||
export type Rgba = [number, number, number, number];
|
||||
|
||||
/** Parses `#rgb[a]`, `#rrggbb[aa]`, `rgb[a](…)` and `color(srgb …)`; `null` when unknown. */
|
||||
export function parseColor(input: string): Rgba | null {
|
||||
const s = input.trim().toLowerCase();
|
||||
if (s.startsWith("#")) {
|
||||
const h = s.slice(1);
|
||||
if (!/^[0-9a-f]+$/.test(h)) return null;
|
||||
if (h.length === 3 || h.length === 4) {
|
||||
const [r, g, b, a = "f"] = h.split("") as [string, string, string, string?];
|
||||
return [parseInt(r + r, 16), parseInt(g + g, 16), parseInt(b + b, 16), parseInt(a + a, 16) / 255];
|
||||
}
|
||||
if (h.length === 6 || h.length === 8) {
|
||||
const a = h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1;
|
||||
return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16), a];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
let m = /^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/.exec(s);
|
||||
if (m) {
|
||||
const alpha = m[4] === undefined ? 1 : m[4].endsWith("%") ? parseFloat(m[4]) / 100 : parseFloat(m[4]);
|
||||
return [Number(m[1]), Number(m[2]), Number(m[3]), alpha];
|
||||
}
|
||||
m = /^color\(srgb\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)(?:\s*\/\s*([\d.]+))?\s*\)$/.exec(s);
|
||||
if (m) {
|
||||
return [Number(m[1]) * 255, Number(m[2]) * 255, Number(m[3]) * 255, m[4] === undefined ? 1 : Number(m[4])];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
export { parseColor, type Rgba };
|
||||
|
||||
export function toHex([r, g, b]: Rgba): string {
|
||||
const h = (n: number) =>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { renderMarkdown } from "./render";
|
||||
import { hasMermaid, renderMermaidIn, resetMermaidForTests } from "./mermaid";
|
||||
import { hasMermaid, renderMermaidIn, resetMermaidForTests, subscribeTheme, themeKey } from "./mermaid";
|
||||
|
||||
const m = vi.hoisted(() => ({
|
||||
loads: 0,
|
||||
@@ -108,6 +108,66 @@ describe("mermaid in the markdown preview", () => {
|
||||
expect(ids.size).toBe(2);
|
||||
});
|
||||
|
||||
it("re-renders when switching between plugin themes of the same light / dark base", async () => {
|
||||
const root = document.documentElement;
|
||||
const changed = vi.fn();
|
||||
const off = subscribeTheme(changed);
|
||||
try {
|
||||
root.dataset.theme = "dark";
|
||||
root.dataset.pluginTheme = "theme-one/one-dark";
|
||||
const firstKey = themeKey();
|
||||
await renderMermaidIn(preview(SEQ)).done;
|
||||
root.dataset.pluginTheme = "theme-solarized/solarized-dark";
|
||||
await Promise.resolve();
|
||||
expect(changed).toHaveBeenCalled();
|
||||
expect(themeKey()).not.toBe(firstKey);
|
||||
await renderMermaidIn(preview(SEQ)).done;
|
||||
expect(m.render).toHaveBeenCalledTimes(2);
|
||||
} finally {
|
||||
off();
|
||||
delete root.dataset.pluginTheme;
|
||||
}
|
||||
});
|
||||
|
||||
it("passes colours a theme writes as hsl() or color-mix() to mermaid as RGB", async () => {
|
||||
const values: Record<string, string> = {
|
||||
"--bg-editor": "hsl(220, 13%, 18%)",
|
||||
"--fg": "color-mix(in srgb, hsl(220, 14%, 71%) 90%, black)",
|
||||
"--danger": "#e06c75",
|
||||
};
|
||||
const resolved: Record<string, string> = {
|
||||
"--bg-editor": "rgb(40, 44, 52)",
|
||||
"--fg": "color(srgb 0.6 0.62 0.66)",
|
||||
};
|
||||
let probe = "";
|
||||
const computed = vi
|
||||
.spyOn(globalThis, "getComputedStyle")
|
||||
.mockImplementation(
|
||||
(el) =>
|
||||
(el === document.documentElement
|
||||
? { getPropertyValue: (n: string) => values[n] ?? "" }
|
||||
: {
|
||||
color: resolved[(probe = (el as HTMLElement).style.color.slice(4, -1))] ?? "",
|
||||
}) as unknown as CSSStyleDeclaration,
|
||||
);
|
||||
try {
|
||||
await renderMermaidIn(preview(SEQ)).done;
|
||||
expect(probe).not.toBe("");
|
||||
expect(m.initialize).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
themeVariables: expect.objectContaining({
|
||||
background: "rgb(40, 44, 52)",
|
||||
textColor: "rgb(153, 158, 168)",
|
||||
errorTextColor: "#e06c75",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(document.querySelector("span[hidden]")).toBeNull();
|
||||
} finally {
|
||||
computed.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("skips superseded renders", async () => {
|
||||
const root = preview(SEQ.replace("test", "stale"));
|
||||
const pass = renderMermaidIn(root);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* turns placeholders into SVG. The library is imported on first use only.
|
||||
*/
|
||||
import type { Mermaid } from "mermaid";
|
||||
import { resolveTokenColor, toRgb } from "@/platform/cssColor";
|
||||
|
||||
export const MERMAID_SELECTOR = "[data-mermaid]";
|
||||
const OUTPUT_CLASS = "mermaid-output";
|
||||
@@ -29,27 +30,34 @@ function loadMermaid(): Promise<Mermaid> {
|
||||
/** Theme identity: anything that changes the colours read below. */
|
||||
export function themeKey(): string {
|
||||
const root = document.documentElement;
|
||||
return `${root.dataset.theme ?? ""}|${root.dataset.contrast ?? ""}|${root.style.getPropertyValue("--accent")}`;
|
||||
const { theme = "", contrast = "", pluginTheme = "" } = root.dataset;
|
||||
return `${theme}|${contrast}|${pluginTheme}|${root.style.getPropertyValue("--accent")}`;
|
||||
}
|
||||
|
||||
/** Calls `cb` when the theme attributes or the accent on the root change. */
|
||||
/** Calls `cb` when the theme attributes (a plugin theme's included) or the accent on the root change. */
|
||||
export function subscribeTheme(cb: () => void): () => void {
|
||||
if (typeof MutationObserver === "undefined") return () => {};
|
||||
const observer = new MutationObserver(cb);
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["data-theme", "data-contrast", "style"],
|
||||
attributeFilter: ["data-theme", "data-contrast", "data-plugin-theme", "style"],
|
||||
});
|
||||
return () => observer.disconnect();
|
||||
}
|
||||
|
||||
/** Diagram colours from the IDE tokens; values mermaid can't parse (e.g. `color-mix`) are skipped. */
|
||||
/**
|
||||
* Diagram colours from the IDE tokens. Mermaid parses only hex and `rgb()`, so
|
||||
* tokens a theme writes as `hsl()`, `color-mix()` or `var()` are resolved to RGB.
|
||||
*/
|
||||
function themeVariables(): Record<string, string | boolean> {
|
||||
const css = getComputedStyle(document.documentElement);
|
||||
const root = document.documentElement;
|
||||
const css = getComputedStyle(root);
|
||||
const token = (...names: string[]) => {
|
||||
for (const n of names) {
|
||||
const v = css.getPropertyValue(n).trim();
|
||||
if (/^(#[0-9a-f]{3,8}|rgba?\([^)]*\))$/i.test(v)) return v;
|
||||
const resolved = v ? resolveTokenColor(n, root) : null;
|
||||
if (resolved) return toRgb(resolved);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
@@ -32,6 +32,40 @@ describe("color schemes", () => {
|
||||
expect(pythonItalic).toContain("--syn-python-static-italic: italic !important");
|
||||
});
|
||||
|
||||
it("applies an explicit scheme over a plugin theme of the same base", () => {
|
||||
const root = document.documentElement;
|
||||
root.dataset.theme = "dark";
|
||||
root.dataset.pluginTheme = "theme-one/one-dark";
|
||||
try {
|
||||
// "Theme default" keeps the plugin theme's own editor colours.
|
||||
expect(schemeCss("theme-default", undefined, root)).toBe("");
|
||||
expect(schemeCss("dark", undefined, root)).toContain("--syn-keyword: #cf8e6d !important");
|
||||
delete root.dataset.pluginTheme;
|
||||
expect(schemeCss("dark", undefined, root)).toBe("");
|
||||
} finally {
|
||||
delete root.dataset.pluginTheme;
|
||||
}
|
||||
});
|
||||
|
||||
it("re-applies the scheme when a plugin theme of the same base is selected", async () => {
|
||||
const root = document.documentElement;
|
||||
root.dataset.theme = "dark";
|
||||
const previous = useProject.getState().settings;
|
||||
useProject.setState({ settings: { editor: { colorScheme: "dark" } } as unknown as Settings });
|
||||
const dispose = installColorScheme();
|
||||
const sheet = () => document.getElementById("openidea-editor-color-scheme")?.textContent ?? "";
|
||||
try {
|
||||
expect(sheet()).toBe("");
|
||||
root.dataset.pluginTheme = "theme-one/one-dark";
|
||||
await Promise.resolve();
|
||||
expect(sheet()).toContain("--syn-keyword: #cf8e6d !important");
|
||||
} finally {
|
||||
dispose();
|
||||
delete root.dataset.pluginTheme;
|
||||
useProject.setState({ settings: previous });
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps editor weights out of the schemes", () => {
|
||||
for (const id of ["light", "dark", "dark-hc"] as const) {
|
||||
expect(schemeTokens(id).has("--fw-code")).toBe(false);
|
||||
|
||||
@@ -267,7 +267,10 @@ const SAFE = /^[^;{}<>]*$/;
|
||||
export function schemeCss(setting: string | undefined, overrides: Record<string, Record<string, TextAttributesOverride>> | undefined, root?: HTMLElement | null): string {
|
||||
const id = effectiveScheme(setting, root);
|
||||
const decls: Array<[string, string]> = [];
|
||||
if (id !== themeScheme(root)) {
|
||||
// A plugin theme brings its own editor colours: an explicit scheme replaces them even on the same base.
|
||||
const el = root === undefined ? (typeof document !== "undefined" ? document.documentElement : null) : root;
|
||||
const pluginTheme = !!el?.dataset.pluginTheme && id === setting;
|
||||
if (id !== themeScheme(root) || pluginTheme) {
|
||||
const tokens = schemeTokens(id);
|
||||
for (const [k, v] of tokens) if (SCHEME_VARS.has(k) || k.startsWith("--syn-")) decls.push([k, v]);
|
||||
// The editor surface follows the scheme, not the island colour of the UI theme.
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { parseColor, resolveTokenColor, toHex, toRgb } from "./cssColor";
|
||||
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
describe("css colours", () => {
|
||||
it("parses hex, rgb() and color(srgb) and formats them back", () => {
|
||||
expect(parseColor("#eee8d5")).toEqual([238, 232, 213, 1]);
|
||||
expect(parseColor("#fff8")).toEqual([255, 255, 255, 0x88 / 255]);
|
||||
expect(parseColor("rgb(33, 37, 43)")).toEqual([33, 37, 43, 1]);
|
||||
expect(parseColor("rgba(0 0 0 / 50%)")).toEqual([0, 0, 0, 0.5]);
|
||||
expect(parseColor("color(srgb 0.5 0.25 0.75 / 0.8)")).toEqual([127.5, 63.75, 191.25, 0.8]);
|
||||
expect(parseColor("hsl(220, 13%, 15%)")).toBeNull();
|
||||
expect(toHex([33, 37, 43, 1])).toBe("#21252b");
|
||||
expect(toHex([127.5, 63.75, 191.25, 0.8])).toBe("#8040bfcc");
|
||||
expect(toRgb([33, 37, 43, 1])).toBe("rgb(33, 37, 43)");
|
||||
expect(toRgb([0, 0, 0, 0.5])).toBe("rgba(0, 0, 0, 0.5)");
|
||||
});
|
||||
|
||||
it("resolves tokens written as hsl(), color-mix() or var() through the browser", () => {
|
||||
const root = document.documentElement;
|
||||
const tokens: Record<string, string> = { "--hex": "#073642", "--hsl": "hsl(220, 13%, 15%)", "--none": "" };
|
||||
vi.spyOn(globalThis, "getComputedStyle").mockImplementation(
|
||||
(el) =>
|
||||
(el === root
|
||||
? { getPropertyValue: (n: string) => tokens[n] ?? "" }
|
||||
: {
|
||||
color: (el as HTMLElement).style.color === "var(--hsl)" ? "rgb(33, 37, 43)" : "",
|
||||
}) as unknown as CSSStyleDeclaration,
|
||||
);
|
||||
expect(resolveTokenColor("--hex", root)).toEqual([7, 54, 66, 1]);
|
||||
expect(resolveTokenColor("--hsl", root)).toEqual([33, 37, 43, 1]);
|
||||
expect(resolveTokenColor("--none", root)).toBeNull();
|
||||
// The probe element is removed again.
|
||||
expect(root.querySelector("span[hidden]")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Design tokens as concrete colours. Themes may write tokens as `hsl()`,
|
||||
* `color-mix()` or `var()`; consumers outside CSS (the native window, Mermaid,
|
||||
* xterm) need RGB, so a token is resolved by the browser and parsed here.
|
||||
*/
|
||||
|
||||
/** Red, green, blue (0–255) and alpha (0–1). */
|
||||
export type Rgba = [number, number, number, number];
|
||||
|
||||
/** Parses `#rgb[a]`, `#rrggbb[aa]`, `rgb[a](…)` and `color(srgb …)`; `null` when unknown. */
|
||||
export function parseColor(input: string): Rgba | null {
|
||||
const s = input.trim().toLowerCase();
|
||||
if (s.startsWith("#")) {
|
||||
const h = s.slice(1);
|
||||
if (!/^[0-9a-f]+$/.test(h)) return null;
|
||||
if (h.length === 3 || h.length === 4) {
|
||||
const [r, g, b, a = "f"] = h.split("") as [string, string, string, string?];
|
||||
return [parseInt(r + r, 16), parseInt(g + g, 16), parseInt(b + b, 16), parseInt(a + a, 16) / 255];
|
||||
}
|
||||
if (h.length === 6 || h.length === 8) {
|
||||
const a = h.length === 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1;
|
||||
return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16), a];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
let m = /^rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)(?:\s*[,/]\s*([\d.]+%?))?\s*\)$/.exec(s);
|
||||
if (m) {
|
||||
const alpha = m[4] === undefined ? 1 : m[4].endsWith("%") ? parseFloat(m[4]) / 100 : parseFloat(m[4]);
|
||||
return [Number(m[1]), Number(m[2]), Number(m[3]), alpha];
|
||||
}
|
||||
m = /^color\(srgb\s+([\d.]+)\s+([\d.]+)\s+([\d.]+)(?:\s*\/\s*([\d.]+))?\s*\)$/.exec(s);
|
||||
if (m) {
|
||||
return [Number(m[1]) * 255, Number(m[2]) * 255, Number(m[3]) * 255, m[4] === undefined ? 1 : Number(m[4])];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The computed colour of token `name` on `root`, whatever syntax the theme wrote it in. */
|
||||
export function resolveTokenColor(name: string, root: HTMLElement = document.documentElement): Rgba | null {
|
||||
if (typeof getComputedStyle === "undefined") return null;
|
||||
const raw = getComputedStyle(root).getPropertyValue(name).trim();
|
||||
if (!raw) return null;
|
||||
const direct = parseColor(raw);
|
||||
if (direct) return direct;
|
||||
const probe = root.ownerDocument.createElement("span");
|
||||
probe.hidden = true;
|
||||
probe.style.color = `var(${name})`;
|
||||
root.appendChild(probe);
|
||||
const color = getComputedStyle(probe).color;
|
||||
probe.remove();
|
||||
return parseColor(color);
|
||||
}
|
||||
|
||||
const byte = (v: number) => Math.min(255, Math.max(0, Math.round(v)));
|
||||
|
||||
/** `#rrggbb`, or `#rrggbbaa` when translucent. */
|
||||
export function toHex([r, g, b, a]: Rgba): string {
|
||||
const parts = [r, g, b].map(byte);
|
||||
if (byte(a * 255) !== 255) parts.push(byte(a * 255));
|
||||
return `#${parts.map((x) => x.toString(16).padStart(2, "0")).join("")}`;
|
||||
}
|
||||
|
||||
/** `rgb(r, g, b)`, or `rgba(r, g, b, a)` when translucent. */
|
||||
export function toRgb([r, g, b, a]: Rgba): string {
|
||||
const [rr, gg, bb] = [r, g, b].map(byte);
|
||||
return a < 1 ? `rgba(${rr}, ${gg}, ${bb}, ${Math.round(a * 1000) / 1000})` : `rgb(${rr}, ${gg}, ${bb})`;
|
||||
}
|
||||
@@ -6,7 +6,6 @@
|
||||
") contain CRLF line separators and core.autocrlf is not set, so they would be committed with CRLF. Convert them with the line separator widget in the status bar, or set": ") 包含 CRLF 行分隔符且未设置 core.autocrlf,因此将以 CRLF 提交。请使用状态栏中的行分隔符组件进行转换,或设置",
|
||||
", e.g.": ",例如",
|
||||
", then press Tab.": ",然后按 Tab。",
|
||||
"? Its query console file stays on disk.": "?其查询控制台文件会保留在磁盘上。",
|
||||
"? This cannot be undone.": "?此操作无法撤消。",
|
||||
"A charset in .editorconfig takes precedence. Change one file's encoding from the status bar.": ".editorconfig 中的 charset 优先。可以从状态栏更改单个文件的编码。",
|
||||
"A command line, e.g. /bin/zsh --login. Leave empty to use the login shell.": "命令行,例如 /bin/zsh --login。留空则使用登录 Shell。",
|
||||
@@ -30,14 +29,12 @@
|
||||
"Accessibility": "辅助功能",
|
||||
"Action id (bind it in Keymap)": "操作 ID(可在“按键映射”中绑定)",
|
||||
"Actions": "操作",
|
||||
"Add Configuration…": "添加配置…",
|
||||
"Actions on Save": "保存时操作",
|
||||
"Actions run when a file is saved with Save (⌘S) or Save All. Trailing spaces and the final line break are configured in": "使用“保存”(⌘S) 或“全部保存”保存文件时运行的操作。尾随空格和末尾换行符的配置位置:",
|
||||
"Activate on Save": "保存时激活",
|
||||
"Activate virtualenv": "激活 virtualenv",
|
||||
"Add": "添加",
|
||||
"Add Column": "添加列",
|
||||
"Add Conditional Breakpoint…": "添加条件断点…",
|
||||
"Add Field Watchpoint": "添加字段监视点",
|
||||
"Add File to Git": "将文件添加到 Git",
|
||||
"Add GitHub Pattern": "添加 GitHub 模式",
|
||||
@@ -45,7 +42,6 @@
|
||||
"Add JIRA Pattern": "添加 JIRA 模式",
|
||||
"Add Keyboard Shortcut": "添加键盘快捷键",
|
||||
"Add Kind": "添加类型",
|
||||
"Add Logpoint…": "添加日志点…",
|
||||
"Add Method Breakpoint": "添加方法断点",
|
||||
"Add New Configuration": "添加新配置",
|
||||
"Add Pattern": "添加模式",
|
||||
@@ -61,7 +57,6 @@
|
||||
"Add to VCS": "添加到 VCS",
|
||||
"Add to Watches": "添加到监视",
|
||||
"Add unambiguous imports on the fly": "动态添加明确的导入",
|
||||
"Add variable": "添加变量",
|
||||
"Added to the environment of new terminal sessions.": "添加到新终端会话的环境中。",
|
||||
"Additional arguments": "附加实参",
|
||||
"Adjust colors for red-green vision deficiency": "针对红绿色觉缺陷调整颜色",
|
||||
@@ -239,7 +234,6 @@
|
||||
"Change Signature…": "更改签名…",
|
||||
"Change actions": "更改操作",
|
||||
"Change font size with Command+Mouse Wheel": "使用 Command+鼠标滚轮更改字体大小",
|
||||
"Change with": "更改方式:",
|
||||
"Changed files": "已更改的文件",
|
||||
"Changelist:": "更改列表:",
|
||||
"Changes": "更改",
|
||||
@@ -307,7 +301,6 @@
|
||||
"Close non-modified files first": "优先关闭未修改的文件",
|
||||
"Close performance overlay": "关闭性能浮层",
|
||||
"Close session when it ends": "会话结束时关闭",
|
||||
"Close stdin (send EOF)": "关闭 stdin(发送 EOF)",
|
||||
"Closed": "已关闭",
|
||||
"Closing Policy": "关闭策略",
|
||||
"Code": "代码",
|
||||
@@ -397,7 +390,6 @@
|
||||
"Copied code keeps the editor's syntax colors and font when pasted into documents and mail.": "复制的代码粘贴到文档和邮件中时会保留编辑器的语法颜色和字体。",
|
||||
"Copy": "复制",
|
||||
"Copy / Paste Special": "复制/选择性粘贴",
|
||||
"Copy Action ID": "复制操作 ID",
|
||||
"Copy All": "全部复制",
|
||||
"Copy Commit Message": "复制提交消息",
|
||||
"Copy Configuration": "复制配置",
|
||||
@@ -632,7 +624,6 @@
|
||||
"Enabled": "已启用",
|
||||
"Enabled tools appear in Find Action (⇧⌘A) under “External Tools” and can get shortcuts in Keymap. Output is shown in the Run tool window.": "已启用的工具会出现在“查找操作”(⇧⌘A) 的“外部工具”下,并可以在“按键映射”中设置快捷键。输出显示在“运行”工具窗口中。",
|
||||
"Encoding of the output of processes started from Run configurations.": "从运行配置启动的进程的输出编码。",
|
||||
"Engine": "引擎",
|
||||
"Enhanced for loop": "增强型 for 循环",
|
||||
"Ensure every saved file ends with a line break": "确保每个已保存的文件都以换行符结尾",
|
||||
"Enter a tag, branch or commit hash:": "输入标签、分支或提交哈希:",
|
||||
@@ -642,7 +633,6 @@
|
||||
"Environment": "环境",
|
||||
"Environment variables": "环境变量",
|
||||
"Error Highlighting": "错误高亮",
|
||||
"Ethan Schoonover's Solarized palette, dark variant.": "Ethan Schoonover 的 Solarized 调色板,深色变体。",
|
||||
"Evaluate": "计算",
|
||||
"Evaluate Expression…": "计算表达式…",
|
||||
"Evaluate expression": "计算表达式",
|
||||
@@ -779,7 +769,6 @@
|
||||
"For symbols not supported by the main font": "用于主字体不支持的符号",
|
||||
"Force": "强制",
|
||||
"Force (with lease)": "强制 (with lease)",
|
||||
"Force Delete": "强制删除",
|
||||
"Force Push": "强制推送",
|
||||
"Force Run to Cursor": "强制运行到光标处",
|
||||
"Force Step Into": "强制步入",
|
||||
@@ -834,7 +823,6 @@
|
||||
"Grey": "灰色",
|
||||
"Greyscale": "灰度",
|
||||
"Group": "组",
|
||||
"Group by Directory": "按目录分组",
|
||||
"Group by Severity": "按严重性分组",
|
||||
"Groups": "组",
|
||||
"Gutter Icons": "装订区域图标",
|
||||
@@ -879,7 +867,7 @@
|
||||
"IDE and Project Settings": "IDE 和项目设置",
|
||||
"IDE font": "IDE 字体",
|
||||
"IDE font size": "IDE 字体大小",
|
||||
"Installed {name}": "已安装 {name}",
|
||||
"Installed {name} {version}": "已安装 {name} {version}",
|
||||
"Installing {name}": "正在安装 {name}",
|
||||
"IPC round trip": "IPC 往返",
|
||||
"Idle seconds": "空闲秒数",
|
||||
@@ -896,7 +884,6 @@
|
||||
"Implementation(s)": "实现",
|
||||
"Imports": "导入",
|
||||
"In Project": "在项目中",
|
||||
"In Selection": "在所选内容中",
|
||||
"Include": "包括",
|
||||
"Include non-project items": "包括非项目项",
|
||||
"Include untracked files": "包括未跟踪的文件",
|
||||
@@ -1048,8 +1035,6 @@
|
||||
"Location:": "位置:",
|
||||
"Log": "日志",
|
||||
"Log filters": "日志过滤器",
|
||||
"Log message": "日志消息",
|
||||
"Log message instead of suspending": "记录消息而不挂起",
|
||||
"Long tasks": "长任务",
|
||||
"Looking for references…": "正在查找引用…",
|
||||
"Looks for TODO and FIXME in added lines.": "在新增的行中查找 TODO 和 FIXME。",
|
||||
@@ -1201,7 +1186,6 @@
|
||||
"No changes to roll back": "没有要回滚的更改",
|
||||
"No changes to shelve": "没有要搁置的更改",
|
||||
"No commits": "没有提交",
|
||||
"No configurations. Click + to add one.": "没有配置。点击 + 添加一个。",
|
||||
"No conflicts": "没有冲突",
|
||||
"No coverage data": "没有覆盖率数据",
|
||||
"No data sources": "没有数据源",
|
||||
@@ -1248,7 +1232,6 @@
|
||||
"No special permissions.": "没有特殊权限。",
|
||||
"No stashes": "没有储藏",
|
||||
"No structure to show": "没有可显示的结构",
|
||||
"No surround templates for this language": "此语言没有包围模板",
|
||||
"No tools match.": "没有匹配的工具。",
|
||||
"No usages found": "未找到用法",
|
||||
"No usages of '{name}' found": "未找到 '{name}' 的用法",
|
||||
@@ -1261,7 +1244,6 @@
|
||||
"Nothing found": "未找到任何内容",
|
||||
"Nothing found in project files": "项目文件中未找到任何内容",
|
||||
"Nothing here": "这里什么都没有",
|
||||
"Nothing is running": "没有正在运行的内容",
|
||||
"Nothing matches": "没有匹配项",
|
||||
"Nothing recorded before this label": "此标签之前没有记录",
|
||||
"Nothing to add: the files are already under version control": "没有要添加的内容:这些文件已受版本控制",
|
||||
@@ -1364,7 +1346,7 @@
|
||||
"Rerun Search": "重新运行搜索",
|
||||
"Right Bottom": "右下",
|
||||
"Right Top": "右上",
|
||||
"Runs jdtls only, like IDEA's JDK for importer; the project keeps compiling and running with the Project JDK. jdtls needs JDK 21 or newer (recent releases 25). The jdtls executable itself is set in Language Servers.": "仅用于运行 jdtls(类似 IDEA 的「导入器 JDK」);项目仍使用项目 JDK 编译和运行。jdtls 需要 JDK 21 或更高版本(较新版本需要 25)。jdtls 可执行文件本身在「语言服务器」中设置。",
|
||||
"Runs jdtls only, like IDEA's JDK for importer; the project keeps compiling and running with the Project JDK. jdtls needs JDK 21 or newer (recent releases 25); Download JDK… in the list fetches one. The jdtls executable itself is set in Language Servers.": "仅用于运行 jdtls(类似 IDEA 的「导入器 JDK」);项目仍使用项目 JDK 编译和运行。jdtls 需要 JDK 21 或更高版本(较新版本需要 25);可通过列表中的「下载 JDK…」获取。jdtls 可执行文件本身在「语言服务器」中设置。",
|
||||
"Language server JDK": "语言服务器 JDK",
|
||||
"Password": "密码",
|
||||
"Password:": "密码:",
|
||||
@@ -1476,7 +1458,6 @@
|
||||
"Protected branches": "受保护的分支",
|
||||
"Prune": "清理",
|
||||
"Prune Docker…": "清理 Docker…",
|
||||
"Prune…": "清理…",
|
||||
"Public function": "公共函数",
|
||||
"Pull Again": "重新拉取",
|
||||
"Pull Docker Image…": "拉取 Docker 镜像…",
|
||||
@@ -1582,7 +1563,6 @@
|
||||
"Remove Template": "移除模板",
|
||||
"Remove Watch": "移除监视",
|
||||
"Remove coverage highlighting from editors and the project view": "从编辑器和项目视图中移除覆盖率高亮",
|
||||
"Remove data source": "移除数据源",
|
||||
"Remove from Favorites": "从收藏夹中移除",
|
||||
"Remove from Recent Projects": "从最近的项目中移除",
|
||||
"Remove from Recent Projects…": "从最近的项目中移除…",
|
||||
@@ -1591,7 +1571,6 @@
|
||||
"Remove successfully applied files from the shelf": "从搁置中移除已成功应用的文件",
|
||||
"Remove trailing blank lines at the end of saved files": "移除已保存文件末尾的空行",
|
||||
"Remove trailing spaces on:": "移除尾随空格:",
|
||||
"Remove variable": "移除变量",
|
||||
"Rename": "重命名",
|
||||
"Rename File Inline": "内联重命名文件",
|
||||
"Rename File…": "重命名文件…",
|
||||
@@ -1725,7 +1704,6 @@
|
||||
"Run Maven Build": "运行 Maven 构建",
|
||||
"Run SQL Script": "运行 SQL 脚本",
|
||||
"Run SQL Script…": "运行 SQL 脚本…",
|
||||
"Run a configuration with": "运行配置:",
|
||||
"Run a configuration with coverage to see which lines its tests execute.": "使用覆盖率运行配置,即可查看其测试执行了哪些行。",
|
||||
"Run a request from an .http file with the gutter ▶ or ⌘↩.": "使用装订区域中的 ▶ 或 ⌘↩ 运行 .http 文件中的请求。",
|
||||
"Run actions": "运行操作",
|
||||
@@ -1757,7 +1735,6 @@
|
||||
"Runs sync, builds and Maven run configurations. A chosen home is used instead of the project's mvnw.": "用于同步、构建和 Maven 运行配置。选定后将代替项目的 mvnw。",
|
||||
"Runs the language server's Organize Imports action.": "运行语言服务器的“整理导入”操作。",
|
||||
"Runs the language server's formatter before an explicit save (Actions on Save).": "在显式保存前运行语言服务器的格式化程序(保存时操作)。",
|
||||
"Runs this project": "运行此项目",
|
||||
"Search history": "搜索历史",
|
||||
"Search history (⌥↑ / ⌥↓)": "搜索历史(⌥↑ / ⌥↓)",
|
||||
"Searching for usages of '{symbol}'": "正在搜索 '{symbol}' 的用法",
|
||||
@@ -1768,7 +1745,6 @@
|
||||
"SQLite can rename or drop an existing column, not change its type, NOT NULL or default.": "SQLite 可以重命名或删除现有列,但不能更改其类型、NOT NULL 或默认值。",
|
||||
"SSH authentication type": "SSH 身份验证类型",
|
||||
"SSL mode": "SSL 模式",
|
||||
"SSL:": "SSL:",
|
||||
"Safe Delete…": "安全删除…",
|
||||
"Sample plugin: inserts today's date and counts words in the active file.": "示例插件:插入今天的日期,并统计当前文件的字数。",
|
||||
"Save": "保存",
|
||||
@@ -1831,7 +1807,6 @@
|
||||
"Select a column": "选择一列",
|
||||
"Select a commit in the Git log": "在 Git 日志中选择一个提交",
|
||||
"Select a commit to view changes": "选择一个提交以查看更改",
|
||||
"Select a configuration": "选择一个配置",
|
||||
"Select a container, image, volume or network.": "选择一个容器、镜像、卷或网络。",
|
||||
"Select a group": "选择一个组",
|
||||
"Select a pull request": "选择一个拉取请求",
|
||||
@@ -1890,7 +1865,6 @@
|
||||
"Show History for File": "显示文件的历史记录",
|
||||
"Show Ignored": "显示已忽略",
|
||||
"Show Infos and Hints": "显示信息和提示",
|
||||
"Show Input Field": "显示输入字段",
|
||||
"Show Local History": "显示本地历史记录",
|
||||
"Show Log": "显示日志",
|
||||
"Show Memory Indicator Details": "显示内存指示器详细信息",
|
||||
@@ -1911,7 +1885,6 @@
|
||||
"Show breadcrumbs for:": "为以下语言显示痕迹导航:",
|
||||
"Show breadcrumbs:": "显示痕迹导航:",
|
||||
"Show changed only": "仅显示已更改的",
|
||||
"Show close button": "显示关闭按钮",
|
||||
"Show code folding outline": "显示代码折叠轮廓",
|
||||
"Show debug window on breakpoint hit": "命中断点时显示调试窗口",
|
||||
"Show details": "显示详细信息",
|
||||
@@ -1939,7 +1912,6 @@
|
||||
"Show sticky lines while scrolling": "滚动时显示粘滞行",
|
||||
"Show suggestions as you type": "输入时显示建议",
|
||||
"Show table data": "显示表数据",
|
||||
"Show tabs in multiple rows": "多行显示标签页",
|
||||
"Show the documentation popup in": "在以下时间后显示文档弹出窗口",
|
||||
"Show the parameter info popup in": "在以下时间后显示形参信息弹出窗口",
|
||||
"Show the selected coverage suite in editors and the project view": "在编辑器和项目视图中显示所选覆盖率套件",
|
||||
@@ -1951,7 +1923,6 @@
|
||||
"Show whitespaces": "显示空白",
|
||||
"Shown as the right margin guide in the editor.": "在编辑器中显示为右边距参考线。",
|
||||
"Side-by-side Viewer": "并排查看器",
|
||||
"Sign-off Commit": "签署提交",
|
||||
"Sign-off commit": "签署提交",
|
||||
"Size": "大小",
|
||||
"Size:": "大小:",
|
||||
@@ -1986,7 +1957,6 @@
|
||||
"Squash Into…": "压缩到…",
|
||||
"Stage": "暂存",
|
||||
"Stage Change (git add hunk)": "暂存更改 (git add hunk)",
|
||||
"Standard input": "标准输入",
|
||||
"Standard library": "标准库",
|
||||
"Start": "启动",
|
||||
"Start Container": "启动容器",
|
||||
@@ -1997,7 +1967,6 @@
|
||||
"Start debugging a run configuration": "开始调试运行配置",
|
||||
"Start directory": "起始目录",
|
||||
"Startup": "启动",
|
||||
"Stash Changes": "储藏更改",
|
||||
"Stash Changes…": "储藏更改…",
|
||||
"Stashes": "储藏",
|
||||
"State": "状态",
|
||||
@@ -2038,7 +2007,6 @@
|
||||
"Surround With…": "包围方式…",
|
||||
"Surround selection on typing quote or brace": "输入引号或括号时包围所选内容",
|
||||
"Suspend on": "挂起时机",
|
||||
"Suspends when empty": "为空时挂起",
|
||||
"Switch to Auto-Commit": "切换到自动提交",
|
||||
"Switcher": "切换器",
|
||||
"Switcher (Backward)": "切换器(向后)",
|
||||
@@ -2148,7 +2116,6 @@
|
||||
"Toggle Performance Overlay (Dev)": "切换性能浮层(开发)",
|
||||
"Toggle Read-only Attribute": "切换只读属性",
|
||||
"Toggle Temporary Line Breakpoint": "切换临时行断点",
|
||||
"Tool": "工具",
|
||||
"Tool Settings": "工具设置",
|
||||
"Tool Windows": "工具窗口",
|
||||
"Tool windows": "工具窗口",
|
||||
@@ -2252,7 +2219,6 @@
|
||||
"Used by the npm run configurations detected from package.json scripts.": "供从 package.json 脚本检测到的 npm 运行配置使用。",
|
||||
"Used for new files.": "用于新文件。",
|
||||
"User": "用户",
|
||||
"User name or email": "用户名或电子邮件",
|
||||
"User settings file": "用户设置文件",
|
||||
"User:": "用户:",
|
||||
"Uses HostName, User, Port, IdentityFile and ProxyJump from ~/.ssh/config, and keys of the SSH agent.": "使用 ~/.ssh/config 中的 HostName、User、Port、IdentityFile 和 ProxyJump,以及 SSH 代理中的密钥。",
|
||||
@@ -2304,7 +2270,6 @@
|
||||
"Yellow": "黄色",
|
||||
"You can also drop a folder onto this window.": "也可以将文件夹拖放到此窗口。",
|
||||
"Your version": "您的版本",
|
||||
"Yours (current branch)": "您的(当前分支)",
|
||||
"Zoom": "缩放",
|
||||
"Zoom In IDE": "放大 IDE",
|
||||
"Zoom Out IDE": "缩小 IDE",
|
||||
@@ -2330,9 +2295,7 @@
|
||||
"found in the changed lines:": "在更改的行中找到:",
|
||||
"from packageManager / lock file": "来自 packageManager / 锁文件",
|
||||
"from ~/.ssh/config": "来自 ~/.ssh/config",
|
||||
"func literal": "func 字面量",
|
||||
"has been resolved and staged.": "已解决并暂存。",
|
||||
"has uncommitted changes. Commit or roll them back before switching to auto-commit?": "有未提交的更改。切换到自动提交前要提交还是回滚这些更改?",
|
||||
"host or ~/.ssh/config alias": "主机或 ~/.ssh/config 别名",
|
||||
"if statement": "if 语句",
|
||||
"impl block": "impl 块",
|
||||
@@ -2365,7 +2328,6 @@
|
||||
"not installed in this project": "此项目中未安装",
|
||||
"occurrences ·": "个匹配项 ·",
|
||||
"or click a line number to set a breakpoint.": "或点击行号设置断点。",
|
||||
"or pick one in the toolbar.": "或在工具栏中选择一个。",
|
||||
"per module, the JDK of its Java version": "按模块使用与其 Java 版本匹配的 JDK",
|
||||
"the project JDK if new enough, else the first installed JDK that is": "项目 JDK 版本足够时使用它,否则使用第一个版本足够的已安装 JDK",
|
||||
"pip output": "pip 输出",
|
||||
@@ -2379,7 +2341,6 @@
|
||||
"select all rows from a table": "从表中选择所有行",
|
||||
"select specific rows from a table": "从表中选择特定行",
|
||||
"select the number of specific rows in a table": "选择表中特定行的数量",
|
||||
"stdin closed": "stdin 已关闭",
|
||||
"surrounded text,": "被包围的文本,",
|
||||
"the JavaScript debugger (vscode-js-debug)": "JavaScript 调试器 (vscode-js-debug)",
|
||||
"the language server's own TypeScript": "语言服务器自带的 TypeScript",
|
||||
@@ -2402,7 +2363,6 @@
|
||||
"{name} is not installed": "{name} 未安装",
|
||||
"· Conflicts": "· 冲突",
|
||||
"· read-only": "· 只读",
|
||||
"… more": "… 更多",
|
||||
"↵ Open · ⇧↵ Open to the side · ⌘↵ Open in Find Window": "↵ 打开 · ⇧↵ 在侧边打开 · ⌘↵ 在查找窗口中打开",
|
||||
"⌘↵ to open in Find tool window": "⌘↵ 在“查找”工具窗口中打开",
|
||||
"⌥← / ⌥→ and ⌥⌫ stop at humps of camelCase names.": "⌥← / ⌥→ 和 ⌥⌫ 会在 camelCase 名称的驼峰处停止。",
|
||||
@@ -2733,8 +2693,6 @@
|
||||
"Client key file:": "客户端密钥文件:",
|
||||
"Client Key File": "客户端密钥文件",
|
||||
"schemas (databases)": "架构(数据库)",
|
||||
"Show system schemas and template databases": "显示系统架构和模板数据库",
|
||||
"Show system schemas": "显示系统架构",
|
||||
"Load Names from Server": "从服务器加载名称",
|
||||
"Driver properties": "驱动程序属性",
|
||||
"Property name": "属性名称",
|
||||
@@ -3055,7 +3013,7 @@
|
||||
"Unresolved database references": "未解析的数据库引用",
|
||||
"Reports tables, columns and aliases in query consoles that the data source's schema does not contain.": "报告查询控制台中数据源架构不包含的表、列和别名。",
|
||||
"Proofreading": "校对",
|
||||
"Reports misspelt words in comments, string literals and the names declarations introduce (camelCase and snake_case parts checked separately), and in plain text and Markdown. Words can be saved to the project or application dictionary (Editor › Natural Languages › Spelling).": "报告注释、字符串字面量和声明引入的名称(camelCase 和 snake_case 的各部分分别检查)以及纯文本和 Markdown 中拼写错误的单词。单词可以保存到项目词典或应用程序词典(编辑器 › 自然语言 › 拼写)。",
|
||||
"Reports misspelt words in comments, string literals and the names declarations introduce (camelCase and snake_case parts checked separately), and in plain text and Markdown. Agent nouns of known words (checkpointer) and, in names, two known words run together (filepath) are accepted. Words can be saved to the project or application dictionary (Editor › Natural Languages › Spelling). Correcting a name renames the symbol and its references.": "报告注释、字符串字面量和声明引入的名称(camelCase 和 snake_case 的各部分分别检查)以及纯文本和 Markdown 中拼写错误的单词。已知单词的施动名词(checkpointer)以及名称中连写的两个已知单词(filepath)视为正确。单词可以保存到项目词典或应用程序词典(编辑器 › 自然语言 › 拼写)。更正名称会重命名该符号及其引用。",
|
||||
"English (USA)": "英语(美国)",
|
||||
"Bundled Hunspell dictionary (SCOWL en_US) and common programming terms.": "捆绑的 Hunspell 词典 (SCOWL en_US) 和常用编程术语。",
|
||||
"Used by Proofreading › Typo": "由“校对 › 拼写错误”使用",
|
||||
@@ -3233,9 +3191,7 @@
|
||||
"Prune {target}": "清理{target}",
|
||||
"Prune {target}…": "清理{target}…",
|
||||
"Pruning {target}…": "正在清理 {target}…",
|
||||
"Pruning {target}": "清理 {target}",
|
||||
"Pulling {name}…": "正在拉取 {name}…",
|
||||
"Pulling {name}": "拉取 {name}",
|
||||
"containers": "容器",
|
||||
"images": "镜像",
|
||||
"volumes": "卷",
|
||||
@@ -3281,7 +3237,6 @@
|
||||
"Thinking": "思考过程",
|
||||
"Plan": "计划",
|
||||
"Message": "消息",
|
||||
"The agent is working. You can type your next message.": "Agent 正在处理,可以先写下一条消息。",
|
||||
"Reply to the agent": "回复 agent",
|
||||
"Send": "发送",
|
||||
"vs": "对比",
|
||||
@@ -3289,7 +3244,6 @@
|
||||
"files": "个文件",
|
||||
"No changes yet": "还没有改动",
|
||||
"What should the agent do?": "让 agent 做点什么?",
|
||||
"Describe the task": "描述任务",
|
||||
"New worktree": "新 worktree",
|
||||
"Starting…": "正在启动…",
|
||||
"Waiting for your answer": "等待你的回答",
|
||||
@@ -3375,6 +3329,334 @@
|
||||
"Copied": "已复制",
|
||||
"'{name}' can only be debugged": "'{name}' 只能调试",
|
||||
"It attaches the debugger to a process started elsewhere.": "它将调试器附加到在别处启动的进程。",
|
||||
"Not found, press again to search from the top": "未找到,再按一次从顶部开始搜索",
|
||||
"Cannot change the setting": "无法更改该设置",
|
||||
"Increase Font Size": "增大字体",
|
||||
"Decrease Font Size": "减小字体",
|
||||
"Cannot make {path} writable": "无法将 {path} 设为可写",
|
||||
"Cannot make {path} read-only": "无法将 {path} 设为只读",
|
||||
"Configure Soft Wraps…": "配置软换行…",
|
||||
"Configure Gutter Icons…": "配置装订区域图标…",
|
||||
"line[:column]": "行[:列]",
|
||||
"Edit Source": "编辑源代码",
|
||||
"Show in Documentation Tool Window": "在文档工具窗口中显示",
|
||||
"No documentation": "无文档",
|
||||
"Whole file": "整个文件",
|
||||
"Selected text": "所选文本",
|
||||
"Code Cleanup on `{ path, fixAll?, optimizeImports?, reformat? }` (all steps by default), opened first.": "对 `{ path, fixAll?, optimizeImports?, reformat? }` 执行代码清理(默认执行全部步骤),先打开该文件。",
|
||||
"Shows `{ text, tone? }` as a transient hint at the caret of the active editor.": "在活动编辑器的光标处以临时提示显示 `{ text, tone? }`。",
|
||||
"Inlay Hints Settings…": "嵌入提示设置…",
|
||||
"Preview isn't available for this action": "此操作不支持预览",
|
||||
"No changes in this file": "此文件中没有更改",
|
||||
"{kind}: no language server is running for this file": "{kind}:此文件没有正在运行的语言服务器",
|
||||
"Cannot find declaration of '{label}'": "找不到 '{label}' 的声明",
|
||||
"Rename in place: ↵ to finish, Esc to cancel": "就地重命名:按 ↵ 完成,按 Esc 取消",
|
||||
"Cannot rename safely: {why}. '{word}' was left unchanged": "无法安全重命名:{why}。'{word}' 保持不变",
|
||||
"'{newName}' is not a valid identifier": "'{newName}' 不是有效的标识符",
|
||||
"{note}: renamed {length} occurrence": "{note}:已重命名 {length} 处",
|
||||
"{note}: renamed {length} occurrences": "{note}:已重命名 {length} 处",
|
||||
"No suggestions for '{word}'": "没有 '{word}' 的建议",
|
||||
"Typo: Rename to…": "拼写错误:重命名为…",
|
||||
"Cannot perform refactoring: {why}": "无法执行重构:{why}",
|
||||
"'{chosen}' is not a valid name; kept '{current}'": "'{chosen}' 不是有效的名称;保留 '{current}'",
|
||||
"'{name}' is also used elsewhere in this file": "'{name}' 也在此文件的其他位置使用",
|
||||
"Safe Delete": "安全删除",
|
||||
"Replace all occurrences": "替换所有匹配项",
|
||||
"↵ to finish, Esc to keep the name": "按 ↵ 完成,按 Esc 保留名称",
|
||||
"Agent": "智能体",
|
||||
"Isolation": "隔离方式",
|
||||
"Opens the New Task page; a string `arg` pre-fills the task description.": "打开“新建任务”页面;字符串 `arg` 会预先填入任务描述。",
|
||||
"Adds the editor selection (or the file) as an @ reference to the agent message typed last; a string[] `arg` sends those paths.": "将编辑器选区(或文件)作为 @ 引用添加到最近输入的智能体消息中;string[] 类型的 `arg` 会发送这些路径。",
|
||||
"Files are checked by their language servers; files without one are skipped.": "文件由各自的语言服务器检查;没有语言服务器的文件将被跳过。",
|
||||
"Files are cleaned up by their language servers and saved; files without one are skipped.": "文件由各自的语言服务器清理并保存;没有语言服务器的文件将被跳过。",
|
||||
"Code Cleanup cancelled after {done} of {length} files": "代码清理已在处理 {done}/{length} 个文件后取消",
|
||||
"Code Cleanup processed {done} files": "代码清理已处理 {done} 个文件",
|
||||
"Load Maven Changes": "加载 Maven 更改",
|
||||
"Load Gradle Changes": "加载 Gradle 更改",
|
||||
"recent": "最近",
|
||||
"Shows `{ symbol, locations }` (found elsewhere, e.g. by Safe Delete) in the Find Usages tool window.": "在“查找用法”工具窗口中显示 `{ symbol, locations }`(由其他功能找到,例如安全删除)。",
|
||||
"Code insight features are unavailable for these files until it is installed.": "安装之前,这些文件无法使用代码洞察功能。",
|
||||
"Coverage of '{name}': {totals} lines": "'{name}' 的覆盖率:{totals} 行",
|
||||
"Show a report collected elsewhere (CI): lcov, JaCoCo XML or a Go cover profile; argument: the file path": "显示在其他地方(CI)收集的报告:lcov、JaCoCo XML 或 Go cover profile;参数:文件路径",
|
||||
"Imported coverage '{name}': {total} lines": "已导入覆盖率 '{name}':{total} 行",
|
||||
"The report covers no files of this project.": "该报告不包含此项目的任何文件。",
|
||||
"Cannot import {file}": "无法导入 {file}",
|
||||
"shown": "已显示",
|
||||
"No aggregation": "不聚合",
|
||||
"Sum": "求和",
|
||||
"Mean": "平均值",
|
||||
"Count": "计数",
|
||||
"Min": "最小值",
|
||||
"Max": "最大值",
|
||||
"X axis": "X 轴",
|
||||
"Y axis": "Y 轴",
|
||||
"Aggregation": "聚合",
|
||||
"Chart type": "图表类型",
|
||||
"Bar": "柱状图",
|
||||
"Line": "折线图",
|
||||
"Chart Settings": "图表设置",
|
||||
"No numeric values to plot": "没有可绘制的数值",
|
||||
"Search": "搜索",
|
||||
"Statements": "语句",
|
||||
"No statements": "没有语句",
|
||||
"Statement": "语句",
|
||||
"Comma": "逗号",
|
||||
"Tab": "制表符",
|
||||
"Semicolon": "分号",
|
||||
"Pipe": "竖线",
|
||||
"Import Data from File": "从文件导入数据",
|
||||
"Cannot read the file": "无法读取该文件",
|
||||
"{table}: {n} row(s) imported": "{table}:已导入 {n} 行",
|
||||
"Nothing was imported": "未导入任何内容",
|
||||
"Choose a CSV or TSV file": "选择 CSV 或 TSV 文件",
|
||||
"Choose File…": "选择文件…",
|
||||
"Separator:": "分隔符:",
|
||||
"Separator": "分隔符",
|
||||
"First row is header (columns matched by name)": "第一行为标题(按名称匹配列)",
|
||||
"Treat empty values as NULL": "将空值视为 NULL",
|
||||
"Columns:": "列:",
|
||||
"Export Data…": "导出数据…",
|
||||
"Data extractor": "数据提取器",
|
||||
"Hide Record / Value / Aggregates": "隐藏记录 / 值 / 聚合",
|
||||
"Show Record / Value / Aggregates": "显示记录 / 值 / 聚合",
|
||||
"Clear local filter": "清除本地筛选",
|
||||
"Search values": "搜索值",
|
||||
"No matching values": "没有匹配的值",
|
||||
"Clear Filter": "清除筛选",
|
||||
"Clear Local Filter For All Columns": "清除所有列的本地筛选",
|
||||
"Value panel": "值面板",
|
||||
"Aggregates": "聚合",
|
||||
"Select a cell": "选择一个单元格",
|
||||
"Copy to Clipboard": "复制到剪贴板",
|
||||
"Export to File": "导出到文件",
|
||||
"Source:": "源:",
|
||||
"Extractor:": "提取器:",
|
||||
"Extractor": "提取器",
|
||||
"Transpose": "转置",
|
||||
"Add column header": "添加列标题",
|
||||
"Add row header": "添加行标题",
|
||||
"Output file:": "输出文件:",
|
||||
"Output file": "输出文件",
|
||||
"Export preview:": "导出预览:",
|
||||
"Export preview": "导出预览",
|
||||
"expression": "表达式",
|
||||
"Chart": "图表",
|
||||
"Explain Again": "重新解释",
|
||||
"Operation": "操作",
|
||||
"Params": "参数",
|
||||
"Query plan": "查询计划",
|
||||
"Empty plan": "空计划",
|
||||
"Sent as run-time parameters when connecting (any server setting, e.g. statement_timeout). application_name names the session; connect_timeout is in seconds.": "连接时作为运行时参数发送(任意服务器设置,例如 statement_timeout)。application_name 为会话命名;connect_timeout 以秒为单位。",
|
||||
"Set as session variables after connecting (SET SESSION name = value). charset and collation pick the connection character set; connect_timeout is in seconds.": "连接后设置为会话变量(SET SESSION name = value)。charset 和 collation 选择连接字符集;connect_timeout 以秒为单位。",
|
||||
"Applied as pragmas when opening the file (PRAGMA name = value).": "打开文件时作为 pragma 应用(PRAGMA name = value)。",
|
||||
"sec.": "秒",
|
||||
"default": "默认",
|
||||
"<unchanged>": "<未更改>",
|
||||
"Single database mode (show only the configured database)": "单数据库模式(仅显示配置的数据库)",
|
||||
"databases": "数据库",
|
||||
"schemas": "架构",
|
||||
"Object filter:": "对象筛选:",
|
||||
"e.g. order*, customers (tables, views, routines, sequences)": "例如 order*、customers(表、视图、例程、序列)",
|
||||
"Show internal system schemas": "显示内部系统架构",
|
||||
"Show template databases": "显示模板数据库",
|
||||
"<user defined>": "<用户定义>",
|
||||
"<value>": "<值>",
|
||||
"{table}: {n} row(s) changed": "{table}:已更改 {n} 行",
|
||||
"Import Data from File…": "从文件导入数据…",
|
||||
"Explain Plan": "解释计划",
|
||||
"Browse Query History": "浏览查询历史记录",
|
||||
"<data source>": "<数据源>",
|
||||
"Cannot explain the statement": "无法解释该语句",
|
||||
"Cannot get the definition of {name}": "无法获取 {name} 的定义",
|
||||
"Cannot copy the {what}": "无法复制 {what}",
|
||||
"Script finished: {length} statement": "脚本已完成:{length} 条语句",
|
||||
"Script finished: {length} statements": "脚本已完成:{length} 条语句",
|
||||
"Import from Clipboard": "从剪贴板导入",
|
||||
"No data source on the clipboard": "剪贴板中没有数据源",
|
||||
"Import/Export": "导入/导出",
|
||||
"{what} is not supported by {debugger}": "{debugger} 不支持{what}",
|
||||
"'{name}': listening for the remote JVM on port {port}": "'{name}':正在端口 {port} 上监听远程 JVM",
|
||||
"“{name}” can't be debugged": "无法调试“{name}”",
|
||||
"It has no debug adapter.": "它没有调试适配器。",
|
||||
"Attach the Java debugger to a JVM listening for JDWP (arg: { name, module, hostName, port, process })": "将 Java 调试器附加到监听 JDWP 的 JVM(参数:{ name, module, hostName, port, process })",
|
||||
"'{name}' has compile errors; debugging the last compiled classes": "'{name}' 存在编译错误;将调试上次编译的类",
|
||||
"Attach to Godot": "附加到 Godot",
|
||||
"Debug the game the Godot editor is running, through the editor's debug adapter": "通过 Godot 编辑器的调试适配器调试编辑器正在运行的游戏",
|
||||
"Before attaching": "附加之前",
|
||||
"Cannot start debugger for {name}": "无法为 {name} 启动调试器",
|
||||
"suspended": "已挂起",
|
||||
"e.g. order.total > 100": "例如 order.total > 100",
|
||||
"e.g. 5 or >= 3": "例如 5 或 >= 3",
|
||||
"Value: {expression}": "值:{expression}",
|
||||
"Cannot attach to {name} ({pid})": "无法附加到 {name} ({pid})",
|
||||
"“{name}” can't be watched": "无法监视“{name}”",
|
||||
"(deleted)": "(已删除)",
|
||||
"'{head}' is the default branch": "'{head}' 是默认分支",
|
||||
"Macro '{name}' stopped": "宏 '{name}' 已停止",
|
||||
"Id": "ID",
|
||||
"Enable": "启用",
|
||||
"{name}: the editor is read-only": "{name}:编辑器为只读",
|
||||
"{name}: no editor to insert into": "{name}:没有可插入的编辑器",
|
||||
"The plugin failed to start.": "插件启动失败。",
|
||||
"Move Directory…": "移动目录…",
|
||||
"Cannot move {name}": "无法移动 {name}",
|
||||
"Cannot rename {name}": "无法重命名 {name}",
|
||||
"Store as project file (.openidea/run.toml); otherwise in the workspace (.openidea/run.local.toml)": "存储为项目文件(.openidea/run.toml);否则存储在工作区中(.openidea/run.local.toml)",
|
||||
"This command line doesn't fit the type's form; it is edited as a command.": "此命令行不符合该类型的表单,将作为命令进行编辑。",
|
||||
"Space separated; quote with ' or \"": "以空格分隔;可用 ' 或 \" 加引号",
|
||||
"New configurations of this type start with these values.": "此类型的新配置以这些值开始。",
|
||||
"To run your code, do one of the following:": "要运行代码,请执行以下操作之一:",
|
||||
"The terminal is not available; turn off \"Emulate terminal in output console\" to see the output.": "终端不可用;请关闭“在输出控制台中模拟终端”以查看输出。",
|
||||
"Expected": "预期",
|
||||
"Actual": "实际",
|
||||
"Terminate": "终止",
|
||||
"Cannot run '{name}'": "无法运行 '{name}'",
|
||||
"External tool '{name}' has no program": "外部工具 '{name}' 未设置程序",
|
||||
"'{name}' needs an open file": "'{name}' 需要一个已打开的文件",
|
||||
"Run a configuration (hold Shift to debug); the title bar widget passes \"select\" to only select one": "运行配置(按住 Shift 进行调试);标题栏小部件传入 \"select\" 时仅选择配置",
|
||||
"Runs with a contributed executor (`{ executor, config? }` or `{ executor, context }`): the given or selected configuration, or the context.": "使用贡献的执行器运行(`{ executor, config? }` 或 `{ executor, context }`):给定或所选的配置,或者上下文。",
|
||||
"Saves the given `RunConfig` as a user configuration and opens it in Edit Configurations.": "将给定的 `RunConfig` 保存为用户配置,并在“编辑配置”中打开。",
|
||||
"No sources for this class": "此类没有源代码",
|
||||
"Error running '{name}'": "运行 '{name}' 时出错",
|
||||
"No JDK can run '{name}'": "没有可以运行 '{name}' 的 JDK",
|
||||
"'{name}' exited with code {code}": "'{name}' 已退出,退出代码为 {code}",
|
||||
"Run a Godot project's main scene or a chosen scene": "运行 Godot 项目的主场景或所选场景",
|
||||
"Project directory": "项目目录",
|
||||
"Project root (holds project.godot)": "项目根目录(包含 project.godot)",
|
||||
"Scene": "场景",
|
||||
"Main scene": "主场景",
|
||||
"Godot command line options, e.g. --debug-collisions": "Godot 命令行选项,例如 --debug-collisions",
|
||||
"Bookmark description": "书签描述",
|
||||
"Cannot open {path}": "无法打开 {path}",
|
||||
"Replace All (⇧⌥↵, ⌥A)": "全部替换 (⇧⌥↵, ⌥A)",
|
||||
"Search context": "搜索上下文",
|
||||
"not": "非",
|
||||
"and": "与",
|
||||
"or": "或",
|
||||
"grouping": "分组",
|
||||
"Cannot log in to {name}": "无法登录到 {name}",
|
||||
"Argument: `containers`, `images`, `allImages`, `volumes`, `networks` or `system` (default).": "参数:`containers`、`images`、`allImages`、`volumes`、`networks` 或 `system`(默认)。",
|
||||
"Force (also untag / remove when used by stopped containers)": "强制(被已停止的容器使用时也取消标记 / 移除)",
|
||||
"Start the services of Compose files": "启动 Compose 文件中的服务",
|
||||
"Saved to .openidea/settings.toml in this project; overrides the global value.": "保存到此项目的 .openidea/settings.toml 中;覆盖全局值。",
|
||||
"custom": "自定义",
|
||||
"Resolving the latest build…": "正在解析最新构建…",
|
||||
"Download JDK": "下载 JDK",
|
||||
"Download": "下载",
|
||||
"Vendor:": "供应商:",
|
||||
"Vendor": "供应商",
|
||||
"Version:": "版本:",
|
||||
"Select a version": "选择版本",
|
||||
"JDK Location": "JDK 位置",
|
||||
"Downloading JDK": "正在下载 JDK",
|
||||
"Use ; to separate name patterns, * for any number of symbols, ? for one.": "使用 ; 分隔名称模式,* 匹配任意数量的字符,? 匹配单个字符。",
|
||||
"Folders are detected from build files (pom.xml, build.gradle, Cargo.toml, go.mod, pyproject.toml, package.json); marking or unmarking a folder overrides detection. Excluded folders are skipped by the Project view, Find in Files and the index (reopen the project to re-index).": "文件夹根据构建文件(pom.xml、build.gradle、Cargo.toml、go.mod、pyproject.toml、package.json)检测;标记或取消标记文件夹会覆盖检测结果。项目视图、在文件中查找和索引会跳过已排除的文件夹(重新打开项目以重新索引)。",
|
||||
"Project virtualenv (auto-detect)": "项目 virtualenv(自动检测)",
|
||||
"<No interpreter>": "<无解释器>",
|
||||
"Package name or requirement, e.g. requests or django==5.1": "软件包名称或需求,例如 requests 或 django==5.1",
|
||||
"Download JDK…": "下载 JDK…",
|
||||
"interpreter": "解释器",
|
||||
"Side tool windows take the full height; the bottom ones fit between them": "侧边工具窗口占满整个高度;底部工具窗口位于两者之间",
|
||||
"Days on which files changed; older revisions are removed when a project opens.": "按文件有更改的天数计算;打开项目时会删除更早的修订。",
|
||||
"Language of menus and dialogs; untranslated text stays in English.": "菜单和对话框的语言;未翻译的文本保持英文。",
|
||||
"Unix and macOS (\\n)": "Unix 和 macOS (\\n)",
|
||||
"Classic Mac OS (\\r)": "经典 Mac OS (\\r)",
|
||||
"Encoding of new files; decodes project files that are not valid UTF-8 and have no BOM.": "新文件的编码;也用于解码不是有效 UTF-8 且没有 BOM 的项目文件。",
|
||||
"Stored in the IDE; used by projects without their own scheme.": "存储在 IDE 中;供没有自己方案的项目使用。",
|
||||
"Semantic highlighting": "语义高亮",
|
||||
"Local variables and parameters get a colour derived from their name (needs a language server).": "局部变量和参数使用根据其名称生成的颜色(需要语言服务器)。",
|
||||
"Rainbow brackets": "彩虹括号",
|
||||
"Color (), [] and {} by nesting depth; unmatched brackets use the error color.": "按嵌套深度为 ()、[] 和 {} 着色;不匹配的括号使用错误颜色。",
|
||||
"Main font weight": "主字重",
|
||||
"Bold font weight": "粗体字重",
|
||||
"Used wherever the color scheme shows text in bold": "用于配色方案中所有以粗体显示文本的地方",
|
||||
"Use ; to separate patterns. * matches every file.": "使用 ; 分隔模式。* 匹配所有文件。",
|
||||
"Function and method suggestions get (); the caret goes inside unless there are no parameters.": "函数和方法建议会补全 ();除非没有参数,否则光标置于括号内。",
|
||||
"Insert paired brackets (), [], {}": "插入成对括号 ()、[]、{}",
|
||||
"word": "单词",
|
||||
"Runs of consecutive matching lines collapse into one expandable “<N internal lines>” row.": "连续匹配的多行会折叠为一个可展开的“<N internal lines>”行。",
|
||||
"Confirm closing a tab with a running process": "关闭有正在运行进程的标签页时确认",
|
||||
"value": "值",
|
||||
"Login shell": "登录 Shell",
|
||||
"Terminal column width": "终端列宽",
|
||||
"e.g. /usr/bin/env": "例如 /usr/bin/env",
|
||||
"Force push is not allowed to these branches. Separate patterns with ; (wildcards * and ? are supported).": "不允许强制推送到这些分支。使用 ; 分隔模式(支持通配符 * 和 ?)。",
|
||||
"Adds a Signed-off-by trailer by default; it can still be switched per commit in Commit Options.": "默认添加 Signed-off-by 尾注;仍可在“提交选项”中针对每次提交切换。",
|
||||
"Configure Live Templates…": "配置实时模板…",
|
||||
"Update Tag with Emmet": "使用 Emmet 更新标签",
|
||||
"Created {fileName}": "已创建 {fileName}",
|
||||
"No tag at the caret": "光标处没有标签",
|
||||
"Cannot update the tag": "无法更新标签",
|
||||
"Templates of this project only": "仅限此项目的模板",
|
||||
"none": "无",
|
||||
"Opens a terminal tab running the shell at `arg` (a path from `termAvailableShells`).": "打开一个运行 `arg` 处 Shell 的终端标签页(`termAvailableShells` 中的路径)。",
|
||||
"Opens a terminal tab running `{ program, args, name?, cwd? }` instead of the shell.": "打开一个运行 `{ program, args, name?, cwd? }` 而不是 Shell 的终端标签页。",
|
||||
"TODO items in files of a changelist": "更改列表中文件的 TODO 项",
|
||||
"Too many matches; showing the first 10 000.": "匹配项过多;仅显示前 10 000 个。",
|
||||
"{name} downloaded": "{name} 已下载",
|
||||
"The Java language server restarts on it.": "Java 语言服务器将使用它重新启动。",
|
||||
"Failed to download JDK {version}": "下载 JDK {version} 失败",
|
||||
"Java Settings…": "Java 设置…",
|
||||
"{name} needs JDK {version} to run": "{name} 需要 JDK {version} 才能运行",
|
||||
"Failed to set the path of {command}": "设置 {command} 的路径失败",
|
||||
"Search for branches, actions, repositories": "搜索分支、操作、仓库",
|
||||
"Git Settings": "Git 设置",
|
||||
"Repositories and branches": "仓库和分支",
|
||||
"Name <email>; empty uses the configured Git user": "姓名 <邮箱>;留空则使用已配置的 Git 用户",
|
||||
"Amend and Push…": "修正并推送…",
|
||||
"Select revisions of one file to compare them": "选择同一文件的修订以进行比较",
|
||||
"local": "本地",
|
||||
"remote": "远程",
|
||||
"tag": "标签",
|
||||
"revision": "修订",
|
||||
"{success}, with conflicts": "{success},但存在冲突",
|
||||
"As new branch": "作为新分支",
|
||||
"A rebase is in progress. The commit will become part of the rebased history; usually you continue the rebase instead.": "正在进行变基。此提交将成为变基后历史的一部分;通常应改为继续变基。",
|
||||
"HEAD is detached: the commit won't belong to any branch and may be lost after checking out another branch. Create a branch first to keep it.": "HEAD 处于分离状态:此提交不属于任何分支,签出其他分支后可能会丢失。请先创建分支以保留它。",
|
||||
"Opens the diff viewer for a `DiffData` argument (e.g. a pull request file).": "为 `DiffData` 参数打开差异查看器(例如拉取请求中的文件)。",
|
||||
"From": "从",
|
||||
"To": "到",
|
||||
"Created worktree at {trim}": "已在 {trim} 创建工作树",
|
||||
"No web URL for remote '{remote}'": "远程 '{remote}' 没有 Web URL",
|
||||
"'{rev}' was not found in the log": "在日志中找不到 '{rev}'",
|
||||
"Compare Versions": "比较版本",
|
||||
"Select two commits to compare": "选择两个提交进行比较",
|
||||
"Label '{value}' added to Local History": "已将标签 '{value}' 添加到本地历史记录",
|
||||
"No {noun} in progress": "没有正在进行的 {noun}",
|
||||
"{doneTitle} successful": "{doneTitle}成功",
|
||||
"{file} updated in {commit}": "已在 {commit} 中更新 {file}",
|
||||
"Shelved {length} file": "已搁置 {length} 个文件",
|
||||
"Shelved {length} files": "已搁置 {length} 个文件",
|
||||
"{what}: {length} file with conflicts": "{what}:{length} 个文件存在冲突",
|
||||
"{what}: {length} files with conflicts": "{what}:{length} 个文件存在冲突",
|
||||
"{what}: {n} file changed": "{what}:已更改 {n} 个文件",
|
||||
"{what}: {n} files changed": "{what}:已更改 {n} 个文件",
|
||||
"Built-in Preview": "内置预览",
|
||||
"Cannot open the browser": "无法打开浏览器",
|
||||
"Browser": "浏览器",
|
||||
"Default Browser": "默认浏览器",
|
||||
"Open the HTML or SVG file in the default browser through the built-in server": "通过内置服务器在默认浏览器中打开 HTML 或 SVG 文件",
|
||||
"Show the HTML file rendered beside the editor": "在编辑器旁显示 HTML 文件的渲染结果",
|
||||
"Project folder not found: {name}": "找不到项目文件夹:{name}",
|
||||
"Cannot open {name}": "无法打开 {name}",
|
||||
"Cannot reopen {name}": "无法重新打开 {name}",
|
||||
"Edit inspection profile setting": "编辑检查配置文件设置",
|
||||
"Disable inspection": "禁用检查",
|
||||
"Change Severity": "更改严重性",
|
||||
"Edit Inspection Profile Setting…": "编辑检查配置文件设置…",
|
||||
"Compiler errors and warnings of the last Build Project, shown at their source positions.": "上次“构建项目”产生的编译器错误和警告,显示在对应的源代码位置。",
|
||||
"Process code": "处理代码",
|
||||
"Process literals": "处理字面量",
|
||||
"Process comments": "处理注释",
|
||||
"Distraction Free Mode": "无干扰模式",
|
||||
"Exit Distraction Free Mode": "退出无干扰模式",
|
||||
"Show Hidden Files": "显示隐藏文件",
|
||||
"Show Excluded Files": "显示已排除的文件",
|
||||
"Compact Middle Packages": "压缩中间软件包",
|
||||
"Sort Folders First": "文件夹排在前面",
|
||||
"Open Files with Single Click": "单击打开文件",
|
||||
"Expand Directories with Single Click": "单击展开目录",
|
||||
"Enable Preview Tab": "启用预览标签页",
|
||||
"Analyze Dependencies…": "分析依赖…",
|
||||
"Shows a Maven module's dependency tree, version conflicts and where each library comes from": "显示 Maven 模块的依赖树、版本冲突以及每个库的引入路径",
|
||||
"Dependency Analyzer": "依赖分析器",
|
||||
@@ -3399,6 +3681,5 @@
|
||||
"omitted for cycle": "因循环依赖而省略",
|
||||
"version managed from {version}": "版本由 {version} 经依赖管理调整",
|
||||
"Cannot find the declaration": "找不到声明",
|
||||
"Cannot exclude {name}": "无法排除 {name}",
|
||||
"Load Maven Changes": "加载 Maven 更改"
|
||||
"Cannot exclude {name}": "无法排除 {name}"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,445 @@
|
||||
// @vitest-environment node
|
||||
/**
|
||||
* Keeps the source-text catalogs (`locales/text/<locale>.json`) in step with the
|
||||
* English text written at call sites. Their keys are the English text itself, so
|
||||
* rewording a string at its call site silently drops its translation; these tests
|
||||
* fail instead, and also on catalog entries whose text no longer exists.
|
||||
*
|
||||
* Text counts as translatable when it is a literal (or a branch of `?:` / `??` /
|
||||
* `||` around one) that reaches the translation layer directly:
|
||||
* - the first argument of `tx` / `txTemplate`, or of a function bound from
|
||||
* `useTx()` / `useTxTemplate()` in the same file;
|
||||
* - the plain-text children of `<T>` and the other `TEXT_CHILDREN` primitives;
|
||||
* - a `TEXT_PROPS` attribute of a component (`<Row hint="…">`), and `data-tooltip`;
|
||||
* - a `TEXT_PROPS` property of an object literal (`{ label: "…" }`): actions, menu
|
||||
* items, select options, dialogs, notifications, settings pages, tool windows;
|
||||
* - the text arguments of `showHint` and `showError`.
|
||||
* Template literals (`Cannot open ${path}`) are checked only where the text is
|
||||
* matched against `{name}` templates (`txTemplate`, hints, notification titles);
|
||||
* elsewhere the lookup is exact and they cannot be translated as written.
|
||||
*
|
||||
* Not seen: text kept in a variable or built by a helper before it reaches `tx`,
|
||||
* and text sent by the backend. Their entries count as used (not stale) as long as
|
||||
* the text appears as a literal anywhere in `src/`, the Rust crates or `languages/`.
|
||||
*/
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import ts from "typescript";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const ROOT = fileURLToPath(new URL("../../../", import.meta.url));
|
||||
const TEXT_DIR = new URL("./text/", import.meta.url);
|
||||
|
||||
/** Props / object keys whose string value the UI passes through `tx`. */
|
||||
const TEXT_PROPS = new Set([
|
||||
"aria-label",
|
||||
"body",
|
||||
"cancelLabel",
|
||||
"category",
|
||||
"checkbox",
|
||||
"description",
|
||||
"detail",
|
||||
"emptyLabel",
|
||||
"hint",
|
||||
"label",
|
||||
"message",
|
||||
"ok",
|
||||
"okLabel",
|
||||
"placeholder",
|
||||
"text",
|
||||
"title",
|
||||
"tooltip",
|
||||
]);
|
||||
|
||||
/** Components whose plain-text children are translated. */
|
||||
const TEXT_CHILDREN = new Set(["T", "Button", "Dialog"]);
|
||||
|
||||
const HINTS = new Set(["showHint"]);
|
||||
const ERRORS = new Set(["showError"]);
|
||||
const NOTIFY = new Set(["notify"]);
|
||||
|
||||
/** Code templates shown as written, and the in-memory backends of plain-browser development. */
|
||||
const UNCHECKED_FILES = new Set([
|
||||
"src/editor/core/backend.ts",
|
||||
"src/features/plugins/backend.ts",
|
||||
"src/features/tools/backend.ts",
|
||||
"src/features/templates/files/builtins.ts",
|
||||
"src/features/templates/live/builtins.ts",
|
||||
"src/features/templates/live/surrounders.ts",
|
||||
"src/features/templates/postfix/model.ts",
|
||||
]);
|
||||
|
||||
/** Entries for text the code assembles from parts, which no literal spells out. */
|
||||
const BUILT_AT_RUNTIME = new Set([
|
||||
"Unused Containers", // `Unused ${group}` in services/ops.ts
|
||||
]);
|
||||
|
||||
/** Text that stays as written in every locale: names, code, formats, shortcuts. */
|
||||
const UNTRANSLATED = new Set<string>([
|
||||
// Products, tools and formats
|
||||
"Amazon Aurora MySQL",
|
||||
"Amazon Redshift",
|
||||
"Cargo Check",
|
||||
"Clippy",
|
||||
"CockroachDB",
|
||||
"CodeLLDB",
|
||||
"CodeLLDB (Rust, C, C++)",
|
||||
"CSV",
|
||||
"debugpy",
|
||||
"debugpy (Python)",
|
||||
"Delve",
|
||||
"Delve (Go)",
|
||||
"Docker",
|
||||
"Docker Compose",
|
||||
"Dockerfile",
|
||||
"Eclipse Temurin",
|
||||
"EditorConfig",
|
||||
"Git",
|
||||
"GitHub",
|
||||
"Go",
|
||||
"Godot",
|
||||
"Gradle",
|
||||
"Greenplum",
|
||||
"Java",
|
||||
"java-debug (Java)",
|
||||
"js-debug (Node.js / TypeScript)",
|
||||
"JSON",
|
||||
"lldb-dap (C, C++, Swift)",
|
||||
"MariaDB",
|
||||
"Markdown",
|
||||
"Maven",
|
||||
"MySQL",
|
||||
"Neon",
|
||||
"Node.js",
|
||||
"npm",
|
||||
"OpenIDEA",
|
||||
"PostgreSQL",
|
||||
"Rust",
|
||||
"Rustfmt",
|
||||
"SingleStore",
|
||||
"SQLite",
|
||||
"Supabase",
|
||||
"TiDB",
|
||||
"TimescaleDB",
|
||||
"TSV",
|
||||
"TypeScript",
|
||||
"Vim",
|
||||
"YugabyteDB",
|
||||
// Key names, shortcuts and terms that read the same in Chinese UIs
|
||||
"<null>",
|
||||
"Backspace",
|
||||
"Code Vision",
|
||||
"Enter",
|
||||
"Fixup",
|
||||
"Fixup…",
|
||||
"IDE",
|
||||
"mod tests",
|
||||
"Windows (\\r\\n)",
|
||||
// Language names, written in their own language
|
||||
"English",
|
||||
"简体中文 (Chinese, Simplified)",
|
||||
// Terms, keywords and acronyms kept in English
|
||||
"DDL",
|
||||
"GOPATH",
|
||||
"GOROOT",
|
||||
"HEAD",
|
||||
"JDK",
|
||||
"JDK 1.4.x",
|
||||
"JDK 5 - 8",
|
||||
"LTS",
|
||||
"NULL",
|
||||
"ORDER BY",
|
||||
"SDK",
|
||||
"SSH/SSL",
|
||||
"SSL",
|
||||
"TODO",
|
||||
"URL",
|
||||
"VCS",
|
||||
"WHERE",
|
||||
"WIP",
|
||||
// Code, commands and example values
|
||||
'"value"',
|
||||
"at org.example.",
|
||||
"bastion.example.com",
|
||||
"build --info",
|
||||
"className()",
|
||||
"clean install -DskipTests",
|
||||
"com.example.Service#process, main.run, parse",
|
||||
"docker-compose.yml; docker-compose.override.yml",
|
||||
"file:src//*&&!file:*test*",
|
||||
"git pull --no-rebase --ff",
|
||||
"git pull --rebase",
|
||||
"go env GOPATH",
|
||||
"kebab-case",
|
||||
"KEY=value; OTHER=value",
|
||||
"NAME",
|
||||
"node_modules/typescript",
|
||||
"PascalCase",
|
||||
"snake_case",
|
||||
// Identifiers that only look like text: icon names, error codes, categories, console output
|
||||
"app",
|
||||
"cancelled",
|
||||
"fileTypes/text",
|
||||
"info",
|
||||
"languageServer",
|
||||
"process",
|
||||
"read-only",
|
||||
"Running processes requires the desktop app.\n",
|
||||
"text",
|
||||
]);
|
||||
|
||||
interface Use {
|
||||
text: string;
|
||||
at: string;
|
||||
template: boolean;
|
||||
}
|
||||
|
||||
function walk(dir: string, accept: (path: string) => boolean, out: string[] = []): string[] {
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
const path = `${dir}/${entry.name}`;
|
||||
if (entry.isDirectory()) {
|
||||
if (entry.name !== "node_modules" && entry.name !== "target") walk(path, accept, out);
|
||||
} else if (accept(path)) out.push(path);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const isTest = (path: string) => /\.test\.tsx?$|\/__tests__\/|\/src\/test\//.test(path);
|
||||
|
||||
const ENTITIES: Record<string, string> = { amp: "&", apos: "'", quot: '"', lt: "<", gt: ">", nbsp: "\u00a0" };
|
||||
|
||||
function decodeEntities(s: string): string {
|
||||
return s.replace(/&(#x[0-9a-f]+|#\d+|\w+);/gi, (m, e: string) => {
|
||||
if (e[0] === "#")
|
||||
return String.fromCodePoint(e[1] === "x" || e[1] === "X" ? parseInt(e.slice(2), 16) : Number(e.slice(1)));
|
||||
return ENTITIES[e] ?? m;
|
||||
});
|
||||
}
|
||||
|
||||
/** JSX text as React renders it: lines trimmed, blank ones dropped, the rest joined with a space. */
|
||||
function jsxText(raw: string): string {
|
||||
const lines = raw.split(/\r?\n/);
|
||||
if (lines.length === 1) return decodeEntities(raw).trim();
|
||||
return decodeEntities(
|
||||
lines
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean)
|
||||
.join(" "),
|
||||
);
|
||||
}
|
||||
|
||||
const unwrap = (e: ts.Expression): ts.Expression => (ts.isParenthesizedExpression(e) ? unwrap(e.expression) : e);
|
||||
|
||||
/**
|
||||
* Template literal text with each `${expr}` as `{name}`; a `${c ? "a" : "b"}` part
|
||||
* yields one text per branch (`Formatted {n} line` / `Formatted {n} lines`).
|
||||
*/
|
||||
function templateTexts(node: ts.TemplateExpression): string[] {
|
||||
let out = [node.head.text];
|
||||
for (const span of node.templateSpans) {
|
||||
const e = unwrap(span.expression);
|
||||
let parts: string[];
|
||||
if (
|
||||
ts.isConditionalExpression(e) &&
|
||||
ts.isStringLiteralLike(unwrap(e.whenTrue)) &&
|
||||
ts.isStringLiteralLike(unwrap(e.whenFalse))
|
||||
)
|
||||
parts = [(unwrap(e.whenTrue) as ts.StringLiteralLike).text, (unwrap(e.whenFalse) as ts.StringLiteralLike).text];
|
||||
else {
|
||||
const words = span.expression.getText().match(/[A-Za-z_]\w*/g) ?? ["value"];
|
||||
parts = [`{${words[words.length - 1]}}`];
|
||||
}
|
||||
out = out.flatMap((head) => parts.map((part) => head + part + span.literal.text));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** `{anything}` → `{}`, so templates compare regardless of placeholder names. */
|
||||
const shape = (text: string) => text.replace(/\{[^{}]*\}/g, "{}");
|
||||
|
||||
const tagName = (node: ts.JsxOpeningLikeElement) => node.tagName.getText();
|
||||
|
||||
const isComponent = (tag: string) => /^[A-Z]/.test(tag) || tag.includes(".");
|
||||
|
||||
/** Climbs through expressions that pass a string operand through unchanged. */
|
||||
function passThrough(node: ts.Node): ts.Node {
|
||||
let n = node;
|
||||
for (;;) {
|
||||
const p = n.parent;
|
||||
if (
|
||||
p &&
|
||||
(ts.isParenthesizedExpression(p) ||
|
||||
ts.isAsExpression(p) ||
|
||||
ts.isSatisfiesExpression(p) ||
|
||||
(ts.isConditionalExpression(p) && p.condition !== n) ||
|
||||
(ts.isBinaryExpression(p) &&
|
||||
(p.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken ||
|
||||
p.operatorToken.kind === ts.SyntaxKind.BarBarToken ||
|
||||
(p.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken && p.right === n))))
|
||||
) {
|
||||
n = p;
|
||||
continue;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
}
|
||||
|
||||
function propName(name: ts.PropertyName | ts.JsxAttributeName): string | undefined {
|
||||
return ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : undefined;
|
||||
}
|
||||
|
||||
type Sink = "exact" | "template";
|
||||
|
||||
const isCallTo = (call: ts.CallExpression, names: Set<string>) => {
|
||||
const callee = call.expression;
|
||||
const name = ts.isIdentifier(callee) ? callee.text : ts.isPropertyAccessExpression(callee) ? callee.name.text : "";
|
||||
return names.has(name);
|
||||
};
|
||||
|
||||
/**
|
||||
* How the text at `node` is translated: `exact` (catalog lookup of the whole
|
||||
* text), `template` (also matches `{name}` templates, see `txTemplate`), or not at all.
|
||||
*/
|
||||
function sinkOf(node: ts.Node, exact: Set<string>, template: Set<string>): Sink | undefined {
|
||||
const top = passThrough(node);
|
||||
const p = top.parent;
|
||||
if (!p) return undefined;
|
||||
if (ts.isCallExpression(p)) {
|
||||
const i = p.arguments.indexOf(top as ts.Expression);
|
||||
if (i === 0 && isCallTo(p, template)) return "template";
|
||||
if (i === 0 && isCallTo(p, exact)) return "exact";
|
||||
if (i === 1 && isCallTo(p, HINTS)) return "template";
|
||||
if (i >= 0 && i <= 1 && isCallTo(p, ERRORS)) return "exact";
|
||||
return undefined;
|
||||
}
|
||||
if (ts.isPropertyAssignment(p)) {
|
||||
const name = propName(p.name) ?? "";
|
||||
if (p.initializer !== top || !TEXT_PROPS.has(name)) return undefined;
|
||||
const call = p.parent.parent;
|
||||
// Notification titles go through `txTemplate`.
|
||||
if (name === "title" && ts.isCallExpression(call) && isCallTo(call, NOTIFY)) return "template";
|
||||
return "exact";
|
||||
}
|
||||
let attr: ts.Node = p;
|
||||
if (ts.isJsxExpression(p)) {
|
||||
if (ts.isJsxElement(p.parent)) return TEXT_CHILDREN.has(tagName(p.parent.openingElement)) ? "exact" : undefined;
|
||||
attr = p.parent;
|
||||
}
|
||||
if (ts.isJsxAttribute(attr)) {
|
||||
const name = propName(attr.name) ?? "";
|
||||
if (name === "data-tooltip") return "exact";
|
||||
return TEXT_PROPS.has(name) && isComponent(tagName(attr.parent.parent)) ? "exact" : undefined;
|
||||
}
|
||||
if (ts.isJsxElement(p)) return TEXT_CHILDREN.has(tagName(p.openingElement)) ? "exact" : undefined;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
interface Scan {
|
||||
/** Literals that reach the translation layer. */
|
||||
translated: Use[];
|
||||
/** Every string literal and JSX text, in shape form. */
|
||||
literals: Set<string>;
|
||||
}
|
||||
|
||||
function scanTypeScript(): Scan {
|
||||
const translated: Use[] = [];
|
||||
const literals = new Set<string>();
|
||||
const files = walk(`${ROOT}src`, (p) => /\.tsx?$/.test(p) && !isTest(p) && !p.endsWith("/bindings.ts"));
|
||||
for (const file of files) {
|
||||
const path = file.slice(ROOT.length);
|
||||
const source = readFileSync(file, "utf8");
|
||||
const sf = ts.createSourceFile(
|
||||
file,
|
||||
source,
|
||||
ts.ScriptTarget.Latest,
|
||||
true,
|
||||
file.endsWith("x") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
||||
);
|
||||
const exact = new Set(["tx"]);
|
||||
const template = new Set(["txTemplate"]);
|
||||
for (const m of source.matchAll(/(?:const|let)\s+(\w+)\s*=\s*useTx(Template)?\(\)/g))
|
||||
(m[2] ? template : exact).add(m[1]!);
|
||||
const checked = !UNCHECKED_FILES.has(path);
|
||||
const visit = (node: ts.Node) => {
|
||||
let texts: string[] | undefined;
|
||||
if (ts.isStringLiteral(node) && ts.isJsxAttribute(node.parent)) texts = [decodeEntities(node.text)];
|
||||
else if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) texts = [node.text];
|
||||
else if (ts.isTemplateExpression(node)) texts = templateTexts(node);
|
||||
else if (ts.isJsxText(node)) texts = [jsxText(node.text)].filter(Boolean);
|
||||
if (texts) {
|
||||
for (const text of texts) {
|
||||
literals.add(shape(text));
|
||||
literals.add(shape(text.trim()));
|
||||
}
|
||||
const sink = checked ? sinkOf(node, exact, template) : undefined;
|
||||
// A template literal only matches a catalog entry where templates are looked up.
|
||||
if (sink && (sink === "template" || !ts.isTemplateExpression(node))) {
|
||||
const at = `${path}:${sf.getLineAndCharacterOfPosition(node.getStart()).line + 1}`;
|
||||
for (const text of texts) translated.push({ text, at, template: ts.isTemplateExpression(node) });
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
};
|
||||
visit(sf);
|
||||
}
|
||||
return { translated, literals };
|
||||
}
|
||||
|
||||
/** String literals of the Rust crates and language definitions (backend messages). */
|
||||
function scanBackend(literals: Set<string>) {
|
||||
const files = [
|
||||
...walk(`${ROOT}crates`, (p) => p.endsWith(".rs")),
|
||||
...walk(`${ROOT}src-tauri/src`, (p) => p.endsWith(".rs")),
|
||||
...walk(`${ROOT}languages`, (p) => p.endsWith(".toml")),
|
||||
];
|
||||
for (const file of files)
|
||||
for (const m of readFileSync(file, "utf8").matchAll(/"((?:[^"\\]|\\.)*)"/g)) literals.add(shape(m[1]!));
|
||||
}
|
||||
|
||||
/** Text that needs no translation: no words, a path / glob / option, or listed in `UNTRANSLATED`. */
|
||||
function needsTranslation(text: string): boolean {
|
||||
if (UNTRANSLATED.has(text)) return false;
|
||||
const words = text.replace(/\{[^{}]*\}/g, "");
|
||||
if (!/[A-Za-z]{2}/.test(words)) return false;
|
||||
return !/^[-.*~/$#@]|:\/\//.test(words);
|
||||
}
|
||||
|
||||
const scan = scanTypeScript();
|
||||
scanBackend(scan.literals);
|
||||
|
||||
const catalogs = Object.fromEntries(
|
||||
readdirSync(TEXT_DIR)
|
||||
.filter((f) => f.endsWith(".json"))
|
||||
.map((f) => [f.slice(0, -5), JSON.parse(readFileSync(new URL(f, TEXT_DIR), "utf8")) as Record<string, string>]),
|
||||
);
|
||||
|
||||
/** `{name}` templates of a catalog, as `txTemplate` matches them. */
|
||||
function templatePatterns(catalog: Record<string, string>): RegExp[] {
|
||||
return Object.keys(catalog)
|
||||
.filter((key) => /\{\w+\}/.test(key))
|
||||
.map((key) => {
|
||||
const parts = key.split(/\{\w+\}/).map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
||||
return new RegExp(`^${parts.join("(.+?)")}$`, "s");
|
||||
});
|
||||
}
|
||||
|
||||
describe.each(Object.keys(catalogs))("source-text catalog %s", (locale) => {
|
||||
const catalog = catalogs[locale]!;
|
||||
const patterns = templatePatterns(catalog);
|
||||
const has = (text: string) => Object.hasOwn(catalog, text);
|
||||
// Placeholders of the source stand for any text, as the runtime values would.
|
||||
const matchesTemplate = (text: string) => patterns.some((p) => p.test(text.replace(/\{[^{}]*\}/g, "\u0001")));
|
||||
|
||||
it("translates every literal UI text", () => {
|
||||
const missing = new Map<string, string>();
|
||||
for (const { text, at, template } of scan.translated)
|
||||
if (!has(text) && !(template && matchesTemplate(text)) && needsTranslation(text) && !missing.has(text))
|
||||
missing.set(text, at);
|
||||
expect([...missing].map(([text, at]) => `${at} ${JSON.stringify(text)}`)).toEqual([]);
|
||||
});
|
||||
|
||||
it("has no entries whose text no longer appears in the source", () => {
|
||||
const stale = Object.keys(catalog).filter((key) => !scan.literals.has(shape(key)) && !BUILT_AT_RUNTIME.has(key));
|
||||
expect(stale).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { rememberStartupTheme, STARTUP_THEME_KEY, windowBackgroundColor, windowBackgroundHex } from "./theme";
|
||||
|
||||
const mockFrame = (token: string, resolved = "") =>
|
||||
vi
|
||||
.spyOn(globalThis, "getComputedStyle")
|
||||
.mockReturnValue({ getPropertyValue: () => token, color: resolved } as unknown as CSSStyleDeclaration);
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
describe("window background", () => {
|
||||
it("resolves the frame to Tauri's RGBA tuple instead of passing CSS hsl() or mixes", () => {
|
||||
mockFrame("#26282c");
|
||||
expect(windowBackgroundColor()).toEqual([38, 40, 44, 255]);
|
||||
mockFrame("hsl(230, 1%, 92%)", "rgb(234, 234, 235)");
|
||||
expect(windowBackgroundColor()).toEqual([234, 234, 235, 255]);
|
||||
expect(windowBackgroundHex()).toBe("#eaeaeb");
|
||||
mockFrame("color-mix(in srgb, #8040bf 80%, transparent)", "color(srgb 0.5 0.25 0.75 / 0.8)");
|
||||
expect(windowBackgroundColor()).toEqual([128, 64, 191, 204]);
|
||||
expect(windowBackgroundHex()).toBe("#8040bfcc");
|
||||
mockFrame("", "");
|
||||
expect(windowBackgroundColor()).toBeNull();
|
||||
expect(windowBackgroundHex()).toBeNull();
|
||||
});
|
||||
|
||||
it("remembers the base and the resolved splash colours for the next window", () => {
|
||||
const root = document.documentElement;
|
||||
root.dataset.theme = "light";
|
||||
const values: Record<string, string> = { "--bg-frame": "#eee8d5", "--fg-muted": "#657b83", "--accent": "#268bd2" };
|
||||
vi.spyOn(globalThis, "getComputedStyle").mockReturnValue({
|
||||
getPropertyValue: (n: string) => values[n] ?? "",
|
||||
} as unknown as CSSStyleDeclaration);
|
||||
rememberStartupTheme(root);
|
||||
expect(JSON.parse(localStorage.getItem(STARTUP_THEME_KEY)!)).toEqual({
|
||||
theme: "light",
|
||||
frame: "#eee8d5",
|
||||
fg: "#657b83",
|
||||
accent: "#268bd2",
|
||||
});
|
||||
});
|
||||
});
|
||||
+54
-9
@@ -4,6 +4,7 @@ import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||
import { create } from "zustand";
|
||||
import { call, commands, isTauri } from "@/platform/ipc";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { resolveTokenColor, toHex } from "@/platform/cssColor";
|
||||
|
||||
export type ThemeSetting = "dark" | "light" | "system" | "dark-hc";
|
||||
/** PyCharm's brand blue: the accent both Islands themes ship with. */
|
||||
@@ -64,7 +65,6 @@ export function useApplyTheme() {
|
||||
const id = override
|
||||
? resolveThemeId({ theme: override }, systemLight)
|
||||
: resolveThemeId(appearance as AppearanceLike | undefined, systemLight);
|
||||
const theme = id === "light" ? "light" : "dark";
|
||||
const accent = appearance?.accent;
|
||||
|
||||
// Until settings arrive, keep the theme public/boot.js restored rather than the light default.
|
||||
@@ -77,13 +77,8 @@ export function useApplyTheme() {
|
||||
const custom = !!accent && ACCENT_RE.test(accent) && !THEME_ACCENTS.has(accent.toLowerCase());
|
||||
if (custom) root.style.setProperty("--accent", accent);
|
||||
else root.style.removeProperty("--accent");
|
||||
try {
|
||||
localStorage.setItem("openidea.theme", JSON.stringify({ theme, contrast: root.dataset.contrast }));
|
||||
} catch {
|
||||
/* storage unavailable */
|
||||
}
|
||||
syncWindowBackground();
|
||||
}, [pending, theme, id, accent]);
|
||||
}, [pending, id, accent]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -102,14 +97,64 @@ export function applyThemeAttributes(id: "dark" | "light" | "dark-hc", root: HTM
|
||||
else delete root.dataset.contrast;
|
||||
}
|
||||
|
||||
/**
|
||||
* The frame colour as Tauri's `[r, g, b, a]` (0–255). Tauri parses neither
|
||||
* `hsl()` nor `color-mix()`, which themes may use, so it is resolved here.
|
||||
*/
|
||||
export function windowBackgroundColor(
|
||||
root: HTMLElement = document.documentElement,
|
||||
): [number, number, number, number] | null {
|
||||
const c = resolveTokenColor("--bg-frame", root);
|
||||
if (!c) return null;
|
||||
const [r, g, b, a] = c;
|
||||
return [Math.round(r), Math.round(g), Math.round(b), Math.round(a * 255)];
|
||||
}
|
||||
|
||||
/** The frame colour as `#rrggbb[aa]`, for native window commands that take a string. */
|
||||
export function windowBackgroundHex(root: HTMLElement = document.documentElement): string | null {
|
||||
const c = resolveTokenColor("--bg-frame", root);
|
||||
return c ? toHex(c) : null;
|
||||
}
|
||||
|
||||
/** localStorage key read by public/boot.js. */
|
||||
export const STARTUP_THEME_KEY = "openidea.theme";
|
||||
|
||||
/**
|
||||
* Remembers the theme for public/boot.js, which applies it to the next window
|
||||
* before any stylesheet loads: the light / dark base, and the resolved frame,
|
||||
* text and accent colours its splash is painted in.
|
||||
*/
|
||||
export function rememberStartupTheme(root: HTMLElement = document.documentElement) {
|
||||
const hex = (name: string) => {
|
||||
const c = resolveTokenColor(name, root);
|
||||
return c ? toHex(c) : undefined;
|
||||
};
|
||||
try {
|
||||
localStorage.setItem(
|
||||
STARTUP_THEME_KEY,
|
||||
JSON.stringify({
|
||||
theme: root.dataset.theme,
|
||||
contrast: root.dataset.contrast,
|
||||
frame: hex("--bg-frame"),
|
||||
fg: hex("--fg-muted"),
|
||||
accent: hex("--accent"),
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
/* storage unavailable */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Where a live resize outruns the page, the native window and the webview (WebView2's default
|
||||
* background) show their own colours; match both to the frame. The webview one is only set at
|
||||
* window creation otherwise, so a theme switched later left it light.
|
||||
* window creation otherwise, so a theme switched later left it light. Also remembers the theme
|
||||
* for the next window's start-up.
|
||||
*/
|
||||
export function syncWindowBackground() {
|
||||
rememberStartupTheme();
|
||||
if (!isTauri()) return;
|
||||
const color = getComputedStyle(document.documentElement).getPropertyValue("--bg-frame").trim();
|
||||
const color = windowBackgroundColor();
|
||||
if (!color) return;
|
||||
getCurrentWebviewWindow()
|
||||
.setBackgroundColor(color)
|
||||
|
||||
Reference in New Issue
Block a user