дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-10 11:45:41 +02:00
Порівняти коміти
8
Коміти
| Автор | SHA1 | Дата | |
|---|---|---|---|
|
|
1699306eaa | ||
|
|
c01a538ea4 | ||
|
|
7cc0d5769d | ||
|
|
9828f18300 | ||
|
|
c580dd7c69 | ||
|
|
da3db5cbd6 | ||
|
|
d1bef52041 | ||
|
|
1d4c0e1cd4 |
No files matched your search
File renamed without changes.
|
До Ширина: | Висота: | Розмір: 86 KiB Після Ширина: | Висота: | Розмір: 86 KiB |
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() };
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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 |
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
@@ -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,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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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.`,
|
||||
);
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "매개변수 이름",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "形参名称",
|
||||
|
||||
@@ -188,6 +188,7 @@ const UNTRANSLATED = new Set<string>([
|
||||
"Rustfmt",
|
||||
"SingleStore",
|
||||
"SQLite",
|
||||
"StarRocks",
|
||||
"Supabase",
|
||||
"TiDB",
|
||||
"TimescaleDB",
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Заблокувати користувача