読者です 読者をやめる 読者になる 読者になる

hkoba blog

プログラマーです。プログラミング言語ミーハーです。よろしくどうぞ(能代口調)

Smalltalk の勉強会に参加してきました(ハッカソン編)

smalltalk

smalltalk.connpass.com

私にとって Smalltalk は学生時代から憧れの言語で、にもかかわらず何度挑戦しても (Emacs keybind が使えないがゆえに) 挫折を繰り返していた言語です。何とか突破口を開けられないかと参加させてもらいました。

以下、教えて頂いたこと・調べたことの備忘録です。間違いなど有るかもしれませんのでツッコミ歓迎です。

ハッカソン(午前)

私は Smalltalk 初心者なので、Pharo5 のキーボード・マッピング周りの仕組みを調べて学ぶことにしました。

途中、梅澤さん mumez (Masashi Umezawa) · GitHub に Pharo のショートカット周りのクラスやそのオーバーライド方法、 クラスブラウザの操作方法を色々教えて頂きました。

  • PharoShortcuts current で取り出されるインスタンスを通して、ショートカットが設定されているらしいので、ここをオーバーライドすれば出来そう、とのこと。(ただ、そのために PharoShortcuts クラス自体もいじらなければならない。)
    • この使用箇所を探すとき、クラスブラウザ上でクラスを選んで右クリック→ AnalyzeClass refs... が役に立つことを見せて頂きました。
    • インスタンス変数は大文字はじまり、と。
  • (Web ブラウザの開発ツールに有るような) マウスで画面要素を直接指して中身を Inspect したい時は、 Shift + Mouse MiddleHalo を呼びだせば良いそう。

雑談

お昼ごはんの時にも雑談で色々教えて頂きました。

  • イベントループはどこに有るの? → WorldState にあるよ
  • Pharo にも Object Table は有るの? → ある。 Newspeak, Pharo, SqueakVM (の設計?) が同じ
  • 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 が呼ばれる、のではないか…

github.com

今日たどり着いたのはここまで!

(でもキーマッピングの話はまだ遠いにょ…)

おしまい

これからビールを飲みながら勉強会です。