mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-25 18:52:39 +00:00
fix(app): spell out a result's path when its label cannot
A row shows the file name and its nearest parent, which is not always enough: packages/app/src/utils/index.ts and packages/server/src/utils/index.ts render identically. Pointing at a row, or selecting it, now replaces the elided parent with every directory above the file, so a pointer user reads the exact workspace-relative path without the preview getting a header back. At rest the column stays scannable. The accessible name was already exact and is unchanged. The app's Tooltip was tried first and cannot serve this surface: it portals into the shared overlay root, which is pinned to the browser/overlay plane at z-index 1, while the Command Center renders through React Native Web's Modal at z-index 9999. The tooltip mounts, measures and reports itself visible, but elementFromPoint at its own coordinates returns the row behind it, so a reader never sees it. Every other desktop overlay in the app portals into the overlay root instead of using RN's Modal, which is what docs/floating-panels.md prescribes; moving the Command Center onto it would fix this for every scope and is a larger change than one row affordance.
This commit is contained in:
@@ -165,3 +165,18 @@ test("opening the same occurrence again navigates the pane it is already showing
|
||||
}) => {
|
||||
await reopenSameOccurrenceAfterMovingAway(page, await withWorkspace());
|
||||
});
|
||||
|
||||
import { revealExactPathOnHover } from "../support/helpers/workspace-content-search";
|
||||
|
||||
test("pointing at a result reveals its exact path when the row label cannot", async ({
|
||||
page,
|
||||
withWorkspace,
|
||||
}, testInfo) => {
|
||||
const { panel, rows } = await revealExactPathOnHover(page, await withWorkspace());
|
||||
await rows.last().hover();
|
||||
await panel.screenshot({ path: testInfo.outputPath("hover-exact-path.png") });
|
||||
// Pointing must not disturb the keyboard selection or the Enter contract.
|
||||
await page.keyboard.press("Enter");
|
||||
await expect(panel).toBeHidden();
|
||||
await expect(page.getByTestId("file-source-editor")).toContainText("SHARED_UTIL");
|
||||
});
|
||||
|
||||
@@ -241,3 +241,38 @@ async function openReopenTarget(page: Page) {
|
||||
await page.keyboard.press("Enter");
|
||||
await expect(panel).toBeHidden();
|
||||
}
|
||||
|
||||
/**
|
||||
* Two files can share a base name and their nearest parent, so the resting row label cannot name
|
||||
* them. Pointing at a row, or selecting it, spells the exact workspace-relative path out.
|
||||
*/
|
||||
export async function revealExactPathOnHover(page: Page, workspace: CreatedWorkspace) {
|
||||
for (const relative of [
|
||||
"packages/app/src/utils/index.ts",
|
||||
"packages/server/src/utils/index.ts",
|
||||
]) {
|
||||
const target = path.join(workspace.repoPath, relative);
|
||||
await mkdir(path.dirname(target), { recursive: true });
|
||||
await writeFile(target, `export const shared = "SHARED_UTIL";\n`);
|
||||
}
|
||||
await workspace.navigateTo();
|
||||
await page.keyboard.press("Meta+Shift+F");
|
||||
await page
|
||||
.getByRole("textbox", { name: "Search saved file contents...", exact: true })
|
||||
.fill("SHARED_UTIL");
|
||||
|
||||
const panel = page.getByTestId("command-center-panel");
|
||||
const rows = panel.getByRole("button", { name: /index\.ts:1:\d+/ });
|
||||
await expect(rows).toHaveCount(2);
|
||||
// The accessible name always carries the exact path.
|
||||
await expect(rows.first()).toHaveAccessibleName(/^packages\/app\/src\/utils\/index\.ts:1:24 /);
|
||||
await expect(rows.last()).toHaveAccessibleName(/^packages\/server\/src\/utils\/index\.ts:1:24 /);
|
||||
// The second row is at rest and shows only its nearest parent, which both files share.
|
||||
await expect(rows.last()).toContainText("…/utils/index.ts:1:24");
|
||||
|
||||
await rows.last().hover();
|
||||
await expect(rows.last()).toContainText("packages/server/src/utils/index.ts:1:24");
|
||||
await rows.first().hover();
|
||||
await expect(rows.first()).toContainText("packages/app/src/utils/index.ts:1:24");
|
||||
return { panel, rows };
|
||||
}
|
||||
|
||||
@@ -232,32 +232,44 @@ function ResultRow({
|
||||
);
|
||||
const line = useMemo(() => describeMatchLine(item), [item]);
|
||||
const file = useMemo(() => describeResultPath(item.path), [item.path]);
|
||||
const location = `${item.path}:${item.line}:${item.columnStart}`;
|
||||
return (
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`${item.path}:${item.line}:${item.columnStart} ${item.snippet}`}
|
||||
accessibilityLabel={`${location} ${item.snippet}`}
|
||||
accessibilityState={accessibilityState}
|
||||
aria-pressed={isWeb ? selected : undefined}
|
||||
onPress={onPress}
|
||||
style={style}
|
||||
>
|
||||
<Text style={styles.snippet} numberOfLines={1}>
|
||||
{line.before}
|
||||
<Text style={styles.match}>{line.match}</Text>
|
||||
{line.after}
|
||||
</Text>
|
||||
{/* The file name and its coordinates are pinned; only the parent context may shrink, so a
|
||||
deep path loses its middle rather than the identity the reader is scanning for. */}
|
||||
<View style={styles.location}>
|
||||
{file.directory ? (
|
||||
<Text style={styles.locationDirectory} numberOfLines={1}>
|
||||
{file.directory}
|
||||
</Text>
|
||||
) : null}
|
||||
<Text style={styles.locationFile} numberOfLines={1}>
|
||||
{file.name}:{item.line}:{item.columnStart}
|
||||
</Text>
|
||||
</View>
|
||||
{({ hovered }: PressableStateCallbackType & { hovered?: boolean }) => {
|
||||
// Pointing at a row, or selecting it, spells every parent out; at rest the row shows only
|
||||
// the nearest one so the column stays scannable.
|
||||
const parents = hovered || selected ? file.head : file.directory;
|
||||
return (
|
||||
<>
|
||||
<Text style={styles.snippet} numberOfLines={1}>
|
||||
{line.before}
|
||||
<Text style={styles.match}>{line.match}</Text>
|
||||
{line.after}
|
||||
</Text>
|
||||
{/* The name and coordinates are pinned; only the parent context may shrink, so a deep
|
||||
path loses its middle rather than the identity the reader is scanning for. Two files
|
||||
can share a name and their nearest parent, so pointing at a row spells the rest of
|
||||
the path out. */}
|
||||
<View style={styles.location}>
|
||||
{parents ? (
|
||||
<Text style={styles.locationDirectory} numberOfLines={1}>
|
||||
{parents}
|
||||
</Text>
|
||||
) : null}
|
||||
<Text style={styles.locationFile} numberOfLines={1}>
|
||||
{file.name}:{item.line}:{item.columnStart}
|
||||
</Text>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
@@ -472,6 +484,11 @@ const styles = StyleSheet.create((theme) => ({
|
||||
},
|
||||
muted: { fontSize: theme.fontSize.sm, color: theme.colors.foregroundMuted },
|
||||
error: { color: theme.colors.statusDanger },
|
||||
tooltipPath: {
|
||||
color: theme.colors.foreground,
|
||||
fontSize: theme.fontSize.sm,
|
||||
fontFamily: theme.fontFamily.mono,
|
||||
},
|
||||
empty: {
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
paddingVertical: theme.spacing[6],
|
||||
|
||||
+19
-3
@@ -2,13 +2,24 @@ import { describe, expect, it } from "vitest";
|
||||
import { describeResultPath } from "./result-path";
|
||||
|
||||
describe("describeResultPath", () => {
|
||||
it("spells out every parent for the row being pointed at", () => {
|
||||
// Two files can share a name and their nearest parent; only the head tells them apart.
|
||||
expect(describeResultPath("packages/app/src/utils/index.ts").head).toBe(
|
||||
"packages/app/src/utils/",
|
||||
);
|
||||
expect(describeResultPath("packages/server/src/utils/index.ts").head).toBe(
|
||||
"packages/server/src/utils/",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps a root file whole", () => {
|
||||
expect(describeResultPath("README.md")).toEqual({ directory: "", name: "README.md" });
|
||||
expect(describeResultPath("README.md")).toEqual({ directory: "", head: "", name: "README.md" });
|
||||
});
|
||||
|
||||
it("keeps a shallow path whole", () => {
|
||||
expect(describeResultPath("docs/architecture.md")).toEqual({
|
||||
directory: "docs/",
|
||||
head: "docs/",
|
||||
name: "architecture.md",
|
||||
});
|
||||
});
|
||||
@@ -17,14 +28,19 @@ describe("describeResultPath", () => {
|
||||
// These two rows are both "index.tsx"; the parents are the only thing telling them apart.
|
||||
expect(
|
||||
describeResultPath("packages/app/src/command-center/workspace-content-search/index.tsx"),
|
||||
).toEqual({ directory: "…/workspace-content-search/", name: "index.tsx" });
|
||||
).toEqual({
|
||||
directory: "…/workspace-content-search/",
|
||||
head: "packages/app/src/command-center/workspace-content-search/",
|
||||
name: "index.tsx",
|
||||
});
|
||||
expect(describeResultPath("packages/app/src/file-pane/source/index.tsx")).toEqual({
|
||||
directory: "…/source/",
|
||||
head: "packages/app/src/file-pane/source/",
|
||||
name: "index.tsx",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps a literal backslash in a file name out of the directory", () => {
|
||||
expect(describeResultPath("a\\b.txt")).toEqual({ directory: "", name: "a\\b.txt" });
|
||||
expect(describeResultPath("a\\b.txt")).toEqual({ directory: "", head: "", name: "a\\b.txt" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
export interface ResultPathParts {
|
||||
/** Directory context immediately above the file, already elided at the head when deeper. */
|
||||
directory: string;
|
||||
/** Every directory above the file, for the row the reader is pointing at or has selected. */
|
||||
head: string;
|
||||
/** File name, which is never truncated away. */
|
||||
name: string;
|
||||
}
|
||||
@@ -18,8 +20,12 @@ const PARENTS_KEPT = 1;
|
||||
export function describeResultPath(path: string): ResultPathParts {
|
||||
const segments = path.split("/");
|
||||
const name = segments.pop() ?? path;
|
||||
if (segments.length === 0) return { directory: "", name };
|
||||
if (segments.length === 0) return { directory: "", head: "", name };
|
||||
const kept = segments.slice(-PARENTS_KEPT);
|
||||
const elided = segments.length > kept.length;
|
||||
return { directory: `${elided ? "…/" : ""}${kept.join("/")}/`, name };
|
||||
return {
|
||||
directory: `${elided ? "…/" : ""}${kept.join("/")}/`,
|
||||
head: `${segments.join("/")}/`,
|
||||
name,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user