Skip to content

Instantly share code, notes, and snippets.

@mizchi
Created May 6, 2014 17:33
Show Gist options
  • Star 72 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • 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 7, 2014

ビルド

src/browser/window-bootstrap.coffee に console.log ’Hello’ 仕込んでからビルドしてみる

script/build
npm install grunt-cli -g
cd build
grunt

適用されてログが吐かれた。

install とか run-specs ってどこにあるんだろう…と思ったら build/tasksの中。それとは別にgrunt-download-atom-shell ってリポジトリでビルドタスクとかテストとか外出しされてる。

atom/grunt-download-atom-shell

とはいえ実体はこれだけ grunt-download-atom-shell/tasks/download-atom-shell-task.coffee at master · atom/grunt-download-atom-shell

テストを走らせる

grunt test 走らせてみた

~/p/a/build (master) $ grunt test
Running "shell:kill-atom" (shell) task

Running "run-specs" task
>> [97499:0507/211153:WARNING:dns_config_service_posix.cc(188)] dns_config has unhandled options!
>> 2014-05-07 21:11:53.644 Atom[97499:507] hello! from main
>> [97505:0507/211154:INFO:renderer_main.cc(227)] Renderer process started
>> ..[97499:0507/211203:INFO:CONSOLE(123)] "Failed to load package named 'package-with-broken-keymap'", source: /Users/mizchi/proj/atom/src/package.coffee (123)
>> .....................................F.....................................-.................................................................................................................................................................................................................................................................................................................................................................................................................................................................-.....................................................................................................................................[97499:0507/211315:INFO:CONSOLE(992)] "0", source: /Users/mizchi/proj/atom/src/display-buffer.coffee (992)
>> [97499:0507/211315:INFO:CONSOLE(992)] "1", source: /Users/mizchi/proj/atom/src/display-buffer.coffee (992)
>> ..........................................................................................................................................................................................................................................................................................................................................................[97499:0507/211355:INFO:CONSOLE(0)] "event.returnValue is deprecated. Please use the standard event.preventDefault() instead.", source:  (0)
>> ..............................................
>>
>> the `atom` global
>>   .isReleasedVersion()
>>     it returns false if the version is a SHA and true otherwise
>>       Error: Leaking subscriptions for paths: /Users/mizchi/.atom/styles.less
>>         at ensureNoPathSubscriptions (/Users/mizchi/proj/atom/spec/spec-helper.coffee:139:15)
>>         at [object Object].<anonymous> (/Users/mizchi/proj/atom/spec/spec-helper.coffee:130:3)
>>
>>
>> Finished in 120.622 seconds
>> 1051 tests, 3567 assertions, 1 failure, 2 skipped
>> [Error] atom core spec(s) failed

なんかぼちぼちこける。やたらCPU負荷が高いと思ったら、async.jsでテストが並列実行されてた

  grunt.registerTask 'run-specs', 'Run the specs', ->
    done = @async()
    startTime = Date.now()

    # TODO: This should really be parallel on both platforms, however our
    # fixtures step on each others toes currently.
    if process.platform is 'darwin'
      method = async.parallel
    else if process.platform is 'win32'
      method = async.series

    method [runCoreSpecs, runPackageSpecs], (error, results) ->
      [coreSpecFailed, failedPackages] = results
      elapsedTime = Math.round((Date.now() - startTime) / 100) / 10
      grunt.verbose.writeln("Total spec time: #{elapsedTime}s")
      failures = failedPackages
      failures.push "atom core" if coreSpecFailed

      grunt.log.error("[Error]".red + " #{failures.join(', ')} spec(s) failed") if failures.length > 0

      # TODO: Mark the build as green on Windows until specs pass.
      if process.platform is 'darwin'
        done(!coreSpecFailed and failedPackages.length == 0)
      else if process.platform is 'win32'
        done(true)

L111

start()

こいつをコメントアウトすると何もしない、ウィンドウすら生成しない

finish-launching で grep してみる

~/p/atom (master) $ git grep finish-launching
src/browser/main.coffee:  app.on 'will-finish-launching', ->
src/browser/main.coffee:  app.on 'finish-launching', ->

あれーどこからこのメッセージくるんだ。あとで調べる。

webviewとしてのエントリポイントは static/index.htmlっぽい

src/browser/atom-window.coffee L22~

  constructor: (settings={}) ->
    {@resourcePath, pathToOpen, initialLine, initialColumn, @isSpec, @exitWhenDone} = settings
    global.atomApplication.addWindow(this)

    @browserWindow = new BrowserWindow show: false, title: 'Atom', icon: @constructor.iconPath
    @handleEvents()

    loadSettings = _.extend({}, settings)
    loadSettings.windowState ?= '{}'
    loadSettings.appVersion = app.getVersion()

    # Only send to the first non-spec window created
    if @constructor.includeShellLoadTime and not @isSpec
      @constructor.includeShellLoadTime = false
      loadSettings.shellLoadTime ?= Date.now() - global.shellStartTime

    loadSettings.initialPath = pathToOpen
    if fs.statSyncNoException(pathToOpen).isFile?()
      loadSettings.initialPath = path.dirname(pathToOpen)

    @browserWindow.loadSettings = loadSettings
    @browserWindow.once 'window:loaded', =>
      @emit 'window:loaded'
      @loaded = true

    @browserWindow.loadUrl @getUrl(loadSettings)
    @browserWindow.focusOnWebView() if @isSpec

    @openPath(pathToOpen, initialLine, initialColumn)

