again 機能

Squeak システムのテキスト編集用キーボードショートカットで、個人的には exchange についで好きなのが again なのですが、これに find の存在意義を脅かしかねないとんでもない機能が隠されているのを発見してしまいました!(和訳:今までとんと知らずに過ごしていました。orz)

ちなみに exchange というのは、編集中のテキストの任意の二箇所を入れ換える機能で、入れ換えたい範囲を続けて選択してから alt/cmd + e とタイプして使用します。たとえば「a, b」のようにカンマで区切られた a と b の入れ換えなど(もちろん a と b は実際にはタイプするのには面倒な程度の長さ)で、カットとペースト作業を何度も繰り返さなくてもよいため、知って使えるようにしておくと重宝します。また、クリップボードの内容を温存できるというメリットもあります。選択範囲を決めるのにポインティングデバイスを使うのなら(つまり、ホームポジションから手を離すのなら、その不便を補うのに十分なメリットを産むために)、ないのは惜しい機能のひとつです。


他方で again(の私の知っていた機能…(^_^;))は、選択範囲を書き換えたあとで、alt/cmd + j とすると、同じ書き換えを繰り返してくれる便利な機能です。Mac や Win では Find/Replace の Replace 機能に相当するものですが、ダイアログ表示を介した対話を必要としないところがちょっと違います。ちなみに、alt/cmd + shift + J とすると挿入ポインタ以降のテキストを対象に全置換となります。


この again を使っていると、たまに何かのタイミングでうまく動作しないことがあります(期待した動作は起きずにただ画面がフラッシュしておしまいになる)。その原因を突き止めようと、この機能をつかさどるメソッドをつらつらと眺めていたところ、謎(?)の挙動を記述した条件分岐に遭遇したわけです。

ParagraphEditor >> againOrSame: useOldKeys many: many
   "Subroutine of search: and again.  If useOldKeys, use same FindText and 
   ChangeText as before.  If many is true, do it repeatedly.  Created 1/26/96
   sw by adding the many argument to #againOrSame."

   | home indices wasTypedKey |

   home := self selectionInterval.  "what was selected when 'again' was invoked"

   "If new keys are to be picked..."
   useOldKeys ifFalse: "Choose as FindText..."
      [FindText := UndoSelection.  "... the last thing replaced."
      "If the last command was in another paragraph, ChangeText is set..."
      paragraph == UndoParagraph ifTrue: "... else set it now as follows."
         [UndoInterval ~= home ifTrue: [self selectInterval: UndoInterval]. "blink"
         ChangeText := ((UndoMessage sends: #undoCutCopy:) and: [self hasSelection])
            ifTrue: [FindText] "== objects signal no model-locking by 'undo copy'"
            ifFalse: [self selection]]]. "otherwise, change text is last-replaced text"

   (wasTypedKey := FindText size = 0)
      ifTrue: "just inserted at a caret"
         [home := self selectionInterval.
         self replaceSelectionWith: self nullText.  "delete search key..."
         FindText := ChangeText] "... and search for it, without replacing"
      ifFalse: "Show where the search will start"
         [home last = self selectionInterval last ifFalse:
            [self selectInterval: home]].

   "Find and Change, recording start indices in the array"
   indices := WriteStream on: (Array new: 20). "an array to store change locs"
   [(self againOnce: indices) & many] whileTrue. "<-- this does the work"
   indices isEmpty ifTrue:  "none found"
      [self flash.
      wasTypedKey ifFalse: [^self]].

   (many | wasTypedKey) ifFalse: "after undo, select this replacement"
      [home := self startIndex to:
         self startIndex + UndoSelection size - 1].

   self undoer: #undoAgain:andReselect:typedKey: with: indices contents with: home with: wasTypedKey

delete search key...? なんじゃそりゃ。…ということで、選択文字を置き換えずに、たんに文字を挿入しただけの状態で again してみる…と、いま入力したばかりのテキストが消え(self replaceSelectionWith: self nullText)て、それを検索テキストにした検索が行なわれたではあ〜りませんか。ガーン!

つまるところ、again は、置換だけでなく、新しい検索テキストによる“モードレス”検索も可能だったのですね。いやぁ、ひさびさに Smalltalk システムの変態 UI に驚かされた蒸し暑い夜のひとコマでありました。


追記:
exchange や again など、テキストエディタで使える(というか、Smalltalk にはアプリケーションという概念は希薄なので、文字を入力できる場所ならどこでも使える)キーボードショートカットの一覧は デスクトップメニュー → help... → command-key help で一覧することができます。簡単な説明しかないので、具体的に何をする機能かはやはりその動作を定義したメソッドのソースを読むのが一番です。^^;

なお、Mac OS X では一部のキーコンビネーション(メソッド名補完機能の cmd + q や、選択部分の検索文字列指定の cmd + h)が OS に奪われてしまうことがあります。そんなときは使いたい機能を cmd + r など今は使われていない(あるいは自分に不要な機能の)キーコンビネーションに割り振り直すとよいでしょう。たとえば、この追記の時点のバージョンである Squeak4.3 であれば、SmalltalkEditor や TextEditor にある #initializeCmdKeyShortcuts や #initializeShiftCmdKeyShortcuts で、該当キーの文字と、機能を記述したメソッドのセレクタ(シンボル)との組み合わせを入れ替えたりペアを追加した後に accept (cmd + s) し、仕上げに「SmalltalkEditor initialize」を do it (cmd + d) すればその直後から新しいキーコンビネーションでお望みの機能が使えるようになります。