Smalltalk の勉強会に参加してきました(ハッカソン編)
私にとって Smalltalk は学生時代から憧れの言語で、にもかかわらず何度挑戦しても (Emacs keybind が使えないがゆえに) 挫折を繰り返していた言語です。何とか突破口を開けられないかと参加させてもらいました。
以下、教えて頂いたこと・調べたことの備忘録です。間違いなど有るかもしれませんのでツッコミ歓迎です。
ハッカソン(午前)
私は Smalltalk 初心者なので、Pharo5 のキーボード・マッピング周りの仕組みを調べて学ぶことにしました。
途中、梅澤さん mumez (Masashi Umezawa) · GitHub に Pharo のショートカット周りのクラスやそのオーバーライド方法、 クラスブラウザの操作方法を色々教えて頂きました。
PharoShortcuts current
で取り出されるインスタンスを通して、ショートカットが設定されているらしいので、ここをオーバーライドすれば出来そう、とのこと。(ただ、そのために PharoShortcuts クラス自体もいじらなければならない。)- この使用箇所を探すとき、クラスブラウザ上でクラスを選んで右クリック→
Analyze
→Class refs...
が役に立つことを見せて頂きました。 - インスタンス変数は大文字はじまり、と。
- この使用箇所を探すとき、クラスブラウザ上でクラスを選んで右クリック→
- (Web ブラウザの開発ツールに有るような) マウスで画面要素を直接指して中身を Inspect したい時は、
Shift + Mouse Middle
で Halo を呼びだせば良いそう。
雑談
お昼ごはんの時にも雑談で色々教えて頂きました。
- イベントループはどこに有るの? →
WorldState
にあるよ - Pharo にも Object Table は有るの? → ある。 Newspeak, Pharo, Squeak は VM (の設計?) が同じ
allInstance
の逆はあるの? →allOwners
というのがある。(Morph のみ?)- バージョン管理機能があると聞いたけど、 CompiledMethod はイメージ内に複数持つの?一つだけ? → 一つだけ。
あと、皆さん pharo を一番使ってたのが印象的でした。 pharo のコア開発者はフランス inria 繋がりの人が多いとか…
<keymap>
は Pragma だった!
以前の挫折ポイントの一つが、この↓ <keymap>
TxTextEditorMorph>>#buildTextEditorKeymapsOn: aBuilder <keymap> { Character home. #moveToLineStart. Character home shift. #selectToLineStart. ...
これは Pharo の Pragma 構文で、所謂 コード注記 と同じ役割を果たしているそうです。 (メソッドに属性としてぶら下がる)。 Pharo は Pragma を色々活用しているとのこと。
ハッカソン (午後)
ところが PharoShortcuts
の中身をよく見たところ、肝心のカーソル移動が定義されていないことに気づきました。
これでは Emacs 風のカーソル移動は定義できません。
そこで方針を変えて、そもそもキーマップ機能と、イベントループ周りがどんな仕組みになっているのかを調べることにしました。
キーマップ関連
TxTextEditorMorph>>#initializeShortcuts: aKMDispatcher aKMDispatcher attachCategory: #TxTextEditorMorph
KMDispatcher というのが関係が深そう…
クラスのコメントを順々に抜き書きしてみます。
KMDispatch
I'm an object that saves a buffer of keyevents for the morph I'm attached. I am the one that dispatches the single and multiple shortcuts. If the morph has a keymap that matches the keyboard event, I tell the keymap event to execute with the morph I'm attached.
KMRepository
I have a singleton instance which can be accessed by executing the following: "self default" I am currently a god object to be refactored =D.
KMRepository>>#reset World setProperty: #kmDispatcher toValue: nil. self default: self new. KMCategory allSubclasses select: [ :c | c is GlobalCategory ] thenDo: [ :c | c new installAsGlobalCategory ]. KMPragmaKeymapBuilder uniqueInstance reset.
KMPragmaKeymapBuilder
I am a singleton object, subscribed to system events, to listen to the creation of methods marked with the <keymap> and keymap:> pragmas. When I listen one of those events, I reinitialize the KMRepository default instance and reload it with all declared keymaps.
う〜ん、らちがあかない…
イベントループ
下から追うのが難しそうなので、今度はイベントループを調べてみます。
教えて頂いた WorldState
と、グローバル変数 World
(こちらは WorldMorph
のインスタンス) が関係が深そう。
WorldMorph>>#doOneCycle worldState doOneCycleFor: self. WorldState>>#doOneCycleFor: aWorld self interCyclePause: MinCycleLapse. self doOneCycleNowFor: aWorld.
WorldState>>#doOneCycleNowFor: aWorld "Immediately do one cycle of the interaction loop. This should not be called directly, but only via doOneCycleFor:" DisplayScreen checkForNewScreenSize. "process user input events" LastCycleTime := Time millisecondClockValue. self handsDo: [:h | ActiveHand := h. h processEvents. "…ここが肝ぽい…" ActiveHand := nil ]. "the default is the primary hand" ActiveHand := self hands first. aWorld runStepMethods. "there are currently some variations here" self displayWorldSafely: aWorld.
displayWorldSafely: aWorld "Update this world's display and keep track of errors during draw methods." [aWorld displayWorld] ifError: [:err :rcvr | "Handle a drawing error" | errCtx errMorph | errCtx := thisContext. [ errCtx := errCtx sender. .... "If the morph causing the problem has already the #drawError flag set, then search for the next morph above in the caller chain." errMorph hasProperty: #errorOnDraw ] whileTrue. errMorph setProperty: #errorOnDraw toValue: true. "Install the old error handler, so we can re-raise the error" rcvr error: err. ].
↑今読み返すと WorldState>>#doOneCycleNowFor: aWorld
の中の ActiveHand processEvent
が鍵ぽいのですが、
この時点では読み取れませんでした。
キー入力周り
再び下から、でも目線を変えてキー入力に関係するコードを探してみようと考えました。
Smalltalk なので、メタキーを扱うコードが有るはず… #meta
の sender を見る…
→ SimulateKeystrokesSpecification>>#testSimulateCmdKeystroke
の中に self simulateKeyStrokes: ...
というコードを発見。
Morph>>#simulateKeyStroke: aCharacter |event| event := KeyboardEvent new setType: #keystroke buttons: 0 position: 0@0 keyValue: aCharacter charCode charCode: aCharacter charCode hand: ActiveHand stamp: 0. self keyStroke: event
KeyboardEvent
クラス! この Class ref から、 HandMorph>>#generateKeyboardEvent
を見つけました。
後はこれの sender を探すのみ。
HandMorph>>#processEvents "Process user input events from the local input devices." | evt evtBuf type hadAny | [(evtBuf := Sensor nextEvent) isNil] whileFalse: [evt := nil. "for unknown event types" type := evtBuf first. type = EventTypeMouse ifTrue: [recentModifiers := evtBuf sixth. evt := self generateMouseEvent: evtBuf]. type = EventTypeKeyboard ifTrue: [recentModifiers := evtBuf fifth. evt := self generateKeyboardEvent: evtBuf]. type = EventTypeDragDropFiles ifTrue: [evt := self generateDropFilesEvent: evtBuf]. type = EventTypeWindow ifTrue: [evt := self generateWindowEvent: evtBuf]. "All other events are ignored" (type ~= EventTypeDragDropFiles and: [evt isNil]) ifTrue: [^self]. evt isNil ifFalse: ["Finally, handle it" self handleEvent: evt. "多分ここで、 HandMorph>>#handleEvent:evt が呼ばれる" hadAny := true. ...]]. ...
そして HandMorph>>#handleEvent:anEvent
が呼ばれる、のではないか…
今日たどり着いたのはここまで!
(でもキーマッピングの話はまだ遠いにょ…)
おしまい
これからビールを飲みながら勉強会です。