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

..
Автор SHA1 Повідомлення Дата
l0ng-ai d0e7558b1c refactor(console): share the console font between the Run and Debug consoles 2026-11-12 18:44:02 +08:00
l0ng-ai 3bd7adda08 fix(debugger): use the console font for the evaluate input too 2026-11-12 18:25:20 +08:00
l0ng-ai 8bd417edaa Merge main 2026-11-12 18:24:35 +08:00
arookieofc fefd60206d fix(debugger): use the same console font as the Run window
DebugConsole rendered with var(--font-mono) and var(--fs-ui), ignoring
the terminal/editor font settings that the Run console resolves via
resolveMonoFont. Now it applies the same font family, size and row height.

Closes #2844
2026-11-12 18:11:56 +08:00
12 змінених файлів з 49 додано та 120 видалено

No files matched your search

-8
Переглянути файл
@@ -11,12 +11,4 @@
/>
</dependentAssembly>
</dependency>
<!-- Win32 calls in dependencies (gitoxide's ownership check of a submodule's
`<worktree>/../../.git/modules/…` path) accept paths past MAX_PATH when
the system enables long paths. -->
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
</windowsSettings>
</application>
</assembly>
+2 -2
Переглянути файл
@@ -56,7 +56,7 @@ async function applyEdit(view: EditorView, edit: WorkspaceEdit) {
async function prepareIndexes(view: EditorView, path: string): Promise<boolean> {
const before = view.state.doc;
try {
await synchronizeDocumentIndexes(path, "refactor");
await synchronizeDocumentIndexes(path);
return view.state.doc === before;
} catch (e) {
reportLspError(view, e, true);
@@ -269,7 +269,7 @@ async function finishRename(view: EditorView, doc: EditorDocContext, pos: Pos, o
let renamed;
const before = view.state.doc;
try {
await synchronizeDocumentIndexes(doc.path, "refactor");
await synchronizeDocumentIndexes(doc.path);
if (renameRequest !== request || view.state.doc !== before) return;
renamed = await lspRequest(() => commands.lspRename(request, doc.doc, pos, newName));
if (renameRequest !== request) return;
+1 -18
Переглянути файл
@@ -160,7 +160,7 @@ describe("findUsages", () => {
const pending = findUsages();
const at = (line: number) => ({ path: "/p/b.rs", range: { start: { line, col: 4 }, end: { line, col: 8 } } });
try {
await vi.waitFor(() => expect(sync).toHaveBeenCalledWith("/p/a.rs", "usages"));
await vi.waitFor(() => expect(sync).toHaveBeenCalledWith("/p/a.rs"));
expect(lspReferences).not.toHaveBeenCalled();
expect(channels).toHaveLength(allocated);
release();
@@ -181,23 +181,6 @@ describe("findUsages", () => {
});
});
describe("findUsages with a contributed index that is not up to date", () => {
it("still searches, and says plugin usages may be missing", async () => {
const off = registerDocumentIndexSync(async () => false);
const at = { path: "/p/b.rs", range: { start: { line: 1, col: 4 }, end: { line: 1, col: 8 } } };
referenceResponse.mockResolvedValueOnce({ locations: [at], incomplete: null });
try {
await findUsages();
const search = activeUsageSearch(useUsages.getState())!;
expect(search.groups).toHaveLength(1);
expect(search.note).toBe("May be incomplete: plugin indexes are still updating");
} finally {
off();
resetUsages();
}
});
});
describe("usage tabs", () => {
const search = (id: number) => ({
id,
+1 -3
Переглянути файл
@@ -6,7 +6,6 @@ import { clearLineTokens } from "@/editor/lineTokens";
import { showPendingHint } from "@/editor/lsp/hint";
import { incompleteNote } from "@/editor/lsp/lsp";
import { commands, IpcError, isCancelled, nextRequestId } from "@/platform/ipc";
import { tx } from "@/platform/i18n";
import { shortcutLabel } from "@/platform/keymap";
import { synchronizeDocumentIndexes, toolWindowService } from "@/workbench/contributions";
import {
@@ -231,7 +230,7 @@ export async function fetchUsages(
let note: string | null = null;
let librariesSkipped = false;
await caretSynced(c);
const indexed = await synchronizeDocumentIndexes(c.path, "usages");
await synchronizeDocumentIndexes(c.path);
if (c.view && c.view.state.doc !== before) throw new IpcError({ kind: "cancelled", message: "", data: null });
let settled = false;
const channel = new Channel<References>();
@@ -269,7 +268,6 @@ export async function fetchUsages(
locs = fb.locs;
note = fb.note;
}
if (!indexed) note ??= tx("May be incomplete: plugin indexes are still updating");
const files = await readManyFileLines(pathsToRead(locs, classified));
return { groups: groupUsages(locs, files, classified), note, librariesSkipped };
}
+10 -3
Переглянути файл
@@ -15,6 +15,7 @@ import { EmptyState, IconButton, showContextMenu, T, type MenuItem } from "@/ui"
import { formatBinding, hasPrimaryModifier, hostBinding, registerKeyClaim } from "@/platform/keymap";
import { tx } from "@/platform/i18n";
import { useWorkflowSettings } from "@/platform/workflowSettings";
import { useConsoleFont } from "@/platform/consoleFont";
import { spanStyle } from "@/ui/ansi";
import { ConsoleFindBar } from "@/ui/ConsoleFindBar";
import { DEFAULT_FIND_OPTIONS, findInLines, type FindOptions, type FindResult } from "@/ui/consoleFind";
@@ -117,6 +118,10 @@ export function DebugConsole({ session, interactive = true }: { session: DebugSe
const [follow, setFollow] = useState(true);
const stick = useRef(true);
stick.current = follow;
// The debug console uses the same font as the Run console.
const consoleFont = useConsoleFont();
const { rowHeight } = consoleFont;
const fontStyle = { fontFamily: consoleFont.family, fontSize: `${consoleFont.size}px` };
// Settings › Console › Use soft wraps is the default; the toggle overrides it for this console.
const defaultWrap = useWorkflowSettings((s) => s.console.softWrap);
const [wrapOverride, setWrapOverride] = useState<boolean | null>(null);
@@ -140,13 +145,13 @@ export function DebugConsole({ session, interactive = true }: { session: DebugSe
const virtualizer = useVirtualizer({
count: lines?.length ?? 0,
getScrollElement: () => scroller.current,
estimateSize: () => 18,
estimateSize: () => rowHeight,
overscan: 30,
getItemKey: (i) => lines?.[i]?.id ?? i,
});
// Wrapping changes every row's height; the end is pinned again below.
useLayoutEffect(() => virtualizer.measure?.(), [softWrap, virtualizer]);
useLayoutEffect(() => virtualizer.measure?.(), [softWrap, rowHeight, virtualizer]);
useLayoutEffect(() => {
const el = scroller.current;
@@ -429,6 +434,7 @@ export function DebugConsole({ session, interactive = true }: { session: DebugSe
<div
ref={scroller}
className={clsx(styles.consoleOutput, !softWrap && styles.noWrap)}
style={{ ...fontStyle, lineHeight: `${rowHeight}px` }}
onScroll={(e) => {
if (programmatic.current) return;
const el = e.currentTarget;
@@ -465,12 +471,13 @@ export function DebugConsole({ session, interactive = true }: { session: DebugSe
<div className={styles.repl}>
{interactive && (
<>
<span className={styles.prompt} aria-hidden>
<span className={styles.prompt} style={fontStyle} aria-hidden>
›
</span>
<input
ref={inputRef}
className={styles.evalInput}
style={fontStyle}
onFocus={() => setConsoleHistoryBrowser(browseHistory)}
onBlur={() => {
setConsoleHistoryBrowser(null);
+5 -31
Переглянути файл
@@ -135,7 +135,7 @@ it("flushes pending changes before usages or rename without waiting for the debo
await vi.advanceTimersByTimeAsync(250);
mocks.index.mockClear();
mocks.events.get("fsChanged")?.({ created: [], modified: ["/project/A.xml"], removed: [] });
await synchronizeDocumentIndexes("/project/A.xml", "refactor");
await synchronizeDocumentIndexes("/project/A.xml");
expect(mocks.index).toHaveBeenCalledExactlyOnceWith(["/project/A.xml"], expect.any(Number));
});
@@ -153,7 +153,7 @@ it("waits for an active batch, then indexes changes queued while it was running"
await vi.advanceTimersByTimeAsync(40);
mocks.events.get("fsChanged")?.({ created: [], modified: ["/project/A.java"], removed: [] });
let settled = false;
const waiting = synchronizeDocumentIndexes("/project/A.xml", "refactor").then(() => {
const waiting = synchronizeDocumentIndexes("/project/A.xml").then(() => {
settled = true;
});
await Promise.resolve();
@@ -168,33 +168,7 @@ it("refuses refactoring if a queued index batch failed", async () => {
await vi.advanceTimersByTimeAsync(250);
mocks.index.mockResolvedValueOnce({ status: "error", error: { kind: "unavailable", message: "indexer failed" } });
mocks.events.get("fsChanged")?.({ created: [], modified: ["/project/A.xml"], removed: [] });
await expect(synchronizeDocumentIndexes("/project/A.xml", "refactor")).rejects.toThrow(
"Plugin indexes could not be updated",
);
});
it("lets usages go ahead, marked incomplete, while a failed batch keeps failing", async () => {
off = startPluginNavigation();
await vi.advanceTimersByTimeAsync(250);
const failure = { status: "error", error: { kind: "unavailable", message: "indexer failed" } };
mocks.index.mockResolvedValue(failure);
mocks.events.get("fsChanged")?.({ created: [], modified: ["/project/A.xml"], removed: [] });
await vi.advanceTimersByTimeAsync(40);
// Every later search used to fail with "still updating" although nothing was running.
await expect(synchronizeDocumentIndexes("/project/A.java", "usages")).resolves.toBe(false);
await expect(synchronizeDocumentIndexes("/project/A.java", "usages")).resolves.toBe(false);
});
it("retries a failed batch before the next usages or refactoring", async () => {
off = startPluginNavigation();
await vi.advanceTimersByTimeAsync(250);
mocks.index.mockResolvedValueOnce({ status: "error", error: { kind: "unavailable", message: "indexer failed" } });
mocks.events.get("fsChanged")?.({ created: [], modified: ["/project/A.xml"], removed: [] });
await vi.advanceTimersByTimeAsync(40);
mocks.index.mockClear();
await expect(synchronizeDocumentIndexes("/project/A.java", "refactor")).resolves.toBe(true);
expect(mocks.index).toHaveBeenCalledWith(["/project/A.xml"], expect.any(Number));
await expect(synchronizeDocumentIndexes("/project/A.java", "usages")).resolves.toBe(true);
await expect(synchronizeDocumentIndexes("/project/A.xml")).rejects.toThrow("Plugin indexes are still updating");
});
it.each(["/project/notes.txt", "/external/Other.xml", "jdt://contents/Other.xml"])(
@@ -205,13 +179,13 @@ it.each(["/project/notes.txt", "/external/Other.xml", "jdt://contents/Other.xml"
mocks.index.mockImplementationOnce(() => new Promise(() => {}));
mocks.events.get("fsChanged")?.({ created: [], modified: ["/project/A.xml"], removed: [] });
await vi.advanceTimersByTimeAsync(40);
await synchronizeDocumentIndexes(path, "refactor");
await synchronizeDocumentIndexes(path);
},
);
it("keeps unrelated language-server queries available when plugin discovery fails", async () => {
mocks.files.mockRejectedValue(new Error("Plugin file inventory is unavailable"));
off = startPluginNavigation();
await expect(synchronizeDocumentIndexes("/project/notes.txt", "refactor")).resolves.toBe(true);
await expect(synchronizeDocumentIndexes("/project/notes.txt")).resolves.toBeUndefined();
expect(mocks.index).not.toHaveBeenCalled();
});
+6 -31
Переглянути файл
@@ -22,12 +22,9 @@ export function startPluginNavigation(): () => void {
let timer: ReturnType<typeof setTimeout> | undefined;
let pending = new Set<string>();
const dirty = new Set<string>();
/** Paths whose last index batch failed: retried on the next usages or refactoring. */
const failed = new Set<string>();
const views = new Map<EditorView, string>();
let discovered = false;
let discovering = false;
let discoveryFailed = false;
const known = new Set<string>();
let signature = "";
let reportedError = false;
@@ -82,22 +79,13 @@ export function startPluginNavigation(): () => void {
await call(commands.pluginNavigationIndex(paths, id));
if (at === generation) {
paths.forEach((p) => {
failed.delete(p);
if (!pending.has(p)) dirty.delete(p);
});
notify();
}
} catch (error) {
if (at === generation && isCancelled(error)) paths.forEach((p) => pending.add(p));
else if (at === generation) {
// Not "still updating": the batch is done, its paths just have no fresh index.
paths.forEach((p) => {
if (pending.has(p)) return;
dirty.delete(p);
failed.add(p);
});
report(error);
}
else if (at === generation) report(error);
} finally {
request = null;
running = false;
@@ -132,14 +120,12 @@ export function startPluginNavigation(): () => void {
if (at !== generation) return;
fileExtensions = new Set(inventory.extensions);
discovered = !inventory.indexing;
discoveryFailed = false;
const fresh = inventory.paths.filter((p) => !known.has(p));
inventory.paths.forEach((p) => known.add(p));
queue(fresh);
queue([...views.values()], true);
} catch (error) {
discovered = false;
discoveryFailed = true;
report(error);
} finally {
if (at === generation) discovering = false;
@@ -153,11 +139,9 @@ export function startPluginNavigation(): () => void {
clearTimeout(timer);
pending.clear();
dirty.clear();
failed.clear();
known.clear();
discovered = false;
discovering = false;
discoveryFailed = false;
discovery = null;
reportedError = false;
notify();
@@ -167,41 +151,32 @@ export function startPluginNavigation(): () => void {
.catch(() => {});
}
const offs = [
registerDocumentIndexSync(async (path, purpose) => {
registerDocumentIndexSync(async (path) => {
if (!ready()) return;
const root = useProject.getState().project!.root;
if (!path.startsWith(root.endsWith("/") ? root : `${root}/`)) return;
const at = generation;
await discover();
if (!eligible(path)) return;
if (failed.size) queue([...failed]);
while (ready() && at === generation && (pending.size || flushing)) {
clearTimeout(timer);
await flush();
}
const complete = at === generation && discovered && !dirty.size && !failed.size;
if (complete) return true;
if (purpose === "usages") return false;
if (at === generation && (failed.size || discoveryFailed)) {
throw new Error(tx`Plugin indexes could not be updated; see the plugin notification`);
if (at !== generation || !discovered || dirty.size) {
throw new Error(tx`Plugin indexes are still updating; try again shortly`);
}
throw new Error(tx`Plugin indexes are still updating; try again shortly`);
}),
pluginsBackend().onEvent((event) => {
if (event.type === "documentIndexesChanged") reset();
}),
registerNavigationProvider(async (view, offset) => {
const doc = docOf(view);
if (!ready() || !doc || doc.virtual || !eligible(doc.path) || dirty.has(doc.path) || failed.has(doc.path))
return null;
if (!ready() || !doc || doc.virtual || !eligible(doc.path) || dirty.has(doc.path)) return null;
const before = view.state.doc;
await whenSynced(view);
const result = await call(commands.pluginNavigationGoto(doc.doc, offsetToPos(before, offset)));
if (!result || view.state.doc !== before) return null;
return {
...rangeToOffsets(before, result.range),
targets: result.targets.filter((t) => !dirty.has(t.path) && !failed.has(t.path)),
};
return { ...rangeToOffsets(before, result.range), targets: result.targets.filter((t) => !dirty.has(t.path)) };
}),
registerEditorContribution({
id: "plugins.navigation",
+3 -6
Переглянути файл
@@ -14,8 +14,7 @@ import {
import clsx from "clsx";
import { useVirtualizer } from "@tanstack/react-virtual";
import { useWorkflowSettings } from "@/platform/workflowSettings";
import { useProject } from "@/platform/project";
import { resolveMonoFont } from "@/platform/fonts";
import { useConsoleFont } from "@/platform/consoleFont";
import { Icon, showContextMenu } from "@/ui";
import { spanStyle } from "@/ui/ansi";
import type { ConsoleLine, ConsoleModel } from "./console";
@@ -185,10 +184,8 @@ export function ConsoleView({
const patterns = useWorkflowSettings((s) => s.console.foldPatterns);
// The Run console uses Tools › Terminal › Font (which can follow Editor › Font),
// so the font and fallback configured there apply to process output too.
const terminalFont = useWorkflowSettings((s) => s.terminal);
const editorFont = useProject((s) => s.settings?.editor);
const consoleFont = resolveMonoFont(terminalFont, editorFont);
const rowHeight = Math.max(16, Math.round(consoleFont.size * 1.5));
const consoleFont = useConsoleFont();
const { rowHeight } = consoleFont;
/** A row's box: at its virtual offset, and never shorter than one line — a blank line has no text. */
const rowStyle = (start: number) => ({
transform: `translateY(${start}px)`,
+14
Переглянути файл
@@ -0,0 +1,14 @@
import { useProject } from "./project";
import { resolveMonoFont } from "./fonts";
import { useWorkflowSettings } from "./workflowSettings";
/**
* The font of the Run and Debug consoles: Tools › Terminal › Font (which can follow
* Editor › Font), with the row height both consoles lay their lines out with.
*/
export function useConsoleFont(): { family: string; size: number; rowHeight: number } {
const terminalFont = useWorkflowSettings((s) => s.terminal);
const editorFont = useProject((s) => s.settings?.editor);
const { family, size } = resolveMonoFont(terminalFont, editorFont);
return { family, size, rowHeight: Math.max(16, Math.round(size * 1.5)) };
}
+1 -3
Переглянути файл
@@ -1344,7 +1344,5 @@
"Close Agent Task": "Close Agent Task",
"Close and New Agent Task": "Close and New Agent Task",
"Closes the agent task tab in the editor; the task keeps running in the Agents tool window.": "Closes the agent task tab in the editor; the task keeps running in the Agents tool window.",
"Closes the agent task tab in the editor, then opens the New Task page.": "Closes the agent task tab in the editor, then opens the New Task page.",
"May be incomplete: plugin indexes are still updating": "May be incomplete: plugin indexes are still updating",
"Plugin indexes could not be updated; see the plugin notification": "Plugin indexes could not be updated; see the plugin notification"
"Closes the agent task tab in the editor, then opens the New Task page.": "Closes the agent task tab in the editor, then opens the New Task page."
}
+1 -3
Переглянути файл
@@ -7731,7 +7731,5 @@
"Download Remote Files Into Folder": "将远程文件下载到文件夹",
"Download Remote Host files into a local folder (arg: { server, paths, target })": "将远程主机中的文件下载到本地文件夹(参数:{ server, paths, target })",
"Delegate IDE build/run actions to Maven": "将 IDE 构建/运行操作委托给 Maven",
"Run and Debug build Maven modules with Maven (the project's javac) instead of the Java language server. Editor problems still come from the language server.": "运行和调试时用 Maven(项目的 javac)构建 Maven 模块,而不是用 Java 语言服务器。编辑器中的问题仍来自语言服务器。",
"May be incomplete: plugin indexes are still updating": "可能不完整:插件索引仍在更新",
"Plugin indexes could not be updated; see the plugin notification": "插件索引更新失败,详见插件通知"
"Run and Debug build Maven modules with Maven (the project's javac) instead of the Java language server. Editor problems still come from the language server.": "运行和调试时用 Maven(项目的 javac)构建 Maven 模块,而不是用 Java 语言服务器。编辑器中的问题仍来自语言服务器。"
}
+5 -12
Переглянути файл
@@ -1825,23 +1825,16 @@ export function addContributedFileTemplate(t: import("@/platform/fileTemplates")
return () => useContributedFileTemplates.setState((s) => ({ templates: s.templates.filter((x) => x !== t) }));
}
/**
* Flush contributed document indexes before workspace usages or refactoring. A refactoring
* refuses (throws) while an index is incomplete; usages go ahead and resolve `false`, so the
* results can say they may be incomplete.
*/
export type DocumentIndexPurpose = "usages" | "refactor";
type DocumentIndexSync = (path: string, purpose: DocumentIndexPurpose) => Promise<boolean | void>;
const documentIndexSync = new Set<DocumentIndexSync>();
export function registerDocumentIndexSync(sync: DocumentIndexSync): () => void {
/** Flush contributed document indexes before workspace usages or refactoring. */
const documentIndexSync = new Set<(path: string) => Promise<void>>();
export function registerDocumentIndexSync(sync: (path: string) => Promise<void>): () => void {
documentIndexSync.add(sync);
return () => {
documentIndexSync.delete(sync);
};
}
export async function synchronizeDocumentIndexes(path: string, purpose: DocumentIndexPurpose): Promise<boolean> {
const complete = await Promise.all([...documentIndexSync].map((sync) => sync(path, purpose)));
return complete.every((c) => c !== false);
export async function synchronizeDocumentIndexes(path: string): Promise<void> {
await Promise.all([...documentIndexSync].map((sync) => sync(path)));
}
/** Font zoom for non-CodeMirror surfaces, owned and released by their features. */