Merge pull request #13046 from atom/dg-reopen-project

Project history api, reopen project menu and command
This commit is contained in:
Damien Guard
2016-10-26 10:21:43 -07:00
committed by GitHub
13 changed files with 822 additions and 2 deletions

View File

@@ -36,6 +36,13 @@
{ label: 'New File', command: 'application:new-file' }
{ label: 'Open…', command: 'application:open' }
{ label: 'Add Project Folder…', command: 'application:add-project-folder' }
{
label: 'Reopen Project',
submenu: [
{ label: 'Clear Project History', command: 'application:clear-project-history' }
{ type: 'separator' }
]
}
{ label: 'Reopen Last Item', command: 'pane:reopen-closed-item' }
{ type: 'separator' }
{ label: 'Save', command: 'core:save' }

View File

@@ -7,6 +7,13 @@
{ label: '&Open File…', command: 'application:open-file' }
{ label: 'Open Folder…', command: 'application:open-folder' }
{ label: 'Add Project Folder…', command: 'application:add-project-folder' }
{
label: 'Reopen Project',
submenu: [
{ label: 'Clear Project History', command: 'application:clear-project-history' }
{ type: 'separator' }
]
}
{ label: 'Reopen Last &Item', command: 'pane:reopen-closed-item' }
{ type: 'separator' }
{ label: '&Save', command: 'core:save' }

View File

@@ -7,6 +7,13 @@
{ label: '&Open File…', command: 'application:open-file' }
{ label: 'Open Folder…', command: 'application:open-folder' }
{ label: 'Add Project Folder…', command: 'application:add-project-folder' }
{
label: 'Reopen Project',
submenu: [
{ label: 'Clear Project History', command: 'application:clear-project-history' }
{ type: 'separator' }
]
}
{ label: 'Reopen Last &Item', command: 'pane:reopen-closed-item' }
{ type: 'separator' }
{ label: 'Se&ttings', command: 'application:show-settings' }

View File

@@ -0,0 +1,199 @@
/** @babel */
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'
describe("HistoryManager", () => {
let historyManager, commandRegistry, project, localStorage, stateStore
let commandDisposable, projectDisposable
beforeEach(() => {
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)
projectDisposable = jasmine.createSpyObj('Disposable', ['dispose'])
project = jasmine.createSpyObj('Project', ['onDidChangePaths'])
project.onDidChangePaths.andCallFake((f) => {
project.didChangePathsListener = f
return projectDisposable
})
historyManager = new HistoryManager({project, commands:commandRegistry, localStorage})
})
describe("constructor", () => {
it("registers the 'clear-project-history' command function", () => {
expect(commandRegistry.add).toHaveBeenCalled()
const cmdCall = commandRegistry.add.calls[0]
expect(cmdCall.args.length).toBe(2)
expect(cmdCall.args[0]).toBe('atom-workspace')
expect(typeof cmdCall.args[1]['application:clear-project-history']).toBe('function')
})
describe("getProjects", () => {
it("returns an array of HistoryProjects", () => {
expect(historyManager.getProjects()).toEqual([
new HistoryProject(['/1', 'c:\\2'], new Date(2016, 9, 17, 17, 16, 23)),
new HistoryProject(['/test'], new Date(2016, 9, 17, 11, 12, 13))
])
})
it("returns an array of HistoryProjects that is not mutable state", () => {
const firstProjects = historyManager.getProjects()
firstProjects.pop()
firstProjects[0].path = 'modified'
const secondProjects = historyManager.getProjects()
expect(secondProjects.length).toBe(2)
expect(secondProjects[0].path).not.toBe('modified')
})
})
describe("clearProjects", () => {
it("clears the list of projects", () => {
expect(historyManager.getProjects().length).not.toBe(0)
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')
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')
expect(historyManager.getProjects().length).toBe(0)
})
})
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'])
const projects = historyManager.getProjects()
expect(projects.length).toBe(3)
expect(projects[0].paths).toEqual(['/a/new', '/path/or/two'])
expect(projects[0].lastOpened).not.toBeLessThan(start)
})
it("listens to project.onDidChangePaths updating an existing project", () => {
const start = new Date()
project.didChangePathsListener(['/test'])
const projects = historyManager.getProjects()
expect(projects.length).toBe(2)
expect(projects[0].paths).toEqual(['/test'])
expect(projects[0].lastOpened).not.toBeLessThan(start)
})
})
describe("loadState", () => {
it("defaults to an empty array if no state", () => {
localStorage.items.history = null
historyManager.loadState()
expect(historyManager.getProjects()).toEqual([])
})
it("defaults to an empty array if no projects", () => {
localStorage.items.history = JSON.stringify('')
historyManager.loadState()
expect(historyManager.getProjects()).toEqual([])
})
})
describe("addProject", () => {
it("adds a new project to the end", () => {
const date = new Date(2010, 10, 9, 8, 7, 6)
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", () => {
const date = new Date()
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", () => {
const date = new Date()
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", () => {
const didChangeSpy = jasmine.createSpy()
const beforeCount = historyManager.getProjects().length
historyManager.onDidChangeProjects(didChangeSpy)
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", () => {
const didChangeSpy = jasmine.createSpy()
const beforeCount = historyManager.getProjects().length
historyManager.onDidChangeProjects(didChangeSpy)
historyManager.addProject(['/test'], new Date())
expect(didChangeSpy).toHaveBeenCalled()
expect(historyManager.getProjects().length).toBe(beforeCount)
})
})
describe("getProject", () => {
it("returns a project that matches the paths", () => {
const project = historyManager.getProject(['/1', 'c:\\2'])
expect(project).not.toBeNull()
expect(project.paths).toEqual(['/1', 'c:\\2'])
})
it("returns null when it can't find the project", () => {
const project = historyManager.getProject(['/1'])
expect(project).toBeNull()
})
})
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'])
})
})
})

