diff --git a/src/editor/lsp/Popups.module.css b/src/editor/lsp/Popups.module.css index ac4077ea4..ca8cf7c3a 100644 --- a/src/editor/lsp/Popups.module.css +++ b/src/editor/lsp/Popups.module.css @@ -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; diff --git a/src/editor/lsp/codeLens.test.ts b/src/editor/lsp/codeLens.test.ts new file mode 100644 index 000000000..d9f450437 --- /dev/null +++ b/src/editor/lsp/codeLens.test.ts @@ -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", 3), + ]), + ]; + expect(lensTargets(items).map((t) => t.name)).toEqual(["Cart", "count"]); + }); +}); diff --git a/src/editor/lsp/codeLens.ts b/src/editor/lsp/codeLens.ts index abcfb84fe..70a33266a 100644 --- a/src/editor/lsp/codeLens.ts +++ b/src/editor/lsp/codeLens.ts @@ -40,12 +40,15 @@ export interface LensTarget { col: number; } +/** Servers name callbacks and anonymous functions (``, `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); } }; diff --git a/src/features/debugger/__tests__/smartStep.test.ts b/src/features/debugger/__tests__/smartStep.test.ts index dc6c7022e..b2ef44085 100644 --- a/src/features/debugger/__tests__/smartStep.test.ts +++ b/src/features/debugger/__tests__/smartStep.test.ts @@ -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", () => { diff --git a/src/features/debugger/actions.ts b/src/features/debugger/actions.ts index 6f3a0eba9..f70c73c73 100644 --- a/src/features/debugger/actions.ts +++ b/src/features/debugger/actions.ts @@ -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] diff --git a/src/features/debugger/editor/smartStep.ts b/src/features/debugger/editor/smartStep.ts index 512aa8147..bd149f125 100644 --- a/src/features/debugger/editor/smartStep.ts +++ b/src/features/debugger/editor/smartStep.ts @@ -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(`(? 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();