Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions apps/web/src/components/CommandPalette.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
buildThreadActionItems,
enumerateCommandPaletteItems,
filterCommandPaletteGroups,
resolveBrowseTabCompletion,
type CommandPaletteGroup,
} from "./CommandPalette.logic";

Expand Down Expand Up @@ -38,6 +39,98 @@ describe("enumerateCommandPaletteItems", () => {
const LOCAL_ENVIRONMENT_ID = EnvironmentId.make("environment-local");
const PROJECT_ID = ProjectId.make("project-1");

describe("resolveBrowseTabCompletion", () => {
const entries = [
{ name: "alpha", fullPath: "/workspace/alpha" },
{ name: "alpine", fullPath: "/workspace/alpine" },
];

it("uses the highlighted directory", () => {
expect(
resolveBrowseTabCompletion({
exactEntry: null,
filteredEntries: entries,
highlightedItemValue: "browse:/workspace/alpine",
}),
).toEqual({ kind: "entry", entry: entries[1] });
});

it("uses the first directory when none is highlighted", () => {
expect(
resolveBrowseTabCompletion({
exactEntry: null,
filteredEntries: entries,
highlightedItemValue: null,
}),
).toEqual({ kind: "entry", entry: entries[0] });
});

it("preserves the highlighted parent-directory action", () => {
expect(
resolveBrowseTabCompletion({
exactEntry: null,
filteredEntries: entries,
highlightedItemValue: "browse:up",
}),
).toEqual({ kind: "up" });
});

it("returns null when there are no matching directories", () => {
expect(
resolveBrowseTabCompletion({
exactEntry: null,
filteredEntries: [],
highlightedItemValue: null,
}),
).toBeNull();
});

it("uses the case-sensitive exact entry before the first prefix match", () => {
const caseVariants = [
{ name: "Docs", fullPath: "/workspace/Docs" },
{ name: "docs", fullPath: "/workspace/docs" },
];

expect(
resolveBrowseTabCompletion({
exactEntry: caseVariants[1] ?? null,
filteredEntries: caseVariants,
highlightedItemValue: null,
}),
).toEqual({ kind: "entry", entry: caseVariants[1] });
});

it("keeps a highlighted row ahead of a different exact entry", () => {
const caseVariants = [
{ name: "Docs", fullPath: "/workspace/Docs" },
{ name: "docs", fullPath: "/workspace/docs" },
];

expect(
resolveBrowseTabCompletion({
exactEntry: caseVariants[1] ?? null,
filteredEntries: caseVariants,
highlightedItemValue: "browse:/workspace/Docs",
}),
).toEqual({ kind: "entry", entry: caseVariants[0] });
});

it("uses an exact entry when a stored highlight no longer resolves", () => {
const caseVariants = [
{ name: "Docs", fullPath: "/workspace/Docs" },
{ name: "docs", fullPath: "/workspace/docs" },
];

expect(
resolveBrowseTabCompletion({
exactEntry: caseVariants[1] ?? null,
filteredEntries: caseVariants,
highlightedItemValue: "browse:/workspace/removed",
}),
).toEqual({ kind: "entry", entry: caseVariants[1] });
});
});

function makeThread(overrides: Partial<Thread> = {}): Thread {
return {
id: ThreadId.make("thread-1"),
Expand Down
31 changes: 31 additions & 0 deletions apps/web/src/components/CommandPalette.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,37 @@ export function filterBrowseEntries(input: {
return { filteredEntries, highlightedEntry, exactEntry };
}

export type BrowseTabCompletion =
| { readonly kind: "up" }
| { readonly kind: "entry"; readonly entry: FilesystemBrowseEntry };

export function resolveBrowseTabCompletion(input: {
exactEntry: FilesystemBrowseEntry | null;
filteredEntries: ReadonlyArray<FilesystemBrowseEntry>;
highlightedItemValue: string | null;
}): BrowseTabCompletion | null {
if (input.highlightedItemValue === "browse:up") {
return { kind: "up" };
}

if (input.highlightedItemValue?.startsWith("browse:")) {
const highlightedPath = input.highlightedItemValue.slice("browse:".length);
const highlightedEntry = input.filteredEntries.find(
(entry) => entry.fullPath === highlightedPath,
);
if (highlightedEntry) {
return { kind: "entry", entry: highlightedEntry };
}
}

if (input.exactEntry) {
return { kind: "entry", entry: input.exactEntry };
}
Comment thread
cursor[bot] marked this conversation as resolved.

const firstEntry = input.filteredEntries[0];
return firstEntry ? { kind: "entry", entry: firstEntry } : null;
}

export function normalizeSearchText(value: string): string {
return value.trim().toLowerCase().replace(/\s+/g, " ");
}
Expand Down
24 changes: 24 additions & 0 deletions apps/web/src/components/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ import {
getCommandPaletteMode,
ITEM_ICON_CLASS,
RECENT_THREAD_LIMIT,
resolveBrowseTabCompletion,
} from "./CommandPalette.logic";
import { resolveEnvironmentOptionLabel } from "./BranchToolbar.logic";
import { CommandPaletteResults } from "./CommandPaletteResults";
Expand Down Expand Up @@ -1569,6 +1570,29 @@ function OpenCommandPaletteDialog(props: {
}
}

const isPlainTab =
event.key === "Tab" && !event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey;

if (isBrowsing && isPlainTab) {
event.preventDefault();

if (relativePathNeedsActiveProject || (isBrowsePending && browseResult === null)) {
return;
}
Comment thread
cursor[bot] marked this conversation as resolved.

const completion = resolveBrowseTabCompletion({
exactEntry: exactBrowseEntry,
filteredEntries: filteredBrowseEntries,
highlightedItemValue,
});
if (completion?.kind === "up") {
browseUp();
} else if (completion?.kind === "entry") {
browseTo(completion.entry.name);
Comment thread
colonelpanic8 marked this conversation as resolved.
}
return;
}

if (addProjectCloneFlow?.step === "repository" && event.key === "Enter") {
event.preventDefault();
void submitAddProjectCloneFlow();
Expand Down
Loading