Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
27 changes: 27 additions & 0 deletions apps/web/src/filePathDisplay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,31 @@ describe("formatWorkspaceRelativePath", () => {
),
).toBe("t3code/apps/web/src/session-logic.ts:501:9");
});

it("does not format a case-distinct posix sibling as workspace-relative", () => {
expect(
formatWorkspaceRelativePath(
"/tmp/t3code-case-test/project/probe.txt",
"/tmp/t3code-case-test/Project",
),
).toBe("/tmp/t3code-case-test/project/probe.txt");
});

it("formats exact-case posix workspace paths as workspace-relative", () => {
expect(
formatWorkspaceRelativePath(
"/Users/mike/Project/src/session-logic.ts",
"/Users/mike/Project",
),
).toBe("Project/src/session-logic.ts");
});

it("keeps windows workspace formatting case-insensitive", () => {
expect(
formatWorkspaceRelativePath(
"c:/users/mike/dev-stuff/t3code/apps/web/src/session-logic.ts",
"C:/Users/Mike/Dev-Stuff/T3Code",
),
).toBe("T3Code/apps/web/src/session-logic.ts");
});
});
62 changes: 46 additions & 16 deletions apps/web/src/filePathDisplay.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import {
isWindowsAbsolutePath,
normalizePathCaseForComparison,
normalizeProjectPathForComparison,
normalizeProjectPathForDispatch,
} from "@t3tools/shared/path";

import { splitPathAndPosition } from "./terminal-links";

function normalizePathSeparators(path: string): string {
return path.replaceAll("\\", "/");
}

function canonicalizeWindowsDrivePath(path: string): string {
return /^\/[A-Za-z]:\//.test(path) ? path.slice(1) : path;
}

function trimTrailingPathSeparators(path: string): string {
return path.replace(/[\\/]+$/, "");
return /^\/[A-Za-z]:[\\/]/.test(path) ? path.slice(1) : path;
}

