diff --git a/src/vs/platform/files/common/fileService.ts b/src/vs/platform/files/common/fileService.ts index a5f6dc5023ab9..b775ea6cf1083 100644 --- a/src/vs/platform/files/common/fileService.ts +++ b/src/vs/platform/files/common/fileService.ts @@ -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 }; diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 57e5c9644846c..cb3b59e0ba9c4 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -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 { @@ -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 { } @@ -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; } diff --git a/src/vs/platform/files/node/diskFileSystemProvider.ts b/src/vs/platform/files/node/diskFileSystemProvider.ts index 3e550845b9cc7..f29c4742883bc 100644 --- a/src/vs/platform/files/node/diskFileSystemProvider.ts +++ b/src/vs/platform/files/node/diskFileSystemProvider.ts @@ -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'; @@ -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); diff --git a/src/vs/platform/files/test/node/diskFileService.integrationTest.ts b/src/vs/platform/files/test/node/diskFileService.integrationTest.ts index 17ba631493ce5..6f9622409e1cf 100644 --- a/src/vs/platform/files/test/node/diskFileService.integrationTest.ts +++ b/src/vs/platform/files/test/node/diskFileService.integrationTest.ts @@ -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); }); diff --git a/src/vs/platform/files/test/node/fixtures/executable/executable b/src/vs/platform/files/test/node/fixtures/executable/executable new file mode 100755 index 0000000000000..e69de29bb2d1d diff --git a/src/vs/platform/files/test/node/fixtures/executable/non_executable b/src/vs/platform/files/test/node/fixtures/executable/non_executable new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index e6ef77dbc0de2..09904ec3d0832 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -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()})`); diff --git a/src/vs/workbench/contrib/chat/test/electron-browser/tools/builtinTools/fetchPageTool.test.ts b/src/vs/workbench/contrib/chat/test/electron-browser/tools/builtinTools/fetchPageTool.test.ts index 9a93089dcce5f..efcb9f37fe17a 100644 --- a/src/vs/workbench/contrib/chat/test/electron-browser/tools/builtinTools/fetchPageTool.test.ts +++ b/src/vs/workbench/contrib/chat/test/electron-browser/tools/builtinTools/fetchPageTool.test.ts @@ -56,7 +56,8 @@ class ExtendedTestFileService extends TestFileService { mtime: 0, ctime: 0, readonly: false, - locked: false + locked: false, + executable: false }; } diff --git a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts index b593f42e76ee2..bf819a3c6d753 100644 --- a/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts +++ b/src/vs/workbench/contrib/terminalContrib/suggest/browser/terminalCompletionService.ts @@ -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(/(? { 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'; @@ -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 }, ]; }); @@ -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 }, ]; }); @@ -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, [ @@ -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, [ @@ -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, [ @@ -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); diff --git a/src/vs/workbench/services/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts index e203250da67d0..1300308ab628c 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -2319,6 +2319,7 @@ suite('EditorService', () => { isSymbolicLink: false, readonly: false, locked: false, + executable: false, children: undefined })); await activeEditorChangePromise; diff --git a/src/vs/workbench/services/history/test/browser/historyService.test.ts b/src/vs/workbench/services/history/test/browser/historyService.test.ts index c76cae22c4f81..229b95e85dfe7 100644 --- a/src/vs/workbench/services/history/test/browser/historyService.test.ts +++ b/src/vs/workbench/services/history/test/browser/historyService.test.ts @@ -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 diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index 7a7093e6a4bc5..d7058ab39474b 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -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); } @@ -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 @@ -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, diff --git a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts index eedb4a2c02ce5..e4fa57a33e846 100644 --- a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts @@ -527,7 +527,8 @@ export class StoredFileWorkingCopy extend etag, value: buffer, readonly: false, - locked: false + locked: false, + executable: false }, true /* dirty (resolved from buffer) */); } @@ -568,7 +569,8 @@ export class StoredFileWorkingCopy 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 @@ -664,6 +666,7 @@ export class StoredFileWorkingCopy extend etag: content.etag, readonly: content.readonly, locked: content.locked, + executable: false, isFile: true, isDirectory: false, isSymbolicLink: false, diff --git a/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts index 62c7cd79ea9f4..574030d5d1d28 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts @@ -118,6 +118,7 @@ export class TestStoredFileWorkingCopyModelWithCustomSave extends TestStoredFile isSymbolicLink: false, readonly: false, locked: false, + executable: false, children: undefined }; } diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 577232d67e5ea..0f96e2407be20 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -487,7 +487,8 @@ export class TestTextFileService extends BrowserTextFileService { value: await createTextBufferFactoryFromStream(content.value), size: 10, readonly: false, - locked: false + locked: false, + executable: false }; } diff --git a/src/vs/workbench/test/common/workbenchTestServices.ts b/src/vs/workbench/test/common/workbenchTestServices.ts index 0000856e22c03..e967c83d2fd4c 100644 --- a/src/vs/workbench/test/common/workbenchTestServices.ts +++ b/src/vs/workbench/test/common/workbenchTestServices.ts @@ -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(), @@ -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)), }; }