From 051e27dbcb733b66349873a96b2458fd88d6fed5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 6 Mar 2017 20:03:40 +0100 Subject: [PATCH 1/7] Expose load settings to `BrowserWindow`s as JSON When accessing objects in the main process via the `remote` module, Electron returns proxy objects that are references to the original ones. This means that trying to access a remote object's property or function results in a synchronous message exchange with the main process. In Atom core we frequently access the load settings coming from the main process, especially during startup. This caused a lot of synchronous I/O which was blocking the renderer process for several milliseconds. With this commit, instead of exposing load settings as a JavaScript object, we serialize them to JSON in the main process and parse them back to a JavaScript object in the renderer processes. This allows us to get a full copy of the object locally and pay for I/O just once when retrieving load settings from the main process for the first time. --- src/get-window-load-settings.js | 2 +- src/main-process/atom-window.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/get-window-load-settings.js b/src/get-window-load-settings.js index 7ee465141..d35b24213 100644 --- a/src/get-window-load-settings.js +++ b/src/get-window-load-settings.js @@ -4,7 +4,7 @@ let windowLoadSettings = null module.exports = () => { if (!windowLoadSettings) { - windowLoadSettings = remote.getCurrentWindow().loadSettings + windowLoadSettings = JSON.parse(remote.getCurrentWindow().loadSettingsJSON) } return windowLoadSettings } diff --git a/src/main-process/atom-window.coffee b/src/main-process/atom-window.coffee index 03386d31a..da903d71e 100644 --- a/src/main-process/atom-window.coffee +++ b/src/main-process/atom-window.coffee @@ -83,7 +83,7 @@ class AtomWindow @representedDirectoryPaths = loadSettings.initialPaths @env = loadSettings.env if loadSettings.env? - @browserWindow.loadSettings = loadSettings + @browserWindow.loadSettingsJSON = JSON.stringify(loadSettings) @browserWindow.on 'window:loaded', => @emit 'window:loaded' From 727472af58246f21bde7da2d3e2a2c65d8ee1659 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 7 Mar 2017 09:35:11 +0100 Subject: [PATCH 2/7] Disable zoom in the main process This commit will register the `display-added` and `display-removed` events only once in the main process in order to disable zoom (see https://github.com/atom/atom/pull/11345) directly instead of unnecessarily paying for I/O in the renderer process during startup. --- src/application-delegate.coffee | 16 +--------------- src/atom-environment.coffee | 2 -- src/main-process/atom-application.coffee | 22 +++++++++++++++++++--- src/main-process/atom-window.coffee | 4 ++++ 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/application-delegate.coffee b/src/application-delegate.coffee index 766ba7aa8..cd7a4d128 100644 --- a/src/application-delegate.coffee +++ b/src/application-delegate.coffee @@ -1,5 +1,5 @@ _ = require 'underscore-plus' -{screen, ipcRenderer, remote, shell, webFrame} = require 'electron' +{ipcRenderer, remote, shell} = require 'electron' ipcHelpers = require './ipc-helpers' {Disposable} = require 'event-kit' getWindowLoadSettings = require './get-window-load-settings' @@ -254,20 +254,6 @@ class ApplicationDelegate openExternal: (url) -> shell.openExternal(url) - disableZoom: -> - outerCallback = -> - webFrame.setZoomLevelLimits(1, 1) - - outerCallback() - # Set the limits every time a display is added or removed, otherwise the - # configuration gets reset to the default, which allows zooming the - # webframe. - screen.on('display-added', outerCallback) - screen.on('display-removed', outerCallback) - new Disposable -> - screen.removeListener('display-added', outerCallback) - screen.removeListener('display-removed', outerCallback) - checkForUpdate: -> ipcRenderer.send('command', 'application:check-for-update') diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index d7fab1924..d834a355f 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -215,8 +215,6 @@ class AtomEnvironment extends Model @stylesElement = @styles.buildStylesElement() @document.head.appendChild(@stylesElement) - @disposables.add(@applicationDelegate.disableZoom()) - @keymaps.subscribeToFileReadFailure() @keymaps.loadBundledKeymaps() diff --git a/src/main-process/atom-application.coffee b/src/main-process/atom-application.coffee index 93e9e3395..295343100 100644 --- a/src/main-process/atom-application.coffee +++ b/src/main-process/atom-application.coffee @@ -6,8 +6,8 @@ StorageFolder = require '../storage-folder' Config = require '../config' FileRecoveryService = require './file-recovery-service' ipcHelpers = require '../ipc-helpers' -{BrowserWindow, Menu, app, dialog, ipcMain, shell} = require 'electron' -{CompositeDisposable} = require 'event-kit' +{BrowserWindow, Menu, app, dialog, ipcMain, shell, screen} = require 'electron' +{CompositeDisposable, Disposable} = require 'event-kit' fs = require 'fs-plus' path = require 'path' os = require 'os' @@ -89,7 +89,7 @@ class AtomApplication if process.platform is 'darwin' and @config.get('core.useCustomTitleBar') @config.unset('core.useCustomTitleBar') @config.set('core.titleBar', 'custom') - + @config.onDidChange 'core.titleBar', @promptForRestart.bind(this) @autoUpdateManager = new AutoUpdateManager( @@ -394,6 +394,8 @@ class AtomApplication @disposable.add ipcHelpers.on ipcMain, 'did-change-paths', => @saveState(false) + @disposable.add(@disableZoomOnDisplayChange()) + setupDockMenu: -> if process.platform is 'darwin' dockMenu = Menu.buildFromTemplate [ @@ -812,3 +814,17 @@ class AtomApplication args.push("--resource-path=#{@resourcePath}") app.relaunch({args}) app.quit() + + disableZoomOnDisplayChange: -> + outerCallback = => + for window in @windows + window.disableZoom() + + # Set the limits every time a display is added or removed, otherwise the + # configuration gets reset to the default, which allows zooming the + # webframe. + screen.on('display-added', outerCallback) + screen.on('display-removed', outerCallback) + new Disposable -> + screen.removeListener('display-added', outerCallback) + screen.removeListener('display-removed', outerCallback) diff --git a/src/main-process/atom-window.coffee b/src/main-process/atom-window.coffee index da903d71e..6013819e3 100644 --- a/src/main-process/atom-window.coffee +++ b/src/main-process/atom-window.coffee @@ -101,6 +101,7 @@ class AtomWindow hasPathToOpen = not (locationsToOpen.length is 1 and not locationsToOpen[0].pathToOpen?) @openLocations(locationsToOpen) if hasPathToOpen and not @isSpecWindow() + @disableZoom() @atomApplication.addWindow(this) @@ -303,3 +304,6 @@ class AtomWindow @atomApplication.saveState() copy: -> @browserWindow.copy() + + disableZoom: -> + @browserWindow.webContents.setZoomLevelLimits(1, 1) From 45d41ca69f00426f911c721882a6500bb83f1154 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 7 Mar 2017 09:40:20 +0100 Subject: [PATCH 3/7] Register enter/leave fullscreen events in the main process This will still notify render processes when such events are triggered without, however, incurring the additional cost of synchronously retrieving a `BrowserWindow` (and its properties) via `remote` during startup. --- src/application-delegate.coffee | 6 ++++++ src/main-process/atom-window.coffee | 6 ++++++ src/window-event-handler.coffee | 10 ++-------- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/application-delegate.coffee b/src/application-delegate.coffee index cd7a4d128..b247fe7c2 100644 --- a/src/application-delegate.coffee +++ b/src/application-delegate.coffee @@ -80,6 +80,12 @@ class ApplicationDelegate setWindowFullScreen: (fullScreen=false) -> ipcHelpers.call('window-method', 'setFullScreen', fullScreen) + onDidEnterFullScreen: (callback) -> + ipcHelpers.on(ipcRenderer, 'did-enter-full-screen', callback) + + onDidLeaveFullScreen: (callback) -> + ipcHelpers.on(ipcRenderer, 'did-leave-full-screen', callback) + openWindowDevTools: -> # Defer DevTools interaction to the next tick, because using them during # event handling causes some wrong input events to be triggered on diff --git a/src/main-process/atom-window.coffee b/src/main-process/atom-window.coffee index 6013819e3..bbc235bc5 100644 --- a/src/main-process/atom-window.coffee +++ b/src/main-process/atom-window.coffee @@ -89,6 +89,12 @@ class AtomWindow @emit 'window:loaded' @resolveLoadedPromise() + @browserWindow.on 'enter-full-screen', => + @browserWindow.webContents.send('did-enter-full-screen') + + @browserWindow.on 'leave-full-screen', => + @browserWindow.webContents.send('did-leave-full-screen') + @browserWindow.loadURL url.format protocol: 'file' pathname: "#{@resourcePath}/static/index.html" diff --git a/src/window-event-handler.coffee b/src/window-event-handler.coffee index 62ce4527a..95cd45de9 100644 --- a/src/window-event-handler.coffee +++ b/src/window-event-handler.coffee @@ -20,14 +20,8 @@ class WindowEventHandler @subscriptions.add listen(@document, 'click', 'a', @handleLinkClick) @subscriptions.add listen(@document, 'submit', 'form', @handleFormSubmit) - browserWindow = @applicationDelegate.getCurrentWindow() - browserWindow.on 'enter-full-screen', @handleEnterFullScreen - @subscriptions.add new Disposable => - browserWindow.removeListener('enter-full-screen', @handleEnterFullScreen) - - browserWindow.on 'leave-full-screen', @handleLeaveFullScreen - @subscriptions.add new Disposable => - browserWindow.removeListener('leave-full-screen', @handleLeaveFullScreen) + @subscriptions.add(@applicationDelegate.onDidEnterFullScreen(@handleEnterFullScreen)) + @subscriptions.add(@applicationDelegate.onDidLeaveFullScreen(@handleLeaveFullScreen)) @subscriptions.add @atomEnvironment.commands.add @window, 'window:toggle-full-screen': @handleWindowToggleFullScreen From cf9a5b13e3d875b87de5bf0e3446c4edf80565f1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 7 Mar 2017 10:37:41 +0100 Subject: [PATCH 4/7] Replace `localStorage` with `StateStore` in `HistoryManager` Instead of using `localStorage` to store and retrieve the project history, with this commit we will use `StateStore` so that we can retrieve state asynchronously without blocking Atom during startup. --- spec/history-manager-spec.js | 103 ++++++++++++++--------------- src/atom-environment.coffee | 15 +++-- src/history-manager.js | 56 ++++++++-------- src/reopen-project-menu-manager.js | 4 +- 4 files changed, 90 insertions(+), 88 deletions(-) diff --git a/spec/history-manager-spec.js b/spec/history-manager-spec.js index 425f1efe0..bc77cb9b8 100644 --- a/spec/history-manager-spec.js +++ b/spec/history-manager-spec.js @@ -4,27 +4,24 @@ import {it, fit, ffit, fffit, beforeEach, afterEach} from './async-spec-helpers' import {Emitter, Disposable, CompositeDisposable} from 'event-kit' import {HistoryManager, HistoryProject} from '../src/history-manager' +import StateStore from '../src/state-store' describe("HistoryManager", () => { - let historyManager, commandRegistry, project, localStorage, stateStore + let historyManager, commandRegistry, project, stateStore let commandDisposable, projectDisposable - beforeEach(() => { + beforeEach(async () => { commandDisposable = jasmine.createSpyObj('Disposable', ['dispose']) commandRegistry = jasmine.createSpyObj('CommandRegistry', ['add']) commandRegistry.add.andReturn(commandDisposable) - localStorage = jasmine.createSpyObj('LocalStorage', ['getItem', 'setItem']) - localStorage.items = { - history: JSON.stringify({ - projects: [ - { paths: ['/1', 'c:\\2'], lastOpened: new Date(2016, 9, 17, 17, 16, 23) }, - { paths: ['/test'], lastOpened: new Date(2016, 9, 17, 11, 12, 13) } - ] - }) - } - localStorage.getItem.andCallFake((key) => localStorage.items[key]) - localStorage.setItem.andCallFake((key, value) => localStorage.items[key] = value) + stateStore = new StateStore('history-manager-test', 1) + await stateStore.save('history-manager', { + projects: [ + {paths: ['/1', 'c:\\2'], lastOpened: new Date(2016, 9, 17, 17, 16, 23)}, + {paths: ['/test'], lastOpened: new Date(2016, 9, 17, 11, 12, 13)} + ] + }) projectDisposable = jasmine.createSpyObj('Disposable', ['dispose']) project = jasmine.createSpyObj('Project', ['onDidChangePaths']) @@ -33,7 +30,12 @@ describe("HistoryManager", () => { return projectDisposable }) - historyManager = new HistoryManager({project, commands:commandRegistry, localStorage}) + historyManager = new HistoryManager({stateStore, project, commands: commandRegistry}) + await historyManager.loadState() + }) + + afterEach(async () => { + await stateStore.clear() }) describe("constructor", () => { @@ -65,33 +67,28 @@ describe("HistoryManager", () => { }) describe("clearProjects", () => { - it("clears the list of projects", () => { + it("clears the list of projects", async () => { expect(historyManager.getProjects().length).not.toBe(0) - historyManager.clearProjects() + await historyManager.clearProjects() expect(historyManager.getProjects().length).toBe(0) }) - it("saves the state", () => { - expect(localStorage.setItem).not.toHaveBeenCalled() - historyManager.clearProjects() - expect(localStorage.setItem).toHaveBeenCalled() - expect(localStorage.setItem.calls[0].args[0]).toBe('history') + it("saves the state", async () => { + await historyManager.clearProjects() + const historyManager2 = new HistoryManager({stateStore, project, commands: commandRegistry}) + await historyManager2.loadState() expect(historyManager.getProjects().length).toBe(0) }) - it("fires the onDidChangeProjects event", () => { - expect(localStorage.setItem).not.toHaveBeenCalled() - historyManager.clearProjects() - expect(localStorage.setItem).toHaveBeenCalled() - expect(localStorage.setItem.calls[0].args[0]).toBe('history') + it("fires the onDidChangeProjects event", async () => { + const didChangeSpy = jasmine.createSpy() + historyManager.onDidChangeProjects(didChangeSpy) + await historyManager.clearProjects() expect(historyManager.getProjects().length).toBe(0) + expect(didChangeSpy).toHaveBeenCalled() }) }) - it("loads state", () => { - expect(localStorage.getItem).toHaveBeenCalledWith('history') - }) - it("listens to project.onDidChangePaths adding a new project", () => { const start = new Date() project.didChangePathsListener(['/a/new', '/path/or/two']) @@ -112,61 +109,61 @@ describe("HistoryManager", () => { }) describe("loadState", () => { - it("defaults to an empty array if no state", () => { - localStorage.items.history = null - historyManager.loadState() + it("defaults to an empty array if no state", async () => { + await stateStore.clear() + await historyManager.loadState() expect(historyManager.getProjects()).toEqual([]) }) - it("defaults to an empty array if no projects", () => { - localStorage.items.history = JSON.stringify('') - historyManager.loadState() + it("defaults to an empty array if no projects", async () => { + await stateStore.save('history-manager', {}) + await historyManager.loadState() expect(historyManager.getProjects()).toEqual([]) }) }) describe("addProject", () => { - it("adds a new project to the end", () => { + it("adds a new project to the end", async () => { const date = new Date(2010, 10, 9, 8, 7, 6) - historyManager.addProject(['/a/b'], date) + await historyManager.addProject(['/a/b'], date) const projects = historyManager.getProjects() expect(projects.length).toBe(3) expect(projects[2].paths).toEqual(['/a/b']) expect(projects[2].lastOpened).toBe(date) }) - it("adds a new project to the start", () => { + it("adds a new project to the start", async () => { const date = new Date() - historyManager.addProject(['/so/new'], date) + await historyManager.addProject(['/so/new'], date) const projects = historyManager.getProjects() expect(projects.length).toBe(3) expect(projects[0].paths).toEqual(['/so/new']) expect(projects[0].lastOpened).toBe(date) }) - it("updates an existing project and moves it to the start", () => { + it("updates an existing project and moves it to the start", async () => { const date = new Date() - historyManager.addProject(['/test'], date) + await historyManager.addProject(['/test'], date) const projects = historyManager.getProjects() expect(projects.length).toBe(2) expect(projects[0].paths).toEqual(['/test']) expect(projects[0].lastOpened).toBe(date) }) - it("fires the onDidChangeProjects event when adding a project", () => { + it("fires the onDidChangeProjects event when adding a project", async () => { const didChangeSpy = jasmine.createSpy() const beforeCount = historyManager.getProjects().length historyManager.onDidChangeProjects(didChangeSpy) - historyManager.addProject(['/test-new'], new Date()) + await historyManager.addProject(['/test-new'], new Date()) expect(didChangeSpy).toHaveBeenCalled() expect(historyManager.getProjects().length).toBe(beforeCount + 1) }) - it("fires the onDidChangeProjects event when updating a project", () => { + it("fires the onDidChangeProjects event when updating a project", async () => { const didChangeSpy = jasmine.createSpy() const beforeCount = historyManager.getProjects().length historyManager.onDidChangeProjects(didChangeSpy) - historyManager.addProject(['/test'], new Date()) + await historyManager.addProject(['/test'], new Date()) expect(didChangeSpy).toHaveBeenCalled() expect(historyManager.getProjects().length).toBe(beforeCount) }) @@ -186,14 +183,12 @@ describe("HistoryManager", () => { }) describe("saveState" ,() => { - it("saves the state", () => { - historyManager.addProject(["/save/state"]) - historyManager.saveState() - expect(localStorage.setItem).toHaveBeenCalled() - expect(localStorage.setItem.calls[0].args[0]).toBe('history') - expect(localStorage.items['history']).toContain('/save/state') - historyManager.loadState() - expect(historyManager.getProjects()[0].paths).toEqual(['/save/state']) + it("saves the state", async () => { + await historyManager.addProject(["/save/state"]) + await historyManager.saveState() + const historyManager2 = new HistoryManager({stateStore, project, commands: commandRegistry}) + await historyManager2.loadState() + expect(historyManager2.getProjects()[0].paths).toEqual(['/save/state']) }) }) }) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index d834a355f..cf5df00d2 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -229,14 +229,12 @@ class AtomEnvironment extends Model @observeAutoHideMenuBar() - @history = new HistoryManager({@project, @commands, localStorage}) + @history = new HistoryManager({@project, @commands, @stateStore}) # Keep instances of HistoryManager in sync - @history.onDidChangeProjects (e) => + @disposables.add @history.onDidChangeProjects (e) => @applicationDelegate.didChangeHistoryManager() unless e.reloaded @disposables.add @applicationDelegate.onDidChangeHistoryManager(=> @history.loadState()) - (new ReopenProjectMenuManager({@menu, @commands, @history, @config, open: (paths) => @open(pathsToOpen: paths)})).update() - attachSaveStateListeners: -> saveState = _.debounce((=> window.requestIdleCallback => @saveState({isUnloading: false}) unless @unloaded @@ -714,7 +712,14 @@ class AtomEnvironment extends Model @openInitialEmptyEditorIfNecessary() - Promise.all([loadStatePromise, updateProcessEnvPromise]) + loadHistoryPromise = @history.loadState().then => + @reopenProjectMenuManager = new ReopenProjectMenuManager({ + @menu, @commands, @history, @config, + open: (paths) => @open(pathsToOpen: paths) + }) + @reopenProjectMenuManager.update() + + Promise.all([loadStatePromise, loadHistoryPromise, updateProcessEnvPromise]) serialize: (options) -> version: @constructor.version diff --git a/src/history-manager.js b/src/history-manager.js index f013957b9..5087c3bf9 100644 --- a/src/history-manager.js +++ b/src/history-manager.js @@ -1,6 +1,6 @@ /** @babel */ -import {Emitter} from 'event-kit' +import {Emitter, CompositeDisposable} from 'event-kit' // Extended: History manager for remembering which projects have been opened. // @@ -8,12 +8,17 @@ import {Emitter} from 'event-kit' // // The project history is used to enable the 'Reopen Project' menu. export class HistoryManager { - constructor ({project, commands, localStorage}) { - this.localStorage = localStorage - commands.add('atom-workspace', {'application:clear-project-history': this.clearProjects.bind(this)}) + constructor ({stateStore, project, commands}) { + this.stateStore = stateStore this.emitter = new Emitter() - this.loadState() - project.onDidChangePaths((projectPaths) => this.addProject(projectPaths)) + this.projects = [] + this.disposables = new CompositeDisposable() + this.disposables.add(commands.add('atom-workspace', {'application:clear-project-history': this.clearProjects.bind(this)})) + this.disposables.add(project.onDidChangePaths((projectPaths) => this.addProject(projectPaths))) + } + + destroy () { + this.disposables.dispose() } // Public: Obtain a list of previously opened projects. @@ -27,9 +32,12 @@ export class HistoryManager { // // Note: This is not a privacy function - other traces will still exist, // e.g. window state. - clearProjects () { + // + // Return a {Promise} that resolves when the history has been successfully + // cleared. + async clearProjects () { this.projects = [] - this.saveState() + await this.saveState() this.didChangeProjects() } @@ -46,7 +54,7 @@ export class HistoryManager { this.emitter.emit('did-change-projects', args || { reloaded: false }) } - addProject (paths, lastOpened) { + async addProject (paths, lastOpened) { if (paths.length === 0) return let project = this.getProject(paths) @@ -57,11 +65,11 @@ export class HistoryManager { project.lastOpened = lastOpened || new Date() this.projects.sort((a, b) => b.lastOpened - a.lastOpened) - this.saveState() + await this.saveState() this.didChangeProjects() } - removeProject (paths) { + async removeProject (paths) { if (paths.length === 0) return let project = this.getProject(paths) @@ -70,7 +78,7 @@ export class HistoryManager { let index = this.projects.indexOf(project) this.projects.splice(index, 1) - this.saveState() + await this.saveState() this.didChangeProjects() } @@ -84,31 +92,25 @@ export class HistoryManager { return null } - loadState () { - const state = JSON.parse(this.localStorage.getItem('history')) - if (state && state.projects) { - this.projects = state.projects.filter(p => Array.isArray(p.paths) && p.paths.length > 0).map(p => new HistoryProject(p.paths, new Date(p.lastOpened))) - this.didChangeProjects({ reloaded: true }) + async loadState () { + const history = await this.stateStore.load('history-manager') + if (history && history.projects) { + this.projects = history.projects.filter(p => Array.isArray(p.paths) && p.paths.length > 0).map(p => new HistoryProject(p.paths, new Date(p.lastOpened))) + this.didChangeProjects({reloaded: true}) } else { this.projects = [] } } - saveState () { - const state = JSON.stringify({ - projects: this.projects.map(p => ({ - paths: p.paths, lastOpened: p.lastOpened - })) - }) - this.localStorage.setItem('history', state) + async saveState () { + const projects = this.projects.map(p => ({paths: p.paths, lastOpened: p.lastOpened})) + await this.stateStore.save('history-manager', {projects}) } async importProjectHistory () { for (let project of await HistoryImporter.getAllProjects()) { - this.addProject(project.paths, project.lastOpened) + await this.addProject(project.paths, project.lastOpened) } - this.saveState() - this.didChangeProjects() } } diff --git a/src/reopen-project-menu-manager.js b/src/reopen-project-menu-manager.js index 79acbba66..3f88e41f0 100644 --- a/src/reopen-project-menu-manager.js +++ b/src/reopen-project-menu-manager.js @@ -58,7 +58,7 @@ export default class ReopenProjectMenuManager { // Windows users can right-click Atom taskbar and remove project from the jump list. // We have to honor that or the group stops working. As we only get a partial list // each time we remove them from history entirely. - applyWindowsJumpListRemovals () { + async applyWindowsJumpListRemovals () { if (process.platform !== 'win32') return if (this.app === undefined) { this.app = require('remote').app @@ -68,7 +68,7 @@ export default class ReopenProjectMenuManager { if (removed.length === 0) return for (let project of this.historyManager.getProjects()) { if (removed.includes(ReopenProjectMenuManager.taskDescription(project.paths))) { - this.historyManager.removeProject(project.paths) + await this.historyManager.removeProject(project.paths) } } } From 7e761d39afc75f171887ced38947ea8238466fe0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 10 Mar 2017 14:37:37 +0100 Subject: [PATCH 5/7] Fall back to local storage when no history can be found --- spec/history-manager-spec.js | 6 +++--- src/atom-environment.coffee | 2 +- src/history-manager.js | 9 +++++++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/spec/history-manager-spec.js b/spec/history-manager-spec.js index bc77cb9b8..ed6aac626 100644 --- a/spec/history-manager-spec.js +++ b/spec/history-manager-spec.js @@ -30,7 +30,7 @@ describe("HistoryManager", () => { return projectDisposable }) - historyManager = new HistoryManager({stateStore, project, commands: commandRegistry}) + historyManager = new HistoryManager({stateStore, localStorage: window.localStorage, project, commands: commandRegistry}) await historyManager.loadState() }) @@ -75,7 +75,7 @@ describe("HistoryManager", () => { it("saves the state", async () => { await historyManager.clearProjects() - const historyManager2 = new HistoryManager({stateStore, project, commands: commandRegistry}) + const historyManager2 = new HistoryManager({stateStore, localStorage: window.localStorage, project, commands: commandRegistry}) await historyManager2.loadState() expect(historyManager.getProjects().length).toBe(0) }) @@ -186,7 +186,7 @@ describe("HistoryManager", () => { it("saves the state", async () => { await historyManager.addProject(["/save/state"]) await historyManager.saveState() - const historyManager2 = new HistoryManager({stateStore, project, commands: commandRegistry}) + const historyManager2 = new HistoryManager({stateStore, localStorage: window.localStorage, project, commands: commandRegistry}) await historyManager2.loadState() expect(historyManager2.getProjects()[0].paths).toEqual(['/save/state']) }) diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index cf5df00d2..6d9b3333f 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -229,7 +229,7 @@ class AtomEnvironment extends Model @observeAutoHideMenuBar() - @history = new HistoryManager({@project, @commands, @stateStore}) + @history = new HistoryManager({@project, @commands, @stateStore, localStorage: window.localStorage}) # Keep instances of HistoryManager in sync @disposables.add @history.onDidChangeProjects (e) => @applicationDelegate.didChangeHistoryManager() unless e.reloaded diff --git a/src/history-manager.js b/src/history-manager.js index 5087c3bf9..084bdc386 100644 --- a/src/history-manager.js +++ b/src/history-manager.js @@ -8,8 +8,9 @@ import {Emitter, CompositeDisposable} from 'event-kit' // // The project history is used to enable the 'Reopen Project' menu. export class HistoryManager { - constructor ({stateStore, project, commands}) { + constructor ({stateStore, localStorage, project, commands}) { this.stateStore = stateStore + this.localStorage = localStorage this.emitter = new Emitter() this.projects = [] this.disposables = new CompositeDisposable() @@ -93,7 +94,11 @@ export class HistoryManager { } async loadState () { - const history = await this.stateStore.load('history-manager') + let history = await this.stateStore.load('history-manager') + if (!history) { + history = JSON.parse(this.localStorage.getItem('history')) + } + if (history && history.projects) { this.projects = history.projects.filter(p => Array.isArray(p.paths) && p.paths.length > 0).map(p => new HistoryProject(p.paths, new Date(p.lastOpened))) this.didChangeProjects({reloaded: true}) From 337ad58434a6a3d20ffd2d261e49e3c0abce7409 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 10 Mar 2017 14:37:49 +0100 Subject: [PATCH 6/7] :fire: HistoryManager.prototype.importProjectHistory --- src/history-manager.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/history-manager.js b/src/history-manager.js index 084bdc386..82ccd781b 100644 --- a/src/history-manager.js +++ b/src/history-manager.js @@ -111,12 +111,6 @@ export class HistoryManager { const projects = this.projects.map(p => ({paths: p.paths, lastOpened: p.lastOpened})) await this.stateStore.save('history-manager', {projects}) } - - async importProjectHistory () { - for (let project of await HistoryImporter.getAllProjects()) { - await this.addProject(project.paths, project.lastOpened) - } - } } function arrayEquivalent (a, b) { From 79e6a6ff3ca7293d2c7605c68c39773264960ed7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 10 Mar 2017 14:45:34 +0100 Subject: [PATCH 7/7] :fire: HistoryImporter --- src/history-manager.js | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/src/history-manager.js b/src/history-manager.js index 82ccd781b..e3dd46653 100644 --- a/src/history-manager.js +++ b/src/history-manager.js @@ -133,32 +133,3 @@ export class HistoryProject { set lastOpened (lastOpened) { this._lastOpened = lastOpened } get lastOpened () { return this._lastOpened } } - -class HistoryImporter { - static async getStateStoreCursor () { - const db = await atom.stateStore.dbPromise - const store = db.transaction(['states']).objectStore('states') - return store.openCursor() - } - - static async getAllProjects (stateStore) { - const request = await HistoryImporter.getStateStoreCursor() - return new Promise((resolve, reject) => { - const rows = [] - request.onerror = reject - request.onsuccess = event => { - const cursor = event.target.result - if (cursor) { - let project = cursor.value.value.project - let storedAt = cursor.value.storedAt - if (project && project.paths && storedAt) { - rows.push(new HistoryProject(project.paths, new Date(Date.parse(storedAt)))) - } - cursor.continue() - } else { - resolve(rows) - } - } - }) - } -}