diff --git a/src/LiveDevelopment/LiveDevelopment.js b/src/LiveDevelopment/LiveDevelopment.js index 0acd376e808..72eb05ea64b 100644 --- a/src/LiveDevelopment/LiveDevelopment.js +++ b/src/LiveDevelopment/LiveDevelopment.js @@ -77,15 +77,14 @@ define(function LiveDevelopment(require, exports, module) { var STATUS_SYNC_ERROR = exports.STATUS_SYNC_ERROR = 5; var Async = require("utils/Async"), - FileIndexManager = require("project/FileIndexManager"), Dialogs = require("widgets/Dialogs"), DefaultDialogs = require("widgets/DefaultDialogs"), DocumentManager = require("document/DocumentManager"), EditorManager = require("editor/EditorManager"), FileServer = require("LiveDevelopment/Servers/FileServer").FileServer, + FileSystemError = require("filesystem/FileSystemError"), FileUtils = require("file/FileUtils"), LiveDevServerManager = require("LiveDevelopment/LiveDevServerManager"), - NativeFileError = require("file/NativeFileError"), NativeApp = require("utils/NativeApp"), PreferencesDialogs = require("preferences/PreferencesDialogs"), ProjectManager = require("project/ProjectManager"), @@ -674,7 +673,7 @@ define(function LiveDevelopment(require, exports, module) { var baseUrl = ProjectManager.getBaseUrl(), hasOwnServerForLiveDevelopment = (baseUrl && baseUrl.length); - FileIndexManager.getFileInfoList("all").done(function (allFiles) { + ProjectManager.getAllFiles().done(function (allFiles) { var projectRoot = ProjectManager.getProjectRoot().fullPath, containingFolder, indexFileFound = false, @@ -1069,7 +1068,7 @@ define(function LiveDevelopment(require, exports, module) { var message; _setStatus(STATUS_ERROR); - if (err === NativeFileError.NOT_FOUND_ERR) { + if (err === FileSystemError.NOT_FOUND) { message = Strings.ERROR_CANT_FIND_CHROME; } else { message = StringUtils.format(Strings.ERROR_LAUNCHING_BROWSER, err); diff --git a/src/brackets.js b/src/brackets.js index e00dcef6770..163ff9aedfb 100644 --- a/src/brackets.js +++ b/src/brackets.js @@ -76,7 +76,7 @@ define(function (require, exports, module) { CommandManager = require("command/CommandManager"), CodeHintManager = require("editor/CodeHintManager"), PerfUtils = require("utils/PerfUtils"), - FileIndexManager = require("project/FileIndexManager"), + FileSystem = require("filesystem/FileSystem"), QuickOpen = require("search/QuickOpen"), Menus = require("command/Menus"), FileUtils = require("file/FileUtils"), @@ -89,7 +89,6 @@ define(function (require, exports, module) { Async = require("utils/Async"), UpdateNotification = require("utils/UpdateNotification"), UrlParams = require("utils/UrlParams").UrlParams, - NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, PreferencesManager = require("preferences/PreferencesManager"), Resizer = require("utils/Resizer"), LiveDevelopmentMain = require("LiveDevelopment/main"), @@ -114,8 +113,17 @@ define(function (require, exports, module) { require("extensibility/InstallExtensionDialog"); require("extensibility/ExtensionManagerDialog"); + // Compatibility shims for filesystem API migration + require("project/FileIndexManager"); + require("file/NativeFileSystem"); + require("file/NativeFileError"); + PerfUtils.addMeasurement("brackets module dependencies resolved"); + + // Initialize the file system + FileSystem.init(require("filesystem/impls/appshell/AppshellFileSystem")); + // Local variables var params = new UrlParams(); @@ -141,7 +149,7 @@ define(function (require, exports, module) { JSUtils : JSUtils, CommandManager : CommandManager, FileSyncManager : FileSyncManager, - FileIndexManager : FileIndexManager, + FileSystem : FileSystem, Menus : Menus, KeyBindingManager : KeyBindingManager, CodeHintManager : CodeHintManager, @@ -162,6 +170,7 @@ define(function (require, exports, module) { RemoteAgent : require("LiveDevelopment/Agents/RemoteAgent"), HTMLInstrumentation : require("language/HTMLInstrumentation"), MultiRangeInlineEditor : require("editor/MultiRangeInlineEditor").MultiRangeInlineEditor, + LanguageManager : LanguageManager, doneLoading : false }; @@ -172,7 +181,7 @@ define(function (require, exports, module) { function _onReady() { PerfUtils.addMeasurement("window.document Ready"); - + EditorManager.setEditorHolder($("#editor-holder")); // Let the user know Brackets doesn't run in a web browser yet @@ -223,12 +232,14 @@ define(function (require, exports, module) { if (!params.get("skipSampleProjectLoad") && !prefs.getValue("afterFirstLaunch")) { prefs.setValue("afterFirstLaunch", "true"); if (ProjectManager.isWelcomeProjectPath(initialProjectPath)) { - var dirEntry = new NativeFileSystem.DirectoryEntry(initialProjectPath); - - dirEntry.getFile("index.html", {}, function (fileEntry) { - var promise = CommandManager.execute(Commands.FILE_ADD_TO_WORKING_SET, { fullPath: fileEntry.fullPath }); - promise.then(deferred.resolve, deferred.reject); - }, deferred.reject); + FileSystem.resolve(initialProjectPath + "index.html", function (err, file) { + if (!err) { + var promise = CommandManager.execute(Commands.FILE_ADD_TO_WORKING_SET, { fullPath: file.fullPath }); + promise.then(deferred.resolve, deferred.reject); + } else { + deferred.reject(); + } + }); } else { deferred.resolve(); } @@ -333,8 +344,9 @@ define(function (require, exports, module) { // TODO: (issue 269) to support IE, need to listen to document instead (and even then it may not work when focus is in an input field?) $(window).focus(function () { + // This call to syncOpenDocuments() *should* be a no-op now that we have + // file watchers, but is still here as a safety net. FileSyncManager.syncOpenDocuments(); - FileIndexManager.markDirty(); }); // Prevent unhandled middle button clicks from triggering native behavior diff --git a/src/document/Document.js b/src/document/Document.js index f607f69c717..935e57f1251 100644 --- a/src/document/Document.js +++ b/src/document/Document.js @@ -28,9 +28,9 @@ define(function (require, exports, module) { "use strict"; - var NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, - EditorManager = require("editor/EditorManager"), + var EditorManager = require("editor/EditorManager"), FileUtils = require("file/FileUtils"), + InMemoryFile = require("document/InMemoryFile"), PerfUtils = require("utils/PerfUtils"), LanguageManager = require("language/LanguageManager"); @@ -69,7 +69,7 @@ define(function (require, exports, module) { * deleted -- When the file for this document has been deleted. All views onto the document should * be closed. The document will no longer be editable or dispatch "change" events. * - * @param {!FileEntry} file Need not lie within the project. + * @param {!File} file Need not lie within the project. * @param {!Date} initialTimestamp File's timestamp when we read it off disk. * @param {!string} rawText Text content of the file. */ @@ -90,9 +90,9 @@ define(function (require, exports, module) { Document.prototype._refCount = 0; /** - * The FileEntry for this document. Need not lie within the project. - * If Document is untitled, this is an InaccessibleFileEntry object. - * @type {!FileEntry} + * The File for this document. Need not lie within the project. + * If Document is untitled, this is an InMemoryFile object. + * @type {!File} */ Document.prototype.file = null; @@ -240,11 +240,16 @@ define(function (require, exports, module) { if (useOriginalLineEndings) { return this._text; } else { - return this._text.replace(/\r\n/g, "\n"); + return Document.normalizeText(this._text); } } }; + /** Normalizes line endings the same way CodeMirror would */ + Document.normalizeText = function (text) { + return text.replace(/\r\n/g, "\n"); + }; + /** * Sets the contents of the document. Treated as an edit. Line endings will be rewritten to * match the document's current line-ending style. @@ -415,16 +420,14 @@ define(function (require, exports, module) { // TODO: (issue #295) fetching timestamp async creates race conditions (albeit unlikely ones) var thisDoc = this; - this.file.getMetadata( - function (metadata) { - thisDoc.diskTimestamp = metadata.modificationTime; - $(exports).triggerHandler("_documentSaved", thisDoc); - }, - function (error) { + this.file.stat(function (err, stat) { + if (!err) { + thisDoc.diskTimestamp = stat.mtime; + } else { console.log("Error updating timestamp after saving file: " + thisDoc.file.fullPath); - $(exports).triggerHandler("_documentSaved", thisDoc); } - ); + $(exports).triggerHandler("_documentSaved", thisDoc); + }); }; /* (pretty toString(), to aid debugging) */ @@ -468,7 +471,7 @@ define(function (require, exports, module) { * @return {boolean} - whether or not the document is untitled */ Document.prototype.isUntitled = function () { - return this.file instanceof NativeFileSystem.InaccessibleFileEntry; + return this.file instanceof InMemoryFile; }; diff --git a/src/document/DocumentCommandHandlers.js b/src/document/DocumentCommandHandlers.js index b35b10ee63d..0436516f163 100644 --- a/src/document/DocumentCommandHandlers.js +++ b/src/document/DocumentCommandHandlers.js @@ -32,12 +32,13 @@ define(function (require, exports, module) { var AppInit = require("utils/AppInit"), CommandManager = require("command/CommandManager"), Commands = require("command/Commands"), - NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, ProjectManager = require("project/ProjectManager"), DocumentManager = require("document/DocumentManager"), EditorManager = require("editor/EditorManager"), + FileSystem = require("filesystem/FileSystem"), FileUtils = require("file/FileUtils"), FileViewController = require("project/FileViewController"), + InMemoryFile = require("document/InMemoryFile"), StringUtils = require("utils/StringUtils"), Async = require("utils/Async"), Dialogs = require("widgets/Dialogs"), @@ -211,7 +212,7 @@ define(function (require, exports, module) { EditorManager.notifyPathDeleted(fullPath); } else { // For performance, we do lazy checking of file existence, so it may be in working set - DocumentManager.removeFromWorkingSet(new NativeFileSystem.FileEntry(fullPath)); + DocumentManager.removeFromWorkingSet(FileSystem.getFileForPath(fullPath)); EditorManager.focusEditor(); } result.reject(); @@ -220,7 +221,7 @@ define(function (require, exports, module) { if (silent) { _cleanup(); } else { - FileUtils.showFileOpenError(fileError.name, fullPath).done(_cleanup); + FileUtils.showFileOpenError(fileError, fullPath).done(_cleanup); } }); } @@ -256,8 +257,8 @@ define(function (require, exports, module) { _defaultOpenDialogFullPath = ProjectManager.getProjectRoot().fullPath; } // Prompt the user with a dialog - NativeFileSystem.showOpenDialog(true, false, Strings.OPEN_FILE, _defaultOpenDialogFullPath, - null, function (paths) { + FileSystem.showOpenDialog(true, false, Strings.OPEN_FILE, _defaultOpenDialogFullPath, null, function (err, paths) { + if (!err) { if (paths.length > 0) { // Add all files to the working set without verifying that // they still exist on disk (for faster opening) @@ -265,7 +266,7 @@ define(function (require, exports, module) { filteredPaths = DragAndDrop.filterFilesToOpen(paths); filteredPaths.forEach(function (file) { - filesToOpen.push(new NativeFileSystem.FileEntry(file)); + filesToOpen.push(FileSystem.getFileForPath(file)); }); DocumentManager.addListToWorkingSet(filesToOpen); @@ -284,7 +285,8 @@ define(function (require, exports, module) { // Reject if the user canceled the dialog result.reject(); } - }); + } + }); } else { result = doOpen(fullPath, silent); } @@ -383,7 +385,6 @@ define(function (require, exports, module) { function _getUntitledFileSuggestion(dir, baseFileName, fileExt, isFolder) { var result = new $.Deferred(); var suggestedName = baseFileName + "-" + _nextUntitledIndexToUse++ + fileExt; - var dirEntry = new NativeFileSystem.DirectoryEntry(dir); result.progress(function attemptNewName(suggestedName) { if (_nextUntitledIndexToUse > 99) { @@ -391,32 +392,18 @@ define(function (require, exports, module) { result.reject(); return; } - - //check this name - var successCallback = function (entry) { - //file exists, notify to the next progress - result.notify(baseFileName + "-" + _nextUntitledIndexToUse++ + fileExt); - }; - var errorCallback = function (error) { - //most likely error is FNF, user is better equiped to handle the rest - result.resolve(suggestedName); - }; - if (isFolder) { - dirEntry.getDirectory( - suggestedName, - {}, - successCallback, - errorCallback - ); - } else { - dirEntry.getFile( - suggestedName, - {}, - successCallback, - errorCallback - ); - } + var path = dir + "/" + suggestedName; + var entry = isFolder ? FileSystem.getDirectoryForPath(path) : FileSystem.getFileForPath(path); + + entry.exists(function (exists) { + if (exists) { + //file exists, notify to the next progress + result.notify(baseFileName + "-" + _nextUntitledIndexToUse++ + fileExt); + } else { + result.resolve(suggestedName); + } + }); }); //kick it off @@ -452,7 +439,7 @@ define(function (require, exports, module) { // ProjectManager.createNewItem() ignores the baseDir we give it and falls back to the project root on its own) var baseDir, selected = ProjectManager.getSelectedItem(); - if ((!selected) || (selected instanceof NativeFileSystem.InaccessibleFileEntry)) { + if ((!selected) || (selected instanceof InMemoryFile)) { selected = ProjectManager.getProjectRoot(); } @@ -523,46 +510,34 @@ define(function (require, exports, module) { /** * Saves a document to its existing path. Does NOT support untitled documents. * @param {!Document} docToSave - * @return {$.Promise} a promise that is resolved with the FileEntry of docToSave (to mirror + * @return {$.Promise} a promise that is resolved with the File of docToSave (to mirror * the API of _doSaveAs()). Rejected in case of IO error (after error dialog dismissed). */ function doSave(docToSave) { var result = new $.Deferred(), - fileEntry = docToSave.file; + file = docToSave.file; function handleError(error) { - _showSaveFileError(error.name, fileEntry.fullPath) + _showSaveFileError(error, file.fullPath) .done(function () { result.reject(error); }); } - + if (docToSave.isDirty) { var writeError = false; - fileEntry.createWriter( - function (writer) { - writer.onwriteend = function () { - // Per spec, onwriteend is called after onerror too - if (!writeError) { - docToSave.notifySaved(); - result.resolve(fileEntry); - } - }; - writer.onerror = function (error) { - writeError = true; - handleError(error); - }; - - // We don't want normalized line endings, so it's important to pass true to getText() - writer.write(docToSave.getText(true)); - }, - function (error) { - handleError(error); - } - ); + // We don't want normalized line endings, so it's important to pass true to getText() + FileUtils.writeText(file, docToSave.getText(true)) + .done(function () { + docToSave.notifySaved(); + result.resolve(file); + }) + .fail(function (err) { + handleError(err); + }); } else { - result.resolve(fileEntry); + result.resolve(file); } result.always(function () { EditorManager.focusEditor(); @@ -574,7 +549,7 @@ define(function (require, exports, module) { * Reverts the Document to the current contents of its file on disk. Discards any unsaved changes * in the Document. * @param {Document} doc - * @return {$.Promise} a Promise that's resolved when done, or rejected with a NativeFileError if the + * @return {$.Promise} a Promise that's resolved when done, or rejected with a FileSystemError if the * file cannot be read (after showing an error dialog to the user). */ function doRevert(doc) { @@ -586,7 +561,7 @@ define(function (require, exports, module) { result.resolve(); }) .fail(function (error) { - FileUtils.showFileOpenError(error.name, doc.file.fullPath) + FileUtils.showFileOpenError(error, doc.file.fullPath) .done(function () { result.reject(error); }); @@ -605,7 +580,7 @@ define(function (require, exports, module) { * @param {?{cursorPos:!Object, selection:!Object, scrollPos:!Object}} settings - properties of * the original document's editor that need to be carried over to the new document * i.e. scrollPos, cursorPos and text selection - * @return {$.Promise} a promise that is resolved with the saved file's FileEntry. Rejected in + * @return {$.Promise} a promise that is resolved with the saved document's File. Rejected in * case of IO error (after error dialog dismissed), or if the Save dialog was canceled. */ function _doSaveAs(doc, settings) { @@ -615,7 +590,7 @@ define(function (require, exports, module) { result = new $.Deferred(); function _doSaveAfterSaveDialog(path) { - var newFile = new NativeFileSystem.FileEntry(path); + var newFile; // Reconstruct old doc's editor's view state, & finally resolve overall promise function _configureEditorAndResolve() { @@ -663,6 +638,7 @@ define(function (require, exports, module) { } // First, write document's current text to new file + newFile = FileSystem.getFileForPath(path); FileUtils.writeText(newFile, doc.getText()).done(function () { // Add new file to project tree ProjectManager.refreshFileTree().done(function () { @@ -681,7 +657,7 @@ define(function (require, exports, module) { result.reject(error); }); }).fail(function (error) { - _showSaveFileError(error.name, path) + _showSaveFileError(error, path) .done(function () { result.reject(error); }); @@ -703,11 +679,13 @@ define(function (require, exports, module) { saveAsDefaultPath = FileUtils.getDirectoryPath(origPath); } defaultName = FileUtils.getBaseName(origPath); - NativeFileSystem.showSaveDialog(Strings.SAVE_FILE_AS, saveAsDefaultPath, defaultName, - _doSaveAfterSaveDialog, - function (error) { - result.reject(error); - }); + FileSystem.showSaveDialog(Strings.SAVE_FILE_AS, saveAsDefaultPath, defaultName, function (err, selection) { + if (!err) { + _doSaveAfterSaveDialog(selection); + } else { + result.reject(err); + } + }); } else { result.reject(); } @@ -717,7 +695,7 @@ define(function (require, exports, module) { /** * Saves the given file. If no file specified, assumes the current document. * @param {?{doc: ?Document}} commandData Document to close, or null - * @return {$.Promise} resolved with the saved file's FileEntry (which MAY DIFFER from the doc + * @return {$.Promise} resolved with the saved document's File (which MAY DIFFER from the doc * passed in, if the doc was untitled). Rejected in case of IO error (after error dialog * dismissed), or if doc was untitled and the Save dialog was canceled (will be rejected with * USER_CANCELED object). @@ -747,14 +725,28 @@ define(function (require, exports, module) { return $.Deferred().reject().promise(); } + /** + * Saves all unsaved documents. Returns a Promise that will be resolved once ALL the save + * operations have been completed. If ANY save operation fails, an error dialog is immediately + * shown but after dismissing we continue saving the other files; after all files have been + * processed, the Promise is rejected if any ONE save operation failed (the error given is the + * first one encountered). If the user cancels any Save As dialog (for untitled files), the + * Promise is immediately rejected. + * + * @param {!Array.} fileList + * @return {!$.Promise} Resolved with {!Array.}, which may differ from 'fileList' + * if any of the files were Unsaved documents. Or rejected with {?FileSystemError}. + */ function _saveFileList(fileList) { // Do in serial because doSave shows error UI for each file, and we don't want to stack // multiple dialogs on top of each other - var userCanceled = false; + var userCanceled = false, + filesAfterSave = []; + return Async.doSequentially( fileList, function (file) { - // Abort remaining saves if user canceled any Save dialog + // Abort remaining saves if user canceled any Save As dialog if (userCanceled) { return (new $.Deferred()).reject().promise(); } @@ -764,8 +756,7 @@ define(function (require, exports, module) { var savePromise = handleFileSave({doc: doc}); savePromise .done(function (newFile) { - file.fullPath = newFile.fullPath; - file.name = newFile.name; + filesAfterSave.push(newFile); }) .fail(function (error) { if (error === USER_CANCELED) { @@ -775,11 +766,14 @@ define(function (require, exports, module) { return savePromise; } else { // working set entry that was never actually opened - ignore + filesAfterSave.push(file); return (new $.Deferred()).resolve().promise(); } }, - false - ); + false // if any save fails, continue trying to save other files anyway; then reject at end + ).then(function () { + return filesAfterSave; + }); } /** @@ -834,7 +828,7 @@ define(function (require, exports, module) { * Closes the specified file: removes it from the working set, and closes the main editor if one * is open. Prompts user about saving changes first, if document is dirty. * - * @param {?{file: FileEntry, promptOnly:boolean}} commandData Optional bag of arguments: + * @param {?{file: File, promptOnly:boolean}} commandData Optional bag of arguments: * file - File to close; assumes the current document if not specified. * promptOnly - If true, only displays the relevant confirmation UI and does NOT actually * close the document. This is useful when chaining file-close together with other user @@ -856,10 +850,10 @@ define(function (require, exports, module) { } // utility function for handleFileClose: closes document & removes from working set - function doClose(fileEntry) { + function doClose(file) { if (!promptOnly) { // This selects a different document if the working set has any other options - DocumentManager.closeFullEditor(fileEntry); + DocumentManager.closeFullEditor(file); EditorManager.focusEditor(); } @@ -944,8 +938,8 @@ define(function (require, exports, module) { } else if (id === Dialogs.DIALOG_BTN_OK) { // "Save" case: wait until we confirm save has succeeded before closing handleFileSave({doc: doc}) - .done(function (newFileEntry) { - doClose(newFileEntry); + .done(function (newFile) { + doClose(newFile); result.resolve(); }) .fail(function () { @@ -978,7 +972,7 @@ define(function (require, exports, module) { } return promise; } - + function _doCloseDocumentList(list, promptOnly, clearCurrentDoc) { var result = new $.Deferred(), unsavedDocs = []; @@ -1046,13 +1040,14 @@ define(function (require, exports, module) { result.reject(); } else if (id === Dialogs.DIALOG_BTN_OK) { // Save all unsaved files, then if that succeeds, close all - _saveFileList(list).done(function () { - result.resolve(); + _saveFileList(list).done(function (listAfterSave) { + // List of files after save may be different, if any were Untitled + result.resolve(listAfterSave); }).fail(function () { result.reject(); }); } else { - // "Don't Save" case--we can just go ahead and close all files. + // "Don't Save" case--we can just go ahead and close all files. result.resolve(); } }); @@ -1061,9 +1056,10 @@ define(function (require, exports, module) { // If all the unsaved-changes confirmations pan out above, then go ahead & close all editors // NOTE: this still happens before any done() handlers added by our caller, because jQ // guarantees that handlers run in the order they are added. - result.done(function () { + result.done(function (listAfterSave) { + listAfterSave = listAfterSave || list; if (!promptOnly) { - DocumentManager.removeListFromWorkingSet(list, (clearCurrentDoc || true)); + DocumentManager.removeListFromWorkingSet(listAfterSave, (clearCurrentDoc || true)); } }); diff --git a/src/document/DocumentManager.js b/src/document/DocumentManager.js index 8495d55d3fa..887497c3c24 100644 --- a/src/document/DocumentManager.js +++ b/src/document/DocumentManager.js @@ -63,13 +63,13 @@ * * To listen for working set changes, you must listen to *all* of these events: * - workingSetAdd -- When a file is added to the working set (see getWorkingSet()). The 2nd arg - * to the listener is the added FileEntry, and the 3rd arg is the index it was inserted at. + * to the listener is the added File, and the 3rd arg is the index it was inserted at. * - workingSetAddList -- When multiple files are added to the working set (e.g. project open, multiple file open). - * The 2nd arg to the listener is the array of added FileEntry objects. + * The 2nd arg to the listener is the array of added File objects. * - workingSetRemove -- When a file is removed from the working set (see getWorkingSet()). The - * 2nd arg to the listener is the removed FileEntry. + * 2nd arg to the listener is the removed File. * - workingSetRemoveList -- When multiple files are removed from the working set (e.g. project close). - * The 2nd arg to the listener is the array of removed FileEntry objects. + * The 2nd arg to the listener is the array of removed File objects. * - workingSetSort -- When the workingSet array is reordered without additions or removals. * Listener receives no arguments. * @@ -91,12 +91,13 @@ define(function (require, exports, module) { var _ = require("thirdparty/lodash"); var DocumentModule = require("document/Document"), - NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, ProjectManager = require("project/ProjectManager"), EditorManager = require("editor/EditorManager"), FileSyncManager = require("project/FileSyncManager"), + FileSystem = require("filesystem/FileSystem"), PreferencesManager = require("preferences/PreferencesManager"), FileUtils = require("file/FileUtils"), + InMemoryFile = require("document/InMemoryFile"), CommandManager = require("command/CommandManager"), Async = require("utils/Async"), PerfUtils = require("utils/PerfUtils"), @@ -134,7 +135,7 @@ define(function (require, exports, module) { /** * @private - * @type {Array.} + * @type {Array.} * @see DocumentManager.getWorkingSet() */ var _workingSet = []; @@ -142,14 +143,14 @@ define(function (require, exports, module) { /** * @private * Contains the same set of items as _workingSet, but ordered by how recently they were _currentDocument (0 = most recent). - * @type {Array.} + * @type {Array.} */ var _workingSetMRUOrder = []; /** * @private * Contains the same set of items as _workingSet, but ordered in the way they where added to _workingSet (0 = last added). - * @type {Array.} + * @type {Array.} */ var _workingSetAddedOrder = []; @@ -160,7 +161,7 @@ define(function (require, exports, module) { var _documentNavPending = false; /** - * All documents with refCount > 0. Maps Document.file.fullPath -> Document. + * All documents with refCount > 0. Maps Document.file.id -> Document. * @private * @type {Object.} */ @@ -176,7 +177,7 @@ define(function (require, exports, module) { * Which items belong in the working set is managed entirely by DocumentManager. Callers cannot * (yet) change this collection on their own. * - * @return {Array.} + * @return {Array.} */ function getWorkingSet() { return _.clone(_workingSet); @@ -186,7 +187,7 @@ define(function (require, exports, module) { * Returns the index of the file matching fullPath in the working set. * Returns -1 if not found. * @param {!string} fullPath - * @param {Array.=} list Pass this arg to search a different array of files. Internal + * @param {Array.=} list Pass this arg to search a different array of files. Internal * use only. * @returns {number} index */ @@ -216,10 +217,10 @@ define(function (require, exports, module) { */ function getAllOpenDocuments() { var result = []; - var path; - for (path in _openDocuments) { - if (_openDocuments.hasOwnProperty(path)) { - result.push(_openDocuments[path]); + var id; + for (id in _openDocuments) { + if (_openDocuments.hasOwnProperty(id)) { + result.push(_openDocuments[id]); } } return result; @@ -230,7 +231,7 @@ define(function (require, exports, module) { * Adds the given file to the end of the working set list, if it is not already in the list * and it does not have a custom viewer. * Does not change which document is currently open in the editor. Completes synchronously. - * @param {!FileEntry} file + * @param {!File} file * @param {number=} index Position to add to list (defaults to last); -1 is ignored * @param {boolean=} forceRedraw If true, a working set change notification is always sent * (useful if suppressRedraw was used with removeFromWorkingSet() earlier) @@ -254,14 +255,7 @@ define(function (require, exports, module) { } return; } - - // Add to _workingSet making sure we store a different instance from the - // one in the Document. See issue #1971 for more details. - if (file instanceof NativeFileSystem.InaccessibleFileEntry) { - file = new NativeFileSystem.InaccessibleFileEntry(file.fullPath, file.mtime); - } else { - file = new NativeFileSystem.FileEntry(file.fullPath); - } + if (!indexRequested) { // If no index is specified, just add the file to the end of the working set. _workingSet.push(file); @@ -294,7 +288,7 @@ define(function (require, exports, module) { * Does not change which document is currently open in the editor. * More efficient than calling addToWorkingSet() (in a loop) for * a list of files because there's only 1 redraw at the end - * @param {!FileEntryArray} fileList + * @param {!Array.} fileList */ function addListToWorkingSet(fileList) { var uniqueFileList = []; @@ -331,7 +325,7 @@ define(function (require, exports, module) { * Warning: low level API - use FILE_CLOSE command in most cases. * Removes the given file from the working set list, if it was in the list. Does not change * the current editor even if it's for this file. Does not prompt for unsaved changes. - * @param {!FileEntry} file + * @param {!File} file * @param {boolean=} true to suppress redraw after removal */ function removeFromWorkingSet(file, suppressRedraw) { @@ -399,7 +393,7 @@ define(function (require, exports, module) { /** * Sorts _workingSet using the compare function - * @param {function(FileEntry, FileEntry): number} compareFn The function that will be used inside JavaScript's + * @param {function(File, File): number} compareFn The function that will be used inside JavaScript's * sort function. The return a value should be >0 (sort a to a lower index than b), =0 (leaves a and b * unchanged with respect to each other) or <0 (sort b to a lower index than a) and must always returns * the same value when given a specific pair of elements a and b as its two arguments. @@ -438,7 +432,7 @@ define(function (require, exports, module) { * Get the next or previous file in the working set, in MRU order (relative to currentDocument). May * return currentDocument itself if working set is length 1. * @param {number} inc -1 for previous, +1 for next; no other values allowed - * @return {?FileEntry} null if working set empty + * @return {?File} null if working set empty */ function getNextPrevFile(inc) { if (inc !== -1 && inc !== +1) { @@ -532,7 +526,7 @@ define(function (require, exports, module) { * * This is a subset of notifyFileDeleted(). Use this for the user-facing Close command. * - * @param {!FileEntry} file + * @param {!File} file * @param {boolean} skipAutoSelect - if true, don't automatically open and select the next document */ function closeFullEditor(file, skipAutoSelect) { @@ -625,6 +619,29 @@ define(function (require, exports, module) { }); } + /** + * Returns the existing open Document for the given file, or null if the file is not open ('open' + * means referenced by the UI somewhere). If you will hang onto the Document, you must addRef() + * it; see {@link getDocumentForPath()} for details. + * @param {!string} fullPath + * @return {?Document} + */ + function getOpenDocumentForPath(fullPath) { + var id; + + // Need to walk all open documents and check for matching path. We can't + // use getFileForPath(fullPath).id since the file it returns won't match + // an Untitled document's InMemoryFile. + for (id in _openDocuments) { + if (_openDocuments.hasOwnProperty(id)) { + if (_openDocuments[id].file.fullPath === fullPath) { + return _openDocuments[id]; + } + } + } + return null; + } + /** * Gets an existing open Document for the given file, or creates a new one if the Document is * not currently open ('open' means referenced by the UI somewhere). Always use this method to @@ -638,57 +655,63 @@ define(function (require, exports, module) { * * @param {!string} fullPath * @return {$.Promise} A promise object that will be resolved with the Document, or rejected - * with a NativeFileError if the file is not yet open and can't be read from disk. + * with a FileSystemError if the file is not yet open and can't be read from disk. */ function getDocumentForPath(fullPath) { - var doc = _openDocuments[fullPath], - pendingPromise = getDocumentForPath._pendingDocumentPromises[fullPath]; + var doc = getOpenDocumentForPath(fullPath); if (doc) { // use existing document return new $.Deferred().resolve(doc).promise(); - } else if (pendingPromise) { - // wait for the result of a previous request - return pendingPromise; } else { - var result = new $.Deferred(), - promise = result.promise(); - - // create a new document - var perfTimerName = PerfUtils.markStart("getDocumentForPath:\t" + fullPath); - - result.done(function () { - PerfUtils.addMeasurement(perfTimerName); - }).fail(function () { - PerfUtils.finalizeMeasurement(perfTimerName); - }); - var fileEntry; + // Should never get here if the fullPath refers to an Untitled document if (fullPath.indexOf(_untitledDocumentPath) === 0) { console.error("getDocumentForPath called for non-open untitled document: " + fullPath); - result.reject(); + return new $.Deferred().reject().promise(); + } + + var file = FileSystem.getFileForPath(fullPath), + pendingPromise = getDocumentForPath._pendingDocumentPromises[file.id]; + + if (pendingPromise) { + // wait for the result of a previous request + return pendingPromise; } else { - // log this document's Promise as pending - getDocumentForPath._pendingDocumentPromises[fullPath] = promise; - - fileEntry = new NativeFileSystem.FileEntry(fullPath); + var result = new $.Deferred(), + promise = result.promise(); - FileUtils.readAsText(fileEntry) + // log this document's Promise as pending + getDocumentForPath._pendingDocumentPromises[file.id] = promise; + + // create a new document + var perfTimerName = PerfUtils.markStart("getDocumentForPath:\t" + fullPath); + + result.done(function () { + PerfUtils.addMeasurement(perfTimerName); + }).fail(function () { + PerfUtils.finalizeMeasurement(perfTimerName); + }); + + FileUtils.readAsText(file) .always(function () { // document is no longer pending - delete getDocumentForPath._pendingDocumentPromises[fullPath]; + delete getDocumentForPath._pendingDocumentPromises[file.id]; }) .done(function (rawText, readTimestamp) { - doc = new DocumentModule.Document(fileEntry, readTimestamp, rawText); + doc = new DocumentModule.Document(file, readTimestamp, rawText); + + // This is a good point to clean up any old dangling Documents + _gcDocuments(); + result.resolve(doc); }) .fail(function (fileError) { result.reject(fileError); }); + + return promise; } - // This is a good point to clean up any old dangling Documents - result.done(_gcDocuments); - return promise; } } @@ -697,38 +720,61 @@ define(function (require, exports, module) { * to request the same document simultaneously before the initial request has completed. * In particular, this happens at app startup where the working set is created and the * intial active document is opened in an editor. This is essential to ensure that only - * 1 Document exists for any FileEntry. + * one Document exists for any File. * @private * @type {Object.} */ getDocumentForPath._pendingDocumentPromises = {}; /** - * Returns the existing open Document for the given file, or null if the file is not open ('open' - * means referenced by the UI somewhere). If you will hang onto the Document, you must addRef() - * it; see {@link getDocumentForPath()} for details. - * @param {!string} fullPath - * @return {?Document} + * Gets the text of a Document (including any unsaved changes), or would-be Document if the + * file is not actually open. More efficient than getDocumentForPath(). Use when you're reading + * document(s) but don't need to hang onto a Document object. + * + * If the file is open this is equivalent to calling getOpenDocumentForPath().getText(). If the + * file is NOT open, this is like calling getDocumentForPath()...getText() but more efficient. + * Differs from plain FileUtils.readAsText() in two ways: (a) line endings are still normalized + * as in Document.getText(); (b) unsaved changes are returned if there are any. + * + * @param {!File} file + * @return {!string} */ - function getOpenDocumentForPath(fullPath) { - return _openDocuments[fullPath]; + function getDocumentText(file) { + var result = new $.Deferred(), + doc = getOpenDocumentForPath(file.fullPath); + if (doc) { + result.resolve(doc.getText()); + } else { + file.read(function (err, contents) { + if (err) { + result.reject(err); + } else { + // Normalize line endings the same way Document would, but don't actually + // new up a Document (which entails a bunch of object churn). + contents = DocumentModule.Document.normalizeText(contents); + result.resolve(contents); + } + }); + } + return result.promise(); } + /** - * Creates an untitled document. The associated FileEntry has a fullPath + * Creates an untitled document. The associated File has a fullPath that * looks like /some-random-string/Untitled-counter.fileExt. * - * @param {number} counter - used in the name of the new Document's FileEntry - * @param {string} fileExt - file extension of the new Document's FileEntry + * @param {number} counter - used in the name of the new Document's File + * @param {string} fileExt - file extension of the new Document's File * @return {Document} - a new untitled Document */ function createUntitledDocument(counter, fileExt) { var filename = Strings.UNTITLED + "-" + counter + fileExt, fullPath = _untitledDocumentPath + "/" + filename, now = new Date(), - fileEntry = new NativeFileSystem.InaccessibleFileEntry(fullPath, now); + file = new InMemoryFile(fullPath, FileSystem); - return new DocumentModule.Document(fileEntry, now, ""); + return new DocumentModule.Document(file, now, ""); } /** @@ -742,7 +788,7 @@ define(function (require, exports, module) { * FUTURE: Instead of an explicit notify, we should eventually listen for deletion events on some * sort of "project file model," making this just a private event handler. * - * @param {!FileEntry} file + * @param {!File} file * @param {boolean} skipAutoSelect - if true, don't automatically open/select the next document */ function notifyFileDeleted(file, skipAutoSelect) { @@ -780,7 +826,7 @@ define(function (require, exports, module) { workingSet.forEach(function (file, index) { // Do not persist untitled document paths - if (!(file instanceof NativeFileSystem.InaccessibleFileEntry)) { + if (!(file instanceof InMemoryFile)) { // flag the currently active editor isActive = currentDoc && (file.fullPath === currentDoc.file.fullPath); @@ -821,7 +867,7 @@ define(function (require, exports, module) { // Add all files to the working set without verifying that // they still exist on disk (for faster project switching) files.forEach(function (value, index) { - filesToOpen.push(new NativeFileSystem.FileEntry(value.file)); + filesToOpen.push(FileSystem.getFileForPath(value.file)); if (value.active) { activeFile = value.file; } @@ -855,31 +901,12 @@ define(function (require, exports, module) { * @param {boolean} isFolder True if path is a folder; False if it is a file. */ function notifyPathNameChanged(oldName, newName, isFolder) { - // Update open documents. This will update _currentDocument too, since - // the current document is always open. - var keysToDelete = []; - _.forEach(_openDocuments, function (doc, path) { - if (FileUtils.isAffectedWhenRenaming(path, oldName, newName, isFolder)) { - // Copy value to new key - var newKey = path.replace(oldName, newName); - _openDocuments[newKey] = doc; - - keysToDelete.push(path); - - // Update document file - FileUtils.updateFileEntryPath(doc.file, oldName, newName, isFolder); - doc._notifyFilePathChanged(); - } - }); - - // Delete the old keys - keysToDelete.forEach(function (fullPath) { - delete _openDocuments[fullPath]; - }); - - // Update working set - _workingSet.forEach(function (fileEntry) { - FileUtils.updateFileEntryPath(fileEntry, oldName, newName, isFolder); + // Notify all open documents + _.forEach(_openDocuments, function (doc, id) { + // TODO: Only notify affected documents? For now _notifyFilePathChange + // just updates the language if the extension changed, so it's fine + // to call for all open docs. + doc._notifyFilePathChanged(); }); // Send a "fileNameChanged" event. This will trigger the views to update. @@ -933,22 +960,22 @@ define(function (require, exports, module) { // For compatibility $(DocumentModule) .on("_afterDocumentCreate", function (event, doc) { - if (_openDocuments[doc.file.fullPath]) { + if (_openDocuments[doc.file.id]) { console.error("Document for this path already in _openDocuments!"); return true; } - _openDocuments[doc.file.fullPath] = doc; + _openDocuments[doc.file.id] = doc; $(exports).triggerHandler("afterDocumentCreate", doc); }) .on("_beforeDocumentDelete", function (event, doc) { - if (!_openDocuments[doc.file.fullPath]) { + if (!_openDocuments[doc.file.id]) { console.error("Document with references was not in _openDocuments!"); return true; } $(exports).triggerHandler("beforeDocumentDelete", doc); - delete _openDocuments[doc.file.fullPath]; + delete _openDocuments[doc.file.id]; }) .on("_documentRefreshed", function (event, doc) { $(exports).triggerHandler("documentRefreshed", doc); @@ -972,6 +999,7 @@ define(function (require, exports, module) { exports._clearCurrentDocument = _clearCurrentDocument; exports.getDocumentForPath = getDocumentForPath; exports.getOpenDocumentForPath = getOpenDocumentForPath; + exports.getDocumentText = getDocumentText; exports.createUntitledDocument = createUntitledDocument; exports.getWorkingSet = getWorkingSet; exports.findInWorkingSet = findInWorkingSet; diff --git a/src/document/InMemoryFile.js b/src/document/InMemoryFile.js new file mode 100644 index 00000000000..5fd275dbd54 --- /dev/null +++ b/src/document/InMemoryFile.js @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + + +/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */ +/*global define, $ */ + +/** + * Represents a file that will never exist on disk - a placeholder backing file for untitled Documents. NO ONE + * other than DocumentManager should create instances of InMemoryFile. It is valid to test for one (`instanceof + * InMemoryFile`), but it's better to check `doc.isUntitled` where possible. + * + * Attempts to read/write an InMemoryFile will always fail, and exists() always yields false. InMemoryFile.fullPath + * is just a placeholder, and should not be displayed anywhere in the UI. + * + * An InMemoryFile is not added to the filesystem index, so if you ask the the filesystem anything about this + * object, it won't know what you're talking about (`filesystem.getFileForPath(someInMemFile.fullPath)` will not + * return someInMemFile). + */ +define(function (require, exports, module) { + "use strict"; + + var File = require("filesystem/File"), + FileSystemError = require("filesystem/FileSystemError"); + + function InMemoryFile(fullPath, fileSystem) { + File.call(this, fullPath, fileSystem); + } + + InMemoryFile.prototype = Object.create(File.prototype); + InMemoryFile.prototype.constructor = InMemoryFile; + InMemoryFile.prototype.parentClass = File.prototype; + + + // Stub out invalid calls inherited from File + + /** + * Reject any attempts to read the file. + * + * Read a file as text. + * + * @param {object=} options Currently unused. + * @param {function (number, string, object)} callback + */ + InMemoryFile.prototype.read = function (options, callback) { + if (typeof (options) === "function") { + callback = options; + } + callback(FileSystemError.NOT_FOUND); + }; + + /** + * Rejects any attempts to write the file. + * + * @param {string} data Data to write. + * @param {string=} encoding Encoding for data. Defaults to UTF-8. + * @param {!function (err, object)} callback Callback that is passed the + * error code and the file's new stats if the write is sucessful. + */ + InMemoryFile.prototype.write = function (data, encoding, callback) { + if (typeof (encoding) === "function") { + callback = encoding; + } + callback(FileSystemError.NOT_FOUND); + }; + + + // Stub out invalid calls inherited from FileSystemEntry + + InMemoryFile.prototype.exists = function (callback) { + callback(false); + }; + + InMemoryFile.prototype.stat = function (callback) { + callback(FileSystemError.NOT_FOUND); + }; + + InMemoryFile.prototype.unlink = function (callback) { + callback(FileSystemError.NOT_FOUND); + }; + + InMemoryFile.prototype.rename = function (newName, callback) { + callback(FileSystemError.NOT_FOUND); + }; + + InMemoryFile.prototype.moveToTrash = function (callback) { + callback(FileSystemError.NOT_FOUND); + }; + + // Export this class + module.exports = InMemoryFile; +}); diff --git a/src/editor/CSSInlineEditor.js b/src/editor/CSSInlineEditor.js index 0c65b6608e5..a8a8b752221 100644 --- a/src/editor/CSSInlineEditor.js +++ b/src/editor/CSSInlineEditor.js @@ -36,7 +36,7 @@ define(function (require, exports, module) { DropdownEventHandler = require("utils/DropdownEventHandler").DropdownEventHandler, EditorManager = require("editor/EditorManager"), Editor = require("editor/Editor").Editor, - FileIndexManager = require("project/FileIndexManager"), + ProjectManager = require("project/ProjectManager"), HTMLUtils = require("language/HTMLUtils"), Menus = require("command/Menus"), MultiRangeInlineEditor = require("editor/MultiRangeInlineEditor"), @@ -49,6 +49,10 @@ define(function (require, exports, module) { var _newRuleCmd, _newRuleHandlers = []; + function _getCSSFilesInProject() { + return ProjectManager.getAllFiles(ProjectManager.getLanguageFilter("css")); + } + /** * Given a position in an HTML editor, returns the relevant selector for the attribute/tag * surrounding that position, or "" if none is found. @@ -277,7 +281,7 @@ define(function (require, exports, module) { */ function _getNoRulesMsg() { var result = new $.Deferred(); - FileIndexManager.getFileInfoList("css").done(function (fileInfos) { + _getCSSFilesInProject().done(function (fileInfos) { result.resolve(fileInfos.length ? Strings.CSS_QUICK_EDIT_NO_MATCHES : Strings.CSS_QUICK_EDIT_NO_STYLESHEETS); }); return result; @@ -395,7 +399,7 @@ define(function (require, exports, module) { result.resolve(cssInlineEditor); // Now that dialog has been built, collect list of stylesheets - var stylesheetsPromise = FileIndexManager.getFileInfoList("css"); + var stylesheetsPromise = _getCSSFilesInProject(); // After both the stylesheets are loaded and the inline editor has been added to the DOM, // update the UI accordingly. (Those can happen in either order, so we need to wait for both.) diff --git a/src/editor/ImageViewer.js b/src/editor/ImageViewer.js index 850bd870bce..39954c2db4b 100644 --- a/src/editor/ImageViewer.js +++ b/src/editor/ImageViewer.js @@ -30,11 +30,11 @@ define(function (require, exports, module) { var DocumentManager = require("document/DocumentManager"), EditorManager = require("editor/EditorManager"), ImageHolderTemplate = require("text!htmlContent/image-holder.html"), - NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, PanelManager = require("view/PanelManager"), ProjectManager = require("project/ProjectManager"), Strings = require("strings"), - StringUtils = require("utils/StringUtils"); + StringUtils = require("utils/StringUtils"), + FileSystem = require("filesystem/FileSystem"); var _naturalWidth = 0; @@ -100,19 +100,18 @@ define(function (require, exports, module) { _naturalWidth = this.naturalWidth; var dimensionString = _naturalWidth + " × " + this.naturalHeight + " " + Strings.UNIT_PIXELS; // get image size - var fileEntry = new NativeFileSystem.FileEntry(fullPath); - fileEntry.getMetadata( - function (metadata) { + var file = FileSystem.getFileForPath(fullPath); + file.stat(function (err, stat) { + if (err) { + $("#img-data").html(dimensionString); + } else { var sizeString = ""; - if (metadata && metadata.size) { - sizeString = " — " + StringUtils.prettyPrintBytes(metadata.size, 2); + if (stat.size) { + sizeString = " — " + StringUtils.prettyPrintBytes(stat.size, 2); } $("#img-data").html(dimensionString + sizeString); - }, - function (error) { - $("#img-data").html(dimensionString); } - ); + }); $("#image-holder").show(); // listen to resize to update the scale sticker $(PanelManager).on("editorAreaResize", _onEditorAreaResize); diff --git a/src/extensibility/ExtensionManager.js b/src/extensibility/ExtensionManager.js index e1fdcecc964..773be271f25 100644 --- a/src/extensibility/ExtensionManager.js +++ b/src/extensibility/ExtensionManager.js @@ -40,10 +40,10 @@ define(function (require, exports, module) { "use strict"; var FileUtils = require("file/FileUtils"), - NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, Package = require("extensibility/Package"), Async = require("utils/Async"), ExtensionLoader = require("utils/ExtensionLoader"), + FileSystem = require("filesystem/FileSystem"), Strings = require("strings"), StringUtils = require("utils/StringUtils"); @@ -176,8 +176,9 @@ define(function (require, exports, module) { * or rejected if there is no package.json or the contents are not valid JSON. */ function _loadPackageJson(folder) { - var result = new $.Deferred(); - FileUtils.readAsText(new NativeFileSystem.FileEntry(folder + "/package.json")) + var file = FileSystem.getFileForPath(folder + "/package.json"), + result = new $.Deferred(); + FileUtils.readAsText(file) .done(function (text) { try { var json = JSON.parse(text); @@ -375,7 +376,7 @@ define(function (require, exports, module) { Object.keys(_idsToUpdate).forEach(function (id) { var filename = _idsToUpdate[id].localPath; if (filename) { - brackets.fs.unlink(filename, function () { }); + FileSystem.getFileForPath(filename).unlink(); } }); _idsToUpdate = {}; @@ -449,8 +450,7 @@ define(function (require, exports, module) { return; } if (installationResult.localPath) { - brackets.fs.unlink(installationResult.localPath, function () { - }); + FileSystem.getFileForPath(installationResult.localPath).unlink(); } delete _idsToUpdate[id]; $(exports).triggerHandler("statusChange", [id]); diff --git a/src/extensibility/InstallExtensionDialog.js b/src/extensibility/InstallExtensionDialog.js index 3cbc4549f80..902bca9bcec 100644 --- a/src/extensibility/InstallExtensionDialog.js +++ b/src/extensibility/InstallExtensionDialog.js @@ -33,6 +33,7 @@ define(function (require, exports, module) { Strings = require("strings"), Commands = require("command/Commands"), CommandManager = require("command/CommandManager"), + FileSystem = require("filesystem/FileSystem"), KeyEvent = require("utils/KeyEvent"), Package = require("extensibility/Package"), NativeApp = require("utils/NativeApp"), @@ -253,9 +254,7 @@ define(function (require, exports, module) { // and the user cancels, we can delete the downloaded file. if (this._installResult && this._installResult.localPath) { var filename = this._installResult.localPath; - brackets.fs.unlink(filename, function () { - // ignore the result - }); + FileSystem.getFileForPath(filename).unlink(); } this._enterState(STATE_CLOSED); } else if (this._state !== STATE_CANCELING_INSTALL) { diff --git a/src/extensibility/Package.js b/src/extensibility/Package.js index ea2726e71ab..8124a85bdad 100644 --- a/src/extensibility/Package.js +++ b/src/extensibility/Package.js @@ -32,6 +32,7 @@ define(function (require, exports, module) { "use strict"; var AppInit = require("utils/AppInit"), + FileSystem = require("filesystem/FileSystem"), FileUtils = require("file/FileUtils"), StringUtils = require("utils/StringUtils"), Strings = require("strings"), @@ -311,9 +312,7 @@ define(function (require, exports, module) { result.localPath = downloadResult.localPath; d.resolve(result); } else { - brackets.fs.unlink(downloadResult.localPath, function (err) { - // ignore errors - }); + FileSystem.getFileForPath(downloadResult.localPath).unlink(); if (result.errors && result.errors.length > 0) { // Validation errors - for now, only return the first one state = STATE_FAILED; @@ -332,9 +331,7 @@ define(function (require, exports, module) { .fail(function (err) { // File IO errors, internal error in install()/validate(), or extension startup crashed state = STATE_FAILED; - brackets.fs.unlink(downloadResult.localPath, function (err) { - // ignore errors - }); + FileSystem.getFileForPath(downloadResult.localPath).unlink(); d.reject(err); // TODO: needs to be err.message ? }); }) @@ -422,7 +419,7 @@ define(function (require, exports, module) { d.reject(error); }) .always(function () { - brackets.fs.unlink(path, function () { }); + FileSystem.getFileForPath(path).unlink(); }); return d.promise(); } diff --git a/src/extensions/default/CloseOthers/unittests.js b/src/extensions/default/CloseOthers/unittests.js index 452388ea905..b855d519d9b 100644 --- a/src/extensions/default/CloseOthers/unittests.js +++ b/src/extensions/default/CloseOthers/unittests.js @@ -28,12 +28,13 @@ define(function (require, exports, module) { "use strict"; var SpecRunnerUtils = brackets.getModule("spec/SpecRunnerUtils"), - FileUtils = brackets.getModule("file/FileUtils"), - CommandManager, - Commands, - Dialogs, - EditorManager, - DocumentManager; + FileUtils = brackets.getModule("file/FileUtils"), + CommandManager, + Commands, + Dialogs, + EditorManager, + DocumentManager, + FileSystem; describe("CloseOthers", function () { var extensionPath = FileUtils.getNativeModuleDirectoryPath(module), @@ -89,6 +90,7 @@ define(function (require, exports, module) { EditorManager = testWindow.brackets.test.EditorManager; Dialogs = testWindow.brackets.test.Dialogs; Commands = testWindow.brackets.test.Commands; + FileSystem = testWindow.brackets.test.FileSystem; }); }); @@ -100,7 +102,7 @@ define(function (require, exports, module) { runs(function () { var fileI = 0; - spyOn(testWindow.brackets.fs, 'showSaveDialog').andCallFake(function (dialogTitle, initialPath, proposedNewName, callback) { + spyOn(FileSystem, 'showSaveDialog').andCallFake(function (dialogTitle, initialPath, proposedNewName, callback) { callback(undefined, getFilename(fileI)); fileI++; }); diff --git a/src/extensions/default/DebugCommands/main.js b/src/extensions/default/DebugCommands/main.js index 7e4e9463ac3..478e58f7211 100644 --- a/src/extensions/default/DebugCommands/main.js +++ b/src/extensions/default/DebugCommands/main.js @@ -35,8 +35,8 @@ define(function (require, exports, module) { KeyBindingManager = brackets.getModule("command/KeyBindingManager"), Menus = brackets.getModule("command/Menus"), Editor = brackets.getModule("editor/Editor").Editor, + FileSystem = brackets.getModule("filesystem/FileSystem"), FileUtils = brackets.getModule("file/FileUtils"), - NativeFileSystem = brackets.getModule("file/NativeFileSystem").NativeFileSystem, ProjectManager = brackets.getModule("project/ProjectManager"), PerfUtils = brackets.getModule("utils/PerfUtils"), NativeApp = brackets.getModule("utils/NativeApp"), @@ -141,8 +141,9 @@ define(function (require, exports, module) { function _handleSwitchLanguage() { var stringsPath = FileUtils.getNativeBracketsDirectoryPath() + "/nls"; - NativeFileSystem.requestNativeFileSystem(stringsPath, function (fs) { - fs.root.createReader().readEntries(function (entries) { + + FileSystem.getDirectoryForPath(stringsPath).getContents(function (err, entries) { + if (!err) { var $dialog, $submit, $select, @@ -150,19 +151,19 @@ define(function (require, exports, module) { curLocale = (brackets.isLocaleDefault() ? null : brackets.getLocale()), languages = []; - function setLanguage(event) { + var setLanguage = function (event) { locale = $select.val(); $submit.prop("disabled", locale === (curLocale || "")); - } + }; // returns the localized label for the given locale // or the locale, if nothing found - function getLocalizedLabel(locale) { + var getLocalizedLabel = function (locale) { var key = "LOCALE_" + locale.toUpperCase().replace("-", "_"), i18n = Strings[key]; return i18n === undefined ? locale : i18n; - } + }; // add system default languages.push({label: Strings.LANGUAGE_SYSTEM_DEFAULT, language: null}); @@ -201,7 +202,7 @@ define(function (require, exports, module) { $select = $dialog.find("select"); $select.on("change", setLanguage).val(curLocale); - }); + } }); } @@ -211,18 +212,18 @@ define(function (require, exports, module) { } // Check for the SpecRunner.html file - var fileEntry = new NativeFileSystem.FileEntry( + var file = FileSystem.getFileForPath( FileUtils.getNativeBracketsDirectoryPath() + "/../test/SpecRunner.html" ); - fileEntry.getMetadata( - function (metadata) { - // If we sucessfully got the metadata for the SpecRunner.html file, - // enable the menu item + file.exists(function (exists) { + if (exists) { + // If the SpecRunner.html file exists, enable the menu item. + // (menu item is already disabled, so no need to disable if the + // file doesn't exist). CommandManager.get(DEBUG_RUN_UNIT_TESTS).setEnabled(true); - }, - function (error) {} /* menu already disabled, ignore errors */ - ); + } + }); } diff --git a/src/extensions/default/JavaScriptCodeHints/ScopeManager.js b/src/extensions/default/JavaScriptCodeHints/ScopeManager.js index 32a2bc292e2..8a41494f3cb 100644 --- a/src/extensions/default/JavaScriptCodeHints/ScopeManager.js +++ b/src/extensions/default/JavaScriptCodeHints/ScopeManager.js @@ -38,11 +38,10 @@ define(function (require, exports, module) { var DocumentManager = brackets.getModule("document/DocumentManager"), LanguageManager = brackets.getModule("language/LanguageManager"), - NativeFileSystem = brackets.getModule("file/NativeFileSystem").NativeFileSystem, ProjectManager = brackets.getModule("project/ProjectManager"), ExtensionUtils = brackets.getModule("utils/ExtensionUtils"), + FileSystem = brackets.getModule("filesystem/FileSystem"), FileUtils = brackets.getModule("file/FileUtils"), - FileIndexManager = brackets.getModule("project/FileIndexManager"), HintUtils = require("HintUtils"), MessageIds = require("MessageIds"), Preferences = require("Preferences"); @@ -81,16 +80,18 @@ define(function (require, exports, module) { library; files.forEach(function (i) { - NativeFileSystem.resolveNativeFileSystemPath(path + i, function (fileEntry) { - FileUtils.readAsText(fileEntry).done(function (text) { - library = JSON.parse(text); - builtinLibraryNames.push(library["!name"]); - ternEnvironment.push(library); - }).fail(function (error) { + FileSystem.resolve(path + i, function (err, file) { + if (!err) { + FileUtils.readAsText(file).done(function (text) { + library = JSON.parse(text); + builtinLibraryNames.push(library["!name"]); + ternEnvironment.push(library); + }).fail(function (error) { + console.log("failed to read tern config file " + i); + }); + } else { console.log("failed to read tern config file " + i); - }); - }, function (error) { - console.log("failed to read tern config file " + i); + } }); }); } @@ -125,28 +126,30 @@ define(function (require, exports, module) { var path = projectRootPath + Preferences.FILE_NAME; - NativeFileSystem.resolveNativeFileSystemPath(path, function (fileEntry) { - FileUtils.readAsText(fileEntry).done(function (text) { - var configObj = null; - try { - configObj = JSON.parse(text); - } catch (e) { - // continue with null configObj which will result in - // default settings. - console.log("Error parsing preference file: " + path); - if (e instanceof SyntaxError) { - console.log(e.message); + FileSystem.resolve(path, function (err, file) { + if (!err) { + FileUtils.readAsText(file).done(function (text) { + var configObj = null; + try { + configObj = JSON.parse(text); + } catch (e) { + // continue with null configObj which will result in + // default settings. + console.log("Error parsing preference file: " + path); + if (e instanceof SyntaxError) { + console.log(e.message); + } } - } - preferences = new Preferences(configObj); - deferredPreferences.resolve(); - }).fail(function (error) { + preferences = new Preferences(configObj); + deferredPreferences.resolve(); + }).fail(function (error) { + preferences = new Preferences(); + deferredPreferences.resolve(); + }); + } else { preferences = new Preferences(); deferredPreferences.resolve(); - }); - }, function (error) { - preferences = new Preferences(); - deferredPreferences.resolve(); + } }); } @@ -185,43 +188,45 @@ define(function (require, exports, module) { */ function forEachFileInDirectory(dir, doneCallback, fileCallback, directoryCallback, errorCallback) { var files = []; - - NativeFileSystem.resolveNativeFileSystemPath(dir, function (dirEntry) { - var reader = dirEntry.createReader(); - - reader.readEntries(function (entries) { - entries.slice(0, preferences.getMaxFileCount()).forEach(function (entry) { - var path = entry.fullPath, - split = HintUtils.splitPath(path), - file = split.file; - - if (fileCallback && entry.isFile) { - - if (file.indexOf(".") > 0) { // ignore .dotfiles - var languageID = LanguageManager.getLanguageForPath(path).getId(); - if (languageID === HintUtils.LANGUAGE_ID) { - fileCallback(path); + + FileSystem.resolve(dir, function (err, directory) { + if (!err && directory.isDirectory) { + directory.getContents(function (err, contents) { + if (!err) { + contents.slice(0, preferences.getMaxFileCount()).forEach(function (entry) { + var path = entry.fullPath, + split = HintUtils.splitPath(path), + file = split.file; + + if (fileCallback && entry.isFile) { + + if (file.indexOf(".") > 0) { // ignore .dotfiles + var languageID = LanguageManager.getLanguageForPath(path).getId(); + if (languageID === HintUtils.LANGUAGE_ID) { + fileCallback(path); + } + } + } else if (directoryCallback && entry.isDirectory) { + var dirName = HintUtils.splitPath(split.dir).file; + if (dirName.indexOf(".") !== 0) { // ignore .dotfiles + directoryCallback(entry.fullPath); + } } + }); + doneCallback(); + } else { + if (errorCallback) { + errorCallback(err); } - } else if (directoryCallback && entry.isDirectory) { - var dirName = HintUtils.splitPath(split.dir).file; - if (dirName.indexOf(".") !== 0) { // ignore .dotfiles - directoryCallback(entry.fullPath); - } + console.log("Unable to refresh directory: ", err); } }); - doneCallback(); - }, function (err) { + } else { if (errorCallback) { errorCallback(err); } - console.log("Unable to refresh directory: ", err); - }); - }, function (err) { - if (errorCallback) { - errorCallback(err); + console.log("Directory \"%s\" does not exist", dir); } - console.log("Directory \"%s\" does not exist", dir); }); } @@ -825,20 +830,26 @@ define(function (require, exports, module) { /** * Helper function to get the text of a given document and send it to tern. - * If we successfully get the document from the DocumentManager then the text of - * the document will be sent to the tern worker. - * The Promise for getDocumentForPath is returned so that custom fail functions can be - * used. + * If DocumentManager successfully gets the file's text then we'll send it to the tern worker. + * The Promise for getDocumentText() is returned so that custom fail functions can be used. * * @param {string} filePath - the path of the file to get the text of - * @return {jQuery.Promise} - the Promise returned from DocumentMangaer.getDocumentForPath + * @return {jQuery.Promise} - the Promise returned from DocumentMangaer.getDocumentText() */ function getDocText(filePath) { - return DocumentManager.getDocumentForPath(filePath).done(function (document) { + if (!FileSystem.isAbsolutePath(filePath)) { + return new $.Deferred().reject(); + } + + var file = FileSystem.getFileForPath(filePath), + promise = DocumentManager.getDocumentText(file); + + promise.done(function (docText) { resolvedFiles[name] = filePath; numResolvedFiles++; - replyWith(name, getTextFromDocument(document)); + replyWith(name, filterText(docText)); }); + return promise; } /** @@ -850,29 +861,29 @@ define(function (require, exports, module) { function findNameInProject() { // check for any files in project that end with the right path. var fileName = HintUtils.splitPath(name).file; - FileIndexManager.getFilenameMatches("all", fileName) - .done(function (files) { - var file; - files = files.filter(function (file) { - var pos = file.fullPath.length - name.length; - return pos === file.fullPath.lastIndexOf(name); - }); - - if (files.length === 1) { - file = files[0]; - } - if (file) { - getDocText(file.fullPath).fail(function () { - replyWith(name, ""); - }); - } else { + + function _fileFilter(entry) { + return entry.name === fileName; + } + + ProjectManager.getAllFiles(_fileFilter).done(function (files) { + var file; + files = files.filter(function (file) { + var pos = file.fullPath.length - name.length; + return pos === file.fullPath.lastIndexOf(name); + }); + + if (files.length === 1) { + file = files[0]; + } + if (file) { + getDocText(file.fullPath).fail(function () { replyWith(name, ""); - } - - }) - .fail(function () { + }); + } else { replyWith(name, ""); - }); + } + }); } getDocText(name).fail(function () { @@ -1426,6 +1437,12 @@ define(function (require, exports, module) { function handleProjectOpen(projectRootPath) { initPreferences(projectRootPath); } + + + /** Used to avoid timing bugs in unit tests */ + function _readyPromise() { + return deferredPreferences; + } exports.getBuiltins = getBuiltins; exports.getResolvedPath = getResolvedPath; @@ -1438,5 +1455,6 @@ define(function (require, exports, module) { exports.requestParameterHint = requestParameterHint; exports.handleProjectClose = handleProjectClose; exports.handleProjectOpen = handleProjectOpen; + exports._readyPromise = _readyPromise; }); diff --git a/src/extensions/default/JavaScriptCodeHints/unittests.js b/src/extensions/default/JavaScriptCodeHints/unittests.js index 045a69104b6..5cd4501dac4 100644 --- a/src/extensions/default/JavaScriptCodeHints/unittests.js +++ b/src/extensions/default/JavaScriptCodeHints/unittests.js @@ -22,7 +22,7 @@ */ /*jslint vars: true, plusplus: true, devel: true, browser: true, nomen: true, indent: 4, maxerr: 50, regexp: true */ -/*global define, describe, it, xit, expect, beforeEach, afterEach, waitsFor, runs, $, brackets, waitsForDone */ +/*global define, describe, it, xit, expect, beforeEach, afterEach, waitsFor, runs, $, brackets, waits, waitsForDone, spyOn */ define(function (require, exports, module) { "use strict"; @@ -32,8 +32,8 @@ define(function (require, exports, module) { DocumentManager = brackets.getModule("document/DocumentManager"), Editor = brackets.getModule("editor/Editor").Editor, EditorManager = brackets.getModule("editor/EditorManager"), + FileSystem = brackets.getModule("filesystem/FileSystem"), FileUtils = brackets.getModule("file/FileUtils"), - NativeFileSystem = brackets.getModule("file/NativeFileSystem").NativeFileSystem, SpecRunnerUtils = brackets.getModule("spec/SpecRunnerUtils"), UnitTestReporter = brackets.getModule("test/UnitTestReporter"), JSCodeHints = require("main"), @@ -425,7 +425,7 @@ define(function (require, exports, module) { } } - function setupTest(path, primePump) { + function setupTest(path, primePump) { // FIXME: primePump argument ignored even though used below DocumentManager.getDocumentForPath(path).done(function (doc) { testDoc = doc; }); @@ -438,6 +438,8 @@ define(function (require, exports, module) { runs(function () { testEditor = createMockEditor(testDoc); preTestText = testDoc.getText(); + + waitsForDone(ScopeManager._readyPromise()); }); } @@ -741,7 +743,7 @@ define(function (require, exports, module) { middle = { line: 6, ch: 3 }, end = { line: 6, ch: 8 }, endplus = { line: 6, ch: 12 }; - + testDoc.replaceRange("A1.prop", start, start); testEditor.setCursorPos(middle); var hintObj = expectHints(JSCodeHints.jsHintProvider); @@ -1541,23 +1543,24 @@ define(function (require, exports, module) { function getPreferences(path) { preferences = null; - NativeFileSystem.resolveNativeFileSystemPath(path, function (fileEntry) { - FileUtils.readAsText(fileEntry).done(function (text) { - var configObj = null; - try { - configObj = JSON.parse(text); - } catch (e) { - // continue with null configObj - console.log(e); - } - preferences = new Preferences(configObj); - }).fail(function (error) { + FileSystem.resolve(path, function (err, file) { + if (!err) { + FileUtils.readAsText(file).done(function (text) { + var configObj = null; + try { + configObj = JSON.parse(text); + } catch (e) { + // continue with null configObj + console.log(e); + } + preferences = new Preferences(configObj); + }).fail(function (error) { + preferences = new Preferences(); + }); + } else { preferences = new Preferences(); - }); - }, function (error) { - preferences = new Preferences(); + } }); - } // Test preferences file with no entries. Preferences should contain diff --git a/src/extensions/default/JavaScriptQuickEdit/main.js b/src/extensions/default/JavaScriptQuickEdit/main.js index 1ee3ee7e397..0634c8bc6c5 100644 --- a/src/extensions/default/JavaScriptQuickEdit/main.js +++ b/src/extensions/default/JavaScriptQuickEdit/main.js @@ -30,11 +30,11 @@ define(function (require, exports, module) { // Brackets modules var MultiRangeInlineEditor = brackets.getModule("editor/MultiRangeInlineEditor").MultiRangeInlineEditor, - FileIndexManager = brackets.getModule("project/FileIndexManager"), EditorManager = brackets.getModule("editor/EditorManager"), DocumentManager = brackets.getModule("document/DocumentManager"), JSUtils = brackets.getModule("language/JSUtils"), - PerfUtils = brackets.getModule("utils/PerfUtils"); + PerfUtils = brackets.getModule("utils/PerfUtils"), + ProjectManager = brackets.getModule("project/ProjectManager"); /** * Return the token string that is at the specified position. @@ -73,11 +73,11 @@ define(function (require, exports, module) { function _findInProject(functionName) { var result = new $.Deferred(); - FileIndexManager.getFileInfoList("all") - .done(function (fileInfos) { - PerfUtils.markStart(PerfUtils.JAVASCRIPT_FIND_FUNCTION); - - JSUtils.findMatchingFunctions(functionName, fileInfos) + PerfUtils.markStart(PerfUtils.JAVASCRIPT_FIND_FUNCTION); + + ProjectManager.getAllFiles() + .done(function (files) { + JSUtils.findMatchingFunctions(functionName, files) .done(function (functions) { PerfUtils.addMeasurement(PerfUtils.JAVASCRIPT_FIND_FUNCTION); result.resolve(functions); diff --git a/src/extensions/default/JavaScriptQuickEdit/unittests.js b/src/extensions/default/JavaScriptQuickEdit/unittests.js index 04b3f7dbd8e..1a0f8d404cf 100644 --- a/src/extensions/default/JavaScriptQuickEdit/unittests.js +++ b/src/extensions/default/JavaScriptQuickEdit/unittests.js @@ -30,12 +30,10 @@ define(function (require, exports, module) { var CommandManager, // loaded from brackets.test EditorManager, // loaded from brackets.test - FileIndexManager, // loaded from brackets.test PerfUtils, // loaded from brackets.test JSUtils, // loaded from brackets.test FileUtils = brackets.getModule("file/FileUtils"), - NativeFileSystem = brackets.getModule("file/NativeFileSystem").NativeFileSystem, SpecRunnerUtils = brackets.getModule("spec/SpecRunnerUtils"), UnitTestReporter = brackets.getModule("test/UnitTestReporter"); @@ -118,7 +116,6 @@ define(function (require, exports, module) { testWindow = w; EditorManager = testWindow.brackets.test.EditorManager; CommandManager = testWindow.brackets.test.CommandManager; - FileIndexManager = testWindow.brackets.test.FileIndexManager; JSUtils = testWindow.brackets.test.JSUtils; }); @@ -193,7 +190,6 @@ define(function (require, exports, module) { testWindow = null; EditorManager = null; CommandManager = null; - FileIndexManager = null; JSUtils = null; SpecRunnerUtils.closeTestWindow(); }); @@ -576,9 +572,6 @@ define(function (require, exports, module) { { measure: PerfUtils.JAVASCRIPT_INLINE_CREATE, children: [ - { - measure: PerfUtils.FILE_INDEX_MANAGER_SYNC - }, { measure: PerfUtils.JAVASCRIPT_FIND_FUNCTION, children: [ diff --git a/src/extensions/default/RecentProjects/main.js b/src/extensions/default/RecentProjects/main.js index 3b5db4f15be..e204b3c0f3c 100644 --- a/src/extensions/default/RecentProjects/main.js +++ b/src/extensions/default/RecentProjects/main.js @@ -37,10 +37,10 @@ define(function (require, exports, module) { Menus = brackets.getModule("command/Menus"), EditorManager = brackets.getModule("editor/EditorManager"), ExtensionUtils = brackets.getModule("utils/ExtensionUtils"), + FileSystem = brackets.getModule("filesystem/FileSystem"), AppInit = brackets.getModule("utils/AppInit"), KeyEvent = brackets.getModule("utils/KeyEvent"), FileUtils = brackets.getModule("file/FileUtils"), - NativeFileSystem = brackets.getModule("file/NativeFileSystem").NativeFileSystem, PopUpManager = brackets.getModule("widgets/PopUpManager"), Strings = brackets.getModule("strings"), ProjectsMenuTemplate = require("text!htmlContent/projects-menu.html"); @@ -61,7 +61,6 @@ define(function (require, exports, module) { $dropdown, $links; - /** * Get the stored list of recent projects, fixing up paths as appropriate. * Warning: unlike most paths in Brackets, these lack a trailing "/" @@ -293,11 +292,11 @@ define(function (require, exports, module) { var recentProjects = getRecentProjects(), index = recentProjects.indexOf(path); if (index !== -1) { - NativeFileSystem.requestNativeFileSystem(path, - function () {}, - function () { + FileSystem.resolve(path, function (err, item) { + if (err) { recentProjects.splice(index, 1); - }); + } + }); } }); closeDropdown(); @@ -305,6 +304,7 @@ define(function (require, exports, module) { } else if (id === "open-folder-link") { CommandManager.execute(Commands.FILE_OPEN_FOLDER); } + }) .on("mouseenter", "a", function () { if ($dropdownItem) { diff --git a/src/extensions/default/UrlCodeHints/main.js b/src/extensions/default/UrlCodeHints/main.js index 279c839d6fa..14dd803c16a 100644 --- a/src/extensions/default/UrlCodeHints/main.js +++ b/src/extensions/default/UrlCodeHints/main.js @@ -33,9 +33,9 @@ define(function (require, exports, module) { CSSUtils = brackets.getModule("language/CSSUtils"), DocumentManager = brackets.getModule("document/DocumentManager"), EditorManager = brackets.getModule("editor/EditorManager"), + FileSystem = brackets.getModule("filesystem/FileSystem"), FileUtils = brackets.getModule("file/FileUtils"), HTMLUtils = brackets.getModule("language/HTMLUtils"), - NativeFileSystem = brackets.getModule("file/NativeFileSystem").NativeFileSystem, ProjectManager = brackets.getModule("project/ProjectManager"), StringUtils = brackets.getModule("utils/StringUtils"), @@ -71,7 +71,7 @@ define(function (require, exports, module) { } var docDir = FileUtils.getDirectoryPath(doc.file.fullPath); - + // get relative path from query string // TODO: handle site-root relative var queryDir = ""; @@ -121,7 +121,8 @@ define(function (require, exports, module) { unfiltered = this.cachedHints.unfiltered; } else { - var self = this; + var directory = FileSystem.getDirectoryForPath(targetDir), + self = this; if (self.cachedHints && self.cachedHints.deferred) { self.cachedHints.deferred.reject(); @@ -131,10 +132,9 @@ define(function (require, exports, module) { self.cachedHints.deferred = $.Deferred(); self.cachedHints.unfiltered = []; - NativeFileSystem.requestNativeFileSystem(targetDir, function (fs) { - fs.root.createReader().readEntries(function (entries) { - - entries.forEach(function (entry) { + directory.getContents(function (err, contents) { + if (!err) { + contents.forEach(function (entry) { if (ProjectManager.shouldShow(entry)) { // convert to doc relative path var entryStr = entry.fullPath.replace(docDir, ""); @@ -171,7 +171,7 @@ define(function (require, exports, module) { } } } - }); + } }); return self.cachedHints.deferred; @@ -759,11 +759,13 @@ define(function (require, exports, module) { // For unit testing exports.hintProvider = urlHints; }); - - $(ProjectManager).on("projectFilesChange", function (event, projectRoot) { + + function _clearCachedHints() { // Cache may or may not be stale. Main benefit of cache is to limit async lookups // during typing. File tree updates cannot happen during typing, so it's probably // not worth determining whether cache may still be valid. Just delete it. exports.hintProvider.cachedHints = null; - }); + } + FileSystem.on("change", _clearCachedHints); + FileSystem.on("rename", _clearCachedHints); }); diff --git a/src/extensions/default/WebPlatformDocs/main.js b/src/extensions/default/WebPlatformDocs/main.js index 8d74067c330..5f0187ff5d8 100644 --- a/src/extensions/default/WebPlatformDocs/main.js +++ b/src/extensions/default/WebPlatformDocs/main.js @@ -30,7 +30,7 @@ define(function (require, exports, module) { // Core modules var EditorManager = brackets.getModule("editor/EditorManager"), - NativeFileSystem = brackets.getModule("file/NativeFileSystem").NativeFileSystem, + FileSystem = brackets.getModule("filesystem/FileSystem"), FileUtils = brackets.getModule("file/FileUtils"), ExtensionUtils = brackets.getModule("utils/ExtensionUtils"), CSSUtils = brackets.getModule("language/CSSUtils"); @@ -51,9 +51,10 @@ define(function (require, exports, module) { if (!_cssDocsPromise) { var result = new $.Deferred(); - var path = ExtensionUtils.getModulePath(module, "css.json"); + var path = ExtensionUtils.getModulePath(module, "css.json"), + file = FileSystem.getFileForPath(path); - FileUtils.readAsText(new NativeFileSystem.FileEntry(path)) + FileUtils.readAsText(file) .done(function (text) { var jsonData; try { diff --git a/src/file/FileUtils.js b/src/file/FileUtils.js index 71618b98eb2..4a128e0f95e 100644 --- a/src/file/FileUtils.js +++ b/src/file/FileUtils.js @@ -33,82 +33,58 @@ define(function (require, exports, module) { require("utils/Global"); - var NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, - NativeFileError = require("file/NativeFileError"), + var FileSystemError = require("filesystem/FileSystemError"), PerfUtils = require("utils/PerfUtils"), Dialogs = require("widgets/Dialogs"), DefaultDialogs = require("widgets/DefaultDialogs"), Strings = require("strings"), - StringUtils = require("utils/StringUtils"), - Encodings = NativeFileSystem.Encodings; + StringUtils = require("utils/StringUtils"); /** * Asynchronously reads a file as UTF-8 encoded text. + * @param {!File} file File to read * @return {$.Promise} a jQuery promise that will be resolved with the - * file's text content plus its timestamp, or rejected with a NativeFileError if + * file's text content plus its timestamp, or rejected with a FileSystemError if * the file can not be read. */ - function readAsText(fileEntry) { - var result = new $.Deferred(), - reader; + function readAsText(file) { + var result = new $.Deferred(); // Measure performance - var perfTimerName = PerfUtils.markStart("readAsText:\t" + fileEntry.fullPath); + var perfTimerName = PerfUtils.markStart("readAsText:\t" + file.fullPath); result.always(function () { PerfUtils.addMeasurement(perfTimerName); }); // Read file - reader = new NativeFileSystem.FileReader(); - fileEntry.file(function (file) { - reader.onload = function (event) { - var text = event.target.result; - - fileEntry.getMetadata( - function (metadata) { - result.resolve(text, metadata.modificationTime); - }, - function (error) { - result.reject(error); - } - ); - }; - - reader.onerror = function (event) { - result.reject(event.target.error); - }; - - reader.readAsText(file, Encodings.UTF8); - }, function (error) { - result.reject(error); + file.read(function (err, data, stat) { + if (!err) { + result.resolve(data, stat.mtime); + } else { + result.reject(err); + } }); - + return result.promise(); } /** * Asynchronously writes a file as UTF-8 encoded text. - * @param {!FileEntry} fileEntry + * @param {!File} file File to write * @param {!string} text * @return {$.Promise} a jQuery promise that will be resolved when - * file writing completes, or rejected with a NativeFileError. + * file writing completes, or rejected with a FileSystemError. */ - function writeText(fileEntry, text) { + function writeText(file, text) { var result = new $.Deferred(); - fileEntry.createWriter(function (fileWriter) { - fileWriter.onwriteend = function (e) { + file.write(text, function (err) { + if (!err) { result.resolve(); - }; - fileWriter.onerror = function (err) { + } else { result.reject(err); - }; - - // TODO (issue #241): NativeFileSystem.BlobBulder - fileWriter.write(text); - }, function (error) { - result.reject(error); + } }); return result.promise(); @@ -167,11 +143,11 @@ define(function (require, exports, module) { // displayed with a generic "(error N)" message. var result; - if (name === NativeFileError.NOT_FOUND_ERR) { + if (name === FileSystemError.NOT_FOUND) { result = Strings.NOT_FOUND_ERR; - } else if (name === NativeFileError.NOT_READABLE_ERR) { + } else if (name === FileSystemError.NOT_READABLE) { result = Strings.NOT_READABLE_ERR; - } else if (name === NativeFileError.NO_MODIFICATION_ALLOWED_ERR) { + } else if (name === FileSystemError.NOT_WRITABLE) { result = Strings.NO_MODIFICATION_ALLOWED_ERR_FILE; } else { result = StringUtils.format(Strings.GENERIC_ERROR, name); @@ -228,7 +204,7 @@ define(function (require, exports, module) { /** * Removes the trailing slash from a path, if it has one. * Warning: this differs from the format of most paths used in Brackets! Use paths ending in "/" - * normally, as this is the format used by DirectoryEntry.fullPath. + * normally, as this is the format used by Directory.fullPath. * * @param {string} path * @return {string} @@ -243,14 +219,14 @@ define(function (require, exports, module) { /** * Warning: Contrary to the name, this does NOT return a canonical path. The canonical format - * used by DirectoryEntry.fullPath actually DOES include the trailing "/" + * used by Directory.fullPath actually DOES include the trailing "/" * @deprecated * * @param {string} path * @return {string} */ function canonicalizeFolderPath(path) { - console.error("Warning: FileUtils.canonicalizeFolderPath() is deprecated. Use paths ending in '/' if possible, like DirectoryEntry.fullPath"); + console.error("Warning: FileUtils.canonicalizeFolderPath() is deprecated. Use paths ending in '/' if possible, like Directory.fullPath"); return stripTrailingSlash(path); } @@ -303,52 +279,6 @@ define(function (require, exports, module) { return path; } - /** - * Checks wheter a path is affected by a rename operation. - * A path is affected if the object being renamed is a file and the given path refers - * to that file or if the object being renamed is a directory and a prefix of the path. - * Always checking for prefixes can create conflicts: - * renaming file "foo" should not affect file "foobar/baz" even though "foo" is a prefix of "foobar". - * @param {!string} path The path potentially affected - * @param {!string} oldName An object's name before renaming - * @param {!string} newName An object's name after renaming - * @param {?boolean} isFolder Whether the renamed object is a folder or not - */ - function isAffectedWhenRenaming(path, oldName, newName, isFolder) { - isFolder = isFolder || oldName.slice(-1) === "/"; - return (isFolder && path.indexOf(oldName) === 0) || (!isFolder && path === oldName); - } - - /** - * Update a file entry path after a file/folder name change. - * @param {FileEntry} entry The FileEntry or DirectoryEntry to update - * @param {string} oldName The full path of the old name - * @param {string} newName The full path of the new name - * @return {boolean} Returns true if the file entry was updated - */ - function updateFileEntryPath(entry, oldName, newName, isFolder) { - if (isAffectedWhenRenaming(entry.fullPath, oldName, newName, isFolder)) { - var oldFullPath = entry.fullPath; - var fullPath = oldFullPath.replace(oldName, newName); - entry.fullPath = fullPath; - - // TODO: Should this be a method on Entry instead? - entry.name = null; // default if extraction fails - if (fullPath) { - var pathParts = fullPath.split("/"); - - // Extract name from the end of the fullPath (account for trailing slash(es)) - while (!entry.name && pathParts.length) { - entry.name = pathParts.pop(); - } - } - - return true; - } - - return false; - } - /** * Get the file extension (excluding ".") given a path OR a bare filename. * Returns "" for names with no extension. If the name starts with ".", the @@ -474,8 +404,6 @@ define(function (require, exports, module) { exports.getNativeModuleDirectoryPath = getNativeModuleDirectoryPath; exports.canonicalizeFolderPath = canonicalizeFolderPath; exports.stripTrailingSlash = stripTrailingSlash; - exports.isAffectedWhenRenaming = isAffectedWhenRenaming; - exports.updateFileEntryPath = updateFileEntryPath; exports.isStaticHtmlFileExt = isStaticHtmlFileExt; exports.isServerHtmlFileExt = isServerHtmlFileExt; exports.getDirectoryPath = getDirectoryPath; diff --git a/src/file/NativeFileError.js b/src/file/NativeFileError.js index 4010d9812a2..4858c15d207 100644 --- a/src/file/NativeFileError.js +++ b/src/file/NativeFileError.js @@ -24,38 +24,19 @@ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50*/ /*global define */ +/** + * @deprecated + * This is a compatibility shim for legacy Brackets APIs that will be removed soon. These + * error codes are *never* returned anymore. Use error codes in FileSystemError instead. + */ define(function () { "use strict"; /** - * Implementation of w3 DOMError interface - * http://www.w3.org/TR/2012/WD-dom-20120105/#interface-domerror - * - * NativeFileError describes possible errors occurred during NativeFileSystem - * operations. It is inteneded to be used in error handling through other means - * than exceptions. - * @constructor - * @implements {DOMError} - * + * @deprecated */ - var NativeFileError = function (name) { - - /** - * The name of the error - * @const - * @type {string} - */ - Object.defineProperty(this, "name", { - value: name, - writable: false - }); - }; + var NativeFileError = {}; - /** - * Possible error name constants for NativeFileSystem operations. For details check: - * http://www.w3.org/TR/file-system-api/#definitions - * http://dev.w3.org/2009/dap/file-system/file-writer.html#definitions - */ NativeFileError.NOT_FOUND_ERR = "NotFoundError"; NativeFileError.SECURITY_ERR = "SecurityError"; NativeFileError.ABORT_ERR = "AbortError"; @@ -68,6 +49,5 @@ define(function () { NativeFileError.TYPE_MISMATCH_ERR = "TypeMismatchError"; NativeFileError.PATH_EXISTS_ERR = "PathExistsError"; - // Define public API return NativeFileError; }); \ No newline at end of file diff --git a/src/file/NativeFileSystem.js b/src/file/NativeFileSystem.js index 9926654d6d0..a65a226a6bc 100644 --- a/src/file/NativeFileSystem.js +++ b/src/file/NativeFileSystem.js @@ -25,118 +25,36 @@ /*global $, define, brackets, InvalidateStateError, window */ /** - * Generally NativeFileSystem mimics the File-System API working draft: - * http://www.w3.org/TR/2011/WD-file-system-api-20110419 - * - * A more recent version of the specs can be found at: - * http://www.w3.org/TR/2012/WD-file-system-api-20120417 - * - * Other relevant w3 specs related to this API are: - * http://www.w3.org/TR/2011/WD-FileAPI-20111020 - * http://www.w3.org/TR/2011/WD-file-writer-api-20110419 - * http://www.w3.org/TR/progress-events - * - * The w3 entry point requestFileSystem is replaced with our own requestNativeFileSystem. - * - * The current implementation is incomplete and notably does not - * support the Blob data type and synchronous APIs. DirectoryEntry - * and FileEntry read/write capabilities are mostly implemented, but - * delete is not. File writing is limited to UTF-8 text. - * - * - * Basic usage examples: - * - * - CREATE A DIRECTORY - * var directoryEntry = ... // NativeFileSystem.DirectoryEntry - * directoryEntry.getDirectory(path, {create: true}); - * - * - * - CHECK IF A FILE OR FOLDER EXISTS - * NativeFileSystem.resolveNativeFileSystemPath(path - * , function(entry) { console.log("Path for " + entry.name + " resolved"); } - * , function(err) { console.log("Error resolving path: " + err.name); }); - * - * - * - READ A FILE - * - * (Using file/NativeFileSystem) - * reader = new NativeFileSystem.FileReader(); - * fileEntry.file(function (file) { - * reader.onload = function (event) { - * var text = event.target.result; - * }; - * - * reader.onerror = function (event) { - * }; - * - * reader.readAsText(file, Encodings.UTF8); - * }); - * - * (Using file/FileUtils) - * FileUtils.readAsText(fileEntry).done(function (rawText, readTimestamp) { - * console.log(rawText); - * }).fail(function (err) { - * console.log("Error reading text: " + err.name); - * }); - * - * - * - WRITE TO A FILE - * - * (Using file/NativeFileSystem) - * writer = fileEntry.createWriter(function (fileWriter) { - * fileWriter.onwriteend = function (e) { - * }; - * - * fileWriter.onerror = function (err) { - * }; - * - * fileWriter.write(text); - * }); - * - * (Using file/FileUtils) - * FileUtils.writeText(text, fileEntry).done(function () { - * console.log("Text successfully updated"); - * }).fail(function (err) { - * console.log("Error writing text: " + err.name); - * ]); - * - * - * - PROMPT THE USER TO SELECT FILES OR FOLDERS WITH OPERATING SYSTEM'S FILE OPEN DIALOG - * NativeFileSystem.showOpenDialog(true, true, "Choose a file...", null, function(files) {}, function(err) {}); + * @deprecated + * This is a compatibility shim for legacy Brackets APIs that will be removed soon. + * Use APIs in the FileSystem module instead. */ - define(function (require, exports, module) { "use strict"; - var Async = require("utils/Async"), + var FileSystem = require("filesystem/FileSystem"), + File = require("filesystem/File"), + Directory = require("filesystem/Directory"), NativeFileError = require("file/NativeFileError"); + + function _warn(oldAPI, newAPI) { + console.error("Warning: '" + oldAPI + "' is deprecated. Use '" + newAPI + "' instead"); + } + + + // Shims for static APIs on NativeFileSystem itself + var NativeFileSystem = { - - /** - * Amount of time we wait for async calls to return (in milliseconds) - * Not all async calls are wrapped with something that times out and - * calls the error callback. Timeouts are not specified in the W3C spec. - * @const - * @type {number} - */ - ASYNC_TIMEOUT: 2000, - ASYNC_NETWORK_TIMEOUT: 20000, // 20 seconds for reading files from network drive - /** - * Shows a modal dialog for selecting and opening files - * - * @param {boolean} allowMultipleSelection Allows selecting more than one file at a time - * @param {boolean} chooseDirectories Allows directories to be opened - * @param {string} title The title of the dialog - * @param {string} initialPath The folder opened inside the window initially. If initialPath - * is not set, or it doesn't exist, the window would show the last - * browsed folder depending on the OS preferences - * @param {Array.} fileTypes List of extensions that are allowed to be opened. A null value - * allows any extension to be selected. - * @param {function(Array.)} successCallback Callback function for successful operations. - Receives an array with the selected paths as first parameter. - * @param {function(DOMError)=} errorCallback Callback function for error operations. + * @deprecated + * @param {boolean} allowMultipleSelection + * @param {boolean} chooseDirectories + * @param {string} title + * @param {string} initialPath + * @param {Array.} fileTypes + * @param {!function(Array.)} successCallback + * @param {!function(string)} errorCallback */ showOpenDialog: function (allowMultipleSelection, chooseDirectories, @@ -145,11 +63,9 @@ define(function (require, exports, module) { fileTypes, successCallback, errorCallback) { - if (!successCallback) { - return; - } - - var files = brackets.fs.showOpenDialog( + _warn("NativeFileSystem.showOpenDialog()", "FileSystem.showOpenDialog()"); + + FileSystem.showOpenDialog( allowMultipleSelection, chooseDirectories, title, @@ -159,35 +75,28 @@ define(function (require, exports, module) { if (!err) { successCallback(data); } else if (errorCallback) { - errorCallback(new NativeFileError(NativeFileSystem._fsErrorToDOMErrorName(err))); + errorCallback(err); } } ); }, /** - * Shows a modal dialog for selecting a new file name - * - * @param {string} title The title of the dialog. - * @param {string} initialPath The folder opened inside the window initially. If initialPath - * is not set, or it doesn't exist, the window would show the last - * browsed folder depending on the OS preferences. - * @param {string} proposedNewFilename Provide a new file name for the user. This could be based on - * on the current file name plus an additional suffix - * @param {function(string} successCallback Callback function for successful operations. - Receives the path of the selected file name. - * @param {function(DOMError)=} errorCallback Callback function for error operations. + * @deprecated + * @param {string} title + * @param {string} initialPath + * @param {string} proposedNewFilename + * @param {!function(string)} successCallback + * @param {!function(string)} errorCallback */ showSaveDialog: function (title, initialPath, proposedNewFilename, successCallback, errorCallback) { - if (!successCallback) { - return; - } - - var newFile = brackets.fs.showSaveDialog( + _warn("NativeFileSystem.showSaveDialog()", "FileSystem.showSaveDialog()"); + + FileSystem.showSaveDialog( title, initialPath, proposedNewFilename, @@ -195,1160 +104,179 @@ define(function (require, exports, module) { if (!err) { successCallback(data); } else if (errorCallback) { - errorCallback(new NativeFileError(NativeFileSystem._fsErrorToDOMErrorName(err))); + errorCallback(err); } } ); }, /** - * Implementation of w3 requestFileSystem entry point - * @param {string} path Path to a directory. This directory will serve as the root of the - * FileSystem instance. - * @param {function(DirectoryEntry)} successCallback Callback function for successful operations. - * Receives a DirectoryEntry pointing to the path - * @param {function(DOMError)=} errorCallback Callback function for errors, including permission errors. + * @deprecated + * @param {string} path + * @param {!function(!{ root: !Directory })} successCallback + * @param {!function(string)} errorCallback */ requestNativeFileSystem: function (path, successCallback, errorCallback) { - brackets.fs.stat(path, function (err, data) { - if (!err) { - successCallback(new NativeFileSystem.FileSystem(path)); - } else if (errorCallback) { - errorCallback(new NativeFileError(NativeFileSystem._fsErrorToDOMErrorName(err))); + _warn("NativeFileSystem.requestNativeFileSystem()", "FileSystem.resolve()"); + + FileSystem.resolve(path, function (err, entry) { + if (err) { + errorCallback(err); + } else { + var fakeNativeFileSystem = { root: entry }; + successCallback(fakeNativeFileSystem); } }); }, /** - * NativeFileSystem implementation of LocalFileSystem.resolveLocalFileSystemURL() - * - * @param {string} path A URL referring to a local file in a filesystem accessable via this API. - * @param {function(Entry)} successCallback Callback function for successful operations. - * @param {function(DOMError)=} errorCallback Callback function for error operations. + * @deprecated + * @param {string} path + * @param {!function(!FileSystemEntry)} successCallback + * @param {!function(string)} errorCallback */ resolveNativeFileSystemPath: function (path, successCallback, errorCallback) { - brackets.fs.stat(path, function (err, stats) { - if (!err) { - var entry; - - if (stats.isDirectory()) { - entry = new NativeFileSystem.DirectoryEntry(path); - } else { - entry = new NativeFileSystem.FileEntry(path); - } - + _warn("NativeFileSystem.resolveNativeFileSystemPath()", "FileSystem.resolve()"); + + FileSystem.resolve(path, function (err, entry) { + if (err) { + errorCallback(err); + } else { successCallback(entry); - } else if (errorCallback) { - errorCallback(new NativeFileError(NativeFileSystem._fsErrorToDOMErrorName(err))); } }); - }, - - /** - * Public static method to check if a file path is relative one - * @param {string} path A file path to check - * @return {boolean} True if the path is relative - */ - isRelativePath: function (path) { - // If the path contains a colon on Windows it must be a full path (colons are - // not valid path characters on mac or in URIs) - if (brackets.platform === "win" && path.indexOf(":") !== -1) { - return false; - } - - // For everyone else, absolute paths start with a "/" - return path[0] !== "/"; - }, - - /** - * Converts a brackets.fs.ERR_* error code to a NativeFileError.* error name - * @param {number} fsErr A brackets.fs error code - * @return {string} An error name out of the possible NativeFileError.* names - */ - _fsErrorToDOMErrorName: function (fsErr) { - var error; - - switch (fsErr) { - // We map ERR_UNKNOWN and ERR_INVALID_PARAMS to SECURITY_ERR, - // since there aren't specific mappings for these. - case brackets.fs.ERR_UNKNOWN: - case brackets.fs.ERR_INVALID_PARAMS: - error = NativeFileError.SECURITY_ERR; - break; - case brackets.fs.ERR_NOT_FOUND: - error = NativeFileError.NOT_FOUND_ERR; - break; - case brackets.fs.ERR_CANT_READ: - error = NativeFileError.NOT_READABLE_ERR; - break; - case brackets.fs.ERR_UNSUPPORTED_ENCODING: - error = NativeFileError.NOT_READABLE_ERR; - break; - case brackets.fs.ERR_CANT_WRITE: - error = NativeFileError.NO_MODIFICATION_ALLOWED_ERR; - break; - case brackets.fs.ERR_OUT_OF_SPACE: - error = NativeFileError.QUOTA_EXCEEDED_ERR; - break; - case brackets.fs.PATH_EXISTS_ERR: - error = NativeFileError.PATH_EXISTS_ERR; - break; - default: - // The HTML file spec says SECURITY_ERR is a catch-all to be used in situations - // not covered by other error codes. - error = NativeFileError.SECURITY_ERR; - } - return error; } + }; - /** - * Static class that contains constants for file - * encoding types. - */ - NativeFileSystem.Encodings = {}; - NativeFileSystem.Encodings.UTF8 = "UTF-8"; - NativeFileSystem.Encodings.UTF16 = "UTF-16"; - /** - * Internal static class that contains constants for file - * encoding types to be used by internal file system - * implementation. - */ - NativeFileSystem._FSEncodings = {}; - NativeFileSystem._FSEncodings.UTF8 = "utf8"; - NativeFileSystem._FSEncodings.UTF16 = "utf16"; + // Shims for constructors - return new File/Directory object instead /** - * Converts an IANA encoding name to internal encoding name. - * http://www.iana.org/assignments/character-sets - * - * @param {string} encoding The IANA encoding string. + * @deprecated + * @param {string} fullPath + * @return {!File} */ - NativeFileSystem.Encodings._IANAToFS = function (encoding) { - //IANA names are case-insensitive - encoding = encoding.toUpperCase(); - - switch (encoding) { - case (NativeFileSystem.Encodings.UTF8): - return NativeFileSystem._FSEncodings.UTF8; - case (NativeFileSystem.Encodings.UTF16): - return NativeFileSystem._FSEncodings.UTF16; - default: - return undefined; - } + NativeFileSystem.FileEntry = function (fullPath) { + _warn("new NativeFileSystem.FileEntry()", "FileSystem.getFileForPath()"); + return FileSystem.getFileForPath(fullPath); }; - var Encodings = NativeFileSystem.Encodings; - var _FSEncodings = NativeFileSystem._FSEncodings; - /** - * Implementation of w3 Entry interface: - * http://www.w3.org/TR/2011/WD-file-system-api-20110419/#the-entry-interface - * - * Base class for representing entries in a file system (FileEntry or DirectoryEntry) - * - * @constructor - * @param {string} fullPath The full absolute path from the root to the entry - * @param {boolean} isDirectory Indicates that the entry is a directory - * @param {FileSystem} fs File system that contains this entry + * @deprecated + * @param {string} fullPath + * @return {!Directory} */ - NativeFileSystem.Entry = function (fullPath, isDirectory, fs) { - this.isDirectory = isDirectory; - this.isFile = !isDirectory; - - if (fullPath) { - // add trailing "/" to directory paths - if (isDirectory && (fullPath.charAt(fullPath.length - 1) !== "/")) { - fullPath = fullPath.concat("/"); - } - } - - this.fullPath = fullPath; - - this.name = null; // default if extraction fails - if (fullPath) { - var pathParts = fullPath.split("/"); - - // Extract name from the end of the fullPath (account for trailing slash(es)) - while (!this.name && pathParts.length) { - this.name = pathParts.pop(); - } - } - - this.filesystem = fs; + NativeFileSystem.DirectoryEntry = function (fullPath) { + _warn("new NativeFileSystem.DirectoryEntry()", "FileSystem.getDirectoryForPath()"); + return FileSystem.getDirectoryForPath(fullPath); }; - /** - * Moves this Entry to a different location on the file system. - * @param {!DirectoryEntry} parent The directory to move the entry to - * @param {string=} newName The new name of the entry. If not specified, defaults to the current name - * @param {function(Array.)=} successCallback Callback function for successful operations - * @param {function(DOMError)=} errorCallback Callback function for error operations - */ - NativeFileSystem.Entry.prototype.moveTo = function (parent, newName, successCallback, errorCallback) { - // TODO (issue #241) - // http://www.w3.org/TR/2011/WD-file-system-api-20110419/#widl-Entry-moveTo - }; - - /** - * Copies this Entry to a different location on the file system. - * @param {!DirectoryEntry} parent The directory to copy the entry to - * @param {string=} newName The new name of the entry. If not specified, defaults to the current name - * @param {function(Array.)=} successCallback Callback function for successful operations - * @param {function(DOMError)=} errorCallback Callback function for error operations - */ - NativeFileSystem.Entry.prototype.copyTo = function (parent, newName, successCallback, errorCallback) { - // TODO (issue #241) - // http://www.w3.org/TR/2011/WD-file-system-api-20110419/#widl-Entry-copyTo - }; - /** - * Generates a URL that can be used to identify this Entry - * @param {string=} mimeType The mime type to be used to interpret the file for a FileEntry - * @returns {string} A usable URL to identify this Entry in the current filesystem - */ - NativeFileSystem.Entry.prototype.toURL = function (mimeType) { - // TODO (issue #241) - // http://www.w3.org/TR/2011/WD-file-system-api-20110419/#widl-Entry-toURL - - // Check updated definition at - // http://www.w3.org/TR/2012/WD-file-system-api-20120417/#widl-Entry-toURL-DOMString - }; + // Shims for members of File/Directory - monkey-patch the prototype to make them available + // without polluting the new filesystem code /** - * Deletes a file or directory by moving to the trash/recycle bin. - * @param {function()} successCallback Callback function for successful operations - * @param {function(DOMError)=} errorCallback Callback function for error operations + * @deprecated + * @param {!function()} callback */ - NativeFileSystem.Entry.prototype.remove = function (successCallback, errorCallback) { - var deleteFunc = brackets.fs.moveToTrash; // Future: Could fallback to unlink + File.prototype.createWriter = function (callback) { + _warn("File.createWriter()", "File.write()"); - if (!deleteFunc) { - // Running in a shell that doesn't support moveToTrash. Return an error. - errorCallback(brackets.fs.ERR_UNKNOWN); - return; - } + var file = this; - deleteFunc(this.fullPath, function (err) { - if (err === brackets.fs.NO_ERROR) { - successCallback(); - } else { - errorCallback(err); - } - }); - }; - - /** - * Look up the parent DirectoryEntry that contains this Entry - * @param {function(Array.)} successCallback Callback function for successful operations - * @param {function(DOMError)=} errorCallback Callback function for error operations - */ - NativeFileSystem.Entry.prototype.getParent = function (successCallback, errorCallback) { - // TODO (issue #241) - // http://www.w3.org/TR/2011/WD-file-system-api-20110419/#widl-Entry-remove - }; - - /** - * Look up metadata about this Entry - * @param {function(Metadata)} successCallback Callback function for successful operations - * @param {function(DOMError)=} errorCallback Callback function for error operations - */ - NativeFileSystem.Entry.prototype.getMetadata = function (successCallBack, errorCallback) { - brackets.fs.stat(this.fullPath, function (err, stat) { - if (err === brackets.fs.NO_ERROR) { - var metadata = new NativeFileSystem.Metadata(stat.mtime, stat.size); - successCallBack(metadata); - } else { - errorCallback(new NativeFileError(NativeFileSystem._fsErrorToDOMErrorName(err))); - } - }); - }; - - - /** - * Implementation of w3 Metadata interface: - * http://www.w3.org/TR/2011/WD-file-system-api-20110419/#the-metadata-interface - * - * Supplies information about the state of a file or directory - * @constructor - * @param {Date} modificationTime Time at which the file or directory was last modified - * @param {Number} size the size in bytes of the file - */ - NativeFileSystem.Metadata = function (modificationTime, size) { - // modificationTime is read only - this.modificationTime = modificationTime; - this.size = size; - }; - - /** - * Implementation of w3 FileEntry interface: - * http://www.w3.org/TR/2011/WD-file-system-api-20110419/#the-fileentry-interface - * - * A FileEntry represents a file on a file system. - * - * @constructor - * @param {string} name Full path of the file in the file system - * @param {FileSystem} fs File system that contains this entry - * @extends {Entry} - */ - NativeFileSystem.FileEntry = function (name, fs) { - NativeFileSystem.Entry.call(this, name, false, fs); - }; - NativeFileSystem.FileEntry.prototype = Object.create(NativeFileSystem.Entry.prototype); - NativeFileSystem.FileEntry.prototype.constructor = NativeFileSystem.FileEntry; - NativeFileSystem.FileEntry.prototype.parentClass = NativeFileSystem.Entry.prototype; - - NativeFileSystem.FileEntry.prototype.toString = function () { - return "[FileEntry " + this.fullPath + "]"; - }; - - /** - * Creates a new FileWriter associated with the file that this FileEntry represents. - * @param {function(FileWriter)} successCallback Callback function for successful operations - * @param {function(DOMError)=} errorCallback Callback function for error operations - */ - NativeFileSystem.FileEntry.prototype.createWriter = function (successCallback, errorCallback) { - var fileEntry = this; - - /** - * Implementation of w3 FileWriter interface: - * http://www.w3.org/TR/2011/WD-file-writer-api-20110419/#the-filewriter-interface - * - * A FileWriter expands on the FileSaver interface to allow for multiple write actions, - * rather than just saving a single Blob. - * - * @constructor - * @param {Blob} data The Blob of data to be saved to a file - * @extends {FileSaver} - */ - var FileWriter = function (data) { - NativeFileSystem.FileSaver.call(this, data); - - // FileWriter private memeber vars - this._length = 0; - this._position = 0; - }; - - /** - * The length of the file - */ - FileWriter.prototype.length = function () { - return this._length; - }; - - /** - * The byte offset at which the next write to the file will occur. - */ - FileWriter.prototype.position = function () { - return this._position; - }; - - /** - * Write the supplied data to the file at position - * @param {string} data The data to write - */ - FileWriter.prototype.write = function (data) { - // TODO (issue #241): handle Blob data instead of string - // http://www.w3.org/TR/2011/WD-file-writer-api-20110419/#widl-FileWriter-write - - if (data === null || data === undefined) { - console.error("FileWriter.write() called with null or undefined data."); - } - - if (this.readyState === NativeFileSystem.FileSaver.WRITING) { - throw new NativeFileSystem.FileException(NativeFileSystem.FileException.INVALID_STATE_ERR); - } - - this._readyState = NativeFileSystem.FileSaver.WRITING; - - if (this.onwritestart) { - // TODO (issue #241): progressevent - this.onwritestart(); - } - - var self = this; - - brackets.fs.writeFile(fileEntry.fullPath, data, _FSEncodings.UTF8, function (err) { - - if ((err !== brackets.fs.NO_ERROR) && self.onerror) { - var fileError = new NativeFileError(NativeFileSystem._fsErrorToDOMErrorName(err)); - - // TODO (issue #241): set readonly FileSaver.error attribute - // self._error = fileError; - self.onerror(fileError); - - // TODO (issue #241): partial write, update length and position - } - // else { - // TODO (issue #241): After changing data argument to Blob, use - // Blob.size to update position and length upon successful - // completion of a write. - - // self.position = ; - // self.length = ; - // } - - // DONE is set regardless of error - self._readyState = NativeFileSystem.FileSaver.DONE; + // Return fake FileWriter object + // (Unlike the original impl, we don't read the file's old contents first) + callback({ + write: function (data) { + var fileWriter = this; - if (self.onwrite) { - // TODO (issue #241): progressevent - self.onwrite(); + if (fileWriter.onwritestart) { + fileWriter.onwritestart(); } - - if (self.onwriteend) { - // TODO (issue #241): progressevent - self.onwriteend(); - } - }); - }; - - /** - * Seek sets the file position at which the next write will occur - * @param {number} offset An absolute byte offset into the file. If offset is greater than - * length, length is used instead. If offset is less than zero, length - * is added to it, so that it is treated as an offset back from the end - * of the file. If it is still less than zero, zero is used - */ - FileWriter.prototype.seek = function (offset) { - // TODO (issue #241) - // http://www.w3.org/TR/2011/WD-file-writer-api-20110419/#widl-FileWriter-seek - }; - - /** - * Changes the length of the file to that specified - * @param {number} size The size to which the length of the file is to be adjusted, - * measured in bytes - */ - FileWriter.prototype.truncate = function (size) { - // TODO (issue #241) - // http://www.w3.org/TR/2011/WD-file-writer-api-20110419/#widl-FileWriter-truncate - }; - - var fileWriter = new FileWriter(); - - // initialize file length - var result = new $.Deferred(); - brackets.fs.readFile(fileEntry.fullPath, _FSEncodings.UTF8, function (err, contents) { - // Ignore "file not found" errors. It's okay if the file doesn't exist yet. - if (err !== brackets.fs.ERR_NOT_FOUND) { - fileWriter._err = err; - } - - if (contents) { - fileWriter._length = contents.length; - } - - result.resolve(); - }); - - result.done(function () { - if (fileWriter._err && (errorCallback !== undefined)) { - errorCallback(new NativeFileError(NativeFileSystem._fsErrorToDOMErrorName(fileWriter._err))); - } else if (successCallback !== undefined) { - successCallback(fileWriter); - } - }); - }; - - /** - * Returns a File that represents the current state of the file that this FileEntry represents - * @param {function(File)} successCallback Callback function for successful operations - * @param {function(DOMError)=} errorCallback Callback function for error operations - */ - NativeFileSystem.FileEntry.prototype.file = function (successCallback, errorCallback) { - var newFile = new NativeFileSystem.File(this); - successCallback(newFile); - - // TODO (issue #241): errorCallback - }; - - /** - * An InaccessibleFileEntry represents an inaccessible file on a file system. - * In particular, InaccessibleFileEntry objects are used as in the representation - * of untitled Documents. - * - * @constructor - * @param {string} name Full path of the file in the file system - * @param {FileSystem} fs File system that contains this entry - * @extends {FileEntry} - */ - NativeFileSystem.InaccessibleFileEntry = function (name, mtime) { - NativeFileSystem.FileEntry.call(this, name, false); - this.mtime = mtime; - }; - - NativeFileSystem.InaccessibleFileEntry.prototype = Object.create(NativeFileSystem.FileEntry.prototype); - NativeFileSystem.InaccessibleFileEntry.prototype.constructor = NativeFileSystem.InaccessibleFileEntry; - NativeFileSystem.InaccessibleFileEntry.prototype.parentClass = NativeFileSystem.FileEntry.prototype; - - NativeFileSystem.InaccessibleFileEntry.prototype.createWriter = function (successCallback, errorCallback) { - console.error("InaccessibleFileEntry.createWriter is unsupported"); - errorCallback(new NativeFileError(NativeFileError.NOT_FOUND_ERR)); - }; - - NativeFileSystem.InaccessibleFileEntry.prototype.file = function (successCallback, errorCallback) { - console.error("InaccessibleFileEntry.file is unsupported"); - errorCallback(new NativeFileError(NativeFileError.NOT_FOUND_ERR)); - }; - - NativeFileSystem.InaccessibleFileEntry.prototype.getMetadata = function (successCallback, errorCallback) { - successCallback(new NativeFileSystem.Metadata(this.mtime)); - }; - - NativeFileSystem.InaccessibleFileEntry.prototype.remove = function (successCallback, errorCallback) { - console.error("InaccessibleFileEntry.remove is unsupported"); - errorCallback(new NativeFileError(NativeFileSystem.NOT_FOUND_ERR)); - }; - - /** - * This class extends the FileException interface described in to add - * several new error codes. Any errors that need to be reported synchronously, - * including all that occur during use of the synchronous filesystem methods, - * are reported using the FileException exception. - * - * @param {?number=} code The code attribute, on getting, must return one of the - * constants of the FileException exception, which must be the most appropriate - * code from the table below. - */ - NativeFileSystem.FileException = function (code) { - this.code = code || 0; - }; - - // FileException constants - Object.defineProperties( - NativeFileSystem.FileException, - { - NOT_FOUND_ERR: { value: 1, writable: false }, - SECURITY_ERR: { value: 2, writable: false }, - ABORT_ERR: { value: 3, writable: false }, - NOT_READABLE_ERR: { value: 4, writable: false }, - ENCODING_ERR: { value: 5, writable: false }, - NO_MODIFICATION_ALLOWED_ERR: { value: 6, writable: false }, - INVALID_STATE_ERR: { value: 7, writable: false }, - SYNTAX_ERR: { value: 8, writable: false }, - QUOTA_EXCEEDED_ERR: { value: 10, writable: false } - } - ); - - /** - * Implementation of w3 FileSaver interface - * http://www.w3.org/TR/2011/WD-file-writer-api-20110419/#the-filesaver-interface - * - * FileSaver provides methods to monitor the asynchronous writing of blobs - * to disk using progress events and event handler attributes. - * - * @constructor - * @param {Blob} data The Blob of data to be saved to a file - */ - NativeFileSystem.FileSaver = function (data) { - // FileSaver private member vars - this._data = data; - this._readyState = NativeFileSystem.FileSaver.INIT; - this._error = null; - }; - - // FileSaver constants - Object.defineProperties( - NativeFileSystem.FileSaver, - { - INIT: { value: 1, writable: false }, - WRITING: { value: 2, writable: false }, - DONE: { value: 3, writable: false } - } - ); - - /** - * The state the FileSaver object is at the moment (INIT, WRITING, DONE) - */ - NativeFileSystem.FileSaver.prototype.readyState = function () { - return this._readyState; - }; - - /** - * Aborts a saving operation - */ - NativeFileSystem.FileSaver.prototype.abort = function () { - // TODO (issue #241): http://dev.w3.org/2009/dap/file-system/file-writer.html#widl-FileSaver-abort-void - - // If readyState is DONE or INIT, terminate this overall series of steps without doing anything else.. - if (this._readyState === NativeFileSystem.FileSaver.INIT || this._readyState === NativeFileSystem.FileSaver.DONE) { - return; - } - - // TODO (issue #241): Terminate any steps having to do with writing a file. - - // Set the error attribute to a FileError object with the code ABORT_ERR. - this._error = new NativeFileError(NativeFileError.ABORT_ERR); - - // Set readyState to DONE. - this._readyState = NativeFileSystem.FileSaver.DONE; - - /* - TODO (issue #241): - Dispatch a progress event called abort - Dispatch a progress event called writeend - Stop dispatching any further progress events. - Terminate this overall set of steps. - */ - }; - - /** - * Implementation of w3 DirectoryEntry interface: - * http://www.w3.org/TR/2011/WD-file-system-api-20110419/#the-directoryentry-interface - * - * The DirectoryEntry class represents a directory on a file system. - * - * @constructor - * @param {string} name Full path of the directory in the file system - * @param {FileSystem} fs File system that contains this entry - * @extends {Entry} - */ - NativeFileSystem.DirectoryEntry = function (name, fs) { - NativeFileSystem.Entry.call(this, name, true, fs); - - // TODO (issue #241): void removeRecursively (VoidCallback successCallback, optional ErrorCallback errorCallback); - }; - NativeFileSystem.DirectoryEntry.prototype = Object.create(NativeFileSystem.Entry.prototype); - NativeFileSystem.DirectoryEntry.prototype.constructor = NativeFileSystem.DirectoryEntry; - NativeFileSystem.DirectoryEntry.prototype.parentClass = NativeFileSystem.Entry.prototype; - - NativeFileSystem.DirectoryEntry.prototype.toString = function () { - return "[DirectoryEntry " + this.fullPath + "]"; - }; - - /** - * Creates or looks up a directory - * @param {string} path Either an absolute path or a relative path from this DirectoryEntry - * to the directory to be looked up or created - * @param {{create:?boolean, exclusive:?boolean}=} options Object with the flags "create" - * and "exclusive" to modify the method behavior based on - * http://www.w3.org/TR/2011/WD-file-system-api-20110419/#widl-DirectoryEntry-getDirectory - * @param {function(Entry)=} successCallback Callback function for successful operations - * @param {function(DOMError)=} errorCallback Callback function for error operations - */ - NativeFileSystem.DirectoryEntry.prototype.getDirectory = function (path, options, successCallback, errorCallback) { - var directoryFullPath = path, - filesystem = this.filesystem; - - // resolve relative paths relative to the DirectoryEntry - if (NativeFileSystem.isRelativePath(path)) { - directoryFullPath = this.fullPath + path; - } - - var createDirectoryEntry = function () { - if (successCallback) { - successCallback(new NativeFileSystem.DirectoryEntry(directoryFullPath, filesystem)); - } - }; - - var createDirectoryError = function (err) { - if (errorCallback) { - errorCallback(new NativeFileError(NativeFileSystem._fsErrorToDOMErrorName(err))); - } - }; - - // Use stat() to check if file exists - brackets.fs.stat(directoryFullPath, function (err, stats) { - if ((err === brackets.fs.NO_ERROR)) { - // NO_ERROR implies the path already exists - - // throw error if the file the path is not a directory - if (!stats.isDirectory()) { - if (errorCallback) { - errorCallback(new NativeFileError(NativeFileError.TYPE_MISMATCH_ERR)); - } - - return; - } - - // throw error if the file exists but create is exclusive - if (options.create && options.exclusive) { - if (errorCallback) { - errorCallback(new NativeFileError(NativeFileError.PATH_EXISTS_ERR)); - } - - return; - } - - // Create a file entry for the existing directory. If create == true, - // a file entry is created without error. - createDirectoryEntry(); - } else if (err === brackets.fs.ERR_NOT_FOUND) { - // ERR_NOT_FOUND implies we write a new, empty file - - // create the file - if (options.create) { - // TODO: Pass permissions. The current implementation of fs.makedir() always - // creates the directory with the full permissions available to the current user. - brackets.fs.makedir(directoryFullPath, 0, function (err) { - if (err) { - createDirectoryError(err); - } else { - createDirectoryEntry(); + + file.write(data, function (err) { + if (err) { + if (fileWriter.onerror) { + fileWriter.onerror(err); } - }); - return; - } - - // throw error if file not found and the create == false - if (errorCallback) { - errorCallback(new NativeFileError(NativeFileError.NOT_FOUND_ERR)); - } - } else { - // all other brackets.fs.stat() errors - createDirectoryError(err); - } - }); - }; - - /** - * Deletes a directory and all of its contents, if any - * @param {function()} successCallback Callback function for successful operations - * @param {function(DOMError)=} errorCallback Callback function for error operations - */ - NativeFileSystem.DirectoryEntry.prototype.removeRecursively = function (successCallback, errorCallback) { - // TODO (issue #241) - // http://www.w3.org/TR/2011/WD-file-system-api-20110419/#widl-DirectoryEntry-removeRecursively - }; - - /** - * Creates a new DirectoryReader to read Entries from this Directory - * @returns {DirectoryReader} A DirectoryReader instance to read the Directory's entries - */ - NativeFileSystem.DirectoryEntry.prototype.createReader = function () { - var dirReader = new NativeFileSystem.DirectoryReader(); - dirReader._directory = this; - - return dirReader; - }; - - /** - * Creates or looks up a file. - * - * @param {string} path Either an absolute path or a relative path from this - * DirectoryEntry to the file to be looked up or created. It is an error - * to attempt to create a file whose immediate parent does not yet - * exist. - * @param {{create:?boolean, exclusive:?boolean}=} options Object with the flags "create" - * and "exclusive" to modify the method behavior based on - * http://www.w3.org/TR/2011/WD-file-system-api-20110419/#widl-DirectoryEntry-getFile - * @param {function(FileEntry)=} successCallback Callback function for successful operations - * @param {function(DOMError)=} errorCallback Callback function for error operations - */ - NativeFileSystem.DirectoryEntry.prototype.getFile = function (path, options, successCallback, errorCallback) { - var fileFullPath = path, - filesystem = this.filesystem; - - // resolve relative paths relative to the DirectoryEntry - if (NativeFileSystem.isRelativePath(path)) { - fileFullPath = this.fullPath + path; - } - - var createFileEntry = function () { - if (successCallback) { - successCallback(new NativeFileSystem.FileEntry(fileFullPath, filesystem)); - } - }; - - var createFileError = function (err) { - if (errorCallback) { - errorCallback(new NativeFileError(NativeFileSystem._fsErrorToDOMErrorName(err))); - } - }; - - // Use stat() to check if file exists - brackets.fs.stat(fileFullPath, function (err, stats) { - if ((err === brackets.fs.NO_ERROR)) { - // NO_ERROR implies the path already exists - - // throw error if the file the path is a directory - if (stats.isDirectory()) { - if (errorCallback) { - errorCallback(new NativeFileError(NativeFileError.TYPE_MISMATCH_ERR)); - } - - return; - } - - // throw error if the file exists but create is exclusive - if (options.create && options.exclusive) { - if (errorCallback) { - errorCallback(new NativeFileError(NativeFileError.PATH_EXISTS_ERR)); - } - - return; - } - - // Create a file entry for the existing file. If create == true, - // a file entry is created without error. - createFileEntry(); - } else if (err === brackets.fs.ERR_NOT_FOUND) { - // ERR_NOT_FOUND implies we write a new, empty file - - // create the file - if (options.create) { - brackets.fs.writeFile(fileFullPath, "", _FSEncodings.UTF8, function (err) { - if (err) { - createFileError(err); - } else { - createFileEntry(); + } else { + if (fileWriter.onwrite) { + fileWriter.onwrite(); } - }); - - return; - } - - // throw error if file not found and the create == false - if (errorCallback) { - errorCallback(new NativeFileError(NativeFileError.NOT_FOUND_ERR)); - } - } else { - // all other brackets.fs.stat() errors - createFileError(err); + if (fileWriter.onwriteend) { + fileWriter.onwriteend(); + } + } + }); } }); }; - - /** - * Implementation of w3 DirectoryReader interface: - * http://www.w3.org/TR/2011/WD-file-system-api-20110419/#the-directoryreader-interface - * - * A DirectoryReader lets a user list files and directories in a directory - * - * @constructor - */ - NativeFileSystem.DirectoryReader = function () { - - }; - + /** - * Read the next block of entries from this directory - * @param {function(Array.)} successCallback Callback function for successful operations - * @param {function(DOMError, ?Array.)=} errorCallback Callback function for error operations, - * which may include an array of entries that could be read without error + * @deprecated + * @return {!{readEntries: !function()}} */ - NativeFileSystem.DirectoryReader.prototype.readEntries = function (successCallback, errorCallback) { - if (!this._directory.fullPath) { - errorCallback(new NativeFileError(NativeFileError.PATH_EXISTS_ERR)); - return; - } - - var rootPath = this._directory.fullPath, - filesystem = this.filesystem, - timeout = NativeFileSystem.ASYNC_TIMEOUT, - networkDetectionResult = new $.Deferred(); + Directory.prototype.createReader = function () { + _warn("Directory.createReader()", "Directory.getContents()"); - if (brackets.fs.isNetworkDrive) { - brackets.fs.isNetworkDrive(rootPath, function (err, remote) { - if (!err && remote) { - timeout = NativeFileSystem.ASYNC_NETWORK_TIMEOUT; - } - networkDetectionResult.resolve(); - }); - } else { - networkDetectionResult.resolve(); - } + var dir = this; - networkDetectionResult.done(function () { - brackets.fs.readdir(rootPath, function (err, filelist) { - if (!err) { - var entries = []; - var lastError = null; - - // call success immediately if this directory has no files - if (filelist.length === 0) { + // Return fake DirectoryReader object + return { + readEntries: function (successCallback, errorCallback) { + dir.getContents(function (err, entries) { + if (err) { + errorCallback(err); + } else { successCallback(entries); - return; } - - // stat() to determine type of each entry, then populare entries array with objects - var masterPromise = Async.doInParallel(filelist, function (filename, index) { - - var deferred = new $.Deferred(); - var itemFullPath = rootPath + filelist[index]; - - brackets.fs.stat(itemFullPath, function (statErr, statData) { - if (!statErr) { - if (statData.isDirectory()) { - entries[index] = new NativeFileSystem.DirectoryEntry(itemFullPath, filesystem); - } else if (statData.isFile()) { - entries[index] = new NativeFileSystem.FileEntry(itemFullPath, filesystem); - } else { - entries[index] = null; // neither a file nor a dir, so don't include it - } - deferred.resolve(); - } else { - entries[index] = null; // failed to stat this file, so don't include it - lastError = new NativeFileError(NativeFileSystem._fsErrorToDOMErrorName(statErr)); - deferred.reject(lastError); - } - }); - - return deferred.promise(); - }, false); - - // We want the error callback to get called after some timeout (in case some deferreds don't return). - // So, we need to wrap masterPromise in another deferred that has this timeout functionality - var timeoutWrapper = Async.withTimeout(masterPromise, timeout); - - // The entries array may have null values if stat returned things that were - // neither a file nor a dir. So, we need to clean those out. - var cleanedEntries = []; - - // Add the callbacks to this top-level Promise, which wraps all the individual deferred objects - timeoutWrapper.always(function () { // always clean the successful entries - entries.forEach(function (entry) { - if (entry) { - cleanedEntries.push(entry); - } - }); - }).done(function () { // success - successCallback(cleanedEntries); - }).fail(function (err) { // error - if (err === Async.ERROR_TIMEOUT) { - // SECURITY_ERR is the HTML5 File catch-all error, and there isn't anything - // more fitting for a timeout. - err = new NativeFileError(NativeFileError.SECURITY_ERR); - } else { - err = lastError; - } - - if (errorCallback) { - errorCallback(err, cleanedEntries); - } - }); - - } else { // There was an error reading the initial directory. - errorCallback(new NativeFileError(NativeFileSystem._fsErrorToDOMErrorName(err))); - } - }); - }); - }; - - /** - * Implementation of w3 FileReader interface: - * http://www.w3.org/TR/2011/WD-FileAPI-20111020/#FileReader-interface - * - * A FileReader provides methods to read File objects or Blob objects into memory, and to - * access the data from those Files or Blobs using progress events and event handler attributes - * - * @constructor - */ - NativeFileSystem.FileReader = function () { - // TODO (issue #241): this classes should extend EventTarget - - // states - this.EMPTY = 0; - this.LOADING = 1; - this.DONE = 2; - - // readyState is read only - this.readyState = this.EMPTY; - - // File or Blob data - // TODO (issue #241): readonly attribute any result; - // TODO (issue #241): readonly attribute DOMError error; - - // event handler attributes - this.onloadstart = null; - this.onprogress = null; - this.onload = null; - this.onabort = null; - this.onerror = null; - this.onloadend = null; - }; - // TODO (issue #241): extend EventTarget (draft status, not implememnted in webkit) - // NativeFileSystem.FileReader.prototype = Object.create(NativeFileSystem.EventTarget.prototype); - - /** - * Reads a Blob as an array buffer - * @param {Blob} blob The data to read - */ - NativeFileSystem.FileReader.prototype.readAsArrayBuffer = function (blob) { - // TODO (issue #241): implement - // http://www.w3.org/TR/2011/WD-FileAPI-20111020/#dfn-readAsArrayBuffer - }; - - /** - * Reads a Blob as a binary string - * @param {Blob} blob The data to read - */ - NativeFileSystem.FileReader.prototype.readAsBinaryString = function (blob) { - // TODO (issue #241): implement - // http://www.w3.org/TR/2011/WD-FileAPI-20111020/#dfn-readAsBinaryStringAsync - }; - - /** - * Reads a Blob as a data url - * @param {Blob} blob The data to read - */ - NativeFileSystem.FileReader.prototype.readAsDataURL = function (blob) { - // TODO (issue #241): implement - // http://www.w3.org/TR/2011/WD-FileAPI-20111020/#dfn-readAsDataURL - }; - - /** - * Aborts a File reading operation - */ - NativeFileSystem.FileReader.prototype.abort = function () { - // TODO (issue #241): implement - // http://www.w3.org/TR/2011/WD-FileAPI-20111020/#dfn-abort + }); + } + }; }; /** - * Reads a Blob as text - * @param {Blob} blob The data to read - * @param {string=} encoding (IANA Encoding Name) + * @deprecated + * @param {string} path + * @param {?Object} options + * @param {!function(!FileSystemEntry)} successCallback + * @param {!function(string)} errorCallback */ - NativeFileSystem.FileReader.prototype.readAsText = function (blob, encoding) { - var self = this; - - if (!encoding) { - encoding = Encodings.UTF8; + Directory.prototype.getFile = function (path, options, successCallback, errorCallback) { + if (options && options.create) { + throw new Error("Directory.getFile() is deprecated and no longer supports 'create: true'. Use File.write(\"\") instead."); + } else { + _warn("Directory.getFile()", "FileSystem.resolve()"); } - var internalEncoding = Encodings._IANAToFS(encoding); - - if (this.readyState === this.LOADING) { - throw new InvalidateStateError(); - } - - this.readyState = this.LOADING; - - if (this.onloadstart) { - this.onloadstart(); // TODO (issue #241): progressevent + // Is it a relative path? + if (path[0] !== "/" && path[1] !== ":") { + path = this.fullPath + path; } - - brackets.fs.readFile(blob._fullPath, internalEncoding, function (err, data) { - - // TODO (issue #241): the event objects passed to these event handlers is fake and incomplete right now - var fakeEvent = { - loaded: 0, - total: 0 - }; - - // The target for this event is the FileReader and the data/err result is stored in the FileReader - fakeEvent.target = self; - self.result = data; - self.error = new NativeFileError(NativeFileSystem._fsErrorToDOMErrorName(err)); - + + FileSystem.resolve(path, function (err, entry) { if (err) { - self.readyState = self.DONE; - if (self.onerror) { - self.onerror(fakeEvent); - } + errorCallback(err); } else { - self.readyState = self.DONE; - - // TODO (issue #241): this should be the file/blob size, but we don't have code to get that yet, so for know assume a file size of 1 - // and since we read the file in one go, assume 100% after the first read - fakeEvent.loaded = 1; - fakeEvent.total = 1; - - if (self.onprogress) { - self.onprogress(fakeEvent); - } - - // TODO (issue #241): onabort not currently supported since our native implementation doesn't support it - // if (self.onabort) - // self.onabort(fakeEvent); - - if (self.onload) { - self.onload(fakeEvent); - } - - if (self.onloadend) { - self.onloadend(); + if (entry.isDirectory) { + errorCallback(NativeFileError.TYPE_MISMATCH_ERR); + } else { + successCallback(entry); } } - }); }; - - /** - * Implementation of w3 Blob interface: - * http://www.w3.org/TR/2011/WD-FileAPI-20111020/#blob - * - * A Blob represents immutable raw data. - * - * @constructor - * @param {string} fullPath Absolute path of the Blob - */ - NativeFileSystem.Blob = function (fullPath) { - this._fullPath = fullPath; - - // TODO (issue #241): implement, readonly - this.size = 0; - - // TODO (issue #241): implement, readonly - this.type = null; - }; - /** - * Returns a new Blob object with bytes ranging from the optional start parameter - * up to but not including the optional end parameter - * @param {number=} start Start point of a slice treated as a byte-order position - * @param {number=} end End point of a slice. If end is undefined, size will be used. If - * end is negative, max(size+end, 0) will be used. In any other case, - * the slice will finish at min(end, size) - * @param {string=} contentType HTTP/1.1 Content-Type header on the Blob - * @returns {Blob} - */ - NativeFileSystem.Blob.prototype.slice = function (start, end, contentType) { - // TODO (issue #241): implement - // http://www.w3.org/TR/2011/WD-FileAPI-20111020/#dfn-slice - }; - /** - * Implementation of w3 File interface: - * http://www.w3.org/TR/2011/WD-FileAPI-20111020/#file - * - * @constructor - * @param {Entry} entry The Entry pointing to the File - * @extends {Blob} - */ - NativeFileSystem.File = function (entry) { - NativeFileSystem.Blob.call(this, entry.fullPath); - - // TODO (issue #241): implement, readonly - this.name = ""; - - // TODO (issue #241): implement, readonly - this.lastModifiedDate = null; - }; + // Fail-fast stubs for key methods that are not shimmed -- callers will break but with clear guidance in the exception message - /** - * Implementation of w3 FileSystem interface - * http://www.w3.org/TR/file-system-api/#the-filesystem-interface - * - * FileSystem represents a file system - */ - NativeFileSystem.FileSystem = function (path) { - - /** - * This is the name of the file system and must be unique across the list - * of exposed file systems. - * @const - * @type {string} - */ - Object.defineProperty(this, "name", { - value: path, - writable: false - }); - - /** - * The root directory of the file system. - * @const - * @type {DirectoryEntry} - */ - Object.defineProperty(this, "root", { - value: new NativeFileSystem.DirectoryEntry(path, this), - writable: false - }); + /** @deprecated */ + Directory.prototype.getDirectory = function () { + throw new Error("Directory.getDirectory() has been removed. Use FileSystem.getDirectoryForPath() and/or Directory.create() instead."); }; - + + // Define public API exports.NativeFileSystem = NativeFileSystem; }); diff --git a/src/filesystem/Directory.js b/src/filesystem/Directory.js new file mode 100644 index 00000000000..293cf074e43 --- /dev/null +++ b/src/filesystem/Directory.js @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + + +/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */ +/*global define */ + +define(function (require, exports, module) { + "use strict"; + + var FileSystemEntry = require("filesystem/FileSystemEntry"); + + /* + * @constructor + * Model for a file system Directory. + * + * This class should *not* be instantiated directly. Use FileSystem.getDirectoryForPath, + * FileSystem.resolve, or Directory.getContents to create an instance of this class. + * + * Note: Directory.fullPath always has a trailing slash. + * + * See the FileSystem class for more details. + * + * @param {!string} fullPath The full path for this Directory. + * @param {!FileSystem} fileSystem The file system associated with this Directory. + */ + function Directory(fullPath, fileSystem) { + this._isDirectory = true; + FileSystemEntry.call(this, fullPath, fileSystem); + } + + Directory.prototype = Object.create(FileSystemEntry.prototype); + Directory.prototype.constructor = Directory; + Directory.prototype.parentClass = FileSystemEntry.prototype; + + /** + * The contents of this directory. This "private" property is used by FileSystem. + * @type {Array} + */ + Directory.prototype._contents = null; + + /** + * The stats for the contents of this directory, such that this._contentsStats[i] + * corresponds to this._contents[i]. + * @type {Array.} + */ + Directory.prototype._contentsStats = null; + + /** + * The stats errors for the contents of this directory. + * @type {object.} fullPaths are mapped to FileSystemError strings + */ + Directory.prototype._contentsStatsErrors = null; + + /** + * Clear any cached data for this directory + * @private + */ + Directory.prototype._clearCachedData = function () { + this.parentClass._clearCachedData.apply(this); + this._contents = undefined; + this._contentsStats = undefined; + this._contentsStatsErrors = undefined; + }; + + /** + * Read the contents of a Directory. + * + * @param {Directory} directory Directory whose contents you want to get + * @param {function (?string, Array.=, Array.=, object.=)} callback + * Callback that is passed an error code or the stat-able contents + * of the directory along with the stats for these entries and a + * fullPath-to-FileSystemError string map of unstat-able entries + * and their stat errors. If there are no stat errors then the last + * parameter shall remain undefined. + */ + Directory.prototype.getContents = function (callback) { + if (this._contentsCallbacks) { + // There is already a pending call for this directory's contents. + // Push the new callback onto the stack and return. + this._contentsCallbacks.push(callback); + return; + } + + if (this._contents) { + // Return cached contents + // Watchers aren't guaranteed to fire immediately, so it's possible this will be somewhat stale. But + // unlike file contents, we're willing to tolerate directory contents being stale. It should at least + // be up-to-date with respect to changes made internally (by this filesystem). + callback(null, this._contents, this._contentsStats, this._contentsStatsErrors); + return; + } + + this._contentsCallbacks = [callback]; + + this._impl.readdir(this.fullPath, function (err, contents, stats) { + if (err) { + this._clearCachedData(); + } else { + this._contents = []; + this._contentsStats = []; + this._contentsStatsErrors = undefined; + + contents.forEach(function (name, index) { + var entryPath = this.fullPath + name, + entry; + + if (this._fileSystem._indexFilter(entryPath, name)) { + var entryStats = stats[index]; + + // Note: not all entries necessarily have associated stats. + if (typeof entryStats === "string") { + // entryStats is an error string + if (this._contentsStatsErrors === undefined) { + this._contentsStatsErrors = {}; + } + this._contentsStatsErrors[entryPath] = entryStats; + } else { + // entryStats is a FileSystemStats object + if (entryStats.isFile) { + entry = this._fileSystem.getFileForPath(entryPath); + + // If file already existed, its cache may now be invalid (a change + // to file content may be messaged EITHER as a watcher change + // directly on that file, OR as a watcher change to its parent dir) + // TODO: move this to FileSystem._handleWatchResult()? + entry._clearCachedData(); + } else { + entry = this._fileSystem.getDirectoryForPath(entryPath); + } + + entry._stat = entryStats; + this._contents.push(entry); + this._contentsStats.push(entryStats); + } + + } + }, this); + } + + // Reset the callback list before we begin calling back so that + // synchronous reentrant calls are handled correctly. + var currentCallbacks = this._contentsCallbacks; + + this._contentsCallbacks = null; + + // Invoke all saved callbacks + currentCallbacks.forEach(function (cb) { + try { + cb(err, this._contents, this._contentsStats, this._contentsStatsErrors); + } catch (ex) { + console.warn("Unhandled exception in callback: ", ex); + } + }, this); + }.bind(this)); + }; + + /** + * Create a directory + * + * @param {function (?string, FileSystemStats=)=} callback Callback resolved with a + * FileSystemError string or the stat object for the created directory. + */ + Directory.prototype.create = function (callback) { + callback = callback || function () {}; + this._impl.mkdir(this._path, function (err, stat) { + if (!err) { + this._stat = stat; + } + + callback(err, stat); + }.bind(this)); + }; + + // Export this class + module.exports = Directory; +}); diff --git a/src/filesystem/File.js b/src/filesystem/File.js new file mode 100644 index 00000000000..5b5e1a084fe --- /dev/null +++ b/src/filesystem/File.js @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + + +/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */ +/*global define */ + +define(function (require, exports, module) { + "use strict"; + + var FileSystemEntry = require("filesystem/FileSystemEntry"); + + + /* + * @constructor + * Model for a File. + * + * This class should *not* be instantiated directly. Use FileSystem.getFileForPath, + * FileSystem.resolve, or Directory.getContents to create an instance of this class. + * + * See the FileSystem class for more details. + * + * @param {!string} fullPath The full path for this File. + * @param {!FileSystem} fileSystem The file system associated with this File. + */ + function File(fullPath, fileSystem) { + this._isFile = true; + FileSystemEntry.call(this, fullPath, fileSystem); + } + + File.prototype = Object.create(FileSystemEntry.prototype); + File.prototype.constructor = File; + File.prototype.parentClass = FileSystemEntry.prototype; + + /** + * Contents of this file. + */ + File.prototype._contents = null; + + /** + * Clear any cached data for this file + * @private + */ + File.prototype._clearCachedData = function () { + this.parentClass._clearCachedData.apply(this); + this._contents = undefined; + }; + + + /** + * Read a file. + * + * @param {object=} options Currently unused. + * @param {function (?string, string=, FileSystemStats=)} callback Callback that is passed the + * FileSystemError string or the file's contents and its stats. + */ + File.prototype.read = function (options, callback) { + if (typeof (options) === "function") { + callback = options; + options = {}; + } + + this._impl.readFile(this._path, options, function (err, data, stat) { + if (!err) { + this._stat = stat; + // this._contents = data; + } + callback(err, data, stat); + }.bind(this)); + }; + + /** + * Write a file. + * + * @param {string} data Data to write. + * @param {object=} options Currently unused. + * @param {!function (?string, FileSystemStats=)=} callback Callback that is passed the + * FileSystemError string or the file's new stats. + */ + File.prototype.write = function (data, options, callback) { + if (typeof (options) === "function") { + callback = options; + options = {}; + } + + callback = callback || function () {}; + + this._fileSystem._beginWrite(); + + this._impl.writeFile(this._path, data, options, function (err, stat) { + try { + if (!err) { + this._stat = stat; + // this._contents = data; + } + callback(err, stat); + } finally { + this._fileSystem._endWrite(); // unblock generic change events + } + }.bind(this)); + }; + + // Export this class + module.exports = File; +}); diff --git a/src/filesystem/FileIndex.js b/src/filesystem/FileIndex.js new file mode 100644 index 00000000000..e6af676c600 --- /dev/null +++ b/src/filesystem/FileIndex.js @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + + +/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */ +/*global define */ + +/** + * FileIndex is an internal module used by FileSystem to maintain an index of all files and directories. + * + * This module is *only* used by FileSystem, and should not be called directly. + */ +define(function (require, exports, module) { + "use strict"; + + /** + * @constructor + */ + function FileIndex() { + this._index = {}; + } + + /** + * Master index + * + * @type{object{string: File|Directory}} Maps a fullPath to a File or Directory object + */ + FileIndex.prototype._index = null; + + /** + * Clear the file index cache. + */ + FileIndex.prototype.clear = function () { + this._index = {}; + }; + + /** + * Visits every entry in the entire index; no stopping condition. + * @param {!function(FileSystemEntry, string):void} Called with an entry and its fullPath + */ + FileIndex.prototype.visitAll = function (visitor) { + var path; + for (path in this._index) { + if (this._index.hasOwnProperty(path)) { + visitor(this._index[path], path); + } + } + }; + + /** + * Add an entry. + * + * @param {FileSystemEntry} entry The entry to add. + */ + FileIndex.prototype.addEntry = function (entry) { + this._index[entry.fullPath] = entry; + }; + + /** + * Remove an entry. + * + * @param {FileSystemEntry} entry The entry to remove. + */ + FileIndex.prototype.removeEntry = function (entry) { + var path = entry.fullPath, + property, + member; + + function replaceMember(property) { + var member = entry[property]; + if (typeof member === "function") { + entry[property] = function () { + console.warn("FileSystemEntry used after being removed from index: ", path); + return member.apply(entry, arguments); + }; + } + } + + delete this._index[path]; + + for (property in entry) { + if (entry.hasOwnProperty(property)) { + replaceMember(property); + } + } + }; + + /** + * Notify the index that an entry has been renamed. This updates + * all affected entries in the index. + * + * @param {string} oldPath + * @param {string} newPath + * @param {boolean} isDirectory + */ + FileIndex.prototype.entryRenamed = function (oldPath, newPath, isDirectory) { + var path, + splitName = oldPath.split("/"), + finalPart = splitName.length - 1, + renameMap = {}; + + // Find all entries affected by the rename and put into a separate map. + for (path in this._index) { + if (this._index.hasOwnProperty(path)) { + // See if we have a match. For directories, see if the path + // starts with the old name. This is safe since paths always end + // with '/'. For files, see if there is an exact match between + // the path and the old name. + if (isDirectory ? path.indexOf(oldPath) === 0 : path === oldPath) { + renameMap[path] = newPath + path.substr(oldPath.length); + } + } + } + + // Do the rename. + for (path in renameMap) { + if (renameMap.hasOwnProperty(path)) { + var item = this._index[path]; + + // Sanity check to make sure the item and path still match + console.assert(item.fullPath === path); + + delete this._index[path]; + this._index[renameMap[path]] = item; + item._setPath(renameMap[path]); + } + } + }; + + /** + * Returns the cached entry for the specified path, or undefined + * if the path has not been cached. + * + * @param {string} path The path of the entry to return. + * @return {File|Directory} The entry for the path, or undefined if it hasn't + * been cached yet. + */ + FileIndex.prototype.getEntry = function (path) { + return this._index[path]; + }; + + // Export public API + module.exports = FileIndex; +}); diff --git a/src/filesystem/FileSystem.js b/src/filesystem/FileSystem.js new file mode 100644 index 00000000000..1df56986221 --- /dev/null +++ b/src/filesystem/FileSystem.js @@ -0,0 +1,803 @@ +/* + * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + +/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */ +/*global define, $ */ + +/** + * FileSystem is a model object representing a complete file system. This object creates + * and manages File and Directory instances, dispatches events when the file system changes, + * and provides methods for showing 'open' and 'save' dialogs. + * + * The FileSystem must be initialized very early during application startup. + * + * There are three ways to get File or Directory instances: + * * Use FileSystem.resolve() to convert a path to a File/Directory object. This will only + * succeed if the file/directory already exists. + * * Use FileSystem.getFileForPath()/FileSystem.getDirectoryForPath() if you know the + * file/directory already exists, or if you want to create a new entry. + * * Use Directory.getContents() to return all entries for the specified Directory. + * + * FileSystem dispatches the following events: + * change - Sent whenever there is a change in the file system. The handler + * is passed one argument -- entry. This argument can be... + * * a File - the contents of the file have changed, and should be reloaded. + * * a Directory - an immediate child of the directory has been added, removed, + * or renamed/moved. Not triggered for "grandchildren". + * * null - a 'wholesale' change happened, and you should assume everything may + * have changed. + * For changes made externally, there may be a significant delay before a "change" event + * is dispatched. + * rename - Sent whenever a File or Directory is renamed. All affected File and Directory + * objects have been updated to reflect the new path by the time this event is dispatched. + * This event should be used to trigger any UI updates that may need to occur when a path + * has changed. + * + * FileSystem may perform caching. But it guarantees: + * * File contents & metadata - reads are guaranteed to be up to date (cached data is not used + * without first veryifying it is up to date). + * * Directory structure / file listing - reads may return cached data immediately, which may not + * reflect external changes made recently. (However, changes made via FileSystem itself are always + * reflected immediately, as soon as the change operation's callback signals success). + * + * The FileSystem doesn't directly read or write contents--this work is done by a low-level + * implementation object. This allows client code to use the FileSystem API without having to + * worry about the underlying storage, which could be a local filesystem or a remote server. + */ +define(function (require, exports, module) { + "use strict"; + + var Directory = require("filesystem/Directory"), + File = require("filesystem/File"), + FileIndex = require("filesystem/FileIndex"); + + /** + * @constructor + * The FileSystem is not usable until init() signals its callback. + */ + function FileSystem() { + // Create a file index + this._index = new FileIndex(); + + // Initialize the set of watched roots + this._watchedRoots = {}; + + // Initialize the watch/unwatch request queue + this._watchRequests = []; + + this._watchResults = []; + } + + /** + * The low-level file system implementation used by this object. + * This is set in the init() function and cannot be changed. + */ + FileSystem.prototype._impl = null; + + /** + * The FileIndex used by this object. This is initialized in the constructor. + */ + FileSystem.prototype._index = null; + + /** + * Refcount of any pending write operations. Used to guarantee file-watcher callbacks don't + * run until after operation-specific callbacks & index fixups complete (this is important for + * distinguishing rename from an unrelated delete-add pair). + * @type {number} + */ + FileSystem.prototype._writeCount = 0; + + /** + * The queue of pending watch/unwatch requests. + * @type {Array.<{fn: function(), cb: function()}>} + */ + FileSystem.prototype._watchRequests = null; + + /** + * Queue of arguments to invoke _handleWatchResult() with; triggered once _writeCount drops to zero + * @type {!Array.<{path:string, stat:Object}>} + */ + FileSystem.prototype._watchResults = null; + + /** Process all queued watcher results, by calling _handleWatchResult() on each */ + FileSystem.prototype._triggerWatchCallbacksNow = function () { + this._watchResults.forEach(function (info) { + this._handleWatchResult(info.path, info.stat); + }, this); + this._watchResults.length = 0; + }; + + /** + * Receives a result from the impl's watcher callback, and either processes it immediately (if + * _writeCount is 0) or stores it for later processing (if _writeCount > 0). + */ + FileSystem.prototype._enqueueWatchResult = function (path, stat) { + this._watchResults.push({path: path, stat: stat}); + if (!this._writeCount) { + this._triggerWatchCallbacksNow(); + } + }; + + + /** + * Dequeue and process all pending watch/unwatch requests + */ + FileSystem.prototype._dequeueWatchRequest = function () { + if (this._watchRequests.length > 0) { + var request = this._watchRequests[0]; + + request.fn.call(null, function () { + // Apply the given callback + var callbackArgs = arguments; + try { + request.cb.apply(null, callbackArgs); + } finally { + // Process the remaining watch/unwatch requests + this._watchRequests.shift(); + this._dequeueWatchRequest(); + } + }.bind(this)); + } + }; + + /** + * Enqueue a new watch/unwatch request. + * + * @param {function()} fn - The watch/unwatch request function. + * @param {callback()} cb - The callback for the provided watch/unwatch + * request function. + */ + FileSystem.prototype._enqueueWatchRequest = function (fn, cb) { + // Enqueue the given watch/unwatch request + this._watchRequests.push({fn: fn, cb: cb}); + + // Begin processing the queue if it is not already being processed + if (this._watchRequests.length === 1) { + this._dequeueWatchRequest(); + } + }; + + /** + * The set of watched roots, encoded as a mapping from full paths to objects + * which contain a file entry, filter function, and change handler function. + * + * @type{Object.} + */ + FileSystem.prototype._watchedRoots = null; + + /** + * Finds a parent watched root for a given path, or returns null if a parent + * watched root does not exist. + * + * @param{string} fullPath The child path for which a parent watched root is to be found + * @return{?{entry: FileSystemEntry, filter: function(string) boolean}} The parent + * watched root, if it exists, or null. + */ + FileSystem.prototype._findWatchedRootForPath = function (fullPath) { + var watchedRoot = null; + + Object.keys(this._watchedRoots).some(function (watchedPath) { + if (fullPath.indexOf(watchedPath) === 0) { + watchedRoot = this._watchedRoots[watchedPath]; + return true; + } + }, this); + + return watchedRoot; + }; + + /** + * Helper function to watch or unwatch a filesystem entry beneath a given + * watchedRoot. + * + * @private + * @param {FileSystemEntry} entry - The FileSystemEntry to watch. Must be a + * non-strict descendent of watchedRoot.entry. + * @param {Object} watchedRoot - See FileSystem._watchedRoots. + * @param {function(?string)} callback - A function that is called once the + * watch is complete, possibly with a FileSystemError string. + * @param {boolean} shouldWatch - Whether the entry should be watched (true) + * or unwatched (false). + */ + FileSystem.prototype._watchOrUnwatchEntry = function (entry, watchedRoot, callback, shouldWatch) { + var watchPaths = [], + allChildren; + + if (!shouldWatch) { + allChildren = []; + } + + var visitor = function (child) { + if (watchedRoot.filter(child.name)) { + if (child.isDirectory || child === watchedRoot.entry) { + watchPaths.push(child.fullPath); + } + + if (!shouldWatch) { + allChildren.push(child); + } + + return true; + } + return false; + }.bind(this); + + entry.visit(visitor, function (err) { + if (err) { + callback(err); + return; + } + + // sort paths by max depth for a breadth-first traversal + var dirCount = {}; + watchPaths.forEach(function (path) { + dirCount[path] = path.split("/").length; + }); + + watchPaths.sort(function (path1, path2) { + var dirCount1 = dirCount[path1], + dirCount2 = dirCount[path2]; + + return dirCount1 - dirCount2; + }); + + this._enqueueWatchRequest(function (callback) { + if (shouldWatch) { + watchPaths.forEach(function (path, index) { + this._impl.watchPath(path); + }, this); + } else { + watchPaths.forEach(function (path, index) { + this._impl.unwatchPath(path); + }, this); + allChildren.forEach(function (child) { + this._index.removeEntry(child); + }, this); + } + + callback(null); + }.bind(this), callback); + }.bind(this)); + }; + + /** + * Watch a filesystem entry beneath a given watchedRoot. + * + * @private + * @param {FileSystemEntry} entry - The FileSystemEntry to watch. Must be a + * non-strict descendent of watchedRoot.entry. + * @param {Object} watchedRoot - See FileSystem._watchedRoots. + * @param {function(?string)} callback - A function that is called once the + * watch is complete, possibly with a FileSystemError string. + */ + FileSystem.prototype._watchEntry = function (entry, watchedRoot, callback) { + this._watchOrUnwatchEntry(entry, watchedRoot, callback, true); + }; + + /** + * Unwatch a filesystem entry beneath a given watchedRoot. + * + * @private + * @param {FileSystemEntry} entry - The FileSystemEntry to watch. Must be a + * non-strict descendent of watchedRoot.entry. + * @param {Object} watchedRoot - See FileSystem._watchedRoots. + * @param {function(?string)} callback - A function that is called once the + * watch is complete, possibly with a FileSystemError string. + */ + FileSystem.prototype._unwatchEntry = function (entry, watchedRoot, callback) { + this._watchOrUnwatchEntry(entry, watchedRoot, callback, false); + }; + + /** + * @param {function(?string)=} callback Callback resolved, possibly with a + * FileSystemError string. + */ + FileSystem.prototype.init = function (impl, callback) { + console.assert(!this._impl, "This FileSystem has already been initialized!"); + + callback = callback || function () {}; + + this._impl = impl; + this._impl.init(function (err) { + if (err) { + callback(err); + return; + } + + // Initialize watchers + this._impl.initWatchers(this._enqueueWatchResult.bind(this)); + callback(null); + }.bind(this)); + }; + + /** + * Close a file system. Clear all caches, indexes, and file watchers. + */ + FileSystem.prototype.close = function () { + this._impl.unwatchAll(); + this._index.clear(); + }; + + /** + * Returns true if the given path should be automatically added to the index & watch list when one of its ancestors + * is a watch-root. (Files are added automatically when the watch-root is first established, or later when a new + * directory is created and its children enumerated). + * + * Entries explicitly created via FileSystem.getFile/DirectoryForPath() are *always* added to the index regardless + * of this filtering - but they will not be watched if the watch-root's filter excludes them. + */ + FileSystem.prototype._indexFilter = function (path, name) { + var parentRoot = this._findWatchedRootForPath(path); + + if (parentRoot) { + return parentRoot.filter(name); + } + + // It might seem more sensible to return false (exclude) for files outside the watch roots, but + // that would break usage of appFileSystem for 'system'-level things like enumerating extensions. + // (Or in general, Directory.getContents() for any Directory outside the watch roots). + return true; + }; + + FileSystem.prototype._beginWrite = function () { + this._writeCount++; + //console.log("> beginWrite -> " + this._writeCount); + }; + + FileSystem.prototype._endWrite = function () { + this._writeCount--; + //console.log("< endWrite -> " + this._writeCount); + + if (this._writeCount < 0) { + console.error("FileSystem _writeCount has fallen below zero!"); + } + + if (!this._writeCount) { + this._triggerWatchCallbacksNow(); + } + }; + + /** + * Determines whether or not the supplied path is absolute, as opposed to relative. + * + * @param {!string} fullPath + * @return {boolean} True if the fullPath is absolute and false otherwise. + */ + FileSystem.isAbsolutePath = function (fullPath) { + return (fullPath[0] === "/" || fullPath[1] === ":"); + }; + + /** + * Returns a canonical version of the path: no duplicated "/"es, no ".."s, + * and directories guaranteed to end in a trailing "/" + * @param {!string} path Absolute path, using "/" as path separator + * @param {boolean=} isDirectory + * @return {!string} + */ + function _normalizePath(path, isDirectory) { + + if (!FileSystem.isAbsolutePath(path)) { + throw new Error("Paths must be absolute: '" + path + "'"); // expect only absolute paths + } + + // Remove duplicated "/"es + path = path.replace(/\/{2,}/g, "/"); + + // Remove ".." segments + if (path.indexOf("..") !== -1) { + var segments = path.split("/"), + i; + for (i = 1; i < segments.length; i++) { + if (segments[i] === "..") { + if (i < 2) { + throw new Error("Invalid absolute path: '" + path + "'"); + } + segments.splice(i - 1, 2); + i -= 2; // compensate so we start on the right index next iteration + } + } + path = segments.join("/"); + } + + if (isDirectory) { + // Make sure path DOES include trailing slash + if (path[path.length - 1] !== "/") { + path += "/"; + } + } + + return path; + } + + /** + * Return a File object for the specified path. + * + * @param {string} path Absolute path of file. + * + * @return {File} The File object. This file may not yet exist on disk. + */ + FileSystem.prototype.getFileForPath = function (path) { + path = _normalizePath(path, false); + var file = this._index.getEntry(path); + + if (!file) { + file = new File(path, this); + this._index.addEntry(file); + } + + return file; + }; + + /** + * Return a Directory object for the specified path. + * + * @param {string} path Absolute path of directory. + * + * @return {Directory} The Directory object. This directory may not yet exist on disk. + */ + FileSystem.prototype.getDirectoryForPath = function (path) { + path = _normalizePath(path, true); + var directory = this._index.getEntry(path); + + if (!directory) { + directory = new Directory(path, this); + this._index.addEntry(directory); + } + + return directory; + }; + + /** + * Resolve a path. + * + * @param {string} path The path to resolve + * @param {function (?string, FileSystemEntry=, FileSystemStats=)} callback Callback resolved + * with a FileSystemError string or with the entry for the provided path. + */ + FileSystem.prototype.resolve = function (path, callback) { + // No need to normalize path here: assume underlying stat() does it internally, + // and it will be normalized anyway when ingested by get*ForPath() afterward + + this._impl.stat(path, function (err, stat) { + var item; + + if (!err) { + if (stat.isFile) { + item = this.getFileForPath(path); + } else { + item = this.getDirectoryForPath(path); + } + } + callback(err, item, stat); + }.bind(this)); + }; + + /** + * @private + * Notify the system when an entry name has changed. + * + * @param {string} oldName + * @param {string} newName + * @param {boolean} isDirectory + */ + FileSystem.prototype._entryRenamed = function (oldName, newName, isDirectory) { + // Update all affected entries in the index + this._index.entryRenamed(oldName, newName, isDirectory); + $(this).trigger("rename", [oldName, newName]); + }; + + /** + * Show an "Open" dialog and return the file(s)/directories selected by the user. + * + * @param {boolean} allowMultipleSelection Allows selecting more than one file at a time + * @param {boolean} chooseDirectories Allows directories to be opened + * @param {string} title The title of the dialog + * @param {string} initialPath The folder opened inside the window initially. If initialPath + * is not set, or it doesn't exist, the window would show the last + * browsed folder depending on the OS preferences + * @param {Array.} fileTypes List of extensions that are allowed to be opened. A null value + * allows any extension to be selected. + * @param {function (?string, Array.=)} callback Callback resolved with a FileSystemError + * string or the selected file(s)/directories. If the user cancels the + * open dialog, the error will be falsy and the file/directory array will + * be empty. + */ + FileSystem.prototype.showOpenDialog = function (allowMultipleSelection, + chooseDirectories, + title, + initialPath, + fileTypes, + callback) { + + this._impl.showOpenDialog(allowMultipleSelection, chooseDirectories, title, initialPath, fileTypes, callback); + }; + + /** + * Show a "Save" dialog and return the path of the file to save. + * + * @param {string} title The title of the dialog. + * @param {string} initialPath The folder opened inside the window initially. If initialPath + * is not set, or it doesn't exist, the window would show the last + * browsed folder depending on the OS preferences. + * @param {string} proposedNewFilename Provide a new file name for the user. This could be based on + * on the current file name plus an additional suffix + * @param {function (?string, string=)} callback Callback that is resolved with a FileSystemError + * string or the name of the file to save. If the user cancels the save, + * the error will be falsy and the name will be empty. + */ + FileSystem.prototype.showSaveDialog = function (title, initialPath, proposedNewFilename, callback) { + this._impl.showSaveDialog(title, initialPath, proposedNewFilename, callback); + }; + + /** + * @private + * Processes a result from the file/directory watchers. Watch results are sent from the low-level implementation + * whenever a directory or file is changed. + * + * @param {string} path The path that changed. This could be a file or a directory. + * @param {FileSystemStats=} stat Optional stat for the item that changed. This param is not always + * passed. + */ + FileSystem.prototype._handleWatchResult = function (path, stat) { + + var fireChangeEvent = function (entry) { + // Trigger a change event + $(this).trigger("change", entry); + }.bind(this); + + if (!path) { + // This is a "wholesale" change event + // Clear all caches (at least those that won't do a stat() double-check before getting used) + this._index.visitAll(function (entry) { + if (entry.isDirectory) { + entry._clearCachedData(); + } + }); + + fireChangeEvent(null); + return; + } + + path = _normalizePath(path, false); + var entry = this._index.getEntry(path); + + if (entry) { + if (entry.isFile) { + // Update stat and clear contents, but only if out of date + if (!stat || !entry._stat || (stat.mtime.getTime() !== entry._stat.mtime.getTime())) { + entry._clearCachedData(); + entry._stat = stat; + } + + fireChangeEvent(entry); + } else { + var oldContents = entry._contents || []; + + // Clear out old contents + entry._clearCachedData(); + entry._stat = stat; + + var watchedRoot = this._findWatchedRootForPath(entry.fullPath); + if (!watchedRoot) { + console.warn("Received change notification for unwatched path: ", path); + return; + } + + // Update changed entries + entry.getContents(function (err, contents) { + + var addNewEntries = function (callback) { + // Check for added directories and scan to add to index + // Re-scan this directory to add any new contents + var entriesToAdd = contents.filter(function (entry) { + return oldContents.indexOf(entry) === -1; + }); + + var addCounter = entriesToAdd.length; + + if (addCounter === 0) { + callback(); + } else { + entriesToAdd.forEach(function (entry) { + this._watchEntry(entry, watchedRoot, function (err) { + if (--addCounter === 0) { + callback(); + } + }); + }, this); + } + }.bind(this); + + var removeOldEntries = function (callback) { + var entriesToRemove = oldContents.filter(function (entry) { + return contents.indexOf(entry) === -1; + }); + + var removeCounter = entriesToRemove.length; + + if (removeCounter === 0) { + callback(); + } else { + entriesToRemove.forEach(function (entry) { + this._unwatchEntry(entry, watchedRoot, function (err) { + if (--removeCounter === 0) { + callback(); + } + }); + }, this); + } + }.bind(this); + + if (err) { + console.warn("Unable to get contents of changed directory: ", path, err); + } else { + removeOldEntries(function () { + addNewEntries(function () { + fireChangeEvent(entry); + }); + }); + } + + }.bind(this)); + } + } + }; + + /** + * Start watching a filesystem root entry. + * + * @param {FileSystemEntry} entry - The root entry to watch. If entry is a directory, + * all subdirectories that aren't explicitly filtered will also be watched. + * @param {function(string): boolean} filter - A function to determine whether + * a particular name should be watched or ignored. Paths that are ignored are also + * filtered from Directory.getContents() results within this subtree. + * @param {function(?string)=} callback - A function that is called when the watch has + * completed. If the watch fails, the function will have a non-null FileSystemError + * string parametr. + */ + FileSystem.prototype.watch = function (entry, filter, callback) { + var fullPath = entry.fullPath, + watchedRoot = { + entry: entry, + filter: filter + }; + + callback = callback || function () {}; + + var watchingParentRoot = this._findWatchedRootForPath(fullPath); + if (watchingParentRoot) { + callback("A parent of this root is already watched"); + return; + } + + var watchingChildRoot = Object.keys(this._watchedRoots).some(function (path) { + var watchedRoot = this._watchedRoots[path], + watchedPath = watchedRoot.entry.fullPath; + + return watchedPath.indexOf(fullPath) === 0; + }, this); + + if (watchingChildRoot) { + callback("A child of this root is already watched"); + return; + } + + this._watchedRoots[fullPath] = watchedRoot; + + this._watchEntry(entry, watchedRoot, function (err) { + if (err) { + console.warn("Failed to watch root: ", entry.fullPath, err); + callback(err); + return; + } + + callback(null); + }.bind(this)); + }; + + /** + * Stop watching a filesystem root entry. + * + * @param {FileSystemEntry} entry - The root entry to stop watching. The unwatch will + * if the entry is not currently being watched. + * @param {function(?string)=} callback - A function that is called when the unwatch has + * completed. If the unwatch fails, the function will have a non-null FileSystemError + * string parameter. + */ + FileSystem.prototype.unwatch = function (entry, callback) { + var fullPath = entry.fullPath, + watchedRoot = this._watchedRoots[fullPath]; + + callback = callback || function () {}; + + if (!watchedRoot) { + callback("Root is not watched."); + return; + } + + delete this._watchedRoots[fullPath]; + + this._unwatchEntry(entry, watchedRoot, function (err) { + if (err) { + console.warn("Failed to unwatch root: ", entry.fullPath, err); + callback(err); + return; + } + + callback(null); + }.bind(this)); + }; + + // The singleton instance + var _instance; + + function _wrap(func) { + return function () { + return func.apply(_instance, arguments); + }; + } + + // Export public methods as proxies to the singleton instance + exports.init = _wrap(FileSystem.prototype.init); + exports.close = _wrap(FileSystem.prototype.close); + exports.shouldShow = _wrap(FileSystem.prototype.shouldShow); + exports.getFileForPath = _wrap(FileSystem.prototype.getFileForPath); + exports.getDirectoryForPath = _wrap(FileSystem.prototype.getDirectoryForPath); + exports.resolve = _wrap(FileSystem.prototype.resolve); + exports.showOpenDialog = _wrap(FileSystem.prototype.showOpenDialog); + exports.showSaveDialog = _wrap(FileSystem.prototype.showSaveDialog); + exports.watch = _wrap(FileSystem.prototype.watch); + exports.unwatch = _wrap(FileSystem.prototype.unwatch); + + // Static public utility methods + exports.isAbsolutePath = FileSystem.isAbsolutePath; + + // Export "on" and "off" methods + + /** + * Add an event listener for a FileSystem event. + * + * @param {string} event The name of the event + * @param {function} handler The handler for the event + */ + exports.on = function (event, handler) { + $(_instance).on(event, handler); + }; + + /** + * Remove an event listener for a FileSystem event. + * + * @param {string} event The name of the event + * @param {function} handler The handler for the event + */ + exports.off = function (event, handler) { + $(_instance).off(event, handler); + }; + + // Export the FileSystem class as "private" for unit testing only. + exports._FileSystem = FileSystem; + + // Create the singleton instance + _instance = new FileSystem(); +}); diff --git a/src/filesystem/FileSystemEntry.js b/src/filesystem/FileSystemEntry.js new file mode 100644 index 00000000000..0ebdd805f45 --- /dev/null +++ b/src/filesystem/FileSystemEntry.js @@ -0,0 +1,358 @@ +/* + * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + + +/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */ +/*global define */ + +define(function (require, exports, module) { + "use strict"; + + var FileSystemError = require("filesystem/FileSystemError"); + + var VISIT_DEFAULT_MAX_DEPTH = 100, + VISIT_DEFAULT_MAX_ENTRIES = 30000; + + /* Counter to give every entry a unique id */ + var nextId = 0; + + /** + * @constructor + * Model for a file system entry. This is the base class for File and Directory, + * and is never used directly. + * + * See the File, Directory, and FileSystem classes for more details. + * + * @param {string} path The path for this entry. + * @param {FileSystem} fileSystem The file system associated with this entry. + */ + function FileSystemEntry(path, fileSystem) { + this._setPath(path); + this._fileSystem = fileSystem; + this._id = nextId++; + } + + // Add "fullPath", "name", "parent", "id", "isFile" and "isDirectory" getters + Object.defineProperties(FileSystemEntry.prototype, { + "fullPath": { + get: function () { return this._path; }, + set: function () { throw new Error("Cannot set fullPath"); } + }, + "name": { + get: function () { return this._name; }, + set: function () { throw new Error("Cannot set name"); } + }, + "parentPath": { + get: function () { return this._parentPath; }, + set: function () { throw new Error("Cannot set parentPath"); } + }, + "id": { + get: function () { return this._id; }, + set: function () { throw new Error("Cannot set id"); } + }, + "isFile": { + get: function () { return this._isFile; }, + set: function () { throw new Error("Cannot set isFile"); } + }, + "isDirectory": { + get: function () { return this._isDirectory; }, + set: function () { throw new Error("Cannot set isDirectory"); } + }, + "_impl": { + get: function () { return this._fileSystem._impl; }, + set: function () { throw new Error("Cannot set _impl"); } + } + }); + + /** + * Cached stat object for this file. + * @type {?FileSystemStats} + */ + FileSystemEntry.prototype._stat = null; + + /** + * Parent file system. + * @type {!FileSystem} + */ + FileSystemEntry.prototype._fileSystem = null; + + /** + * The path of this entry. + * @type {string} + */ + FileSystemEntry.prototype._path = null; + + /** + * The name of this entry. + * @type {string} + */ + FileSystemEntry.prototype._name = null; + + /** + * The parent of this entry. + * @type {string} + */ + FileSystemEntry.prototype._parentPath = null; + + /** + * Whether or not the entry is a file + * @type {boolean} + */ + FileSystemEntry.prototype._isFile = false; + + /** + * Whether or not the entry is a directory + * @type {boolean} + */ + FileSystemEntry.prototype._isDirectory = false; + + /** + * Update the path for this entry + * @private + * @param {String} newPath + */ + FileSystemEntry.prototype._setPath = function (newPath) { + var parts = newPath.split("/"); + if (this.isDirectory) { + parts.pop(); // Remove the empty string after last trailing "/" + } + this._name = parts[parts.length - 1]; + parts.pop(); // Remove name + this._parentPath = parts.join("/") + "/"; + + this._path = newPath; + }; + + /** + * Clear any cached data for this entry + * @private + */ + FileSystemEntry.prototype._clearCachedData = function () { + this._stat = undefined; + }; + + /** + * Helpful toString for debugging purposes + */ + FileSystemEntry.prototype.toString = function () { + return "[" + (this.isDirectory ? "Directory " : "File ") + this._path + "]"; + }; + + /** + * Check to see if the entry exists on disk. + * + * @param {function (boolean)} callback Callback with a single parameter. + */ + FileSystemEntry.prototype.exists = function (callback) { + this._impl.exists(this._path, callback); + }; + + /** + * Returns the stats for the entry. + * + * @param {function (?string, FileSystemStats=)} callback Callback with a + * FileSystemError string or FileSystemStats object. + */ + FileSystemEntry.prototype.stat = function (callback) { + this._impl.stat(this._path, function (err, stat) { + if (!err) { + this._stat = stat; + } + callback(err, stat); + }.bind(this)); + }; + + /** + * Rename this entry. + * + * @param {string} newFullPath New path & name for this entry. + * @param {function (?string)=} callback Callback with a single FileSystemError + * string parameter. + */ + FileSystemEntry.prototype.rename = function (newFullPath, callback) { + callback = callback || function () {}; + this._fileSystem._beginWrite(); + this._impl.rename(this._path, newFullPath, function (err) { + try { + if (!err) { + // Notify the file system of the name change + this._fileSystem._entryRenamed(this._path, newFullPath, this.isDirectory); + } + callback(err); // notify caller + } finally { + this._fileSystem._endWrite(); // unblock generic change events + } + }.bind(this)); + }; + + /** + * Unlink (delete) this entry. For Directories, this will delete the directory + * and all of its contents. + * + * @param {function (?string)=} callback Callback with a single FileSystemError + * string parameter. + */ + FileSystemEntry.prototype.unlink = function (callback) { + callback = callback || function () {}; + + this._clearCachedData(); + + this._impl.unlink(this._path, function (err) { + if (!err) { + this._fileSystem._index.removeEntry(this); + } + + callback.apply(undefined, arguments); + }.bind(this)); + }; + + /** + * Move this entry to the trash. If the underlying file system doesn't support move + * to trash, the item is permanently deleted. + * + * @param {function (?string)=} callback Callback with a single FileSystemError + * string parameter. + */ + FileSystemEntry.prototype.moveToTrash = function (callback) { + callback = callback || function () {}; + if (!this._impl.moveToTrash) { + this.unlink(callback); + return; + } + + this._clearCachedData(); + + this._impl.moveToTrash(this._path, function (err) { + if (!err) { + this._fileSystem._index.removeEntry(this); + } + + callback.apply(undefined, arguments); + }.bind(this)); + }; + + /** + * Private helper function for FileSystemEntry.visit that requires sanitized options. + * + * @private + * @param {function(FileSystemEntry): boolean} visitor - A visitor function, which is + * applied to descendent FileSystemEntry objects. If the function returns false for + * a particular Directory entry, that directory's descendents will not be visited. + * @param {{failFast: boolean, maxDepth: number, maxEntriesCounter: {value: number}}} options + * @param {function(?string)=} callback Callback with single FileSystemError string parameter. + */ + FileSystemEntry.prototype._visitHelper = function (visitor, options, callback) { + var failFast = options.failFast, + maxDepth = options.maxDepth, + maxEntriesCounter = options.maxEntriesCounter; + + if (maxEntriesCounter.value-- <= 0 || maxDepth-- < 0) { + callback(failFast ? FileSystemError.TOO_MANY_ENTRIES : null); + return; + } + + if (!visitor(this) || this.isFile) { + callback(null); + return; + } + + this.getContents(function (err, entries) { + var counter = entries ? entries.length : 0, + nextOptions = { + failFast: failFast, + maxDepth: maxDepth, + maxEntriesCounter: maxEntriesCounter + }; + + if (err || counter === 0) { + callback(failFast ? err : null); + return; + } + + entries.forEach(function (entry) { + entry._visitHelper(visitor, nextOptions, function (err) { + if (err && failFast) { + counter = 0; + callback(err); + return; + } + + if (--counter === 0) { + callback(null); + } + }); + }); + }.bind(this)); + }; + + /** + * Visit this entry and its descendents with the supplied visitor function. + * + * @param {function(FileSystemEntry): boolean} visitor - A visitor function, which is + * applied to descendent FileSystemEntry objects. If the function returns false for + * a particular Directory entry, that directory's descendents will not be visited. + * @param {{failFast: boolean=, maxDepth: number=, maxEntries: number=}=} options + * @param {function(?string)=} callback Callback with single FileSystemError string parameter. + */ + FileSystemEntry.prototype.visit = function (visitor, options, callback) { + if (typeof options === "function") { + callback = options; + options = {}; + } else if (options === undefined) { + options = {}; + } + + if (options.failFast === undefined) { + options.failFast = false; + } + + if (options.maxDepth === undefined) { + options.maxDepth = VISIT_DEFAULT_MAX_DEPTH; + } + + if (options.maxEntries === undefined) { + options.maxEntries = VISIT_DEFAULT_MAX_ENTRIES; + } + + options.maxEntriesCounter = { value: options.maxEntries }; + + this._visitHelper(visitor, options, function (err) { + if (callback) { + if (err) { + callback(err); + return; + } + + if (options.maxEntriesCounter.value < 0) { + callback(FileSystemError.TOO_MANY_ENTRIES); + return; + } + + callback(null); + } + }); + }; + + // Export this class + module.exports = FileSystemEntry; +}); diff --git a/src/filesystem/FileSystemError.js b/src/filesystem/FileSystemError.js new file mode 100644 index 00000000000..d54130b21b3 --- /dev/null +++ b/src/filesystem/FileSystemError.js @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + + +/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */ +/*global define */ + +/** + * FileSystemError describes the errors that can occur when using the FileSystem, File, + * and Directory modules. + * + * Error values are strings. Any "falsy" value: null, undefined or "" means "no error". + */ +define(function (require, exports, module) { + "use strict"; + + module.exports = { + UNKNOWN : "Unknown", + INVALID_PARAMS : "InvalidParams", + NOT_FOUND : "NotFound", + NOT_READABLE : "NotReadable", + NOT_WRITABLE : "NotWritable", + OUT_OF_SPACE : "OutOfSpace", + TOO_MANY_ENTRIES : "TooManyEntries", + ALREADY_EXISTS : "AlreadyExists" + // FUTURE: Add remote connection errors: timeout, not logged in, connection err, etc. + }; +}); diff --git a/src/filesystem/FileSystemStats.js b/src/filesystem/FileSystemStats.js new file mode 100644 index 00000000000..7c7da6d9063 --- /dev/null +++ b/src/filesystem/FileSystemStats.js @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + + +/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */ +/*global define */ + +/** + * The FileSystemStats represents a particular FileSystemEntry's stats. + */ +define(function (require, exports, module) { + "use strict"; + + /** + * @constructor + * @param {{isFile: boolean, mtime: Date, size: Number}} options + */ + function FileSystemStats(options) { + this._isFile = options.isFile; + this._isDirectory = !options.isFile; + this._mtime = options.mtime; + this._size = options.size; + } + + // Add "isFile", "isDirectory", "mtime" and "size" getters + Object.defineProperties(FileSystemStats.prototype, { + "isFile": { + get: function () { return this._isFile; }, + set: function () { throw new Error("Cannot set isFile"); } + }, + "isDirectory": { + get: function () { return this._isDirectory; }, + set: function () { throw new Error("Cannot set isDirectory"); } + }, + "mtime": { + get: function () { return this._mtime; }, + set: function () { throw new Error("Cannot set mtime"); } + }, + "size": { + get: function () { return this._size; }, + set: function () { throw new Error("Cannot set size"); } + } + }); + + /** + * Whether or not this is a stats object for a file + * @type {boolean} + */ + FileSystemStats.prototype._isFile = false; + + /** + * Whether or not this is a stats object for a directory + * @type {boolean} + */ + FileSystemStats.prototype._isDirectory = false; + + /** + * Modification time for a file + * @type {Date} + */ + FileSystemStats.prototype._mtime = null; + + /** + * Size in bytes of a file + * @type {Number} + */ + FileSystemStats.prototype._size = null; + + module.exports = FileSystemStats; +}); diff --git a/src/filesystem/impls/appshell/AppshellFileSystem.js b/src/filesystem/impls/appshell/AppshellFileSystem.js new file mode 100644 index 00000000000..80b3768a954 --- /dev/null +++ b/src/filesystem/impls/appshell/AppshellFileSystem.js @@ -0,0 +1,412 @@ +/* + * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + + +/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */ +/*global define, appshell, $, window */ + +define(function (require, exports, module) { + "use strict"; + + var FileUtils = require("file/FileUtils"), + FileSystemStats = require("filesystem/FileSystemStats"), + FileSystemError = require("filesystem/FileSystemError"), + NodeConnection = require("utils/NodeConnection"); + + /** + * @const + * Amount of time to wait before automatically rejecting the connection + * deferred. If we hit this timeout, we'll never have a node connection + * for the file watcher in this run of Brackets. + */ + var NODE_CONNECTION_TIMEOUT = 30000, // 30 seconds - TODO: share with StaticServer & Package? + FILE_WATCHER_BATCH_TIMEOUT = 200; // 200ms - granularity of file watcher changes + + /** + * @private + * @type{jQuery.Deferred.} + * A deferred which is resolved with a NodeConnection or rejected if + * we are unable to connect to Node. + */ + var _nodeConnectionDeferred; + + var _changeCallback, // Callback to notify FileSystem of watcher changes + _changeTimeout, // Timeout used to batch up file watcher changes + _pendingChanges = {}; // Pending file watcher changes + + function _mapError(err) { + if (!err) { + return null; + } + + switch (err) { + case appshell.fs.ERR_INVALID_PARAMS: + return FileSystemError.INVALID_PARAMS; + case appshell.fs.ERR_NOT_FOUND: + return FileSystemError.NOT_FOUND; + case appshell.fs.ERR_CANT_READ: + return FileSystemError.NOT_READABLE; + case appshell.fs.ERR_CANT_WRITE: + return FileSystemError.NOT_WRITABLE; + case appshell.fs.ERR_UNSUPPORTED_ENCODING: + return FileSystemError.NOT_READABLE; + case appshell.fs.ERR_OUT_OF_SPACE: + return FileSystemError.OUT_OF_SPACE; + case appshell.fs.ERR_FILE_EXISTS: + return FileSystemError.ALREADY_EXISTS; + } + return FileSystemError.UNKNOWN; + } + + /** Returns the path of the item's containing directory (item may be a file or a directory) */ + function _parentPath(path) { + var lastSlash = path.lastIndexOf("/"); + if (lastSlash === path.length - 1) { + lastSlash = path.lastIndexOf("/", lastSlash - 1); + } + return path.substr(0, lastSlash + 1); + } + + + function init(callback) { + /* Temporarily disable file watchers + if (!_nodeConnectionDeferred) { + _nodeConnectionDeferred = new $.Deferred(); + + // TODO: This code is a copy of the AppInit function in extensibility/Package.js. This should be refactored + // into common code. + + + // Start up the node connection, which is held in the + // _nodeConnectionDeferred module variable. (Use + // _nodeConnectionDeferred.done() to access it. + var connectionTimeout = window.setTimeout(function () { + console.error("[AppshellFileSystem] Timed out while trying to connect to node"); + _nodeConnectionDeferred.reject(); + }, NODE_CONNECTION_TIMEOUT); + + var _nodeConnection = new NodeConnection(); + _nodeConnection.connect(true).then(function () { + var domainPath = FileUtils.getNativeBracketsDirectoryPath() + "/" + FileUtils.getNativeModuleDirectoryPath(module) + "/node/FileWatcherDomain"; + + _nodeConnection.loadDomains(domainPath, true) + .then( + function () { + window.clearTimeout(connectionTimeout); + _nodeConnectionDeferred.resolve(_nodeConnection); + }, + function () { // Failed to connect + console.error("[AppshellFileSystem] Failed to connect to node", arguments); + window.clearTimeout(connectionTimeout); + _nodeConnectionDeferred.reject(); + } + ); + }); + } + */ + + // Don't want to block on _nodeConnectionDeferred because we're needed as the 'root' fs + // at startup -- and the Node-side stuff isn't needed for most functionality anyway. + if (callback) { + callback(); + } + } + + function _wrap(cb) { + return function (err) { + var args = Array.prototype.slice.call(arguments); + args[0] = _mapError(args[0]); + cb.apply(null, args); + }; + } + + function showOpenDialog(allowMultipleSelection, chooseDirectories, title, initialPath, fileTypes, callback) { + appshell.fs.showOpenDialog(allowMultipleSelection, chooseDirectories, title, initialPath, fileTypes, _wrap(callback)); + } + + function showSaveDialog(title, initialPath, proposedNewFilename, callback) { + appshell.fs.showSaveDialog(title, initialPath, proposedNewFilename, _wrap(callback)); + } + + function stat(path, callback) { + appshell.fs.stat(path, function (err, stats) { + if (err) { + callback(_mapError(err)); + } else { + var options = { isFile: stats.isFile(), mtime: stats.mtime, size: stats.size }, + fsStats = new FileSystemStats(options); + + callback(null, fsStats); + } + }); + } + + function exists(path, callback) { + stat(path, function (err) { + if (err) { + callback(false); + } else { + callback(true); + } + }); + } + + function readdir(path, callback) { + appshell.fs.readdir(path, function (err, contents) { + if (err) { + callback(_mapError(err)); + return; + } + + var count = contents.length; + if (!count) { + callback(null, [], []); + return; + } + + var stats = []; + contents.forEach(function (val, idx) { + stat(path + "/" + val, function (err, stat) { + stats[idx] = err || stat; + count--; + if (count <= 0) { + callback(null, contents, stats); + } + }); + }); + }); + } + + function mkdir(path, mode, callback) { + if (typeof mode === "function") { + callback = mode; + mode = parseInt("0755", 8); + } + appshell.fs.makedir(path, mode, function (err) { + if (err) { + callback(_mapError(err)); + } else { + stat(path, function (err, stat) { + try { + callback(err, stat); + } finally { + // Fake a file-watcher result until real watchers respond quickly + _changeCallback(_parentPath(path)); + } + }); + } + }); + } + + function rename(oldPath, newPath, callback) { + appshell.fs.rename(oldPath, newPath, _wrap(callback)); + // No need to fake a file-watcher result here: FileSystem already updates index on rename() + } + + /* + * Note: if either the read or the stat call fails then neither the read data + * or stat will be passed back, and the call should be considered to have failed. + * If both calls fail, the error from the read call is passed back. + */ + function readFile(path, options, callback) { + var encoding = options.encoding || "utf8"; + + // Execute the read and stat calls in parallel + var done = false, data, stat, err; + + appshell.fs.readFile(path, encoding, function (_err, _data) { + if (_err) { + callback(_mapError(_err)); + return; + } + + if (done) { + callback(err, err ? null : _data, stat); + } else { + done = true; + data = _data; + } + }); + + exports.stat(path, function (_err, _stat) { + if (done) { + callback(_err, _err ? null : data, _stat); + } else { + done = true; + stat = _stat; + err = _err; + } + }); + } + + function writeFile(path, data, options, callback) { + var encoding = options.encoding || "utf8"; + + exists(path, function (alreadyExists) { + appshell.fs.writeFile(path, data, encoding, function (err) { + if (err) { + callback(_mapError(err)); + } else { + stat(path, function (err, stat) { + try { + callback(err, stat); + } finally { + // Fake a file-watcher result until real watchers respond quickly + if (alreadyExists) { + _changeCallback(path, stat); // existing file modified + } else { + _changeCallback(_parentPath(path)); // new file created + } + } + }); + } + }); + }); + + } + + function unlink(path, callback) { + appshell.fs.unlink(path, function (err) { + try { + callback(_mapError(err)); + } finally { + // Fake a file-watcher result until real watchers respond quickly + _changeCallback(_parentPath(path)); + } + }); + } + + function moveToTrash(path, callback) { + appshell.fs.moveToTrash(path, function (err) { + try { + callback(_mapError(err)); + } finally { + // Fake a file-watcher result until real watchers respond quickly + _changeCallback(_parentPath(path)); + } + }); + } + + /* File watchers are temporarily disabled + function _notifyChanges(callback) { + var change; + + for (change in _pendingChanges) { + if (_pendingChanges.hasOwnProperty(change)) { + callback(change); + delete _pendingChanges[change]; + } + } + } + + function _fileWatcherChange(evt, path, event, filename) { + var change; + + if (event === "change") { + // Only register change events if filename is passed + if (filename) { + change = path + "/" + filename; + } + } else if (event === "rename") { + change = path; + } + if (change && !_pendingChanges.hasOwnProperty(change)) { + if (!_changeTimeout) { + _changeTimeout = window.setTimeout(function () { + _changeTimeout = null; + _notifyChanges(_fileWatcherChange.callback); + }, FILE_WATCHER_BATCH_TIMEOUT); + } + + _pendingChanges[change] = true; + } + } + */ + + function initWatchers(callback) { + _changeCallback = callback; + + /* File watchers are temporarily disabled. For now, send + a "wholesale" change when the window is focused. */ + $(window).on("focus", function () { + callback(null); + }); + + /* + _nodeConnectionDeferred.done(function (nodeConnection) { + if (nodeConnection.connected()) { + _fileWatcherChange.callback = callback; + $(nodeConnection).on("fileWatcher.change", _fileWatcherChange); + } + }); + */ + } + + function watchPath(path) { + /* + _nodeConnectionDeferred.done(function (nodeConnection) { + if (nodeConnection.connected()) { + nodeConnection.domains.fileWatcher.watchPath(path); + } + }); + */ + } + + function unwatchPath(path) { + /* + _nodeConnectionDeferred.done(function (nodeConnection) { + if (nodeConnection.connected()) { + nodeConnection.domains.fileWatcher.unwatchPath(path); + } + }); + */ + } + + function unwatchAll() { + /* + _nodeConnectionDeferred.done(function (nodeConnection) { + if (nodeConnection.connected()) { + nodeConnection.domains.fileWatcher.unwatchAll(); + } + }); + */ + } + + // Export public API + exports.init = init; + exports.showOpenDialog = showOpenDialog; + exports.showSaveDialog = showSaveDialog; + exports.exists = exists; + exports.readdir = readdir; + exports.mkdir = mkdir; + exports.rename = rename; + exports.stat = stat; + exports.readFile = readFile; + exports.writeFile = writeFile; + exports.unlink = unlink; + exports.moveToTrash = moveToTrash; + exports.initWatchers = initWatchers; + exports.watchPath = watchPath; + exports.unwatchPath = unwatchPath; + exports.unwatchAll = unwatchAll; +}); diff --git a/src/language/CSSUtils.js b/src/language/CSSUtils.js index 90406250f58..9466e897997 100644 --- a/src/language/CSSUtils.js +++ b/src/language/CSSUtils.js @@ -38,8 +38,7 @@ define(function (require, exports, module) { DocumentManager = require("document/DocumentManager"), EditorManager = require("editor/EditorManager"), HTMLUtils = require("language/HTMLUtils"), - FileIndexManager = require("project/FileIndexManager"), - NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, + ProjectManager = require("project/ProjectManager"), TokenUtils = require("utils/TokenUtils"); // Constants @@ -947,8 +946,7 @@ define(function (require, exports, module) { /** Finds matching selectors in CSS files; adds them to 'resultSelectors' */ function _findMatchingRulesInCSSFiles(selector, resultSelectors) { - var result = new $.Deferred(), - cssFilesResult = FileIndexManager.getFileInfoList("css"); + var result = new $.Deferred(); // Load one CSS file and search its contents function _loadFileAndScan(fullPath, selector) { @@ -970,13 +968,14 @@ define(function (require, exports, module) { return oneFileResult.promise(); } - // Load index of all CSS files; then process each CSS file in turn (see above) - cssFilesResult.done(function (fileInfos) { - Async.doInParallel(fileInfos, function (fileInfo, number) { - return _loadFileAndScan(fileInfo.fullPath, selector); - }) - .then(result.resolve, result.reject); - }); + ProjectManager.getAllFiles(ProjectManager.getLanguageFilter("css")) + .done(function (cssFiles) { + // Load index of all CSS files; then process each CSS file in turn (see above) + Async.doInParallel(cssFiles, function (fileInfo, number) { + return _loadFileAndScan(fileInfo.fullPath, selector); + }) + .then(result.resolve, result.reject); + }); return result.promise(); } diff --git a/src/language/CodeInspection.js b/src/language/CodeInspection.js index 1746e38a919..915d312aa20 100644 --- a/src/language/CodeInspection.js +++ b/src/language/CodeInspection.js @@ -150,35 +150,26 @@ define(function (require, exports, module) { * This method doesn't update the Brackets UI, just provides inspection results. * These results will reflect any unsaved changes present in the file that is currently opened. * - * @param {!FileEntry} fileEntry File that will be inspected for errors. + * @param {!File} file File that will be inspected for errors. * @param ?{{name:string, scanFile:function(string, string):?{!errors:Array, aborted:boolean}} provider * @return {$.Promise} a jQuery promise that will be resolved with ?{!errors:Array, aborted:boolean} */ - function inspectFile(fileEntry, provider) { + function inspectFile(file, provider) { var response = new $.Deferred(); - provider = provider || getProviderForPath(fileEntry.fullPath); + provider = provider || getProviderForPath(file.fullPath); if (!provider) { response.resolve(null); return response.promise(); } - var doc = DocumentManager.getOpenDocumentForPath(fileEntry.fullPath), - fileTextPromise; - - if (doc) { - fileTextPromise = new $.Deferred().resolve(doc.getText()); - } else { - fileTextPromise = FileUtils.readAsText(fileEntry); - } - - fileTextPromise + DocumentManager.getDocumentText(file) .done(function (fileText) { var result, - perfTimerInspector = PerfUtils.markStart("CodeInspection '" + provider.name + "':\t" + fileEntry.fullPath); + perfTimerInspector = PerfUtils.markStart("CodeInspection '" + provider.name + "':\t" + file.fullPath); try { - result = provider.scanFile(fileText, fileEntry.fullPath); + result = provider.scanFile(fileText, file.fullPath); } catch (err) { console.error("[CodeInspection] Provider " + provider.name + " threw an error: " + err); response.reject(err); @@ -189,7 +180,7 @@ define(function (require, exports, module) { response.resolve(result); }) .fail(function (err) { - console.error("[CodeInspection] Could not read file for inspection: " + fileEntry.fullPath); + console.error("[CodeInspection] Could not read file for inspection: " + file.fullPath); response.reject(err); }); diff --git a/src/language/JSUtils.js b/src/language/JSUtils.js index 03e9b733770..8d76c4dc2e1 100644 --- a/src/language/JSUtils.js +++ b/src/language/JSUtils.js @@ -36,9 +36,10 @@ define(function (require, exports, module) { var Async = require("utils/Async"), DocumentManager = require("document/DocumentManager"), ChangedDocumentTracker = require("document/ChangedDocumentTracker"), + FileSystem = require("filesystem/FileSystem"), FileUtils = require("file/FileUtils"), - NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, PerfUtils = require("utils/PerfUtils"), + ProjectManager = require("project/ProjectManager"), StringUtils = require("utils/StringUtils"); /** @@ -230,16 +231,15 @@ define(function (require, exports, module) { result.resolve(false); } else { // If a cache exists, check the timestamp on disk - var file = new NativeFileSystem.FileEntry(fileInfo.fullPath); + var file = FileSystem.getFileForPath(fileInfo.fullPath); - file.getMetadata( - function (metadata) { - result.resolve(fileInfo.JSUtils.timestamp === metadata.diskTimestamp); - }, - function (error) { - result.reject(error); + file.stat(function (err, stat) { + if (!err) { + result.resolve(fileInfo.JSUtils.timestamp.getTime() === stat.mtime.getTime()); + } else { + result.reject(err); } - ); + }); } } else { // Use the cache if the file did not change and the cache exists @@ -264,9 +264,9 @@ define(function (require, exports, module) { rangeResults = []; docEntries.forEach(function (docEntry) { - // Need to call CollectionUtils.hasProperty here since docEntry.functions could - // have an entry for "hasOwnProperty", which results in an error if trying to - // invoke docEntry.functions.hasOwnProperty(). + // Need to call _.has here since docEntry.functions could have an + // entry for "hasOwnProperty", which results in an error if trying + // to invoke docEntry.functions.hasOwnProperty(). if (_.has(docEntry.functions, functionName)) { var functionsInDocument = docEntry.functions[functionName]; matchedDocuments.push({doc: docEntry.doc, fileInfo: docEntry.fileInfo, functions: functionsInDocument}); @@ -366,7 +366,7 @@ define(function (require, exports, module) { * Return all functions that have the specified name, searching across all the given files. * * @param {!String} functionName The name to match. - * @param {!Array.} fileInfos The array of files to search. + * @param {!Array.} fileInfos The array of files to search. * @param {boolean=} keepAllFiles If true, don't ignore non-javascript files. * @return {$.Promise} that will be resolved with an Array of objects containing the * source document, start line, and end line (0-based, inclusive range) for each matching function list. diff --git a/src/nls/root/strings.js b/src/nls/root/strings.js index 36256efd92e..f86cf620aa6 100644 --- a/src/nls/root/strings.js +++ b/src/nls/root/strings.js @@ -67,8 +67,8 @@ define({ // Application error strings "ERROR_IN_BROWSER_TITLE" : "Oops! {APP_NAME} doesn't run in browsers yet.", "ERROR_IN_BROWSER" : "{APP_NAME} is built in HTML, but right now it runs as a desktop app so you can use it to edit local files. Please use the application shell in the github.com/adobe/brackets-shell repo to run {APP_NAME}.", - - // FileIndexManager error string + + // ProjectManager max files error string "ERROR_MAX_FILES_TITLE" : "Error Indexing Files", "ERROR_MAX_FILES" : "The maximum number of files have been indexed. Actions that look up files in the index may function incorrectly.", diff --git a/src/project/FileIndexManager.js b/src/project/FileIndexManager.js index 06e3617b87b..8ba5c524ff6 100644 --- a/src/project/FileIndexManager.js +++ b/src/project/FileIndexManager.js @@ -25,443 +25,61 @@ /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ /*global define, $, brackets */ -/* - * Manages a collection of FileIndexes where each index maintains a list of information about - * files that meet the criteria specified by the index. The indexes are created lazily when - * they are queried and marked dirty when Brackets becomes active. - * - * TODO (issue 325 ) - FileIndexer doesn't currently add a file to the index when the user createa - * a new file within brackets. - * +/** + * @deprecated + * This is a compatibility shim for legacy Brackets APIs that will be removed soon. + * Use ProjectManager.getAllFiles() instead. */ - - define(function (require, exports, module) { "use strict"; - var _ = require("thirdparty/lodash"); - - var PerfUtils = require("utils/PerfUtils"), - ProjectManager = require("project/ProjectManager"), - FileUtils = require("file/FileUtils"), - Dialogs = require("widgets/Dialogs"), - DefaultDialogs = require("widgets/DefaultDialogs"), - Strings = require("strings"); - - /** - * All the indexes are stored in this object. The key is the name of the index - * and the value is a FileIndex. - */ - var _indexList = {}; - - /** - * Tracks whether _indexList should be considered dirty and invalid. Calls that access - * any data in _indexList should call syncFileIndex prior to accessing the data. - * Note that if _scanDeferred is non-null, the index is dirty even if _indexListDirty is false. - * @type {boolean} - */ - var _indexListDirty = true; - - /** - * A serial number that we use to figure out if a scan has been restarted. When this - * changes, any outstanding async callbacks for previous scans should no-op. - * @type {number} - */ - var _scanID = 0; - - /** - * Store whether the index manager has exceeded the limit so the warning dialog only - * appears once. - * @type {boolean} - */ - var _maxFileDialogDisplayed = false; - - /** class FileIndex - * - * A FileIndex contains an array of fileInfos that meet the criteria specified by - * the filterFunction. FileInfo's in the fileInfo array should unique map to one file. - * - * @constructor - * @param {!string} indexname - * @param {function({!entry})} filterFunction returns true to indicate the entry - * should be included in the index - */ - function FileIndex(indexName, filterFunction) { - this.name = indexName; - this.fileInfos = []; - this.filterFunction = filterFunction; - } - - /** class FileInfo - * - * Class to hold info about a file that a FileIndex wishes to retain. - * - * @constructor - * @param {!string} - */ - function FileInfo(entry) { - this.name = entry.name; - this.fullPath = entry.fullPath; - } - - - /** - * Adds a new index to _indexList and marks the list dirty - * - * A future performance optimization is to only build the new index rather than - * marking them all dirty - * - * @private - * @param {!string} indexName must be unque - * @param {!function({entry} filterFunction should return true to include an - * entry in the index - */ - function _addIndex(indexName, filterFunction) { - if (_indexList.hasOwnProperty(indexName)) { - console.error("Duplicate index name"); - return; - } - if (typeof filterFunction !== "function") { - console.error("Invalid arguments"); - return; - } - - _indexList[indexName] = new FileIndex(indexName, filterFunction); - - _indexListDirty = true; - } - - - /** - * Checks the entry against the filterFunction for each index and adds - * a fileInfo to the index if the entry meets the criteria. FileInfo's are - * shared between indexes. - * - * @private - * @param {!entry} entry to be added to the indexes - */ - // future use when files are incrementally added - // - function _addFileToIndexes(entry) { - - // skip invisible files - if (!ProjectManager.shouldShow(entry)) { - return; - } - - var fileInfo = new FileInfo(entry); - - // skip zipped/binary files - if (ProjectManager.isBinaryFile(fileInfo.name)) { - return; - } - - //console.log(entry.name); - - _.forEach(_indexList, function (index, indexName) { - if (index.filterFunction(entry)) { - index.fileInfos.push(fileInfo); - } - }); - } - - /** - * Error dialog when max files in index is hit - * @return {Dialog} - */ - function _showMaxFilesDialog() { - return Dialogs.showModalDialog( - DefaultDialogs.DIALOG_ID_ERROR, - Strings.ERROR_MAX_FILES_TITLE, - Strings.ERROR_MAX_FILES - ); - } - - /** - * Clears the fileInfo array for all the indexes in _indexList - * @private - */ - function _clearIndexes() { - _.forEach(_indexList, function (index, indexName) { - index.fileInfos = []; - }); - } - - /* Recursively visits all files that are descendent of dirEntry and adds - * files files to each index when the file matches the filter criteria. - * If a scan is already in progress when this is called, the existing scan - * is aborted and its promise will never resolve. - * @private - * @param {!DirectoryEntry} dirEntry - * @returns {$.Promise} - */ - function _scanDirectorySubTree(dirEntry) { - if (!dirEntry) { - console.error("Bad dirEntry passed to _scanDirectorySubTree"); - return; - } - - // Clear out our existing data structures. - _clearIndexes(); - - // Increment the scan ID, so any callbacks from a previous scan will know not to do anything. - _scanID++; - - // keep track of directories as they are asynchronously read. We know we are done - // when dirInProgress becomes empty again. - var state = { fileCount: 0, - dirInProgress: {}, // directory names that are in progress of being read - dirError: {}, // directory names with read errors. key=dir path, value=error - maxFilesHit: false // used to show warning dialog only once - }; - - var deferred = new $.Deferred(), - curScanID = _scanID; - - // inner helper function - function _dirScanDone() { - var key; - for (key in state.dirInProgress) { - if (state.dirInProgress.hasOwnProperty(key)) { - return false; - } - } - return true; - } - - function _finishDirScan(dirEntry) { - //console.log("finished: " + dirEntry.fullPath); - delete state.dirInProgress[dirEntry.fullPath]; - - if (_dirScanDone()) { - //console.log("dir scan completly done"); - deferred.resolve(); - } - } - - // inner helper function - function _scanDirectoryRecurse(dirEntry) { - // skip invisible directories - if (!ProjectManager.shouldShow(dirEntry)) { - return; - } - - state.dirInProgress[dirEntry.fullPath] = true; - //console.log("started dir: " + dirEntry.fullPath); - - dirEntry.createReader().readEntries( - // success callback - function (entries) { - if (curScanID !== _scanID) { - // We're a callback for an aborted scan. Do nothing. - return; - } - - // inspect all children of dirEntry - entries.forEach(function (entry) { - // For now limit the number of files that are indexed by preventing adding files - // or scanning additional directories once a max has been hit. Also notify the - // user once via a dialog. This limit could be increased - // if files were indexed in a worker thread so scanning didn't block the UI - if (state.fileCount > 16000) { - if (!state.maxFilesHit) { - state.maxFilesHit = true; - if (!_maxFileDialogDisplayed) { - _showMaxFilesDialog(); - _maxFileDialogDisplayed = true; - } else { - console.warn("The maximum number of files have been indexed. Actions " + - "that lookup files in the index may function incorrectly."); - } - } - return; - } - - if (entry.isFile) { - _addFileToIndexes(entry); - state.fileCount++; - - } else if (entry.isDirectory) { - _scanDirectoryRecurse(entry); - } - }); - _finishDirScan(dirEntry); - }, - // error callback - function (error) { - state.dirError[dirEntry.fullPath] = error; - _finishDirScan(dirEntry); - } - ); - } - - _scanDirectoryRecurse(dirEntry); - - return deferred.promise(); - } + var ProjectManager = require("project/ProjectManager"), + FileUtils = require("file/FileUtils"); - // debug - function _logFileList(list) { - list.forEach(function (fileInfo) { - console.log(fileInfo.name); - }); - console.log("length: " + list.length); + function _warn() { + console.error("Warning: FileIndexManager is deprecated. Use ProjectManager.getAllFiles() instead"); } - /** - * Used by syncFileIndex function to prevent reentrancy - * @private - */ - var _scanDeferred = null; - - /** - * Clears and rebuilds all of the fileIndexes and sets _indexListDirty to false - * @return {$.Promise} resolved when index has been updated - */ - function syncFileIndex() { - if (_indexListDirty) { - _indexListDirty = false; - - // If we already had an existing scan going, we want to use its deferred for - // notifying when the new scan is complete (so existing callers will get notified), - // and we don't want to start a new measurement. - if (!_scanDeferred) { - _scanDeferred = new $.Deferred(); - PerfUtils.markStart(PerfUtils.FILE_INDEX_MANAGER_SYNC); - } - - // If there was already a scan running, this will abort it and start a new - // scan. The old scan's promise will never resolve, so the net result is that - // the `done` handler below will only execute when the final scan actually - // completes. - _scanDirectorySubTree(ProjectManager.getProjectRoot()) - .done(function () { - PerfUtils.addMeasurement(PerfUtils.FILE_INDEX_MANAGER_SYNC); - _scanDeferred.resolve(); - _scanDeferred = null; - //_logFileList(_indexList["all"].fileInfos); - //_logFileList(_indexList["css"].fileInfos); - }); - return _scanDeferred.promise(); + function _getFilter(indexName) { + if (indexName === "css") { + return ProjectManager.getLanguageFilter("css"); + } else if (indexName === "all") { + return null; } else { - // If we're in the middle of a scan, return its promise, otherwise resolve immediately. - return _scanDeferred ? _scanDeferred.promise() : new $.Deferred().resolve().promise(); + throw new Error("Invalid index name:", indexName); } } - - /** - * Markes all file indexes dirty - */ - function markDirty() { - _indexListDirty = true; - - // If there's a scan already in progress, abort and restart it. - if (_scanDeferred) { - syncFileIndex(); - } - } - + /** - * Returns the FileInfo array for the specified index + * @deprecated * @param {!string} indexname - * @return {$.Promise} a promise that is resolved with an Array of FileInfo's + * @return {$.Promise} a promise that is resolved with an Array of File objects */ function getFileInfoList(indexName) { - var result = new $.Deferred(); - - if (!_indexList.hasOwnProperty(indexName)) { - console.error("indexName not found"); - return; - } - - syncFileIndex() - .done(function () { - result.resolve(_indexList[indexName].fileInfos); - }); - - return result.promise(); - } - - /** - * Calls the filterFunction on every in the index specified by indexName - * and return a a new list of FileInfo's - * @param {!string} - * @param {function({string})} filterFunction - * @return {$.Promise} a promise that is resolved with an Array of FileInfo's - */ - function getFilteredList(indexName, filterFunction) { - var result = new $.Deferred(); - - if (!_indexList.hasOwnProperty(indexName)) { - console.error("indexName not found"); - return; - } - - syncFileIndex() - .done(function () { - var resultList = []; - getFileInfoList(indexName) - .done(function (fileList) { - resultList = fileList.filter(function (fileInfo) { - return filterFunction(fileInfo.name); - }); - - result.resolve(resultList); - }); - }); - - return result.promise(); + _warn(); + return ProjectManager.getAllFiles(_getFilter(indexName)); } /** - * returns an array of fileInfo's that match the filename parameter + * @deprecated * @param {!string} indexName - * @param {!filename} - * @return {$.Promise} a promise that is resolved with an Array of FileInfo's + * @param {!string} filename + * @return {$.Promise} a promise that is resolved with an Array of File objects */ function getFilenameMatches(indexName, filename) { - return getFilteredList(indexName, function (item) { - return item === filename; + _warn(); + + var indexFilter = _getFilter(indexName); + + return ProjectManager.getAllFiles(function (file) { + if (indexFilter && !indexFilter(file)) { + return false; + } + return file.name === filename; }); } - /** - * Add the indexes - */ - - _addIndex( - "all", - function (entry) { - return true; - } - ); - - _addIndex( - "css", - function (entry) { - return FileUtils.getFileExtension(entry.name) === "css"; - } - ); - - /** - * When a new project is opened set the flag for index exceeding maximum - * warning back to false. - */ - $(ProjectManager).on("projectOpen", function (event, projectRoot) { - _maxFileDialogDisplayed = false; - markDirty(); - }); - - $(ProjectManager).on("projectFilesChange", function (event, projectRoot) { - markDirty(); - }); - - PerfUtils.createPerfMeasurement("FILE_INDEX_MANAGER_SYNC", "syncFileIndex"); - - exports.markDirty = markDirty; exports.getFileInfoList = getFileInfoList; exports.getFilenameMatches = getFilenameMatches; - - }); diff --git a/src/project/FileSyncManager.js b/src/project/FileSyncManager.js index 10c96d9615c..cd27b78146d 100644 --- a/src/project/FileSyncManager.js +++ b/src/project/FileSyncManager.js @@ -51,7 +51,7 @@ define(function (require, exports, module) { Strings = require("strings"), StringUtils = require("utils/StringUtils"), FileUtils = require("file/FileUtils"), - NativeFileError = require("file/NativeFileError"); + FileSystemError = require("filesystem/FileSystemError"); /** @@ -100,34 +100,39 @@ define(function (require, exports, module) { var result = new $.Deferred(); // Check file timestamp / existence - doc.file.getMetadata( - function (metadata) { - // Does file's timestamp differ from last sync time on the Document? - if (metadata.modificationTime.getTime() !== doc.diskTimestamp.getTime()) { - if (doc.isDirty) { - editConflicts.push(doc); - } else { - toReload.push(doc); - } - } - result.resolve(); - }, - function (error) { - // File has been deleted externally - if (error.name === NativeFileError.NOT_FOUND_ERR) { - if (doc.isDirty) { - deleteConflicts.push(doc); - } else { - toClose.push(doc); + + if (doc.isUntitled()) { + result.resolve(); + } else { + doc.file.stat(function (err, stat) { + if (!err) { + // Does file's timestamp differ from last sync time on the Document? + if (stat.mtime.getTime() !== doc.diskTimestamp.getTime()) { + if (doc.isDirty) { + editConflicts.push(doc); + } else { + toReload.push(doc); + } } result.resolve(); } else { - // Some other error fetching metadata: treat as a real error - console.log("Error checking modification status of " + doc.file.fullPath, error.name); - result.reject(); + // File has been deleted externally + if (err === FileSystemError.NOT_FOUND) { + if (doc.isDirty) { + deleteConflicts.push(doc); + } else { + toClose.push(doc); + } + result.resolve(); + } else { + // Some other error fetching metadata: treat as a real error + console.log("Error checking modification status of " + doc.file.fullPath, err); + result.reject(); + } } - } - ); + }); + } + return result.promise(); } @@ -149,23 +154,22 @@ define(function (require, exports, module) { function checkWorkingSetFile(file) { var result = new $.Deferred(); - file.getMetadata( - function (metadata) { + file.stat(function (err, stat) { + if (!err) { // File still exists result.resolve(); - }, - function (error) { + } else { // File has been deleted externally - if (error.name === NativeFileError.NOT_FOUND_ERR) { + if (err === FileSystemError.NOT_FOUND) { DocumentManager.notifyFileDeleted(file); result.resolve(); } else { // Some other error fetching metadata: treat as a real error - console.log("Error checking for deletion of " + file.fullPath, error.name); + console.log("Error checking for deletion of " + file.fullPath, err); result.reject(); } } - ); + }); return result.promise(); } @@ -189,7 +193,7 @@ define(function (require, exports, module) { doc.refreshText(text, readTimestamp); }); promise.fail(function (error) { - console.log("Error reloading contents of " + doc.file.fullPath, error.name); + console.log("Error reloading contents of " + doc.file.fullPath, error); }); return promise; } @@ -217,7 +221,7 @@ define(function (require, exports, module) { StringUtils.format( Strings.ERROR_RELOADING_FILE, StringUtils.breakableUrl(doc.file.fullPath), - FileUtils.getFileErrorString(error.name) + FileUtils.getFileErrorString(error) ) ); } diff --git a/src/project/ProjectManager.js b/src/project/ProjectManager.js index cf9b81a3231..5b2a7415dc3 100644 --- a/src/project/ProjectManager.js +++ b/src/project/ProjectManager.js @@ -35,8 +35,6 @@ * - projectOpen -- after _projectRoot changes and the tree is re-rendered * - projectRefresh -- when project tree is re-rendered for a reason other than * a project being opened (e.g. from the Refresh command) - * - projectFilesChange -- sent if one of the project files has changed-- - * added, removed, renamed, etc. * * These are jQuery events, so to listen for them you do something like this: * $(ProjectManager).on("eventname", handler); @@ -53,31 +51,41 @@ define(function (require, exports, module) { // Load dependent modules var AppInit = require("utils/AppInit"), - NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, PreferencesDialogs = require("preferences/PreferencesDialogs"), PreferencesManager = require("preferences/PreferencesManager"), DocumentManager = require("document/DocumentManager"), + InMemoryFile = require("document/InMemoryFile"), CommandManager = require("command/CommandManager"), Commands = require("command/Commands"), Dialogs = require("widgets/Dialogs"), DefaultDialogs = require("widgets/DefaultDialogs"), + LanguageManager = require("language/LanguageManager"), Menus = require("command/Menus"), StringUtils = require("utils/StringUtils"), Strings = require("strings"), + FileSystem = require("filesystem/FileSystem"), FileViewController = require("project/FileViewController"), PerfUtils = require("utils/PerfUtils"), ViewUtils = require("utils/ViewUtils"), FileUtils = require("file/FileUtils"), - NativeFileError = require("file/NativeFileError"), + FileSystemError = require("filesystem/FileSystemError"), Urls = require("i18n!nls/urls"), KeyEvent = require("utils/KeyEvent"), Async = require("utils/Async"), + FileSyncManager = require("project/FileSyncManager"), EditorManager = require("editor/EditorManager"); /** * @private - * File and Folder names which are not displayed or searched + * Forward declaration for the _fileSystemChange and _fileSystemRename functions to make JSLint happy. + */ + var _fileSystemChange, + _fileSystemRename; + + /** + * @private + * File and folder names which are not displayed or searched * TODO: We should add the rest of the file names that TAR excludes: * http://www.gnu.org/software/tar/manual/html_section/exclude.html * @type {RegExp} @@ -105,15 +113,7 @@ define(function (require, exports, module) { * @type {jQueryObject} */ var _projectTree = null; - - function canonicalize(path) { - if (path.length > 0 && path[path.length - 1] === "/") { - return path.slice(0, -1); - } else { - return path; - } - } - + /** * @private * Reference to previous selected jstree leaf node when ProjectManager had @@ -201,10 +201,10 @@ define(function (require, exports, module) { } /** - * Returns the FileEntry or DirectoryEntry corresponding to the item selected in the file tree, or null + * Returns the File or Directory corresponding to the item selected in the file tree, or null * if no item is selected in the tree (though the working set may still have a selection; use * getSelectedItem() to get the selection regardless of whether it's in the tree or working set). - * @return {?Entry} + * @return {?(File|Directory)} */ function _getTreeSelectedItem() { var selected = _projectTree.jstree("get_selected"); @@ -215,11 +215,11 @@ define(function (require, exports, module) { } /** - * Returns the FileEntry or DirectoryEntry corresponding to the item selected in the sidebar panel, whether in + * Returns the File or Directory corresponding to the item selected in the sidebar panel, whether in * the file tree OR in the working set; or null if no item is selected anywhere in the sidebar. * May NOT be identical to the current Document - a folder may be selected in the sidebar, or the sidebar may not * have the current document visible in the tree & working set. - * @return {?Entry} + * @return {?(File|Directory)} */ function getSelectedItem() { // Prefer file tree selection, else use working set selection @@ -275,7 +275,7 @@ define(function (require, exports, module) { /** * Returns the root folder of the currently loaded project, or null if no project is open (during * startup, or running outside of app shell). - * @return {DirectoryEntry} + * @return {Directory} */ function getProjectRoot() { return _projectRoot; @@ -373,7 +373,8 @@ define(function (require, exports, module) { if (entry.fullPath) { fullPath = entry.fullPath; - // Truncate project path prefix, remove the trailing slash + // Truncate project path prefix (including its last slash) AND remove trailing slash suffix + // So "/foo/bar/projroot/abc/xyz/" -> "abc/xyz" shortPath = fullPath.slice(projectPathLength, -1); // Determine depth of the node by counting path separators. @@ -640,14 +641,22 @@ define(function (require, exports, module) { return Async.withTimeout(result.promise(), 1000); } + /** + * @private + * See shouldShow + */ + function _shouldShowName(name) { + return !name.match(_exclusionListRegEx); + } + /** * Returns false for files and directories that are not commonly useful to display. * - * @param {Entry} entry File or directory to filter + * @param {FileSystemEntry} entry File or directory to filter * @return boolean true if the file should be displayed */ function shouldShow(entry) { - return !entry.name.match(_exclusionListRegEx); + return _shouldShowName(entry.name); } /** @@ -661,14 +670,14 @@ define(function (require, exports, module) { /** * @private - * Given an array of NativeFileSystem entries, returns a JSON array representing them in the format + * Given an array of file system entries, returns a JSON array representing them in the format * required by jsTree. Saves the corresponding Entry object as metadata (which jsTree will store in * the DOM via $.data()). * * Does NOT recursively traverse the file system: folders are marked as expandable but are given no * children initially. * - * @param {Array.} entries Array of NativeFileSystem entry objects. + * @param {Array.} entries Array of FileSystemEntry entry objects. * @return {Array} jsTree node data: array of JSON objects */ function _convertEntriesToJSON(entries) { @@ -756,34 +765,32 @@ define(function (require, exports, module) { dirEntry = _projectRoot; isProjectRoot = true; } else { - // All other nodes: the DirectoryEntry is saved as jQ data in the tree (by _convertEntriesToJSON()) + // All other nodes: the Directory is saved as jQ data in the tree (by _convertEntriesToJSON()) dirEntry = treeNode.data("entry"); } // Fetch dirEntry's contents - dirEntry.createReader().readEntries( - processEntries, - function (error, entries) { - if (entries) { + dirEntry.getContents(function (err, contents, stats, statsErrs) { + if (err) { + Dialogs.showModalDialog( + DefaultDialogs.DIALOG_ID_ERROR, + Strings.ERROR_LOADING_PROJECT, + StringUtils.format( + Strings.READ_DIRECTORY_ENTRIES_ERROR, + StringUtils.breakableUrl(dirEntry.fullPath), + err + ) + ); + // Reject the render promise so we can move on. + deferred.reject(); + } else { + if (statsErrs) { // some but not all entries failed to load, so render what we can console.warn("Error reading a subset of folder " + dirEntry); - processEntries(entries); - } else { - Dialogs.showModalDialog( - DefaultDialogs.DIALOG_ID_ERROR, - Strings.ERROR_LOADING_PROJECT, - StringUtils.format( - Strings.READ_DIRECTORY_ENTRIES_ERROR, - StringUtils.breakableUrl(dirEntry.fullPath), - error.name - ) - ); - // Reject the render promise so we can move on. - deferred.reject(); } + processEntries(contents); } - ); - + }); } /** @@ -869,6 +876,49 @@ define(function (require, exports, module) { return updateWelcomeProjectPath(_prefs.getValue("projectPath")); } + /** + * Error dialog when max files in index is hit + * @return {Dialog} + */ + function _showMaxFilesDialog() { + return Dialogs.showModalDialog( + DefaultDialogs.DIALOG_ID_ERROR, + Strings.ERROR_MAX_FILES_TITLE, + Strings.ERROR_MAX_FILES + ); + } + + function _watchProjectRoot(rootPath) { + FileSystem.on("change", _fileSystemChange); + FileSystem.on("rename", _fileSystemRename); + + FileSystem.watch(FileSystem.getDirectoryForPath(rootPath), _shouldShowName, function (err) { + if (err === FileSystemError.TOO_MANY_ENTRIES) { + _showMaxFilesDialog(); + } else if (err) { + console.error("Error watching project root: ", rootPath, err); + } + }); + } + + + /** + * @private + * Close the file system and remove listeners. + */ + function _unwatchProjectRoot() { + if (_projectRoot) { + FileSystem.off("change", _fileSystemChange); + FileSystem.off("rename", _fileSystemRename); + + FileSystem.unwatch(_projectRoot, function (err) { + if (err) { + console.error("Error unwatching project root: ", _projectRoot.fullPath, err); + } + }); + } + } + /** * Loads the given folder as a project. Normally, you would call openProject() instead to let the * user choose a folder. @@ -895,9 +945,11 @@ define(function (require, exports, module) { // close current project $(exports).triggerHandler("beforeProjectClose", _projectRoot); } - + // close all the old files DocumentManager.closeAll(); + + _unwatchProjectRoot(); } // Clear project path map @@ -915,10 +967,13 @@ define(function (require, exports, module) { // Populate file tree as long as we aren't running in the browser if (!brackets.inBrowser) { + if (!isUpdating) { + _watchProjectRoot(rootPath); + } // Point at a real folder structure on local disk - NativeFileSystem.requestNativeFileSystem(rootPath, - function (fs) { - var rootEntry = fs.root; + var rootEntry = FileSystem.getDirectoryForPath(rootPath); + rootEntry.exists(function (exists) { + if (exists) { var projectRootChanged = (!_projectRoot || !rootEntry) || _projectRoot.fullPath !== rootEntry.fullPath; var i; @@ -959,15 +1014,14 @@ define(function (require, exports, module) { resultRenderTree.always(function () { PerfUtils.addMeasurement(perfTimerName); }); - }, - function (error) { + } else { Dialogs.showModalDialog( DefaultDialogs.DIALOG_ID_ERROR, Strings.ERROR_LOADING_PROJECT, StringUtils.format( Strings.REQUEST_NATIVE_FILE_SYSTEM_ERROR, StringUtils.breakableUrl(rootPath), - error.name + FileSystemError.NOT_FOUND ) ).done(function () { // The project folder stored in preference doesn't exist, so load the default @@ -982,9 +1036,8 @@ define(function (require, exports, module) { }); }); } - ); + }); } - return result.promise(); } @@ -992,7 +1045,7 @@ define(function (require, exports, module) { * Finds the tree node corresponding to the given file/folder (rejected if the path lies * outside the project, or if it doesn't exist). * - * @param {!Entry} entry FileEntry or DirectoryEntry to find + * @param {!(File|Directory)} entry File or Directory to find * @return {$.Promise} Resolved with jQ obj for the jsTree tree node; or rejected if not found */ function _findTreeNode(entry) { @@ -1009,7 +1062,7 @@ define(function (require, exports, module) { // We're going to traverse from root of tree, one segment at a time var pathSegments = projRelativePath.split("/"); if (entry.isDirectory) { - pathSegments.pop(); // DirectoryEntry always has a trailing "/" + pathSegments.pop(); // Directory always has a trailing "/" } function findInSubtree($nodes, segmentI) { @@ -1077,7 +1130,7 @@ define(function (require, exports, module) { * Expands tree nodes to show the given file or folder and selects it. Silently no-ops if the * path lies outside the project, or if it doesn't exist. * - * @param {!Entry} entry FileEntry or DirectoryEntry to show + * @param {!(File|Directory)} entry File or Directory to show * @return {$.Promise} Resolved when done; or rejected if not found */ function showInTree(entry) { @@ -1114,8 +1167,8 @@ define(function (require, exports, module) { _loadProject(path, false).then(result.resolve, result.reject); } else { // Pop up a folder browse dialog - NativeFileSystem.showOpenDialog(false, true, Strings.CHOOSE_FOLDER, _projectRoot.fullPath, null, - function (files) { + FileSystem.showOpenDialog(false, true, Strings.CHOOSE_FOLDER, _projectRoot.fullPath, null, function (err, files) { + if (!err) { // If length == 0, user canceled the dialog; length should never be > 1 if (files.length > 0) { // Load the new project into the folder tree @@ -1123,16 +1176,15 @@ define(function (require, exports, module) { } else { result.reject(); } - }, - function (error) { + } else { Dialogs.showModalDialog( DefaultDialogs.DIALOG_ID_ERROR, Strings.ERROR_LOADING_PROJECT, - StringUtils.format(Strings.OPEN_DIALOG_ERROR, error.name) + StringUtils.format(Strings.OPEN_DIALOG_ERROR, err) ); result.reject(); } - ); + }); } }) .fail(function () { @@ -1181,7 +1233,7 @@ define(function (require, exports, module) { * @param initialName {string} Initial name for the item * @param skipRename {boolean} If true, don't allow the user to rename the item * @param isFolder {boolean} If true, create a folder instead of a file - * @return {$.Promise} A promise object that will be resolved with the FileEntry + * @return {$.Promise} A promise object that will be resolved with the File * of the created object, or rejected if the user cancelled or entered an illegal * filename. */ @@ -1194,12 +1246,12 @@ define(function (require, exports, module) { result = new $.Deferred(), wasNodeOpen = true; - // get the FileEntry or DirectoryEntry + // get the File or Directory if (selection) { selectionEntry = selection.data("entry"); } - // move selection to parent DirectoryEntry + // move selection to parent Directory if (selectionEntry) { if (selectionEntry.isFile) { position = "after"; @@ -1219,7 +1271,7 @@ define(function (require, exports, module) { } } - // use the project root DirectoryEntry + // use the project root Directory if (!selectionEntry) { selectionEntry = getProjectRoot(); } @@ -1278,34 +1330,26 @@ define(function (require, exports, module) { if (!isFolder) { _projectTree.jstree("set_text", data.rslt.obj, ViewUtils.getFileEntryDisplay(entry)); } - - // Notify listeners that the project model has changed - $(exports).triggerHandler("projectFilesChange"); result.resolve(entry); }; - var errorCallback = function (error) { + var errorCallback = function (error, entry) { var entryType = isFolder ? Strings.DIRECTORY : Strings.FILE, oppositeEntryType = isFolder ? Strings.FILE : Strings.DIRECTORY; - if (error.name === NativeFileError.PATH_EXISTS_ERR) { - Dialogs.showModalDialog( - DefaultDialogs.DIALOG_ID_ERROR, - StringUtils.format(Strings.INVALID_FILENAME_TITLE, entryType), - StringUtils.format(Strings.FILE_ALREADY_EXISTS, entryType, - StringUtils.breakableUrl(data.rslt.name)) - ); - } else if (error.name === NativeFileError.TYPE_MISMATCH_ERR) { + if (error === FileSystemError.ALREADY_EXISTS) { + var useOppositeType = (isFolder === entry.isFile); Dialogs.showModalDialog( DefaultDialogs.DIALOG_ID_ERROR, StringUtils.format(Strings.INVALID_FILENAME_TITLE, entryType), - StringUtils.format(Strings.FILE_ALREADY_EXISTS, oppositeEntryType, + StringUtils.format(Strings.FILE_ALREADY_EXISTS, + useOppositeType ? oppositeEntryType : entryType, StringUtils.breakableUrl(data.rslt.name)) ); } else { - var errString = error.name === NativeFileError.NO_MODIFICATION_ALLOWED_ERR ? + var errString = error === FileSystemError.NOT_WRITABLE ? Strings.NO_MODIFICATION_ALLOWED_ERR : - StringUtils.format(Strings.GENERIC_ERROR, error.name); + StringUtils.format(Strings.GENERIC_ERROR, error); Dialogs.showModalDialog( DefaultDialogs.DIALOG_ID_ERROR, @@ -1318,23 +1362,38 @@ define(function (require, exports, module) { errorCleanup(); }; - if (isFolder) { - // Use getDirectory() to create the new folder - selectionEntry.getDirectory( - data.rslt.name, - {create: true, exclusive: true}, - successCallback, - errorCallback - ); - } else { - // Use getFile() to create the new file - selectionEntry.getFile( - data.rslt.name, - {create: true, exclusive: true}, - successCallback, - errorCallback - ); - } + var newItemPath = selectionEntry.fullPath + data.rslt.name; + + FileSystem.resolve(newItemPath, function (err, item) { + if (!err) { + // Item already exists, fail with error + errorCallback(FileSystemError.ALREADY_EXISTS, item); + } else { + if (isFolder) { + var directory = FileSystem.getDirectoryForPath(newItemPath); + + directory.create(function (err) { + if (err) { + errorCallback(err); + } else { + successCallback(directory); + } + }); + } else { + // Create an empty file + var file = FileSystem.getFileForPath(newItemPath); + + file.write("", function (err) { + if (err) { + errorCallback(err); + } else { + successCallback(file); + } + }); + } + } + }); + } else { //escapeKeyPressed errorCleanup(); } @@ -1394,31 +1453,12 @@ define(function (require, exports, module) { if (oldName === newName) { result.resolve(); - return result; + return result.promise(); } - // TODO: This should call FileEntry.moveTo(), but that isn't implemented - // yet. For now, call directly to the low-level fs.rename() - brackets.fs.rename(oldName, newName, function (err) { + var entry = isFolder ? FileSystem.getDirectoryForPath(oldName) : FileSystem.getFileForPath(oldName); + entry.rename(newName, function (err) { if (!err) { - // Update all nodes in the project tree. - // All other updating is done by DocumentManager.notifyPathNameChanged() below - var nodes = _projectTree.find(".jstree-leaf, .jstree-open, .jstree-closed"), - i; - - for (i = 0; i < nodes.length; i++) { - var node = $(nodes[i]); - FileUtils.updateFileEntryPath(node.data("entry"), oldName, newName, isFolder); - } - - // Notify that one of the project files has changed - $(exports).triggerHandler("projectFilesChange"); - - // Tell the document manager about the name change. This will update - // all of the model information and send notification to all views - DocumentManager.notifyPathNameChanged(oldName, newName, isFolder); - - // Finally, re-open the selected document if (EditorManager.getCurrentlyViewedPath()) { FileViewController.openAndSelectDocument( EditorManager.getCurrentlyViewedPath(), @@ -1427,7 +1467,6 @@ define(function (require, exports, module) { } _redraw(true); - result.resolve(); } else { // Show an error alert @@ -1437,23 +1476,22 @@ define(function (require, exports, module) { StringUtils.format( Strings.ERROR_RENAMING_FILE, StringUtils.breakableUrl(newName), - err === brackets.fs.ERR_FILE_EXISTS ? + err === FileSystemError.ALREADY_EXISTS ? Strings.FILE_EXISTS_ERR : FileUtils.getFileErrorString(err) ) ); - result.reject(err); } }); - return result; + return result.promise(); } /** * Initiates a rename of the selected item in the project tree, showing an inline editor * for input. Silently no-ops if the entry lies outside the tree or doesn't exist. - * @param {!Entry} entry FileEntry or DirectoryEntry to rename + * @param {!(File|Directory)} entry File or Directory to rename */ function renameItemInline(entry) { // First make sure the item in the tree is visible - jsTree's rename API doesn't do anything to ensure inline input is visible @@ -1483,6 +1521,7 @@ define(function (require, exports, module) { var oldName = selected.data("entry").fullPath; // Folder paths have to end with a slash. Use look-head (?=...) to only replace the folder's name, not the slash as well + var oldNameEndPattern = isFolder ? "(?=\/$)" : "$"; var oldNameRegex = new RegExp(StringUtils.regexEscape(data.rslt.old_name) + oldNameEndPattern); var newName = oldName.replace(oldNameRegex, data.rslt.new_name); @@ -1517,64 +1556,150 @@ define(function (require, exports, module) { /** * Delete file or directore from project - * @param {!Entry} entry FileEntry or DirectoryEntry to delete + * @param {!(File|Directory)} entry File or Directory to delete */ function deleteItem(entry) { var result = new $.Deferred(); - entry.remove(function () { - _findTreeNode(entry).done(function ($node) { - _projectTree.one("delete_node.jstree", function () { - // When a node is deleted, the previous node is automatically selected. - // This works fine as long as the previous node is a file, but doesn't - // work so well if the node is a folder - var sel = _projectTree.jstree("get_selected"), - entry = sel ? sel.data("entry") : null; - - if (entry && entry.isDirectory) { - // Make sure it didn't turn into a leaf node. This happens if - // the only file in the directory was deleted - if (sel.hasClass("jstree-leaf")) { - sel.removeClass("jstree-leaf jstree-open"); - sel.addClass("jstree-closed"); + entry.moveToTrash(function (err) { + if (!err) { + _findTreeNode(entry).done(function ($node) { + _projectTree.one("delete_node.jstree", function () { + // When a node is deleted, the previous node is automatically selected. + // This works fine as long as the previous node is a file, but doesn't + // work so well if the node is a folder + var sel = _projectTree.jstree("get_selected"), + entry = sel ? sel.data("entry") : null; + + if (entry && entry.isDirectory) { + // Make sure it didn't turn into a leaf node. This happens if + // the only file in the directory was deleted + if (sel.hasClass("jstree-leaf")) { + sel.removeClass("jstree-leaf jstree-open"); + sel.addClass("jstree-closed"); + } } - } + }); + var oldSuppressToggleOpen = suppressToggleOpen; + suppressToggleOpen = true; + _projectTree.jstree("delete_node", $node); + suppressToggleOpen = oldSuppressToggleOpen; }); - var oldSuppressToggleOpen = suppressToggleOpen; - suppressToggleOpen = true; - _projectTree.jstree("delete_node", $node); - suppressToggleOpen = oldSuppressToggleOpen; - }); - - // Notify that one of the project files has changed - $(exports).triggerHandler("projectFilesChange"); - if (DocumentManager.getCurrentDocument()) { - DocumentManager.notifyPathDeleted(entry.fullPath); + + if (DocumentManager.getCurrentDocument()) { + DocumentManager.notifyPathDeleted(entry.fullPath); + } else { + EditorManager.notifyPathDeleted(entry.fullPath); + } + + _redraw(true); + result.resolve(); } else { - EditorManager.notifyPathDeleted(entry.fullPath); + // Show an error alert + Dialogs.showModalDialog( + Dialogs.DIALOG_ID_ERROR, + Strings.ERROR_DELETING_FILE_TITLE, + StringUtils.format( + Strings.ERROR_DELETING_FILE, + _.escape(entry.fullPath), + FileUtils.getFileErrorString(err) + ) + ); + + result.reject(err); } - - _redraw(true); - result.resolve(); - }, function (err) { - // Show an error alert - Dialogs.showModalDialog( - Dialogs.DIALOG_ID_ERROR, - Strings.ERROR_DELETING_FILE_TITLE, - StringUtils.format( - Strings.ERROR_DELETING_FILE, - _.escape(entry.fullPath), - FileUtils.getFileErrorString(err) - ) - ); - - result.reject(err); }); return result.promise(); } + /** + * Returns an Array of all files for this project, optionally including + * files in the working set that are *not* under the project root. Files filtered + * out by shouldShow() OR isBinaryFile() are excluded. + * + * @param {function (File, number):boolean=} filter Optional function to filter + * the file list (does not filter directory traversal). API matches Array.filter(). + * @param {boolean=} includeWorkingSet If true, include files in the working set + * that are not under the project root (*except* for untitled documents). + * + * @return {$.Promise} Promise that is resolved with an Array of File objects. + */ + function getAllFiles(filter, includeWorkingSet) { + var deferred = new $.Deferred(), + result = []; + + // The filter and includeWorkingSet params are both optional. + // Handle the case where filter is omitted but includeWorkingSet is + // specified. + if (includeWorkingSet === undefined && typeof (filter) !== "function") { + includeWorkingSet = filter; + filter = null; + } + + + function visitor(entry) { + if (entry.isFile && !isBinaryFile(entry.name)) { + result.push(entry); + } + + return true; + } + + // First gather all files in project proper + getProjectRoot().visit(visitor, function (err) { + // Add working set entries, if requested + if (includeWorkingSet) { + DocumentManager.getWorkingSet().forEach(function (file) { + if (result.indexOf(file) === -1 && !(file instanceof InMemoryFile)) { + result.push(file); + } + }); + } + + // Filter list, if requested + if (filter) { + result = result.filter(filter); + } + + deferred.resolve(result); + }); + + return deferred.promise(); + } + + /** + * Returns a filter for use with getAllFiles() that filters files based on LanguageManager language id + * @param {!string} languageId + * @return {!function(File):boolean} + */ + function getLanguageFilter(languageId) { + return function languageFilter(file) { + return (LanguageManager.getLanguageForPath(file.fullPath).getId() === languageId); + }; + } + + /** + * @private + * Respond to a FileSystem change event. + */ + _fileSystemChange = function (event, item) { + // TODO: Refresh file tree too - once watchers are precise enough to notify only + // when real changes occur, instead of on every window focus! + + FileSyncManager.syncOpenDocuments(); + }; + /** + * @private + * Respond to a FileSystem rename event. + */ + _fileSystemRename = function (event, oldName, newName) { + // Tell the document manager about the name change. This will update + // all of the model information and send notification to all views + DocumentManager.notifyPathNameChanged(oldName, newName); + }; + // Initialize variables and listeners that depend on the HTML DOM AppInit.htmlReady(function () { $projectTreeContainer = $("#project-files-container"); @@ -1603,7 +1728,8 @@ define(function (require, exports, module) { // Event Handlers $(FileViewController).on("documentSelectionFocusChange", _documentSelectionFocusChange); $(FileViewController).on("fileViewFocusChange", _fileViewFocusChange); - + $(exports).on("beforeAppClose", _unwatchProjectRoot); + // Commands CommandManager.register(Strings.CMD_OPEN_FOLDER, Commands.FILE_OPEN_FOLDER, openProject); CommandManager.register(Strings.CMD_PROJECT_SETTINGS, Commands.FILE_PROJECT_SETTINGS, _projectSettings); @@ -1628,4 +1754,6 @@ define(function (require, exports, module) { exports.forceFinishRename = forceFinishRename; exports.showInTree = showInTree; exports.refreshFileTree = refreshFileTree; + exports.getAllFiles = getAllFiles; + exports.getLanguageFilter = getLanguageFilter; }); diff --git a/src/project/WorkingSetSort.js b/src/project/WorkingSetSort.js index d33430b6309..2f3e0beb240 100644 --- a/src/project/WorkingSetSort.js +++ b/src/project/WorkingSetSort.js @@ -174,7 +174,7 @@ define(function (require, exports, module) { * @private * * @param {string} commandID A valid command identifier. - * @param {function(FileEntry, FileEntry): number} compareFn A valid sort + * @param {function(File, File): number} compareFn A valid sort * function (see register for a longer explanation). * @param {string} events Space-separated DocumentManager possible events * ending with ".sort". @@ -190,7 +190,7 @@ define(function (require, exports, module) { return this._commandID; }; - /** @return {function(FileEntry, FileEntry): number} The compare function */ + /** @return {function(File, File): number} The compare function */ Sort.prototype.getCompareFn = function () { return this._compareFn; }; @@ -223,7 +223,7 @@ define(function (require, exports, module) { /** * Registers a working set sort method. * @param {(string|Command)} command A command ID or a command object - * @param {function(FileEntry, FileEntry): number} compareFn The function that + * @param {function(File, File): number} compareFn The function that * will be used inside JavaScript's sort function. The return a value * should be >0 (sort a to a lower index than b), =0 (leaves a and b * unchanged with respect to each other) or <0 (sort b to a lower index diff --git a/src/project/WorkingSetView.js b/src/project/WorkingSetView.js index 45e00be4889..03b9610a27d 100644 --- a/src/project/WorkingSetView.js +++ b/src/project/WorkingSetView.js @@ -93,7 +93,7 @@ define(function (require, exports, module) { /** * @private * Adds directory names to elements representing passed files in working tree - * @param {Array.} filesList - list of FileEntries with the same filename + * @param {Array.} filesList - list of Files with the same filename */ function _addDirectoryNamesToWorkingTreeFiles(filesList) { // filesList must have at least two files in it for this to make sense @@ -465,7 +465,7 @@ define(function (require, exports, module) { /** * Builds the UI for a new list item and inserts in into the end of the list * @private - * @param {FileEntry} file + * @param {File} file * @return {HTMLLIElement} newListItem */ function _createNewListItem(file) { @@ -517,7 +517,7 @@ define(function (require, exports, module) { /** * Finds the listItem item assocated with the file. Returns null if not found. * @private - * @param {!FileEntry} file + * @param {!File} file * @return {HTMLLIItem} */ function _findListItemFromFile(file) { @@ -607,7 +607,7 @@ define(function (require, exports, module) { /** * @private - * @param {FileEntry} file + * @param {File} file * @param {boolean=} suppressRedraw If true, suppress redraw */ function _handleFileRemoved(file, suppressRedraw) { diff --git a/src/search/FindInFiles.js b/src/search/FindInFiles.js index 05d0612ae59..e2eebf4275e 100644 --- a/src/search/FindInFiles.js +++ b/src/search/FindInFiles.js @@ -54,11 +54,12 @@ define(function (require, exports, module) { DocumentModule = require("document/Document"), DocumentManager = require("document/DocumentManager"), EditorManager = require("editor/EditorManager"), - PanelManager = require("view/PanelManager"), - FileIndexManager = require("project/FileIndexManager"), - FileViewController = require("project/FileViewController"), - NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, + FileSystem = require("filesystem/FileSystem"), FileUtils = require("file/FileUtils"), + FileViewController = require("project/FileViewController"), + PerfUtils = require("utils/PerfUtils"), + InMemoryFile = require("document/InMemoryFile"), + PanelManager = require("view/PanelManager"), KeyEvent = require("utils/KeyEvent"), AppInit = require("utils/AppInit"), StatusBar = require("widgets/StatusBar"), @@ -69,7 +70,8 @@ define(function (require, exports, module) { searchSummaryTemplate = require("text!htmlContent/search-summary.html"), searchResultsTemplate = require("text!htmlContent/search-results.html"); - /** @cost Constants used to define the maximum results show per page and found in a single file */ + /** @const Constants used to define the maximum results show per page and found in a single file */ + var RESULTS_PER_PAGE = 100, FIND_IN_FILE_MAX = 300, UPDATE_TIMEOUT = 400; @@ -92,7 +94,7 @@ define(function (require, exports, module) { /** @type {RegExp} The current search query regular expression */ var currentQueryExpr = null; - /** @type {Array.} An array of the files where it should look or null/empty to search the entire project */ + /** @type {Array.} An array of the files where it should look or null/empty to search the entire project */ var currentScope = null; /** @type {boolean} True if the matches in a file reached FIND_IN_FILE_MAX */ @@ -609,18 +611,18 @@ define(function (require, exports, module) { /** * @private - * @param {!FileInfo} fileInfo File in question - * @param {?Entry} scope Search scope, or null if whole project + * @param {!File} file File in question + * @param {?Entry} scope Search scope, or null if whole project * @return {boolean} */ - function _inScope(fileInfo, scope) { + function _inScope(file, scope) { if (scope) { if (scope.isDirectory) { // Dirs always have trailing slash, so we don't have to worry about being // a substring of another dir name - return fileInfo.fullPath.indexOf(scope.fullPath) === 0; + return file.fullPath.indexOf(scope.fullPath) === 0; } else { - return fileInfo.fullPath === scope.fullPath; + return file.fullPath === scope.fullPath; } } return true; @@ -666,23 +668,26 @@ define(function (require, exports, module) { dialog = null; return; } - FileIndexManager.getFileInfoList("all") + + var scopeName = currentScope ? currentScope.fullPath : ProjectManager.getProjectRoot().fullPath, + perfTimer = PerfUtils.markStart("FindIn: " + scopeName + " - " + query); + + ProjectManager.getAllFiles(true) .done(function (fileListResult) { - Async.doInParallel(fileListResult, function (fileInfo) { + Async.doInParallel(fileListResult, function (file) { var result = new $.Deferred(); - if (!_inScope(fileInfo, currentScope)) { + if (!_inScope(file, currentScope)) { result.resolve(); } else { - // Search one file - DocumentManager.getDocumentForPath(fileInfo.fullPath) - .done(function (doc) { - _addSearchMatches(fileInfo.fullPath, doc.getText(), currentQueryExpr); + DocumentManager.getDocumentText(file) + .done(function (text) { + _addSearchMatches(file.fullPath, text, currentQueryExpr); result.resolve(); }) .fail(function (error) { - // Error reading this file. This is most likely because the file isn't a text file. - // Resolve here so we move on to the next file. + // Always resolve. If there is an error, this file + // is skipped and we move on to the next file. result.resolve(); }); } @@ -692,11 +697,13 @@ define(function (require, exports, module) { // Done searching all files: show results _showSearchResults(); StatusBar.hideBusyIndicator(); + PerfUtils.addMeasurement(perfTimer); $(DocumentModule).on("documentChange.findInFiles", _documentChangeHandler); }) .fail(function () { console.log("find in files failed."); StatusBar.hideBusyIndicator(); + PerfUtils.finalizeMeasurement(perfTimer); }); }); } @@ -799,7 +806,7 @@ define(function (require, exports, module) { return; } - if (scope instanceof NativeFileSystem.InaccessibleFileEntry) { + if (scope instanceof InMemoryFile) { CommandManager.execute(Commands.FILE_OPEN, { fullPath: scope.fullPath }).done(function () { CommandManager.execute(Commands.EDIT_FIND); }); @@ -867,25 +874,47 @@ define(function (require, exports, module) { /** * @private - * Deletes the results from the deleted file and updates the results list, if required + * Handle a FileSystem "change" event * @param {$.Event} event - * @param {string} path + * @param {FileSystemEntry} entry */ - function _pathDeletedHandler(event, path) { - var resultsChanged = false; - - if (searchResultsPanel.isVisible()) { - // Update the search results - _.forEach(searchResults, function (item, fullPath) { - if (FileUtils.isAffectedWhenRenaming(fullPath, path)) { - delete searchResults[fullPath]; - resultsChanged = true; - } - }); + function _fileSystemChangeHandler(event, entry) { + if (entry && entry.isDirectory) { + var resultsChanged = false; - // Restore the results if needed - if (resultsChanged) { - _restoreSearchResults(); + // This is a temporary watcher implementation that needs to be updated + // once we have our final watcher API. Specifically, we will be adding + // 'added' and 'removed' parameters to this function to easily determine + // which files/folders have been added or removed. + // + // In the meantime, do a quick check for directory changed events to see + // if any of the search results files have been deleted. + if (searchResultsPanel.isVisible()) { + entry.getContents(function (err, contents) { + if (!err) { + var _includesPath = function (fullPath) { + return _.some(contents, function (item) { + return item.fullPath === fullPath; + }); + }; + + // Update the search results + _.forEach(searchResults, function (item, fullPath) { + if (fullPath.lastIndexOf("/") === entry.fullPath.length - 1) { + // The changed directory includes this entry. Make sure the file still exits. + if (!_includesPath(fullPath)) { + delete searchResults[fullPath]; + resultsChanged = true; + } + } + }); + + // Restore the results if needed + if (resultsChanged) { + _restoreSearchResults(); + } + } + }); } } } @@ -904,9 +933,10 @@ define(function (require, exports, module) { // Initialize: register listeners $(DocumentManager).on("fileNameChange", _fileNameChangeHandler); - $(DocumentManager).on("pathDeleted", _pathDeletedHandler); $(ProjectManager).on("beforeProjectClose", _hideSearchResults); + FileSystem.on("change", _fileSystemChangeHandler); + // Initialize: command handlers CommandManager.register(Strings.CMD_FIND_IN_FILES, Commands.EDIT_FIND_IN_FILES, _doFindInFiles); CommandManager.register(Strings.CMD_FIND_IN_SUBTREE, Commands.EDIT_FIND_IN_SUBTREE, _doFindInSubtree); diff --git a/src/search/QuickOpen.js b/src/search/QuickOpen.js index af9135a6e64..79594fb1382 100644 --- a/src/search/QuickOpen.js +++ b/src/search/QuickOpen.js @@ -40,8 +40,7 @@ define(function (require, exports, module) { "use strict"; - var FileIndexManager = require("project/FileIndexManager"), - DocumentManager = require("document/DocumentManager"), + var DocumentManager = require("document/DocumentManager"), EditorManager = require("editor/EditorManager"), CommandManager = require("command/CommandManager"), Strings = require("strings"), @@ -523,7 +522,7 @@ define(function (require, exports, module) { } function searchFileList(query, matcher) { - // FileIndexManager may still be loading asynchronously - if so, can't return a result yet + // The file index may still be loading asynchronously - if so, can't return a result yet if (!fileList) { // Smart Autocomplete allows us to return a Promise instead... var asyncResult = new $.Deferred(); @@ -840,9 +839,9 @@ define(function (require, exports, module) { this.setSearchFieldValue(prefix, initialString); - // Start fetching the file list, which will be needed the first time the user enters an un-prefixed query. If FileIndexManager's + // Start fetching the file list, which will be needed the first time the user enters an un-prefixed query. If file index // caches are out of date, this list might take some time to asynchronously build. See searchFileList() for how this is handled. - fileListPromise = FileIndexManager.getFileInfoList("all") + fileListPromise = ProjectManager.getAllFiles(true) .done(function (files) { fileList = files; fileListPromise = null; diff --git a/src/utils/BuildInfoUtils.js b/src/utils/BuildInfoUtils.js index 5961df26854..7b7f7449a0e 100644 --- a/src/utils/BuildInfoUtils.js +++ b/src/utils/BuildInfoUtils.js @@ -33,9 +33,9 @@ define(function (require, exports, module) { "use strict"; var Global = require("utils/Global"), - FileUtils = require("file/FileUtils"), - NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem; - + FileSystem = require("filesystem/FileSystem"), + FileUtils = require("file/FileUtils"); + var _bracketsSHA; /** @@ -48,11 +48,10 @@ define(function (require, exports, module) { if (brackets.inBrowser) { result.reject(); } else { - var fileEntry = new NativeFileSystem.FileEntry(path); - // HEAD contains a SHA in detached-head mode; otherwise it contains a relative path // to a file in /refs which in turn contains the SHA - FileUtils.readAsText(fileEntry).done(function (text) { + var file = FileSystem.getFileForPath(path); + FileUtils.readAsText(file).done(function (text) { if (text.indexOf("ref: ") === 0) { // e.g. "ref: refs/heads/branchname" var basePath = path.substr(0, path.lastIndexOf("/")), diff --git a/src/utils/DragAndDrop.js b/src/utils/DragAndDrop.js index 2dce7a9dd36..3a5dbf3ee59 100644 --- a/src/utils/DragAndDrop.js +++ b/src/utils/DragAndDrop.js @@ -34,6 +34,7 @@ define(function (require, exports, module) { Dialogs = require("widgets/Dialogs"), DefaultDialogs = require("widgets/DefaultDialogs"), DocumentManager = require("document/DocumentManager"), + FileSystem = require("filesystem/FileSystem"), EditorManager = require("editor/EditorManager"), FileUtils = require("file/FileUtils"), ProjectManager = require("project/ProjectManager"), @@ -100,34 +101,34 @@ define(function (require, exports, module) { var errorFiles = [], filteredFiles = filterFilesToOpen(files); - return Async.doInParallel(filteredFiles, function (file, idx) { + return Async.doInParallel(filteredFiles, function (path, idx) { var result = new $.Deferred(); - // Only open files - brackets.fs.stat(file, function (err, stat) { - if (!err && stat.isFile()) { + // Only open files. + FileSystem.resolve(path, function (err, item) { + if (!err && item.isFile) { // If the file is already open, and this isn't the last // file in the list, return. If this *is* the last file, // always open it so it gets selected. if (idx < filteredFiles.length - 1) { - if (DocumentManager.findInWorkingSet(file) !== -1) { + if (DocumentManager.findInWorkingSet(path) !== -1) { result.resolve(); return; } } CommandManager.execute(Commands.FILE_ADD_TO_WORKING_SET, - {fullPath: file, silent: true}) + {fullPath: path, silent: true}) .done(function () { result.resolve(); }) .fail(function () { - errorFiles.push(file); + errorFiles.push(path); result.reject(); }); - } else if (!err && stat.isDirectory() && filteredFiles.length === 1) { + } else if (!err && item.isDirectory && filteredFiles.length === 1) { // One folder was dropped, open it. - ProjectManager.openProject(file) + ProjectManager.openProject(path) .done(function () { result.resolve(); }) @@ -136,7 +137,7 @@ define(function (require, exports, module) { result.reject(); }); } else { - errorFiles.push(file); + errorFiles.push(path); result.reject(); } }); diff --git a/src/utils/ExtensionLoader.js b/src/utils/ExtensionLoader.js index 355a498617c..53d257b8121 100644 --- a/src/utils/ExtensionLoader.js +++ b/src/utils/ExtensionLoader.js @@ -39,7 +39,7 @@ define(function (require, exports, module) { require("utils/Global"); - var NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, + var FileSystem = require("filesystem/FileSystem"), FileUtils = require("file/FileUtils"), Async = require("utils/Async"); @@ -192,11 +192,9 @@ define(function (require, exports, module) { function testExtension(name, config, entryPoint) { var result = new $.Deferred(), extensionPath = config.baseUrl + "/" + entryPoint + ".js"; - - var fileExists = false, statComplete = false; - brackets.fs.stat(extensionPath, function (err, stat) { - statComplete = true; - if (err === brackets.fs.NO_ERROR && stat.isFile()) { + + FileSystem.resolve(extensionPath, function (err, entry) { + if (!err && entry.isFile) { // unit test file exists var extensionRequire = brackets.libRequire.config({ context: name, @@ -231,48 +229,40 @@ define(function (require, exports, module) { function _loadAll(directory, config, entryPoint, processExtension) { var result = new $.Deferred(); - NativeFileSystem.requestNativeFileSystem(directory, - function (fs) { - fs.root.createReader().readEntries( - function (entries) { - var i, - extensions = []; - - for (i = 0; i < entries.length; i++) { - if (entries[i].isDirectory) { - // FUTURE (JRB): read package.json instead of just using the entrypoint "main". - // Also, load sub-extensions defined in package.json. - extensions.push(entries[i].name); - } - } - - if (extensions.length === 0) { - result.resolve(); - return; - } - - Async.doInParallel(extensions, function (item) { - var extConfig = { - baseUrl: config.baseUrl + "/" + item, - paths: config.paths - }; - return processExtension(item, extConfig, entryPoint); - }).always(function () { - // Always resolve the promise even if some extensions had errors - result.resolve(); - }); - }, - function (error) { - console.error("[Extension] Error -- could not read native directory: " + directory); - result.reject(); + FileSystem.getDirectoryForPath(directory).getContents(function (err, contents) { + if (!err) { + var i, + extensions = []; + + for (i = 0; i < contents.length; i++) { + if (contents[i].isDirectory) { + // FUTURE (JRB): read package.json instead of just using the entrypoint "main". + // Also, load sub-extensions defined in package.json. + extensions.push(contents[i].name); } - ); - }, - function (error) { - console.error("[Extension] Error -- could not open native directory: " + directory); + } + + if (extensions.length === 0) { + result.resolve(); + return; + } + + Async.doInParallel(extensions, function (item) { + var extConfig = { + baseUrl: config.baseUrl + "/" + item, + paths: config.paths + }; + return processExtension(item, extConfig, entryPoint); + }).always(function () { + // Always resolve the promise even if some extensions had errors + result.resolve(); + }); + } else { + console.error("[Extension] Error -- could not read native directory: " + directory); result.reject(); - }); - + } + }); + return result.promise(); } @@ -328,8 +318,7 @@ define(function (require, exports, module) { // Load extensions before restoring the project - // Create a new DirectoryEntry and call getDirectory() on the user extension - // directory. If the directory doesn't exist, it will be created. + // Get a Directory for the user extension directory and create it if it doesn't exist. // Note that this is an async call and there are no success or failure functions passed // in. If the directory *doesn't* exist, it will be created. Extension loading may happen // before the directory is finished being created, but that is okay, since the extension @@ -337,13 +326,11 @@ define(function (require, exports, module) { // If the directory *does* exist, nothing else needs to be done. It will be scanned normally // during extension loading. var extensionPath = getUserExtensionPath(); - new NativeFileSystem.DirectoryEntry().getDirectory(extensionPath, - {create: true}); + FileSystem.getDirectoryForPath(extensionPath).create(); // Create the extensions/disabled directory, too. var disabledExtensionPath = extensionPath.replace(/\/user$/, "/disabled"); - new NativeFileSystem.DirectoryEntry().getDirectory(disabledExtensionPath, - {create: true}); + FileSystem.getDirectoryForPath(disabledExtensionPath).create(); var promise = Async.doSequentially(paths, function (item) { var extensionPath = item; diff --git a/src/utils/NativeApp.js b/src/utils/NativeApp.js index 71c88937fb4..ca3617b2f95 100644 --- a/src/utils/NativeApp.js +++ b/src/utils/NativeApp.js @@ -27,7 +27,8 @@ define(function (require, exports, module) { "use strict"; - var Async = require("utils/Async"); + var Async = require("utils/Async"), + FileSystemError = require("filesystem/FileSystemError"); /** * @private @@ -35,11 +36,11 @@ define(function (require, exports, module) { */ function _browserErrToFileError(err) { if (err === brackets.fs.ERR_NOT_FOUND) { - return FileError.NOT_FOUND_ERR; + return FileSystemError.NOT_FOUND; } - // All other errors are mapped to the generic "security" error - return FileError.SECURITY_ERR; + // All other errors are mapped to the generic "unknown" error + return FileSystemError.UNKNOWN; } var liveBrowserOpenedPIDs = []; diff --git a/src/utils/ViewUtils.js b/src/utils/ViewUtils.js index d67978fe2e6..f7e60af39b6 100644 --- a/src/utils/ViewUtils.js +++ b/src/utils/ViewUtils.js @@ -342,7 +342,7 @@ define(function (require, exports, module) { /** * HTML formats a file entry name for display in the sidebar. - * @param {!FileEntry} entry File entry to display + * @param {!File} entry File entry to display * @return {string} HTML formatted string */ function getFileEntryDisplay(entry) { diff --git a/test/SpecRunner.js b/test/SpecRunner.js index c4e55ce9a3b..f5c7676ca2f 100644 --- a/test/SpecRunner.js +++ b/test/SpecRunner.js @@ -48,9 +48,9 @@ define(function (require, exports, module) { SpecRunnerUtils = require("spec/SpecRunnerUtils"), ExtensionLoader = require("utils/ExtensionLoader"), Async = require("utils/Async"), + FileSystem = require("filesystem/FileSystem"), FileUtils = require("file/FileUtils"), Menus = require("command/Menus"), - NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, UrlParams = require("utils/UrlParams").UrlParams, UnitTestReporter = require("test/UnitTestReporter").UnitTestReporter, NodeConnection = require("utils/NodeConnection"), @@ -89,6 +89,9 @@ define(function (require, exports, module) { */ var NODE_CONNECTION_TIMEOUT = 30000; // 30 seconds - TODO: share with StaticServer? + // Initialize the file system + FileSystem.init(require("filesystem/impls/appshell/AppshellFileSystem")); + // parse URL parameters params.parse(); resultsPath = params.get("resultsPath"); @@ -150,19 +153,21 @@ define(function (require, exports, module) { function writeResults(path, text) { // check if the file already exists - brackets.fs.stat(path, function (err, stat) { - if (err === brackets.fs.ERR_NOT_FOUND) { - // file not found, write the new file with xml content - brackets.fs.writeFile(path, text, NativeFileSystem._FSEncodings.UTF8, function (err) { - if (err) { - _writeResults.reject(); - } else { - _writeResults.resolve(); - } - }); - } else { + var file = FileSystem.getFileForPath(path); + + file.exists(function (exists) { + if (exists) { // file exists, do not overwrite _writeResults.reject(); + } else { + // file not found, write the new file with xml content + FileUtils.writeText(file, text) + .done(function () { + _writeResults.resolve(); + }) + .fail(function (err) { + _writeResults.reject(err); + }); } }); } @@ -183,7 +188,7 @@ define(function (require, exports, module) { } /** - * Patch JUnitXMLReporter to use brackets.fs and to consolidate all results + * Patch JUnitXMLReporter to use FileSystem and to consolidate all results * into a single file. */ function _patchJUnitReporter() { @@ -413,6 +418,6 @@ define(function (require, exports, module) { brackets.testing = { getNodeConnectionDeferred: getNodeConnectionDeferred }; - + init(); }); diff --git a/test/UnitTestSuite.js b/test/UnitTestSuite.js index c2e1a81f439..2506ab890f1 100644 --- a/test/UnitTestSuite.js +++ b/test/UnitTestSuite.js @@ -44,7 +44,7 @@ define(function (require, exports, module) { require("spec/ExtensionLoader-test"); require("spec/ExtensionManager-test"); require("spec/ExtensionUtils-test"); - require("spec/FileIndexManager-test"); + require("spec/FileSystem-test"); require("spec/FileUtils-test"); require("spec/FindReplace-test"); require("spec/HTMLInstrumentation-test"); @@ -58,7 +58,6 @@ define(function (require, exports, module) { require("spec/LowLevelFileIO-test"); require("spec/Menu-test"); require("spec/MultiRangeInlineEditor-test"); - require("spec/NativeFileSystem-test"); require("spec/NativeMenu-test"); require("spec/NodeConnection-test"); require("spec/PreferencesManager-test"); diff --git a/test/spec/CSSUtils-test.js b/test/spec/CSSUtils-test.js index f508d877c6d..b67be3f9047 100644 --- a/test/spec/CSSUtils-test.js +++ b/test/spec/CSSUtils-test.js @@ -27,8 +27,8 @@ define(function (require, exports, module) { "use strict"; - var NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, - Async = require("utils/Async"), + var Async = require("utils/Async"), + FileSystem = require("filesystem/FileSystem"), FileUtils = require("file/FileUtils"), CSSUtils = require("language/CSSUtils"), HTMLUtils = require("language/HTMLUtils"), @@ -36,14 +36,15 @@ define(function (require, exports, module) { TextRange = require("document/TextRange").TextRange; var testPath = SpecRunnerUtils.getTestPath("/spec/CSSUtils-test-files"), - simpleCssFileEntry = new NativeFileSystem.FileEntry(testPath + "/simple.css"), - universalCssFileEntry = new NativeFileSystem.FileEntry(testPath + "/universal.css"), - groupsFileEntry = new NativeFileSystem.FileEntry(testPath + "/groups.css"), - offsetsCssFileEntry = new NativeFileSystem.FileEntry(testPath + "/offsets.css"), - bootstrapCssFileEntry = new NativeFileSystem.FileEntry(testPath + "/bootstrap.css"), - escapesCssFileEntry = new NativeFileSystem.FileEntry(testPath + "/escaped-identifiers.css"), - embeddedHtmlFileEntry = new NativeFileSystem.FileEntry(testPath + "/embedded.html"), - cssRegionsFileEntry = new NativeFileSystem.FileEntry(testPath + "/regions.css"); + simpleCssFileEntry = FileSystem.getFileForPath(testPath + "/simple.css"), + universalCssFileEntry = FileSystem.getFileForPath(testPath + "/universal.css"), + groupsFileEntry = FileSystem.getFileForPath(testPath + "/groups.css"), + offsetsCssFileEntry = FileSystem.getFileForPath(testPath + "/offsets.css"), + bootstrapCssFileEntry = FileSystem.getFileForPath(testPath + "/bootstrap.css"), + escapesCssFileEntry = FileSystem.getFileForPath(testPath + "/escaped-identifiers.css"), + embeddedHtmlFileEntry = FileSystem.getFileForPath(testPath + "/embedded.html"), + cssRegionsFileEntry = FileSystem.getFileForPath(testPath + "/regions.css"); + var contextTestCss = require("text!spec/CSSUtils-test-files/contexts.css"), selectorPositionsTestCss = require("text!spec/CSSUtils-test-files/selector-positions.css"), @@ -203,7 +204,7 @@ define(function (require, exports, module) { describe("with sprint 4 exemptions", function () { beforeEach(function () { - var sprint4exemptions = new NativeFileSystem.FileEntry(testPath + "/sprint4.css"); + var sprint4exemptions = FileSystem.getFileForPath(testPath + "/sprint4.css"); init(this, sprint4exemptions); }); diff --git a/test/spec/DocumentCommandHandlers-test.js b/test/spec/DocumentCommandHandlers-test.js index b2f2942aad9..b2a238f5953 100644 --- a/test/spec/DocumentCommandHandlers-test.js +++ b/test/spec/DocumentCommandHandlers-test.js @@ -34,10 +34,10 @@ define(function (require, exports, module) { DocumentCommandHandlers, // loaded from brackets.test DocumentManager, // loaded from brackets.test Dialogs, // loaded from brackets.test + FileSystem, // loaded from brackets.test FileViewController, // loaded from brackets.test EditorManager, // loaded from brackets.test SpecRunnerUtils = require("spec/SpecRunnerUtils"), - NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, FileUtils = require("file/FileUtils"), StringUtils = require("utils/StringUtils"), Editor = require("editor/Editor"); @@ -66,6 +66,7 @@ define(function (require, exports, module) { DocumentCommandHandlers = testWindow.brackets.test.DocumentCommandHandlers; DocumentManager = testWindow.brackets.test.DocumentManager; Dialogs = testWindow.brackets.test.Dialogs; + FileSystem = testWindow.brackets.test.FileSystem; FileViewController = testWindow.brackets.test.FileViewController; EditorManager = testWindow.brackets.test.EditorManager; }); @@ -184,7 +185,7 @@ define(function (require, exports, module) { }); runs(function () { - spyOn(testWindow.brackets.fs, 'showSaveDialog').andCallFake(function (dialogTitle, initialPath, proposedNewName, callback) { + spyOn(FileSystem, 'showSaveDialog').andCallFake(function (dialogTitle, initialPath, proposedNewName, callback) { callback(undefined, newFilePath); }); @@ -221,7 +222,7 @@ define(function (require, exports, module) { }); runs(function () { - spyOn(testWindow.brackets.fs, 'showSaveDialog').andCallFake(function (dialogTitle, initialPath, proposedNewName, callback) { + spyOn(FileSystem, 'showSaveDialog').andCallFake(function (dialogTitle, initialPath, proposedNewName, callback) { callback(undefined, newFilePath); }); @@ -262,7 +263,7 @@ define(function (require, exports, module) { return {done: function (callback) { callback(Dialogs.DIALOG_BTN_OK); } }; }); - spyOn(testWindow.brackets.fs, 'showSaveDialog').andCallFake(function (dialogTitle, initialPath, proposedNewName, callback) { + spyOn(FileSystem, 'showSaveDialog').andCallFake(function (dialogTitle, initialPath, proposedNewName, callback) { callback(undefined, newFilePath); }); @@ -325,7 +326,7 @@ define(function (require, exports, module) { return {done: function (callback) { callback(Dialogs.DIALOG_BTN_OK); } }; }); - spyOn(testWindow.brackets.fs, 'showSaveDialog').andCallFake(function (dialogTitle, initialPath, proposedNewName, callback) { + spyOn(FileSystem, 'showSaveDialog').andCallFake(function (dialogTitle, initialPath, proposedNewName, callback) { callback(undefined, ""); // "" means cancel }); @@ -428,7 +429,7 @@ define(function (require, exports, module) { runs(function () { var fileI = 0; - spyOn(testWindow.brackets.fs, 'showSaveDialog').andCallFake(function (dialogTitle, initialPath, proposedNewName, callback) { + spyOn(FileSystem, 'showSaveDialog').andCallFake(function (dialogTitle, initialPath, proposedNewName, callback) { callback(undefined, getFilename(fileI)); fileI++; }); @@ -468,7 +469,7 @@ define(function (require, exports, module) { }); var fileI = 0; - spyOn(testWindow.brackets.fs, 'showSaveDialog').andCallFake(function (dialogTitle, initialPath, proposedNewName, callback) { + spyOn(FileSystem, 'showSaveDialog').andCallFake(function (dialogTitle, initialPath, proposedNewName, callback) { callback(undefined, getFilename(fileI)); fileI++; }); @@ -496,7 +497,7 @@ define(function (require, exports, module) { runs(function () { var fileI = 0; - spyOn(testWindow.brackets.fs, 'showSaveDialog').andCallFake(function (dialogTitle, initialPath, proposedNewName, callback) { + spyOn(FileSystem, 'showSaveDialog').andCallFake(function (dialogTitle, initialPath, proposedNewName, callback) { if (fileI === 0) { // save first file callback(undefined, getFilename(fileI)); @@ -551,7 +552,7 @@ define(function (require, exports, module) { }); var fileI = 0; - spyOn(testWindow.brackets.fs, 'showSaveDialog').andCallFake(function (dialogTitle, initialPath, proposedNewName, callback) { + spyOn(FileSystem, 'showSaveDialog').andCallFake(function (dialogTitle, initialPath, proposedNewName, callback) { if (fileI === 0) { // save first file callback(undefined, getFilename(fileI)); @@ -663,7 +664,7 @@ define(function (require, exports, module) { // confirm file contents var actualContent = null, error = -1; runs(function () { - promise = FileUtils.readAsText(new NativeFileSystem.FileEntry(filePath)) + promise = FileUtils.readAsText(FileSystem.getFileForPath(filePath)) .done(function (actualText) { expect(actualText).toBe(TEST_JS_NEW_CONTENT); }); @@ -672,7 +673,7 @@ define(function (require, exports, module) { // reset file contents runs(function () { - promise = FileUtils.writeText(new NativeFileSystem.FileEntry(filePath), TEST_JS_CONTENT); + promise = FileUtils.writeText(FileSystem.getFileForPath(filePath), TEST_JS_CONTENT); waitsForDone(promise, "Revert test file"); }); }); @@ -687,11 +688,11 @@ define(function (require, exports, module) { // create test files (Git rewrites line endings, so these can't be kept in src control) runs(function () { - promise = FileUtils.writeText(new NativeFileSystem.FileEntry(crlfPath), crlfText); + promise = FileUtils.writeText(FileSystem.getFileForPath(crlfPath), crlfText); waitsForDone(promise, "Create CRLF test file"); }); runs(function () { - promise = FileUtils.writeText(new NativeFileSystem.FileEntry(lfPath), lfText); + promise = FileUtils.writeText(FileSystem.getFileForPath(lfPath), lfText); waitsForDone(promise, "Create LF test file"); }); @@ -721,7 +722,7 @@ define(function (require, exports, module) { // verify file contents runs(function () { - promise = FileUtils.readAsText(new NativeFileSystem.FileEntry(crlfPath)) + promise = FileUtils.readAsText(FileSystem.getFileForPath(crlfPath)) .done(function (actualText) { expect(actualText).toBe(crlfText.replace("line2", "line2a\r\nline2b")); }); @@ -729,7 +730,7 @@ define(function (require, exports, module) { }); runs(function () { - promise = FileUtils.readAsText(new NativeFileSystem.FileEntry(lfPath)) + promise = FileUtils.readAsText(FileSystem.getFileForPath(lfPath)) .done(function (actualText) { expect(actualText).toBe(lfText.replace("line2", "line2a\nline2b")); }); @@ -772,7 +773,7 @@ define(function (require, exports, module) { }); runs(function () { - spyOn(testWindow.brackets.fs, 'showSaveDialog').andCallFake(function (dialogTitle, initialPath, proposedNewName, callback) { + spyOn(FileSystem, 'showSaveDialog').andCallFake(function (dialogTitle, initialPath, proposedNewName, callback) { callback(undefined, newFilePath); }); @@ -810,7 +811,7 @@ define(function (require, exports, module) { }); runs(function () { - spyOn(testWindow.brackets.fs, 'showSaveDialog').andCallFake(function (dialogTitle, initialPath, proposedNewName, callback) { + spyOn(FileSystem, 'showSaveDialog').andCallFake(function (dialogTitle, initialPath, proposedNewName, callback) { callback(undefined, newFilePath); }); @@ -846,7 +847,7 @@ define(function (require, exports, module) { }); runs(function () { - spyOn(testWindow.brackets.fs, 'showSaveDialog').andCallFake(function (dialogTitle, initialPath, proposedNewName, callback) { + spyOn(FileSystem, 'showSaveDialog').andCallFake(function (dialogTitle, initialPath, proposedNewName, callback) { callback("Error", undefined); }); @@ -891,7 +892,7 @@ define(function (require, exports, module) { // save the file opened above to a different filename DocumentManager.setCurrentDocument(targetDoc); - spyOn(testWindow.brackets.fs, 'showSaveDialog').andCallFake(function (dialogTitle, initialPath, proposedNewName, callback) { + spyOn(FileSystem, 'showSaveDialog').andCallFake(function (dialogTitle, initialPath, proposedNewName, callback) { callback(undefined, newFilePath); }); diff --git a/test/spec/ExtensionInstallation-test.js b/test/spec/ExtensionInstallation-test.js index ee8b09ad7a6..32a5d4db23d 100644 --- a/test/spec/ExtensionInstallation-test.js +++ b/test/spec/ExtensionInstallation-test.js @@ -32,7 +32,7 @@ define(function (require, exports, module) { var SpecRunnerUtils = require("spec/SpecRunnerUtils"), ExtensionLoader = require("utils/ExtensionLoader"), - NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, + FileSystem = require("filesystem/FileSystem"), Package = require("extensibility/Package"), NodeConnection = require("utils/NodeConnection"); @@ -159,14 +159,14 @@ define(function (require, exports, module) { var expectedPath = mockGetUserExtensionPath() + "/basic-valid-extension"; expect(lastExtensionLoad.config.baseUrl).toEqual(expectedPath); expect(lastExtensionLoad.entryPoint).toEqual("main"); - NativeFileSystem.resolveNativeFileSystemPath(extensionsRoot + "/user/basic-valid-extension/main.js", - function () { + FileSystem.resolve(extensionsRoot + "/user/basic-valid-extension/main.js", function (err, item) { + if (!err) { mainCheckComplete = true; - }, - function () { + } else { mainCheckComplete = true; expect("basic-valid-extension directory and main.js to exist").toEqual(true); - }); + } + }); }); waitsFor(function () { return mainCheckComplete; }, 1000, "checking for main.js file"); @@ -182,14 +182,14 @@ define(function (require, exports, module) { expect(packageData.disabledReason).toBeTruthy(); expect(packageData.name).toEqual("incompatible-version"); expect(lastExtensionLoad).toEqual({}); - NativeFileSystem.resolveNativeFileSystemPath(extensionsRoot + "/disabled/incompatible-version", - function () { + FileSystem.resolve(extensionsRoot + "/disabled/incompatible-version", function (err, item) { + if (!err) { directoryCheckComplete = true; - }, - function () { + } else { directoryCheckComplete = true; expect("incompatible-version path to exist in the disabled directory").toEqual(true); - }); + } + }); waitsFor(function () { return directoryCheckComplete; }, 1000, "checking for disabled extension directory"); @@ -204,14 +204,14 @@ define(function (require, exports, module) { handlePackage(installPath, Package.remove); }); runs(function () { - NativeFileSystem.resolveNativeFileSystemPath(installPath, - function () { + FileSystem.resolve(installPath, function (err, item) { + if (!err) { checkComplete = true; expect("installation path was removed").toEqual(true); - }, - function () { + } else { checkComplete = true; - }); + } + }); waitsFor(function () { return checkComplete; }, 1000, "checking for extension folder removal"); }); diff --git a/test/spec/ExtensionManager-test.js b/test/spec/ExtensionManager-test.js index 158d56e820e..0fdbaccfb95 100644 --- a/test/spec/ExtensionManager-test.js +++ b/test/spec/ExtensionManager-test.js @@ -42,13 +42,12 @@ define(function (require, exports, module) { InstallExtensionDialog = require("extensibility/InstallExtensionDialog"), Package = require("extensibility/Package"), ExtensionLoader = require("utils/ExtensionLoader"), - NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, - NativeFileError = require("file/NativeFileError"), SpecRunnerUtils = require("spec/SpecRunnerUtils"), NativeApp = require("utils/NativeApp"), Dialogs = require("widgets/Dialogs"), CommandManager = require("command/CommandManager"), Commands = require("command/Commands"), + FileSystem = require("filesystem/FileSystem"), Strings = require("strings"), StringUtils = require("utils/StringUtils"), mockRegistryText = require("text!spec/ExtensionManager-test-files/mockRegistry.json"), @@ -609,6 +608,7 @@ define(function (require, exports, module) { it("should unmark an extension for update, deleting the package and raising an event", function () { var id = "registered-extension", filename = "/path/to/downloaded/file.zip", + file = FileSystem.getFileForPath(filename), calledId; runs(function () { $(model).on("change", function (e, id) { @@ -620,10 +620,10 @@ define(function (require, exports, module) { installationStatus: "NEEDS_UPDATE" }); calledId = null; - spyOn(brackets.fs, "unlink"); + spyOn(file, "unlink"); ExtensionManager.removeUpdate(id); expect(calledId).toBe(id); - expect(brackets.fs.unlink).toHaveBeenCalledWith(filename, jasmine.any(Function)); + expect(file.unlink).toHaveBeenCalled(); expect(ExtensionManager.isMarkedForUpdate()).toBe(false); }); }); @@ -652,7 +652,8 @@ define(function (require, exports, module) { it("should update extensions marked for update", function () { var id = "registered-extension", - filename = "/path/to/downloaded/file.zip"; + filename = "/path/to/downloaded/file.zip", + file = FileSystem.getFileForPath("/path/to/downloaded/file.zip"); runs(function () { ExtensionManager.updateFromDownload({ localPath: filename, @@ -660,14 +661,14 @@ define(function (require, exports, module) { installationStatus: "NEEDS_UPDATE" }); expect(ExtensionManager.isMarkedForUpdate()).toBe(false); - spyOn(brackets.fs, "unlink"); + spyOn(file, "unlink"); var d = $.Deferred(); spyOn(Package, "installUpdate").andReturn(d.promise()); d.resolve(); waitsForDone(ExtensionManager.updateExtensions()); }); runs(function () { - expect(brackets.fs.unlink).not.toHaveBeenCalled(); + expect(file.unlink).not.toHaveBeenCalled(); expect(Package.installUpdate).toHaveBeenCalledWith(filename, id); }); }); @@ -1232,7 +1233,8 @@ define(function (require, exports, module) { it("should undo marking an extension for update", function () { var id = "mock-extension-3", - filename = "/path/to/downloaded/file.zip"; + filename = "/path/to/downloaded/file.zip", + file = FileSystem.getFileForPath(filename); mockLoadExtensions(["user/" + id]); setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel); runs(function () { @@ -1241,11 +1243,11 @@ define(function (require, exports, module) { installationStatus: "NEEDS_UPDATE", localPath: filename }); - spyOn(brackets.fs, "unlink"); + spyOn(file, "unlink"); var $undoLink = $("a.undo-update[data-extension-id=" + id + "]", view.$el); $undoLink.click(); expect(ExtensionManager.isMarkedForUpdate(id)).toBe(false); - expect(brackets.fs.unlink).toHaveBeenCalledWith(filename, jasmine.any(Function)); + expect(file.unlink).toHaveBeenCalled(); var $button = $("button.remove[data-extension-id=" + id + "]", view.$el); expect($button.length).toBe(1); }); @@ -1386,7 +1388,8 @@ define(function (require, exports, module) { it("should not update extensions or quit if the user hits Cancel on the confirmation dialog", function () { var id = "mock-extension-3", - filename = "/path/to/downloaded/file.zip"; + filename = "/path/to/downloaded/file.zip", + file = FileSystem.getFileForPath(filename); mockLoadExtensions(["user/" + id]); setupViewWithMockData(ExtensionManagerViewModel.InstalledViewModel); runs(function () { @@ -1396,14 +1399,14 @@ define(function (require, exports, module) { installationStatus: Package.InstallationStatuses.NEEDS_UPDATE }); expect(ExtensionManager.isMarkedForUpdate(id)).toBe(true); - spyOn(brackets.fs, "unlink"); + spyOn(file, "unlink"); // Don't expect the model to be disposed until after the dialog is dismissed. ExtensionManagerDialog._performChanges(); dialogDeferred.resolve("cancel"); expect(removedPath).toBeFalsy(); expect(ExtensionManager.isMarkedForUpdate("mock-extension-3")).toBe(false); expect(didQuit).toBe(false); - expect(brackets.fs.unlink).toHaveBeenCalledWith(filename, jasmine.any(Function)); + expect(file.unlink).toHaveBeenCalled(); }); }); diff --git a/test/spec/FileIndexManager-test-files/dir1/dir2/file_eight.css b/test/spec/FileIndexManager-test-files/dir1/dir2/file_eight.css deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/test/spec/FileIndexManager-test-files/dir1/dir2/file_seven.js b/test/spec/FileIndexManager-test-files/dir1/dir2/file_seven.js deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/test/spec/FileIndexManager-test-files/dir1/file_six.js b/test/spec/FileIndexManager-test-files/dir1/file_six.js deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/test/spec/FileIndexManager-test-files/file_five.css b/test/spec/FileIndexManager-test-files/file_five.css deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/test/spec/FileIndexManager-test-files/file_four.css b/test/spec/FileIndexManager-test-files/file_four.css deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/test/spec/FileIndexManager-test-files/file_one.js b/test/spec/FileIndexManager-test-files/file_one.js deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/test/spec/FileIndexManager-test-files/file_three.js b/test/spec/FileIndexManager-test-files/file_three.js deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/test/spec/FileIndexManager-test-files/file_two.js b/test/spec/FileIndexManager-test-files/file_two.js deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/test/spec/FileIndexManager-test.js b/test/spec/FileIndexManager-test.js deleted file mode 100644 index afd1dd2c169..00000000000 --- a/test/spec/FileIndexManager-test.js +++ /dev/null @@ -1,467 +0,0 @@ -/* - * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved. - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - * - */ - - -/*jslint vars: true, plusplus: true, devel: true, browser: true, nomen: true, indent: 4, maxerr: 50 */ -/*global define, describe, brackets, beforeEach, afterEach, it, runs, waitsFor, waitsForDone, expect, spyOn, jasmine, beforeFirst, afterLast */ - -define(function (require, exports, module) { - 'use strict'; - - // Load dependent modules - var SpecRunnerUtils = require("spec/SpecRunnerUtils"); - - describe("FileIndexManager", function () { - - describe("unit tests", function () { - var ProjectManager = require("project/ProjectManager"), - FileIndexManager = require("project/FileIndexManager"), - curProjectRoot; - - beforeEach(function () { - curProjectRoot = null; - spyOn(ProjectManager, "shouldShow").andCallFake(function () { - return true; - }); - spyOn(ProjectManager, "getProjectRoot").andCallFake(function () { - return curProjectRoot; - }); - }); - - function makeFakeProjectDirectory(fakePath, fakeReadEntries) { - var readEntriesSpy = jasmine.createSpy().andCallFake(fakeReadEntries); - return { - fullPath: fakePath, - createReader: function () { - return { - readEntries: readEntriesSpy - }; - }, - readEntriesSpy: readEntriesSpy - }; - } - - it("should abort a running scan and start a new one if another scan is requested while dirty", function () { - var firstCB, secondCB, firstFileInfoResult, secondFileInfoResult, - firstProject = makeFakeProjectDirectory("fakeProject1", function (cb) { - firstCB = cb; - }), - secondProject = makeFakeProjectDirectory("fakeProject2", function (cb) { - secondCB = cb; - }); - - curProjectRoot = firstProject; - - // Ensure it's dirty to begin with - FileIndexManager.markDirty(); - - // Request file infos for the first project. This should start a scan. - FileIndexManager.getFileInfoList("all").done(function (infos) { - firstFileInfoResult = infos; - }); - - // "Switch" to a new project - curProjectRoot = secondProject; - - // Mark it dirty again and start a second file info request before the first one has "read" its folder - FileIndexManager.markDirty(); - FileIndexManager.getFileInfoList("all").done(function (infos) { - secondFileInfoResult = infos; - }); - - // "Complete" the first scan's read request - firstCB([{ isFile: true, name: "test1FirstProjectFile.js", fullPath: "test1FirstProjectFile.js" }]); - - // Since the first scan was aborted, we shouldn't have gotten any result for it. - expect(firstFileInfoResult).toBeUndefined(); - - // "Complete" the second scan's read request - secondCB([{ isFile: true, name: "test1SecondProjectFile.js", fullPath: "test1SecondProjectFile.js" }]); - - // Now both callers should have received the info from the second project. - expect(firstFileInfoResult.length).toBe(1); - expect(firstFileInfoResult[0].name).toEqual("test1SecondProjectFile.js"); - expect(secondFileInfoResult.length).toBe(1); - expect(secondFileInfoResult[0].name).toEqual("test1SecondProjectFile.js"); - - // Each readEntries should only have been called once. - expect(firstProject.readEntriesSpy.callCount).toBe(1); - expect(secondProject.readEntriesSpy.callCount).toBe(1); - }); - - it("should abort a running scan and start a new one when marked dirty (even before another scan is requested)", function () { - var firstCB, secondCB, firstFileInfoResult, - firstProject = makeFakeProjectDirectory("fakeProject1", function (cb) { - firstCB = cb; - }), - secondProject = makeFakeProjectDirectory("fakeProject2", function (cb) { - secondCB = cb; - }); - - curProjectRoot = firstProject; - - // Ensure it's dirty to begin with - FileIndexManager.markDirty(); - - // Request file infos for the first project. This should start a scan. - FileIndexManager.getFileInfoList("all").done(function (infos) { - firstFileInfoResult = infos; - }); - - // "Switch" to a new project - curProjectRoot = secondProject; - - // Mark it dirty again without making a new request. This should still cause the scan to restart. - FileIndexManager.markDirty(); - - // "Complete" the first scan's read request - firstCB([{ isFile: true, name: "test2FirstProjectFile.js", fullPath: "test2FirstProjectFile.js" }]); - - // Since the first scan was aborted, we shouldn't have gotten any result for it. - expect(firstFileInfoResult).toBeUndefined(); - - // "Complete" the second scan's read request - secondCB([{ isFile: true, name: "test2SecondProjectFile.js", fullPath: "test2SecondProjectFile.js" }]); - - // Now the initial caller should have received the info from the second project. - expect(firstFileInfoResult.length).toBe(1); - expect(firstFileInfoResult[0].name).toEqual("test2SecondProjectFile.js"); - - // Each readEntries should only have been called once. - expect(firstProject.readEntriesSpy.callCount).toBe(1); - expect(secondProject.readEntriesSpy.callCount).toBe(1); - }); - - it("should not start a new scan if another scan is requested while not dirty", function () { - var firstCB, secondCB, firstFileInfoResult, secondFileInfoResult; - curProjectRoot = makeFakeProjectDirectory("fakeProject1", function (cb) { - firstCB = cb; - }); - - // Ensure it's dirty to begin with - FileIndexManager.markDirty(); - - // Start the first file info request - FileIndexManager.getFileInfoList("all").done(function (infos) { - firstFileInfoResult = infos; - }); - - // The first scan should have requested the project root - expect(ProjectManager.getProjectRoot).toHaveBeenCalled(); - expect(ProjectManager.getProjectRoot.callCount).toBe(1); - - // Start a second file info request without marking dirty or changing the project root - FileIndexManager.getFileInfoList("all").done(function (infos) { - secondFileInfoResult = infos; - }); - - // We shouldn't have started a second scan, so the project root should not have been requested again. - expect(ProjectManager.getProjectRoot.callCount).toBe(1); - expect(curProjectRoot.readEntriesSpy.callCount).toBe(1); - - // "Complete" the scan's read request - firstCB([{ isFile: true, name: "test3ProjectFile.js", fullPath: "test3ProjectFile.js" }]); - - // Both callers should have received the info from the first project. - expect(firstFileInfoResult.length).toBe(1); - expect(firstFileInfoResult[0].name).toEqual("test3ProjectFile.js"); - expect(secondFileInfoResult.length).toBe(1); - expect(secondFileInfoResult[0].name).toEqual("test3ProjectFile.js"); - }); - }); - - describe("integration tests", function () { - - this.category = "integration"; - - var testPath = SpecRunnerUtils.getTestPath("/spec/FileIndexManager-test-files"), - FileIndexManager, - ProjectManager; - - function createTestWindow(spec) { - SpecRunnerUtils.createTestWindowAndRun(spec, function (testWindow) { - // Load module instances from brackets.test - FileIndexManager = testWindow.brackets.test.FileIndexManager; - ProjectManager = testWindow.brackets.test.ProjectManager; - }); - } - - function closeTestWindow() { - FileIndexManager = null; - ProjectManager = null; - SpecRunnerUtils.closeTestWindow(); - } - - describe("multiple requests", function () { - beforeEach(function () { - createTestWindow(this); - - // Load spec/FileIndexManager-test-files - SpecRunnerUtils.loadProjectInTestWindow(testPath); - }); - - afterEach(closeTestWindow); - - it("should handle identical simultaneous requests without doing extra work", function () { // #330 - var projectRoot, - allFiles1, - allFiles2; - - runs(function () { - projectRoot = ProjectManager.getProjectRoot(); - spyOn(projectRoot, "createReader").andCallThrough(); - - // Kick off two index requests in parallel - var promise1 = FileIndexManager.getFileInfoList("all") - .done(function (result) { - allFiles1 = result; - }); - var promise2 = FileIndexManager.getFileInfoList("all") // same request again - .done(function (result) { - allFiles2 = result; - }); - - waitsForDone(promise1, "First FileIndexManager.getFileInfoList()"); - waitsForDone(promise2, "Second FileIndexManager.getFileInfoList()"); - }); - - runs(function () { - // Correct response to both promises - expect(allFiles1.length).toEqual(8); - expect(allFiles2.length).toEqual(8); - - // Didn't scan project tree twice - expect(projectRoot.createReader.callCount).toBe(1); - }); - }); - - it("should handle differing simultaneous requests without doing extra work", function () { // #330 - var projectRoot, - allFiles1, - allFiles2; - - runs(function () { - projectRoot = ProjectManager.getProjectRoot(); - spyOn(projectRoot, "createReader").andCallThrough(); - - // Kick off two index requests in parallel - var promise1 = FileIndexManager.getFileInfoList("all") - .done(function (result) { - allFiles1 = result; - }); - var promise2 = FileIndexManager.getFileInfoList("css") // different request in parallel - .done(function (result) { - allFiles2 = result; - }); - - waitsForDone(promise1, "First FileIndexManager.getFileInfoList()"); - waitsForDone(promise2, "Second FileIndexManager.getFileInfoList()"); - }); - - runs(function () { - // Correct response to both promises - expect(allFiles1.length).toEqual(8); - expect(allFiles2.length).toEqual(3); - - // Didn't scan project tree twice - expect(projectRoot.createReader.callCount).toBe(1); - }); - }); - }); - - describe("ProjectManager integration", function () { - - beforeFirst(function () { - createTestWindow(this); - }); - - afterLast(closeTestWindow); - - beforeEach(function () { - // Load spec/FileIndexManager-test-files - SpecRunnerUtils.loadProjectInTestWindow(testPath); - }); - - it("should index files in directory", function () { - var allFiles, cssFiles; - runs(function () { - FileIndexManager.getFileInfoList("all") - .done(function (result) { - allFiles = result; - }); - }); - waitsFor(function () { return allFiles; }, "FileIndexManager.getFileInfoList() timeout", 1000); - - runs(function () { - FileIndexManager.getFileInfoList("css") - .done(function (result) { - cssFiles = result; - }); - }); - waitsFor(function () { return cssFiles; }, "FileIndexManager.getFileInfoList() timeout", 1000); - - runs(function () { - expect(allFiles.length).toEqual(8); - expect(cssFiles.length).toEqual(3); - }); - - }); - - it("should match a specific filename and return the correct FileInfo", function () { - var fileList; - - runs(function () { - FileIndexManager.getFilenameMatches("all", "file_four.css") - .done(function (results) { - fileList = results; - }); - }); - - waitsFor(function () { return fileList; }, 1000); - - runs(function () { - expect(fileList.length).toEqual(1); - expect(fileList[0].name).toEqual("file_four.css"); - expect(fileList[0].fullPath).toEqual(testPath + "/file_four.css"); - }); - }); - - it("should update the indicies on project change", function () { - var allFiles; - runs(function () { - FileIndexManager.getFileInfoList("all") - .done(function (result) { - allFiles = result; - }); - }); - - waitsFor(function () { return allFiles; }, "FileIndexManager.getFileInfoList() timeout", 1000); - - runs(function () { - expect(allFiles.length).toEqual(8); - }); - - // load a subfolder in the test project - // spec/FileIndexManager-test-files/dir1/dir2 - SpecRunnerUtils.loadProjectInTestWindow(testPath + "/dir1/dir2/"); - - var dir2Files; - runs(function () { - FileIndexManager.getFileInfoList("all") - .done(function (result) { - dir2Files = result; - }); - }); - - waitsFor(function () { return dir2Files; }, "FileIndexManager.getFileInfoList() timeout", 1000); - - runs(function () { - expect(dir2Files.length).toEqual(2); - expect(dir2Files[0].name).toEqual("file_eight.css"); - expect(dir2Files[1].name).toEqual("file_seven.js"); - }); - }); - - it("should update the indicies after being marked dirty", function () { - var allFiles; // set by checkAllFileCount - - // helper function to validate base state of 8 files - function checkAllFileCount(fileCount) { - var files; - runs(function () { - FileIndexManager.getFileInfoList("all") - .done(function (result) { - files = result; - }); - }); - - waitsFor(function () { return files; }, "FileIndexManager.getFileInfoList() timeout", 1000); - - runs(function () { - allFiles = files; - expect(files.length).toEqual(fileCount); - }); - } - - // verify 8 files in base state - checkAllFileCount(8); - - // add a temporary file to the folder - var entry; - - // create a 9th file - runs(function () { - var root = ProjectManager.getProjectRoot(); - root.getFile("new-file.txt", - { create: true, exclusive: true }, - function (fileEntry) { entry = fileEntry; }); - }); - - waitsFor(function () { return entry; }, "getFile() timeout", 1000); - - runs(function () { - // mark FileIndexManager dirty after new file was created - FileIndexManager.markDirty(); - }); - - // verify 9 files - checkAllFileCount(9); - - var cleanupComplete = false; - - // verify the new file was added to the "all" index - runs(function () { - var filtered = allFiles.filter(function (value) { - return (value.name === "new-file.txt"); - }); - expect(filtered.length).toEqual(1); - - // remove the 9th file - brackets.fs.unlink(entry.fullPath, function (err) { - cleanupComplete = (err === brackets.fs.NO_ERROR); - }); - }); - - // wait for the file to be deleted - waitsFor(function () { return cleanupComplete; }, 1000); - - runs(function () { - // mark FileIndexManager dirty after new file was deleted - FileIndexManager.markDirty(); - }); - - // verify that we're back to 8 files - checkAllFileCount(8); - - // make sure the 9th file was removed from the index - runs(function () { - var filtered = allFiles.filter(function (value) { - return (value.name === "new-file.txt"); - }); - expect(filtered.length).toEqual(0); - }); - }); - }); - }); - }); -}); diff --git a/test/spec/FileSystem-test.js b/test/spec/FileSystem-test.js new file mode 100644 index 00000000000..426db24033e --- /dev/null +++ b/test/spec/FileSystem-test.js @@ -0,0 +1,798 @@ +/* + * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + +/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ +/*global define, describe, it, expect, beforeEach, afterEach, waits, waitsFor, runs, $, window */ + +define(function (require, exports, module) { + "use strict"; + + var Directory = require("filesystem/Directory"), + File = require("filesystem/File"), + FileSystem = require("filesystem/FileSystem"), + FileSystemError = require("filesystem/FileSystemError"), + MockFileSystemImpl = require("./MockFileSystemImpl"); + + describe("FileSystem", function () { + + // Setup + + var fileSystem; + + beforeEach(function () { + // Create an FS instance for testing + MockFileSystemImpl.reset(); + fileSystem = new FileSystem._FileSystem(); + fileSystem.init(MockFileSystemImpl); + fileSystem.watch(fileSystem.getDirectoryForPath("/"), function () {return true; }, function () {}); + }); + + // Callback factories + function resolveCallback() { + var callback = function (err, entry) { + callback.error = err; + callback.entry = entry; + callback.wasCalled = true; + }; + return callback; + } + + function errorCallback() { + var callback = function (err) { + callback.error = err; + callback.wasCalled = true; + }; + return callback; + } + + function readCallback() { + var callback = function (err, data, stat) { + callback.error = err; + callback.data = data; + callback.stat = stat; + callback.wasCalled = true; + }; + return callback; + } + + // Utilities + + /** Pass this to when() as the 'callback' or 'notify' value to delay invoking the callback or change handler by a fixed amount of time */ + function delay(ms) { + function generateCbWrapper(cb) { + function cbReadyToRun() { + var _args = arguments; + window.setTimeout(function () { + cb.apply(null, _args); + }, ms); + } + return cbReadyToRun; + } + return generateCbWrapper; + } + + function getContentsCallback() { + var callback = function (err, contents) { + callback.error = err; + callback.contents = contents; + callback.wasCalled = true; + }; + return callback; + } + + describe("Path normalization", function () { + // Auto-prepended to both origPath & normPath in all the test helpers below + var prefix = ""; + + function expectNormFile(origPath, normPath) { + var file = fileSystem.getFileForPath(prefix + origPath); + expect(file.fullPath).toBe(prefix + normPath); + } + function expectNormDir(origPath, normPath) { + var dir = fileSystem.getDirectoryForPath(prefix + origPath); + expect(dir.fullPath).toBe(prefix + normPath); + } + function expectInvalidFile(origPath) { + function tryToMakeFile() { + return fileSystem.getFileForPath(prefix + origPath); + } + expect(tryToMakeFile).toThrow(); + } + + // Runs all the tests N times, once with each prefix + function testPrefixes(prefixes, tests) { + prefixes.forEach(function (pre) { + prefix = pre; + tests(); + }); + prefix = ""; + } + + it("should ensure trailing slash on directory paths", function () { + testPrefixes(["", "c:"], function () { + expectNormDir("/foo", "/foo/"); + expectNormDir("/foo/bar", "/foo/bar/"); + + // Paths *with* trailing slash should be unaffected + expectNormDir("/", "/"); + expectNormDir("/foo/", "/foo/"); + expectNormDir("/foo/bar/", "/foo/bar/"); + }); + }); + + it("should eliminate duplicated (contiguous) slashes", function () { + testPrefixes(["", "c:"], function () { + expectNormDir("//", "/"); + expectNormDir("///", "/"); + expectNormDir("//foo", "/foo/"); + expectNormDir("/foo//", "/foo/"); + expectNormDir("//foo//", "/foo/"); + expectNormDir("///foo///", "/foo/"); + expectNormDir("/foo//bar", "/foo/bar/"); + expectNormDir("/foo///bar", "/foo/bar/"); + + expectNormFile("//foo", "/foo"); + expectNormFile("///foo", "/foo"); + expectNormFile("/foo//bar", "/foo/bar"); + expectNormFile("/foo///bar", "/foo/bar"); + expectNormFile("//foo///bar", "/foo/bar"); + expectNormFile("///foo///bar", "/foo/bar"); + expectNormFile("///foo//bar", "/foo/bar"); + expectNormFile("///foo/bar", "/foo/bar"); + }); + }); + + it("should normalize out '..' segments", function () { + testPrefixes(["", "c:"], function () { + expectNormDir("/foo/..", "/"); + expectNormDir("/foo/bar/..", "/foo/"); + expectNormDir("/foo/../bar", "/bar/"); + expectNormDir("/foo//../bar", "/bar/"); // even with duplicated "/"es + expectNormDir("/foo/..//bar", "/bar/"); // even with duplicated "/"es + expectNormDir("/foo/one/two/three/../../../bar", "/foo/bar/"); + expectNormDir("/foo/one/two/../two/three", "/foo/one/two/three/"); + expectNormDir("/foo/one/two/../three/../bar", "/foo/one/bar/"); + + expectNormFile("/foo/../bar", "/bar"); + expectNormFile("/foo//../bar", "/bar"); // even with duplicated "/"es + expectNormFile("/foo/..//bar", "/bar"); // even with duplicated "/"es + expectNormFile("/foo/one/two/three/../../../bar", "/foo/bar"); + expectNormFile("/foo/one/two/../two/three", "/foo/one/two/three"); + expectNormFile("/foo/one/two/../three/../bar", "/foo/one/bar"); + + // Can't go back past root + expectInvalidFile("/.."); + expectInvalidFile("/../"); + expectInvalidFile("/foo/../../bar"); + expectInvalidFile("/foo/../bar/../.."); + }); + }); + }); + + describe("parent and name properties", function () { + it("should have a name property", function () { + var file = fileSystem.getFileForPath("/subdir/file3.txt"), + directory = fileSystem.getDirectoryForPath("/subdir/foo/"); + + expect(file.name).toBe("file3.txt"); + expect(directory.name).toBe("foo"); + }); + it("should have a parentPath property", function () { + var file = fileSystem.getFileForPath("/subdir/file3.txt"), + directory = fileSystem.getDirectoryForPath("/subdir/foo/"); + + expect(file.parentPath).toBe("/subdir/"); + expect(directory.parentPath).toBe("/subdir/"); + }); + }); + + describe("Singleton enforcement", function () { + it("should return the same File object for the same path", function () { + var cb = resolveCallback(); + expect(fileSystem.getFileForPath("/file1.txt")).toEqual(fileSystem.getFileForPath("/file1.txt")); + expect(fileSystem.getFileForPath("/file1.txt")).not.toEqual(fileSystem.getFileForPath("/file2.txt")); + runs(function () { + fileSystem.resolve("/file1.txt", cb); + }); + waitsFor(function () { return cb.wasCalled; }); + runs(function () { + expect(cb.error).toBeFalsy(); + expect(fileSystem.getFileForPath("/file1.txt")).toEqual(cb.entry); + }); + }); + + it("should return the same Directory object for the same path", function () { + var cb = resolveCallback(); + expect(fileSystem.getDirectoryForPath("/subdir/")).toEqual(fileSystem.getFileForPath("/subdir/")); + expect(fileSystem.getDirectoryForPath("/subdir/")).not.toEqual(fileSystem.getFileForPath("/subdir2/")); + runs(function () { + fileSystem.resolve("/subdir/", cb); + }); + waitsFor(function () { return cb.wasCalled; }); + runs(function () { + expect(cb.error).toBeFalsy(); + expect(fileSystem.getDirectoryForPath("/subdir/")).toEqual(cb.entry); + }); + }); + }); + + describe("Resolve", function () { + function testResolve(path, expectedError, expectedType) { + var cb = resolveCallback(); + runs(function () { + fileSystem.resolve(path, cb); + }); + waitsFor(function () { return cb.wasCalled; }); + runs(function () { + if (!expectedError) { + expect(cb.error).toBeFalsy(); + } else { + expect(cb.error).toBe(expectedError); + } + if (expectedType) { + expect(cb.entry instanceof expectedType).toBeTruthy(); + } + }); + } + + it("should resolve a File", function () { + testResolve("/subdir/file3.txt", null, File); + }); + it("should resolve a Directory", function () { + testResolve("/subdir/", null, Directory); + }); + it("should return an error if the File/Directory is not found", function () { + testResolve("/doesnt-exist.txt", FileSystemError.NOT_FOUND); + testResolve("/doesnt-exist/", FileSystemError.NOT_FOUND); + }); + }); + + describe("Rename", function () { + it("should rename a File", function () { + var file = fileSystem.getFileForPath("/file1.txt"), + cb = errorCallback(); + + runs(function () { + file.rename("/file1-renamed.txt", cb); + }); + waitsFor(function () { return cb.wasCalled; }); + runs(function () { + expect(cb.error).toBeFalsy(); + expect(file.fullPath).toBe("/file1-renamed.txt"); + expect(fileSystem.getFileForPath("/file1-renamed.txt")).toBe(file); + expect(fileSystem.getFileForPath("/file1.txt")).not.toBe(file); + }); + }); + + it("should fail if the file doesn't exist", function () { + var file = fileSystem.getFileForPath("/doesnt-exist.txt"), + cb = errorCallback(); + + runs(function () { + file.rename("foo.txt", cb); + }); + waitsFor(function () { return cb.wasCalled; }); + runs(function () { + expect(cb.error).toBe(FileSystemError.NOT_FOUND); + }); + }); + + it("should fail if the new name already exists", function () { + var file = fileSystem.getFileForPath("/file1.txt"), + cb = errorCallback(); + + runs(function () { + file.rename("/file2.txt", cb); + }); + waitsFor(function () { return cb.wasCalled; }); + runs(function () { + expect(cb.error).toBe(FileSystemError.ALREADY_EXISTS); + }); + }); + + it("should rename a Directory", function () { + var directory = fileSystem.getDirectoryForPath("/subdir/"), + file = fileSystem.getFileForPath("/subdir/file3.txt"), + cb = errorCallback(); + + runs(function () { + directory.rename("/subdir-renamed/", cb); + }); + waitsFor(function () { return cb.wasCalled; }); + runs(function () { + expect(cb.error).toBeFalsy(); + expect(directory.fullPath).toBe("/subdir-renamed/"); + expect(file.fullPath).toBe("/subdir-renamed/file3.txt"); + }); + }); + }); + + + describe("Read directory", function () { + it("should read a Directory", function () { + var directory = fileSystem.getDirectoryForPath("/subdir/"), + cb = getContentsCallback(); + + runs(function () { + directory.getContents(cb); + }); + waitsFor(function () { return cb.wasCalled; }); + runs(function () { + expect(cb.error).toBeFalsy(); + expect(cb.contents.length).toBe(2); + expect(cb.contents[0].fullPath).toBe("/subdir/file3.txt"); + }); + }); + + it("should return an error if the Directory can't be found", function () { + var directory = fileSystem.getDirectoryForPath("/doesnt-exist/"), + cb = getContentsCallback(); + + runs(function () { + directory.getContents(cb); + }); + waitsFor(function () { return cb.wasCalled; }); + runs(function () { + expect(cb.error).toBe(FileSystemError.NOT_FOUND); + }); + }); + + it("should only call the impl once for simultaneous read requests", function () { + var directory = fileSystem.getDirectoryForPath("/subdir/"), + cb = getContentsCallback(), + cb2 = getContentsCallback(), + cbCount = 0; + + function delayedCallback(cb) { + return function () { + var args = arguments; + window.setTimeout(function () { + cbCount++; + cb.apply(null, args); + }, 300); + }; + } + + MockFileSystemImpl.when("readdir", "/subdir/", {callback: delayedCallback}); + + // Fire off 2 getContents() calls in rapid succession + runs(function () { + // Make sure cached data is cleared + directory._contents = undefined; + directory.getContents(cb); + directory.getContents(cb2); + expect(cb.wasCalled).toBeFalsy(); // Callback should *not* have been called yet + }); + waitsFor(function () { return cb.wasCalled && cb2.wasCalled; }); + runs(function () { + expect(cb.wasCalled).toBe(true); + expect(cb.error).toBeFalsy(); + expect(cb2.wasCalled).toBe(true); + expect(cb2.error).toBeFalsy(); + expect(cb.contents).toEqual(cb2.contents); + expect(cbCount).toBe(1); + }); + }); + }); + + describe("Create directory", function () { + it("should create a Directory", function () { + var directory = fileSystem.getDirectoryForPath("/subdir2/"), + cb = errorCallback(), + cbCalled = false; + + runs(function () { + directory.exists(function (exists) { + expect(exists).toBe(false); + cbCalled = true; + }); + }); + waitsFor(function () { return cbCalled; }); + runs(function () { + directory.create(cb); + }); + waitsFor(function () { return cb.wasCalled; }); + runs(function () { + expect(cb.error).toBeFalsy(); + directory.exists(function (exists) { + expect(exists).toBe(true); + }); + }); + }); + }); + + describe("Read and write files", function () { + it("should read and write files", function () { + var file = fileSystem.getFileForPath("/subdir/file4.txt"), + newContents = "New file contents", + firstReadCB = readCallback(), + writeCB = errorCallback(), + secondReadCB = readCallback(); + + // Verify initial contents + runs(function () { + file.read(firstReadCB); + }); + waitsFor(function () { return firstReadCB.wasCalled; }); + runs(function () { + expect(firstReadCB.error).toBeFalsy(); + expect(firstReadCB.data).toBe("File 4 Contents"); + }); + + // Write new contents + runs(function () { + file.write(newContents, writeCB); + }); + waitsFor(function () { return writeCB.wasCalled; }); + runs(function () { + expect(writeCB.error).toBeFalsy(); + }); + + // Verify new contents + runs(function () { + file.read(secondReadCB); + }); + waitsFor(function () { return secondReadCB.wasCalled; }); + runs(function () { + expect(secondReadCB.error).toBeFalsy(); + expect(secondReadCB.data).toBe(newContents); + }); + }); + + it("should return an error if the file can't be found", function () { + var file = fileSystem.getFileForPath("/doesnt-exist.txt"), + cb = readCallback(); + + runs(function () { + file.read(cb); + }); + waitsFor(function () { return cb.wasCalled; }); + runs(function () { + expect(cb.error).toBe(FileSystemError.NOT_FOUND); + }); + }); + + it("should create a new file if needed", function () { + var file = fileSystem.getFileForPath("/new-file.txt"), + cb = errorCallback(), + readCb = readCallback(), + cbCalled = false, + newContents = "New file contents"; + + runs(function () { + file.exists(function (exists) { + expect(exists).toBe(false); + cbCalled = true; + }); + }); + waitsFor(function () { return cbCalled; }); + runs(function () { + file.write(newContents, cb); + }); + waitsFor(function () { return cb.wasCalled; }); + runs(function () { + expect(cb.error).toBeFalsy(); + file.read(readCb); + }); + waitsFor(function () { return readCb.wasCalled; }); + runs(function () { + expect(readCb.error).toBeFalsy(); + expect(readCb.data).toBe(newContents); + }); + }); + }); + + describe("FileSystemEntry.visit", function () { + beforeEach(function () { + function initEntry(entry, command, args) { + var cb = getContentsCallback(); + + args.push(cb); + runs(function () { + entry[command].apply(entry, args); + }); + waitsFor(function () { return cb.wasCalled; }); + runs(function () { + expect(cb.error).toBeFalsy(); + }); + } + + function initDir(path) { + initEntry(fileSystem.getDirectoryForPath(path), "create", []); + } + + function initFile(path) { + initEntry(fileSystem.getFileForPath(path), "write", ["abc"]); + } + + initDir("/visit/"); + initFile("/visit/file.txt"); + initDir("/visit/subdir1/"); + initDir("/visit/subdir2/"); + initFile("/visit/subdir1/subfile11.txt"); + initFile("/visit/subdir1/subfile12.txt"); + initFile("/visit/subdir2/subfile21.txt"); + initFile("/visit/subdir2/subfile22.txt"); + }); + + it("should visit all entries by default", function () { + var directory = fileSystem.getDirectoryForPath("/visit/"), + results = {}, + visitor = function (entry) { + results[entry.fullPath] = entry; + return true; + }; + + var cb = getContentsCallback(); + runs(function () { + directory.visit(visitor, cb); + }); + waitsFor(function () { return cb.wasCalled; }); + runs(function () { + expect(cb.error).toBeFalsy(); + expect(Object.keys(results).length).toBe(8); + expect(results["/visit/"]).toBeTruthy(); + expect(results["/visit/file.txt"]).toBeTruthy(); + expect(results["/visit/subdir1/"]).toBeTruthy(); + expect(results["/visit/subdir2/"]).toBeTruthy(); + expect(results["/visit/subdir1/subfile11.txt"]).toBeTruthy(); + expect(results["/visit/subdir1/subfile12.txt"]).toBeTruthy(); + expect(results["/visit/subdir2/subfile21.txt"]).toBeTruthy(); + expect(results["/visit/subdir2/subfile21.txt"]).toBeTruthy(); + expect(results["/"]).not.toBeTruthy(); + }); + }); + + it("should visit with a specified maximum depth", function () { + var directory = fileSystem.getDirectoryForPath("/visit/"), + results = {}, + visitor = function (entry) { + results[entry.fullPath] = entry; + return true; + }; + + var cb = getContentsCallback(); + runs(function () { + directory.visit(visitor, {maxDepth: 1}, cb); + }); + waitsFor(function () { return cb.wasCalled; }); + runs(function () { + expect(cb.error).toBeFalsy(); + expect(Object.keys(results).length).toBe(4); + expect(results["/visit/"]).toBeTruthy(); + expect(results["/visit/file.txt"]).toBeTruthy(); + expect(results["/visit/subdir1/"]).toBeTruthy(); + expect(results["/visit/subdir2/"]).toBeTruthy(); + expect(results["/visit/subdir1/subfile11.txt"]).not.toBeTruthy(); + expect(results["/visit/subdir1/subfile12.txt"]).not.toBeTruthy(); + expect(results["/visit/subdir2/subfile21.txt"]).not.toBeTruthy(); + expect(results["/visit/subdir2/subfile21.txt"]).not.toBeTruthy(); + expect(results["/"]).not.toBeTruthy(); + }); + }); + + it("should visit with a specified maximum number of entries", function () { + var directory = fileSystem.getDirectoryForPath("/visit/"), + results = {}, + visitor = function (entry) { + results[entry.fullPath] = entry; + return true; + }; + + var cb = getContentsCallback(); + runs(function () { + directory.visit(visitor, {maxEntries: 4}, cb); + }); + waitsFor(function () { return cb.wasCalled; }); + runs(function () { + expect(cb.error).toBe(FileSystemError.TOO_MANY_ENTRIES); + expect(Object.keys(results).length).toBe(4); + expect(results["/visit/"]).toBeTruthy(); + expect(results["/visit/file.txt"]).toBeTruthy(); + expect(results["/"]).not.toBeTruthy(); + }); + }); + + it("should visit only children of directories admitted by the filter", function () { + var directory = fileSystem.getDirectoryForPath("/visit/"), + results = {}, + visitor = function (entry) { + results[entry.fullPath] = entry; + return entry.name === "visit" || /1$/.test(entry.name); + }; + + var cb = getContentsCallback(); + runs(function () { + directory.visit(visitor, cb); + }); + waitsFor(function () { return cb.wasCalled; }); + runs(function () { + expect(cb.error).toBeFalsy(); + expect(Object.keys(results).length).toBe(6); + expect(results["/visit/"]).toBeTruthy(); + expect(results["/visit/file.txt"]).toBeTruthy(); + expect(results["/visit/subdir1/"]).toBeTruthy(); + expect(results["/visit/subdir2/"]).toBeTruthy(); + expect(results["/visit/subdir1/subfile11.txt"]).toBeTruthy(); + expect(results["/visit/subdir1/subfile12.txt"]).toBeTruthy(); + expect(results["/visit/subdir2/subfile21.txt"]).not.toBeTruthy(); + expect(results["/visit/subdir2/subfile21.txt"]).not.toBeTruthy(); + expect(results["/"]).not.toBeTruthy(); + }); + }); + }); + + describe("Event timing", function () { + + it("should notify rename callback before 'change' event", function () { + var origFilePath = "/file1.txt", + origFile = fileSystem.getFileForPath(origFilePath), + renamedFilePath = "/file1_renamed.txt"; + + runs(function () { + var renameDone = false, changeDone = false; + + // Delay impl callback to happen after impl watcher notification + MockFileSystemImpl.when("rename", origFilePath, { + callback: delay(250) + }); + + $(fileSystem).on("change", function (evt, entry) { + expect(renameDone).toBe(true); // this is the important check: callback should have already run! + changeDone = true; + }); + + origFile.rename(renamedFilePath, function (err) { + expect(err).toBeFalsy(); + renameDone = true; + }); + + waitsFor(function () { return changeDone && renameDone; }); + }); + + runs(function () { + expect(origFile.fullPath).toBe(renamedFilePath); + }); + }); + + + it("should notify write callback before 'change' event", function () { + var testFilePath = "/file1.txt", + testFile = fileSystem.getFileForPath(testFilePath); + + runs(function () { + var writeDone = false, changeDone = false; + + // Delay impl callback to happen after impl watcher notification + MockFileSystemImpl.when("writeFile", testFilePath, { + callback: delay(250) + }); + + $(fileSystem).on("change", function (evt, entry) { + expect(writeDone).toBe(true); // this is the important check: callback should have already run! + changeDone = true; + }); + + testFile.write("Foobar", function (err) { + expect(err).toBeFalsy(); + writeDone = true; + }); + + waitsFor(function () { return changeDone && writeDone; }); + }); + }); + + // Used for various tests below where two write operations (to two different files) overlap in various ways + function dualWrite(cb1Delay, watcher1Delay, cb2Delay, watcher2Delay) { + var testFile1 = fileSystem.getFileForPath("/file1.txt"), + testFile2 = fileSystem.getFileForPath("/file2.txt"); + + runs(function () { + var write1Done = false, change1Done = false; + var write2Done = false, change2Done = false; + + // Delay impl callback to happen after impl watcher notification + MockFileSystemImpl.when("writeFile", "/file1.txt", { + callback: delay(cb1Delay), + notify: delay(watcher1Delay) + }); + MockFileSystemImpl.when("writeFile", "/file2.txt", { + callback: delay(cb2Delay), + notify: delay(watcher2Delay) + }); + + $(fileSystem).on("change", function (evt, entry) { + // this is the important check: both callbacks should have already run! + expect(write1Done).toBe(true); + expect(write2Done).toBe(true); + + expect(entry.fullPath === "/file1.txt" || entry.fullPath === "/file2.txt").toBe(true); + if (entry.fullPath === "/file1.txt") { + change1Done = true; + } else { + change2Done = true; + } + }); + + // We always *start* both operations together, synchronously + // What varies is when the impl callbacks for for each op, and when the impl's watcher notices each op + fileSystem.getFileForPath("/file1.txt").write("Foobar 1", function (err) { + expect(err).toBeFalsy(); + write1Done = true; + }); + fileSystem.getFileForPath("/file2.txt").write("Foobar 2", function (err) { + expect(err).toBeFalsy(); + write2Done = true; + }); + + waitsFor(function () { return change1Done && write1Done && change2Done && write2Done; }); + }); + } + + it("should handle overlapping writes to different files - 2nd file finishes much faster", function () { + dualWrite(100, 200, 0, 0); + }); + it("should handle overlapping writes to different files - 2nd file finishes much faster, 1st file watcher runs early", function () { + dualWrite(200, 100, 0, 0); + }); + it("should handle overlapping writes to different files - 1st file finishes much faster", function () { + dualWrite(0, 0, 100, 200); + }); + it("should handle overlapping writes to different files - 1st file finishes much faster, 2nd file watcher runs early", function () { + dualWrite(0, 0, 200, 100); + }); + it("should handle overlapping writes to different files - both watchers run early", function () { + dualWrite(100, 0, 200, 0); + }); + it("should handle overlapping writes to different files - both watchers run early, reversed", function () { + dualWrite(100, 50, 200, 0); + }); + it("should handle overlapping writes to different files - 2nd file finishes faster, both watchers run early", function () { + dualWrite(200, 0, 100, 0); + }); + it("should handle overlapping writes to different files - 2nd file finishes faster, both watchers run early, reversed", function () { + dualWrite(200, 50, 100, 0); + }); + it("should handle overlapping writes to different files - watchers run in order", function () { + dualWrite(0, 100, 0, 200); + }); + it("should handle overlapping writes to different files - watchers reversed", function () { + dualWrite(0, 200, 0, 100); + }); + it("should handle overlapping writes to different files - nonoverlapping in order", function () { + dualWrite(0, 50, 100, 200); + }); + it("should handle overlapping writes to different files - nonoverlapping reversed", function () { + dualWrite(100, 200, 0, 50); + }); + it("should handle overlapping writes to different files - overlapped in order", function () { + dualWrite(0, 100, 50, 200); + }); + it("should handle overlapping writes to different files - overlapped reversed", function () { + dualWrite(50, 200, 0, 100); + }); + }); + + + }); +}); diff --git a/test/spec/HTMLInstrumentation-test.js b/test/spec/HTMLInstrumentation-test.js index d8766d7e984..a1b8d93a60b 100644 --- a/test/spec/HTMLInstrumentation-test.js +++ b/test/spec/HTMLInstrumentation-test.js @@ -30,8 +30,7 @@ define(function (require, exports, module) { "use strict"; // Load dependent modules - var NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, - FileUtils = require("file/FileUtils"), + var FileUtils = require("file/FileUtils"), HTMLInstrumentation = require("language/HTMLInstrumentation"), HTMLSimpleDOM = require("language/HTMLSimpleDOM"), RemoteFunctions = require("text!LiveDevelopment/Agents/RemoteFunctions.js"), diff --git a/test/spec/InlineEditorProviders-test.js b/test/spec/InlineEditorProviders-test.js index 6611c6cc955..ecb7a2fcff9 100644 --- a/test/spec/InlineEditorProviders-test.js +++ b/test/spec/InlineEditorProviders-test.js @@ -30,13 +30,11 @@ define(function (require, exports, module) { var Commands, // loaded from brackets.test EditorManager, // loaded from brackets.test - FileIndexManager, // loaded from brackets.test FileSyncManager, // loaded from brackets.test DocumentManager, // loaded from brackets.test FileViewController, // loaded from brackets.test InlineWidget = require("editor/InlineWidget").InlineWidget, Dialogs = require("widgets/Dialogs"), - NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, KeyEvent = require("utils/KeyEvent"), FileUtils = require("file/FileUtils"), SpecRunnerUtils = require("spec/SpecRunnerUtils"); @@ -168,7 +166,6 @@ define(function (require, exports, module) { // Load module instances from brackets.test Commands = testWindow.brackets.test.Commands; EditorManager = testWindow.brackets.test.EditorManager; - FileIndexManager = testWindow.brackets.test.FileIndexManager; FileSyncManager = testWindow.brackets.test.FileSyncManager; DocumentManager = testWindow.brackets.test.DocumentManager; FileViewController = testWindow.brackets.test.FileViewController; @@ -623,7 +620,6 @@ define(function (require, exports, module) { }); }); - describe("Inline Editor syncing from disk", function () { it("should close inline editor when file deleted on disk", function () { @@ -632,7 +628,8 @@ define(function (require, exports, module) { savedTempCSSFile = false; runs(function () { - var promise = SpecRunnerUtils.createTextFile(tempPath + "/tempCSS.css", "#anotherDiv {}") + // Important: must create file using test window's FS so that it sees the new file right away + var promise = SpecRunnerUtils.createTextFile(tempPath + "/tempCSS.css", "#anotherDiv {}", testWindow.brackets.test.FileSystem) .done(function (entry) { fileToWrite = entry; }) @@ -645,9 +642,6 @@ define(function (require, exports, module) { // Open inline editor for that file runs(function () { - // force FileIndexManager to re-sync and pick up the new tempCSS.css file - FileIndexManager.markDirty(); - initInlineTest("test1.html", 6, true); }); // initInlineTest() inserts a waitsFor() automatically, so must end runs() block here @@ -812,7 +806,6 @@ define(function (require, exports, module) { }); }); - describe("Inline Editor range updating", function () { var fullEditor, diff --git a/test/spec/InstallExtensionDialog-test.js b/test/spec/InstallExtensionDialog-test.js index b353c7f5eb2..ea270480602 100644 --- a/test/spec/InstallExtensionDialog-test.js +++ b/test/spec/InstallExtensionDialog-test.js @@ -32,6 +32,7 @@ define(function (require, exports, module) { var SpecRunnerUtils = require("spec/SpecRunnerUtils"), KeyEvent = require("utils/KeyEvent"), NativeApp = require("utils/NativeApp"), + FileSystem, Strings; describe("Install Extension Dialog", function () { @@ -44,6 +45,7 @@ define(function (require, exports, module) { SpecRunnerUtils.createTestWindowAndRun(this, function (w) { testWindow = w; Strings = testWindow.require("strings"); + FileSystem = testWindow.brackets.test.FileSystem; }); }); @@ -725,14 +727,15 @@ define(function (require, exports, module) { installer = makeInstaller(null, deferred); setUrl(); fields.$okButton.click(); - var packageFilename = "/path/to/downloaded/package.zip"; + var packageFilename = "/path/to/downloaded/package.zip", + file = FileSystem.getFileForPath(packageFilename); deferred.resolve({ installationStatus: "ALREADY_INSTALLED", localPath: packageFilename }); - spyOn(testWindow.brackets.fs, "unlink"); + spyOn(file, "unlink"); fields.$cancelButton.click(); - expect(testWindow.brackets.fs.unlink).toHaveBeenCalledWith(packageFilename, jasmine.any(Function)); + expect(file.unlink).toHaveBeenCalled(); expect(fields.$dlg.is(":visible")).toBe(false); }); @@ -741,19 +744,20 @@ define(function (require, exports, module) { installer = makeInstaller(null, deferred); setUrl(); fields.$okButton.click(); - var packageFilename = "/path/to/downloaded/package.zip"; + var packageFilename = "/path/to/downloaded/package.zip", + file = FileSystem.getFileForPath(packageFilename); deferred.resolve({ installationStatus: "ALREADY_INSTALLED", localPath: packageFilename }); - spyOn(testWindow.brackets.fs, "unlink"); + spyOn(file, "unlink"); var dialogDone = false; dialog._dialogDeferred.done(function (result) { dialogDone = true; expect(result.installationStatus).toBe("ALREADY_INSTALLED"); }); fields.$okButton.click(); - expect(testWindow.brackets.fs.unlink).not.toHaveBeenCalled(); + expect(file.unlink).not.toHaveBeenCalled(); expect(fields.$dlg.is(":visible")).toBe(false); expect(dialogDone).toBe(true); }); diff --git a/test/spec/JSUtils-test.js b/test/spec/JSUtils-test.js index 5a9281b8610..76d42502349 100644 --- a/test/spec/JSUtils-test.js +++ b/test/spec/JSUtils-test.js @@ -29,12 +29,12 @@ define(function (require, exports, module) { 'use strict'; var DocumentManager, // loaded from brackets.test - FileIndexManager, // loaded from brackets.test FileViewController, // loaded from brackets.test + ProjectManager, // loaded from brackets.test JSUtils = require("language/JSUtils"), + FileSystem = require("filesystem/FileSystem"), FileUtils = require("file/FileUtils"), - NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, SpecRunnerUtils = require("spec/SpecRunnerUtils"); var testPath = SpecRunnerUtils.getTestPath("/spec/JSUtils-test-files"), @@ -48,13 +48,13 @@ define(function (require, exports, module) { return this.actual.functionName.trim() === expected; }; - var simpleJsFileEntry = new NativeFileSystem.FileEntry(testPath + "/simple.js"); - var trickyJsFileEntry = new NativeFileSystem.FileEntry(testPath + "/tricky.js"); - var invalidJsFileEntry = new NativeFileSystem.FileEntry(testPath + "/invalid.js"); - var jQueryJsFileEntry = new NativeFileSystem.FileEntry(testPath + "/jquery-1.7.js"); - var braceEndJsFileEntry = new NativeFileSystem.FileEntry(testPath + "/braceEnd.js"); - var eofJsFileEntry = new NativeFileSystem.FileEntry(testPath + "/eof.js"); - var eof2JsFileEntry = new NativeFileSystem.FileEntry(testPath + "/eof2.js"); + var simpleJsFileEntry = FileSystem.getFileForPath(testPath + "/simple.js"); + var trickyJsFileEntry = FileSystem.getFileForPath(testPath + "/tricky.js"); + var invalidJsFileEntry = FileSystem.getFileForPath(testPath + "/invalid.js"); + var jQueryJsFileEntry = FileSystem.getFileForPath(testPath + "/jquery-1.7.js"); + var braceEndJsFileEntry = FileSystem.getFileForPath(testPath + "/braceEnd.js"); + var eofJsFileEntry = FileSystem.getFileForPath(testPath + "/eof.js"); + var eof2JsFileEntry = FileSystem.getFileForPath(testPath + "/eof2.js"); function init(spec, fileEntry) { if (fileEntry) { @@ -452,8 +452,8 @@ define(function (require, exports, module) { // Load module instances from brackets.test var brackets = testWindow.brackets; DocumentManager = brackets.test.DocumentManager; - FileIndexManager = brackets.test.FileIndexManager; FileViewController = brackets.test.FileViewController; + ProjectManager = brackets.test.ProjectManager; JSUtils = brackets.test.JSUtils; SpecRunnerUtils.loadProjectInTestWindow(testPath); @@ -462,9 +462,9 @@ define(function (require, exports, module) { afterEach(function () { DocumentManager = null; - FileIndexManager = null; FileViewController = null; JSUtils = null; + ProjectManager = null; SpecRunnerUtils.closeTestWindow(); }); @@ -490,12 +490,11 @@ define(function (require, exports, module) { runs(function () { var result = new $.Deferred(); - FileIndexManager.getFileInfoList("all") - .done(function (fileInfos) { - invokeFind(fileInfos) - .done(function (functionsResult) { functions = functionsResult; }) - .then(result.resolve, result.reject); - }); + ProjectManager.getAllFiles().done(function (files) { + invokeFind(files) + .done(function (functionsResult) { functions = functionsResult; }) + .then(result.resolve, result.reject); + }); waitsForDone(result, "Index and invoke JSUtils.findMatchingFunctions()"); }); diff --git a/test/spec/LanguageManager-test.js b/test/spec/LanguageManager-test.js index dd9561cee71..f7e295d0653 100644 --- a/test/spec/LanguageManager-test.js +++ b/test/spec/LanguageManager-test.js @@ -33,7 +33,7 @@ define(function (require, exports, module) { DocumentManager = require("document/DocumentManager"), PathUtils = require("thirdparty/path-utils/path-utils.min"), SpecRunnerUtils = require("spec/SpecRunnerUtils"), - FileUtils = require("file/FileUtils"); + FileSystem = require("filesystem/FileSystem"); describe("LanguageManager", function () { @@ -402,37 +402,98 @@ define(function (require, exports, module) { }); describe("rename file extension", function () { - + this.category = "integration"; + it("should update the document's language when a file is renamed", function () { - var javascript = LanguageManager.getLanguage("javascript"), - html = LanguageManager.getLanguage("html"), - doc = SpecRunnerUtils.createMockActiveDocument({ filename: "foo.js", language: "javascript" }), - spy = jasmine.createSpy("languageChanged event handler"); - - // sanity check language - expect(doc.getLanguage()).toBe(javascript); - - // Documents are only 'active' while referenced; they won't be maintained by DocumentManager - // for global updates like rename otherwise. - doc.addRef(); + var tempDir = SpecRunnerUtils.getTempDirectory(), + oldFilename = tempDir + "/foo.js", + newFilename = tempDir + "/dummy.html", + spy = jasmine.createSpy("languageChanged event handler"), + javascript, + html, + oldFile, + doc; + + var DocumentManager, + FileSystem, + LanguageManager, + _$; + + SpecRunnerUtils.createTempDirectory(); + + SpecRunnerUtils.createTestWindowAndRun(this, function (w) { + // Load module instances from brackets.test + FileSystem = w.brackets.test.FileSystem; + LanguageManager = w.brackets.test.LanguageManager; + DocumentManager = w.brackets.test.DocumentManager; + _$ = w.$; + }); - // listen for event - $(doc).on("languageChanged", spy); + var writeDeferred = $.Deferred(); + runs(function () { + oldFile = FileSystem.getFileForPath(oldFilename); + oldFile.write("", function (err) { + if (err) { + writeDeferred.reject(err); + } else { + writeDeferred.resolve(); + } + }); + }); + waitsForDone(writeDeferred.promise(), "old file creation"); + + SpecRunnerUtils.loadProjectInTestWindow(tempDir); - // trigger a rename - DocumentManager.notifyPathNameChanged(doc.file.name, "dummy.html", false); + runs(function () { + waitsForDone(DocumentManager.getDocumentForPath(oldFilename).done(function (_doc) { + doc = _doc; + }), "get document"); + }); + + var renameDeferred = $.Deferred(); + runs(function () { + javascript = LanguageManager.getLanguage("javascript"); + + // sanity check language + expect(doc.getLanguage()).toBe(javascript); + + // Documents are only 'active' while referenced; they won't be maintained by DocumentManager + // for global updates like rename otherwise. + doc.addRef(); + + // listen for event + _$(doc).on("languageChanged", spy); + + // trigger a rename + oldFile.rename(newFilename, function (err) { + if (err) { + renameDeferred.reject(err); + } else { + renameDeferred.resolve(); + } + }); + }); + waitsForDone(renameDeferred.promise(), "old file rename"); - // language should change - expect(doc.getLanguage()).toBe(html); - expect(spy).toHaveBeenCalled(); - expect(spy.callCount).toEqual(1); + runs(function () { + html = LanguageManager.getLanguage("html"); + + // language should change + expect(doc.getLanguage()).toBe(html); + expect(spy).toHaveBeenCalled(); + expect(spy.callCount).toEqual(1); + + // check callback args (arg 0 is a jQuery event) + expect(spy.mostRecentCall.args[1]).toBe(javascript); + expect(spy.mostRecentCall.args[2]).toBe(html); + + // cleanup + doc.releaseRef(); + }); - // check callback args (arg 0 is a jQuery event) - expect(spy.mostRecentCall.args[1]).toBe(javascript); - expect(spy.mostRecentCall.args[2]).toBe(html); + SpecRunnerUtils.closeTestWindow(); - // cleanup - doc.releaseRef(); + SpecRunnerUtils.removeTempDirectory(); }); it("should update the document's language when a language is added", function () { @@ -444,7 +505,7 @@ define(function (require, exports, module) { runs(function () { // Create a scheme script file - doc = SpecRunnerUtils.createMockActiveDocument({ filename: "file.scheme" }); + doc = SpecRunnerUtils.createMockActiveDocument({ filename: "/file.scheme" }); // Initial language will be unknown (scheme is not a default language) unknown = LanguageManager.getLanguage("unknown"); @@ -494,7 +555,7 @@ define(function (require, exports, module) { promise; // Create a foo script file - doc = SpecRunnerUtils.createMockActiveDocument({ filename: "test.foo" }); + doc = SpecRunnerUtils.createMockActiveDocument({ filename: "/test.foo" }); // Initial language will be unknown (foo is not a default language) unknown = LanguageManager.getLanguage("unknown"); diff --git a/test/spec/LiveDevelopment-test.js b/test/spec/LiveDevelopment-test.js index 73d38f94760..b1af49ebabe 100644 --- a/test/spec/LiveDevelopment-test.js +++ b/test/spec/LiveDevelopment-test.js @@ -32,7 +32,7 @@ define(function (require, exports, module) { PreferencesDialogs = require("preferences/PreferencesDialogs"), Strings = require("strings"), StringUtils = require("utils/StringUtils"), - NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, + FileSystem = require("filesystem/FileSystem"), FileUtils = require("file/FileUtils"), DefaultDialogs = require("widgets/DefaultDialogs"), FileServer = require("LiveDevelopment/Servers/FileServer").FileServer, @@ -367,7 +367,7 @@ define(function (require, exports, module) { instrumentedHtml = "", elementIds = {}, testPath = SpecRunnerUtils.getTestPath("/spec/HTMLInstrumentation-test-files"), - WellFormedFileEntry = new NativeFileSystem.FileEntry(testPath + "/wellformed.html"); + WellFormedFileEntry = FileSystem.getFileForPath(testPath + "/wellformed.html"); function init(fileEntry) { if (fileEntry) { @@ -974,7 +974,6 @@ define(function (require, exports, module) { describe("Default HTML Document", function () { var brackets, - FileIndexManager, LiveDevelopment, ProjectManager, testWindow; @@ -986,7 +985,6 @@ define(function (require, exports, module) { // Load module instances from brackets.test brackets = testWindow.brackets; LiveDevelopment = brackets.test.LiveDevelopment; - FileIndexManager = brackets.test.FileIndexManager; ProjectManager = brackets.test.ProjectManager; }); }); @@ -994,7 +992,6 @@ define(function (require, exports, module) { afterLast(function () { brackets = null; - FileIndexManager = null; LiveDevelopment = null; ProjectManager = null; testWindow = null; @@ -1002,9 +999,8 @@ define(function (require, exports, module) { SpecRunnerUtils.closeTestWindow(); }); - function loadFileAndUpdateFileIndex(fileToLoadIntoEditor) { + function loadFile(fileToLoadIntoEditor) { runs(function () { - FileIndexManager.markDirty(); waitsForDone(SpecRunnerUtils.openProjectFiles([fileToLoadIntoEditor]), "SpecRunnerUtils.openProjectFiles " + fileToLoadIntoEditor); }); } @@ -1043,7 +1039,7 @@ define(function (require, exports, module) { indexFile = "sub/sub2/index.html"; SpecRunnerUtils.loadProjectInTestWindow(testPath + "/static-project-1"); - loadFileAndUpdateFileIndex(cssFile); + loadFile(cssFile); runs(function () { promise = LiveDevelopment._getInitialDocFromCurrent(); @@ -1068,7 +1064,7 @@ define(function (require, exports, module) { SpecRunnerUtils.loadProjectInTestWindow(testPath + "/static-project-2"); - loadFileAndUpdateFileIndex(cssFile); + loadFile(cssFile); runs(function () { promise = LiveDevelopment._getInitialDocFromCurrent(); @@ -1092,7 +1088,7 @@ define(function (require, exports, module) { indexFile = "index.html"; SpecRunnerUtils.loadProjectInTestWindow(testPath + "/static-project-3"); - loadFileAndUpdateFileIndex(cssFile); + loadFile(cssFile); runs(function () { promise = LiveDevelopment._getInitialDocFromCurrent(); @@ -1116,7 +1112,7 @@ define(function (require, exports, module) { indexFile = "sub/sub2/index.html"; SpecRunnerUtils.loadProjectInTestWindow(testPath + "/static-project-4"); - loadFileAndUpdateFileIndex(cssFile); + loadFile(cssFile); runs(function () { promise = LiveDevelopment._getInitialDocFromCurrent(); @@ -1140,7 +1136,7 @@ define(function (require, exports, module) { indexFile = "sub/index.html"; SpecRunnerUtils.loadProjectInTestWindow(testPath + "/static-project-5"); - loadFileAndUpdateFileIndex(cssFile); + loadFile(cssFile); runs(function () { promise = LiveDevelopment._getInitialDocFromCurrent(); @@ -1163,7 +1159,7 @@ define(function (require, exports, module) { var cssFile = "top2/test.css"; SpecRunnerUtils.loadProjectInTestWindow(testPath + "/static-project-6"); - loadFileAndUpdateFileIndex(cssFile); + loadFile(cssFile); runs(function () { promise = LiveDevelopment._getInitialDocFromCurrent(); @@ -1215,7 +1211,7 @@ define(function (require, exports, module) { indexFile = "sub/sub2/index.php"; SpecRunnerUtils.loadProjectInTestWindow(testPath + "/dynamic-project-1"); - loadFileAndUpdateFileIndex(cssFile); + loadFile(cssFile); runs(function () { ProjectManager.setBaseUrl("http://localhost:1111/"); @@ -1240,7 +1236,7 @@ define(function (require, exports, module) { indexFile = "sub/index.php"; SpecRunnerUtils.loadProjectInTestWindow(testPath + "/dynamic-project-2"); - loadFileAndUpdateFileIndex(cssFile); + loadFile(cssFile); runs(function () { ProjectManager.setBaseUrl("http://localhost:2222/"); @@ -1265,7 +1261,7 @@ define(function (require, exports, module) { indexFile = "index.php"; SpecRunnerUtils.loadProjectInTestWindow(testPath + "/dynamic-project-3"); - loadFileAndUpdateFileIndex(cssFile); + loadFile(cssFile); runs(function () { ProjectManager.setBaseUrl("http://localhost:3333/"); @@ -1290,7 +1286,7 @@ define(function (require, exports, module) { indexFile = "sub/index.php"; SpecRunnerUtils.loadProjectInTestWindow(testPath + "/dynamic-project-5"); - loadFileAndUpdateFileIndex(cssFile); + loadFile(cssFile); runs(function () { ProjectManager.setBaseUrl("http://localhost:5555/"); @@ -1314,7 +1310,7 @@ define(function (require, exports, module) { var cssFile = "top2/test.css"; SpecRunnerUtils.loadProjectInTestWindow(testPath + "/dynamic-project-6"); - loadFileAndUpdateFileIndex(cssFile); + loadFile(cssFile); runs(function () { ProjectManager.setBaseUrl("http://localhost:6666/"); diff --git a/test/spec/LowLevelFileIO-test.js b/test/spec/LowLevelFileIO-test.js index 7f5f6dc3e9b..0b2d484e412 100644 --- a/test/spec/LowLevelFileIO-test.js +++ b/test/spec/LowLevelFileIO-test.js @@ -32,7 +32,9 @@ define(function (require, exports, module) { // Load dependent modules var SpecRunnerUtils = require("spec/SpecRunnerUtils"); - var _FSEncodings = require("file/NativeFileSystem").NativeFileSystem._FSEncodings; + + var UTF8 = "utf8", + UTF16 = "utf16"; // These are tests for the low-level file io routines in brackets-app. Make sure // you have the latest brackets-app before running. @@ -252,7 +254,7 @@ define(function (require, exports, module) { var cb = readFileSpy(); runs(function () { - brackets.fs.readFile(baseDir + "/file_one.txt", _FSEncodings.UTF8, cb); + brackets.fs.readFile(baseDir + "/file_one.txt", UTF8, cb); }); waitsFor(function () { return cb.wasCalled; }, 1000); @@ -267,7 +269,7 @@ define(function (require, exports, module) { var cb = readFileSpy(); runs(function () { - brackets.fs.readFile("/This/file/doesnt/exist.txt", _FSEncodings.UTF8, cb); + brackets.fs.readFile("/This/file/doesnt/exist.txt", UTF8, cb); }); waitsFor(function () { return cb.wasCalled; }, 1000); @@ -281,7 +283,7 @@ define(function (require, exports, module) { var cb = readFileSpy(); runs(function () { - brackets.fs.readFile(baseDir + "/file_one.txt", _FSEncodings.UTF16, cb); + brackets.fs.readFile(baseDir + "/file_one.txt", UTF16, cb); }); waitsFor(function () { return cb.wasCalled; }, 1000); @@ -309,7 +311,7 @@ define(function (require, exports, module) { var cb = readFileSpy(); runs(function () { - brackets.fs.readFile(baseDir, _FSEncodings.UTF8, cb); + brackets.fs.readFile(baseDir, UTF8, cb); }); waitsFor(function () { return cb.wasCalled; }, 1000); @@ -329,7 +331,7 @@ define(function (require, exports, module) { readFileCB = readFileSpy(); runs(function () { - brackets.fs.writeFile(baseDir + "/write_test.txt", contents, _FSEncodings.UTF8, cb); + brackets.fs.writeFile(baseDir + "/write_test.txt", contents, UTF8, cb); }); waitsFor(function () { return cb.wasCalled; }, 1000); @@ -340,7 +342,7 @@ define(function (require, exports, module) { // Read contents to verify runs(function () { - brackets.fs.readFile(baseDir + "/write_test.txt", _FSEncodings.UTF8, readFileCB); + brackets.fs.readFile(baseDir + "/write_test.txt", UTF8, readFileCB); }); waitsFor(function () { return readFileCB.wasCalled; }, 1000); @@ -356,7 +358,7 @@ define(function (require, exports, module) { var cb = errSpy(); runs(function () { - brackets.fs.writeFile(baseDir + "/cant_write_here/write_test.txt", contents, _FSEncodings.UTF8, cb); + brackets.fs.writeFile(baseDir + "/cant_write_here/write_test.txt", contents, UTF8, cb); }); waitsFor(function () { return cb.wasCalled; }, 1000); @@ -386,7 +388,7 @@ define(function (require, exports, module) { var cb = errSpy(); runs(function () { - brackets.fs.writeFile(baseDir, contents, _FSEncodings.UTF8, cb); + brackets.fs.writeFile(baseDir, contents, UTF8, cb); }); waitsFor(function () { return cb.wasCalled; }, 1000); @@ -410,7 +412,7 @@ define(function (require, exports, module) { statCB = statSpy(); runs(function () { - brackets.fs.writeFile(filename, contents, _FSEncodings.UTF8, writeFileCB); + brackets.fs.writeFile(filename, contents, UTF8, writeFileCB); }); waitsFor(function () { return writeFileCB.wasCalled; }, 1000); @@ -422,7 +424,7 @@ define(function (require, exports, module) { // Read contents to verify runs(function () { - brackets.fs.readFile(filename, _FSEncodings.UTF8, readFileCB); + brackets.fs.readFile(filename, UTF8, readFileCB); }); waitsFor(function () { return readFileCB.wasCalled; }, 1000); @@ -822,7 +824,7 @@ define(function (require, exports, module) { // Create a file runs(function () { - brackets.fs.writeFile(newFileName, "", _FSEncodings.UTF8, writeFileCB); + brackets.fs.writeFile(newFileName, "", UTF8, writeFileCB); }); waitsFor(function () { return writeFileCB.wasCalled; }); diff --git a/test/spec/MockFileSystemImpl.js b/test/spec/MockFileSystemImpl.js new file mode 100644 index 00000000000..30ed0851936 --- /dev/null +++ b/test/spec/MockFileSystemImpl.js @@ -0,0 +1,373 @@ +/* + * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + */ + +/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50 */ +/*global define, $ */ + +define(function (require, exports, module) { + "use strict"; + + var FileSystemError = require("filesystem/FileSystemError"), + FileSystemStats = require("filesystem/FileSystemStats"); + + // Watcher callback function + var _watcherCallback; + + // Initial file system data. + var _initialData = { + "/": { + isFile: false, + mtime: Date.now() + }, + "/file1.txt": { + isFile: true, + mtime: Date.now(), + contents: "File 1 Contents" + }, + "/file2.txt": { + isFile: true, + mtime: Date.now(), + contents: "File 2 Contents" + }, + "/subdir/": { + isFile: false, + mtime: Date.now() + }, + "/subdir/file3.txt": { + isFile: true, + mtime: Date.now(), + contents: "File 3 Contents" + }, + "/subdir/file4.txt": { + isFile: true, + mtime: Date.now(), + contents: "File 4 Contents" + } + }; + + // "Live" data for this instance of the file system. Use reset() to + // initialize with _initialData + var _data; + + // Callback hooks, set in when(). See when() for more details. + var _hooks; + + function _getHookEntry(method, path) { + return _hooks[method] && _hooks[method][path]; + } + + function _getCallback(method, path, cb) { + var entry = _getHookEntry(method, path), + result = entry && entry.callback && entry.callback(cb); + + if (!result) { + result = cb; + } + return result; + } + + function _getNotification(method, path, cb) { + var entry = _getHookEntry(method, path), + result = entry && entry.notify && entry.notify(cb); + + if (!result) { + result = cb; + } + return result; + } + + function _getStat(path) { + var entry = _data[path], + stat = null; + + if (entry) { + stat = new FileSystemStats({ + isFile: entry.isFile, + mtime: entry.mtime, + size: 0 + }); + } + + return stat; + } + + function _sendWatcherNotification(path) { + if (_watcherCallback) { + _watcherCallback(path); + } + } + + function _sendDirectoryWatcherNotification(path) { + // Path may be a file or a directory. If it's a file, + // strip the file name off + if (path[path.length - 1] !== "/") { + path = path.substr(0, path.lastIndexOf("/") + 1); + } + _sendWatcherNotification(path); + } + + function init(callback) { + if (callback) { + callback(); + } + } + + function showOpenDialog(allowMultipleSelection, chooseDirectories, title, initialPath, fileTypes, callback) { + // Not implemented + callback(null, null); + } + + function showSaveDialog(title, initialPath, proposedNewFilename, callback) { + // Not implemented + callback(null, null); + } + + function exists(path, callback) { + var cb = _getCallback("exists", path, callback); + cb(!!_data[path]); + } + + function readdir(path, callback) { + var cb = _getCallback("readdir", path, callback), + entry, + contents = [], + stats = []; + + if (!_data[path]) { + cb(FileSystemError.NOT_FOUND); + return; + } + + for (entry in _data) { + if (_data.hasOwnProperty(entry)) { + var isDir = false; + if (entry[entry.length - 1] === "/") { + entry = entry.substr(0, entry.length - 1); + isDir = true; + } + if (entry !== path && + entry.indexOf(path) === 0 && + entry.lastIndexOf("/") === path.lastIndexOf("/")) { + contents.push(entry.substr(entry.lastIndexOf("/")) + (isDir ? "/" : "")); + stats.push(_getStat(entry + (isDir ? "/" : ""))); + } + } + } + cb(null, contents, stats); + } + + function mkdir(path, mode, callback) { + if (typeof (mode) === "function") { + callback = mode; + mode = null; + } + var cb = _getCallback("mkdir", path, callback), + notify = _getNotification("mkdir", path, _sendDirectoryWatcherNotification); + + if (_data[path]) { + cb(FileSystemError.ALREADY_EXISTS); + } else { + var entry = { + isFile: false, + mtime: Date.now() + }; + _data[path] = entry; + cb(null, _getStat(path)); + + // Strip the trailing slash off the directory name so the + // notification gets sent to the parent + var notifyPath = path.substr(0, path.length - 1); + notify(notifyPath); + } + } + + function rename(oldPath, newPath, callback) { + var cb = _getCallback("rename", oldPath, callback), + notify = _getNotification("rename", oldPath, _sendDirectoryWatcherNotification); + + if (_data[newPath]) { + cb(FileSystemError.ALREADY_EXISTS); + } else if (!_data[oldPath]) { + cb(FileSystemError.NOT_FOUND); + } else { + _data[newPath] = _data[oldPath]; + delete _data[oldPath]; + if (!_data[newPath].isFile) { + var entry, i, + toDelete = []; + + for (entry in _data) { + if (_data.hasOwnProperty(entry)) { + if (entry.indexOf(oldPath) === 0) { + _data[newPath + entry.substr(oldPath.length)] = _data[entry]; + toDelete.push(entry); + } + } + } + for (i = toDelete.length; i; i--) { + delete _data[toDelete.pop()]; + } + } + cb(null); + + // If renaming a Directory, remove the slash from the notification + // name so the *parent* directory is notified of the change + var notifyPath; + + if (oldPath[oldPath.length - 1] === "/") { + notifyPath = oldPath.substr(0, oldPath.length - 1); + } else { + notifyPath = oldPath; + } + notify(notifyPath); + } + } + + function stat(path, callback) { + var cb = _getCallback("stat", path, callback); + if (!_data[path]) { + cb(FileSystemError.NOT_FOUND); + } else { + cb(null, _getStat(path)); + } + } + + function readFile(path, options, callback) { + if (typeof (options) === "function") { + callback = options; + options = null; + } + + var cb = _getCallback("readFile", path, callback); + + if (!_data[path]) { + cb(FileSystemError.NOT_FOUND); + } else { + cb(null, _data[path].contents); + } + } + + function writeFile(path, data, options, callback) { + if (typeof (options) === "function") { + callback = options; + options = null; + } + + exists(path, function (exists) { + var cb = _getCallback("writeFile", path, callback), + notification = exists ? _sendWatcherNotification : _sendDirectoryWatcherNotification, + notify = _getNotification("writeFile", path, notification); + + if (!_data[path]) { + _data[path] = { + isFile: true + }; + } + _data[path].contents = data; + _data[path].mtime = Date.now(); + cb(null); + notify(path); + }); + } + + function unlink(path, callback) { + var cb = _getCallback("unlink", path, callback), + notify = _getNotification("unlink", path, _sendDirectoryWatcherNotification); + + if (!_data[path]) { + cb(FileSystemError.NOT_FOUND); + } else { + delete _data[path]; + cb(null); + notify(path); + } + } + + function initWatchers(callback) { + _watcherCallback = callback; + } + + function watchPath(path) { + } + + function unwatchPath(path) { + } + + function unwatchAll() { + } + + + exports.init = init; + exports.showOpenDialog = showOpenDialog; + exports.showSaveDialog = showSaveDialog; + exports.exists = exists; + exports.readdir = readdir; + exports.mkdir = mkdir; + exports.rename = rename; + exports.stat = stat; + exports.readFile = readFile; + exports.writeFile = writeFile; + exports.unlink = unlink; + exports.initWatchers = initWatchers; + exports.watchPath = watchPath; + exports.unwatchPath = unwatchPath; + exports.unwatchAll = unwatchAll; + + // Test methods + exports.reset = function () { + _data = {}; + $.extend(_data, _initialData); + _hooks = {}; + }; + + /** + * Add a callback and notification hooks to be used when specific + * methods are called with a specific path. + * + * @param {string} method The name of the method + * @param {string} path The path that must be matched + * @param {object} callbacks Object with optional 'callback' and 'notify' + * fields. These are functions that have one parameter and + * must return a function. + * + * Here is an example that delays the callback and change notifications by 300ms when + * writing a file named "/foo.txt". + * + * function delayedCallback(cb) { + * return function () { + * var args = arguments; + * setTimeout(function () { + * cb.apply(null, args); + * }, 300); + * }; + * } + * + * MockFileSystem.when("writeFile", "/foo.txt", {callback: delayedCallback, notify: delayedCallback}); + */ + exports.when = function (method, path, callbacks) { + if (!_hooks[method]) { + _hooks[method] = {}; + } + _hooks[method][path] = callbacks; + }; +}); diff --git a/test/spec/NativeFileSystem-test-files/cant_read_here.txt b/test/spec/NativeFileSystem-test-files/cant_read_here.txt deleted file mode 100755 index 3bb817bffca..00000000000 --- a/test/spec/NativeFileSystem-test-files/cant_read_here.txt +++ /dev/null @@ -1 +0,0 @@ -cant_read_here \ No newline at end of file diff --git a/test/spec/NativeFileSystem-test-files/cant_write_here.txt b/test/spec/NativeFileSystem-test-files/cant_write_here.txt deleted file mode 100644 index b05b2b1f407..00000000000 --- a/test/spec/NativeFileSystem-test-files/cant_write_here.txt +++ /dev/null @@ -1 +0,0 @@ -cant_write_here \ No newline at end of file diff --git a/test/spec/NativeFileSystem-test-files/dir1/file2 b/test/spec/NativeFileSystem-test-files/dir1/file2 deleted file mode 100644 index b14ef684bb1..00000000000 --- a/test/spec/NativeFileSystem-test-files/dir1/file2 +++ /dev/null @@ -1 +0,0 @@ -this is file2 in dir1 diff --git a/test/spec/NativeFileSystem-test-files/emptydir/placeholder b/test/spec/NativeFileSystem-test-files/emptydir/placeholder deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/test/spec/NativeFileSystem-test-files/file1 b/test/spec/NativeFileSystem-test-files/file1 deleted file mode 100644 index 1294121351b..00000000000 --- a/test/spec/NativeFileSystem-test-files/file1 +++ /dev/null @@ -1 +0,0 @@ -Here is file1 \ No newline at end of file diff --git a/test/spec/NativeFileSystem-test.js b/test/spec/NativeFileSystem-test.js deleted file mode 100644 index 4902d2d0625..00000000000 --- a/test/spec/NativeFileSystem-test.js +++ /dev/null @@ -1,856 +0,0 @@ -/* - * Copyright (c) 2012 Adobe Systems Incorporated. All rights reserved. - * - * Permission is hereby granted, free of charge, to any person obtaining a - * copy of this software and associated documentation files (the "Software"), - * to deal in the Software without restriction, including without limitation - * the rights to use, copy, modify, merge, publish, distribute, sublicense, - * and/or sell copies of the Software, and to permit persons to whom the - * Software is furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - * DEALINGS IN THE SOFTWARE. - * - */ - - -/*jslint vars: true, plusplus: true, devel: true, browser: true, nomen: true, indent: 4, maxerr: 50 */ -/*global brackets, $, define, describe, it, xit, expect, beforeEach, afterEach, FileError, waitsFor, waitsForDone, waitsForFail, runs */ - -define(function (require, exports, module) { - 'use strict'; - - require("utils/Global"); - - // Load dependent modules - var NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, - NativeFileError = require("file/NativeFileError"), - SpecRunnerUtils = require("spec/SpecRunnerUtils"); - - var Encodings = NativeFileSystem.Encodings; - var _FSEncodings = NativeFileSystem._FSEncodings; - - describe("NativeFileSystem", function () { - - var _err; - - beforeEach(function () { - var self = this; - - SpecRunnerUtils.createTempDirectory(); - - runs(function () { - var testFiles = SpecRunnerUtils.getTestPath("/spec/NativeFileSystem-test-files"); - self.path = SpecRunnerUtils.getTempDirectory(); - - waitsForDone(SpecRunnerUtils.copyPath(testFiles, self.path)); - }); - - runs(function () { - self.file1content = "Here is file1"; - }); - }); - - describe("Checking file path", function () { - - it("should return false on absolute path", function () { - var deferred = new $.Deferred(), - isRelative = true, - path; - - //Set the correct path for the platform - if (brackets.platform === "win") { - path = "C:\\an\\absolute\\path"; - } else if (brackets.platform === "mac" || brackets.platform === "linux") { - //Mac and Linux will have the same path type - path = "/an/absolute/path"; - } - - runs(function () { - isRelative = NativeFileSystem.isRelativePath(path); - deferred.resolve(); - waitsForDone(deferred, "isRelativePath", 2000); - }); - - runs(function () { - expect(isRelative).toBe(false); - }); - }); - - it("should return true on relative path", function () { - var deferred = new $.Deferred(), - isRelative = false, - path; - - //Set the correct path for the platform - if (brackets.platform === "win") { - path = "a\\relative\\path"; - } else if (brackets.platform === "mac" || brackets.platform === "linux") { - //Mac and Linux will have the same path type - path = "a/relative/path"; - } - - runs(function () { - isRelative = NativeFileSystem.isRelativePath(path); - deferred.resolve(); - waitsForDone(deferred, "isRelativePath", 2000); - }); - - runs(function () { - expect(isRelative).toBe(true); - }); - }); - }); - - afterEach(function () { - SpecRunnerUtils.removeTempDirectory(); - }); - - describe("Reading a directory", function () { - - it("should read a directory from disk", function () { - var entries = null, - deferred = new $.Deferred(); - - function requestNativeFileSystemSuccessCB(nfs) { - var reader = nfs.root.createReader(); - - var successCallback = function (e) { entries = e; deferred.resolve(); }; - var errorCallback = function () { deferred.reject(); }; - - reader.readEntries(successCallback, errorCallback); - } - - runs(function () { - NativeFileSystem.requestNativeFileSystem(this.path, requestNativeFileSystemSuccessCB); - waitsForDone(deferred, "requestNativeFileSystem", 2000); - }); - - runs(function () { - expect(entries.some(function (element) { - return (element.isDirectory && element.name === "dir1"); - })).toBe(true); - - expect(entries.some(function (element) { - return (element.isFile && element.name === "file1"); - })).toBe(true); - - expect(entries.some(function (element) { - return (element.isFile && element.name === "file2"); - })).toBe(false); - }); - }); - - // This test is intermittently failing on the build machine, and - // *only* on the build machine. Removing for now. Issue - // #2333 logged to resolve this. - xit("should be able to read a drive", function () { - var entries = null, - deferred = new $.Deferred(); - - function requestNativeFileSystemSuccessCB(nfs) { - var reader = nfs.root.createReader(); - - var successCallback = function (e) { entries = e; deferred.resolve(); }; - var errorCallback = function () { deferred.reject(); }; - - reader.readEntries(successCallback, errorCallback); - } - - var drivePath = this.path.substr(0, this.path.indexOf("/") + 1); - - runs(function () { - NativeFileSystem.requestNativeFileSystem(drivePath, requestNativeFileSystemSuccessCB); - waitsForDone(deferred, "requestNativeFileSystem", 10000); - }); - - runs(function () { - expect(entries).toBeTruthy(); - }); - }); - - it("should return an error if the directory doesn't exist", function () { - var deferred = new $.Deferred(), - error; - - runs(function () { - NativeFileSystem.requestNativeFileSystem(this.path + '/nonexistent-dir', function (data) { - deferred.resolve(); - }, function (err) { - error = err; - deferred.reject(); - }); - - waitsForFail(deferred, "requestNativeFileSystem", 2000); - }); - - runs(function () { - expect(error.name).toBe(NativeFileError.NOT_FOUND_ERR); - }); - }); - - it("should return an error if you pass a bad parameter", function () { - var deferred = new $.Deferred(), - error; - - runs(function () { - NativeFileSystem.requestNativeFileSystem( - 0xDEADBEEF, - function (data) { - deferred.resolve(); - }, - function (err) { - error = err; - deferred.reject(); - } - ); - - waitsForFail(deferred); - }); - - runs(function () { - expect(error.name).toBe(NativeFileError.SECURITY_ERR); - }); - }); - - it("should be okay to not pass an error callback", function () { - var deferred = new $.Deferred(), - entries = null; - - runs(function () { - NativeFileSystem.requestNativeFileSystem(this.path, function (data) { - entries = data; - deferred.resolve(); - }); - - waitsForDone(deferred); - }); - - runs(function () { - expect(entries).toBeTruthy(); - }); - }); - - it("can read an empty folder", function () { - // TODO: (issue #241): Implement DirectoryEntry.getDirectory() and remove this empty folder workaround. - // We need an empty folder for testing in this spec. Unfortunately, it's impossible - // to check an empty folder in to git, and we don't have low level fs calls to create an emtpy folder. - // So, for now, we have a folder called "emptydir" which contains a single 0-length file called - // "placeholder". We delete that file at the beginning of each test, and then recreate it at the end. - // - // If we add NativeFileSystem commands to create a folder, we should change this test to simply create - // a new folder (rather than remove a placeholder, etc.) - var deferred = new $.Deferred(), - entries = null, - accessedFolder = false, - placeholderDeleted = false, - gotErrorReadingContents = false, - placeholderRecreated = false, - dirPath = this.path + "/emptydir", - placeholderPath = dirPath + "/placeholder"; - - function requestNativeFileSystemSuccessCB(nfs) { - accessedFolder = true; - - function recreatePlaceholder(deferred) { - nfs.root.getFile("placeholder", - { create: true, exclusive: true }, - function () { placeholderRecreated = true; deferred.resolve(); }, - function () { placeholderRecreated = false; deferred.reject(); }); - } - - function readDirectory() { - var reader = nfs.root.createReader(); - var successCallback = function (e) { - entries = e; - recreatePlaceholder(deferred); - }; - var errorCallback = function () { - gotErrorReadingContents = true; - recreatePlaceholder(deferred); - }; - reader.readEntries(successCallback, errorCallback); - } - - - function deletePlaceholder(successCallback) { - // TODO: (issue #241): implement FileEntry.remove() - // once NativeFileSystem has a delete/unlink, should use that - brackets.fs.unlink(placeholderPath, function (err) { - if (!err) { - placeholderDeleted = true; - } - // Even if there was an error, we want to read the directory - // because it could be that the placeholder is just missing. - // If we continue, we'll create the placeholder and the test - // will (maybe) pass next time - readDirectory(); - }); - } - - deletePlaceholder(); // which calls readDirectory which calls recreatePlaceholder - - } - - runs(function () { - NativeFileSystem.requestNativeFileSystem( - dirPath, - requestNativeFileSystemSuccessCB, - function () { deferred.reject(); } - ); - - waitsForDone(deferred, "requestNativeFileSystem", 2000); - }); - - runs(function () { - expect(accessedFolder).toBe(true); - expect(placeholderDeleted).toBe(true); - expect(gotErrorReadingContents).toBe(false); - expect(entries).toEqual([]); - expect(placeholderRecreated).toBe(true); - }); - }); - - it("should timeout with error when reading dir if low-level stat call takes too long", function () { - var statCalled = false, readComplete = false, gotError = false, theError = null; - var oldStat = brackets.fs.stat; - - function requestNativeFileSystemSuccessCB(nfs) { - var reader = nfs.root.createReader(); - - var successCallback = function (e) { readComplete = true; }; - var errorCallback = function (error) { readComplete = true; gotError = true; theError = error; }; - - // mock up new low-level stat that never calls the callback - brackets.fs.stat = function (path, callback) { - statCalled = true; - - // Can't do this as a spy or as a spec.after() because - // after each callbacks (like SpecRunnerUtils.removeTempDirecotry) - // will see the mock function still. - // https://github.com/pivotal/jasmine/issues/236 - brackets.fs.stat = oldStat; - }; - - reader.readEntries(successCallback, errorCallback); - } - - runs(function () { - NativeFileSystem.requestNativeFileSystem(this.path, requestNativeFileSystemSuccessCB); - }); - - waitsFor(function () { return readComplete; }, "DirectoryReader.readEntries timeout", NativeFileSystem.ASYNC_TIMEOUT * 2); - - runs(function () { - expect(readComplete).toBe(true); - expect(statCalled).toBe(true); - expect(gotError).toBe(true); - expect(theError.name).toBe(NativeFileError.SECURITY_ERR); - }); - }); - }); - - describe("Reading a file", function () { - - var readFile = function (encoding) { - return function () { - var gotFile = false, readFile = false, gotError = false, content; - var fileEntry = new NativeFileSystem.FileEntry(this.path + "/file1"); - fileEntry.file(function (file) { - gotFile = true; - var reader = new NativeFileSystem.FileReader(); - reader.onload = function (event) { - readFile = true; - content = event.target.result; - }; - reader.onerror = function (event) { - gotError = true; - }; - reader.readAsText(file, encoding); - }); - - waitsFor(function () { return gotFile && readFile; }, 1000); - - runs(function () { - expect(gotFile).toBe(true); - expect(readFile).toBe(true); - expect(gotError).toBe(false); - expect(content).toBe(this.file1content); - }); - }; - }; - - it("should read a file from disk", readFile(Encodings.UTF8)); - it("should read a file from disk with lower case encoding", readFile(Encodings.UTF8.toLowerCase())); - it("should read a file from disk with upper case encoding", readFile(Encodings.UTF8.toUpperCase())); - - it("should return an error if the file is not found", function () { - var deferred = new $.Deferred(), - errorName; - - runs(function () { - var fileEntry = new NativeFileSystem.FileEntry(this.path + "/idontexist"); - fileEntry.file(function (file) { - var reader = new NativeFileSystem.FileReader(); - reader.onload = function (event) { - deferred.resolve(); - }; - reader.onerror = function (event) { - errorName = event.target.error.name; - deferred.reject(); - }; - reader.readAsText(file, Encodings.UTF8); - }); - - waitsForFail(deferred, "readAsText"); - }); - - runs(function () { - expect(errorName).toBe(NativeFileError.NOT_FOUND_ERR); - }); - }); - - it("should fire appropriate events when the file is done loading", function () { - var gotFile = false, gotLoad = false, gotLoadStart = false, gotLoadEnd = false, - gotProgress = false, gotError = false, gotAbort = false; - - runs(function () { - var fileEntry = new NativeFileSystem.FileEntry(this.path + "/file1"); - fileEntry.file(function (file) { - gotFile = true; - var reader = new NativeFileSystem.FileReader(); - reader.onload = function (event) { - gotLoad = true; - }; - reader.onloadstart = function (event) { - gotLoadStart = true; - }; - reader.onloadend = function (event) { - gotLoadEnd = true; - }; - reader.onprogress = function (event) { - gotProgress = true; - }; - reader.onerror = function (event) { - gotError = true; - }; - reader.onabort = function (event) { - gotAbort = true; - }; - reader.readAsText(file, Encodings.UTF8); - }); - }); - - waitsFor(function () { return gotLoad && gotLoadEnd && gotProgress; }, 1000); - - runs(function () { - expect(gotFile).toBe(true); - expect(gotLoadStart).toBe(true); - expect(gotLoad).toBe(true); - expect(gotLoadEnd).toBe(true); - expect(gotProgress).toBe(true); - expect(gotError).toBe(false); - expect(gotAbort).toBe(false); - }); - }); - - it("should return an error but not crash if you create a bad FileEntry", function () { - var gotFile = false, readFile = false, gotError = false; - - runs(function () { - var fileEntry = new NativeFileSystem.FileEntry(null); - fileEntry.file(function (file) { - gotFile = true; - var reader = new NativeFileSystem.FileReader(); - reader.onload = function (event) { - readFile = true; - }; - reader.onerror = function (event) { - gotError = true; - }; - reader.readAsText(file, Encodings.UTF8); - }); - }); - - waitsFor(function () { return gotError; }, 1000); - - runs(function () { - expect(gotFile).toBe(true); - expect(readFile).toBe(false); - expect(gotError).toBe(true); - }); - }); - }); - - describe("Writing", function () { - - beforeEach(function () { - var nfs = null; - - runs(function () { - NativeFileSystem.requestNativeFileSystem(this.path, function (fs) { - nfs = fs; - }); - }); - waitsFor(function () { return nfs; }, 1000); - - runs(function () { - this.nfs = nfs; - }); - - // set permissions - runs(function () { - waitsForDone(SpecRunnerUtils.chmod(this.path + "/cant_read_here.txt", "222")); - waitsForDone(SpecRunnerUtils.chmod(this.path + "/cant_write_here.txt", "444")); - }); - }); - - afterEach(function () { - // restore permissions - runs(function () { - waitsForDone(SpecRunnerUtils.chmod(this.path + "/cant_read_here.txt", "644")); - waitsForDone(SpecRunnerUtils.chmod(this.path + "/cant_write_here.txt", "644")); - }); - }); - - it("should create new, zero-length files", function () { - var fileEntry = null; - var writeComplete = false; - - // create a new file exclusively - runs(function () { - var successCallback = function (entry) { - fileEntry = entry; - writeComplete = true; - }; - var errorCallback = function () { - writeComplete = true; - }; - - this.nfs.root.getFile("new-zero-length-file.txt", { create: true, exclusive: true }, successCallback, errorCallback); - }); - - waitsFor(function () { return writeComplete; }, 1000); - - // fileEntry is non-null on success - runs(function () { - expect(fileEntry).toBeTruthy(); - }); - - var actualContents = null; - - // read the new file - runs(function () { - brackets.fs.readFile(fileEntry.fullPath, _FSEncodings.UTF8, function (err, contents) { - actualContents = contents; - }); - }); - - // wait for content to be read - waitsFor(function () { return (actualContents !== null); }, 1000); - - // verify actual content to be empty - var cleanupComplete = false; - runs(function () { - expect(actualContents).toEqual(""); - - // cleanup - var self = this; - brackets.fs.unlink(fileEntry.fullPath, function (err) { - cleanupComplete = (err === brackets.fs.NO_ERROR); - }); - }); - - waitsFor(function () { return cleanupComplete; }, 1000); - }); - - it("should report an error when a file does not exist and create = false", function () { - var fileEntry = null; - var writeComplete = false; - var error = null; - - // create a new file exclusively - runs(function () { - var successCallback = function (entry) { - fileEntry = entry; - writeComplete = true; - }; - var errorCallback = function (err) { - error = err; - writeComplete = true; - }; - - this.nfs.root.getFile("does-not-exist.txt", { create: false }, successCallback, errorCallback); - }); - - waitsFor(function () { return writeComplete; }, 1000); - - // fileEntry is null on error - runs(function () { - expect(fileEntry).toBe(null); - expect(error.name).toBe(NativeFileError.NOT_FOUND_ERR); - }); - }); - - it("should return an error if file exists and exclusive is true", function () { - var fileEntry = null; - var writeComplete = false; - var error = null; - - // try to create a new file exclusively when the file name already exists - runs(function () { - var successCallback = function (entry) { - fileEntry = entry; - writeComplete = true; - }; - var errorCallback = function (err) { - error = err; - writeComplete = true; - }; - - this.nfs.root.getFile("file1", { create: true, exclusive: true }, successCallback, errorCallback); - }); - - // wait for success or error to return - waitsFor(function () { return writeComplete; }, 1000); - - runs(function () { - // fileEntry will be null when errorCallback is handled - expect(fileEntry).toBe(null); - - // errorCallback should be called with PATH_EXISTS_ERR - expect(error.name).toEqual(NativeFileError.PATH_EXISTS_ERR); - }); - }); - - it("should return an error if the path is a directory", function () { - var fileEntry = null; - var writeComplete = false; - var error = null; - - // try to write to a path that is a directory instead of a file - runs(function () { - var successCallback = function (entry) { - fileEntry = entry; - writeComplete = true; - }; - var errorCallback = function (err) { - error = err; - writeComplete = true; - }; - - this.nfs.root.getFile("dir1", { create: false }, successCallback, errorCallback); - }); - - // wait for success or error to return - waitsFor(function () { return writeComplete; }, 1000); - - runs(function () { - // fileEntry will be null when errorCallback is handled - expect(fileEntry).toBe(null); - - // errorCallback should be called with TYPE_MISMATCH_ERR - expect(error.name).toEqual(NativeFileError.TYPE_MISMATCH_ERR); - }); - }); - - it("should create overwrite files with new content", function () { - var fileEntry = null; - var writeComplete = false; - var error = null; - - runs(function () { - var successCallback = function (entry) { - fileEntry = entry; - - fileEntry.createWriter(function (fileWriter) { - fileWriter.onwriteend = function (e) { - writeComplete = true; - }; - fileWriter.onerror = function (err) { - writeComplete = true; - }; - - // TODO (issue #241): BlobBulder - fileWriter.write("FileWriter.write"); - }); - }; - var errorCallback = function () { - writeComplete = true; - }; - - this.nfs.root.getFile("file1", { create: false }, successCallback, errorCallback); - }); - - waitsFor(function () { return writeComplete && fileEntry; }, 1000); - - var actualContents = null; - - runs(function () { - brackets.fs.readFile(fileEntry.fullPath, _FSEncodings.UTF8, function (err, contents) { - actualContents = contents; - }); - }); - - waitsFor(function () { return !!actualContents; }, 1000); - - var rewriteComplete = false; - - runs(function () { - expect(actualContents).toEqual("FileWriter.write"); - - // reset file1 content - // reset file1 content - brackets.fs.writeFile(this.path + "/file1", this.file1content, _FSEncodings.UTF8, function () { - rewriteComplete = true; - }); - }); - - waitsFor(function () { return rewriteComplete; }, 1000); - }); - - - it("should write an empty file", function () { - var fileEntry = null; - var writeComplete = false; - var error = null; - - runs(function () { - var successCallback = function (entry) { - fileEntry = entry; - - fileEntry.createWriter(function (fileWriter) { - fileWriter.onwriteend = function (e) { - writeComplete = true; - }; - fileWriter.onerror = function (err) { - writeComplete = true; - error = err; - }; - - // TODO (jasonsj): BlobBulder - fileWriter.write(""); - }); - }; - var errorCallback = function () { - writeComplete = true; - }; - - this.nfs.root.getFile("file1", { create: false }, successCallback, errorCallback); - }); - - waitsFor(function () { return writeComplete && fileEntry; }, 1000); - - var actualContents = null; - - runs(function () { - brackets.fs.readFile(fileEntry.fullPath, _FSEncodings.UTF8, function (err, contents) { - actualContents = contents; - }); - }); - - waitsFor(function () { return (actualContents !== null); }, 1000); - - var rewriteComplete = false; - - runs(function () { - expect(actualContents).toEqual(""); - - // reset file1 content - brackets.fs.writeFile(this.path + "/file1", this.file1content, _FSEncodings.UTF8, function () { - rewriteComplete = true; - }); - }); - - waitsFor(function () { return rewriteComplete; }, 1000); - }); - - // This is Mac only because the chmod implementation on Windows supports disallowing write via - // FILE_ATTRIBUTE_READONLY, but does not support disallowing read. - it("should report an error when writing to a file that cannot be read (Mac only)", function () { - if (brackets.platform !== "mac") { - return; - } - - var complete = false; - var error = null; - - // createWriter() should return an error for files it can't read - runs(function () { - this.nfs.root.getFile( - "cant_read_here.txt", - { create: false }, - function (entry) { - entry.createWriter( - function () { complete = true; }, - function (err) { error = err; } - ); - } - ); - }); - waitsFor(function () { return complete || error; }, 1000); - - runs(function () { - expect(complete).toBeFalsy(); - expect(error.name).toBe(NativeFileError.NOT_READABLE_ERR); - }); - }); - - it("should report an error when writing to a file that cannot be written", function () { - var writeComplete = false; - var error = null; - - runs(function () { - var successCallback = function (entry) { - entry.createWriter(function (fileWriter) { - fileWriter.onwriteend = function (e) { - writeComplete = true; - }; - fileWriter.onerror = function (err) { - writeComplete = true; - error = err; - }; - - // TODO (issue #241): BlobBulder - fileWriter.write("FileWriter.write"); - }); - }; - var errorCallback = function () { - writeComplete = true; - }; - - this.nfs.root.getFile("cant_write_here.txt", { create: false }, successCallback, errorCallback); - }); - - // fileWriter.onerror handler should be invoked for read only files - waitsFor( - function () { - return writeComplete && error && - (error.name === NativeFileError.NO_MODIFICATION_ALLOWED_ERR); - }, - 1000 - ); - }); - - xit("should append to existing files", function () { - this.fail("TODO (issue #241): not supported for sprint 1"); - }); - - xit("should seek into a file before writing", function () { - this.fail("TODO (issue #241): not supported for sprint 1"); - }); - - xit("should truncate files", function () { - this.fail("TODO (issue #241): not supported for sprint 1"); - }); - }); - }); -}); diff --git a/test/spec/ProjectManager-test.js b/test/spec/ProjectManager-test.js index 60ac37d5625..91a9fc9c31b 100644 --- a/test/spec/ProjectManager-test.js +++ b/test/spec/ProjectManager-test.js @@ -30,9 +30,11 @@ define(function (require, exports, module) { var ProjectManager, // Load from brackets.test CommandManager, // Load from brackets.test + FileSystem, // Load from brackets.test Dialogs = require("widgets/Dialogs"), DefaultDialogs = require("widgets/DefaultDialogs"), Commands = require("command/Commands"), + FileSystemError = require("filesystem/FileSystemError"), SpecRunnerUtils = require("spec/SpecRunnerUtils"); @@ -52,6 +54,7 @@ define(function (require, exports, module) { brackets = testWindow.brackets; ProjectManager = testWindow.brackets.test.ProjectManager; CommandManager = testWindow.brackets.test.CommandManager; + FileSystem = testWindow.brackets.test.FileSystem; SpecRunnerUtils.loadProjectInTestWindow(testPath); }); @@ -80,8 +83,10 @@ define(function (require, exports, module) { var error, stat, complete = false; var filePath = testPath + "/Untitled.js"; + var file = FileSystem.getFileForPath(filePath); + runs(function () { - brackets.fs.stat(filePath, function (err, _stat) { + file.stat(function (err, _stat) { error = err; stat = _stat; complete = true; @@ -90,21 +95,21 @@ define(function (require, exports, module) { waitsFor(function () { return complete; }, 1000); - var unlinkError = brackets.fs.NO_ERROR; + var unlinkError = null; runs(function () { expect(error).toBeFalsy(); - expect(stat.isFile()).toBe(true); + expect(stat.isFile).toBe(true); // delete the new file complete = false; - brackets.fs.unlink(filePath, function (err) { + file.unlink(function (err) { unlinkError = err; complete = true; }); }); waitsFor( function () { - return complete && (unlinkError === brackets.fs.NO_ERROR); + return complete && (unlinkError === null); }, "unlink() failed to cleanup Untitled.js, err=" + unlinkError, 1000 @@ -224,7 +229,7 @@ define(function (require, exports, module) { describe("deleteItem", function () { it("should delete the selected file in the project tree", function () { var complete = false, - newFileName = testPath + "/delete_me.js", + newFile = FileSystem.getFileForPath(testPath + "/delete_me.js"), selectedFile, error, stat; @@ -233,7 +238,7 @@ define(function (require, exports, module) { // by explicitly deleting the test file if it exists. runs(function () { complete = false; - brackets.fs.unlink(newFileName, function (err) { + newFile.unlink(function (err) { complete = true; }); }); @@ -249,7 +254,7 @@ define(function (require, exports, module) { runs(function () { complete = false; - brackets.fs.stat(newFileName, function (err, _stat) { + newFile.stat(function (err, _stat) { error = err; stat = _stat; complete = true; @@ -260,7 +265,7 @@ define(function (require, exports, module) { // Verify the existence of the new file and make sure it is selected in the project tree. runs(function () { expect(error).toBeFalsy(); - expect(stat.isFile()).toBe(true); + expect(stat.isFile).toBe(true); selectedFile = ProjectManager.getSelectedItem(); expect(selectedFile.fullPath).toBe(testPath + "/delete_me.js"); }); @@ -278,7 +283,7 @@ define(function (require, exports, module) { // Verify that file no longer exists. runs(function () { complete = false; - brackets.fs.stat(newFileName, function (err, _stat) { + newFile.stat(function (err, _stat) { error = err; stat = _stat; complete = true; @@ -287,7 +292,7 @@ define(function (require, exports, module) { waitsFor(function () { return complete; }, 1000); runs(function () { - expect(error).toBe(brackets.fs.ERR_NOT_FOUND); + expect(error).toBe(FileSystemError.NOT_FOUND); // Verify that some other file is selected in the project tree. var curSelectedFile = ProjectManager.getSelectedItem(); @@ -306,8 +311,9 @@ define(function (require, exports, module) { // Make sure we don't have any test files/folders left from previous failure // by explicitly deleting the root test folder if it exists. runs(function () { + var rootFolder = FileSystem.getDirectoryForPath(rootFolderName); complete = false; - brackets.fs.moveToTrash(rootFolderName, function (err) { + rootFolder.moveToTrash(function (err) { complete = true; }); }); @@ -322,8 +328,9 @@ define(function (require, exports, module) { waitsFor(function () { return complete; }, "ProjectManager.createNewItem() timeout", 1000); runs(function () { + var newFolder = FileSystem.getDirectoryForPath(newFolderName); complete = false; - brackets.fs.stat(newFolderName, function (err, _stat) { + newFolder.stat(function (err, _stat) { error = err; stat = _stat; complete = true; @@ -333,7 +340,7 @@ define(function (require, exports, module) { runs(function () { expect(error).toBeFalsy(); - expect(stat.isDirectory()).toBe(true); + expect(stat.isDirectory).toBe(true); rootFolderEntry = ProjectManager.getSelectedItem(); expect(rootFolderEntry.fullPath).toBe(testPath + "/toDelete/"); @@ -349,8 +356,10 @@ define(function (require, exports, module) { runs(function () { newFolderName += "toDelete1/"; + + var newFolder = FileSystem.getDirectoryForPath(newFolderName); complete = false; - brackets.fs.stat(newFolderName, function (err, _stat) { + newFolder.stat(function (err, _stat) { error = err; stat = _stat; complete = true; @@ -360,7 +369,7 @@ define(function (require, exports, module) { runs(function () { expect(error).toBeFalsy(); - expect(stat.isDirectory()).toBe(true); + expect(stat.isDirectory).toBe(true); }); // Create a file in the sub folder just created. @@ -372,8 +381,9 @@ define(function (require, exports, module) { waitsFor(function () { return complete; }, "ProjectManager.createNewItem() timeout", 1000); runs(function () { + var file = FileSystem.getFileForPath(newFolderName + "/toDelete2.txt"); complete = false; - brackets.fs.stat(newFolderName + "toDelete2.txt", function (err, _stat) { + file.stat(function (err, _stat) { error = err; stat = _stat; complete = true; @@ -383,7 +393,7 @@ define(function (require, exports, module) { runs(function () { expect(error).toBeFalsy(); - expect(stat.isFile()).toBe(true); + expect(stat.isFile).toBe(true); }); // Delete the root folder and all files/folders in it. @@ -397,8 +407,9 @@ define(function (require, exports, module) { // Verify that the root folder no longer exists. runs(function () { + var rootFolder = FileSystem.getDirectoryForPath(rootFolderName); complete = false; - brackets.fs.stat(rootFolderName, function (err, _stat) { + rootFolder.stat(function (err, _stat) { error = err; stat = _stat; complete = true; @@ -407,7 +418,7 @@ define(function (require, exports, module) { waitsFor(function () { return complete; }, 1000); runs(function () { - expect(error).toBe(brackets.fs.ERR_NOT_FOUND); + expect(error).toBe(FileSystemError.NOT_FOUND); // Verify that some other file is selected in the project tree. var curSelectedFile = ProjectManager.getSelectedItem(); diff --git a/test/spec/SpecRunnerUtils.js b/test/spec/SpecRunnerUtils.js index a5797a8efb4..ba397d76003 100644 --- a/test/spec/SpecRunnerUtils.js +++ b/test/spec/SpecRunnerUtils.js @@ -27,13 +27,14 @@ define(function (require, exports, module) { 'use strict'; - var NativeFileSystem = require("file/NativeFileSystem").NativeFileSystem, - Commands = require("command/Commands"), + var Commands = require("command/Commands"), FileUtils = require("file/FileUtils"), Async = require("utils/Async"), DocumentManager = require("document/DocumentManager"), Editor = require("editor/Editor").Editor, EditorManager = require("editor/EditorManager"), + FileSystemError = require("filesystem/FileSystemError"), + FileSystem = require("filesystem/FileSystem"), PanelManager = require("view/PanelManager"), ExtensionLoader = require("utils/ExtensionLoader"), UrlParams = require("utils/UrlParams").UrlParams, @@ -45,6 +46,7 @@ define(function (require, exports, module) { OPEN_TAG = "{{", CLOSE_TAG = "}}", RE_MARKER = /\{\{(\d+)\}\}/g, + absPathPrefix = (brackets.platform === "win" ? "c:/" : "/"), _testSuites = {}, _testWindow, _doLoadExtensions, @@ -61,17 +63,29 @@ define(function (require, exports, module) { */ function deletePath(fullPath, silent) { var result = new $.Deferred(); - - brackets.fs.unlink(fullPath, function (err) { - // ignore ERR_NOT_FOUND errors - if (!err || (err === brackets.fs.ERR_NOT_FOUND && silent)) { - result.resolve(); + FileSystem.resolve(fullPath, function (err, item) { + if (!err) { + item.unlink(function (err) { + if (!err) { + result.resolve(); + } else { + if (err === FileSystemError.NOT_FOUND && silent) { + result.resolve(); + } else { + console.error("Unable to remove " + fullPath, err); + result.reject(err); + } + } + }); } else { - console.error("unable to remove " + fullPath + " Error code " + err); - result.reject(err); + if (err === FileSystemError.NOT_FOUND && silent) { + result.resolve(); + } else { + console.error("Unable to remove " + fullPath, err); + result.reject(err); + } } }); - return result.promise(); } @@ -84,7 +98,7 @@ define(function (require, exports, module) { */ function chmod(path, mode) { var deferred = new $.Deferred(); - + brackets.fs.chmod(path, parseInt(mode, 8), function (err) { if (err) { deferred.reject(err); @@ -127,25 +141,23 @@ define(function (require, exports, module) { /** - * Resolves a path string to a FileEntry or DirectoryEntry + * Resolves a path string to a File or Directory * @param {!string} path Path to a file or directory * @return {$.Promise} A promise resolved when the file/directory is found or * rejected when any error occurs. */ function resolveNativeFileSystemPath(path) { - var deferred = new $.Deferred(); + var result = new $.Deferred(); - NativeFileSystem.resolveNativeFileSystemPath( - path, - function success(entry) { - deferred.resolve(entry); - }, - function error(domError) { - deferred.reject(); + FileSystem.resolve(path, function (err, item) { + if (!err) { + result.resolve(item); + } else { + result.reject(err); } - ); + }); - return deferred.promise(); + return result.promise(); } @@ -186,10 +198,6 @@ define(function (require, exports, module) { function getRoot() { var deferred = new $.Deferred(); - if (nfs) { - deferred.resolve(nfs.root); - } - resolveNativeFileSystemPath("/").then(deferred.resolve, deferred.reject); return deferred.promise(); @@ -222,8 +230,8 @@ define(function (require, exports, module) { var deferred = new $.Deferred(); runs(function () { - brackets.fs.makedir(getTempDirectory(), 0, function (err) { - if (err && err !== brackets.fs.ERR_FILE_EXISTS) { + var dir = FileSystem.getDirectoryForPath(getTempDirectory()).create(function (err) { + if (err && err !== FileSystemError.ALREADY_EXISTS) { deferred.reject(err); } else { deferred.resolve(); @@ -234,23 +242,6 @@ define(function (require, exports, module) { waitsForDone(deferred, "Create temp directory", 500); } - /** - * @private - */ - function _stat(pathname) { - var promise = new $.Deferred(); - - brackets.fs.stat(pathname, function (err, _stat) { - if (err === brackets.fs.NO_ERROR) { - promise.resolve(_stat); - } else { - promise.reject(err); - } - }); - - return promise; - } - function _resetPermissionsOnSpecialTempFolders() { var i, folders = [], @@ -263,19 +254,19 @@ define(function (require, exports, module) { promise = Async.doSequentially(folders, function (folder) { var deferred = new $.Deferred(); - _stat(folder) - .done(function () { + FileSystem.resolve(folder, function (err, entry) { + if (!err) { // Change permissions if the directory exists - chmod(folder, 777).then(deferred.resolve, deferred.reject); - }) - .fail(function (err) { - if (err === brackets.fs.ERR_NOT_FOUND) { + chmod(folder, "777").then(deferred.resolve, deferred.reject); + } else { + if (err === FileSystemError.NOT_FOUND) { // Resolve the promise since the folder to reset doesn't exist deferred.resolve(); } else { deferred.reject(); } - }); + } + }); return deferred.promise(); }, true); @@ -325,11 +316,11 @@ define(function (require, exports, module) { */ function createMockActiveDocument(options) { var language = options.language || LanguageManager.getLanguage("javascript"), - filename = options.filename || "_unitTestDummyFile_" + Date.now() + "." + language._fileExtensions[0], + filename = options.filename || (absPathPrefix + "_unitTestDummyPath_/_dummyFile_" + Date.now() + "." + language._fileExtensions[0]), content = options.content || ""; // Use unique filename to avoid collissions in open documents list - var dummyFile = new NativeFileSystem.FileEntry(filename); + var dummyFile = FileSystem.getFileForPath(filename); var docToShim = new DocumentManager.Document(dummyFile, new Date(), content); // Prevent adding doc to working set @@ -572,7 +563,7 @@ define(function (require, exports, module) { var result = _testWindow.brackets.test.ProjectManager.openProject(path); // wait for file system to finish loading - waitsForDone(result, "ProjectManager.openProject()"); + waitsForDone(result, "ProjectManager.openProject()", 10000); }); } @@ -686,7 +677,7 @@ define(function (require, exports, module) { /** * Parses offsets from a file using offset markup (e.g. "{{1}}" for offset 1). - * @param {!FileEntry} entry File to open + * @param {!File} entry File to open * @return {$.Promise} A promise resolved with a record that contains parsed offsets, * the file text without offset markup, the original file content, and the corresponding * file entry. @@ -746,23 +737,20 @@ define(function (require, exports, module) { * Create or overwrite a text file * @param {!string} path Path for a file to be created/overwritten * @param {!string} text Text content for the new file + * @param {!FileSystem} fileSystem FileSystem instance to use. Normally, use the instance from + * testWindow so the test copy of Brackets is aware of the newly-created file. * @return {$.Promise} A promise resolved when the file is written or rejected when an error occurs. */ - function createTextFile(path, text) { - var deferred = new $.Deferred(); - - getRoot().done(function (nfs) { - // create the new FileEntry - nfs.getFile(path, { create: true }, function success(entry) { - // write text this new FileEntry - FileUtils.writeText(entry, text).done(function () { - deferred.resolve(entry); - }).fail(function () { - deferred.reject(); - }); - }, function error(err) { + function createTextFile(path, text, fileSystem) { + var deferred = new $.Deferred(), + file = fileSystem.getFileForPath(path); + + file.write(text, function (err) { + if (!err) { + deferred.resolve(file); + } else { deferred.reject(err); - }); + } }); return deferred.promise(); @@ -770,7 +758,7 @@ define(function (require, exports, module) { /** * Copy a file source path to a destination - * @param {!FileEntry} source Entry for the source file to copy + * @param {!File} source Entry for the source file to copy * @param {!string} destination Destination path to copy the source file * @param {?{parseOffsets:boolean}} options parseOffsets allows optional * offset markup parsing. File is written to the destination path @@ -796,8 +784,8 @@ define(function (require, exports, module) { offsets = parseInfo.offsets; } - // create the new FileEntry - createTextFile(destination, text).done(function (entry) { + // create the new File + createTextFile(destination, text, FileSystem).done(function (entry) { deferred.resolve(entry, offsets, text); }).fail(function (err) { deferred.reject(err); @@ -812,7 +800,7 @@ define(function (require, exports, module) { /** * Copy a directory source to a destination - * @param {!DirectoryEntry} source Entry for the source directory to copy + * @param {!Directory} source Directory for the source directory to copy * @param {!string} destination Destination path to copy the source directory * @param {?{parseOffsets:boolean, infos:Object, removePrefix:boolean}}} options * parseOffsets - allows optional offset markup parsing. File is written to the @@ -833,54 +821,53 @@ define(function (require, exports, module) { var parseOffsets = options.parseOffsets || false, removePrefix = options.removePrefix || true, - deferred = new $.Deferred(); + deferred = new $.Deferred(), + destDir = FileSystem.getDirectoryForPath(destination); // create the destination folder - brackets.fs.makedir(destination, parseInt("644", 8), function callback(err) { - if (err && err !== brackets.fs.ERR_FILE_EXISTS) { + destDir.create(function (err) { + if (err && err !== FileSystemError.ALREADY_EXISTS) { deferred.reject(); return; } - source.createReader().readEntries(function handleEntries(entries) { - if (entries.length === 0) { - deferred.resolve(); - return; - } - - // copy all children of this directory - var copyChildrenPromise = Async.doInParallel( - entries, - function copyChild(child) { - var childDestination = destination + "/" + child.name, - promise; - - if (child.isDirectory) { - promise = copyDirectoryEntry(child, childDestination, options); - } else { - promise = copyFileEntry(child, childDestination, options); + source.getContents(function (err, contents) { + if (!err) { + // copy all children of this directory + var copyChildrenPromise = Async.doInParallel( + contents, + function copyChild(child) { + var childDestination = destination + "/" + child.name, + promise; - if (parseOffsets) { - // save offset data for each file path - promise.done(function (destinationEntry, offsets, text) { - options.infos[childDestination] = { - offsets : offsets, - fileEntry : destinationEntry, - text : text - }; - }); + if (child.isDirectory) { + promise = copyDirectoryEntry(child, childDestination, options); + } else { + promise = copyFileEntry(child, childDestination, options); + + if (parseOffsets) { + // save offset data for each file path + promise.done(function (destinationEntry, offsets, text) { + options.infos[childDestination] = { + offsets : offsets, + fileEntry : destinationEntry, + text : text + }; + }); + } } + + return promise; } - - return promise; - }, - true - ); - - copyChildrenPromise.then(deferred.resolve, deferred.reject); + ); + + copyChildrenPromise.then(deferred.resolve, deferred.reject); + } else { + deferred.reject(err); + } }); }); - + deferred.always(function () { // remove destination path prefix if (removePrefix && options.infos) { @@ -1027,8 +1014,7 @@ define(function (require, exports, module) { } return message; } - - + /** * Searches the DOM tree for text containing the given content. Useful for verifying * that data you expect to show up in the UI somewhere is actually there.