View File

@@ -0,0 +1,267 @@
/** @babel */
import {it, fit, ffit, fffit, beforeEach, afterEach} from './async-spec-helpers'
import {Emitter, Disposable, CompositeDisposable} from 'event-kit'
const ReopenProjectMenuManager = require('../src/reopen-project-menu-manager')
numberRange = (low, high) => {
const size = high - low
const result = new Array(size)
for (var i = 0; i < size; i++)
result[i] = low + i
return result
}
describe("ReopenProjectMenuManager", () => {
let menuManager, commandRegistry, config, historyManager, reopenProjects
let commandDisposable, configDisposable, historyDisposable
beforeEach(() => {
menuManager = jasmine.createSpyObj('MenuManager', ['add'])
menuManager.add.andReturn(new Disposable())
commandRegistry = jasmine.createSpyObj('CommandRegistry', ['add'])
commandDisposable = jasmine.createSpyObj('Disposable', ['dispose'])
commandRegistry.add.andReturn(commandDisposable)
config = jasmine.createSpyObj('Config', ['onDidChange', 'get'])
config.get.andReturn(10)
configDisposable = jasmine.createSpyObj('Disposable', ['dispose'])
config.didChangeListener = { }
config.onDidChange.andCallFake((key, fn) => {
config.didChangeListener[key] = fn
return configDisposable
})
historyManager = jasmine.createSpyObj('historyManager', ['getProjects','onDidChangeProjects'])
historyManager.getProjects.andReturn([])
historyDisposable = jasmine.createSpyObj('Disposable', ['dispose'])
historyManager.onDidChangeProjects.andCallFake((fn) => {
historyManager.changeProjectsListener = fn
return historyDisposable
})
openFunction = jasmine.createSpy()
reopenProjects = new ReopenProjectMenuManager({menu:menuManager, commands: commandRegistry, history: historyManager, config, open:openFunction})
})
describe("constructor", () => {
it("registers the 'reopen-project' command function", () => {
expect(commandRegistry.add).toHaveBeenCalled()
const cmdCall = commandRegistry.add.calls[0]
expect(cmdCall.args.length).toBe(2)
expect(cmdCall.args[0]).toBe('atom-workspace')
expect(typeof cmdCall.args[1]['application:reopen-project']).toBe('function')
})
})
describe("dispose", () => {
it("disposes of the history, command and config disposables", () => {
reopenProjects.dispose()
expect(historyDisposable.dispose).toHaveBeenCalled()
expect(configDisposable.dispose).toHaveBeenCalled()
expect(commandDisposable.dispose).toHaveBeenCalled()
})
it("disposes of the menu disposable once used", () => {
const menuDisposable = jasmine.createSpyObj('Disposable', ['dispose'])
menuManager.add.andReturn(menuDisposable)
reopenProjects.update()
expect(menuDisposable.dispose).not.toHaveBeenCalled()
reopenProjects.dispose()
expect(menuDisposable.dispose).toHaveBeenCalled()
})
})
describe("the command", () => {
it("calls open with the paths of the project specified by the detail index", () => {
historyManager.getProjects.andReturn([ { paths: ['/a'] }, { paths: ['/b', 'c:\\'] }])
reopenProjects.update()
reopenProjectCommand = commandRegistry.add.calls[0].args[1]['application:reopen-project']
reopenProjectCommand({ detail: { index: 1 } })
expect(openFunction).toHaveBeenCalled()
expect(openFunction.calls[0].args[0]).toEqual(['/b', 'c:\\'])
})
it("does not call open when no command detail is supplied", () => {
reopenProjectCommand = commandRegistry.add.calls[0].args[1]['application:reopen-project']
reopenProjectCommand({})
expect(openFunction).not.toHaveBeenCalled()
})
it("does not call open when no command detail index is supplied", () => {
reopenProjectCommand = commandRegistry.add.calls[0].args[1]['application:reopen-project']
reopenProjectCommand({ detail: { anything: 'here' } })
expect(openFunction).not.toHaveBeenCalled()
})
})
describe("update", () => {
it("adds menu items to MenuManager based on projects from HistoryManager", () => {
historyManager.getProjects.andReturn([ { paths: ['/a'] }, { paths: ['/b', 'c:\\'] }])
reopenProjects.update()
expect(historyManager.getProjects).toHaveBeenCalled()
expect(menuManager.add).toHaveBeenCalled()
const menuArg = menuManager.add.calls[0].args[0]
expect(menuArg.length).toBe(1)
expect(menuArg[0].label).toBe('File')
expect(menuArg[0].submenu.length).toBe(1)
const projectsMenu = menuArg[0].submenu[0]
expect(projectsMenu.label).toBe('Reopen Project')
expect(projectsMenu.submenu.length).toBe(2)
const first = projectsMenu.submenu[0]
expect(first.label).toBe('/a')
expect(first.command).toBe('application:reopen-project')
expect(first.commandDetail).toEqual({ index: 0 })
const second = projectsMenu.submenu[1]
expect(second.label).toBe('b, c:\\')
expect(second.command).toBe('application:reopen-project')
expect(second.commandDetail).toEqual({ index: 1 })
})
it("adds only the number of menu items specified in the 'core.reopenProjectMenuCount' config", () => {
historyManager.getProjects.andReturn(numberRange(1, 100).map(i => ({ paths: [ '/test/' + i ] })))
reopenProjects.update()
expect(menuManager.add).toHaveBeenCalled()
const menu = menuManager.add.calls[0].args[0][0]
expect(menu.label).toBe('File')
expect(menu.submenu.length).toBe(1)
expect(menu.submenu[0].label).toBe('Reopen Project')
expect(menu.submenu[0].submenu.length).toBe(10)
})
it("disposes the previously menu built", () => {
const menuDisposable = jasmine.createSpyObj('Disposable', ['dispose'])
menuManager.add.andReturn(menuDisposable)
reopenProjects.update()
expect(menuDisposable.dispose).not.toHaveBeenCalled()
reopenProjects.update()
expect(menuDisposable.dispose).toHaveBeenCalled()
})
it("is called when the Config changes for 'core.reopenProjectMenuCount'", () => {
historyManager.getProjects.andReturn(numberRange(1, 100).map(i => ({ paths: [ '/test/' + i ] })))
reopenProjects.update()
config.get.andReturn(25)
config.didChangeListener['core.reopenProjectMenuCount']({oldValue:10, newValue: 25})
const finalArgs = menuManager.add.calls[1].args[0]
const projectsMenu = finalArgs[0].submenu[0].submenu
expect(projectsMenu.length).toBe(25)
})
it("is called when the HistoryManager's projects change", () => {
reopenProjects.update()
historyManager.getProjects.andReturn([ { paths: ['/a'] }, { paths: ['/b', 'c:\\'] } ])
historyManager.changeProjectsListener()
expect(menuManager.add.calls.length).toBe(2)
const finalArgs = menuManager.add.calls[1].args[0]
const projectsMenu = finalArgs[0].submenu[0]
const first = projectsMenu.submenu[0]
expect(first.label).toBe('/a')
expect(first.command).toBe('application:reopen-project')
expect(first.commandDetail).toEqual({ index: 0 })
const second = projectsMenu.submenu[1]
expect(second.label).toBe('b, c:\\')
expect(second.command).toBe('application:reopen-project')
expect(second.commandDetail).toEqual({ index: 1 })
})
})
describe("updateProjects", () => {
it("creates correct menu items commands for recent projects", () => {
const projects = [
{ paths: [ '/users/neila' ] },
{ paths: [ '/users/buzza', 'users/michaelc' ] }
]
const menu = ReopenProjectMenuManager.createProjectsMenu(projects)
expect(menu.label).toBe('File')
expect(menu.submenu.length).toBe(1)
const recentMenu = menu.submenu[0]
expect(recentMenu.label).toBe('Reopen Project')
expect(recentMenu.submenu.length).toBe(2)
const first = recentMenu.submenu[0]
expect(first.label).toBe('/users/neila')
expect(first.command).toBe('application:reopen-project')
expect(first.commandDetail).toEqual({index: 0})
const second = recentMenu.submenu[1]
expect(second.label).toBe('buzza, michaelc')
expect(second.command).toBe('application:reopen-project')
expect(second.commandDetail).toEqual({index: 1})
})
})
describe("createLabel", () => {
it("returns the Unix path unchanged if there is only one", () => {
const label = ReopenProjectMenuManager.createLabel({ paths: ['/a/b/c/d/e/f'] })
expect(label).toBe('/a/b/c/d/e/f')
})
it("returns the Windows path unchanged if there is only one", () => {
const label = ReopenProjectMenuManager.createLabel({ paths: ['c:\\missions\\apollo11'] })
expect(label).toBe('c:\\missions\\apollo11')
})
it("returns the URL unchanged if there is only one", () => {
const label = ReopenProjectMenuManager.createLabel({ paths: ['https://launch.pad/apollo/11'] })
expect(label).toBe('https://launch.pad/apollo/11')
})
it("returns a comma-seperated list of base names if there are multiple", () => {
const project = { paths: [ '/var/one', '/usr/bin/two', '/etc/mission/control/three' ] }
const label = ReopenProjectMenuManager.createLabel(project)
expect(label).toBe('one, two, three')
})
describe("betterBaseName", () => {
it("returns the standard base name for an absolute Unix path", () => {
const name = ReopenProjectMenuManager.betterBaseName('/one/to/three')
expect(name).toBe('three')
})
it("returns the standard base name for a relative Windows path", () => {
if (process.platform is 'win32') {
const name = ReopenProjectMenuManager.betterBaseName('.\\one\\two')
expect(name).toBe('two')
}
})
it("returns the standard base name for an absolute Windows path", () => {
if (process.platform is 'win32') {
const name = ReopenProjectMenuManager.betterBaseName('c:\\missions\\apollo\\11')
expect(name).toBe('11')
}
})
it("returns the drive root for a Windows drive name", () => {
const name = ReopenProjectMenuManager.betterBaseName('d:')
expect(name).toBe('d:\\')
})
it("returns the drive root for a Windows drive root", () => {
const name = ReopenProjectMenuManager.betterBaseName('e:\\')
expect(name).toBe('e:\\')
})
it("returns the final path for a URI", () => {
const name = ReopenProjectMenuManager.betterBaseName('https://something/else')
expect(name).toBe('else')
})
})
})
})

