netclip: SSH-native clipboard bridge

SSH 越しにローカルクリップボードへコピーしたい、という要求は昔からある。OSC52 を使う方法もあるし、実際今回も改めて試してみた。ただ、私の環境では GNU screen を挟むことが多く、少し大きめの payload を扱うと安定性と信頼性の面で不安が残った。

私自身も2年前、netclip というツールを自作していた。当時使えそうな OSS を探してみたけれど、どれも自分の用途には合わなさそうだったので、結局自作することにした。作った当初は使っていたものの、段々と使わなくなった。最近また同じことをやりたくなってきたので、今回改めて設計し直して作り直した。この記事では、その過程とツール紹介を兼ねて整理してみる。

なおリポジトリはこちら

https://github.com/kamichidu/go-netclip

初版 netclip

当時の設計前提

当時は SSH を前提にしたくなかった。複数端末間でクリップボードを共有したいし、daemon を置きたくない、あるいは置けない環境も扱っていた。copy だけでなく paste も含めた双方向の利用を想定していた。

実装

その結果、実装は gRPC を中心に据え、Firestore や GCS を backend に利用する構成になった。イベント通知を受けて処理を振り分け、必要になったら後から pull して取得する。

結果

ただ、結局は自分でも使わなくなった。

利用シーンが変わったこともあるが、単純に体験設計と実際のユースケースがずれていたと思う。欲しいものに対して、少し回り道が多かったし、動かないときのトラブルシュートも面倒だった。

今回の再整理

設計前提を変えた

作って使って、使わなくなった理由を改めて整理した。支配的なユースケースだけを取り出し、そのための最小コストを目指すことにした。

頻度の低いユースケースは、ツール自身ではなく周辺インフラで吸収する前提にした。複数端末間での共有や Cloud Shell のような環境であれば、Cloud Run に配置して IAM で保護し、GCS を backend にすればよい。それを netclip 自身の機能として持つ必要はない。

代わりに、SSH を前提として受け入れ、小さなローカルdaemonを SSH セッションに対して透過的に起動することにした。環境に依存する部分も、どこへ依存を押し込むのが一番自然な体験になるかを優先して整理した。

整理した責務配置

SSH:
  transport
  authentication
  session lifecycle
HTTP:
  protocol
curl:
  client
netclip:
  clipboard adapter

結果として、netclip が持つ機能はかなり小さくなった。

仕組み

図は README に掲載している Mermaid をみてください。

https://github.com/kamichidu/go-netclip/blob/main/README.adoc

リモート側では curl が Unix Domain Socket (UDS) に対して HTTP で投げ、それを RemoteForward でローカル側へ転送する。ローカルでは netclip daemon が待ち受け、最終的に pbcopyclip.exewl-copy のような OS ごとの clipboard command を呼び出す。

UDS を使っているのは、SSH 先で固定スクリプトで接続するため。TCP ポート 0 を使った案もあったが、SSH 側で動的ポートを取得するために機能が増えるため、固定ポートを利用し、UDS を使って隠蔽するようにした。

セットアップ

SSH 設定、shell helper、利用例を示す。

Host *
  PermitLocalCommand yes
  LocalCommand netclip daemon --listen 127.0.0.1:45555 --background
  RemoteForward /tmp/netclip-%r.sock 127.0.0.1:45555
  StreamLocalBindUnlink yes
  ExitOnForwardFailure yes

この設定によって、SSH するだけでローカル側に netclip daemon が透過的に起動し、待ち受けポートを UDS で隠蔽するところまで OpenSSH の処理に隠せる。しかも一度設定したら外部環境起因での変化は最小になる。

netclip() {
  local sock="${NETCLIP_SOCK:-/tmp/netclip-${USER}.sock}"

  curl --silent --show-error --fail \
    --unix-socket "${sock}" \
    -X POST \
    --data-binary @- \
    http://netclip/copy
}

このヘルパ関数を用意、またはファイル化しておくことで、基本的にメンテナンスフリーで利用できる。

ここまで来れば、git diff | netclip のように普段の操作に組み込んで使い出せる。

制約事項

