sci

最果て風呂

mto の VSCode 機能拡張を作成する

deno による TypeScript スクリプトの作成がうまくいったので、長年考えていた VSCode 機能拡張版を作成することにしました。

"Hello, World!" のチュートリアルを終えたのですが、これはコマンドパレットからコマンドを実行し、ポップアップウィンドウにメッセージを表示するというものです。書かれている通りに進めていけば入門は完了です。

しかし、どの言語でもそうなのですが、「動いた!で?ここから先はどうやって進めばいいの?」って思うのです。

文字列を変換する機能は deno 版ですでに作ってあるのですが、「エディタ上で選択範囲の文字列を取得する」部分と、「自作の関数で変換した文字列でそれを置換する」部分がわかりません。

日本語で検索しても思ったような情報にたどり着けません。英語で text, selected, replace などで検索して、GitHubMicrosoft による VSCode Samples を発見しました。この中に自分のやりたいことと合致しているものがあったので、これをそのまま使うことにしました。

関数の実装

本来は vsc-mto プロジェクトとして新規に yo code から始めるべきなのですが、そうするとデータ通信量を消費してしまい、新年早々に速度制限なんてことになったらたまりません。入門で使った HelloWorld を再利用しました(まぁ雛形ですし)。

まずは extension.ts に関数を書いていきますが、deno で書いたものがそのまま動きました。

// https://github.com/microsoft/vscode-extension-samples/tree/main/document-editing-sample
// 上記を参考に(というか○パク)してエディタで選択した範囲の文字列を取得する
// これを自作の関数に渡して変換して戻し、選択範囲の文字列を置換する

