import { URI } from 'vscode-uri' import { pathFunctionsForURI, posixFilePaths, windowsFilePaths } from '../common/path' import { type RangeData, displayLineRange } from '../common/range' /** * Convert an absolute URI to a (possibly shorter) path to display to the user. The display path is * always a path (not a full URI string) and is typically is relative to the nearest workspace root. * The path uses OS-native path separators ('/' on macOS/Linux, '\' on Windows). * * The returned path string MUST ONLY be used for display purposes. It MUST NOT be used to identify * or locate files. * * You MUST call {@link setDisplayPathEnvInfo} at static init time to provide the information about * the environment necessary to construct the correct display path. * * Why is this needed? Why not just use: * * - `uri.fsPath`: because this is the full path, which is much harder to read than the relative * path * - `vscode.workspace.asRelativePath`: because it's not available in webviews, and it does not * handle custom URI schemes (such as if we want to represent remote files that exist on the * Sourcegraph instance). * @param location The absolute URI to convert to a display path. */ export function displayPath(location: URI): string { const result = _displayPath(location, checkEnvInfo()) return typeof result === 'string' ? result : result.toString() } /** * Displays the path of a URI {@link displayPath} with zero-based line values extracted from range. * Example: `src/foo/bar.ts:5-10` * * @param location - The URI to display the path for. * @param range - The line range data to display. * @returns The formatted path with the start and end lines of the range appended to it. */ export function displayPathWithLines(location: URI, range: RangeData): string { return `${displayPath(location)}:${displayLineRange(range)}` } /** * Dirname of the location's display path, to display to the user. Similar to * `dirname(displayPath(location))`, but it uses the right path separators in `dirname` ('\' for * file URIs on Windows, '/' otherwise). * * The returned path string MUST ONLY be used for display purposes. It MUST NOT be used to identify * or locate files. * * Use this instead of other seemingly simpler techniques to avoid a few subtle * bugs/inconsistencies: * * - On Windows, Node's `dirname(uri.fsPath)` breaks on a non-`file` URI on Windows because * `dirname` would use '\' path separators but the URI would have '/' path separators. * - In a single-root workspace, Node's `dirname(uri.fsPath)` would return the root directory name, * which is usually superfluous for display purposes. For example, if VS Code is open to a * directory named `myproject` and there is a list of 2 search results, one `file1.txt` (at the * root) and `dir/file2.txt`, then the VS Code-idiomatic way to present the results is as * `file1.txt` and `file2.txt ` (try it in the search sidebar to see). */ export function displayPathDirname(location: URI): string { const envInfo = checkEnvInfo() const result = _displayPath(location, envInfo) // File path. if (typeof result === 'string') { // If the result is a string, it is a path (not a URI), so we must // use the correct path functions. return envInfo.isWindows ? windowsFilePaths.dirname(result) : posixFilePaths.dirname(result) } // Otherwise, URI. const dirname = pathFunctionsForURI(location, envInfo.isWindows).dirname return result.with({ path: dirname(result.path) }).toString() } /** * Similar to `basename(displayPath(location))`, but it uses the right path separators in `basename` * ('\' for file URIs on Windows, '/' otherwise). */ export function displayPathBasename(location: URI): string { const envInfo = checkEnvInfo() const result = _displayPath(location, envInfo) // File path. if (typeof result === 'string') { // If the result is a string, it is a path (not a URI), so we must // use the correct path functions. return envInfo.isWindows ? windowsFilePaths.basename(result) : posixFilePaths.basename(result) } // Otherwise, URI. return posixFilePaths.basename(result.path) } /** * Like {@link displayPath}, but does not show `/` as a prefix if the * location is in a workspace folder and there are 2 or more workspace folders. */ export function displayPathWithoutWorkspaceFolderPrefix(location: URI): string { const result = _displayPath(location, checkEnvInfo(), false) return typeof result === 'string' ? result : result.toString() } function checkEnvInfo(): DisplayPathEnvInfo { if (!envInfo) { throw new Error( 'no environment info for displayPath function (call setDisplayPathEnvInfo; see displayPath docstring for more info)' ) } return envInfo } function _displayPath( location: URI, { workspaceFolders, isWindows }: DisplayPathEnvInfo, includeWorkspaceFolderWhenMultiple = true ): string | URI { const uri = typeof location === 'string' ? URI.parse(location) : URI.from(location) // Mimic the behavior of vscode.workspace.asRelativePath. const includeWorkspaceFolder = includeWorkspaceFolderWhenMultiple && workspaceFolders.length >= 2 for (const folder of workspaceFolders) { if (uriHasPrefix(uri, folder, isWindows)) { const pathFunctions = pathFunctionsForURI(folder) const workspacePrefix = folder.path.endsWith('/') ? folder.path.slice(0, -1) : folder.path const workspaceDisplayPrefix = includeWorkspaceFolder ? pathFunctions.basename(folder.path) + pathFunctions.separator : '' return fixPathSep( workspaceDisplayPrefix + uri.path.slice(workspacePrefix.length + 1), isWindows, uri.scheme ) } } if (uri.scheme === 'file') { // Show the absolute file path because we couldn't find a parent workspace folder. return fixPathSep(uri.fsPath, isWindows, uri.scheme) } // Show the full URI for anything else. return uri } /** * Fixes the path separators for Windows paths. This makes it possible to write cross-platform * tests. */ function fixPathSep(fsPath: string, isWindows: boolean, scheme: string): string { return isWindows && scheme === 'file' ? fsPath.replaceAll('/', '\\') : fsPath } export function uriHasPrefix(uri: URI, prefix: URI, isWindows: boolean): boolean { // On Windows, it's common to have drive letter casing mismatches (VS Code's APIs tend to normalize // to lowercase, but many other tools use uppercase and we don't know where the context file came // from). const uriPath = isWindows && uri.scheme === 'file' ? uri.path.slice(0, 2).toUpperCase() + uri.path.slice(2) : uri.path const prefixPath = isWindows && prefix.scheme === 'file' ? prefix.path.slice(0, 2).toUpperCase() + prefix.path.slice(2) : prefix.path return ( uri.scheme === prefix.scheme && (uri.authority ?? '') === (prefix.authority ?? '') && // different URI impls treat empty different (uriPath === prefixPath || uriPath.startsWith(prefixPath.endsWith('/') ? prefixPath : `${prefixPath}/`) || (prefixPath.endsWith('/') && uriPath === prefixPath.slice(0, -1))) ) } /** The information necessary for {@link displayPath} to compute a display path. */ export interface DisplayPathEnvInfo { workspaceFolders: URI[] isWindows: boolean } let envInfo: DisplayPathEnvInfo | null = null /** * Provide the information necessary for {@link displayPath} to compute a display path. */ export function setDisplayPathEnvInfo(newEnvInfo: DisplayPathEnvInfo | null): DisplayPathEnvInfo | null { const prev = envInfo envInfo = newEnvInfo return prev }