現状では、同一ユーザーによる複数 SSH セッションを明示的には扱っていない。後から接続したものが socket を取り直すため、必要であれば socket path を分離して利用する。

また、リモート側では curl --unix-socket が利用できることを前提としている。

まとめ

今回やったことは、実装を書き直したというより、前提条件を整理し直しただけだった。まぁ実装も捨てて書き直した訳だが。

SSH を前提にし、ローカルdaemonを許容すると、認証も通知も永続化も不要になり、最小機能で目的を達成できた。

これなら、使っていることを忘れるような体験にできそうな気がしている。しばらく実運用してみて、改めて評価したい。

A sincere fu*k to all ambiguous widths.

私はもう長いことSKKを使っています。WindowsではSKKFEP、MacではAquaSKK。業務上必要であれば普通のIMEも使う訳だが、一番思考と直結した感覚で使えるのがSKKなのは変わってません。

ところで私はシェルの住民である。ブラウザなんかも使うが、日々の業務のほとんどをシェル上で行っている。ターミナルエミュレータ上でシェルを使っていると避けて通れないのが曖昧幅文字の文字幅設定ですね。SKKの都合で2固定です。

曖昧幅文字というのは東アジア圏では頻出しますが、英語圏だとほぼ無関係になるというしょうもない問題です。みなさんも何かコマンド叩いたときに、文字表示がずれたり崩れたりといった現象を見たことあるかもしれません。

で、最近AIが業務に入ってきまして。 Gemini CLIを使ったところ、曖昧幅文字によって画面崩壊の憂き目に遭いました。 そう、ずれ程度ではないんです、崩壊です。文字通りの。 細かい現象はスクショ見てもらうとして、それで使うの嫌で半年以上ブラウザ版Geminiを使ってたんですが、そろそろなんとかなってるかと業務上の必要もあって久しぶりにGemini CLIを起動したんですね。崩壊ですね。

Issueを探したところ、直近のものがありました。 既知問題だった訳です。

https://github.com/google-gemini/gemini-cli/issues/15585

なんということでしょう、Issueコメントを見てもすぐに直りそうにありません。 使いたいときに使えない、厳密には使いものにならないほど壊れた画面を見続けるのは苦痛です、なのでもうサクッと自分で直すことにしました。

ちなみにフォント設定、ターミナルエミュレータ変更は試しましたが、なんでたかがAIツールを使うのに私のやり方を変えないといけないのかと。 forkも論外、なんでAIを使うだけでこの先もメンテしたりしないといけないのか。

SKKから通常IMEに乗り換えれば曖昧幅を1にできるので再現しないんですが、なぜたかがAIのためにSKKを捨てなければならないのかと。何様やと。

ということで、この際だからPTYを噛ませて、曖昧幅だけ吸収するレイヤを挟む汎用ツールとして仕立てることにしました。 そうして faw コマンドを作るに至りました。 細かい使い方とかはREADME読んでください。

https://github.com/kamichidu/go-fu-kin-ambiwidth

before after

実装としては、シンプルです。 PTYを使い、文字列を監視し、曖昧幅文字を設定ベースで置き換える。 未知の文字が出てきたら捕捉して設定ファイルに書き出すオプションもつけてます。 一度見たら二度はない、衛生害虫並みの扱いですね。 そのくらいで、さすがAIさま!となる実用度になりました。

公開した後で、参考程度にIssueにコメントを残しましたが、まぁ私は善意の塊ですし?世の中のために少しは役立てればと。

これで私の環境の平和は保たれました。 もう faw なしでの生活は考えられません、非常に良いものを作ったと満足です。

その後、別の方がPRを作ってくれましたが、最終的にはrejectという判断になったっぽいです。 まあ、影響範囲もでかいし、なんならGemini CLIではなくInkの問題ですしねぇ。

https://github.com/google-gemini/gemini-cli/pull/26041

少しだけ趣深い話をすると、 faw のREADME序文は faw を噛ませたGemini CLIに英訳してもらいました。 たかがAIのために作ったんだから、少しは役立てと。 溢れ出る呪詛を凝縮してプロンプトに食わせて、熱量を保持したまま英語にせよと命じたところ、社会的に配慮された文章が返ってきました。 すかさず私は、レビューして直させました。

今回のタスクにおいて、社会的配慮と感情的配慮を取り違えてはいけません、今のタスクにおいては元の熱量を維持することが配慮です。つまり unfortunately などではなく fuck とするほうが良いです。

うん、良い仕事した。

そんな経緯で生まれた faw ですが、さきほどお伝えしたようにもはや私にとって呼吸と同じような立ち位置に座っています。 細かい挙動とかは気が向いたら直す可能性もありますが、もう安定してるので大きな変更は予定してません。 世の中の誰か、特に東アジア圏で曖昧幅文字でお困りの方にとっての最終兵器となりますように。

A sincere fu*k to all ambiguous widths.

とても善良で模範的な日本人らしい締めで、お後がよろしいようで。

Lima で Artifact Registry に接続する

仕事で m3 mac 使ってて、 Lima を使ってコンテナを利用している。 Artifact Registry への認証の仕組みを整備しているので、備忘のためメモ。

前提は direnv 使ってディレクトリごとに認証情報とか設定を環境変数で切り替えてる。 Google 認証情報も Application Default Credentials (ADC)主体。

環境

  • macOS Sequoia
  • Lima 2.0.3

方針

  1. ホスト側で ADC を管理する
  2. Lima VM へ環境変数を伝搬する
  3. VM 内では docker-credential-gcr を利用する
  4. Artifact Registry 認証は docker-credential-gcr に委譲する

つまり認証情報はホスト側へ集約。lima vmは参照のみ。

構成は以下:

Host
 +- ADC
 +- wrapper script
 +- Lima

Lima VM
 +- docker-credential-gcr
 +- nerdctl + containerd / docker + dockerd

Lima VM の準備

VM 内へ docker-credential-gcr を導入する。これは Lima VM のスタートアップスクリプトを仕込む。 またホスト側のホームディレクトリはマウントしておく。 ADC の application_default_credentials.json はホーム以下に存在する前提。 docker-credential-gcr は latest を取得しているため、再現性を重視する場合は取得するリリースを固定する。

# lima.yaml
provision:
- mode: system
  script: |
    #!/bin/bash
    
    set -e -u -o pipefail
    
    # Global variable for secure temporary directory (for exit trap scope)
    __temp_dir=""
    
    # Check if a list of required commands are installed and available
    check-required-commands() {
        local cmd
        for cmd in "$@"; do
            if ! command -v "${cmd}" >/dev/null 2>&1; then
                printf "ERROR: '%s' is required but not installed.\n" "${cmd}" >&2
                return 1
            fi
        done
    }
    
    # Fetch the latest release asset URL for the target repository
    get-latest-asset-url() {
        local api_url='https://api.github.com/repos/GoogleCloudPlatform/docker-credential-gcr/releases/latest'
        local url
        url="$(curl -sSf "${api_url}" | jq -r '.assets[] | select(.name | contains("linux_arm64")) | .browser_download_url')"
        if [[ -z "${url}" || "${url}" == "null" ]]; then
            return 1
        fi
        printf "%s\n" "${url}"
    }
    
    main() {
        # 1. Verify required commands are available
        if ! check-required-commands jq curl tar; then
            return 1
        fi
    
        # 2. Fetch the latest release asset URL via helper function
        printf "Fetching latest release metadata from GitHub API...\n"
        local download_url
        if ! download_url="$(get-latest-asset-url)"; then
            printf "ERROR: Failed to find asset in the latest release.\n" >&2
            return 1
        fi
        printf "Found asset URL: %s\n" "${download_url}"
    
        # 3. Create a secure temporary directory and assign to global variable
        __temp_dir=$(mktemp -d -t docker-credential-gcr-bootstrap-XXXXXX)
        # Ensure cleanup of the temp directory on exit or error (runs in global scope)
        trap '[[ -n "${__temp_dir}" ]] && rm -rf "${__temp_dir}"' EXIT
    
        # 4. Extract archive name from the URL and download/extract it inside the temporary directory
        local archive_name
        archive_name=$(basename "${download_url}")
        printf "Downloading asset '%s' to temporary directory: %s\n" "${archive_name}" "${__temp_dir}"
        curl -sSfL -o "${__temp_dir}/${archive_name}" "${download_url}"
    
        printf "Extracting archive...\n"
        tar -xzf "${__temp_dir}/${archive_name}" -C "${__temp_dir}"
    
        # 5. Move the binary to /usr/local/bin directly (mv fails strictly if file is missing)
        local binary_name="docker-credential-gcr"
        local dest_dir="/usr/local/bin"
        printf "Moving binary to %s ...\n" "${dest_dir}"
        mv "${__temp_dir}/${binary_name}" "${dest_dir}/"
        chmod +x "${dest_dir}/${binary_name}"
        printf "Successfully installed docker-credential-gcr to %s/%s!\n" "${dest_dir}" "${binary_name}"
    }
    
    main "$@"
- mode: system
  script: |
    #!/bin/bash

    set -eux -o pipefail

    docker-credential-gcr configure-docker --registries asia-northeast1-docker.pkg.dev,asia-northeast2-docker.pkg.dev

Lima VM を起動して、 ~/.docker/config.json に以下の認証ヘルパが登録されていたらOK:

{
  "credHelpers": {
    "asia-northeast1-docker.pkg.dev": "gcr",
    "asia-northeast2-docker.pkg.dev": "gcr"
  }
}

環境変数の伝搬

ホスト側でラッパースクリプトを用意し、パスを通しておく。 これで単にホストからラッパースクリプトを叩くだけで、透過的に扱える。 以下の例では、 lima コマンドに渡すコマンド exec に対して、変数をコマンドスコープで指定している。

#!/bin/bash

# lima whichなど、limaコマンドを指定コマンド実行前に実行すると、STDINが消費されてしまう
# 明示的に退避が必須
__command='nerdctl'
if lima which docker </dev/null >/dev/null 2>&1; then
    __command='docker'
fi
__env=()
if [[ -n "${GOOGLE_APPLICATION_CREDENTIALS:-}" ]]; then
    __env+=("GOOGLE_APPLICATION_CREDENTIALS=${GOOGLE_APPLICATION_CREDENTIALS}")
fi
exec lima "${__env[@]}" exec "${__command}" "$@"

動作確認

ホスト側で以下のように、Artifact Registry から pull & push を実行する。

docker pull asia-northeast2-docker.pkg.dev/PROJECT/REPOSITORY/IMAGE:TAG
docker push asia-northeast2-docker.pkg.dev/PROJECT/REPOSITORY/IMAGE:TAG

メモ

Lima VM 内へ Google Cloud SDK を導入する構成も考えられるが、以下の理由で筋悪いなと思って採用してない。

  1. VM ごとに gcloud を管理したくない
  2. VM ごとに認証状態を持たせたくない
  3. 認証情報の管理箇所を増やしたくない

また、ADC ファイルを VM 側へコピーする構成も論外。認証情報を複製したくなかったため、ホスト側で管理する前提とした。

まとめ

  1. ADC はホスト側で管理
  2. Lima VM へ環境変数を伝搬
  3. docker-credential-gcr を利用
  4. Artifact Registry 認証は helper に委譲

これでホスト側の認証情報を管理するだけで、Lima VM 側に透過的に認証状態を参照させることが可能。 Lima VM はたまに再作成するので、気軽に消せるようになって大変よろしい。

参考文献

  1. https://github.com/lima-vm/lima/blob/v2.0.3/templates/default.yaml
  2. https://github.com/googlecloudplatform/docker-credential-gcr

VimでJavaを書く方法 2026年版

2026年現在、 Vim で Java を書く方法を調べると、ほぼ確実に Eclipse JDT Language Server (JDT LS) に行き着きます。 補完、定義ジャンプ、リファクタリングなど、Java開発に必要な機能は一通り揃っており、非常に有力な選択肢となります。

では他の選択肢を探してみます。

なんということでしょう、見つけられませんでした。

もちろん過去には eclim や javacomplete など様々な試みがありました。しかし2026年現在、積極的に選択肢として挙げられるものは JDT LS くらいです。

困りました。 比較記事を書こうと思ったのですが、比較対象がありません。 比較記事を書くには、まず選択肢を列挙する必要があります。

そこで go-jls という選択肢を1つ、新たに作成しました。 折角なので、自分が欲しいものを作ることにしました。 面倒なので、細かいことはリポジトリの README をご参照ください。

https://github.com/kamichidu/go-jls/blob/v0.0.0/README.adoc

さて今日の記事では、

  • Vim + JDT LS
  • Vim + go-jls

この2つを比較してみます。 2026年6月12日時点での比較となります。

JDT LS は Eclipse JDT を利用した Language Server です。 そのため Java の解析や補完については、長年 Eclipse が蓄積してきた資産を利用できます。

JDT LS が強い理由も、ある意味では単純です。 元になっている Eclipse が強いからです。

ところで JDT LS って実質 Eclipse を持ってきてるんですよね。 私自身 Eclipse にはかなり思い入れがありますし、なんなら今でも使っています。 Java をがっつり書く必要があるときは大体 Eclipse でした。

Eclipse Rich Client Platform を利用した業務アプリケーション開発なんかもやっていました。 あれは良いですね。 Java の機能が一通り使えますし、GUI 部品なんかも良い抽象度で提供されています。 カスタム部品なんかも作っていました。 パラメータとか全部 Object 型でしたけどね。

そんな訳で Eclipse に対する印象はかなり良いです。 JDT LS についても同様で、2026年現在の標準的な選択肢と言って良いでしょう。

比較するとこんな感じです。

項目 JDT LS go-jls
補完
定義ジャンプ
自動 import
リファクタリング ×
Maven / Gradle 連携
エラーチェック ×
Vim 親和性
  • ○ = 対応
  • △ = 一部対応または制限あり
  • × = 未対応

面倒なので、細かい機能紹介とかは世の中にたくさん記事があります。そちらをご覧ください。 しかし JDT LS は本当に強い選択肢です。導入は面倒ですし、私がシェル世界の住人であるため、起動から動作するまでが結構かかる印象です。なので使わなくなっちゃいました。 基本的に Eclipse 起動してます。

一方で、go-jls でも私が普段欲しい機能は一通り揃うようになってきました。 go-jls は私が作成した Java Language Server です。 まだ未実装の機能もありますし、JDT LS と比較すると足りない部分も多くありますし、万人向けではありません。 ただ自分で作って使っていますが、今のところ私は特に困っていません。 このサクサクと思考を邪魔しない動作が私は好きです。

さて、これで Vim で Java を書く選択肢が1つ増えました。 良かったですね。

参考文献:

vim-javim の実装範囲

前回 は Pure Vim script で Java Virtual Machine (JVM) を実装し、Vim から Java を実行する方法を紹介しました。

Java を動かそうとすると、Garbage Collection (GC) やスレッド管理など様々な構成要素が登場します。 ただし、Java を動かすための最小実装はもっと小さいです。

本記事では、公開時点の vim-javim の実装範囲について整理します。

https://github.com/kamichidu/vim-javim/tree/54711c41b96d0e5578a8a5afab83493abeebdc4f

実装状況

実装状況は下表の基準で分類しています。

Status Meaning Description
Full 対象範囲を実装済み vim-javim の現在の実行モデルで必要な範囲を実装
Partial 部分実装 HelloWorld / Fibonacci など、公開時点の実行対象に必要な範囲を中心に実装
Unimplemented 未実装 公開時点では未対応

公開時点の実装状況を下表に示します。

JVMS Section Area Status Note
4 Class File Format Partial class file の基本構造を読み込む。対応属性や検証は限定的
4.3 Descriptors Full method descriptor を解析し、引数型と戻り値型を扱う
4.4 Constant Pool Partial "Utf8", "Integer", "Long", "Class", "String", "Fieldref", "Methodref", "NameAndType" などを扱う
4.5 Fields Partial field 情報を読み込み、static / instance field の解決に利用
4.6 Methods Partial method 情報を読み込み、"Code" attribute を実行対象として扱う
4.7 Attributes Partial "Code" を中心に扱う。"LineNumberTable" など実行に不要な属性は読み飛ばし
5.3 Loading Partial classpath directory / jar extraction cache / built-in runtime から class を読み込む
5.4 Linking Partial 検証は行わず、必要な解決処理と static field へのデフォルト値割り当てを実装
5.5 Initialization Full "()V" を class loading 時に実行
2.5 Runtime Data Areas Full heap, method area, static fields, pc, stack / frame 相当を Vim のデータ構造で管理
2.6 Frames Full Local Variables と Operand Stack を frame 単位で管理
2.9 Special Methods Partial "" と "" を扱う
2.10 Exceptions Unimplemented "athrow" や例外伝播は未実装
2.11, 6 Instruction Set Partial int / reference 系の基本命令、分岐、field access、method invocation を中心に実装
2.8 Floating-Point Arithmetic Unimplemented float / double 系の演算は未実装
2.5.6 Native Method Stacks Unimplemented native method stack は未実装
2.11.10 Synchronization Unimplemented monitor / synchronization 系は未実装

詳細な対応状況については、以下のリンク先を参照:

実装対象選定

まず動くJVMを作ることを優先し、公開時点では実行対象の Bytecode から必要仕様を逆算する方針を取りました。

Bytecode は "javap" コマンドで確認できます。

HelloWorld

例えば、前回利用した HelloWorld を見てみます。

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

"javap -c" の結果は以下:

Compiled from "HelloWorld.java"
public class test.classes.HelloWorld {
  public test.classes.HelloWorld();
    Code:
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return

  public static void main(java.lang.String[]);
    Code:
         0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #13                 // String Hello, World!
         5: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
}

HelloWorld の "main()" では、

  • "java.lang.System.out"
  • "java.io.PrintStream.println(String)"

が利用されています。 また、コンストラクタでは "java.lang.Object.()V" が呼び出されています。

そのため、 Opcode の実行だけではなく、少なくとも以下の機能が必要になります。

  • ClassFile Format
  • Constant Pool
  • Frame
  • Operand Stack
  • Local Variables
  • Static Field Resolution
  • Method Invocation
  • Java SE Runtime

Fibonacci

次に Fibonacci を見てみます。

public class Fibonacci {
    public static int fib(int n) {
        if (n <= 1) {
            return n;
        }

        return fib(n - 1) + fib(n - 2);
    }
}

"javap -c" の結果は以下:

Compiled from "Fibonacci.java"
public class test.classes.Fibonacci {
  public test.classes.Fibonacci();
    Code:
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return

  public static int fib(int);
    Code:
         0: iload_0
         1: iconst_1
         2: if_icmpgt     7
         5: iload_0
         6: ireturn
         7: iload_0
         8: iconst_1
         9: isub
        10: invokestatic  #7                  // Method fib:(I)I
        13: iload_0
        14: iconst_2
        15: isub
        16: invokestatic  #7                  // Method fib:(I)I
        19: iadd
        20: ireturn

  public static void main(java.lang.String[]);
    Code:
         0: getstatic     #13                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: bipush        10
         5: invokestatic  #7                  // Method fib:(I)I
         8: invokevirtual #19                 // Method java/io/PrintStream.println:(I)V
        11: return
}

Fibonacci では、

  • 条件分岐
  • 整数演算
  • static method invocation

が利用されています。 また、"fib()" は再帰呼び出しを行います。 そのため、method invocation ごとに独立した実行状態を保持する必要があります。 公開時点の vim-javim では、このための Frame および Operand Stack を実装しています。

上述の通り、HelloWorld や Fibonacci の実行に必要な範囲だけを見ると、JVMS 全体のうち実際に必要となる仕様は限定的です。 そのため、公開時点では必要仕様を優先して実装しています。

Java を動かすためのその他の構成要素

ClassFile を読み込み、Opcode を実行できるだけでは Java は動作しません。 例えば HelloWorld を実行するだけでも、

  • "java.lang.Object"
  • "java.lang.String"
  • "java.lang.System"
  • "java.io.PrintStream"

