Support line ranges in assistant file links (#1067)
This commit is contained in:
@@ -0,0 +1,697 @@
|
||||
import { isAbsolutePath } from "@/utils/path";
|
||||
|
||||
export interface InlinePathTarget {
|
||||
raw: string;
|
||||
path: string;
|
||||
lineStart?: number;
|
||||
lineEnd?: number;
|
||||
}
|
||||
|
||||
const FILE_PROTOCOL = "file:";
|
||||
const INLINE_LINE_FRAGMENT = /^L([0-9]+)(?:C[0-9]+)?(?:-L?([0-9]+)(?:C[0-9]+)?)?$/i;
|
||||
const INLINE_COLON_LINE_SUFFIX = /^(.+?):([0-9]+)(?::[0-9]+)?(?:-([0-9]+)(?::[0-9]+)?)?$/;
|
||||
const INLINE_PAREN_LINE_SUFFIX = /^(.+?)\(([0-9]+)(?:,[0-9]+)?(?:-([0-9]+)(?:,[0-9]+)?)?\)$/;
|
||||
const INLINE_WORD_LINE_SUFFIX = /^(.+?)\s+lines?\s+([0-9]+)(?:-([0-9]+))?$/i;
|
||||
const ASSISTANT_FILE_EXTENSIONS = new Set([
|
||||
"astro",
|
||||
"bash",
|
||||
"c",
|
||||
"cc",
|
||||
"cjs",
|
||||
"cpp",
|
||||
"cs",
|
||||
"css",
|
||||
"cts",
|
||||
"cxx",
|
||||
"env",
|
||||
"fish",
|
||||
"go",
|
||||
"gql",
|
||||
"gradle",
|
||||
"graphql",
|
||||
"h",
|
||||
"hpp",
|
||||
"htm",
|
||||
"html",
|
||||
"ini",
|
||||
"java",
|
||||
"js",
|
||||
"json",
|
||||
"jsonc",
|
||||
"jsx",
|
||||
"kt",
|
||||
"kts",
|
||||
"less",
|
||||
"lock",
|
||||
"lua",
|
||||
"md",
|
||||
"mdx",
|
||||
"mjs",
|
||||
"mts",
|
||||
"php",
|
||||
"proto",
|
||||
"py",
|
||||
"rb",
|
||||
"rs",
|
||||
"sass",
|
||||
"scss",
|
||||
"sh",
|
||||
"sql",
|
||||
"svelte",
|
||||
"swift",
|
||||
"toml",
|
||||
"ts",
|
||||
"tsx",
|
||||
"txt",
|
||||
"vue",
|
||||
"xml",
|
||||
"yaml",
|
||||
"yml",
|
||||
"zsh",
|
||||
]);
|
||||
|
||||
export interface AssistantHrefParseOptions {
|
||||
workspaceRoot?: string;
|
||||
}
|
||||
|
||||
export type AssistantFileLinkClassification =
|
||||
| {
|
||||
kind: "external";
|
||||
raw: string;
|
||||
}
|
||||
| {
|
||||
kind: "directFile";
|
||||
target: InlinePathTarget;
|
||||
}
|
||||
| {
|
||||
kind: "ambiguousFileCandidate";
|
||||
target: InlinePathTarget;
|
||||
};
|
||||
|
||||
export interface NormalizedInlinePathTarget {
|
||||
directory: string;
|
||||
file?: string;
|
||||
}
|
||||
|
||||
function normalizePathToken(value: string): string | null {
|
||||
const trimmed = value
|
||||
.trim()
|
||||
.replace(/^['"`]/, "")
|
||||
.replace(/['"`]$/, "");
|
||||
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return trimmed.replace(/\\/g, "/");
|
||||
}
|
||||
|
||||
function parseLineFragment(value: string): Pick<InlinePathTarget, "lineStart" | "lineEnd"> | null {
|
||||
const rawFragment = value.startsWith("#") ? value.slice(1) : value;
|
||||
if (!rawFragment) {
|
||||
return { lineStart: undefined, lineEnd: undefined };
|
||||
}
|
||||
|
||||
const lineMatch = rawFragment.match(INLINE_LINE_FRAGMENT);
|
||||
const lineStart = lineMatch?.[1] ? parseInt(lineMatch[1], 10) : undefined;
|
||||
const lineEnd = lineMatch?.[2] ? parseInt(lineMatch[2], 10) : undefined;
|
||||
|
||||
if (
|
||||
(lineStart !== undefined && (!Number.isFinite(lineStart) || lineStart <= 0)) ||
|
||||
(lineEnd !== undefined && (!Number.isFinite(lineEnd) || lineEnd <= 0)) ||
|
||||
(lineStart !== undefined && lineEnd !== undefined && lineEnd < lineStart)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { lineStart, lineEnd };
|
||||
}
|
||||
|
||||
/**
|
||||
* Strict VSCode-style markers only.
|
||||
*
|
||||
* Supported:
|
||||
* - `filename:linenumber`
|
||||
* - `filename:lineStart-lineEnd`
|
||||
*
|
||||
* Not supported (by design):
|
||||
* - plain `filename` (no line)
|
||||
* - `:linenumber` (range-only)
|
||||
*/
|
||||
export function parseInlinePathToken(value: string): InlinePathTarget | null {
|
||||
const rawValue = value ?? "";
|
||||
const trimmed = rawValue.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const match =
|
||||
trimmed.match(INLINE_COLON_LINE_SUFFIX) ??
|
||||
trimmed.match(INLINE_PAREN_LINE_SUFFIX) ??
|
||||
trimmed.match(INLINE_WORD_LINE_SUFFIX);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const basePathRaw = match[1]?.trim();
|
||||
if (!basePathRaw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Avoid accidentally treating URLs as file paths.
|
||||
if (basePathRaw.includes("://")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedPath = normalizePathToken(basePathRaw);
|
||||
if (!normalizedPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lineStart = parseInt(match[2], 10);
|
||||
if (!Number.isFinite(lineStart) || lineStart <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lineEnd = match[3] ? parseInt(match[3], 10) : undefined;
|
||||
if (lineEnd !== undefined) {
|
||||
if (!Number.isFinite(lineEnd) || lineEnd <= 0) {
|
||||
return null;
|
||||
}
|
||||
if (lineEnd < lineStart) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
raw: rawValue,
|
||||
path: normalizedPath,
|
||||
lineStart,
|
||||
lineEnd,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseFileProtocolUrl(value: string): InlinePathTarget | null {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let parsedUrl: URL;
|
||||
try {
|
||||
parsedUrl = new URL(trimmed);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (parsedUrl.protocol !== FILE_PROTOCOL) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedPath = normalizeFileUrlPath(parsedUrl.pathname);
|
||||
if (!normalizedPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lines = parseLineFragment(parsedUrl.hash);
|
||||
if (!lines) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
raw: value,
|
||||
path: normalizedPath,
|
||||
...lines,
|
||||
};
|
||||
}
|
||||
|
||||
function parseAssistantInlinePathLink(
|
||||
value: string,
|
||||
options: AssistantHrefParseOptions,
|
||||
): InlinePathTarget | null {
|
||||
const inlinePathTarget = parseInlinePathToken(value);
|
||||
if (!inlinePathTarget) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedPath = normalizePathToken(inlinePathTarget.path);
|
||||
if (!normalizedPath || !isAbsolutePath(normalizedPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isAllowedAbsolutePath(normalizedPath, options.workspaceRoot)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...inlinePathTarget,
|
||||
path: normalizedPath,
|
||||
};
|
||||
}
|
||||
|
||||
export function classifyAssistantFileLink(
|
||||
value: string,
|
||||
options: AssistantHrefParseOptions = {},
|
||||
): AssistantFileLinkClassification | null {
|
||||
const raw = value ?? "";
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isExternalHref(trimmed)) {
|
||||
return {
|
||||
kind: "external",
|
||||
raw,
|
||||
};
|
||||
}
|
||||
|
||||
const target = parseAssistantFileLink(trimmed, options);
|
||||
if (!target) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isAmbiguousWorkspaceCandidate(trimmed, target, options.workspaceRoot)) {
|
||||
return {
|
||||
kind: "ambiguousFileCandidate",
|
||||
target,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
kind: "directFile",
|
||||
target,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseAssistantFileLink(
|
||||
value: string,
|
||||
options: AssistantHrefParseOptions = {},
|
||||
): InlinePathTarget | null {
|
||||
const fileUrlTarget = parseFileProtocolUrl(value);
|
||||
if (fileUrlTarget) {
|
||||
return fileUrlTarget;
|
||||
}
|
||||
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isExternalHref(trimmed)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const inlinePathTarget = parseAssistantInlinePathLink(trimmed, {
|
||||
workspaceRoot: options.workspaceRoot,
|
||||
});
|
||||
if (inlinePathTarget) {
|
||||
return inlinePathTarget;
|
||||
}
|
||||
|
||||
const windowsPathMatch = trimmed.match(/^([A-Za-z]:[\\/][^?#]*)(#[^?]+)?$/);
|
||||
if (windowsPathMatch) {
|
||||
const normalizedPath = normalizePathToken(windowsPathMatch[1] ?? "");
|
||||
if (!normalizedPath || !isAllowedAbsolutePath(normalizedPath, options.workspaceRoot)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lines = parseLineFragment(windowsPathMatch[2] ?? "");
|
||||
if (!lines) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
raw: value,
|
||||
path: normalizedPath,
|
||||
...lines,
|
||||
};
|
||||
}
|
||||
|
||||
const relativeTarget = parseWorkspaceRelativeFileLink(trimmed, {
|
||||
workspaceRoot: options.workspaceRoot,
|
||||
});
|
||||
if (relativeTarget) {
|
||||
return relativeTarget;
|
||||
}
|
||||
|
||||
if (!isAbsolutePath(trimmed)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let parsedUrl: URL;
|
||||
try {
|
||||
parsedUrl = new URL(trimmed, "http://paseo.invalid");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedPath = normalizePathToken(decodeURIComponent(parsedUrl.pathname));
|
||||
if (!normalizedPath || !isAbsolutePath(normalizedPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isAllowedAbsolutePath(normalizedPath, options.workspaceRoot)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lines = parseLineFragment(parsedUrl.hash);
|
||||
if (!lines) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
raw: value,
|
||||
path: normalizedPath,
|
||||
...lines,
|
||||
};
|
||||
}
|
||||
|
||||
export function isFileLookingAssistantToken(value: string): boolean {
|
||||
const normalized = normalizePathToken(value);
|
||||
if (!normalized || normalized.includes("?") || normalized.includes("://")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const path = getHeuristicLocalPath(normalized);
|
||||
if (!path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isPlausibleAssistantLocalPath(path);
|
||||
}
|
||||
|
||||
function parseWorkspaceRelativeFileLink(
|
||||
value: string,
|
||||
options: AssistantHrefParseOptions,
|
||||
): InlinePathTarget | null {
|
||||
const workspaceRoot = normalizePathInput(options.workspaceRoot);
|
||||
if (!workspaceRoot) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = parseLocalPathParts(value);
|
||||
if (!parsed || isAbsolutePath(parsed.path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedPath = resolveRelativePathUnderRoot(parsed.path, workspaceRoot);
|
||||
if (!normalizedPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
raw: value,
|
||||
path: normalizedPath,
|
||||
...parsed.lines,
|
||||
};
|
||||
}
|
||||
|
||||
function parseLocalPathParts(
|
||||
value: string,
|
||||
): { path: string; lines: Pick<InlinePathTarget, "lineStart" | "lineEnd"> } | null {
|
||||
const normalized = normalizePathToken(value);
|
||||
if (!normalized || normalized.includes("?")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hashIndex = normalized.indexOf("#");
|
||||
const beforeHash = hashIndex >= 0 ? normalized.slice(0, hashIndex) : normalized;
|
||||
const hash = hashIndex >= 0 ? normalized.slice(hashIndex) : "";
|
||||
const fragmentLines = parseLineFragment(hash);
|
||||
if (!fragmentLines) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const inlinePathTarget = parseInlinePathToken(beforeHash);
|
||||
if (inlinePathTarget) {
|
||||
if (!isPlausibleAssistantLocalPath(inlinePathTarget.path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
path: inlinePathTarget.path,
|
||||
lines: {
|
||||
lineStart: inlinePathTarget.lineStart,
|
||||
lineEnd: inlinePathTarget.lineEnd,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (!beforeHash || beforeHash.includes(":")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isPlausibleAssistantLocalPath(beforeHash)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
path: beforeHash,
|
||||
lines: fragmentLines,
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeInlinePathTarget(
|
||||
rawPath: string,
|
||||
cwd?: string,
|
||||
): NormalizedInlinePathTarget | null {
|
||||
if (!rawPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedInput = normalizePathInput(rawPath);
|
||||
if (!normalizedInput) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let normalized = normalizedInput;
|
||||
const cwdRelative = resolvePathAgainstCwd(normalized, cwd);
|
||||
if (cwdRelative) {
|
||||
normalized = cwdRelative;
|
||||
}
|
||||
|
||||
if (normalized.startsWith("./")) {
|
||||
normalized = normalized.slice(2) || ".";
|
||||
}
|
||||
|
||||
if (!normalized.length) {
|
||||
normalized = ".";
|
||||
}
|
||||
|
||||
if (normalized === ".") {
|
||||
return { directory: "." };
|
||||
}
|
||||
|
||||
if (normalized.endsWith("/")) {
|
||||
const dir = normalized.replace(/\/+$/, "");
|
||||
return { directory: dir.length > 0 ? dir : "." };
|
||||
}
|
||||
|
||||
const lastSlash = normalized.lastIndexOf("/");
|
||||
const directory = lastSlash >= 0 ? normalized.slice(0, lastSlash) : ".";
|
||||
|
||||
return {
|
||||
directory: directory.length > 0 ? directory : ".",
|
||||
file: normalized,
|
||||
};
|
||||
}
|
||||
|
||||
function isAllowedAbsolutePath(pathValue: string, workspaceRoot?: string): boolean {
|
||||
const normalizedWorkspaceRoot = normalizePathInput(workspaceRoot);
|
||||
if (!normalizedWorkspaceRoot) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const comparePath = normalizePathForCompare(pathValue);
|
||||
const compareWorkspaceRoot = normalizePathForCompare(
|
||||
normalizedWorkspaceRoot.replace(/\/+$/, "") || "/",
|
||||
);
|
||||
const comparePrefix = compareWorkspaceRoot === "/" ? "/" : `${compareWorkspaceRoot}/`;
|
||||
|
||||
return comparePath === compareWorkspaceRoot || comparePath.startsWith(comparePrefix);
|
||||
}
|
||||
|
||||
function isExternalHref(value: string): boolean {
|
||||
if (value.includes("://")) {
|
||||
return !value.toLowerCase().startsWith(`${FILE_PROTOCOL}//`);
|
||||
}
|
||||
|
||||
const inlinePathTarget = parseInlinePathToken(value);
|
||||
if (inlinePathTarget) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return /^[A-Za-z][A-Za-z0-9+.-]*:/.test(value) && !/^[A-Za-z]:[\\/]/.test(value);
|
||||
}
|
||||
|
||||
function isAmbiguousWorkspaceCandidate(
|
||||
value: string,
|
||||
target: InlinePathTarget,
|
||||
workspaceRoot?: string,
|
||||
): boolean {
|
||||
const normalizedWorkspaceRoot = normalizePathInput(workspaceRoot);
|
||||
if (!normalizedWorkspaceRoot || !isAllowedAbsolutePath(target.path, normalizedWorkspaceRoot)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parsed = parseLocalPathParts(value);
|
||||
if (!parsed || isAbsolutePath(parsed.path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !parsed.path.includes("/");
|
||||
}
|
||||
|
||||
function getHeuristicLocalPath(value: string): string | null {
|
||||
const hashIndex = value.indexOf("#");
|
||||
const beforeHash = hashIndex >= 0 ? value.slice(0, hashIndex) : value;
|
||||
const hash = hashIndex >= 0 ? value.slice(hashIndex) : "";
|
||||
if (!parseLineFragment(hash)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const inlinePathTarget = parseInlinePathToken(beforeHash);
|
||||
if (inlinePathTarget) {
|
||||
return inlinePathTarget.path;
|
||||
}
|
||||
|
||||
if (!beforeHash || beforeHash.includes(":")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return beforeHash;
|
||||
}
|
||||
|
||||
function isPlausibleAssistantLocalPath(pathValue: string): boolean {
|
||||
const normalized = normalizePathToken(pathValue);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isAbsolutePath(normalized)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const explicitRelative =
|
||||
normalized.startsWith("./") || normalized.startsWith("../") || normalized.startsWith("~/");
|
||||
if (explicitRelative) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const segments = normalized.split("/").filter(Boolean);
|
||||
const firstSegment = segments[0];
|
||||
if (!firstSegment) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (segments.length > 1) {
|
||||
return !isDomainLikePathSegment(firstSegment);
|
||||
}
|
||||
|
||||
if (firstSegment.startsWith(".") && firstSegment.length > 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const lastDot = firstSegment.lastIndexOf(".");
|
||||
if (lastDot < 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const extension = firstSegment.slice(lastDot + 1).toLowerCase();
|
||||
return ASSISTANT_FILE_EXTENSIONS.has(extension);
|
||||
}
|
||||
|
||||
function isDomainLikePathSegment(segment: string): boolean {
|
||||
return /^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/.test(segment);
|
||||
}
|
||||
|
||||
function resolveRelativePathUnderRoot(pathValue: string, workspaceRoot: string): string | null {
|
||||
const normalizedPath = normalizePathToken(pathValue);
|
||||
if (!normalizedPath || isAbsolutePath(normalizedPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const root = workspaceRoot.replace(/\/+$/, "") || "/";
|
||||
const pathSegments = normalizedPath.split("/");
|
||||
const resolvedSegments: string[] = [];
|
||||
for (const segment of pathSegments) {
|
||||
if (!segment || segment === ".") {
|
||||
continue;
|
||||
}
|
||||
if (segment === "..") {
|
||||
if (resolvedSegments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
resolvedSegments.pop();
|
||||
continue;
|
||||
}
|
||||
resolvedSegments.push(segment);
|
||||
}
|
||||
|
||||
if (resolvedSegments.length === 0) {
|
||||
return root;
|
||||
}
|
||||
|
||||
return root === "/" ? `/${resolvedSegments.join("/")}` : `${root}/${resolvedSegments.join("/")}`;
|
||||
}
|
||||
|
||||
function normalizeFileUrlPath(pathname: string): string | null {
|
||||
if (!pathname) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const decoded = decodeURIComponent(pathname).replace(/\\/g, "/");
|
||||
if (!decoded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (/^\/[A-Za-z]:\//.test(decoded)) {
|
||||
return decoded.slice(1);
|
||||
}
|
||||
|
||||
return decoded;
|
||||
}
|
||||
|
||||
function normalizePathInput(value: string | undefined): string | null {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmed = value
|
||||
.trim()
|
||||
.replace(/^['"`]/, "")
|
||||
.replace(/['"`]$/, "");
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return trimmed.replace(/\\/g, "/").replace(/\/{2,}/g, "/");
|
||||
}
|
||||
|
||||
function resolvePathAgainstCwd(pathValue: string, cwd?: string): string | null {
|
||||
const normalizedCwd = normalizePathInput(cwd);
|
||||
if (!normalizedCwd || !isAbsolutePath(pathValue) || !isAbsolutePath(normalizedCwd)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedCwdBase = normalizedCwd.replace(/\/+$/, "") || "/";
|
||||
const comparePath = normalizePathForCompare(pathValue);
|
||||
const compareCwd = normalizePathForCompare(normalizedCwdBase);
|
||||
const prefix = normalizedCwdBase === "/" ? "/" : `${normalizedCwdBase}/`;
|
||||
const comparePrefix = normalizePathForCompare(prefix);
|
||||
|
||||
if (comparePath === compareCwd) {
|
||||
return ".";
|
||||
}
|
||||
|
||||
if (comparePath.startsWith(comparePrefix)) {
|
||||
return pathValue.slice(prefix.length) || ".";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizePathForCompare(value: string): string {
|
||||
return /^[A-Za-z]:/.test(value) ? value.toLowerCase() : value;
|
||||
}
|
||||
Reference in New Issue
Block a user