Skip to content
20 changes: 20 additions & 0 deletions apps/web/src/components/CommandPalette.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,26 @@ describe("filterPinnedBrowseEntries", () => {
visibleEntries: windowsEntries,
exactEntry: windowsEntries[0],
});
const decomposed = { name: "De\u0301veloppement", fullPath: "/Users/me/De\u0301veloppement" };
expect(
filterPinnedBrowseEntries({
browseEntries: [decomposed],
filterQuery: "D\u00e9veloppement",
pinnedDirectoryName: "repo",
caseSensitive: true,
}).exactEntry,
).toBe(decomposed);
expect(
filterPinnedBrowseEntries({
browseEntries: [
{ name: "\u212B", fullPath: "/home/me/\u212B" },
{ name: "A\u030A", fullPath: "/home/me/A\u030A" },
],
filterQuery: "\u00C5",
pinnedDirectoryName: "repo",
caseSensitive: true,
}).exactEntry,
).toBeNull();
});
});

Expand Down
7 changes: 6 additions & 1 deletion apps/web/src/components/CommandPalette.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -749,9 +749,14 @@ export function filterPinnedBrowseEntries(input: {
? ""
: input.filterQuery;
const { visibleEntries } = filterFilesystemBrowseEntries(input.browseEntries, visibleFilterQuery);
const normalizedFilterQuery = input.filterQuery.normalize("NFC");
const normalizedMatches = input.browseEntries.filter((entry) =>
namesMatch(entry.name.normalize("NFC"), normalizedFilterQuery),
);
const exactEntry =
input.filterQuery.length > 0
? (input.browseEntries.find((entry) => namesMatch(entry.name, input.filterQuery)) ?? null)
? (input.browseEntries.find((entry) => namesMatch(entry.name, input.filterQuery)) ??
(normalizedMatches.length === 1 ? normalizedMatches[0] : null))
: null;
return { visibleEntries, exactEntry };
Comment thread
saphid marked this conversation as resolved.
Resolved
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}
Expand Down
22 changes: 22 additions & 0 deletions packages/client-runtime/src/state/filesystem.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { AuthFilesystemReadScope, AuthOrchestrationReadScope } from "@t3tools/co

import {
createBrowseNavigationCoordinator,
filterFilesystemBrowseEntries,
getFilesystemBrowsePath,
getFilesystemBrowseTabCompletion,
resolveFilesystemReadAccess,
Expand Down Expand Up @@ -107,6 +108,27 @@ describe("filesystem browse model", () => {
});
expect(getFilesystemBrowseTabCompletion([], "x")).toBeNull();
});

it("matches folder names stored decomposed, as macOS Finder names them", () => {
const entry = { name: "De\u0301veloppement", fullPath: "/Users/me/De\u0301veloppement" };
expect(filterFilesystemBrowseEntries([entry], "d\u00e9v")).toEqual({
visibleEntries: [entry],
exactEntry: null,
});
expect(filterFilesystemBrowseEntries([entry], "De").visibleEntries).toEqual([entry]);
expect(filterFilesystemBrowseEntries([entry], "D\u00e9veloppement").exactEntry).toBe(entry);
// Linux keeps both spellings as separate folders; the one typed exactly wins.
const composed = { name: "D\u00e9veloppement", fullPath: "/home/me/D\u00e9veloppement" };
expect(filterFilesystemBrowseEntries([entry, composed], "D\u00e9veloppement").exactEntry).toBe(
composed,
);
// Two spellings that both normalize to the query are ambiguous; neither is picked.
const angstroms = [
{ name: "\u212B", fullPath: "/home/me/\u212B" },
{ name: "A\u030A", fullPath: "/home/me/A\u030A" },
];
expect(filterFilesystemBrowseEntries(angstroms, "\u00C5").exactEntry).toBeNull();
});
});

describe("browse navigation", () => {
Expand Down
14 changes: 12 additions & 2 deletions packages/client-runtime/src/state/filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,25 @@ export function filterFilesystemBrowseEntries(
entries: ReadonlyArray<FilesystemBrowseEntry>,
query: string,
) {
// macOS Finder stores names decomposed (NFD), while typed text is composed (NFC).
const normalizedQuery = query.normalize("NFC");
const lowerQuery = query.toLowerCase();
const lowerNormalizedQuery = normalizedQuery.toLowerCase();
const showHidden = query.startsWith(".");
const visibleEntries = entries.filter(
(entry) =>
entry.name.toLowerCase().startsWith(lowerQuery) &&
(entry.name.toLowerCase().startsWith(lowerQuery) ||
entry.name.normalize("NFC").toLowerCase().startsWith(lowerNormalizedQuery)) &&
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
(showHidden || !entry.name.startsWith(".")),
);
const normalizedMatches = visibleEntries.filter(
(entry) => entry.name.normalize("NFC") === normalizedQuery,
);
const exactEntry =
query.length > 0 ? (visibleEntries.find((entry) => entry.name === query) ?? null) : null;
query.length > 0
? (visibleEntries.find((entry) => entry.name === query) ??
(normalizedMatches.length === 1 ? normalizedMatches[0] : null))
: null;

return { visibleEntries, exactEntry };
}
Expand Down
Loading