Skip to content

Instantly share code, notes, and snippets.

@mizchi
Created May 6, 2014 17:33
Show Gist options
  • Save mizchi/4a447e74f23915d0b2e2 to your computer and use it in GitHub Desktop.
Save mizchi/4a447e74f23915d0b2e2 to your computer and use it in GitHub Desktop.

Atomコードリーディングメモ

ビルド方法

script/build

起動したらsrc/window-bootstrap.coffeeが起動時間のログを出してるので、そいつをgrepすると/src/broweser/atom-application.coffee が引っかかる。

src/broweser/atom-application.coffee は、 src/browser/main.coffee に呼ばれている 起動プロセスはどうもここっぽい。

  app.on 'finish-launching', ->
    app.removeListener 'open-file', addPathToOpen
    app.removeListener 'open-url', addUrlToOpen

    args.pathsToOpen = args.pathsToOpen.map (pathToOpen) ->
      path.resolve(args.executedFrom ? process.cwd(), pathToOpen)

    require('coffee-script').register()
    if args.devMode
      require(path.join(args.resourcePath, 'src', 'coffee-cache')).register()
      AtomApplication = require path.join(args.resourcePath, 'src', 'browser', 'atom-application')
    else
      AtomApplication = require './atom-application'

    AtomApplication.open(args)
    console.log("App load time: #{Date.now() - global.shellStartTime}ms") unless args.test

args.devModeってなんだろう…便利そうなのでおってみる

  options.alias('d', 'dev').boolean('d').describe('d', 'Run in development mode.')
  options.alias('f', 'foreground').boolean('f').describe('f', 'Keep the browser process in the foreground.')
  options.alias('h', 'help').boolean('h').describe('h', 'Print this usage message.')
  options.alias('l', 'log-file').string('l').describe('l', 'Log all output to file.')
  options.alias('n', 'new-window').boolean('n').describe('n', 'Open a new window.')
  options.alias('s', 'spec-directory').string('s').describe('s', 'Set the spec directory (default: Atom\'s spec directory).')
  options.boolean('safe').describe('safe', 'Do not load packages from ~/.atom/packages or ~/.atom/dev/packages.')
  options.alias('t', 'test').boolean('t').describe('t', 'Run the specified specs and exit with error code on failures.')
  options.alias('v', 'version').boolean('v').describe('v', 'Print the version.')
  options.alias('w', 'wait').boolean('w').describe('w', 'Wait for window to be closed before returning.')

-d で起動する。その方法はあとで調べる。

main.coffeeはどうやって指定されてるんだろう。grepする

~/p/atom (master) $ git grep main.coffee
script/utils/compile-main-to-app:coffee -c -o /Applications/Atom.app/Contents/Resources/app/src/ src/main.coffee

ほう。

script/utils/compile-main-to-app

#!/bin/sh

coffee -c -o /Applications/Atom.app/Contents/Resources/app/src/ src/main.coffee

うおー直接放り込んでるー!!!便利!!!!

ということで、ここでわかったのは直接中のコードいじりながらハックしたいときは /Applications/Atom.app/Contents/Resources/app/src/ の中身を直接触るのが楽そう

ところで, main.jsどこで生成されるんだろう

~/p/atom (master) $ git grep main.js
package.json:  "main": "./src/browser/main.js",

package.jsに書いてあった。たぶんnodeパッケージの仕組みを使って、ここからブートするんだと思う。それ以上は後で調べる。 というわけでmain.coffeeがエントリと思ってよさそう。 でも↑のcompile-main-to-app発見しないとここに至るの厳しいな…。あんまり行儀は良くない。

気を取り直して、atom-applicationを追う。

src/browser/atom-application.coffee

AtomWindow = require './atom-window'
ApplicationMenu = require './application-menu'
AtomProtocolHandler = require './atom-protocol-handler'
AutoUpdateManager = require './auto-update-manager'
BrowserWindow = require 'browser-window'
Menu = require 'menu'
app = require 'app'
dialog = require 'dialog'
fs = require 'fs'
ipc = require 'ipc'
path = require 'path'
os = require 'os'
net = require 'net'
shell = require 'shell'
url = require 'url'
{EventEmitter} = require 'events'
_ = require 'underscore-plus'
src/browser/atom-application.coffee

