Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions src/vs/platform/files/common/fileService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ export class FileService extends Disposable implements IFileService {
size: stat.size,
readonly: Boolean((stat.permissions ?? 0) & FilePermission.Readonly) || Boolean(provider.capabilities & FileSystemProviderCapabilities.Readonly),
locked: Boolean((stat.permissions ?? 0) & FilePermission.Locked),
executable: Boolean((stat.permissions ?? 0) & FilePermission.Executable),
etag: etag({ mtime: stat.mtime, size: stat.size }),
children: undefined
};
Expand Down
15 changes: 14 additions & 1 deletion src/vs/platform/files/common/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,13 @@ export enum FilePermission {
* to edit the contents and ask the user upon saving to
* remove the lock.
*/
Locked = 2
Locked = 2,

/**
* File is executable. Relevant for Unix-like systems where
* the executable bit determines if a file can be run.
*/
Executable = 4
}

export interface IStat {
Expand Down Expand Up @@ -1247,6 +1253,12 @@ export interface IBaseFileStat {
* remove the lock.
*/
readonly locked?: boolean;

/**
* File is executable. Relevant for Unix-like systems where
* the executable bit determines if a file can be run.
*/
readonly executable?: boolean;
}

export interface IBaseFileStatWithMetadata extends Required<IBaseFileStat> { }
Expand Down Expand Up @@ -1287,6 +1299,7 @@ export interface IFileStatWithMetadata extends IFileStat, IBaseFileStatWithMetad
readonly size: number;
readonly readonly: boolean;
readonly locked: boolean;
readonly executable: boolean;
readonly children: IFileStatWithMetadata[] | undefined;
}

