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
ビルド
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
走らせてみたなんかぼちぼちこける。やたらCPU負荷が高いと思ったら、async.jsでテストが並列実行されてた
L111
こいつをコメントアウトすると何もしない、ウィンドウすら生成しない
finish-launching で grep してみる
あれーどこからこのメッセージくるんだ。あとで調べる。
webviewとしてのエントリポイントは static/index.htmlっぽい
src/browser/atom-window.coffee L22~
static/index.html
static/index.js
ipcってのがブラウザタブと本体アプリが通信するもののようにみえる。
ipc.sendChannel('window-command', 'window:loaded')
で各種初期化処理が走りそうipcは追っていったらatom-shell側に定義してある。atom/atom では完結しなさそうなので一旦諦めるとして、window:loaded を追う
アプリケーションの全体初期化と、個別のウィンドウの初期化かな?
atom-window.coffee L42~
atom-application.coffee L96
初期化終わったのでアップデートマネージャに割り込んでもいいよ的なメッセージを投げてる気配。たぶんこっから追ってもどうしようもなさそうなので、別の方いこう。
経路としては index.js => atom-window => atom-application っぽい。オブザーバーお化けってほどじゃないけど、それなりに複雑。