fix: Limit Smart Step Into to the paused line; drop usages hints on callbacks; show full intention titles

Adapters that list every remaining call in the function (debugpy) made Smart
Step Into fall back to a chooser that offered calls from later lines. Targets
are now narrowed to the calls on the paused line, so they are picked in place.

Anonymous functions and callbacks no longer get a 'no usages' hint, and
intention titles are no longer cut short when there is room for them.
Цей коміт міститься в:
l0ng-ai committed 2026-10-25 09:34:37 +08:00
1 джерело d41daa8721
коміт 697a9b0ccb
6 files changed
+69 -7

No files matched your search

+6 -2
Переглянути файл
@@ -108,11 +108,15 @@
color: var(--fg-faint);
}
.label {
flex: none;
max-width: 60%;
flex: 0 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.label:has(+ .preview) {
flex: none;
max-width: 60%;
}
.preview {
min-width: 80px;
flex: 0 1 auto;
+20
Переглянути файл
@@ -0,0 +1,20 @@
import { describe, expect, it } from "vitest";
import type { OutlineItem } from "@/platform/ipc";
import { lensTargets } from "./codeLens";
const item = (name: string, kind: OutlineItem["kind"], line: number, children: OutlineItem[] = []): OutlineItem => {
const r = { start: { line, col: 0 }, end: { line, col: 1 } };
return { name, kind, detail: null, range: r, selection_range: r, children };
};
describe("usages lens targets", () => {
it("skips callbacks and anonymous functions", () => {
const items = [
item("Cart", "class", 0, [
item("count", "method", 1, [item("reduce() callback", "function", 2)]),
item("<function>", "function", 3),
]),
];
expect(lensTargets(items).map((t) => t.name)).toEqual(["Cart", "count"]);
});
});
+4 -1
Переглянути файл
@@ -40,12 +40,15 @@ export interface LensTarget {
col: number;
}
/** Servers name callbacks and anonymous functions (`<function>`, `map() callback`); they have no usages. */
const anonymous = (name: string) => /^<.*>$/.test(name) || / callback$/.test(name);
/** Declarations worth a usages lens, in document order. Exported for tests. */
export function lensTargets(items: readonly OutlineItem[]): LensTarget[] {
const out: LensTarget[] = [];
const walk = (list: readonly OutlineItem[]) => {
for (const it of list) {
if (LENS_KINDS.has(it.kind)) out.push({ name: it.name, line: it.selection_range.start.line, col: it.selection_range.start.col });
if (LENS_KINDS.has(it.kind) && !anonymous(it.name)) out.push({ name: it.name, line: it.selection_range.start.line, col: it.selection_range.start.col });
if (it.children.length) walk(it.children);
}
};
+9
Переглянути файл
@@ -10,6 +10,7 @@ import {
locateStepTargets,
smartStepTargets,
startInlineSmartStep,
targetsOnLine,
} from "../editor/smartStep";
const ipc = vi.hoisted(() => ({ debugStepInTarget: vi.fn() }));
@@ -51,6 +52,14 @@ describe("locating Smart Step Into targets", () => {
]);
expect(locateStepTargets(line, 3, [target(1, "missing()")])).toBeNull();
});
it("keeps only the paused line's calls when the adapter lists the whole function", () => {
const line = " total = add(total, square(i))";
const all = [target(0, "square(i)"), target(1, "add(total, square(i))"), target(2, 'print("total", total)')];
expect(targetsOnLine(line, 11, all).map((t) => t.id)).toEqual([0, 1]);
expect(targetsOnLine(line, 11, [{ ...target(5, "print()"), line: 11 }, { ...target(6, "add()"), line: 12 }]).map((t) => t.id)).toEqual([5]);
expect(targetsOnLine("pass", 0, all)).toBe(all);
});
});
describe("picking in the editor", () => {
+2 -2
Переглянути файл
@@ -33,7 +33,7 @@ import {
} from "./state";
import { focusedDebugView } from "./ui/focusedView";
import { expressionAt } from "./editor/valueTooltip";
import { startInlineSmartStep } from "./editor/smartStep";
import { pausedLineTargets, startInlineSmartStep } from "./editor/smartStep";
import {
openBreakpointsDialog,
openConfigChooser,
@@ -92,7 +92,7 @@ async function smartStepInto() {
await call(commands.debugStep(t.s.id, t.thread, "into"));
return;
}
const targets = await call(commands.debugStepInTargets(t.s.id, frame.id));
const targets = pausedLineTargets(await call(commands.debugStepInTargets(t.s.id, frame.id)));
if (targets.length <= 1) {
await call(
targets[0]
+28 -2
Переглянути файл
@@ -23,6 +23,11 @@ export function calleeName(label: string): string | null {
return name && [...name].every((c) => IDENT.test(c)) ? name : null;
}
/** A call of `name`: `name(` not preceded by an identifier character. */
function callPattern(name: string, flags: string): RegExp {
return new RegExp(`(?<![\\p{L}\\p{N}_$])${name.replace(/\$/g, "\\$")}\\s*\\(`, flags);
}
function identifierEnd(text: string, from: number): number {
let i = from;
while (i < text.length && IDENT.test(text[i]!)) i++;
@@ -46,8 +51,7 @@ export function locateStepTargets(lineText: string, line: number, targets: StepI
} else {
const name = calleeName(t.label);
if (!name) return null;
const re = new RegExp(`(?<![\\p{L}\\p{N}_$])${name.replace(/\$/g, "\\$")}\\s*\\(`, "gu");
for (const m of lineText.matchAll(re)) {
for (const m of lineText.matchAll(callPattern(name, "gu"))) {
if (used.has(m.index!)) continue;
from = m.index!;
to = from + name.length;
@@ -61,6 +65,28 @@ export function locateStepTargets(lineText: string, line: number, targets: StepI
return out.sort((a, b) => a.from - b.from);
}
/**
* Targets that belong to the paused line. Some adapters (debugpy) list every
* call left in the function without positions; those are kept only when their
* callee appears on the line. Unchanged when nothing would remain.
*/
export function targetsOnLine(lineText: string, line: number, targets: StepInTarget[]): StepInTarget[] {
const kept = targets.filter((t) => {
if (t.line != null) return t.line === line;
if (t.column != null) return true;
const name = calleeName(t.label);
return !name || callPattern(name, "u").test(lineText);
});
return kept.length ? kept : targets;
}
/** Targets of the paused line, when that line is open in an editor. */
export function pausedLineTargets(targets: StepInTarget[]): StepInTarget[] {
const { path, line } = useExecutionPoint.getState();
const doc = path ? editorShowing(path)?.view.state.doc : null;
return doc && line < doc.lines ? targetsOnLine(doc.line(line + 1).text, line, targets) : targets;
}
/** Editor to pick in: the active one when it shows `path`, else any showing it. */
function editorShowing(path: string): TextEditorHandle | null {
const active = activeEditor();