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

..
Автор SHA1 Повідомлення Дата
l0ng-ai 21338d557c fix(project-view): paste a clipboard image as img.png
Pasting into the Project view only handled files, so a screenshot on the
clipboard did nothing. With no files to paste, Paste now saves the clipboard
image in the target folder as img.png (img_1.png, img_2.png ... when taken)
and selects it; Paste is enabled while the clipboard holds an image.

The image is read natively: public.png, or public.tiff converted, on macOS;
the registered PNG format, or CF_DIB (24/32-bit) encoded as PNG, on Windows;
GTK on Linux as before.

Fixes #3486
2026-11-15 17:09:42 +08:00
82 змінених файлів з 181 додано та 2722 видалено

No files matched your search

+3 -3
Переглянути файл
@@ -98,10 +98,10 @@ jobs:
# Test fixtures write LF files; keep Git from converting them on checkout.
- run: git config --global core.autocrlf false
- run: cargo nextest run --workspace --no-fail-fast
# nextest ran export_bindings, which rewrites src/bindings.ts and the command snapshot.
- name: Bindings and command snapshot are up to date
# nextest ran export_bindings, which rewrites src/bindings.ts.
- name: Bindings are up to date
if: runner.os == 'Linux'
run: git diff --exit-code src/bindings.ts src-tauri/tests/commands.snap
run: git diff --exit-code src/bindings.ts
# The remote development server ships as a static Linux binary (release-build.yml):
# keep that build working.
-3
Переглянути файл
@@ -327,8 +327,6 @@ pub struct RunSettings {
/// Ask before closing a Run tab whose process is still running.
pub confirm_terminate: bool,
pub show_on_start: bool,
/// New Run/Debug tabs start pinned: a later run doesn't take their place once finished.
pub pin_tabs: bool,
/// Grace period between interrupt and kill when stopping a process.
pub termination_timeout_ms: u32,
}
@@ -340,7 +338,6 @@ impl Default for RunSettings {
confirm_rerun: true,
confirm_terminate: true,
show_on_start: true,
pin_tabs: false,
termination_timeout_ms: 3000,
}
}
-30
Переглянути файл
@@ -325,20 +325,6 @@ pub fn migrate(data: &Path, local: &Path) {
if cfg!(target_os = "windows") && data != local {
migrate_entries(data, local);
}
if cfg!(target_os = "windows") {
relative_tool_shims(&local.join("tools"));
}
}
/// Wrappers written before they named their tool relative to themselves hold its absolute
/// path, which cmd.exe can't find through a non-ASCII user directory (#3338).
fn relative_tool_shims(tools: &Path) {
let Ok(entries) = fs::read_dir(tools) else { return };
for dir in entries.flatten().map(|e| e.path()).filter(|p| p.is_dir()) {
if let Err(error) = relocate_shims(&dir, &dir) {
tracing::warn!(%error, ?dir, "cannot make the tool's wrappers relative");
}
}
}
fn migrate_entries(data: &Path, local: &Path) {
@@ -665,22 +651,6 @@ mod tests {
fs::set_permissions(&dir, fs::Permissions::from_mode(0o755)).unwrap();
}
#[test]
fn installed_tool_wrappers_become_relative() {
let tmp = tempfile::tempdir().unwrap();
let tools = tmp.path().join("tools");
let tool = tools.join("taplo");
fs::create_dir_all(tool.join("bin")).unwrap();
let shim = tool.join("bin/taplo.cmd");
fs::write(&shim, format!("@echo off\r\n\"{}\\taplo.exe\" %*\r\n", tool.display())).unwrap();
// A wrapper the user re-saved in another encoding is left alone.
fs::create_dir_all(tools.join("other/bin")).unwrap();
fs::write(tools.join("other/bin/x.cmd"), b"@echo off\r\n\"C:\\\xd5\xc5\" %*\r\n").unwrap();
relative_tool_shims(&tools);
assert_eq!(fs::read_to_string(&shim).unwrap(), "@echo off\r\n\"%~dp0..\\taplo.exe\" %*\r\n");
relative_tool_shims(&tmp.path().join("missing"));
}
#[test]
fn wrappers_accept_mixed_separators_without_rewriting_neighbor_paths() {
let text = "@echo off\r\n\"C:\\old\\tools/demo\\payload.exe\" \"C:\\old\\tools/demo-other\\x\" %*\r\n";
+12 -38
Переглянути файл
@@ -251,12 +251,18 @@ impl KotlinBridge {
}
}
other => {
let (text, notify) = match other {
Ok(Ok(outcome)) => failed_build(&outcome),
Ok(Err(error)) => (error.message, true),
Err(error) => (error.to_string(), true),
let text = match other {
Ok(Ok(outcome)) => outcome
.messages
.iter()
.filter(|m| m.severity == Severity::Error)
.map(|m| m.text.as_str())
.collect::<Vec<_>>()
.join("\n"),
Ok(Err(error)) => error.message,
Err(error) => error.to_string(),
};
self.report(text, notify, &mut diagnostics, &mut last_failure);
self.failure(text, &mut diagnostics, &mut last_failure);
remove_directory(directory).await;
}
}
@@ -270,12 +276,6 @@ impl KotlinBridge {
}
fn failure(&self, text: String, paths: &mut BTreeSet<PathBuf>, previous: &mut String) {
self.report(text, true, paths, previous);
}
/// Publishes a failed build in Problems (on `pom.xml`) and, when `notify`, as a
/// notification once per distinct message.
fn report(&self, text: String, notify: bool, paths: &mut BTreeSet<PathBuf>, previous: &mut String) {
let text = if text.is_empty() { "Kotlin compilation failed".to_owned() } else { text };
let text = format!("{text}\nJava temporarily uses the last successfully compiled Kotlin API.");
self.clear_diagnostics(paths);
@@ -295,7 +295,7 @@ impl KotlinBridge {
deprecated: false,
}],
});
if notify && previous != &text {
if previous != &text {
self.sink.emit(IdeEvent::Notify {
level: NotifyLevel::Error,
title: "Kotlin editor compilation failed".into(),
@@ -306,15 +306,6 @@ impl KotlinBridge {
}
}
/// The error text of a failed bridge build, and whether to notify about it. Compiler errors
/// in the user's own Kotlin already show in its editor and stay in Problems; only a build
/// that failed without one (Maven itself, a missing plugin) is worth a notification.
fn failed_build(outcome: &Outcome) -> (String, bool) {
let errors: Vec<_> = outcome.messages.iter().filter(|m| m.severity == Severity::Error).collect();
let text = errors.iter().map(|m| m.text.as_str()).collect::<Vec<_>>().join("\n");
(text, !errors.iter().any(|m| m.path.is_some()))
}
fn relevant_change(root: &Path, path: &Path, settings: &ide_core::settings::Settings) -> bool {
let input = path.extension().is_some_and(|e| e == "kt")
|| path.file_name().is_some_and(|n| n == "pom.xml" || n == "mvnw" || n == "mvnw.cmd")
@@ -392,23 +383,6 @@ mod tests {
assert!(!relevant_change(dir.path(), &dir.path().join("module/src/build/pkg"), &Default::default()));
}
#[test]
fn source_errors_stay_out_of_notifications() {
let message = |text: &str, path: Option<&str>| ide_build::compile::BuildMessage {
severity: Severity::Error,
text: text.into(),
path: path.map(|p| PathBuf::from(p).into()),
range: None,
code: None,
};
let outcome = |messages| Outcome { projects: Vec::new(), messages, success: false };
let (text, notify) = failed_build(&outcome(vec![message("No value passed", Some("/p/src/A.kt"))]));
assert_eq!((text.as_str(), notify), ("No value passed", false));
let (text, notify) = failed_build(&outcome(vec![message("Plugin not found", None)]));
assert_eq!((text.as_str(), notify), ("Plugin not found", true));
assert!(failed_build(&outcome(Vec::new())).1);
}
#[test]
fn extensionless_root_file_does_not_trigger() {
let dir = tempfile::tempdir().unwrap();
+30 -141
Переглянути файл
@@ -296,142 +296,39 @@ impl crate::Host {
}
}
/// Commands allowed in Restricted Mode, by exact name: those which only inspect/edit source or
/// operate user-owned application state. A new command is denied until deliberately listed here.
/// Commands which only inspect/edit source or operate user-owned application state.
/// New execution commands are denied until deliberately classified here.
pub fn restricted_command_allowed(cmd: &str) -> bool {
if [
"fs_",
"doc_",
"syntax_",
"search_",
"nav_",
"history_",
"spell_",
"analysis_",
"structural_",
"refactor_",
"bytes_",
"window_",
"window_group_",
"term_",
"settings_",
"project_trust_",
// New Project writes only where the user chose and runs no code of an open project.
"scaffold_",
"ssh_",
"git_credential",
]
.iter()
.any(|p| cmd.starts_with(p))
{
return true;
}
matches!(
cmd,
"fs_compare_directories"
| "fs_copy"
| "fs_copy_merge"
| "fs_create_dir"
| "fs_create_file"
| "fs_create_file_bytes"
| "fs_delete"
| "fs_entry_kind"
| "fs_open_with_system"
| "fs_probe"
| "fs_read_dir"
| "fs_read_image"
| "fs_read_range"
| "fs_read_text"
| "fs_rename"
| "fs_resolve_reference"
| "fs_set_writable"
| "doc_close"
| "doc_edit"
| "doc_folding"
| "doc_highlights"
| "doc_open"
| "doc_open_untitled"
| "doc_open_virtual"
| "doc_outline"
| "doc_python_facts"
| "doc_save"
| "doc_save_as"
| "doc_save_all"
| "doc_selection_ranges"
| "doc_set_bom"
| "doc_set_encoding"
| "doc_set_line_ending"
| "doc_snapshot"
| "doc_sync_from_disk"
| "doc_ui_mode"
| "syntax_errors"
| "syntax_expression_at"
| "syntax_identifiers"
| "syntax_identifiers_in_files"
| "syntax_locals"
| "syntax_node_at"
| "syntax_structural_ranges"
| "syntax_support"
| "search_start"
| "search_validate"
| "nav_index_definitions"
| "nav_inheritance"
| "nav_resolve_module"
| "history_content"
| "history_list"
| "history_list_dir"
| "history_put_label"
| "history_restore"
| "spell_check"
| "spell_suggest"
| "analysis_locate_duplicates"
| "structural_languages"
| "structural_search"
| "structural_validate"
| "refactor_can_undo_file_move"
| "refactor_file_move_edits"
| "refactor_file_move_text_edits"
| "refactor_move_files"
| "refactor_undo_file_move"
| "window_close_context"
| "window_complete_launch"
| "window_cycle"
| "window_flash_taskbar"
| "window_group_cancel_close"
| "window_group_close"
| "window_group_close_tabs"
| "window_group_current"
| "window_group_detach"
| "window_group_merge"
| "window_group_reorder"
| "window_group_restore"
| "window_group_select"
| "window_is_fullscreen"
| "window_new"
| "window_open_comparison"
| "window_open_deployment"
| "window_open_diff"
| "window_open_file_in_new_window"
| "window_open_files"
| "window_open_project"
| "window_open_projects"
| "window_open_remote"
| "window_open_settings"
| "window_set_titlebar_height"
| "window_start_drag"
| "window_take_launch"
| "window_toggle_fullscreen"
| "window_toggle_zoom"
| "term_available_shells"
| "term_default_shell"
| "term_is_busy"
| "term_kill"
| "term_resize"
| "term_spawn"
| "term_store_image"
| "term_write"
| "settings_export"
| "settings_get"
| "settings_get_global"
| "settings_import"
| "settings_list_jetbrains_ides"
| "settings_read_jetbrains"
| "settings_set"
| "project_trust_add_location"
| "project_trust_list"
| "project_trust_remove_location"
| "project_trust_set"
| "project_trust_status"
// New Project writes only where the user chose and runs no code of an open project.
| "scaffold_catalog"
| "scaffold_create"
| "scaffold_options"
| "scaffold_requirements"
| "ssh_askpass_answer"
| "ssh_connected"
| "ssh_disconnect"
| "ssh_needs_secret"
| "ssh_open_terminal"
| "ssh_password_saved"
| "ssh_set_secret"
| "ssh_test"
| "ssh_trust_host"
| "git_credential_forget"
| "git_credentials"
| "app_info"
"app_info"
| "app_build_info"
| "app_cli_install"
| "app_cli_uninstall"
@@ -676,14 +573,6 @@ mod tests {
assert!(super::restricted_command_allowed("clipboard_write_text"));
}
#[test]
fn restricted_mode_denies_new_commands_whatever_their_name() {
for cmd in ["fs_new_command", "term_new_command", "ssh_new_command", "settings_new_command", "git_credential_x"]
{
assert!(!super::restricted_command_allowed(cmd), "{cmd} is allowed without being listed");
}
}
#[test]
fn remote_project_decisions_are_kept_on_this_machine() {
let tmp = tempfile::tempdir().unwrap();
-171
Переглянути файл
@@ -12,7 +12,6 @@ pub(super) fn detect(dir: &Path, out: &mut Vec<RunConfig>) {
let Some(src) = read(&dir.join("build.zig")) else { return };
let src = code_only(&src);
out.push(config(BUILD_ID, "zig build", "zig", "zig", &["build"]));
let roots = test_roots(&src);
for step in steps(&src) {
// `install` is what plain `zig build` runs.
if step == "install" || step == "uninstall" {
@@ -21,9 +20,6 @@ pub(super) fn detect(dir: &Path, out: &mut Vec<RunConfig>) {
let mut c = config(format!("zig:{step}"), format!("zig build {step}"), "zig", "zig", &["build", &step]);
if step == "test" || step.starts_with("test-") || step.starts_with("test_") {
c = c.test();
if let Some((_, root)) = roots.iter().find(|(name, _)| *name == step) {
c.extras.params.insert("testRoot".into(), root.clone());
}
}
out.push(c);
}
@@ -165,137 +161,6 @@ fn executables(src: &str) -> Vec<Executable> {
out
}
/// The call's argument text: from just after its `(` to the matching `)`.
fn call_body(src: &str, start: usize) -> &str {
let (mut depth, mut in_string, mut escaped) = (1usize, false, false);
for (i, c) in src[start..].char_indices() {
if in_string {
match c {
_ if escaped => escaped = false,
'\\' => escaped = true,
'"' => in_string = false,
_ => {}
}
continue;
}
match c {
'"' => in_string = true,
'(' | '{' | '[' => depth += 1,
')' | '}' | ']' => {
depth -= 1;
if depth == 0 {
return &src[start..start + i];
}
}
_ => {}
}
}
src
}
/// The first `.root_source_file = <receiver>.path("…")` in `body`.
fn root_source_file(body: &str) -> Option<String> {
for (i, _) in body.match_indices(".root_source_file") {
let Some(v) = body[i + ".root_source_file".len()..].trim_start().strip_prefix('=') else { continue };
let v = v.trim_start();
if let Some(call) = v.find(".path(")
&& v[..call].chars().all(|c| c.is_alphanumeric() || c == '_')
{
return string_at(&v[call + ".path(".len()..]);
}
}
None
}
/// The identifier assigned just before `at`: `const mod = b.addModule(…` → `mod`.
fn assigned_var(src: &str, at: usize) -> Option<&str> {
let before = src[..at].trim_end();
let before = before.strip_suffix('.')?.trim_end();
// Drop the receiver (`b`) that sits between the `=` and the call.
let receiver =
before.char_indices().rev().take_while(|(_, c)| c.is_alphanumeric() || *c == '_').last().map(|(i, _)| i)?;
let before = before[..receiver].trim_end();
let before = before.strip_suffix('=')?.trim_end();
let start =
before.char_indices().rev().take_while(|(_, c)| c.is_alphanumeric() || *c == '_').last().map(|(i, _)| i)?;
Some(&before[start..])
}
/// The test root behind each test-named step (`zig build <step>`): the step's `dependOn` runs
/// an `addTest` artifact — a variable (`const tests = b.addTest(…)`) or an inline call — whose
/// module's `root_source_file` this resolves (through `addModule`/`createModule` variables).
/// `zig build` forwards no args to the test binary, so rerunning a single failed test goes
/// through a standalone `zig test <root> --test-filter …` on the step's own test module.
fn test_roots(src: &str) -> Vec<(String, String)> {
let mut modules: Vec<(String, String)> = Vec::new();
for key in ["addModule(", "createModule("] {
for (i, _) in src.match_indices(key) {
let Some(root) = root_source_file(call_body(src, i + key.len())) else { continue };
if let Some(var) = assigned_var(src, i)
&& !modules.iter().any(|(m, _)| *m == var)
{
modules.push((var.to_owned(), root));
}
}
}
let module_root = |body: &str| -> Option<String> {
root_source_file(body).or_else(|| {
let at = body.find(".root_module")?;
let v = body[at + ".root_module".len()..].trim_start().strip_prefix('=')?.trim_start();
let var: String = v.chars().take_while(|c| c.is_alphanumeric() || *c == '_').collect();
modules.iter().find(|(m, _)| *m == var).map(|(_, root)| root.clone())
})
};
let mut tests: Vec<(String, String)> = Vec::new();
for (i, _) in src.match_indices("addTest(") {
let Some(root) = module_root(call_body(src, i + "addTest(".len())) else { continue };
if let Some(var) = assigned_var(src, i)
&& !tests.iter().any(|(v, _)| *v == var)
{
tests.push((var.to_owned(), root));
}
}
let mut steps: Vec<(String, String)> = Vec::new();
for (i, _) in src.match_indices(".step(\"") {
let Some(name) = string_at(&src[i + ".step(".len()..]) else { continue };
// The match includes the dot, so the assignment ends one character earlier.
if let Some(var) = assigned_var(src, i + 1) {
steps.push((var.to_owned(), name));
}
}
let mut out: Vec<(String, String)> = Vec::new();
for (i, _) in src.match_indices(".dependOn(&") {
let Some(step) = receiver_var(src, i + 1) else { continue };
let Some((_, name)) = steps.iter().find(|(v, _)| v == step) else { continue };
let after = &src[i + ".dependOn(&".len()..];
let artifact = if let Some(at) = after.find("addRunArtifact(") {
let v = &after[at + "addRunArtifact(".len()..];
let var: String = v.chars().take_while(|c| c.is_alphanumeric() || *c == '_').collect();
tests.iter().find(|(v, _)| *v == var).map(|(_, root)| root.clone())
} else if let Some(at) = after.find("addTest(") {
module_root(call_body(after, at + "addTest(".len()))
} else {
None
};
if let Some(root) = artifact
&& name.starts_with("test")
&& !out.iter().any(|(n, _)| n == name)
{
out.push((name.clone(), root));
}
}
out
}
/// The identifier a method is called on just before `at`: `test_step.dependOn(…` → `test_step`.
fn receiver_var(src: &str, at: usize) -> Option<&str> {
let before = src[..at].trim_end();
let before = before.strip_suffix('.')?;
let start =
before.char_indices().rev().take_while(|(_, c)| c.is_alphanumeric() || *c == '_').last().map(|(i, _)| i)?;
Some(&before[start..])
}
#[cfg(test)]
mod tests {
use super::*;
@@ -325,9 +190,7 @@ pub fn build(b: *std.Build) void {
}) });
_ = tool;
const run_step = b.step("run", "Run the app");
const tests = b.addTest(.{ .root_module = mod });
const test_step = b.step("test", "Run tests");
test_step.dependOn(&b.addRunArtifact(tests).step);
const docs = b.step("docs", "Generate docs // not a comment");
_ = .{ run_step, test_step, docs };
}
@@ -365,39 +228,5 @@ pub fn build(b: *std.Build) void {
assert_eq!(app.extras.before_launch[0].kind, "run");
assert_eq!(app.extras.before_launch[0].target, BUILD_ID);
assert_eq!(app.extras.params.get("source").map(String::as_str), Some("src/main.zig"));
assert_eq!(test.extras.params.get("testRoot").map(String::as_str), Some("src/root.zig"));
}
const TEST_ROOTS: &str = r#"const std = @import("std");
pub fn build(b: *std.Build) void {
const tally = b.addModule("tally", .{ .root_source_file = b.path("src/tally.zig") });
const unit = b.addTest(.{ .root_module = tally });
const test_step = b.step("test", "Run the unit tests");
test_step.dependOn(&b.addRunArtifact(unit).step);
const failing = b.addTest(.{ .root_module = b.createModule(.{
.root_source_file = b.path("src/failing.zig"),
}) });
const test_failing_step = b.step("test-failing", "Run the failing tests");
test_failing_step.dependOn(&b.addRunArtifact(failing).step);
const direct = b.addTest(.{ .root_source_file = b.path("src/direct.zig") });
const test_direct_step = b.step("test-direct", "Run the direct tests");
test_direct_step.dependOn(&b.addRunArtifact(direct).step);
const run_step = b.step("run", "Run the app");
run_step.dependOn(&b.addRunArtifact(unit).step);
}
"#;
#[test]
fn test_roots_come_from_the_step_s_own_artifact() {
let src = code_only(TEST_ROOTS);
assert_eq!(
test_roots(&src),
[
("test".to_owned(), "src/tally.zig".to_owned()),
("test-failing".to_owned(), "src/failing.zig".to_owned()),
("test-direct".to_owned(), "src/direct.zig".to_owned()),
]
);
}
}
+3 -28
Переглянути файл
@@ -331,8 +331,8 @@ pub fn env_wrapper(bin_dir: &Path, command: &str, target: &Path, env: &[(&str, &
#[cfg(not(unix))]
{
let link = bin_dir.join(format!("{command}.cmd"));
let vars: String = env.iter().map(|(k, v)| format!("set \"{k}={}\"\r\n", cmd_path(bin_dir, v))).collect();
fs::write(&link, format!("@echo off\r\nsetlocal\r\n{vars}\"{}\" %*\r\n", cmd_path(bin_dir, target)))?;
let vars: String = env.iter().map(|(k, v)| format!("set \"{k}={}\"\r\n", v.display())).collect();
fs::write(&link, format!("@echo off\r\nsetlocal\r\n{vars}\"{}\" %*\r\n", target.display()))?;
Ok(link)
}
}
@@ -357,25 +357,11 @@ pub fn link_executable(bin_dir: &Path, command: &str, target: &Path) -> IdeResul
{
let link = bin_dir.join(format!("{command}.cmd"));
let runner = if target.extension().is_some_and(|e| e == "js") { "node " } else { "" };
fs::write(&link, format!("@echo off\r\n{runner}\"{}\" %*\r\n", cmd_path(bin_dir, target)))?;
fs::write(&link, format!("@echo off\r\n{runner}\"{}\" %*\r\n", target.display()))?;
Ok(link)
}
}
/// `path` as a `.cmd` shim in `bin_dir` names it: relative to the shim (`%~dp0..\…`) when it
/// is in the tool's own directory. cmd.exe reads a batch file in the console's code page, so
/// an absolute path through a non-ASCII user directory, written as UTF-8, isn't found (#3338).
#[cfg_attr(unix, allow(dead_code))]
fn cmd_path(bin_dir: &Path, path: &Path) -> String {
match bin_dir.parent().and_then(|tool| path.strip_prefix(tool).ok()) {
Some(rest) => {
let rest: Vec<_> = rest.components().map(|c| c.as_os_str().to_string_lossy()).collect();
format!("%~dp0..\\{}", rest.join("\\"))
}
None => path.display().to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -757,17 +743,6 @@ mod tests {
assert!(found.ends_with("server/com.microsoft.java.debug.plugin-0.53.2.jar"));
}
#[test]
fn windows_shims_name_the_tool_relative_to_themselves() {
let tool = Path::new("/Users/张三/AppData/Local/ai.l0ng.openidea/tools/taplo");
let bin = tool.join("bin");
assert_eq!(cmd_path(&bin, &tool.join("taplo.exe")), "%~dp0..\\taplo.exe");
assert_eq!(cmd_path(&bin, &tool.join("gems/bin/solargraph")), "%~dp0..\\gems\\bin\\solargraph");
// Outside the tool's directory (a system SDK) stays absolute.
let sdk = Path::new("/usr/local/share/dotnet");
assert_eq!(cmd_path(&bin, sdk), sdk.display().to_string());
}
#[cfg(unix)]
#[test]
fn wrapper_runs_target() {
+2 -2
Переглянути файл
@@ -197,5 +197,5 @@ You don't write anything special for remote development. In a remote window, `sr
1. Put the logic in the owning crate; convert errors to `IdeError` at its edge; use IPC coordinates.
2. Add the shared implementation to `ide-host::ops::<domain>` if it touches a project; register it with the remote dispatcher if remote windows need it.
3. Add the `#[tauri::command] #[specta::specta]` adapter in `src-tauri/src/commands/<domain>.rs` and list it in `collect_commands!`.
4. Decide its trust class: list it by exact name in `restricted_command_allowed` (also in Restricted Mode) or `projectless_command_allowed` (also with no project open), or leave it out to require a trusted project. There are no name prefixes; if it executes anything, leave it out.
5. `pnpm gen:bindings`, then call it with `call(commands.yourCommand(...))`. It also rewrites `src-tauri/tests/commands.snap`; check that the new line shows the trust class and placement you meant.
4. If it executes anything, leave it out of the restricted-mode allowlist (it is denied in untrusted projects by default).
5. `pnpm gen:bindings`, then call it with `call(commands.yourCommand(...))`.
-2
Переглянути файл
@@ -28,8 +28,6 @@ Style components with CSS Modules: `Thing.module.css` next to `Thing.tsx`, impor
Build from the primitives in `src/ui/` before writing your own: `Button`, `BarButton`, `Input`, `Checkbox`, `Select`, `Dialog`, `Popup`, `Menu`, `Tooltip`, `List`, `Tree`, `Tabs`, `Splitter`, `Scrollbar`, `Spinner`, `Kbd`, and `T` for translated text. They already handle focus, keyboard navigation, theming and translation of their labels. Workbench-level pieces (tool windows, status items, overlays) come from [contributions](/en/contribute/architecture/frontend#contributions), not from custom layout.
Toolbar icon actions use `IconButton`: its full hit target uses `--bg-toolbar-hover` on hover, while pressed and selected states use their corresponding toolbar tokens. Keep semantic icon colours and the keyboard focus ring intact.
## Accessibility
- Use real `<button>` and `<input>` elements (or the primitives that render them), never clickable `<div>`s.
+1 -1
Переглянути файл
@@ -31,7 +31,7 @@ Opening a project folder can run code that the project controls: language server
- When a project that hasn't been decided opens, the **Trust and Open Project?** dialog offers **Trust Project**, **Preview in Restricted Mode** and **Don't Open**. Decisions and trusted locations are managed on Settings' **Project Trust** page (group **Security**), and **Revoke Trust** is an action.
- Trust is user-owned state (`crates/ide-host/src/trust.rs`). **Project settings never grant trust.**
- Enforcement is in the backend, not the UI: every IPC command passes `Host::authorize_command` in the Tauri invoke handler (`src-tauri/src/lib.rs`) before it runs. In an untrusted project only commands on an explicit allowlist (`restricted_command_allowed`) run: reading and editing files and documents, syntax, search, navigation, local history, spelling, analysis, refactoring, settings and similar.
- The allowlist is a default-deny list of exact names: **a new command that executes anything is denied in restricted mode until you deliberately classify it there.** Don't add an execution command to the allowlist to make a feature "work" in restricted mode. `src-tauri/tests/commands.snap` lists every command's trust class, so a change shows up in review.
- The allowlist is a default-deny list: **a new command that executes anything is denied in restricted mode until you deliberately classify it there.** Don't add an execution command to the allowlist to make a feature "work" in restricted mode.
- Below the command layer, language servers don't start (`LspManager` execution switch), and git runs with execution disabled and without the project's `git` executable setting until the project is trusted. Project plugins (`.openidea/plugins/`) load only for trusted projects.
- In a remote window the desktop checks the remote project's trust before forwarding a command, and `openidea-server` authorizes every `invoke` again.
+6 -6
Переглянути файл
@@ -7,12 +7,12 @@ Smoke tests check each language end to end with the real tools the IDE uses: lan
They are not part of `cargo nextest run` or `pnpm test`, because they need the tools installed. CI runs them in the **Language smoke** workflow with pinned versions.
| Suite | What it checks | Languages |
| ----------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------ |
| `crates/ide-host/tests/language_smoke.rs` | Code intelligence through the `ops` the app's commands call | typescript, python, rust, go, java, kotlin, php |
| `crates/ide-host/tests/project_smoke.rs` | Opening a fixture with its build tool and SDK | `maven_sync`, `jdk`, `gradle_sync`, `python_venv` |
| `smoke/run.test.ts` | Run and test configurations through the run store and a real `openidea-server` | typescript, python, rust, go, java, kotlin, php, zig |
| `smoke/debug.test.ts` | Debugging the entry point with the debugger the IDE installs | typescript, python, rust, go, java, kotlin, c, cpp, zig, php |
| Suite | What it checks | Languages |
| ----------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------- |
| `crates/ide-host/tests/language_smoke.rs` | Code intelligence through the `ops` the app's commands call | typescript, python, rust, go, java, kotlin, php |
| `crates/ide-host/tests/project_smoke.rs` | Opening a fixture with its build tool and SDK | `maven_sync`, `jdk`, `gradle_sync`, `python_venv` |
| `smoke/run.test.ts` | Run and test configurations through the run store and a real `openidea-server` | typescript, python, rust, go, java, kotlin, php |
| `smoke/debug.test.ts` | Debugging the entry point with the debugger the IDE installs | typescript, python, rust, go, java, kotlin, c, cpp, php |
## Language smoke
+2 -2
Переглянути файл
@@ -197,5 +197,5 @@ onIdeEvent("projectTrustChanged", (e) => {
1. 逻辑放在所属的 crate 里;在其边界把错误转成 `IdeError`;使用 IPC 坐标。
2. 如果涉及项目,把共享实现加到 `ide-host::ops::<domain>`;远程窗口需要的话,注册到远程 dispatcher。
3. 在 `src-tauri/src/commands/<domain>.rs` 添加 `#[tauri::command] #[specta::specta]` 适配函数,并加入 `collect_commands!`。
4. 决定它的信任级别:在 `restricted_command_allowed`(受限模式下也可运行)或 `projectless_command_allowed`(未打开项目时也可运行)中按完整名称列出它,或者不列出,即需要受信任的项目。没有名称前缀规则;会执行任何东西的命令不要列出。
5. 运行 `pnpm gen:bindings`,然后用 `call(commands.yourCommand(...))` 调用。它也会重写 `src-tauri/tests/commands.snap`;检查新增的那一行是否是你想要的信任级别和运行位置。
4. 如果它会执行任何东西,不要把它加进受限模式白名单(未受信任的项目里默认拒绝)。
5. 运行 `pnpm gen:bindings`,然后用 `call(commands.yourCommand(...))` 调用。
-2
Переглянути файл
@@ -28,8 +28,6 @@ description: "前端 UI 代码的规则:设计 token、CSS Modules、图标、
先用 `src/ui/` 里的基础组件,再考虑自己写:`Button`、`BarButton`、`Input`、`Checkbox`、`Select`、`Dialog`、`Popup`、`Menu`、`Tooltip`、`List`、`Tree`、`Tabs`、`Splitter`、`Scrollbar`、`Spinner`、`Kbd`,以及用于翻译文本的 `T`。它们已经处理好焦点、键盘导航、主题和标签翻译。工作台级别的部件(工具窗口、状态栏项、浮层)来自 [contribution](/zh/contribute/architecture/frontend),不要自己写布局。
工具栏图标操作使用 `IconButton`:悬停时整个点击区域使用 `--bg-toolbar-hover`,按下和选中状态使用对应的工具栏 token。保留图标的语义颜色和键盘焦点环。
## 无障碍
- 使用真正的 `<button>` 和 `<input>` 元素(或渲染它们的基础组件),不要用可点击的 `<div>`。
+1 -1
Переглянути файл
@@ -28,7 +28,7 @@ description: "如何报告漏洞,以及贡献者需要维护的安全模型:
- 打开一个尚未做过决定的项目时,会弹出 **信任并打开项目?** 对话框,提供 **信任项目**、**以受限模式预览** 和 **不打开**。已有决定和受信任的位置在设置的 **项目信任** 页面(**安全** 分组)里管理,**撤销信任** 是一个 action。
- 信任是用户自己的状态(`crates/ide-host/src/trust.rs`)。**项目设置永远不能授予信任。**
- 拦截在后端而不是 UI:每个 IPC 命令执行前,都要在 Tauri 的 invoke handler(`src-tauri/src/lib.rs`)里经过 `Host::authorize_command`。未受信任的项目里,只有显式白名单(`restricted_command_allowed`)中的命令能运行:读写文件和文档、语法、搜索、导航、本地历史、拼写、分析、重构、设置等。
- 这份白名单按完整名称列出,默认拒绝:**新增的任何会执行东西的命令,在你有意识地把它归类之前,受限模式下一律拒绝。** 不要为了让某个功能在受限模式下“能用”就把执行类命令加进白名单。`src-tauri/tests/commands.snap` 列出每个命令的信任级别,改动会出现在审查的 diff 里。
- 这份白名单是默认拒绝的:**新增的任何会执行东西的命令,在你有意识地把它归类之前,受限模式下一律拒绝。** 不要为了让某个功能在受限模式下“能用”就把执行类命令加进白名单。
- 在命令层之下,项目受信任之前语言服务器不会启动(`LspManager` 的执行开关),git 以禁止执行的方式运行,也不使用项目里配置的 `git` 可执行文件。项目插件(`.openidea/plugins/`)只在受信任的项目中加载。
- 远程窗口中,桌面端在转发命令前会先确认远程项目是否受信任,`openidea-server` 对每个 `invoke` 还会再校验一次。
+6 -6
Переглянути файл
@@ -7,12 +7,12 @@ description: "在 fixtures 示例项目上,用真实语言服务器跑语言
由于需要安装这些工具,冒烟测试不包含在 `cargo nextest run` 或 `pnpm test` 中。CI 在 **Language smoke** 工作流里用固定版本运行它们。
| 套件 | 检查内容 | 语言 |
| ----------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
| `crates/ide-host/tests/language_smoke.rs` | 经由应用命令所调用的 `ops` 检查代码智能 | typescript、python、rust、go、java、kotlin、php |
| `crates/ide-host/tests/project_smoke.rs` | 用构建工具和 SDK 打开示例项目 | `maven_sync`、`jdk`、`gradle_sync`、`python_venv` |
| `smoke/run.test.ts` | 通过 run store 和真实的 `openidea-server` 执行运行与测试配置 | typescript、python、rust、go、java、kotlin、php、zig |
| `smoke/debug.test.ts` | 用 IDE 安装的调试器调试入口点 | typescript、python、rust、go、java、kotlin、c、cpp、zig、php |
| 套件 | 检查内容 | 语言 |
| ----------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------- |
| `crates/ide-host/tests/language_smoke.rs` | 经由应用命令所调用的 `ops` 检查代码智能 | typescript、python、rust、go、java、kotlin、php |
| `crates/ide-host/tests/project_smoke.rs` | 用构建工具和 SDK 打开示例项目 | `maven_sync`、`jdk`、`gradle_sync`、`python_venv` |
| `smoke/run.test.ts` | 通过 run store 和真实的 `openidea-server` 执行运行与测试配置 | typescript、python、rust、go、java、kotlin、php |
| `smoke/debug.test.ts` | 用 IDE 安装的调试器调试入口点 | typescript、python、rust、go、java、kotlin、c、cpp、php |
## 语言冒烟测试
+2 -64
Переглянути файл
@@ -174,36 +174,12 @@ test("⌘R replaces every match in the file", async ({ workbench: page }) => {
await expect(editor.locator(".cm-line", { hasText: "OrderDto" })).toHaveCount(2);
});
test("Markdown preview highlights ⌘F matches, selected words and scales diagrams with font zoom", async ({
workbench: page,
}) => {
await page.evaluate(async () => {
const svg =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 96" width="320" height="96"><!-- svg-source:excalidraw --><rect width="320" height="96"/></svg>';
const url = `data:image/svg+xml;base64,${btoa(svg)}`;
const routeModule = "/src/platform/tauriCore.ts";
const { setInvokeRoute } = await import(/* @vite-ignore */ routeModule);
setInvokeRoute((cmd: string) => (cmd === "fs_read_image" ? Promise.resolve(url) : null));
});
test("Markdown preview highlights ⌘F matches and selected word occurrences", async ({ workbench: page }) => {
const editor = await openFromTree(page, "README.md");
const content = editor.locator(".cm-content");
await content.click();
await page.keyboard.press("Meta+a");
await page.keyboard.insertText(
[
"# Order Service",
"",
"Manage **Order** items and `Order` status.",
"",
"```mermaid",
"flowchart LR",
" A --> B",
"```",
"",
"![Diagram](./arch.excalidraw.svg)",
].join("\n"),
);
await page.keyboard.insertText("# Order Service\n\nManage **Order** items and `Order` status.\n");
const preview = page.getByRole("document", { name: "Markdown preview" });
await expect(preview).toContainText("Manage Order items");
@@ -233,44 +209,6 @@ test("Markdown preview highlights ⌘F matches, selected words and scales diagra
})),
)
.toEqual({ current: ["Order"], other: ["Order", "Order"] });
const mermaidSvg = preview.locator(".mermaid-output svg");
const excalidrawImg = preview.locator('img[alt="Diagram"]');
await expect(mermaidSvg).toBeVisible();
await expect(excalidrawImg).toHaveAttribute("src", /^data:image\/svg\+xml/);
const widths = async () => ({
mermaid: (await mermaidSvg.boundingBox())!.width,
excalidraw: (await excalidrawImg.boundingBox())!.width,
});
const base = await widths();
expect(base.mermaid).toBeGreaterThan(50);
expect(base.excalidraw).toBeCloseTo(320, 0);
const runAction = (id: string, times = 1) =>
page.evaluate(
async ({ id, times }) => {
const actionsModule = "/src/platform/actions.ts";
const { executeAction } = await import(/* @vite-ignore */ actionsModule);
for (let i = 0; i < times; i++) await executeAction(id);
},
{ id, times },
);
await runAction("editor.increaseFontSize", 4);
const zoomedIn = await widths();
expect(zoomedIn.mermaid / base.mermaid).toBeCloseTo(17 / 13, 2);
expect(zoomedIn.excalidraw / base.excalidraw).toBeCloseTo(17 / 13, 2);
await runAction("editor.decreaseFontSize", 6);
const zoomedOut = await widths();
expect(zoomedOut.mermaid / base.mermaid).toBeCloseTo(11 / 13, 2);
expect(zoomedOut.excalidraw / base.excalidraw).toBeCloseTo(11 / 13, 2);
await runAction("editor.resetFontSize");
const reset = await widths();
expect(reset.mermaid).toBeCloseTo(base.mermaid, 1);
expect(reset.excalidraw).toBeCloseTo(base.excalidraw, 1);
});
test("the bookmark gutter takes no width until the file has a bookmark", async ({ workbench: page }) => {
-91
Переглянути файл
@@ -1,91 +0,0 @@
import { expect, openFromTree, test } from "./fixtures";
for (const theme of ["light", "dark"] as const) {
test(`${theme}: toolbar icon buttons provide hover feedback without changing their layout or icons`, async ({
workbench: page,
}) => {
const gear = page.getByRole("button", { name: "IDE and Project Settings", exact: true });
await gear.click();
await page.getByRole("menuitem", { name: "Theme", exact: true }).hover();
await page
.getByRole("menuitemcheckbox", { name: `Islands ${theme === "light" ? "Light" : "Dark"}`, exact: true })
.click();
await expect(page.locator("html")).toHaveAttribute("data-theme", theme);
const toolbar = page.locator("header").first();
const disabledRun = toolbar.getByRole("button", { name: "Run", exact: true });
await expect(disabledRun).toBeDisabled();
await disabledRun.hover();
await expect(disabledRun).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
await openFromTree(page, "App.tsx");
await page.locator("[data-run-chooser]").click();
await page.getByRole("option", { name: "Current File", exact: true }).click();
const project = page.getByRole("region", { name: "Project", exact: true });
const buttons = [
...["Select Opened File", "Expand All", "Collapse All", "Options", "Hide"].map((name) =>
project.getByRole("button", { name, exact: true }),
),
...[
"Run Current File",
"Debug Current File",
"More Actions",
"Search Everywhere",
"IDE and Project Settings",
].map((name) => toolbar.getByRole("button", { name, exact: true })),
];
for (const button of buttons) {
await page.mouse.move(700, 500);
const bounds = await button.boundingBox();
const iconColors = await button.locator("svg [fill], svg [stroke]").evaluateAll((nodes) =>
nodes.map((node) => {
const style = getComputedStyle(node);
return { fill: style.fill, stroke: style.stroke };
}),
);
await expect(button).toBeEnabled();
// Hover inside the hit target but outside its centred icon.
await button.hover({ position: { x: bounds!.width / 2, y: 1 } });
await expect(button).not.toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
expect(await button.boundingBox()).toEqual(bounds);
if (/^(Run|Debug) Current File$/.test((await button.getAttribute("aria-label")) ?? "")) {
expect(
await button.locator("svg [fill], svg [stroke]").evaluateAll((nodes) =>
nodes.map((node) => {
const style = getComputedStyle(node);
return { fill: style.fill, stroke: style.stroke };
}),
),
).toEqual(iconColors);
}
await button.focus();
await page.keyboard.press("Shift+Tab");
await page.keyboard.press("Tab");
await expect(button).toBeFocused();
await expect(button).toHaveCSS("outline-style", "solid");
await expect(button).toHaveCSS("outline-width", "2px");
}
const expand = project.getByRole("button", { name: "Expand All", exact: true });
await expand.hover();
const hover = await expand.evaluate((button) => getComputedStyle(button).backgroundColor);
await page.mouse.down();
await expect(expand).not.toHaveCSS("background-color", hover);
await page.mouse.up();
await expect(project.getByRole("treeitem", { name: "src", exact: true })).toHaveAttribute("aria-expanded", "true");
await project.getByRole("button", { name: "Collapse All", exact: true }).click();
await expect(project.getByRole("treeitem", { name: "src", exact: true })).toHaveAttribute("aria-expanded", "false");
await project.getByRole("button", { name: "Options", exact: true }).click();
await expect(page.getByRole("menu", { name: "Project options", exact: true })).toBeVisible();
await page.keyboard.press("Escape");
await toolbar.getByRole("button", { name: "More Actions", exact: true }).click();
await expect(page.getByRole("menuitem", { name: /^Edit Configurations…/ })).toBeVisible();
await page.keyboard.press("Escape");
await toolbar.getByRole("button", { name: "Search Everywhere", exact: true }).click();
await expect(page.getByRole("dialog", { name: "Search Everywhere", exact: true })).toBeVisible();
await page.keyboard.press("Escape");
await project.getByRole("button", { name: "Hide", exact: true }).click();
await expect(project).toBeHidden();
});
}
-7
Переглянути файл
@@ -28,11 +28,4 @@ pub fn build(b: *std.Build) void {
const tests = b.addTest(.{ .root_module = tally });
const test_step = b.step("test", "Run the tests");
test_step.dependOn(&b.addRunArtifact(tests).step);
const failing_tests = b.addTest(.{ .root_module = b.createModule(.{
.root_source_file = b.path("src/failing.zig"),
.target = target,
}) });
const test_failing_step = b.step("test-failing", "Run the tests that fail on purpose");
test_failing_step.dependOn(&b.addRunArtifact(failing_tests).step);
}
-5
Переглянути файл
@@ -1,5 +0,0 @@
//! Fails on purpose: the `test-failing` step, for checking that a run's failure shows up
//! in the test tree and Rerun Failed Tests re-runs it alone.
test "intentionally fails" {
return error.OpenideaFail;
}
+1 -2
Переглянути файл
@@ -3,8 +3,7 @@ const tally = @import("tally");
pub fn main() !void {
const prices = [_]u32{ 4999, 2499, 5999 };
const sum = tally.total(&prices);
const sum = tally.total(&prices); // Set a breakpoint here
var buf: [16]u8 = undefined;
// Set a breakpoint here (QA: debugger) and inspect `sum`.
std.debug.print("Total: {s}\n", .{try tally.format(&buf, sum)});
}
-11
Переглянути файл
@@ -164,17 +164,6 @@ const CASES: Record<string, Case> = {
variable: "line",
evaluate: ["print line", / = /],
},
// Zig ships no debugger of its own: CodeLLDB debugs the binary `zig build` produces.
// The breakpoint lands on the last use of `sum`: zig's debuginfo gives a const local a
// location only within its live range, so `prices` (used earlier) is not in the frame.
zig: {
fixture: "zig-app",
tools: ["codelldb"],
adapter: "codelldb",
file: "src/main.zig",
variable: "sum",
evaluate: ["sum", /13497/],
},
php: {
fixture: "php-app",
setup: (root) =>
-13
Переглянути файл
@@ -135,19 +135,6 @@ const CASES: Record<string, Case> = {
file: "ThresholdCustomsTest.php",
},
},
// The failing run uses the fixture's dedicated `test-failing` step (`zig build` forwards
// no args to the test binary, so a failing test can't be switched on through the config).
zig: {
fixture: "zig-app",
run: { config: "zig:run", prints: "Total: $134.97" },
tests: {
config: "zig:test-failing",
failing: (c) => c,
passed: 0,
failed: "intentionally fails",
file: "src/failing.zig",
},
},
};
let server: Server | null = null;
-699
Переглянути файл
@@ -1,699 +0,0 @@
# Generated by `pnpm gen:bindings`; do not edit. One line per command:
# name, trust class, placement in a remote window, in bindings.ts, how Tauri runs it.
agent_accept_changes trusted project_host bindings async
agent_archive always project_host bindings sync
agent_base_text trusted project_host bindings async
agent_cancel always project_host bindings sync
agent_changes trusted project_host bindings async
agent_commands always project_host bindings async
agent_create trusted project_host bindings async
agent_create_ephemeral trusted desktop bindings async
agent_custom_import trusted desktop bindings sync
agent_custom_remove trusted desktop bindings sync
agent_custom_save trusted desktop bindings sync
agent_delete trusted project_host bindings async
agent_entries always project_host bindings sync
agent_external_sessions trusted project_host bindings async
agent_fork trusted project_host bindings async
agent_grouped_changes trusted project_host bindings async
agent_import_session trusted project_host bindings async
agent_mcp_import always project_host bindings sync
agent_mcp_list always project_host bindings sync
agent_mcp_remove always project_host bindings sync
agent_mcp_save always project_host bindings sync
agent_mcp_set_enabled always project_host bindings sync
agent_probe trusted project_host bindings async
agent_prompt trusted project_host bindings async
agent_providers trusted project_host bindings sync
agent_refresh_models trusted project_host bindings sync
agent_registry_install always project_host bindings async
agent_registry_list always project_host bindings async
agent_registry_remove always project_host bindings async
agent_rename trusted project_host bindings sync
agent_respond trusted project_host bindings sync
agent_rollback trusted project_host bindings async
agent_rollback_changes trusted project_host bindings async
agent_send_queued_now trusted project_host bindings sync
agent_set_command trusted project_host bindings sync
agent_set_config trusted project_host bindings async
agent_set_env trusted project_host bindings sync
agent_stop_background always project_host bindings async
agent_subscribe always project_host bindings sync
agent_unarchive always project_host bindings sync
agent_unqueue always project_host bindings sync
analysis_locate_duplicates always project_host bindings async
android_devices trusted desktop bindings async
android_logcat trusted desktop bindings async
app_build_info always desktop bindings sync
app_cancel_quit always desktop bindings sync
app_check_update always desktop bindings async
app_cli_install always desktop bindings async
app_cli_uninstall always desktop bindings async
app_info always desktop bindings sync
app_install_update always desktop bindings async
app_log_location always desktop bindings sync
app_metrics always desktop bindings async
app_quit always desktop bindings sync
app_release_notes trusted desktop bindings async
app_restart always desktop bindings sync
app_update_announcement always desktop bindings sync
app_update_status always desktop bindings sync
app_update_window_ready always desktop bindings sync
apply_workspace_edit always project_host bindings async
build_compile_file trusted project_host bindings async
build_model trusted project_host bindings async
build_run trusted project_host bindings async
build_sync trusted project_host bindings async
caches_local_data_info trusted desktop bindings sync
caches_schedule_invalidation always desktop bindings sync
caches_set_local_data_dir trusted desktop bindings sync
cancel_request always project_host bindings sync
cancel_task always project_host bindings sync
clear_caches always desktop bindings async
clipboard_file_list always desktop bindings async
clipboard_has_files always desktop bindings sync
clipboard_has_image always desktop bindings sync
clipboard_image always desktop bindings async
clipboard_set_file_list always desktop bindings async
clipboard_text trusted desktop bindings async
clipboard_write_text always desktop bindings async
close_project always project_host bindings spawn
coverage_collect trusted project_host bindings async
coverage_import always project_host bindings async
coverage_prepare trusted project_host bindings async
db_apply_edits trusted project_host bindings async
db_completion trusted project_host bindings async
db_connect trusted project_host bindings async
db_connected trusted project_host bindings sync
db_console_path always project_host bindings sync
db_consoles always project_host bindings sync
db_database_catalog trusted project_host bindings async
db_databases trusted project_host bindings async
db_ddl trusted project_host bindings async
db_disconnect trusted project_host bindings async
db_execute trusted project_host bindings async
db_explain trusted project_host bindings async
db_export_file always desktop bindings async
db_finish_transaction trusted project_host bindings async
db_history trusted project_host bindings async
db_object_ddl trusted project_host bindings async
db_remove_history trusted project_host bindings async
db_retain_catalog trusted project_host bindings async
db_save_sources trusted project_host bindings async
db_saved_catalog trusted project_host bindings async
db_schema_objects trusted project_host bindings async
db_schemas trusted project_host bindings async
db_session_context trusted project_host bindings async
db_set_manual_commit trusted project_host bindings async
db_set_password trusted project_host bindings async
db_set_ssh_secret trusted project_host bindings async
db_sources trusted project_host bindings sync
db_statements trusted project_host bindings sync
db_table_details trusted project_host bindings async
db_table_page trusted project_host bindings async
db_tables trusted project_host bindings async
db_test trusted project_host bindings async
db_test_tunnel trusted project_host bindings async
db_trust_ssh_host trusted project_host bindings async
debug_attachable_processes always project_host bindings async
debug_breakpoint_states trusted project_host bindings sync
debug_capabilities trusted project_host bindings sync
debug_completions trusted project_host bindings async
debug_data_breakpoint_info trusted project_host bindings async
debug_evaluate trusted project_host bindings async
debug_evaluate_in trusted project_host bindings async
debug_exception_filters trusted project_host bindings sync
debug_force_step_over trusted project_host bindings async
debug_free_port trusted project_host bindings async
debug_heap_capture trusted project_host bindings async
debug_heap_classes trusted project_host bindings async
debug_heap_close trusted project_host bindings async
debug_heap_compare trusted project_host bindings async
debug_heap_dominators trusted project_host bindings async
debug_heap_open trusted project_host bindings async
debug_heap_path trusted project_host bindings async
debug_heap_save trusted project_host bindings async
debug_hot_reload trusted desktop bindings async
debug_hot_restart trusted desktop bindings async
debug_js_custom_breakpoints trusted project_host bindings async
debug_js_response_body trusted project_host bindings async
debug_js_response_overrides trusted project_host bindings async
debug_js_start_profile trusted project_host bindings async
debug_js_stop_profile trusted project_host bindings async
debug_jvm_memory_classes trusted project_host bindings async
debug_jvm_memory_fields trusted project_host bindings async
debug_jvm_memory_instances trusted project_host bindings async
debug_jvm_memory_referrers trusted project_host bindings async
debug_listen_for_jvm trusted project_host bindings async
debug_pause trusted project_host bindings async
debug_redefine_classes trusted project_host bindings async
debug_restart_frame trusted project_host bindings async
debug_resume trusted project_host bindings async
debug_run_to trusted project_host bindings async
debug_scopes trusted project_host bindings async
debug_send_input trusted project_host bindings async
debug_sessions always project_host bindings sync
debug_set_breakpoints trusted project_host bindings async
debug_set_data_breakpoints trusted project_host bindings async
debug_set_exception_breakpoints trusted project_host bindings async
debug_set_function_breakpoints trusted project_host bindings async
debug_set_variable trusted project_host bindings async
debug_stack_trace trusted project_host bindings async
debug_start trusted project_host bindings async
debug_step trusted project_host bindings async
debug_step_in_target trusted project_host bindings async
debug_step_in_targets trusted project_host bindings async
debug_stop always project_host bindings async
debug_threads trusted project_host bindings async
debug_variables trusted project_host bindings async
dependency_versions trusted project_host bindings async
deploy_compare trusted project_host bindings async
deploy_coverage always project_host bindings async
deploy_delete_remote trusted project_host bindings async
deploy_download trusted project_host bindings async
deploy_info trusted project_host bindings async
deploy_server_root trusted project_host bindings async
deploy_sync trusted project_host bindings async
deploy_test_connection always project_host bindings async
deploy_upload trusted project_host bindings async
doc_close always project_host bindings async
doc_edit always project_host bindings async
doc_folding always project_host bindings async
doc_highlights always project_host bindings async
doc_open always project_host bindings async
doc_open_untitled always project_host bindings async
doc_open_virtual always project_host bindings async
doc_outline always project_host bindings async
doc_python_facts always project_host bindings async
doc_save always project_host bindings async
doc_save_all always project_host bindings async
doc_save_as always project_host bindings async
doc_selection_ranges always project_host bindings async
doc_set_bom always project_host bindings async
doc_set_encoding always project_host bindings async
doc_set_line_ending always project_host bindings async
doc_snapshot always project_host bindings async
doc_sync_from_disk always project_host bindings async
doc_ui_mode always project_host bindings async
docker_compose_op trusted desktop bindings async
docker_container_op trusted desktop bindings async
docker_container_stats trusted desktop bindings async
docker_container_top trusted desktop bindings async
docker_events trusted desktop bindings async
docker_image_history trusted desktop bindings async
docker_inspect trusted desktop bindings async
docker_logs trusted desktop bindings async
docker_prune trusted desktop bindings async
docker_pull trusted desktop bindings async
docker_registry_login trusted desktop bindings async
docker_remove trusted desktop bindings async
docker_run_env always project_host bindings async
docker_save_settings always desktop bindings async
docker_set_registry_password always desktop bindings async
docker_settings always desktop bindings async
docker_snapshot trusted desktop bindings async
end_process trusted desktop bindings async
env_reload always project_host bindings async
file_dialog_needs_gtk trusted desktop bindings async
find_dirs always project_host bindings sync
find_files always project_host bindings sync
find_files_named always project_host bindings async
find_symbols always project_host bindings sync
fs_compare_directories always project_host bindings async
fs_copy always project_host bindings async
fs_copy_merge always project_host bindings async
fs_create_dir always project_host bindings sync
fs_create_file always project_host bindings sync
fs_create_file_bytes always project_host bindings async
fs_delete always project_host bindings async
fs_entry_kind always project_host bindings async
fs_open_with_system always desktop bindings async
fs_probe always project_host bindings async
fs_read_dir always project_host bindings async
fs_read_image always project_host bindings async
fs_read_range always project_host bindings async
fs_read_text always project_host bindings async
fs_rename always project_host bindings async
fs_resolve_reference always project_host bindings async
fs_set_writable always project_host bindings sync
gh_account_repos trusted desktop bindings async
gh_api trusted desktop bindings async
gh_availability trusted desktop bindings async
gh_pr_checkout trusted desktop bindings async
gh_pr_comment trusted desktop bindings async
gh_pr_create trusted desktop bindings async
gh_pr_list trusted desktop bindings async
gh_pr_prepare_diff trusted desktop bindings async
gh_pr_review trusted desktop bindings async
gh_pr_review_comments trusted desktop bindings async
gh_pr_view trusted desktop bindings async
git_credential_forget always desktop bindings sync
git_credentials always desktop bindings sync
gradle_dependencies trusted project_host bindings async
gradle_link_projects trusted project_host bindings async
gradle_published_catalog trusted project_host bindings async
gradle_unlink_projects trusted project_host bindings async
gtk_pick_paths trusted desktop bindings async
history_content always project_host bindings async
history_list always project_host bindings async
history_list_dir always project_host bindings async
history_put_label always project_host bindings sync
history_restore always project_host bindings async
http_curl trusted desktop bindings async
http_environment_file trusted desktop bindings async
http_environments trusted desktop bindings async
http_execute trusted desktop bindings async
http_from_curl trusted desktop bindings sync
http_history trusted desktop bindings sync
http_history_clear trusted desktop bindings sync
http_history_get trusted desktop bindings sync
http_requests trusted desktop bindings sync
http_variable_definitions trusted desktop bindings async
index_file_extensions always project_host bindings sync
index_status always project_host bindings sync
inspect_code trusted project_host bindings async
java_build_workspace trusted project_host bindings async
java_debug_server trusted project_host bindings async
java_resolve_launch trusted project_host bindings async
jdk_download always project_host bindings async
jdk_download_location always project_host bindings sync
jdk_download_releases always project_host bindings async
jdk_download_vendors always project_host bindings sync
jdtls_required_java always project_host bindings async
jvm_file_presentations always project_host bindings async
languages always desktop bindings sync
light_edit_open always desktop bindings async
light_edit_open_untitled always desktop bindings async
log_frontend always desktop bindings sync
lsp_apply_code_action trusted project_host bindings async
lsp_call_hierarchy trusted project_host bindings async
lsp_can_download_sources trusted project_host bindings async
lsp_code_action_query trusted project_host bindings async
lsp_code_actions trusted project_host bindings async
lsp_code_lens trusted project_host bindings async
lsp_completion trusted project_host bindings async
lsp_document_highlights trusted project_host bindings async
lsp_document_links trusted project_host bindings async
lsp_execute_code_action trusted project_host bindings async
lsp_execute_command trusted project_host bindings async
lsp_format trusted project_host bindings async
lsp_goto trusted project_host bindings async
lsp_goto_implementation trusted project_host bindings async
lsp_hierarchy_members trusted project_host bindings async
lsp_hierarchy_refactor trusted project_host bindings async
lsp_hover trusted project_host bindings async
lsp_implementations trusted project_host bindings async
lsp_inlay_hints trusted project_host bindings async
lsp_inline_completion trusted project_host bindings async
lsp_library_sources trusted project_host bindings async
lsp_prepare_rename trusted project_host bindings async
lsp_prepare_type_hierarchy trusted project_host bindings async
lsp_references trusted project_host bindings async
lsp_rename trusted project_host bindings async
lsp_resend_diagnostics always project_host bindings sync
lsp_resolve_class_file trusted project_host bindings async
lsp_resolve_code_action trusted project_host bindings async
lsp_resolve_command trusted project_host bindings async
lsp_resolve_completion trusted project_host bindings async
lsp_restart trusted project_host bindings async
lsp_restart_named trusted desktop bindings async
lsp_semantic_tokens trusted project_host bindings async
lsp_servers always project_host bindings sync
lsp_signature_help trusted project_host bindings async
lsp_type_hierarchy trusted project_host bindings async
lsp_virtual_document trusted project_host bindings async
lsp_workspace_symbols trusted project_host bindings async
mark_recent always project_host bindings sync
maven_dependency_completion trusted project_host bindings async
maven_dependency_declaration trusted project_host bindings async
maven_exclude_dependency trusted project_host bindings async
maven_goto_declaration trusted project_host bindings async
mcp_agent_run_closed trusted desktop bindings sync
mcp_regenerate_token trusted desktop bindings sync
mcp_report_problems trusted project_host bindings sync
mcp_respond trusted desktop bindings sync
mcp_server_status trusted desktop bindings sync
mcp_set_focused_editor trusted project_host bindings sync
mcp_set_open_editors trusted project_host bindings sync
mcp_ui_ready trusted desktop bindings sync
menu_configure always desktop bindings sync
menu_model always desktop bindings sync
menu_refresh always desktop bindings sync
menu_set_disabled always desktop bindings sync
nav_index_definitions always project_host bindings async
nav_inheritance always project_host bindings async
nav_resolve_module always project_host bindings async
notebook_cell_docs trusted project_host bindings sync
notebook_close always project_host bindings async
notebook_edit always project_host bindings async
notebook_execute trusted project_host bindings async
notebook_get always project_host bindings sync
notebook_interrupt always project_host bindings async
notebook_open always project_host bindings async
notebook_redo always project_host bindings async
notebook_reload always project_host bindings async
notebook_restart trusted project_host bindings async
notebook_save always project_host bindings async
notebook_undo always project_host bindings async
open_project always project_host bindings async
path_completions always project_host bindings async
plugin_activation_event trusted desktop bindings async
plugin_answer_command trusted project_host bindings sync
plugin_answer_commit trusted project_host bindings sync
plugin_answer_prompt trusted project_host bindings sync
plugin_answer_run_target trusted project_host bindings async
plugin_assets always desktop bindings async
plugin_cancel_task trusted project_host bindings sync
plugin_catalog always desktop bindings async
plugin_choose_program outside_restricted project_host bindings async
plugin_color_presentations trusted project_host bindings async
plugin_commit_checks trusted project_host bindings async
plugin_content_tab_action trusted project_host bindings sync
plugin_context_keys trusted project_host bindings sync
plugin_debug_attach_target trusted project_host bindings async
plugin_decoration_sets trusted project_host bindings sync
plugin_endpoints trusted project_host bindings async
plugin_event_kinds trusted project_host bindings async
plugin_finish_operation trusted project_host bindings sync
plugin_formatters trusted project_host bindings sync
plugin_grant outside_restricted desktop bindings async
plugin_install_dir outside_restricted desktop bindings async
plugin_installed_intellij_themes always desktop bindings async
plugin_language_visuals trusted project_host bindings async
plugin_line_markers trusted project_host bindings async
plugin_link trusted desktop bindings async
plugin_list always desktop bindings async
plugin_log_tab_spawn trusted desktop bindings async
plugin_market always desktop bindings async
plugin_market_download always desktop bindings async
plugin_market_url always desktop bindings sync
plugin_navigation_files trusted project_host bindings async
plugin_navigation_goto trusted project_host bindings async
plugin_navigation_index trusted project_host bindings async
plugin_navigation_invalidate trusted project_host bindings sync
plugin_navigation_markers trusted project_host bindings async
plugin_parse_test_results trusted project_host bindings async
plugin_post_events trusted project_host bindings sync
plugin_prepare_debug_run trusted project_host bindings async
plugin_preview_install always desktop bindings async
plugin_program outside_restricted project_host bindings async
plugin_read_intellij_theme always desktop bindings async
plugin_reload outside_restricted desktop bindings async
plugin_revoke_net_host outside_restricted desktop bindings async
plugin_roll_back outside_restricted desktop bindings async
plugin_run_command trusted project_host bindings async
plugin_run_targets trusted project_host bindings async
plugin_secrets trusted desktop bindings sync
plugin_select_tests trusted project_host bindings async
plugin_service_detail trusted project_host bindings async
plugin_service_log trusted project_host bindings async
plugin_set_active_file always project_host bindings sync
plugin_set_capability outside_restricted desktop bindings async
plugin_set_enabled outside_restricted desktop bindings async
plugin_set_market_url always desktop bindings sync
plugin_set_secret trusted desktop bindings sync
plugin_set_theme always desktop bindings sync
plugin_set_theme_frame always desktop bindings sync
plugin_set_ui_locale always project_host bindings async
plugin_stage_package always desktop bindings async
plugin_status_items trusted project_host bindings sync
plugin_subscribe always desktop bindings sync
plugin_sync_languages trusted project_host - -
plugin_sync_packages trusted project_host - -
plugin_terminal_session_close trusted project_host bindings sync
plugin_terminal_session_spawn trusted project_host bindings async
plugin_uninstall outside_restricted desktop bindings async
plugin_unlink trusted desktop bindings async
plugin_unpack_package always desktop bindings async
plugin_view_event trusted project_host bindings async
plugin_view_render trusted project_host bindings async
plugin_web_page trusted desktop bindings sync
plugin_web_pty_spawn trusted desktop bindings async
plugin_web_read_file trusted desktop bindings async
plugin_web_save_file trusted desktop bindings async
prepare_workspace_edit always project_host bindings async
profiler_collect trusted desktop bindings async
profiler_gc_before_stop trusted desktop bindings async
profiler_prepare trusted desktop bindings async
project_current always project_host bindings sync
project_rename always desktop bindings sync
project_roots always project_host bindings sync
project_trust_add_location always desktop bindings async
project_trust_list always project_host bindings sync
project_trust_remove_location always project_host bindings async
project_trust_set always project_host bindings async
project_trust_status always project_host bindings sync
proxy_check_connection always desktop bindings async
recent_files always project_host bindings sync
recent_project_groups always desktop bindings sync
recent_projects always desktop bindings async
redo_workspace_edit always project_host bindings async
refactor_can_undo_file_move always project_host bindings sync
refactor_file_move_edits always project_host bindings async
refactor_file_move_text_edits always project_host bindings async
refactor_move_files always project_host bindings async
refactor_undo_file_move always project_host bindings async
remote_bootstrap_list always desktop bindings async
remote_connect always desktop bindings async
remote_current always desktop bindings sync
remote_delete trusted project_host bindings async
remote_disconnect always desktop bindings sync
remote_download_into trusted project_host bindings async
remote_forward_port trusted desktop bindings async
remote_forwarded_ports trusted desktop bindings sync
remote_import_local trusted desktop bindings async
remote_invoke always desktop bindings async
remote_list always project_host bindings async
remote_mkdir trusted project_host bindings async
remote_of always project_host bindings sync
remote_open always project_host bindings async
remote_rename trusted project_host bindings async
remote_stop_forwarding trusted desktop bindings async
remote_trust_set always desktop bindings sync
remote_trust_status always desktop bindings sync
remove_recent always project_host bindings sync
remove_recent_project always desktop bindings sync
replace_in_files trusted project_host bindings async
replace_preview trusted project_host bindings async
run_application_build trusted project_host bindings async
run_application_publish trusted project_host bindings async
run_apply_settings trusted project_host bindings async
run_close_stdin trusted project_host bindings sync
run_configs trusted project_host bindings async
run_dotnet_resolve trusted project_host bindings async
run_env_files trusted project_host bindings async
run_gradle_init_script trusted project_host bindings async
run_gradle_jvm_args_script always project_host bindings async
run_import_configs trusted project_host bindings async
run_import_sources trusted project_host bindings async
run_list always project_host bindings sync
run_resize trusted project_host bindings sync
run_save_configs trusted project_host bindings async
run_save_templates trusted project_host bindings async
run_start trusted project_host bindings async
run_stdin trusted project_host bindings sync
run_stop always project_host bindings sync
run_system_env always project_host bindings sync
run_templates trusted project_host bindings async
run_test_reports trusted project_host bindings async
run_tomcat_artifacts trusted project_host bindings async
run_tomcat_build trusted project_host bindings async
run_tomcat_home trusted project_host bindings async
run_tomcat_installations trusted project_host bindings async
run_tomcat_sync trusted project_host bindings async
run_tomcat_update trusted project_host bindings async
run_without_derived trusted project_host bindings async
scaffold_catalog always project_host bindings sync
scaffold_create always project_host bindings async
scaffold_options always project_host bindings async
scaffold_requirements always project_host bindings async
sdk_container_python trusted project_host bindings sync
sdk_container_python_refresh trusted project_host bindings async
sdk_detect trusted project_host bindings async
sdk_locate_tools trusted project_host bindings async
sdk_php_composer_install trusted project_host bindings async
sdk_php_effective trusted project_host bindings sync
sdk_php_language_level trusted project_host bindings async
sdk_php_xdebug trusted project_host bindings async
sdk_project_libraries trusted project_host bindings async
sdk_python_create_venv trusted project_host bindings async
sdk_python_effective trusted project_host bindings sync
sdk_python_interpreter trusted project_host bindings async
sdk_python_packages trusted project_host bindings async
sdk_python_pip trusted project_host bindings async
sdk_python_project_interpreters trusted project_host bindings async
search_start always project_host bindings async
search_validate always project_host bindings sync
set_recent_project_groups always desktop bindings sync
settings_export always desktop bindings async
settings_get always project_host bindings sync
settings_get_global always desktop bindings sync
settings_import always desktop bindings async
settings_list_jetbrains_ides always desktop bindings async
settings_read_jetbrains always desktop bindings async
settings_set always project_host bindings sync
spell_check always project_host bindings async
spell_suggest always project_host bindings async
spring_route_sources trusted desktop bindings sync
ssh_askpass_answer always desktop bindings sync
ssh_connected always project_host bindings sync
ssh_disconnect always project_host bindings sync
ssh_needs_secret always project_host bindings sync
ssh_open_terminal always desktop bindings async
ssh_password_saved always project_host bindings sync
ssh_set_secret always project_host bindings sync
ssh_test always project_host bindings async
ssh_trust_host always project_host bindings async
structural_languages always project_host bindings async
structural_search always project_host bindings async
structural_validate always project_host bindings async
syntax_errors always project_host bindings async
syntax_expression_at always project_host bindings async
syntax_identifiers always project_host bindings async
syntax_identifiers_in_files always project_host bindings async
syntax_locals always project_host bindings async
syntax_node_at always project_host bindings async
syntax_structural_ranges always project_host bindings async
syntax_support always project_host bindings sync
system_font_families always desktop bindings async
system_probe_hotkeys trusted desktop bindings sync
term_available_shells always project_host bindings sync
term_default_shell always project_host bindings sync
term_is_busy always project_host bindings async
term_kill always project_host bindings sync
term_resize always project_host bindings sync
term_spawn always project_host bindings async
term_store_image always project_host bindings async
term_write always project_host bindings sync
tools_install trusted project_host bindings async
tools_list trusted project_host bindings async
tools_uninstall trusted project_host bindings async
tools_updates trusted project_host bindings async
undo_workspace_edit always project_host bindings async
user_font_data always desktop - async
vcs_add_to_ignore trusted project_host bindings async
vcs_add_worktree trusted project_host bindings async
vcs_apply_patch trusted project_host bindings async
vcs_blame always project_host bindings async
vcs_block_history always project_host bindings async
vcs_branch_info always project_host bindings async
vcs_branches always project_host bindings async
vcs_changelists_load always project_host bindings async
vcs_changelists_save trusted project_host bindings sync
vcs_checkout trusted project_host bindings async
vcs_clone always project_host bindings async
vcs_commit trusted project_host bindings async
vcs_commit_files always project_host bindings async
vcs_commits_changes always project_host bindings async
vcs_compare_paths always project_host bindings async
vcs_compare_scope always project_host bindings async
vcs_config_get always project_host bindings async
vcs_conflict_versions always project_host bindings async
vcs_create_branch trusted project_host bindings async
vcs_create_patch trusted project_host bindings async
vcs_create_tag trusted project_host bindings async
vcs_delete_branch trusted project_host bindings async
vcs_delete_tag trusted project_host bindings async
vcs_detected_git always project_host bindings sync
vcs_diff_hunks always project_host bindings async
vcs_diff_numstat always project_host bindings async
vcs_diff_revisions always project_host bindings async
vcs_diff_text always project_host bindings async
vcs_diff_with_worktree always project_host bindings async
vcs_discard trusted project_host bindings async
vcs_edit_remote trusted project_host bindings async
vcs_file_at always project_host bindings async
vcs_file_mode trusted project_host bindings async
vcs_git_version trusted project_host bindings async
vcs_has_hook always project_host bindings async
vcs_ignore_blame_revision trusted project_host bindings async
vcs_init always project_host bindings async
vcs_line_hunks always project_host bindings async
vcs_log always project_host bindings async
vcs_log_details always project_host bindings async
vcs_log_details_batch always project_host bindings async
vcs_log_refs always project_host bindings async
vcs_log_users always project_host bindings async
vcs_merge trusted project_host bindings async
vcs_merge_base always project_host bindings async
vcs_merge_with trusted project_host bindings async
vcs_move_worktree trusted project_host bindings async
vcs_open_external_log always project_host bindings async
vcs_operation trusted project_host bindings async
vcs_patch_preview trusted project_host bindings async
vcs_rebase trusted project_host bindings async
vcs_rebase_with trusted project_host bindings async
vcs_remote trusted project_host bindings async
vcs_remotes always project_host bindings async
vcs_remove_worktree trusted project_host bindings async
vcs_rename_branch trusted project_host bindings async
vcs_restore_branch trusted project_host bindings async
vcs_restore_head trusted project_host bindings async
vcs_restore_tag trusted project_host bindings async
vcs_revision_children always project_host bindings async
vcs_revision_files always project_host bindings async
vcs_reword trusted project_host bindings async
vcs_roots always project_host bindings sync
vcs_shelf_delete trusted project_host bindings async
vcs_shelf_diff trusted project_host bindings async
vcs_shelf_import trusted project_host bindings async
vcs_shelf_patch always project_host bindings async
vcs_shelf_rename trusted project_host bindings async
vcs_shelve trusted project_host bindings async
vcs_shelves always project_host bindings async
vcs_stage trusted project_host bindings async
vcs_stage_hunks trusted project_host bindings async
vcs_stage_patch trusted project_host bindings async
vcs_stash_apply trusted project_host bindings async
vcs_stash_drop trusted project_host bindings async
vcs_stash_push trusted project_host bindings async
vcs_stashes always project_host bindings async
vcs_status always project_host bindings async
vcs_status_paths always project_host bindings async
vcs_tags always project_host bindings async
vcs_undo_commit trusted project_host bindings async
vcs_unshelve trusted project_host bindings async
vcs_unstage trusted project_host bindings async
vcs_worktrees always project_host bindings async
vcs_write_patch always project_host bindings async
web_browsers always desktop bindings async
web_goto always project_host bindings async
web_open always desktop bindings async
web_serves_page trusted desktop bindings async
web_unresolved_references always project_host bindings async
web_url always project_host bindings async
window_close_context always desktop bindings sync
window_complete_launch always desktop bindings sync
window_cycle always desktop bindings sync
window_flash_taskbar always desktop bindings sync
window_group_cancel_close always desktop bindings sync
window_group_close always desktop bindings sync
window_group_close_tabs always desktop bindings sync
window_group_current always desktop bindings sync
window_group_detach always desktop bindings async
window_group_merge always desktop bindings async
window_group_reorder always desktop bindings async
window_group_restore always desktop bindings async
window_group_select always desktop bindings async
window_is_fullscreen always desktop bindings sync
window_new always desktop bindings async
window_open_comparison always desktop bindings async
window_open_deployment always desktop bindings async
window_open_diff always desktop bindings async
window_open_file_in_new_window always desktop bindings async
window_open_files always desktop bindings async
window_open_project always desktop bindings async
window_open_projects always desktop bindings sync
window_open_remote always desktop bindings async
window_open_settings always desktop bindings async
window_set_titlebar_height always desktop bindings sync
window_start_drag always desktop bindings sync
window_take_launch always desktop bindings sync
window_toggle_fullscreen always desktop bindings sync
window_toggle_zoom always desktop bindings sync
workspace_attach always desktop bindings async
workspace_create always desktop bindings async
workspace_discover_projects always desktop bindings async
workspace_update always desktop bindings async
wsl_distros always desktop bindings async
wsl_resolve always desktop bindings sync
+2 -93
Переглянути файл
@@ -1,98 +1,7 @@
//! `cargo test -p openidea --test export_bindings` regenerates `src/bindings.ts` and
//! `src-tauri/tests/commands.snap` without launching the app (used by `pnpm gen:bindings`
//! and CI drift checks).
use ide_host::trust::{projectless_command_allowed, restricted_command_allowed};
use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use tauri_specta::{BuilderConfiguration, LanguageExt};
//! `cargo test -p openidea --test export_bindings` regenerates `src/bindings.ts`
//! without launching the app (used by `pnpm gen:bindings` and CI drift checks).
#[test]
fn export_bindings() {
openidea_lib::export_bindings(&openidea_lib::specta_builder());
}
/// Commands registered outside tauri-specta (`src-tauri/src/lib.rs`), with how Tauri runs them.
const RAW_COMMANDS: &[(&str, &str)] = &[("user_font_data", "async")];
/// Every command, one line each: its trust class, where a remote window runs it, whether it is
/// in `bindings.ts`, and how Tauri runs it. A change to any of these shows up in this file's diff.
#[test]
fn export_command_snapshot() {
let target = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/commands.snap");
let fresh = std::env::temp_dir().join(format!("openidea-commands-{}.snap", std::process::id()));
openidea_lib::specta_builder().export(CommandSnapshot, &fresh).expect("command snapshot");
let text = std::fs::read(&fresh).expect("exported command snapshot");
let _ = std::fs::remove_file(&fresh);
if std::fs::read(&target).ok().as_deref() != Some(text.as_slice()) {
std::fs::write(&target, text).expect("failed to write the command snapshot");
}
}
struct CommandSnapshot;
impl LanguageExt for CommandSnapshot {
type Error = std::io::Error;
fn export(self, cfg: &BuilderConfiguration, path: &Path) -> std::io::Result<()> {
let spawned = spawned_commands();
let mut modes: BTreeMap<&str, &str> = cfg
.commands
.iter()
.map(|f| {
let mode = if spawned.contains(f.name()) {
"spawn"
} else if f.asyncness() {
"async"
} else {
"sync"
};
(f.name(), mode)
})
.collect();
let in_bindings: BTreeSet<&str> = modes.keys().copied().collect();
modes.extend(RAW_COMMANDS.iter().copied());
let routed: BTreeSet<&str> = ide_host::rpc::client_commands().into_iter().collect();
let names: BTreeSet<&str> = modes.keys().chain(&routed).copied().collect();
let mut out = String::from(
"# Generated by `pnpm gen:bindings`; do not edit. One line per command:\n\
# name, trust class, placement in a remote window, in bindings.ts, how Tauri runs it.\n",
);
for name in names {
let trust = if restricted_command_allowed(name) {
"always"
} else if projectless_command_allowed(name) {
"outside_restricted"
} else {
"trusted"
};
let placement = if routed.contains(name) { "project_host" } else { "desktop" };
let bindings = if in_bindings.contains(name) { "bindings" } else { "-" };
let mode = modes.get(name).copied().unwrap_or("-");
out.push_str(&format!("{name:<44} {trust:<18} {placement:<12} {bindings:<8} {mode}\n"));
}
std::fs::write(path, out)
}
}
/// Sync commands declared `#[tauri::command(async)]`, which Tauri runs off the main thread.
fn spawned_commands() -> BTreeSet<String> {
let mut out = BTreeSet::new();
let mut dirs = vec![Path::new(env!("CARGO_MANIFEST_DIR")).join("src")];
while let Some(dir) = dirs.pop() {
for entry in std::fs::read_dir(&dir).unwrap() {
let path = entry.unwrap().path();
if path.is_dir() {
dirs.push(path);
continue;
}
let source = std::fs::read_to_string(&path).unwrap_or_default();
for (_, rest) in source.match_indices("#[tauri::command(async)]").map(|(i, _)| source.split_at(i)) {
let name = rest.split("fn ").nth(1).and_then(|s| s.split('(').next());
out.insert(name.expect("a function after #[tauri::command(async)]").trim().to_owned());
}
}
}
out
}
-2
Переглянути файл
@@ -9463,8 +9463,6 @@ export type RunSettings = {
/** Ask before closing a Run tab whose process is still running. */
confirmTerminate?: boolean,
showOnStart?: boolean,
/** New Run/Debug tabs start pinned: a later run doesn't take their place once finished. */
pinTabs?: boolean,
/** Grace period between interrupt and kill when stopping a process. */
terminationTimeoutMs?: number,
};
-18
Переглянути файл
@@ -1,6 +1,5 @@
import { describe, expect, it } from "vitest";
import { EditorState } from "@codemirror/state";
import { getIndentation, indentUnit } from "@codemirror/language";
import { completedSyntaxTree } from "@/test/syntax";
import { classHighlighter, highlightTree } from "@lezer/highlight";
import { findLanguageDescription, loadSupport, loadedSupport } from "./languages";
@@ -82,23 +81,6 @@ describe("frontend string interpolation", () => {
expect(out.some((t) => t.text.includes("{brace}") && t.style === "tok-string")).toBe(true);
});
it.each(["Kotlin", "Scala", "Dart", "Groovy", "C#"])(
"%s indents brackets opened after a string from the line's indentation",
async (mode) => {
const support = await loadSupport(findLanguageDescription(mode, "")!);
const doc = 'f(\n pair("a", g(\n x\n )),\n)';
const state = EditorState.create({
doc,
extensions: [support!, indentUnit.of(" "), EditorState.tabSize.of(4)],
});
completedSyntaxTree(state);
const indent = (line: number) => getIndentation(state, state.doc.line(line).from);
expect(indent(3)).toBe(8);
// C#'s mode leaves a closing bracket's indentation to the editor.
if (mode !== "C#") expect(indent(4)).toBe(4);
},
);
it("keeps Groovy dotted interpolation in code", async () => {
const out = await tokens("Groovy", '"$person.name tail"');
expect(out).toContainEqual(expect.objectContaining({ text: "name", style: "tok-variableName" }));
+1 -5
Переглянути файл
@@ -114,11 +114,7 @@ export function interpolationParser<S>(base: StreamParser<S>, dialect: Interpola
}
if (opening && style !== "comment" && style !== "string.special") {
stream.pos = from + opening.length;
// Let the base tokenizer see an empty string at the literal's own column: a stream
// at column 0 would read as a new line and reset the base's line indentation.
const empty = new StringStream(stream.string.slice(0, from) + '""', 4, state.indentUnit);
empty.pos = empty.start = from;
base.token(empty, frame.code);
base.token(new StringStream('""', 4, state.indentUnit), frame.code);
frame.literal = {
quote,
braces,
+1 -25
Переглянути файл
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { DocModel } from "./model/document";
import { offerMakeWritable, readOnlyQuestion, toggleReadOnly } from "./fileAttributes";
import { readOnlyQuestion, toggleReadOnly } from "./fileAttributes";
function model(readOnly: boolean, extra: Partial<{ virtual: boolean; banner: string | null }> = {}) {
const meta = { readOnly, banner: extra.banner ?? (readOnly ? "readOnly" : null) };
@@ -43,27 +43,3 @@ describe("Toggle Read-only Attribute", () => {
expect(readOnlyQuestion("a.txt", false).confirm).toBe("Make Read-only");
});
});
describe("editing a read-only file", () => {
it("offers to make it writable once, then lets edits through", async () => {
const m = model(true);
answer = true;
expect(offerMakeWritable(m)).toBe(true);
expect(offerMakeWritable(m)).toBe(true);
expect(confirm).toHaveBeenCalledTimes(1);
expect(confirm).toHaveBeenCalledWith(expect.objectContaining({ title: "Make File Writable" }));
await vi.waitFor(() => expect(m.setReadOnly).toHaveBeenCalledWith(false));
expect(offerMakeWritable(m)).toBe(false);
});
it("refuses edits without asking for library, binary and too-large documents", () => {
for (const m of [
model(true, { virtual: true }),
model(true, { banner: "binary" }),
model(true, { banner: "tooLarge" }),
])
expect(offerMakeWritable(m)).toBe(true);
expect(confirm).not.toHaveBeenCalled();
expect(offerMakeWritable(model(false))).toBe(false);
});
});
+1 -21
Переглянути файл
@@ -1,8 +1,7 @@
/**
* File › File Properties › Make File Read-only / Writable (IDEA's "Toggle
* Read-only Attribute", also the status bar lock): asks, then flips the
* file's writable bit and the open document's editability. Typing into a file
* whose read-only attribute is set asks the same (IDEA's Clear Read-Only Status).
* file's writable bit and the open document's editability.
*/
import { call, commands, isTauri } from "@/platform/ipc";
import { basename } from "@/platform/project";
@@ -42,22 +41,3 @@ export function toggleReadOnly(model: DocModel) {
(ok) => ok && void setFileWritable(model, meta.readOnly),
);
}
let asking: DocModel | null = null;
/**
* An edit was attempted in `model`. When only the file's read-only attribute is in the
* way (not a binary, too large or library file), offers to make it writable, once at a
* time. Returns whether the edit was refused: the caller drops it either way.
*/
export function offerMakeWritable(model: DocModel): boolean {
const meta = model.getMeta();
if (!meta.readOnly) return false;
if (model.virtual || meta.banner !== "readOnly" || asking) return true;
asking = model;
const q = readOnlyQuestion(basename(model.path), true);
void confirm({ title: q.title, message: q.message, ok: q.confirm })
.then((ok) => ok && model.getMeta().readOnly && setFileWritable(model, true))
.finally(() => (asking = null));
return true;
}
-11
Переглянути файл
@@ -34,7 +34,6 @@ import {
import { showContextMenu } from "@/ui";
import { baseExtensions, compartments, contributionExtensions } from "../cm/setup";
import { gutterMenuItems, inGutterMenuArea } from "../gutterMenu";
import { offerMakeWritable } from "../fileAttributes";
import type { DocModel } from "./document";
import type { WorkspaceHistoryTarget } from "./workspaceHistory";
import { isNavigation } from "./navHistory";
@@ -148,12 +147,6 @@ export class EditorSlot {
return this.state;
}
private refuseReadOnlyEdit(e: Event): boolean {
if (!offerMakeWritable(this.model)) return false;
e.preventDefault();
return true;
}
private slotExtensions() {
return [
this.model.manager.workspaceHistory.extension(this.historyTarget),
@@ -176,10 +169,6 @@ export class EditorSlot {
this.host.onContextMenu(this, e);
return true;
},
// Typing, pasting or dropping into a read-only file offers to make it writable.
beforeinput: (e) => this.refuseReadOnlyEdit(e),
paste: (e) => this.refuseReadOnlyEdit(e),
drop: (e) => this.refuseReadOnlyEdit(e),
}),
ViewPlugin.define((view) => {
// Gutters sit outside the content DOM. Gutter extensions with their own menu (breakpoints,
+3 -5
Переглянути файл
@@ -85,7 +85,7 @@ export function UsagesTabs() {
id: String(t.id),
label: label(t),
closable: true,
pinned: t.pinned,
icon: t.pinned ? "pin" : undefined,
}))}
activeId={String(search.id)}
onSelect={(id) => useUsages.setState({ active: Number(id) })}
@@ -97,18 +97,16 @@ export function UsagesTabs() {
onContextMenu={(id, e) => {
const t = tabs.find((x) => x.id === Number(id));
if (!t) return;
// IDEA's content tab menu.
showContextMenu(e, [
{ id: "pin", label: t.pinned ? "Unpin Tab" : "Pin Tab", run: () => pinUsageTab(t.id, !t.pinned) },
{ kind: "separator" },
{ id: "close", label: "Close Tab", run: () => closeUsageTab(t.id) },
{ id: "closeAll", label: "Close All Tabs", run: () => tabs.forEach((o) => closeUsageTab(o.id)) },
{
id: "closeOthers",
label: "Close Other Tabs",
disabled: tabs.length < 2,
run: () => tabs.forEach((o) => o.id !== t.id && closeUsageTab(o.id)),
},
{ kind: "separator" },
{ id: "pin", label: t.pinned ? "Unpin Tab" : "Pin Tab", run: () => pinUsageTab(t.id, !t.pinned) },
]);
}}
/>
-25
Переглянути файл
@@ -372,31 +372,6 @@ describe("run config → launch config", () => {
},
);
it("Zig under CodeLLDB: no source-language mode, so evaluation stays LLDB's own", () => {
const exe = launchConfigFor(
rc({ kind: "zig-application", command: "./zig-out/bin/app", debugAdapter: "codelldb" }),
"/p",
)!;
expect(exe.adapter).toBe("codelldb");
expect(extra(exe)).toEqual({ sourceLanguages: [] });
const single = launchConfigFor(
rc({
kind: "native",
command: "/tmp/openidea-run/calc-1",
debugAdapter: "codelldb",
params: { language: "zig" },
}),
"/p",
)!;
expect(extra(single)).toEqual({ sourceLanguages: [] });
// Rust-compiled binaries keep CodeLLDB's default (Rust) source mode.
const rust = launchConfigFor(
rc({ kind: "native", command: "/tmp/openidea-run/app-1", debugAdapter: "codelldb" }),
"/p",
)!;
expect(rust.extraJson).toBeNull();
});
it("Godot: the editor's adapter plays the main scene or the chosen one of the project", () => {
const project = rc({
kind: "godot",
+1 -47
Переглянути файл
@@ -6,7 +6,7 @@ import {
removeAllBreakpoints,
useBreakpoints,
} from "../breakpoints/store";
import { handleDebugEvent, pinSession, rerunSession, startPreparedSession, startSession, useDebug } from "../state";
import { handleDebugEvent, startPreparedSession, startSession, useDebug } from "../state";
import type { LaunchConfig } from "@/platform/ipc";
import { useProject } from "@/platform/project";
import {
@@ -237,49 +237,3 @@ it("replaces finished tabs as soon as a prepared start begins", async () => {
await expect(started).resolves.toBe(43);
expect(Object.keys(useDebug.getState().sessions)).toEqual(["43"]);
});
it("replaces only the finished tab of the same name; other finished tabs stay", async () => {
ipc.debugStart.mockResolvedValueOnce({ status: "ok", data: 1 }).mockResolvedValueOnce({ status: "ok", data: 2 });
await startSession({ ...config("launch"), name: "Server" });
await startSession({ ...config("launch"), name: "Client" });
handleDebugEvent(1, { kind: "terminated" });
handleDebugEvent(2, { kind: "terminated" });
ipc.debugStart.mockResolvedValueOnce({ status: "ok", data: 3 });
await startSession({ ...config("launch"), name: "Server" });
expect(Object.values(useDebug.getState().sessions).map((s) => [s.name, s.status])).toEqual([
["Client", "terminated"],
["Server", "running"],
]);
ipc.debugStart.mockResolvedValueOnce({ status: "ok", data: 4 });
const started = startPreparedSession({ id: "other", name: "Other" } as never, async () => ({
...config("launch"),
name: "Other",
}));
// No finished tab named "Other": the first finished one (Client) gives way.
expect(Object.values(useDebug.getState().sessions).map((s) => s.name)).toEqual(["Server", "Other"]);
await started;
expect(Object.values(useDebug.getState().sessions).map((s) => s.name)).toEqual(["Server", "Other"]);
});
it("keeps a pinned finished tab; Rerun on it takes its place and keeps it pinned", async () => {
ipc.debugStart.mockResolvedValueOnce({ status: "ok", data: 1 });
await startSession({ ...config("launch"), name: "Server" });
pinSession(1, true);
handleDebugEvent(1, { kind: "terminated" });
ipc.debugStart.mockResolvedValueOnce({ status: "ok", data: 2 });
await startSession({ ...config("launch"), name: "Server" });
const tabs = () => Object.values(useDebug.getState().sessions).map((s) => [s.id, s.status, !!s.pinned]);
expect(tabs()).toEqual([
[1, "terminated", true],
[2, "running", false],
]);
handleDebugEvent(2, { kind: "terminated" });
ipc.debugStart.mockResolvedValueOnce({ status: "ok", data: 3 });
await rerunSession(1);
expect(tabs()).toEqual([
[2, "terminated", false],
[3, "running", true],
]);
});
-2
Переглянути файл
@@ -18,7 +18,6 @@ import {
useDebugRunSessions,
} from "@/workbench/contributions";
import { registerDebugActions } from "./actions";
import { registerDebugTabActions } from "./tabActions";
import { loadBreakpoints } from "./breakpoints/store";
import { loadExceptionBreakpoints } from "./breakpoints/exceptions";
import { loadJsBreakpoints, watchPageSessions } from "./js/eventBreakpoints";
@@ -73,7 +72,6 @@ export function activate(): () => void {
const disposers = [
() => useDebugRunSessions.setState({ sessions: [], serviceSessions: [], Console: null }),
registerDebugActions(),
registerDebugTabActions(),
// Remote JVM Debug, Go Remote, …: Run/Debug Configurations › Add New Configuration.
...REMOTE_TYPES.map(registerRunConfigType),
registerRunConfigType(jsBrowserType),
-4
Переглянути файл
@@ -359,10 +359,6 @@ function nativeLaunch(rc: RunConfig, cwd: string, adapter: string): LaunchConfig
env: Object.entries(rc.env).map(([k, v]) => `${k}=${v}`),
...(from && to && from !== to ? { sourceMap: [[from, to]] } : {}),
});
} else if (rc.kind === "zig-application" || rc.params?.language === "zig") {
// Zig under CodeLLDB: without a source language, evaluation stays LLDB's own
// (the default Rust mode would shape `Evaluate Expression` as Rust).
out.extraJson = JSON.stringify({ sourceLanguages: [] });
}
return out;
}
+25 -56
Переглянути файл
@@ -74,8 +74,6 @@ export interface DebugSession {
programInput?: boolean;
/** Process the session is attached to (a Run tab's build tool); stopped with the session. */
process: RunId | null;
/** Pinned tab (root sessions): once finished, a later session opens a new tab instead of taking its place. */
pinned?: boolean;
status: SessionStatus;
stoppedThread: number | null;
stopReason: string | null;
@@ -306,30 +304,15 @@ function settleStart(id: DebugSessionId | null, discard = false) {
for (const s of leftovers) if (isLive(useDebug.getState().sessions[s])) markTerminated(s);
}
/** The session being rerun: its tab is the one the new session takes, pinned or not. */
let rerunOf: DebugSessionId | null = null;
/**
* As IDEA reuses run content: a new session takes the place of one finished tab (with its
* children) that isn't pinned, preferring one named like it, then the selected one; other
* finished tabs stay. Returns whether the new tab is pinned: as the tab it replaced on a
* rerun, else per the settings.
*/
function replaceFinished(name: string): boolean | undefined {
const { sessions, active } = useDebug.getState();
const own = rerunOf != null ? sessions[rerunOf] : undefined;
rerunOf = null;
if (own && !isLive(own)) {
removeSession(own.id);
return own.pinned;
}
const finished = Object.values(sessions).filter((s) => s.parent == null && !isLive(s) && !s.pinned);
const selected = active != null ? sessions[active] : undefined;
const ordered =
selected && finished.includes(selected) ? [selected, ...finished.filter((s) => s !== selected)] : finished;
const reused = ordered.find((s) => s.name === name) ?? ordered[0];
if (reused) removeSession(reused.id);
return workflowSettings().run.pinTabs || undefined;
/** Drops finished sessions; running ones stay (multiple concurrent sessions are fine). */
function dropFinished() {
useDebug.setState((s) => {
const sessions = Object.fromEntries(Object.entries(s.sessions).filter(([, v]) => isLive(v)));
const consoleLines = Object.fromEntries(Object.entries(s.console).filter(([k]) => sessions[+k]));
for (const id of ansiStreams.keys()) if (!sessions[id]) ansiStreams.delete(id);
return { sessions, console: consoleLines, active: s.active != null && sessions[s.active] ? s.active : null };
});
publishContext();
}
/**
@@ -687,17 +670,12 @@ export async function startPreparedSession(
const id = --preparationSeq as DebugSessionId;
const controller = new AbortController();
preparations.set(id, controller);
// The new tab takes the place of a finished one while its build runs, not once the adapter starts.
const pinned = replaceFinished(config.name);
// The new tab takes the place of finished ones while its build runs, not once the adapter starts.
dropFinished();
useDebug.setState((s) => ({
sessions: {
...s.sessions,
[id]: blank(id, {
name: config.name,
adapter: config.debugAdapter ?? "",
runConfig: config,
pinned,
}),
[id]: blank(id, { name: config.name, adapter: config.debugAdapter ?? "", runConfig: config }),
},
console: { ...s.console, [id]: [] },
active: id,
@@ -750,7 +728,7 @@ export async function startSession(
// Attaching to a JVM that may be gone: the finished session stays until the attach succeeds.
const afterDetach = justDetached(options.process);
const previousActive = useDebug.getState().active;
let pinned = !afterDetach && !options.preparation ? replaceFinished(launch.name) : undefined;
if (!afterDetach) dropFinished();
try {
if (options.show !== false && workflowSettings().run.showOnStart) toolWindowService().show("debug");
} catch {
@@ -765,7 +743,7 @@ export async function startSession(
const id = await call(commands.debugStart(launch));
console.info("Debug adapter started", { name: launch.name, ms: performance.now() - adapterStarted });
owned.add(id);
if (afterDetach && !options.preparation) pinned = replaceFinished(launch.name);
if (afterDetach) dropFinished();
if (options.preparation?.signal.aborted) {
await call(commands.debugStop(id)).catch(() => undefined);
settleStart(id);
@@ -797,7 +775,6 @@ export async function startSession(
adapter: cur.adapter || launch.adapter,
launch: options.rerunnable === false ? null : launch,
process: options.process ?? null,
pinned: preparation ? preparation.pinned : pinned,
status: cur.status === "starting" ? "running" : cur.status,
},
},
@@ -914,11 +891,6 @@ export async function closeSessionTab(id: DebugSessionId): Promise<boolean> {
return true;
}
/** Pins or unpins a session's tab. */
export function pinSession(id: DebugSessionId, pinned: boolean) {
patch(id, { pinned });
}
/** Forgets a session and its children; a selected one passes the selection to the tab on its left. */
export function removeSession(id: DebugSessionId) {
const st = useDebug.getState();
@@ -946,7 +918,6 @@ export function removeSession(id: DebugSessionId) {
});
for (const goneId of gone) {
owned.delete(goneId);
ansiStreams.delete(goneId);
unfollowProcessOutput(goneId);
}
updateExecutionPoint();
@@ -957,20 +928,18 @@ export async function rerunSession(id: DebugSessionId, options: { show?: boolean
const s = useDebug.getState().sessions[id];
if (!s) return;
const root = rootOf(s);
const launch = root.launch;
if (!root.runConfig && !launch) return;
if (isLive(root)) await stopSession(root.id);
rerunOf = root.id;
try {
if (root.runConfig)
await executeAction(
"run.debugConfig",
options.show === false ? { config: root.runConfig, show: false } : root.runConfig,
);
else await startSession(launch!, { show: options.show });
} finally {
rerunOf = null;
if (root.runConfig) {
if (isLive(root)) await stopSession(root.id);
await executeAction(
"run.debugConfig",
options.show === false ? { config: root.runConfig, show: false } : root.runConfig,
);
return;
}
const launch = root.launch;
if (!launch) return;
if (isLive(root)) await stopSession(root.id);
await startSession(launch, { show: options.show });
}
async function loadCapabilities(id: DebugSessionId) {
-94
Переглянути файл
@@ -1,94 +0,0 @@
import { registerActions, type ActionContext } from "@/platform/actions";
import type { MenuItem } from "@/ui";
import type { DebugSessionId } from "@/platform/ipc";
import { closeSessionTab, pinSession, useDebug } from "./state";
import { sessionTabs } from "./ui/sessionTabs";
/** The sessions the Debug tabs close: each tab's owner, in tab order. */
function owners(): DebugSessionId[] {
return [...new Set(sessionTabs(Object.values(useDebug.getState().sessions)).map((t) => t.owner))];
}
/** The tab owner an action is for: its argument, else the selected tab's. */
function target({ arg }: ActionContext): DebugSessionId | null {
const { sessions, active } = useDebug.getState();
const id = typeof arg === "number" ? arg : active;
if (id == null || !sessions[id]) return null;
return sessionTabs(Object.values(sessions)).find((t) => t.session.id === id || t.owner === id)?.owner ?? null;
}
/** `id` and the sessions it runs under: closing one of those would close `id` too. */
function lineage(id: DebugSessionId): Set<DebugSessionId> {
const { sessions } = useDebug.getState();
const out = new Set<DebugSessionId>();
for (let cur = sessions[id]; cur && !out.has(cur.id); cur = cur.parent != null ? sessions[cur.parent] : undefined)
out.add(cur.id);
return out;
}
/** Closes the tabs one after another; a running session the user keeps stops the rest. */
async function closeTabs(ids: readonly DebugSessionId[]) {
for (const id of ids) if (useDebug.getState().sessions[id] && !(await closeSessionTab(id))) return;
}
/** A pinnable tab: a user-started session's (a child's tab goes with its parent). */
function pinnable(id: DebugSessionId | null) {
const s = id != null ? useDebug.getState().sessions[id] : undefined;
return s && s.parent == null ? s : undefined;
}
export function registerDebugTabActions(): () => void {
return registerActions([
{
id: "debug.closeTab",
title: "Close Tab",
category: "Debug",
enabled: (c) => target(c) !== null,
run: (c) => void closeSessionTab(target(c)!),
},
{
id: "debug.closeOtherTabs",
title: "Close Other Tabs",
category: "Debug",
enabled: (c) => target(c) !== null && owners().length > 1,
run: async (c) => {
const keep = lineage(target(c)!);
await closeTabs(owners().filter((id) => !keep.has(id)));
},
},
{
id: "debug.closeAllTabs",
title: "Close All Tabs",
category: "Debug",
enabled: () => owners().length > 0,
run: () => closeTabs(owners()),
},
{
id: "debug.pinTab",
title: "Pin / Unpin Tab",
category: "Debug",
icon: "pin",
enabled: (c) => !!pinnable(target(c)),
run: (c) => {
const s = pinnable(target(c));
if (s) pinSession(s.id, !s.pinned);
},
},
]);
}
/** The context menu of a Debug tab (IDEA's content tab menu); `owner` is the session the tab stands for. */
export function debugTabMenu(owner: DebugSessionId): MenuItem[] {
const s = pinnable(owner);
return [
{ kind: "action", action: "debug.closeTab", arg: owner },
{ kind: "action", action: "debug.closeAllTabs", arg: owner },
{ kind: "action", action: "debug.closeOtherTabs", arg: owner },
...(s
? ([
{ kind: "separator" },
{ kind: "action", action: "debug.pinTab", arg: owner, label: s.pinned ? "Unpin Tab" : "Pin Tab" },
] satisfies MenuItem[])
: []),
];
}
+2 -10
Переглянути файл
@@ -41,7 +41,6 @@ import {
} from "../state";
import { remoteWaitingNote } from "../remote";
import { sessionTabs } from "./sessionTabs";
import { debugTabMenu } from "../tabActions";
import { stopNote } from "../js/eventBreakpoints";
import { NetworkView } from "../js/NetworkView";
import { liveWatchesApply, useLiveWatches } from "../js/liveWatches";
@@ -139,15 +138,13 @@ export function DebugTabs() {
const list = Object.values(sessions);
if (!list.length) return null;
const tabs = sessionTabs(list);
const ownerOf = (id: string) => tabs.find((t) => t.session.id === +id)?.owner ?? +id;
const items: TabItem[] = tabs.map(({ session: s, child, owner }) => ({
const items: TabItem[] = tabs.map(({ session: s, child }) => ({
id: String(s.id),
label: child ? tx`${s.name} (child)` : s.name,
text: s.name,
icon: "debug",
italic: s.status === "terminated",
closable: true,
pinned: !child && !!sessions[owner]?.pinned,
tooltip: statusText(s),
}));
// A browser launch's own session has no tab: its page's tab stands for it.
@@ -160,12 +157,7 @@ export function DebugTabs() {
items={items}
activeId={shown ? String(shown.session.id) : null}
onSelect={(id) => selectSession(+id)}
onClose={(id) => void closeSessionTab(ownerOf(id))}
onTogglePin={(id) => void executeAction("debug.pinTab", ownerOf(id))}
onContextMenu={(id, e) => {
e.preventDefault();
showContextMenu(e, debugTabMenu(ownerOf(id)), { owner: e.currentTarget });
}}
onClose={(id) => void closeSessionTab(tabs.find((t) => t.session.id === +id)?.owner ?? +id)}
panelIdPrefix="openidea-debug"
/>
);
+1 -1
Переглянути файл
@@ -33,7 +33,7 @@
font-size: var(--fs-small, var(--fs-ui));
}
.button:hover,
.button.text:hover,
.button[aria-expanded="true"] {
background-color: var(--bg-toolbar-hover);
}
+13 -26
Переглянути файл
@@ -2,19 +2,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
import type { RunConfig } from "@/platform/ipc";
import type { RunTargetValid } from "@/platform/remoteSettings";
import { useAgentRunSettings, useWorkflowSettings } from "@/platform/workflowSettings";
import {
EmptyState,
Icon,
IconButton,
Menu,
SplitGroup,
SplitHandle,
SplitPanel,
Tabs,
type TabItem,
T,
showContextMenu,
} from "@/ui";
import { EmptyState, Icon, IconButton, Menu, SplitGroup, SplitHandle, SplitPanel, Tabs, type TabItem, T } from "@/ui";
import { executeAction } from "@/platform/actions";
import { formatBinding, hostBinding, shortcutLabel } from "@/platform/keymap";
import { ConsoleModel } from "./console";
@@ -27,6 +15,7 @@ import {
baseConfigOf,
canRunSelectedTests,
clearConsole,
closeSessionTab,
closeStdin,
debugSession,
failedTests,
@@ -42,13 +31,11 @@ import {
writeTerminal,
type RunSession,
isLive,
pinSession,
} from "./store";
import { rerunFailedConfigs } from "./testing/rerun";
import { nodeOutput } from "./testing";
import { TestTreeView, formatDuration } from "./TestTreeView";
import { StackTracePanel, useStackTraces, type StackTraceTab } from "./stackTrace";
import { closeRunTab, runTabMenu } from "./runTabs";
import { StackTracePanel, closeStackTrace, useStackTraces, type StackTraceTab } from "./stackTrace";
import { showApplicationUpdate, useApplicationUpdates } from "./applicationUpdate";
import { isUpdatable } from "./updatePolicies";
import { configIcon } from "./configs";
@@ -56,6 +43,7 @@ import { agentName } from "./agentConfig";
import { archivedAgentSessions, closeAgentArchive } from "./agentRuns";
import styles from "./Run.module.css";
import { tx } from "@/platform/i18n";
import { toolWindowService } from "@/workbench/contributions";
function failed(s: RunSession): boolean {
return s.status === "failed" || (s.status === "finished" && s.exitCode !== 0 && !s.stopped);
@@ -290,7 +278,6 @@ function sessionItems(sessions: readonly RunSession[], targets: readonly RunTarg
// An agent's command that ended, however it ended.
dimmed: agentName(s.config) !== null && !isLive(s),
closable: true,
pinned: s.pinned,
tooltip:
s.status === "finished"
? tx`Finished in ${formatDuration((s.endedAt ?? s.startedAt) - s.startedAt)}, exit code ${s.exitCode ?? "?"}`
@@ -312,6 +299,14 @@ function currentTab(
return traces.length ? { trace: traces[traces.length - 1]! } : null;
}
/** Closes a session or stack trace tab; as in IDEA, the tool window goes away with its last tab. */
export async function closeRunTab(id: string, traces: readonly StackTraceTab[]) {
if (traces.some((t) => t.id === id)) closeStackTrace(id);
else if (!(await closeSessionTab(id))) return;
if (!useRun.getState().sessions.length && !useStackTraces.getState().tabs.length)
toolWindowService().hide("run", { removeFromStripe: true });
}
/** Session (and stack trace) tabs in the tool window header. */
export function RunTabs() {
const sessions = useRun((s) => s.sessions);
@@ -336,15 +331,7 @@ export function RunTabs() {
items={items}
activeId={activeId}
onSelect={(id) => useRun.setState({ activeSession: id })}
onClose={(id) => void closeRunTab(id)}
onTogglePin={(id) => {
const s = sessions.find((x) => x.id === id);
if (s) pinSession(s.id, !s.pinned);
}}
onContextMenu={(id, e) => {
e.preventDefault();
showContextMenu(e, runTabMenu(id), { owner: e.currentTarget });
}}
onClose={(id) => void closeRunTab(id, traces)}
panelIdPrefix="openidea-run"
trailing={archived.length > 0 && <AgentArchive sessions={archived} activeId={activeId} />}
/>
+4 -4
Переглянути файл
@@ -1,7 +1,7 @@
import { expect, it, vi } from "vitest";
import type { RunConfig } from "@/platform/ipc";
import { provideToolWindowService, type ToolWindowService } from "@/workbench/contributions";
import { closeRunTab } from "./runTabs";
import { closeRunTab } from "./RunView";
import { useStackTraces } from "./stackTrace";
import { useRun, type RunSession } from "./store";
@@ -20,9 +20,9 @@ it("hides the Run tool window once its last tab is closed", async () => {
provideToolWindowService({ hide } as unknown as ToolWindowService);
useRun.setState({ sessions: [finished("a"), finished("b")] });
useStackTraces.setState({ tabs: [{ id: "t", title: "Trace", text: "" } as never] });
await closeRunTab("a");
await closeRunTab("t");
await closeRunTab("a", useStackTraces.getState().tabs);
await closeRunTab("t", useStackTraces.getState().tabs);
expect(hide).not.toHaveBeenCalled();
await closeRunTab("b");
await closeRunTab("b", useStackTraces.getState().tabs);
expect(hide).toHaveBeenCalledWith("run", { removeFromStripe: true });
});
+3 -105
Переглянути файл
@@ -1,36 +1,15 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import type { RunConfig } from "@/platform/ipc";
import { useProject } from "@/platform/project";
import { menuGroupItems, registerMenuGroup } from "@/workbench/contributions";
import type { MenuItem } from "@/ui";
import { canDebugProduced, choiceItems, compiledDebugPlan, contextMenuItems } from "./contextRun";
import { OPTIMISTIC, produceContextChoices, type ToolEnv } from "./producers";
import { canDebugProduced, choiceItems, contextMenuItems } from "./contextRun";
import { OPTIMISTIC, produceContextChoices } from "./producers";
import { gutterTestState, markerIcon, targetMenuItems } from "./runGutter";
import { useRun } from "./store";
import type { RunTarget } from "./targets";
import { TestResults } from "./testing";
import { clearTestStates, recordTestStates } from "./testing/states";
import { setInvokeRoute } from "@/platform/tauriCore";
const mocks = vi.hoisted(() => ({
tauri: { value: false },
read: { value: null as string | null },
toolEnv: { value: null as ((...args: unknown[]) => unknown) | null },
}));
vi.mock("@/platform/ipc", async (importOriginal) => {
const actual = await importOriginal<typeof import("@/platform/ipc")>();
return { ...actual, isTauri: () => mocks.tauri.value };
});
vi.mock("./runtimes", async (importOriginal) => {
const actual = await importOriginal<typeof import("./runtimes")>();
return {
...actual,
toolEnv: (...args: Parameters<typeof actual.toolEnv>) =>
mocks.toolEnv.value ? mocks.toolEnv.value(...args) : actual.toolEnv(...args),
};
});
const ROOT = "/work/app";
const SCRATCHES = "/Users/me/scratches";
@@ -224,23 +203,6 @@ describe("run gutter popup", () => {
describe("Debug entry", () => {
const config = (c: Partial<RunConfig>) =>
({ id: "c", name: "c", command: "", args: [], cwd: "", env: {}, ...c }) as RunConfig;
const MAIN_ZIG = [
'const std = @import("std");',
"pub fn main() !void {",
' std.debug.print("hi\\n", .{});',
"}",
].join("\n");
const CALC_ZIG = [
'const std = @import("std");',
'test "adds two numbers" {',
" try std.testing.expect(true);",
"}",
].join("\n");
const zigEnv: ToolEnv = {
tool: (n) => (n === "zig" ? { path: "/bin/zig", version: "0.17.1" } : null),
compileDir: "/tmp/openidea-run",
};
it("is offered for Maven and Gradle tests, which run with a JDWP agent", () => {
const mavenTest = config({ kind: "maven", command: "mvn", args: ["test", "-Dtest=A#b"], isTest: true });
expect(canDebugProduced({ name: "A.b", scope: "element", config: mavenTest })).toBe(true);
@@ -255,70 +217,6 @@ describe("Debug entry", () => {
canDebugProduced({ name: "lint", scope: "file", config: config({ kind: "npm", args: ["run", "lint"] }) }),
).toBe(false);
});
beforeEach(() => {
useProject.setState({ project: { name: "app", root: ROOT, trusted: true, isGit: false, settings: {} } as never });
useRun.setState({ configs: [], loaded: true });
mocks.tauri.value = true;
mocks.toolEnv.value = () => zigEnv;
// Bindings' `typedError` wraps the raw invoke result: `fs_read_text` resolves to its text.
setInvokeRoute((cmd) =>
cmd === "fs_read_text" ? Promise.resolve(mocks.read.value) : Promise.reject(new Error("not stubbed")),
);
});
afterEach(() => {
mocks.tauri.value = false;
mocks.read.value = null;
mocks.toolEnv.value = null;
setInvokeRoute(null);
useProject.setState({ project: null });
useRun.setState({ configs: [], loaded: false });
});
it("re-debug of a saved zig file config rebuilds the build-then-debug plan", async () => {
mocks.read.value = MAIN_ZIG;
const saved = config({
id: "context:file:src/main.zig",
kind: "zig",
command: "zig",
args: ["run", "src/main.zig"],
cwd: null,
});
const plan = await compiledDebugPlan(saved);
expect(plan?.debug).toBeTruthy();
expect(plan!.debug!.build.command).toMatch(
/zig build-exe src\/main\.zig -femit-bin=\/tmp\/openidea-run\/main-\w+$/,
);
expect(plan!.debug!.config).toMatchObject({
kind: "native",
debugAdapter: "codelldb",
params: { language: "zig" },
});
});
it("re-debug of a saved zig test config re-targets its test", async () => {
mocks.read.value = CALC_ZIG;
const saved = config({
id: "context:file:src/calc.zig#adds two numbers",
kind: "zig",
command: "zig",
args: ["test", "src/calc.zig", "--test-filter", "adds two numbers"],
cwd: null,
});
const plan = await compiledDebugPlan(saved);
expect(plan?.debug).toBeTruthy();
expect(plan!.debug!.build.command).toMatch(
/zig test src\/calc\.zig --test-filter 'adds two numbers' --test-no-exec/,
);
expect(plan!.debug!.config.args).toEqual([]);
});
it("leaves other configurations alone", async () => {
expect(await compiledDebugPlan(config({ id: "c", kind: "shell", command: "make" }))).toBeNull();
expect(
await compiledDebugPlan(config({ id: "user:x", kind: "zig", command: "zig", args: ["run", "x.zig"], cwd: null })),
).toBeNull();
});
});
describe("Run with Fuzzing", () => {
+3 -3
Переглянути файл
@@ -197,9 +197,9 @@ export async function compiledDebugPlan(config: RunConfig): Promise<Produced | n
const input: ContextInput = { ...projectContext(), root, path, content };
const env = await toolEnv(neededTools(input), path, root, false);
if (m[2]) {
// A test: find the target by name again (targets and the id carry the unescaped name).
// A test: find the target by name again.
if (config.kind === "zig") {
const test = zigTargets(content ?? "").find((t) => t.name === m[2]);
const test = zigTargets(content ?? "").find((t) => t.name.replace(/\\(.)/g, "$1") === m[2]);
return test ? produceContext({ ...input, line: test.line }, env) : null;
}
const lines = (content ?? "").split("\n");
@@ -365,7 +365,7 @@ function peekContent(path: string): string | undefined {
/** Whether the file's runnability depends on its text (`package main`, `fn main`, `#!`…). */
function needsContent(path: string): boolean {
const name = path.slice(path.lastIndexOf("/") + 1);
return /\.(go|rs|java|kt|c|cpp|cc|cxx|zig)$/.test(name) || !name.includes(".");
return /\.(go|rs|java|kt|c|cpp|cc|cxx)$/.test(name) || !name.includes(".");
}
export async function prepareMenu(_menu: ContextMenuId, ctx: MenuGroupContext): Promise<void> {
+1 -3
Переглянути файл
@@ -43,7 +43,6 @@ import { registerExternalTools } from "./externalTools";
import { openImport } from "./importConfigs";
import { registerComposerInstall } from "./composerInstall";
import { registerGoActions } from "./goActions";
import { registerRunTabActions } from "./runTabs";
import { invalidateGoModules } from "./goModules";
import { invalidatePythonPackages } from "./pythonPackages";
import { RunWidget } from "./RunWidget";
@@ -186,7 +185,7 @@ async function debugConfig(config: RunConfig, chain: readonly string[] = [], sho
await executeAction("debug.startConfig", show === false ? { config, show: false } : config);
return;
}
// A single compiled file (rustc / clang / zig): build with debug info, then debug the binary.
// A single compiled file (rustc / clang): build with debug info, then debug the binary.
const compiled = await compiledDebugPlan(config);
if (compiled?.debug) {
await launchProduced(compiled, true, show);
@@ -404,7 +403,6 @@ export function activate(): () => void {
registerBuiltinTypes(),
registerRunConfigType(junitType),
registerGoActions(),
registerRunTabActions(),
registerComposerInstall(),
registerOverlay({ id: "run.chooser", component: ConfigChooserOverlay }),
registerOverlay({
+1 -6
Переглянути файл
@@ -424,12 +424,7 @@ describe("single-file producers (scratches)", () => {
expect(main.debug!.build.command).toMatch(
/^mkdir -p \/tmp\/openidea-run && \/bin\/zig build-exe calc\.zig -femit-bin=\/tmp\/openidea-run\/calc-\w+$/,
);
expect(main.debug!.config).toMatchObject({
kind: "native",
debugAdapter: "codelldb",
args: [],
params: { language: "zig" },
});
expect(main.debug!.config).toMatchObject({ kind: "native", debugAdapter: "codelldb", args: [] });
const test = produceContext(scratch("calc.zig", src, 3), e)!;
expect(test).toMatchObject({ name: "adds two numbers", scope: "element" });
+1 -9
Переглянути файл
@@ -660,15 +660,7 @@ function zigFile(input: ContextInput, env: ToolEnv, s: FileSite, test?: { name:
isTest: false,
debugAdapter: null,
};
const binary: RunConfig = {
...config,
kind: "native",
command: out,
args: [],
isTest: false,
// Marks the debugged program as Zig: CodeLLDB's source mode stays off (see nativeLaunch).
params: { language: "zig" },
};
const binary: RunConfig = { ...config, kind: "native", command: out, args: [], isTest: false };
return { name, scope, config, debug: { build: buildConfig, config: binary } };
}
-71
Переглянути файл
@@ -1,71 +0,0 @@
import { afterEach, beforeEach, expect, it, vi } from "vitest";
import type { RunConfig } from "@/platform/ipc";
const ipc = vi.hoisted(() => ({
runStart: vi.fn(async () => ({ status: "ok", data: 1 })),
runStop: vi.fn(async () => ({ status: "ok", data: null })),
}));
vi.mock("@/platform/ipc", async (importOriginal) => {
const real = await importOriginal<typeof import("@/platform/ipc")>();
return { ...real, isTauri: () => true, commands: { ...real.commands, ...ipc } };
});
vi.mock("@tauri-apps/api/core", async (importOriginal) => ({
...(await importOriginal<typeof import("@tauri-apps/api/core")>()),
Channel: class {
onmessage: ((m: unknown) => void) | null = null;
},
}));
import { useProject } from "@/platform/project";
import {
provideNotificationService,
provideToolWindowService,
type ToolWindowService,
} from "@/workbench/contributions";
import { resetRuns, startRun, useRun, type RunSession } from "./store";
const config = (id: string): RunConfig =>
({ id, name: id, kind: "shell", command: "true", args: [], cwd: null, env: {} }) as unknown as RunConfig;
const finished = (id: string, extra: Partial<RunSession> = {}): RunSession =>
({
id,
config: config(id),
runId: null,
status: "finished",
exitCode: 0,
startedAt: 0,
endedAt: 1,
...extra,
}) as RunSession;
const tabs = () => useRun.getState().sessions.map((s) => `${s.id}:${s.config.id}${s.pinned ? "*" : ""}`);
beforeEach(() => {
vi.clearAllMocks();
useProject.setState({ settings: null });
provideNotificationService({ notify: vi.fn() });
provideToolWindowService({ show: vi.fn(), hide: vi.fn() } as unknown as ToolWindowService);
});
afterEach(() => resetRuns());
it("takes the place of the same configuration's finished tab, then the selected one, never a pinned one", async () => {
useRun.setState({ sessions: [finished("a"), finished("b"), finished("c", { pinned: true })], activeSession: "b" });
await startRun(config("a"), undefined, undefined, { prepared: true });
expect(tabs()).toEqual(["a:a", "b:b", "c:c*"]);
useRun.setState({ activeSession: "b" });
await startRun(config("x"), undefined, undefined, { prepared: true });
expect(tabs()).toEqual(["a:a", "b:x", "c:c*"]);
});
it("opens a new tab when every finished tab is pinned, pinned when the setting asks", async () => {
useProject.setState({ settings: { run: { pinTabs: true } } as never });
useRun.setState({ sessions: [finished("a", { pinned: true })], activeSession: "a" });
await startRun(config("a"), undefined, undefined, { prepared: true });
expect(tabs()).toEqual(["a:a*", expect.stringMatching(/^run\d+:a\*$/)]);
});
it("keeps an agent's commands out of other tabs", async () => {
const agent = { ...config("agent:build"), params: { "mcp.agentRun": "build" } } as RunConfig;
useRun.setState({ sessions: [finished("a")], activeSession: "a" });
await startRun(agent, undefined, undefined, { prepared: true });
expect(useRun.getState().sessions).toHaveLength(2);
});
-95
Переглянути файл
@@ -1,95 +0,0 @@
import { registerActions, type ActionContext } from "@/platform/actions";
import { agentRunSettings } from "@/platform/workflowSettings";
import type { MenuItem } from "@/ui";
import { toolWindowService } from "@/workbench/contributions";
import { archivedAgentSessions } from "./agentRuns";
import { closeStackTrace, useStackTraces } from "./stackTrace";
import { closeSessionTab, pinSession, useRun } from "./store";
/** Ids of the tabs in the Run tool window's strip: sessions (less archived agent tabs), then stack traces. */
export function runTabIds(): string[] {
const { sessions } = useRun.getState();
const archived = new Set(archivedAgentSessions(sessions, agentRunSettings().tabLimit).map((s) => s.id));
return [
...sessions.filter((s) => !archived.has(s.id)).map((s) => s.id),
...useStackTraces.getState().tabs.map((t) => t.id),
];
}
/** Closes a session or stack trace tab; as in IDEA, the tool window goes away with its last tab. */
export async function closeRunTab(id: string): Promise<boolean> {
if (useStackTraces.getState().tabs.some((t) => t.id === id)) closeStackTrace(id);
else if (!(await closeSessionTab(id))) return false;
if (!useRun.getState().sessions.length && !useStackTraces.getState().tabs.length)
toolWindowService().hide("run", { removeFromStripe: true });
return true;
}
/** Closes the tabs one after another; a running process the user keeps stops the rest. */
async function closeTabs(ids: readonly string[]) {
for (const id of ids) if (!(await closeRunTab(id))) return;
}
function target({ arg }: ActionContext): string | null {
const id = typeof arg === "string" ? arg : useRun.getState().activeSession;
return id != null && runTabIds().includes(id) ? id : null;
}
const session = (id: string | null) => useRun.getState().sessions.find((s) => s.id === id);
export function registerRunTabActions(): () => void {
return registerActions([
{
id: "run.closeTab",
title: "Close Tab",
category: "Run",
enabled: (c) => target(c) !== null,
run: (c) => void closeRunTab(target(c)!),
},
{
id: "run.closeOtherTabs",
title: "Close Other Tabs",
category: "Run",
enabled: (c) => target(c) !== null && runTabIds().length > 1,
run: async (c) => {
const id = target(c)!;
await closeTabs(runTabIds().filter((t) => t !== id));
if (session(id)) useRun.setState({ activeSession: id });
},
},
{
id: "run.closeAllTabs",
title: "Close All Tabs",
category: "Run",
enabled: () => runTabIds().length > 0,
run: () => closeTabs(runTabIds()),
},
{
id: "run.pinTab",
title: "Pin / Unpin Tab",
category: "Run",
icon: "pin",
enabled: (c) => !!session(target(c)),
run: (c) => {
const s = session(target(c));
if (s) pinSession(s.id, !s.pinned);
},
},
]);
}
/** The context menu of a Run tab (IDEA's content tab menu). */
export function runTabMenu(id: string): MenuItem[] {
const s = session(id);
return [
{ kind: "action", action: "run.closeTab", arg: id },
{ kind: "action", action: "run.closeAllTabs", arg: id },
{ kind: "action", action: "run.closeOtherTabs", arg: id },
...(s
? ([
{ kind: "separator" },
{ kind: "action", action: "run.pinTab", arg: id, label: s.pinned ? "Unpin Tab" : "Pin Tab" },
] satisfies MenuItem[])
: []),
];
}
+2 -24
Переглянути файл
@@ -93,8 +93,6 @@ export interface RunSession {
terminal?: boolean;
/** The HTTP port the application's embedded web server reported it started on. */
httpPort?: number;
/** Pinned tab: once finished, a later run opens a new tab instead of taking its place. */
pinned?: boolean;
}
interface SessionRuntime {
@@ -754,25 +752,7 @@ export function sessionsOf(config: RunConfig, sessions = useRun.getState().sessi
return sessions.filter((s) => ids.has(s.config.id));
}
/**
* The finished tab a run of `config` takes the place of, as IDEA reuses run content: not a
* pinned one; that of the same configuration first, then the selected one, then the first.
* Agents' commands keep to their own tabs.
*/
function reusableTab(sessions: readonly RunSession[], active: string | null, config: RunConfig) {
const free = sessions.filter((s) => !isLive(s) && !s.pinned);
const same = free.find((s) => s.config.id === config.id);
if (same || agentName(config) !== null) return same;
const others = free.filter((s) => agentName(s.config) === null);
return others.find((s) => s.id === active) ?? others[0];
}
/** Pins or unpins a session's tab. */
export function pinSession(id: string, pinned: boolean) {
patchSession(id, { pinned });
}
/** Starts `config`, reusing the tab of a finished (or the given) session (see `reusableTab`). */
/** Starts `config`, reusing the tab of a finished (or the given) session for the same config. */
export async function startRun(
config: RunConfig,
reuse?: string,
@@ -840,8 +820,7 @@ export async function startRun(
// The new process starts once the old one is gone (it may still hold a port or a lock). As in
// IDEA there is no time limit: a first Stop waits for the shutdown, a second one kills it.
await waitForExit(stopping, null);
const now = useRun.getState();
session ??= reusableTab(now.sessions, now.activeSession, config);
session ??= state.sessions.find((s) => s.config.id === config.id && !busy(s));
try {
await maybeEditorService()?.saveAll();
@@ -885,7 +864,6 @@ export async function startRun(
endedAt: null,
stdinClosed: false,
baseConfig,
pinned: workflowSettings().run.pinTabs || undefined,
};
runtimes.set(session.id, newRuntime(config));
const created = session;
-24
Переглянути файл
@@ -167,30 +167,6 @@ describe("rerunFailedConfig", () => {
expect(new Set(configs.map((config) => config.id)).size).toBe(2);
});
it("zig: --test-filter per failed test, straight for `zig test` and via the module root for `zig build`", async () => {
expect(
(await rerunFailedConfig(cfg("zig", "zig", ["test", "src/calc.zig"]), [
["calc", "adds"],
["calc", "subs"],
]))!.args,
).toEqual(["test", "src/calc.zig", "--test-filter", "adds", "--test-filter", "subs"]);
// Existing filters are replaced, not stacked.
expect(
(await rerunFailedConfig(cfg("zig", "zig", ["test", "src/calc.zig", "--test-filter", "adds"]), [
["calc", "subs"],
]))!.args,
).toEqual(["test", "src/calc.zig", "--test-filter", "subs"]);
// `zig build` forwards no args to the test binary: rerun goes through the module root
// the detection recorded.
expect(
(await rerunFailedConfig({ ...cfg("zig", "zig", ["build", "test"]), params: { testRoot: "src/tally.zig" } }, [
["tally", "subs"],
]))!.args,
).toEqual(["test", "src/tally.zig", "--test-filter", "subs"]);
// Without a recorded root a build-based run can't narrow.
expect(await rerunFailedConfig(cfg("zig", "zig", ["build", "test"]), [["tally", "subs"]])).toBeNull();
});
it("gradle and maven filters", async () => {
expect((await rerunFailedConfig(cfg("gradle", "./gradlew", ["test"]), [["com.a.FooTest", "bar()"]]))!.args).toEqual(
["test", "--tests", "com.a.FooTest.bar"],
+11 -10
Переглянути файл
@@ -250,17 +250,18 @@ function builtinTestsConfig(config: RunConfig, paths: string[][], name: string):
args = goArgs(config.args, paths, config.params?.scope === "file" ? config.params.target : undefined);
break;
case "zig": {
// `zig test <file>` filters directly. A build-based run (`zig build test`) forwards no
// args to the test binary, so it re-runs through the module root the detection recorded
// — exact for projects whose tests live in one module's import graph.
const file = config.args[0] === "test" ? config.args[1] : config.params?.testRoot;
// Only a standalone `zig test <file>` can filter: `zig build` forwards no args to the test binary.
const file = config.args[0] === "test" ? config.args[1] : undefined;
const names = unique(paths.map((p) => p[p.length - 1]!).filter(Boolean));
if (!file || !names.length) {
args = null;
break;
}
const rest = dropFlag(config.args[0] === "test" ? config.args.slice(2) : [], ["--test-filter"]);
args = ["test", file, ...rest, ...names.flatMap((n) => ["--test-filter", n])];
args =
file && names.length
? [
config.args[0]!,
file,
...dropFlag(config.args.slice(2), ["--test-filter"]),
...names.flatMap((n) => ["--test-filter", n]),
]
: null;
break;
}
case "jvm":
+3 -5
Переглянути файл
@@ -175,7 +175,7 @@ export function FindTabs() {
id: String(t.id),
label: t.title,
closable: true,
pinned: t.pinned,
icon: t.pinned ? "pin" : undefined,
}))}
activeId={String(tab.id)}
onSelect={(id) => useFindWindow.setState({ active: Number(id) })}
@@ -187,18 +187,16 @@ export function FindTabs() {
onContextMenu={(id, e) => {
const t = tabs.find((x) => x.id === Number(id));
if (!t) return;
// IDEA's content tab menu.
showContextMenu(e, [
{ id: "pin", label: t.pinned ? "Unpin Tab" : "Pin Tab", run: () => pinFindTab(t.id, !t.pinned) },
{ kind: "separator" },
{ id: "close", label: "Close Tab", run: () => closeFindTab(t.id) },
{ id: "closeAll", label: "Close All Tabs", run: () => tabs.forEach((o) => closeFindTab(o.id)) },
{
id: "closeOthers",
label: "Close Other Tabs",
disabled: tabs.length < 2,
run: () => tabs.forEach((o) => o.id !== t.id && closeFindTab(o.id)),
},
{ kind: "separator" },
{ id: "pin", label: t.pinned ? "Unpin Tab" : "Pin Tab", run: () => pinFindTab(t.id, !t.pinned) },
]);
}}
/>
-6
Переглянути файл
@@ -384,12 +384,6 @@ export const EXECUTION_PAGES: PageSpec[] = [
key: "run.showOnStart",
label: "Show Run/Debug window when a process is started",
},
{
kind: "toggle",
key: "run.pinTabs",
label: "Make configurations pinned by default",
hint: "A finished pinned tab stays open: a later run opens a new tab instead of taking its place.",
},
{
kind: "number",
key: "run.terminationTimeoutMs",
-20
Переглянути файл
@@ -3,7 +3,6 @@ import { commands } from "@/platform/ipc";
import { usePlatform } from "@/platform/platformInfo";
import { isRemoteWindow } from "@/platform/remote";
import { handleFileDrop } from "@/workbench/fileDrop";
import { dragSession, FILES_DRAG } from "@/ui";
import { droppedPathsText, installTerminalDrop } from "./drop";
vi.mock("@/platform/remote", () => ({ isRemoteWindow: vi.fn(() => false) }));
@@ -110,23 +109,4 @@ describe("terminal drop", () => {
remove();
}
});
it("pastes paths dragged from the Project view, also in a remote window", () => {
vi.mocked(isRemoteWindow).mockReturnValue(true);
const host = document.createElement("div");
const child = document.createElement("span");
host.append(child);
document.body.replaceChildren(host);
document.elementFromPoint = (() => child) as typeof document.elementFromPoint;
const paste = vi.fn();
const remove = installTerminalDrop(host, paste);
try {
const drag = dragSession({ type: FILES_DRAG, payload: "/p/src/a b.ts\n/p/docs/" });
expect(drag.move(1, 1)).toBe(true);
expect(drag.drop(1, 1)).toBe(true);
expect(paste.mock.calls).toEqual([["'/p/src/a b.ts' /p/docs"]]);
} finally {
remove();
}
});
});
+7 -26
Переглянути файл
@@ -1,14 +1,13 @@
/**
* Files dropped from the OS or dragged from the Project view onto a terminal: the
* path is pasted at the prompt (shell-quoted when needed), separated by spaces,
* instead of the window-wide drop (open in the editor, "Open project in" for a
* folder) or the browser / shell falling through to a system dialog. Images are
* treated the same way: showing the path is the expected behaviour.
* Files dropped from the OS onto a terminal: the path is pasted at the prompt
* (shell-quoted when needed), separated by spaces, instead of the window-wide
* drop (open in the editor, "Open project in" for a folder) or the browser /
* shell falling through to a system dialog. Images are treated the same way:
* showing the path is the expected behaviour.
*/
import { formatArgs } from "@/platform/commandLine";
import { usePlatform } from "@/platform/platformInfo";
import { isRemoteWindow } from "@/platform/remote";
import { FILES_DRAG, registerDropTarget } from "@/ui";
import { registerFileDropHandler } from "@/workbench/fileDrop";
/** Shell-quoted paths for the prompt; the user presses Enter to execute. */
@@ -18,31 +17,13 @@ export function droppedPathsText(paths: readonly string[]): string {
return formatArgs(paths);
}
/** Registers the terminal's drop handlers; the disposer unregisters them. */
/** Registers the terminal's drop handler; the disposer unregisters it. */
export function installTerminalDrop(host: HTMLElement, paste: (text: string) => void): () => void {
const removeOsDrop = registerFileDropHandler((paths, x, y) => {
// A remote terminal can't reach this machine's files.
return registerFileDropHandler((paths, x, y) => {
if (!paths.length || isRemoteWindow()) return false;
const el = document.elementFromPoint(x, y);
if (!el || !host.contains(el)) return false;
paste(droppedPathsText(paths));
return true;
});
// Project view paths are the project's own, so they also hold in a remote terminal.
const removeProjectDrop = registerDropTarget(host, {
accepts: (type) => type === FILES_DRAG,
over: () => {},
leave: () => {},
drop: (data) => {
const paths = data.payload
.split("\n")
.filter(Boolean)
.map((p) => (p.length > 1 && p.endsWith("/") ? p.slice(0, -1) : p));
if (paths.length) paste(droppedPathsText(paths));
},
});
return () => {
removeOsDrop();
removeProjectDrop();
};
}
+2 -34
Переглянути файл
@@ -39,7 +39,7 @@ import { bindingsFor, normalizeBinding, shortcutLabel, strokeOf } from "@/platfo
import { tx } from "@/platform/i18n";
import { useProject } from "@/platform/project";
import { isVirtualUri } from "@/platform/virtualDocs";
import { IconButton, Select, Spinner, T, isPopupOpen, showContextMenu } from "@/ui";
import { IconButton, Select, Spinner, T, showContextMenu } from "@/ui";
import { registerSoftWrapSurface } from "@/workbench/softWrap";
import { editorService, type EditorInput } from "@/workbench/contributions";
import { changedLineGutters, diffOverview, diffWords, languageFor, viewerBase } from "./cm";
@@ -218,10 +218,6 @@ const editableExtensions = (): Extension => [history(), keymap.of(historyKeymap)
type Side = "a" | "b";
type ViewUpdateFn = (u: { docChanged: boolean; selectionSet: boolean }) => void;
/** Esc alone, not while composing text. */
const plainEscape = (e: KeyboardEvent) =>
e.key === "Escape" && !e.shiftKey && !e.altKey && !e.metaKey && !e.ctrlKey && !e.nativeEvent.isComposing;
/** Whether `e` is one of the strokes bound to `actionId`. */
function isStrokeOf(e: KeyboardEvent, actionId: string): boolean {
const stroke = strokeOf(e.nativeEvent);
@@ -233,7 +229,6 @@ export function DiffEditor({
toolbarActions,
onNeighbour,
onJumpToSource,
onClose,
}: {
input: EditorInput;
toolbarActions?: ReactNode;
@@ -245,8 +240,6 @@ export function DiffEditor({
onNeighbour?: (data: DiffData) => void;
/** Called as Jump to Source opens the file in an editor: a modal host closes, or the editor stays hidden behind it. */
onJumpToSource?: () => void;
/** Closes the diff's editor tab (Esc, IDEA's Close Diff Editor); hosts that aren't tabs leave it out. */
onClose?: () => void;
}) {
const data = input.data as DiffData;
const { path, left, right } = data;
@@ -983,18 +976,6 @@ export function DiffEditor({
closeFind();
return true;
};
/** Select Opposite Diff Pane (⌃⇧⇥): the other side keeps its own caret, as in IDEA. */
const focusOpposite = () => {
const views = viewsRef.current;
if (!views?.a) return false;
(focusedSide.current === "a" ? views.b : views.a).focus();
return true;
};
const close = () => {
if (!onClose) return false;
onClose();
return true;
};
const onKeyDown = (e: KeyboardEvent) => {
if (e.defaultPrevented) return;
if (isStrokeOf(e, "find.inFile")) openFind(false);
@@ -1003,8 +984,7 @@ export function DiffEditor({
if (!findOpen.current) findSide.current = focusedSide.current;
if (!searchAgain(findTarget, isStrokeOf(e, "find.next"))) openFind(false);
} else if (e.key === "Escape" && findOpen.current) closeFind();
// Esc that nothing in the diff took (find bar, selection, completion) closes its tab, as in IDEA.
else if (!plainEscape(e) || isPopupOpen() || !close()) return;
else return;
e.preventDefault();
e.stopPropagation();
};
@@ -1142,8 +1122,6 @@ export function DiffEditor({
applyChange,
file,
showSettings,
focusOpposite,
close,
viewer: viewerOf,
});
useLayoutEffect(() => {
@@ -1157,8 +1135,6 @@ export function DiffEditor({
applyChange,
file,
showSettings,
focusOpposite,
close,
viewer: viewerOf,
});
});
@@ -1374,14 +1350,6 @@ export function DiffEditor({
onClick={() => void executeAction("vcs.showDiffInNewWindow", data)}
/>
)}
{auxiliaryWindow()?.kind === "diff" && getAction("vcs.showDiffInEditorTab") && (
<IconButton
icon="editor-only"
title="Show Diff in Editor Tab"
size="small"
onClick={() => void executeAction("vcs.showDiffInEditorTab")}
/>
)}
<IconButton
icon="settings"
title="Settings"
+9 -38
Переглянути файл
@@ -26,14 +26,12 @@ import { TitleBar } from "@/workbench/TitleBar";
import { WindowResizeHandles } from "@/workbench/WindowResizeHandles";
import { useApplyTheme, windowBackgroundHex } from "@/workbench/theme";
import { DiffEditor } from "./DiffEditor";
import { DIFF_KIND, diffInput, diffTitle, openDiff, type DiffData, type DiffSide } from "./diffInput";
import { DIFF_KIND, diffInput, diffTitle, type DiffData, type DiffSide } from "./diffInput";
import { refreshNow, useVcs } from "./store";
import styles from "./common.module.css";
/** Opens `path` in the project window's editor (DiffEditor's "Jump to Source"). */
const OPEN_SOURCE = "openidea://diff/open-source";
/** Opens a diff in an editor tab of the project window (IDEA's "Show Diff in Editor Tab"). */
const OPEN_DIFF = "openidea://diff/open-diff";
const useDiff = create<{
data: DiffData | null;
@@ -107,24 +105,6 @@ export function activateDiffWindow(parent: string): () => void {
}
},
});
// IDEA's Show Diff in Editor Tab: the diff moves to the project window, edits saved first.
const unregisterToEditor = registerAction({
id: "vcs.showDiffInEditorTab",
title: "Show Diff in Editor Tab",
category: "Git",
icon: "editor-only",
run: async () => {
const data = useDiff.getState().data;
if (!data) return;
try {
await documents.save();
await emitTo(parent, OPEN_DIFF, data);
await native.close();
} catch (error) {
showError(tx("Cannot show diff in an editor tab"), error instanceof Error ? error.message : String(error));
}
},
});
const savingReady = installDiffWindowSave(parent, documents.save).then((off) => {
if (disposed) off();
else stopSave = off;
@@ -218,7 +198,6 @@ export function activateDiffWindow(parent: string): () => void {
stopClose?.();
stopSave?.();
unregisterSave();
unregisterToEditor();
window.removeEventListener("keydown", onKey);
window.removeEventListener("keydown", onCloseShortcut, true);
};
@@ -228,31 +207,23 @@ export function activateDiffWindow(parent: string): () => void {
export function installDiffEditorHost(): () => void {
if (!isTauri()) return () => {};
let disposed = false;
const stops: Array<() => void> = [];
const keep = (off: () => void) => {
if (disposed) off();
else stops.push(off);
};
const native = getCurrentWindow();
void native
let stop: (() => void) | undefined;
void getCurrentWindow()
.listen<{ path: string; options?: OpenOptions }>(OPEN_SOURCE, ({ payload }) => {
const folder = projectFolder(useProject.getState().project, payload.path);
if (folder && payload.path.startsWith(`${folder.root}/`)) {
void editorService()
.openFile(payload.path, payload.options)
.then(() => native.setFocus());
.then(() => getCurrentWindow().setFocus());
}
})
.then(keep);
void native
.listen<DiffData>(OPEN_DIFF, ({ payload }) => {
if (!projectFolder(useProject.getState().project, payload.path)) return;
void openDiff(payload).then(() => native.setFocus());
})
.then(keep);
.then((off) => {
if (disposed) off();
else stop = off;
});
return () => {
disposed = true;
for (const stop of stops) stop();
stop?.();
};
}
-2
Переглянути файл
@@ -356,8 +356,6 @@ export function MergeEditor({ input }: { input: EditorInput }) {
applyChange: () => false,
file: () => {},
showSettings: () => void actionHost.run("settings"),
focusOpposite: () => false,
close: () => false,
}));
useEffect(
() => () => {
-43
Переглянути файл
@@ -231,46 +231,3 @@ describe("Find in the diff viewer", () => {
expect(v.state.selection.main.from).toBe(8);
});
});
describe("Esc and the opposite pane", () => {
const escape = (v: EditorView) => fireEvent.keyDown(v.contentDOM, { key: "Escape", code: "Escape", keyCode: 27 });
it("closes the tab with Esc only once nothing in the diff takes it", async () => {
useKeymap.setState({ overrides: KEYMAP });
const onClose = vi.fn();
render(<DiffEditor input={revisions} onClose={onClose} />);
const v = await press(1, "KeyF");
fireEvent.change(await screen.findByRole("textbox", { name: "Find" }), { target: { value: "foo" } });
await waitFor(() => expect(v.state.selection.main.empty).toBe(false));
v.focus();
escape(v); // closes the find bar
await waitFor(() => expect(screen.queryByRole("search")).toBeNull());
escape(v); // collapses the found match
expect(v.state.selection.main.empty).toBe(true);
expect(onClose).not.toHaveBeenCalled();
escape(v);
expect(onClose).toHaveBeenCalledOnce();
});
it("leaves Esc alone where the diff isn't a tab", async () => {
render(<DiffEditor input={revisions} />);
const v = await pane(1);
v.focus();
const event = new KeyboardEvent("keydown", { key: "Escape", bubbles: true, cancelable: true });
v.contentDOM.dispatchEvent(event);
expect(event.defaultPrevented).toBe(false);
});
it("moves focus to the other side with Select Opposite Diff Pane", async () => {
const { focusedDiffNavigator } = await import("../diffNavigator");
render(<DiffEditor input={revisions} />);
const left = await pane(0);
const right = await pane(1);
right.focus();
await waitFor(() => expect(focusedDiffNavigator()).not.toBeNull());
expect(focusedDiffNavigator()!.focusOpposite()).toBe(true);
expect(left.hasFocus).toBe(true);
expect(focusedDiffNavigator()!.focusOpposite()).toBe(true);
expect(right.hasFocus).toBe(true);
});
});
-18
Переглянути файл
@@ -175,24 +175,6 @@ export function registerCompareActions(): () => void {
when: "diffEditorFocus",
run: () => focusedDiffNavigator()?.showSettings(),
},
{
// Diff.FocusOppositePane; it beats the Switcher's ⌃⇧⇥ while a side-by-side diff is focused.
id: "diff.focusOppositePane",
title: "Select Opposite Diff Pane",
category: "Diff",
when: "diffEditorFocus",
keyWhen: "diffEditorFocus && !popupOpen",
enabled: () => (focusedDiffNavigator()?.viewer?.()?.panes().length ?? 0) > 1,
run: () => void focusedDiffNavigator()?.focusOpposite(),
},
{
// CloseDiffEditor: Esc in a diff tab that nothing in the diff takes (see DiffEditor).
id: "diff.closeEditor",
title: "Close Diff Editor",
category: "Diff",
when: "diffEditorFocus",
run: () => void focusedDiffNavigator()?.close(),
},
{
id: "diff.nextFile",
title: "Compare Next File",
+1 -2
Переглянути файл
@@ -1,4 +1,4 @@
import { editorService, type EditorInput } from "@/workbench/contributions";
import type { EditorInput } from "@/workbench/contributions";
import { hideAllToolWindows, useToolWindowLayout } from "@/workbench/toolwindows/store";
import { shortcutLabel } from "@/platform/keymap";
import { IconButton } from "@/ui";
@@ -11,7 +11,6 @@ export function DiffEditorTab({ input }: { input: EditorInput }) {
return (
<DiffEditor
input={input}
onClose={() => void editorService().closeInput?.(input.key)}
toolbarActions={
<IconButton
icon={maximized ? "minimize" : "maximize"}
-4
Переглянути файл
@@ -11,10 +11,6 @@ export interface DiffNavigator {
file(dir: 1 | -1): void;
/** The viewer's settings as a popup menu (⇧⌘D). */
showSettings(): void;
/** IDEA's Select Opposite Diff Pane: focus the other side of a side-by-side diff; false when there is none. */
focusOpposite(): boolean;
/** IDEA's Close Diff Editor (Esc): closes the diff's editor tab; false when the diff isn't one. */
close(): boolean;
/** The built panes, for diff viewer contributions' actions (null while loading). */
viewer?(): DiffViewerHost | null;
}
+2 -14
Переглянути файл
@@ -130,11 +130,7 @@
margin: 1.5em 0;
}
.preview :global(img) {
max-width: calc(100% * var(--markdown-zoom-scale, 1));
}
.preview :global(img[data-natural-width]) {
width: calc(min(var(--image-width), 100%) * var(--markdown-zoom-scale, 1));
height: auto;
max-width: 100%;
}
.preview :global(ul.contains-task-list) {
list-style: none;
@@ -159,18 +155,10 @@
display: none;
}
.preview :global(.mermaid-output) {
overflow-x: auto;
overflow-y: hidden;
overflow: hidden;
text-align: center;
}
.preview :global(.mermaid-output svg[data-natural-width]) {
display: block;
margin: 0 auto;
width: calc(min(var(--diagram-width), 100%) * var(--markdown-zoom-scale, 1));
height: auto;
}
.preview :global(.mermaid-output[data-zoomed="true"]) {
overflow: hidden;
cursor: grab;
touch-action: none;
user-select: none;
+4 -30
Переглянути файл
@@ -51,7 +51,6 @@ import {
resolveLink,
slugify,
} from "./render";
import { applyImageZoomWidth, svgDataUrlSize } from "./svgSize";
import styles from "./Markdown.module.css";
import { tx } from "@/platform/i18n";
@@ -111,7 +110,6 @@ function MarkdownPreview({ path, sync }: { path: string; sync: boolean }) {
const text = useLiveMarkdown((s) => s[path]?.text);
const fontZoom = useLiveMarkdown((s) => s[path]?.fontZoom ?? 0);
const fontSize = useMarkdownPreviewFontSize();
const zoomScale = Number((Math.max(4, fontSize + fontZoom) / fontSize).toFixed(4));
const topLine = useLiveMarkdown((s) => s[path]?.topLine ?? 0);
const scrollSeq = useLiveMarkdown((s) => s[path]?.scrollSeq ?? 0);
const caretLine = useLiveMarkdown((s) => s[path]?.caretLine ?? 0);
@@ -163,32 +161,10 @@ function MarkdownPreview({ path, sync }: { path: string; sync: boolean }) {
const root = ref.current;
if (!root) return;
let live = true;
const bindImageUrl = (img: HTMLImageElement, url: string, asyncLoad: boolean) => {
const svgSize = svgDataUrlSize(url);
if (svgSize && applyImageZoomWidth(img, svgSize) && asyncLoad) {
setDiagramsDrawn((n) => n + 1);
}
img.onload = () => {
if (!live) return;
if (!img.hasAttribute("data-natural-width") && img.naturalWidth > 0 && img.naturalHeight > 0) {
if (applyImageZoomWidth(img, { w: img.naturalWidth, h: img.naturalHeight })) {
setDiagramsDrawn((n) => n + 1);
}
} else if (asyncLoad) {
setDiagramsDrawn((n) => n + 1);
}
};
img.src = url;
if (!img.hasAttribute("data-natural-width") && img.complete && img.naturalWidth > 0 && img.naturalHeight > 0) {
applyImageZoomWidth(img, { w: img.naturalWidth, h: img.naturalHeight });
}
};
const imgs = [...root.querySelectorAll<HTMLImageElement>("img[data-src]")];
imgs.forEach((img) => {
applyImageZoomWidth(img);
root.querySelectorAll<HTMLImageElement>("img[data-src]").forEach((img) => {
const src = img.dataset.src ?? "";
if (/^data:image\//i.test(src)) {
bindImageUrl(img, src, false);
img.src = src;
return;
}
const target = resolveLink(path, src);
@@ -198,7 +174,7 @@ function MarkdownPreview({ path, sync }: { path: string; sync: boolean }) {
return;
}
imageUrl(target.path).then(
(url) => live && bindImageUrl(img, url, true),
(url) => live && (img.src = url),
() => live && img.classList.add(styles.blocked!),
);
});
@@ -211,7 +187,6 @@ function MarkdownPreview({ path, sync }: { path: string; sync: boolean }) {
});
return () => {
live = false;
for (const img of imgs) img.onload = null;
};
}, [html, path]);
@@ -220,7 +195,7 @@ function MarkdownPreview({ path, sync }: { path: string; sync: boolean }) {
const root = ref.current;
if (!root || !sync) return;
root.scrollTop = topLine <= 0 ? 0 : previewOffsetForLine(blocksOf(root), topLine, root.scrollHeight);
}, [html, topLine, scrollSeq, sync, diagramsDrawn, fontZoom, fontSize]);
}, [html, topLine, scrollSeq, sync, diagramsDrawn]);
// The preview's own scroll offset: tab switches and mode toggles unmount the
// preview, so Preview Only would otherwise start over at the top. Writes made
@@ -401,7 +376,6 @@ function MarkdownPreview({ path, sync }: { path: string; sync: boolean }) {
{
"--markdown-preview-font-size": `${fontSize}px`,
"--markdown-font-zoom": `${fontZoom}px`,
"--markdown-zoom-scale": String(zoomScale),
} as CSSProperties
}
tabIndex={-1}
-9
Переглянути файл
@@ -396,15 +396,6 @@ describe("mermaid in the markdown preview", () => {
expect(root.querySelector("[data-mermaid]")?.getAttribute("data-mermaid-state")).toBe("pending");
});
it("records a diagram's natural width so preview font zoom scales it without clamping", async () => {
const svg = await renderedSvg(
'<svg id="openidea-mermaid-1" width="100%" style="max-width: 540px;" viewBox="0 0 540 320"><text>flow</text></svg>',
);
expect(svg.hasAttribute("data-natural-width")).toBe(true);
expect(svg.style.getPropertyValue("--diagram-width")).toBe("540px");
expect(svg.style.maxWidth).toBe("calc(min(var(--diagram-width), 100%) * var(--markdown-zoom-scale, 1))");
});
it("puts full-screen and download tools on a drawn diagram", async () => {
const root = preview(SEQ);
await renderMermaidIn(root).done;
+1 -10
Переглянути файл
@@ -11,7 +11,6 @@ import { attachPanZoom } from "../panZoom";
import { attachDiagramTools, TOOLS_ATTR } from "./mermaidTools";
import { localSvg } from "./localSvg";
import { reid } from "./svgIds";
import { svgElementSize } from "./svgSize";
export const MERMAID_SELECTOR = "[data-mermaid]";
const OUTPUT_CLASS = "mermaid-output";
@@ -251,15 +250,7 @@ function show(block: HTMLElement, result: Result | null, stale?: Node) {
if (block.getAttribute(RENDERED_ATTR) === "ok") {
attachPanZoom(out);
const svg = out.querySelector("svg");
if (svg) {
const size = svgElementSize(svg);
if (size) {
svg.dataset.naturalWidth = "";
svg.style.setProperty("--diagram-width", `${Number(size.w.toFixed(2))}px`);
svg.style.maxWidth = "calc(min(var(--diagram-width), 100%) * var(--markdown-zoom-scale, 1))";
}
attachDiagramTools(block, svg);
}
if (svg) attachDiagramTools(block, svg);
}
}
+5 -3
Переглянути файл
@@ -11,7 +11,6 @@ import { openLayer, trapTab } from "@/ui/popupState";
import { notifications } from "@/workbench/contributions";
import { attachPanZoom } from "../panZoom";
import { reid } from "./svgIds";
import { svgElementSize } from "./svgSize";
import styles from "./MermaidTools.module.css";
/** Marks the tool strip, so a redrawing block replaces it rather than stacking one. */
@@ -162,8 +161,11 @@ function fittedCopy(svg: SVGSVGElement, availW: number, availH: number): SVGSVGE
}
function naturalSize(svg: SVGSVGElement): { w: number; h: number } {
const declared = svgElementSize(svg);
if (declared) return declared;
const box = (svg as SVGSVGElement & { viewBox?: SVGAnimatedRect }).viewBox?.baseVal;
if (box && box.width > 0 && box.height > 0) return { w: box.width, h: box.height };
const width = Number(svg.getAttribute("width"));
const height = Number(svg.getAttribute("height"));
if (width > 0 && height > 0) return { w: width, h: height };
const rect = svg.getBoundingClientRect();
return { w: rect.width > 0 ? rect.width : 800, h: rect.height > 0 ? rect.height : 600 };
}
-147
Переглянути файл
@@ -1,147 +0,0 @@
/**
* Intrinsic CSS pixel dimensions of SVGs and Markdown preview images, so
* diagrams and embedded `.svg` / raster assets scale with `--markdown-zoom-scale`.
*/
const UNIT_PX: Record<string, number> = {
"": 1,
px: 1,
pt: 96 / 72,
pc: 16,
in: 96,
cm: 96 / 2.54,
mm: 96 / 25.4,
};
/** Parses an absolute SVG/HTML length into CSS pixels (ignoring `%`, `em`, etc.). */
export function parseLengthPx(raw: string | null | undefined): number | null {
if (!raw) return null;
const m = /^\s*(\d+(?:\.\d+)?|\.\d+)\s*([a-z%]*)\s*$/i.exec(raw);
if (!m) return null;
const factor = UNIT_PX[m[2]!.toLowerCase()];
if (!factor) return null;
const n = Number(m[1]) * factor;
return Number.isFinite(n) && n > 0 ? n : null;
}
function parsePercent(raw: string | null | undefined): number | null {
if (!raw) return null;
const m = /^\s*(\d+(?:\.\d+)?|\.\d+)%\s*$/.exec(raw);
if (!m) return null;
const n = Number(m[1]);
return Number.isFinite(n) && n > 0 ? n : null;
}
/** Parses `viewBox="minX minY width height"` into `{ w, h }`. */
export function parseViewBoxSize(raw: string | null | undefined): { w: number; h: number } | null {
if (!raw) return null;
const parts = raw
.trim()
.split(/[\s,]+/)
.map(Number);
if (parts.length !== 4) return null;
const w = parts[2]!;
const h = parts[3]!;
return Number.isFinite(w) && Number.isFinite(h) && w > 0 && h > 0 ? { w, h } : null;
}
function resolveSvgDims(
style: string | null | undefined,
widthAttr: string | null | undefined,
heightAttr: string | null | undefined,
viewBoxAttr: string | null | undefined,
): { w: number; h: number } | null {
const vb = parseViewBoxSize(viewBoxAttr);
const storedW = parseLengthPx(/(?:^|;)\s*--diagram-width\s*:\s*([^;]+)/i.exec(style ?? "")?.[1]);
const inlineMaxW = parseLengthPx(/(?:^|;)\s*max-width\s*:\s*([^;]+)/i.exec(style ?? "")?.[1]);
const attrW = parseLengthPx(widthAttr);
const attrH = parseLengthPx(heightAttr);
const w = storedW ?? inlineMaxW ?? attrW ?? (attrH && vb ? (attrH * vb.w) / vb.h : (vb?.w ?? null));
if (!w) return null;
const h = attrH ?? (vb ? (w * vb.h) / vb.w : w * 0.75);
return { w, h };
}
/** Intrinsic CSS pixel size of an in-DOM `<svg>`, when declared on the element. */
export function svgElementSize(svg: SVGSVGElement): { w: number; h: number } | null {
const fromAttrs = resolveSvgDims(
svg.getAttribute("style"),
svg.getAttribute("width"),
svg.getAttribute("height"),
svg.getAttribute("viewBox"),
);
if (fromAttrs) return fromAttrs;
const box = (svg as SVGSVGElement & { viewBox?: SVGAnimatedRect }).viewBox?.baseVal;
if (box && box.width > 0 && box.height > 0) return { w: box.width, h: box.height };
return null;
}
function readAttr(tag: string, name: string): string | null {
const re = new RegExp(`(?:^|\\s)${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`, "i");
const m = re.exec(tag);
return m ? (m[1] ?? m[2] ?? m[3] ?? null) : null;
}
/**
* Intrinsic CSS pixel size of a `data:image/svg+xml` URL from its `<svg>` header.
* Decodes only the leading chunk so large `.excalidraw.svg` scenes stay cheap.
*/
export function svgDataUrlSize(url: string): { w: number; h: number } | null {
const comma = url.indexOf(",");
if (comma < 0) return null;
const header = url.slice(0, comma);
if (!/^data:image\/svg\+xml\b/i.test(header)) return null;
const payload = url.slice(comma + 1, comma + 1 + 16384);
let head: string;
try {
if (/;base64\b/i.test(header)) {
const clean = payload.replace(/\s+/g, "");
const aligned = clean.slice(0, clean.length - (clean.length % 4));
head = atob(aligned);
} else {
const safe = payload.replace(/%[0-9a-f]?$/i, "");
head = decodeURIComponent(safe);
}
} catch {
return null;
}
const withoutComments = head.replace(/<!--[\s\S]*?-->/g, "");
const open = /<svg\b((?:"[^"]*"|'[^']*'|[^'">])*)>/i.exec(withoutComments);
if (!open) return null;
const attrs = open[1]!;
return resolveSvgDims(
readAttr(attrs, "style"),
readAttr(attrs, "width"),
readAttr(attrs, "height"),
readAttr(attrs, "viewBox"),
);
}
const formatPx = (n: number) => `${Number(n.toFixed(2))}px`;
/**
* Sets an `<img>`'s 1× base width so CSS scales it by `--markdown-zoom-scale`,
* respecting explicit HTML `width` / `height` attributes when present.
*/
export function applyImageZoomWidth(img: HTMLImageElement, intrinsic?: { w: number; h: number } | null): boolean {
const rawWidth = img.getAttribute("width");
const pct = parsePercent(rawWidth);
if (pct !== null) {
img.style.width = `calc(${pct}% * var(--markdown-zoom-scale, 1))`;
img.style.height = "auto";
return true;
}
const attrW = parseLengthPx(rawWidth);
if (attrW !== null) {
img.style.setProperty("--image-width", formatPx(attrW));
img.dataset.naturalWidth = "";
return true;
}
if (!intrinsic || intrinsic.w <= 0 || intrinsic.h <= 0) return false;
const attrH = parseLengthPx(img.getAttribute("height"));
const baseW = attrH !== null ? (attrH * intrinsic.w) / intrinsic.h : intrinsic.w;
img.style.setProperty("--image-width", formatPx(baseW));
img.dataset.naturalWidth = "";
return true;
}
-88
Переглянути файл
@@ -101,102 +101,14 @@ describe("editor font size actions", () => {
);
const preview = screen.getByRole("document", { name: "Markdown preview" });
expect(preview.style.getPropertyValue("--markdown-font-zoom")).toBe("0px");
expect(preview.style.getPropertyValue("--markdown-zoom-scale")).toBe("1");
await act(() => executeAction("editor.increaseFontSize"));
await act(() => executeAction("editor.increaseFontSize"));
expect(preview.style.getPropertyValue("--markdown-font-zoom")).toBe("2px");
expect(preview.style.getPropertyValue("--markdown-zoom-scale")).toBe("1.1538");
await act(() => executeAction("editor.decreaseFontSize"));
await act(() => executeAction("editor.decreaseFontSize"));
await act(() => executeAction("editor.decreaseFontSize"));
expect(preview.style.getPropertyValue("--markdown-font-zoom")).toBe("-1px");
expect(preview.style.getPropertyValue("--markdown-zoom-scale")).toBe("0.9231");
await act(() => executeAction("editor.resetFontSize"));
expect(zoom(md)).toBe(0);
expect(preview.style.getPropertyValue("--markdown-font-zoom")).toBe("0px");
expect(preview.style.getPropertyValue("--markdown-zoom-scale")).toBe("1");
});
it("sizes embedded excalidraw.svg, graph SVGs and raster images so they scale with font zoom", async () => {
const excalidrawSvg =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 940" width="1280" height="940"><!-- svg-source:excalidraw --></svg>';
const excalidrawUrl = `data:image/svg+xml;base64,${btoa(excalidrawSvg)}`;
const graphvizUrl = `data:image/svg+xml;utf8,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="300pt" height="150pt" viewBox="0 0 300 150"></svg>')}`;
const wordmarkUrl = `data:image/svg+xml;base64,${btoa('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 280 56"></svg>')}`;
const pngUrl = "data:image/png;base64,iVBORw0KGgo=";
const ipc = await import("@/platform/ipc");
const fsReadImage = vi.spyOn(ipc.commands, "fsReadImage").mockImplementation(async (p: string) => {
if (p.endsWith("architecture.excalidraw.svg")) return { status: "ok", data: excalidrawUrl };
if (p.endsWith("wordmark.svg")) return { status: "ok", data: wordmarkUrl };
if (p.endsWith("shot.png")) return { status: "ok", data: pngUrl };
return { status: "error", error: { kind: "notFound", message: p } } as never;
});
try {
const doc = [
"![Architecture](./architecture.excalidraw.svg)",
"",
`![Graph](${graphvizUrl})`,
"",
'<img src="./wordmark.svg" alt="Wordmark" height="56">',
"",
'<img src="./wordmark.svg" alt="Icon" width="80">',
"",
"![Shot](./shot.png)",
].join("\n");
const live = editorContributions().find((c) => c.id === "viewers.markdownLive")!;
const view = new EditorView({
state: EditorState.create({ doc, extensions: [fontZoom, live.extensions(mdCtx)] }),
});
const handle = { view, ctx: mdCtx };
_addEditor(handle);
disposers.push(() => _removeEditor(handle));
views.push(view);
activate(view);
act(() => useMarkdownModes.setState((s) => ({ byPath: { ...s.byPath, [mdCtx.path]: "preview" } })));
render(
<MarkdownEditor path={mdCtx.path} tabId="t">
<div />
</MarkdownEditor>,
);
const preview = screen.getByRole("document", { name: "Markdown preview" });
await vi.waitFor(() => {
const arch = preview.querySelector<HTMLImageElement>('img[alt="Architecture"]');
expect(arch?.getAttribute("src")).toBe(excalidrawUrl);
});
const arch = preview.querySelector<HTMLImageElement>('img[alt="Architecture"]')!;
expect(arch.hasAttribute("data-natural-width")).toBe(true);
expect(arch.style.getPropertyValue("--image-width")).toBe("1280px");
const graph = preview.querySelector<HTMLImageElement>('img[alt="Graph"]')!;
expect(graph.hasAttribute("data-natural-width")).toBe(true);
expect(graph.style.getPropertyValue("--image-width")).toBe("400px");
const wordmark = preview.querySelector<HTMLImageElement>('img[alt="Wordmark"]')!;
expect(wordmark.hasAttribute("data-natural-width")).toBe(true);
expect(wordmark.style.getPropertyValue("--image-width")).toBe("280px");
const icon = preview.querySelector<HTMLImageElement>('img[alt="Icon"]')!;
expect(icon.hasAttribute("data-natural-width")).toBe(true);
expect(icon.style.getPropertyValue("--image-width")).toBe("80px");
const shot = preview.querySelector<HTMLImageElement>('img[alt="Shot"]')!;
Object.defineProperty(shot, "naturalWidth", { value: 960, configurable: true });
Object.defineProperty(shot, "naturalHeight", { value: 600, configurable: true });
act(() => {
shot.dispatchEvent(new Event("load"));
});
expect(shot.hasAttribute("data-natural-width")).toBe(true);
expect(shot.style.getPropertyValue("--image-width")).toBe("960px");
} finally {
fsReadImage.mockRestore();
}
});
it("scales a configured editor font size", () => {
-5
Переглянути файл
@@ -50,11 +50,6 @@ export function attachPanZoom(viewport: HTMLElement, options: PanZoomOptions = {
(e) => {
if (!(e.metaKey || e.ctrlKey) || !e.deltaY) return;
e.preventDefault();
if (view.z === origin.z && (viewport.scrollLeft || viewport.scrollTop)) {
view = { ...view, x: view.x - viewport.scrollLeft, y: view.y - viewport.scrollTop };
viewport.scrollLeft = 0;
viewport.scrollTop = 0;
}
const rect = viewport.getBoundingClientRect();
// One wheel notch (~100 px, 3 lines or a page) zooms about 15%; a trackpad
// sends small pixel deltas, so zooming follows the gesture continuously.
-1
Переглянути файл
@@ -85,7 +85,6 @@ export const CLIENT_SETTINGS_KEYS = [
"run.confirmRerun",
"run.confirmTerminate",
"run.showOnStart",
"run.pinTabs",
"vcs.showDirtyDirectories",
"vcs.git.inlineBlame",
"vcs.git.showBranchInProjectView",
-1
Переглянути файл
@@ -97,7 +97,6 @@ export const eclipseMacos: Record<string, string[]> = {
"diff.nextDifference": ["Ctrl+F7"],
"diff.prevFile": [],
"diff.nextFile": [],
"diff.focusOppositePane": [],
"navigate.nextOccurrence": [],
"editor.nextTab": ["Meta+F6", "Meta+Alt+Right"],
"db.nextResultTab": ["Meta+F6", "Meta+Alt+Right"],
-1
Переглянути файл
@@ -99,7 +99,6 @@ export const eclipseWindows: Record<string, string[]> = {
"diff.nextDifference": ["Ctrl+F7"],
"diff.prevFile": ["Ctrl+Alt+Shift+Right"],
"diff.nextFile": ["Ctrl+Alt+Shift+Left"],
"diff.focusOppositePane": [],
"diff.applyLeftSide": [],
"diff.applyRightSide": [],
"navigate.nextOccurrence": [],
-1
Переглянути файл
@@ -338,7 +338,6 @@ export const intellijMacos: Record<string, string[]> = {
"diff.nextFile": ["Ctrl+Shift+Right"],
"diff.prevFile": ["Ctrl+Shift+Left"],
"diff.showSettings": ["Meta+Shift+D"],
"diff.focusOppositePane": ["Ctrl+Shift+Tab"],
// After Show Diff: ⌘D is Compare With… in the project view.
"project.compareWith": ["Meta+D"],
// Editing extras, image viewer (keyWhen: image focused) and Markdown editor layouts
-1
Переглянути файл
@@ -336,7 +336,6 @@ export const intellijWindows: Record<string, string[]> = {
"diff.nextFile": ["Alt+Shift+Right"],
"diff.prevFile": ["Alt+Shift+Left"],
"diff.showSettings": ["Ctrl+Shift+D"],
"diff.focusOppositePane": ["Ctrl+Shift+Tab"],
// After Show Diff: Ctrl+D is Compare With… in the project view.
"project.compareWith": ["Ctrl+D"],
// Editing extras, image viewer (keyWhen: image focused) and Markdown editor layouts
-1
Переглянути файл
@@ -350,7 +350,6 @@ export const vscodeMacos: Record<string, string[]> = {
"diff.nextFile": [],
"diff.prevFile": [],
"diff.showSettings": [],
"diff.focusOppositePane": [],
"diff.prevDifference": ["Shift+F7"],
"diff.jumpToSource": [],
// HTTP Client / Database console: run the request or statement at the caret.
-6
Переглянути файл
@@ -1,10 +1,4 @@
{
"Select Opposite Diff Pane": "Select Opposite Diff Pane",
"Close Diff Editor": "Close Diff Editor",
"Show Diff in Editor Tab": "Show Diff in Editor Tab",
"Cannot show diff in an editor tab": "Cannot show diff in an editor tab",
"Make configurations pinned by default": "Make configurations pinned by default",
"A finished pinned tab stays open: a later run opens a new tab instead of taking its place.": "A finished pinned tab stays open: a later run opens a new tab instead of taking its place.",
"Cannot paste the image into {name}": "Cannot paste the image into {name}",
"Worktree Conflict": "Worktree Conflict",
"Branch '{branch}' is already checked out in a worktree at:\n{path}": "Branch '{branch}' is already checked out in a worktree at:\n{path}",
-6
Переглянути файл
@@ -1,10 +1,4 @@
{
"Select Opposite Diff Pane": "选择对侧差异窗格",
"Close Diff Editor": "关闭差异编辑器",
"Show Diff in Editor Tab": "在编辑器标签页中显示差异",
"Cannot show diff in an editor tab": "无法在编辑器标签页中显示差异",
"Make configurations pinned by default": "默认固定运行/调试标签页",
"A finished pinned tab stays open: a later run opens a new tab instead of taking its place.": "固定的标签页结束后会保留:之后的运行会打开新标签页,而不是替换它。",
"Cannot paste the image into {name}": "无法将图片粘贴为 {name}",
"Worktree Conflict": "工作树冲突",
"Branch '{branch}' is already checked out in a worktree at:\n{path}": "分支 '{branch}' 已在以下位置的工作树中签出:\n{path}",
+1 -11
Переглянути файл
@@ -116,8 +116,6 @@ export interface RunSettingsValid {
confirmRerun: boolean;
confirmTerminate: boolean;
showOnStart: boolean;
/** New Run/Debug tabs start pinned, so a later run doesn't take their place (IDEA's advanced setting). */
pinTabs: boolean;
terminationTimeoutMs: number;
}
@@ -306,14 +304,7 @@ export const DEFAULT_WORKFLOW: WorkflowSettings = {
debugServerPage: false,
python: { attachSubprocess: true, gevent: false },
},
run: {
defaultTarget: "",
confirmRerun: true,
confirmTerminate: true,
showOnStart: true,
pinTabs: false,
terminationTimeoutMs: 3000,
},
run: { defaultTarget: "", confirmRerun: true, confirmTerminate: true, showOnStart: true, terminationTimeoutMs: 3000 },
terminal: {
startDirectory: "",
env: {},
@@ -486,7 +477,6 @@ export function validateRun(raw: unknown): RunSettingsValid {
confirmRerun: bool(v.confirmRerun, d.confirmRerun),
confirmTerminate: bool(v.confirmTerminate, d.confirmTerminate),
showOnStart: bool(v.showOnStart, d.showOnStart),
pinTabs: bool(v.pinTabs, d.pinTabs),
terminationTimeoutMs: num(
v.terminationTimeoutMs,
d.terminationTimeoutMs,
-56
Переглянути файл
@@ -1501,62 +1501,6 @@ html {
--db-connected: var(--success);
--db-connected-ring: var(--success);
}
/*
* The bundled schemes' red-green variants (IDEA's deuteranopia / protanopia values): syntax
* without the green-red and purple-blue pairs, errors in orange, highlights in blue / olive.
* Other colour themes and plugin themes keep their own editor colours.
*/
:root[data-red-green]:not(
[data-theme="light"],
[data-theme="dark-hc"],
[data-contrast="high"],
[data-color-theme],
[data-plugin-theme]
) {
--syn-keyword: #597cc2;
--syn-string: #94907f;
--syn-number: #66a6ff;
--syn-constant: #828eba;
--syn-property: #828eba;
--syn-function: #d9af6c;
--syn-annotation: #66a6ff;
--syn-decorator: #66a6ff;
--syn-escape: #597cc2;
--syn-todo: #bfa222;
--syn-invalid: #bd693c;
--syn-markup-heading: var(--syn-keyword);
--syn-markup-code: var(--syn-string);
--wave-error: #e57132;
--editor-error-decoration: underline wavy #e57132;
--editor-warning-background: #454122;
--wave-typo: #4b734f;
--bg-identifier-under-caret: #32394c;
--bg-identifier-under-caret-write: #253873;
--bg-search-result: #404861;
--bg-text-search-result: #363827;
--bg-text-search-result-current: #66731d;
--bg-breakpoint-line: #42372b;
--coverage-covered: #445956;
--coverage-partial: #807540;
--coverage-uncovered: #704b25;
}
:root[data-theme="light"][data-red-green]:not([data-color-theme], [data-plugin-theme]) {
--syn-keyword: #00428c;
--syn-string: #6a84db;
--syn-number: #3777e6;
--syn-constant: #004b9f;
--syn-property: #004b9f;
--syn-function: #121314;
--syn-annotation: #3381ff;
--syn-decorator: #3381ff;
--syn-todo: #3777e6;
--syn-invalid: #d95716;
--syn-markup-heading: var(--syn-keyword);
--syn-markup-code: var(--syn-string);
--editor-warning-background: #fff0a6;
--bg-identifier-under-caret: #d9e6ff;
--bg-identifier-under-caret-write: #e0e0cc;
}
/* PHP editors: variables and function calls use the PHP colours (PHP_VAR / PHP_FUNCTION_CALL). */
.cm-editor[data-language="PHP"] {
--syn-variable: var(--syn-php-variable);
+1 -2
Переглянути файл
@@ -72,7 +72,7 @@
color: var(--fg-faint);
}
/* Toolbar / action buttons share the toolbar hover, pressed and selected tokens. */
/* Toolbar / action buttons: transparent on hover, filled when pressed or selected. */
.iconButton {
flex: none;
display: inline-flex;
@@ -89,7 +89,6 @@
outline-offset: -2px;
}
.iconButton:not(:disabled):hover {
background: var(--bg-toolbar-hover);
color: var(--fg-strong);
}
.iconButton:not(:disabled):active {
-4
Переглянути файл
@@ -146,10 +146,6 @@
background: transparent;
color: var(--fg-secondary);
}
.mainMenuButton:hover,
.mainMenuButton[aria-expanded="true"] {
background: var(--bg-toolbar-hover);
}
.mainMenuButton:focus-visible,
.menuTitle:focus-visible,
.caption:focus-visible {