mirror of https://github.com/microsoft/vscode.git
files - allow to resolve `realpath` and adopt (#251690)
This commit is contained in:
parent
2cd6a151f5
commit
d59fac320f
|
@ -56,7 +56,8 @@ export class DiskFileSystemProviderClient extends Disposable implements
|
|||
FileSystemProviderCapabilities.FileAtomicRead |
|
||||
FileSystemProviderCapabilities.FileAtomicWrite |
|
||||
FileSystemProviderCapabilities.FileAtomicDelete |
|
||||
FileSystemProviderCapabilities.FileClone;
|
||||
FileSystemProviderCapabilities.FileClone |
|
||||
FileSystemProviderCapabilities.FileRealpath;
|
||||
|
||||
if (this.extraCapabilities.pathCaseSensitive) {
|
||||
this._capabilities |= FileSystemProviderCapabilities.PathCaseSensitive;
|
||||
|
@ -78,6 +79,10 @@ export class DiskFileSystemProviderClient extends Disposable implements
|
|||
return this.channel.call('stat', [resource]);
|
||||
}
|
||||
|
||||
realpath(resource: URI): Promise<string> {
|
||||
return this.channel.call('realpath', [resource]);
|
||||
}
|
||||
|
||||
readdir(resource: URI): Promise<[string, FileType][]> {
|
||||
return this.channel.call('readdir', [resource]);
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ import { extUri, extUriIgnorePathCase, IExtUri, isAbsolutePath } from '../../../
|
|||
import { consumeStream, isReadableBufferedStream, isReadableStream, listenStream, newWriteableStream, peekReadable, peekStream, transform } from '../../../base/common/stream.js';
|
||||
import { URI } from '../../../base/common/uri.js';
|
||||
import { localize } from '../../../nls.js';
|
||||
import { ensureFileSystemProviderError, etag, ETAG_DISABLED, FileChangesEvent, IFileDeleteOptions, FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FilePermission, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, hasFileAtomicReadCapability, hasFileFolderCopyCapability, hasFileReadStreamCapability, hasOpenReadWriteCloseCapability, hasReadWriteCapability, ICreateFileOptions, IFileContent, IFileService, IFileStat, IFileStatWithMetadata, IFileStreamContent, IFileSystemProvider, IFileSystemProviderActivationEvent, IFileSystemProviderCapabilitiesChangeEvent, IFileSystemProviderRegistrationEvent, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IReadFileOptions, IReadFileStreamOptions, IResolveFileOptions, IFileStatResult, IFileStatResultWithMetadata, IResolveMetadataFileOptions, IStat, IFileStatWithPartialMetadata, IWatchOptions, IWriteFileOptions, NotModifiedSinceFileOperationError, toFileOperationResult, toFileSystemProviderErrorCode, hasFileCloneCapability, TooLargeFileOperationError, hasFileAtomicDeleteCapability, hasFileAtomicWriteCapability, IWatchOptionsWithCorrelation, IFileSystemWatcher, IWatchOptionsWithoutCorrelation } from './files.js';
|
||||
import { ensureFileSystemProviderError, etag, ETAG_DISABLED, FileChangesEvent, IFileDeleteOptions, FileOperation, FileOperationError, FileOperationEvent, FileOperationResult, FilePermission, FileSystemProviderCapabilities, FileSystemProviderErrorCode, FileType, hasFileAtomicReadCapability, hasFileFolderCopyCapability, hasFileReadStreamCapability, hasOpenReadWriteCloseCapability, hasReadWriteCapability, ICreateFileOptions, IFileContent, IFileService, IFileStat, IFileStatWithMetadata, IFileStreamContent, IFileSystemProvider, IFileSystemProviderActivationEvent, IFileSystemProviderCapabilitiesChangeEvent, IFileSystemProviderRegistrationEvent, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, IReadFileOptions, IReadFileStreamOptions, IResolveFileOptions, IFileStatResult, IFileStatResultWithMetadata, IResolveMetadataFileOptions, IStat, IFileStatWithPartialMetadata, IWatchOptions, IWriteFileOptions, NotModifiedSinceFileOperationError, toFileOperationResult, toFileSystemProviderErrorCode, hasFileCloneCapability, TooLargeFileOperationError, hasFileAtomicDeleteCapability, hasFileAtomicWriteCapability, IWatchOptionsWithCorrelation, IFileSystemWatcher, IWatchOptionsWithoutCorrelation, hasFileRealpathCapability } from './files.js';
|
||||
import { readFileIntoStream } from './io.js';
|
||||
import { ILogService } from '../../log/common/log.js';
|
||||
import { ErrorNoTelemetry } from '../../../base/common/errors.js';
|
||||
|
@ -317,6 +317,18 @@ export class FileService extends Disposable implements IFileService {
|
|||
return this.toFileStat(provider, resource, stat, undefined, true, () => false /* Do not resolve any children */);
|
||||
}
|
||||
|
||||
async realpath(resource: URI): Promise<URI | undefined> {
|
||||
const provider = await this.withProvider(resource);
|
||||
|
||||
if (hasFileRealpathCapability(provider)) {
|
||||
const realpath = await provider.realpath(resource);
|
||||
|
||||
return resource.with({ path: realpath });
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async exists(resource: URI): Promise<boolean> {
|
||||
const provider = await this.withProvider(resource);
|
||||
|
||||
|
|
|
@ -133,6 +133,14 @@ export interface IFileService {
|
|||
*/
|
||||
stat(resource: URI): Promise<IFileStatWithPartialMetadata>;
|
||||
|
||||
/**
|
||||
* Attempts to resolve the real path of the provided resource. The real path can be
|
||||
* different from the resource path for example when it is a symlink.
|
||||
*
|
||||
* Will return `undefined` if the real path cannot be resolved.
|
||||
*/
|
||||
realpath(resource: URI): Promise<URI | undefined>;
|
||||
|
||||
/**
|
||||
* Finds out if a file/folder identified by the resource exists.
|
||||
*/
|
||||
|
@ -635,7 +643,12 @@ export const enum FileSystemProviderCapabilities {
|
|||
/**
|
||||
* Provider support to clone files atomically.
|
||||
*/
|
||||
FileClone = 1 << 17
|
||||
FileClone = 1 << 17,
|
||||
|
||||
/**
|
||||
* Provider support to resolve real paths.
|
||||
*/
|
||||
FileRealpath = 1 << 18
|
||||
}
|
||||
|
||||
export interface IFileSystemProvider {
|
||||
|
@ -693,6 +706,14 @@ export function hasFileCloneCapability(provider: IFileSystemProvider): provider
|
|||
return !!(provider.capabilities & FileSystemProviderCapabilities.FileClone);
|
||||
}
|
||||
|
||||
export interface IFileSystemProviderWithFileRealpathCapability extends IFileSystemProvider {
|
||||
realpath(resource: URI): Promise<string>;
|
||||
}
|
||||
|
||||
export function hasFileRealpathCapability(provider: IFileSystemProvider): provider is IFileSystemProviderWithFileRealpathCapability {
|
||||
return !!(provider.capabilities & FileSystemProviderCapabilities.FileRealpath);
|
||||
}
|
||||
|
||||
export interface IFileSystemProviderWithOpenReadWriteCloseCapability extends IFileSystemProvider {
|
||||
open(resource: URI, opts: IFileOpenOptions): Promise<number>;
|
||||
close(fd: number): Promise<void>;
|
||||
|
|
|
@ -18,7 +18,7 @@ import { newWriteableStream, ReadableStreamEvents } from '../../../base/common/s
|
|||
import { URI } from '../../../base/common/uri.js';
|
||||
import { IDirent, Promises, RimRafMode, SymlinkSupport } from '../../../base/node/pfs.js';
|
||||
import { localize } from '../../../nls.js';
|
||||
import { createFileSystemProviderError, IFileAtomicReadOptions, IFileDeleteOptions, IFileOpenOptions, IFileOverwriteOptions, IFileReadStreamOptions, FileSystemProviderCapabilities, FileSystemProviderError, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileCloneCapability, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, isFileOpenForWriteOptions, IStat, FilePermission, IFileSystemProviderWithFileAtomicWriteCapability, IFileSystemProviderWithFileAtomicDeleteCapability, IFileChange } from '../common/files.js';
|
||||
import { createFileSystemProviderError, IFileAtomicReadOptions, IFileDeleteOptions, IFileOpenOptions, IFileOverwriteOptions, IFileReadStreamOptions, FileSystemProviderCapabilities, FileSystemProviderError, FileSystemProviderErrorCode, FileType, IFileWriteOptions, IFileSystemProviderWithFileAtomicReadCapability, IFileSystemProviderWithFileCloneCapability, IFileSystemProviderWithFileFolderCopyCapability, IFileSystemProviderWithFileReadStreamCapability, IFileSystemProviderWithFileReadWriteCapability, IFileSystemProviderWithOpenReadWriteCloseCapability, isFileOpenForWriteOptions, IStat, FilePermission, IFileSystemProviderWithFileAtomicWriteCapability, IFileSystemProviderWithFileAtomicDeleteCapability, IFileChange, IFileSystemProviderWithFileRealpathCapability } from '../common/files.js';
|
||||
import { readFileIntoStream } from '../common/io.js';
|
||||
import { AbstractNonRecursiveWatcherClient, AbstractUniversalWatcherClient, ILogMessage } from '../common/watcher.js';
|
||||
import { ILogService } from '../../log/common/log.js';
|
||||
|
@ -34,7 +34,8 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple
|
|||
IFileSystemProviderWithFileAtomicReadCapability,
|
||||
IFileSystemProviderWithFileAtomicWriteCapability,
|
||||
IFileSystemProviderWithFileAtomicDeleteCapability,
|
||||
IFileSystemProviderWithFileCloneCapability {
|
||||
IFileSystemProviderWithFileCloneCapability,
|
||||
IFileSystemProviderWithFileRealpathCapability {
|
||||
|
||||
private static TRACE_LOG_RESOURCE_LOCKS = false; // not enabled by default because very spammy
|
||||
|
||||
|
@ -61,7 +62,8 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple
|
|||
FileSystemProviderCapabilities.FileAtomicRead |
|
||||
FileSystemProviderCapabilities.FileAtomicWrite |
|
||||
FileSystemProviderCapabilities.FileAtomicDelete |
|
||||
FileSystemProviderCapabilities.FileClone;
|
||||
FileSystemProviderCapabilities.FileClone |
|
||||
FileSystemProviderCapabilities.FileRealpath;
|
||||
|
||||
if (isLinux) {
|
||||
this._capabilities |= FileSystemProviderCapabilities.PathCaseSensitive;
|
||||
|
@ -99,6 +101,12 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple
|
|||
}
|
||||
}
|
||||
|
||||
async realpath(resource: URI): Promise<string> {
|
||||
const filePath = this.toFilePath(resource);
|
||||
|
||||
return Promises.realpath(filePath);
|
||||
}
|
||||
|
||||
async readdir(resource: URI): Promise<[string, FileType][]> {
|
||||
try {
|
||||
const children = await Promises.readdir(this.toFilePath(resource), { withFileTypes: true });
|
||||
|
|
|
@ -38,6 +38,7 @@ export abstract class AbstractDiskFileSystemProviderChannel<T> extends Disposabl
|
|||
|
||||
switch (command) {
|
||||
case 'stat': return this.stat(uriTransformer, arg[0]);
|
||||
case 'realpath': return this.realpath(uriTransformer, arg[0]);
|
||||
case 'readdir': return this.readdir(uriTransformer, arg[0]);
|
||||
case 'open': return this.open(uriTransformer, arg[0], arg[1]);
|
||||
case 'close': return this.close(arg[0]);
|
||||
|
@ -80,6 +81,12 @@ export abstract class AbstractDiskFileSystemProviderChannel<T> extends Disposabl
|
|||
return this.provider.stat(resource);
|
||||
}
|
||||
|
||||
private realpath(uriTransformer: IURITransformer, _resource: UriComponents): Promise<string> {
|
||||
const resource = this.transformIncoming(uriTransformer, _resource, true);
|
||||
|
||||
return this.provider.realpath(resource);
|
||||
}
|
||||
|
||||
private readdir(uriTransformer: IURITransformer, _resource: UriComponents): Promise<[string, FileType][]> {
|
||||
const resource = this.transformIncoming(uriTransformer, _resource);
|
||||
|
||||
|
|
|
@ -72,7 +72,8 @@ export class TestDiskFileSystemProvider extends DiskFileSystemProvider {
|
|||
FileSystemProviderCapabilities.FileAtomicRead |
|
||||
FileSystemProviderCapabilities.FileAtomicWrite |
|
||||
FileSystemProviderCapabilities.FileAtomicDelete |
|
||||
FileSystemProviderCapabilities.FileClone;
|
||||
FileSystemProviderCapabilities.FileClone |
|
||||
FileSystemProviderCapabilities.FileRealpath;
|
||||
|
||||
if (isLinux) {
|
||||
this._testCapabilities |= FileSystemProviderCapabilities.PathCaseSensitive;
|
||||
|
@ -427,7 +428,7 @@ flakySuite('Disk File Service', function () {
|
|||
assert.strictEqual(r2.name, 'deep');
|
||||
});
|
||||
|
||||
test('resolve - folder symbolic link', async () => {
|
||||
test('resolve / realpath - folder symbolic link', async () => {
|
||||
const link = URI.file(join(testDir, 'deep-link'));
|
||||
await promises.symlink(join(testDir, 'deep'), link.fsPath, 'junction');
|
||||
|
||||
|
@ -435,6 +436,10 @@ flakySuite('Disk File Service', function () {
|
|||
assert.strictEqual(resolved.children!.length, 4);
|
||||
assert.strictEqual(resolved.isDirectory, true);
|
||||
assert.strictEqual(resolved.isSymbolicLink, true);
|
||||
|
||||
const realpath = await service.realpath(link);
|
||||
assert.ok(realpath);
|
||||
assert.strictEqual(basename(realpath.fsPath), 'deep');
|
||||
});
|
||||
|
||||
(isWindows ? test.skip /* windows: cannot create file symbolic link without elevated context */ : test)('resolve - file symbolic link', async () => {
|
||||
|
|
|
@ -18,6 +18,7 @@ import { env as processEnv } from '../../../../../base/common/process.js';
|
|||
import type { IProcessEnvironment } from '../../../../../base/common/platform.js';
|
||||
import { timeout } from '../../../../../base/common/async.js';
|
||||
import { gitBashToWindowsPath } from './terminalGitBashHelpers.js';
|
||||
import { isEqual } from '../../../../../base/common/resources.js';
|
||||
|
||||
export const ITerminalCompletionService = createDecorator<ITerminalCompletionService>('terminalCompletionService');
|
||||
|
||||
|
@ -367,6 +368,7 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo
|
|||
// - (tilde) `cd ~/src/` -> `cd ~/src/folder1/`, ...
|
||||
for (const child of stat.children) {
|
||||
let kind: TerminalCompletionItemKind | undefined;
|
||||
let detail: string | undefined = undefined;
|
||||
if (foldersRequested && child.isDirectory) {
|
||||
if (child.isSymbolicLink) {
|
||||
kind = TerminalCompletionItemKind.SymbolicLinkFolder;
|
||||
|
@ -403,11 +405,23 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo
|
|||
}
|
||||
}
|
||||
|
||||
// Try to resolve symlink target for symbolic links
|
||||
if (child.isSymbolicLink) {
|
||||
try {
|
||||
const realpath = await this._fileService.realpath(child.resource);
|
||||
if (realpath && !isEqual(child.resource, realpath)) {
|
||||
detail = `${getFriendlyPath(child.resource, resourceRequestConfig.pathSeparator, kind, shellType)} -> ${getFriendlyPath(realpath, resourceRequestConfig.pathSeparator, kind, shellType)}`;
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors resolving symlink targets - they may be dangling links
|
||||
}
|
||||
}
|
||||
|
||||
resourceCompletions.push({
|
||||
label,
|
||||
provider,
|
||||
kind,
|
||||
detail: getFriendlyPath(child.resource, resourceRequestConfig.pathSeparator, kind, shellType),
|
||||
detail: detail ?? getFriendlyPath(child.resource, resourceRequestConfig.pathSeparator, kind, shellType),
|
||||
replacementIndex: cursorPosition - lastWord.length,
|
||||
replacementLength: lastWord.length
|
||||
});
|
||||
|
|
|
@ -98,7 +98,7 @@ suite('TerminalCompletionService', () => {
|
|||
let configurationService: TestConfigurationService;
|
||||
let capabilities: TerminalCapabilityStore;
|
||||
let validResources: URI[];
|
||||
let childResources: { resource: URI; isFile?: boolean; isDirectory?: boolean }[];
|
||||
let childResources: { resource: URI; isFile?: boolean; isDirectory?: boolean; isSymbolicLink?: boolean }[];
|
||||
let terminalCompletionService: TerminalCompletionService;
|
||||
const provider = 'testProvider';
|
||||
|
||||
|
@ -122,8 +122,16 @@ suite('TerminalCompletionService', () => {
|
|||
count(childFsPath, '/') === count(parentFsPath, '/') + 1
|
||||
);
|
||||
});
|
||||
return createFileStat(resource, undefined, undefined, undefined, children);
|
||||
return createFileStat(resource, undefined, undefined, undefined, undefined, children);
|
||||
},
|
||||
async realpath(resource: URI): Promise<URI | undefined> {
|
||||
if (resource.path.includes('symlink-file')) {
|
||||
return resource.with({ path: '/target/actual-file.txt' });
|
||||
} else if (resource.path.includes('symlink-folder')) {
|
||||
return resource.with({ path: '/target/actual-folder' });
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
terminalCompletionService = store.add(instantiationService.createInstance(TerminalCompletionService));
|
||||
terminalCompletionService.processEnv = testEnv;
|
||||
|
@ -677,4 +685,36 @@ suite('TerminalCompletionService', () => {
|
|||
});
|
||||
});
|
||||
}
|
||||
if (!isWindows) {
|
||||
suite('symlink support', () => {
|
||||
test('should include symlink target information in completions', async () => {
|
||||
const resourceRequestConfig: TerminalResourceRequestConfig = {
|
||||
cwd: URI.parse('file:///test'),
|
||||
pathSeparator,
|
||||
filesRequested: true,
|
||||
foldersRequested: true
|
||||
};
|
||||
|
||||
validResources = [URI.parse('file:///test')];
|
||||
|
||||
// Create mock children including a symbolic link
|
||||
childResources = [
|
||||
{ resource: URI.parse('file:///test/regular-file.txt'), isFile: true },
|
||||
{ resource: URI.parse('file:///test/symlink-file'), isFile: true, isSymbolicLink: true },
|
||||
{ resource: URI.parse('file:///test/symlink-folder'), isDirectory: true, isSymbolicLink: true },
|
||||
{ resource: URI.parse('file:///test/regular-folder'), isDirectory: true },
|
||||
];
|
||||
|
||||
const result = await terminalCompletionService.resolveResources(resourceRequestConfig, 'ls ', 3, provider, capabilities);
|
||||
|
||||
// Find the symlink completion
|
||||
const symlinkFileCompletion = result?.find(c => c.label === './symlink-file');
|
||||
const symlinkFolderCompletion = result?.find(c => c.label === './symlink-folder/');
|
||||
assert.strictEqual(symlinkFileCompletion?.detail, '/test/symlink-file -> /target/actual-file.txt', 'Symlink file detail should match target');
|
||||
assert.strictEqual(symlinkFolderCompletion?.detail, '/test/symlink-folder -> /target/actual-folder', 'Symlink folder detail should match target');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -68,6 +68,10 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple
|
|||
return this.provider.stat(resource);
|
||||
}
|
||||
|
||||
realpath(resource: URI): Promise<string> {
|
||||
return this.provider.realpath(resource);
|
||||
}
|
||||
|
||||
readdir(resource: URI): Promise<[string, FileType][]> {
|
||||
return this.provider.readdir(resource);
|
||||
}
|
||||
|
|
|
@ -1120,6 +1120,10 @@ export class TestFileService implements IFileService {
|
|||
return this.resolve(resource, { resolveMetadata: true });
|
||||
}
|
||||
|
||||
async realpath(resource: URI): Promise<URI> {
|
||||
return resource;
|
||||
}
|
||||
|
||||
async resolveAll(toResolve: { resource: URI; options?: IResolveFileOptions }[]): Promise<IFileStatResult[]> {
|
||||
const stats = await Promise.all(toResolve.map(resourceAndOption => this.resolve(resourceAndOption.resource, resourceAndOption.options)));
|
||||
|
||||
|
|
|
@ -226,7 +226,7 @@ export class TestWorkingCopy extends Disposable implements IWorkingCopy {
|
|||
}
|
||||
}
|
||||
|
||||
export function createFileStat(resource: URI, readonly = false, isFile?: boolean, isDirectory?: boolean, children?: { resource: URI; isFile?: boolean; isDirectory?: boolean }[] | undefined): IFileStatWithMetadata {
|
||||
export function createFileStat(resource: URI, readonly = false, isFile?: boolean, isDirectory?: boolean, isSymbolicLink?: boolean, children?: { resource: URI; isFile?: boolean; isDirectory?: boolean; isSymbolicLink?: boolean }[] | undefined): IFileStatWithMetadata {
|
||||
return {
|
||||
resource,
|
||||
etag: Date.now().toString(),
|
||||
|
@ -235,11 +235,11 @@ export function createFileStat(resource: URI, readonly = false, isFile?: boolean
|
|||
size: 42,
|
||||
isFile: isFile ?? true,
|
||||
isDirectory: isDirectory ?? false,
|
||||
isSymbolicLink: false,
|
||||
isSymbolicLink: isSymbolicLink ?? false,
|
||||
readonly,
|
||||
locked: false,
|
||||
name: basename(resource),
|
||||
children: children?.map(c => createFileStat(c.resource, false, c.isFile, c.isDirectory))
|
||||
children: children?.map(c => createFileStat(c.resource, false, c.isFile, c.isDirectory, c.isSymbolicLink)),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue