Compare commits

..
Author SHA1 Message Date
l0ng-ai 8ec4032cf7 fix(vcs): let Commit and Shelf tree names use the whole row width
File and folder names in the Commit tool window's changes tree (and the Shelf tree, which shares the styles) were capped at 75% of the row, so a long folder name was cut with an ellipsis while a quarter of the row stayed empty, and the gap grew as the window widened. The name now takes the whole row; when it does not fit, the grey detail after it (the path or file count) is truncated first. Stash rows keep their previous shrinking.

Fixes #1367
2026-11-05 08:25:29 +08:00
15 changed files with 26 additions and 435 deletions

No files matched your search

+1 -7
View File
@@ -3,9 +3,7 @@ name: Language smoke
# with a quick fix, an intention, formatting, rename) per language, against the language
# server the app starts by default: crates/ide-host/tests/language_smoke.rs.
# Then running and testing each fixture through the run store and openidea-server
# (Run, Run tests, the test tree, Rerun Failed Tests): smoke/run.test.ts; and debugging its
# entry point with the debugger the IDE installs (breakpoint, frames, variables, evaluate,
# Step Over, Resume): smoke/debug.test.ts.
# (Run, Run tests, the test tree, Rerun Failed Tests): smoke/run.test.ts.
on:
push:
branches: [main]
@@ -19,7 +17,6 @@ on:
- "Cargo.toml"
- "Cargo.lock"
- "src/features/run/**"
- "src/features/debugger/**"
- "src/platform/**"
- "smoke/**"
- "package.json"
@@ -111,6 +108,3 @@ jobs:
- run: pnpm install --frozen-lockfile
- run: cargo build --locked -p ide-host --bin openidea-server
- run: pnpm smoke smoke/run.test.ts -t "^${{ matrix.language }}$"
- run: pnpm smoke smoke/debug.test.ts -t "^${{ matrix.language }}$"
# The IDE installs the debugger from GitHub releases: authenticated, against rate limits.
env: { GITHUB_TOKEN: "${{ github.token }}" }
-3
View File
@@ -71,9 +71,6 @@ Shared agent skills live in `.agents/skills/` (`.claude/skills/` links to them):
on `fixtures/` with that language's real server installed (CI: `Language smoke`, pinned versions).
- `pnpm smoke smoke/run.test.ts -t <language>` — run and test that language's fixture through the
run store and a real `openidea-server` (build it first: `cargo build -p ide-host --bin openidea-server`).
- `pnpm smoke smoke/debug.test.ts -t <language>` — debug that fixture's entry point the same way, with
the debugger the IDE installs (on macOS, CodeLLDB and Delve may need Developer Mode:
`sudo DevToolsSecurity -enable`).
- `pnpm test`, `pnpm e2e`, `pnpm lint`, `pnpm format:check`, `pnpm knip`, `pnpm typecheck`,
`pnpm gen:bindings`.
- `pnpm perf` (UI interaction latency / frames / leaks), `pnpm perf:rust` (criterion
@@ -36,23 +36,6 @@ impl Default for InspectionSettings {
}
}
impl InspectionSettings {
/// Overrides of the profile in effect; `Default`'s when `profile` names none that exists.
pub fn active(&self) -> &BTreeMap<String, InspectionOverride> {
let custom = |level: &str, key: &str| match level {
"ide" => self.ide_profiles.get(key),
"project" => self.project_profiles.get(key),
_ => None,
};
match self.profile.as_str() {
"project" => &self.project,
id => {
id.split_once(':').and_then(|(level, key)| custom(level, key)).map_or(&self.default, |p| &p.overrides)
}
}
}
}
/// A named profile made with Copy.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, Type)]
#[serde(default, rename_all = "camelCase")]
@@ -128,20 +111,4 @@ mod tests {
let back: InspectionSettings = serde_json::from_str(&serde_json::to_string(&s).unwrap()).unwrap();
assert_eq!(back, s);
}
#[test]
fn active_profile_falls_back_to_default() {
let off = |id: &str| {
BTreeMap::from([(id.to_owned(), InspectionOverride { enabled: Some(false), ..Default::default() })])
};
let mut s = InspectionSettings { default: off("a"), project: off("b"), ..Default::default() };
s.ide_profiles.insert("strict".into(), CustomInspectionProfile { name: "Strict".into(), overrides: off("c") });
assert!(s.active().contains_key("a"));
s.profile = "project".into();
assert!(s.active().contains_key("b"));
s.profile = "ide:strict".into();
assert!(s.active().contains_key("c"));
s.profile = "project:gone".into();
assert!(s.active().contains_key("a"));
}
}
+1 -6
View File
@@ -175,12 +175,7 @@ pub fn set_settings(
host.apply_proxy();
}
// A new interpreter / toolchain / server executable restarts the language servers it affects.
// So do inspections that are compiler options (`ide-sdk` `configure_java_compiler`).
let code_style = key == "editor"
|| key == "editor.codeStyle"
|| key.starts_with("editor.codeStyle.")
|| key == "inspections"
|| key.starts_with("inspections.");
let code_style = key == "editor" || key == "editor.codeStyle" || key.starts_with("editor.codeStyle.");
let toolchain = ["languages", "tools", "languageServers", "projectStructure"]
.iter()
.any(|k| key == *k || key.starts_with(&format!("{k}.")));
-1
View File
@@ -209,7 +209,6 @@ impl Host {
cfg.launch_error = Some(e);
None
});
ide_sdk::apply::configure_java_compiler(cfg, &data_dir, &effective.inspections);
ide_sdk::java::configure_jdtls(
cfg,
server_root,
+8 -71
View File
@@ -5,7 +5,7 @@ use crate::discover::{project_venv_python, workspace_tsdk};
use crate::exec::{path_with, search_path};
use crate::{angular, godot, pinned, vue};
use crate::{bin_dir, exe, venv_of};
use ide_core::settings::{InspectionLevel, InspectionSettings, LanguageServerSettings, Settings};
use ide_core::settings::{LanguageServerSettings, Settings};
use ide_core::version_managers::Tool;
use ide_language::{LanguageConfig, LanguageServerConfig};
use ide_run::RunConfig;
@@ -442,41 +442,15 @@ pub(crate) fn merge_init_options(cfg: &mut LanguageServerConfig, layer: Value) {
}
}
/// Editor › Inspections › Java: "Serializable class without 'serialVersionUID'" (off by default,
/// as in IntelliJ IDEA). ECJ reports it, so it is a compiler option rather than a filter.
pub const SERIAL_VERSION_INSPECTION: &str = "java:missing-serial-version";
/// ECJ's level for a missing `serialVersionUID`, from the inspection profile in effect.
fn missing_serial_version(inspections: &InspectionSettings) -> Option<&'static str> {
let o = inspections.active().get(SERIAL_VERSION_INSPECTION)?;
if o.enabled != Some(true) {
return None;
}
Some(match o.severity {
Some(InspectionLevel::Error) => "error",
None | Some(InspectionLevel::Warning) => "warning",
Some(_) => "info",
})
}
/// Workspace compiler defaults; Eclipse project preferences still take precedence.
/// Each variant has its own file, so changing an inspection restarts the server with it.
pub fn configure_java_compiler(cfg: &mut LanguageServerConfig, data_dir: &Path, inspections: &InspectionSettings) {
pub(crate) fn configure_java_compiler(cfg: &mut LanguageServerConfig, data_dir: &Path) {
let file = data_dir.join("lsp/jdtls/compiler-defaults.prefs");
const DEFAULTS: &str = "org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning\n\
org.eclipse.jdt.core.compiler.problem.discouragedReference=warning\n";
let serial = missing_serial_version(inspections);
let (name, content) = match serial {
None => ("compiler-defaults.prefs".to_owned(), DEFAULTS.to_owned()),
Some(level) => (
format!("compiler-defaults-serial-{level}.prefs"),
format!("{DEFAULTS}org.eclipse.jdt.core.compiler.problem.missingSerialVersion={level}\n"),
),
};
let file = data_dir.join("lsp/jdtls").join(name);
let write = || -> ide_core::IdeResult<()> {
std::fs::create_dir_all(file.parent().expect("compiler preferences directory"))?;
if std::fs::read_to_string(&file).ok().as_deref() != Some(content.as_str()) {
ide_vfs::write_atomic(&file, content.as_bytes())?;
if std::fs::read_to_string(&file).ok().as_deref() != Some(DEFAULTS) {
ide_vfs::write_atomic(&file, DEFAULTS.as_bytes())?;
}
Ok(())
};
@@ -1121,50 +1095,13 @@ mod java_compiler_tests {
serde_json::to_value(cfg.initialization_options.as_ref().unwrap()).unwrap()
}
#[test]
fn serial_version_inspection_sets_the_compiler_option() {
let tmp = tempfile::tempdir().unwrap();
let url = |inspections: &InspectionSettings| {
let mut cfg = custom_server("jdtls", &LanguageServerSettings::default());
configure_java_compiler(&mut cfg, tmp.path(), inspections);
options(&cfg).pointer("/settings/java/settings/url").unwrap().as_str().unwrap().to_owned()
};
let set = |o: ide_core::settings::InspectionOverride| {
let mut s = InspectionSettings::default();
s.default.insert(SERIAL_VERSION_INSPECTION.into(), o);
s
};
let off = url(&InspectionSettings::default());
assert!(!std::fs::read_to_string(&off).unwrap().contains("missingSerialVersion"));
let on = url(&set(ide_core::settings::InspectionOverride { enabled: Some(true), ..Default::default() }));
assert_ne!(on, off, "a new file, so the server restarts with it");
assert!(std::fs::read_to_string(&on).unwrap().ends_with("compiler.problem.missingSerialVersion=warning\n"));
let error = url(&set(ide_core::settings::InspectionOverride {
enabled: Some(true),
severity: Some(InspectionLevel::Error),
..Default::default()
}));
assert!(std::fs::read_to_string(error).unwrap().ends_with("missingSerialVersion=error\n"));
let weak = url(&set(ide_core::settings::InspectionOverride {
enabled: Some(true),
severity: Some(InspectionLevel::WeakWarning),
..Default::default()
}));
assert!(std::fs::read_to_string(weak).unwrap().ends_with("missingSerialVersion=info\n"));
let severity_only = url(&set(ide_core::settings::InspectionOverride {
severity: Some(InspectionLevel::Error),
..Default::default()
}));
assert_eq!(severity_only, off, "off by default");
}
#[test]
fn compiler_defaults_live_in_the_cache_and_preserve_other_options() {
let tmp = tempfile::tempdir().unwrap();
let cache = tmp.path().join("cache with spaces 测试");
let mut cfg = custom_server("jdtls", &LanguageServerSettings::default());
merge_init_options(&mut cfg, json!({"settings": {"java": {"format": {"tabSize": 4}}}}));
configure_java_compiler(&mut cfg, &cache, &InspectionSettings::default());
configure_java_compiler(&mut cfg, &cache);
let options = options(&cfg);
let path = PathBuf::from(options.pointer("/settings/java/settings/url").unwrap().as_str().unwrap());
assert!(path.starts_with(&cache));
@@ -1175,7 +1112,7 @@ mod java_compiler_tests {
}
assert_eq!(options.pointer("/settings/java/format/tabSize"), Some(&json!(4)));
let modified = std::fs::metadata(&path).unwrap().modified().unwrap();
configure_java_compiler(&mut cfg, &cache, &InspectionSettings::default());
configure_java_compiler(&mut cfg, &cache);
assert_eq!(std::fs::metadata(path).unwrap().modified().unwrap(), modified);
assert_eq!(self::options(&cfg), options);
}
@@ -1185,7 +1122,7 @@ mod java_compiler_tests {
let tmp = tempfile::tempdir().unwrap();
let mut cfg = custom_server("jdtls", &LanguageServerSettings::default());
merge_init_options(&mut cfg, json!({"settings": {"java": {"settings": {"url": "own.prefs"}}}}));
configure_java_compiler(&mut cfg, tmp.path(), &InspectionSettings::default());
configure_java_compiler(&mut cfg, tmp.path());
assert_eq!(options(&cfg).pointer("/settings/java/settings/url"), Some(&json!("own.prefs")));
for settings in [
+1
View File
@@ -369,6 +369,7 @@ pub fn configure_jdtls(
if cfg.name != "jdtls" {
return;
}
crate::apply::configure_java_compiler(cfg, data_dir);
// A Lombok agent from the user's JVM arguments wins.
let own_agent = crate::lombok::agent_in_args(cfg.args.iter().map(String::as_str));
let agent = own_agent.as_deref().or(lombok);
+3 -4
View File
@@ -20,10 +20,9 @@ The fixtures are standalone: `rust-app` declares its own `[workspace]` and
The language smoke test (`crates/ide-host/tests/language_smoke.rs`) runs completion,
navigation, diagnostics, quick fixes, formatting and rename on these projects with real
language servers, `smoke/run.test.ts` runs each project's run and test configurations
(the intentionally failing test included, then Rerun Failed Tests), and `smoke/debug.test.ts`
debugs its entry point from the "Set a breakpoint here" line: when you change a fixture, run
them for that language.
language servers, and `smoke/run.test.ts` runs each project's run and test configurations
(the intentionally failing test included, then Rerun Failed Tests): when you change a
fixture, run both for that language.
## Setup
-251
View File
@@ -1,251 +0,0 @@
/**
* Debugging each language's `fixtures/` project the way the editor does: a breakpoint on the
* entry point's "Set a breakpoint here" line, Debug from the file's context, then the paused
* frame, its variables, evaluation, Step Over and Resume, with the debugger the IDE installs.
*/
import { execFileSync } from "node:child_process";
import { cpSync, mkdtempSync, readFileSync, realpathSync, rmSync } from "node:fs";
import { get } from "node:http";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterAll, afterEach, beforeAll, expect, test } from "vitest";
import { Channel } from "@tauri-apps/api/core";
import { commands, type InstallEvent } from "@/bindings";
import { executeAction } from "@/platform/actions";
import { call, nextRequestId, onIdeEvent } from "@/platform/ipc";
import { useBuildModel } from "@/platform/buildModel";
import { openProject, useProject } from "@/platform/project";
import { activate as activateBuildTools } from "@/features/build-tools";
import { useBuild } from "@/features/build-tools/store";
import { activate as activateDebugger } from "@/features/debugger";
import { addBreakpoint } from "@/features/debugger/breakpoints/store";
import { activeSession, evaluate, useDebug } from "@/features/debugger/state";
import { activate as activateRun } from "@/features/run";
import { resetRuns, useRun } from "@/features/run/store";
import { activateWorkbench } from "@/workbench/activate";
import { useNotifications } from "@/workbench/notificationStore";
import { connectDesktop } from "./desktop";
import { repoRoot, Server } from "./server";
interface Case {
fixture: string;
/** Runs in the project copy first (installs its dependencies). */
setup?: (root: string) => void;
/** The IDE's tool that debugs it. */
debugger: string;
/** The entry point, debugged from its context. */
file: string;
/** A variable of the paused frame. */
variable: string;
/** An expression evaluated there (Evaluate Expression), and its value. */
evaluate: [string, RegExp];
/** A server: the breakpoint is in this request's handler, and the session is stopped at the end. */
request?: string;
}
const CASES: Record<string, Case> = {
typescript: {
fixture: "ts-app",
setup: (root) => execFileSync("npm", ["install", "--no-audit", "--no-fund"], { cwd: root, stdio: "ignore" }),
debugger: "js-debug",
file: "src/index.ts",
variable: "cart",
evaluate: ["cart.items.length", /^2$/],
},
python: {
fixture: "python-app",
setup: (root) => execFileSync("python3", ["-m", "venv", ".venv"], { cwd: root, stdio: "ignore" }),
debugger: "debugpy",
file: "src/tasks/cli.py",
variable: "tasks",
evaluate: ["len(tasks.pending())", /^3$/],
},
rust: {
fixture: "rust-app",
debugger: "codelldb",
file: "src/main.rs",
variable: "inventory",
evaluate: ["inventory", /\S/],
},
go: {
fixture: "go-app",
debugger: "delve",
file: "cmd/server/main.go",
variable: "r",
evaluate: ["r.Method", /^"GET"$/],
request: "http://127.0.0.1:8080/notes?q=openidea",
},
java: {
fixture: "java-app",
debugger: "java-debug",
file: "src/main/java/com/example/orders/App.java",
variable: "book",
evaluate: ["book.size()", /^3$/],
},
kotlin: {
fixture: "kotlin-gradle",
debugger: "kotlin-debug-adapter",
file: "src/main/kotlin/com/example/library/App.kt",
variable: "library",
evaluate: ["library", /Library/],
},
};
let deactivate: (() => void) | null = null;
let server: Server | null = null;
let disconnect: (() => void) | null = null;
let base = "";
/** Backend events received, for failure messages. */
const events: string[] = [];
beforeAll(() => {
onIdeEvent("*", (e) => events.push(e.type === "debug" ? `debug ${e.session} ${e.event.kind}` : e.type));
const disposers = [activateWorkbench(), activateBuildTools(), activateRun(), activateDebugger()];
deactivate = () => disposers.reverse().forEach((d) => d());
});
afterAll(() => deactivate?.());
afterEach(() => {
resetRuns();
disconnect?.();
server?.close();
if (base) rmSync(base, { recursive: true, force: true });
});
async function open(c: Case): Promise<string> {
base = realpathSync(mkdtempSync(join(tmpdir(), "openidea-smoke-")));
const root = join(base, c.fixture);
cpSync(join(repoRoot, "fixtures", c.fixture), root, { recursive: true });
c.setup?.(root);
server = await Server.start(join(base, "home"));
disconnect = connectDesktop(server);
await openProject(root);
const trusted = await commands.projectTrustSet(root, true, false);
if (trusted.status !== "ok") throw new Error(trusted.error.message);
useProject.setState({ project: { ...useProject.getState().project!, trusted: true } });
// Run and Debug follow the build (a Gradle project's Application runs through Gradle).
await until(
"the build model",
() => {
const error = useBuild.getState().error;
if (error) throw new Error(`the build model did not load: ${error}`);
return useBuildModel.getState().root === root && useBuildModel.getState().model;
},
5 * 60_000,
);
return root;
}
async function install(tool: string) {
const tools = await call(commands.toolsList());
if (tools.find((t) => t.name === tool)?.status === "installed") return;
const channel = new Channel<InstallEvent>();
const info = await call(commands.toolsInstall(tool, nextRequestId(), channel));
expect(info.status, `${tool} installs`).toBe("installed");
}
/** What the Debug and Run tool windows show, for failure messages. */
function shown(): string {
const { sessions, console } = useDebug.getState();
const debug = Object.values(sessions).map(
(s) =>
`debug ${s.id} ${s.name} ${s.status} ${s.stopReason ?? ""}\n` +
(console[s.id] ?? [])
.slice(-40)
.map((l) => ` ${l.text}`)
.join(""),
);
const runs = useRun.getState().sessions.map((s) => `run ${s.id} ${s.config.name} ${s.status} ${s.exitCode ?? ""}`);
const notes = useNotifications.getState().items.map((n) => `${n.level}: ${n.title} ${n.body ?? ""}`);
return [...debug, ...runs, ...notes, `events: ${events.slice(-60).join(", ")}`, server?.log() ?? ""].join("\n");
}
/** Waits for `value` to be truthy; fails with what the tool windows show. */
async function until<T>(what: string, value: () => T | null | undefined | false, timeoutMs = 60_000): Promise<T> {
const end = Date.now() + timeoutMs;
for (;;) {
const v = value();
if (v) return v;
if (Date.now() > end) throw new Error(`timed out waiting for ${what}\n${shown()}`);
await new Promise((r) => setTimeout(r, 200));
}
}
/** Requests `url` once the debugged server accepts connections (the response waits for Resume). */
async function requestWhenListening(url: string) {
for (let i = 0; i < 600; i++) {
const connected = await new Promise<boolean>((resolve) => {
const req = get(url, (res) => {
res.resume();
resolve(true);
});
req.on("socket", (socket) => socket.on("connect", () => resolve(true)));
req.on("error", () => resolve(false));
});
if (connected) return;
await new Promise((r) => setTimeout(r, 500));
}
}
const paused = () => {
const s = activeSession();
return s?.status === "paused" && s.frames.length > 0 ? s : null;
};
/** Paused, failing as soon as every session has ended instead. */
const pausedOrEnded = () => {
const sessions = Object.values(useDebug.getState().sessions);
if (sessions.length > 0 && sessions.every((s) => s.status === "terminated"))
throw new Error(`the debugger ended before the breakpoint\n${shown()}`);
return paused();
};
async function smoke(language: string) {
const c = CASES[language]!;
const root = await open(c);
await install(c.debugger);
const path = join(root, c.file);
const content = readFileSync(path, "utf8");
const marker = content.split("\n").findIndex((l) => l.includes("Set a breakpoint here"));
const line = marker + 1;
addBreakpoint(path, line);
await executeAction("run.debugContext", { path, content });
if (c.request) void requestWhenListening(c.request);
const s = await until("the breakpoint", pausedOrEnded, 5 * 60_000);
expect(s.stopReason, `${s.stopDescription}\n${shown()}`).toBe("breakpoint");
const top = s.frames[0]!;
expect(top.path).toBe(path);
expect(top.line).toBe(line);
const scopes = await call(commands.debugScopes(s.id, top.id));
const locals = await call(commands.debugVariables(s.id, scopes[0]!.variablesReference));
expect(locals.map((v) => v.name)).toContain(c.variable);
const value = await evaluate(c.evaluate[0], "repl", s);
expect(value.value, c.evaluate[0]).toMatch(c.evaluate[1]);
// Python 3.12 reports a conditional expression's line twice: Step Over may stop on it
// (and its breakpoint) again.
for (let step = 0; step < 3 && paused()!.frames[0]!.line === line; step++) {
const stops = useDebug.getState().stopSeq;
await executeAction("debug.stepOver");
await until("Step Over", () => useDebug.getState().stopSeq > stops && paused());
}
const after = paused()!;
expect(after.stopReason, shown()).toBe("step");
expect(after.frames[0]!.line, shown()).toBeGreaterThan(line);
await executeAction("debug.resume");
if (c.request) {
await until("the request to be served", () => useDebug.getState().sessions[s.id]?.status === "running");
await executeAction("debug.stop");
}
// Every session ends (a JVM that listens again as its debugger detaches may be attached once more).
await until("the end", () => Object.values(useDebug.getState().sessions).every((d) => d.status === "terminated"));
}
for (const language of Object.keys(CASES)) {
test(language, () => smoke(language));
}
+7 -11
View File
@@ -3,25 +3,23 @@
* (callbacks, `Channel`s, the event plugin) in-process, and every app command sent to an
* `openidea-server` through the route remote development uses (`src/platform/tauriCore.ts`).
*/
import { homedir, tmpdir } from "node:os";
import { join } from "node:path";
import { setInvokeRoute } from "@/platform/tauriCore";
import { encodeArgs } from "@/platform/remote";
import { Server } from "./server";
type Callback = (message: unknown) => void;
// One runtime per page, there from the start as in the app: the frontend registers its event
// listener once (`src/platform/ipc.ts`), possibly before a server is connected, and keeps it
// across the servers connected one after another.
// One runtime per page, as in the app: the frontend registers its event listeners once
// (`src/platform/ipc.ts`), so they outlive the servers connected one after another.
const callbacks = new Map<number, Callback>();
const listeners: { event: string; handler: number }[] = [];
const channels = new Map<number, { onmessage: (data: unknown) => void }>();
let nextCallback = 1;
/** `BaseDirectory` values (`@tauri-apps/api/path`) the frontend resolves. */
const DIRECTORIES: Record<number, string> = { 12: tmpdir(), 21: homedir() };
let installed = false;
{
function install() {
if (installed) return;
installed = true;
const internals = {
metadata: { currentWindow: { label: "main" }, currentWebview: { label: "main", windowLabel: "main" } },
transformCallback(cb: Callback) {
@@ -40,9 +38,6 @@ const DIRECTORIES: Record<number, string> = { 12: tmpdir(), 21: homedir() };
return listeners.length;
}
if (cmd === "plugin:window|get_all_windows") return [];
// This machine's directories (`@tauri-apps/api/path`), as on the desktop.
if (cmd === "plugin:path|resolve_directory") return DIRECTORIES[args.directory as number] ?? null;
if (cmd === "plugin:path|join") return join(...(args.paths as string[]));
if (cmd.startsWith("plugin:")) return null;
throw { kind: "unavailable", message: `${cmd} has no route`, detail: null };
},
@@ -55,6 +50,7 @@ const DIRECTORIES: Record<number, string> = { 12: tmpdir(), 21: homedir() };
/** Connects this page's frontend to `server`; returns a function that disconnects it. */
export function connectDesktop(server: Server): () => void {
install();
channels.clear();
server.onChannel = (id, data) => channels.get(id)?.onmessage(data);
server.onEvent = (target, event) => {
-3
View File
@@ -3,7 +3,6 @@
* JSON-RPC with `Content-Length` framing over the server's stdin / stdout.
*/
import { type ChildProcessWithoutNullStreams, spawn } from "node:child_process";
import { appendFileSync } from "node:fs";
import { dirname, join } from "node:path";
const PROTOCOL_VERSION = 2;
@@ -36,8 +35,6 @@ export class Server {
private constructor(private readonly child: ChildProcessWithoutNullStreams) {
child.stdout.on("data", (chunk: Buffer) => this.receive(chunk));
child.stderr.on("data", (chunk: Buffer) => {
// The whole log, when asked for (OPENIDEA_SMOKE_LOG=<file>; OPENIDEA_LOG sets its level).
if (process.env.OPENIDEA_SMOKE_LOG) appendFileSync(process.env.OPENIDEA_SMOKE_LOG, chunk);
this.stderr.push(chunk.toString());
if (this.stderr.length > 200) this.stderr.shift();
});
@@ -15,7 +15,7 @@ import {
type InspectionNode,
} from "../pages/editor/inspectionsModel";
import { splitPageTarget } from "../model/staging";
import { JAVA_SERIAL_VERSION_INSPECTION, validateInspections } from "@/platform/inspections";
import { validateInspections } from "@/platform/inspections";
import { NATURAL_LANGUAGES_PAGE, SPELLING_PAGE } from "../pages/editor/spelling";
import { INSPECTIONS_PAGE } from "../pages/editor/inspections";
import { EDITOR_PAGES } from "../pages/editor";
@@ -69,7 +69,6 @@ describe("inspections tree", () => {
expect(tree.map((g) => g.name)).toEqual([
"Cargo",
"General",
"Java",
"JavaScript",
"Other",
"Proofreading",
@@ -110,19 +109,6 @@ describe("inspections tree", () => {
expect(checkState(o, python)).toBe("on");
});
it("keeps an inspection that is off by default off until turned on", () => {
const serial = find(tree, `Java/${JAVA_SERIAL_VERSION_INSPECTION}`)!;
expect(checkState({}, serial)).toBe("off");
let o = setEnabled({}, serial, true);
expect(o).toEqual({ [JAVA_SERIAL_VERSION_INSPECTION]: { enabled: true } });
expect(checkState(o, serial)).toBe("on");
o = setEnabled(o, serial, false);
expect(o).toEqual({});
// Turning a whole group on or off only records what differs from each default.
const java = find(tree, "group:Java")!;
expect(setEnabled({}, java, true)).toEqual({ [JAVA_SERIAL_VERSION_INSPECTION]: { enabled: true } });
});
it("keeps severity and enabled overrides independent", () => {
let o = setLevel({}, "lsp:ruff", "error");
o = setEnabled(o, find(tree, "Python/lsp:ruff")!, false);
@@ -8,7 +8,6 @@ import {
INSPECTION_LEVELS,
codeInspectionId,
customProfileId,
enabledByDefault,
newProfileKey,
overridesOf,
producerInspectionId,
@@ -160,7 +159,7 @@ export function inspectionIds(node: InspectionNode): string[] {
return [...(node.id ? [node.id] : []), ...node.children.flatMap(inspectionIds)];
}
export const isEnabled = (o: ProfileOverrides, id: string) => o[id]?.enabled ?? enabledByDefault(id);
export const isEnabled = (o: ProfileOverrides, id: string) => o[id]?.enabled !== false;
export type CheckState = "on" | "off" | "mixed";
@@ -182,12 +181,9 @@ function withOverride(o: ProfileOverrides, id: string, patch: Partial<Inspection
return next;
}
/** Turns a node and all its descendants on or off; an inspection back at its default drops the override. */
/** Turns a node and all its descendants on or off (on is the default, so it removes the override). */
export function setEnabled(o: ProfileOverrides, node: InspectionNode, enabled: boolean): ProfileOverrides {
return inspectionIds(node).reduce(
(acc, id) => withOverride(acc, id, { enabled: enabled === enabledByDefault(id) ? undefined : enabled }),
o,
);
return inspectionIds(node).reduce((acc, id) => withOverride(acc, id, { enabled: enabled ? undefined : false }), o);
}
/** `undefined` restores the default ("As reported"). */
+1 -21
View File
@@ -216,14 +216,9 @@ export interface BuiltinInspection {
description: string;
/** Highlighting level when the profile sets none (else as reported). */
defaultLevel?: InspectionLevel;
/** `false`: off until a profile turns it on. */
defaultEnabled?: boolean;
options?: readonly InspectionOptionSpec[];
}
/** Turned on, the Java language server's compiler reports it (`ide-sdk` `SERIAL_VERSION_INSPECTION`). */
export const JAVA_SERIAL_VERSION_INSPECTION = "java:missing-serial-version";
/** Producer key of the Typo inspection's diagnostics. */
export const SPELLING_PRODUCER = "spelling";
export const SPELLING_INSPECTION = "builtin:spelling";
@@ -288,16 +283,6 @@ export const BUILTIN_INSPECTIONS: readonly BuiltinInspection[] = [
group: "General",
description: "Compiler errors and warnings of the last Build Project, shown at their source positions.",
},
{
id: JAVA_SERIAL_VERSION_INSPECTION,
producer: "jdt-serial-version",
name: "Serializable class without 'serialVersionUID'",
group: "Java",
description:
"Reports classes that implement Serializable but declare no serialVersionUID field. Alt+Enter on the class name adds a default or a generated ID. The Java language server checks it, and restarts when this is changed.",
defaultLevel: "warning",
defaultEnabled: false,
},
{
id: "builtin:sql",
producer: "sql-inspections",
@@ -325,11 +310,6 @@ export const BUILTIN_INSPECTIONS: readonly BuiltinInspection[] = [
const BUILTIN_BY_PRODUCER = new Map(BUILTIN_INSPECTIONS.map((b) => [b.producer, b]));
const BUILTIN_BY_ID = new Map(BUILTIN_INSPECTIONS.map((b) => [b.id, b]));
/** Whether inspection `id` is on when a profile doesn't say. */
export function enabledByDefault(id: string): boolean {
return BUILTIN_BY_ID.get(id)?.defaultEnabled ?? true;
}
/** Option `key` of inspection `id` under `profile` (its default when unset). */
export function inspectionOption(profile: ProfileOverrides, id: string, key: string): boolean {
const value = profile[id]?.options?.[key];
@@ -383,7 +363,7 @@ export function applyProfile<D extends Diagnostic>(
): readonly D[] {
const base = producerInspectionId(producer);
const own = profile[base];
if (!(own?.enabled ?? enabledByDefault(base))) return [];
if (own?.enabled === false) return [];
const fallback = BUILTIN_BY_PRODUCER.get(producer)?.defaultLevel;
let changed = false;
const out: D[] = [];
-2
View File
@@ -32,8 +32,6 @@
"Not found": "未找到",
"Not found on PATH": "未在 PATH 中找到",
"Reports newer crate versions that Cargo.toml requirements do not already allow.": "报告 Cargo.toml 版本要求尚未允许的较新 crate 版本。",
"Serializable class without 'serialVersionUID'": "没有 'serialVersionUID' 的可序列化类",
"Reports classes that implement Serializable but declare no serialVersionUID field. Alt+Enter on the class name adds a default or a generated ID. The Java language server checks it, and restarts when this is changed.": "报告实现了 Serializable 但没有声明 serialVersionUID 字段的类。在类名上按 Alt+Enter 可添加默认 ID 或生成的 ID。此检查由 Java 语言服务器执行,更改后语言服务器会重启。",
"Run `auggie login` in a terminal.": "在终端中运行 `auggie login`。",
"Run `claude` in a terminal and sign in.": "在终端中运行 `claude` 并登录。",
"Run `codex login` in a terminal.": "在终端中运行 `codex login`。",