などが必要になります。

これらは JVMS ではなく Java SE API の領域です。 通常は Java Runtime が提供するクラスライブラリを利用しますが、vim-javim では Java Runtime へ依存せず、plugin 内で built-in runtime を提供しています。 また、依存クラスの解決を行う ClassLoader 相当の仕組みについても javim 独自実装となっており、 classpath や built-in runtime の解決もここで実装しています。

※ built-in runtime だからといって高速になる訳ではありません。vim-javim は Pure Vim8 script による実装であり、一般的な JVM と比較すると性能面では大きく劣ります。

まとめ

本記事では公開時点の vim-javim の実装範囲について整理しました。 公開時点では、実行対象の Bytecode から必要仕様を逆算し、実装対象を選定しています。 詳細な実装状況についてはリポジトリを参照してください。

参考文献

VimでJavaを実行する方法 2026年最新版

どうも、kamichiduです。

この記事は Vim 駅伝 2026-06-03 の記事となります。

早速ですがみなさん、開発をしていて Vim から Java を実行したいと感じたことはないでしょうか。 私はあります。 特にちょっとしたコードやアルゴリズムを試したいとき、端末へ切り替えたり、ビルドしたり、実行したりという往復は意外と面倒です。

そこで今回は、Vim から Java を実行する方法について紹介します。

先に実行環境は以下です:

  • Vim: 9.1.2100
  • Java Runtime: 25.0.2
  • OS: macOS arm64

なお、本プロジェクトは Vim9 script ではなく Vim8 script で実装しています。 特に Vim9 固有機能を必要としておらず、可搬性を優先しています。

さて、Vim から Java を実行するときの代表的な方法といえば、やはりコマンド実行でしょう。

:!java package.Class

というやつです。 もちろんこれでも動きます。 ただこの方法、Java の悪名高き起動コストが効きます。 それはもう効きます。 HelloWorld 程度ならともかく、ちょっと試したいだけなのに JVM の起動を待つのはあまり嬉しくありません。

Java の起動が遅いなら、Java を起動せずに実行すれば良いのではないでしょうか。 つまり、Pure Vim script で Java Virtual Machine (JVM) を実装すれば、Java を起動せずに Java を実行できます。

幸い、過去に通勤電車で Java Virtual Machine Specification (JVMS) を読んでいたため、実装方針自体は把握しています。 ClassFile を読み込み、Frame を実装し、Bytecode Interpreter を作れば、とりあえず動くはずです。 そのため今回、JVMS に従って Java のコンパイル済み ClassFile を実行する Vim plugin を実装しました。

GitHub: https://github.com/kamichidu/vim-javim

現在のところ、

  • ClassFile の読み込み
  • Constant Pool の解釈
  • JVM Frame
  • Bytecode Interpreter
  • メソッド呼び出し
  • 分岐命令
  • 整数演算

あたりを実装しています。

HelloWorld は動きます:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}
:JavimRun test.classes.HelloWorld
Hello, World!

Fibonacci も動きます:

public class Fibonacci {
    public static int fib(int n) {
        if (n <= 1) {
            return n;
        }

        return fib(n - 1) + fib(n - 2);
    }

    public static void main(String[] args) {
        System.out.println(fib(10));
    }
}
:JavimRun test.classes.Fibonacci
55

とりあえず Java は動いていると言って良さそうです。

これで Java 起動コストを気にせず、 Vim から Java を実行できるようになりました。 Vim script の表現力がさらに増えて嬉しいですね。

続き書きました:

参考文献:

terraform cli 備忘録

google providerなどのpluginはファイルサイズがでかくなる。 色んなフォルダでinitしまくると、ディスク容量を圧迫する。

~/.terraformrc で plugin_cache_dir を設定すると、設定したフォルダをpluginの実体ファイル置き場にでき、ディスク容量の圧迫度合いがマシになる。

環境変数 TF_CLI_CONFIG_FILE で、設定ファイルパスを変更可能。

terraformプロセスを実行するユーザが、plugin_cache_dir への書き込み権限などを持っている必要あり。