AtomWindowとかはだいたい予想がつくので、window-bootstrapを読んでる箇所から逆にたどってみる

L323~

      if devMode
        try
          bootstrapScript = require.resolve(path.join(global.devResourcePath, 'src', 'window-bootstrap'))
          resourcePath = global.devResourcePath

      bootstrapScript ?= require.resolve('../window-bootstrap')
      resourcePath ?= @resourcePath
      openedWindow = new AtomWindow({pathToOpen, initialLine, initialColumn, bootstrapScript, resourcePath, devMode, safeMode, windowDimensions})

この呼び出し元を辿るとここらへんに行き着く。

# L139~
    @on 'application:open', -> @promptForPath(type: 'all')
    @on 'application:open-file', -> @promptForPath(type: 'file')
    @on 'application:open-folder', -> @promptForPath(type: 'folder')
    @on 'application:open-dev', -> @promptForPath(devMode: true)
    @on 'application:open-safe', -> @promptForPath(safeMode: true)

たぶん初期タブか何かは偽のプロンプトメッセージを受けて初期化されてそう

~/p/atom (master) $ git grep "application:open"
keymaps/darwin.cson:  'cmd-O': 'application:open-dev'
keymaps/darwin.cson:  'cmd-o': 'application:open'
...

ユーザーから使えるキーバインドにもマップしてある。

あと気になったのがここ

src/workspace-view.coffee:    @command 'application:open', -> ipc.send('command', 'application:open')
src/workspace-view.coffee:    @command 'application:open-file', -> ipc.send('command', 'application:open-file')
src/workspace-view.coffee:    @command 'application:open-folder', -> ipc.send('command', 'application:open-folder')
src/workspace-view.coffee:    @command 'application:open-dev', -> ipc.send('command', 'application:open-dev')
src/workspace-view.coffee:    @command 'application:open-safe', -> ipc.send('command', 'application:open-safe')
src/workspace-view.coffee:    @command 'application:open-your-config', -> ipc.send('command', 'application:open-your-config')
src/workspace-view.coffee:    @command 'application:open-your-init-script', -> ipc.send('command', 'application:open-your-init-script')
src/workspace-view.coffee:    @command 'application:open-your-keymap', -> ipc.send('command', 'application:open-your-keymap')
src/workspace-view.coffee:    @command 'application:open-your-snippets', -> ipc.send('command', 'application:open-your-snippets')
src/workspace-view.coffee:    @command 'application:open-your-stylesheet', -> ipc.send('command', 'application:open-your-stylesheet')
src/workspace-view.coffee:    @command 'application:open-license', => @model.openLicense()

workspace-viewってのが主要なUIっぽい。 workspace-viewのコードを追っていったら spacepen が出てきた。

space-pen by atom たしかそういう感じのテンプレートエンジンもどきがあるっていう話があったのは覚えてたけど、ここで出てくるのか。

一旦深追いをやめて、window-bootstrap.coffeeを呼んでる。

src/window-bootstrap.coffee

# Like sands through the hourglass, so are the days of our lives.
startTime = Date.now()

require './window'

Atom = require './atom'
window.atom = Atom.loadOrCreate('editor')
atom.initialize()
atom.startEditorWindow()
window.atom.loadTime = Date.now() - startTime
console.log "Window load time: #{atom.getWindowLoadTime()}ms"

とりあえず atom.coffeeが本体っぽいように見える。 とりあえずsrc/window.coffee を読んでみる

# Public: Measure how long a function takes to run.
#
# description - A {String} description that will be logged to the console when
#               the function completes.
# fn - A {Function} to measure the duration of.
#
# Returns the value returned by the given function.
window.measure = (description, fn) ->
  start = Date.now()
  value = fn()
  result = Date.now() - start
  console.log description, result
  value

# Public: Create a dev tools profile for a function.
#
# description - A {String} description that will be available in the Profiles
#               tab of the dev tools.
# fn - A {Function} to profile.
#
# Returns the value returned by the given function.
window.profile = (description, fn) ->
  measure description, ->
    console.profile(description)
    value = fn()
    console.profileEnd(description)
    value

