Bundle autosave

This commit is contained in:
confused-Techie 2023-03-11 20:52:36 -08:00
parent c22b4c7e6d
commit 9f72376543
11 changed files with 677 additions and 0 deletions

1
packages/autosave/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules/

View File

@ -0,0 +1,20 @@
Copyright (c) 2014 GitHub Inc.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,32 @@
# Autosave package
Autosaves editor when they lose focus, are destroyed, or when the window is closed.
This package is disabled by default and can be enabled via the `autosave.enabled` config
setting or by checking *Enabled* in the settings for the *autosave* package in the
Settings view.
## Service API
The service exposes an object with a function `dontSaveIf`, which accepts a callback.
Callbacks will be invoked with each pane item eligible for an autosave and if the callback
returns true, the item will be skipped.
### Usage
#### package.json
``` json
"consumedServices": {
"autosave": {
"versions": {
"1.0.0": "consumeAutosave"
}
}
}
```
#### package initialize
``` javascript
consumeAutosave({dontSaveIf}) {
dontSaveIf(paneItem -> paneItem.getPath() === '/dont/autosave/me.coffee')
}
```

View File

@ -0,0 +1,66 @@
const fs = require('fs')
const {CompositeDisposable, Disposable} = require('atom')
const {dontSaveIf, shouldSave} = require('./controls')
module.exports = {
subscriptions: null,
provideService () {
return {dontSaveIf}
},
activate () {
this.subscriptions = new CompositeDisposable()
const handleBlur = event => {
if (event.target === window) {
this.autosaveAllPaneItems()
} else if (event.target.matches('atom-text-editor:not(mini)')) {
return this.autosavePaneItem(event.target.getModel())
}
}
window.addEventListener('blur', handleBlur, true)
this.subscriptions.add(new Disposable(() => window.removeEventListener('blur', handleBlur, true)))
this.subscriptions.add(atom.workspace.onDidAddPaneItem(({item}) => this.autosavePaneItem(item, true)))
this.subscriptions.add(atom.workspace.onWillDestroyPaneItem(({item}) => this.autosavePaneItem(item)))
},
deactivate () {
this.subscriptions.dispose()
return this.autosaveAllPaneItems()
},
autosavePaneItem (paneItem, create = false) {
if (!atom.config.get('autosave.enabled')) return
if (!paneItem) return
if (typeof paneItem.getURI !== 'function' || !paneItem.getURI()) return
if (typeof paneItem.isModified !== 'function' || !paneItem.isModified()) return
if (typeof paneItem.getPath !== 'function' || !paneItem.getPath()) return
if (!shouldSave(paneItem)) return
try {
const stats = fs.statSync(paneItem.getPath())
if (!stats.isFile()) return
} catch (e) {
if (e.code !== 'ENOENT') return
if (!create) return
}
const pane = atom.workspace.paneForItem(paneItem)
let promise = Promise.resolve()
if (pane) {
promise = pane.saveItem(paneItem)
} else if (typeof paneItem.save === 'function') {
promise = paneItem.save()
}
return promise
},
autosaveAllPaneItems () {
return Promise.all(
atom.workspace.getPaneItems().map((paneItem) => this.autosavePaneItem(paneItem))
)
}
}

21
packages/autosave/lib/controls.js vendored Normal file
View File

@ -0,0 +1,21 @@
const tests = []
module.exports = {
// Public: Add a predicate to set of tests
//
// * `predicate` A {Function} determining if a {PaneItem} should autosave.
//
// Returns `undefined`.
dontSaveIf (predicate) {
tests.push(predicate)
},
// Public: Test whether a paneItem should be autosaved.
//
// * `paneItem` A pane item {Object}.
//
// Returns `Boolean`.
shouldSave (paneItem) {
return !tests.some(test => test(paneItem))
}
}

166
packages/autosave/package-lock.json generated Normal file
View File

