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
restoreWindowDimensions() => getDefaultWindowDimensions() とみてたらlocalStorageを触ってる部分があった
コンソールで中身をみてみる
atom.keymaps => KeymapManager で別ライブラリ。あとで。
atom.contextMenu: ContextMenuManager => src/context-menu-manager.coffee
外部パッケージ化されてないし、右クリックのときの挙動管理だろうから、ここを調べてみる。
API見た感じ、 atom.contextMenu に直接 add してるように見えるし、atom.contextMenuでgrepしてみると、次の行がでてくる
各シーンやコンテキストに合わせて右クリックのアイテム一覧が再構築されるっぽい。
あとwidow-event-handler
ついでにこれ気になった。a タグの挙動のオーバーライド。
shellとは?
atom-shell ではないんだけど、なんだろうこれ
つまり location.href は基本的に変わらないものだと思ってよさそう。
ThemeManager
脱線したけど、次に ThemeManager#loadBaseStylesheets() を追う。名前通りのことをしていると思う。
loadBaseStylesheets()
CSSかLESSのパスを指定して読み込む。
思ってたより泥臭い系。
htmlElement.find("head").append "<style class='#{ttype}' id='#{@stringToId(path)}'>#{text}</style>"
がキモかな。headでスタイルシートを展開する。よくあるやつ。loadBaseStylesheetsに戻って、 @reloadBaseStylesheets() をみてみる
bootstrap展開後にatomのスタイルシートを展開している。ここで static/atom.less をみてみる
めっちゃ less な感じだった。モジュールで分割されてるので、必要となったら見れば良さそう。
PackageManager
引き続き startEditorWindowから @packages.loadPackages() を追う。
まずコンストラクタから
safeMode is true ならパッケージを読み込まない。と読める。devModeではdevパッケージを読み込んでいる。
ファイルパス周りをあんまり深追いしたくないので、loadPackages() を調べる。
srcの外の、 exports/atom.coffee
あれ、ここ副作用あるのかな…
次に、@loadPackage を見る。 本命っぽい
ちょっとシェル叩いてみて @getLoadedPackages() してみる。
ここにパッケージのインスタンスが格納されてるみたいだ。
メタデータ探して、それがThemePackageかPackageか判定して、pack.load() する。
Package
Package追ってみる。コンストラクタから。
なんかよくわかんないけど、なんらかの仕組みで外に提供されるんだろう。
次に pack.load()
loadKeymaps
設定ファイルから読み込んで構築してる。まだアプリ側に提供しているわけじゃなさそうだ。
loadMenus, loadStylesheetsも同上。
grammarsPromise, scopedPropertiesPromise は読み込み時にロード開始されてるけど、activate時にキャッチされてるっぽい。
loadGrammars
ここのDeferredの使い方ヘッタクソに見えるが… あとでちゃんとみる。
atom.syntax.readGrammar見たほうがよさそう… と思ったがsrc/syntax.coffee にそんなメソッドはない。じゃあなんだこれ。一旦無視するか。
ScopedPropertiesがなんなのか理解してないが、パッケージ単位でコンテキストが限定された変数か何かかな
そんな気がする。
次、戻って pack.loadに戻って、requireMainModule
多重ロード防止のガードを自分で作ってるようにみえる。
autosaveモジュールを引いたっぽい。これがrequireされると
同じく多重ロード防止。metadataっての、これpackage.jsonかそれ相当のものかな。mainが定義されてあればそのパスを使い、なかったらindex.jsを読む。
これで一応Packageモジュールは終了。 Atom#startEditorWindowに戻る。
@deserializeEditorWindow()
GUIの初期化をしそう。
とりあえず初期化フローでは {} かな
ここでProjectをnewしている。
Project
deserializersにこのクラス自信を登録している。なるほど。
そういえばそろそろModelのコード読んだほうが良いのではないか。頻出なので。
追っていったらここっぽい。
https://github.com/atom/theorist/blob/master/src/model.coffee
さらにemissaryとかdelegatoという自前のモジュールに依存している。これめんどいしやっぱ忘れよう…
pathがプロジェクトルートで、buffersがタブに相当する予感がする。
このDirectoryというのはatom/node-pathwatcherっぽい
まあたぶんオブザーバーでディレクトリ監視なんだろう。
Git とは
冒頭のREADME読むのがよさそう。
実装眺めてみたら
git-utils
というの依存ているように見えるatom/git-utils
libgit2のラッパーっぽい。
次に
の影響範囲を探す。
ワークスペースのタイトルが変わるのはわかるが、他は結構辛そう… 必要になったら調べよう
今ふと色々眺めてみたんだけど、このProjectクラスに生えてるメソッドは拡張とか書いたりするには実際便利そう。タブ生成したり破棄したりできる。
たとえば、今開いてるタブの本文一覧が取りたかったら次のコードでいける。
Workspace
Projectの生成が終わったら、次はWorkspaceの生成。
WorkspaceとWorkspaceViewをnewしてる。で、@workspaceViewParentSelectorとかいう何かに投げ込んでる
bodyだこれ!