| 2004/10 | ||||||
|---|---|---|---|---|---|---|
| 1 | 2 | |||||
| 3 | 4 | 5 | 6 | 7 | 8 | 9 |
| 10 | 11 | 12 | 13 | 14 | 15 | 16 |
| 17 | 18 | 19 | 20 | 21 | 22 | 23 |
| 24 | 25 | 26 | 27 | 28 | 29 | 30 |
| 31 | ||||||
| 2004/11 | ||||||
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 | ||||
| 2004/12 | ||||||
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 | |
top > かいはつにっき 2004 Q4
なんと、大晦日ですよ。。。

けっこう(30cm くらい?)積もったので、玄関まで雪かきしたら死亡。しかも雪かきし終えたらまた降り始めるとは何事ですか。。。
とりあえず xyzzy 的なナローイングでいってみようかなーと。
具体的に実装する場合どうなるかを考えてみると、とりあえずバッファ内のテキストにナローイングされている・されていないという区別がつくことになるので、その境界を管理する必要が出てきます。
そいで、その境界はナローイングされていないテキストを編集することで変化するので、その補正をしなければいけません。そういう仕組みはマークが持っているので、境界をマークの 1 つとしてもてばよさそうです。
マークの仕組みを、作った本人自身が忘れているので一応おさらいすると、他のエディタでしおりとかブックマークとか呼ばれている機能です。一般的には行指向のものが多いのに対し、萌ディタのマークは文字指向になっています。ある文字と文字の間にマークを挟んでおくと、その前後のテキストを編集しても自動的に位置を補正してくれる。
内部的には、バッファを生成するごとにユーザーマーク 1 〜 9、システムマーク 0 が確保されます。それから、ビューがバッファを参照するごとにビューごとのキャレットマークが確保されます。ここに、ナローイングの端点を示すマークを含めるようにすればよさそうです。
ただ、端点のうち右の 1 点は従来のマークそのままでよいのですが、左の 1 点はちょっと動作が違う。文字の間にキャレットとマークがどういう風に位置するかというと、概念的には

という風になってます。なので、キャレット位置に文字を挿入するとマークはそのぶんずれていくわけです。これに対し、ナローイングの左の端点に対応するマークは、キャレットと左の文字の間に位置する必要がある。
ということで、とりあえずマークにスナップする方向を指定できるようにしてみました。
ひとまずキャレットのメソッドに Restrict() と UnRestrict() を追加。
今度こそ本格的に雪が降ってきましたよ。
実装の面から考えてみます。
範囲内の置換なので、選択範囲を抜き出して、検索・置換を行って、その結果を元の選択範囲にはめこめばよいことになります。このとき、1度の検索・置換ごとに再表示とかするとめんどかったり、効率が良くなかったりするので、「範囲内の置換時は全置換のみ有効」という仕様のエディタが多いのだと思います。
これを、範囲内の置換であっても通常の置換と同じ操作性にしたいわけです。で、それを emacs でいうナローイングで実現しようとしているのですが、emacs 的なナローイングとするか、xyzzy 的なそれとするかで実装の仕方が変わってくる。
前者の場合、ナローイング中はバッファにも置換対象の部分しか存在しないのですから、置換処理にも表示処理にも修正は必要ありません。ナローイングの仕組みだけを新しくつくればよさそうです。
ただ、スクリプト中などでユーザが知らないところでナローイングされたままユーザが気付かない場合なんかが出てきそうなので、ナローイング中であるということをかなり目立つように明示しないとまずそうです。あるいは、置換が終了したら常にナローイングを解除してしまって、ナローイングしながらの編集ということをさせないか。
後者の場合は、ナローイングの境界を常に意識しなければいけません。キャレットの移動、文字の送出・削除、表示。。。などなど修正するところも多そうです。ただ、ナローイングされていることすぐわかるし、目で見えるというのはかなりのメリットだと思います。
んー、どっちにしたものかな。
なんか本格的に雪が降ってきましたよ‼‼
とおもたらやんだ。
また降ってきた。
またやんだ。
たとえば emacs だと、あらかじめナローイングしておくことで選択範囲内の置換に相当することができるようになっていると思います。そういう仕組みがあると、「範囲内の置換時は全置換のみ有効」などの制限をかけなくてよいのでよさそうです。
ちなみに emacs(Meadow さん)ではナローイングすると、本当にそのリージョンしか表示されなくなるのに対し、xyzzy だと表示はされるけどアクセスできないという扱いになるようです。なるほど。
選択範囲内の置換を考えてみます。
これがあると、置換したい部分だけ選択して全置換、ということができるので楽です。逆にこれがないと置換したくないところまで更新してしまう恐れがあったりするので大変危険。
で、面倒なのは、今のところ検索にマッチした部分についても範囲選択を使用しているということです。検索して、マッチした部分を選択して、それを置換する。選択範囲の中の一部をさらに選択ということはありえないので、全体としての選択範囲と部分としての選択範囲のどちらかを、「選択範囲ではないけれど範囲を示す何か」というものにする必要がある。そうすると表示の部分をいろいろと拡張しなければなりません。
全置換の場合だけ選択範囲内のみというのを許可する、というのなら、その何かは置換処理時だけ考えればいいので(少なくとも表示についての修正は不要)多少簡単にはなると思います。他のエディタの場合も、範囲内の置換を選択した場合は全置換のみ可能、というのが多いです。
でもそれだと、「選択した範囲外は絶対に更新したくないけど、選択範囲内について置換したい個所としたくない個所があるので、対話的に置換したい」という要件には応えられない。
萌ディタスレより、スクリプトからカレントバッファの拡張子クラスを変更に対応。チェックボックスつきバッファリストの不具合を修正。
なんか雪降ってきましたー。
と思ったらやんだ。
いよいよマウスがおかしくなってきました。くるくる 1 刻みで最後まで一気にスクロールするとか。
しょうがないので、前使ってたのを引っ張り出して使うことに。これはこれで、いまいちくるくるの反応が悪かったり、カーソルが勝手に飛んだりするのですが。。。
バルーンはコモンコントロールそのままなのですが、どうせ漫画的なものにするなら

みたいなのも用意してくれればいいのになー。
ふと、ナビゲータにバルーンなツールチップを出せるようにしたらおもしろいかも、と作ってみました。