// 新仮名遣いを旧仮名遣いに変換
const modernToTrad = vscode.commands.registerCommand('vsc-mto.modernToTrad', function () {
const editor = vscode.window.activeTextEditor;
if (editor) {
    const document = editor.document;
    const selection = editor.selection;
    const selectedString = document.getText(selection); // 選択した文字列を取得
    let text = modernToTradFunc(selectedString);        // 自作の関数に渡して戻す
    editor.edit(editBuilder => {
        editBuilder.replace(selection, text);           // これをエディタ上で置換
    });
}

...

// 新仮名遣いから旧仮名遣いへ変換
function modernToTradFunc(text:string): string {
    let buf:string =  text;
    for (let i = 0; i < kanaArray.length; i++) {
        buf = gsub(buf, kanaArray[i][0], kanaArray[i][1]);
    };
    return buf;
}

registerCommand() への第 1 引数(ここでは 'vsc-mto.modernToTrad')が次のメニュー実装の "command" に渡すコマンド名になります。

メニューの実装

エディタからこれらの関数を利用するためには package.json に記述する必要があります。activeationEvents には機能拡張が有効となるファイルタイプを 4 つ記述しました。("*" とするとパッケージ化の際に怒られます)

"activationEvents": [
    "onLanguage:markdown",
    "onLanguage:html",
    "onLanguage:tex",
    "onLanguage:plaintext"
],
...

しかしこれ、例えば hoge.md を編集してひとたび機能が有効になると、別のファイルタイプ(fuga.ts)を編集していても有効になったままなんです。Emacs の mode や Vimプラグインとは挙動が違うので悩みます。自分で deactivate しないといけないのかな?

コンテキストメニューに機能を登録するため、"editor/context" に記述します。これで文字列を選択し、メニューからコマンドを実行することができるようになります。

"contributes": {
    "commands": [
        {
            "command": "vsc-mto.modernToTrad",
            "title": "新仮名から旧仮名へ"
        },
...

"menus": {
    "editor/context": [
        {
            "when": "editorHasSelection",
            "command": "vsc-mto.modernToTrad",
            "alt": "",
            "group": "navigation"
        },
...

以上で変換する機能の実装は終了です。

データの取り扱い

最後まで時間がかかったのが変換辞書の扱いについてです。当初は辞書原本をそのまま持ち込んで、機能拡張内の関数で内部辞書(配列)にしようと思っていたのですが、パス関係(ファイルをどこに置いて参照すれば良いのか)がよくわかりませんでした。

そこで、Sinatra 版でやっているように、別途変換プログラムで二次元配列を出力し、変数名をつけてそのまま配列として読み込ませることにしました。

現在では辞書の増強はほとんどありませんので、昔のように変換の都度、内部辞書を新規生成する必要はなくなりました。エディタの起動時に読み込む形で十分だと思います。

辞書には .json の拡張子をつけて const kanaArray = require('./kanajisyo.js') のようにして読み込むことができ、期待通りの動作をしました。しかし、TypeScript のコンパイラout/ に出力されるものが extension.js のみという点に不安を感じてしまいました。(実際はどうかわからないけれども)パッケージ化する際に辞書が漏れてしまうのではないかと。

export をつけるとモジュール化することができて import により読み込むことができるようになるそうです(わかってない)。そのため、辞書の拡張子を .ts に変更し、配列名を次のようにして取り込む (import { kanaArray } from "./kanajisyo";) ことにしました。(ただのデータなのにモジュールとは?)

export const kanaArray: Array<[string, string, Array<string>]> =
[
  ["植え", "植ゑ", []],

これでコンパイル時に extension.js と共に kanajisyo.js も出力されるようになりました。動作も問題なしです。

パッケージ化するには npmvsce をインストールしなければならないのですが、どのくらいの通信量になるのか不安でまだ実行していません。

追記 (2022.01.10)

vsce をダンロードしてパッケージ化しました。

> vsce package
Executing prepublish script 'npm run vscode:prepublish'...

> vsc-mto@0.0.1 vscode:prepublish
> npm run compile

> vsc-mto@0.0.1 compile
> tsc -p ./

ERROR Couldn't detect the repository where this extension is published. The image 'images/vsc-mto.gif' will be broken in README.md. GitHub/GitLab repositories will be automatically detected. Otherwise, please provide the repository URL in package.json or use the --baseContentUrl and --baseImagesUrl options.

いきなりエラーです。README.md ファイルに説明のためのスクリーンキャスト↓

f:id:nakinor:20220112211948g:plain

を使ったのですが、サイトどこやねんって言われています。いやいや、README には画像とか使って説明すると親切だよって言うたのは自分ちゃうんかい?リンクは https:// で始まるものでなければならないようです。仕方なくコメントアウトして続行します。

>vsce package
Executing prepublish script 'npm run vscode:prepublish'...

> vsc-mto@0.0.1 vscode:prepublish
> npm run compile

> vsc-mto@0.0.1 compile
> tsc -p ./

WARNING A 'repository' field is missing from the 'package.json' manifest file.
Do you want to continue? [y/N] y
WARNING Using '*' activation is usually a bad idea as it impacts performance.
More info: https://code.visualstudio.com/api/references/activation-events#Start-up
Do you want to continue? [y/N] y
WARNING LICENSE.md, LICENSE.txt or LICENSE not found
Do you want to continue? [y/N] y
DONE Packaged: /opt/vsc-mto/vsc-mto-0.0.1.vsix (12 files, 302.75KB)

repository 項目が無いよ?アクティベーションされるモードに '*' が指定してあるけど、エディタのパフォーマンスが悪くなるからそういうのやめて?LICENSE ファイルが無いよ?と 3 つの警告が出ていますが、VSIX ファイルは生成されました。

repository は無視して、LICENSE ファイルを追加し、"activationEvents""onLanguage:markdown" 等を指定して再度パッケージを作成しました。

VSCode のエクステンションメニューから install from VSIX... を選択し、生成されたものを指定すればインストールが完了します。

追記 (2022.01.14)

辞書の要素数を表示する機能を追加し、コマンドパレットから実行できるように "activationEvents" 項目に "onCommand:vsc-mto.showDictSize" 等を追記しました。これで ⌘-Shift-P でコマンドパレットを表示し、>vsc-mto.showDictSize と実行すればポップアップメッセージに要素数が表示されます。一度実行すれば "recently used" として上位に出てくるので再実行が簡単です。

ちなみに >vsc-mto.modernToTrad なども実行することができるので、マウスを使わずキーボードだけでも操ることができます。

README から説明のための gif ファイルを削除しました。これで 303KB あった vsix のサイズが 48KB にダイエットできました。

まとめ

この機能拡張は Visual Studio Code 上で TypeScript により作成しましたが、編集時にエラーや警告を指摘してくれるのでとてもありがたい(うざい)です。8 年落ちの Mac ですが若干のカクツキがあるものの Xcode よりは軽く、実用することが可能でした。

辞書データをモジュールとして読み込むために export をつけたり、タプルにすればいいのに Array<[string, string, Array<string>]> なんて面倒な型指定をしたり、そもそも JavaScript には tuple はないんだからとか、もう面倒だから any にしちゃえとか、変更しないんだから readonly でいいじゃん?でもつけるとエラーになるぞ?とか、わけがわからないままな状態です。

旧字旧仮名の文章を変換する辞書に手をつけてから 10 年以上が経過しました。作成当初は旧仮名にかなりの違和感がありましたが、5 年を過ぎてまぁまぁ、現在ではぼぼ違和感なく読むことができるようになりました。

自分自身にとってはすでに不要なものとなっているのですが、現在でもあやふやな部分がありますし、使い続けるつもりです。

ウェブブラウザで使える変換サイトは好評のようで、dyno 不足で月末に停止することが多くなりました。ログは取れないので、訪問しているのは クローラーbot だけという可能性も?ありますが、変換の需要はあるのではなかと思っています。

旧字旧仮名に馴染のない人が手軽に触れることができ、何回も、何日も、何年も触れ続けて、いつかこれが不要になるくらいに身につけてもらえたら嬉しいな。