ベンチマーク用のヘルパが生えてる。windowに副作用を及ぼすのを限定したいからwindow.coffeeっぽい。まあ気持ちはわかる。

というわけで実際のアプリケーション的なエントリポイントはここ

window.atom = Atom.loadOrCreate('editor')
atom.initialize()
atom.startEditorWindow()

とりあえずAtomクラスの冒頭部分を読む

class Atom extends Model
  @version: 1  # Increment this when the serialization format changes

  # 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) ->
    @deserialize(@loadState(mode)) ? new this({mode, @version})

  # Deserializes the Atom environment from a state object
  @deserialize: (state) ->
    new this(state) if state?.version is @version

なんでモデルを継承してるんでしょうね…(困惑) Atom.version が書き換わったら仮にインスタンスがあっても捨てて新しいのを作る、という風に読める。

  # Loads and returns the serialized state corresponding to this window
  # if it exists; otherwise returns undefined.
  @loadState: (mode) ->
    statePath = @getStatePath(mode)

    if fs.existsSync(statePath)
      try
        stateString = fs.readFileSync(statePath, 'utf8')
      catch error
        console.warn "Error reading window state: #{statePath}", error.stack, error
    else
      stateString = @getLoadSettings().windowState

    try
      JSON.parse(stateString) if stateString?
    catch error
      console.warn "Error parsing window state: #{statePath} #{error.stack}", error
@mizchi
Copy link
Author

mizchi commented May 12, 2014

Workspace

class Workspace extends Model
  atom.deserializers.add(this)
  Serializable.includeInto(this)

  @delegatesProperty 'activePane', 'activePaneItem', toProperty: 'paneContainer'

  @properties
    paneContainer: -> new PaneContainer
    fullScreen: false
    destroyedItemUris: -> []

  constructor: ->
    super

    @openers = []

    @subscribe @paneContainer, 'item-destroyed', @onPaneItemDestroyed
    @registerOpener (filePath) =>
      switch filePath
        when 'atom://.atom/stylesheet'
          @open(atom.themes.getUserStylesheetPath())
        when 'atom://.atom/keymap'
          @open(atom.keymaps.getUserKeymapPath())
        when 'atom://.atom/config'
          @open(atom.config.getUserConfigPath())
        when 'atom://.atom/init-script'
          @open(atom.getUserInitScriptPath())

PaneContainerが画面分割管理の実体っぽい。
@paneContainer内部で要素が削除された時、 @onPaneItemDestroyed が呼ばれる

  # Adds the destroyed item's uri to the list of items to reopen.
  onPaneItemDestroyed: (item) =>
    if uri = item.getUri?()
      @destroyedItemUris.push(uri)

あーなるほど、タブ復元のために破棄したアイテムのuri(あとで調べる)をpushしてる。

registerOpener

  registerOpener: (opener) ->
    @openers.push(opener)

@openersにコールバック登録、かな

filepathがatomの設定ファイルならば、open を呼ぶ。stylesheet とかkeymap は前回見たからいいとして、init-script は初めて見たので調べる。

src/atom.coffee

  getUserInitScriptPath: ->
    initScriptPath = fs.resolve(@getConfigDirPath(), 'init', ['js', 'coffee'])
    initScriptPath ? path.join(@getConfigDirPath(), 'init.coffee')
  getUserInitScriptPath: ->
    initScriptPath = fs.resolve(@getConfigDirPath(), 'init', ['js', 'coffee'])
    initScriptPath ? path.join(@getConfigDirPath(), 'init.coffee')

~/.atom/init.coffeeかな?自分のローカルの設定ファイルを見てみる。

# Your init script
#
# Atom will evaluate this file each time a new window is opened. It is run
# after packages are loaded/activated and after the previous editor state
# has been restored.
#
# An example hack to make opened Markdown files always be soft wrapped:
#
# path = require 'path'
#
# atom.workspaceView.eachEditorView (editorView) ->
#   editor = editorView.getEditor()
#   if path.extname(editor.getPath()) is '.md'
#     editor.setSoftWrap(true)

こいつを読み込んでどっかで実行するんだろう

PaneCotainer