View File

@@ -244,6 +244,17 @@ class ApplicationDelegate
didCancelWindowUnload: ->
ipcRenderer.send('did-cancel-window-unload')
onDidChangeHistoryManager: (callback) ->
outerCallback = (event, message) ->
callback(event)
ipcRenderer.on('did-change-history-manager', outerCallback)
new Disposable ->
ipcRenderer.removeListener('did-change-history-manager', outerCallback)
didChangeHistoryManager: ->
ipcRenderer.send('did-change-history-manager')
openExternal: (url) ->
shell.openExternal(url)

View File

@@ -22,6 +22,8 @@ KeymapManager = require './keymap-extensions'
TooltipManager = require './tooltip-manager'
CommandRegistry = require './command-registry'
GrammarRegistry = require './grammar-registry'
{HistoryManager, HistoryProject} = require './history-manager'
ReopenProjectMenuManager = require './reopen-project-menu-manager'
StyleManager = require './style-manager'
PackageManager = require './package-manager'
ThemeManager = require './theme-manager'
@@ -94,6 +96,9 @@ class AtomEnvironment extends Model
# Public: A {GrammarRegistry} instance
grammars: null
# Public: A {HistoryManager} instance
history: null
# Public: A {PackageManager} instance
packages: null
@@ -226,6 +231,14 @@ class AtomEnvironment extends Model
@observeAutoHideMenuBar()
@history = new HistoryManager({@project, @commands, localStorage})
# Keep instances of HistoryManager in sync
@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)})
checkPortableHomeWritable = =>
responseChannel = "check-portable-home-writable-response"
ipcRenderer.on responseChannel, (event, response) ->

View File

@@ -126,6 +126,11 @@ const configSchema = {
type: 'boolean',
default: true
},
reopenProjectMenuCount: {
description: 'How many recent projects to show in the Reopen Project menu.',
type: 'integer',
default: 15
},
automaticallyUpdate: {
description: 'Automatically update Atom when a new release is available.',
type: 'boolean',

141
src/history-manager.js Normal file
View File

@@ -0,0 +1,141 @@
/** @babel */
import {Emitter} from 'event-kit'
// Extended: History manager for remembering which projects have been opened.
//
// An instance of this class is always available as the `atom.history` global.
//
// 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)})
this.emitter = new Emitter()
this.loadState()
project.onDidChangePaths((projectPaths) => this.addProject(projectPaths))
}
// Public: Obtain a list of previously opened projects.
//
// Returns an {Array} of {HistoryProject} objects, most recent first.
getProjects () {
return this.projects.map(p => new HistoryProject(p.paths, p.lastOpened))
}
// Public: Clear all projects from the history.
//
// Note: This is not a privacy function - other traces will still exist,
// e.g. window state.
clearProjects () {
this.projects = []
this.saveState()
this.didChangeProjects()
}
// Public: Invoke the given callback when the list of projects changes.
//
// * `callback` {Function}
//
// Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
onDidChangeProjects (callback) {
return this.emitter.on('did-change-projects', callback)
}
didChangeProjects (args) {
this.emitter.emit('did-change-projects', args || { reloaded: false })
}
addProject (paths, lastOpened) {
let project = this.getProject(paths)
if (!project) {
project = new HistoryProject(paths)
this.projects.push(project)
}
project.lastOpened = lastOpened || new Date()
this.projects.sort((a, b) => b.lastOpened - a.lastOpened)
this.saveState()
this.didChangeProjects()
}
getProject (paths) {
const pathsString = paths.toString()
for (var i = 0; i < this.projects.length; i++) {
if (this.projects[i].paths.toString() === pathsString) {
return this.projects[i]
}
}
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 })
} 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 importProjectHistory () {
for (let project of await HistoryImporter.getAllProjects()) {
this.addProject(project.paths, project.lastOpened)
}
this.saveState()
this.didChangeProjects()
}
}
export class HistoryProject {
constructor (paths, lastOpened) {
this.paths = paths
this.lastOpened = lastOpened || new Date()
}
set paths (paths) { this._paths = paths }
get paths () { return this._paths }
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)
}
}
})
}
}

View File

@@ -142,8 +142,8 @@ class ApplicationMenu
item.metadata ?= {}
if item.command
item.accelerator = @acceleratorForCommand(item.command, keystrokesByCommand)
item.click = -> global.atomApplication.sendCommand(item.command)
item.metadata.windowSpecific = true unless /^application:/.test(item.command)
item.click = -> global.atomApplication.sendCommand(item.command, item.commandDetail)
item.metadata.windowSpecific = true unless /^application:/.test(item.command, item.commandDetail)
@translateTemplate(item.submenu, keystrokesByCommand) if item.submenu
template

View File

@@ -279,6 +279,12 @@ class AtomApplication
@disposable.add ipcHelpers.on ipcMain, 'restart-application', =>
@restart()
@disposable.add ipcHelpers.on ipcMain, 'did-change-history-manager', (event) =>
for atomWindow in @windows
webContents = atomWindow.browserWindow.webContents
if webContents isnt event.sender
webContents.send('did-change-history-manager')
# A request from the associated render process to open a new render process.
@disposable.add ipcHelpers.on ipcMain, 'open', (event, options) =>
window = @atomWindowForEvent(event)

View File

@@ -0,0 +1,64 @@
/** @babel */
import { SelectListView } from 'atom-space-pen-views'
export default class ReopenProjectListView extends SelectListView {
initialize (callback) {
this.callback = callback
super.initialize()
this.addClass('reopen-project')
this.list.addClass('mark-active')
}
getFilterKey () {
return 'name'
}
destroy () {
this.cancel()
}
viewForItem (project) {
let element = document.createElement('li')
if (project.name === this.currentProjectName) {
element.classList.add('active')
}
element.textContent = project.name
return element
}
cancelled () {
if (this.panel != null) {
this.panel.destroy()
}
this.panel = null
this.currentProjectName = null
}
confirmed (project) {
this.cancel()
this.callback(project.value)
}
attach () {
this.storeFocusedElement()
if (this.panel == null) {
this.panel = atom.workspace.addModalPanel({item: this})
}
this.focusFilterEditor()
}
toggle () {
if (this.panel != null) {
this.cancel()
} else {
this.currentProjectName = atom.project != null ? this.makeName(atom.project.getPaths()) : null
this.setItems(atom.history.getProjects().map(p => ({ name: this.makeName(p.paths), value: p.paths })))
this.attach()
}
}
makeName (paths) {
return paths.join(', ')
}
}

