дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-12 12:45:39 +02:00
Порівняти коміти
1
Коміти
| Автор | SHA1 | Дата | |
|---|---|---|---|
|
|
4d596c7943 |
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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": "插件索引更新失败,详见插件通知"
|
||||
}
|
||||
@@ -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. */
|
||||
|
||||
Reference in new issue
Заблокувати користувача