diff --git a/.gitmodules b/.gitmodules index a38f27ab4c9..32bb7eae68e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -25,3 +25,9 @@ [submodule "src/extensions/default/JSLint/thirdparty/jslint"] path = src/extensions/default/JSLint/thirdparty/jslint url = https://github.com/peterflynn/JSLint.git +[submodule "src/thirdparty/makedrive"] + path = src/thirdparty/makedrive + url = https://github.com/mozilla/makedrive.git +[submodule "src/extensions/default/makedrive-sync-icon"] + path = src/extensions/default/makedrive-sync-icon + url = git@github.com:cdot-brackets-extensions/makedrive-sync-icon.git diff --git a/README.md b/README.md index f2d74109ed1..7c1e2df2185 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,9 @@ -Welcome to Brackets! [![Build Status](https://travis-ci.org/adobe/brackets.svg?branch=master)](https://travis-ci.org/adobe/brackets) -------------------- +# Bramble is based on Brackets Brackets is a modern open-source code editor for HTML, CSS and JavaScript that's *built* in HTML, CSS and JavaScript. -What makes Brackets different from other web code editors? - -* **Tools shouldn't get in your way.** Instead of cluttering up your coding -environment with lots of panels and icons, the Quick Edit UI in Brackets puts -context-specific code and tools inline. -* **Brackets is in sync with your browser.** With Live Preview, Brackets -works directly with your browser to push code edits instantly and jump -back and forth between your real source code and the browser view. -* **Do it yourself.** Because Brackets is open source, and built with HTML, CSS -and JavaScript, you can [help build](https://github.com/adobe/brackets/blob/master/CONTRIBUTING.md) the best code editor for the web. - -Brackets may have reached version 1, but we're not stopping there. We have many feature ideas on our +Brackets is at 1.0 and we're not stopping there. We have many feature ideas on our [trello board](http://bit.ly/BracketsTrelloBoard) that we're anxious to add and other innovative web development workflows that we're planning to build into Brackets. So take Brackets out for a spin and let us know how we can make it your favorite editor. @@ -24,96 +12,47 @@ You can see some [screenshots of Brackets](https://github.com/adobe/brackets/wiki/Brackets-Screenshots) on the wiki, [intro videos](http://www.youtube.com/user/CodeBrackets) on YouTube, and news on the [Brackets blog](http://blog.brackets.io/). -How to install and run Brackets -------------------------------- -#### Download - -Installers for the latest stable build for Mac, Windows and Linux (Debian/Ubuntu) can be [downloaded here](http://brackets.io/). - -The Linux version has most of the features of the Mac and Windows versions, but -is still missing a few things. See the [Linux wiki page](https://github.com/adobe/brackets/wiki/Linux-Version) -for a list of known issues and to find out how you can help. - -#### Usage - -By default, Brackets opens a folder containing some simple "Getting Started" content. -You can choose a different folder to edit using *File > Open Folder*. - -Most of Brackets should be pretty self-explanatory, but for information on how -to use its unique features, like Quick Edit and Live Preview, please read -[How to Use Brackets](http://github.com/adobe/brackets/wiki/How-to-Use-Brackets). -Also, see the [release notes](http://github.com/adobe/brackets/wiki/Release-Notes) -for a list of new features and known issues in each build. - -In addition to the core features built into Brackets, there is a large and growing -community of developers building extensions that add all sorts of useful functionality. -See the [Brackets Extension Registry](https://brackets-registry.aboutweb.com/) -for a list of available extensions. For installation instructions, -see the [extensions wiki page](https://github.com/adobe/brackets/wiki/Brackets-Extensions). - -#### Need help? - -Having problems starting Brackets the first time, or not sure how to use Brackets? Please -review [Troubleshooting](https://github.com/adobe/brackets/wiki/Troubleshooting), which helps -you to fix common problems and find extra help if needed. - - -Helping Brackets ----------------- - -#### I found a bug! - -If you found a repeatable bug, and [troubleshooting](https://github.com/adobe/brackets/wiki/Troubleshooting) -tips didn't help, then be sure to [search existing issues](https://github.com/adobe/brackets/issues) first. -Include steps to consistently reproduce the problem, actual vs. expected results, screenshots, and your OS and -Brackets version number. Disable all extensions to verify the issue is a core Brackets bug. -[Read more guidelines for filing good bugs.](https://github.com/adobe/brackets/wiki/How-to-Report-an-Issue) - - -#### I have a new suggestion, but don't know how to program! - -For feature requests please first check our [Trello board](http://bit.ly/BracketsBacklog) to -see if it's already there; you can upvote it if so. If not, feel free to file it as an issue as above; we'll -move it to the feature backlog for you. - - -#### I want to help with the code! - -Awesome! _There are lots of ways you can help._ First read -[CONTRIBUTING.md](https://github.com/adobe/brackets/blob/master/CONTRIBUTING.md), -then learn how to [pull the repo and hack on Brackets](https://github.com/adobe/brackets/wiki/How-to-Hack-on-Brackets). - The text editor inside Brackets is based on [CodeMirror](http://github.com/codemirror/CodeMirror)—thanks to Marijn for taking our pull requests, implementing feature requests and fixing bugs! See [Notes on CodeMirror](https://github.com/adobe/brackets/wiki/Notes-on-CodeMirror) for info on how we're using CodeMirror. -Although Brackets is built in HTML/CSS/JS, it currently runs as a desktop -application in a thin native shell, so that it can access your local files. -(If you just try to open the index.html file in a browser, it won't work yet.) -The native shell for Brackets lives in a separate repo, -[adobe/brackets-shell](https://github.com/adobe/brackets-shell/). +# How to setup Bramble (Brackets) in your local machine + +Step 01: Make sure you fork and clone [Bramble](https://github.com/humphd/brackets). + +``` +$ git clone https://github.com/[yourusername]/brackets --recursive +``` + +Step 02: Install its dependencies + +Navigate to the root of the directory you cloned and run + +``` +$ npm install +``` +``` +$ git submodule update --init +``` -I want to keep track of how Brackets is doing! ----------------------------------------------- +``` +Grunt commands to be added +``` -Not sure you needed the exclamation point there, but we like your enthusiasm. +Step 03: Run Bramble: -#### What's Brackets working on next? +Run one of the suggested local servers (or your own) from the root directory of Bramble: +* [Apache Webserver](http://www.apache.org/) +* Host on [github pages](https://help.github.com/articles/what-are-github-pages) +* [Python WebServer](https://docs.python.org/2/library/simplehttpserver.html) -* In our [feature backlog](http://bit.ly/BracketsBacklog), the columns to the right - (starting from "Development") list the features that we're currently working on. - "Ready" shows what we'll be working on next. -* Watch our [GitHub activity stream](https://github.com/adobe/brackets/pulse). -* Watch our [Waffle Kanban board](https://waffle.io/adobe/brackets): Work items in [![Stories in Ready](https://badge.waffle.io/adobe/brackets.svg?label=ready&title=Ready)](http://waffle.io/adobe/brackets) are next. The entire development process is outlined in the [Developer Guide](https://github.com/adobe/brackets/wiki/Brackets-Developers-Guide). +Assuming you have Bramble running on port `8080`. Now you can visit [http://localhost:8080/src](http://localhost:8080/src). -#### Contact info +-------------- -* **Slack:** [Brackets on Slack](https://brackets.slack.com) (You can join by [requesting an invite](https://brackets-slack.herokuapp.com/)) -* **Developers mailing list:** http://groups.google.com/group/brackets-dev -* **Twitter:** [@brackets](https://twitter.com/brackets) -* **Blog:** http://blog.brackets.io/ -* **IRC:** [#brackets on freenode](http://webchat.freenode.net/?channels=brackets) +## After installation +After you have everything setup, you can now run the server you chose in the root of your local Bramble directory and see it in action by visiting [http://localhost:8080/src](http://localhost:8080/src). diff --git a/src/brackets.js b/src/brackets.js index f9e01523cd2..982a40963be 100644 --- a/src/brackets.js +++ b/src/brackets.js @@ -211,15 +211,6 @@ define(function (require, exports, module) { function _onReady() { PerfUtils.addMeasurement("window.document Ready"); - // Let the user know Brackets doesn't run in a web browser yet - if (brackets.inBrowser) { - Dialogs.showModalDialog( - DefaultDialogs.DIALOG_ID_ERROR, - Strings.ERROR_IN_BROWSER_TITLE, - Strings.ERROR_IN_BROWSER - ); - } - // Use quiet scrollbars if we aren't on Lion. If we're on Lion, only // use native scroll bars when the mouse is not plugged in or when // using the "Always" scroll bar setting. diff --git a/src/extensions/default/makedrive-sync-icon b/src/extensions/default/makedrive-sync-icon new file mode 160000 index 00000000000..82bab57ae85 --- /dev/null +++ b/src/extensions/default/makedrive-sync-icon @@ -0,0 +1 @@ +Subproject commit 82bab57ae85d5cf26fc76bfb89a5de53cdb7b778 diff --git a/src/filesystem/impls/makedrive/MakeDriveFileSystem.js b/src/filesystem/impls/makedrive/MakeDriveFileSystem.js new file mode 100644 index 00000000000..2449641e3a2 --- /dev/null +++ b/src/filesystem/impls/makedrive/MakeDriveFileSystem.js @@ -0,0 +1,354 @@ +/*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 FileSystemError = require("filesystem/FileSystemError"), + FileSystemStats = require("filesystem/FileSystemStats"), + Dialogs = require("widgets/Dialogs"), + DefaultDialogs = require("widgets/DefaultDialogs"), + // TODO: we have to figure out how we're going to build/deploy makedrive.js, this is hacky. + // since it requires a manual `grunt build` step in src/thirdparty/makedrive + MakeDrive = require("thirdparty/makedrive/client/dist/makedrive"), + OpenDialog = require("filesystem/impls/makedrive/open-dialog"); + + var fs = MakeDrive.fs(), + Path = MakeDrive.Path, + watchers = {}; + + var _changeCallback; // Callback to notify FileSystem of watcher changes + + // Give extensions access to MakeDrive's sync functionality. The hosting app + // needs to call sync.connect(serverURL) when the user is logged in, for example. + appshell.MakeDrive = MakeDrive; + + var sync = fs.sync; + + // Try to upgrade to a syncing filesystem + sync.connect('ws://localhost:9090'); + + //TODO: Do we want to do anything other than console.log for all these events? + sync.on('syncing', function() { + console.log('sync started'); + }); + sync.on('error', function(e) { + console.log('sync error: ', e); + }); + sync.on('completed', function() { + console.log('sync completed'); + }); + sync.on('updates', function() { + console.log('server has updates'); + }); + + function showOpenDialog(allowMultipleSelection, chooseDirectories, title, initialPath, fileTypes, callback) { + OpenDialog.showOpenDialog.apply(null, arguments); + } + + function showSaveDialog(title, initialPath, defaultName, callback) { + var selectedPath; + var saveResponse = window.prompt(title, defaultName); + if(saveResponse){ + initialPath = initialPath || '/'; + selectedPath = initialPath + saveResponse; + callback(null, selectedPath); + } + else{ + callback(); + } + } + + /** + * Convert Filer error codes to FileSystemError values. + * + * @param {?number} err A Filer error code + * @return {?string} A FileSystemError string, or null if there was no error code. + * @private + **/ + function _mapError(err) { + if (!err) { + return null; + } + + switch (err.code) { + case 'EINVAL': + return FileSystemError.INVALID_PARAMS; + case 'ENOENT': + return FileSystemError.NOT_FOUND; + case 'EROFS': + return FileSystemError.NOT_WRITABLE; + case 'ENOSPC': + return FileSystemError.OUT_OF_SPACE; + case 'ENOTEMPTY': + case 'EEXIST': + return FileSystemError.ALREADY_EXISTS; + case 'ENOTDIR': + return FileSystemError.INVALID_PARAMS; + case 'EBADF': + return FileSystemError.NOT_READABLE; + } + + return FileSystemError.UNKNOWN; + } + + /** + * Convert a callback to one that transforms its first parameter from a + * Filer error code to a FileSystemError string. + * + * @param {function(?number)} cb A callback that expects an Filer error code + * @return {function(?string)} A callback that expects a FileSystemError string + * @private + **/ + function _wrap(cb) { + return function (err) { + var args = Array.prototype.slice.call(arguments); + args[0] = _mapError(args[0]); + cb.apply(null, args); + }; + } + + function stat(path, callback) { + fs.stat(path, function(err, stats) { + if (err){ + callback(_mapError(err)); + return; + } + + var mtime = new Date(stats.mtime); + + var options = { + isFile: stats.isFile(), + mtime: mtime, + size: stats.size, + // TODO: figure out how to deal with realPath + realPath: path, + hash: mtime.getTime() + }; + + var fsStats = new FileSystemStats(options); + + callback(null, fsStats); + }); + } + + + function exists(path, callback) { + fs.exists(path, function(exists) { + callback(null, exists); + }); + } + + function readdir(path, callback) { + path = Path.normalize(path); + + 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.join(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; + } + + fs.mkdir(path, mode, function (err) { + if (err) { + callback(_mapError(err)); + return; + } + stat(path, callback); + }); + } + + function rename(oldPath, newPath, callback) { + fs.rename(oldPath, newPath, _wrap(callback)); + } + + function readFile(path, options, callback) { + if(typeof options === 'function') { + callback = options; + } + options = options || {}; + + var encoding = options.encoding || "utf8"; + + // Execute the read and stat calls in parallel. Callback early if the + // read call completes first with an error; otherwise wait for both + // to finish. + var done = false, data, stat, err; + + if (options.stat) { + done = true; + stat = options.stat; + } else { + exports.stat(path, function (_err, _stat) { + if (done) { + callback(_err, _err ? null : data, _stat); + } else { + done = true; + stat = _stat; + err = _err; + } + }); + } + + 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; + } + }); + } + + function writeFile(path, data, options, callback) { + if(typeof options === 'function') { + callback = options; + } + options = options || {}; + + var encoding = options.encoding || "utf8"; + + function _finishWrite(created) { + fs.writeFile(path, data, encoding, function (err) { + if (err) { + callback(_mapError(err)); + return; + } + stat(path, function (err, stat) { + callback(err, stat, created); + }); + }); + } + + stat(path, function (err, stats) { + if (err) { + switch (err) { + case FileSystemError.NOT_FOUND: + _finishWrite(true); + break; + default: + callback(err); + } + return; + } + + if (options.hasOwnProperty("expectedHash") && options.expectedHash !== stats._hash) { + console.error("Blind write attempted: ", path, stats._hash, options.expectedHash); + + if (options.hasOwnProperty("expectedContents")) { + fs.readFile(path, encoding, function (_err, _data) { + if (_err || _data !== options.expectedContents) { + callback(FileSystemError.CONTENTS_MODIFIED); + return; + } + + _finishWrite(false); + }); + return; + } else { + callback(FileSystemError.CONTENTS_MODIFIED); + return; + } + } + + _finishWrite(false); + }); + } + + function unlink(path, callback) { + fs.unlink(path, function(err){ + callback(_mapError(err)); + }); + } + + function moveToTrash(path, callback) { + // TODO: do we want to support a .trash/ dir or the like? + unlink(path, callback); + } + + function initWatchers(changeCallback, offlineCallback) { + _changeCallback = changeCallback; + } + + function watchPath(path, callback) { + path = Path.normalize(path); + + if(watchers[path]) { + return; + } + watchers[path] = fs.watch(path, {recursive: true}, function(event, filename) { + stat(filename, function(err, stats) { + if(err) { + return; + } + _changeCallback(filename, stats); + }); + }); + callback(); + } + + function unwatchPath(path, callback) { + path = Path.normalize(path); + + if(watchers[path]) { + watchers[path].close(); + delete watchers[path]; + } + callback(); + } + + function unwatchAll(callback) { + Object.keys(watchers).forEach(function(path) { + unwatchPath(path, function(){}); + }); + callback(); + } + + // Export public API + 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; + + exports.recursiveWatch = true; + exports.normalizeUNCPaths = false; +}); diff --git a/src/filesystem/impls/makedrive/MakeDriveFileSystem.js~ b/src/filesystem/impls/makedrive/MakeDriveFileSystem.js~ new file mode 100644 index 00000000000..cc7a612ab69 --- /dev/null +++ b/src/filesystem/impls/makedrive/MakeDriveFileSystem.js~ @@ -0,0 +1,316 @@ +/*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 FileSystemError = require("filesystem/FileSystemError"), + FileSystemStats = require("filesystem/FileSystemStats"), + Dialogs = require("widgets/Dialogs"), + DefaultDialogs = require("widgets/DefaultDialogs"), + Filer = require("thirdparty/filer/dist/filer.js"); + + var fs = new Filer.FileSystem({ provider: new Filer.FileSystem.providers.Fallback() }), + Path = Filer.Path, + watchers = {}; + + var _changeCallback; // Callback to notify FileSystem of watcher changes + + function showOpenDialog(allowMultipleSelection, chooseDirectories, title, initialPath, fileTypes, callback) { + // FIXME: Here we need to create a new dialog that at least + // lists all files/folders on filesystem + // require("text!htmlContent/filesystem-dialog.html"); + // 17:51 pflynn: oh, you can store .html templates in your extension + // itself... you can load them with require() and then just pass + // them to Dialogs.showModalDialogUsingTemplate() driectly + throw new Error(); + } + + function showSaveDialog(title, initialPath, x, callback) { + // FIXME + throw new Error(); + } + + /** + * Strip trailing slash (or multiple slashes) from a path + */ + function stripTrailingSlash(path) { + return path.replace(/\/*$/, ''); + } + + /** + * Convert Filer error codes to FileSystemError values. + * + * @param {?number} err A Filer error code + * @return {?string} A FileSystemError string, or null if there was no error code. + * @private + **/ + function _mapError(err) { + if (!err) { + return null; + } + + switch (err.code) { + case 'EINVAL': + return FileSystemError.INVALID_PARAMS; + case 'ENOENT': + return FileSystemError.NOT_FOUND; + case 'EROFS': + return FileSystemError.NOT_WRITABLE; + case 'ENOSPC': + return FileSystemError.OUT_OF_SPACE; + case 'ENOTEMPTY': + case 'EEXIST': + return FileSystemError.ALREADY_EXISTS; + case 'ENOTDIR': + return FileSystemError.INVALID_PARAMS; + case 'EBADF': + return FileSystemError.NOT_READABLE; + } + + return FileSystemError.UNKNOWN; + } + + /** + * Convert a callback to one that transforms its first parameter from a + * Filer error code to a FileSystemError string. + * + * @param {function(?number)} cb A callback that expects an Filer error code + * @return {function(?string)} A callback that expects a FileSystemError string + * @private + **/ + function _wrap(cb) { + return function (err) { + var args = Array.prototype.slice.call(arguments); + args[0] = _mapError(args[0]); + cb.apply(null, args); + }; + } + + function stat(path, callback) { + fs.stat(path, function(err, stats) { + if (err){ + callback(_mapError(err)); + return; + } + + var mtime = new Date(stats.mtime); + + var options = { + isFile: stats.isFile(), + mtime: mtime, + size: stats.size, + // TODO: figure out how to deal with realPath + realPath: path, + hash: mtime.getTime() + }; + + var fsStats = new FileSystemStats(options); + + callback(null, fsStats); + }); + } + + + function exists(path, callback) { + fs.exists(path, function(exists) { + callback(null, exists); + }); + } + + function readdir(path, callback) { + path = stripTrailingSlash(path); + 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.join(path, val), function (err, stat) { + stats[idx] = err || stat; + count--; + if (count <= 0) { + callback(null, contents, stats); + } + }); + }); + }); + } + + function mkdir(path, mode, callback) { + fs.mkdir(path, mode, function (err) { + if (err) { + callback(_mapError(err)); + return; + } + stat(path, callback); + }); + } + + function rename(oldPath, newPath, callback) { + fs.rename(oldPath, newPath, _wrap(callback)); + } + + function readFile(path, options, callback) { + var encoding = options.encoding || "utf8"; + + // Execute the read and stat calls in parallel. Callback early if the + // read call completes first with an error; otherwise wait for both + // to finish. + var done = false, data, stat, err; + + if (options.stat) { + done = true; + stat = options.stat; + } else { + exports.stat(path, function (_err, _stat) { + if (done) { + callback(_err, _err ? null : data, _stat); + } else { + done = true; + stat = _stat; + err = _err; + } + }); + } + + 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; + } + }); + } + + function writeFile(path, data, options, callback) { + var encoding = options.encoding || "utf8"; + + function _finishWrite(created) { + fs.writeFile(path, data, encoding, function (err) { + if (err) { + callback(_mapError(err)); + return; + } + stat(path, function (err, stat) { + callback(err, stat, created); + }); + }); + } + + stat(path, function (err, stats) { + if (err) { + switch (err) { + case FileSystemError.NOT_FOUND: + _finishWrite(true); + break; + default: + callback(err); + } + return; + } + + if (options.hasOwnProperty("expectedHash") && options.expectedHash !== stats._hash) { + console.error("Blind write attempted: ", path, stats._hash, options.expectedHash); + + if (options.hasOwnProperty("expectedContents")) { + fs.readFile(path, encoding, function (_err, _data) { + if (_err || _data !== options.expectedContents) { + callback(FileSystemError.CONTENTS_MODIFIED); + return; + } + + _finishWrite(false); + }); + return; + } else { + callback(FileSystemError.CONTENTS_MODIFIED); + return; + } + } + + _finishWrite(false); + }); + } + + function unlink(path, callback) { + fs.unlink(path, function(err){ + callback(_mapError(err)); + }); + } + + function moveToTrash(path, callback) { + // TODO: do we want to support a .trash/ dir or the like? + unlink(path, callback); + } + + function initWatchers(changeCallback, offlineCallback) { + _changeCallback = changeCallback; + } + + function watchPath(path, callback) { + // Strip trailing slash on pathname (probably /brackets/) + path = path.replace(/\/$/, ''); + if(watchers[path]) { + return; + } + watchers[path] = fs.watch(path, {recursive: true}, function(event, filename) { + stat(filename, function(err, stats) { + if(err) { + return; + } + _changeCallback(filename, stats); + }); + }); + callback(); + } + + function unwatchPath(path, callback) { + if(watchers[path]) { + watchers[path].close(); + delete watchers[path]; + } + callback(); + } + + function unwatchAll(callback) { + Object.keys(watchers).forEach(function(path) { + unwatchPath(path, function(){}); + }); + callback(); + } + + // Export public API + 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; + + exports.recursiveWatch = true; + exports.normalizeUNCPaths = false; +}); diff --git a/src/filesystem/impls/makedrive/SampleFileSystem.js b/src/filesystem/impls/makedrive/SampleFileSystem.js new file mode 100644 index 00000000000..792b4d4bc37 --- /dev/null +++ b/src/filesystem/impls/makedrive/SampleFileSystem.js @@ -0,0 +1,38 @@ +/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */ +/*global define, appshell, $, window */ + +// Generate a sample set of files and folders + +define(function (require, exports, module) { + "use strict"; + + exports.ensureSampleFileSystem = function(fs) { + + var create = function createFileIfMissing(path, contents, callback) { + callback = callback || function(){}; + fs.exists(path, function(exists) { + if(exists) { + return callback(null); + } + + fs.writeFile(path, contents, callback); + }); + }; + + create('/index.html', '\n\n\n

Why hello there, Brackets running in a browser!

\n\n'); + create('/styles.css', 'p { color: green; }'); + +/** + fs.mkdir('/project1', function() { + create('/project1/index.html', ''); +// create('/project1/code.js', require('text!filesystem/impls/makedrive/MakeDriveFileSystem.js')); + }); + + fs.mkdir('/dialogs', function() { + create('/dialogs/open-dialog.html', require('text!filesystem/impls/makedrive/open-dialog.html')); + create('/dialogs/open-dialog.js', require('text!filesystem/impls/makedrive/open-dialog.js')); + }); + +**/ + }; +}); diff --git a/src/filesystem/impls/makedrive/open-dialog.html b/src/filesystem/impls/makedrive/open-dialog.html new file mode 100644 index 00000000000..27ed305e5c5 --- /dev/null +++ b/src/filesystem/impls/makedrive/open-dialog.html @@ -0,0 +1,14 @@ + diff --git a/src/filesystem/impls/makedrive/open-dialog.js b/src/filesystem/impls/makedrive/open-dialog.js new file mode 100644 index 00000000000..e9278afb305 --- /dev/null +++ b/src/filesystem/impls/makedrive/open-dialog.js @@ -0,0 +1,132 @@ +/*global define, $, brackets, Mustache, console */ +define(function (require, exports, module) { + "use strict"; + + var _ = require("thirdparty/lodash"); + var Dialogs = require("widgets/Dialogs"); + var FileSystem = require("filesystem/FileSystem"); + var ViewUtils = brackets.getModule("utils/ViewUtils"); + var openDialog = require("text!filesystem/impls/makedrive/open-dialog.html"); + + var nodeId = 0; + + function fileToTreeJSON(file) { + var json = { + data: file.name, + attr: { id: "node" + nodeId++ }, + metadata: { + file: file + } + }; + + if (file.isDirectory) { + json.children = []; + json.state = "closed"; + json.data = _.escape(json.data); + } else { + json.data = ViewUtils.getFileEntryDisplay(file); + } + + return json; + } + + // TODO: support all args here + function showOpenDialog(allowMultipleSelection, chooseDirectories, title, initialPath, fileTypes, callback) { + + function initializeEventHandlers($dialog) { + $dialog.find(".dialog-button[data-button-id='cancel']") + .on("click", closeModal); + + $(window).on('keydown.makedrive', function (event) { + if (event.keyCode === 27) { + closeModal(); + } + }); + + $dialog.find(".dialog-button[data-button-id='open']").on("click", function () { + var paths = $dialog.find('.jstree-clicked') + .closest('li') + .map(function() { + return $(this).data().file.fullPath; + }) + .get(); + + if (!paths.length) { return; } + + closeModal(); + callback(null, paths); + }); + } + + function fileTreeDataProvider($tree, callback) { + var directory; + + // $tree is -1 when requesting the root + if ($tree === -1) { + directory = FileSystem.getDirectoryForPath(initialPath); + } else { + directory = $tree.data('file'); + } + + directory.getContents(function(err, files) { + var json = files.map(fileToTreeJSON); + callback(json); + }); + } + + function handleFileDoubleClick(event) { + var file = $(event.target).closest('li').data('file'); + + if (file && file.isFile) { + callback(null, [file.fullPath]); + closeModal(); + } + } + + function closeModal() { + if(dialog) { + dialog.close(); + } + } + + var data = { + title: title, + cancel: "Cancel", + open: "Open" + }; + + var dialog = Dialogs.showModalDialogUsingTemplate(Mustache.render(openDialog, data), false); + + var $dialog = $(".makedrive.instance"); + + initializeEventHandlers($dialog); + + var $container = $dialog.find('.open-files-container'); + + var jstree = $container.jstree({ + plugins: ["ui", "themes", "json_data", "crrm"], + json_data: { + data: fileTreeDataProvider, + correct_state: false }, + core: { + html_titles: true, + animation: 0, + strings : { + loading : "Loading", + new_node : "New node" + } + }, + themes: { + theme: "brackets", + url: "styles/jsTreeTheme.css", + dots: false, + icons: false + } + }); + + jstree.on('dblclick.jstree', handleFileDoubleClick); + } + + exports.showOpenDialog = showOpenDialog; + +}); diff --git a/src/index.html b/src/index.html index a8bbba5ccdd..3315166efd6 100644 --- a/src/index.html +++ b/src/index.html @@ -46,6 +46,7 @@ + diff --git a/src/main.js b/src/main.js index 27298fd08b1..3c5dc7d1501 100644 --- a/src/main.js +++ b/src/main.js @@ -35,7 +35,7 @@ require.config({ // The file system implementation. Change this value to use different // implementations (e.g. cloud-based storage). - "fileSystemImpl" : "filesystem/impls/appshell/AppshellFileSystem" + "fileSystemImpl" : "filesystem/impls/makedrive/MakeDriveFileSystem" }, map: { "*": { diff --git a/src/project/ProjectManager.js b/src/project/ProjectManager.js index 8e8e3826e57..a848df26009 100644 --- a/src/project/ProjectManager.js +++ b/src/project/ProjectManager.js @@ -640,7 +640,9 @@ define(function (require, exports, module) { * @return {!string} fullPath reference */ function _getWelcomeProjectPath() { - return ProjectModel._getWelcomeProjectPath(Urls.GETTING_STARTED, FileUtils.getNativeBracketsDirectoryPath()); + // XXXhumphd: just use root dir for now in browser + return '/'; + //return ProjectModel._getWelcomeProjectPath(Urls.GETTING_STARTED, FileUtils.getNativeBracketsDirectoryPath()); } /** @@ -729,7 +731,9 @@ define(function (require, exports, module) { * first launch. */ function getInitialProjectPath() { - return updateWelcomeProjectPath(PreferencesManager.getViewState("projectPath")); + // XXXhumphd: just use root dir for now in browser + return '/'; + //return updateWelcomeProjectPath(PreferencesManager.getViewState("projectPath")); } /** @@ -889,73 +893,71 @@ define(function (require, exports, module) { PreferencesManager._stateProjectLayer.setProjectPath(rootPath); } - // 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 - var rootEntry = FileSystem.getDirectoryForPath(rootPath); - rootEntry.exists(function (err, exists) { - if (exists) { - var projectRootChanged = (!model.projectRoot || !rootEntry) || - model.projectRoot.fullPath !== rootEntry.fullPath; - - // Success! - var perfTimerName = PerfUtils.markStart("Load Project: " + rootPath); - - _projectWarnedForTooManyFiles = false; - - _setProjectRoot(rootEntry).always(function () { - model.setBaseUrl(PreferencesManager.getViewState("project.baseUrl", context) || ""); - - if (projectRootChanged) { - _reloadProjectPreferencesScope(); - PreferencesManager._setCurrentFile(rootPath); - } + // Populate file tree + if (!isUpdating) { + _watchProjectRoot(rootPath); + } + // Point at a real folder structure on local disk + var rootEntry = FileSystem.getDirectoryForPath(rootPath); + rootEntry.exists(function (err, exists) { + if (exists) { + var projectRootChanged = (!model.projectRoot || !rootEntry) || + model.projectRoot.fullPath !== rootEntry.fullPath; + + // Success! + var perfTimerName = PerfUtils.markStart("Load Project: " + rootPath); + + _projectWarnedForTooManyFiles = false; + + _setProjectRoot(rootEntry).always(function () { + model.setBaseUrl(PreferencesManager.getViewState("project.baseUrl", context) || ""); + + if (projectRootChanged) { + _reloadProjectPreferencesScope(); + PreferencesManager._setCurrentFile(rootPath); + } - // If this is the most current welcome project, record it. In future launches, we want - // to substitute the latest welcome project from the current build instead of using an - // outdated one (when loading recent projects or the last opened project). - if (rootPath === _getWelcomeProjectPath()) { - addWelcomeProjectPath(rootPath); - } + // If this is the most current welcome project, record it. In future launches, we want + // to substitute the latest welcome project from the current build instead of using an + // outdated one (when loading recent projects or the last opened project). + if (rootPath === _getWelcomeProjectPath()) { + addWelcomeProjectPath(rootPath); + } - if (projectRootChanged) { - // Allow asynchronous event handlers to finish before resolving result by collecting promises from them - exports.trigger("projectOpen", model.projectRoot); - result.resolve(); - } else { - exports.trigger("projectRefresh", model.projectRoot); - result.resolve(); - } - PerfUtils.addMeasurement(perfTimerName); - }); - } else { - console.log("error loading project"); - _showErrorDialog(ERR_TYPE_LOADING_PROJECT_NATIVE, true, err || FileSystemError.NOT_FOUND, rootPath) - .done(function () { - // Reset _projectRoot to null so that the following _loadProject call won't - // run the 'beforeProjectClose' event a second time on the original project, - // which is now partially torn down (see #6574). - model.projectRoot = null; - - // The project folder stored in preference doesn't exist, so load the default - // project directory. - // TODO (issue #267): When Brackets supports having no project directory - // defined this code will need to change - _getFallbackProjectPath().done(function (path) { - _loadProject(path).always(function () { - // Make sure not to reject the original deferred until the fallback - // project is loaded, so we don't violate expectations that there is always - // a current project before continuing after _loadProject(). - result.reject(); - }); + if (projectRootChanged) { + // Allow asynchronous event handlers to finish before resolving result by collecting promises from them + exports.trigger("projectOpen", model.projectRoot); + result.resolve(); + } else { + exports.trigger("projectRefresh", model.projectRoot); + result.resolve(); + } + PerfUtils.addMeasurement(perfTimerName); + }); + } else { + console.log("error loading project"); + _showErrorDialog(ERR_TYPE_LOADING_PROJECT_NATIVE, true, err || FileSystemError.NOT_FOUND, rootPath) + .done(function () { + // Reset _projectRoot to null so that the following _loadProject call won't + // run the 'beforeProjectClose' event a second time on the original project, + // which is now partially torn down (see #6574). + model.projectRoot = null; + + // The project folder stored in preference doesn't exist, so load the default + // project directory. + // TODO (issue #267): When Brackets supports having no project directory + // defined this code will need to change + _getFallbackProjectPath().done(function (path) { + _loadProject(path).always(function () { + // Make sure not to reject the original deferred until the fallback + // project is loaded, so we don't violate expectations that there is always + // a current project before continuing after _loadProject(). + result.reject(); }); }); - } - }); - } + }); + } + }); }); return result.promise(); diff --git a/src/thirdparty/browser-appshell.js b/src/thirdparty/browser-appshell.js new file mode 100644 index 00000000000..5ce95f09bbc --- /dev/null +++ b/src/thirdparty/browser-appshell.js @@ -0,0 +1,116 @@ +/** + * Browser shim for Brackets AppShell API. See: + * https://github.com/adobe/brackets-shell + * https://github.com/adobe/brackets-shell/blob/adf27a65d501fae71cc6d8905d6dc3d9460208a9/appshell/appshell_extensions.js + * http://www.hyperlounge.com/blog/hacking-the-brackets-shell/ + */ + +(function(global, navigator) { + + var startupTime = Date.now(); + function getElapsedMilliseconds() { + return (Date.now()) - startupTime; + } + + function functionNotImplemented() { + throw "Function not (yet) implemented in browser-appshell."; + } + + // Provide an implementation of appshell.app as expected by src/utils/Global.js + // and other parts of the code. + var appshell = global.appshell = global.appshell || {}; + + appshell.app = { + ERR_NODE_FAILED: -3, + ERR_NODE_NOT_YET_STARTED: -1, + ERR_NODE_PORT_NOT_YET_SET: -2, + NO_ERROR: 0, + + // TODO: deal with getter *and* setter + language: navigator.language, + + abortQuit: function() { + functionNotImplemented(); + }, + addMenu: function(title, id, position, relativeId, callback) { + functionNotImplemented(); + }, + addMenuItem: function(parentId, title, id, key, displayStr, position, relativeId, callback) { + functionNotImplemented(); + }, + closeLiveBrowser: function(callback) { + functionNotImplemented(); + }, + dragWindow: function() { + functionNotImplemented(); + }, + getApplicationSupportDirectory: function() { + return '/ApplicationSupport'; + }, + getDroppedFiles: function(callback) { + functionNotImplemented(); + }, + getElapsedMilliseconds: getElapsedMilliseconds, + getMenuItemState: function(commandid, callback) { + functionNotImplemented(); + }, + getMenuPosition: function(commandId, callback) { + functionNotImplemented(); + }, + getMenuTitle: function(commandid, callback) { + functionNotImplemented(); + }, + getPendingFilesToOpen: function(callback) { + // No files are passed to the app on startup, unless we want to support + // URL based params with a list. For now return an empty array. + callback(null, []); + }, + getRemoteDebuggingPort: function() { + functionNotImplemented(); + }, + getUserDocumentsDirectory: function() { + functionNotImplemented(); + }, + openLiveBrowser: function(url, enableRemoteDebugging, callback) { + functionNotImplemented(); + }, + openURLInDefaultBrowser: function(url, callback) { + functionNotImplemented(); + }, + quit: function() { + functionNotImplemented(); + }, + removeMenu: function(commandId, callback) { + functionNotImplemented(); + }, + removeMenuItem: function(commandId, callback) { + functionNotImplemented(); + }, + setMenuItemShortcut: function(commandId, shortcut, displayStr, callback) { + functionNotImplemented(); + }, + setMenuItemState: function(commandid, enabled, checked, callback) { + functionNotImplemented(); + }, + setMenuTitle: function(commandid, title, callback) { + functionNotImplemented(); + }, + showDeveloperTools: function() { + functionNotImplemented(); + }, + showExtensionsFolder: function(appURL, callback) { + functionNotImplemented(); + }, + showOSFolder: function(path, callback) { + functionNotImplemented(); + }, + + // https://github.com/adobe/brackets-shell/blob/959836be7a3097e2ea3298729ebd616247c83dce/appshell/appshell_node_process.h#L30 + getNodeState: function(callback) { + callback(/* BRACKETS_NODE_FAILED = -3 */ -3); + } + }; + + global.brackets = appshell; + +}(window, window.navigator)); diff --git a/src/thirdparty/makedrive b/src/thirdparty/makedrive new file mode 160000 index 00000000000..3ab4cdbc14a --- /dev/null +++ b/src/thirdparty/makedrive @@ -0,0 +1 @@ +Subproject commit 3ab4cdbc14a44c0221d4e58e59697a5847c3e6c9 diff --git a/src/utils/ExtensionLoader.js b/src/utils/ExtensionLoader.js index 154bd7254e7..5f4252a688c 100644 --- a/src/utils/ExtensionLoader.js +++ b/src/utils/ExtensionLoader.js @@ -391,6 +391,42 @@ define(function (require, exports, module) { return new $.Deferred().resolve().promise(); } + // Load *subset* of the usual builtin extensions list, and don't try to find any user/dev extensions + if (brackets.inBrowser) { + var basePath = PathUtils.directory(window.location.href) + "extensions/default/", + defaultExtensions = [ + // Core extensions we want to support in the browser + "CSSCodeHints", + "HTMLCodeHints", + "HtmlEntityCodeHints", + "InlineColorEditor", + "JavaScriptQuickEdit", + "JSLint", + "LESSSupport", + "QuickOpenCSS", + "QuickOpenHTML", + "QuickOpenJavaScript", + "QuickView", + "RecentProjects", + "UrlCodeHints", + "WebPlatformDocs", + + // Custom extensions we want loaded by default + // NOTE: Maps to a folder inside /src/extensions/default/ + "makedrive-sync-icon" + + // "ExampleExtension", + ]; + + return Async.doInParallel(defaultExtensions, function (item) { + var extConfig = { + baseUrl: basePath + item + }; + return loadExtension(item, extConfig, "main"); + }); + } + + if (!paths) { params.parse();