static/index.html

<!DOCTYPE html>
<html style="background: #fff">
<head>
  <title></title>

  <meta http-equiv="Content-Security-Policy" content="default-src *; script-src 'self'; style-src 'self' 'unsafe-inline';">

  <script src="index.js"></script>
</head>
<body tabindex="-1">
</body>
</html>

static/index.js

window.onload = function() {
  var path = require('path');
  var ipc = require('ipc');
  try {
    // Skip "?loadSettings=".
    var loadSettings = JSON.parse(decodeURIComponent(location.search.substr(14)));

    // Start the crash reporter before anything else.
    require('crash-reporter').start({
      productName: 'Atom',
      companyName: 'GitHub',
      // By explicitly passing the app version here, we could save the call
      // of "require('remote').require('app').getVersion()".
      extra: {_version: loadSettings.appVersion}
    });

    require('vm-compatibility-layer');
    require('coffee-script').register();
    require(path.resolve(__dirname, '..', 'src', 'coffee-cache')).register();
    require(loadSettings.bootstrapScript);
    ipc.sendChannel('window-command', 'window:loaded')
  }
  catch (error) {
    var currentWindow = require('remote').getCurrentWindow();
    currentWindow.setSize(800, 600);
    currentWindow.center();
    currentWindow.show();
    currentWindow.openDevTools();
    console.error(error.stack || error);
  }
}

ipcってのがブラウザタブと本体アプリが通信するもののようにみえる。

ipc.sendChannel('window-command', 'window:loaded') で各種初期化処理が走りそう

ipcは追っていったらatom-shell側に定義してある。atom/atom では完結しなさそうなので一旦諦めるとして、window:loaded を追う

~/p/atom (master) $ git grep "window:loaded"
src/browser/atom-application.coffee:    window.once 'window:loaded', =>
src/browser/atom-window.coffee:    @browserwindow.once 'window:loaded', =>
src/browser/atom-window.coffee:      @emit 'window:loaded'
src/browser/atom-window.coffee:      @browserWindow.once 'window:loaded', => @openPath(pathToOpen, initialLine, initialColumn)
static/index.js:    <ipc class="se"></ipc>ndChannel('window-command', 'window:loaded')

アプリケーションの全体初期化と、個別のウィンドウの初期化かな?

atom-window.coffee L42~

    @browserWindow.loadSettings = loadSettings
    @browserWindow.once 'window:loaded', =>
      @emit 'window:loaded'
      @loaded = true

atom-application.coffee L96

    window.once 'window:loaded', =>
      @autoUpdateManager.emitUpdateAvailableEvent(window)

初期化終わったのでアップデートマネージャに割り込んでもいいよ的なメッセージを投げてる気配。たぶんこっから追ってもどうしようもなさそうなので、別の方いこう。

経路としては index.js => atom-window => atom-application っぽい。オブザーバーお化けってほどじゃないけど、それなりに複雑。

@mizchi
Copy link
Author

mizchi commented May 8, 2014

window-bootstrap.coffee のinitialize~startEditorWindow 付近を追う。

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

両方共コメントアウトしてみる => クラッシュ

atom.initialize() だけ呼んでみる。 => Check for update とかのメニューだけ出現し、ウィンドウはまだ生成されず。

ここで気づいたんだけど、今までは src/browser の中の処理を追ってたけど、ここから src/ が相対パスになるっぽい。browserという言葉に惑わされてたけど、想定していたのと逆だった。

というわけで やっと src/atom.coffee を読むことになる。

atom.coffee

冒頭に便利っぽいコメントを見つけた。

# Public: Atom global for dealing with packages, themes, menus, and the window.
#
# An instance of this class is always available as the `atom` global.
#
# ## Useful properties available:
#
#  * `atom.clipboard`     - A {Clipboard} instance
#  * `atom.config`        - A {Config} instance
#  * `atom.contextMenu`   - A {ContextMenuManager} instance
#  * `atom.deserializers` - A {DeserializerManager} instance
#  * `atom.keymaps`        - A {Keymap} instance
#  * `atom.menu`          - A {MenuManager} instance
#  * `atom.packages`      - A {PackageManager} instance
#  * `atom.project`       - A {Project} instance
#  * `atom.syntax`        - A {Syntax} instance
#  * `atom.themes`        - A {ThemeManager} instance
#  * `atom.workspace`     - A {Workspace} instance
#  * `atom.workspaceView` - A {WorkspaceView} instance

シェルに入ると確かにこれらの変数がある。atom.clipboardは挙動の予想がつくので触ってみる。

> atom.clipboard
Clipboard {metadata: null, signatureForMetadata: null, md5: function, write: function, read: function…}
> atom.clipboard.read()
"atom-reading.md https://gist.github.com/mizchi/4a447e74f23915d0b2e2"
> atom.clipboard.write('faffsda')
undefined
> faffsda # Cmd+V

確かにまあ動く感じ。

atom.config

Config {configDirPath: "/Users/mizchi/.atom", resourcePath: "/Applications/Atom.app/Contents/Resources/app", defaultSettings: Object, settings: Object, configFileHasErrors: false…}
configDirPath: "/Users/mizchi/.atom"
configFileHasErrors: false
configFilePath: "/Users/mizchi/.atom/config.cson"
defaultSettings: Object
eventHandlersByEventName: Object
eventHandlersByNamespace: Object
resourcePath: "/Applications/Atom.app/Contents/Resources/app"
settings: Object
subscriptionCounts: Object
watchSubscription: PathWatcher
__proto__: Config

