WindowsでUWSCやAutoHotkeyを使って外部エディタとしてgvimを起動する

[Vim Advent Calendar 2013](http://atnd.org/events/45072)の115日目(2014-03-25)の記事です。

Windowsでのテキスト編集を、普段使い慣れたエディタでやりたかったので、
外部エディタとしてgvimを起動する[UWSC](http://www.uwsc.info/)スクリプトを作ってみました。
(AutoIt版とAutoHotkey版もあり)

Outlookのメール編集や、IE/Firefox/Chromeのフォーム等で使えます。

なお、同じ方法でgvim以外のエディタも使えます。

以下の処理をするスクリプトです。
+ アプリに`Ctrl + C`を送り付け(クリップボードへのコピー)
+ クリップボードから読んで一時ファイルを作成
+ エディタを起動して一時ファイルを開く。編集終了まで待つ
+ 編集後の一時ファイルを読み込んでクリップボードに
+ アプリに`Ctrl + V`を送り付け(クリップボードからのペースト)

## 準備
1. gvim.exeにPATHを通しておいてください。
   (もしくは、external-editor.uws内でgvimをEXECで起動している部分を
   フルパス指定に変更してください)
2. 一時ファイルを置くディレクトリ(`C:\temp`)を作成。
   (もしくは、external-editor.uws内でtmpfileのパスを変更してください)
3. 拡張子.UWSをUWSCで開くよう関連付け設定
4. external-editor.uwsのショートカットをデスクトップに作成
5. ショートカットのプロパティダイアログを開いて、ショートカットキーを設定(例: `Ctrl + Alt + V`)。
   (一度ログオフしてログオンし直さないとショートカットキーは有効にならないかも)

## 動作
アクティブなウィンドウ上で、設定したショートカットキーを押すと、
external-editor.uwsが起動されます。

external-editor.uwsは以下の処理を行います。

1. `Alt + Tab`を送り付けて、UWSCのウィンドウから直前のウィンドウに戻る。
2. クリップボードへのコピー(`Ctrl + C`)。
   文字列が選択されていない場合は全選択してからコピー(`Ctrl + A`, `Ctrl + C`)
3. クリップボード内容を一時ファイルに書き出し
4. gvimを起動して一時ファイルを開く。編集終了まで待つ
5. 一時ファイル内容を読み込みクリップボードにコピー
6. クリップボードからペースト(`Ctrl + V`)

4.では、新しくgvimを起動します。
既に起動しているgvimで開きたい場合は、external-editor.uws内のEXEC部分を変更してください。
`--remote-wait-silent`や、新しいタブで開く場合は`--remote-tab-wait-silent`

## 制限事項
1. 一時ファイル名が固定なので、同時に複数実行すると、後から実行した側により上書きされます。
   (プロセスIDを`STATUS(id, ST_PROCESS)`で取得して付与はできるが、
   同一ブラウザの各タブや各フォームから起動するとぶつかるし、
   ファイル削除処理(DOSCMDやScripting.FileSystemObject使用)の追加が必要)。
   (AutoIt版では解決済)

2. 一時ファイル置き場が`C:\temp`固定。
   UWSCで環境変数(`TEMP`)を簡単に参照する方法がわからなかったので固定にしています。
   (AutoIt版、AutoHotkey版では解決済)

3. Windows 8.1だと、最初の`Alt + Tab`の送り付けが効かないため動作しない。
   回避策案として、`Alt + Tab`が駄目だった場合に、
   マウスクリックを行う処理をコメントアウトした形で入れてありますが、
   マウスポインタの位置によっては意図通り動きません。
   (その他の回避策として、`Alt + Tab`送り付け後に`Sleep(1)`を入れておいて、
   手で`Alt + Tab`操作をすれば動かせなくはないが面倒)。
   (AutoHotkey版ならOK)

4. アプリ側が以下のショートカットキーに対応している必要あり。
 * `Ctrl + C`でクリップボードへのコピー
 * `Ctrl + V`でクリップボードからのペースト
 * `Ctrl + A`で全選択

5. タブブラウザで、コピー時とは別のタブに切り替えた状態でエディタを終了すると、
   別のタブに対してペーストを行ってしまう。
   対処案は、ウィンドウタイトルが表示中タブを反映して変更されるなら、
   ウィンドウタイトルがコピー時と異なる場合はペーストを中断して、
   かつ一時ファイルを削除しないようにする処理の追加。

## UWSCスクリプト
```:external-editor.uws
// 外部エディタで編集を行う

// uwsc起動直前にいたウィンドウに戻る
KBD(VK_ALT,DOWN,100)
KBD(VK_TAB,CLICK,40)
KBD(VK_ALT,UP,40)
// Windows 8.1だとAlt+Tabが効かないので、マウスクリック。
// XXX: マウスポインタの位置によっては意図しないウィンドウになる場合あり
//IF GETID(GET_ACTIVE_WIN) = GETID(GET_THISUWSC_WIN) THEN BTN(LEFT, CLICK)

// クリップボードにコピー
id = GETID(GET_ACTIVE_WIN)
IF id = -1 OR id = GETID(GET_THISUWSC_WIN) THEN EXITEXIT
saveclip = GETSTR(0) // クリップボード内容を退避
SENDSTR(0, "", 0, TRUE, FALSE) // まず空に。空をCtrl+Cしても古いままなので
SCKEY(id, VK_CTRL, C)
// クリップボードから文字列を取得
// XXX: Sleep無し、かつ既に起動しているgvimが無い場合、
// コピー前のクリップボード内容を取得することあり
Sleep(0.1)
str = GETSTR(0)
// 文字列が選択されていない場合は全選択してコピー
Ifb str = "" then
    SCKEY(id, VK_CTRL, A, C)
    Sleep(0.1)
    str = GETSTR(0)
endif

// 取得した文字列を一時ファイルに書き出す
// TODO: 同時に複数実行時用にユニークなファイル名を付けて、終了時に削除する
// XXX: %Temp%を簡単に参照する方法がわからなかったのでc:\temp固定
tmpfile = "c:\temp\uwscgvim.txt"
f = FOPEN(tmpfile, F_WRITE)
FPUT(f, str, F_ALLTEXT)
FCLOSE(f)

SENDSTR(0, saveclip, 0, TRUE, FALSE) // クリップボード内容を戻す
// gvimを起動して一時ファイルを開く。編集終了まで待つ
EXEC("gvim " + tmpfile, TRUE)
// 既に起動中のgvimで開く場合
//EXEC("gvim --remote-wait-silent " + tmpfile, TRUE)
//EXEC("gvim --remote-tab-wait-silent " + tmpfile, TRUE)

// 一時ファイルを読み込む
f = FOPEN(tmpfile, F_READ)
str = FGET(f, F_ALLTEXT)
FCLOSE(f)

// 読み込んだ一時ファイル内容をクリップボードにコピー
SENDSTR(0, str, 0, TRUE, FALSE)

// クリップボードからペースト
SCKEY(id, VK_CTRL, V)
```

## [AutoIt](http://www.autoitscript.com/site/autoit/)版
Windows 8.1でUWSCを使うと、最初の`Alt + Tab`の送り付けが効かず動作しないので、
AutoItなら動くかと思って試してみましたが同じ状況でした。

制限事項1, 2に関してはAutoIt版では解決済。そのため、準備2は不要です。

```:external-editor.au3
#Include <File.au3> 
Local $sTempFile = _TempFile()

Send("!{TAB}")
Sleep(10)
; XXX: on Windows 8.1, Alt+Tab is ignored
;If WinGetTitle("") == "" Then
;    MouseClick("left")
;Endif
;Sleep(10)
Local $hwnd = WinGetHandle("[ACTIVE]")

Local $saveclip = ClipGet()
ClipPut("")
Send("^c")
Sleep(100)
Local $text = ClipGet()
If $text == "" Then
    Send("^a")
    Send("^c")
    Sleep(100)
    $text = ClipGet()
Endif
FileWrite($sTempFile, $text)
ClipPut($saveclip)

RunWait("gvim.exe " & $sTempFile)
;RunWait("gvim --remote-wait-silent " & $sTempFile)
;RunWait("gvim --remote-tab-wait-silent " & $sTempFile)

Local $sFileRead = FileRead($sTempFile)
FileDelete($sTempFile)
ClipPut($sFileRead)
WinActivate($hwnd)
If $sFileRead == "" Then
    Send("{DEL}") ; delete selection
Else
    Send("^v")
Endif
```

## [AutoHotkey](http://www.autohotkey.com/)版
AutoHotkeyだと、Windows 8.1でも、最初の`Alt + Tab`の送り付けが効きました。
逆にWindows 7では最初の`Alt + Tab`の送り付けが不要。

制限事項2, 3に関してはAutoHotkey版では解決済。そのため、準備2は不要です。
また、4行目のコメントを外して常駐するようにすれば、準備3以降も不要です。

```ahk:external-editor.ahk
; for Windows 8.1
;SendInput !{TAB}
; for persistent script
;^!v::
target_wid := WinExist("A")
saveclip := Clipboard
Clipboard := ""
SendInput ^c
ClipWait, 0.1
if (Clipboard = "")
{
    SendInput ^a^c
    ClipWait, 0.1
}
tempfile := A_Temp . "\ahkexted.tmp"
file := FileOpen(tempfile, "w")
if !IsObject(file)
{
    MsgBox Cannot open "%tempfile%" for writing.
    return
}
file.Write(Clipboard)
file.Close()
Clipboard := saveclip

RunWait "gvim " %tempfile%

file := FileOpen(tempfile, "r")
if !IsObject(file)
{
    MsgBox Cannot open "%tempfile%" for reading.
    return
}
Clipboard := file.Read()
file.Close()
FileDelete, %tempfile%

WinActivate ahk_id %target_wid%
WinWaitActive, ahk_id %target_wid%, , 2
if (Clipboard = "")
    SendInput {DEL}
else
    SendInput ^v
return
```

## 参考
* [Use gvim as an external editor for Windows apps - Vim Tips Wiki](http://vim.wikia.com/wiki/Use_gvim_as_an_external_editor_for_Windows_apps):
  vbscript版、AutoIt版、AutoHotkey版等あり。

* UNIX用
 * [Use gvim as an external editor for Linux apps - Vim Tips Wiki](http://vim.wikia.com/wiki/Use_gvim_as_an_external_editor_for_Linux_apps):
   xautomation等を使ってクリップボード経由で一時ファイルを編集。
 * [uim-external-editor](https://github.com/deton/uim-external-editor):
   IMのsurrounding text APIを使って文字列を取得して一時ファイルを編集。

* [tsf-vim](https://gist.github.com/deton/7393058):
  Windows用IMEとしてVi風操作を実装してみたのですが、
  やはりvimに比べると足りない機能(`H`,`L`やマーク等)が多くていまいち使いにくいので、この記事を書きました。