@ -0,0 +1,166 @@
{
"name": "autosave",
"version": "0.24.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "autosave",
"version": "0.24.6",
"dependencies": {
"fs-plus": "^3.0.0"
},
"engines": {
"atom": ">0.27.0"
}
},
"node_modules/async": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
"integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w=="
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"node_modules/fs-plus": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/fs-plus/-/fs-plus-3.1.1.tgz",
"integrity": "sha512-Se2PJdOWXqos1qVTkvqqjb0CSnfBnwwD+pq+z4ksT+e97mEShod/hrNg0TRCCsXPbJzcIq+NuzQhigunMWMJUA==",
"dependencies": {
"async": "^1.5.2",
"mkdirp": "^0.5.1",
"rimraf": "^2.5.2",
"underscore-plus": "1.x"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/minimist": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz",
"integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
}
},
"node_modules/underscore": {
"version": "1.13.6",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz",
"integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A=="
},
"node_modules/underscore-plus": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/underscore-plus/-/underscore-plus-1.7.0.tgz",
"integrity": "sha512-A3BEzkeicFLnr+U/Q3EyWwJAQPbA19mtZZ4h+lLq3ttm9kn8WC4R3YpuJZEXmWdLjYP47Zc8aLZm9kwdv+zzvA==",
"dependencies": {
"underscore": "^1.9.1"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
}
}
}

View File

@ -0,0 +1,25 @@
{
"name": "autosave",
"main": "./lib/autosave",
"version": "0.24.6",
"private": true,
"description": "Save editors when they lose focus or are closed",
"repository": "https://github.com/pulsar-edit/autosave",
"engines": {
"atom": ">0.27.0"
},
"providedServices": {
"autosave": {
"description": "A configuration object to control what is autosaved",
"versions": {
"1.0.0": "provideService"
}
}
},
"configSchema": {
"enabled": {
"type": "boolean",
"default": false
}
}
}

View File

@ -0,0 +1,43 @@
exports.beforeEach = function beforeEach (fn) {
global.beforeEach(function () {
const result = fn()
if (result instanceof Promise) {
waitsForPromise(() => result)
}
})
}
exports.afterEach = function afterEach (fn) {
global.afterEach(function () {
const result = fn()
if (result instanceof Promise) {
waitsForPromise(() => result)
}
})
}
;['it', 'fit', 'ffit', 'fffit'].forEach(function (name) {
exports[name] = function (description, fn) {
if (fn === undefined) {
global[name](description)
return
}
global[name](description, function () {
const result = fn()
if (result instanceof Promise) {
waitsForPromise(() => result)
}
})
}
})
function waitsForPromise (fn) {
const promise = fn()
global.waitsFor('spec promise to resolve', function (done) {
promise.then(done, function (error) {
jasmine.getEnv().currentSpec.fail(error)
done()
})
})
}

View File