ん、~/.atom に設定ファイルがある?

~/p/a/build (debug) $ la  ~/.atom/
total 40
drwxr-xr-x   12 mizchi  staff   408B  5  7 21:01 .
drwxr-xr-x+ 119 mizchi  staff   4.0K  5  7 22:56 ..
drwxr-xr-x    4 mizchi  staff   136B  5  7 00:53 .node-gyp
drwxr-xr-x  251 mizchi  staff   8.3K  5  7 22:57 .npm
drwxr-xr-x    5 mizchi  staff   170B  5  7 01:24 compile-cache
-rw-r--r--    1 mizchi  staff   317B  5  7 01:29 config.cson
-rw-r--r--    1 mizchi  staff   459B  5  7 00:41 init.coffee
-rw-r--r--    1 mizchi  staff   479B  5  7 00:41 keymap.cson
drwxr-xr-x    3 mizchi  staff   102B  5  7 00:41 packages
-rw-r--r--    1 mizchi  staff   415B  5  7 00:41 snippets.cson
drwxr-xr-x    4 mizchi  staff   136B  5  7 22:53 storage
-rw-r--r--    1 mizchi  staff   282B  5  7 00:41 styles.less

なるほど把握。

そろそろAtomに戻ってコンストラクタから順番に処理を追う。

L124

  # Call .loadOrCreate instead
  constructor: (@state) ->
    {@mode} = @state
    DeserializerManager = require './deserializer-manager'
    @deserializers = new DeserializerManager()

DeserializerManager を追う

# Public: Manages the deserializers used for serialized state
#
# An instance of this class is always available as the `atom.deserializers`
# global.
#

何のデシリアライザなのかこれだけじゃわからんのでインスタンスをみてみる

