Порівняти коміти

...
8 Коміти
Автор SHA1 Повідомлення Дата
arookieofc 1699306eaa fix(vcs): make side-by-side diff divider draggable (#2441)
* fix(vcs): make side-by-side diff divider draggable

Allow dragging the middle band between unaligned panes to resize
left/right share (10-90%), with keyboard arrows, double-click
reset and localStorage persistence. Fixes #2437.

* test(vcs): cover diff divider drag, keys and reset

* chore: fix README formatting (pre-existing oxfmt violation blocking CI)
2026-11-10 17:43:29 +08:00
arookieofc c01a538ea4 fix(db): connect to StarRocks via a MySQL-compatible preset (#2463)
* fix(db): connect to StarRocks via a MySQL-compatible preset (#2452)

StarRocks speaks the MySQL wire protocol but rejects the non-constant
SET sql_mode=(SELECT ...) handshake sqlx sends by default. Add a
StarRocks data source preset (MySQL driver, port 9030) that turns off
pipes_as_concat/no_engine_substitution, and recognize the starrocks URL
scheme.

* test(i18n): allowlist StarRocks product name in text catalog
2026-11-10 17:43:18 +08:00
arookieofc 7cc0d5769d fix(boot): ignore ResizeObserver loop also when delivered as an Error (#2474)
* fix(boot): ignore ResizeObserver loop also when delivered as an Error (#2468)

WebKit (Tauri on Linux) can report the benign loop notification with
an Error object attached, which bypassed the bare-message guard and
showed a fatal startup dialog. Match on the engine-generated message
instead; no genuine startup failure carries these strings.

* chore: fix README formatting for oxfmt check
2026-11-10 17:43:08 +08:00
arookieofc 9828f18300 fix(vcs): let Git Log ref labels size to content (#2450)
* fix(vcs): let Git Log ref labels size to content (#2447)

Remove the unconditional 180px per-label cap and let the refs row use
the full subject cell when space allows, shrinking with ellipsis (full
name kept in the title tooltip) only when space truly runs out.

* chore: fix README formatting (pre-existing oxfmt violation blocking CI)
2026-11-10 17:30:16 +08:00
arookieofc c580dd7c69 fix(editor): make Ctrl+wheel font zoom apply to all open editors (#2488)
Ctrl/Cmd+wheel (and the Increase/Decrease Font Size actions) only resized
the focused editor. They now step every open editor to the same value, and
newly opened editors inherit the shared zoom; Reset Font Size resets it.

Closes #2480
2026-11-10 17:30:06 +08:00
arookieofc da3db5cbd6 fix(build): scope Build Artifacts to the active file's module (#2491)
* fix(build): scope Build Artifacts to the active file's module

Build Artifacts always packaged the whole reactor (all 57 modules in the
reporter's project) because the frontend passed a null file. Route it
through moduleTarget like Build Module does, so Maven runs
'-pl <module> -am package' and Gradle runs '<module>:assemble'; with no
active file it still falls back to the whole project. Backend already
supported module-scoped artifacts for Maven and Gradle, no Rust change.

Refs #2476

* fix(build): translate the module-scoped Build Artifacts description

The new action description needs a zh catalog entry (the only complete
locale) and the old text must go from every catalog (the stale-entry
check covers all locales); otherwise textCatalog.test.ts fails.

Refs #2476
2026-11-10 17:29:54 +08:00
arookieofc d1bef52041 fix(keymap): keep Shift+F6 as a rename fallback in the Eclipse preset (#2498)
Alt+Shift+R is swallowed by overlays that hook the Alt+R family (NVIDIA
performance overlay, Xbox Game Bar), leaving Eclipse-preset users with no
way to trigger refactor/project/db rename. Shift+F6 (IntelliJ's rename,
free in this preset) now works as an alternative entry point.

Refs #1876
2026-11-10 17:29:45 +08:00
l0ng-ai 1d4c0e1cd4 docs: remove docs/ and keep plugin docs in plugins/ (#2503)
* docs: remove unreferenced user guides

Drop docs/command-line.md, databases.md, java.md, unity-external-editor.md
and docs/media/debug.jpg. Nothing in the repository links to them.
Plugin, theme and README assets under docs/ stay.

* docs: keep only plugin docs under docs/

Remove docs/themes.md and docs/data-directories.md. Move the upstream
Atom One source revisions into THIRD_PARTY_LICENSES.md, drop the links
to the removed page, and move the README's WeChat QR code to
.github/assets/.

* docs: move plugin docs next to plugins/README.md

docs/plugins/themes.md and indexing.md move to plugins/. Update the
theme doc generator, its test, the plugin template, AGENTS.md and every
link to the new paths.
2026-11-10 16:16:32 +08:00
49 змінених файлів з 490 додано та 361 видалено

No files matched your search

Переглянути файл
File renamed without changes.

До

Ширина:  |  Висота:  |  Розмір: 86 KiB

Після

Ширина:  |  Висота:  |  Розмір: 86 KiB

+1 -1
Переглянути файл
@@ -12,7 +12,7 @@
"src/editor/core/cm/angular/parser*.ts",
"src/editor/core/cm/jsp/parser*.ts",
"src/editor/core/cm/astro/parser*.ts",
"docs/plugins/themes.md",
"plugins/themes.md",
"pnpm-lock.yaml",
"**/*.toml",
"fixtures",
+1 -1
Переглянути файл
@@ -21,7 +21,7 @@ Visual style: dark, low-contrast greys, one cool accent
- `crates/ide-host` — the `openidea-server` binary for remote development and the
`openidea-plugin` CLI (plugin starter templates live in its `templates.rs`).
- `wit/` — plugin interface definitions; `plugins/` — sample and bundled plugins.
Plugin docs: `plugins/README.md`, `docs/plugins/themes.md`.
Plugin docs: `plugins/README.md`, `plugins/themes.md`.
- `java/` — JDT LS extension bundles (`java-usages`, `kotlin-bridge`) and the patched
Lombok agent, embedded into the app by `ide-sdk` / `ide-tools`.
- `e2e/` — Playwright UI tests (`pnpm e2e`). `fixtures/` — sample projects per language
+1 -1
Переглянути файл
@@ -64,7 +64,7 @@ the [official plugins](plugins/official). The plugin API is still a preview.
- [Discord](https://discord.gg/2Zy4yn7gP) — questions, feedback and release news.
- WeChat group — scan to join:
<img src="docs/assets/wechat-group.png" alt="WeChat group QR code" width="200">
<img src=".github/assets/wechat-group.png" alt="WeChat group QR code" width="200">
### Contributors
+6 -2
Переглянути файл
@@ -262,7 +262,7 @@ END OF TERMS AND CONDITIONS
The Solarized palettes in `src/styles/tokens.css` are adapted from
[Ethan Schoonover’s Solarized](https://github.com/altercation/solarized/tree/62f656a02f93c5190a8753159e34b385588d5ff3).
See [theme sources and mapping](docs/themes.md). MIT license:
MIT license:
```text
Copyright (c) 2011 Ethan Schoonover
@@ -290,7 +290,11 @@ THE SOFTWARE.
The One palettes and semantic syntax mapping in `src/styles/tokens.css` are
adapted from GitHub’s Atom One syntax and UI themes.
Source revisions are listed in [theme sources and mapping](docs/themes.md).
Source revisions:
[one-light-syntax d845790](https://github.com/atom/one-light-syntax/tree/d84579027410c576086dfca14d934c4bd74b0438),
[one-dark-syntax 9c96f44](https://github.com/atom/one-dark-syntax/tree/9c96f4454362267ac45322063e193ccf9d2debb1),
[one-light-ui a27bce3](https://github.com/atom/one-light-ui/tree/a27bce35088ecd323e44f5c2510bcf2d4e9efcf8),
[one-dark-ui 18c2143](https://github.com/atom/one-dark-ui/tree/18c2143d41e17b337fff6dc7024562c4d999be17).
Both syntax themes use the following MIT notice:
```text
+18
Переглянути файл
@@ -594,6 +594,13 @@ fn is_identifier(name: &str) -> bool {
&& chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.')
}
/// sqlx's handshake flags for `sql_mode`: with either enabled it sends
/// `SET sql_mode=(SELECT CONCAT(...))`, a non-constant subquery that MySQL-protocol
/// servers like StarRocks reject (#2452), so the StarRocks preset turns both off.
fn mysql_mode_flags(vendor: &str) -> (bool, bool) {
if vendor == "starrocks" { (false, false) } else { (true, true) }
}
/// A MySQL `SET` value: numbers and keywords as they are, anything else quoted.
fn mysql_value(v: &str) -> String {
let keyword = ["ON", "OFF", "TRUE", "FALSE", "DEFAULT"].iter().any(|k| k.eq_ignore_ascii_case(v));
@@ -839,6 +846,8 @@ impl Pool {
if let Some(c) = &setup.collation {
opts = opts.collation(c);
}
let (pipes_as_concat, no_engine_substitution) = mysql_mode_flags(&source.vendor);
opts = opts.pipes_as_concat(pipes_as_concat).no_engine_substitution(no_engine_substitution);
if let Some(n) = setup.statement_cache {
opts = opts.statement_cache_capacity(n);
}
@@ -1465,6 +1474,15 @@ mod tests {
assert!(Setup::of(&DataSource { properties: vec![prop("connect_timeout", "soon")], ..s }).is_err());
}
#[test]
fn starrocks_disables_the_non_constant_sql_mode_handshake() {
// MySQL and its other wire-compatible presets keep sqlx's defaults.
for vendor in ["", "mysql", "mariadb", "tidb"] {
assert_eq!(mysql_mode_flags(vendor), (true, true), "{vendor}");
}
assert_eq!(mysql_mode_flags("starrocks"), (false, false));
}
#[test]
fn sqlite_properties_become_pragmas() {
let s = DataSource { properties: vec![prop("cache_size", "-2000")], ..Default::default() };
+2 -1
Переглянути файл
@@ -270,7 +270,7 @@ pub fn scheme_driver(scheme: &str) -> Option<DbDriver> {
match scheme.to_ascii_lowercase().as_str() {
"postgres" | "postgresql" | "pgsql" | "redshift" | "cockroachdb" | "cockroach" | "yugabytedb"
| "timescaledb" | "greenplum" => Some(DbDriver::Postgres),
"mysql" | "mariadb" | "tidb" | "singlestore" | "memsql" | "oceanbase" => Some(DbDriver::Mysql),
"mysql" | "mariadb" | "tidb" | "singlestore" | "memsql" | "oceanbase" | "starrocks" => Some(DbDriver::Mysql),
"sqlserver" | "mssql" | "mssqlserver" => Some(DbDriver::SqlServer),
"sqlite" | "sqlite3" => Some(DbDriver::Sqlite),
"clickhouse" | "http" | "https" => Some(DbDriver::Clickhouse),
@@ -559,6 +559,7 @@ mod tests {
assert_eq!((u.host.as_str(), u.port, u.user.as_str()), ("::1", None, "root"));
assert_eq!(parse_url("jdbc:redshift://r:5439/dev").unwrap().driver, Some(DbDriver::Postgres));
assert_eq!(parse_url("tidb://h:4000").unwrap().driver, Some(DbDriver::Mysql));
assert_eq!(parse_url("starrocks://h:9030").unwrap().driver, Some(DbDriver::Mysql));
assert_eq!(parse_url("sqlite:/tmp/a.db").unwrap().file.as_deref(), Some("/tmp/a.db"));
let u = parse_url("oracle://scott:[email protected]:1521/ORCLPDB1").unwrap();
assert_eq!(u.driver, Some(DbDriver::Oracle));
+1 -1
Переглянути файл
@@ -208,7 +208,7 @@ authors = []
engines.openidea = "^1.0"
# One entry per theme. `editor-scheme` is optional: its tokens are merged over
# the theme's (IntelliJ's .theme.json + .icls pair). Reference: docs/plugins/themes.md.
# the theme's (IntelliJ's .theme.json + .icls pair). Reference: plugins/themes.md.
[[contributes.themes]]
path = "themes/__SLUG__.theme.json"
editor-scheme = "themes/__SLUG__.scheme.json"
-41
Переглянути файл
@@ -1,41 +0,0 @@
# Command-line launcher
Install the `openidea` command from **OpenIDEA → Install 'openidea' Command** on
macOS, or **Tools → Install 'openidea' Command** on Windows and Linux. The same
action is available in Find Action. Open a new terminal after installation.
- **macOS:** move OpenIDEA to Applications first. The command links to the launcher
inside the app bundle. It uses `~/.local/bin` when that directory is on PATH;
otherwise it uses `/usr/local/bin` and may request an administrator password.
- **Windows:** installation creates `openidea.cmd` in
`%LOCALAPPDATA%\OpenIDEA\bin` and adds that directory to the user PATH. This
works with installed and portable copies, without administrator rights.
- **Linux:** installation creates `~/.local/bin/openidea`. If another command
takes precedence or this directory is absent from PATH, the notification asks
you to put it at the beginning of your shell's PATH. For Bash and Zsh, add
`export PATH="$HOME/.local/bin:$PATH"` to your shell profile. For Fish, run
`fish_add_path --prepend ~/.local/bin`. An AppImage launcher targets the original
AppImage; keep that file in a permanent location.
```sh
openidea .
openidea src/main.rs README.md
openidea --line 12 --column 5 src/main.rs
openidea src/main.rs:12:5
openidea diff before.txt after.txt
openidea --help
openidea --version
```
Folders open as projects. Files open in their project's window, or in LightEdit
when no project contains them. Lines and columns start at 1. With no paths, the
command focuses OpenIDEA. It starts the app if necessary and returns after handing
off the request. Unknown options and missing files exit with code 1.
Use `--` before a filename beginning with `-`, or before a file named `diff`.
The existing GUI executable and OS file associations still work.
**Uninstall 'openidea' Command** removes only OpenIDEA's launcher, and on Windows
its dedicated user PATH entry. A command belonging to another application is
never overwritten or removed. Reinstall the command after moving a portable
copy or AppImage to a different location.
-27
Переглянути файл
@@ -1,27 +0,0 @@
# Data directories
OpenIDEA keeps its rebuildable data, such as downloaded tools, language servers,
JDKs and index caches, in the operating system's local data directory:
- **Windows:** `%LOCALAPPDATA%\ai.l0ng.openidea`
- **macOS:** `~/Library/Application Support/ai.l0ng.openidea`
- **Linux:** `~/.local/share/ai.l0ng.openidea`
To keep this data elsewhere, for example on another drive, set
`OPENIDEA_LOCAL_DATA_DIR` to an absolute path before starting OpenIDEA. The IDE
then uses `<OPENIDEA_LOCAL_DATA_DIR>/ai.l0ng.openidea` and creates it when
missing. If the path is relative or cannot be written, OpenIDEA shows an error
and exits instead of falling back to the default location.
```bat
setx OPENIDEA_LOCAL_DATA_DIR "E:\OpenIDEA"
```
Settings, plugins, scratches and other user data stay in the operating system's
data and configuration directories. Logs stay in their usual location, so they
are still written when the variable is wrong. Existing data is not moved: quit
OpenIDEA and move only `tools/`, `language-server-runtimes/`, `index/`, `cache/`,
`lsp/`, `notebook-runtime/`, `server/`, `logs/`, `agents/tools/`,
`agents/registry.json` and top-level `java-usages-*.jar` / `java-usages-*.tmp` to
the new location, preserving their relative paths, or let the IDE download and
rebuild them. Leave everything else in place.
-69
Переглянути файл
@@ -1,69 +0,0 @@
# Databases
The Database tool window (View ▸ Tool Windows ▸ Database) connects to SQLite,
PostgreSQL, MySQL/MariaDB, Microsoft SQL Server, ClickHouse and Oracle, plus the
wire-compatible products listed in **New ▸ Data Source**. Connections are saved
per project in `.openidea/data-sources.toml`; passwords live outside the project
(and never in that file).
The Database tree groups procedures and functions in one `routines` folder by
default. Enable **Separate Procedures and Functions** in the Database window’s
**Options** menu to show `procedures` and `functions` folders instead. This view
option is remembered across sessions.
## Oracle: Oracle Instant Client
Oracle support uses the Oracle Call Interface, so the Oracle client libraries
must be installed on the machine. OpenIDEA does not bundle them: install the
**Oracle Instant Client** for your platform and point the data source at it.
1. Download the _Basic_ (or _Basic Light_) package of Oracle Instant Client
19c or later for your OS from
<https://www.oracle.com/database/technologies/instant-client/downloads.html>.
2. Unpack it somewhere stable, then, in the data source's **Options** tab, set
**Instant Client** to that directory.
3. General tab: host, port (1521), and the **service name** (for example
`ORCLPDB1`) in the Database field. Connect with the schema's user and its
password.
`java.sql`-style URLs also work in the URL field:
```
oracle://scott:[email protected]:1521/ORCLPDB1
jdbc:oracle:thin:@//db.example.com:1521/ORCLPDB1
```
A TNS descriptor or alias in the Database field is passed to the client as is.
### Platform notes
- **macOS** — the _Basic_ ZIP for ARM (Apple silicon) or x86-64 must match the
app's architecture. Remove the quarantine flag if macOS blocks the libraries:
`xattr -dr com.apple.quarantine /opt/oracle/instantclient_23_5`.
- **Windows** — use the 64-bit package; the app is 64-bit. `PATH` does not have
to include the directory when the Options tab names it.
- **Linux** — install the RPM/DEB or unzip the ZIP; the directory needs the
`libclntsh.so` and `libclntshcore.so` libraries (the ZIP's `libaio`-based
builds also need the `libaio` package).
Without a working client, Test Connection fails with a clear message
(`DPI-1047: Cannot locate a 64-bit Oracle Client library…`). If the Options tab
is empty and `ORACLE_HOME` is not set, the client is looked up on the system
search path.
### What Oracle data sources support
- Schemas = Oracle users (`ALL_USERS`); system accounts (`SYS`, `SYSTEM`,
`XDB`, `MDSYS`…) appear only with **Schemas ▸ Show system**.
- Tables, views, columns, indexes, foreign keys, triggers, procedures, functions
and sequences are read from the `ALL_*` views.
- The query console runs SQL and PL/SQL blocks; `BEGIN … END;` blocks (nested
`;` included) are sent as one statement, and a lone `/` on its own line ends
one as in SQL*Plus.
- Explain Plan runs `EXPLAIN PLAN` and reads `PLAN_TABLE` (`DBMS_XPLAN` output
is not required).
- Table editor (INSERT/UPDATE/DELETE in one transaction), Import/Export and
generated SQL use Oracle syntax: `"UPPER_CASE"` identifiers, `FETCH FIRST n
ROWS ONLY`, `MODIFY (…)` for column changes.
Table editing needs a primary key, and the connection must not be read-only.
-21
Переглянути файл
@@ -1,21 +0,0 @@
# Java language servers
Settings › Languages & Frameworks › Language Servers lists Eclipse JDT Language
Server (`jdtls`) and [kmp-lsp](https://github.com/Hessesian/kmp-lsp). Select either
tool and use **Language server for Java** to choose one. The choice can be global
or limited to the current project. **Automatic** prefers jdtls and falls back to
kmp-lsp when jdtls is not installed; **None** disables both.
Install kmp-lsp from that page, or point its executable path at an existing
installation. The IDE downloads a platform distribution containing both kmp-lsp
and its native JAR indexer. Reinstall and uninstall manage both together.
kmp-lsp offers syntax-based Java code insight without running a JVM. It does not
provide JDT's compiler analysis, Lombok integration, or JDT-specific refactorings.
Java Application builds, classpath discovery and the Microsoft Java debugger still
require jdtls; use jdtls for those flows. Maven and Gradle build tasks can run
independently of the selected code-insight server.
This built-in choice applies to Java. Kotlin and Swift keep their existing server
choices. To use another server, add it with **New Language Server**, assign its
languages, and disable the servers it replaces.
Бінарний файл не відображається.

До

Ширина:  |  Висота:  |  Розмір: 907 KiB

-100
Переглянути файл
@@ -1,100 +0,0 @@
# Built-in theme sources
OpenIDEA includes Islands Light, Islands Dark, High Contrast, Solarized Light,
Solarized Dark, One Light and One Dark. Select them in Settings › Appearance,
Welcome › Customize or the existing quick theme menu. The same themes are
available as explicit editor schemes; “Theme default” follows the UI theme.
“Sync with OS” uses the existing preferred light/dark theme settings. Existing
ids (`light`, `dark`, `dark-hc`, `system`) and attribute overrides remain valid.
## Upstream palettes and licenses
All sources below are the original authors’ repositories, pinned for reproducibility.
Full MIT notices are preserved in [THIRD_PARTY_LICENSES.md](../THIRD_PARTY_LICENSES.md).
| Theme | Original source revision | Palette / mapping source |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| Solarized Light / Dark | [Ethan Schoonover, 62f656a](https://github.com/altercation/solarized/tree/62f656a02f93c5190a8753159e34b385588d5ff3) | README’s sixteen exact sRGB hex colours; `vim-colors-solarized/colors/solarized.vim` semantic roles; iTerm2 ANSI slots |
| One Light syntax | [GitHub / Atom, d845790](https://github.com/atom/one-light-syntax/tree/d84579027410c576086dfca14d934c4bd74b0438) | `styles/colors.less`, `styles/syntax-variables.less` |
| One Dark syntax | [GitHub / Atom, 9c96f44](https://github.com/atom/one-dark-syntax/tree/9c96f4454362267ac45322063e193ccf9d2debb1) | `styles/colors.less`, `styles/syntax-variables.less` |
| One Light UI | [GitHub / Atom, a27bce3](https://github.com/atom/one-light-ui/tree/a27bce35088ecd323e44f5c2510bcf2d4e9efcf8) | `styles/ui-variables.less`, `styles/ui-variables-custom.less` |
| One Dark UI | [GitHub / Atom, 18c2143](https://github.com/atom/one-dark-ui/tree/18c2143d41e17b337fff6dc7024562c4d999be17) | `styles/ui-variables.less`, `styles/ui-variables-custom.less` |
## Mapping into OpenIDEA
`src/platform/themes.ts` registers the stable ids. `data-theme` retains the
light/dark kind for icons, native controls and existing selectors;
`data-color-theme` selects the additional palette. High Contrast continues to
use `data-contrast="high"`. Plugin themes clear the built-in palette attribute
and retain their existing light/dark fallback; removing a plugin restores the
chosen built-in palette.
`src/styles/tokens.css` contains the shared mapping for all colour tokens and
four palette blocks. No new dependency is required. Solarized uses base3/base2
for light backgrounds and base03/base02 for dark backgrounds, base00/base0 for
text, and the original eight accents. Vim’s statements are green, constants
and strings cyan, identifiers blue, types yellow, and comments the original
subdued base1/base01. Solarized’s original ANSI slot order is preserved (the
bright slots intentionally include orange, violet and neutrals).
One retains the exact HSL definitions from Atom, avoiding rounded palette
approximations: 230° / 1% / 98% light and 220° / 13% / 18% dark editor backgrounds;
purple keywords, green strings, blue functions, red variables/tags, orange
numbers and attributes, yellow classes, and the original mono-3 comments.
UI frame/popup levels use Atom’s relative background lightness formulas with
the matching syntax theme. OpenIDEA’s shared UI text uses the syntax foreground
and mono-2 for readable secondary labels. Selection and cursor-line values
follow Atom’s syntax variables. Atom does not define a terminal ANSI palette:
One’s ANSI slots reuse its exact semantic syntax hues, repeating them for bright
colours rather than inventing a different palette. One Light maps ANSI white
and bright white to its mono-2 and mono-1 text colours so they remain visible
on the light terminal background.
Components without an upstream equivalent (feedback banners, tool window states,
search fills and focus decoration) use the original semantic hues, neutrals,
or explicit `color-mix()` tints. This adapts colours to the existing layout;
it does not recreate Atom’s or Vim’s UI. Platform traffic-light colours stay
platform colours. Original subdued comments and some syntax accents have lower
contrast than a high-contrast accessibility theme; High Contrast remains available.
Native windows open in the theme's frame colour before their page loads:
`src-tauri/src/theme_frame.rs` reads `--bg-frame` of each built-in theme from
`tokens.css` itself (resolving `var()` and `hsl()`), so there is no second
colour table to keep in sync. Elsewhere the frontend resolves HSL and
`color-mix()` tokens to RGB (`src/platform/cssColor.ts`) for Tauri's window
API, the native Settings window, Mermaid and the terminal.
`public/boot.js` restores the palette (`data-color-theme`) and the splash's
resolved frame, text and accent colours before the application stylesheet
loads, so a restart in One Dark or Solarized shows no flash of another theme.
The backend's settings persistence remains authoritative. Terminal, Mermaid
and editor-scheme observers also watch palette changes within the same
light/dark kind. Custom accents, explicit editor schemes, per-scheme attribute
overrides, compact mode and accessibility options retain their precedence.
`src/features/settings/__tests__/contrast.test.ts` requires every palette theme
to define every colour token of the base themes (in the shared mapping or its
own block), so a token added to `tokens.css` later can't silently fall back to
an Islands colour.
## Browser evidence
These approved screenshots are reused from the original theme implementation on
0.1.15; regression checks are rerun against the upstream contribution base.
Captured at 1400 × 900 in Linux Chromium 154 using OpenIDEA's public in-memory
`acme-api` project / Kotlin fixture and synthetic ANSI terminal output. The
terminal is a real xterm renderer, without a native PTY or shell. These images
cover the shell, sidebar, editor, gutter, find matches, terminal, Settings,
focused controls, disabled settings and the selected theme popup.
| Theme | Workbench, editor and terminal | Settings and theme popup |
| --------------- | -------------------------------------------------------- | ---------------------------------------------------- |
| Solarized Light | [Screenshot](media/themes/solarized-light-workbench.png) | [Screenshot](media/themes/solarized-light-popup.png) |
| Solarized Dark | [Screenshot](media/themes/solarized-dark-workbench.png) | [Screenshot](media/themes/solarized-dark-popup.png) |
| One Light | [Screenshot](media/themes/one-light-workbench.png) | [Screenshot](media/themes/one-light-popup.png) |
| One Dark | [Screenshot](media/themes/one-dark-workbench.png) | [Screenshot](media/themes/one-dark-popup.png) |
The browser regression also checks refresh / startup cache restoration,
selection visibility, same-kind switches, the quick menu, existing themes,
explicit editor schemes and preferred OS themes. Native startup colours are
covered by Rust unit tests.
-42
Переглянути файл
@@ -1,42 +0,0 @@
# Open scripts from Unity
OpenIDEA can open scripts and Console source locations without a Unity plugin.
1. In Unity, open **Edit > Preferences > External Tools** (on macOS,
**Unity > Settings/Preferences > External Tools**).
2. Under **External Script Editor**, choose **Browse** and select OpenIDEA's
executable (or its `.app` on macOS).
3. Set **External Script Editor Args** to:
```text
$(ProjectPath) --file $(File) --line $(Line) --column $(Column)
```
Unity 2022.3 quotes the expanded project and file paths itself, including paths
with spaces. Do not add another pair of quotes around these placeholders.
Double-click a script in the Project window or a source location in the Console.
OpenIDEA opens the project and file, or focuses its existing project window, then
places the caret at the supplied location. Other open tabs stay in place.
The project argument makes cold launches open the Unity project rather than a
standalone file. Without it, `--file` opens in an existing matching project window
or in LightEdit. Relative file paths resolve against the supplied project folder,
or against the launch working directory when no project is supplied.
For a direct invocation of the application executable, quote paths as required
by your shell. The separately installed `openidea` command has its own syntax:
```text
/path/to/openidea "/path/to/My Game" --file "/path/to/My Game/Assets/Player.cs" --line 12 --column 5
```
`--file=...`, `--line=...` and `--column=...` also work. Lines and columns are
1-based; Unity's `0` and `-1` mean an unknown position. An omitted or unknown
column uses the start of the line. Without a known line, the file opens without
an explicit caret position. Invalid arguments and missing files report an error.
On Windows, use the same permissions for Unity and OpenIDEA when possible.
If a running instance does not receive a request, restart both applications with
matching permissions and retry. This integration opens local projects; it does
not generate C# project files or attach a debugger.
+3 -3
Переглянути файл
@@ -251,7 +251,7 @@ its component with `plugins/build.sh crates/ide-plugin/tests/fixtures/file-templ
`[[contributes.documentIndexers]]` declares file `extensions` and requires the optional
`document-indexer` export. It publishes cross-file associations (definitions, references,
aliases and gutter markers) in background batches, not on the editing path. See
[Background document indexers](../docs/plugins/indexing.md).
[Background document indexers](indexing.md).
API 1.3 also uses this graph for Find Usages and Rename on published definitions
and references, alongside language-server results. Name anchors must cover exactly
@@ -1957,7 +1957,7 @@ colours, merged over the theme). Pick a theme in Settings › Plugins › _Apply
Start with `openidea-plugin new <dir> --template theme` (or a copy of
[`official/harbor-night`](official/harbor-night)); the full guide, token
reference and IntelliJ / VS Code porting tables are in
[`docs/plugins/themes.md`](../docs/plugins/themes.md).
[`plugins/themes.md`](themes.md).
## 5. File icon themes
@@ -2134,7 +2134,7 @@ granted, and starts the approved program.
the package at its root or in its only folder, or a folder's `plugin.toml`;
a zip without a `plugin.toml` there, such as an IntelliJ plugin, is refused; IntelliJ
theme plugins are imported instead: _Import IntelliJ Theme from File…_ and _Import Theme
from JetBrains IDE…_, see [`docs/plugins/themes.md`](../docs/plugins/themes.md)). The install
from JetBrains IDE…_, see [`plugins/themes.md`](themes.md)). The install
page lists every capability and whether the package is signed by a publisher
the marketplace trusts; nothing is copied until you accept.
- **Versions:** installed plugins live in `<app-data>/plugins/<id>/<version>/`
Переглянути файл
File renamed without changes.
+1 -1
Переглянути файл
@@ -4,7 +4,7 @@ An OpenIDEA theme plugin, **Harbor Night**: a dark theme with slate surfaces, a
teal accent and a warm syntax palette. It is also where new themes start:
`openidea-plugin new --template theme` copies its theme files, or copy this
folder. Full documentation, the token reference and porting guides are in
[`docs/plugins/themes.md`](../../../docs/plugins/themes.md).
[`plugins/themes.md`](../../themes.md).
```
harbor-night/
+1 -1
Переглянути файл
@@ -1,7 +1,7 @@
// Harbor Night: UI theme. Keys are the CSS variables of src/styles/tokens.css,
// grouped like its Islands Light block. Delete a line to keep the built-in value
// of the base named by "kind". Comments are allowed (// and /* */).
// Full reference: docs/plugins/themes.md.
// Full reference: plugins/themes.md.
{
"id": "harbor-night",
"name": "Harbor Night",
+5 -5
Переглянути файл
@@ -1,6 +1,6 @@
# Theme plugins
OpenIDEA ships seven [built-in themes](../themes.md): Islands Light, Islands Dark,
OpenIDEA ships seven built-in themes: Islands Light, Islands Dark,
High Contrast, Solarized Light, Solarized Dark, One Light and One Dark. Additional
themes come from **theme plugins**, the same model as
IntelliJ IDEA, where most themes are plugins rather than part of the IDE.
@@ -18,7 +18,7 @@ my-theme/
```
The quickest way to start is `openidea-plugin new --template theme`, or a copy of
[`plugins/official/harbor-night`](../../plugins/official/harbor-night).
[`plugins/official/harbor-night`](official/harbor-night).
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
reference, porting themes from IntelliJ IDEA and VS Code, and testing.
@@ -39,7 +39,7 @@ editor-scheme = "themes/harbor-night.scheme.json" # optional
```
Paths are relative to the package directory and may not leave it. The other
manifest fields are described in [`plugins/README.md`](../../plugins/README.md).
manifest fields are described in [`plugins/README.md`](README.md).
## The theme file
@@ -55,7 +55,7 @@ manifest fields are described in [`plugins/README.md`](../../plugins/README.md).
}
```
- **`tokens`** are the CSS variables of [`src/styles/tokens.css`](../../src/styles/tokens.css),
- **`tokens`** are the CSS variables of [`src/styles/tokens.css`](../src/styles/tokens.css),
the IDE's single source of colours. Components never use colour literals, so
every surface, border, text colour, syntax colour and status colour can be
themed through them. The [reference](#token-reference) lists them all.
@@ -206,7 +206,7 @@ ship no `.icls` at all keep the built-in syntax colours.
| `--bg-toggle-*` | `ToggleButton.onBackground`, `.offBackground`, `.buttonColor` |
| `--bg-feedback-*`, `--border-feedback-*` | `Banner.infoBackground`, `Banner.errorBorderColor`, … |
The full table is `UI_KEYS` in [`src/features/plugins/intellijTheme.ts`](../../src/features/plugins/intellijTheme.ts).
The full table is `UI_KEYS` in [`src/features/plugins/intellijTheme.ts`](../src/features/plugins/intellijTheme.ts).
#### `.icls` → editor scheme
+6 -4
Переглянути файл
@@ -90,11 +90,13 @@
if (target?.tagName === "SCRIPT" || (target?.tagName === "LINK" && target.rel === "modulepreload")) {
show(`Could not load startup script: ${target.src || target.href}`);
} else if (event.error || event.message) {
// ResizeObserver defers undelivered notifications to the next frame; startup can continue.
// ResizeObserver defers undelivered notifications to the next frame; startup can
// continue. Blink reports the loop as a bare message, WebKit (Tauri on Linux) as an
// Error object — ignore both. No genuine startup failure carries these engine strings.
const message = event.error?.message ?? event.message;
if (
!event.error &&
(event.message === "ResizeObserver loop completed with undelivered notifications." ||
event.message === "ResizeObserver loop limit exceeded")
message === "ResizeObserver loop completed with undelivered notifications." ||
message === "ResizeObserver loop limit exceeded"
)
return;
show(event.error || `${event.message}\n${event.filename || ""}:${event.lineno || 0}:${event.colno || 0}`);
+2 -2
Переглянути файл
@@ -1,12 +1,12 @@
/**
* Regenerates the token reference in docs/plugins/themes.md from
* Regenerates the token reference in plugins/themes.md from
* src/styles/tokens.css (`pnpm gen:theme-docs`). scripts/theme/theme.test.ts
* fails when the checked-in reference is stale.
*/
import { readFileSync, writeFileSync } from "node:fs";
import { referenceMarkdown, themeTokenGroups, withReference } from "./tokens.ts";
const DOC = "docs/plugins/themes.md";
const DOC = "plugins/themes.md";
const groups = themeTokenGroups(readFileSync("src/styles/tokens.css", "utf8"));
const doc = readFileSync(DOC, "utf8");
const next = withReference(doc, referenceMarkdown(groups));
+1 -1
Переглянути файл
@@ -59,5 +59,5 @@ if (scheme) {
console.log(
`${theme.name} (${theme.kind}): ${Object.keys(theme.tokens).length} UI tokens` +
(scheme ? `, ${Object.keys(scheme.tokens).length} editor tokens` : "") +
` → ${values.out}\nUnmapped tokens keep the built-in ${theme.kind} values; see docs/plugins/themes.md to fill them in.`,
` → ${values.out}\nUnmapped tokens keep the built-in ${theme.kind} values; see plugins/themes.md to fill them in.`,
);
+2 -2
Переглянути файл
@@ -40,8 +40,8 @@ describe("theme token reference", () => {
expect(() => themeTokenGroups(bad)).toThrow(/--b .*section comment/);
});
it("is current in docs/plugins/themes.md (run pnpm gen:theme-docs)", () => {
const doc = read("docs/plugins/themes.md");
it("is current in plugins/themes.md (run pnpm gen:theme-docs)", () => {
const doc = read("plugins/themes.md");
expect(withReference(doc, referenceMarkdown(groups))).toBe(doc);
});
});
+1 -1
Переглянути файл
@@ -131,7 +131,7 @@ export function tokenSets(groups: readonly TokenGroup[]): { ui: string[]; scheme
const cell = (v: string) => "`" + v.replace(/\|/g, "\\|") + "`";
const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
/** Markdown token reference (one table per group) for docs/plugins/themes.md. */
/** Markdown token reference (one table per group) for plugins/themes.md. */
export function referenceMarkdown(groups: readonly TokenGroup[]): string {
const { ui, scheme } = tokenSets(groups);
const parts = [
+1 -1
Переглянути файл
@@ -33,7 +33,7 @@ and references. Existing indexers work without another export; declare
cover exactly the name, using UTF-16 columns. The host merges native results,
previews indexed renames and applies them as one undoable workspace edit.
The existing `project.read` capability and trusted-project boundary apply.
See [the indexing contract](../../../docs/plugins/indexing.md#find-usages-and-rename-api-13)
See [the indexing contract](../../../plugins/indexing.md#find-usages-and-rename-api-13)
and [the minimal fixture](../../../crates/ide-plugin/tests/fixtures/indexer).
API 1.3 also adds diagnostics for unsaved documents. Declare
+4 -6
Переглянути файл
@@ -70,8 +70,7 @@ import { copyReferenceToClipboard, declaredName, qualifiedReference, referenceOf
import { declarationChain } from "./view/outline";
import { recordClipboard } from "./clipboardHistory";
import { useDocMeta, useOutline } from "./model/document";
import { fontZoom, setFontZoom, stepFontZoom } from "./cm/prefs";
import { useEditorRegistry } from "@/editor/api";
import { resetAllEditorsFontZoom, stepAllEditorsFontZoom } from "./cm/prefs";
import { useProject } from "@/platform/project";
import { call, commands } from "@/platform/ipc";
import { editorPrefs } from "@/platform/editorSettings";
@@ -121,7 +120,7 @@ function stepSurfaceFontSize(delta: 1 | -1): void {
if (cmEl) {
const cmView = EditorView.findFromDOM(cmEl);
if (cmView) {
stepFontZoom(cmView, delta);
stepAllEditorsFontZoom(cmView, delta);
return;
}
}
@@ -129,7 +128,7 @@ function stepSurfaceFontSize(delta: 1 | -1): void {
const slot = activeSlot();
if (!slot?.view) return;
stepFontZoom(slot.view, delta);
stepAllEditorsFontZoom(slot.view, delta);
if (!slot.view.hasFocus && !getContext().popupOpen) slot.view.focus();
}
@@ -898,8 +897,7 @@ export function registerEditorActions(): () => void {
keywords: ["zoom", "font size", "mouse wheel"],
when: "projectOpen",
run: () => {
for (const ed of useEditorRegistry.getState().editors)
if (ed.view.state.field(fontZoom, false)) ed.view.dispatch({ effects: setFontZoom.of(0) });
resetAllEditorsFontZoom();
if (typeof document !== "undefined") {
resetSurfaceFontZoom();
}
+34 -3
Переглянути файл
@@ -36,6 +36,7 @@ import {
import { markdownImages } from "./markdownImages";
import { selectionMarks } from "./selectionMarks";
import { seamlessSelection } from "./selectionLayer";
import { useEditorRegistry } from "@/editor/api";
import type { EditorExtras } from "@/platform/editorSettings";
import { openideaHighlightStyle } from "./highlight";
import {
@@ -374,10 +375,16 @@ const richCopy = EditorView.domEventHandlers({
// --- caret / scrolling / mouse ----------------------------------------------
/**
* Font zoom new editors start from. ⌘/Ctrl+wheel keeps every open editor on the
* same value (#2480), so editors opened afterwards match the others.
*/
let sharedFontZoom = 0;
/** Font zoom of one editor (⌘ + wheel), in px relative to the configured size. */
export const setFontZoom = StateEffect.define<number>();
export const fontZoom = StateField.define<number>({
create: () => 0,
create: () => sharedFontZoom,
update: (v, tr) => tr.effects.reduce((acc, e) => (e.is(setFontZoom) ? e.value : acc), v),
// Highest precedence: its style comes after the configured font size, which it scales.
provide: (f) =>
@@ -400,11 +407,35 @@ export function stepFontZoom(view: EditorView, delta: number): void {
if (next !== z) view.dispatch({ effects: setFontZoom.of(next) });
}
const wheelZoom = EditorView.domEventHandlers({
/**
* Steps the font zoom of every open editor to the same value, so ⌘/Ctrl+wheel
* resizes all editor areas together (#2480) instead of only the focused one.
*/
export function stepAllEditorsFontZoom(origin: EditorView, delta: number): void {
const current = origin.state.field(fontZoom, false);
if (current === undefined) return;
const next = Math.max(-6, Math.min(36, current + delta));
if (next === current) return;
sharedFontZoom = next;
for (const ed of useEditorRegistry.getState().editors) {
if (ed.view.state.field(fontZoom, false) === undefined) continue;
if (ed.view.state.field(fontZoom) !== next) ed.view.dispatch({ effects: setFontZoom.of(next) });
}
}
/** Resets every open editor's font zoom, and the value new editors start from. */
export function resetAllEditorsFontZoom(): void {
sharedFontZoom = 0;
for (const ed of useEditorRegistry.getState().editors)
if (ed.view.state.field(fontZoom, false)) ed.view.dispatch({ effects: setFontZoom.of(0) });
}
/** Exported for tests: the ⌘/Ctrl+wheel handler of the prefs compartment. */
export const wheelZoom = EditorView.domEventHandlers({
wheel(e, view) {
if (!e.metaKey && !e.ctrlKey) return false;
e.preventDefault();
stepFontZoom(view, e.deltaY < 0 ? 1 : -1);
stepAllEditorsFontZoom(view, e.deltaY < 0 ? 1 : -1);
return true;
},
});
+77
Переглянути файл
@@ -0,0 +1,77 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { EditorState } from "@codemirror/state";
import { EditorView } from "@codemirror/view";
import { useEditorRegistry, type TextEditorHandle } from "@/editor/api";
import { fontZoom, resetAllEditorsFontZoom, setFontZoom, stepAllEditorsFontZoom, wheelZoom } from "./cm/prefs";
const zoomOf = (view: EditorView) => view.state.field(fontZoom, false);
describe("shared editor font zoom (#2480)", () => {
const handles: TextEditorHandle[] = [];
const makeView = (extensions = [fontZoom, wheelZoom]) => {
const parent = document.createElement("div");
document.body.append(parent);
const view = new EditorView({ parent, state: EditorState.create({ extensions }) });
const handle = { view } as TextEditorHandle;
useEditorRegistry.getState().editors.add(handle);
handles.push(handle);
return view;
};
beforeEach(() => resetAllEditorsFontZoom());
afterEach(() => {
for (const handle of handles) {
useEditorRegistry.getState().editors.delete(handle);
handle.view.destroy();
handle.view.dom.parentElement?.remove();
}
handles.length = 0;
resetAllEditorsFontZoom();
});
it("zooms every open editor from ⌘/Ctrl+wheel on one of them", () => {
const first = makeView();
const second = makeView();
first.contentDOM.dispatchEvent(new WheelEvent("wheel", { bubbles: true, ctrlKey: true, deltaY: -10 }));
expect(zoomOf(first)).toBe(1);
expect(zoomOf(second)).toBe(1);
second.contentDOM.dispatchEvent(new WheelEvent("wheel", { bubbles: true, ctrlKey: true, deltaY: 10 }));
expect(zoomOf(first)).toBe(0);
expect(zoomOf(second)).toBe(0);
});
it("ignores the wheel without ⌘/Ctrl", () => {
const first = makeView();
makeView();
first.contentDOM.dispatchEvent(new WheelEvent("wheel", { bubbles: true, deltaY: -10 }));
expect(zoomOf(first)).toBe(0);
});
it("keeps the shared zoom within -6..36", () => {
const first = makeView();
const second = makeView();
first.dispatch({ effects: setFontZoom.of(36) });
stepAllEditorsFontZoom(first, 1);
expect(zoomOf(first)).toBe(36);
expect(zoomOf(second)).toBe(0);
stepAllEditorsFontZoom(first, -1);
expect(zoomOf(first)).toBe(35);
expect(zoomOf(second)).toBe(35);
});
it("leaves views without the zoom field alone", () => {
const first = makeView();
const plain = makeView([]);
stepAllEditorsFontZoom(first, 1);
expect(zoomOf(first)).toBe(1);
expect(zoomOf(plain)).toBeUndefined();
});
it("seeds newly opened editors and resets them all", () => {
const first = makeView();
stepAllEditorsFontZoom(first, 2);
expect(zoomOf(makeView())).toBe(2);
resetAllEditorsFontZoom();
expect(zoomOf(first)).toBe(0);
expect(zoomOf(makeView())).toBe(0);
});
});
+24
Переглянути файл
@@ -324,6 +324,30 @@ describe("build feature", () => {
});
});
it("scopes Build Artifacts to the active file's module, or the whole project without one", async () => {
provideEditorService({
openFile,
saveAll: () => Promise.resolve(),
activePath: () => "/p/core/src/A.java",
} as unknown as EditorService);
await executeAction("build.buildArtifacts", { activate: false });
expect(fake.calls.at(-1)).toMatchObject({ kind: "artifacts", file: "/p/core/src/A.java" });
await act(async () => {
fake.resolve(result([], true));
});
provideEditorService({
openFile,
saveAll: () => Promise.resolve(),
activePath: () => null,
} as unknown as EditorService);
await executeAction("build.buildArtifacts", { activate: false });
expect(fake.calls.at(-1)).toMatchObject({ kind: "artifacts", file: null });
await act(async () => {
fake.resolve(result([], true));
});
});
it("starts a build without a workbench", async () => {
show.mockImplementation(() => {
throw new Error("No workbench");
+3 -3
Переглянути файл
@@ -102,14 +102,14 @@ export function activate(): () => void {
{
id: "build.buildArtifacts",
title: "Build Artifacts",
description: "Packages the project (mvn package, gradle assemble, cargo build --release, npm run build)",
description: "Packages the current module's artifacts, or the whole project when no file is active",
category: "Build",
when: WHEN,
keywords: ["package", "assemble", "jar", "release"],
keywords: ["package", "assemble", "jar", "release", "module"],
run: ({ arg }) => {
const options = typeof arg === "object" && arg !== null ? arg : {};
const activate = !("activate" in options && options.activate === false);
const build = startBuild("artifacts", null, activate);
const build = startBuild("artifacts", moduleTarget(arg), activate);
if ("wait" in options && options.wait === true) return build.then(() => undefined);
},
},
+8
Переглянути файл
@@ -212,6 +212,14 @@ describe("data source types", () => {
expect(labels).toEqual([...labels].sort((a, b) => a.localeCompare(b)));
});
it("adds StarRocks on the MySQL driver with the 9030 preset (#2452)", () => {
expect(vendorPreset(vendorById("starrocks")!)).toEqual({ driver: "mysql", vendor: "starrocks", port: 9030 });
expect(sourceUrl({ driver: "mysql", vendor: "starrocks", host: "sr", port: 9030, database: "" })).toBe(
"jdbc:starrocks://sr:9030",
);
expect(parseSourceUrl("starrocks://[email protected]:9030/test")?.source.vendor).toBe("starrocks");
});
it("falls back to the driver for unknown or mismatched presets", () => {
expect(vendorOf({ driver: "mysql", vendor: "tidb" }).label).toBe("TiDB");
expect(vendorOf({ driver: "postgres", vendor: "tidb" }).id).toBe("postgres");
+10
Переглянути файл
@@ -75,6 +75,15 @@ export const VENDORS: readonly DbVendor[] = [
scheme: "singlestore",
},
{ id: "sqlite", label: "SQLite", driver: "sqlite", port: null, icon: "db-sqlite", scheme: "sqlite" },
{
id: "starrocks",
label: "StarRocks",
driver: "mysql",
port: 9030,
// No dedicated glyph yet: StarRocks speaks the MySQL protocol, so reuse its icon.
icon: "db-mysql",
scheme: "starrocks",
},
{
id: "supabase",
label: "Supabase",
@@ -138,6 +147,7 @@ const SCHEMES: Record<string, string> = {
singlestore: "singlestore",
memsql: "singlestore",
oceanbase: "oceanbase",
starrocks: "starrocks",
oracle: "oracle",
oracledb: "oracle",
sqlserver: "sqlServer",
+9
Переглянути файл
@@ -239,6 +239,15 @@
background: var(--bg-app);
border-left: 1px solid var(--border);
border-right: 1px solid var(--border);
cursor: ew-resize;
touch-action: none;
}
.host :global(.cm-diffDivider:hover) {
background: var(--bg-hover);
}
.host :global(.cm-diffDivider:focus-visible) {
outline: 1px solid var(--accent);
outline-offset: -1px;
}
/* Only the band layer: the chevrons inside the arrow buttons are centred by the button's flex box. */
.host :global(.cm-diffDivider > svg) {
+5 -2
Переглянути файл
@@ -310,7 +310,9 @@
display: inline-flex;
align-items: center;
gap: 8px;
max-width: 50%;
/* May use the whole subject cell when space allows (#2447); shrinks with
per-label ellipsis before crowding out the commit message. */
max-width: 100%;
overflow: hidden;
margin-left: auto;
}
@@ -328,7 +330,8 @@
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
max-width: 180px;
/* Sizes to content (#2447): truncates with ellipsis only when .refs runs
out of room; the full name stays available via the title tooltip. */
}
/* Outlined tag glyph in the ref's colour. */
.ref::before {
+96
Переглянути файл
@@ -0,0 +1,96 @@
import { describe, expect, it } from "vitest";
import type { MergeView } from "@codemirror/merge";
import { Text } from "@codemirror/state";
import type { EditorView } from "@codemirror/view";
import { UnalignedPanes, DIFF_SPLIT_STORAGE_KEY } from "../diffScroll";
const doc = (s: string) => Text.of(s.split("\n"));
function panes(initialRatio = 50) {
try {
localStorage.removeItem(DIFF_SPLIT_STORAGE_KEY);
} catch {
/* ignore */
}
const root = document.createElement("div");
const text = doc(Array.from({ length: 50 }, () => "line").join("\n"));
const view = () => {
const wrapper = root.appendChild(document.createElement("div"));
const dom = wrapper.appendChild(document.createElement("div"));
const scrollDOM = dom.appendChild(document.createElement("div"));
Object.defineProperty(scrollDOM, "scrollHeight", { value: 1000 });
return {
dom,
scrollDOM,
state: { doc: text },
get documentTop() {
return -scrollDOM.scrollTop;
},
lineBlockAt: (pos: number) => ({ bottom: text.lineAt(pos).number * 20 }),
} as unknown as EditorView;
};
const a = view();
const b = view();
const controller = new UnalignedPanes({ sync: true, initialRatio });
controller.attach({ a, b, chunks: [] } as unknown as MergeView);
const divider = root.querySelector(".cm-diffDivider") as HTMLElement;
return { a, b, controller, divider };
}
const down = (el: HTMLElement, clientX: number) =>
el.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true, button: 0, clientX }));
const move = (el: HTMLElement, clientX: number) =>
el.dispatchEvent(new PointerEvent("pointermove", { bubbles: true, clientX }));
const up = (el: HTMLElement) => el.dispatchEvent(new PointerEvent("pointerup", { bubbles: true }));
describe("diff divider dragging (issue #2437)", () => {
it("starts at an even split", () => {
const p = panes();
try {
expect((p.a.dom.parentElement as HTMLElement).style.flex).toBe("50 1 0px");
expect((p.b.dom.parentElement as HTMLElement).style.flex).toBe("50 1 0px");
} finally {
p.controller.destroy();
}
});
it("drags the divider to resize the panes and persists the ratio", () => {
const p = panes();
try {
down(p.divider, 100);
// jsdom has no layout (total falls back to 1px), so a small move clamps to the 90% edge.
move(p.divider, 100.6);
expect((p.a.dom.parentElement as HTMLElement).style.flex).toBe("90 1 0px");
expect((p.b.dom.parentElement as HTMLElement).style.flex).toBe("10 1 0px");
up(p.divider);
expect(localStorage.getItem(DIFF_SPLIT_STORAGE_KEY)).toBe("90");
} finally {
p.controller.destroy();
}
});
it("ignores drags started on arrow buttons", () => {
const p = panes();
try {
const btn = document.createElement("button");
p.divider.appendChild(btn);
btn.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true, button: 0, clientX: 100 }));
move(p.divider, 200);
expect((p.a.dom.parentElement as HTMLElement).style.flex).toBe("50 1 0px");
} finally {
p.controller.destroy();
}
});
it("nudges with arrow keys and resets on double-click", () => {
const p = panes();
try {
p.divider.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "ArrowRight" }));
expect((p.a.dom.parentElement as HTMLElement).style.flex).toBe("52 1 0px");
p.divider.dispatchEvent(new MouseEvent("dblclick", { bubbles: true }));
expect((p.a.dom.parentElement as HTMLElement).style.flex).toBe("50 1 0px");
} finally {
p.controller.destroy();
}
});
});
+32
Переглянути файл
@@ -0,0 +1,32 @@
import { readFileSync } from "node:fs";
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { RefLabels } from "../GitLog";
const css = readFileSync("src/features/vcs/GitLog.module.css", "utf8");
const LONG_LOCAL = "feature-connection-storm-v2";
const LONG_REMOTE = "gitlab/feature-connection-storm-v2";
const REMOTES = new Set([LONG_REMOTE]);
describe("RefLabels long branch names (#2447)", () => {
it("renders full names with title tooltips in regular mode", () => {
render(<RefLabels refs={[LONG_LOCAL, LONG_REMOTE]} remoteNames={REMOTES} />);
expect(screen.getByTitle(LONG_LOCAL)).toHaveTextContent(LONG_LOCAL);
expect(screen.getByTitle(LONG_REMOTE)).toHaveTextContent(LONG_REMOTE);
});
it("absorbs the remote into the local label in compact mode", () => {
render(<RefLabels refs={[LONG_LOCAL, LONG_REMOTE]} compact remoteNames={REMOTES} />);
expect(screen.getByTitle(`${LONG_LOCAL}, ${LONG_REMOTE}`)).toHaveTextContent(`gitlab & ${LONG_LOCAL}`);
});
it("lets labels size to content: no fixed pixel cap, ellipsis and clipping kept", () => {
const refBlocks = [...css.matchAll(/\.ref\s*\{([^}]*)\}/g)].map((m) => m[1]);
expect(refBlocks.length).toBeGreaterThan(0);
for (const block of refBlocks) expect(block).not.toMatch(/max-width:\s*\d+px/);
expect(refBlocks[0]).toMatch(/text-overflow:\s*ellipsis/);
const refsBlock = css.match(/\.refs\s*\{([^}]*)\}/)?.[1] ?? "";
expect(refsBlock).toMatch(/overflow:\s*hidden/);
});
});
+109
Переглянути файл
@@ -146,6 +146,27 @@ export interface UnalignedPanesOptions {
sync: boolean;
/** Divider arrows toward the editable sides; `apply` copies chunk `index` from `source`. */
arrows?: { toA: boolean; toB: boolean; apply(index: number, source: Side): void };
/** Initial left-pane share (percent); defaults to the persisted value, then 50. */
initialRatio?: number;
}
/** localStorage key for the side-by-side pane split, shared by every diff viewer. */
export const DIFF_SPLIT_STORAGE_KEY = "openidea:diff-pane-ratio";
const MIN_RATIO = 10;
const MAX_RATIO = 90;
function loadRatio(fallback = 50): number {
try {
const raw = localStorage.getItem(DIFF_SPLIT_STORAGE_KEY);
if (raw != null) {
const n = Number(raw);
if (Number.isFinite(n)) return Math.min(MAX_RATIO, Math.max(MIN_RATIO, n));
}
} catch {
/* Storage may be unavailable (private mode, SSR). */
}
return fallback;
}
/**
@@ -170,14 +191,25 @@ export class UnalignedPanes {
private frame = -1;
private resize: ResizeObserver | null = null;
private readonly cleanup: (() => void)[] = [];
/** Left pane share (percent) of the side-by-side split; dragging the divider updates it. */
private ratio: number;
constructor(private readonly opts: UnalignedPanesOptions) {
this.sync = opts.sync;
this.ratio =
opts.initialRatio != null && Number.isFinite(opts.initialRatio)
? Math.min(MAX_RATIO, Math.max(MIN_RATIO, opts.initialRatio))
: loadRatio();
this.extension = [gapMarks, changedLineGutters, EditorView.updateListener.of(() => this.schedule())];
this.divider.className = "cm-diffDivider";
// A lone arrow is centred in the divider; a pair keeps to the two edges.
this.divider.classList.toggle("cm-diffDivider-lone", !!opts.arrows && opts.arrows.toA !== opts.arrows.toB);
this.divider.setAttribute("data-wheel-scroll", "");
this.divider.setAttribute("role", "separator");
this.divider.setAttribute("aria-orientation", "vertical");
this.divider.setAttribute("aria-label", "Resize diff panes");
this.divider.setAttribute("title", "Drag to resize panes (double-click to reset)");
this.divider.tabIndex = 0;
this.svg.setAttribute("aria-hidden", "true");
this.svg.appendChild(this.bandLayer);
this.divider.appendChild(this.svg);
@@ -201,6 +233,71 @@ export class UnalignedPanes {
el.removeEventListener("pointerdown", onPointer);
});
}
this.applyRatio();
// Dragging the divider resizes the panes like IDEA's splitter (double-click resets).
let dragX: number | null = null;
let dragRatio = this.ratio;
const persist = () => {
try {
localStorage.setItem(DIFF_SPLIT_STORAGE_KEY, String(this.ratio));
} catch {
/* Storage may be unavailable. */
}
};
const onPointerDown = (e: PointerEvent) => {
if (e.button !== 0 || (e.target as HTMLElement | null)?.closest?.("button")) return;
e.preventDefault();
dragX = e.clientX;
dragRatio = this.ratio;
try {
this.divider.setPointerCapture(e.pointerId);
} catch {
/* jsdom / touch without capture support. */
}
};
const onPointerMove = (e: PointerEvent) => {
if (dragX === null || !this.mv) return;
const editors = this.divider.parentElement;
const total = (editors?.clientWidth ?? 0) - this.divider.offsetWidth || 1;
const next = dragRatio + ((e.clientX - dragX) / total) * 100;
this.ratio = Math.min(MAX_RATIO, Math.max(MIN_RATIO, next));
this.applyRatio();
};
const endDrag = () => {
if (dragX === null) return;
dragX = null;
persist();
this.schedule();
};
const onKeyDown = (e: KeyboardEvent) => {
if (e.key !== "ArrowLeft" && e.key !== "ArrowRight") return;
e.preventDefault();
this.ratio = Math.min(MAX_RATIO, Math.max(MIN_RATIO, this.ratio + (e.key === "ArrowRight" ? 2 : -2)));
this.applyRatio();
persist();
this.schedule();
};
const onDblClick = (e: MouseEvent) => {
if ((e.target as HTMLElement | null)?.closest?.("button")) return;
this.ratio = 50;
this.applyRatio();
persist();
this.schedule();
};
this.divider.addEventListener("pointerdown", onPointerDown);
this.divider.addEventListener("pointermove", onPointerMove);
this.divider.addEventListener("pointerup", endDrag);
this.divider.addEventListener("pointercancel", endDrag);
this.divider.addEventListener("keydown", onKeyDown);
this.divider.addEventListener("dblclick", onDblClick);
this.cleanup.push(() => {
this.divider.removeEventListener("pointerdown", onPointerDown);
this.divider.removeEventListener("pointermove", onPointerMove);
this.divider.removeEventListener("pointerup", endDrag);
this.divider.removeEventListener("pointercancel", endDrag);
this.divider.removeEventListener("keydown", onKeyDown);
this.divider.removeEventListener("dblclick", onDblClick);
});
// The divider scrolls the right pane (and, when synchronized, through it the left one).
const onWheel = (e: WheelEvent) => {
e.preventDefault();
@@ -261,6 +358,18 @@ export class UnalignedPanes {
this.mv = null;
}
/** Applies the current split to the two pane wrappers (flex-grow shares of the free width). */
private applyRatio() {
const mv = this.mv;
if (!mv) return;
const left = mv.a.dom.parentElement;
const right = mv.b.dom.parentElement;
if (!left || !right) return;
left.style.flex = `${this.ratio} 1 0`;
right.style.flex = `${100 - this.ratio} 1 0`;
this.divider.setAttribute("aria-valuenow", String(Math.round(this.ratio)));
}
private view(side: Side): EditorView {
return side === "a" ? this.mv!.a : this.mv!.b;
}
+10 -5
Переглянути файл
@@ -4,7 +4,7 @@ import { act, cleanup, render, screen } from "@testing-library/react";
import { EditorState } from "@codemirror/state";
import { EditorView } from "@codemirror/view";
import { _addEditor, _removeEditor, editorContributions, type EditorDocContext } from "@/editor/api";
import { fontZoom, setFontZoom } from "@/editor/core/cm/prefs";
import { fontZoom, resetAllEditorsFontZoom, setFontZoom } from "@/editor/core/cm/prefs";
import { fontExtension } from "@/editor/core/cm/setup";
import type { EditorSlot } from "@/editor/core/model/slot";
import { executeAction } from "@/platform/actions";
@@ -51,6 +51,7 @@ describe("editor font size actions", () => {
const zoom = (view: EditorView) => view.state.field(fontZoom);
beforeEach(() => {
resetAllEditorsFontZoom();
disposers.push(registerEditorActions(), registerMarkdownLiveSource());
useContextKeys.setState({ projectOpen: true, textEditorActive: true, editorFocus: true, popupOpen: true });
});
@@ -63,26 +64,30 @@ describe("editor font size actions", () => {
useContextKeys.setState({}, true);
});
it("zooms only the active editor, clamped to -6..36", async () => {
it("zooms all open editors together, clamped to -6..36", async () => {
const md = open(mdCtx);
const ts = open(tsCtx);
activate(md);
await executeAction("editor.increaseFontSize");
await executeAction("editor.increaseFontSize");
expect(zoom(md)).toBe(2);
expect(zoom(ts)).toBe(0);
expect(zoom(ts)).toBe(2);
activate(ts);
await executeAction("editor.decreaseFontSize");
expect(zoom(ts)).toBe(-1);
expect(zoom(md)).toBe(2);
expect(zoom(ts)).toBe(1);
expect(zoom(md)).toBe(1);
md.dispatch({ effects: setFontZoom.of(36) });
ts.dispatch({ effects: setFontZoom.of(36) });
await executeAction("editor.increaseFontSize");
expect(zoom(ts)).toBe(36);
expect(zoom(md)).toBe(36);
md.dispatch({ effects: setFontZoom.of(-6) });
ts.dispatch({ effects: setFontZoom.of(-6) });
await executeAction("editor.decreaseFontSize");
expect(zoom(ts)).toBe(-6);
expect(zoom(md)).toBe(-6);
});
it("scales the Markdown preview with its editor and resets with it", async () => {
+10 -4
Переглянути файл
@@ -54,10 +54,16 @@ it.each(resizeObserverMessages)("still times out an unresolved startup after %s"
expect(document.querySelector("textarea")?.value).toContain("Startup did not finish within 30 seconds.");
});
it.each(resizeObserverMessages)("keeps actual errors with the same message fatal: %s", (message) => {
window.dispatchEvent(new ErrorEvent("error", { message, error: new Error(message) }));
expect(document.getElementById("splash")).toBeNull();
expect(document.querySelector("textarea")?.value).toContain(message);
it.each(resizeObserverMessages)("ignores the loop notification delivered as an Error: %s", (message) => {
// WebKit (Tauri on Linux) reports the loop with an Error object; Blink as a bare message.
// No genuine startup failure carries these engine-generated strings.
window.dispatchEvent(new ErrorEvent("error", { message, filename: "tauri://localhost", error: new Error(message) }));
expect(document.getElementById("splash")).not.toBeNull();
expect(document.getElementById("boot-error")).toBeNull();
expect(document.documentElement.dataset.boot).toBeUndefined();
window.dispatchEvent(new Event("openidea:boot-ready"));
vi.advanceTimersByTime(30_000);
expect(document.getElementById("boot-error")).toBeNull();
});
it.each(resizeObserverMessages)("keeps rejections with the same message fatal: %s", (message) => {
+3 -3
Переглянути файл
@@ -113,9 +113,9 @@ export const eclipseWindows: Record<string, string[]> = {
"code.reformat": ["Ctrl+Alt+L", "Shift+Ctrl+F"],
"project.reformatCode": ["Ctrl+Alt+L", "Shift+Ctrl+F"],
"code.unwrap": ["Alt+R"],
"refactor.rename": ["Shift+Alt+R"],
"project.rename": ["Shift+Alt+R"],
"db.rename": ["Shift+Alt+R"],
"refactor.rename": ["Shift+Alt+R", "Shift+F6"],
"project.rename": ["Shift+Alt+R", "Shift+F6"],
"db.rename": ["Shift+Alt+R", "Shift+F6"],
"find.replaceInFile": ["Ctrl+F"],
"find.replaceInPath": [],
"run.rerun": ["Ctrl+F11"],
-1
Переглянути файл
@@ -1476,7 +1476,6 @@
"Package manager": "Administrador de paquetes",
"Package to install": "Paquete a instalar",
"Packages": "Paquetes",
"Packages the project (mvn package, gradle assemble, cargo build --release, npm run build)": "Empaqueta el proyecto (mvn package, gradle assemble, cargo build --release, npm run build)",
"Page size": "Tamaño de página",
"Parameter Info": "Información de parámetros",
"Parameter names": "Nombres de parámetros",
-1
Переглянути файл
@@ -1404,7 +1404,6 @@
"Package manager": "Gestionnaire de paquets",
"Package to install": "Paquet à installer",
"Packages": "Paquets",
"Packages the project (mvn package, gradle assemble, cargo build --release, npm run build)": "Empaquette le projet (mvn package, gradle assemble, cargo build --release, npm run build)",
"Page size": "Taille de page",
"Parameter Info": "Informations sur les paramètres",
"Parameter names": "Noms des paramètres",
-1
Переглянути файл
@@ -1518,7 +1518,6 @@
"Package manager": "패키지 관리자",
"Package to install": "설치할 패키지",
"Packages": "패키지",
"Packages the project (mvn package, gradle assemble, cargo build --release, npm run build)": "프로젝트를 패키징합니다(mvn package, gradle assemble, cargo build --release, npm run build)",
"Page size": "페이지 크기",
"Parameter Info": "매개변수 정보",
"Parameter names": "매개변수 이름",
-1
Переглянути файл
@@ -1466,7 +1466,6 @@
"Package manager": "Menedżer pakietów",
"Package to install": "Pakiet do zainstalowania",
"Packages": "Pakiety",
"Packages the project (mvn package, gradle assemble, cargo build --release, npm run build)": "Pakuje projekt (mvn package, gradle assemble, cargo build --release, npm run build)",
"Page size": "Rozmiar strony",
"Parameter Info": "Informacje o parametrach",
"Parameter names": "Nazwy parametrów",
-1
Переглянути файл
@@ -1466,7 +1466,6 @@
"Package manager": "Gerenciador de pacotes",
"Package to install": "Pacote a instalar",
"Packages": "Pacotes",
"Packages the project (mvn package, gradle assemble, cargo build --release, npm run build)": "Empacota o projeto (mvn package, gradle assemble, cargo build --release, npm run build)",
"Page size": "Tamanho da página",
"Parameter Info": "Informações de parâmetros",
"Parameter names": "Nomes dos parâmetros",
+1 -1
Переглянути файл
@@ -1670,7 +1670,7 @@
"Package manager": "软件包管理器",
"Package to install": "要安装的软件包",
"Packages": "软件包",
"Packages the project (mvn package, gradle assemble, cargo build --release, npm run build)": "打包项目(mvn package、gradle assemble、cargo build --release、npm run build)",
"Packages the current module's artifacts, or the whole project when no file is active": "打包当前模块的产物,无活动文件时则打包整个项目",
"Page size": "页面大小",
"Parameter Info": "形参信息",
"Parameter names": "形参名称",
+1
Переглянути файл
@@ -188,6 +188,7 @@ const UNTRANSLATED = new Set<string>([
"Rustfmt",
"SingleStore",
"SQLite",
"StarRocks",
"Supabase",
"TiDB",
"TimescaleDB",
+1 -1
Переглянути файл
@@ -1017,7 +1017,7 @@ html {
}
/* Solarized and Atom One adaptations. Exact upstream palettes and MIT notices:
* docs/themes.md and THIRD_PARTY_LICENSES.md. The shared mapping covers every
* THIRD_PARTY_LICENSES.md. The shared mapping covers every
* colour token; only palette roles below differ. UI-only states use explicit
* mixes of original colours where the source has no equivalent component.
* Keep before appearance options so accessibility preferences still win.