@ -0,0 +1,267 @@
const fs = require('fs')
const {it, fit, ffit, beforeEach} = require('./async-spec-helpers') // eslint-disable-line
describe('Autosave', () => {
let workspaceElement, initialActiveItem, otherItem1, otherItem2
beforeEach(async () => {
workspaceElement = atom.views.getView(atom.workspace)
jasmine.attachToDOM(workspaceElement)
await atom.packages.activatePackage('autosave')
await atom.workspace.open('sample.js')
initialActiveItem = atom.workspace.getActiveTextEditor()
if (atom.workspace.createItemForURI != null) {
otherItem1 = await atom.workspace.createItemForURI('sample.coffee')
} else {
otherItem1 = await atom.workspace.open('sample.coffee', {activateItem: false})
}
otherItem2 = otherItem1.copy()
spyOn(initialActiveItem, 'save').andCallFake(() => Promise.resolve())
spyOn(otherItem1, 'save').andCallFake(() => Promise.resolve())
spyOn(otherItem2, 'save').andCallFake(() => Promise.resolve())
})
describe('when the item is not modified', () => {
it('does not autosave the item', () => {
atom.config.set('autosave.enabled', true)
atom.workspace.getActivePane().splitRight({items: [otherItem1]})
expect(initialActiveItem.save).not.toHaveBeenCalled()
})
})
describe('when the buffer is modified', () => {
beforeEach(() => initialActiveItem.setText('i am modified'))
it('autosaves newly added items', async () => {
const newItem = await atom.workspace.createItemForURI('notyet.js')
spyOn(newItem, 'isModified').andReturn(true)
atom.config.set('autosave.enabled', true)
spyOn(atom.workspace.getActivePane(), 'saveItem').andCallFake(() => Promise.resolve())
atom.workspace.getActivePane().addItem(newItem)
expect(atom.workspace.getActivePane().saveItem).toHaveBeenCalledWith(newItem)
})
describe('when a pane loses focus', () => {
it('saves the item if autosave is enabled and the item has a uri', () => {
document.body.focus()
expect(initialActiveItem.save).not.toHaveBeenCalled()
workspaceElement.focus()
atom.config.set('autosave.enabled', true)
document.body.focus()
expect(initialActiveItem.save).toHaveBeenCalled()
})
it('suppresses autosave if the file does not exist', () => {
document.body.focus()
expect(initialActiveItem.save).not.toHaveBeenCalled()
workspaceElement.focus()
atom.config.set('autosave.enabled', true)
const originalPath = atom.workspace.getActiveTextEditor().getPath()
const tmpPath = `${originalPath}~`
fs.renameSync(originalPath, tmpPath)
document.body.focus()
expect(initialActiveItem.save).not.toHaveBeenCalled()
fs.renameSync(tmpPath, originalPath)
})
it('suppresses autosave if the focused element is contained by the editor, such as occurs when opening the autocomplete menu', () => {
atom.config.set('autosave.enabled', true)
const focusStealer = document.createElement('div')
focusStealer.setAttribute('tabindex', -1)
const textEditorElement = atom.views.getView(atom.workspace.getActiveTextEditor())
textEditorElement.appendChild(focusStealer)
focusStealer.focus()
expect(initialActiveItem.save).not.toHaveBeenCalled()
})
})
describe('when a new pane is created', () => {
it('saves the item if autosave is enabled and the item has a uri', () => {
const leftPane = atom.workspace.getActivePane()
const rightPane = leftPane.splitRight()
expect(initialActiveItem.save).not.toHaveBeenCalled()
rightPane.destroy()
leftPane.activate()
atom.config.set('autosave.enabled', true)
leftPane.splitRight()
expect(initialActiveItem.save).toHaveBeenCalled()
})
})
describe('when an item is destroyed', () => {
describe('when the item is the active item', () => {
it('does not save the item if autosave is enabled and the item has a uri', async () => {
let leftPane = atom.workspace.getActivePane()
const rightPane = leftPane.splitRight({items: [otherItem1]})
leftPane.activate()
expect(initialActiveItem).toBe(atom.workspace.getActivePaneItem())
leftPane.destroyItem(initialActiveItem)
expect(initialActiveItem.save).not.toHaveBeenCalled()
otherItem2.setText('I am also modified')
atom.config.set('autosave.enabled', true)
leftPane = rightPane.splitLeft({items: [otherItem2]})
expect(otherItem2).toBe(atom.workspace.getActivePaneItem())
await leftPane.destroyItem(otherItem2)
expect(otherItem2.save).toHaveBeenCalled()
})
})
describe('when the item is NOT the active item', () => {
it('does not save the item if autosave is enabled and the item has a uri', () => {
let leftPane = atom.workspace.getActivePane()
const rightPane = leftPane.splitRight({items: [otherItem1]})
expect(initialActiveItem).not.toBe(atom.workspace.getActivePaneItem())
leftPane.destroyItem(initialActiveItem)
expect(initialActiveItem.save).not.toHaveBeenCalled()
otherItem2.setText('I am also modified')
atom.config.set('autosave.enabled', true)
leftPane = rightPane.splitLeft({items: [otherItem2]})
rightPane.focus()
expect(otherItem2).not.toBe(atom.workspace.getActivePaneItem())
leftPane.destroyItem(otherItem2)
expect(otherItem2.save).toHaveBeenCalled()
})
})
})
describe('when the item does not have a URI', () => {
it('does not save the item', async () => {
await atom.workspace.open()
const pathLessItem = atom.workspace.getActiveTextEditor()
spyOn(pathLessItem, 'save').andCallThrough()
pathLessItem.setText('text!')
expect(pathLessItem.getURI()).toBeFalsy()
atom.config.set('autosave.enabled', true)
atom.workspace.getActivePane().destroyItem(pathLessItem)
expect(pathLessItem.save).not.toHaveBeenCalled()
})
})
})
describe('when the window is blurred', () => {
it('saves all items', () => {
atom.config.set('autosave.enabled', true)
const leftPane = atom.workspace.getActivePane()
leftPane.splitRight({items: [otherItem1]})
initialActiveItem.insertText('a')
otherItem1.insertText('b')
window.dispatchEvent(new FocusEvent('blur'))
expect(initialActiveItem.save).toHaveBeenCalled()
expect(otherItem1.save).toHaveBeenCalled()
})
})
describe('when the package is deactivated', () => {
it('saves all items and waits for saves to complete', () => {
atom.config.set('autosave.enabled', true)
const leftPane = atom.workspace.getActivePane()
leftPane.splitRight({items: [otherItem1]})
initialActiveItem.insertText('a')
otherItem1.insertText('b')
let deactivated = false
let asyncDeactivateSupported = true
let resolveInitial = () => {}
let resolveOther = () => {}
initialActiveItem.save.andCallFake(() => {
return new Promise(resolve => {
resolveInitial = resolve
})
})
otherItem1.save.andCallFake(() => {
return new Promise(resolve => {
resolveOther = resolve
})
})
let deactivatePromise = atom.packages.deactivatePackage('autosave')
if (!deactivatePromise || !deactivatePromise.then || typeof deactivatePromise.then !== 'function') {
// Atom does not support asynchronous package deactivation.
// This keeps us from failing on 1.20
asyncDeactivateSupported = false
deactivatePromise = Promise.resolve()
}
deactivatePromise.then((result) => {
if (result === undefined) {
// This keeps us from failing on 1.21-beta1
asyncDeactivateSupported = false
}
deactivated = true
})
waitsForPromise(() => Promise.resolve())
runs(() => {
if (asyncDeactivateSupported) {
expect(deactivated).toBe(false)
}
resolveInitial()
resolveOther()
})
waitsFor(() => !asyncDeactivateSupported || deactivated)
})
})
it("saves via the item's Pane so that write errors are handled via notifications", async () => {
const saveError = new Error('Save failed')
saveError.code = 'EACCES'
saveError.path = initialActiveItem.getPath()
initialActiveItem.save.andThrow(saveError)
const errorCallback = jasmine.createSpy('errorCallback').andCallFake(({preventDefault}) => preventDefault())
atom.onWillThrowError(errorCallback)
spyOn(atom.notifications, 'addWarning')
initialActiveItem.insertText('a')
atom.config.set('autosave.enabled', true)
await atom.workspace.destroyActivePaneItem()
expect(initialActiveItem.save).toHaveBeenCalled()
expect(atom.notifications.addWarning.callCount > 0 || errorCallback.callCount > 0).toBe(true)
})
describe('dontSaveIf service', () => {
it("doesn't save a paneItem if a predicate function registered via the dontSaveIf service returns true", async () => {
atom.workspace.getActivePane().addItem(otherItem1)
atom.config.set('autosave.enabled', true)
const service = atom.packages.getActivePackage('autosave').mainModule.provideService()
service.dontSaveIf(paneItem => paneItem === initialActiveItem)
initialActiveItem.setText('foo')
otherItem1.setText('bar')
window.dispatchEvent(new FocusEvent('blur'))
expect(initialActiveItem.save).not.toHaveBeenCalled()
expect(otherItem1.save).toHaveBeenCalled()
})
})
})

View File

@ -0,0 +1,23 @@
class Quicksort
sort: (items) ->
return items if items.length <= 1
pivot = items.shift()
left = []
right = []
# Comment in the middle (and add the word 'items' again)
while items.length > 0
current = items.shift()
if current < pivot
left.push(current)
else
right.push(current)
sort(left).concat(pivot).concat(sort(right))
noop: ->
# just a noop
exports.modules = quicksort

View File

@ -0,0 +1,13 @@
var quicksort = function () {
var sort = function(items) {
if (items.length <= 1) return items;
var pivot = items.shift(), current, left = [], right = [];
while(items.length > 0) {
current = items.shift();
current < pivot ? left.push(current) : right.push(current);
}
return sort(left).concat(pivot).concat(sort(right));
};
return sort(Array.apply(this, arguments));
};