WIP: Make atom global a telepath model

Specs seem to be green but hang on what I'm assuming to be a long GC
pause near the end. I need to investigate what's going on memory wise.
This commit is contained in:
Nathan Sobo 2013-12-11 20:37:52 -08:00
parent b9b8c61c11
commit 23957d7f66
12 changed files with 224 additions and 241 deletions

View File

@ -43,7 +43,7 @@
"season": "0.14.0",
"semver": "1.1.4",
"space-pen": "2.0.1",
"telepath": "0.70.0",
"telepath": "0.71.0",
"temp": "0.5.0",
"underscore-plus": "0.5.0"
},

View File

@ -442,7 +442,7 @@ describe "the `atom` global", ->
describe ".isReleasedVersion()", ->
it "returns false if the version is a SHA and true otherwise", ->
version = '0.1.0'
spyOn(atom, 'getVersion').andCallFake -> version
spyOn(atom.constructor, 'getVersion').andCallFake -> version
expect(atom.isReleasedVersion()).toBe true
version = '36b5518'
expect(atom.isReleasedVersion()).toBe false

View File

@ -4,7 +4,7 @@ require('crash-reporter').start(productName: 'Atom', companyName: 'GitHub')
try
require '../src/window'
Atom = require '../src/atom'
window.atom = new Atom()
window.atom = Atom.loadOrCreate('spec')
window.atom.show() unless atom.getLoadSettings().exitWhenDone
{runSpecSuite} = require './jasmine-helper'

View File

@ -1,6 +1,6 @@
require '../src/window'
atom.setUpEnvironment('spec')
atom.restoreDimensions()
atom.initialize()
atom.restoreWindowDimensions()
require '../vendor/jasmine-jquery'
path = require 'path'
@ -27,9 +27,8 @@ keyBindingsToRestore = atom.keymap.getKeyBindings()
$(window).on 'core:close', -> window.close()
$(window).on 'unload', ->
atom.windowMode = 'spec'
atom.getWindowState().set('dimensions', atom.getDimensions())
atom.saveWindowState()
atom.storeWindowDimensions()
atom.saveSync()
$('html,body').css('overflow', 'auto')
jasmine.getEnv().addEqualityTester(_.isEqual) # Use underscore's definition of equality for toEqual assertions
@ -50,15 +49,15 @@ if specDirectory
beforeEach ->
$.fx.off = true
projectPath = specProjectPath ? path.join(@specDirectory, 'fixtures')
atom.project = atom.getWindowState().set('project', new Project(path: projectPath))
atom.project = atom.project = new Project(path: projectPath)
atom.keymap.keyBindings = _.clone(keyBindingsToRestore)
window.resetTimeouts()
atom.packages.packageStates = {}
serializedWindowState = null
spyOn(atom, 'saveWindowState').andCallFake -> serializedWindowState = @getWindowState().serialize()
spyOn(atom, 'loadSerializedWindowState').andCallFake -> serializedWindowState
spyOn(atom, 'saveSync')
atom.syntax.clearGrammarOverrides()
atom.syntax.clearProperties()
@ -109,13 +108,17 @@ afterEach ->
atom.workspaceView?.remove?()
atom.workspaceView = null
atom.state.remove('workspaceView')
atom.project?.destroy?()
atom.project = null
console.log atom.state.has('packageStates')
$('#jasmine-content').empty() unless window.debugContent
delete atom.windowState
jasmine.unspy(atom, 'saveWindowState')
jasmine.unspy(atom, 'saveSync')
ensureNoPathSubscriptions()
atom.syntax.off()
waits(0) # yield to ui thread to make screen update more frequently
@ -134,7 +137,7 @@ jasmine.StringPrettyPrinter.prototype.emitObject = (obj) ->
emitObject.call(this, obj)
jasmine.unspy = (object, methodName) ->
throw new Error("Not a spy") unless object[methodName].originalValue?
throw new Error("Not a spy") unless object[methodName].hasOwnProperty('originalValue')
object[methodName] = object[methodName].originalValue
addCustomMatchers = (spec) ->

View File

