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
window-bootstrap.coffee のinitialize~startEditorWindow 付近を追う。
両方共コメントアウトしてみる => クラッシュ
atom.initialize() だけ呼んでみる。 => Check for update とかのメニューだけ出現し、ウィンドウはまだ生成されず。
ここで気づいたんだけど、今までは
src/browser
の中の処理を追ってたけど、ここからsrc/
が相対パスになるっぽい。browserという言葉に惑わされてたけど、想定していたのと逆だった。というわけで やっと
src/atom.coffee
を読むことになる。atom.coffee
冒頭に便利っぽいコメントを見つけた。
シェルに入ると確かにこれらの変数がある。atom.clipboardは挙動の予想がつくので触ってみる。
確かにまあ動く感じ。
atom.config
ん、~/.atom に設定ファイルがある?
なるほど把握。
そろそろAtomに戻ってコンストラクタから順番に処理を追う。
L124
DeserializerManager を追う
何のデシリアライザなのかこれだけじゃわからんのでインスタンスをみてみる
ウッこれただのグローバル変数じゃ…
まだこれが何なのか判断しかねるので次へ
initializeをみる
ここでrequireのパス周りに手を加えているので追ってみよう。
getLoadSettingsは何をしているのだろう。
シェルで確認してみる
location.searchにconfigが埋め込まれてたっぽい。気が向いたらbrowser側で何をやってるか追っても良い。
resourcePathのディレクトリ覗いてみる
えっと、これつまり node_modulesが存在するからそこからもrequireできるってことかな…
apmで入れるパッケージっぽい。自分はまだ何も入れてないので、こんな感じか。
これを念頭に次のコードを追う
src/window-event-handler.coffee
windowのeventや、親windowから渡されてくるイベントを購読しまくって横流ししてる
src/editor.coffee
編集領域の実装。Cursorやundo/redo制御。長い。EditorViewにインスタンス化されるモデル。
Atom#startEditorWindow の続きを読む。
ここでやっと画面表示してるっぽい
resourcePathがなんなのかシェルで実行してみる
dispalyWindow()
なんか描画の都合でsetImmediateでずらしてる模様。たしかにこういうコードよく書く。糞だけど。
show()
atom-shell側にウィンド表示しろっていう命令出してる。
atom/browser/atom-application.coffee L218
送られてきたウィンドウを識別して(今気づいたけど同じプロセス上に複数のウィンドウが存在するみたいだ)、このケースだとwin.show()を読んでる。
focus() はたぶん俺にフォーカスしろ!っていう命令。あとはフルスクリーン化フラグがあればフルスクリーン化。
ここまで一連の初期化処理は処理は終了っぽい。アプリケーションのコアドメインはstartEditorWindowに集中しているようにみえる。次はそこを調べる。