なんかヤなナビゲータですが。。。
このナビゲータというのは MS OFFICE のアシスタントをパク、いえ着想を得ているわけですが、いまのところ特別な機能を持ってはいないので、居なくても全然困らない。
本家のほうを見てみると、まずユーザからの質問を自然言語のまま解釈できるヘルプ機能、というのが売りの 1 つになっていると思います。でも、その機能が一般的に受け入れられているか、というと必ずしもそうではない。それどころか、そもそもアシスタント自体を非表示にしている人も多いのではないか。
アシスタントがどのくらいのレベルの形態素解析をおこなうのかはわかりませんけれども、Microsoft のことですからそれなりに複雑なことはしていると思います。が、それでもいまいち実用にならないのだから、コンピュータが自然言語を人間のように理解するのはまだまだ先のことなのでしょう。
その他の売りとしては、ユーザの操作を監視して、より効率的な方法があれば提案してくれたりします。この機能はアシスタントへの質問に比べたらいくぶん実用的ですが、特にインテリジェントという感じはしないです。
何を言いたいかというと、アシスタントの理想形に技術が追いついてないんじゃないかなーと。これを踏まえた上でユーザを補助するというベクトルの機能をナビゲータに付加することを考えると、とりあえず Microsoft の後ろを追いかけることは到底無理だし、追いかけた先にあまりよいものはなさそうだということは先達が実証済みなので、もうちょっと違うところから攻めていかないといけないのではないか。
かいはつにっきを時系列順に読み直すのがやりづらいので各々リンクを張ってみました。。。
そういえば今日は、マイナーテキストエディタ愛好会に書き込んでちょうど 1 年だったりします。
653 名前:名無しさん@お腹いっぱい。 :03/12/18 05:38 ID:CPUlF2Li マイナーなエディタというわけですよ。 http://uploader.org/normal/data/up071.jpg
で、左がそのときアップした画像で、右が今の。
あんまり変わってないですね。
改行コードの指定がおかしくなっているのを修正。
そういえば、タブ型のエディタに限らない、広義の MDI なアプリケーションって、たいてい同一のファイルを複数のウィンドウなりタブで開くことはできないようにしていると思いますが、既存のファイル foo を開いたあと新規バッファを作って、foo の名前をつけて保存すると、結果的に同じファイルを参照しているウィンドウなりタブが複数できてしまいます。
これ、名前を付けて保存するときとかにチェックかけないでいいのかな。
置換のとき、例えば '$' を '!' にしたりする場合を考慮しないと無限ループになってしまいます。なってしまいますというか、そういうバグを出し済みだったりするのですが。なんか特別扱いしないですっきり書けないものかなー。
あ、画像には特に意味はありません(なんとなく AQUA っぽいものを作りたくなった)。
ということで、置換のダイアログは常に表示されっぱなしになるようにしてみました。まあまだ途中なんですが。。。
置換ですが、とりあえず選択範囲内の置換ができないと不便です。それから、置換のダイアログが検索にヒットするたびに生成・破棄を繰り返していて、表示がちらつくのがやな感じなのでそれも修正してみます。
後者からいじってみようかな。
まず置換のシーケンスですが、スクリプト側でいう Caret.Replace() は、萌ディタ内部ではキャレットではなくビューが持つメソッドになっています。これは、検索のオプションで「すべてのバッファを対象」というのがあって、検索の途中でキャレットが切り替わってしまう可能性があるためです。
そいで、実際に検索/置換する処理自体はキャレットが持ってて、逐次ビューから呼ばれるようになっている。その中で、検索にヒットした場合置換ダイアログを生成して表示して。。。とやっています。つまり、ダイアログは呼び出しの最後の最後ということです。
置換ダイアログをちらつかないようにするには、置換を開始したらダイアログを表示しっぱなしにしておけばよさそうですが。ダイアログがモーダルなので、ダイアログ側に処理の主導権を握られてしまう。ダイアログをまず表示して、そこから検索/置換を制御する形になる。そうすると置換のシーケンスがごっそり変わってしまいます。
どうするかというと、ごっそり変えます。
ええと、選択範囲外が変更されるのは気にしないことにしました。
スクリプトからは App.Caret.Selection.Indent(); みたいな感じで呼び出します。ついでなので Selection オブジェクトに SelectWord()、SelectParagraph()、SelectAll() も追加。
インデントについてもいろいろなエディタを見比べてみると、特にインデント後の選択範囲とキャレットの扱いに様々な形があるみたいです。
と、
のいずれかの組み合わせになる感じ。
あと、インデントした領域について Undo したときに選択範囲を再現するかというと、再現しないエディタが多勢っぽいですが、萌ディタの場合は選択範囲への入力操作を Undo したときは選択範囲を再現するようにしているので、インデントについても同じような感じにしてみました。
次は置換。
まずインデント。たいていのエディタで、インデントは tab キーにバインドされていると思います。tab はタブそのものの入力でもあるので、その区別をどうするかというと「選択範囲が存在するか」ということになる。
でも、ときには「選択範囲をタブで置き換えたい」ということもありうるので、選択の形態でさらに条件付ける必要があります。
もう少し詳しく見てみると、ある段落についての範囲選択の状態は、
というのがあります。ここで、1 と 2 は同時に成り立つこともあるのですが、その場合は
□□□□□□□□□□□ □□□□■■■■□□□ □□□□
このように段落内の一部が選択されているということなので(白い部分が選択範囲外のテキストで、黒い部分が選択範囲。。。のつもりです)、つまりこれが「選択範囲をタブで置き換えたい」状況ということだと思います。なので、このときはインデントを行わない。
1 〜 3 を図解してみると、
□□□□□□□□□□□ (1 の状態) □□□□■■■■■■■ ■■■■■■ ■■■■■■■■■■■ (3 の状態) ■■■■■■■■ ■■■■■■■■■■■ (2 の状態) ■■■□□□□
とこんな感じです。この状態でインデントしたとき、2 と 3 は段落先頭にインデントをつければよいのですが、1 はどうなるのだろう。1 についても段落先頭にインデントをつけるとすると、選択範囲外の場所が変更されることになるので微妙に違和感があります。
となると、
というのが考えられます。
どっちがいいのかな。。。
さて、最後に残った PHP ですが。実は PHP の拡張子クラスは萌ディタスレで作っていただいたものをちょっといじった程度で、わたしは PHP を使ったことがないのです。
これはよくないということで、AN HTTPD と PHP を入れてみました(PHP5 だと cgi の実行に php.exe ではなく php-cgi.exe を使うようになったということにハマったというのはないしょ)。リファレンスはこのあたりを参照してみます。
それにしても膨大な組み込み関数、定数。
とりあえず php や javascript や vbscript や css を含んだ html ファイルとかで、各々の言語に応じた補完処理を呼び出せるようになった。
でもぼちぼち実使用してみると、
がないのが致命的です。このへんを作ってみます。インデント系はなかやとさんにスクリプトを作って戴いてますが、萌ディタ内で実装したほうがメモリ上の効率はよさそうなので一応。。。
CSS の場合、セレクタを記述する部分、プロパティを記述する部分、値を記述する部分とおおまかに 3 つのブロックに分かれていると思います。
セレクタはとりあえずタグを補完してあげればよさそうですが、セレクタの文法というのは意外に複雑で、フルに対応するのはとてつもなくめんどうです。とりあえずセレクタが [.#[] を含んでいる場合は、そこにタグを補完してもしょうがないので、その場合はなにもしないようにしました(すごい適当ですね。。。)。
プロパティは単純にプロパティを補完。
値の部分はとりあえず周辺から識別子を拾うことにします。ここでタグとか余計なものまで拾ってしまうけど、数文字打ってから補完を開始すればそれほど違和感はない。。。といいなあ。
ということで、そうしてみました。なんかぱっと見にはテキストの構造を正確に把握してそうな感じに補完するようになった気がしないでもないというか。。。
ところで html 中のスクリプトやスタイルシートをいじっているときは、それぞれの補完が効いてくれた方がよいのですが、どうしたものか。
スクリプト側で振り分けることもできますが、そうすると拡張子クラスが他の拡張子クラスの面倒を見なければいけないので、あまり良くないです。補完のためだけなら onKeySpace() と onKeyPrintable() あたりで対応するだけですが、他のキーバインドについても各々の拡張子クラスに固有のそれを有効にしたいとかになったら、html クラスに逐一振り分ける処理を書かなければならなくなる。
となるとキー入力系のイベントを起こす前に、萌ディタ側で実際にどの拡張子クラスに対してイベントを起こすかの判断をしてもらうのがよさそうですが。。。これもまたむずかしい。ある字句解析器がどの拡張子クラスに参照されているか、というのを持っていないのです(逆の接続は、プロパティ 'lex' で持ってる)。というか、持っていたとしても、字句解析器 1 つに対し複数の拡張子クラスから参照されていたら結局どの拡張子クラスかを特定できない。
よい考えが思いつかないので、ひとまず html の拡張子クラスで振り分けるようにしておきます。
キー入力系のイベントを起こす直前に、キャレット位置についてどの拡張子クラスのイベントを起こすかを問い合わせるイベントを起こせばいいのかなあ。
ということで、そうしてみました。なにかキー入力されると onQueryKeyContext というイベントが起こって、スクリプト側では App.Caret.LexState をみて実際にどの拡張子クラスの onKey なんとかイベントを起動するかを返す。。。という感じです。
つ[nightly build]
補完であと残ってるのは CSS と PHP です。どっちも補完候補がどえらいあるなあ。
ダイアモンドカーソルがあるとホームポジションから手を離さずにキャレットを動かせるので便利ですが、Windows の標準のショートカット(Ctrl+X、C、V)とかち合うので最近はあまり見かけません。
ダイアモンドカーソル自体は WordStar が起源。。。なんでしょうか。ちょっともったいないです。
朝起きたら雪が降ってたので狂喜乱舞してたら、午後になって晴れちゃった。
つまらないなり。
VBScript、C にも補完を組み込んでみました。コメントや文字列中の URL もクリッカブルになるようにしてみました。
とかやってたらマシンが落ちた。
URL の上にマウスカーソルがあるとき、その形も変わってくれるととてもわかりやすいわけですが。WM_MOUSEMOVE のたびにカーソル下の段落について字句解析しなおしたら重そうです。
右クリックのイベントは、やはり、やめにしました。
Enter 入力時の動作をちょっと調整。
キャレット行のガイドラインを細かくしてみました。
他の拡張子クラスも補完を組み込みたいところですが、クリッカブル URL に対応するのにもスクリプトの修正が必要なので、先にそっちを考えてみます。
クリッカブル URL とは。。。なんて説明は不要だと思いますが、萌ディタ上でどう実装するかというと、
というふうに、URL に限らない汎用的なしくみというか、スクリプトに丸投げというか、な感じになると思います。それぞれについて考察します。
まずクリックですが。何がめんどいかというと、「クリック」という動作はより低レベルな「マウスのボタンを押した」「マウスのボタンを離した」という動作の組み合わせで成り立っているわけです。ある領域でマウスのボタンを押して、かつ同じ領域でボタンを離した時点でクリックとみなす〜みたいな。
で、どのレベルまでスクリプト側へイベントとして公開するか。範囲選択やルーラ上のスライダのドラッグなどですでにそういう低レベルなイベントは使っているので、それをスクリプト側にも公開するとつじつまあわせが大変な気がします。
とりあえずクリック、ダブルクリックでいいかなあ。
次に、何がクリックされたかを取得。。。ですが。ビューといっても内部にルーラや行番号領域を含んでいるので、とりあえずそのレベルは引数で渡すようにしようかな。
そういえば、URL をクリックしたらブラウザに渡せばいいんでしょうけど、「クリック」だと URL の範囲選択がやりにくいような気もします。他のエディタでは、ダブルクリックや ctrl+クリックでブラウザ的な動作をするものが多いのでそれにあわせてみます。というか、スクリプトをそういう風に書くだけなのですが。
というわけで、半分書いてみました。
上でクリックすると、onMouseClick イベントが発生します。とりあえず URL らしき文字列を ctrl+クリックするとブラウザを開くようにしてみました。あとはマウスカーソルをどうするか。。。
調子に乗って右クリックで onContextPopup イベントが発生するようにもしてみました。必要ならこのイベント内でコンテキストメニューを出すことで、標準のそれを迂回することができます。
ただ、よく考えるとコンテキストメニューについては、メインメニューと同様に仮想項目を割り当てられるようにしたほうが拡張しやすいような気も。このへんはさらに仕様を詰める必要がありそうです。
Caret.LexState プロパティの取得を高速化。補完に先立ちバッファへ文字を送出した時点で変更された行について字句解析しなおしているので、そのタイミングで字句解析の結果を保存しておくことにしました。
そういえば、アイコンが適当なままでした。
一応今のところ、補完する文字列の区分けとして
なんかを用意しているのですが、ものすごく適当です。さすがに VB から引っこ抜いたアイコンをそのまま使うのはあれなので、とりあえずそれっぽいアイコンにしてみます。。。
補完周りが中途半端ですが、とりあえず nightly build をあげときます。
javascript の場合。
html の場合。
共通しているのは、
です。これらのプロパティは srcfile.javascript.txt で定義していますが、萌ディタが認識しないプロパティなのでむずかしい設定には出てこないです。スクリプトを直接書き換えるか、フックするか、実行中に ctrl+E を押して App.Prop('srcfile', 'auto-completion-delay-count') = -1; とかを評価するか、で変更します。
なんかいじる個所がいろいろなところに及んで混乱してきたのでまとめてみます。
まず字句解析器の定義で
lex = App.Lexes.Add('html-tag');
lex.LeadingIdentifiers = '[a-zA-Z]';
lex.FollowingIdentifiers = '[-:a-zA-Z0-9]';
なんてふうにします。デフォルトの値は LeadingIdentifier が [_a-zA-Z]、FollowingIdentifier が [_a-zA-Z0-9] です。段落中のこれらにマッチする文字列に対して Lex.AddKeywords で設定するキーワードは色分けを行うようになります。
次に補完の起動ですが。たとえば Ctrl+Space のように(文字列中とかでも)必ず補完を開始したい場合は簡単で、
f.onKeySpace = function (arg, classname, methodname) {
switch (arg & KEYMASK) {
case KEYMASK_CTRL:
invoke(arg, classname, 'onCompleteRequest');
break;
default:
invoke(arg, this.parent, methodname);
break;
}
};
と invoke で onCompleteRequest イベントを呼ぶだけです。
これに対して、'.' とかの場合は条件が超めんどうで
f.onKeyPrintable = function (arg, classname, methodname) {
var needcompletion = false;
var delaychars;
// 規定の文字数分識別子が打たれたら補完が必要
if (!App.CompletionList.Active &&
arg.match(/[_a-zA-Z0-9]/) &&
(delaychars = App.Prop(classname, 'auto-completion-delay-count') - 0) >= 0 &&
App.Caret.Paragraph.substring(0, App.Caret.Col + 1).match(
/[^_a-zA-Z]?([_a-zA-Z][_a-zA-Z0-9]*)$/) &&
RegExp.$1.length >= delaychars) {
needcompletion = true;
}
// '.' が打たれたら補完が必要
if (!App.CompletionList.Active &&
arg.match(/[.]/) &&
App.Prop(classname, 'member-completion-enabled')) {
needcompletion = true;
}
invoke(arg, this.parent, methodname);
// 補完が必要で、解析器の状態にマッチするなら補完開始
if (needcompletion) {
var ls = App.Caret.LexState(App.Caret.Col - 1);
if (ls.Name == 'js' && ls.State == 1)
invoke(arg, classname, 'onCompleteRequest');
}
};
なんてふうに泥臭くなってしまいます。ここを簡単にできないものか。
onCompleteRequest イベントハンドラは
f.onCompleteRequest = function (arg, classname, methodname) {
App.CompletionList.Clear();
// 識別子の直前が '.' でなければキーワードなども候補に入れる
var s = App.Caret.Paragraph.substring(
0, App.Caret.Col + 1).replace(/[_a-zA-Z][_a-zA-Z0-9]*$/, '');
if (s.charAt(s.length - 1) != '.') {
App.CompletionList.Add(
'keyword',
'break\ncase\n' + (省略) + 'while\nwith');
App.CompletionList.Add(
'future-keyword',
'abstract\nboolean\n' + (省略) + 'transient\nvolatile');
App.CompletionList.Add(
'std-object',
'Global\nObject\n' + (省略) + 'RegExp\nError');
}
// キャレット付近の識別子を吸い出して候補に入れる
App.CompletionList.Add(
'identifier',
getDynCompletionCandidates().join('\n'));
// 補完開始
App.CompletionList.Popup();
};
な感じです。ここも判定が泥臭い。
とりあえず javascript と html に補完を組み込んでみました。
んーなんというか、便利なような、絶妙に微妙。
補完の対象となる識別子ですが、言語によってその定義が違います。たいていのプログラミング言語だと識別子は [_a-zA-Z][_a-zA-Z0-9]* あたりだと思いますが、XHTML だと http-equiv とか xml:lang とかがあったり。
ということで、字句解析器に識別子にマッチするパターンを定義できるようにしてみるとよいのかも。問題は、どこに定義するかです。プロパティに定義するようにすると、複数の言語を含んだテキストで文脈に応じた補完をすることができなくなってしまう。
つまり、XHTML だから識別子は [-:a-z]+ だ、とかいうふうにすると、スクリプトやスタイルシート内で補完しようとしたときにそれぞれの言語にマッチしない識別子を用いることになってしまう。
ということは、識別子の定義は字句解析器ごとに行うのがよさそうです。それはそれでめどそうですが。。。
バージョン情報でリンクにカーソルを合わせると、IE みたいなカーソルになるはずが、全然別のものになってました。実はこれ、開発の相当初期から発生しつつ原因がさっぱりわからなかったですよ。リンクしてるリソースをすべて洗い出してもそれっぽいものは入ってないし。。。
で、なんかリソースファイルのなかに壊れてるのがあって、そこに隠れてたみたいです。Delphi 付属のイメージエディタでも、Resource Workshop でも、VC でも VB でも見えない(けど、リンクはされる)謎のデータになってたもよう。不思議なこともあるものだなー。
ついでなので VCL ベースのドラッグ&ドロップに使用されるカーソルも OLE の標準の奴に合わせるようにしてみました。ツールバービルダとか。
薬が切れた。
よくスクリーンショットに出てくるフォントが欲しいと要望があったのでとりあえず公開してみます。
補完候補の生成をはげしく妄想してみます。とりあえず javascript の場合、
あたりが補完されるとうれしいのではないかなー、と。で、補完を開始したときに
ものすごく単純に考えると大体こんな感じになると思うのですが、これ、めちゃくちゃ大変そうです。ほとんど IDE がやってることと変わらなくなってしまいます。javascript だと変数定義の際に型を明示するわけではないからさらに面倒です。
なので、メソッド名とかプロパティ名とかで限定するのはやめて、「識別子」でひとまとめにして考えてみる。
まずキーワードですが、おとといも書きましたが、これを補完の対象とすべきかどうかはびみょー。。。'if' なんて Ctrl+Space 打って選択して。。。より直接打ったほうがよっぽど速いです。
なにより、まず Ctrl+Space を打たなければならないというのが地味にめどい。'.' を打つことで自動的にプロパティ・メソッド名の補完が開始する場合は、'.' がソースを構成する普通の文字なので入力のリズムが乱れないわけです。キーワード補完についてもそういうスムーズさを求めると、たとえば文が開始するべき位置の先頭で [a-z] が 1 文字打たれたら自動的に補完開始、なんてのが考えられます。
これがスムーズといえるのか、うざいだけなのかは人によって感じ方が全然違うと思うのでなんともいえません(わたしは 'if ' まで打ったあと「えーとなんだっけ。。。」とその近辺の行をうろうろしたりするので、いちいち補完リストが出たら邪魔だと思います)。
なんとなくですが、打つのがめんどいのは識別子であって、キーワードや '(' とかの記号類、つまり、言語の文法(if 文であれば 'if (式) 文 [else 文]' のような)を見たときの終端記号を自動的に打たれるとむしろ混乱してしまう気がします。それに、終端記号を自分で打たないとなんかソースが頭に入らないような。
キーワードの中には 'instanceof' とかの長いものもあるので、そういうときには補完を利用することもありでしょうけど、キーワードの補完についてエディタが必要以上に親切になる必要はないんじゃないかなーと思います。
次に変数名。。。というか、識別子の補完ですが。とりあえず書いているソースの文法に即した補完候補を生成するのはめんどいので、キャレット位置から上下に n 行分の間に位置する識別子を拾って候補にする、というのを考えています。どれくらい効果があるのかなー。
ということでそうしてみました。Ctrl+Space のほかに、'.' を入力しても補完リストが開くようにしてみました。
あんまり効果ないかも。というか、あるのかもしれないけど単純にキャレット近辺の識別子を拾っているだけなので、本物の IntelliSense とかに比べるとなんか出てくる候補がすごくうさんくさいです。動作がそっくりなだけに。。。
ローカル変数はよさそうですが、オブジェクトのメンバを補完しようとするとがっかりすると思います。
「文が開始するべき位置の先頭で [a-z] が 1 文字打たれたら自動的に補完開始」ですが、試しに組んでみました。何文字識別子を打ったら補完開始するかはプロパティで設定できるようにしてみました。
1 に設定すると想像を絶する邪魔くささ。3 くらいだと可愛げのある邪魔くささ。5 くらいがいいのかな?
まあ基本的に邪魔くさいものであるのは確かなようです。
あと、コメントの中とか文字列の中とかで補完リストがでてもしょうがないので、キャレットオブジェクトにキャレットの現在位置に対応する字句解析器の名称とその状態を得るプロパティがあればいいのかも。
実際のところ表示するものが補完候補しかないのでしょうがないのですが、なんか殺風景なリストのような。もうすこし何か色気が欲しいなあ。
アイコンがいちばんよさそうですが、たとえば「予約語」「プロパティ」「クラス」「構造体」「列挙型」「インターフェース」。。。とあらかじめそれっぽい区分けのアイコンを保持しておくとして、それに当てはまらないものに対してはどうやってアイコンを割り当てるのか? とか。そうすると CodeInsight のように、function なら 'function' と表示するほうが賢そうではあります。
とりあえず適当にちりばめてみました。色気ついたかなあ? アイコンは VB から抜き出したのですが、こういう使い方はたぶん思いっきりライセンス違反なんだろうなあ。。。
そういえば、すでにある拡張子クラスのイベントハンドラを上書きするスクリプトを書くときなんですけども。単純に上書きだと、同じイベントハンドラを上書きするスクリプトを複数登録したとき最後に登録したスクリプトしか有効にならないので、こんな風にするとよいと思います。
// フック関数の定義 function js_onKeySpace_hook (originalHandler) { return function (arg, classname, methodname) { // オリジナルのハンドラの実行前に追加する処理をここに書きます App.Notice('フックしたよ'); // オリジナルのハンドラを呼ぶ if (originalHandler) originalHandler.call(this, arg, classname, methodname); else invoke(arg, this.parent, methodname); // オリジナルのハンドラの実行後に追加する処理をここに書きます App.Notice('フックしたよ'); } } // フックする class_js.prototype.onKeySpace = js_onKeySpace_hook(class_js.prototype.onKeySpace);
赤い字の部分が、それぞれのスクリプトを書く人が変更する必要のある個所です。特に関数名(サンプル中の 'js_onKeySpace_hook')はフックするそれぞれのスクリプトごと異なる必要があるので、そこだけ気をつけます。
とりあえず境界を、プログラミング言語に一般的な識別子である [_A-Za-z][_A-Za-z0-9]* として作っている途中。
補完リストで使用するフォントは、ビュー側のプロパティ 'font-basic-latin' に定義してあるものに従うようにしてみました。わりと違和感があるような、ないような。
ところでとりあえず javascript の拡張子クラスで予約語を補完させてみたのですが、よくよく考えると IntelliSense も CodeInsight も補完の候補は関数・変数のたぐいで、予約語は補完しないんですね。
予約語は数も固定しているし、打つのが面倒なほど長いわけでもないし、打ち間違えても色分けで判断できるし、当たり前といえば当たり前か。。。
IntelliSense や CodeInsight の動きをみてみると、Ctrl+Space で補完を開始するほかに、'::' とか '.' とかを打っても開始します。また、補完の途中で '(' とか '[' とか '.' を打つと補完が確定します。これは確かに入力のリズムに沿っているので、理にかなっていると思います。
それを踏まえると、HTML の場合は例えば '<' を打ったらタグの補完(スペースか '>' で確定)、タグの中でスペースを打ったら属性の補完('=' か '>' で確定)とかいう動きにするとスムーズに入力できそうです。というか、もしかして HTML 入力に特化したエディタはそういう風になってるのかな?
ためしに ez-HTML を落として試してみたらだいたいそういうふうになってるみたいですね。
萌ディタで似たような動作をさせるには、補完開始はそれぞれのキーバインドをそういう風にするだけですが、補完確定は補完状態のときに「補完を確定させる表示可能な文字」を認識させないとならない。
補完中のキー入力について。補完リストがアクティブになっている間は、キー入力に対応する通常のイベント(onKey なんとかとか)は発生しないようになります。そのかわり、onKeyCompletion イベントが発生します。
このイベントに、打たれたキーの名前と修飾キーの状態が渡されます。そいで、どのキーを押されたら〜という判断をして、適当な値を返す。
適当な値というのは
var
COMPLETION_NOP = 0,
COMPLETION_CANCEL = 1,
COMPLETION_OK = 2,
COMPLETION_LIST_HOME = 3,
COMPLETION_LIST_END = 4,
COMPLETION_LIST_UP = 5,
COMPLETION_LIST_DOWN = 6,
COMPLETION_LIST_PGUP = 7,
COMPLETION_LIST_PGDN = 8,
COMPLETION_CARET_LEFT = 9,
COMPLETION_CARET_RIGHT = 10,
COMPLETION_CARET_DEL = 11,
COMPLETION_CARET_BS = 12;
というふうに定義してみました。
//補完中に表示不可能なキーが入力されるごとに呼ばれる
f.onKeyCompletion = function (arg, classname, methodname) {
var key = arg('key');
var modifier = arg('modifier');
// Ctrl+P で 1 つ上の項目を選択
if ((key == 'P') && (modifier & KEYMASK_CTRL))
return COMPLETION_LIST_UP;
// Ctrl+N で 1 つ下の項目を選択
if ((key == 'N') && (modifier & KEYMASK_CTRL))
return COMPLETION_LIST_DOWN;
return COMPLETION_NOP;
};
こんなふうにイベント内で使います。どちらかというと C-p、C-n より、古き良きダイヤモンドカーソルのほうが使いやすいかも。
昨日出てきた補完対象範囲の境界を何を以って決めるの? というのを考え中なのですが、ぜんぜんおもいつかなかったりするわけですよ。
ずっと前に途中まで作ったまま放置していた補完周りについて。
補完といえば IntelliSense(MS 製の補完)や CodeInsight(Borland 製の補完)が有名なので、それを参考にしてみます。エディタではサクラエディタや Alpha も標準で補完機能を持っているので、それも。
とりあえず補完動作のシーケンスですが。まず補完を開始するのはスクリプト側です。たとえば Ctrl+Space が押されたら〜とか。で、補完の対象となるリストを作成します。これもスクリプト側。それから App.PopupList() を呼んで「補完状態」に入ります。
「補完状態」に入っていると、すべてのキー入力をポップアップした補完リストが処理することになります。そいで、補完が確定したら確定した文字列を入力します。大まかに考えればこれだけです。
もちろん確定時に単純に入力してはだめで、文字列を途中まで打って補完状態に入った場合や、補完の対象について大文字・小文字の区別をしない場合のことを考える必要があるけど、後述。
そのほか思いつくのは、
補完状態のとき、入力した文字列にしたがって候補を絞るかどうか? IntelliSense 的な動作だと絞らずに、いちばん近い候補の先頭にフォーカスします。これ、候補がソートされている前提になるのかな? これのメリットは、「補完の候補として最良ではない前後の候補も見られる」、デメリットは「どれが補完の候補になっているのかぱっと見わからない」ということだと思います。

CodeInsight 的な動作だと、打った文字列にしたがい候補が動的に絞られます。こちらはソートされてなくてもかまわない(実際、スコープ順・名前順を切り替えられる)。これのメリット・デメリットは IntelliSense とぴったり表裏一体で、「どれが補完の候補かがはっきりわかる」けど「補完の候補として最良ではない前後の候補を選びにくい(入力した文字を消さなくてはならない)」ということだと思います。

字句解析の結果を利用できると便利かも? たとえば HTML のときは、タグの外や '<' の直後にキャレットが位置する場合はタグ名に対する補完、それ以外は属性名の補完をしたいはずです。これをいっしょくたにするよりは、分けたほうが親切なような気がしないでもありません。

サクラエディタや Alpha はすべていっしょにしているようです。でもここまで候補が絞られているなら、いっしょでも問題ないかなー
補完状態のキー入力もスクリプト側へイベントとして投げられたほうがいいんじゃないのかな? 仮にエディタ側でキャレット移動を C-p とか C-n とかにしてたら、補完リストもそれに合わせたほうが混乱しなさそうです
補完しようとしたら、補完の対象が 1 つだけだったら、リストを表示しないで直接入力したほうがよいのか、一応リストを表示するのかどうか?
補完の対象に IME を経由しないと入力できない文字があった場合はどうするのか? フルに対応しようとすると狂おしくめどそう。例えば「補完対象」を補完しようとしても、「補」1 文字だけ変換するのは却って手間です。あるいはよみがなを取得しておくとか?
なんという奥の深さ。
補完を開始するときの動作について。「補完」ということで、その動作も「足りないものを補って、完全にする」わけですが、内部的には「補う」というよりは置き換えるような感じになると思います。
どういうことかというと、補完の対象の大文字・小文字を区別しない場合なんかは、たとえば
var s = new str| // 補完開始
^^^ 補完の対象(すでに打たれた文字列: 'str')
↓
var s = new | // 補完の対象を一旦削除する
↓
var s = new String| // 決定した補完候補を送出
と、すでに打たれた文字についても補完候補で置き換える必要があるためです。
また、たとえばすでに完成している文字列内で補完を開始した場合は、
var s = new Str|ing('abc'); // 補完開始
^^^^^^^ 補完の対象(すでに打たれた文字列: 'String')
↓
var s = new |('abc'); // 補完の対象を一旦削除する
↓
var s = new String|('abc'); // 決定した補完候補を送出
なんて動作になります。要するに、補完に先立ちキャレットの前後を走査して、どの範囲までを補完(というか、置き換え)の対象にするかを決めないとだめだということだと思います。IntelliSense、CodeInsight はそんな感じで動いているように見えますが、サクラエディタはうまく動いてくれないような('ht|tp-equiv' で補完すると、'html|tp-equiv' になってしまう)。Alpha は補完開始時の動作にちょっと不具合があるようです。
で、範囲を決めるためには「どの文字が範囲外なのか?」というのをあらかじめ定義しておく必要があります。こういう補完は主に英単語が対象だと思うので、アルファベット(といくつかの記号類)に限ればいいのですが、いわゆる動的補完とか dabbrev(abbrev は略語展開なので厳密には違う?)というもので得られる補完候補が日本語の文とかだったりするとつじつまが合わなくなってしまう。
オートメーションオブジェクトの草案に、Buffers.Item(Windows.Current.Views.Current.LinkedBufferIndex) というのを書いているのですが、たとえばドットの直後で折り返してくれてもよさそうなのですが、IE も FireFox もそうしてくれないです。
p タグのスタイルを text-justify:inter-ideograph; としているので、IE の場合に限って均等割付っぽくなっている(はず)ですが、前述のコードの場合はそれが災いして、前後の日本語がびろーんと伸びてしまってかっこわるいです。
FireFox の場合はコード全体が次の行に送られて、前の行がぽつんとしてしまいます。
ということで、<nobr> 〜 </nobr> で囲んだり、折り返して欲しいところに <wbr> を挟んだりしてみる(
<wbr> の代わりに、たとえば ­ とか(Buffers.Item(Windows.Current.Views.Current.LinkedBufferIndex))。でも英単語の内部で折り返すわけではないので、これはこれで変ですね。
かまださんの日記によると、ZERO WIDTH SPACE だとよさそうとのことです。なるほどー。こんな感じです:Buffers.Item(Windows.Current.Views.Current.LinkedBufferIndex。でもフォント(と、ページのエンコーディング?)によっては ZERO WIDTH にならないことがあるみたいです。
あ、そういえば、0.6 を上げました。
coolbar の状態を ini ファイルに書き出して、起動時に再現するようにしてみました。しまった。めどいと思ってたらけっこう簡単だった。0.6 に入れればよかった。
カラーテーマ周りのちいさいバグを修正。カラーテーマって、画像のフィルタと絡めるとものすごく使いにくいなあ。
メニューのうち、
については、メニューを開く直前にメニューの項目を更新しているのですが、毎回するのは無駄なのでリストが更新されたときだけにしてみました。
TMenuItem.Delete() というのがあって、あるメニューアイテムの任意の子供を削除するのですが、なんかリストからはずすだけでメモリの解放はしてくれないっぽい(ヘルプに書いてあった。。。)ので修正。
げげ。じゃあいままで、メニューを開くたび漏れまくってたってこと?
萌ディタと関係ありませんが、マウスのくるくるを向こう側に回すと、たまに反対に回転していると認識されるようになってしまいました。ブラウザとかで上方向にスクロールしようと思うと下にスクロールしてしまったりするのでめちゃくちゃ使いにくいよ。。。
ということで分解してみた。なんかホイールの軸の片方に部品がついてて、その中にぎざぎざのついたわっかと、ぎざぎざ 1 個にちょうどはまる盛り上がりのついた金具が入っていました。こういうしくみなのかー。
とりあえずホイールの軸のごみを取ってみたら多少良くなった。ついでに金具の盛り上がりをドライバーで押し込んだら回すのが軽くなったですよ。
すべてのウィンドウを最小化したとき SetProcessWorkingSetSize() で強制的にスワップアウトするようにしてみました。それより最小化してないときのメモリ使用量をなんとかしたら? とセルフ突っ込み。
後から追加のスクリプトでそれぞれの機能を上書きしやすいように、
アクション NativeWallpaper、LightWallpaper、DarkWallPaper、BlendWallpaper を削除。新設した WallpaperFilterList で代替します。
スクリプトからフィルタを変更するときは、plaintext の wallpaper-filter を直接いじります。
とりあえずスクリプトでメニューを定義できるようになった。
次は仮想項目まわり。
仮想項目まわりができた。
あとはアクションに関連付けられていないメニューに対して、実行するスクリプトを指定できるようにすれば、萌ディタスレでのこのあたりの要望はクリアしたことになると思います。
スクリプトを引っ掛けられるようにした。
メニューの定義まわりをドキュメントにまとめないとよく分からないですね。。。地味に大きな修正をしたので、ドキュメント上げたら 0.6 で公開というところでしょうか。
めあどについに変なのが来た。
高木さん(誰)、1 クリック 30 円でも、そもそも GeoCities って広告とか貼るの禁止だと思います。
拡張キー入力の途中で無効な入力をされたときの挙動を修正。http://yuiko.moemoe.gr.jp/dokugon/?20041111_01 を参照しました。
ちなみにウィンドウを CreateWindowExW() で作っていれば WM_CHAR は UTF-16 の文字が返ってくるので、それをキー入力にしてるだけです。2 ストロークめが無効かどうか判断するのに結局 WM_DEADCHAR も見るようにしましたが。。。
メニューを定義するのはやっぱりオブジェクトに対して行うことにします。メニュー項目 1 つにつき 1 つのオブジェクトが対応します。

のような感じに。必要なプロパティは、
それから、メソッド。メニューの項目自体が、その下に階層化されたメニュー項目の親になりえるので、コレクション的な役割も持たせる必要があると思います。これはまあ、ありがちな感じに。
ところでツリー状に構成するので、すべてのルートとなる項目が 1 つ存在することになりますが、メニューバーには出ないです。当たり前だけど。
複数メニューのツリーを定義して、それぞれのルート項目をリストで持てば、実行中にまったく別体系のメニューに動的に変更できるなどという、無駄にスゴい機能を実現できるなー。
そんなもの必要なのかな。。。
たとえば 2 ちゃんねるブラウザを萌ディタ上で実装したとすると、メニューの体系がエディタ的なそれと全然異なります。そういう場合に使えばいいのかしら。
ちょっとやりすぎな感じがするので、とりあえずルートとなる項目はただ 1 つだけにしてみます。
思うに、メニューを定義するとして、まったく異なった体系にしようとしない限り、だいたいはどの拡張子クラスでも同じ構成になるはずです。その合間に、拡張子クラスに特化した項目が挟まる形になるのではないかと思います。
それを踏まえると、たとえば現状、ファイルメニューは
という構成になっています。メニューを定義する段階で、「ここは拡張子クラスごとに変わるよ」という特別なメニュー項目を置いといて、その実体を別のプロパティなりオブジェクトなりに定義しておけばよいのかなー。つまり、メニュー自体は
のような感じで定義しておく。実際にメニューが選択されたとき、仮想項目の実体化を行い(それぞれをプロパティで設定するか、イベントが発生)、その後実際にメニューが
なんていう風に表示される。こうすると、既存の環境へ割り込ませることもたやすいと思います。
どこかの拡張子クラスで定義されている仮想項目にさらに追加する場合は、
var f = class_js.prototype;
f.onRequestMenu(arg, classname, methodname) {
switch (arg("kind")) {
case "file":
swich (arg("kind2")) {
case "new":
// まず親クラスで実体化して
result = invoke(arg, this.parent, methodname);
// それに追加する
result += '追加する項目';
break;
}
break;
}
}
のような感じに。ということは、仮想項目の実体化はイベントで、ということになるのかな。なんかいわゆるプログラミング言語の仮想関数と考え方がちょっと似てますね。
とりあえず、かいはつにっきと同じ内容をかいはつ blog にも書くようにしてみたのですが。前者は同じ日付内では時系列が下へ進むのに対し、後者は(日付単位でなく個々の記事単位に並ぶので)時系列が常に上へ進むのが混乱するようなしないような。
まあ、こだわらないことにします。
他のメニューについても考察。
| ファイル | 編集 | 書式 | ビュー | ツール | ヘルプ | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 新規 | 元に戻す | カラーテーマ | 上下に分割 | キーボードマクロ | トピックの検索 | |||||||
| 指定して新規 | やり直し | カラーテーマの一覧 | 左右に分割 | 記録開始/終了 | - | |||||||
| 開く... | - | - | - | 記録中止 | バージョン情報 | |||||||
| 最近のファイル | 切り取り | 変更... | 現在のビューを削除する | 再生 | ||||||||
| 最近のファイル一覧 | コピー | エディタの背景 | 他のビューをすべて削除する | - | ||||||||
| - | 貼り付け | エディタの背景の一覧 | キャレット位置へ出力 | |||||||||
| 上書き保存 | 削除 | - | ファイルへ保存... | |||||||||
| 名前を付けて保存 | - | 変更... | - | |||||||||
| - | 検索... | - | マクロの一覧 | |||||||||
| 閉じる | 次を検索 | そのまま表示する | - | |||||||||
| - | 置換... | 明るく表示する | マクロの管理... | |||||||||
| ページ設定... | 行へ移動... | 暗く表示する | ||||||||||
| 印刷... | マーク | 背景色と合成する | ||||||||||
| - | 移動 | - | ||||||||||
| 萌ディタの終了 | 1 | ツールバーを表示 | ||||||||||
| 2 | ファンクションキーを表示 | |||||||||||
| 3 | ステータスバーを表示 | |||||||||||
| 4 | スクリプトを有効 | |||||||||||
| 5 | - | |||||||||||
| 6 | ナビゲータを呼ぶ | |||||||||||
| 7 | むずかしい設定... | |||||||||||
| 8 | ||||||||||||
| 9 | ||||||||||||
| 前 | ||||||||||||
| 次 | ||||||||||||
| 設定/解除 | ||||||||||||
| 1 | ||||||||||||
| 2 | ||||||||||||
| 3 | ||||||||||||
| 4 | ||||||||||||
| 5 | ||||||||||||
| 6 | ||||||||||||
| 7 | ||||||||||||
| 8 | ||||||||||||
| 9 | ||||||||||||
| - | ||||||||||||
| すべてクリア | ||||||||||||
| - | ||||||||||||
| すべて選択 | ||||||||||||
| 日付と時刻 | ||||||||||||
とりあえず現状のメニュー構成を踏まえて、
という仮想項目を設けることにします。ここに、必要ならそれぞれの拡張子クラスにローカルなメニュー項目をはめ込むことになる。
∧_∧
( ´・ω・) nightly build が入りましたよ・・・・。
( つ旦O
と_)_) 旦旦旦旦旦旦旦旦旦旦旦旦旦旦旦旦旦旦旦旦
メインメニューの再定義を可能にするとして、どういう形にしたものか。
という内容で 2 回くらい書いてみたのですが、微妙にまとまらないです。何がまとまらないかというと、部分的な継承が可能か? という点です。
メニューの定義を拡張子クラスのプロパティで行うようにすれば、テキストファイルのときはこのメニュー、html のときはこのメニュー、なんてことができて便利そうなのです。でもメニューの定義自体は複数のメニュー項目を含むことになるので、ある項目だけ拡張子に依存する、という風にはならず、メニュー全体を取り替えてしまうことになる。
何のことかというと、textfile クラスでファイルメニューを
新規(N)
開く(O)
:
萌ディタの終了(X)
と定義したとします。それから、例えばマクロのテンプレートを書いたので、js クラスでテンプレートを開くメニューを追加したいと思ったとして、そういうふうにするには、
新規(N)
新規マクロ(M)
開く(O)
:
萌ディタの終了(X)
と、追加したい項目を含めて全体を定義しなおすことになってしまう。これは、差分による環境定義とはちょっといえないと思います。
インターフェースをどうしたものかな。たとえば、字句解析器の定義と似た感じで
var menu = App.Menus.Add('std');
var filemenu = menu.Add('ファイル(&F)');
filemenu.AddAction('new');
filemenu.AddAction('manualnew');
filemenu.AddSeparator();
filemenu.AddAction('open');
:
:
App.Prop('textfile', 'menu') = 'std';
のような。メニュー項目 1 つを 1 つのオブジェクトとみなすわけです。わりと筋の通ったやりかただと思いますが、これが起動時に走るのはちょっと重そうな気がしないでもないです。
var menu = App.Menus.Add('std');
var filemenu = menu.Add('ファイル(&F)');
filemenu.MenuText =
'$(new)\n' +
'$(manualnew)\n' +
'-\n' +
'$(open)';
なんてふうに、文字列の形で一気に定義するメソッドもつくればいいかな?
メニューに割り当てるのは、基本的にはアクションなのですが、例外的にアクション以外の何かも割り当てられないとだめそうです。単なるセパレータなんかがそうだし、メニュー表示時に自動的に展開されるアクション(「最近のファイル」とか)は、便宜上その親となるダミーのメニューが必要です。
で、どうせなら、アクションに直接関連していないメニュー項目は、実行するスクリプトを指定できてしまうと良いのではないかなーと。
改行を多く含んだ長いテキストを貼り付けると異常にもっさりするのを修正。原因は恥ずかしいので書きません。。。
FireFox でも IE とだいたい同じに見えるようにしてみました。
最大でどーんと 300MB だそうですが、何度見ても最大容量 15MB のままです。【和塩】geocitiesスレ4【ジオシティーズ】をみると塩の管理って鮮やかすぎるほど適当みたいなので、まあそんなものなのかも。
あなたのホームページ容量がアップ! ホームページ容量が通常で50MBに、さらにジオプラス(Yahoo! BB会員は無料)なら300MBものホームページ容量が使えるようになります。写真や動画を使ったホームページも容量を気にせず安心して作れます。 ※10月下旬より、順次増量となります。
無料の塩の場合だと 50MB なんですね。
「順次」というのが逃げ道ですね。
ちょっと気が早いですが、冬っぽくしてみました。なんかこんな感じの色合いのビールがあったような気もしますが。。。
nightly build。どんなもんでしょうか。
UPX をかけると 600KB 弱くらいまで縮みます。たぶん DFM(コンポーネントの状態をシリアライズしたテキスト)の圧縮率がいいんだろうなあ。DFM をバイナリで持つようにしても、実行ファイル内のそれはテキストになるような気がするけどそういうものなのかな?
メニューを自由にいじれるようにするとアクセラレータの衝突が避けられないことになるのですが、TToolbar のメニューのエミュレーションにはその辺をあまり考えないっぽいです。メニューのエミュレーションのために TToolbar 内部で TPopupMenu を 1 個保持しているのですが、またしても private なので AutoHotkeys(アクセラレータの重複を勝手に修正するか、のプロパティ)をどうやってもいじれないです。とりあえず例によってソースを抜き出して改造。
ということで、TPopupMenu のインスタンス生成と初期化部分を仮想関数にしたり、外部にイベントを投げるようにしてみたり。改造って言ってもそれだけなのですが。
萌ディタ側でのメニューのキャプションの持ち方がちょっと特殊というのもあるのでこの辺はもうちょっと煮詰める必要がありますが、とりあえず。
Visual Studio に DataObjectViewer というのがおまけで付いてきます。これはドラッグ&ドロップのときのデータの受け渡しをする IDataObject についての情報を表示してくれるものなのですが、萌ディタからドロップするとなぜか E_OUTOFMEMORY になてしまう。
そのエラーメッセージはあんまり当てにならなくて、どうも IDataObject.EnumFormatEtc を実装しないとだめっぽいなあ。ということで実装。でも DataObjectViewer 以外に EnumFormatEtc を呼ぶアプリケーションを知らないので何が変わるのかいまいち不明な今日この頃。。。
うちの WZEditor は 4.00F です。なんか VillageCenter の掲示板にはもっともっと新しいのも時々アップされてたみたいですが、見てないのでよく分からない。
で、4.00F ですが、萌ディタでドラッグを開始して WZEditor にドロップしようとしても、ドロップできないのが気になります。なしてだべー。
まあこっちがバグっているんでしょうが、試しに OLE ドラッグ&ドロップを行う他のエディタでも試してみたら、どのエディタからもドロップを受け付けないっぽいです。
これは、WZEditor 側のバグじゃないのかなー。
あ、今見たら最新は 4.00Fe なんですね。とりあえず上げてみました。。。変わらないなー。
256 色でのアイコンを描いてみましたが。エディタだからといって鉛筆と紙というのはちょっとストレートすぎるような気もしてきました。せっかくだから萌え萌え〜なアイコンにすればいいのかなあ。ネコミミを付けるとか。。。
それはそれで(かなり)恥ずかしいですが、親切な誰かが萌え萌えなアイコンを描いてくれたら勢いで採用してしまうかもしれません。
空白の表示をこんな感じにしてみました。
とりあえずnightly build。スクリプトがフルセットになっているので、上書きする際それらを書き換えている方は注意を。
Undo まわりにバグが潜んでいるっぽいので調べ中。
あと、バグとはちょっと違いますが、バッファに対する複数の操作をひとまとめにできると便利そうです。実は内部的にはそうできるようになってたりするのですが、スクリプトに公開することを考えると微妙にいじる必要が出てくる。
で、少し考える必要があるのは、グループ化された操作を「編集の履歴」やステータスバーに表示する際、何が表示されるのか? ということです。可視化された操作が常に固定の文字列で表示されると作るのは楽ですが、「次の Undo: グループ操作」とかだと何を Undo するのかよくわかりません。
内部的には、バッファオブジェクトに対して
// グループ化開始
fBuffer.OperationList.BeginUpdate;
// 複数の操作。。。
// 代表となる操作
fBuffer.OperationList.Add(
TSendSelectionOperation.Create, true);
// グループ化終了
fBuffer.OperationList.EndUpdate;
なんて感じで、操作を管理するオブジェクトに BeginUpdate 〜 EndUpdate が用意してあって、その間で追加した操作が 1 グループとして扱われるようになっています。で、Add する際の最後の引数が true のとき、その操作がグループを代表するものとして扱われる。
が、これだと Undo の対象となる操作が事前に決まっていないとだめで、自由にグループを作るようにするためには少し変更しなければいけません。
こうなりました。スクリプトでは、
// グループ化開始
App.Caret.BeginOperateGroup('何かの操作');
try {
// 複数の操作。。。
}
finally {
App.Caret.EndOperateGroup();
}
のような感じです。これにより、Undo/Redo したときの対象は、複数の操作をまとめたグループになります。shift+ctrl+Z の編集の履歴では、「グループ操作 何かの操作」みたいな感じで、BeginOperateGroup に渡した引数が表示に用いられます。
萌ディタスレ でいただくバグ報告はほとんど XP だったりするのですが、2k ではなぜか再現できないものもある感じです。でも XP もってないんですよね。。。
どうしたものか。
nightly build を出しますよー。バグは減ったり増えたり。
コモンコントロールの UpDown コントロールをラップしている VCL の TUpDown が、16 ビットの入力範囲にしか対応してないよ。。。しかも例によって、private の奥底で SmallInt(Delphi の符号つき 16 ビット型)と定義されているのでいじることができません。
しかたないので Rebar 同様 VCL からソースを引っ張ってきてローカルで改造。といっても、SmallInt を Integer にして、UpDown にメッセージを投げている個所を 32 ビット版のほうに置き換えるだけなのですが。。。どうも VCL ってコモンコントロール回りは弱いのかなあ。
ということで「行へ移動...」を実装。
どうでもいいのですが、UpDown コントロールって上のボタンを押すとプラスされますが、テキストって下方向に向かって行番号が増えてくので微妙に混乱するような。。。三角ボタンじゃなく「+」「−」を表示してくれればいいのに。
さて、環境設定画面を書き直したのはいいのだけど、まだ解決していない問題があります。
スクリプトで何か設定するとします。たとえば
App.Prop('c', 'tab-width') = 8;
とか。そいで、使ってるうちに「やっぱりタブ幅 4 にしよう」と、環境設定画面でタブ幅を 4 にします。そして終了し、ini ファイルに
tab-width="4"
と記録されます。次に起動すると、この値が再現される。問題なさそうですが、ここでスクリプトを書き換えて、
App.Prop('c', 'tab-width') = 2;
とかやっても、起動すると ini ファイル上の設定で上書きされて、見た目上何も変化してないようになってしまう。実際、これで「バグかしら?」と焦ってしまったことが多々あるのでなんとかしなければいけません。で、どうするか。
んー。たとえば、ini ファイル上に記録するのは値 4 だけではなく、「スクリプトで定義された値 8 を 4 に書き換えた」という形で持てばいいのかな。で、スクリプトの初期化後 ini ファイルの値を上書きするかどうかは、それら 2 つの値、とスクリプトで定義された値との関係で導くようにする。
これだと、よさそうな予感。
ほかに、例えばスクリプトの中に目印を埋め込んでおいて
/* automaticaly update section start */
App.Prop('c', 'tab-width') = 8;
/* automaticaly update section end */
ini ファイルに値を記録する代わりに、この目印に囲まれた領域にスクリプトの形で値の設定を書き出しちゃおうかなーとか。これはちょっと。。。
ということで、前者のやり方で行ってみます。やることは簡単で、スクリプトの初期化が完了して、ini ファイルの読み込みをする直前にプロパティのすべての値をコピーしておけばよいです。で、ini ファイルにはそれも書き出しちゃおうと。プロパティの TPersistent.Assign(Clone()、あるいはコピーコンストラクタのようなもの)は環境設定画面を書き直す段階で書いたのでそれを使うだけ。
まとめると、まず ini ファイルには、同一の拡張子クラスについて「スクリプトの初期化が終わった時点のプロパティ」と「萌ディタの終了時点でのプロパティ」の 2 つを書き出すようにします。
で、次に起動するときに。
という動きをします。これで、スクリプトをでプロパティの更新まわりを書き換えたら、書き換えたプロパティだけが反映されるというふうになるわけです。なんか 1 年分くらい頭を使ってしまったよ。
起動時に読み込むスクリプトで、addClass() するときにすでに親になる拡張子クラスが存在していないと起動できません。これは exe 側で、プロパティオブジェクトがツリー上のリストに保持されているからです。親が存在しなければツリーに参加できない。
そいで、プロパティリストに新しいオブジェクトを追加したとき、親が存在しない場合は、拡張子クラスの名前、親になる拡張子クラスの名前、拡張子のセットを覚えておくようにしました。迷子を一時的に預かっておくということです。これをリストの orphan_list に追加しておきます。
で、ini ファイルの [script] セクションのファイルをすべて評価し終わったら(この時点ではまだスクリプトの onInitProp ハンドラはどれも呼び出されていない)、orphan_list をみて、迷子の親を親元へ返します。迷子の親も迷子だったりする場合もあるので、誰の親も見つからないか、すべての子の親が見つかるまで繰り返します。
ということで、スクリプトを登録する順番はあるていど自由になりました。でも、すべてが解決したわけではありません。これは拡張子クラスの追加の順序が自由になっただけで、javascript 側での拡張子クラスが存在していなければ起動できません。
つまり、9 月 30 日 に書いたような、既存の拡張子クラス(の、javascript 側の実体)に関数を追加するようなスクリプトは、まだ登録の順序に依存するということです。
どうしようかなー。いっそ、拡張子クラスを追加するスクリプトと、AddOn 的なスクリプトを区別して、初期化のフェーズを 2 段階にするとか。そこまですることないかな?
ということで、環境設定画面を書き直す準備としてプロパティに値を設定するときのチェック処理を書き中。
チェックのために、数値型であれば下限・上限値、列挙型であれば列挙の個数と文字列との変換とかが必要なのですが、Delphi の生の RTTI を参照するようにしてみました。RTTI 周りってなんでヘルプからごっそり抜け落ちてるんでしょうか。。。
こんなふうになりました。
若干動作が変わります。
環境設定画面を DLL に追い出してみたら、なんか exe のサイズが数 KB しか減らないじゃないですか。。。しかも、アプリケーションで使う VCL と DLL で使う VCL とが独立するので、DLL 側のサイズもフォーム 1 枚だけなのに 700KB くらいになってしまう。。。なんか意味ないなあ。
ということで、exe 1 つにまとめたほうがよほど小容量という驚愕の結果になってしまいました。
実行時パッケージを使うようにするとどうなのかなー。Delphi の実行時パッケージというのは、いわば VB ランタイムみたいなものです。この場合、
などと構成がやたら複雑になるうえに、トータルするとやっぱり exe 1 つにまとめた場合より大きくなるような気が。。。てか、実行時パッケージを使わないでビルドすると 1.5MB 位なので、ランタイムだけですでにそのサイズを超えてます。
これはアレですね、exe 1 つにまとめると、リンカはランタイム内で実際に呼ばれているコードだけリンクするのに対し、実行時パッケージだとすべてまるごと持ってないとだめだからですね。
というわけで、DLL に追い出すのはあきらめます。。。○| ̄|_
次の問題は、メモリマネージャがアプリケーションと DLL で独立してしまうということです。昨日のように string を返す DLL 側の関数は、DLL 側で確保した文字列をアプリケーション側が受けることになります。
Delphi の string 型は、参照カウントを元に自動的に解放が行われます。なので、DLL 側で確保された文字列をアプリケーション側で定義した string 変数で受けて、メソッドを抜けたりすると、アプリケーションが確保したわけではないメモリ領域を解放しようとして、アドレスエラーが出てしまう。
ということで、Delphi の場合、アプリケーションと DLL でメモリ確保を共有するときは、それぞれのメモリ確保処理を BorlandMM.DLL にリダイレクトしましょうというのが定石なんだそうです。
とここまで書いて実際アドレスエラーが起きるところまで試してみたところ、DLL が公開する関数(string を返す)だと確かにアドレスエラーが発生するのですが、オブジェクトを DLL から取得してそのメソッドを呼んだ場合、戻り値の型が widestring でも string でも滞りなく実行してしまうという不思議な結果に。
まとめるとこんな感じに。
| DLL が公開する関数 | DLL が返したオブジェクトのメソッド | |
|---|---|---|
| string | アドレスエラー | 正常 |
| widestring | 正常 | 正常 |
widestring はともかく、string でもメソッドの場合はエラーにならないのが不思議です。それとも、たまたまエラーになってないだけなのかなあ? メソッドを呼んだとしても、アプリケーション側のメモリマネージャは使ってないようなので、確かに DLL 側で確保しているはずなのですが。。。
なんか謎に満ちています。
そういえば忘れていましたが、「行へ移動(G)」というのをまだ作ってないや。行へ移動するために数値を入力してもらうことになるので、そのためのダイアログが必要なのですが。
この、実行中になんか入力してもらうという状況はスクリプトが絡む場合もあると思うのですが、スクリプトからどのレベルで GUI を自由にするかということを考える必要があるかも。自由にダイアログを定義できるエディタというと、VxEditor、xyzzy、WzEditor とかかな。あるいは、html を書いて IE コンポーネントでどうのこうの、というのも考えられます。
またしても知ったかぶりをしてみますよ?
環境設定ダイアログが仕様に追いついていないのと使いにくい(色の指定などでテーマカラーを指定できない、フォントの指定がめどい、マクロを編集できない、など)ので、書き直そうと思います。
で、そうすると今以上に大きなコードになって、頻繁に使うわけでもないのに実行中メモリに常駐するのはもったいないです。というわけで、DLL に追い出そうかなーと。印刷とかはさらに大規模になると思うので、その布石という意味もあります。
DLL に追い出すとき、問題になるのはまず、ふつうの DLL は関数単位でしか公開できないということです。おのおののクラスがプロパティなりメソッドをもっているという今時のプログラムと整合をとりにくい。これをどうするか。
TFoo = class
public
function GetTime : widestring; virtual; abstract;
end;
なんていう抽象クラスを含んだユニットを、アプリケーションと DLL の双方で参照します。
function CreateFoo : TObject; begin result := TFooImpl.Create; //TFooImpl は定義済みとする end; exports CreateFoo;
DLL 側で TFoo を実装した TFooImpl のインスタンスを生成して返す関数を公開します。
h := SafeLoadLibrary('〜.dll'); // DLL をロード
p := GetProcAddress('CreateFoo'); // 関数のアドレスを取得
o := p() as TFoo; // インスタンスを生成してもらう
とやったとき、o は正しくインスタンスが入っているのか? というと 'as' 演算子で失敗します。同じソースを参照した同じクラスのはずなのに、異なるモジュール間ではそれぞれ異なるクラスとみなされてます。これは、オブジェクトに対する 'as' 演算子の動作は VMT のアドレスの比較だからですね。同じ理由で 'is' 演算子も失敗します。
そのかわり、o := TFoo(p()); とすると呼び出せるようにはなるけど、これじゃむりやりキャストしてるだけなので安全とはちょっといえないし、'as' と 'is' が正しく働かないのは問題です。
ということで、抽象クラスの代わりにインターフェースを定義します。
IBar = interface ['{732BA125-7052-42BC-BF77-5BF761EB771A}']
function GetTime : widestring;
end;
function CreateBar : IBar; begin result := TBarImpl.Create; //TBarImpl は定義済みとする end; exports CreateBar;
h := SafeLoadLibrary('〜.dll'); // DLL をロード
p := GetProcAddress('CreateBar'); // 関数のアドレスを取得
o := p() as IBar; // インスタンスを生成してもらう
これだと 'as' も 'is' も動きます(対象がインターフェースの場合、GUID を比較するから)。IBar でアプリケーション側のオブジェクトを参照することもあるので、ユニット U' にはアプリケーション、DLL 双方で参照されるオブジェクトのインターフェースをどばーっと定義することになると思います。
バグフィクス版を出してみます。
メニューを再定義可能にする前に、いろいろと準備というか考えることがあります。
まず、メニューに割り当てるコマンドは、スクリプトからは App.Actions('...') で得られるオブジェクトです。アクションが主で、メニューが従。なので、定義の仕方としては、メニューの項目にどのアクションを割り当てるか〜という感じになると思います。
ところが、アクションはその名の通り、なにかの動作 1 つを表しているのですが、一部例外的な、しかも現状のメニュー構成に依存するものがあります。これをクリアしないと汎用的にしにくい。
たとえば OpenFromRecent というアクションがあって、これは動作自体は何もしませんが、メニューに割り当てられたときに、そのメニューの「子メニューに」最近使ったファイルの一覧を設定するようになっています(ツールバーに割り当てられたときは、ポップアップメニューに設定)。ほかに同じ動作をするのは、背景画像とか、カラーテーマとか、そんな感じの。
これに対して、マクロの一覧は子メニューではなく、割り当てられたメニューの「直下に」設定するようになってる。これはまあ、alt+T -> (何か) -> (何か) と 3 回キーを押すよりは 2 回の方がいいかなーということなのですが。このあたりの各種リストを子メニューに生成するか、メニューの直下に生成するかの違いが、いまのメニュー構成に依存している部分です。これを統一したほうがいいのか、どっちに統一するのか、あるいは選択可能にすべきか? とか。
それから、複数のアクションが暗黙的にラジオボタン的なグループを形成しているもの。背景画像をそのまま・明るく・暗く・合成あたりのアクションがこれです。ラジオボタンなので、どれか 1 つ選ばれたら他のはクリアしなければなりません。いまは、これをメニュー側の機能で勝手にやってもらっていますが、汎用的にするとなると判定のしかたを微妙に変えることになる。
ということで、いろいろいじってみました。一覧表示系のアクションは、子メニューではなくアクションが割り当てられているメニューの直下に出力するよう統一。とりあえずメニューの構成に依存する処理はなくしたので、メニューを再定義してもだいじょうぶそうです。
あとはどうやって定義するか。。。オートメーションオブジェクトは用意するとして、メニューの定義を拡張子クラスごとにプロパティで保持するとなると、最終的には文字列ベースでシリアライズしないといけない。なんかどでかい文字列になってしまう気が。。。
よく考えてみるとメニューまで Rebar に入れているエディタってあまりないのかな? また無駄な機能を組み込んでしまった。。。
もしかしてメニューのループにも手を入れないとだめなのかな? Explorer だと、隠れているメニューをアクティブにすると(マウスでもキーボードでも)、自動でシェブロンを押したことになりますね。
まあ現状でおかしくないといえばおかしくない(「編集(&E)」とかのメニューが隠れていても、隠れてないときと同じ位置にメニューが出る)ので、とりあえずよしとします。これをいじるとなると、たぶん TToolBar も深く手を入れる必要が。。。
とりあえずバグを片付けよう。
ということでいろいろバグを潰し中。
シェブロンが押されたときメニューを出すわけですが、メニューが階層化されているととてつもなく面倒です。しかも、シェブロンの対象がツールバーだったりメニューだったりするので、余計に混乱します。
だいたい Rebar に共通してるのは、
あたりが、言われてみれば確かにそうだなあという感じの機能だと思います。最初のくらい、ツールバーがそういう機能を持っててもよさそうな気もしますが。。。
あと、シェブロンの幅ってどうやって求めるのかよくわからない。RB_GETRECT や RB_GETBANDBORDERS してもシェブロンは無視されるしなあ。。。とりあえずハードコーディング(12 ピクセル)してしまいました。
めどいこと山の如し。

というか、バグフィクスしてない。。。
ということで、しばらくバグフィクス中心にしてみます。
とりあえず書いたソースを眺めてみる。
ツールバーをたくさん定義したときにシェブロンが出ないな。。。Rebar にしてしまおうかな。ためしにやってみたら、VCL が提供する TCoolbar はシェブロンのことを微塵も考えてないのでした(Delphi6 だから?)。とりあえず隙間をぬって表示させるようにしてみましたが、ウィンドウをリサイズするとものすごくちらつく。。。

いろいろいじってふつうの Rebar にしてみましたが。シェブロンから出したメニューでなにか選択すると、そのあと誰かがマウスをキャプチャしっぱなしになるような? 意味不明。
でも Rebar なウィンドウはなんかハイテクな感じがしますね。
せっかくなので TCoolbar でのシェブロンの出し方。まず、シェブロンを出すためには TCoolbar の private なメソッドを変更しなければならないので、TCoolbar 自体は使えません。VCL のソースから引っ張ってきます。そいで、適当なパッケージに新規コンポーネントとして作ります。なんか情けない話ですが。。。
それから、
あとパッケージは {$R-} しておけばかんぺき。やらないと即死。
まあ、こんなちまちましたことをするよりも、Toolbar2000 とかを使うほうが楽なのかもしれません。
CodeView 2.2。Direct3D を使ったテキストエディタ。Shift+Home とか Shift+End で超なめらかにズームするのがすごいです。でも常時 CPU 使用率 100% なのが。。。
行番号領域の背景色を指定できるようにしてみました。
マクロのインターフェース。マクロのリストは拡張子クラスのプロパティで設定するので、マクロ自体へのアクセスを提供するオブジェクトも拡張子クラスに属するのが自然なのですが、よく考えると拡張子クラスに関するものは App.Prop しかない。
これを、たとえば
App.ExtClasses('plaintext').Prop('macro-list') = '...';
App.ExtClasses('plaintext').Macros(
'touppercase.javascript.txt').Execute();
などのようにすると収まりはいいのですが。拡張子クラスは javascript 側でイベントハンドラを定義するのが主な役目で、exe 側では単なるハッシュリストでしかなく、あまり重要な役割を持ってない。
逆に
App.ExtClasses('plaintext').onKeyPrintable =
function () {...};
のように exe 側から見た拡張子クラスに多くのものを持たせるようにすることも考えられますが、イベントハンドラみたいな生の javascript の関数は、javascript に持たせておいたほうが何かとよいと思います。
ということで、わりときれいじゃないですが、App.Macros というオブジェクトを持たせて、そこからマクロを起動したりするようにしてみます。
ということでそうしてみました。
めあど作ってみました。
どんと来い、スパム。
こんなふうに。
まず、萌ディタを置いておくディレクトリに、'macro' ディレクトリが増えます。この中にマクロを置いておきます。
プロパティ 'macro-list' が増えます。このプロパティに、マクロのファイル名(ただし、パスは 'macro' 以降)を '|' で繋いで指定します。ふつうに継承できるので、拡張子クラスごとに定義するとよいと思います。
マクロを書きます。
// %menu 大文字化(&U)
// %desc 選択範囲をすべて大文字にします
if (App.Caret.Selection.Mode != 0)
App.Caret.Send(App.Caret.Selection.Text.toUpperCase());
else
App.Notice('選択されてないよ。');
%menu 以降の文字列が、メニューのキャプション。%desc 以降が、マクロの概要になる。。。という決まりがあります。このファイルを、'macro' ディレクトリの下に置いておきます。
登録したマクロは、こんな感じでメニューに出てくるので、ここから実行できたりします。
マクロのキャプションや、中身そのものを参照しようとしたときはメモリ上にキャッシュしたタイムスタンプとファイルのそれを比較して、更新されていれば読み直すようになっています。キャッシュするのは最後に使った最大 16 個までのマクロです。
オートメーションオブジェクトの仕様は。。。いま考え中。
最大化した状態で終了すると次回起動時に再現されないのを修正。
アンダーラインなどを適用した行以降がスタイルなしの場合、スタイルがリセットされないのを修正。
このかいはつにっきを blog にしたら何かが楽になるのかしら、と試しに作ってみたのですが。。。ほとばしるめどさ。あんまりスタイルシートとかを自由にいじろうと思ってはいけないものなのかな? あと textarea で込み入ったことをするのは辛すぎます。
めんどくさいので、やめた。ActiveX なコントロールとかで、IE に貼り付けて動く高機能なエディタコンポーネントがあればいいのになー。これ、もしかしてナイスアイデア?
と思ったらいろいろあるんですね。。。ていうか、ぐぐれば山のようにでてきた。
全然関係ありませんが、多機能系にリンクされているのが、な、なんだってー! という感じ。わたしはてっきり、一般的には萌ディタはへぼナイスなメモ帳くらいの位置付けだったりすると思っていました。
で、動的に実行できるスクリプトですが。動的というのは、起動時に評価されるスクリプトに対して動的ということです。起動用のスクリプトは、萌ディタを起動しなおさない限り更新しても反映されませんが、動的なスクリプトは実行中にファイル名を指定してそのつど読み込み(キャッシュはしますけど)、評価する〜みたいな感じです。キーボードマクロもその一種にしてしまえばいろいろとすっきりしそうです。
具体的には、選択範囲を大文字化するとか、コメントにするとかのありがちな処理を書けばよいと思います。
といいつつ、あまり全体のイメージが固まってないので、例によっていろいろなエディタで類似の機能をみてみると、動的なスクリプトのしくみ自体は珍しくなくて、むしろ複数のスクリプトをどのように管理するか、という面に目を向けたほうがよさそうです。たいてい、スクリプトはマクロメニューとかツールメニューに登録することができるようになっていると思いますが、メニューをたどって実行させるのはめんどいのです。
かといって、単純に登録したスクリプトごとにショートカットを割り振れるような仕組みを作ったとして、他のキーバインドの隙間をぬって割り振ることになると思うのでなかなか直感的には覚えきれないような。いまのところ、標準のキーバインドに Alt+Shift とか Alt+Ctrl とかの修飾は使っていないので、その空間を動的なスクリプト専用としてもよいですが。。。なんか押しにくそうだしな。。。
ということで、たとえば ctrl+U を押したらキャレット位置近辺にコンテキストメニューを出して、そこに登録されたスクリプトを出せばいいんじゃないかなーなどと考えてみました。
ということで、
とかを作ることになると思います。そこそこめどそうなので、現時点までの nightly build 出しちゃいますね。。。
ええと、こんな感じに。
まず ctrl+shift+; で記録開始。
適当に操作して、それが記録された場合はステータスバーに出ます。
また ctrl+shift+; で記録完了。
あとは、こんな感じにキャレット位置に出力したり、ファイルに保存できたりするわけです。見てのとおり、そのままスクリプトの形になっています。
ちなみに、やや制限があって、
で、記録されたものがそのまま JScript のコードになっているというところがミソで、動的に実行できるスクリプトみたいなものを平行して考える必要があります。
萌ディタスレにも書きましたが、ロードマップとか。
次は、キーボードマクロか、動的にファイルから読み込んで実行するマクロを実装します。
これでエディタに必要そうで大きいものはだいたい片付くので、その後しばらくは安定化とか、既存機能の改良に重点をおきつつ、細かいものを実装みたいな感じになると思います。たとえば、
あたり。
で、キーボードマクロです。たいていのアプリケーションの場合、動作がコマンド化されて、キーストロークとは直接結びつかなくなっていると思いますが、そうするとキーボードマクロで登録する対象として、キーストロークそのものか、キーストロークによって実行されたコマンドか、のどちらかが考えられます。
また、キーボードマクロを保存する際、生のバイナリとして保存するか、テキストの形で保存するかが考えられます。どっちでもいいですが、どちらかといえば後者のほうが再利用性があってよいと思います。
あと、コマンドがマクロに記録する対象になるかどうか? というのを何で判断するか。バッファに対する操作は当然として、例えば 2 つのビューを表示しているとして、一方のビューのテキストをコピーして、他方のビューに切り替えて貼り付けて、元のビューに戻るなんてマクロを記録したい場合は、「ビューの切り替え」というコマンドも記録の対象としなければいけません。
それから、インタラクティブな操作が入る場合。マクロの記録中に検索ダイアログを表示させて何か入力したら、記録されたマクロの動作はどうなるのか。ダイアログは表示せず、常に入力された文字列で検索を行うのか、ダイアログを毎回開くのか。検索に失敗した場合、マクロの実行を継続するのか。とか。
こうしてみると、キーボードマクロも奥が深いですねー。
とりあえずいろいろなエディタで試してみます。ちなみに最近 .NET Framework を入れてみました。この重さはなんなんでしょうか。。。
| エディタ | 記録の対象 | 保存されたマクロの種別 | インタラクティブな動作(検索ダイアログ) |
|---|---|---|---|
| 1Editor 1.0.7 | キーストローク? | テキスト? | 記録されるっぽいけど、実行するとダイアログを表示したところで中断 |
| Alpha 0.7.5.7α | コマンド? | バイナリ | 記録されない |
| Cannia 1.4.1.0β | コマンド | テキスト(スクリプト) | 入力された文字列を記録 |
| Dana 1.14.01 | コマンド | テキスト(スクリプト) | 入力された文字列を記録 |
| EmEditor 4.01 | コマンド | テキスト(スクリプト) | 入力された文字列を記録 |
| FirstPad 0.21β | コマンド | テキスト(スクリプト) | ダイアログを開く。入力すると後続するマクロを実行 |
| 秀丸エディタ 4.10 β14 | コマンド | テキスト(スクリプト) | 入力された文字列を記録 |
| JmEditor 2.0.24 | キーストローク | テキスト | ダイアログを開く。入力すると後続するマクロを実行 |
| K2Editor r.1.4.21 | コマンド | テキスト(スクリプト) | 入力された文字列を記録 |
| KNTEditor 2.2.16 | JmEditor と同じ | ||
| MKEditor 3.7.8-J | JmEditor と同じ | ||
| NoEditor r.1.13.10 | JmEditor と同じ | ||
| otbedit 3.0.4.1 | キーストローク? | テキスト | 検索ダイアログが表示されず、ダイアログに入力されるべき文字列がバッファに入力される |
| サクラエディタ 1.4.6.0 | コマンド | テキスト(スクリプト) | 入力された文字列を記録 |
| Space editor 2.44 | キーストローク? | バイナリ | 入力された文字列を記録 |
| VxEditor 0.4.5.0 | JmEditor と同じ | ||
| WHiNNY 1.1.1.39 | JmEditor と同じ | ||
| WZEditor 4.00F | コマンド | テキスト(スクリプト) | 入力した文字列を記録。でも実行すると検索ダイアログが開いたり、動作が変です |
| Xerxes β2003/11/30 | キーストローク | テキストっぽいバイナリ | 記録されない |
| xyzzy 0.2.2.229 | コマンド + キーストローク | テキスト(キーストロークを保持したコマンド) | 入力された文字列を記録 |
で、萌ディタの場合どうするかですが。とりあえず記録の対象はコマンド、というかオートメーションオブジェクトのメソッド単位になると思います。バイナリかテキストかは、テキストの形で持ったほうがよさそうです。
インタラクティブな操作は。。。例えば、マクロの記録中に検索ダイアログとかでなんか入力されたら、

のように問い合わせるとか。もともとキーボードマクロって使い捨ての感じがするので、ここまでやるのはちょっと親切すぎるような気も。。。常にマクロの中に文字列を埋め込む形でいいかなあ。
Caret オブジェクトに Marks オブジェクトを実装しました。で、Caret 直下のマーク関連のプロパティ/メソッドが移動します。
| 修正前 | 修正後 |
|---|---|
Caret.MoveToMark(index) | Caret.Marks(index).Move() |
Caret.MoveToNextMark() | Caret.Marks.MoveToNext() |
Caret.MoveToPrevMark() | Caret.Marks.MoveToPrev() |
Caret.SetMark(index) | Caret.Marks(index).Set() |
Caret.DeleteMark(index) | Caret.Marks(index).Reset() |
Caret.DeleteAllMarks() | Caret.Marks.ResetAll() |
Caret.Marks(index).Valid(マークが設定されているとき True) |
同じ感じで Caret オブジェクトに Selection オブジェクトを実装しました。
| 修正前 | 修正後 |
|---|---|
Caret.SelectMode | Caret.Selection.Mode |
Caret.DefaultSelectMode | Caret.Selection.DefaultMode |
Caret.SelectedText | Caret.Selection.Text |
Caret.Selection.Start.Row | |
Caret.Selection.Start.Col | |
Caret.Selection.End.Row | |
Caret.Selection.End.Col |
そういえば仮免をとったことに満足して、免許をとってないことに気が付きました。。。
でも、わたしが免許をとっても、おばちゃん走行(?)しかできませんよ?
マルチストロークについて。esc + a とか、ctrl-Q + z とか、処理を振り分けるキー入力に先立ってプリフィクスとなる入力が必要になる場合、プロパティ '$multi-stroke' と '$multi-stroke-count' を使用します。前者に適当な文字列、後者にプリフィクスとなる入力の回数を設定します(ふつうは 1)。
f.onKeyEscape = function (arg, classname, methodname) {
App.Prop('plaintext', '$multi-stroke') = 'esc';
App.Prop('plaintext', '$multi-stroke-count') = 1;
};
のような感じです。後者は、キー入力してスクリプトが実行された後にチェックされ、カウントが 0 であれば前者を空文字列に更新します。0 でなければデクリメントします。
次に、何か文字を打ったとき、$multi-stroke が空の場合は、onKeyPrintable イベントが発生します。空でなければ、onMultiStroke イベントです。このイベントの中で、$multi-stroke に格納されている文字列と、引数 arg(キー入力された文字)を元に、処理を振り分けます。
f.onMultiStroke = function (arg, classname, methodname) {
switch (App.Prop('plaintext', '$multi-stroke')) {
case 'esc':
switch (arg) {
case 'a':
//esc - a の処理
App.Notice('esc - a');
break;
:
:
表示可能ではないキー入力の場合は、マルチストロークに反応する必要のあるキーのイベントハンドラごとに処理を書きます。
f.onKeyReturn = function (arg, classname, methodname) {
switch (App.Prop('plaintext', '$multi-stroke')) {
case '': //マルチストロークではない
App.Caret.Send('\n');
break;
case 'esc' //esc がプリフィクス
:
:
}
とりあえず、EndUpdate() の第 2 引数は常に「バッファを更新した」で固定、第 3 引数は常に「キャレット移動に伴い選択範囲を変更する」で固定という扱いにして、
var ctx = App.Caret.BeginUpdate; //再描画を禁止
try {
App.Caret.Send('ん?');
App.Caret.Send('あれ?');
}
finally {
App.Caret.EndUpdate(ctx); //再描画を許可
}
という感じで再描画を抑制しつつ編集できるようにしてみました。あんまりめどさが変わってない気がしますが。。。
キャレットとは別に、Buffer.Paragraph(Index) という感じでバッファオブジェクトから直接段落を参照できるようにしてみました。
データ用のハードディスクに不良セクタがぼこぼこあるなあ。
なんか geocities が統合したとかで、頼みもしないのに新しいアドレスができたり、容量が増えたり、ページごとのアクセス解析がついてきたりしました。
そういえばアク解で、たまに海外の大学(スタンフォード大とか、ミネソタ大とか)からくるのは何の間違いなんだろ。。。
スタイルに 'font:bold' とか指定できるようにしてみました。italic も付けてみましたが。。。文字幅はスタイルなしの状態から変えないので、フォントによっては見にくいです。
バッファの描画を一時的に抑制する機能について。exe 側のネイティブな Caret オブジェクトには、すでにそういう機能があるので、スクリプト側に公開すること自体は簡単です。
ただ、そのまま公開すると内部的なフラグ類とか引数がおおいのでうまくオブラートに包まないと使いにくそう。
内部的には、BeginUpdate() 〜 EndUpdate() の組の内部が再描画をしないようになります。で、まず BeginUpdate() を呼ぶとコンテキスト情報を返して、EndUpdate() を呼ぶときにそれを渡すとかのルールがあります。それから、キャレットが移動しただけか、バッファを変更したかの別と、キャレット移動に伴い選択範囲を変更するかのフラグも渡します。
これだけでもめんどいのに、BeginUpdate() 〜 EndUpdate() の組は必ずセットになっていなければならないので、それを考慮しつつ書くとしたら
var ctx = App.Caret.BeginUpdate; //再描画を禁止
try {
App.Caret.Send('ん?');
App.Caret.Send('あれ?');
}
finally {
App.Caret.EndUpdate(ctx, 0, 0); //再描画を許可
}
な感じに。果てしなくめどいですね。。。
関係ありませんが、finally の説明をぐぐってみると「エラーが起きたとき必ず実行される処理を記述する」うんぬん、というのがけっこう多いのですが(MSDN とかもそう)、まぎらわしいので「例外が発生しても発生しなくても必ず実行される処理を〜」、と書いて欲しいと思った。
Lex.Count、Lex.Insert、Lex.InsertKeywords、Lex.Item、Lex.Remove を実装。
Lex.Item は Rule オブジェクトを返し、プロパティ Pattern、Style を通して更新ができます。
'transit:js:3' みたいに、遷移先の解析器の初期状態を指定できるようにしてみました。例えば php のように '<?php' から遷移する場合と '<script>' から遷移する場合とで、戻るためのキーワードを区別しつつ、解析器の中身は共用したい〜なんてときに使うとよいと思います。
解析器間の遷移を *return* で通すようにするにはけっこう頭を使いますね。。。
どうやら EmEditor の場合、<script> 内は language="なんちゃら"(type ではなく)の場合のみ見ているみたい。
秋ということで。
なんかうちのマシン、ブラウザを立ち上げてスクロールしていくと途中で青画面になって落ちるようになってしまったのですが。。。
ハードディスクが寿命なんでしょうか。