@ -10,13 +10,10 @@ app = remote.require 'app'
_ = require 'underscore-plus'
telepath = require 'telepath'
{Document} = telepath
{Document, Model} = telepath
fs = require 'fs-plus'
{Subscriber} = require 'emissary'
{$} = require './space-pen-extensions'
DeserializerManager = require './deserializer-manager'
Package = require './package'
SiteShim = require './site-shim'
WindowEventHandler = require './window-event-handler'
@ -37,24 +34,103 @@ WindowEventHandler = require './window-event-handler'
# * `atom.syntax` - A {Syntax} instance
# * `atom.themes` - A {ThemeManager} instance
module.exports =
class Atom
Subscriber.includeInto(this)
class Atom extends Model
# Public: Load or create the Atom environment in the given mode
#
# - mode: Pass 'editor' or 'spec' depending on the kind of environment you
# want to build.
#
# Returns an Atom instance, fully initialized
@loadOrCreate: (mode) ->
telepath.devMode = not @isReleasedVersion()
# Private:
constructor: ->
@loadTime = null
@workspaceViewParentSelector = 'body'
if documentState = @loadDocumentState(mode)
environment = @deserialize(documentState)
environment ? @createAsRoot({mode})
# Private: Loads and returns the serialized state corresponding to this window
# if it exists; otherwise returns undefined.
@loadDocumentState: (mode) ->
statePath = @getStatePath(mode)
if fs.existsSync(statePath)
try
documentStateString = fs.readFileSync(statePath, 'utf8')
catch error
console.warn "Error reading window state: #{statePath}", error.stack, error
else
documentStateString = @getLoadSettings().windowState
try
JSON.parse(documentStateString) if documentStateString?
catch error
console.warn "Error parsing window state: #{statePath}", error.stack, error
# Private: Returns the path where the state for the current window will be
# located if it exists.
@getStatePath: (mode) ->
switch mode
when 'spec'
filename = 'spec'
when 'editor'
{initialPath} = @getLoadSettings()
if initialPath
sha1 = crypto.createHash('sha1').update(initialPath).digest('hex')
filename = "editor-#{sha1}"
if filename
path.join(@getStorageDirPath(), filename)
else
null
# Public: Get the directory path to Atom's configuration area.
#
# Returns the absolute path to ~/.atom
@getConfigDirPath: ->
@configDirPath ?= fs.absolute('~/.atom')
# Public: Get the path to Atom's storage directory.
#
# Returns the absolute path to ~/.atom/storage
@getStorageDirPath: ->
@storageDirPath ?= path.join(@getConfigDirPath(), 'storage')
# Private: Returns the load settings hash associated with the current window.
@getLoadSettings: ->
_.deepClone(@loadSettings ?= _.deepClone(@getCurrentWindow().loadSettings))
# Public:
@getCurrentWindow: ->
remote.getCurrentWindow()
# Public: Get the version of the Atom application.
@getVersion: ->
app.getVersion()
# Public: Determine whether the current version is an official release.
@isReleasedVersion: ->
not /\w{7}/.test(@getVersion()) # Check if the release is a 7-character SHA prefix
@properties
mode: null
project: null
workspaceViewParentSelector: 'body'
# Private: Called by telepath. I'd like this to be merged with initialize eventually.
created: ->
DeserializerManager = require './deserializer-manager'
@deserializers = new DeserializerManager()
# Private: Initialize all the properties in this object.
# Public: Sets up the basic services that should be available in all modes
# (both spec and application). Call after this instance has been assigned to
# the `atom` global.
initialize: ->
@unsubscribe()
@setBodyPlatformClass()
{devMode, resourcePath} = atom.getLoadSettings()
configDirPath = @getConfigDirPath()
telepath.devMode = not @isReleasedVersion()
@loadTime = null
Config = require './config'
Keymap = require './keymap'
@ -64,115 +140,30 @@ class Atom
ThemeManager = require './theme-manager'
ContextMenuManager = require './context-menu-manager'
MenuManager = require './menu-manager'
{devMode, resourcePath} = @getLoadSettings()
configDirPath = @getConfigDirPath()
@config = new Config({configDirPath, resourcePath})
@keymap = new Keymap({configDirPath, resourcePath})
@packages = new PackageManager({devMode, configDirPath, resourcePath})
@subscribe @packages, 'activated', => @watchThemes()
@themes = new ThemeManager({packageManager: @packages, configDirPath, resourcePath})
@contextMenu = new ContextMenuManager(devMode)
@menu = new MenuManager({resourcePath})
@pasteboard = new Pasteboard()
@syntax = @deserializers.deserialize(@getWindowState('syntax')) ? new Syntax()
@syntax = @deserializers.deserialize(@state.get('syntax')) ? new Syntax()
@site = new SiteShim(this)
# Private: This method is called in any window needing a general environment, including specs
setUpEnvironment: (@windowMode) ->
@initialize()
@subscribe @packages, 'activated', => @watchThemes()
# Private:
setBodyPlatformClass: ->
document.body.classList.add("platform-#{process.platform}")
# Public: Create a new telepath model. We won't need to define this method when
# the atom global is a telepath model itself because all model subclasses inherit
# a create method.
create: (model) ->
@site.createDocument(model)
# Public: Get the current window
getCurrentWindow: ->
remote.getCurrentWindow()
# Public: Get the dimensions of this window.
#
# Returns an object with x, y, width, and height keys.
getDimensions: ->
browserWindow = @getCurrentWindow()
[x, y] = browserWindow.getPosition()
[width, height] = browserWindow.getSize()
{x, y, width, height}
# Public: Set the dimensions of the window.
#
# The window will be centered if either the x or y coordinate is not set
# in the dimensions parameter.
#
# * dimensions:
# + x:
# The new x coordinate.
# + y:
# The new y coordinate.
# + width:
# The new width.
# + height:
# The new height.
setDimensions: ({x, y, width, height}) ->
browserWindow = @getCurrentWindow()
browserWindow.setSize(width, height)
if x? and y?
browserWindow.setPosition(x, y)
else
browserWindow.center()
# Private:
restoreDimensions: ->
dimensions = @getWindowState().getObject('dimensions')
unless dimensions?.width and dimensions?.height
{height, width} = @getLoadSettings().initialSize ? {}
height ?= screen.availHeight
width ?= Math.min(screen.availWidth, 1024)
dimensions = {width, height}
@setDimensions(dimensions)
# Public: Get the load settings for the current window.
#
# Returns an object containing all the load setting key/value pairs.
getLoadSettings: ->
@loadSettings ?= _.deepClone(@getCurrentWindow().loadSettings)
_.deepClone(@loadSettings)
# Private:
deserializeProject: ->
Project = require './project'
@project = @getWindowState('project')
unless @project instanceof Project
@project = new Project(path: @getLoadSettings().initialPath)
@setWindowState('project', @project)
TextBuffer = require './text-buffer'
TokenizedBuffer = require './tokenized-buffer'
DisplayBuffer = require './display-buffer'
Editor = require './editor'
@registerModelClasses(Project, TextBuffer, TokenizedBuffer, DisplayBuffer, Editor)
# Private:
deserializeWorkspaceView: ->
WorkspaceView = require './workspace-view'
state = @getWindowState()
@workspaceView = @deserializers.deserialize(state.get('workspaceView'))
unless @workspaceView?
@workspaceView = new WorkspaceView()
state.set('workspaceView', @workspaceView.getState())
$(@workspaceViewParentSelector).append(@workspaceView)
# Private:
deserializePackageStates: ->
state = @getWindowState()
@packages.packageStates = state.getObject('packageStates') ? {}
state.remove('packageStates')
# Private:
deserializeEditorWindow: ->
@deserializePackageStates()
@deserializeProject()
@deserializeWorkspaceView()
# Private: This method is only called when opening a real application window
# Private: Call this method when establishing a real application window.
startEditorWindow: ->
if process.platform is 'darwin'
CommandInstaller = require './command-installer'
@ -180,7 +171,7 @@ class Atom
CommandInstaller.installApmCommand()
@windowEventHandler = new WindowEventHandler
@restoreDimensions()
@restoreWindowDimensions()
@config.load()
@config.setDefaults('core', require('./workspace-view').configDefaults)
@config.setDefaults('editor', require('./editor-view').configDefaults)
@ -200,33 +191,62 @@ class Atom
@displayWindow()
# Private:
saveSync: ->
if statePath = @constructor.getStatePath(@mode)
super(statePath)
else
@getCurrentWindow().loadSettings.windowState = JSON.stringify(@serializeForPersistence())
# Private:
setBodyPlatformClass: ->
document.body.classList.add("platform-#{process.platform}")
# Public: Get the load settings for the current window.
#
# Returns an object containing all the load setting key/value pairs.
getLoadSettings: ->
@constructor.getLoadSettings()
# Private:
deserializeProject: ->
Project = require './project'
@project ?= new Project(path: @getLoadSettings().initialPath)
# Private:
deserializeWorkspaceView: ->
WorkspaceView = require './workspace-view'
@workspaceView = @deserializers.deserialize(@state.get('workspaceView'))
unless @workspaceView?
@workspaceView = new WorkspaceView()
@state.set('workspaceView', @workspaceView.getState())
$(@workspaceViewParentSelector).append(@workspaceView)
# Private:
deserializePackageStates: ->
@packages.packageStates = @state.getObject('packageStates') ? {}
@state.remove('packageStates')
# Private:
deserializeEditorWindow: ->
@deserializePackageStates()
@deserializeProject()
@deserializeWorkspaceView()
# Private:
unloadEditorWindow: ->
return if not @project and not @workspaceView
windowState = @getWindowState()
windowState.set('project', @project)
windowState.set('syntax', @syntax.serialize())
windowState.set('workspaceView', @workspaceView.serialize())
@state.set('syntax', @syntax.serialize())
@state.set('workspaceView', @workspaceView.serialize())
@packages.deactivatePackages()
windowState.set('packageStates', @packages.packageStates)
@saveWindowState()
@state.set('packageStates', @packages.packageStates)
@saveSync()
@workspaceView.remove()
@project.destroy()
@windowEventHandler?.unsubscribe()
@windowState = null
# Set up the default event handlers and menus for a non-editor window.
#
# This can be used by packages to have a minimum level of keybindings and
# menus available when not using the standard editor window.
#
# This should only be called after setUpEnvironment() has been called.
setUpDefaultEvents: ->
@windowEventHandler = new WindowEventHandler
@keymap.loadBundledKeymaps()
@menu.update()
# Private:
loadThemes: ->
@themes.load()
@ -239,6 +259,51 @@ class Atom
pack.reloadStylesheets?()
null
# Public:
getCurrentWindow: ->
@constructor.getCurrentWindow()
# Public: Get the dimensions of this window.
#
# Returns an object with x, y, width, and height keys.
getWindowDimensions: ->
browserWindow = @getCurrentWindow()
[x, y] = browserWindow.getPosition()
[width, height] = browserWindow.getSize()
{x, y, width, height}
# Public: Set the dimensions of the window.
#
# The window will be centered if either the x or y coordinate is not set
# in the dimensions parameter. If x or y are omitted the window will be
# centered. If height or width are omitted only the position will be changed.
#
# * dimensions:
# + x: The new x coordinate.
# + y: The new y coordinate.
# + width: The new width.
# + height: The new height.
setWindowDimensions: ({x, y, width, height}) ->
browserWindow = @getCurrentWindow()
if width? and height?
browserWindow.setSize(width, height)
if x? and y?
browserWindow.setPosition(x, y)
else
browserWindow.center()
# Private:
storeWindowDimensions: ->
@state.set('windowDimensions', @getWindowDimensions())
# Private:
restoreWindowDimensions: ->
windowDimensions = @state.getObject('windowDimensions') ? {}
{initialSize} = @getLoadSettings
windowDimensions.height ?= initialSize?.height ? global.screen.availHeight
windowDimensions.width ?= initialSize?.width ? Math.min(global.screen.availWidth, 1024)
@setWindowDimensions(windowDimensions)
# Public: Open a new Atom window using the given options.
#
# Calling this method without an options parameter will open a prompt to pick
@ -362,11 +427,11 @@ class Atom
# Public: Get the version of the Atom application.
getVersion: ->
app.getVersion()
@constructor.getVersion()
# Public: Determine whether the current version is an official release.
isReleasedVersion: ->
not /\w{7}/.test(@getVersion()) # Check if the release is a 7-character SHA prefix
@constructor.isReleasedVersion()
getGitHubAuthTokenName: ->
'Atom GitHub API Token'
@ -383,87 +448,9 @@ class Atom
#
# Returns the absolute path to ~/.atom
getConfigDirPath: ->
@constructor.getConfigDirPath()
@configDirPath ?= fs.absolute('~/.atom')
# Public: Get the directory path to Atom's storage area.
#
# Returns the absoluste path to ~/.atom/storage
getStorageDirPath: ->
@storageDirPath ?= path.join(@getConfigDirPath(), 'storage')
# Private:
getWindowStatePath: ->
switch @windowMode
when 'spec'
filename = @windowMode
when 'editor'
{initialPath} = @getLoadSettings()
if initialPath
sha1 = crypto.createHash('sha1').update(initialPath).digest('hex')
filename = "editor-#{sha1}"
if filename
path.join(@getStorageDirPath(), filename)
else
null
# Public: Set the window state of the given keypath to the value.
setWindowState: (keyPath, value) ->
windowState = @getWindowState()
windowState.set(keyPath, value)
windowState
# Private
loadSerializedWindowState: ->
if windowStatePath = @getWindowStatePath()
if fs.existsSync(windowStatePath)
try
documentStateJson = fs.readFileSync(windowStatePath, 'utf8')
catch error
console.warn "Error reading window state: #{windowStatePath}", error.stack, error
else
documentStateJson = @getLoadSettings().windowState
try
documentState = JSON.parse(documentStateJson) if documentStateJson
catch error
console.warn "Error parsing window state: #{windowStatePath}", error.stack, error
# Private:
loadWindowState: ->
serializedWindowState = @loadSerializedWindowState()
doc = Document.deserialize(serializedWindowState) if serializedWindowState?
doc ?= Document.create()
doc.registerModelClasses(
require('./text-buffer'),
require('./project'),
require('./tokenized-buffer'),
require('./display-buffer'),
require('./editor')
)
# TODO: Remove this when everything is using telepath models
if @site?
@site.setRootDocument(doc)
else
@site = new SiteShim(doc)
doc
# Private:
saveWindowState: ->
windowState = @getWindowState()
if windowStatePath = @getWindowStatePath()
windowState.saveSync(windowStatePath)
else
@getCurrentWindow().loadSettings.windowState = JSON.stringify(windowState.serializeForPersistence())
# Public: Get the window state for the key path.
getWindowState: (keyPath) ->
@windowState ?= @loadWindowState()
if keyPath
@windowState.get(keyPath)
else
@windowState
# Public: Get the time taken to completely load the current window.
#
# This time include things like loading and activating packages, creating
@ -474,10 +461,6 @@ class Atom
getWindowLoadTime: ->
@loadTime
# Private: Returns a replicated copy of the current state.
replicate: ->
@getWindowState().replicate()
# Private:
crashMainProcess: ->
remote.process.crash()