function basenameOfPath(path: string): string {
Expand All @@ -21,29 +24,56 @@ function stripRelativePrefixes(path: string): string {
return path.replace(/^\.\/+/, "").replace(/^\/+/, "");
}

export function resolveWorkspaceRelativePath(path: string, workspaceRoot: string): string | null {
Comment thread
AksharP5 marked this conversation as resolved.
const normalizedPath = canonicalizeWindowsDrivePath(path);
const normalizedRoot = canonicalizeWindowsDrivePath(
normalizeProjectPathForDispatch(workspaceRoot),
);
const pathForCompare = normalizePathCaseForComparison(normalizedPath);
const rootForCompare = normalizeProjectPathForComparison(normalizedRoot);

if (pathForCompare === rootForCompare) return "";

const separator = isWindowsAbsolutePath(normalizedRoot) ? "\\" : "/";
const rootPrefix = rootForCompare.endsWith(separator)
? rootForCompare
: `${rootForCompare}${separator}`;
if (!pathForCompare.startsWith(rootPrefix)) return null;

const relativeStart =
normalizedRoot.endsWith("/") || normalizedRoot.endsWith("\\")
? normalizedRoot.length
: normalizedRoot.length + 1;
return normalizePathSeparators(normalizedPath.slice(relativeStart));
}

export function formatWorkspaceRelativePath(
pathWithPosition: string,
workspaceRoot: string | undefined,
): string {
const { path, line, column } = splitPathAndPosition(pathWithPosition);
const normalizedPath = canonicalizeWindowsDrivePath(normalizePathSeparators(path));
const canonicalPath = canonicalizeWindowsDrivePath(path);
const normalizedPath = normalizePathSeparators(canonicalPath);

let displayPath = normalizedPath;
if (workspaceRoot) {
const normalizedWorkspaceRoot = canonicalizeWindowsDrivePath(
normalizePathSeparators(trimTrailingPathSeparators(workspaceRoot)),
const canonicalWorkspaceRoot = canonicalizeWindowsDrivePath(
normalizeProjectPathForDispatch(workspaceRoot),
);
const normalizedWorkspaceRoot = normalizePathSeparators(canonicalWorkspaceRoot);
const workspaceLabel = basenameOfPath(normalizedWorkspaceRoot);
const pathForCompare = normalizedPath.toLowerCase();
const workspaceForCompare = normalizedWorkspaceRoot.toLowerCase();
const workspaceWithSeparator = `${workspaceForCompare}/`;
const workspaceLabelWithSeparator = `${workspaceLabel.toLowerCase()}/`;
const workspaceRelativePath = resolveWorkspaceRelativePath(path, workspaceRoot);
const caseInsensitive = isWindowsAbsolutePath(canonicalWorkspaceRoot);
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Outdated
const pathForCompare = caseInsensitive ? normalizedPath.toLowerCase() : normalizedPath;
const workspaceLabelForCompare = caseInsensitive
? workspaceLabel.toLowerCase()
: workspaceLabel;
const workspaceLabelWithSeparator = `${workspaceLabelForCompare}/`;

if (pathForCompare === workspaceForCompare) {
if (workspaceRelativePath === "") {
displayPath = workspaceLabel;
} else if (pathForCompare.startsWith(workspaceWithSeparator)) {
const relativeSuffix = normalizedPath.slice(normalizedWorkspaceRoot.length + 1);
displayPath = `${workspaceLabel}/${relativeSuffix}`;
} else if (workspaceRelativePath !== null) {
displayPath = `${workspaceLabel}/${workspaceRelativePath}`;
Comment thread
AksharP5 marked this conversation as resolved.
} else if (!normalizedPath.startsWith("/")) {
const relativePath = stripRelativePrefixes(normalizedPath);
displayPath = pathForCompare.startsWith(workspaceLabelWithSeparator)
Expand Down
56 changes: 56 additions & 0 deletions apps/web/src/markdown-links.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,62 @@ describe("resolveMarkdownFileLinkTarget", () => {
});
});

it("keeps posix workspace containment case-sensitive", () => {
expect(
resolveMarkdownFileLinkMeta(
"/tmp/t3code-case-test/project/probe.txt",
"/tmp/t3code-case-test/Project",
),
).toMatchObject({
displayPath: "/tmp/t3code-case-test/project/probe.txt",
workspaceRelativePath: null,
});
});

it("creates preview paths for exact-case posix workspace files", () => {
expect(
resolveMarkdownFileLinkMeta(
"/Users/mike/Project/src/session-logic.ts",
"/Users/mike/Project",
),
).toMatchObject({
displayPath: "Project/src/session-logic.ts",
workspaceRelativePath: "src/session-logic.ts",
});
});

it("preserves trailing whitespace in encoded workspace file paths", () => {
expect(resolveMarkdownFileLinkMeta("/tmp/repo/file.ts%20", "/tmp/repo")).toMatchObject({
targetPath: "/tmp/repo/file.ts ",
displayPath: "repo/file.ts ",
workspaceRelativePath: "file.ts ",
});
});

it("keeps windows workspace containment case-insensitive", () => {
expect(
resolveMarkdownFileLinkMeta(
"c:/users/mike/dev-stuff/t3code/apps/web/src/session-logic.ts",
"C:/Users/Mike/Dev-Stuff/T3Code",
),
).toMatchObject({
displayPath: "T3Code/apps/web/src/session-logic.ts",
workspaceRelativePath: "apps/web/src/session-logic.ts",
});
});

it("keeps unc workspace containment case-insensitive", () => {
expect(
resolveMarkdownFileLinkMeta(
"\\\\SERVER\\Share\\Project\\src\\session-logic.ts",
"\\\\server\\share\\project",
),
).toMatchObject({
displayPath: "project/src/session-logic.ts",
workspaceRelativePath: "src/session-logic.ts",
});
});

it("normalizes slash-prefixed windows drive paths before resolving", () => {
expect(
resolveMarkdownFileLinkTarget(
Expand Down
17 changes: 2 additions & 15 deletions apps/web/src/markdown-links.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { formatWorkspaceRelativePath } from "./filePathDisplay";
import { formatWorkspaceRelativePath, resolveWorkspaceRelativePath } from "./filePathDisplay";
import { resolvePathLinkTarget, splitPathAndPosition } from "./terminal-links";

const WINDOWS_DRIVE_PATH_PATTERN = /^[A-Za-z]:[\\/]/;
Expand Down Expand Up @@ -363,19 +363,6 @@ function basenameOfPath(path: string): string {
return separatorIndex >= 0 ? path.slice(separatorIndex + 1) : path;
}

function workspaceRelativePath(path: string, workspaceRoot: string | undefined): string | null {
if (!workspaceRoot) return null;
const normalizedPath = normalizeWindowsDrivePath(path.replaceAll("\\", "/"));
const normalizedRoot = normalizeWindowsDrivePath(workspaceRoot.replaceAll("\\", "/")).replace(
/\/+$/,
"",
);
const pathForCompare = normalizedPath.toLowerCase();
const rootForCompare = normalizedRoot.toLowerCase();
if (!pathForCompare.startsWith(`${rootForCompare}/`)) return null;
return normalizedPath.slice(normalizedRoot.length + 1);
}

export function resolveMarkdownFileLinkMeta(
href: string | undefined,
cwd?: string,
Expand All @@ -396,7 +383,7 @@ function buildFileLinkMetaFromTarget(targetPath: string, cwd?: string): Markdown
filePath: path,
targetPath,
displayPath: formatWorkspaceRelativePath(targetPath, cwd),
workspaceRelativePath: workspaceRelativePath(path, cwd),
workspaceRelativePath: cwd ? resolveWorkspaceRelativePath(path, cwd) : null,
basename: basenameOfPath(path),
...(lineNumber !== undefined ? { line: lineNumber } : {}),
...(columnNumber !== undefined ? { column: columnNumber } : {}),
Expand Down
6 changes: 6 additions & 0 deletions packages/shared/src/path.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
isUncPath,
isWindowsAbsolutePath,
isWindowsDrivePath,
normalizePathCaseForComparison,
} from "./path.ts";

describe("path helpers", () => {
Expand Down Expand Up @@ -31,4 +32,9 @@ describe("path helpers", () => {
expect(isExplicitRelativePath("..\\repo")).toBe(true);
expect(isExplicitRelativePath("~/repo")).toBe(false);
});

it("normalizes windows path casing without trimming filenames", () => {
expect(normalizePathCaseForComparison("C:/Repo/file.ts ")).toBe("c:\\repo\\file.ts ");
expect(normalizePathCaseForComparison("/repo/file.ts ")).toBe("/repo/file.ts ");
});
});
13 changes: 8 additions & 5 deletions packages/shared/src/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,13 @@ export function normalizeProjectPathForDispatch(value: string): string {
return trimTrailingPathSeparators(value.trim());
}

export function normalizeProjectPathForComparison(value: string): string {
const normalized = normalizeProjectPathForDispatch(value);
if (isWindowsDrivePath(normalized) || isUncPath(normalized)) {
return normalized.replaceAll("/", "\\").toLowerCase();
export function normalizePathCaseForComparison(value: string): string {
if (isWindowsDrivePath(value) || isUncPath(value)) {
return value.replaceAll("/", "\\").toLowerCase();
}
return normalized;
return value;
}

export function normalizeProjectPathForComparison(value: string): string {
return normalizePathCaseForComparison(normalizeProjectPathForDispatch(value));
}
Loading