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

...
5 Коміти
Автор SHA1 Повідомлення Дата
l0ng-ai 1e0ce64b0c fix(plugins): don't block Find Usages on a failed plugin index batch (#2941)
A plugin indexer batch that failed left its paths dirty forever, so every later Find Usages and rename failed with "Plugin indexes are still updating". Failed paths are now retried before the next usages or refactoring; usages go ahead with a "may be incomplete" note while an index is not up to date, and refactoring refuses with a message that says the index failed.

Fixes #2913
2026-11-12 18:37:36 +08:00
HL-slcтаHL-slc 6e7b39023c fix(vcs): show changes in submodules whose git dir path exceeds MAX_PATH on Windows (#2902)
gitoxide checks ownership of a submodule's unnormalized `<worktree>/../../.git/modules/…` path with GetNamedSecurityInfoW, which rejects paths past 260 characters, so deep submodules were skipped and the parent's status fell back to git. The app manifest now declares longPathAware, so Win32 path APIs accept long paths where the system enables them.

Co-authored-by: HL-slc <[email protected]>
2026-11-12 18:28:04 +08:00
aconeshanaтаaconeshana c5d319c298 fix(run): keep a blank output line free of a zero-width space (#2906)
A blank console row rendered a U+200B text node to keep its height, and selections and Copy took that node as content. Blank rows now get their height from the row box (minHeight = rowHeight) and render no text.

Closes #2878

Co-authored-by: aconeshana <[email protected]>
2026-11-12 18:24:19 +08:00
arookieofc 0780171704 test(i18n): whitelist Bun product names in own-script locales (#2931) 2026-11-12 18:11:08 +08:00
l0ng-ai 0324a81ce4 fix(ssh): update russh to 0.65 (RUSTSEC-2026-0495) (#2935)
The advisory covers russh's SSH agent server, which signed with confirm-constrained keys without asking; OpenIDEA only uses the agent client, so it was not affected, but cargo deny fails on main. 0.65 passes the verified key to the server's auth_publickey; the test SSH server follows.
2026-11-12 18:05:33 +08:00
16 змінених файлів з 282 додано та 32 видалено

No files matched your search

Створено
+2 -3
Переглянути файл
@@ -2637,7 +2637,6 @@ version = "1.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "337d46834ee672ab3e48caca2cb0c78cc174fb12b3a68d0d88f99a0519a5e36e"
dependencies = [
"generic-array 0.14.7",
"rustversion",
"typenum",
]
@@ -7712,9 +7711,9 @@ dependencies = [
[[package]]
name = "russh"
version = "0.64.1"
version = "0.65.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba61e87b9ec9a39a59a6bbed4c0b8ff7fb07073405b24a767a1f2c8560ac8f58"
checksum = "94be3d86c71b9ed220a675c405e35c7a02565a009bf8da2e9cc13a700320a5c5"
dependencies = [
"aes",
"bitflags 2.13.2",
+1 -1
Переглянути файл
@@ -18,7 +18,7 @@ dirs.workspace = true
uuid.workspace = true
keyring.workspace = true
keyring-core.workspace = true
russh = { version = "0.64", default-features = false, features = ["ring", "rsa", "flate2"] }
russh = { version = "0.65", default-features = false, features = ["ring", "rsa", "flate2"] }
russh-sftp = "2.4"
flate2 = "1"
suppaftp = { version = "12.2.1", default-features = false, features = ["tokio-rustls-ring"] }
+3 -2
Переглянути файл
@@ -44,8 +44,9 @@ impl server::Handler for Sshd {
Ok(if user == "dev" && password == "secret" { Auth::Accept } else { Auth::reject() })
}
async fn auth_publickey(&mut self, user: &str, key: &PublicKey) -> Result<Auth, Self::Error> {
Ok(if user == "dev" && key.key_data() == self.client_key.key_data() { Auth::Accept } else { Auth::reject() })
async fn auth_publickey(&mut self, user: &str, key: server::VerifiedPublicKey<'_>) -> Result<Auth, Self::Error> {
let accept = user == "dev" && key.public_key.key_data() == self.client_key.key_data();
Ok(if accept { Auth::Accept } else { Auth::reject() })
}
async fn channel_open_session(
+67
Переглянути файл
@@ -103,3 +103,70 @@ test("Select All takes the whole buffer, not just the mounted rows", async ({ pa
expect(copied).toContain("output 0\n");
expect(copied).toMatch(/output 499$/);
});
/** Mount the real console over the given output, wrapped or not. */
async function consoleOf(page: Page, text: string, softWrap: boolean) {
await page.goto("/");
await page.evaluate(
async ({ text, softWrap }) => {
const reactPath = "/node_modules/.vite/deps/react.js";
const domPath = "/node_modules/.vite/deps/react-dom_client.js";
const viewPath = "/src/features/run/ConsoleView.tsx";
const modelPath = "/src/features/run/console.ts";
const react = await import(reactPath);
const dom = await import(domPath);
const { createElement, useState } = react.default ?? react;
const { createRoot } = dom.default ?? dom;
const { ConsoleView } = await import(viewPath);
const { ConsoleModel } = await import(modelPath);
const model = new ConsoleModel();
model.append({ kind: "stdout", text });
const host = document.createElement("div");
host.style.cssText = "position:fixed;inset:0;z-index:99999;display:flex;height:300px;background:white";
document.body.append(host);
function Console() {
const [following, setFollowing] = useState(false);
return createElement(ConsoleView, {
model,
version: 1,
softWrap,
follow: following,
onFollowChange: setFollowing,
cwd: "/",
});
}
createRoot(host).render(createElement(Console));
},
{ text, softWrap },
);
const log = page.getByRole("log", { name: "Console", exact: true });
await expect(log.locator("[data-index]").first()).toBeVisible();
return log;
}
for (const softWrap of [false, true]) {
test(`takes a blank output line as a line break, not a zero-width space (soft wrap ${softWrap ? "on" : "off"})`, async ({
page,
}) => {
const log = await consoleOf(page, "first\n\nsecond\n", softWrap);
// A blank line is still a whole row: stripping the character must not shrink it.
const heights = await log.evaluate((el) =>
[...el.querySelectorAll("[data-index]")].map((row) => Math.round(row.getBoundingClientRect().height)),
);
expect(heights).toHaveLength(3);
expect(new Set(heights).size).toBe(1);
expect(heights[0]).toBeGreaterThan(0);
const selected = await log.evaluate((el) => {
const range = document.createRange();
range.selectNodeContents(el.firstElementChild!);
const selection = window.getSelection()!;
selection.removeAllRanges();
selection.addRange(range);
return selection.toString();
});
expect(selected).not.toContain("\u200b");
// Chromium drops the trailing break of a selection; the blank line in the middle stays one.
expect(selected.split("\n")).toEqual(["first", "", "second"]);
});
}
+8
Переглянути файл
@@ -11,4 +11,12 @@
/>
</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);
await synchronizeDocumentIndexes(path, "refactor");
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);
await synchronizeDocumentIndexes(doc.path, "refactor");
if (renameRequest !== request || view.state.doc !== before) return;
renamed = await lspRequest(() => commands.lspRename(request, doc.doc, pos, newName));
if (renameRequest !== request) return;
+18 -1
Переглянути файл
@@ -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"));
await vi.waitFor(() => expect(sync).toHaveBeenCalledWith("/p/a.rs", "usages"));
expect(lspReferences).not.toHaveBeenCalled();
expect(channels).toHaveLength(allocated);
release();
@@ -181,6 +181,23 @@ 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,
+3 -1
Переглянути файл
@@ -6,6 +6,7 @@ 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 {
@@ -230,7 +231,7 @@ export async function fetchUsages(
let note: string | null = null;
let librariesSkipped = false;
await caretSynced(c);
await synchronizeDocumentIndexes(c.path);
const indexed = await synchronizeDocumentIndexes(c.path, "usages");
if (c.view && c.view.state.doc !== before) throw new IpcError({ kind: "cancelled", message: "", data: null });
let settled = false;
const channel = new Channel<References>();
@@ -268,6 +269,7 @@ 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 };
}
+31 -5
Переглянути файл
@@ -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");
await synchronizeDocumentIndexes("/project/A.xml", "refactor");
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").then(() => {
const waiting = synchronizeDocumentIndexes("/project/A.xml", "refactor").then(() => {
settled = true;
});
await Promise.resolve();
@@ -168,7 +168,33 @@ 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")).rejects.toThrow("Plugin indexes are still updating");
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);
});
it.each(["/project/notes.txt", "/external/Other.xml", "jdt://contents/Other.xml"])(
@@ -179,13 +205,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);
await synchronizeDocumentIndexes(path, "refactor");
},
);
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")).resolves.toBeUndefined();
await expect(synchronizeDocumentIndexes("/project/notes.txt", "refactor")).resolves.toBe(true);
expect(mocks.index).not.toHaveBeenCalled();
});
+31 -6
Переглянути файл
@@ -22,9 +22,12 @@ 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;
@@ -79,13 +82,22 @@ 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) report(error);
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);
}
} finally {
request = null;
running = false;
@@ -120,12 +132,14 @@ 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;
@@ -139,9 +153,11 @@ 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();
@@ -151,32 +167,41 @@ export function startPluginNavigation(): () => void {
.catch(() => {});
}
const offs = [
registerDocumentIndexSync(async (path) => {
registerDocumentIndexSync(async (path, purpose) => {
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();
}
if (at !== generation || !discovered || dirty.size) {
throw new Error(tx`Plugin indexes are still updating; try again shortly`);
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`);
}
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)) return null;
if (!ready() || !doc || doc.virtual || !eligible(doc.path) || dirty.has(doc.path) || failed.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)) };
return {
...rangeToOffsets(before, result.range),
targets: result.targets.filter((t) => !dirty.has(t.path) && !failed.has(t.path)),
};
}),
registerEditorContribution({
id: "plugins.navigation",
+11 -4
Переглянути файл
@@ -51,7 +51,8 @@ const Line = memo(
if (link.kind === "url") void openUrl(link.url);
else void openTarget(link.target, cwd);
};
if (segments.length === 0) return <>{"​"}</>;
// A blank line is an empty row: its height comes from CSS, never from a character in the text.
if (segments.length === 0) return null;
return (
<>
{segments.map((seg, i) => {
@@ -188,6 +189,12 @@ export function ConsoleView({
const editorFont = useProject((s) => s.settings?.editor);
const consoleFont = resolveMonoFont(terminalFont, editorFont);
const rowHeight = Math.max(16, Math.round(consoleFont.size * 1.5));
/** 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)`,
height: softWrap ? undefined : rowHeight,
minHeight: rowHeight,
});
const [expanded, setExpanded] = useState<ReadonlySet<number>>(() => new Set());
const [internalFindOpen, setInternalFindOpen] = useState(false);
const findOpen = controlledFindOpen ?? internalFindOpen;
@@ -567,7 +574,7 @@ export function ConsoleView({
data-index={item.index}
ref={softWrap ? virtualizer.measureElement : undefined}
className={styles.line}
style={{ transform: `translateY(${item.start}px)`, height: softWrap ? undefined : rowHeight }}
style={rowStyle(item.start)}
>
{pendingInput}
</div>
@@ -580,7 +587,7 @@ export function ConsoleView({
data-index={item.index}
ref={softWrap ? virtualizer.measureElement : undefined}
className={styles.line}
style={{ transform: `translateY(${item.start}px)`, height: softWrap ? undefined : rowHeight }}
style={rowStyle(item.start)}
>
{foldButton(row.fold, false)}
</div>
@@ -594,7 +601,7 @@ export function ConsoleView({
data-index={item.index}
ref={softWrap ? virtualizer.measureElement : undefined}
className={clsx(styles.line, styles[line.kind])}
style={{ transform: `translateY(${item.start}px)`, height: softWrap ? undefined : rowHeight }}
style={rowStyle(item.start)}
>
<Line line={line} marks={marksByLine.get(row.index)} length={line.text.length} cwd={cwd} />
{row.fold && foldButton(row.fold, true)}
+85
Переглянути файл
@@ -0,0 +1,85 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { usePlatform } from "@/platform/platformInfo";
import { ConsoleModel } from "./console";
import { ConsoleView } from "./ConsoleView";
const clipboard = vi.hoisted(() => ({ writeText: vi.fn(async (_text: string) => {}) }));
vi.mock("@tanstack/react-virtual", () => ({
useVirtualizer: ({ count }: { count: number }) => ({
getTotalSize: () => count * 20,
getVirtualItems: () => Array.from({ length: count }, (_, index) => ({ index, start: index * 20, key: index })),
scrollToIndex: () => {},
measureElement: () => {},
measure: () => {},
}),
}));
beforeEach(() => {
usePlatform.setState({ os: "macos" });
clipboard.writeText.mockClear();
Object.defineProperty(navigator, "clipboard", { configurable: true, value: clipboard });
window.getSelection()?.removeAllRanges();
});
function setup(text: string, softWrap = false) {
const model = new ConsoleModel();
model.append({ kind: "stdout", text });
model.flush();
const view = render(
<ConsoleView model={model} version={1} softWrap={softWrap} follow={false} onFollowChange={() => {}} cwd="/" />,
);
return { model, log: screen.getByRole("log"), view };
}
/** The text a Select All + Copy takes: every mounted row, in order. */
function selectedText(log: HTMLElement) {
const inner = log.firstElementChild!;
const range = document.createRange();
range.selectNodeContents(inner);
const selection = window.getSelection()!;
selection.removeAllRanges();
selection.addRange(range);
return selection.toString();
}
describe("Copying run output", () => {
it("takes blank lines from the buffer, adding no zero-width space", () => {
const { model, log } = setup("first\n\nsecond\n");
expect(model.text()).toBe("first\n\nsecond");
// A blank line is one empty row: it may only contribute its own line break.
expect(selectedText(log).split("\n")).toEqual(["first", "", "second", ""]);
expect(log.textContent).not.toContain("\u200b");
});
it("keeps the zero-width space out of soft-wrapped output too", () => {
const { log } = setup("first\n\nsecond\n", true);
expect(selectedText(log).split("\n")).toEqual(["first", "", "second", ""]);
expect(log.textContent).not.toContain("\u200b");
});
it("still renders a row for every line, blank ones included", () => {
const { model, log } = setup("first\n\n\nsecond\n");
expect(model.lines).toHaveLength(4);
expect(log.querySelectorAll("[data-index]")).toHaveLength(model.lines.length);
expect(log.textContent?.replace(/\n/g, "")).toBe("first" + "second");
});
it("puts the same text on the clipboard when a selection is copied with ⌘C", async () => {
const { log } = setup("first\n\nsecond\n");
// A selection of the rows, not Select All: that path copies the model and would hide a character
// the DOM adds.
const rows = log.querySelectorAll("[data-index]");
const range = document.createRange();
range.setStart(rows[0]!, 0);
range.setEnd(rows[2]!, rows[2]!.childNodes.length);
const selection = window.getSelection()!;
selection.removeAllRanges();
selection.addRange(range);
fireEvent.keyDown(log, { key: "c", code: "KeyC", metaKey: true });
await waitFor(() => expect(clipboard.writeText).toHaveBeenCalledTimes(1));
// Three rows, each ending in its own break: the blank one contributes nothing else.
expect(clipboard.writeText.mock.calls[0]![0]).toBe("first\n\nsecond\n");
});
});
+2
Переглянути файл
@@ -287,6 +287,8 @@ describe("catalogs", () => {
"CMake",
"f-string",
"Tailwind CSS",
"Bun",
"bun-dap (Bun)",
]);
const translated = SUPPORTED_LOCALES.filter((locale) => locale !== "en").sort();
+3 -1
Переглянути файл
@@ -1344,5 +1344,7 @@
"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."
"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"
}
+3 -1
Переглянути файл
@@ -7731,5 +7731,7 @@
"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 语言服务器。编辑器中的问题仍来自语言服务器。"
"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": "插件索引更新失败,详见插件通知"
}
+12 -5
Переглянути файл
@@ -1825,16 +1825,23 @@ 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. */
const documentIndexSync = new Set<(path: string) => Promise<void>>();
export function registerDocumentIndexSync(sync: (path: string) => Promise<void>): () => void {
/**
* 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 {
documentIndexSync.add(sync);
return () => {
documentIndexSync.delete(sync);
};
}
export async function synchronizeDocumentIndexes(path: string): Promise<void> {
await Promise.all([...documentIndexSync].map((sync) => sync(path)));
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);
}
/** Font zoom for non-CodeMirror surfaces, owned and released by their features. */