Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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: permissions ?? undefined
};
} catch (error) {
throw this.toFileSystemProviderError(error);
Expand Down
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 @@ -468,6 +473,17 @@ 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) {
try {
const childStat = await this._fileService.stat(child.resource);
if (!childStat.executable) {
return;
}
} catch {
return;
}
}
if (child.isSymbolicLink) {
kind = TerminalCompletionItemKind.SymbolicLinkFile;
} else {
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
1 change: 1 addition & 0 deletions src/vs/workbench/test/common/workbenchTestServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ export function createFileStat(resource: URI, readonly = false, isFile?: boolean
isSymbolicLink: isSymbolicLink ?? false,
readonly,
locked: false,
executable: false,
name: basename(resource),
children: children?.map(c => createFileStat(c.resource, false, c.isFile, c.isDirectory, c.isSymbolicLink)),
};
Expand Down
Loading