class PaneContainer extends Model
  atom.deserializers.add(this)
  Serializable.includeInto(this)

  @properties
    root: -> new Pane
    activePane: null

  previousRoot: null

  @behavior 'activePaneItem', ->
    @$activePane
      .switch((activePane) -> activePane?.$activeItem)
      .distinctUntilChanged()

  constructor: (params) ->
    super
    @subscribe @$root, @onRootChanged
    @destroyEmptyPanes() if params?.destroyEmptyPanes

なんかrootを軸にList構造作ってから配置する感じだろうか

module.exports 
class Pane extends Model
  atom.deserializers.add(this)
  Serializable.includeInto(this)

# ...

  constructor: (params) ->
    super

    @items = Sequence.fromArray(compact(params?.items ? []))
    @activeItem ?= @items[0]

    @subscribe @items.onEach (item) =>
      if typeof item.on is 'function'
        @subscribe item, 'destroyed', => @removeItem(item, true)

    @subscribe @items.onRemoval (item, index) =>
      @unsubscribe item if typeof item.on is 'function'

    @activate() if params?.active

activate

  # Public: Makes this pane the *active* pane, causing it to gain focus
  # immediately.
  activate: ->
    @container?.activePane = this
    @emit 'activated'

次にWorkspaceViewを調べるか

WorkspaceView

ありがたいコメント

# Public: The top-level view for the entire window. An instance of this class is
# available via the `atom.workspaceView` global.

Viewを継承してるのでconstructorじゃなくてinitializeを使う。Backboneと同じ設計。

  initialize: (@model) ->
    @model = atom.workspace ? new Workspace unless @model?

    panes = new PaneContainerView(@model.paneContainer)
    @panes.replaceWith(panes)
    @panes = panes

    @subscribe @model, 'uri-opened', => @trigger 'uri-opened'

PaneContainerView

class PaneContainerView extends View
  Delegator.includeInto(this)

  @delegatesMethod 'saveAll', toProperty: 'model'

  @content: ->
    @div class: 'panes'

  initialize: (params) ->
    if params instanceof PaneContainer
      @model = params
    else
      @model = new PaneContainer({root: params?.root?.model})

    @subscribe @model.$root, @onRootChanged
    @subscribe @model.$activePaneItem.changes, @onActivePaneItemChanged

WorkspaceViewはinitializeでイベントを登録してる。それに対応するのはbrowser/atom-application側でhandleされてる

src/workspace-view.coffee

    @command 'application:about', -> ipc.send('command', 'application:about')
    @command 'application:run-all-specs', -> ipc.send('command', 'application:run-all-specs')
    @command 'application:run-benchmarks', -> ipc.send('command', 'application:run-benchmarks')
    @command 'application:show-settings', -> ipc.send('command', 'application:show-settings')
    @command 'application:quit', -> ipc.send('command', 'application:quit')
    @command 'application:hide', -> ipc.send('command', 'application:hide')
    @command 'application:hide-other-applications', -> ipc.send('command', 'application:hide-other-applications')
    @command 'application:unhide-all-applications', -> ipc.send('command', 'application:unhide-all-applications')
    @command 'application:new-window', -> ipc.send('command', 'application:new-window')
    @command 'application:new-file', -> ipc.send('command', 'application:new-file')
    @command 'application:open', -> ipc.send('command', 'application:open')
    ...

src/borwser/atom-application.coffee

  # Registers basic application commands, non-idempotent.
  handleEvents: ->
    @on 'application:run-all-specs', -> @runSpecs(exitWhenDone: false, resourcePath: global.devResourcePath)
    @on 'application:run-benchmarks', -> @runBenchmarks()
    @on 'application:quit', -> app.quit()
    @on 'application:new-window', -> @openPath(windowDimensions: @focusedWindow()?.getDimensions())
    @on 'application:new-file', -> (@focusedWindow() ? this).openPath()
    @on 'application:open', -> @promptForPath(type: 'all')
    @on 'application:open-file', -> @promptForPath(type: 'file')
    @on 'application:open-folder', -> @promptForPath(type: 'folder')
    @on 'application:open-dev', -> @promptForPath(devMode: true)
    @on 'application:open-safe', -> @promptForPath(safeMode: true)
    @on 'application:inspect', ({x,y, atomWindow}) ->
    ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment