├── .gitignore ├── LICENSE ├── README.md ├── config.json ├── package.json ├── scripts ├── build.js ├── config.js ├── patches │ └── src │ │ ├── atom-environment.coffee.patch │ │ └── compile-cache.js.patch ├── release.js ├── standalone-atom.js ├── testpage.html └── verify.js └── shims ├── clipboard.js ├── electron ├── index.js └── package.json ├── git-utils ├── index.js └── package.json ├── keyboard-layout ├── index.js └── package.json ├── marker-index ├── helpers.js ├── index.js ├── iterator.js ├── node.js └── point-helpers.js ├── module.js ├── nslog ├── index.js └── package.json ├── oniguruma ├── index.js └── package.json ├── pathwatcher ├── directory.coffee ├── directory.js ├── file.coffee ├── file.js ├── index.js └── package.json ├── remote.js ├── screen.js ├── scrollbar-style ├── index.js └── package.json └── shell.js /.gitignore: -------------------------------------------------------------------------------- 1 | /config.local.json 2 | node_modules 3 | /out 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD License 2 | 3 | For atom-in-orbit software 4 | 5 | Copyright (c) 2016-present, Facebook, Inc. All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without modification, 8 | are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name Facebook nor the names of its contributors may be used to 18 | endorse or promote products derived from this software without specific 19 | prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 22 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 25 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 28 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # atom-in-orbit 2 | 3 | The goal of this project is to produce a version of Atom that runs in Chrome 4 | from Atom's source that is as faithful to the desktop version as possible. 5 | 6 | ## Motivation 7 | 8 | There are already [many offerings](https://www.slant.co/topics/713/~cloud-ides) 9 | that provide a browser-based IDE: do we really need another one? There are two 10 | questions here: 11 | 12 | 1. Why would someone want a browser-based IDE instead of a desktop one? 13 | 2. Assuming we're convinced we want an IDE in the browser, why should we prefer 14 | Atom over exisitng offerings? 15 | 16 | Many of the advantages of a web-based IDE aren't specific to IDEs, but to 17 | webapps, in general: 18 | 19 | * Zero setup / barrier to entry. 20 | * Everything is stored to the cloud, so we can access it from anywhere, it is 21 | automatically backed up, etc. 22 | * It is inherently cross-platform. 23 | * It is generally fair to assume the user is online while using the app. 24 | * Unlike native apps, users do not need to "download" the entire webapp before 25 | using it. Webapps lend themselves to incremental updates by judicious use 26 | of the browser cache. 27 | * Webapps can be used effectively from machines with limited resources because 28 | most of the "heavy lifting" is done on the server. 29 | * Keeping the bulk of information on the server in a datacenter as opposed to 30 | spreading it across a multitude of clients in the wild generally makes it 31 | easier to secure. 32 | * Inherent support for deep-linking into the application. 33 | * Simplified release process: all users are always on the same version, which is 34 | the latest and greatest. 35 | 36 | Admittedly, there is no reason why a desktop IDE cannot exhibit these 37 | properties, thereby providing the same advantages of web-based IDEs, but it is 38 | generally a bit more work. 39 | 40 | In terms of **Why Atom?**, here are a few reasons: 41 | 42 | * **Extensibility.** Atom has built up a rich developer ecosystem around it with 43 | [thousands of packages](https://atom.io/packages). 44 | * **Many of us are already using it!** If you are a power user of Atom, but you 45 | want the option of using it as a webapp for the reasons listed above, wouldn't 46 | it be nice to use the same tool on the web as on the desktop rather than 47 | learning yet another editor? Don't you want to take all of your keyboard 48 | shortcuts, themes, and other customizations with you? 49 | * **Designed with fewer constraints.** When you design something as a webapp, 50 | it's natural for the limitations of the browser to constrain your thinking. 51 | Fortunately, Atom does not suffer from that. For example, Atom implements 52 | certain libraries in C instead of JavaScript where it makes sense, which is not 53 | something your average web developer would consider doing. Being desktop-first 54 | is also reflected in the Atom community in that they have provided many packages 55 | to support the development of mobile/desktop software, which is unlikely to be a 56 | priority for those supporting a web-only IDE. 57 | 58 | Nuclide's support for remote development is a compelling example of combining 59 | the best features of desktop and web-based IDEs. All services in Nuclide are 60 | written using the [Nuclide service framework]( 61 | https://github.com/facebook/nuclide/wiki/Remote-Nuclide-Services), which ensures 62 | that features that are designed for local development will automatically work 63 | the same way when used as part of remote development in Nuclide. For example, 64 | consider the [Flow language service](https://nuclide.io/docs/languages/flow/) in 65 | Nuclide, which provides autocomplete, click-to-symbol, diagnostics, and other 66 | language features when editing Flow-typed JavaScript code. The service can 67 | assume that it is running local to the JavaScript code it is servicing while 68 | Nuclide takes care of proxying the requests and responses to the user's local 69 | instance of Nuclide. (Effectively, local development is just a special case 70 | where Nuclide and the service are running on the same machine.) 71 | In this way, from a single codebase, Nuclide can 72 | simultaneously support offline, local development on a beefy laptop in addition 73 | to the "thin client" model that users expect from a webapp when editing remote 74 | files. 75 | 76 | Given Nuclide's ability to straddle the line between desktop and web, why would 77 | we go back to the browser? Again, the list of advantages of webapps over native 78 | apps is substantial, and even if we could theoretically achieve 100% parity from 79 | a technical perspective, **changing user expectations around webapps vs. native 80 | apps remains a challenge**. For this reason, it seems worth providing Atom as 81 | both a desktop app and a webapp to broaden its appeal. 82 | 83 | ## Challenges 84 | 85 | This project aims to run Atom in the browser. Because Atom is [mostly] 86 | built using web technologies, much of its code can be run in the browser 87 | verbatim. However, there is a number of "freedoms" that Atom-on-the-desktop 88 | enjoys that Atom-in-the-browser does not: 89 | 90 | * Synchronous access to the filesystem via the `fs` module. 91 | * All resources are available locally and are assumed to be cheap to access. 92 | * Natively implemented dependencies. 93 | * Unrestricted access to the internet. 94 | * Access to native APIs. 95 | 96 | Fortunately, there are workarounds to all of these issues with some clever 97 | engineering. 98 | 99 | ### Synchronous Access to the Filesystem 100 | 101 | Initial experiments have shown [BrowserFS](https://github.com/jvilk/BrowserFS) 102 | to be a powerful shim for `fs` in the browser. 103 | 104 | ### Cheap Access to Resources 105 | 106 | A key challenge of this project is that of *packaging*. 107 | Initially, we have been using [browserify](http://browserify.org/) to build the 108 | prototype, but it produces a webapp that has to download 30MB of JavaScript 109 | before it runs any code, so clearly we need a more sophisticated solution. 110 | 111 | ### Natively Implemented Dependencies 112 | 113 | The plan is to use [Emscripten](http://kripken.github.io/emscripten-site/), but 114 | this has not been put to the test yet. 115 | 116 | ### Unrestricted Access to the Internet 117 | 118 | On the desktop, you can perform any sort of I/O you like and can access the 119 | Internet however you want. By comparison, in the browser, all you have are 120 | `XMLHttpRequest` and `WebSocket`. In general, and you are subject to the 121 | same-origin policy, though maybe if you're lucky you can use [CORS]( 122 | https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS). 123 | 124 | When designing a webapp, one could provide true "unrestricted" access to the 125 | Internet via an open redirect on your server, but that is not a good choice from 126 | a security perspective. Realistically, your IDE should not need "unrestricted" 127 | access, but a deliberate server API that proxies requests, as necessary. 128 | 129 | ### Access to Native APIs 130 | 131 | Because Atom runs in Electron, it is able to do things like configure the 132 | window's native menu bar and context menu items. Admittedly, these have to be 133 | faked in the browser by rebuilding the native UI using DOM elements. 134 | 135 | ## Building the Webapp 136 | 137 | First, you must create a `config.local.json` file in the root of your project 138 | with some configuration information. Specifically, it needs the location of a 139 | [source checkout of Atom](https://github.com/atom/atom) that has been built at 140 | the revision specified in the `config.json` file. 141 | 142 | ``` 143 | { 144 | "ATOM_SRC": "/home/mbolin/code/atom" 145 | } 146 | ``` 147 | 148 | You can build the local demo by running (this takes 10s on my Linux box): 149 | 150 | ``` 151 | $ npm run build 152 | ``` 153 | 154 | (Unfortunately, the build script currently leaves a local change to 155 | `src/compile-cache.js` in `ATOM_SRC`. This is lame -- I will fix the build 156 | process!) 157 | 158 | Assuming the build script succeeds, open `out/testpage.html` in Google Chrome 159 | and you should see Atom running in the browser. If you open the Chrome Dev 160 | Tools, you will see that the global `atom` has been installed and you can play 161 | with it just like you can in the Dev Tools in Atom itself. For example, try 162 | running the following in Chrome Dev Tools: 163 | 164 | ``` 165 | atom.config.set('core.themes', ['atom-light-ui', 'atom-light-syntax']); 166 | atom.config.set('core.themes', ['atom-dark-ui', 'atom-dark-syntax']); 167 | atom.notifications.addInfo('Wow, this really works!'); 168 | ``` 169 | 170 | The list of Atom packages that is currently included by the demo is conservative 171 | because the JavaScript is already so large. The list is specified in 172 | `scripts/build.js`, so feel free to play with it and add more packages by 173 | default. 174 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "ATOM_REVISION": "f7d3f0210bf6ff1b4193d8a8b8a54c199b561bc2" 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atom-in-orbit", 3 | "version": "0.0.0", 4 | "engines": { 5 | "node": ">=6.5.0" 6 | }, 7 | "scripts": { 8 | "build": "node ./scripts/build.js", 9 | "release": "node ./scripts/release.js" 10 | }, 11 | "devDependencies": { 12 | "browserfs": "bolinfest/BrowserFS#894ee30a1188258851c6af94c6cc1a0085ce94b7", 13 | "browserify": "13.1.1", 14 | "chokidar": "1.6.1", 15 | "fs-plus": "2.9.2", 16 | "through": "2.3.8", 17 | "watchify": "3.7.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const JS_FILE_HEADER = `\ 4 | /* 5 | Copyright (c) 2016-present, Facebook, Inc. All rights reserved. 6 | 7 | The examples provided by Facebook are for non-commercial testing and evaluation 8 | purposes only. Facebook reserves all rights not expressly granted. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 11 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 12 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 13 | FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 14 | ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 15 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | */ 17 | `; 18 | 19 | const config = require('./config'); 20 | if (config.ATOM_SRC == null) { 21 | throw Error('Must specify ATOM_SRC in config.local.json'); 22 | } 23 | const path = require('path'); 24 | const ATOM_SRC = path.normalize(config.ATOM_SRC); 25 | 26 | // TODO(mbolin): Run `yarn check || yarn --force` in root? 27 | const root = path.normalize(path.join(__dirname, '..')); 28 | 29 | // As of Atom f7d3f0210bf6ff1b4193d8a8b8a54c199b561bc2, we need to apply some 30 | // patches to Atom to get this prototype to work. 31 | 32 | const fs = require('fs-plus'); 33 | // All of our generated files will be written to this directory. 34 | const outDir = root + '/out'; 35 | makeCleanDir(outDir); 36 | 37 | // This is our clone of the Atom directory that we hack up for our needs. 38 | // Note that Atom should have already been built in ATOM_SRC when this script is 39 | // run so we pick up all of its node_modules. 40 | const atomDir = outDir + '/atom'; 41 | makeCleanDir(atomDir); 42 | 43 | const {execFileSync, spawnSync} = require('child_process'); 44 | 45 | // These executions of `patch` are really bad because they leave ATOM_SRC in a modified state, so we 46 | // really need to do better than this. The real solution is to upstream the appropriate fixes such 47 | // that these patches are unnecessary. 48 | 49 | // This is fixed in Atom master as of 089fa92117f5d0ead54b56ee208a2baa24d9c4e2. 50 | execFileSync('patch', [ 51 | path.join(ATOM_SRC, 'src/atom-environment.coffee'), 52 | path.join(root, 'scripts/patches/src/atom-environment.coffee.patch'), 53 | ]); 54 | 55 | // Custom change so we can export the functions that we need from compile-cache. 56 | execFileSync('patch', [ 57 | path.join(ATOM_SRC, 'src/compile-cache.js'), 58 | path.join(root, 'scripts/patches/src/compile-cache.js.patch'), 59 | ]); 60 | const {COMPILERS, compileFileAtPath, setAtomHomeDirectory} = require( 61 | path.join(ATOM_SRC, 'src/compile-cache')); 62 | 63 | const browserify = require('browserify'); 64 | const chokidar = require('chokidar'); 65 | const through = require('through'); 66 | const watchify = require('watchify'); 67 | 68 | const willWatch = process.argv[2] == '-w'; // poor man's arg parsing. 69 | let startedWatching = false; 70 | 71 | function build() { 72 | // This is necessary to set the compile-cache. 73 | setAtomHomeDirectory(path.join(fs.getHomeDirectory(), '.atom')); 74 | 75 | copyFileSyncWatch(ATOM_SRC + '/package.json', atomDir + '/package.json'); 76 | 77 | // When we copy src/ and exports/, we must also transpile everything inside. 78 | copySyncWatch( 79 | ATOM_SRC + '/src', 80 | atomDir + '/src', 81 | tree => fs.traverseTreeSync(tree, transpileFile, () => true) 82 | ); 83 | copySyncWatch( 84 | ATOM_SRC + '/exports', 85 | atomDir + '/exports', 86 | tree => fs.traverseTreeSync(tree, transpileFile, () => true) 87 | ); 88 | copySyncWatch( 89 | ATOM_SRC + '/static', 90 | atomDir + '/static', 91 | tree => {} 92 | ); 93 | copySyncWatch( 94 | ATOM_SRC + '/node_modules', 95 | atomDir + '/node_modules', 96 | tree => {} 97 | ); 98 | copyFileSyncWatch(ATOM_SRC + '/static/octicons.woff', outDir + '/octicons.woff'); 99 | 100 | // All built-in Atom packages that are installed under node_modules/ in 101 | // ATOM_SRC because Atom's own build script has special handling for the 102 | // "packageDependencies" section in its package.json file. I am pretty sure 103 | // that something bad would happen if they had a transitive dependency on a 104 | // Node module with the same name as one of their built-in Atom packages. 105 | const atomPackages = [ 106 | // To include an Atom package in the prototype, add it to this list. Its 107 | // code must exist under node_modules. Even though these are Atom packages, 108 | // installing them under node_modules helps ensure their dependencies get 109 | // deduped properly by npm. 110 | 'command-palette', 111 | 'find-and-replace', 112 | 'go-to-line', 113 | 'markdown-preview', 114 | 'notifications', 115 | 'status-bar', 116 | 'tabs', 117 | 'tree-view', 118 | ]; 119 | const filesTypesToCopyFromPackage = new Set(['.cson', '.js', '.json', '.less']); 120 | const atomPackageData = {}; 121 | const nodeModules = atomDir + '/node_modules'; 122 | for (const pkg of atomPackages) { 123 | atomPackageData[pkg] = {}; 124 | 125 | // Some Atom packages are written in CoffeeScript, so they need to be 126 | // transpiled for Browserify. 127 | const destinationDir = `${nodeModules}/${pkg}`; 128 | copySyncWatch( 129 | `${ATOM_SRC}/node_modules/${pkg}`, 130 | destinationDir, 131 | tree => fs.traverseTreeSync( 132 | tree, 133 | transpileFile, 134 | directoryName => { 135 | return directoryName !== 'node_modules'; 136 | } 137 | ) 138 | ); 139 | 140 | const entries = atomPackageData[pkg]['files'] = {}; 141 | fs.traverseTreeSync( 142 | destinationDir, 143 | fileName => { 144 | const extension = path.extname(fileName); 145 | if (filesTypesToCopyFromPackage.has(extension)) { 146 | entries[fileName] = fs.readFileSync(fileName, 'utf8'); 147 | } 148 | }, 149 | directoryName => { 150 | return directoryName !== 'node_modules'; 151 | } 152 | ); 153 | 154 | // Resolve the "main" attribute of package.json. 155 | const manifest = JSON.parse(fs.readFileSync(`${destinationDir}/package.json`), 'utf8'); 156 | let {main} = manifest; 157 | 158 | if (main == null) { 159 | main = `${destinationDir}/index.js`; 160 | } else { 161 | main = path.resolve(destinationDir, main); 162 | if (fs.isDirectorySync(main)) { 163 | main = `${path.normalize(main)}/index.js`; 164 | } 165 | if (!fs.isFileSync(main)) { 166 | main = main + '.js'; 167 | } 168 | } 169 | atomPackageData[pkg]['metadata'] = {main}; 170 | } 171 | 172 | // Insert some shims. 173 | copyFileSyncWatch( 174 | root + '/shims/clipboard.js', 175 | atomDir + '/src/clipboard.js'); 176 | [ 177 | 'remote', 178 | 'screen', 179 | 'shell', 180 | ].forEach(createShimPackageFromSingleFile.bind(null, nodeModules)); 181 | 182 | // Call browserify on node_modules/atom/src/standalone-atom.js. 183 | const browserifyInputFile = atomDir + '/src/standalone-atom.js'; 184 | copyFileSyncWatch(root + '/scripts/standalone-atom.js', browserifyInputFile); 185 | 186 | const modulesToFilter = new Set([ 187 | // Modules with native dependencies that we do not expect to exercise at runtime. 188 | 'onig-reg-exp', 189 | 'runas', 190 | './squirrel-update', 191 | 'tls', 192 | '../src/main-process/win-shell', // From exports/atom.js 193 | ]); 194 | 195 | const fullShims = new Set([ 196 | 'electron', 197 | 'git-utils', 198 | 'keyboard-layout', 199 | 'marker-index', 200 | 'nslog', 201 | 'oniguruma', 202 | 'pathwatcher', 203 | 'scrollbar-style', 204 | ]); 205 | fullShims.forEach(createShimPackageFromDirectory.bind(null, nodeModules)); 206 | 207 | const bundler = browserify( 208 | [ 209 | browserifyInputFile, 210 | ], 211 | { 212 | // filter() is documented at: https://github.com/substack/module-deps#var-d--mdepsopts. 213 | filter(id) { 214 | return !modulesToFilter.has(id); 215 | }, 216 | packageFilter(pkg, dir) { 217 | const {name} = pkg; 218 | if (fullShims.has(name)) { 219 | const clone = Object.assign({}, pkg); 220 | clone.browser = `${nodeModules}/${name}/index.js`; 221 | return clone; 222 | } else { 223 | return pkg; 224 | } 225 | }, 226 | builtins: Object.assign( 227 | { 228 | atom: atomDir + '/exports/atom.js', 229 | electron: `${root}/shims/electron/index.js` 230 | }, 231 | require('browserify/lib/builtins'), 232 | { 233 | buffer: require.resolve('browserfs/dist/shims/buffer.js'), 234 | fs: require.resolve('browserfs/dist/shims/fs.js'), 235 | path: require.resolve('browserfs/dist/shims/path.js'), 236 | } 237 | ), 238 | insertGlobalVars: { 239 | // process, Buffer, and BrowserFS globals. 240 | // BrowserFS global is not required if you include browserfs.js 241 | // in a script tag. 242 | process() { return "require('browserfs/dist/shims/process.js')" }, 243 | Buffer() { return "require('buffer').Buffer" }, 244 | BrowserFS() { return "require('" + require.resolve('browserfs') + "')" }, 245 | }, 246 | cache: {}, 247 | packageCache: {}, 248 | verbose: true, 249 | } 250 | ).on('log', console.log); 251 | 252 | // Map of absolute paths to file contents. 253 | // Each of these entries will be added to the BrowserFS.FileSystem.InMemory file store at startup. 254 | const ATOM_FILES_TO_ADD = {}; 255 | 256 | const transformSuffixes = { 257 | // Currently, this matches: 258 | // out/atom/node_modules/atom-space-pen-views/lib/select-list-view.js 259 | // Though ultimately we likely want to use this to overwrite require.resolve(), in general. 260 | '/node_modules/atom-space-pen-views/lib/select-list-view.js': function(file, src) { 261 | // TODO(mbolin): Replace this crude transform with a more precise and efficient one. 262 | 263 | // Here, we are trying to patch up: 264 | // 265 | // atom.themes.requireStylesheet(require.resolve('../stylesheets/select-list.less')); 266 | // 267 | // The piece that is matched by our regex is: 268 | // 269 | // ../stylesheets/select-list.less 270 | // 271 | // Recall that we need to make it look like the file exists on the filesystem at: 272 | // `node_modules/atom-space-pen-views/stylesheets/select-list.less` 273 | // in the case of the find-and-replace package. 274 | // 275 | // Because we are going to replace the require.resolve() call altogether in this case, 276 | // there will be no require() leftover, so Browserify will not try to resolve this file at 277 | // all, ony the BrowserFS.FileSystem.InMemory will have to. 278 | return src.replace(/require.resolve\(['"]([^\)]+)['"]\)/, function(fullMatch, arg) { 279 | const absolutePath = path.join(path.dirname(file), arg); 280 | // We need to ensure this resource is available at this path in BrowserFS. 281 | ATOM_FILES_TO_ADD[absolutePath] = fs.readFileSync(absolutePath, 'utf8'); 282 | 283 | // Remember to stringify because the replacement must be a string literal. 284 | return JSON.stringify(absolutePath); 285 | }); 286 | }, 287 | }; 288 | bundler.transform( 289 | function (file) { 290 | let patchTransform = null; 291 | for (const suffix in transformSuffixes) { 292 | if (file.endsWith(suffix)) { 293 | patchTransform = transformSuffixes[suffix]; 294 | break; 295 | } 296 | } 297 | 298 | // TODO(mbolin): Prefer Node's built-in transform streams over through. 299 | if (patchTransform == null) { 300 | function write(buf) { 301 | this.queue(buf); 302 | } 303 | function end() { 304 | this.queue(null); 305 | } 306 | return through(write, end); 307 | } else { 308 | const data = []; 309 | function write(buf) { 310 | data.push(buf); 311 | } 312 | function end() { 313 | const src = data.join(''); 314 | this.queue(patchTransform(file, src)); 315 | this.queue(null); 316 | } 317 | return through(write, end); 318 | } 319 | }, 320 | // We must set {global:true} so that transforms apply to things under node_modules. 321 | {global: true} 322 | ); 323 | 324 | const ATOM_RESOURCE_PATH = '/Users/bolinfest/resourcePath'; 325 | const resourceFoldersToCopy = [ 326 | '/keymaps', 327 | '/menus', 328 | '/node_modules/atom-dark-syntax', 329 | '/node_modules/atom-dark-ui', 330 | '/node_modules/atom-light-syntax', 331 | '/node_modules/atom-light-ui', 332 | '/node_modules/atom-ui', 333 | '/resources', 334 | '/static', 335 | ]; 336 | for (const folder of resourceFoldersToCopy) { 337 | fs.traverseTreeSync( 338 | ATOM_SRC + folder, 339 | fileName => { 340 | const relative = path.relative(ATOM_SRC, fileName); 341 | const entry = path.join(ATOM_RESOURCE_PATH, relative); 342 | ATOM_FILES_TO_ADD[entry] = fs.readFileSync(fileName, 'utf8'); 343 | }, 344 | directoryName => true 345 | ); 346 | } 347 | 348 | function rmFile(filename) { 349 | try { 350 | fs.unlinkSync(filename); 351 | } catch(e) { 352 | // do nothing 353 | } 354 | } 355 | 356 | // Clear out files before we start appending to them. 357 | const atomJsFile = `${outDir}/atom.js`; 358 | rmFile(atomJsFile); 359 | const resourcesFile = `${outDir}/atom-resources.js`; 360 | rmFile(resourcesFile); 361 | 362 | const bundle = ids => { 363 | if (ids) { 364 | console.log('Changed', ids); 365 | } 366 | return bundler.bundle((error, content) => { 367 | if (error != null) { 368 | if (error.stack) { 369 | console.error(error.stack); 370 | } else { 371 | console.error(String(error)); 372 | } 373 | } else { 374 | fs.appendFileSync(atomJsFile, JS_FILE_HEADER); 375 | fs.appendFileSync(atomJsFile, content); 376 | 377 | // Some stylesheet insists on loading octicons.woff relative to the .html page, so we 378 | // include both testpage.html and octicons.woff in the out/ directory. 379 | try { 380 | fs.symlinkSync(root + '/scripts/testpage.html', outDir + '/testpage.html'); 381 | } catch(e) { 382 | // do nothing 383 | } 384 | 385 | function writeResources(data) { 386 | fs.appendFileSync(resourcesFile, data); 387 | } 388 | 389 | writeResources(JS_FILE_HEADER); 390 | 391 | writeResources(`var ATOM_RESOURCE_PATH = `); 392 | writeResources(JSON.stringify(ATOM_RESOURCE_PATH)); 393 | writeResources(';\n'); 394 | 395 | writeResources(`var ATOM_FILES_TO_ADD = `); 396 | writeResources(JSON.stringify(ATOM_FILES_TO_ADD)); 397 | writeResources(';\n'); 398 | 399 | writeResources(`var ATOM_PACKAGE_DATA = `); 400 | writeResources(JSON.stringify(atomPackageData)); 401 | writeResources(';\n'); 402 | 403 | writeResources('var ATOM_PACKAGE_ROOT_FROM_BROWSERIFY = '); 404 | writeResources(JSON.stringify(nodeModules)); 405 | writeResources(';\n'); 406 | 407 | startedWatching = willWatch; 408 | } 409 | }); 410 | }; 411 | 412 | if (willWatch) { 413 | bundler 414 | .plugin(watchify) 415 | .on('update', bundle); 416 | // Example of how to watch a one-off file and have it rebulid everything: 417 | chokidar.watch(root + '/keymaps').on('all', () => { 418 | if (startedWatching) { 419 | bundle(); 420 | } 421 | }); 422 | } 423 | 424 | bundle(); 425 | } 426 | 427 | function transpileFile(absolutePath) { 428 | const ext = path.extname(absolutePath); 429 | if (!COMPILERS.hasOwnProperty(ext)) { 430 | return; 431 | } 432 | 433 | const compiler = COMPILERS[ext]; 434 | const transpiledSource = compileFileAtPath(compiler, absolutePath, ext); 435 | 436 | // Replace the original file extension with .js. 437 | const outputFile = absolutePath.substring(0, absolutePath.length - ext.length) + '.js'; 438 | fs.writeFileSync(outputFile, transpiledSource); 439 | } 440 | 441 | function createShimPackageFromSingleFile(nodeModules, moduleName) { 442 | const moduleDirectory = `${nodeModules}/${moduleName}`; 443 | makeCleanDir(moduleDirectory); 444 | copyFileSyncWatch( 445 | `${root}/shims/${moduleName}.js`, 446 | `${moduleDirectory}/${moduleName}.js`); 447 | fs.writeFileSync( 448 | moduleDirectory + '/package.json', 449 | JSON.stringify({ 450 | name: moduleName, 451 | main: `./${moduleName}.js`, 452 | }, 453 | /* replacer */ undefined, 454 | 2)); 455 | } 456 | 457 | function createShimPackageFromDirectory(nodeModules, moduleName) { 458 | const moduleDirectory = `${nodeModules}/${moduleName}`; 459 | makeCleanDir(moduleDirectory); 460 | copySyncWatch(`${root}/shims/${moduleName}`, moduleDirectory, tree => {}); 461 | } 462 | 463 | function copySyncWatch(from, to, then) { 464 | fs.copySync(from, to); 465 | then(to); 466 | if (willWatch) { 467 | console.log('Will watch', from); 468 | chokidar.watch(from).on('all', (a, b) => { 469 | if (startedWatching) { 470 | fs.copySync(from, to); 471 | then(to); 472 | } 473 | }); 474 | } 475 | } 476 | 477 | function copyFileSyncWatch(from, to) { 478 | fs.copyFileSync(from, to); 479 | if (willWatch) { 480 | console.log('Will watch file', from); 481 | chokidar.watch(from).on('all', () => { 482 | if (startedWatching) { 483 | fs.copyFileSync(from, to); 484 | } 485 | }); 486 | } 487 | } 488 | 489 | function makeCleanDir(dir) { 490 | fs.removeSync(dir); 491 | fs.makeTreeSync(dir); 492 | } 493 | 494 | build(); 495 | -------------------------------------------------------------------------------- /scripts/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | const universalConfig = require('../config.json'); 7 | let userConfig = {}; 8 | try { 9 | const json = fs.readFileSync( 10 | path.join(__dirname, '../config.local.json'), 11 | 'utf8'); 12 | userConfig = JSON.parse(json); 13 | } catch (e) { 14 | // If the file does not exist, then that's fine, but if it contains malformed 15 | // JSON, then the user should know. 16 | if (e.code !== 'ENOENT') { 17 | throw e; 18 | } 19 | } 20 | 21 | module.exports = Object.assign({}, universalConfig, userConfig); 22 | -------------------------------------------------------------------------------- /scripts/patches/src/atom-environment.coffee.patch: -------------------------------------------------------------------------------- 1 | --- /Users/mbolin/src/atom/src/atom-environment.coffee 2016-12-13 20:21:11.000000000 -0800 2 | +++ scripts/patches/src/atom-environment.coffee 2016-12-13 20:16:37.000000000 -0800 3 | @@ -656,7 +656,7 @@ 4 | restoreWindowDimensions: -> 5 | unless @windowDimensions? and @isValidDimensions(@windowDimensions) 6 | @windowDimensions = @getDefaultWindowDimensions() 7 | - @setWindowDimensions(@windowDimensions).then -> @windowDimensions 8 | + @setWindowDimensions(@windowDimensions).then => @windowDimensions 9 | 10 | restoreWindowBackground: -> 11 | if backgroundColor = window.localStorage.getItem('atom:window-background-color') 12 | -------------------------------------------------------------------------------- /scripts/patches/src/compile-cache.js.patch: -------------------------------------------------------------------------------- 1 | --- /Users/mbolin/src/atom/src/compile-cache.js 2016-11-02 19:50:02.000000000 -0700 2 | +++ scripts/patches/src/compile-cache.js 2016-12-13 20:16:37.000000000 -0800 3 | @@ -29,6 +29,8 @@ 4 | packageTranspilationRegistry.removeTranspilerConfigForPath(packagePath) 5 | } 6 | 7 | +exports.COMPILERS = COMPILERS 8 | + 9 | var cacheStats = {} 10 | var cacheDirectory = null 11 | 12 | @@ -37,12 +39,13 @@ 13 | if (process.env.USER === 'root' && process.env.SUDO_USER && process.env.SUDO_USER !== process.env.USER) { 14 | cacheDir = path.join(cacheDir, 'root') 15 | } 16 | - this.setCacheDirectory(cacheDir) 17 | + setCacheDirectory(cacheDir) 18 | } 19 | 20 | -exports.setCacheDirectory = function (directory) { 21 | +function setCacheDirectory (directory) { 22 | cacheDirectory = directory 23 | } 24 | +exports.setCacheDirectory = setCacheDirectory 25 | 26 | exports.getCacheDirectory = function () { 27 | return cacheDirectory 28 | @@ -95,6 +98,7 @@ 29 | } 30 | return sourceCode 31 | } 32 | +exports.compileFileAtPath = compileFileAtPath 33 | 34 | function readCachedJavascript (relativeCachePath) { 35 | var cachePath = path.join(cacheDirectory, relativeCachePath) 36 | @@ -206,6 +210,11 @@ 37 | } 38 | 39 | Object.keys(COMPILERS).forEach(function (extension) { 40 | + // This will happen when run in a browser. 41 | + if (require.extensions == null) { 42 | + return; 43 | + } 44 | + 45 | var compiler = COMPILERS[extension] 46 | 47 | Object.defineProperty(require.extensions, extension, { 48 | -------------------------------------------------------------------------------- /scripts/release.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // TODO(mbolin): We need a release process. 4 | 5 | const config = require('./config'); 6 | 7 | console.log(JSON.stringify(config)); 8 | -------------------------------------------------------------------------------- /scripts/standalone-atom.js: -------------------------------------------------------------------------------- 1 | // By default, browserify sets process.platform to 'browser'. Atom needs a 2 | // real value for process.platform because it does a lot of resource loading 3 | // based on this variable (including keyboard shortcut registration!). 4 | function detectPlatform() { 5 | let platform = 'browser'; 6 | let userAgentPlatform; 7 | try { 8 | userAgentPlatform = window.navigator.platform; 9 | } catch (e) { 10 | console.error(`Could not find the platform: assuming '${platform}'.`); 11 | return platform; 12 | } 13 | 14 | if (userAgentPlatform.includes('Mac')) { 15 | platform = 'darwin'; 16 | } else if (userAgentPlatform.includes('Linux')) { 17 | platform = 'linux'; 18 | } else if (userAgentPlatform.includes('Win')) { 19 | platform = 'win32'; 20 | } 21 | 22 | return platform; 23 | } 24 | process.platform = detectPlatform(); 25 | 26 | window.setImmediate = function(callback) { 27 | Promise.resolve().then(callback); 28 | }; 29 | 30 | const pathModule = require('path'); 31 | 32 | const resourcePath = ATOM_RESOURCE_PATH; 33 | 34 | // This exists in a GitHub checkout of Atom, but I cannot seem to 35 | // find it under /Applications/Atom.app/. 36 | const menusDirPath = resourcePath + '/menus'; 37 | const menusConfigFile = menusDirPath + '/menu.json'; 38 | 39 | // process.env.ATOM_DEV_RESOURCE_PATH = '/This/is/fake'; 40 | process.env.ATOM_HOME = '/This/is/.atom'; 41 | process.resourcesPath = resourcePath; 42 | 43 | window.location.hash = '#' + JSON.stringify({ 44 | initialPaths: [], 45 | locationsToOpen: [{}], 46 | // windowInitializationScript: 'atom/src/initialize-application-window.coffee', 47 | resourcePath, 48 | devMode: false, 49 | safeMode: false, 50 | profileStartup: false, 51 | clearWindowState: false, 52 | env: { 53 | ATOM_HOME: process.env.ATOM_HOME, 54 | ATOM_DEV_RESOURCE_PATH: '/This/is/fake', 55 | }, 56 | appVersion: '1.11.2', 57 | atomHome: '', 58 | shellLoadTime: 999, 59 | }); 60 | 61 | process.binding = (arg) => { 62 | console.warn(`process.binding() called with ${arg}: not supported.`); 63 | return {}; 64 | }; 65 | 66 | const inMemoryFs = new BrowserFS.FileSystem.InMemory(); 67 | BrowserFS.initialize(inMemoryFs); 68 | 69 | // Define these environment variables for the benefit of fs-plus's 70 | // getHomeDirectory() function and anyone else who might need it. 71 | process.env.HOME = '/Users/bolinfest'; 72 | process.env.USERPROFILE = '/Users/bolinfest'; 73 | 74 | const fs = require('fs'); 75 | const fsPlus = require('fs-plus'); 76 | function addFile(file, contents) { 77 | fsPlus.makeTreeSync(pathModule.dirname(file)); 78 | fs.writeFileSync(file, contents); 79 | } 80 | 81 | // Unfortunately, I'm not sure why this hack works. Between fs, multiple versions 82 | // of fs-plus, and browserfs, there are a lot of entities trying to do funny things 83 | // with the fs module. We need to do some work to ensure only one instance of is 84 | // is used in the system. lstatSyncNoException is an API introduced by fs-plus, but 85 | // somehow it was missing when calling atom.project.addPath() when tree-view is loaded. 86 | fs.lstatSyncNoException = fsPlus.lstatSyncNoException = function(filePath) { 87 | try { 88 | return fs.lstatSync(filePath); 89 | } catch (e) { 90 | return null; 91 | } 92 | }; 93 | 94 | for (const fileName in ATOM_FILES_TO_ADD) { 95 | addFile(fileName, ATOM_FILES_TO_ADD[fileName]); 96 | } 97 | 98 | const atomPackages = []; 99 | 100 | for (const pkgName in ATOM_PACKAGE_DATA) { 101 | const packageData = ATOM_PACKAGE_DATA[pkgName]; 102 | atomPackages.push({ 103 | name: pkgName, 104 | main: packageData.metadata.main, 105 | }); 106 | const entryMap = packageData['files']; 107 | for (const fileName in entryMap) { 108 | const contents = entryMap[fileName]; 109 | addFile(fileName, contents); 110 | } 111 | } 112 | 113 | fsPlus.resolveOnLoadPath = function(...args) { 114 | return fsPlus.resolve.apply(fsPlus, require('module').globalPaths.concat(args)); 115 | }; 116 | 117 | // TODO: Find a better way to hack this? 118 | require('module').globalPaths = []; 119 | require('module').paths = []; 120 | 121 | // Ultimately, two things should happen: 122 | // 1. tree-view should be fixed so it can tolerate an empty state. 123 | // 2. This should be able to be specified from the caller if someone 124 | // creates a webapp that 125 | const atomPackageInitialState = { 126 | 'tree-view': { 127 | attached: true, 128 | }, 129 | }; 130 | 131 | window.loadAtom = function(callback) { 132 | const initializeApplicationWindow = require('./initialize-application-window'); 133 | 134 | // Various things try to write to the BlobStore. 135 | const FileSystemBlobStore = require('./file-system-blob-store.js'); 136 | const blobStore = new FileSystemBlobStore('/tmp'); 137 | 138 | initializeApplicationWindow({blobStore}).then(() => { 139 | require('electron').ipcRenderer.send('window-command', 'window:loaded'); 140 | 141 | for (const atomPackage of atomPackages) { 142 | const {name, main} = atomPackage; 143 | atom.packages.activatePackage(ATOM_PACKAGE_ROOT_FROM_BROWSERIFY + '/' + name); 144 | const initialState = atomPackageInitialState[name]; 145 | // TODO(mbolin): Use main to eliminate the repeated calls to require() with 146 | // one line of code in this loop. May be a problem for browserify's static pass. 147 | } 148 | 149 | require('command-palette/lib/command-palette-view').activate(); 150 | require('find-and-replace/lib/find.js').activate(); 151 | require('go-to-line/lib/go-to-line-view').activate(); 152 | require('markdown-preview/lib/main.js').activate(); 153 | require('notifications/lib/main.js').activate(); 154 | require('status-bar/lib/main.js').activate(); 155 | 156 | // For whatever reason, Atom seems to think tabs should not be auto-activated? 157 | // atom.packages.loadedPackages['tabs'].mainModulePath is undefined. 158 | // Though even if it could, it's unclear that it would load the path that Browserify 159 | // has prepared, so we may be better off loading it explicitly. 160 | require('tabs/lib/main.js').activate(); 161 | 162 | 163 | // tree-view does not seem to tolerate the case where it receives an empty state 164 | // from the previous session, so we make sure to pass one explicitly. 165 | const treeViewState = {attached: true}; 166 | require('tree-view/lib/main.js').activate(treeViewState); 167 | 168 | const paramsForCaller = { 169 | atom, 170 | fs: fsPlus, 171 | }; 172 | callback(paramsForCaller); 173 | }); 174 | } 175 | -------------------------------------------------------------------------------- /scripts/testpage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 19 | 20 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /scripts/verify.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // TODO(mbolin): Call this verification code. Having strong verification checks 4 | // will likely help end-users avoid problems when trying to build from source. 5 | 6 | const config = require('./config'); 7 | 8 | // Verify that ATOM_SRC exists and is at revision ATOM_REVISION. 9 | // Should also verify that ./scripts/build has been run. 10 | -------------------------------------------------------------------------------- /shims/clipboard.js: -------------------------------------------------------------------------------- 1 | function Clipboard() { 2 | this.metadata = null; 3 | this.signatureForMetadata = null; 4 | } 5 | 6 | var _currentClipboard = ''; 7 | 8 | Clipboard.prototype = { 9 | md5: function(text) { 10 | // TODO: Pure JS implementation of md5. 11 | return "6d5c9d3b291785b69f79aa5bda210b79cbb8bd94"; 12 | }, 13 | 14 | write: function(text, metadata) { 15 | _currentClipboard = text; 16 | }, 17 | read: function() { 18 | return _currentClipboard; 19 | }, 20 | readWithMetadata: function() { 21 | return {text: _currentClipboard}; 22 | }, 23 | }; 24 | 25 | ['cut', 'copy', 'paste'].forEach(function(eventName) { 26 | document.addEventListener(eventName, function(e) { 27 | var editor = Array.from(atom.textEditors.editors)[0]; 28 | 29 | if (eventName === 'paste') { 30 | _currentClipboard = e.clipboardData.getData('text/plain'); 31 | editor.pasteText(); 32 | } 33 | 34 | if (eventName === 'copy') { 35 | editor.copySelectedText(); 36 | e.clipboardData.setData('text/plain', _currentClipboard); 37 | } 38 | 39 | if (eventName === 'cut') { 40 | editor.cutSelectedText(); 41 | e.clipboardData.setData('text/plain', _currentClipboard); 42 | } 43 | 44 | e.preventDefault(); 45 | }); 46 | }); 47 | 48 | module.exports = Clipboard; 49 | -------------------------------------------------------------------------------- /shims/electron/index.js: -------------------------------------------------------------------------------- 1 | const listeners = {}; 2 | const handlers = {}; 3 | 4 | function registerGetterSetter(action, ...initialValue) { 5 | var value = initialValue; 6 | handlers['set-' + action] = function(...args) { 7 | value = args; 8 | dispatch('ipc-helpers-set-' + action + '-response', ...value); 9 | } 10 | handlers['get-' + action] = function(...args) { 11 | value = args; 12 | dispatch('ipc-helpers-get-' + action + '-response', ...value); 13 | } 14 | } 15 | function registerMethod(action, value) { 16 | handlers[action] = function(...args) { 17 | dispatch('ipc-helpers-' + action + '-response', value); 18 | } 19 | } 20 | 21 | let temporaryWindowState = JSON.stringify({ 22 | version: 1, 23 | project: { 24 | deserializer: "Project", 25 | paths: [], 26 | buffers: [] 27 | }, 28 | workspace: { 29 | deserializer: "Workspace", 30 | paneContainer: { 31 | deserializer: "PaneContainer", 32 | version: 1, 33 | root: { 34 | deserializer: "Pane", 35 | id: 3, 36 | items: [], 37 | }, 38 | }, 39 | packagesWithActiveGrammars: [ 40 | 41 | ], 42 | destroyedItemURIs: [ 43 | 44 | ], 45 | }, 46 | fullScreen: false, 47 | windowDimensions: { 48 | x: 130, 49 | y: 45, 50 | width: 918, 51 | height: 760, 52 | maximized: false, 53 | }, 54 | textEditors: { 55 | editorGrammarOverrides: {}, 56 | }, 57 | }); 58 | 59 | // TODO(mbolin): Figure out how to use the above instead of this opaque string. 60 | temporaryWindowState = '{"version":1,"project":{"deserializer":"Project","paths":[],"buffers":[{"id":"118017ce453321af3b41bd5ece2d8413","text":"","defaultMarkerLayerId":"34","markerLayers":{"1":{"id":"1","maintainHistory":false,"persistent":true,"markersById":{},"version":2},"3":{"id":"3","maintainHistory":true,"persistent":true,"markersById":{"1":{"range":{"start":{"row":0,"column":0},"end":{"row":0,"column":0}},"properties":{},"reversed":false,"tailed":false,"valid":true,"invalidate":"never"}},"version":2},"4":{"id":"4","maintainHistory":false,"persistent":true,"markersById":{},"version":2}},"displayLayers":{"0":{"id":0,"foldsMarkerLayerId":"1"}},"nextMarkerLayerId":40,"nextDisplayLayerId":1,"history":{"version":5,"nextCheckpointId":1,"undoStack":[],"redoStack":[],"maxUndoEntries":10000},"encoding":"utf8","preferredLineEnding":"\\n","nextMarkerId":2}]},"workspace":{"deserializer":"Workspace","paneContainer":{"deserializer":"PaneContainer","version":1,"root":{"deserializer":"Pane","id":3,"items":[{"deserializer":"TextEditor","version":1,"displayBuffer":{"tokenizedBuffer":{"deserializer":"TokenizedBuffer","bufferId":"118017ce453321af3b41bd5ece2d8413","tabLength":2,"largeFileMode":false}},"tokenizedBuffer":{"deserializer":"TokenizedBuffer","bufferId":"118017ce453321af3b41bd5ece2d8413","tabLength":2,"largeFileMode":false},"displayLayerId":0,"selectionsMarkerLayerId":"3","firstVisibleScreenRow":0,"firstVisibleScreenColumn":0,"atomicSoftTabs":true,"softWrapHangingIndentLength":0,"id":4,"softTabs":true,"softWrapped":false,"softWrapAtPreferredLineLength":false,"preferredLineLength":80,"mini":false,"width":881,"largeFileMode":false,"registered":true,"invisibles":{"eol":"¬","space":"·","tab":"»","cr":"¤"},"showInvisibles":false,"showIndentGuide":false,"autoHeight":false}],"itemStackIndices":[0],"activeItemIndex":0,"focused":false,"flexScale":1},"activePaneId":3},"packagesWithActiveGrammars":["language-hyperlink","language-todo"],"destroyedItemURIs":[]},"packageStates":{"bookmarks":{"4":{"markerLayerId":"4"}},"fuzzy-finder":{},"metrics":{"sessionLength":42118},"tree-view":{"directoryExpansionStates":{},"hasFocus":false,"attached":false,"scrollLeft":0,"scrollTop":0,"width":0}},"grammars":{"grammarOverridesByPath":{}},"fullScreen":false,"windowDimensions":{"x":130,"y":45,"width":918,"height":760,"maximized":false},"textEditors":{"editorGrammarOverrides":{}}}'; 61 | 62 | registerGetterSetter('temporary-window-state', temporaryWindowState); 63 | registerGetterSetter('window-size'); 64 | registerGetterSetter('window-position'); 65 | registerMethod('window-method'); 66 | registerMethod('show-window'); 67 | registerMethod('focus-window'); 68 | 69 | function dispatch(action, ...args) { 70 | (listeners[action] || []).forEach(function(listener) { 71 | listener(action, ...args); 72 | }) 73 | } 74 | 75 | module.exports = { 76 | app: { 77 | getPath(arg) { 78 | if (arg === 'home') { 79 | return require('fs-plus').getHomeDirectory(); 80 | } else { 81 | console.error(`app.getPath() called with ${arg}: not supported.`); 82 | } 83 | }, 84 | 85 | getVersion() { 86 | // TODO: Read this from Atom's package.json. 87 | return '0.37.8'; 88 | }, 89 | 90 | on(eventName, callback) { 91 | console.error(`Dropping ${eventName} on the floor in Electron.`); 92 | }, 93 | 94 | setAppUserModelId(modelId) { 95 | 96 | }, 97 | }, 98 | 99 | ipcRenderer: { 100 | on(action, cb) { 101 | if (!listeners[action]) { 102 | listeners[action] = []; 103 | } 104 | listeners[action].push(cb); 105 | if (action === 'ipc-helpers-get-temporary-window-state-response') { 106 | dispatch('ipc-helpers-get-temporary-window-state-response', temporaryWindowState); 107 | } 108 | }, 109 | 110 | send(action, ...args) { 111 | var handler = handlers[action]; 112 | if (!handler) { 113 | console.warn('Ignored IPC call', action, ...args); 114 | return; 115 | } 116 | handler(...args) 117 | }, 118 | 119 | sendSync(action, ...args) { 120 | var handler = handlers[action]; 121 | if (!handler) { 122 | console.warn('Ignored synchronous IPC call', action, ...args); 123 | return; 124 | } 125 | handler(...args) 126 | }, 127 | 128 | removeAllListeners(action) { 129 | delete listeners[action]; 130 | }, 131 | 132 | removeListener(action, callback) { 133 | const listenersForAction = listeners[action] || []; 134 | const index = listenersForAction.indexOf(callback); 135 | if (index !== -1) { 136 | listenersForAction.splice(index, 1); 137 | } 138 | }, 139 | }, 140 | 141 | remote: { 142 | getCurrentWindow() { 143 | return { 144 | on() {}, 145 | isFullScreen() { return false; }, 146 | getPosition() { return [0, 0]; }, 147 | getSize() { return [800, 600]; }, 148 | isMaximized() { return false; }, 149 | isWebViewFocused() { return true; }, 150 | removeListener(action, callback) { 151 | console.warn(`Failing to remove listener for ${action} in remote.getCurrentWindow().`); 152 | }, 153 | } 154 | }, 155 | 156 | screen: { 157 | getPrimaryDisplay() { 158 | return { 159 | workAreaSize: {}, 160 | }; 161 | }, 162 | }, 163 | }, 164 | 165 | webFrame: { 166 | setZoomLevelLimits: function() {}, 167 | }, 168 | 169 | screen: { 170 | on() {}, 171 | removeListener(action, callback) {}, 172 | }, 173 | }; 174 | -------------------------------------------------------------------------------- /shims/electron/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron" 3 | } 4 | -------------------------------------------------------------------------------- /shims/git-utils/index.js: -------------------------------------------------------------------------------- 1 | // TODO: Add fake implementations, as necessary. 2 | module.exports = {}; 3 | -------------------------------------------------------------------------------- /shims/git-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "git-utils" 3 | } 4 | -------------------------------------------------------------------------------- /shims/keyboard-layout/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | observeCurrentKeyboardLayout() { 3 | 4 | }, 5 | getCurrentKeyboardLayout() { 6 | return 'com.apple.keylayout.US'; 7 | }, 8 | getCurrentKeymap() { 9 | return null; 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /shims/keyboard-layout/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "keyboard-layout" 3 | } 4 | -------------------------------------------------------------------------------- /shims/marker-index/helpers.js: -------------------------------------------------------------------------------- 1 | module.exports.addToSet = function addToSet (target, source) { 2 | source.forEach(target.add, target) 3 | } 4 | -------------------------------------------------------------------------------- /shims/marker-index/index.js: -------------------------------------------------------------------------------- 1 | const Random = require('random-seed'); 2 | 3 | const Iterator = require('./iterator'); 4 | const {addToSet} = require('./helpers'); 5 | const {compare, isZero, traversal, traverse} = require('./point-helpers'); 6 | 7 | const MAX_PRIORITY = 2147483647 // max 32 bit signed int (unboxed in v8) 8 | 9 | module.exports = class MarkerIndex { 10 | constructor (seed) { 11 | this.random = new Random(seed) 12 | this.root = null 13 | this.startNodesById = {} 14 | this.endNodesById = {} 15 | this.iterator = new Iterator(this) 16 | this.exclusiveMarkers = new Set() 17 | this.nodePositionCache = new Map() 18 | } 19 | 20 | dump () { 21 | return this.iterator.dump() 22 | } 23 | 24 | getRange (markerId) { 25 | return [this.getStart(markerId), this.getEnd(markerId)] 26 | } 27 | 28 | getStart (markerId) { 29 | return this.getNodePosition(this.startNodesById[markerId]) 30 | } 31 | 32 | getEnd (markerId) { 33 | return this.getNodePosition(this.endNodesById[markerId]) 34 | } 35 | 36 | compare (markerId1, markerId2) { 37 | switch (compare(this.getStart(markerId1), this.getStart(markerId2))) { 38 | case -1: 39 | return -1; 40 | case 1: 41 | return 1; 42 | default: 43 | return compare(this.getEnd(markerId2), this.getEnd(markerId1)) 44 | } 45 | } 46 | 47 | insert (markerId, start, end) { 48 | let startNode = this.iterator.insertMarkerStart(markerId, start, end) 49 | let endNode = this.iterator.insertMarkerEnd(markerId, start, end) 50 | 51 | this.nodePositionCache.set(startNode, start) 52 | this.nodePositionCache.set(endNode, end) 53 | 54 | startNode.startMarkerIds.add(markerId) 55 | endNode.endMarkerIds.add(markerId) 56 | 57 | startNode.priority = this.random(MAX_PRIORITY) 58 | this.bubbleNodeUp(startNode) 59 | 60 | endNode.priority = this.random(MAX_PRIORITY) 61 | this.bubbleNodeUp(endNode) 62 | 63 | this.startNodesById[markerId] = startNode 64 | this.endNodesById[markerId] = endNode 65 | } 66 | 67 | setExclusive (markerId, exclusive) { 68 | if (exclusive) { 69 | this.exclusiveMarkers.add(markerId) 70 | } else { 71 | this.exclusiveMarkers.delete(markerId) 72 | } 73 | } 74 | 75 | isExclusive (markerId) { 76 | return this.exclusiveMarkers.has(markerId) 77 | } 78 | 79 | delete (markerId) { 80 | let startNode = this.startNodesById[markerId] 81 | let endNode = this.endNodesById[markerId] 82 | 83 | let node = startNode 84 | while (node) { 85 | node.rightMarkerIds.delete(markerId) 86 | node = node.parent 87 | } 88 | 89 | node = endNode 90 | while (node) { 91 | node.leftMarkerIds.delete(markerId) 92 | node = node.parent 93 | } 94 | 95 | startNode.startMarkerIds.delete(markerId) 96 | endNode.endMarkerIds.delete(markerId) 97 | 98 | if (!startNode.isMarkerEndpoint()) { 99 | this.deleteNode(startNode) 100 | } 101 | 102 | if (endNode !== startNode && !endNode.isMarkerEndpoint()) { 103 | this.deleteNode(endNode) 104 | } 105 | 106 | delete this.startNodesById[markerId] 107 | delete this.endNodesById[markerId] 108 | } 109 | 110 | splice (start, oldExtent, newExtent) { 111 | this.nodePositionCache.clear() 112 | 113 | let invalidated = { 114 | touch: new Set, 115 | inside: new Set, 116 | overlap: new Set, 117 | surround: new Set 118 | } 119 | 120 | if (!this.root || isZero(oldExtent) && isZero(newExtent)) return invalidated 121 | 122 | let isInsertion = isZero(oldExtent) 123 | let startNode = this.iterator.insertSpliceBoundary(start, false) 124 | let endNode = this.iterator.insertSpliceBoundary(traverse(start, oldExtent), isInsertion) 125 | 126 | startNode.priority = -1 127 | this.bubbleNodeUp(startNode) 128 | endNode.priority = -2 129 | this.bubbleNodeUp(endNode) 130 | 131 | let startingInsideSplice = new Set 132 | let endingInsideSplice = new Set 133 | 134 | if (isInsertion) { 135 | startNode.startMarkerIds.forEach(markerId => { 136 | if (this.isExclusive(markerId)) { 137 | startNode.startMarkerIds.delete(markerId) 138 | startNode.rightMarkerIds.delete(markerId) 139 | endNode.startMarkerIds.add(markerId) 140 | this.startNodesById[markerId] = endNode 141 | } 142 | }) 143 | 144 | startNode.endMarkerIds.forEach(markerId => { 145 | if (!this.isExclusive(markerId) || endNode.startMarkerIds.has(markerId)) { 146 | startNode.endMarkerIds.delete(markerId) 147 | if (!endNode.startMarkerIds.has(markerId)) { 148 | startNode.rightMarkerIds.add(markerId) 149 | } 150 | endNode.endMarkerIds.add(markerId) 151 | this.endNodesById[markerId] = endNode 152 | } 153 | }) 154 | } else { 155 | this.getStartingAndEndingMarkersWithinSubtree(startNode.right, startingInsideSplice, endingInsideSplice) 156 | 157 | endingInsideSplice.forEach(markerId => { 158 | endNode.endMarkerIds.add(markerId) 159 | if (!startingInsideSplice.has(markerId)) { 160 | startNode.rightMarkerIds.add(markerId) 161 | } 162 | this.endNodesById[markerId] = endNode 163 | }) 164 | 165 | endNode.endMarkerIds.forEach(markerId => { 166 | if (this.isExclusive(markerId) && !endNode.startMarkerIds.has(markerId)) { 167 | endingInsideSplice.add(markerId) 168 | } 169 | }) 170 | 171 | startingInsideSplice.forEach(markerId => { 172 | endNode.startMarkerIds.add(markerId) 173 | this.startNodesById[markerId] = endNode 174 | }) 175 | 176 | startNode.startMarkerIds.forEach(markerId => { 177 | if (this.isExclusive(markerId) && !startNode.endMarkerIds.has(markerId)) { 178 | startNode.startMarkerIds.delete(markerId) 179 | startNode.rightMarkerIds.delete(markerId) 180 | endNode.startMarkerIds.add(markerId) 181 | this.startNodesById[markerId] = endNode 182 | startingInsideSplice.add(markerId) 183 | } 184 | }) 185 | } 186 | 187 | this.populateSpliceInvalidationSets(invalidated, startNode, endNode, startingInsideSplice, endingInsideSplice) 188 | 189 | startNode.right = null 190 | endNode.leftExtent = traverse(start, newExtent) 191 | 192 | if (compare(startNode.leftExtent, endNode.leftExtent) === 0) { 193 | endNode.startMarkerIds.forEach(markerId => { 194 | startNode.startMarkerIds.add(markerId) 195 | startNode.rightMarkerIds.add(markerId) 196 | this.startNodesById[markerId] = startNode 197 | }) 198 | endNode.endMarkerIds.forEach(markerId => { 199 | startNode.endMarkerIds.add(markerId) 200 | if (endNode.leftMarkerIds.has(markerId)) { 201 | startNode.leftMarkerIds.add(markerId) 202 | endNode.leftMarkerIds.delete(markerId) 203 | } 204 | this.endNodesById[markerId] = startNode 205 | }) 206 | this.deleteNode(endNode) 207 | } else if (endNode.isMarkerEndpoint()) { 208 | endNode.priority = this.random(MAX_PRIORITY) 209 | this.bubbleNodeDown(endNode) 210 | } else { 211 | this.deleteNode(endNode) 212 | } 213 | 214 | if (startNode.isMarkerEndpoint()) { 215 | startNode.priority = this.random(MAX_PRIORITY) 216 | this.bubbleNodeDown(startNode) 217 | } else { 218 | this.deleteNode(startNode) 219 | } 220 | 221 | return invalidated 222 | } 223 | 224 | findIntersecting (start, end = start) { 225 | let intersecting = new Set() 226 | this.iterator.findIntersecting(start, end, intersecting) 227 | return intersecting 228 | } 229 | 230 | findContaining (start, end = start) { 231 | let containing = new Set() 232 | this.iterator.findContaining(start, containing) 233 | if (compare(end, start) !== 0) { 234 | let containingEnd = new Set() 235 | this.iterator.findContaining(end, containingEnd) 236 | containing.forEach(function (markerId) { 237 | if (!containingEnd.has(markerId)) containing.delete(markerId) 238 | }) 239 | } 240 | return containing 241 | } 242 | 243 | findContainedIn (start, end) { 244 | let containedIn = new Set() 245 | this.iterator.findContainedIn(start, end, containedIn) 246 | return containedIn 247 | } 248 | 249 | findStartingIn (start, end) { 250 | let startingIn = new Set() 251 | this.iterator.findStartingIn(start, end, startingIn) 252 | return startingIn 253 | } 254 | 255 | findEndingIn (start, end) { 256 | let endingIn = new Set() 257 | this.iterator.findEndingIn(start, end, endingIn) 258 | return endingIn 259 | } 260 | 261 | findStartingAt (position) { 262 | return this.findStartingIn(position, position) 263 | } 264 | 265 | findEndingAt (position) { 266 | return this.findEndingIn(position, position) 267 | } 268 | 269 | getNodePosition (node) { 270 | let position = this.nodePositionCache.get(node) 271 | if (!position) { 272 | position = node.leftExtent 273 | let currentNode = node 274 | while (currentNode.parent) { 275 | if (currentNode.parent.right === currentNode) { 276 | position = traverse(currentNode.parent.leftExtent, position) 277 | } 278 | currentNode = currentNode.parent 279 | } 280 | this.nodePositionCache.set(node, position) 281 | } 282 | return position 283 | } 284 | 285 | deleteNode (node) { 286 | this.nodePositionCache.delete(node) 287 | node.priority = Infinity 288 | this.bubbleNodeDown(node) 289 | if (node.parent) { 290 | if (node.parent.left === node) { 291 | node.parent.left = null 292 | } else { 293 | node.parent.right = null 294 | } 295 | } else { 296 | this.root = null 297 | } 298 | } 299 | 300 | bubbleNodeUp (node) { 301 | while (node.parent && node.priority < node.parent.priority) { 302 | if (node === node.parent.left) { 303 | this.rotateNodeRight(node) 304 | } else { 305 | this.rotateNodeLeft(node) 306 | } 307 | } 308 | } 309 | 310 | bubbleNodeDown (node) { 311 | while (true) { 312 | let leftChildPriority = node.left ? node.left.priority : Infinity 313 | let rightChildPriority = node.right ? node.right.priority : Infinity 314 | 315 | if (leftChildPriority < rightChildPriority && leftChildPriority < node.priority) { 316 | this.rotateNodeRight(node.left) 317 | } else if (rightChildPriority < node.priority) { 318 | this.rotateNodeLeft(node.right) 319 | } else { 320 | break 321 | } 322 | } 323 | } 324 | 325 | rotateNodeLeft (pivot) { 326 | let root = pivot.parent 327 | 328 | if (root.parent) { 329 | if (root.parent.left === root) { 330 | root.parent.left = pivot 331 | } else { 332 | root.parent.right = pivot 333 | } 334 | } else { 335 | this.root = pivot 336 | } 337 | pivot.parent = root.parent 338 | 339 | root.right = pivot.left 340 | if (root.right) { 341 | root.right.parent = root 342 | } 343 | 344 | pivot.left = root 345 | pivot.left.parent = pivot 346 | 347 | pivot.leftExtent = traverse(root.leftExtent, pivot.leftExtent) 348 | 349 | addToSet(pivot.rightMarkerIds, root.rightMarkerIds) 350 | 351 | pivot.leftMarkerIds.forEach(function (markerId) { 352 | if (root.leftMarkerIds.has(markerId)) { 353 | root.leftMarkerIds.delete(markerId) 354 | } else { 355 | pivot.leftMarkerIds.delete(markerId) 356 | root.rightMarkerIds.add(markerId) 357 | } 358 | }) 359 | } 360 | 361 | rotateNodeRight (pivot) { 362 | let root = pivot.parent 363 | 364 | if (root.parent) { 365 | if (root.parent.left === root) { 366 | root.parent.left = pivot 367 | } else { 368 | root.parent.right = pivot 369 | } 370 | } else { 371 | this.root = pivot 372 | } 373 | pivot.parent = root.parent 374 | 375 | root.left = pivot.right 376 | if (root.left) { 377 | root.left.parent = root 378 | } 379 | 380 | pivot.right = root 381 | pivot.right.parent = pivot 382 | 383 | root.leftExtent = traversal(root.leftExtent, pivot.leftExtent) 384 | 385 | root.leftMarkerIds.forEach(function (markerId) { 386 | if (!pivot.startMarkerIds.has(markerId)) { // don't do this when pivot is at position 0 387 | pivot.leftMarkerIds.add(markerId) 388 | } 389 | }) 390 | 391 | pivot.rightMarkerIds.forEach(function (markerId) { 392 | if (root.rightMarkerIds.has(markerId)) { 393 | root.rightMarkerIds.delete(markerId) 394 | } else { 395 | pivot.rightMarkerIds.delete(markerId) 396 | root.leftMarkerIds.add(markerId) 397 | } 398 | }) 399 | } 400 | 401 | getStartingAndEndingMarkersWithinSubtree (node, startMarkerIds, endMarkerIds) { 402 | if (node == null) return 403 | 404 | this.getStartingAndEndingMarkersWithinSubtree(node.left, startMarkerIds, endMarkerIds) 405 | addToSet(startMarkerIds, node.startMarkerIds) 406 | addToSet(endMarkerIds, node.endMarkerIds) 407 | this.getStartingAndEndingMarkersWithinSubtree(node.right, startMarkerIds, endMarkerIds) 408 | } 409 | 410 | populateSpliceInvalidationSets (invalidated, startNode, endNode, startingInsideSplice, endingInsideSplice) { 411 | addToSet(invalidated.touch, startNode.endMarkerIds) 412 | addToSet(invalidated.touch, endNode.startMarkerIds) 413 | startNode.rightMarkerIds.forEach((markerId) => { 414 | invalidated.touch.add(markerId) 415 | invalidated.inside.add(markerId) 416 | }) 417 | endNode.leftMarkerIds.forEach(function (markerId) { 418 | invalidated.touch.add(markerId) 419 | invalidated.inside.add(markerId) 420 | }) 421 | startingInsideSplice.forEach(function (markerId) { 422 | invalidated.touch.add(markerId) 423 | invalidated.inside.add(markerId) 424 | invalidated.overlap.add(markerId) 425 | if (endingInsideSplice.has(markerId)) invalidated.surround.add(markerId) 426 | }) 427 | endingInsideSplice.forEach(function (markerId) { 428 | invalidated.touch.add(markerId) 429 | invalidated.inside.add(markerId) 430 | invalidated.overlap.add(markerId) 431 | }) 432 | } 433 | } 434 | -------------------------------------------------------------------------------- /shims/marker-index/iterator.js: -------------------------------------------------------------------------------- 1 | const Node = require('./node'); 2 | const {addToSet} = require('./helpers'); 3 | const {compare, isZero, traversal, traverse} = require('./point-helpers'); 4 | 5 | module.exports = class Iterator { 6 | constructor (markerIndex) { 7 | this.markerIndex = markerIndex 8 | } 9 | 10 | reset () { 11 | this.currentNode = this.markerIndex.root 12 | this.currentNodePosition = this.currentNode ? this.currentNode.leftExtent : null 13 | this.leftAncestorPosition = {row: 0, column: 0} 14 | this.rightAncestorPosition = {row: Infinity, column: Infinity} 15 | this.leftAncestorPositionStack = [] 16 | this.rightAncestorPositionStack = [] 17 | } 18 | 19 | insertMarkerStart (markerId, startPosition, endPosition) { 20 | this.reset() 21 | 22 | if (!this.currentNode) { 23 | let node = new Node(null, startPosition) 24 | this.markerIndex.root = node 25 | return node 26 | } 27 | 28 | while (true) { 29 | let comparison = compare(startPosition, this.currentNodePosition) 30 | if (comparison === 0) { 31 | this.markRight(markerId, startPosition, endPosition) 32 | return this.currentNode 33 | } else if (comparison < 0) { 34 | this.markRight(markerId, startPosition, endPosition) 35 | if (this.currentNode.left) { 36 | this.descendLeft() 37 | } else { 38 | this.insertLeftChild(startPosition) 39 | this.descendLeft() 40 | this.markRight(markerId, startPosition, endPosition) 41 | return this.currentNode 42 | } 43 | } else { // startPosition > this.currentNodePosition 44 | if (this.currentNode.right) { 45 | this.descendRight() 46 | } else { 47 | this.insertRightChild(startPosition) 48 | this.descendRight() 49 | this.markRight(markerId, startPosition, endPosition) 50 | return this.currentNode 51 | } 52 | } 53 | } 54 | } 55 | 56 | insertMarkerEnd (markerId, startPosition, endPosition) { 57 | this.reset() 58 | 59 | if (!this.currentNode) { 60 | let node = new Node(null, endPosition) 61 | this.markerIndex.root = node 62 | return node 63 | } 64 | 65 | while (true) { 66 | let comparison = compare(endPosition, this.currentNodePosition) 67 | if (comparison === 0) { 68 | this.markLeft(markerId, startPosition, endPosition) 69 | return this.currentNode 70 | } else if (comparison < 0) { 71 | if (this.currentNode.left) { 72 | this.descendLeft() 73 | } else { 74 | this.insertLeftChild(endPosition) 75 | this.descendLeft() 76 | this.markLeft(markerId, startPosition, endPosition) 77 | return this.currentNode 78 | } 79 | } else { // endPosition > this.currentNodePosition 80 | this.markLeft(markerId, startPosition, endPosition) 81 | if (this.currentNode.right) { 82 | this.descendRight() 83 | } else { 84 | this.insertRightChild(endPosition) 85 | this.descendRight() 86 | this.markLeft(markerId, startPosition, endPosition) 87 | return this.currentNode 88 | } 89 | } 90 | } 91 | } 92 | 93 | insertSpliceBoundary (position, isInsertionEnd) { 94 | this.reset() 95 | 96 | while (true) { 97 | let comparison = compare(position, this.currentNodePosition) 98 | if (comparison === 0 && !isInsertionEnd) { 99 | return this.currentNode 100 | } else if (comparison < 0) { 101 | if (this.currentNode.left) { 102 | this.descendLeft() 103 | } else { 104 | this.insertLeftChild(position) 105 | return this.currentNode.left 106 | } 107 | } else { // position > this.currentNodePosition 108 | if (this.currentNode.right) { 109 | this.descendRight() 110 | } else { 111 | this.insertRightChild(position) 112 | return this.currentNode.right 113 | } 114 | } 115 | } 116 | } 117 | 118 | findIntersecting (start, end, resultSet) { 119 | this.reset() 120 | if (!this.currentNode) return 121 | 122 | while (true) { 123 | this.cacheNodePosition() 124 | if (compare(start, this.currentNodePosition) < 0) { 125 | if (this.currentNode.left) { 126 | this.checkIntersection(start, end, resultSet) 127 | this.descendLeft() 128 | } else { 129 | break 130 | } 131 | } else { 132 | if (this.currentNode.right) { 133 | this.checkIntersection(start, end, resultSet) 134 | this.descendRight() 135 | } else { 136 | break 137 | } 138 | } 139 | } 140 | 141 | do { 142 | this.checkIntersection(start, end, resultSet) 143 | this.moveToSuccessor() 144 | this.cacheNodePosition() 145 | } while (this.currentNode && compare(this.currentNodePosition, end) <= 0) 146 | } 147 | 148 | findContaining (position, resultSet) { 149 | this.reset() 150 | if (!this.currentNode) return 151 | 152 | while (true) { 153 | this.checkIntersection(position, position, resultSet) 154 | this.cacheNodePosition() 155 | 156 | if (compare(position, this.currentNodePosition) < 0) { 157 | if (this.currentNode.left) { 158 | this.descendLeft() 159 | } else { 160 | break 161 | } 162 | } else { 163 | if (this.currentNode.right) { 164 | this.descendRight() 165 | } else { 166 | break 167 | } 168 | } 169 | } 170 | } 171 | 172 | findContainedIn (start, end, resultSet) { 173 | this.reset() 174 | if (!this.currentNode) return 175 | 176 | this.seekToFirstNodeGreaterThanOrEqualTo(start) 177 | 178 | let started = new Set() 179 | while (this.currentNode && compare(this.currentNodePosition, end) <= 0) { 180 | addToSet(started, this.currentNode.startMarkerIds) 181 | this.currentNode.endMarkerIds.forEach(function (markerId) { 182 | if (started.has(markerId)) { 183 | resultSet.add(markerId) 184 | } 185 | }) 186 | this.cacheNodePosition() 187 | this.moveToSuccessor() 188 | } 189 | } 190 | 191 | findStartingIn (start, end, resultSet) { 192 | this.reset() 193 | if (!this.currentNode) return 194 | 195 | this.seekToFirstNodeGreaterThanOrEqualTo(start) 196 | 197 | while (this.currentNode && compare(this.currentNodePosition, end) <= 0) { 198 | addToSet(resultSet, this.currentNode.startMarkerIds) 199 | this.cacheNodePosition() 200 | this.moveToSuccessor() 201 | } 202 | } 203 | 204 | findEndingIn (start, end, resultSet) { 205 | this.reset() 206 | if (!this.currentNode) return 207 | 208 | this.seekToFirstNodeGreaterThanOrEqualTo(start) 209 | 210 | while (this.currentNode && compare(this.currentNodePosition, end) <= 0) { 211 | addToSet(resultSet, this.currentNode.endMarkerIds) 212 | this.cacheNodePosition() 213 | this.moveToSuccessor() 214 | } 215 | } 216 | 217 | dump () { 218 | this.reset() 219 | 220 | while (this.currentNode && this.currentNode.left) { 221 | this.cacheNodePosition() 222 | this.descendLeft() 223 | } 224 | 225 | let snapshot = {} 226 | 227 | while (this.currentNode) { 228 | this.currentNode.startMarkerIds.forEach(markerId => { 229 | snapshot[markerId] = {start: this.currentNodePosition, end: null} 230 | }) 231 | 232 | this.currentNode.endMarkerIds.forEach(markerId => { 233 | snapshot[markerId].end = this.currentNodePosition 234 | }) 235 | 236 | this.cacheNodePosition() 237 | this.moveToSuccessor() 238 | } 239 | 240 | return snapshot 241 | } 242 | 243 | seekToFirstNodeGreaterThanOrEqualTo (position) { 244 | while (true) { 245 | let comparison = compare(position, this.currentNodePosition) 246 | 247 | this.cacheNodePosition() 248 | if (comparison === 0) { 249 | break 250 | } else if (comparison < 0) { 251 | if (this.currentNode.left) { 252 | this.descendLeft() 253 | } else { 254 | break 255 | } 256 | } else { 257 | if (this.currentNode.right) { 258 | this.descendRight() 259 | } else { 260 | break 261 | } 262 | } 263 | } 264 | 265 | if (compare(this.currentNodePosition, position) < 0) this.moveToSuccessor() 266 | } 267 | 268 | markLeft (markerId, startPosition, endPosition) { 269 | if (!isZero(this.currentNodePosition) && compare(startPosition, this.leftAncestorPosition) <= 0 && compare(this.currentNodePosition, endPosition) <= 0) { 270 | this.currentNode.leftMarkerIds.add(markerId) 271 | } 272 | } 273 | 274 | markRight (markerId, startPosition, endPosition) { 275 | if (compare(this.leftAncestorPosition, startPosition) < 0 && 276 | compare(startPosition, this.currentNodePosition) <= 0 && 277 | compare(this.rightAncestorPosition, endPosition) <= 0) { 278 | this.currentNode.rightMarkerIds.add(markerId) 279 | } 280 | } 281 | 282 | ascend () { 283 | if (this.currentNode.parent) { 284 | if (this.currentNode.parent.left === this.currentNode) { 285 | this.currentNodePosition = this.rightAncestorPosition 286 | } else { 287 | this.currentNodePosition = this.leftAncestorPosition 288 | } 289 | this.leftAncestorPosition = this.leftAncestorPositionStack.pop() 290 | this.rightAncestorPosition = this.rightAncestorPositionStack.pop() 291 | this.currentNode = this.currentNode.parent 292 | } else { 293 | this.currentNode = null 294 | this.currentNodePosition = null 295 | this.leftAncestorPosition = {row: 0, column: 0} 296 | this.rightAncestorPosition = {row: Infinity, column: Infinity} 297 | } 298 | } 299 | 300 | descendLeft () { 301 | this.leftAncestorPositionStack.push(this.leftAncestorPosition) 302 | this.rightAncestorPositionStack.push(this.rightAncestorPosition) 303 | 304 | this.rightAncestorPosition = this.currentNodePosition 305 | this.currentNode = this.currentNode.left 306 | this.currentNodePosition = traverse(this.leftAncestorPosition, this.currentNode.leftExtent) 307 | } 308 | 309 | descendRight () { 310 | this.leftAncestorPositionStack.push(this.leftAncestorPosition) 311 | this.rightAncestorPositionStack.push(this.rightAncestorPosition) 312 | 313 | this.leftAncestorPosition = this.currentNodePosition 314 | this.currentNode = this.currentNode.right 315 | this.currentNodePosition = traverse(this.leftAncestorPosition, this.currentNode.leftExtent) 316 | } 317 | 318 | moveToSuccessor () { 319 | if (!this.currentNode) return 320 | 321 | if (this.currentNode.right) { 322 | this.descendRight() 323 | while (this.currentNode.left) { 324 | this.descendLeft() 325 | } 326 | } else { 327 | while (this.currentNode.parent && this.currentNode.parent.right === this.currentNode) { 328 | this.ascend() 329 | } 330 | this.ascend() 331 | } 332 | } 333 | 334 | insertLeftChild (position) { 335 | this.currentNode.left = new Node(this.currentNode, traversal(position, this.leftAncestorPosition)) 336 | } 337 | 338 | insertRightChild (position) { 339 | this.currentNode.right = new Node(this.currentNode, traversal(position, this.currentNodePosition)) 340 | } 341 | 342 | cacheNodePosition () { 343 | this.markerIndex.nodePositionCache.set(this.currentNode, this.currentNodePosition) 344 | } 345 | 346 | checkIntersection (start, end, resultSet) { 347 | if (compare(this.leftAncestorPosition, end) <= 0 && compare(start, this.currentNodePosition) <= 0) { 348 | addToSet(resultSet, this.currentNode.leftMarkerIds) 349 | } 350 | 351 | if (compare(start, this.currentNodePosition) <= 0 && compare(this.currentNodePosition, end) <= 0) { 352 | addToSet(resultSet, this.currentNode.startMarkerIds) 353 | addToSet(resultSet, this.currentNode.endMarkerIds) 354 | } 355 | 356 | if (compare(this.currentNodePosition, end) <= 0 && compare(start, this.rightAncestorPosition) <= 0) { 357 | addToSet(resultSet, this.currentNode.rightMarkerIds) 358 | } 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /shims/marker-index/node.js: -------------------------------------------------------------------------------- 1 | module.exports = class Node { 2 | constructor (parent, leftExtent) { 3 | this.parent = parent 4 | this.left = null 5 | this.right = null 6 | this.leftExtent = leftExtent 7 | this.leftMarkerIds = new Set() 8 | this.rightMarkerIds = new Set() 9 | this.startMarkerIds = new Set() 10 | this.endMarkerIds = new Set() 11 | this.priority = null 12 | this.id = this.constructor.idCounter++ 13 | } 14 | 15 | isMarkerEndpoint () { 16 | return (this.startMarkerIds.size + this.endMarkerIds.size) > 0 17 | } 18 | } 19 | 20 | Node.idCounter = 1 21 | -------------------------------------------------------------------------------- /shims/marker-index/point-helpers.js: -------------------------------------------------------------------------------- 1 | module.exports.traverse = function traverse (start, traversal) { 2 | if (traversal.row === 0) { 3 | return { 4 | row: start.row, 5 | column: start.column + traversal.column 6 | } 7 | } else { 8 | return { 9 | row: start.row + traversal.row, 10 | column: traversal.column 11 | } 12 | } 13 | } 14 | 15 | module.exports.traversal = function traversal (end, start) { 16 | if (end.row === start.row) { 17 | return {row: 0, column: end.column - start.column} 18 | } else { 19 | return {row: end.row - start.row, column: end.column} 20 | } 21 | } 22 | 23 | module.exports.compare = function compare (a, b) { 24 | if (a.row < b.row) { 25 | return -1 26 | } else if (a.row > b.row) { 27 | return 1 28 | } else { 29 | if (a.column < b.column) { 30 | return -1 31 | } else if (a.column > b.column) { 32 | return 1 33 | } else { 34 | return 0 35 | } 36 | } 37 | } 38 | 39 | module.exports.max = function max (a, b) { 40 | if (compare(a, b) > 0) { 41 | return a 42 | } else { 43 | return b 44 | } 45 | } 46 | 47 | module.exports.isZero = function isZero (point) { 48 | return point.row === 0 && point.column === 0 49 | } 50 | 51 | module.exports.format = function format (point) { 52 | return `(${point.row}, ${point.column})` 53 | } 54 | -------------------------------------------------------------------------------- /shims/module.js: -------------------------------------------------------------------------------- 1 | exports.globalPaths = []; 2 | -------------------------------------------------------------------------------- /shims/nslog/index.js: -------------------------------------------------------------------------------- 1 | module.exports = console.log.bind(console); 2 | -------------------------------------------------------------------------------- /shims/nslog/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nslog" 3 | } 4 | -------------------------------------------------------------------------------- /shims/oniguruma/index.js: -------------------------------------------------------------------------------- 1 | class OnigScanner { 2 | findNextMatchSync(line, position) { 3 | return null; 4 | } 5 | } 6 | class OnigString {} 7 | 8 | module.exports = { 9 | OnigScanner, 10 | OnigString, 11 | }; 12 | -------------------------------------------------------------------------------- /shims/oniguruma/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "oniguruma" 3 | } 4 | -------------------------------------------------------------------------------- /shims/pathwatcher/directory.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | 3 | async = require 'async' 4 | {Emitter, Disposable} = require 'event-kit' 5 | fs = require 'fs-plus' 6 | Grim = require 'grim' 7 | 8 | File = require './file' 9 | PathWatcher = require './main' 10 | 11 | # Extended: Represents a directory on disk that can be watched for changes. 12 | module.exports = 13 | class Directory 14 | realPath: null 15 | subscriptionCount: 0 16 | 17 | ### 18 | Section: Construction 19 | ### 20 | 21 | # Public: Configures a new Directory instance, no files are accessed. 22 | # 23 | # * `directoryPath` A {String} containing the absolute path to the directory 24 | # * `symlink` (optional) A {Boolean} indicating if the path is a symlink. 25 | # (default: false) 26 | constructor: (directoryPath, @symlink=false, includeDeprecatedAPIs=Grim.includeDeprecatedAPIs) -> 27 | @emitter = new Emitter 28 | 29 | if includeDeprecatedAPIs 30 | @on 'contents-changed-subscription-will-be-added', @willAddSubscription 31 | @on 'contents-changed-subscription-removed', @didRemoveSubscription 32 | 33 | if directoryPath 34 | directoryPath = path.normalize(directoryPath) 35 | # Remove a trailing slash 36 | if directoryPath.length > 1 and directoryPath[directoryPath.length - 1] is path.sep 37 | directoryPath = directoryPath.substring(0, directoryPath.length - 1) 38 | @path = directoryPath 39 | 40 | @lowerCasePath = @path.toLowerCase() if fs.isCaseInsensitive() 41 | @reportOnDeprecations = true if Grim.includeDeprecatedAPIs 42 | 43 | # Public: Creates the directory on disk that corresponds to `::getPath()` if 44 | # no such directory already exists. 45 | # 46 | # * `mode` (optional) {Number} that defaults to `0777`. 47 | # 48 | # Returns a {Promise} that resolves once the directory is created on disk. It 49 | # resolves to a boolean value that is true if the directory was created or 50 | # false if it already existed. 51 | create: (mode = 0o0777) -> 52 | @exists().then (isExistingDirectory) => 53 | return false if isExistingDirectory 54 | 55 | throw Error("Root directory does not exist: #{@getPath()}") if @isRoot() 56 | 57 | @getParent().create().then => 58 | new Promise (resolve, reject) => 59 | fs.mkdir @getPath(), mode, (error) -> 60 | if error 61 | reject error 62 | else 63 | resolve true 64 | ### 65 | Section: Event Subscription 66 | ### 67 | 68 | # Public: Invoke the given callback when the directory's contents change. 69 | # 70 | # * `callback` {Function} to be called when the directory's contents change. 71 | # 72 | # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 73 | onDidChange: (callback) -> 74 | @willAddSubscription() 75 | @trackUnsubscription(@emitter.on('did-change', callback)) 76 | 77 | willAddSubscription: => 78 | @subscribeToNativeChangeEvents() if @subscriptionCount is 0 79 | @subscriptionCount++ 80 | 81 | didRemoveSubscription: => 82 | @subscriptionCount-- 83 | @unsubscribeFromNativeChangeEvents() if @subscriptionCount is 0 84 | 85 | trackUnsubscription: (subscription) -> 86 | new Disposable => 87 | subscription.dispose() 88 | @didRemoveSubscription() 89 | 90 | ### 91 | Section: Directory Metadata 92 | ### 93 | 94 | # Public: Returns a {Boolean}, always false. 95 | isFile: -> false 96 | 97 | # Public: Returns a {Boolean}, always true. 98 | isDirectory: -> true 99 | 100 | # Public: Returns a {Boolean} indicating whether or not this is a symbolic link 101 | isSymbolicLink: -> 102 | @symlink 103 | 104 | # Public: Returns a promise that resolves to a {Boolean}, true if the 105 | # directory exists, false otherwise. 106 | exists: -> 107 | new Promise (resolve) => fs.exists(@getPath(), resolve) 108 | 109 | # Public: Returns a {Boolean}, true if the directory exists, false otherwise. 110 | existsSync: -> 111 | fs.existsSync(@getPath()) 112 | 113 | # Public: Return a {Boolean}, true if this {Directory} is the root directory 114 | # of the filesystem, or false if it isn't. 115 | isRoot: -> 116 | @getParent().getRealPathSync() is @getRealPathSync() 117 | 118 | ### 119 | Section: Managing Paths 120 | ### 121 | 122 | # Public: Returns the directory's {String} path. 123 | # 124 | # This may include unfollowed symlinks or relative directory entries. Or it 125 | # may be fully resolved, it depends on what you give it. 126 | getPath: -> @path 127 | 128 | # Public: Returns this directory's completely resolved {String} path. 129 | # 130 | # All relative directory entries are removed and symlinks are resolved to 131 | # their final destination. 132 | getRealPathSync: -> 133 | unless @realPath? 134 | try 135 | @realPath = fs.realpathSync(@path) 136 | @lowerCaseRealPath = @realPath.toLowerCase() if fs.isCaseInsensitive() 137 | catch e 138 | @realPath = @path 139 | @lowerCaseRealPath = @lowerCasePath if fs.isCaseInsensitive() 140 | @realPath 141 | 142 | # Public: Returns the {String} basename of the directory. 143 | getBaseName: -> 144 | path.basename(@path) 145 | 146 | # Public: Returns the relative {String} path to the given path from this 147 | # directory. 148 | relativize: (fullPath) -> 149 | return fullPath unless fullPath 150 | 151 | # Normalize forward slashes to back slashes on windows 152 | fullPath = fullPath.replace(/\//g, '\\') if process.platform is 'win32' 153 | 154 | if fs.isCaseInsensitive() 155 | pathToCheck = fullPath.toLowerCase() 156 | directoryPath = @lowerCasePath 157 | else 158 | pathToCheck = fullPath 159 | directoryPath = @path 160 | 161 | if pathToCheck is directoryPath 162 | return '' 163 | else if @isPathPrefixOf(directoryPath, pathToCheck) 164 | return fullPath.substring(directoryPath.length + 1) 165 | 166 | # Check real path 167 | @getRealPathSync() 168 | if fs.isCaseInsensitive() 169 | directoryPath = @lowerCaseRealPath 170 | else 171 | directoryPath = @realPath 172 | 173 | if pathToCheck is directoryPath 174 | '' 175 | else if @isPathPrefixOf(directoryPath, pathToCheck) 176 | fullPath.substring(directoryPath.length + 1) 177 | else 178 | fullPath 179 | 180 | # Given a relative path, this resolves it to an absolute path relative to this 181 | # directory. If the path is already absolute or prefixed with a URI scheme, it 182 | # is returned unchanged. 183 | # 184 | # * `uri` A {String} containing the path to resolve. 185 | # 186 | # Returns a {String} containing an absolute path or `undefined` if the given 187 | # URI is falsy. 188 | resolve: (relativePath) -> 189 | return unless relativePath 190 | 191 | if relativePath?.match(/[A-Za-z0-9+-.]+:\/\//) # leave path alone if it has a scheme 192 | relativePath 193 | else if fs.isAbsolute(relativePath) 194 | path.normalize(fs.absolute(relativePath)) 195 | else 196 | path.normalize(fs.absolute(path.join(@getPath(), relativePath))) 197 | 198 | ### 199 | Section: Traversing 200 | ### 201 | 202 | # Public: Traverse to the parent directory. 203 | # 204 | # Returns a {Directory}. 205 | getParent: -> 206 | new Directory(path.join @path, '..') 207 | 208 | # Public: Traverse within this Directory to a child File. This method doesn't 209 | # actually check to see if the File exists, it just creates the File object. 210 | # 211 | # * `filename` The {String} name of a File within this Directory. 212 | # 213 | # Returns a {File}. 214 | getFile: (filename...) -> 215 | new File(path.join @getPath(), filename...) 216 | 217 | # Public: Traverse within this a Directory to a child Directory. This method 218 | # doesn't actually check to see if the Directory exists, it just creates the 219 | # Directory object. 220 | # 221 | # * `dirname` The {String} name of the child Directory. 222 | # 223 | # Returns a {Directory}. 224 | getSubdirectory: (dirname...) -> 225 | new Directory(path.join @path, dirname...) 226 | 227 | # Public: Reads file entries in this directory from disk synchronously. 228 | # 229 | # Returns an {Array} of {File} and {Directory} objects. 230 | getEntriesSync: -> 231 | directories = [] 232 | files = [] 233 | for entryPath in fs.listSync(@path) 234 | try 235 | stat = fs.lstatSync(entryPath) 236 | symlink = stat.isSymbolicLink() 237 | stat = fs.statSync(entryPath) if symlink 238 | 239 | if stat?.isDirectory() 240 | directories.push(new Directory(entryPath, symlink)) 241 | else if stat?.isFile() 242 | files.push(new File(entryPath, symlink)) 243 | 244 | directories.concat(files) 245 | 246 | # Public: Reads file entries in this directory from disk asynchronously. 247 | # 248 | # * `callback` A {Function} to call with the following arguments: 249 | # * `error` An {Error}, may be null. 250 | # * `entries` An {Array} of {File} and {Directory} objects. 251 | getEntries: (callback) -> 252 | fs.list @path, (error, entries) -> 253 | return callback(error) if error? 254 | 255 | directories = [] 256 | files = [] 257 | addEntry = (entryPath, stat, symlink, callback) -> 258 | if stat?.isDirectory() 259 | directories.push(new Directory(entryPath, symlink)) 260 | else if stat?.isFile() 261 | files.push(new File(entryPath, symlink)) 262 | callback() 263 | 264 | statEntry = (entryPath, callback) -> 265 | fs.lstat entryPath, (error, stat) -> 266 | if stat?.isSymbolicLink() 267 | fs.stat entryPath, (error, stat) -> 268 | addEntry(entryPath, stat, true, callback) 269 | else 270 | addEntry(entryPath, stat, false, callback) 271 | 272 | async.eachLimit entries, 1, statEntry, -> 273 | callback(null, directories.concat(files)) 274 | 275 | # Public: Determines if the given path (real or symbolic) is inside this 276 | # directory. This method does not actually check if the path exists, it just 277 | # checks if the path is under this directory. 278 | # 279 | # * `pathToCheck` The {String} path to check. 280 | # 281 | # Returns a {Boolean} whether the given path is inside this directory. 282 | contains: (pathToCheck) -> 283 | return false unless pathToCheck 284 | 285 | # Normalize forward slashes to back slashes on windows 286 | pathToCheck = pathToCheck.replace(/\//g, '\\') if process.platform is 'win32' 287 | 288 | if fs.isCaseInsensitive() 289 | directoryPath = @lowerCasePath 290 | pathToCheck = pathToCheck.toLowerCase() 291 | else 292 | directoryPath = @path 293 | 294 | return true if @isPathPrefixOf(directoryPath, pathToCheck) 295 | 296 | # Check real path 297 | @getRealPathSync() 298 | if fs.isCaseInsensitive() 299 | directoryPath = @lowerCaseRealPath 300 | else 301 | directoryPath = @realPath 302 | 303 | @isPathPrefixOf(directoryPath, pathToCheck) 304 | 305 | ### 306 | Section: Private 307 | ### 308 | 309 | subscribeToNativeChangeEvents: -> 310 | @watchSubscription ?= PathWatcher.watch @path, (eventType) => 311 | if eventType is 'change' 312 | @emit 'contents-changed' if Grim.includeDeprecatedAPIs 313 | @emitter.emit 'did-change' 314 | 315 | unsubscribeFromNativeChangeEvents: -> 316 | if @watchSubscription? 317 | @watchSubscription.close() 318 | @watchSubscription = null 319 | 320 | # Does given full path start with the given prefix? 321 | isPathPrefixOf: (prefix, fullPath) -> 322 | fullPath.indexOf(prefix) is 0 and fullPath[prefix.length] is path.sep 323 | 324 | if Grim.includeDeprecatedAPIs 325 | EmitterMixin = require('emissary').Emitter 326 | EmitterMixin.includeInto(Directory) 327 | 328 | Directory::on = (eventName) -> 329 | if eventName is 'contents-changed' 330 | Grim.deprecate("Use Directory::onDidChange instead") 331 | else if @reportOnDeprecations 332 | Grim.deprecate("Subscribing via ::on is deprecated. Use documented event subscription methods instead.") 333 | 334 | EmitterMixin::on.apply(this, arguments) 335 | -------------------------------------------------------------------------------- /shims/pathwatcher/directory.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const async = require('async'); 4 | const { Emitter, Disposable } = require('event-kit'); 5 | const fs = require('fs-plus'); 6 | const Grim = require('grim'); 7 | 8 | const File = require('./file'); 9 | // This is the native code in this module that we have to avoid. 10 | // const PathWatcher = require('./main'); 11 | 12 | // Extended: Represents a directory on disk that can be watched for changes. 13 | class Directory { 14 | static initClass() { 15 | this.prototype.realPath = null; 16 | this.prototype.subscriptionCount = 0; 17 | } 18 | 19 | /* 20 | Section: Construction 21 | */ 22 | 23 | // Public: Configures a new Directory instance, no files are accessed. 24 | // 25 | // * `directoryPath` A {String} containing the absolute path to the directory 26 | // * `symlink` (optional) A {Boolean} indicating if the path is a symlink. 27 | // (default: false) 28 | constructor(directoryPath, symlink=false, includeDeprecatedAPIs=Grim.includeDeprecatedAPIs) { 29 | this.willAddSubscription = this.willAddSubscription.bind(this); 30 | this.didRemoveSubscription = this.didRemoveSubscription.bind(this); 31 | this.symlink = symlink; 32 | this.emitter = new Emitter; 33 | 34 | if (includeDeprecatedAPIs) { 35 | this.on('contents-changed-subscription-will-be-added', this.willAddSubscription); 36 | this.on('contents-changed-subscription-removed', this.didRemoveSubscription); 37 | } 38 | 39 | if (directoryPath) { 40 | directoryPath = path.normalize(directoryPath); 41 | // Remove a trailing slash 42 | if (directoryPath.length > 1 && directoryPath[directoryPath.length - 1] === path.sep) { 43 | directoryPath = directoryPath.substring(0, directoryPath.length - 1); 44 | } 45 | } 46 | this.path = directoryPath; 47 | 48 | if (fs.isCaseInsensitive()) { this.lowerCasePath = this.path.toLowerCase(); } 49 | if (Grim.includeDeprecatedAPIs) { this.reportOnDeprecations = true; } 50 | } 51 | 52 | // Public: Creates the directory on disk that corresponds to `::getPath()` if 53 | // no such directory already exists. 54 | // 55 | // * `mode` (optional) {Number} that defaults to `0777`. 56 | // 57 | // Returns a {Promise} that resolves once the directory is created on disk. It 58 | // resolves to a boolean value that is true if the directory was created or 59 | // false if it already existed. 60 | create(mode = 0o0777) { 61 | return this.exists().then(isExistingDirectory => { 62 | if (isExistingDirectory) { return false; } 63 | 64 | if (this.isRoot()) { throw Error(`Root directory does not exist: ${this.getPath()}`); } 65 | 66 | return this.getParent().create().then(() => { 67 | return new Promise((resolve, reject) => { 68 | return fs.mkdir(this.getPath(), mode, function(error) { 69 | if (error) { 70 | return reject(error); 71 | } else { 72 | return resolve(true); 73 | } 74 | }); 75 | } 76 | ); 77 | } 78 | ); 79 | } 80 | ); 81 | } 82 | /* 83 | Section: Event Subscription 84 | */ 85 | 86 | // Public: Invoke the given callback when the directory's contents change. 87 | // 88 | // * `callback` {Function} to be called when the directory's contents change. 89 | // 90 | // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 91 | onDidChange(callback) { 92 | this.willAddSubscription(); 93 | return this.trackUnsubscription(this.emitter.on('did-change', callback)); 94 | } 95 | 96 | willAddSubscription() { 97 | if (this.subscriptionCount === 0) { this.subscribeToNativeChangeEvents(); } 98 | return this.subscriptionCount++; 99 | } 100 | 101 | didRemoveSubscription() { 102 | this.subscriptionCount--; 103 | if (this.subscriptionCount === 0) { return this.unsubscribeFromNativeChangeEvents(); } 104 | } 105 | 106 | trackUnsubscription(subscription) { 107 | return new Disposable(() => { 108 | subscription.dispose(); 109 | return this.didRemoveSubscription(); 110 | } 111 | ); 112 | } 113 | 114 | /* 115 | Section: Directory Metadata 116 | */ 117 | 118 | // Public: Returns a {Boolean}, always false. 119 | isFile() { return false; } 120 | 121 | // Public: Returns a {Boolean}, always true. 122 | isDirectory() { return true; } 123 | 124 | // Public: Returns a {Boolean} indicating whether or not this is a symbolic link 125 | isSymbolicLink() { 126 | return this.symlink; 127 | } 128 | 129 | // Public: Returns a promise that resolves to a {Boolean}, true if the 130 | // directory exists, false otherwise. 131 | exists() { 132 | return new Promise(resolve => fs.exists(this.getPath(), resolve)); 133 | } 134 | 135 | // Public: Returns a {Boolean}, true if the directory exists, false otherwise. 136 | existsSync() { 137 | return fs.existsSync(this.getPath()); 138 | } 139 | 140 | // Public: Return a {Boolean}, true if this {Directory} is the root directory 141 | // of the filesystem, or false if it isn't. 142 | isRoot() { 143 | return this.getParent().getRealPathSync() === this.getRealPathSync(); 144 | } 145 | 146 | /* 147 | Section: Managing Paths 148 | */ 149 | 150 | // Public: Returns the directory's {String} path. 151 | // 152 | // This may include unfollowed symlinks or relative directory entries. Or it 153 | // may be fully resolved, it depends on what you give it. 154 | getPath() { return this.path; } 155 | 156 | // Public: Returns this directory's completely resolved {String} path. 157 | // 158 | // All relative directory entries are removed and symlinks are resolved to 159 | // their final destination. 160 | getRealPathSync() { 161 | if (this.realPath == null) { 162 | try { 163 | this.realPath = fs.realpathSync(this.path); 164 | if (fs.isCaseInsensitive()) { this.lowerCaseRealPath = this.realPath.toLowerCase(); } 165 | } catch (e) { 166 | this.realPath = this.path; 167 | if (fs.isCaseInsensitive()) { this.lowerCaseRealPath = this.lowerCasePath; } 168 | } 169 | } 170 | return this.realPath; 171 | } 172 | 173 | // Public: Returns the {String} basename of the directory. 174 | getBaseName() { 175 | return path.basename(this.path); 176 | } 177 | 178 | // Public: Returns the relative {String} path to the given path from this 179 | // directory. 180 | relativize(fullPath) { 181 | if (!fullPath) { return fullPath; } 182 | 183 | // Normalize forward slashes to back slashes on windows 184 | if (process.platform === 'win32') { fullPath = fullPath.replace(/\//g, '\\'); } 185 | 186 | if (fs.isCaseInsensitive()) { 187 | var pathToCheck = fullPath.toLowerCase(); 188 | var directoryPath = this.lowerCasePath; 189 | } else { 190 | var pathToCheck = fullPath; 191 | var directoryPath = this.path; 192 | } 193 | 194 | if (pathToCheck === directoryPath) { 195 | return ''; 196 | } else if (this.isPathPrefixOf(directoryPath, pathToCheck)) { 197 | return fullPath.substring(directoryPath.length + 1); 198 | } 199 | 200 | // Check real path 201 | this.getRealPathSync(); 202 | if (fs.isCaseInsensitive()) { 203 | var directoryPath = this.lowerCaseRealPath; 204 | } else { 205 | var directoryPath = this.realPath; 206 | } 207 | 208 | if (pathToCheck === directoryPath) { 209 | return ''; 210 | } else if (this.isPathPrefixOf(directoryPath, pathToCheck)) { 211 | return fullPath.substring(directoryPath.length + 1); 212 | } else { 213 | return fullPath; 214 | } 215 | } 216 | 217 | // Given a relative path, this resolves it to an absolute path relative to this 218 | // directory. If the path is already absolute or prefixed with a URI scheme, it 219 | // is returned unchanged. 220 | // 221 | // * `uri` A {String} containing the path to resolve. 222 | // 223 | // Returns a {String} containing an absolute path or `undefined` if the given 224 | // URI is falsy. 225 | resolve(relativePath) { 226 | if (!relativePath) { return; } 227 | 228 | if (__guard__(relativePath, x => x.match(/[A-Za-z0-9+-.]+:\/\//))) { // leave path alone if it has a scheme 229 | return relativePath; 230 | } else if (fs.isAbsolute(relativePath)) { 231 | return path.normalize(fs.absolute(relativePath)); 232 | } else { 233 | return path.normalize(fs.absolute(path.join(this.getPath(), relativePath))); 234 | } 235 | } 236 | 237 | /* 238 | Section: Traversing 239 | */ 240 | 241 | // Public: Traverse to the parent directory. 242 | // 243 | // Returns a {Directory}. 244 | getParent() { 245 | return new Directory(path.join(this.path, '..')); 246 | } 247 | 248 | // Public: Traverse within this Directory to a child File. This method doesn't 249 | // actually check to see if the File exists, it just creates the File object. 250 | // 251 | // * `filename` The {String} name of a File within this Directory. 252 | // 253 | // Returns a {File}. 254 | getFile(...filename) { 255 | return new File(path.join(this.getPath(), ...filename)); 256 | } 257 | 258 | // Public: Traverse within this a Directory to a child Directory. This method 259 | // doesn't actually check to see if the Directory exists, it just creates the 260 | // Directory object. 261 | // 262 | // * `dirname` The {String} name of the child Directory. 263 | // 264 | // Returns a {Directory}. 265 | getSubdirectory(...dirname) { 266 | return new Directory(path.join(this.path, ...dirname)); 267 | } 268 | 269 | // Public: Reads file entries in this directory from disk synchronously. 270 | // 271 | // Returns an {Array} of {File} and {Directory} objects. 272 | getEntriesSync() { 273 | let directories = []; 274 | let files = []; 275 | for (let entryPath of fs.listSync(this.path)) { 276 | try { 277 | var stat = fs.lstatSync(entryPath); 278 | var symlink = stat.isSymbolicLink(); 279 | if (symlink) { stat = fs.statSync(entryPath); } 280 | } catch (error) {} 281 | 282 | if (__guard__(stat, x => x.isDirectory())) { 283 | directories.push(new Directory(entryPath, symlink)); 284 | } else if (__guard__(stat, x1 => x1.isFile())) { 285 | files.push(new File(entryPath, symlink)); 286 | } 287 | } 288 | 289 | return directories.concat(files); 290 | } 291 | 292 | // Public: Reads file entries in this directory from disk asynchronously. 293 | // 294 | // * `callback` A {Function} to call with the following arguments: 295 | // * `error` An {Error}, may be null. 296 | // * `entries` An {Array} of {File} and {Directory} objects. 297 | getEntries(callback) { 298 | return fs.list(this.path, function(error, entries) { 299 | if (error != null) { return callback(error); } 300 | 301 | let directories = []; 302 | let files = []; 303 | let addEntry = function(entryPath, stat, symlink, callback) { 304 | if (__guard__(stat, x => x.isDirectory())) { 305 | directories.push(new Directory(entryPath, symlink)); 306 | } else if (__guard__(stat, x1 => x1.isFile())) { 307 | files.push(new File(entryPath, symlink)); 308 | } 309 | return callback(); 310 | }; 311 | 312 | let statEntry = (entryPath, callback) => 313 | fs.lstat(entryPath, function(error, stat) { 314 | if (__guard__(stat, x => x.isSymbolicLink())) { 315 | return fs.stat(entryPath, (error, stat) => addEntry(entryPath, stat, true, callback)); 316 | } else { 317 | return addEntry(entryPath, stat, false, callback); 318 | } 319 | }) 320 | ; 321 | 322 | return async.eachLimit(entries, 1, statEntry, () => callback(null, directories.concat(files))); 323 | }); 324 | } 325 | 326 | // Public: Determines if the given path (real or symbolic) is inside this 327 | // directory. This method does not actually check if the path exists, it just 328 | // checks if the path is under this directory. 329 | // 330 | // * `pathToCheck` The {String} path to check. 331 | // 332 | // Returns a {Boolean} whether the given path is inside this directory. 333 | contains(pathToCheck) { 334 | if (!pathToCheck) { return false; } 335 | 336 | // Normalize forward slashes to back slashes on windows 337 | if (process.platform === 'win32') { pathToCheck = pathToCheck.replace(/\//g, '\\'); } 338 | 339 | if (fs.isCaseInsensitive()) { 340 | var directoryPath = this.lowerCasePath; 341 | pathToCheck = pathToCheck.toLowerCase(); 342 | } else { 343 | var directoryPath = this.path; 344 | } 345 | 346 | if (this.isPathPrefixOf(directoryPath, pathToCheck)) { return true; } 347 | 348 | // Check real path 349 | this.getRealPathSync(); 350 | if (fs.isCaseInsensitive()) { 351 | var directoryPath = this.lowerCaseRealPath; 352 | } else { 353 | var directoryPath = this.realPath; 354 | } 355 | 356 | return this.isPathPrefixOf(directoryPath, pathToCheck); 357 | } 358 | 359 | /* 360 | Section: Private 361 | */ 362 | 363 | subscribeToNativeChangeEvents() { 364 | return this.watchSubscription != null ? this.watchSubscription : (this.watchSubscription = PathWatcher.watch(this.path, eventType => { 365 | if (eventType === 'change') { 366 | if (Grim.includeDeprecatedAPIs) { this.emit('contents-changed'); } 367 | return this.emitter.emit('did-change'); 368 | } 369 | } 370 | )); 371 | } 372 | 373 | unsubscribeFromNativeChangeEvents() { 374 | if (this.watchSubscription != null) { 375 | this.watchSubscription.close(); 376 | return this.watchSubscription = null; 377 | } 378 | } 379 | 380 | // Does given full path start with the given prefix? 381 | isPathPrefixOf(prefix, fullPath) { 382 | return fullPath.indexOf(prefix) === 0 && fullPath[prefix.length] === path.sep; 383 | } 384 | }; 385 | Directory.initClass(); 386 | 387 | if (Grim.includeDeprecatedAPIs) { 388 | let EmitterMixin = require('emissary').Emitter; 389 | EmitterMixin.includeInto(Directory); 390 | 391 | Directory.prototype.on = function(eventName) { 392 | if (eventName === 'contents-changed') { 393 | Grim.deprecate("Use Directory::onDidChange instead"); 394 | } else if (this.reportOnDeprecations) { 395 | Grim.deprecate("Subscribing via ::on is deprecated. Use documented event subscription methods instead."); 396 | } 397 | 398 | return EmitterMixin.prototype.on.apply(this, arguments); 399 | }; 400 | } 401 | 402 | function __guard__(value, transform) { 403 | return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined; 404 | } 405 | 406 | module.exports = Directory; 407 | -------------------------------------------------------------------------------- /shims/pathwatcher/file.coffee: -------------------------------------------------------------------------------- 1 | crypto = require 'crypto' 2 | path = require 'path' 3 | 4 | _ = require 'underscore-plus' 5 | {Emitter, Disposable} = require 'event-kit' 6 | fs = require 'fs-plus' 7 | Grim = require 'grim' 8 | 9 | runas = null # Defer until used 10 | iconv = null # Defer until used 11 | 12 | Directory = null 13 | PathWatcher = require './main' 14 | 15 | # Extended: Represents an individual file that can be watched, read from, and 16 | # written to. 17 | module.exports = 18 | class File 19 | encoding: 'utf8' 20 | realPath: null 21 | subscriptionCount: 0 22 | 23 | ### 24 | Section: Construction 25 | ### 26 | 27 | # Public: Configures a new File instance, no files are accessed. 28 | # 29 | # * `filePath` A {String} containing the absolute path to the file 30 | # * `symlink` A {Boolean} indicating if the path is a symlink (default: false). 31 | constructor: (filePath, @symlink=false, includeDeprecatedAPIs=Grim.includeDeprecatedAPIs) -> 32 | filePath = path.normalize(filePath) if filePath 33 | @path = filePath 34 | @emitter = new Emitter 35 | 36 | if includeDeprecatedAPIs 37 | @on 'contents-changed-subscription-will-be-added', @willAddSubscription 38 | @on 'moved-subscription-will-be-added', @willAddSubscription 39 | @on 'removed-subscription-will-be-added', @willAddSubscription 40 | @on 'contents-changed-subscription-removed', @didRemoveSubscription 41 | @on 'moved-subscription-removed', @didRemoveSubscription 42 | @on 'removed-subscription-removed', @didRemoveSubscription 43 | 44 | @cachedContents = null 45 | @reportOnDeprecations = true 46 | 47 | # Public: Creates the file on disk that corresponds to `::getPath()` if no 48 | # such file already exists. 49 | # 50 | # Returns a {Promise} that resolves once the file is created on disk. It 51 | # resolves to a boolean value that is true if the file was created or false if 52 | # it already existed. 53 | create: -> 54 | @exists().then (isExistingFile) => 55 | unless isExistingFile 56 | parent = @getParent() 57 | parent.create().then => 58 | @write('').then -> true 59 | else 60 | false 61 | 62 | ### 63 | Section: Event Subscription 64 | ### 65 | 66 | # Public: Invoke the given callback when the file's contents change. 67 | # 68 | # * `callback` {Function} to be called when the file's contents change. 69 | # 70 | # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 71 | onDidChange: (callback) -> 72 | @willAddSubscription() 73 | @trackUnsubscription(@emitter.on('did-change', callback)) 74 | 75 | # Public: Invoke the given callback when the file's path changes. 76 | # 77 | # * `callback` {Function} to be called when the file's path changes. 78 | # 79 | # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 80 | onDidRename: (callback) -> 81 | @willAddSubscription() 82 | @trackUnsubscription(@emitter.on('did-rename', callback)) 83 | 84 | # Public: Invoke the given callback when the file is deleted. 85 | # 86 | # * `callback` {Function} to be called when the file is deleted. 87 | # 88 | # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 89 | onDidDelete: (callback) -> 90 | @willAddSubscription() 91 | @trackUnsubscription(@emitter.on('did-delete', callback)) 92 | 93 | # Public: Invoke the given callback when there is an error with the watch. 94 | # When your callback has been invoked, the file will have unsubscribed from 95 | # the file watches. 96 | # 97 | # * `callback` {Function} callback 98 | # * `errorObject` {Object} 99 | # * `error` {Object} the error object 100 | # * `handle` {Function} call this to indicate you have handled the error. 101 | # The error will not be thrown if this function is called. 102 | onWillThrowWatchError: (callback) -> 103 | @emitter.on('will-throw-watch-error', callback) 104 | 105 | willAddSubscription: => 106 | @subscriptionCount++ 107 | try 108 | @subscribeToNativeChangeEvents() 109 | 110 | didRemoveSubscription: => 111 | @subscriptionCount-- 112 | @unsubscribeFromNativeChangeEvents() if @subscriptionCount is 0 113 | 114 | trackUnsubscription: (subscription) -> 115 | new Disposable => 116 | subscription.dispose() 117 | @didRemoveSubscription() 118 | 119 | ### 120 | Section: File Metadata 121 | ### 122 | 123 | # Public: Returns a {Boolean}, always true. 124 | isFile: -> true 125 | 126 | # Public: Returns a {Boolean}, always false. 127 | isDirectory: -> false 128 | 129 | # Public: Returns a {Boolean} indicating whether or not this is a symbolic link 130 | isSymbolicLink: -> 131 | @symlink 132 | 133 | # Public: Returns a promise that resolves to a {Boolean}, true if the file 134 | # exists, false otherwise. 135 | exists: -> 136 | new Promise (resolve) => 137 | fs.exists @getPath(), resolve 138 | 139 | # Public: Returns a {Boolean}, true if the file exists, false otherwise. 140 | existsSync: -> 141 | fs.existsSync(@getPath()) 142 | 143 | # Public: Get the SHA-1 digest of this file 144 | # 145 | # Returns a promise that resolves to a {String}. 146 | getDigest: -> 147 | if @digest? 148 | Promise.resolve(@digest) 149 | else 150 | @read().then => @digest # read assigns digest as a side-effect 151 | 152 | # Public: Get the SHA-1 digest of this file 153 | # 154 | # Returns a {String}. 155 | getDigestSync: -> 156 | @readSync() unless @digest 157 | @digest 158 | 159 | setDigest: (contents) -> 160 | @digest = crypto.createHash('sha1').update(contents ? '').digest('hex') 161 | 162 | # Public: Sets the file's character set encoding name. 163 | # 164 | # * `encoding` The {String} encoding to use (default: 'utf8') 165 | setEncoding: (encoding='utf8') -> 166 | # Throws if encoding doesn't exist. Better to throw an exception early 167 | # instead of waiting until the file is saved. 168 | 169 | if encoding isnt 'utf8' 170 | iconv ?= require 'iconv-lite' 171 | iconv.getCodec(encoding) 172 | 173 | @encoding = encoding 174 | 175 | # Public: Returns the {String} encoding name for this file (default: 'utf8'). 176 | getEncoding: -> @encoding 177 | 178 | ### 179 | Section: Managing Paths 180 | ### 181 | 182 | # Public: Returns the {String} path for the file. 183 | getPath: -> @path 184 | 185 | # Sets the path for the file. 186 | setPath: (@path) -> 187 | @realPath = null 188 | 189 | # Public: Returns this file's completely resolved {String} path. 190 | getRealPathSync: -> 191 | unless @realPath? 192 | try 193 | @realPath = fs.realpathSync(@path) 194 | catch error 195 | @realPath = @path 196 | @realPath 197 | 198 | # Public: Returns a promise that resolves to the file's completely resolved {String} path. 199 | getRealPath: -> 200 | if @realPath? 201 | Promise.resolve(@realPath) 202 | else 203 | new Promise (resolve, reject) => 204 | fs.realpath @path, (err, result) => 205 | if err? 206 | reject(err) 207 | else 208 | resolve(@realPath = result) 209 | 210 | # Public: Return the {String} filename without any directory information. 211 | getBaseName: -> 212 | path.basename(@path) 213 | 214 | ### 215 | Section: Traversing 216 | ### 217 | 218 | # Public: Return the {Directory} that contains this file. 219 | getParent: -> 220 | Directory ?= require './directory' 221 | new Directory(path.dirname @path) 222 | 223 | ### 224 | Section: Reading and Writing 225 | ### 226 | 227 | readSync: (flushCache) -> 228 | if not @existsSync() 229 | @cachedContents = null 230 | else if not @cachedContents? or flushCache 231 | encoding = @getEncoding() 232 | if encoding is 'utf8' 233 | @cachedContents = fs.readFileSync(@getPath(), encoding) 234 | else 235 | iconv ?= require 'iconv-lite' 236 | @cachedContents = iconv.decode(fs.readFileSync(@getPath()), encoding) 237 | 238 | @setDigest(@cachedContents) 239 | @cachedContents 240 | 241 | writeFileSync: (filePath, contents) -> 242 | encoding = @getEncoding() 243 | if encoding is 'utf8' 244 | fs.writeFileSync(filePath, contents, {encoding}) 245 | else 246 | iconv ?= require 'iconv-lite' 247 | fs.writeFileSync(filePath, iconv.encode(contents, encoding)) 248 | 249 | # Public: Reads the contents of the file. 250 | # 251 | # * `flushCache` A {Boolean} indicating whether to require a direct read or if 252 | # a cached copy is acceptable. 253 | # 254 | # Returns a promise that resolves to a String. 255 | read: (flushCache) -> 256 | if @cachedContents? and not flushCache 257 | promise = Promise.resolve(@cachedContents) 258 | else 259 | promise = new Promise (resolve, reject) => 260 | content = [] 261 | readStream = @createReadStream() 262 | 263 | readStream.on 'data', (chunk) -> 264 | content.push(chunk) 265 | 266 | readStream.on 'end', -> 267 | resolve(content.join('')) 268 | 269 | readStream.on 'error', (error) -> 270 | if error.code == 'ENOENT' 271 | resolve(null) 272 | else 273 | reject(error) 274 | 275 | promise.then (contents) => 276 | @setDigest(contents) 277 | @cachedContents = contents 278 | 279 | # Public: Returns a stream to read the content of the file. 280 | # 281 | # Returns a {ReadStream} object. 282 | createReadStream: -> 283 | encoding = @getEncoding() 284 | if encoding is 'utf8' 285 | fs.createReadStream(@getPath(), {encoding}) 286 | else 287 | iconv ?= require 'iconv-lite' 288 | fs.createReadStream(@getPath()).pipe(iconv.decodeStream(encoding)) 289 | 290 | # Public: Overwrites the file with the given text. 291 | # 292 | # * `text` The {String} text to write to the underlying file. 293 | # 294 | # Returns a {Promise} that resolves when the file has been written. 295 | write: (text) -> 296 | @exists().then (previouslyExisted) => 297 | @writeFile(@getPath(), text).then => 298 | @cachedContents = text 299 | @setDigest(text) 300 | @subscribeToNativeChangeEvents() if not previouslyExisted and @hasSubscriptions() 301 | undefined 302 | 303 | # Public: Returns a stream to write content to the file. 304 | # 305 | # Returns a {WriteStream} object. 306 | createWriteStream: -> 307 | encoding = @getEncoding() 308 | if encoding is 'utf8' 309 | fs.createWriteStream(@getPath(), {encoding}) 310 | else 311 | iconv ?= require 'iconv-lite' 312 | stream = iconv.encodeStream(encoding) 313 | stream.pipe(fs.createWriteStream(@getPath())) 314 | stream 315 | 316 | # Public: Overwrites the file with the given text. 317 | # 318 | # * `text` The {String} text to write to the underlying file. 319 | # 320 | # Returns undefined. 321 | writeSync: (text) -> 322 | previouslyExisted = @existsSync() 323 | @writeFileWithPrivilegeEscalationSync(@getPath(), text) 324 | @cachedContents = text 325 | @setDigest(text) 326 | @subscribeToNativeChangeEvents() if not previouslyExisted and @hasSubscriptions() 327 | undefined 328 | 329 | safeWriteSync: (text) -> 330 | try 331 | fd = fs.openSync(@getPath(), 'w') 332 | fs.writeSync(fd, text) 333 | 334 | # Ensure file contents are really on disk before proceeding 335 | fs.fdatasyncSync(fd) 336 | fs.closeSync(fd) 337 | 338 | # Ensure file directory entry is really on disk before proceeding 339 | # 340 | # Windows doesn't support syncing on directories so we'll just have to live 341 | # with less safety on that platform. 342 | unless process.platform is 'win32' 343 | try 344 | directoryFD = fs.openSync(path.dirname(@getPath()), 'r') 345 | fs.fdatasyncSync(directoryFD) 346 | fs.closeSync(directoryFD) 347 | catch error 348 | console.warn("Non-fatal error syncing parent directory of #{@getPath()}") 349 | return 350 | catch error 351 | if error.code is 'EACCES' and process.platform is 'darwin' 352 | runas ?= require 'runas' 353 | # Use dd to read from stdin and write to the file path. 354 | unless runas('/bin/dd', ["of=#{@getPath()}"], stdin: text, admin: true) is 0 355 | throw error 356 | # Use sync to force completion of pending disk writes. 357 | if runas('/bin/sync', [], admin: true) isnt 0 358 | throw error 359 | else 360 | throw error 361 | 362 | writeFile: (filePath, contents) -> 363 | encoding = @getEncoding() 364 | if encoding is 'utf8' 365 | new Promise (resolve, reject) -> 366 | fs.writeFile filePath, contents, {encoding}, (err, result) -> 367 | if err? 368 | reject(err) 369 | else 370 | resolve(result) 371 | else 372 | iconv ?= require 'iconv-lite' 373 | new Promise (resolve, reject) -> 374 | fs.writeFile filePath, iconv.encode(contents, encoding), (err, result) -> 375 | if err? 376 | reject(err) 377 | else 378 | resolve(result) 379 | 380 | # Writes the text to specified path. 381 | # 382 | # Privilege escalation would be asked when current user doesn't have 383 | # permission to the path. 384 | writeFileWithPrivilegeEscalationSync: (filePath, text) -> 385 | try 386 | @writeFileSync(filePath, text) 387 | catch error 388 | if error.code is 'EACCES' and process.platform is 'darwin' 389 | runas ?= require 'runas' 390 | # Use dd to read from stdin and write to the file path, same thing could 391 | # be done with tee but it would also copy the file to stdout. 392 | unless runas('/bin/dd', ["of=#{filePath}"], stdin: text, admin: true) is 0 393 | throw error 394 | else 395 | throw error 396 | 397 | safeRemoveSync: -> 398 | try 399 | # Ensure new file contents are really on disk before proceeding 400 | fd = fs.openSync(@getPath(), 'a') 401 | fs.fdatasyncSync(fd) 402 | fs.closeSync(fd) 403 | 404 | fs.removeSync(@getPath()) 405 | return 406 | catch error 407 | if error.code is 'EACCES' and process.platform is 'darwin' 408 | runas ?= require 'runas' 409 | # Use sync to force completion of pending disk writes. 410 | if runas('/bin/sync', [], admin: true) isnt 0 411 | throw error 412 | if runas('/bin/rm', ['-f', @getPath()], admin: true) isnt 0 413 | throw error 414 | else 415 | throw error 416 | 417 | ### 418 | Section: Private 419 | ### 420 | 421 | handleNativeChangeEvent: (eventType, eventPath) -> 422 | switch eventType 423 | when 'delete' 424 | @unsubscribeFromNativeChangeEvents() 425 | @detectResurrectionAfterDelay() 426 | when 'rename' 427 | @setPath(eventPath) 428 | @emit 'moved' if Grim.includeDeprecatedAPIs 429 | @emitter.emit 'did-rename' 430 | when 'change', 'resurrect' 431 | oldContents = @cachedContents 432 | handleReadError = (error) => 433 | # We cant read the file, so we GTFO on the watch 434 | @unsubscribeFromNativeChangeEvents() 435 | 436 | handled = false 437 | handle = -> handled = true 438 | error.eventType = eventType 439 | @emitter.emit('will-throw-watch-error', {error, handle}) 440 | unless handled 441 | newError = new Error("Cannot read file after file `#{eventType}` event: #{@path}") 442 | newError.originalError = error 443 | newError.code = "ENOENT" 444 | newError.path 445 | # I want to throw the error here, but it stops the event loop or 446 | # something. No longer do interval or timeout methods get run! 447 | # throw newError 448 | console.error newError 449 | 450 | try 451 | handleResolve = (newContents) => 452 | unless oldContents is newContents 453 | @emit 'contents-changed' if Grim.includeDeprecatedAPIs 454 | @emitter.emit 'did-change' 455 | 456 | @read(true).then(handleResolve, handleReadError) 457 | catch error 458 | handleReadError(error) 459 | 460 | detectResurrectionAfterDelay: -> 461 | _.delay (=> @detectResurrection()), 50 462 | 463 | detectResurrection: -> 464 | @exists().then (exists) => 465 | if exists 466 | @subscribeToNativeChangeEvents() 467 | @handleNativeChangeEvent('resurrect', @getPath()) 468 | else 469 | @cachedContents = null 470 | @emit 'removed' if Grim.includeDeprecatedAPIs 471 | @emitter.emit 'did-delete' 472 | 473 | subscribeToNativeChangeEvents: -> 474 | @watchSubscription ?= PathWatcher.watch @path, (args...) => 475 | @handleNativeChangeEvent(args...) 476 | 477 | unsubscribeFromNativeChangeEvents: -> 478 | if @watchSubscription? 479 | @watchSubscription.close() 480 | @watchSubscription = null 481 | 482 | if Grim.includeDeprecatedAPIs 483 | EmitterMixin = require('emissary').Emitter 484 | EmitterMixin.includeInto(File) 485 | 486 | File::on = (eventName) -> 487 | switch eventName 488 | when 'contents-changed' 489 | Grim.deprecate("Use File::onDidChange instead") 490 | when 'moved' 491 | Grim.deprecate("Use File::onDidRename instead") 492 | when 'removed' 493 | Grim.deprecate("Use File::onDidDelete instead") 494 | else 495 | if @reportOnDeprecations 496 | Grim.deprecate("Subscribing via ::on is deprecated. Use documented event subscription methods instead.") 497 | 498 | EmitterMixin::on.apply(this, arguments) 499 | else 500 | File::hasSubscriptions = -> 501 | @subscriptionCount > 0 502 | -------------------------------------------------------------------------------- /shims/pathwatcher/file.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const path = require('path'); 3 | 4 | const _ = require('underscore-plus'); 5 | const { Emitter, Disposable } = require('event-kit'); 6 | const fs = require('fs-plus'); 7 | const Grim = require('grim'); 8 | 9 | let runas = null; // Defer until used 10 | let iconv = null; // Defer until used 11 | 12 | let Directory = null; 13 | // This is the native code in this module that we have to avoid. 14 | // const PathWatcher = require('./main'); 15 | 16 | // Extended: Represents an individual file that can be watched, read from, and 17 | // written to. 18 | class File { 19 | static initClass() { 20 | this.prototype.encoding = 'utf8'; 21 | this.prototype.realPath = null; 22 | this.prototype.subscriptionCount = 0; 23 | } 24 | 25 | /* 26 | Section: Construction 27 | */ 28 | 29 | // Public: Configures a new File instance, no files are accessed. 30 | // 31 | // * `filePath` A {String} containing the absolute path to the file 32 | // * `symlink` A {Boolean} indicating if the path is a symlink (default: false). 33 | constructor(filePath, symlink=false, includeDeprecatedAPIs=Grim.includeDeprecatedAPIs) { 34 | this.willAddSubscription = this.willAddSubscription.bind(this); 35 | this.didRemoveSubscription = this.didRemoveSubscription.bind(this); 36 | this.symlink = symlink; 37 | if (filePath) { filePath = path.normalize(filePath); } 38 | this.path = filePath; 39 | this.emitter = new Emitter; 40 | 41 | if (includeDeprecatedAPIs) { 42 | this.on('contents-changed-subscription-will-be-added', this.willAddSubscription); 43 | this.on('moved-subscription-will-be-added', this.willAddSubscription); 44 | this.on('removed-subscription-will-be-added', this.willAddSubscription); 45 | this.on('contents-changed-subscription-removed', this.didRemoveSubscription); 46 | this.on('moved-subscription-removed', this.didRemoveSubscription); 47 | this.on('removed-subscription-removed', this.didRemoveSubscription); 48 | } 49 | 50 | this.cachedContents = null; 51 | this.reportOnDeprecations = true; 52 | } 53 | 54 | // Public: Creates the file on disk that corresponds to `::getPath()` if no 55 | // such file already exists. 56 | // 57 | // Returns a {Promise} that resolves once the file is created on disk. It 58 | // resolves to a boolean value that is true if the file was created or false if 59 | // it already existed. 60 | create() { 61 | return this.exists().then(isExistingFile => { 62 | if (!isExistingFile) { 63 | let parent = this.getParent(); 64 | return parent.create().then(() => { 65 | return this.write('').then(() => true); 66 | } 67 | ); 68 | } else { 69 | return false; 70 | } 71 | } 72 | ); 73 | } 74 | 75 | /* 76 | Section: Event Subscription 77 | */ 78 | 79 | // Public: Invoke the given callback when the file's contents change. 80 | // 81 | // * `callback` {Function} to be called when the file's contents change. 82 | // 83 | // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 84 | onDidChange(callback) { 85 | this.willAddSubscription(); 86 | return this.trackUnsubscription(this.emitter.on('did-change', callback)); 87 | } 88 | 89 | // Public: Invoke the given callback when the file's path changes. 90 | // 91 | // * `callback` {Function} to be called when the file's path changes. 92 | // 93 | // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 94 | onDidRename(callback) { 95 | this.willAddSubscription(); 96 | return this.trackUnsubscription(this.emitter.on('did-rename', callback)); 97 | } 98 | 99 | // Public: Invoke the given callback when the file is deleted. 100 | // 101 | // * `callback` {Function} to be called when the file is deleted. 102 | // 103 | // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe. 104 | onDidDelete(callback) { 105 | this.willAddSubscription(); 106 | return this.trackUnsubscription(this.emitter.on('did-delete', callback)); 107 | } 108 | 109 | // Public: Invoke the given callback when there is an error with the watch. 110 | // When your callback has been invoked, the file will have unsubscribed from 111 | // the file watches. 112 | // 113 | // * `callback` {Function} callback 114 | // * `errorObject` {Object} 115 | // * `error` {Object} the error object 116 | // * `handle` {Function} call this to indicate you have handled the error. 117 | // The error will not be thrown if this function is called. 118 | onWillThrowWatchError(callback) { 119 | return this.emitter.on('will-throw-watch-error', callback); 120 | } 121 | 122 | willAddSubscription() { 123 | this.subscriptionCount++; 124 | try { 125 | return this.subscribeToNativeChangeEvents(); 126 | } catch (error) {} 127 | } 128 | 129 | didRemoveSubscription() { 130 | this.subscriptionCount--; 131 | if (this.subscriptionCount === 0) { return this.unsubscribeFromNativeChangeEvents(); } 132 | } 133 | 134 | trackUnsubscription(subscription) { 135 | return new Disposable(() => { 136 | subscription.dispose(); 137 | return this.didRemoveSubscription(); 138 | } 139 | ); 140 | } 141 | 142 | /* 143 | Section: File Metadata 144 | */ 145 | 146 | // Public: Returns a {Boolean}, always true. 147 | isFile() { return true; } 148 | 149 | // Public: Returns a {Boolean}, always false. 150 | isDirectory() { return false; } 151 | 152 | // Public: Returns a {Boolean} indicating whether or not this is a symbolic link 153 | isSymbolicLink() { 154 | return this.symlink; 155 | } 156 | 157 | // Public: Returns a promise that resolves to a {Boolean}, true if the file 158 | // exists, false otherwise. 159 | exists() { 160 | return new Promise(resolve => { 161 | return fs.exists(this.getPath(), resolve); 162 | } 163 | ); 164 | } 165 | 166 | // Public: Returns a {Boolean}, true if the file exists, false otherwise. 167 | existsSync() { 168 | return fs.existsSync(this.getPath()); 169 | } 170 | 171 | // Public: Get the SHA-1 digest of this file 172 | // 173 | // Returns a promise that resolves to a {String}. 174 | getDigest() { 175 | if (this.digest != null) { 176 | return Promise.resolve(this.digest); 177 | } else { 178 | return this.read().then(() => this.digest); // read assigns digest as a side-effect 179 | } 180 | } 181 | 182 | // Public: Get the SHA-1 digest of this file 183 | // 184 | // Returns a {String}. 185 | getDigestSync() { 186 | if (!this.digest) { this.readSync(); } 187 | return this.digest; 188 | } 189 | 190 | setDigest(contents) { 191 | return this.digest = crypto.createHash('sha1').update(contents != null ? contents : '').digest('hex'); 192 | } 193 | 194 | // Public: Sets the file's character set encoding name. 195 | // 196 | // * `encoding` The {String} encoding to use (default: 'utf8') 197 | setEncoding(encoding='utf8') { 198 | // Throws if encoding doesn't exist. Better to throw an exception early 199 | // instead of waiting until the file is saved. 200 | 201 | if (encoding !== 'utf8') { 202 | if (typeof iconv === 'undefined' || iconv === null) { iconv = require('iconv-lite'); } 203 | iconv.getCodec(encoding); 204 | } 205 | 206 | return this.encoding = encoding; 207 | } 208 | 209 | // Public: Returns the {String} encoding name for this file (default: 'utf8'). 210 | getEncoding() { return this.encoding; } 211 | 212 | /* 213 | Section: Managing Paths 214 | */ 215 | 216 | // Public: Returns the {String} path for the file. 217 | getPath() { return this.path; } 218 | 219 | // Sets the path for the file. 220 | setPath(path1) { 221 | this.path = path1; 222 | return this.realPath = null; 223 | } 224 | 225 | // Public: Returns this file's completely resolved {String} path. 226 | getRealPathSync() { 227 | if (this.realPath == null) { 228 | try { 229 | this.realPath = fs.realpathSync(this.path); 230 | } catch (error) { 231 | this.realPath = this.path; 232 | } 233 | } 234 | return this.realPath; 235 | } 236 | 237 | // Public: Returns a promise that resolves to the file's completely resolved {String} path. 238 | getRealPath() { 239 | if (this.realPath != null) { 240 | return Promise.resolve(this.realPath); 241 | } else { 242 | return new Promise((resolve, reject) => { 243 | return fs.realpath(this.path, (err, result) => { 244 | if (err != null) { 245 | return reject(err); 246 | } else { 247 | return resolve(this.realPath = result); 248 | } 249 | } 250 | ); 251 | } 252 | ); 253 | } 254 | } 255 | 256 | // Public: Return the {String} filename without any directory information. 257 | getBaseName() { 258 | return path.basename(this.path); 259 | } 260 | 261 | /* 262 | Section: Traversing 263 | */ 264 | 265 | // Public: Return the {Directory} that contains this file. 266 | getParent() { 267 | if (typeof Directory === 'undefined' || Directory === null) { Directory = require('./directory'); } 268 | return new Directory(path.dirname(this.path)); 269 | } 270 | 271 | /* 272 | Section: Reading and Writing 273 | */ 274 | 275 | readSync(flushCache) { 276 | if (!this.existsSync()) { 277 | this.cachedContents = null; 278 | } else if ((this.cachedContents == null) || flushCache) { 279 | let encoding = this.getEncoding(); 280 | if (encoding === 'utf8') { 281 | this.cachedContents = fs.readFileSync(this.getPath(), encoding); 282 | } else { 283 | if (typeof iconv === 'undefined' || iconv === null) { iconv = require('iconv-lite'); } 284 | this.cachedContents = iconv.decode(fs.readFileSync(this.getPath()), encoding); 285 | } 286 | } 287 | 288 | this.setDigest(this.cachedContents); 289 | return this.cachedContents; 290 | } 291 | 292 | writeFileSync(filePath, contents) { 293 | let encoding = this.getEncoding(); 294 | if (encoding === 'utf8') { 295 | return fs.writeFileSync(filePath, contents, {encoding}); 296 | } else { 297 | if (typeof iconv === 'undefined' || iconv === null) { iconv = require('iconv-lite'); } 298 | return fs.writeFileSync(filePath, iconv.encode(contents, encoding)); 299 | } 300 | } 301 | 302 | // Public: Reads the contents of the file. 303 | // 304 | // * `flushCache` A {Boolean} indicating whether to require a direct read or if 305 | // a cached copy is acceptable. 306 | // 307 | // Returns a promise that resolves to a String. 308 | read(flushCache) { 309 | if ((this.cachedContents != null) && !flushCache) { 310 | var promise = Promise.resolve(this.cachedContents); 311 | } else { 312 | var promise = new Promise((resolve, reject) => { 313 | let content = []; 314 | let readStream = this.createReadStream(); 315 | 316 | readStream.on('data', chunk => content.push(chunk)); 317 | 318 | readStream.on('end', () => resolve(content.join(''))); 319 | 320 | return readStream.on('error', function(error) { 321 | if (error.code === 'ENOENT') { 322 | return resolve(null); 323 | } else { 324 | return reject(error); 325 | } 326 | }); 327 | } 328 | ); 329 | } 330 | 331 | return promise.then(contents => { 332 | this.setDigest(contents); 333 | return this.cachedContents = contents; 334 | } 335 | ); 336 | } 337 | 338 | // Public: Returns a stream to read the content of the file. 339 | // 340 | // Returns a {ReadStream} object. 341 | createReadStream() { 342 | let encoding = this.getEncoding(); 343 | if (encoding === 'utf8') { 344 | return fs.createReadStream(this.getPath(), {encoding}); 345 | } else { 346 | if (typeof iconv === 'undefined' || iconv === null) { iconv = require('iconv-lite'); } 347 | return fs.createReadStream(this.getPath()).pipe(iconv.decodeStream(encoding)); 348 | } 349 | } 350 | 351 | // Public: Overwrites the file with the given text. 352 | // 353 | // * `text` The {String} text to write to the underlying file. 354 | // 355 | // Returns a {Promise} that resolves when the file has been written. 356 | write(text) { 357 | return this.exists().then(previouslyExisted => { 358 | return this.writeFile(this.getPath(), text).then(() => { 359 | this.cachedContents = text; 360 | this.setDigest(text); 361 | if (!previouslyExisted && this.hasSubscriptions()) { this.subscribeToNativeChangeEvents(); } 362 | return undefined; 363 | } 364 | ); 365 | } 366 | ); 367 | } 368 | 369 | // Public: Returns a stream to write content to the file. 370 | // 371 | // Returns a {WriteStream} object. 372 | createWriteStream() { 373 | let encoding = this.getEncoding(); 374 | if (encoding === 'utf8') { 375 | return fs.createWriteStream(this.getPath(), {encoding}); 376 | } else { 377 | if (typeof iconv === 'undefined' || iconv === null) { iconv = require('iconv-lite'); } 378 | let stream = iconv.encodeStream(encoding); 379 | stream.pipe(fs.createWriteStream(this.getPath())); 380 | return stream; 381 | } 382 | } 383 | 384 | // Public: Overwrites the file with the given text. 385 | // 386 | // * `text` The {String} text to write to the underlying file. 387 | // 388 | // Returns undefined. 389 | writeSync(text) { 390 | let previouslyExisted = this.existsSync(); 391 | this.writeFileWithPrivilegeEscalationSync(this.getPath(), text); 392 | this.cachedContents = text; 393 | this.setDigest(text); 394 | if (!previouslyExisted && this.hasSubscriptions()) { this.subscribeToNativeChangeEvents(); } 395 | return undefined; 396 | } 397 | 398 | safeWriteSync(text) { 399 | try { 400 | let fd = fs.openSync(this.getPath(), 'w'); 401 | fs.writeSync(fd, text); 402 | 403 | // Ensure file contents are really on disk before proceeding 404 | fs.fdatasyncSync(fd); 405 | fs.closeSync(fd); 406 | 407 | // Ensure file directory entry is really on disk before proceeding 408 | // 409 | // Windows doesn't support syncing on directories so we'll just have to live 410 | // with less safety on that platform. 411 | if (process.platform !== 'win32') { 412 | try { 413 | let directoryFD = fs.openSync(path.dirname(this.getPath()), 'r'); 414 | fs.fdatasyncSync(directoryFD); 415 | fs.closeSync(directoryFD); 416 | } catch (error) { 417 | console.warn(`Non-fatal error syncing parent directory of ${this.getPath()}`); 418 | } 419 | } 420 | return; 421 | } catch (error) { 422 | if (error.code === 'EACCES' && process.platform === 'darwin') { 423 | if (typeof runas === 'undefined' || runas === null) { runas = require('runas'); } 424 | // Use dd to read from stdin and write to the file path. 425 | if (runas('/bin/dd', [`of=${this.getPath()}`], {stdin: text, admin: true}) !== 0) { 426 | throw error; 427 | } 428 | // Use sync to force completion of pending disk writes. 429 | if (runas('/bin/sync', [], {admin: true}) !== 0) { 430 | throw error; 431 | } 432 | } else { 433 | throw error; 434 | } 435 | } 436 | } 437 | 438 | writeFile(filePath, contents) { 439 | let encoding = this.getEncoding(); 440 | if (encoding === 'utf8') { 441 | return new Promise((resolve, reject) => 442 | fs.writeFile(filePath, contents, {encoding}, function(err, result) { 443 | if (err != null) { 444 | return reject(err); 445 | } else { 446 | return resolve(result); 447 | } 448 | }) 449 | ); 450 | } else { 451 | if (typeof iconv === 'undefined' || iconv === null) { iconv = require('iconv-lite'); } 452 | return new Promise((resolve, reject) => 453 | fs.writeFile(filePath, iconv.encode(contents, encoding), function(err, result) { 454 | if (err != null) { 455 | return reject(err); 456 | } else { 457 | return resolve(result); 458 | } 459 | }) 460 | ); 461 | } 462 | } 463 | 464 | // Writes the text to specified path. 465 | // 466 | // Privilege escalation would be asked when current user doesn't have 467 | // permission to the path. 468 | writeFileWithPrivilegeEscalationSync(filePath, text) { 469 | try { 470 | return this.writeFileSync(filePath, text); 471 | } catch (error) { 472 | if (error.code === 'EACCES' && process.platform === 'darwin') { 473 | if (typeof runas === 'undefined' || runas === null) { runas = require('runas'); } 474 | // Use dd to read from stdin and write to the file path, same thing could 475 | // be done with tee but it would also copy the file to stdout. 476 | if (runas('/bin/dd', [`of=${filePath}`], {stdin: text, admin: true}) !== 0) { 477 | throw error; 478 | } 479 | } else { 480 | throw error; 481 | } 482 | } 483 | } 484 | 485 | safeRemoveSync() { 486 | try { 487 | // Ensure new file contents are really on disk before proceeding 488 | let fd = fs.openSync(this.getPath(), 'a'); 489 | fs.fdatasyncSync(fd); 490 | fs.closeSync(fd); 491 | 492 | fs.removeSync(this.getPath()); 493 | return; 494 | } catch (error) { 495 | if (error.code === 'EACCES' && process.platform === 'darwin') { 496 | if (typeof runas === 'undefined' || runas === null) { runas = require('runas'); } 497 | // Use sync to force completion of pending disk writes. 498 | if (runas('/bin/sync', [], {admin: true}) !== 0) { 499 | throw error; 500 | } 501 | if (runas('/bin/rm', ['-f', this.getPath()], {admin: true}) !== 0) { 502 | throw error; 503 | } 504 | } else { 505 | throw error; 506 | } 507 | } 508 | } 509 | 510 | /* 511 | Section: Private 512 | */ 513 | 514 | handleNativeChangeEvent(eventType, eventPath) { 515 | switch (eventType) { 516 | case 'delete': 517 | this.unsubscribeFromNativeChangeEvents(); 518 | return this.detectResurrectionAfterDelay(); 519 | case 'rename': 520 | this.setPath(eventPath); 521 | if (Grim.includeDeprecatedAPIs) { this.emit('moved'); } 522 | return this.emitter.emit('did-rename'); 523 | case 'change': case 'resurrect': 524 | let oldContents = this.cachedContents; 525 | let handleReadError = error => { 526 | // We cant read the file, so we GTFO on the watch 527 | this.unsubscribeFromNativeChangeEvents(); 528 | 529 | let handled = false; 530 | let handle = () => handled = true; 531 | error.eventType = eventType; 532 | this.emitter.emit('will-throw-watch-error', {error, handle}); 533 | if (!handled) { 534 | let newError = new Error(`Cannot read file after file \`${eventType}\` event: ${this.path}`); 535 | newError.originalError = error; 536 | newError.code = "ENOENT"; 537 | newError.path; 538 | // I want to throw the error here, but it stops the event loop or 539 | // something. No longer do interval or timeout methods get run! 540 | // throw newError 541 | return console.error(newError); 542 | } 543 | }; 544 | 545 | try { 546 | let handleResolve = newContents => { 547 | if (oldContents !== newContents) { 548 | if (Grim.includeDeprecatedAPIs) { this.emit('contents-changed'); } 549 | return this.emitter.emit('did-change'); 550 | } 551 | }; 552 | 553 | return this.read(true).then(handleResolve, handleReadError); 554 | } catch (error) { 555 | return handleReadError(error); 556 | } 557 | } 558 | } 559 | 560 | detectResurrectionAfterDelay() { 561 | return _.delay((() => this.detectResurrection()), 50); 562 | } 563 | 564 | detectResurrection() { 565 | return this.exists().then(exists => { 566 | if (exists) { 567 | this.subscribeToNativeChangeEvents(); 568 | return this.handleNativeChangeEvent('resurrect', this.getPath()); 569 | } else { 570 | this.cachedContents = null; 571 | if (Grim.includeDeprecatedAPIs) { this.emit('removed'); } 572 | return this.emitter.emit('did-delete'); 573 | } 574 | } 575 | ); 576 | } 577 | 578 | subscribeToNativeChangeEvents() { 579 | throw('TODO: Need a workaround here!'); 580 | return this.watchSubscription != null ? this.watchSubscription : (this.watchSubscription = PathWatcher.watch(this.path, (...args) => { 581 | return this.handleNativeChangeEvent(...args); 582 | } 583 | )); 584 | } 585 | 586 | unsubscribeFromNativeChangeEvents() { 587 | if (this.watchSubscription != null) { 588 | this.watchSubscription.close(); 589 | return this.watchSubscription = null; 590 | } 591 | } 592 | }; 593 | File.initClass(); 594 | 595 | if (Grim.includeDeprecatedAPIs) { 596 | let EmitterMixin = require('emissary').Emitter; 597 | EmitterMixin.includeInto(File); 598 | 599 | File.prototype.on = function(eventName) { 600 | switch (eventName) { 601 | case 'contents-changed': 602 | Grim.deprecate("Use File::onDidChange instead"); 603 | break; 604 | case 'moved': 605 | Grim.deprecate("Use File::onDidRename instead"); 606 | break; 607 | case 'removed': 608 | Grim.deprecate("Use File::onDidDelete instead"); 609 | break; 610 | default: 611 | if (this.reportOnDeprecations) { 612 | Grim.deprecate("Subscribing via ::on is deprecated. Use documented event subscription methods instead."); 613 | } 614 | } 615 | 616 | return EmitterMixin.prototype.on.apply(this, arguments); 617 | }; 618 | } else { 619 | File.prototype.hasSubscriptions = function() { 620 | return this.subscriptionCount > 0; 621 | }; 622 | } 623 | 624 | module.exports = File; 625 | -------------------------------------------------------------------------------- /shims/pathwatcher/index.js: -------------------------------------------------------------------------------- 1 | exports.Directory = require('./directory'); 2 | exports.File = require('./file'); 3 | 4 | exports.watch = function(path, callback) { 5 | 6 | }; 7 | 8 | exports.closeAllWatchers = function() { 9 | 10 | }; 11 | 12 | exports.closeAllWatchers = function() { 13 | 14 | }; 15 | 16 | exports.getWatchedPaths = function() { 17 | return []; 18 | }; 19 | -------------------------------------------------------------------------------- /shims/pathwatcher/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pathwatcher" 3 | } 4 | -------------------------------------------------------------------------------- /shims/remote.js: -------------------------------------------------------------------------------- 1 | var lastWindow; 2 | exports.getCurrentWindow = function() { 3 | if (lastWindow) { 4 | return lastWindow; 5 | } 6 | 7 | lastWindow = { 8 | domWindow: window, 9 | loadSettings: {}, 10 | getPosition: function() { 11 | return [window.screenX, window.screenY]; 12 | }, 13 | getSize: function() { 14 | return [window.innerWidth, window.innerHeight]; 15 | }, 16 | } 17 | 18 | return lastWindow; 19 | }; 20 | -------------------------------------------------------------------------------- /shims/screen.js: -------------------------------------------------------------------------------- 1 | exports.getPrimaryDisplay = function() { 2 | return { 3 | workAreaSize: { 4 | width: 1024, 5 | height: 800, 6 | }, 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /shims/scrollbar-style/index.js: -------------------------------------------------------------------------------- 1 | const {Emitter} = require('event-kit'); 2 | 3 | const emitter = new Emitter(); 4 | 5 | module.exports = { 6 | getPreferredScrollbarStyle() { 7 | // 'overlay' seems more appropriate than 'legacy'. 8 | return 'overlay'; 9 | }, 10 | 11 | onDidChangePreferredScrollbarStyle(callback) { 12 | return emitter.on('did-change-preferred-scrollbar-style', callback); 13 | }, 14 | 15 | observePreferredScrollbarStyle(callback) { 16 | callback(this.getPreferredScrollbarStyle()); 17 | return this.onDidChangePreferredScrollbarStyle(callback); 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /shims/scrollbar-style/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scrollbar-style" 3 | } 4 | -------------------------------------------------------------------------------- /shims/shell.js: -------------------------------------------------------------------------------- 1 | exports.beep = function() { 2 | // TODO: beep(). 3 | }; 4 | --------------------------------------------------------------------------------