Smalltalk環境におけるモードレスの真骨頂である“again”の進化を調べてみた

#“again”とは

　againは、Smalltalk環境のテキスト編集をモードレスにするために組み込まれた cut、copy、paste、undo に続いて導入された、ダイアログボックス必要としない検索＆置換機能です。

参考： [Smalltalk again #02 - find and replace (動画)](https://www.youtube.com/watch?v=tUZI1iUAPu0)

　ラリー・テスラーによって発明された“コピー＆ペースト”～すなわち「対象を選択して操作する」～に象徴されるモードレスな編集スタイルは、ゼロックス・パロアルト研究所で開発された世界初のWYSIWYGエディタであるBravoのUI部分をテスラーが改造することによって作られたGypsyエディタで試され、その後彼が参加したアラン・ケイらの「暫定的ダイナブック環境」すなわちSmalltalk環境を整備する過程で結実したものです。この「選択→操作」のモードレス編集スタイルはさらに、アップル社のLisaやMacintosh（旧世代Mac）をはじめ、それに続く多くの WIMP（マルチウインドウのサポート、アイコンベースのファイラ、メニューによる操作、ポインティングデバイス採用）な GUI を備えた OS で採用され、現在主流となっています。

　1979年に値上がり必至のアップル社公開予定株に絡むゼロックス本社との「駆け引き」により、ケイやテスラーたちが開発中だったSmalltalk-76の「本気のデモ」を見ることができたスティーブ・ジョブズらアップル社のエンジニアたちは（もとより、ラリー・テスラー本人がその後、同社に移り、LisaやMacintoshの開発に参加するようになったこともあり）積極的に、同環境のモードレスネスを彼らの新製品にも取り込みました。

　ところがそこから漏れてしまったのがこの“again”です。なお、LisaやMacのテキスト編集機能を持つアプリケーションにおいて、検索や置換はダイアログボックスを用いたモーダルでオーソドックスな方法が採用されています。

#again登場の時期

　BitBLT（Bit Block Transfer）と呼ばれる、コンパクトで強力な描画ルーチンがダン・インガルスらにより発明され組み込まれた（当初はSmalltalk自体に。その後、プラットフォーム・ハードウエアである「アルト」のマイクロコードで記述された１つのCPU命令として─）ことにより、Smalltalk-72のGUI処理能力は飛躍的に向上し、特にポップアップメニューを瞬時に表示し、さらにメニューアイテムを選択後ただちに元の画面を復元する操作が可能になると、本格的なGUIが整備されたSmalltalk-74が開発されました。

<img src=http://squab.no-ip.com/collab/uploads/61/smalltalk-72-simple.jpg width=320><br/><i>Smalltalk-72の初期のGUI</i>

<img src=http://squab.no-ip.com/collab/uploads/61/smalltalk-72-1977.jpg width=320><br/><i>Smalltalk-74（BitBLTによる高速描画版Smalltalk-72 [クリックで拡大]）</i>

　ただ、ここに登場するポップアップメニューを見る限り、undo/copy/paste/cut はありますが 肝心のagainはどうやら見当たりません。againはこれらより少し後のSmalltalk-76からの追加であることが伺えます。

<img src=https://qiita-image-store.s3.amazonaws.com/0/49275/80f74ebf-562d-5256-1a19-a7deb07f40a0.png width=480><br/><i>コンテクストメニューに登場したagain（<a href=https://www.youtube.com/watch?v=aqW6Sp279Z0&t=17m20s>Smalltalkの変遷を記録した当時の映像のSmalltalk-76部分～17:30あたりから～</a>より）</i>


#againの実装の変遷

　Smalltalk-76、Smalltalk-80 v0、Apple Smalltalk（リリース前のバージョンであるSmalltalk-80 v1を独自に拡張）、Smalltalk-80 v2（リリース・バージョン）のソースコードがネット経由などで入手できたので、againの実装であるagainメソッドとagainOnce等の関連メソッドのコードを眺めてみましょう。

##Smalltlak-76のagain

　Smalltalk-76は、非同期ながら本当にメッセージを送っていたSmalltalk-72と違い、現在のSmalltalk同様にメソッド（クラス内関数）の動的呼び出しによりメッセージ送信を代替させることで高速な処理を優先した機構（コールするメソッドがないときにだけメッセージをハンドリングできるようになる）が採用されたSmalltalkの第二世代目の処理系です。

　Smalltalk-80（つまり、それ以降あまり変化のない今のSmalltalk）との大きな違いのひとつは、特定のレシーバーを省略することでALGOLライク（今でいえばCライク）なループ構文を特殊なメソッド（ここではwhile▷do▷メソッドが登場）により可能にしていることです（なお、この機構はSmalltalk-80では廃止されました）。また、このwhile▷do▷にもあるように、コロン（:）の代わりにオープンコロン（ここでは▷で代用）に続く引数は評価されずトークンのまま渡されたり、それに関連してブロックが第一級オブジェクトではないこと、また代入記号（←）をメソッド名に含めることができるなど細かいところでいろいろと今のSmalltalkとは異なります。条件分岐は前述のとおり、ALGOLライクなif-then-elseもif:then▷else▷メソッドによりシミュレートできますが、通常はSmalltalk-72寄りの`真偽値 ⇒ [真時の処理] 偽時の処理`という構文を用います。この構文は、偽時の処理に同構文を重ねることでswitch-case的な処理もスマートに書くことが可能です。ただそのために、真時処理はもちろん偽時処理にも複数式を記述できるようになっているので、メソッド内で逐次実行される別の四季と分離するには、（前後しますが、下の`again`メソッドの３行目にあるように…）ブロックで処理の塊を括る必要があります。

　次がSmalltalk-76のagainの実装です。見ての通り、機能面もコードも非常にシンプルです。

```:Smalltalk-76
"ParagraphEditor"
Class new title: 'ParagraphEditor'
	subclassof: TextImage
	fields: 'oldEntity sel'
	declare: 'on off ';
	asFollows▟

Editing

again  | many
	[many ← user leftShiftKey.
	[self fintype ⇒ [Scrap ← Scrap text. self select]].
	many ⇒ [while▷ self againOnce do▷ []]
	self againOnce ⇒ [] frame flash]

againOnce  | t
	[t ← para findString: Deletion startingAt: c2.
	t=0 ⇒ [⇑false]
	self unselect.
	c1 ← t.
	c2 ← c1 + Deletion length.
	self replace: Scrap; selectAndScroll]


Class new title: 'TextImage'
	subclassof: Textframe
	fields: 'c1 c2 begintypein superimage figure ground'
	declare: 'cut esc paste aurora Scrap scrap bs Deletion aurorarunning paragraphmenu ctlw ';
	asFollows▟

EDITING

fintype
	[begintypein ⇒
		[	[begintypein < c1 ⇒
				[Scrap ← para copy: begintypein to: c1-1.
				c1 ← begintypein]].
		begintypein ← false]
	⇑false]
```

　`many`フラグはagainを繰り返し適用するかを決めるもので、againが実行された際に左側のシフトキーが押されているかどうかで判断されます。

　`self fintype` は文字入力の最中ならそれを終わらせて、そこまで入力した文字列をパラグラフとして`Scrap`、つまりクリップボードに相当する大域変数に代入します。今のSmalltalkと違い、Smalltalk-76では`false`以外は真なので、文字入力の最中（もしくは直後）、つまり真時であれば、続くブロック内が評価されます（`Scrap`にそれにすでに代入されているパラグラフの参照するテキストオブジェクトが再代入後、該当範囲が選択`self select`される）。

　あとの処理は字面どおりで難しくないと思います。`many`フラグが立っていればテキスト最後まで`self againOnce`を`false`を返すまで繰り返し、そうでなければ`againOnce`を１度だけコールしてその戻り値が`false`、つまり同様の置換を続くテキスト内で行なうことができなければ、ウインドウの作業中の枠内を反転させてそれを知らせます。

　`againOnce`では、直前の置き換え操作で削除されたテキストである`Deletion`を探し、見つからなければ`false`を返し、見つかれば`Scrap`と置き換えています。

##Smalltalk-80 v0のagain

　Smalltalk-80 v0は、Smalltalk-76からSmalltalk-80への過渡期の処理系と思われます。レシーバーの省略構文や条件分岐構文が廃止され、すべてメッセージングで統一した文法に変化しています。後の`whileTrue:`が`whileTrueDo:`なのが興味深いですね。

```:Smalltalk-80&nbsp;v0
"239" TextImage$'Editing'
[again | many |
	many ← user leftShiftKey.
	self fintype
	  ifTrue:
		[Scrap ← Scrap text.
		self select].
	many
	  ifTrue: [[self againOnce] whileTrueDo: []]
	  ifFalse:
		[self againOnce
		  ifFalse: [frame flash]]].

"513" TextImage$'SELECTION'
[againOnce | t |
	t ← para findString: Deletion startingAt: c2.
	t = 0
	  ifTrue: [↑false].
	self unselect.
	c1 ← t.
	c2 ← c1 + Deletion length.
	self replace: Scrap.
	self selectAndScroll].
```

　内容は、Smalltalk-80に書き換えられいるだけで、Smalltalk-76のときとはまったく変わらないようです。

##Apple Smalltalkのagain

　Apple Smalltalkは、Smalltalkの普及のためゼロックスが、テクトロニクス社、アップル社、ヒューレット・パッカード社、ディジタル・イクイップメント社に仮想マシンの開発技術とテスト用の仮想イメージを提供するかたちで、各社の製造するコンピューターに移植されたSmalltalk-80実装（リリース前のv1ベース）のうちのアップル社の実装です。当初Lisa向けに開発され、後にMacintoshにも移植されさらに開発が続けられました。アップル社の開発者向けサービスであるAPDAを通じて100ドル程度と比較的安価で販売されていたこともあり、当時の代理店であるゼロワンショップの店頭などで触ったり、自ら購入して入手した方もいらっしゃるかと思います。

　Macintosh Toolboxへの対応の他、ゼロックスからの移籍組のラリー・テスラーをはじめ、Smalltalkの三世代（-72、-76、-80）の実装を手がけたダン・インガルスや、オブジェクト指向仮想メモリやラジオボタンの発明者でもあるテッド・ケーラー、もちろんアラン・ケイも含めたSmalltalk開発のオリジナルメンバーらによる拡張がなされた点でも他の実装と一線を画す存在です。テクトロニクス社などはv2のライセンスを受けて販売を開始しましたが、アップルはv1のまま止まり、自社製品としてSmalltalk-80 v2を開発・販売することはありませんでした。

参考： [What's the story behind Macintosh Smalltalk?](https://www.quora.com/Whats-the-story-behind-Macintosh-Smalltalk)

　また、このApple Smalltalkは時を経て、後のSqueakのベースとなったことでも知られています。

参考： [Squeak Introduction Page!](http://www.mars.dti.ne.jp/~umejava/smalltalk/squeak/index.html)

　この実装ではこれまでの`againOnce`が大幅に加筆され、`againOrSame:` というメソッドに切り出されています。大きな違いは、クリップボードである`Scrap`を流用した安易な実装から、それを汚さない改良がなされたことでしょう。

```Smalltalk:Apple&nbsp;Smalltalk&nbsp;(Smalltalk-80&nbsp;v1ベース)
ScrollController subclass: #ParagraphEditor
	  instanceVariableNames: 'paragraph startBlock stopBlock beginTypeInBlock emphasisHere initialText selectionShowing otherInterval '
	  classVariableNames: 'TextEditorYellowButtonMessages CurrentSelection Keyboard Undone UndoMessage UndoInterval UndoParagraph ChangeText TextEditorYellowButtonMenu FindText UndoSelection '
	  poolDictionaries: 'TextConstants '
	  category: 'Graphics-Editors'!
ParagraphEditor comment:
'I am a Controller for editing a Paragraph. I am a kind of ScrollController, so that 
more text can be created for the Paragraph than can be viewed on the screen. 
Editing messages are sent by issuing commands from a yellow button menu or from 
keys on the keyboard. My instances keep control as long as the cursor is within the 
view when the red or yellow mouse button is pressed; they give up control if the 
blue button is pressed.'!

!ParagraphEditor methodsFor: 'menu messages'!

again
	"Text substitution. If the left shift key is down, the substitution is made
	throughout the entire Paragraph. Otherwise, only the next possible
	substitution is made.
	Undoer & Redoer: #undoAgain:andReselect:typedKey:."

	"If last command was also 'again', use same keys as before"
	self againOrSame: (UndoMessage sends: #undoAgain:andReselect:typedKey:)!

!ParagraphEditor methodsFor: 'private'!

againOnce: indices
	"Find the next occurrence of FindText.  If none, answer false.
	Append the start index of the occurrence to the stream indices, and, if
	ChangeText is not the same object as FindText, replace the occurrence by it."

	| where |
	where ← paragraph text findString: FindText startingAt: stopBlock stringIndex.
	where = 0 ifTrue: [↑false].
	self deselect; selectInvisiblyFrom: where to: where + FindText size - 1.
	ChangeText ~~ FindText ifTrue: [self zapSelectionWith: ChangeText].
	indices nextPut: where.
	self selectAndScroll.
	↑true!

againOrSame: useOldKeys
	"Subroutine of search: and again.  If useOldKeys, use same FindText and ChangeText
	as before."

	| many home indices wasTypedKey |
	many ← sensor leftShiftDown.
	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: [startBlock ~= stopBlock])
				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 ← startBlock stringIndex to:
			startBlock stringIndex + UndoSelection size - 1].

	self undoer: #undoAgain:andReselect:typedKey: with: indices contents with: home with: wasTypedKey!
```
　もう一つ別の大きなポイントは`"just inserted at a caret"`の部分のインプレイス検索に対応した拡張です。キャレットを任意の場所に移動して、おもむろに検索したい文字列をタイプし、againを実行すると、キャレット以降にある当該文字列を検索してハイライトする機能です。検索に用いるために挿入した文字列は自動的に削除されます。

<img src=https://qiita-image-store.s3.amazonaws.com/0/49275/62dfb7c1-1c30-b3b1-bcf7-aeaf9aced3cc.png width=480</img><br/><i>Apple Smalltalkのagainコマンド拡張に関する記述（マニュアルより抜粋）</i>

参考： [Smalltalk again #01 - in place find (動画)](https://www.youtube.com/watch?v=-jKdn_9dKeQ)

　againの特にこのインプレイス検索は、一度慣れてしまったら最後、それまで当たり前に思ってあきらめていたダイアログボックスを開くオーソドックスな方式に対し、強い違和感を感じるようになってしまうほどの中毒性があり非常に危険な存在です。^^;

#Smalltalk-80 v2

　ゼロックスからSmalltalkの開発と販売のために独立したParcPlace SystemsよりリリースされたSmalltalk-80です。Apple Smalltalkでagainに施された拡張はこれにはみられません。

```Smalltalk:Smalltalk-80&nbsp;v2
ScrollController subclass: #ParagraphEditor
	  instanceVariableNames: 'paragraph startBlock stopBlock beginTypeInBlock emphasisHere initialText selectionShowing '
	  classVariableNames: 'CurrentSelection Keyboard TextEditorYellowButtonMenu TextEditorYellowButtonMessages UndoSelection '
	  poolDictionaries: 'TextConstants '
	  category: 'Interface-Text'!
ParagraphEditor comment:
'I contain the main handling of text editing.  I ought to be used only on smallish paragraphs.

Instance Variables

	  paragraph
<Paragraph>  A pointer to the textForm being edited.  The Paragraph is 
optimized for managing replacements in it*s stylizedString and for scrolling.

	  startBlock
	  stopBlock
<CharacterBlock>  These variables contain the string indices, characters, 
and bounding boxes of the starting and stopping characters in the stylizedString 
which will be operated upon by the next command.

	  beginTypeInBlock
<CharacterBlock>  Used to manage typing and to distinguish selection arising 
from the last characters typed in from selection with the mouse or which is 
the result of some special keystroke.

	  initialText
<Text>  A copy of the string held by the Paragraph at the time the text editor 
was instantiated.  It is currently used to manage the editing of Smalltalk code, 
and is reinstalled in the Paragraph when a *cancel* is executed and is overwritten 
when an *accept* is executed."

	  selectionState
<Integer>  1 = on, 0 = off.  Since selection involves raw modification of the bits 
in the destinationForm of the Paragraph, it must be monitored closely and with care."
'!

!ParagraphEditor methodsFor: 'menu messages'!

again
	"Text subsititution.  If the left shift key is down, the substitution is made
	throughout the entire Paragraph.  Otherwise, only the next possible
	subsitution is made."

	| many |
	many ← sensor leftShiftDown.
	self deselect.
	self closeTypeIn.
	self select.
	many
		ifTrue: [[self againOnce] whileTrue]
		ifFalse: [self againOnce ifFalse: [self flash]].
	self moveMarker!

!ParagraphEditor methodsFor: 'private'!

againOnce
	| nextStartIndex |
	nextStartIndex ←
		paragraph text findString: UndoSelection startingAt: stopBlock stringIndex.
	nextStartIndex = 0 ifTrue: [↑false].
	self deselect.
	startBlock ← paragraph characterBlockForIndex: nextStartIndex.
	stopBlock ← paragraph characterBlockForIndex: nextStartIndex + UndoSelection size.
	CurrentSelection = UndoSelection
		ifFalse: [self replaceSelectionWith: CurrentSelection].
	self selectAndScroll.
	↑true!
```