> atom.deserializers
DeserializerManager {deserializers: Object, add: function, remove: function, deserialize: function, get: function}
deserializers: Object
  ArchiveEditor: function ArchiveEditor(_arg) {
  Editor: function Editor(_arg) {
  ImageEditor: function ImageEditor(filePath) {
  MarkdownPreviewView: function MarkdownPreviewView(_arg) {
  Pane: function Pane(params) {
  PaneAxis: function PaneAxis(_arg) {
  PaneContainer: function PaneContainer(params) {
  Project: function Project(_arg) {
  ReleaseNotesView: Object
  SettingsView: Object
  StyleguideView: Object
  Syntax: function Syntax() {
  TextBuffer: function TextBuffer(params) {
  TimecopView: Object
  Workspace: function Workspace() {

ウッこれただのグローバル変数じゃ…

> atom.deserializers.deserializers.SettingsView
Object {name: "SettingsView", version: 2, deserialize: function}

まだこれが何なのか判断しかねるので次へ

initializeをみる

  # 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: ->
    window.onerror = =>
      @openDevTools()
      @executeJavaScriptInDevTools('InspectorFrontendAPI.showConsole()')
      @emit 'uncaught-error', arguments...

    @unsubscribe()
    @setBodyPlatformClass()

    @loadTime = null

    Config = require './config'
    KeymapManager = require './keymap-extensions'
    PackageManager = require './package-manager'
    Clipboard = require './clipboard'
    Syntax = require './syntax'
    ThemeManager = require './theme-manager'
    ContextMenuManager = require './context-menu-manager'
    MenuManager = require './menu-manager'
    {devMode, safeMode, resourcePath} = @getLoadSettings()
    configDirPath = @getConfigDirPath()

    # Add 'src/exports' to module search path.
    exportsPath = path.resolve(resourcePath, 'exports')
    require('module').globalPaths.push(exportsPath)
    # Still set NODE_PATH since tasks may need it.
    process.env.NODE_PATH = exportsPath

    # Make react.js faster
    process.env.NODE_ENV ?= 'production'

    @config = new Config({configDirPath, resourcePath})
    @keymaps = new KeymapManager({configDirPath, resourcePath})
    @keymap = @keymaps # Deprecated
    @packages = new PackageManager({devMode, configDirPath, resourcePath, safeMode})
    @themes = new ThemeManager({packageManager: @packages, configDirPath, resourcePath})
    @contextMenu = new ContextMenuManager(devMode)
    @menu = new MenuManager({resourcePath})
    @clipboard = new Clipboard()

    @syntax = @deserializers.deserialize(@state.syntax) ? new Syntax()

    @subscribe @packages, 'activated', => @watchThemes()

    Project = require './project'
    TextBuffer = require 'text-buffer'
    @deserializers.add(TextBuffer)
    TokenizedBuffer = require './tokenized-buffer'
    DisplayBuffer = require './display-buffer'
    Editor = require './editor'

    @windowEventHandler = new WindowEventHandler

ここでrequireのパス周りに手を加えているので追ってみよう。

    {devMode, safeMode, resourcePath} = @getLoadSettings()
    configDirPath = @getConfigDirPath()

    # Add 'src/exports' to module search path.
    exportsPath = path.resolve(resourcePath, 'exports')

getLoadSettingsは何をしているのだろう。

  # Returns the load settings hash associated with the current window.
  @getLoadSettings: ->
    @loadSettings ?= JSON.parse(decodeURIComponent(location.search.substr(14)))
    cloned = _.deepClone(@loadSettings)
    # The loadSettings.windowState could be large, request it only when needed.
    cloned.__defineGetter__ 'windowState', =>
      @getCurrentWindow().loadSettings.windowState
    cloned.__defineSetter__ 'windowState', (value) =>
      @getCurrentWindow().loadSettings.windowState = value
    cloned

シェルで確認してみる

> JSON.parse(decodeURIComponent(location.search.substr(14)))
Object {bootstrapScript: "/Applications/Atom.app/Contents/Resources/app/src/window-bootstrap.js", resourcePath: "/Applications/Atom.app/Contents/Resources/app", appVersion: "fb6782e"}

location.searchにconfigが埋め込まれてたっぽい。気が向いたらbrowser側で何をやってるか追っても良い。

resourcePathのディレクトリ覗いてみる

~/p/a/build (debug) $ la /Applications/Atom.app/Contents/Resources/app
total 848
drwxr-xr-x   18 mizchi  admin   612B  5  7 23:03 .
drwxr-xr-x    8 mizchi  admin   272B  5  7 23:03 ..
-rw-r--r--    1 mizchi  admin   411K  5  7 23:03 LICENSE.md
drwxr-xr-x    5 mizchi  admin   170B  5  7 23:03 apm
-rwxr-xr-x    1 mizchi  admin   2.2K  5  7 23:03 atom.sh
drwxr-xr-x    6 mizchi  admin   204B  5  7 23:03 benchmark
drwxr-xr-x    9 mizchi  admin   306B  5  7 23:03 dot-atom
drwxr-xr-x    3 mizchi  admin   102B  5  7 23:03 exports
drwxr-xr-x    7 mizchi  admin   238B  5  7 23:03 keymaps
drwxr-xr-x   10 mizchi  admin   340B  5  7 23:03 less-compile-cache
drwxr-xr-x    5 mizchi  admin   170B  5  7 23:03 menus
drwxr-xr-x  119 mizchi  admin   4.0K  5  7 23:03 node_modules
-rw-r--r--    1 mizchi  admin   4.0K  5  7 23:03 package.json
drwxr-xr-x    4 mizchi  admin   136B  5  7 23:03 resources
drwxr-xr-x   43 mizchi  admin   1.4K  5  7 23:03 spec
drwxr-xr-x   73 mizchi  admin   2.4K  5  7 23:03 src
drwxr-xr-x   29 mizchi  admin   986B  5  7 23:03 static
drwxr-xr-x    4 mizchi  admin   136B  5  7 23:03 vendor

えっと、これつまり node_modulesが存在するからそこからもrequireできるってことかな…

~/p/a/build (debug) $ ls /Applications/Atom.app/Contents/Resources/app/node_modules/
archive-view                bootstrap                   first-mate                  jasmine-tagged              language-less               language-text               open-on-github              scrollbar-style             tabs
async                       bracket-matcher             fs-plus                     keybinding-resolver         language-make               language-todo               optimist                    season                      temp
atom-dark-syntax            clear-cut                   fstream                     language-c                  language-objective-c        language-toml               package-generator           semver                      text-buffer
atom-dark-ui                coffee-script               fuzzaldrin                  language-coffee-script      language-perl               language-xml                pathwatcher                 serializable                theorist
atom-keymap                 coffeestack                 fuzzy-finder                language-css                language-php                language-yaml               property-accessors          settings-view               timecop
atom-light-syntax           command-palette             git-diff                    language-gfm                language-property-list      less-cache                  q                           snippets                    tree-view
atom-light-ui               delegato                    git-utils                   language-git                language-python             link                        random-words                solarized-dark-syntax       underscore-plus
autocomplete                deprecation-cop             go-to-line                  language-go                 language-ruby               markdown-preview            react                       solarized-light-syntax      update-package-dependencies
autoflow                    dev-live-reload             grammar-selector            language-html               language-ruby-on-rails      metrics                     reactionary                 space-pen                   vm-compatibility-layer
autosave                    emissary                    grim                        language-hyperlink          language-sass               mixto                       release-notes               spell-check                 welcome
background-tips             exception-reporting         guid                        language-java               language-shellscript        mkdirp                      runas                       status-bar                  whitespace
base16-tomorrow-dark-theme  feedback                    humanize-plus               language-javascript         language-source             nslog                       scandal                     styleguide                  wrap-guide
bookmarks                   find-and-replace            image-view                  language-json               language-sql                oniguruma                   scoped-property-store       symbols-view

apmで入れるパッケージっぽい。自分はまだ何も入れてないので、こんな感じか。

これを念頭に次のコードを追う

src/window-event-handler.coffee

windowのeventや、親windowから渡されてくるイベントを購読しまくって横流ししてる

src/editor.coffee

編集領域の実装。Cursorやundo/redo制御。長い。EditorViewにインスタンス化されるモデル。

Atom#startEditorWindow の続きを読む。

  # Call this method when establishing a real application window.
  startEditorWindow: ->
    CommandInstaller = require './command-installer'
    resourcePath = atom.getLoadSettings().resourcePath
    CommandInstaller.installAtomCommand resourcePath, false, (error) ->
      console.warn error.message if error?
    CommandInstaller.installApmCommand resourcePath, false, (error) ->
      console.warn error.message if error?

    @restoreWindowDimensions() # 画面分割状態の再構成っぽい
    @config.load()
    @config.setDefaults('core', require('./workspace-view').configDefaults) # デフォでワークスペースあるよ
    @config.setDefaults('editor', require('./editor-view').configDefaults) # デフォでエディタビューあるよ
    @keymaps.loadBundledKeymaps() # デフォルトのキーマップ初期化するよ
    @themes.loadBaseStylesheets() # デフォルトCSS読み込むよ
    @packages.loadPackages() # パッケージ読み込むよ
    @deserializeEditorWindow() # 未だにdeserializeが何を指してるのかわからん
    @packages.activate() # パッケージ有効化するよ
    @keymaps.loadUserKeymap() # ユーザーサイドのキーバインド有効化するよ
    @requireUserInitScript() # ユーザー側で定義された初期化スクリプト流すよ
    @menu.update() # メニュー更新するよ

    $(window).on 'unload', =>
      $(document.body).css('visibility', 'hidden')
      @unloadEditorWindow()
      false

    @displayWindow() # 画面表示

ここでやっと画面表示してるっぽい

resourcePathがなんなのかシェルで実行してみる

> atom.getLoadSettings().resourcePath
"/Applications/Atom.app/Contents/Resources/app"

dispalyWindow()

  # Schedule the window to be shown and focused on the next tick.
  #
  # This is done in a next tick to prevent a white flicker from occurring
  # if called synchronously.
  displayWindow: ->
    setImmediate =>
      @show()
      @focus()
      @setFullScreen(true) if @workspaceView.fullScreen

なんか描画の都合でsetImmediateでずらしてる模様。たしかにこういうコードよく書く。糞だけど。

show()

  # Public: Show the current window.
  show: ->
    ipc.send('call-window-method', 'show')

atom-shell側にウィンド表示しろっていう命令出してる。

atom/browser/atom-application.coffee L218

    ipc.on 'call-window-method', (event, method, args...) ->
      win = BrowserWindow.fromWebContents(event.sender)
      win[method](args...)

送られてきたウィンドウを識別して(今気づいたけど同じプロセス上に複数のウィンドウが存在するみたいだ)、このケースだとwin.show()を読んでる。

focus() はたぶん俺にフォーカスしろ!っていう命令。あとはフルスクリーン化フラグがあればフルスクリーン化。

ここまで一連の初期化処理は処理は終了っぽい。アプリケーションのコアドメインはstartEditorWindowに集中しているようにみえる。次はそこを調べる。

@mizchi
Copy link
Author

mizchi commented May 8, 2014

restoreWindowDimensions() => getDefaultWindowDimensions() とみてたらlocalStorageを触ってる部分があった

    try
      dimensions = JSON.parse(localStorage.getItem("defaultWindowDimensions"))
    catch error
      console.warn "Error parsing default window dimensions", error
      localStorage.removeItem("defaultWindowDimensions")

コンソールで中身をみてみる

> localStorage.getItem("defaultWindowDimensions")
"{"x":161,"y":140,"width":1335,"height":702}"

atom.keymaps => KeymapManager で別ライブラリ。あとで。

atom.contextMenu: ContextMenuManager => src/context-menu-manager.coffee

外部パッケージ化されてないし、右クリックのときの挙動管理だろうから、ここを調べてみる。

API見た感じ、 atom.contextMenu に直接 add してるように見えるし、atom.contextMenuでgrepしてみると、次の行がでてくる

package.coffee:    atom.contextMenu.add(menuPath, map['context-menu']) for [menuPath, map] in @menus

各シーンやコンテキストに合わせて右クリックのアイテム一覧が再構築されるっぽい。

あとwidow-event-handler

window-event-handler.coffee:      $(atom.contextMenu.activeElement).trigger(command, args...)
window-event-handler.coffee:      atom.contextMenu.showForEvent(e)

ついでにこれ気になった。a タグの挙動のオーバーライド。

    @subscribe $(document), 'click', 'a', @openLink
  openLink: (event) ->
    location = $(event.target).attr('href')
    if location and location[0] isnt '#' and /^https?:\/\//.test(location)
      shell.openExternal(location)
    false

shellとは?

shell = require 'shell'

atom-shell ではないんだけど、なんだろうこれ
つまり location.href は基本的に変わらないものだと思ってよさそう。

ThemeManager

脱線したけど、次に ThemeManager#loadBaseStylesheets() を追う。名前通りのことをしていると思う。

  loadBaseStylesheets: ->
    @requireStylesheet('bootstrap/less/bootstrap')
    @reloadBaseStylesheets()

loadBaseStylesheets()

  # Public: Resolve and apply the stylesheet specified by the path.
  #
  # This supports both CSS and LESS stylsheets.
  #
  # stylesheetPath - A {String} path to the stylesheet that can be an absolute
  #                  path or a relative path that will be resolved against the
  #                  load path.
  #
  # Returns the absolute path to the required stylesheet.
  requireStylesheet: (stylesheetPath, ttype = 'bundled', htmlElement) ->
    if fullPath = @resolveStylesheet(stylesheetPath)
      content = @loadStylesheet(fullPath)
      @applyStylesheet(fullPath, content, ttype = 'bundled', htmlElement)
    else
      throw new Error("Could not find a file at path '#{stylesheetPath}'")

    fullPath

CSSかLESSのパスを指定して読み込む。

  applyStylesheet: (path, text, ttype = 'bundled', htmlElement=$('html')) ->
    styleElement = @stylesheetElementForId(@stringToId(path), htmlElement)
    if styleElement.length
      styleElement.text(text)
    else
      if htmlElement.find("head style.#{ttype}").length
        htmlElement.find("head style.#{ttype}:last").after "<style class='#{ttype}' id='#{@stringToId(path)}'>#{text}</style>"
      else
        htmlElement.find("head").append "<style class='#{ttype}' id='#{@stringToId(path)}'>#{text}</style>"
    @emit 'stylesheets-changed'

思ってたより泥臭い系。htmlElement.find("head").append "<style class='#{ttype}' id='#{@stringToId(path)}'>#{text}</style>" がキモかな。headでスタイルシートを展開する。よくあるやつ。

loadBaseStylesheetsに戻って、 @reloadBaseStylesheets() をみてみる

  reloadBaseStylesheets: ->
    @requireStylesheet('../static/atom')
    if nativeStylesheetPath = fs.resolveOnLoadPath(process.platform, ['css', 'less'])
      @requireStylesheet(nativeStylesheetPath)

bootstrap展開後にatomのスタイルシートを展開している。ここで static/atom.less をみてみる

// Import from the syntax theme's variables with a fallback to ./variables/syntax-variables.less
@import "syntax-variables";

// Import from the ui theme's variables with a fallback to ./variables/ui-variables.less
@import "ui-variables";

@import "octicon-utf-codes";
@import "octicon-mixins";

@import "workspace-view";
@import "bootstrap";
@import "buttons";
@import "icons";
@import "links";
@import "panes";
@import "panels";
@import "sections";
@import "overlay";
@import "lists";
@import "popover-list";
@import "notification";
@import "messages";
@import "markdown";
@import "editor";
@import "select-list";
@import "syntax";
@import "utilities";
@import "octicons";

めっちゃ less な感じだった。モジュールで分割されてるので、必要となったら見れば良さそう。

PackageManager

引き続き startEditorWindowから @packages.loadPackages() を追う。

まずコンストラクタから

module.exports =
class PackageManager
  Emitter.includeInto(this)

  constructor: ({configDirPath, devMode, safeMode, @resourcePath}) ->
    @packageDirPaths = []
    unless safeMode
      if devMode
        @packageDirPaths.push(path.join(configDirPath, "dev", "packages"))
      @packageDirPaths.push(path.join(configDirPath, "packages"))

    @loadedPackages = {}
    @activePackages = {}
    @packageStates = {}

    @packageActivators = []
    @registerPackageActivator(this, ['atom', 'textmate'])

safeMode is true ならパッケージを読み込まない。と読める。devModeではdevパッケージを読み込んでいる。
ファイルパス周りをあんまり深追いしたくないので、loadPackages() を調べる。

  loadPackages: ->
    # Ensure atom exports is already in the require cache so the load time
    # of the first package isn't skewed by being the first to require atom
    require '../exports/atom'

    packagePaths = @getAvailablePackagePaths()
    packagePaths = packagePaths.filter (packagePath) => not @isPackageDisabled(path.basename(packagePath))
    packagePaths = _.uniq packagePaths, (packagePath) -> path.basename(packagePath)
    @loadPackage(packagePath) for packagePath in packagePaths
    @emit 'loaded'

srcの外の、 exports/atom.coffee

{Point, Range} = require 'text-buffer'

module.exports =
  BufferedNodeProcess: require '../src/buffered-node-process'
  BufferedProcess: require '../src/buffered-process'
  Git: require '../src/git'
  Point: Point
  Range: Range

# The following classes can't be used from a Task handler and should therefore
# only be exported when not running as a child node process
unless process.env.ATOM_SHELL_INTERNAL_RUN_AS_NODE
  {$, $$, $$$, View} = require '../src/space-pen-extensions'

  module.exports.$ = $
  module.exports.$$ = $$
  module.exports.$$$ = $$$
  module.exports.EditorView = require '../src/editor-view'
  module.exports.ScrollView = require '../src/scroll-view'
  module.exports.SelectListView = require '../src/select-list-view'
  module.exports.Task = require '../src/task'
  module.exports.View = View
  module.exports.WorkspaceView = require '../src/workspace-view'
  module.exports.Workspace = require '../src/workspace'

あれ、ここ副作用あるのかな…

次に、@loadPackage を見る。 本命っぽい

    @loadPackage(packagePath) for packagePath in packagePaths
  loadPackage: (nameOrPath) ->
    if packagePath = @resolvePackagePath(nameOrPath)
      name = path.basename(nameOrPath)
      return pack if pack = @getLoadedPackage(name)

      try
        metadata = Package.loadMetadata(packagePath) ? {}
        if metadata.theme
          pack = new ThemePackage(packagePath, metadata)
        else
          pack = new Package(packagePath, metadata)
        pack.load()
        @loadedPackages[pack.name] = pack
        pack
      catch error
        console.warn "Failed to load package.json '#{path.basename(packagePath)}'", error.stack ? error

    else
      throw new Error("Could not resolve '#{nameOrPath}' to a package path")

ちょっとシェル叩いてみて @getLoadedPackages() してみる。

> atom.packages.getLoadedPackages()
[ThemePackage, ThemePackage, ThemePackage, ThemePackage, ThemePackage, ThemePackage,ThemePackage, Package, Package, ...]

ここにパッケージのインスタンスが格納されてるみたいだ。

        metadata = Package.loadMetadata(packagePath) ? {}
        if metadata.theme
          pack = new ThemePackage(packagePath, metadata)
        else
          pack = new Package(packagePath, metadata)
        pack.load()
        @loadedPackages[pack.name] = pack
        pack

メタデータ探して、それがThemePackageかPackageか判定して、pack.load() する。

Package

Package追ってみる。コンストラクタから。

  constructor: (@path, @metadata) ->
    @metadata ?= Package.loadMetadata(@path)
    @name = @metadata?.name ? path.basename(@path)
    @reset()
  reset: ->
    @stylesheets = []
    @keymaps = []
    @menus = []
    @grammars = []
    @scopedProperties = []

なんかよくわかんないけど、なんらかの仕組みで外に提供されるんだろう。

次に pack.load()

  load: ->
    @measure 'loadTime', =>
      try
        @loadKeymaps()
        @loadMenus()
        @loadStylesheets()
        @grammarsPromise = @loadGrammars()
        @scopedPropertiesPromise = @loadScopedProperties()
        @requireMainModule() unless @hasActivationEvents()

      catch error
        console.warn "Failed to load package named '#{@name}'", error.stack ? error
    this

loadKeymaps

  loadKeymaps: ->
    @keymaps = @getKeymapPaths().map (keymapPath) -> [keymapPath, CSON.readFileSync(keymapPath)]

設定ファイルから読み込んで構築してる。まだアプリ側に提供しているわけじゃなさそうだ。

loadMenus, loadStylesheetsも同上。

grammarsPromise, scopedPropertiesPromise は読み込み時にロード開始されてるけど、activate時にキャッチされてるっぽい。

  activate: ->
    unless @activationDeferred?
      @activationDeferred = Q.defer()
      @measure 'activateTime', =>
        @activateResources()
        if @hasActivationEvents()
          @subscribeToActivationEvents()
        else
          @activateNow()

    Q.all([@grammarsPromise, @scopedPropertiesPromise, @activationDeferred.promise])

loadGrammars

  loadGrammars: ->
    @grammars = []

    loadGrammar = (grammarPath, callback) =>
      atom.syntax.readGrammar grammarPath, (error, grammar) =>
        if error?
          console.warn("Failed to load grammar: #{grammarPath}", error.stack ? error)
        else
          @grammars.push(grammar)
          grammar.activate() if @grammarsActivated
        callback()

    deferred = Q.defer()
    grammarsDirPath = path.join(@path, 'grammars')
    fs.list grammarsDirPath, ['json', 'cson'], (error, grammarPaths=[]) ->
      async.each grammarPaths, loadGrammar, -> deferred.resolve()
    deferred.promise

ここのDeferredの使い方ヘッタクソに見えるが… あとでちゃんとみる。

atom.syntax.readGrammar見たほうがよさそう… と思ったがsrc/syntax.coffee にそんなメソッドはない。じゃあなんだこれ。一旦無視するか。

  loadScopedProperties: ->
    @scopedProperties = []

    loadScopedPropertiesFile = (scopedPropertiesPath, callback) =>
      ScopedProperties.load scopedPropertiesPath, (error, scopedProperties) =>
        if error?
          console.warn("Failed to load scoped properties: #{scopedPropertiesPath}", error.stack ? error)
        else
          @scopedProperties.push(scopedProperties)
          scopedProperties.activate() if @scopedPropertiesActivated
        callback()

    deferred = Q.defer()
    scopedPropertiesDirPath = path.join(@path, 'scoped-properties')
    fs.list scopedPropertiesDirPath, ['json', 'cson'], (error, scopedPropertiesPaths=[]) ->
      async.each scopedPropertiesPaths, loadScopedPropertiesFile, -> deferred.resolve()
    deferred.promise

ScopedPropertiesがなんなのか理解してないが、パッケージ単位でコンテキストが限定された変数か何かかな

  @load: (scopedPropertiesPath, callback) ->
    CSON.readFile scopedPropertiesPath, (error, scopedProperties={}) ->
      if error?
        callback(error)
      else
        callback(null, new ScopedProperties(scopedPropertiesPath, scopedProperties))

そんな気がする。

次、戻って pack.loadに戻って、requireMainModule

  requireMainModule: ->
    return @mainModule if @mainModule?
    mainModulePath = @getMainModulePath()
    @mainModule = require(mainModulePath) if fs.isFileSync(mainModulePath)

多重ロード防止のガードを自分で作ってるようにみえる。

> pack = atom.packages.getLoadedPackages()[10]
> pack.getMainModulePath()
"/Applications/Atom.app/Contents/Resources/app/node_modules/autosave/lib/autosave.js"

autosaveモジュールを引いたっぽい。これがrequireされると

  getMainModulePath: ->
    return @mainModulePath if @resolvedMainModulePath
    @resolvedMainModulePath = true
    mainModulePath =
      if @metadata.main
        path.join(@path, @metadata.main)
      else
        path.join(@path, 'index')
    @mainModulePath = fs.resolveExtension(mainModulePath, ["", _.keys(require.extensions)...])

同じく多重ロード防止。metadataっての、これpackage.jsonかそれ相当のものかな。mainが定義されてあればそのパスを使い、なかったらindex.jsを読む。

これで一応Packageモジュールは終了。 Atom#startEditorWindowに戻る。

@deserializeEditorWindow()

GUIの初期化をしそう。

  deserializeEditorWindow: ->
    @deserializePackageStates()
    @deserializeProject()
    @deserializeWorkspaceView()
  deserializePackageStates: ->
    @packages.packageStates = @state.packageStates ? {}
    delete @state.packageStates

とりあえず初期化フローでは {} かな

  deserializeProject: ->
    Project = require './project'
    @project ?= @deserializers.deserialize(@state.project) ? new Project(path: @getLoadSettings().initialPath)

ここでProjectをnewしている。

Project

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

  # Public: Find the local path for the given repository URL.
  @pathForRepositoryUrl: (repoUrl) ->
    [repoName] = url.parse(repoUrl).path.split('/')[-1..]
    repoName = repoName.replace(/\.git$/, '')
    path.join(atom.config.get('core.projectHome'), repoName)

  constructor: ({path, @buffers}={}) ->
    @buffers ?= []

    for buffer in @buffers
      do (buffer) =>
        buffer.once 'destroyed', => @removeBuffer(buffer)

    @setPath(path)

deserializersにこのクラス自信を登録している。なるほど。
そういえばそろそろModelのコード読んだほうが良いのではないか。頻出なので。

追っていったらここっぽい。

https://github.com/atom/theorist/blob/master/src/model.coffee

さらにemissaryとかdelegatoという自前のモジュールに依存している。これめんどいしやっぱ忘れよう…

pathがプロジェクトルートで、buffersがタブに相当する予感がする。

  # Public: Sets the project's fullpath.
  setPath: (projectPath) ->
    @path = projectPath
    @rootDirectory?.off()

    @destroyRepo()
    if projectPath?
      directory = if fs.isDirectorySync(projectPath) then projectPath else path.dirname(projectPath)
      @rootDirectory = new Directory(directory)
      if @repo = Git.open(projectPath, project: this)
      1
        @repo.refreshIndex()
        @repo.refreshStatus()
    else
      @rootDirectory = null

    @emit "path-changed"
{Directory} = require 'pathwatcher'

このDirectoryというのはatom/node-pathwatcherっぽい

まあたぶんオブザーバーでディレクトリ監視なんだろう。

Git とは

Git = require './git'

冒頭のREADME読むのがよさそう。

# Public: Represents the underlying git operations performed by Atom.
#
# This class shouldn't be instantiated directly but instead by accessing the
# `atom.project` global and calling `getRepo()`. Note that this will only be
# available when the project is backed by a Git repository.
#
# This class handles submodules automatically by taking a `path` argument to many
# of the methods.  This `path` argument will determine which underlying
# repository is used.

実装眺めてみたら git-utils というの依存ているように見える

atom/git-utils

Helpers for working with Git repositories built natively on top of libgit2.

libgit2のラッパーっぽい。

次に

    @emit "path-changed"

の影響範囲を探す。

~/p/atom (remove-unused-requirements) $ git grep path-changed
spec/editor-view-spec.coffee:  describe "editor:path-changed event", ->
spec/editor-view-spec.coffee:      editorView.on 'editor:path-changed', eventHandler
spec/editor-view-spec.coffee:      editorView.on 'editor:path-changed', eventHandler
spec/editor-view-spec.coffee:        editorView.on 'editor:path-changed', eventHandler
spec/git-spec.coffee:      editor.getBuffer().emit 'path-changed'
spec/git-spec.coffee:      editor.getBuffer().emit 'path-changed'
src/editor-view.coffee:    @subscribe @editor, "path-changed", =>
src/editor-view.coffee:      @trigger 'editor:path-changed'
src/editor-view.coffee:    @trigger 'editor:path-changed'
src/editor.coffee:    @subscribe @buffer, "path-changed", =>
src/editor.coffee:      @emit "path-changed"
src/git.coffee:    @subscribe buffer, 'saved reloaded path-changed', =>
src/project.coffee:    @emit "path-changed"
src/tokenized-buffer.coffee:    @subscribe @buffer, "path-changed", => @bufferPath = @buffer.getPath()
src/workspace-view.coffee:    atom.project.on 'path-changed', => @updateTitle()

ワークスペースのタイトルが変わるのはわかるが、他は結構辛そう… 必要になったら調べよう

今ふと色々眺めてみたんだけど、このProjectクラスに生えてるメソッドは拡張とか書いたりするには実際便利そう。タブ生成したり破棄したりできる。

たとえば、今開いてるタブの本文一覧が取りたかったら次のコードでいける。

atom.project.getBuffers().map(function(buffer){return buffer.getLines().join('')})

Workspace

Projectの生成が終わったら、次はWorkspaceの生成。

  deserializeWorkspaceView: ->
    Workspace = require './workspace'
    WorkspaceView = require './workspace-view'
    @workspace = Workspace.deserialize(@state.workspace) ? new Workspace
    @workspaceView = new WorkspaceView(@workspace)
    @keymaps.defaultTarget = @workspaceView[0]
    $(@workspaceViewParentSelector).append(@workspaceView)

WorkspaceとWorkspaceViewをnewしてる。で、@workspaceViewParentSelectorとかいう何かに投げ込んでる

  workspaceViewParentSelector: 'body'

bodyだこれ!

@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