diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index ffc2c12d3542031fb792f6235a1c9092ea06fad7..20440a71ce9874387b6491b5cdc0829d217d0b0d 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -108,6 +108,10 @@ export function readlink(path: string): TPromise { return nfcall(fs.readlink, path); } +export function utimes(path: string, atime:Date, mtime:Date): Promise { + return nfcall(fs.utimes, path, atime, mtime); +} + function doStatMultiple(paths: string[]): TPromise<{ path: string; stats: fs.Stats; }> { let path = paths.shift(); return stat(path).then((value) => { diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 0d87bddbfe213f3a842e2a44f80890cae755e7f5..3b4c241d8e562839c23142141f2bac56c8d6c400 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -92,6 +92,12 @@ export interface IFileService { */ rename(resource: URI, newName: string): TPromise; + /** + * Creates a new empty file if the given path does not exist and otherwise + * will set the mtime and atime of the file to the current date. + */ + touchFile(resource: URI): TPromise; + /** * Deletes the provided file. The optional useTrash parameter allows to * move the file to trash. diff --git a/src/vs/workbench/parts/files/browser/fileActions.ts b/src/vs/workbench/parts/files/browser/fileActions.ts index b6228845bb5627fb1d1e94afd770982d6c692890..b0edd8d968f1c63ff65914bc5967bb842420080a 100644 --- a/src/vs/workbench/parts/files/browser/fileActions.ts +++ b/src/vs/workbench/parts/files/browser/fileActions.ts @@ -1510,7 +1510,7 @@ export abstract class BaseSaveFileAction extends BaseActionWithErrorReporting { } // Just save - return this.textFileService.save(source); + return this.textFileService.save(source, { force: true /* force a change to the file to trigger external watchers if any */}); } return TPromise.as(false); diff --git a/src/vs/workbench/parts/files/common/files.ts b/src/vs/workbench/parts/files/common/files.ts index 2826e979506127246154ac0476cd0906bbaf79ce..0469b7cb5d3a29946ed373f67d1cc8b366590dcf 100644 --- a/src/vs/workbench/parts/files/common/files.ts +++ b/src/vs/workbench/parts/files/common/files.ts @@ -301,6 +301,15 @@ export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport isDisposed(): boolean; } +export interface ISaveOptions { + + /** + * Save the file on disk even if not dirty. If the file is not dirty, it will be touched + * so that mtime and atime are updated. This helps to trigger external file watchers. + */ + force: boolean; +} + export interface ITextFileService extends IDisposable { _serviceBrand: any; onAutoSaveConfigurationChange: Event; @@ -338,7 +347,7 @@ export interface ITextFileService extends IDisposable { * @param resource the resource to save * @return true iff the resource was saved. */ - save(resource: URI): TPromise; + save(resource: URI, options?: ISaveOptions): TPromise; /** * Saves the provided resource asking the user for a file name. diff --git a/src/vs/workbench/parts/files/common/textFileService.ts b/src/vs/workbench/parts/files/common/textFileService.ts index d237f05826899e08d7b5b8d7f3b7110c24cb1156..eb9b543966a97e2c29e37ce01bc6e000984f6024 100644 --- a/src/vs/workbench/parts/files/common/textFileService.ts +++ b/src/vs/workbench/parts/files/common/textFileService.ts @@ -11,7 +11,7 @@ import DOM = require('vs/base/browser/dom'); import errors = require('vs/base/common/errors'); import objects = require('vs/base/common/objects'); import Event, {Emitter} from 'vs/base/common/event'; -import {IResult, ITextFileOperationResult, ITextFileService, IRawTextContent, IAutoSaveConfiguration, AutoSaveMode, ITextFileEditorModelManager, ITextFileEditorModel} from 'vs/workbench/parts/files/common/files'; +import {IResult, ITextFileOperationResult, ITextFileService, IRawTextContent, IAutoSaveConfiguration, AutoSaveMode, ITextFileEditorModelManager, ITextFileEditorModel, ISaveOptions} from 'vs/workbench/parts/files/common/files'; import {ConfirmResult} from 'vs/workbench/common/editor'; import {ILifecycleService} from 'vs/platform/lifecycle/common/lifecycle'; import {IWorkspaceContextService} from 'vs/platform/workspace/common/workspace'; @@ -244,7 +244,13 @@ export abstract class TextFileService implements ITextFileService { return this.untitledEditorService.getDirty().some(dirty => !resource || dirty.toString() === resource.toString()); } - public save(resource: URI): TPromise { + public save(resource: URI, options?: ISaveOptions): TPromise { + + // touch resource if options tell so and file is not dirty + if (options && options.force && resource.scheme === 'file' && !this.isDirty(resource)) { + return this.fileService.touchFile(resource).then(() => true); + } + return this.saveAll([resource]).then(result => result.results.length === 1 && result.results[0].success); } diff --git a/src/vs/workbench/services/files/electron-browser/fileService.ts b/src/vs/workbench/services/files/electron-browser/fileService.ts index 15cb35d152ed8991367ba226702330d94d793498..abcf4a615a1ec6d32a1a7d9db1874f025a1137b7 100644 --- a/src/vs/workbench/services/files/electron-browser/fileService.ts +++ b/src/vs/workbench/services/files/electron-browser/fileService.ts @@ -216,6 +216,10 @@ export class FileService implements IFileService { return this.raw.createFolder(resource); } + public touchFile(resource: uri): TPromise { + return this.raw.touchFile(resource); + } + public rename(resource: uri, newName: string): TPromise { return this.raw.rename(resource, newName); } diff --git a/src/vs/workbench/services/files/node/fileService.ts b/src/vs/workbench/services/files/node/fileService.ts index f49b9b0b1093c66d89b6aef81f04a254739398e9..1851192c0981f30400e07f2a766a376b7e50e79d 100644 --- a/src/vs/workbench/services/files/node/fileService.ts +++ b/src/vs/workbench/services/files/node/fileService.ts @@ -314,6 +314,31 @@ export class FileService implements IFileService { }); } + public touchFile(resource: uri): TPromise { + const absolutePath = this.toAbsolutePath(resource); + + // 1.) check file + return this.checkFile(absolutePath).then(exists => { + let createPromise: TPromise; + if (exists) { + createPromise = TPromise.as(null); + } else { + createPromise = this.createFile(resource); + } + + // 2.) create file as needed + return createPromise.then(() => { + + // 3.) update atime and mtime + return pfs.utimes(absolutePath, new Date(), new Date()).then(() => { + + // 4.) resolve + return this.resolve(resource); + }); + }); + }); + } + public rename(resource: uri, newName: string): TPromise { const newPath = paths.join(paths.dirname(resource.fsPath), newName); @@ -532,7 +557,7 @@ export class FileService implements IFileService { return null; } - private checkFile(absolutePath: string, options: IUpdateContentOptions): TPromise { + private checkFile(absolutePath: string, options: IUpdateContentOptions = Object.create(null)): TPromise { return pfs.exists(absolutePath).then(exists => { if (exists) { return pfs.stat(absolutePath).then(stat => { diff --git a/src/vs/workbench/services/files/test/node/fileService.test.ts b/src/vs/workbench/services/files/test/node/fileService.test.ts index 61e0f8af2ed17f400d5cc36dce1d8631490e0bb4..8117b359be5709e5c0b12b126ae2976f11bf586d 100644 --- a/src/vs/workbench/services/files/test/node/fileService.test.ts +++ b/src/vs/workbench/services/files/test/node/fileService.test.ts @@ -10,6 +10,7 @@ import path = require('path'); import os = require('os'); import assert = require('assert'); +import {TPromise} from 'vs/base/common/winjs.base'; import {FileService, IEncodingOverride} from 'vs/workbench/services/files/node/fileService'; import {EventType, FileChangesEvent, FileOperationResult, IFileOperationResult} from 'vs/platform/files/common/files'; import {nfcall} from 'vs/base/common/async'; @@ -79,6 +80,26 @@ suite('FileService', () => { }); }); + test('touchFile', function (done: () => void) { + service.touchFile(uri.file(path.join(testDir, 'test.txt'))).done(s => { + assert.equal(s.name, 'test.txt'); + assert.equal(fs.existsSync(s.resource.fsPath), true); + assert.equal(fs.readFileSync(s.resource.fsPath).length, 0); + + const stat = fs.statSync(s.resource.fsPath); + + return TPromise.timeout(10).then(() => { + return service.touchFile(s.resource).done(s => { + const statNow = fs.statSync(s.resource.fsPath); + assert.ok(statNow.mtime.getTime() >= stat.mtime.getTime()); // one some OS the resolution seems to be 1s, so we use >= here + assert.equal(statNow.size, stat.size); + + done(); + }); + }); + }); + }); + test('renameFile', function (done: () => void) { service.resolveFile(uri.file(path.join(testDir, 'index.html'))).done(source => { return service.rename(source.resource, 'other.html').then(renamed => {