Expand Down
16 changes: 14 additions & 2 deletions src/vs/platform/files/node/diskFileSystemProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Stats, promises } from 'fs';
import { Stats, constants, promises } from 'fs';
import { Barrier, retry } from '../../../base/common/async.js';
import { ResourceMap } from '../../../base/common/map.js';
import { VSBuffer } from '../../../base/common/buffer.js';
Expand Down Expand Up @@ -73,12 +73,24 @@ export class DiskFileSystemProvider extends AbstractDiskFileSystemProvider imple
try {
const { stat, symbolicLink } = await SymlinkSupport.stat(this.toFilePath(resource)); // cannot use fs.stat() here to support links properly

let permissions: FilePermission | undefined = undefined;
if ((stat.mode & 0o200) === 0) {
permissions = FilePermission.Locked;
}
if (
stat.mode & constants.S_IXUSR ||
stat.mode & constants.S_IXGRP ||
stat.mode & constants.S_IXOTH
) {
permissions = (permissions ?? 0) | FilePermission.Executable;
}

return {
type: this.toType(stat, symbolicLink),
ctime: stat.birthtime.getTime(), // intentionally not using ctime here, we want the creation time
mtime: stat.mtime.getTime(),
size: stat.size,
permissions: (stat.mode & 0o200) === 0 ? FilePermission.Locked : undefined
permissions
};
} catch (error) {
throw this.toFileSystemProviderError(error);
Expand Down
15 changes: 15 additions & 0 deletions src/vs/platform/files/test/node/diskFileService.integrationTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,21 @@ flakySuite('Disk File Service', function () {
assert.ok(result.ctime > 0);
});

// The executable bit does not exist on Windows so use a condition not skip
if (!isWindows) {
test('stat - executable', async () => {
const nonExecutable = FileAccess.asFileUri('vs/platform/files/test/node/fixtures/executable/non_executable');
let resolved = await service.stat(nonExecutable);
assert.strictEqual(resolved.isFile, true);
assert.strictEqual(resolved.executable, false);

const executable = FileAccess.asFileUri('vs/platform/files/test/node/fixtures/executable/executable');
resolved = await service.stat(executable);
assert.strictEqual(resolved.isFile, true);
assert.strictEqual(resolved.executable, true);
});
}

test('deleteFile (non recursive)', async () => {
return testDeleteFile(false, false);
});
Expand Down
Empty file.
Empty file.
4 changes: 2 additions & 2 deletions src/vs/workbench/api/common/extHostNotebook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,8 +385,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape {
size: stat.size,
readonly: Boolean((stat.permissions ?? 0) & files.FilePermission.Readonly) || !this._extHostFileSystem.value.isWritableFileSystem(uri.scheme),
locked: Boolean((stat.permissions ?? 0) & files.FilePermission.Locked),
etag: files.etag({ mtime: stat.mtime, size: stat.size }),
children: undefined
executable: Boolean((stat.permissions ?? 0) & files.FilePermission.Executable),
etag: files.etag({ mtime: stat.mtime, size: stat.size })
};

this.trace(`exit saveNotebook(versionId: ${versionId}, ${uri.toString()})`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ class ExtendedTestFileService extends TestFileService {
mtime: 0,
ctime: 0,
readonly: false,
locked: false
locked: false,
executable: false
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,11 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo
const resourceCompletions: ITerminalCompletion[] = [];
const cursorPrefix = promptValue.substring(0, cursorPosition);

// Determine if we're completing the command (first word) vs an argument
// We're in command position if there are no unescaped spaces before cursor
const wordsBeforeCursor = cursorPrefix.split(/(?<!\\) /);
const isCommandPosition = wordsBeforeCursor.length <= 1 && !cursorPrefix.endsWith(' ');

// TODO: Leverage Fig's tokens array here?
// The last word (or argument). When the cursor is following a space it will be the empty
// string
Expand Down Expand Up @@ -406,7 +411,10 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo
return resourceCompletions;
}

const stat = await this._fileService.resolve(lastWordFolderResource, { resolveSingleChildDescendants: true });
const stat = await this._fileService.resolve(lastWordFolderResource, {
resolveMetadata: true,
resolveSingleChildDescendants: true
});
if (!stat?.children) {
return;
}
Expand Down Expand Up @@ -468,6 +476,12 @@ export class TerminalCompletionService extends Disposable implements ITerminalCo
kind = TerminalCompletionItemKind.Folder;
}
} else if (showFiles && child.isFile) {
// When completing the command (first word) on Unix, only show executable files
if (isCommandPosition && !useWindowsStylePath) {
if (!child.executable) {
return;
}
}
if (child.isSymbolicLink) {
kind = TerminalCompletionItemKind.SymbolicLinkFile;
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ suite('TerminalCompletionService', () => {
let configurationService: TestConfigurationService;
let capabilities: TerminalCapabilityStore;
let validResources: URI[];
let childResources: { resource: URI; isFile?: boolean; isDirectory?: boolean; isSymbolicLink?: boolean }[];
let childResources: { resource: URI; isFile?: boolean; isDirectory?: boolean; isSymbolicLink?: boolean; executable?: boolean }[];
let terminalCompletionService: TerminalCompletionService;
const provider = 'testProvider';

Expand Down Expand Up @@ -232,10 +232,10 @@ suite('TerminalCompletionService', () => {
setup(() => {
validResources = [URI.parse('file:///test')];
childResources = [
{ resource: URI.parse('file:///test/.hiddenFile'), isFile: true },
{ resource: URI.parse('file:///test/.hiddenFile'), isFile: true, executable: true },
{ resource: URI.parse('file:///test/.hiddenFolder/'), isDirectory: true },
{ resource: URI.parse('file:///test/folder1/'), isDirectory: true },
{ resource: URI.parse('file:///test/file1.txt'), isFile: true },
{ resource: URI.parse('file:///test/file1.txt'), isFile: true, executable: true },
];
});

Expand Down Expand Up @@ -307,7 +307,7 @@ suite('TerminalCompletionService', () => {
childResources = [
{ resource: URI.parse('file:///home/vscode'), isDirectory: true },
{ resource: URI.parse('file:///home/vscode/foo'), isDirectory: true },
{ resource: URI.parse('file:///home/vscode/bar.txt'), isFile: true },
{ resource: URI.parse('file:///home/vscode/bar.txt'), isFile: true, executable: true },
];
});

Expand Down Expand Up @@ -712,7 +712,7 @@ suite('TerminalCompletionService', () => {
];
childResources = [
{ resource: URI.file('C:\\Users\\foo\\bar'), isDirectory: true, isFile: false },
{ resource: URI.file('C:\\Users\\foo\\baz.txt'), isFile: true }
{ resource: URI.file('C:\\Users\\foo\\baz.txt'), isFile: true, executable: true }
];
const result = await terminalCompletionService.resolveResources(resourceOptions, 'C:/Users/foo/', 13, provider, capabilities, WindowsShellType.GitBash);
assertCompletions(result, [
Expand All @@ -735,7 +735,7 @@ suite('TerminalCompletionService', () => {
];
childResources = [
{ resource: URI.file('C:\\Users\\foo\\bar'), isDirectory: true },
{ resource: URI.file('C:\\Users\\foo\\baz.txt'), isFile: true }
{ resource: URI.file('C:\\Users\\foo\\baz.txt'), isFile: true, executable: true }
];
const result = await terminalCompletionService.resolveResources(resourceOptions, './', 2, provider, capabilities, WindowsShellType.GitBash);
assertCompletions(result, [
Expand All @@ -760,7 +760,7 @@ suite('TerminalCompletionService', () => {
];
childResources = [
{ resource: URI.file('C:\\Users\\foo\\bar'), isDirectory: true },
{ resource: URI.file('C:\\Users\\foo\\baz.txt'), isFile: true }
{ resource: URI.file('C:\\Users\\foo\\baz.txt'), isFile: true, executable: true }
];
const result = await terminalCompletionService.resolveResources(resourceOptions, '/c/Users/foo/', 13, provider, capabilities, WindowsShellType.GitBash);
assertCompletions(result, [
Expand Down Expand Up @@ -814,7 +814,7 @@ suite('TerminalCompletionService', () => {
{ resource: URI.parse('file:///test/[folder1]/'), isDirectory: true },
{ resource: URI.parse('file:///test/folder 2/'), isDirectory: true },
{ resource: URI.parse('file:///test/!special$chars&/'), isDirectory: true },
{ resource: URI.parse('file:///test/!special$chars2&'), isFile: true }
{ resource: URI.parse('file:///test/!special$chars2&'), isFile: true, executable: true }
];
const result = await terminalCompletionService.resolveResources(resourceOptions, '', 0, provider, capabilities);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2319,6 +2319,7 @@ suite('EditorService', () => {
isSymbolicLink: false,
readonly: false,
locked: false,
executable: false,
children: undefined
}));
await activeEditorChangePromise;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,7 @@ suite('HistoryService', function () {
name: 'other.txt',
readonly: false,
locked: false,
executable: false,
size: 0,
resource: toResource.call(this, '/path/other.txt'),
children: undefined
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
value: buffer,
encoding: preferredEncoding.encoding,
readonly: false,
locked: false
locked: false,
executable: false
}, true /* dirty (resolved from buffer) */, options);
}

Expand Down Expand Up @@ -419,7 +420,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
value: await createTextBufferFactoryFromStream(await this.textFileService.getDecodedStream(this.resource, backup.value, { encoding: UTF8 })),
encoding,
readonly: false,
locked: false
locked: false,
executable: false
}, true /* dirty (resolved from backup) */, options);

// Restore orphaned flag based on state
Expand Down Expand Up @@ -517,6 +519,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
etag: content.etag,
readonly: content.readonly,
locked: content.locked,
executable: false,
isFile: true,
isDirectory: false,
isSymbolicLink: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,8 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend
etag,
value: buffer,
readonly: false,
locked: false
locked: false,
executable: false
}, true /* dirty (resolved from buffer) */);
}

Expand Down Expand Up @@ -568,7 +569,8 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend
etag: backup.meta ? backup.meta.etag : ETAG_DISABLED, // etag disabled if unknown!
value: backup.value,
readonly: false,
locked: false
locked: false,
executable: false
}, true /* dirty (resolved from backup) */);

// Restore orphaned flag based on state
Expand Down Expand Up @@ -664,6 +666,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend
etag: content.etag,
readonly: content.readonly,
locked: content.locked,
executable: false,
isFile: true,
isDirectory: false,
isSymbolicLink: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export class TestStoredFileWorkingCopyModelWithCustomSave extends TestStoredFile
isSymbolicLink: false,
readonly: false,
locked: false,
executable: false,
children: undefined
};
}
Expand Down
3 changes: 2 additions & 1 deletion src/vs/workbench/test/browser/workbenchTestServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,8 @@ export class TestTextFileService extends BrowserTextFileService {
value: await createTextBufferFactoryFromStream(content.value),
size: 10,
readonly: false,
locked: false
locked: false,
executable: false
};
}

Expand Down
5 changes: 3 additions & 2 deletions src/vs/workbench/test/common/workbenchTestServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ export class TestWorkingCopy extends Disposable implements IWorkingCopy {
}
}

export function createFileStat(resource: URI, readonly = false, isFile?: boolean, isDirectory?: boolean, isSymbolicLink?: boolean, children?: { resource: URI; isFile?: boolean; isDirectory?: boolean; isSymbolicLink?: 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; executable?: boolean }[] | undefined, executable?: boolean): IFileStatWithMetadata {
return {
resource,
etag: Date.now().toString(),
Expand All @@ -257,8 +257,9 @@ export function createFileStat(resource: URI, readonly = false, isFile?: boolean
isSymbolicLink: isSymbolicLink ?? false,
readonly,
locked: false,
executable: executable ?? false,
name: basename(resource),
children: children?.map(c => createFileStat(c.resource, false, c.isFile, c.isDirectory, c.isSymbolicLink)),
children: children?.map(c => createFileStat(c.resource, false, c.isFile, c.isDirectory, c.isSymbolicLink, undefined, c.executable)),
};
}

Expand Down
Loading