mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-13 04:34:21 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32bfbc3a78 |
No files matched your search
@@ -194,6 +194,19 @@ where
|
||||
Ok(serde_json::from_value(serde_json::Value::deserialize(d)?).unwrap_or_default())
|
||||
}
|
||||
|
||||
/// A theme id, with the built-in themes that were removed read as the Islands theme of their kind.
|
||||
fn theme_id<'de, D>(d: D) -> Result<String, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let id = String::deserialize(d)?;
|
||||
Ok(match id.as_str() {
|
||||
"one-light" => "light".into(),
|
||||
"one-dark" => "dark".into(),
|
||||
_ => id,
|
||||
})
|
||||
}
|
||||
|
||||
/// [`AgentSettings::defaults`], keeping only the text values of each agent's table.
|
||||
fn agent_defaults<'de, D>(d: D) -> Result<BTreeMap<String, BTreeMap<String, String>>, D::Error>
|
||||
where
|
||||
@@ -557,13 +570,19 @@ pub enum NavigationBarPosition {
|
||||
pub struct AppearanceSettings {
|
||||
/// A built-in theme id (`light`, `dark`, `dark-hc`, …), `system` (legacy
|
||||
/// "Sync with OS"), or a plugin's theme: `plugin:<plugin-id>/<theme-id>`.
|
||||
#[serde(deserialize_with = "theme_id")]
|
||||
#[specta(type = String)]
|
||||
pub theme: String,
|
||||
pub accent: String,
|
||||
pub ui_font_size: f32,
|
||||
pub locale: String,
|
||||
/// Follow the OS light/dark mode with the preferred themes below.
|
||||
pub sync_with_os: bool,
|
||||
#[serde(deserialize_with = "theme_id")]
|
||||
#[specta(type = String)]
|
||||
pub preferred_light_theme: String,
|
||||
#[serde(deserialize_with = "theme_id")]
|
||||
#[specta(type = String)]
|
||||
pub preferred_dark_theme: String,
|
||||
/// A plugin's file icon theme (`<plugin-id>/<theme-id>`); empty = the built-in icons.
|
||||
pub file_icon_theme: String,
|
||||
@@ -1424,6 +1443,19 @@ impl Settings {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn removed_one_themes_read_as_islands_themes() {
|
||||
let settings: Settings = serde_json::from_str(
|
||||
r#"{"appearance":{"theme":"one-dark","preferredLightTheme":"one-light","preferredDarkTheme":"one-dark"}}"#,
|
||||
)
|
||||
.unwrap();
|
||||
let a = settings.appearance;
|
||||
assert_eq!((a.theme.as_str(), a.preferred_light_theme.as_str()), ("dark", "light"));
|
||||
assert_eq!(a.preferred_dark_theme, "dark");
|
||||
let kept: Settings = serde_json::from_str(r#"{"appearance":{"theme":"solarized-dark"}}"#).unwrap();
|
||||
assert_eq!(kept.appearance.theme, "solarized-dark");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_close_settings_default_off_and_round_trip() {
|
||||
for old in [r#"{}"#, r#"{"general":{"confirmExit":false}}"#] {
|
||||
|
||||
@@ -11,7 +11,6 @@ Open **Settings › Appearance & Behavior › Appearance** and pick a **Theme**.
|
||||
| ----------------------- | -------------- |
|
||||
| Islands Light (default) | Islands Dark |
|
||||
| Solarized Light | Solarized Dark |
|
||||
| One Light | One Dark |
|
||||
| | High Contrast |
|
||||
|
||||
Themes from installed plugins appear in the same list, under the plugin's name.
|
||||
|
||||
@@ -3,9 +3,8 @@ title: "Themes"
|
||||
description: "Write a colour theme plugin: the theme and editor scheme files, how themes are applied, importing IntelliJ themes, porting VS Code themes, and the full token reference."
|
||||
---
|
||||
|
||||
OpenIDEA ships seven built-in themes: Islands Light, Islands Dark, High Contrast,
|
||||
Solarized Light, Solarized Dark, One Light and One Dark. Other themes come from **theme
|
||||
plugins**.
|
||||
OpenIDEA ships five built-in themes: Islands Light, Islands Dark, High Contrast,
|
||||
Solarized Light and Solarized Dark. Other themes come from **theme plugins**.
|
||||
|
||||
A theme plugin is data only. Like an IntelliJ theme (a `.theme.json` plus an optional
|
||||
`.icls` editor scheme), it has no code: no WebAssembly, nothing to compile, no
|
||||
|
||||
@@ -94,25 +94,25 @@ Settings › Appearance & Behavior › System Settings, unless noted.
|
||||
|
||||
Settings › Appearance & Behavior › Appearance, unless noted.
|
||||
|
||||
| Key | Default | Description |
|
||||
| -------------------------------------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `theme` | `"light"` | `light` (Islands Light), `dark` (Islands Dark), `dark-hc` (High Contrast), `solarized-light`, `solarized-dark`, `one-light`, `one-dark`, or a plugin theme as `plugin:<plugin-id>/<theme-id>`. |
|
||||
| `syncWithOs` | `false` | **Sync with OS**. |
|
||||
| `preferredLightTheme` / `preferredDarkTheme` | `"light"` / `"dark"` | Themes used with **Sync with OS**. |
|
||||
| `accent` | `"#3871e1"` | **Accent color**. |
|
||||
| `fileIconTheme` | `""` | A plugin's file icon theme as `<plugin-id>/<theme-id>`; empty uses the built-in icons. |
|
||||
| `zoom` | `100` | **Zoom**, in percent (50 to 300). |
|
||||
| `useCustomFont` | `false` | **Use custom font:** apply `uiFontFamily` and `uiFontSize`. |
|
||||
| `uiFontFamily` / `uiFontSize` | `"Inter"` / `13` | UI font. |
|
||||
| `uiFontFallback` | `""` | **UI fallback font**. |
|
||||
| `contrastScrollbars` | `false` | **Use contrast scrollbars**. |
|
||||
| `redGreenVision` | `false` | **Adjust colors for red-green vision deficiency**. |
|
||||
| `compactMode` | `false` | **Compact mode**. |
|
||||
| `showMainToolbar` / `showStatusBar` | `true` / `true` | **Show main toolbar**, **Show status bar**. |
|
||||
| `navigationBar` | `"statusBar"` | **Navigation bar**: `top`, `statusBar` or `hidden`. |
|
||||
| `mainMenuStyle` | `"hamburger"` | **Main menu**: `hamburger` or `merged` (with the main toolbar). |
|
||||
| `ideAntialiasing` / `editorAntialiasing` | `"greyscale"` | `subpixel`, `greyscale` or `none`. |
|
||||
| `locale` | `"en"` | **Language** (Language and Region page): `en`, `zh`, `ko`, `fr`, `pl`, `pt`, `es`. |
|
||||
| Key | Default | Description |
|
||||
| -------------------------------------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `theme` | `"light"` | `light` (Islands Light), `dark` (Islands Dark), `dark-hc` (High Contrast), `solarized-light`, `solarized-dark`, or a plugin theme as `plugin:<plugin-id>/<theme-id>`. |
|
||||
| `syncWithOs` | `false` | **Sync with OS**. |
|
||||
| `preferredLightTheme` / `preferredDarkTheme` | `"light"` / `"dark"` | Themes used with **Sync with OS**. |
|
||||
| `accent` | `"#3871e1"` | **Accent color**. |
|
||||
| `fileIconTheme` | `""` | A plugin's file icon theme as `<plugin-id>/<theme-id>`; empty uses the built-in icons. |
|
||||
| `zoom` | `100` | **Zoom**, in percent (50 to 300). |
|
||||
| `useCustomFont` | `false` | **Use custom font:** apply `uiFontFamily` and `uiFontSize`. |
|
||||
| `uiFontFamily` / `uiFontSize` | `"Inter"` / `13` | UI font. |
|
||||
| `uiFontFallback` | `""` | **UI fallback font**. |
|
||||
| `contrastScrollbars` | `false` | **Use contrast scrollbars**. |
|
||||
| `redGreenVision` | `false` | **Adjust colors for red-green vision deficiency**. |
|
||||
| `compactMode` | `false` | **Compact mode**. |
|
||||
| `showMainToolbar` / `showStatusBar` | `true` / `true` | **Show main toolbar**, **Show status bar**. |
|
||||
| `navigationBar` | `"statusBar"` | **Navigation bar**: `top`, `statusBar` or `hidden`. |
|
||||
| `mainMenuStyle` | `"hamburger"` | **Main menu**: `hamburger` or `merged` (with the main toolbar). |
|
||||
| `ideAntialiasing` / `editorAntialiasing` | `"greyscale"` | `subpixel`, `greyscale` or `none`. |
|
||||
| `locale` | `"en"` | **Language** (Language and Region page): `en`, `zh`, `ko`, `fr`, `pl`, `pt`, `es`. |
|
||||
|
||||
### `[editor]`
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ description: "选择界面主题和编辑器配色方案,导入 IntelliJ 主
|
||||
| -------------------- | -------------- |
|
||||
| Islands 浅色(默认) | Islands 深色 |
|
||||
| Solarized 浅色 | Solarized 深色 |
|
||||
| One 浅色 | One 深色 |
|
||||
| | 高对比度 |
|
||||
|
||||
已安装插件提供的主题也会出现在这个列表中,归在插件名下。
|
||||
|
||||
@@ -3,7 +3,7 @@ title: "主题"
|
||||
description: "编写配色主题插件:主题文件与编辑器配色方案文件、主题如何生效、导入 IntelliJ 主题、移植 VS Code 主题,以及完整的 token 参考。"
|
||||
---
|
||||
|
||||
OpenIDEA 内置七个主题:Islands 浅色、Islands 深色、高对比度、Solarized 浅色、Solarized 深色、One 浅色和 One 深色。
|
||||
OpenIDEA 内置五个主题:Islands 浅色、Islands 深色、高对比度、Solarized 浅色和 Solarized 深色。
|
||||
其他主题都来自 **主题插件**。
|
||||
|
||||
主题插件是纯数据插件。和 IntelliJ 主题(一个 `.theme.json` 加一个可选的 `.icls` 编辑器配色方案)一样,它没有代码:
|
||||
|
||||
@@ -94,25 +94,25 @@ exclude = ["**/.git", "**/node_modules", "**/target", "**/dist"]
|
||||
|
||||
除特别说明外,对应 设置 › 外观与行为 › 外观。
|
||||
|
||||
| 键 | 默认值 | 说明 |
|
||||
| -------------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `theme` | `"light"` | `light`(Islands 浅色)、`dark`(Islands 深色)、`dark-hc`(高对比度)、`solarized-light`、`solarized-dark`、`one-light`、`one-dark`,或以 `plugin:<plugin-id>/<theme-id>` 表示的插件主题。 |
|
||||
| `syncWithOs` | `false` | **与操作系统同步**。 |
|
||||
| `preferredLightTheme` / `preferredDarkTheme` | `"light"` / `"dark"` | 与操作系统同步时使用的主题。 |
|
||||
| `accent` | `"#3871e1"` | **强调色**。 |
|
||||
| `fileIconTheme` | `""` | 插件提供的文件图标主题,写作 `<plugin-id>/<theme-id>`;为空时使用内置图标。 |
|
||||
| `zoom` | `100` | **缩放**,百分比(50 到 300)。 |
|
||||
| `useCustomFont` | `false` | **使用自定义字体:**,启用 `uiFontFamily` 和 `uiFontSize`。 |
|
||||
| `uiFontFamily` / `uiFontSize` | `"Inter"` / `13` | 界面字体。 |
|
||||
| `uiFontFallback` | `""` | **UI 备用字体**。 |
|
||||
| `contrastScrollbars` | `false` | **使用对比度滚动条**。 |
|
||||
| `redGreenVision` | `false` | **针对红绿色觉缺陷调整颜色**。 |
|
||||
| `compactMode` | `false` | **紧凑模式**。 |
|
||||
| `showMainToolbar` / `showStatusBar` | `true` / `true` | **显示主工具栏**、**显示状态栏**。 |
|
||||
| `navigationBar` | `"statusBar"` | **导航栏**:`top`、`statusBar` 或 `hidden`。 |
|
||||
| `mainMenuStyle` | `"hamburger"` | **主菜单**:`hamburger`(汉堡按钮)或 `merged`(与主工具栏合并)。 |
|
||||
| `ideAntialiasing` / `editorAntialiasing` | `"greyscale"` | `subpixel`、`greyscale` 或 `none`。 |
|
||||
| `locale` | `"en"` | **语言**(语言和地区页):`en`、`zh`、`ko`、`fr`、`pl`、`pt`、`es`。 |
|
||||
| 键 | 默认值 | 说明 |
|
||||
| -------------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `theme` | `"light"` | `light`(Islands 浅色)、`dark`(Islands 深色)、`dark-hc`(高对比度)、`solarized-light`、`solarized-dark`,或以 `plugin:<plugin-id>/<theme-id>` 表示的插件主题。 |
|
||||
| `syncWithOs` | `false` | **与操作系统同步**。 |
|
||||
| `preferredLightTheme` / `preferredDarkTheme` | `"light"` / `"dark"` | 与操作系统同步时使用的主题。 |
|
||||
| `accent` | `"#3871e1"` | **强调色**。 |
|
||||
| `fileIconTheme` | `""` | 插件提供的文件图标主题,写作 `<plugin-id>/<theme-id>`;为空时使用内置图标。 |
|
||||
| `zoom` | `100` | **缩放**,百分比(50 到 300)。 |
|
||||
| `useCustomFont` | `false` | **使用自定义字体:**,启用 `uiFontFamily` 和 `uiFontSize`。 |
|
||||
| `uiFontFamily` / `uiFontSize` | `"Inter"` / `13` | 界面字体。 |
|
||||
| `uiFontFallback` | `""` | **UI 备用字体**。 |
|
||||
| `contrastScrollbars` | `false` | **使用对比度滚动条**。 |
|
||||
| `redGreenVision` | `false` | **针对红绿色觉缺陷调整颜色**。 |
|
||||
| `compactMode` | `false` | **紧凑模式**。 |
|
||||
| `showMainToolbar` / `showStatusBar` | `true` / `true` | **显示主工具栏**、**显示状态栏**。 |
|
||||
| `navigationBar` | `"statusBar"` | **导航栏**:`top`、`statusBar` 或 `hidden`。 |
|
||||
| `mainMenuStyle` | `"hamburger"` | **主菜单**:`hamburger`(汉堡按钮)或 `merged`(与主工具栏合并)。 |
|
||||
| `ideAntialiasing` / `editorAntialiasing` | `"greyscale"` | `subpixel`、`greyscale` 或 `none`。 |
|
||||
| `locale` | `"en"` | **语言**(语言和地区页):`en`、`zh`、`ko`、`fr`、`pl`、`pt`、`es`。 |
|
||||
|
||||
### `[editor]`
|
||||
|
||||
|
||||
@@ -51,12 +51,12 @@ test("stripe tooltips stay inside the viewport with a gap on either side and at
|
||||
|
||||
test("stripe themes inherit base tooltip tokens and keep shortcut text readable", async ({ workbench: page }) => {
|
||||
const button = page.locator('[data-tooltip-stripe="left"][data-tooltip-shortcut]').first();
|
||||
for (const theme of ["dark", "light", "dark-hc", "solarized-light", "solarized-dark", "one-light", "one-dark"]) {
|
||||
for (const theme of ["dark", "light", "dark-hc", "solarized-light", "solarized-dark"]) {
|
||||
await page.mouse.move(700, 400);
|
||||
await page.evaluate((theme) => {
|
||||
const root = document.documentElement;
|
||||
root.dataset.theme = theme.endsWith("light") ? "light" : theme === "dark-hc" ? "dark-hc" : "dark";
|
||||
if (theme.includes("-light") || ["solarized-dark", "one-dark"].includes(theme)) root.dataset.colorTheme = theme;
|
||||
if (theme.includes("-light") || theme === "solarized-dark") root.dataset.colorTheme = theme;
|
||||
else delete root.dataset.colorTheme;
|
||||
}, theme);
|
||||
await button.hover();
|
||||
|
||||
+13
-45
@@ -20,22 +20,6 @@ const palettes = [
|
||||
fg: "rgb(131, 148, 150)",
|
||||
frame: [7, 54, 66, 255],
|
||||
},
|
||||
{
|
||||
id: "one-light",
|
||||
label: "One Light",
|
||||
kind: "light",
|
||||
bg: "rgb(250, 250, 250)",
|
||||
fg: "rgb(56, 58, 66)",
|
||||
frame: [234, 234, 235, 255],
|
||||
},
|
||||
{
|
||||
id: "one-dark",
|
||||
label: "One Dark",
|
||||
kind: "dark",
|
||||
bg: "rgb(40, 44, 52)",
|
||||
fg: "rgb(171, 178, 191)",
|
||||
frame: [33, 37, 43, 255],
|
||||
},
|
||||
];
|
||||
|
||||
async function choose(page: Page, control: string, label: string) {
|
||||
@@ -100,15 +84,7 @@ for (const palette of palettes) {
|
||||
return activeSession().term.options.theme.background;
|
||||
}),
|
||||
)
|
||||
.toBe(
|
||||
palette.id === "solarized-light"
|
||||
? "#fdf6e3"
|
||||
: palette.id === "solarized-dark"
|
||||
? "#002b36"
|
||||
: palette.id === "one-light"
|
||||
? "#fafafa"
|
||||
: "#282c34",
|
||||
);
|
||||
.toBe(palette.id === "solarized-light" ? "#fdf6e3" : "#002b36");
|
||||
await editor.locator(".cm-content").click();
|
||||
await page.keyboard.press("Meta+a");
|
||||
await expect(editor.locator(".cm-selectionBackground:visible").first()).toBeVisible();
|
||||
@@ -135,15 +111,7 @@ for (const palette of palettes) {
|
||||
test("same-kind switching, legacy themes, system mode and explicit editor schemes", async ({ workbench: page }) => {
|
||||
await openFromTree(page, "build.gradle.kts");
|
||||
const dialog = await settings(page);
|
||||
for (const label of [
|
||||
"Solarized Light",
|
||||
"One Light",
|
||||
"Solarized Dark",
|
||||
"One Dark",
|
||||
"Islands Light",
|
||||
"Islands Dark",
|
||||
"High Contrast",
|
||||
]) {
|
||||
for (const label of ["Solarized Light", "Solarized Dark", "Islands Light", "Islands Dark", "High Contrast"]) {
|
||||
await choose(page, "Theme", label);
|
||||
await dialog.getByRole("button", { name: "Apply", exact: true }).click();
|
||||
if (label === "High Contrast") await expect(page.locator("html")).toHaveAttribute("data-contrast", "high");
|
||||
@@ -160,7 +128,7 @@ test("same-kind switching, legacy themes, system mode and explicit editor scheme
|
||||
: "rgb(0, 0, 0)");
|
||||
await expect(page.locator(".cm-editor").first()).toHaveCSS("background-color", bg);
|
||||
}
|
||||
await choose(page, "Theme", "One Light");
|
||||
await choose(page, "Theme", "Solarized Light");
|
||||
await choose(page, "Editor color scheme", "Solarized Dark");
|
||||
await dialog.getByRole("button", { name: "Apply", exact: true }).click();
|
||||
await expect(page.locator(".cm-editor").first()).toHaveCSS("background-color", "rgb(0, 43, 54)");
|
||||
@@ -168,16 +136,16 @@ test("same-kind switching, legacy themes, system mode and explicit editor scheme
|
||||
await dialog.getByLabel("Sync with OS", { exact: true }).check();
|
||||
await dialog.getByRole("button", { name: "Preferred themes", exact: true }).click();
|
||||
await choose(page, "Preferred light theme", "Solarized Light");
|
||||
await choose(page, "Preferred dark theme", "One Dark");
|
||||
await choose(page, "Preferred dark theme", "Solarized Dark");
|
||||
await page.keyboard.press("Escape");
|
||||
await dialog.getByRole("button", { name: "Apply", exact: true }).click();
|
||||
await page.emulateMedia({ colorScheme: "light" });
|
||||
await expect(page.locator("html")).toHaveAttribute("data-color-theme", "solarized-light");
|
||||
await page.emulateMedia({ colorScheme: "dark" });
|
||||
await expect(page.locator("html")).toHaveAttribute("data-color-theme", "one-dark");
|
||||
await expect(page.locator("html")).toHaveAttribute("data-color-theme", "solarized-dark");
|
||||
});
|
||||
|
||||
test("quick theme menu offers all four palettes and applies their kind", async ({ workbench: page }) => {
|
||||
test("quick theme menu offers both palettes and applies their kind", async ({ workbench: page }) => {
|
||||
for (const palette of palettes) {
|
||||
await page.getByRole("button", { name: "IDE and Project Settings", exact: true }).click();
|
||||
await page.getByText("Theme", { exact: true }).click();
|
||||
@@ -206,14 +174,14 @@ test("gear › Theme previews the theme under the pointer and keeps the one chos
|
||||
|
||||
await gear.click();
|
||||
await page.getByRole("menuitem", { name: "Theme", exact: true }).hover();
|
||||
await theme("One Dark").hover();
|
||||
await expect(html).toHaveAttribute("data-color-theme", "one-dark");
|
||||
await theme("One Dark").click();
|
||||
await expect(html).toHaveAttribute("data-color-theme", "one-dark");
|
||||
await theme("Solarized Dark").hover();
|
||||
await expect(html).toHaveAttribute("data-color-theme", "solarized-dark");
|
||||
await theme("Solarized Dark").click();
|
||||
await expect(html).toHaveAttribute("data-color-theme", "solarized-dark");
|
||||
// Chosen, not previewed: the next start-up opens with it.
|
||||
await expect
|
||||
.poll(() => page.evaluate(() => JSON.parse(localStorage.getItem("openidea.theme") ?? "{}").colorTheme))
|
||||
.toBe("one-dark");
|
||||
.toBe("solarized-dark");
|
||||
});
|
||||
|
||||
test("Settings › Appearance › Theme previews the highlighted theme until Cancel", async ({ workbench: page }) => {
|
||||
@@ -226,8 +194,8 @@ test("Settings › Appearance › Theme previews the highlighted theme until Can
|
||||
await expect(html).toHaveAttribute("data-theme", "light");
|
||||
await expect(html).not.toHaveAttribute("data-color-theme");
|
||||
// Picked: it shows until Apply or Cancel.
|
||||
await choose(page, "Theme", "One Dark");
|
||||
await expect(html).toHaveAttribute("data-color-theme", "one-dark");
|
||||
await choose(page, "Theme", "Solarized Dark");
|
||||
await expect(html).toHaveAttribute("data-color-theme", "solarized-dark");
|
||||
await dialog.getByRole("button", { name: "Cancel", exact: true }).click();
|
||||
await expect(html).toHaveAttribute("data-theme", "light");
|
||||
await expect(html).not.toHaveAttribute("data-color-theme");
|
||||
|
||||
@@ -133,8 +133,6 @@ mod tests {
|
||||
("dark-hc", Color(0x1a, 0x1b, 0x1e, 0xff)),
|
||||
("solarized-light", Color(0xee, 0xe8, 0xd5, 0xff)),
|
||||
("solarized-dark", Color(0x07, 0x36, 0x42, 0xff)),
|
||||
("one-light", Color(0xea, 0xea, 0xeb, 0xff)),
|
||||
("one-dark", Color(0x21, 0x25, 0x2b, 0xff)),
|
||||
] {
|
||||
assert_eq!(builtin_frame(id), Some(color), "{id}");
|
||||
}
|
||||
|
||||
@@ -1803,12 +1803,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn additional_palettes_keep_native_startup_and_os_preferences_in_sync() {
|
||||
for (id, color) in [
|
||||
("solarized-light", Color(0xee, 0xe8, 0xd5, 0xff)),
|
||||
("solarized-dark", Color(0x07, 0x36, 0x42, 0xff)),
|
||||
("one-light", Color(0xea, 0xea, 0xeb, 0xff)),
|
||||
("one-dark", Color(0x21, 0x25, 0x2b, 0xff)),
|
||||
] {
|
||||
for (id, color) in
|
||||
[("solarized-light", Color(0xee, 0xe8, 0xd5, 0xff)), ("solarized-dark", Color(0x07, 0x36, 0x42, 0xff))]
|
||||
{
|
||||
let mut a = AppearanceSettings { theme: id.into(), ..Default::default() };
|
||||
assert_eq!(frame_color(&a, false), color);
|
||||
assert_eq!(frame_color(&a, true), color);
|
||||
|
||||
+67
-3
@@ -2269,7 +2269,9 @@ export type AppMetrics = {
|
||||
memory: ProcessMemory[],
|
||||
};
|
||||
|
||||
export type AppearanceSettings = {
|
||||
export type AppearanceSettings = AppearanceSettings_Serialize | AppearanceSettings_Deserialize;
|
||||
|
||||
export type AppearanceSettings_Deserialize = {
|
||||
/**
|
||||
* A built-in theme id (`light`, `dark`, `dark-hc`, …), `system` (legacy
|
||||
* "Sync with OS"), or a plugin's theme: `plugin:<plugin-id>/<theme-id>`.
|
||||
@@ -2331,6 +2333,68 @@ export type AppearanceSettings = {
|
||||
mainToolbar?: MainToolbarSettings | null,
|
||||
};
|
||||
|
||||
export type AppearanceSettings_Serialize = {
|
||||
/**
|
||||
* A built-in theme id (`light`, `dark`, `dark-hc`, …), `system` (legacy
|
||||
* "Sync with OS"), or a plugin's theme: `plugin:<plugin-id>/<theme-id>`.
|
||||
*/
|
||||
theme: string,
|
||||
accent: string,
|
||||
uiFontSize: number | null,
|
||||
locale: string,
|
||||
/** Follow the OS light/dark mode with the preferred themes below. */
|
||||
syncWithOs: boolean,
|
||||
preferredLightTheme: string,
|
||||
preferredDarkTheme: string,
|
||||
/** A plugin's file icon theme (`<plugin-id>/<theme-id>`); empty = the built-in icons. */
|
||||
fileIconTheme: string,
|
||||
fileIconThemeOptions: { [key in string]: FileIconThemeOptions },
|
||||
/** IDE zoom in percent (50–300). */
|
||||
zoom: number,
|
||||
/** Apply `ui_font_family` / `ui_font_size` (otherwise the theme's). */
|
||||
useCustomFont: boolean,
|
||||
uiFontFamily: string,
|
||||
/** Font tried when `ui_font_family` lacks a glyph; empty = none. */
|
||||
uiFontFallback: string,
|
||||
contrastScrollbars: boolean,
|
||||
/** Adjust colours for red-green vision deficiency. */
|
||||
redGreenVision: boolean,
|
||||
differentToolWindowBackground: boolean,
|
||||
compactMode: boolean,
|
||||
/** IDEA "Use smaller indents in trees": tool window trees indent less per level. */
|
||||
smallTreeIndents: boolean,
|
||||
fullPathInTitle: boolean,
|
||||
projectColorsInToolbar: boolean,
|
||||
dndWithAltOnly: boolean,
|
||||
menuIcons: boolean,
|
||||
/** `hamburger` | `merged` | `toolbar` (a separate row): main menu presentation in the frameless title bar. */
|
||||
mainMenuStyle: string,
|
||||
/** A lone Alt press focuses or opens the main menu (IDEA `ide.windows.main.menu.focus.on.alt`). */
|
||||
mainMenuAlt: boolean,
|
||||
/** `subpixel` | `greyscale` | `none` */
|
||||
ideAntialiasing: string,
|
||||
editorAntialiasing: string,
|
||||
showToolWindowBars: boolean,
|
||||
showToolWindowNames: boolean,
|
||||
showToolWindowBadges: boolean,
|
||||
widescreenToolWindows: boolean,
|
||||
/** View › Appearance › Toolbar; hidden leaves IDEA's compact window header. */
|
||||
showMainToolbar: boolean,
|
||||
showStatusBar: boolean,
|
||||
/**
|
||||
* View › Appearance › Navigation Bar: the file path breadcrumbs, shown in
|
||||
* the status bar by default, above the editor, or not at all.
|
||||
*/
|
||||
navigationBar: NavigationBarPosition,
|
||||
/**
|
||||
* Status bar widgets shown (`true`) or hidden (`false`) by the user
|
||||
* (status bar context menu); widgets not listed use their default.
|
||||
*/
|
||||
statusBarWidgets: { [key in string]: boolean },
|
||||
/** Customize Main Toolbar; `None` = the default toolbar. */
|
||||
mainToolbar: MainToolbarSettings | null,
|
||||
};
|
||||
|
||||
export type ApplyOutcome = {
|
||||
/** Repository-relative paths changed without conflicts. */
|
||||
applied: string[],
|
||||
@@ -9067,7 +9131,7 @@ export type SettingsScope = "global" | "project";
|
||||
|
||||
export type Settings_Deserialize = {
|
||||
general?: GeneralSettings,
|
||||
appearance?: AppearanceSettings,
|
||||
appearance?: AppearanceSettings_Deserialize,
|
||||
editor?: EditorSettings_Deserialize,
|
||||
keymap?: string,
|
||||
files?: FileSettings,
|
||||
@@ -9131,7 +9195,7 @@ export type Settings_Deserialize = {
|
||||
|
||||
export type Settings_Serialize = {
|
||||
general: GeneralSettings,
|
||||
appearance: AppearanceSettings,
|
||||
appearance: AppearanceSettings_Serialize,
|
||||
editor: EditorSettings_Serialize,
|
||||
keymap: string,
|
||||
files: FileSettings,
|
||||
|
||||
@@ -320,6 +320,25 @@ describe("store", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("leaves a theme another window selected alone until this window's settings change", async () => {
|
||||
const stop = [syncProjection(), followThemeSetting()];
|
||||
try {
|
||||
useProject.setState({ settings: { appearance: { theme: "plugin:theme-harbor-night/harbor-night" } } as never });
|
||||
await refreshPlugins();
|
||||
await waitFor(() => expect(usePlugins.getState().activeTheme?.theme.id).toBe("harbor-night"));
|
||||
const setTheme = vi.spyOn(backend, "setTheme");
|
||||
// Another window picked a built-in theme; this window's settings still name the plugin's.
|
||||
act(() => usePlugins.setState({ activeTheme: null }));
|
||||
await Promise.resolve();
|
||||
expect(setTheme).not.toHaveBeenCalled();
|
||||
act(() => useProject.setState({ settings: { appearance: { theme: "dark" } } as never }));
|
||||
expect(setTheme).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
stop.forEach((s) => s());
|
||||
useProject.setState({ settings: null as never });
|
||||
}
|
||||
});
|
||||
|
||||
it("installs from a folder, its plugin.toml, or a package file unpacked first", async () => {
|
||||
setConsentPrompt(async () => ({ optional: [] }));
|
||||
for (const [picked, unpacked] of [
|
||||
|
||||
@@ -150,4 +150,14 @@ describe("plugin theme at start-up", () => {
|
||||
expect(setFrame).toHaveBeenCalledTimes(2);
|
||||
stop();
|
||||
});
|
||||
|
||||
it("keeps the stylesheet when the same theme is applied again", () => {
|
||||
applyPluginTheme(ONE_DARK);
|
||||
const style = document.getElementById(PLUGIN_THEME_STYLE_ID);
|
||||
applyPluginTheme(structuredClone(ONE_DARK));
|
||||
expect(document.getElementById(PLUGIN_THEME_STYLE_ID)).toBe(style);
|
||||
applyPluginTheme(SOLARIZED_LIGHT);
|
||||
expect(document.getElementById(PLUGIN_THEME_STYLE_ID)).not.toBe(style);
|
||||
expect(document.documentElement.dataset.pluginTheme).toBe("theme-solarized/solarized-light");
|
||||
});
|
||||
});
|
||||
@@ -104,7 +104,12 @@ export function pluginThemeCss(theme: ActiveTheme["theme"]): string {
|
||||
*/
|
||||
export function applyPluginTheme(theme: ActiveTheme | null, root: HTMLElement = document.documentElement) {
|
||||
const doc = root.ownerDocument;
|
||||
doc.getElementById(PLUGIN_THEME_STYLE_ID)?.remove();
|
||||
const current = doc.getElementById(PLUGIN_THEME_STYLE_ID);
|
||||
const id = theme && `${theme.pluginId}/${theme.theme.id}`;
|
||||
const css = theme && pluginThemeCss(theme.theme);
|
||||
// The same theme again (each window hears every selection): replacing its stylesheet restyles the page.
|
||||
if (theme && current?.textContent === css && root.dataset.pluginTheme === id) return;
|
||||
current?.remove();
|
||||
const had = root.dataset.pluginTheme !== undefined;
|
||||
if (!theme) {
|
||||
delete root.dataset.pluginTheme;
|
||||
@@ -113,9 +118,9 @@ export function applyPluginTheme(theme: ActiveTheme | null, root: HTMLElement =
|
||||
} else {
|
||||
const style = doc.createElement("style");
|
||||
style.id = PLUGIN_THEME_STYLE_ID;
|
||||
style.textContent = pluginThemeCss(theme.theme);
|
||||
style.textContent = css;
|
||||
doc.head.appendChild(style);
|
||||
root.dataset.pluginTheme = `${theme.pluginId}/${theme.theme.id}`;
|
||||
root.dataset.pluginTheme = id!;
|
||||
root.dataset.pluginThemeKind = theme.theme.kind === "light" ? "light" : "dark";
|
||||
applyThemeAttributes(currentThemeId(), root);
|
||||
}
|
||||
@@ -240,8 +245,10 @@ export function followThemeSetting(): () => void {
|
||||
};
|
||||
reconcile();
|
||||
const disposers = [
|
||||
// Not on the host's theme changing: another window selects it before this one's settings
|
||||
// say so, and selecting this window's choice back would flip it until they agree.
|
||||
usePlugins.subscribe((s, prev) => {
|
||||
if (s.loaded !== prev.loaded || s.activeTheme !== prev.activeTheme) reconcile();
|
||||
if (s.loaded !== prev.loaded) reconcile();
|
||||
}),
|
||||
useProject.subscribe((s, prev) => {
|
||||
if (s.settings?.appearance !== prev.settings?.appearance) reconcile();
|
||||
|
||||
@@ -144,7 +144,7 @@ describe("palette themes", () => {
|
||||
it("have a tokens.css block for every palette theme id, and no other", () => {
|
||||
const blocks = [...css.matchAll(/:root\[data-color-theme="([a-z0-9-]+)"\]/g)].map((m) => m[1]);
|
||||
expect([...new Set(blocks)].sort()).toEqual(palettes.map((t) => t.id).sort());
|
||||
expect(palettes.length).toBeGreaterThanOrEqual(4);
|
||||
expect(palettes.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it.each(palettes)("$id defines every token or inherits it from its $kind base on purpose", ({ id, kind }) => {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { act, fireEvent, render, screen, within } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { applyThemeAttributes, currentThemeId, useThemeOverride } from "@/workbench/theme";
|
||||
import { THEME_PREVIEW_DELAY } from "@/workbench/quickSwitch";
|
||||
import { DEFAULT_SETTINGS } from "../model/schema";
|
||||
import { openSettings, resetPage } from "../model/staging";
|
||||
import { PageContext } from "../components/context";
|
||||
@@ -49,25 +48,21 @@ describe("Settings › Appearance › Theme preview", () => {
|
||||
expect(shown()).toEqual(["dark", "solarized-dark"]);
|
||||
});
|
||||
|
||||
it("goes back to the selected theme when it is picked again before the hover preview shows", () => {
|
||||
it("goes back to the selected theme when it is picked again", () => {
|
||||
renderThemeRow();
|
||||
expect(shown()).toEqual(["light", null]);
|
||||
pick("Solarized Dark");
|
||||
expect(shown()).toEqual(["dark", "solarized-dark"]);
|
||||
// Clicked quicker than the hover delay: the list itself never previewed anything.
|
||||
act(() => vi.advanceTimersByTime(THEME_PREVIEW_DELAY / 2));
|
||||
pick("Islands Light");
|
||||
act(() => vi.advanceTimersByTime(THEME_PREVIEW_DELAY));
|
||||
expect(screen.getByRole("button", { name: "Theme" })).toHaveTextContent("Islands Light");
|
||||
expect(shown()).toEqual(["light", null]);
|
||||
});
|
||||
|
||||
it("goes back to the selected theme after hovering it long enough", () => {
|
||||
it("goes back to the selected theme after hovering it", () => {
|
||||
renderThemeRow();
|
||||
pick("Solarized Dark");
|
||||
fireEvent.click(screen.getByRole("button", { name: "Theme" }));
|
||||
fireEvent.pointerEnter(within(screen.getByRole("listbox")).getByText("Islands Light"));
|
||||
act(() => vi.advanceTimersByTime(THEME_PREVIEW_DELAY));
|
||||
fireEvent.click(within(screen.getByRole("listbox")).getByText("Islands Light"));
|
||||
expect(shown()).toEqual(["light", null]);
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import type { ITheme, Terminal } from "@xterm/xterm";
|
||||
import { fontWeightOf } from "@/platform/editorSettings";
|
||||
import { familyFaces, fontsLoaded, monoStack, resolveMonoFont } from "@/platform/fonts";
|
||||
import { parseColor, type Rgba } from "@/platform/cssColor";
|
||||
import { computedTokenColor, parseColor, type Rgba } from "@/platform/cssColor";
|
||||
|
||||
export { parseColor, type Rgba };
|
||||
|
||||
@@ -133,12 +133,7 @@ export function documentTokenReader(): TokenReader {
|
||||
return (name) => {
|
||||
const raw = style.getPropertyValue(name).trim();
|
||||
if (!raw || parseColor(raw)) return raw;
|
||||
const probe = Object.assign(document.createElement("span"), { hidden: true });
|
||||
probe.style.color = `var(${name})`;
|
||||
root.appendChild(probe);
|
||||
const resolved = getComputedStyle(probe).color;
|
||||
probe.remove();
|
||||
return resolved;
|
||||
return computedTokenColor(name, root);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -264,7 +264,7 @@ describe("mermaid in the markdown preview", () => {
|
||||
const changed = vi.fn();
|
||||
const off = subscribeTheme(changed);
|
||||
try {
|
||||
root.dataset.colorTheme = "one-dark";
|
||||
root.dataset.colorTheme = "solarized-light";
|
||||
const firstKey = themeKey();
|
||||
await renderMermaidIn(preview(SEQ)).done;
|
||||
root.dataset.colorTheme = "solarized-dark";
|
||||
@@ -380,7 +380,8 @@ describe("mermaid in the markdown preview", () => {
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(document.querySelector("span[hidden]")).toBeNull();
|
||||
// One probe element serves every token.
|
||||
expect(document.querySelectorAll("span[hidden]")).toHaveLength(1);
|
||||
} finally {
|
||||
computed.mockRestore();
|
||||
}
|
||||
@@ -488,7 +489,7 @@ describe("mermaid in the markdown preview", () => {
|
||||
const html = document.documentElement;
|
||||
const previous = html.dataset.colorTheme;
|
||||
try {
|
||||
html.dataset.colorTheme = "one-dark";
|
||||
html.dataset.colorTheme = "solarized-light";
|
||||
const root = preview(SEQ);
|
||||
await renderMermaidIn(root).done;
|
||||
root.querySelector<HTMLButtonElement>('[data-mermaid-tools] button[aria-label="Full screen"]')!.click();
|
||||
@@ -510,7 +511,7 @@ describe("mermaid in the markdown preview", () => {
|
||||
expect(pan.style.transform).toBe(zoomed);
|
||||
|
||||
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));
|
||||
html.dataset.colorTheme = "one-dark";
|
||||
html.dataset.colorTheme = "solarized-light";
|
||||
await renderMermaidIn(root).done;
|
||||
expect(document.querySelector("[data-mermaid-preview]")).toBeNull();
|
||||
} finally {
|
||||
|
||||
@@ -246,7 +246,7 @@ describe("color schemes", () => {
|
||||
});
|
||||
|
||||
it("switches a chosen scheme of the other kind back to the theme's own when the theme turns light or dark", async () => {
|
||||
expect(schemeAfterThemeChange("one-dark", "light")).toBe("theme-default");
|
||||
expect(schemeAfterThemeChange("solarized-dark", "light")).toBe("theme-default");
|
||||
expect(schemeAfterThemeChange("solarized-light", "light")).toBeNull();
|
||||
expect(schemeAfterThemeChange("theme-default", "light")).toBeNull();
|
||||
const customs = { "custom-night": { name: "Night", parent: "dark" as const, attributes: {} } };
|
||||
@@ -257,7 +257,7 @@ describe("color schemes", () => {
|
||||
root.dataset.theme = "dark";
|
||||
const previous = useProject.getState().settings;
|
||||
const scheme = () => useProject.getState().settings?.editor?.colorScheme;
|
||||
useProject.setState({ settings: { editor: { colorScheme: "one-dark" } } as unknown as Settings });
|
||||
useProject.setState({ settings: { editor: { colorScheme: "solarized-dark" } } as unknown as Settings });
|
||||
const dispose = installColorScheme();
|
||||
try {
|
||||
root.dataset.theme = "light";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { parseColor, resolveTokenColor, toHex, toRgb } from "./cssColor";
|
||||
import { computedTokenColor, parseColor, resolveTokenColor, toHex, toRgb } from "./cssColor";
|
||||
|
||||
afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
@@ -31,7 +31,24 @@ describe("css colours", () => {
|
||||
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();
|
||||
});
|
||||
|
||||
it("resolves every token through one probe element, adding none per token", () => {
|
||||
const root = document.documentElement;
|
||||
vi.spyOn(globalThis, "getComputedStyle").mockImplementation(
|
||||
(el) =>
|
||||
(el === root
|
||||
? { getPropertyValue: () => "hsl(220, 13%, 15%)" }
|
||||
: { color: "rgb(33, 37, 43)" }) as unknown as CSSStyleDeclaration,
|
||||
);
|
||||
root.querySelector("[data-token-probe]")?.remove();
|
||||
const added = vi.spyOn(root, "appendChild");
|
||||
for (const name of ["--a", "--b", "--c"]) expect(resolveTokenColor(name, root)).toEqual([33, 37, 43, 1]);
|
||||
expect(added).toHaveBeenCalledTimes(1);
|
||||
expect(root.querySelectorAll("[data-token-probe]")).toHaveLength(1);
|
||||
// Replaced when something removed it.
|
||||
root.querySelector("[data-token-probe]")!.remove();
|
||||
expect(computedTokenColor("--a", root)).toBe("rgb(33, 37, 43)");
|
||||
expect(root.querySelectorAll("[data-token-probe]")).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@@ -35,20 +35,31 @@ export function parseColor(input: string): Rgba | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
const probes = new WeakMap<HTMLElement, HTMLElement>();
|
||||
|
||||
/**
|
||||
* `var(name)` (a `color-mix()`, `hsl()`, …) as the browser computes it under `root`. One hidden
|
||||
* element per root stays for it: inserting one per token restyled the whole page each time.
|
||||
*/
|
||||
export function computedTokenColor(name: string, root: HTMLElement = document.documentElement): string {
|
||||
let probe = probes.get(root);
|
||||
if (probe?.parentElement !== root) {
|
||||
probe = root.ownerDocument.createElement("span");
|
||||
probe.hidden = true;
|
||||
probe.dataset.tokenProbe = "";
|
||||
root.appendChild(probe);
|
||||
probes.set(root, probe);
|
||||
}
|
||||
probe.style.color = `var(${name})`;
|
||||
return getComputedStyle(probe).color;
|
||||
}
|
||||
|
||||
/** 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);
|
||||
return parseColor(raw) ?? parseColor(computedTokenColor(name, root));
|
||||
}
|
||||
|
||||
const byte = (v: number) => Math.min(255, Math.max(0, Math.round(v)));
|
||||
|
||||
@@ -3939,8 +3939,6 @@
|
||||
"Cannot exclude {name}": "No se puede excluir {name}",
|
||||
"Solarized Light": "Solarized claro",
|
||||
"Solarized Dark": "Solarized oscuro",
|
||||
"One Light": "One Light",
|
||||
"One Dark": "One Dark",
|
||||
"Copied reference: {reference}": "Referencia copiada: {reference}",
|
||||
"{count} lines": "{count} líneas",
|
||||
"Preview of {path}": "Vista previa de {path}",
|
||||
|
||||
@@ -3806,8 +3806,6 @@
|
||||
"Cannot exclude {name}": "Impossible d'exclure {name}",
|
||||
"Solarized Light": "Solarized clair",
|
||||
"Solarized Dark": "Solarized sombre",
|
||||
"One Light": "One Light",
|
||||
"One Dark": "One Dark",
|
||||
"Copied reference: {reference}": "Référence copiée : {reference}",
|
||||
"{count} lines": "{count} lignes",
|
||||
"Preview of {path}": "Aperçu de {path}",
|
||||
|
||||
@@ -3999,8 +3999,6 @@
|
||||
"Cannot exclude {name}": "{name}을(를) 제외할 수 없습니다",
|
||||
"Solarized Light": "Solarized 라이트",
|
||||
"Solarized Dark": "Solarized 다크",
|
||||
"One Light": "One 라이트",
|
||||
"One Dark": "One 다크",
|
||||
"Copied reference: {reference}": "참조 복사됨: {reference}",
|
||||
"{count} lines": "{count}줄",
|
||||
"Preview of {path}": "{path} 미리 보기",
|
||||
|
||||
@@ -3899,8 +3899,6 @@
|
||||
"Cannot exclude {name}": "Nie można wykluczyć {name}",
|
||||
"Solarized Light": "Solarized Jasny",
|
||||
"Solarized Dark": "Solarized Ciemny",
|
||||
"One Light": "One Light",
|
||||
"One Dark": "One Dark",
|
||||
"Copied reference: {reference}": "Skopiowano odnośnik: {reference}",
|
||||
"{count} lines": "Liczba wierszy: {count}",
|
||||
"Preview of {path}": "Podgląd {path}",
|
||||
|
||||
@@ -3905,8 +3905,6 @@
|
||||
"Cannot exclude {name}": "Não é possível excluir {name}",
|
||||
"Solarized Light": "Solarized Claro",
|
||||
"Solarized Dark": "Solarized Escuro",
|
||||
"One Light": "One Light",
|
||||
"One Dark": "One Dark",
|
||||
"Copied reference: {reference}": "Referência copiada: {reference}",
|
||||
"{count} lines": "{count} linhas",
|
||||
"Preview of {path}": "Pré-visualização de {path}",
|
||||
|
||||
@@ -4521,8 +4521,6 @@
|
||||
"Cannot exclude {name}": "无法排除 {name}",
|
||||
"Solarized Light": "Solarized 浅色",
|
||||
"Solarized Dark": "Solarized 深色",
|
||||
"One Light": "One 浅色",
|
||||
"One Dark": "One 深色",
|
||||
"Copied reference: {reference}": "已复制引用:{reference}",
|
||||
"{count} lines": "{count} 行",
|
||||
"Preview of {path}": "{path} 的预览",
|
||||
|
||||
@@ -61,30 +61,28 @@ describe("built-in palettes", () => {
|
||||
expect(schemeTokens("solarized-dark").get("--bg-editor")).toBe("#002b36");
|
||||
expect(schemeTokens("solarized-dark").get("--syn-string")).toBe("#2aa198");
|
||||
expect(schemeTokens("solarized-light").get("--ansi-bright-blue")).toBe("#839496");
|
||||
expect(schemeTokens("one-light").get("--syn-keyword")).toBe("hsl(301, 63%, 40%)");
|
||||
expect(schemeTokens("one-dark").get("--syn-string")).toBe("hsl(95, 38%, 62%)");
|
||||
});
|
||||
|
||||
it("keeps explicit editor schemes and per-scheme edits independent of UI palettes", () => {
|
||||
applyThemeAttributes("solarized-light");
|
||||
expect(schemeCss("one-dark", undefined)).toContain("--bg-editor: hsl(220, 13%, 18%) !important");
|
||||
expect(schemeCss("solarized-dark", undefined)).toContain("--bg-editor: #002b36 !important");
|
||||
expect(schemeCss("theme-default", { "solarized-light": { keyword: { foreground: "#123456" } } })).toContain(
|
||||
"--syn-keyword: #123456 !important",
|
||||
);
|
||||
applyThemeAttributes("one-light");
|
||||
applyThemeAttributes("solarized-dark");
|
||||
expect(schemeCss("theme-default", { "solarized-light": { keyword: { foreground: "#123456" } } })).toBe("");
|
||||
});
|
||||
|
||||
it("clears the palette for plugins and restores it when the plugin is removed", () => {
|
||||
const root = document.documentElement;
|
||||
applyThemeAttributes("one-dark", root);
|
||||
applyThemeAttributes("solarized-dark", root);
|
||||
root.dataset.pluginThemeKind = "light";
|
||||
applyThemeAttributes("one-dark", root);
|
||||
applyThemeAttributes("solarized-dark", root);
|
||||
expect(root.dataset.colorTheme).toBeUndefined();
|
||||
expect(root.dataset.theme).toBe("light");
|
||||
delete root.dataset.pluginThemeKind;
|
||||
applyThemeAttributes("one-dark", root);
|
||||
expect(root.dataset.colorTheme).toBe("one-dark");
|
||||
applyThemeAttributes("solarized-dark", root);
|
||||
expect(root.dataset.colorTheme).toBe("solarized-dark");
|
||||
applyThemeAttributes("dark-hc", root);
|
||||
expect(root.dataset.colorTheme).toBeUndefined();
|
||||
expect(root.dataset.contrast).toBe("high");
|
||||
|
||||
@@ -7,8 +7,6 @@ export const BUILTIN_THEMES = [
|
||||
{ id: "dark-hc", label: "High Contrast", kind: "dark" },
|
||||
{ id: "solarized-light", label: "Solarized Light", kind: "light" },
|
||||
{ id: "solarized-dark", label: "Solarized Dark", kind: "dark" },
|
||||
{ id: "one-light", label: "One Light", kind: "light" },
|
||||
{ id: "one-dark", label: "One Dark", kind: "dark" },
|
||||
] as const;
|
||||
|
||||
export type BuiltinThemeId = (typeof BUILTIN_THEMES)[number]["id"];
|
||||
|
||||
@@ -1430,92 +1430,6 @@ html {
|
||||
--bg-editor-selection-inactive: #073642;
|
||||
}
|
||||
|
||||
:root[data-color-theme="one-light"] {
|
||||
--palette-bg: hsl(230, 1%, 98%);
|
||||
--palette-panel: hsl(230, 1%, 92%);
|
||||
--palette-popup: hsl(230, 1%, 95%);
|
||||
--palette-fg: hsl(230, 8%, 24%);
|
||||
--palette-muted: hsl(230, 6%, 44%);
|
||||
--palette-border: hsl(230, 1%, 86%);
|
||||
--palette-hover: hsl(230, 1%, 90%);
|
||||
--palette-selection: hsl(230, 1%, 90%);
|
||||
--palette-line: color-mix(in srgb, hsl(230, 8%, 24%) 5%, hsl(230, 1%, 98%));
|
||||
--palette-accent: hsl(230, 100%, 66%);
|
||||
--palette-on-accent: #ffffff;
|
||||
--palette-cyan: hsl(198, 99%, 37%);
|
||||
--palette-blue: hsl(221, 87%, 60%);
|
||||
--palette-magenta: hsl(301, 63%, 40%);
|
||||
--palette-green: hsl(119, 34%, 47%);
|
||||
--palette-red: hsl(5, 74%, 59%);
|
||||
--palette-orange: hsl(41, 99%, 30%);
|
||||
--palette-yellow: hsl(41, 99%, 38%);
|
||||
--palette-comment: hsl(230, 4%, 64%);
|
||||
--palette-keyword: hsl(301, 63%, 40%);
|
||||
--palette-type: hsl(41, 99%, 38%);
|
||||
--palette-number: hsl(41, 99%, 30%);
|
||||
--palette-variable: hsl(5, 74%, 59%);
|
||||
--palette-property: hsl(230, 8%, 24%);
|
||||
--palette-ansi-black: hsl(230, 8%, 24%);
|
||||
--palette-ansi-red: hsl(5, 74%, 59%);
|
||||
--palette-ansi-green: hsl(119, 34%, 47%);
|
||||
--palette-ansi-yellow: hsl(41, 99%, 38%);
|
||||
--palette-ansi-blue: hsl(221, 87%, 60%);
|
||||
--palette-ansi-magenta: hsl(301, 63%, 40%);
|
||||
--palette-ansi-cyan: hsl(198, 99%, 37%);
|
||||
--palette-ansi-white: hsl(230, 6%, 44%);
|
||||
--palette-ansi-bright-black: hsl(230, 6%, 44%);
|
||||
--palette-ansi-bright-red: hsl(5, 74%, 59%);
|
||||
--palette-ansi-bright-green: hsl(119, 34%, 47%);
|
||||
--palette-ansi-bright-yellow: hsl(41, 99%, 38%);
|
||||
--palette-ansi-bright-blue: hsl(221, 87%, 60%);
|
||||
--palette-ansi-bright-magenta: hsl(301, 63%, 40%);
|
||||
--palette-ansi-bright-cyan: hsl(198, 99%, 37%);
|
||||
--palette-ansi-bright-white: hsl(230, 8%, 24%);
|
||||
}
|
||||
|
||||
:root[data-color-theme="one-dark"] {
|
||||
--palette-bg: hsl(220, 13%, 18%);
|
||||
--palette-panel: hsl(220, 13%, 15%);
|
||||
--palette-popup: hsl(220, 13%, 16.5%);
|
||||
--palette-fg: hsl(220, 14%, 71%);
|
||||
--palette-muted: hsl(220, 9%, 55%);
|
||||
--palette-border: hsl(220, 13%, 10.8%);
|
||||
--palette-hover: hsl(220, 13%, 22%);
|
||||
--palette-selection: hsl(220, 13%, 28%);
|
||||
--palette-line: color-mix(in srgb, hsl(220, 100%, 80%) 4%, hsl(220, 13%, 18%));
|
||||
--palette-accent: hsl(220, 100%, 66%);
|
||||
--palette-on-accent: #000000;
|
||||
--palette-cyan: hsl(187, 47%, 55%);
|
||||
--palette-blue: hsl(207, 82%, 66%);
|
||||
--palette-magenta: hsl(286, 60%, 67%);
|
||||
--palette-green: hsl(95, 38%, 62%);
|
||||
--palette-red: hsl(355, 65%, 65%);
|
||||
--palette-orange: hsl(29, 54%, 61%);
|
||||
--palette-yellow: hsl(39, 67%, 69%);
|
||||
--palette-comment: hsl(220, 10%, 40%);
|
||||
--palette-keyword: hsl(286, 60%, 67%);
|
||||
--palette-type: hsl(39, 67%, 69%);
|
||||
--palette-number: hsl(29, 54%, 61%);
|
||||
--palette-variable: hsl(355, 65%, 65%);
|
||||
--palette-property: hsl(220, 14%, 71%);
|
||||
--palette-ansi-black: hsl(220, 13%, 15%);
|
||||
--palette-ansi-red: hsl(355, 65%, 65%);
|
||||
--palette-ansi-green: hsl(95, 38%, 62%);
|
||||
--palette-ansi-yellow: hsl(39, 67%, 69%);
|
||||
--palette-ansi-blue: hsl(207, 82%, 66%);
|
||||
--palette-ansi-magenta: hsl(286, 60%, 67%);
|
||||
--palette-ansi-cyan: hsl(187, 47%, 55%);
|
||||
--palette-ansi-white: hsl(220, 14%, 71%);
|
||||
--palette-ansi-bright-black: hsl(220, 9%, 55%);
|
||||
--palette-ansi-bright-red: hsl(355, 65%, 65%);
|
||||
--palette-ansi-bright-green: hsl(95, 38%, 62%);
|
||||
--palette-ansi-bright-yellow: hsl(39, 67%, 69%);
|
||||
--palette-ansi-bright-blue: hsl(207, 82%, 66%);
|
||||
--palette-ansi-bright-magenta: hsl(286, 60%, 67%);
|
||||
--palette-ansi-bright-cyan: hsl(187, 47%, 55%);
|
||||
--palette-ansi-bright-white: #ffffff;
|
||||
}
|
||||
|
||||
/*
|
||||
* Appearance options (Settings › Appearance), applied as attributes on :root
|
||||
* by the settings runtime.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { act, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { ContextMenuHost, hideContextMenu, MenuList } from "@/ui";
|
||||
import { showQuickSwitch, THEME_PREVIEW_DELAY, themePreview } from "./quickSwitch";
|
||||
import { showQuickSwitch, themePreview } from "./quickSwitch";
|
||||
import {
|
||||
applyThemeAttributes,
|
||||
currentThemeId,
|
||||
@@ -40,20 +40,15 @@ describe("menu items", () => {
|
||||
});
|
||||
|
||||
describe("Quick Switch theme preview", () => {
|
||||
it("shows the active theme after a moment, in this window only, and takes it back", () => {
|
||||
it("shows the active theme right away, in this window only, and takes it back", () => {
|
||||
syncWindowBackground();
|
||||
const startup = localStorage.getItem(STARTUP_THEME_KEY);
|
||||
const before = shown();
|
||||
const preview = themePreview();
|
||||
preview.preview("solarized-dark");
|
||||
vi.advanceTimersByTime(THEME_PREVIEW_DELAY - 1);
|
||||
expect(shown()).toEqual(before);
|
||||
// Moving on restarts the delay: only the theme the user stays on shows.
|
||||
preview.preview("one-light");
|
||||
vi.advanceTimersByTime(THEME_PREVIEW_DELAY - 1);
|
||||
expect(shown()).toEqual(before);
|
||||
vi.advanceTimersByTime(1);
|
||||
expect(shown()).toEqual(["light", "one-light"]);
|
||||
expect(shown()).toEqual(["dark", "solarized-dark"]);
|
||||
preview.preview("solarized-light");
|
||||
expect(shown()).toEqual(["light", "solarized-light"]);
|
||||
// Nothing is selected or saved, not even as the next start-up's theme.
|
||||
expect(useThemeOverride.getState().theme).toBeNull();
|
||||
expect(localStorage.getItem(STARTUP_THEME_KEY)).toBe(startup);
|
||||
@@ -62,20 +57,18 @@ describe("Quick Switch theme preview", () => {
|
||||
expect(shown()).toEqual(before);
|
||||
expect(localStorage.getItem(STARTUP_THEME_KEY)).toBe(startup);
|
||||
preview.preview("solarized-dark");
|
||||
vi.advanceTimersByTime(THEME_PREVIEW_DELAY);
|
||||
preview.end();
|
||||
expect(shown()).toEqual(before);
|
||||
});
|
||||
|
||||
it("selects the chosen theme and keeps it when the menu closes", () => {
|
||||
const preview = themePreview();
|
||||
preview.preview("solarized-dark");
|
||||
vi.advanceTimersByTime(THEME_PREVIEW_DELAY);
|
||||
preview.choose("one-dark");
|
||||
expect(useThemeOverride.getState().theme).toBe("one-dark");
|
||||
expect(shown()).toEqual(["dark", "one-dark"]);
|
||||
preview.preview("solarized-light");
|
||||
preview.choose("solarized-dark");
|
||||
expect(useThemeOverride.getState().theme).toBe("solarized-dark");
|
||||
expect(shown()).toEqual(["dark", "solarized-dark"]);
|
||||
preview.end();
|
||||
expect(shown()).toEqual(["dark", "one-dark"]);
|
||||
expect(shown()).toEqual(["dark", "solarized-dark"]);
|
||||
});
|
||||
|
||||
it("previews from the Theme submenu and takes the preview back when the menu closes", async () => {
|
||||
@@ -85,7 +78,6 @@ describe("Quick Switch theme preview", () => {
|
||||
fireEvent.pointerEnter(screen.getByRole("menuitem", { name: "Theme" }));
|
||||
act(() => vi.advanceTimersByTime(200));
|
||||
fireEvent.pointerEnter(screen.getByRole("menuitemcheckbox", { name: "Solarized Light" }));
|
||||
act(() => vi.advanceTimersByTime(THEME_PREVIEW_DELAY));
|
||||
expect(shown()).toEqual(["light", "solarized-light"]);
|
||||
|
||||
// Back on another entry: the selected theme again.
|
||||
@@ -95,7 +87,6 @@ describe("Quick Switch theme preview", () => {
|
||||
fireEvent.pointerEnter(screen.getByRole("menuitem", { name: "Theme" }));
|
||||
act(() => vi.advanceTimersByTime(200));
|
||||
fireEvent.pointerEnter(screen.getByRole("menuitemcheckbox", { name: "Solarized Light" }));
|
||||
act(() => vi.advanceTimersByTime(THEME_PREVIEW_DELAY));
|
||||
act(() => hideContextMenu());
|
||||
await act(async () => {});
|
||||
expect(shown()).toEqual(before);
|
||||
|
||||
@@ -22,14 +22,10 @@ async function setGlobal(key: string, value: unknown, what: string) {
|
||||
|
||||
export const selectKeymap = (id: string) => setGlobal("keymap", id, "keymap");
|
||||
|
||||
/** How long a theme stays under the pointer or keyboard before Quick Switch shows it (IDEA's delay). */
|
||||
export const THEME_PREVIEW_DELAY = 500;
|
||||
|
||||
/**
|
||||
* IDEA's instant theme switch (`ide.instant.theme.switch`): in Quick Switch
|
||||
* the active theme shows after a moment, in this window only; choosing one
|
||||
* selects it, and closing the menu without a choice brings the selected
|
||||
* theme back.
|
||||
* Instant theme switch: in Quick Switch the active theme shows right away, in
|
||||
* this window only; choosing one selects it, and closing the menu without a
|
||||
* choice brings the selected theme back.
|
||||
*/
|
||||
export interface ThemePreview {
|
||||
preview(id: ThemeChoiceId): void;
|
||||
@@ -41,25 +37,20 @@ export interface ThemePreview {
|
||||
}
|
||||
|
||||
export function themePreview(): ThemePreview {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
let shown: ThemeChoiceId | null = null;
|
||||
let chosen = false;
|
||||
const restore = () => {
|
||||
clearTimeout(timer);
|
||||
if (shown !== null && !chosen) previewTheme(null);
|
||||
shown = null;
|
||||
};
|
||||
return {
|
||||
preview(id) {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
shown = id;
|
||||
previewTheme(id);
|
||||
}, THEME_PREVIEW_DELAY);
|
||||
if (shown === id) return;
|
||||
shown = id;
|
||||
previewTheme(id);
|
||||
},
|
||||
restore,
|
||||
choose(id) {
|
||||
clearTimeout(timer);
|
||||
chosen = true;
|
||||
// Shown right away; selecting it then changes nothing visible.
|
||||
if (shown !== id) previewTheme(id);
|
||||
|
||||
Reference in new issue
Block a user