дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
feat(editor): Copy Reference copies path:line unless the caret is on a declaration's name (#1507)
Copy Reference used the outline symbols enclosing the caret, so anywhere inside a method it copied the method's qualified name and the line was lost. It now names a symbol only with the caret on a declaration's name (its selection range) and copies `path:line` everywhere else, for every language. The `package …;` symbol jdtls reports is no longer qualified, so that line copies `path:1`. Co-authored-by: jaw52 <[email protected]>
Цей коміт міститься в:
4 files changed
+53
-18
No files matched your search
@@ -63,7 +63,7 @@ import { cloneCaretAbove, cloneCaretBelow } from "./cm/verticalMotion";
|
||||
import { pasteAsPlainText, pasteFromHistory, unwrapAtCaret } from "./extras";
|
||||
import { cyclicExpandWord } from "./wordCompletion";
|
||||
import { qualifiedReference } from "./copyReference";
|
||||
import { symbolChain } from "./view/outline";
|
||||
import { declarationChain } from "./view/outline";
|
||||
import { recordClipboard } from "./clipboardHistory";
|
||||
import { useDocMeta, useOutline } from "./model/document";
|
||||
import { fontZoom, setFontZoom, stepFontZoom } from "./cm/prefs";
|
||||
@@ -414,12 +414,14 @@ function copyReference(view: EditorView, slot: EditorSlot) {
|
||||
const line = state.doc.lineAt(head);
|
||||
const meta = slot.model.getMeta();
|
||||
const outline = useOutline.getState()[slot.model.path] ?? [];
|
||||
// A package is not a member to qualify: jdtls lists the whole `package …;` statement as one.
|
||||
const chain = declarationChain(outline, line.number - 1, head - line.from).filter((i) => i.kind !== "package");
|
||||
const reference = qualifiedReference({
|
||||
language: meta.language,
|
||||
path: slot.model.path,
|
||||
root: useProject.getState().project?.root ?? null,
|
||||
text: state.doc.toString(),
|
||||
chain: symbolChain(outline, line.number - 1, head - line.from).map((i) => i.name),
|
||||
chain: chain.map((i) => i.name),
|
||||
line: line.number - 1,
|
||||
});
|
||||
recordClipboard(reference);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Edit › Copy Reference (⌥⇧⌘C): the fully qualified name of the symbol at
|
||||
* the caret, built from the file's outline (symbols enclosing the caret) and
|
||||
* the language's module conventions; `path:line` where there is no symbol.
|
||||
* Edit › Copy Reference (⌥⇧⌘C): with the caret on a declaration's name, its
|
||||
* fully qualified name, built from the file's outline and the language's module
|
||||
* conventions; anywhere else `path:line`, as in IDEA.
|
||||
*/
|
||||
|
||||
export interface ReferenceInput {
|
||||
@@ -11,7 +11,7 @@ export interface ReferenceInput {
|
||||
/** Project root, for project-relative paths and module names. */
|
||||
root: string | null;
|
||||
text: string;
|
||||
/** Names of the outline symbols enclosing the caret, outermost first. */
|
||||
/** Names of the outline symbols down to the one named at the caret, outermost first; empty off a name. */
|
||||
chain: string[];
|
||||
/** 0-based caret line. */
|
||||
line: number;
|
||||
@@ -59,42 +59,37 @@ const declared = (text: string, re: RegExp) => re.exec(text)?.[1] ?? null;
|
||||
export function qualifiedReference(r: ReferenceInput): string {
|
||||
const fallback = `${relativePath(r.path, r.root)}:${r.line + 1}`;
|
||||
const chain = r.chain.filter(Boolean);
|
||||
if (!chain.length) return fallback;
|
||||
switch (r.language) {
|
||||
case "java":
|
||||
case "groovy": {
|
||||
if (!chain.length) return fallback;
|
||||
const pkg = declared(r.text, /^\s*package\s+([\w.]+)\s*;?/m);
|
||||
const [type, ...rest] = chain;
|
||||
const typePath = [pkg, type].filter(Boolean).join(".");
|
||||
// `pkg.Outer.Inner#member`: the last symbol is the member unless the caret is in a type only.
|
||||
// `pkg.Outer.Inner#member`: the last symbol is the member unless the caret is on a type's name.
|
||||
return rest.length ? `${[typePath, ...rest.slice(0, -1)].join(".")}#${rest[rest.length - 1]}` : typePath;
|
||||
}
|
||||
case "kotlin":
|
||||
case "scala": {
|
||||
if (!chain.length) return fallback;
|
||||
const pkg = declared(r.text, /^\s*package\s+([\w.]+)/m);
|
||||
return [pkg, ...chain].filter(Boolean).join(".");
|
||||
}
|
||||
case "csharp": {
|
||||
if (!chain.length) return fallback;
|
||||
case "csharp":
|
||||
return chain.join(".");
|
||||
}
|
||||
case "rust": {
|
||||
if (!chain.length) return rustModulePath(r.path);
|
||||
case "rust":
|
||||
return [rustModulePath(r.path), ...chain.map((n) => (n.startsWith("impl") ? rustImplType(n) : n))].join("::");
|
||||
}
|
||||
case "python":
|
||||
return [pythonModulePath(r.path, r.root), ...chain].join(".");
|
||||
case "go": {
|
||||
const pkg = declared(r.text, /^\s*package\s+(\w+)/m) ?? stem(r.path);
|
||||
return chain.length ? [pkg, ...chain].join(".") : fallback;
|
||||
return [pkg, ...chain].join(".");
|
||||
}
|
||||
case "cpp":
|
||||
case "c":
|
||||
case "objective-cpp":
|
||||
case "php":
|
||||
return chain.length ? chain.join("::") : fallback;
|
||||
return chain.join("::");
|
||||
default:
|
||||
return chain.length ? `${relativePath(r.path, r.root)}:${chain.join(".")}` : fallback;
|
||||
return `${relativePath(r.path, r.root)}:${chain.join(".")}`;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,9 @@ import { describe, expect, it } from "vitest";
|
||||
import { EditorState } from "@codemirror/state";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import { cyclicExpandWord, expandCandidates, prefixStart } from "./wordCompletion";
|
||||
import type { OutlineItem } from "@/platform/ipc";
|
||||
import { pythonModulePath, qualifiedReference, rustImplType, rustModulePath } from "./copyReference";
|
||||
import { declarationChain } from "./view/outline";
|
||||
|
||||
describe("cyclic expand word", () => {
|
||||
const text = "alphaOne x alphaTwo y al| alphaThree alphaOne";
|
||||
@@ -101,5 +103,33 @@ describe("copy reference", () => {
|
||||
expect(
|
||||
qualifiedReference({ ...base, root: null, language: "markdown", path: "/x/a.md", text: "", chain: [] }),
|
||||
).toBe("/x/a.md:5");
|
||||
expect(qualifiedReference({ ...base, language: "python", path: "/p/pkg/mod.py", text: "", chain: [] })).toBe(
|
||||
"pkg/mod.py:5",
|
||||
);
|
||||
expect(qualifiedReference({ ...base, language: "rust", path: "/p/src/a.rs", text: "", chain: [] })).toBe(
|
||||
"src/a.rs:5",
|
||||
);
|
||||
});
|
||||
|
||||
it("names a symbol only with the caret on its name, like IDEA", () => {
|
||||
const item = (name: string, range: number[], sel: number[], children: OutlineItem[] = []): OutlineItem => ({
|
||||
name,
|
||||
detail: null,
|
||||
kind: "class",
|
||||
range: { start: { line: range[0]!, col: range[1]! }, end: { line: range[2]!, col: range[3]! } },
|
||||
selection_range: { start: { line: sel[0]!, col: sel[1]! }, end: { line: sel[2]!, col: sel[3]! } },
|
||||
children,
|
||||
});
|
||||
// class Book { line 0, name at cols 6-10
|
||||
// void sum() { line 1, name at cols 7-10
|
||||
// total += 1; line 2
|
||||
const outline = [item("Book", [0, 0, 4, 1], [0, 6, 0, 10], [item("sum", [1, 2, 3, 3], [1, 7, 1, 10])])];
|
||||
const names = (line: number, col: number) => declarationChain(outline, line, col).map((i) => i.name);
|
||||
expect(names(1, 8)).toEqual(["Book", "sum"]);
|
||||
expect(names(1, 10)).toEqual(["Book", "sum"]);
|
||||
expect(names(0, 6)).toEqual(["Book"]);
|
||||
expect(names(2, 6)).toEqual([]);
|
||||
expect(names(1, 2)).toEqual([]);
|
||||
expect(names(5, 0)).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -14,3 +14,11 @@ export function symbolChain(items: readonly OutlineItem[], line: number, col: nu
|
||||
level = hit.children;
|
||||
}
|
||||
}
|
||||
|
||||
/** The chain up to the innermost item whose name (selection range) holds (line, col); empty off every name. */
|
||||
export function declarationChain(items: readonly OutlineItem[], line: number, col: number): OutlineItem[] {
|
||||
const pos = { line, col };
|
||||
const chain = symbolChain(items, line, col);
|
||||
const named = chain.findLastIndex((i) => before(i.selection_range.start, pos) && before(pos, i.selection_range.end));
|
||||
return chain.slice(0, named + 1);
|
||||
}
|
||||
Reference in new issue
Заблокувати користувача