View File

@ -15,7 +15,7 @@ class PaneAxis extends View
@state.get('children').each (child, index) =>
@addChild(atom.deserializers.deserialize(child), index, updateState: false)
else
@state = atom.site.createDocument(deserializer: @className(), children: [])
@state = atom.create(deserializer: @className(), children: [])
@addChild(child) for child in args
@state.get('children').on 'changed', ({index, insertedValues, removedValues, siteId}) =>

View File

@ -23,7 +23,7 @@ class PaneContainer extends View
@state = state
@setRoot(atom.deserializers.deserialize(@state.get('root')))
else
@state = atom.site.createDocument(deserializer: 'PaneContainer')
@state = atom.create(deserializer: 'PaneContainer')
@subscribe @state, 'changed', ({newValues, siteId}) =>
return if siteId is @state.siteId

View File

@ -39,7 +39,7 @@ class Pane extends View
item
else
@items = args
@state = atom.site.createDocument
@state = atom.create
deserializer: 'Pane'
items: @items.map (item) -> item.getState?() ? item.serialize()

View File

@ -1,11 +1,8 @@
# Private: TODO remove once telepath upgrades are complete.
module.exports =
class SiteShim
constructor: (document) ->
@setRootDocument(document)
setRootDocument: (@document) ->
@id = @document.siteId
constructor: (@environment) ->
{@id} = @environment.state.siteId
createDocument: (values) ->
@document.create({values})
@environment.create()

View File

@ -7,8 +7,8 @@ require('crash-reporter').start(productName: 'Atom', companyName: 'GitHub')
require './window'
Atom = require './atom'
window.atom = new Atom()
atom.setUpEnvironment('editor')
window.atom = Atom.loadOrCreate('editor')
atom.initialize()
atom.startEditorWindow()
window.atom.loadTime = Date.now() - startTime
console.log "Window load time: #{atom.getWindowLoadTime()}ms"

View File

@ -38,7 +38,7 @@ class WindowEventHandler
confirmed
@subscribe $(window), 'unload', ->
atom.getWindowState().set('dimensions', atom.getDimensions())
atom.storeWindowDimensions()
@subscribeToCommand $(window), 'window:toggle-full-screen', => atom.toggleFullScreen()

View File

@ -70,7 +70,7 @@ class WorkspaceView extends View
panes = atom.deserializers.deserialize(state.get('panes'))
else
panes = new PaneContainer
@state = atom.site.createDocument
@state = atom.create
deserializer: @constructor.name
version: @constructor.version
panes: panes.getState()