View File

@@ -0,0 +1,93 @@
/** @babel */
import {CompositeDisposable} from 'event-kit'
import path from 'path'
export default class ReopenProjectMenuManager {
constructor ({menu, commands, history, config, open}) {
this.menuManager = menu
this.historyManager = history
this.config = config
this.open = open
this.projects = []
this.subscriptions = new CompositeDisposable()
this.subscriptions.add(
history.onDidChangeProjects(this.update.bind(this)),
config.onDidChange('core.reopenProjectMenuCount', ({oldValue, newValue}) => {
this.update()
}),
commands.add('atom-workspace', { 'application:reopen-project': this.reopenProjectCommand.bind(this) })
)
}
reopenProjectCommand (e) {
if (e.detail != null && e.detail.index != null) {
this.open(this.projects[e.detail.index].paths)
} else {
this.createReopenProjectListView()
}
}
createReopenProjectListView () {
if (this.reopenProjectListView == null) {
const ReopenProjectListView = require('./reopen-project-list-view')
this.reopenProjectListView = new ReopenProjectListView(paths => {
if (paths != null) {
this.open(paths)
}
})
}
this.reopenProjectListView.toggle()
}
update () {
this.disposeProjectMenu()
this.projects = this.historyManager.getProjects().slice(0, this.config.get('core.reopenProjectMenuCount'))
const newMenu = ReopenProjectMenuManager.createProjectsMenu(this.projects)
this.lastProjectMenu = this.menuManager.add([newMenu])
}
dispose () {
this.subscriptions.dispose()
this.disposeProjectMenu()
if (this.reopenProjectListView != null) {
this.reopenProjectListView.dispose()
}
}
disposeProjectMenu () {
if (this.lastProjectMenu) {
this.lastProjectMenu.dispose()
this.lastProjectMenu = null
}
}
static createProjectsMenu (projects) {
return {
label: 'File',
submenu: [
{
label: 'Reopen Project',
submenu: projects.map((project, index) => ({
label: this.createLabel(project),
command: 'application:reopen-project',
commandDetail: {index: index}
}))
}
]
}
}
static createLabel (project) {
return project.paths.length === 1
? project.paths[0]
: project.paths.map(this.betterBaseName).join(', ')
}
static betterBaseName (directory) {
// Handles Windows roots better than path.basename which returns '' for 'd:' and 'd:\'
const match = directory.match(/^([a-z]:)[\\]?$/i)
return match ? match[1] + '\\' : path.basename(directory)
}
}