メインコンテンツへスキップ

🧷 VS Code拡張を常駐HTTPサーバとして使い、未保存(isDirty)状態を外部から取得する

🧭 はじめに

VS Codeで編集中のファイルを、外部スクリプト(PowerShell / Python など)から同期・上書きする場合、VS Code側で「未保存(dirty)」の状態にあるファイルを誤って上書きしてしまう事故が最も危険である。

本記事では、

  • VS Codeは常に起動している前提
  • VS Code拡張機能を常駐HTTPサーバとして動作させる
  • 外部からファイルパスを渡して isDirty を問い合わせる

という構成で、「APIっぽく」未保存状態を取得する方法を解説する。

VS Code拡張APIには TextDocument.isDirty という公式プロパティがあり、これを使うのが未保存判定の唯一の正攻法。


🧩 なぜHTTPサーバ方式なのか

VS Code拡張には「外部プロセスに値を返すCLI API」は存在しない。 そのため、

  • code --command のような方法では 戻り値(true/false)を受け取れない
  • URIハンドラも 同期的なレスポンスを返せない

という制約がある。

VS Codeのコマンド実行は「VS Code内部で完結」する設計で、外部CLIから関数の戻り値を直接受け取る用途は想定されていない。

そこで、

  • VS Code拡張を ローカルHTTPサーバとして常駐
  • PowerShell / Python から HTTPリクエストで問い合わせ
  • JSONで結果を返す

という構成を取ることで、**実質的に「isDirty API」**を実現する。


🏗️ 全体構成

🎯 目的

外部スクリプト
  ↓ HTTP
VS Code 拡張(常駐)
  ↓
TextDocument.isDirty を判定
  ↓
JSON { isDirty: true/false } を返す

🧱 前提条件

  • VS Code が起動している
  • 対象ファイルは VS Code で一度は開かれている
  • ローカル通信(127.0.0.1)を許可できる環境

同期処理の直前に1回HTTPで問い合わせるだけなので、既存のPowerShell/Pythonスクリプトに組み込みやすい。


🔌 HTTP API仕様(例)

📍 エンドポイント

GET http://127.0.0.1:17891/isDirty

📥 クエリパラメータ

名前 内容
path 判定したいファイルの絶対パス

📤 レスポンス(JSON)

{
  "path": "/path/to/file.txt",
  "isDirty": true,
  "found": true
}
  • found=false の場合は「VS Codeで開かれていない」ことを意味する

未保存判定は「VS Codeで開かれているファイル」にしか適用できない。未オープンファイルは dirty 判定不能。


🧠 VS Code拡張側:実装の要点

🔑 1. HTTPサーバを起動する

  • Node.js の http モジュールを使用
  • 127.0.0.1 にバインド(外部公開しない)
  • 拡張 activate() 時に起動
  • deactivate() 時に必ず close

`0.0.0.0` にバインドすると、LAN越しにファイル状態を覗かれるリスクがある。必ず 127.0.0.1 に限定する。


🔍 2. path から TextDocument を探す

  • vscode.workspace.textDocuments を走査
  • doc.uri.fsPath === path で一致判定
  • doc.isDirty を返す

URI比較ではなく fsPath 比較にすることで、file:// の表記揺れを避けられる。


⏱️ 3. isDirty のタイミング問題

VS Codeでは、次のようなケースがある。

  • onDidChangeTextDocument 直後
  • まだ isDirty === false の瞬間がある

そのため、

  • HTTPリクエスト受信時に 即座に現在値を読む
  • 状態更新イベントに依存しない

設計が安定する。

イベント駆動でキャッシュした isDirty を返すと、タイミングずれで誤判定しやすい。


🧪 VS Code拡張:最小実装イメージ(概念)

拡張機能(コード)

import * as vscode from 'vscode'
import * as http from 'http'
import * as url from 'url'

/*
 * VS Code 拡張機能本体
 *
 * 目的:
 *  - VS Code を常駐プロセスとして利用し
 *  - ローカル HTTP 経由で「指定ファイルが未保存(isDirty)か」を問い合わせ可能にする
 *
 * 前提:
 *  - VS Code は起動している
 *  - 問い合わせ対象のファイルは VS Code で一度は開かれている
 *  - 外部からは http://127.0.0.1:17891/isDirty?path=... を叩く
 *
 * 注意:
 *  - 未保存状態(isDirty)は VS Code 内部状態のため
 *    OS / Git / PowerShell / Python 単体では取得できない
 */

let server: http.Server | undefined

export function activate(context: vscode.ExtensionContext) {
  /*
   * HTTP サーバを待ち受けるポート番号
   * 他プロセスとの衝突を避けたい場合は固定値を変更する
   */
  const port = 17891

  /*
   * Node.js 標準の http モジュールを使って
   * ローカル専用の HTTP サーバを立てる
   */
  server = http.createServer((req, res) => {
    /*
     * URL が取得できないケースは異常なので 400
     */
    if (!req.url) {
      res.writeHead(400)
      res.end()
      return
    }

    /*
     * クエリパラメータを含めて URL を分解
     * 例: /isDirty?path=C:\repo\foo.ts
     */
    const parsed = url.parse(req.url, true)

    /*
     * 対応するエンドポイントは /isDirty のみ
     */
    if (parsed.pathname !== '/isDirty') {
      res.writeHead(404)
      res.end()
      return
    }

    /*
     * 必須パラメータ: path
     * 判定対象となるファイルの「絶対パス」
     */
    const path = parsed.query.path
    if (typeof path !== 'string') {
      res.writeHead(400, { 'Content-Type': 'application/json' })
      res.end(JSON.stringify({ error: 'path query parameter is required' }))
      return
    }

    /*
     * VS Code で現在「開かれている」全ドキュメントから
     * 指定されたパスと一致するものを探す
     *
     * - scheme === 'file' に限定(untitled 等を除外)
     * - fsPath で比較(file:// URI の表記揺れ対策)
     */
    const doc = vscode.workspace.textDocuments.find(d =>
      d.uri.scheme === 'file' && d.uri.fsPath === path
    )

    /*
     * レスポンス内容
     * - found   : VS Code で開かれているか
     * - isDirty : 未保存かどうか(found=false の場合は常に false)
     */
    const result = {
      path,
      found: !!doc,
      isDirty: doc ? doc.isDirty : false
    }

    /*
     * JSON としてレスポンスを返却
     */
    res.writeHead(200, { 'Content-Type': 'application/json' })
    res.end(JSON.stringify(result))
  })

  /*
   * セキュリティ上の理由から 127.0.0.1 に限定して待ち受ける
   * 0.0.0.0 にすると LAN 経由で状態を覗かれる可能性がある
   */
  server.listen(port, '127.0.0.1')
}

/*
 * 拡張機能が無効化される際に呼ばれる
 * HTTP サーバを確実に停止しておく
 */
export function deactivate() {
  if (server) {
    server.close()
    server = undefined
  }
}

🧷 PowerShell からの利用例

$path = "C:\repo\src\foo.ts"
$url  = "http://127.0.0.1:17891/isDirty?path=$([uri]::EscapeDataString($path))"

$res = Invoke-RestMethod $url

if ($res.found -and $res.isDirty) {
    Write-Error "VS Code側で未保存のため同期中断: $path"
    exit 2
}

HTTPレスポンスが即時返るため、同期前チェックとして自然に組み込める。


🐍 Python からの利用例

import requests
import sys
from urllib.parse import quote

path = r"C:\repo\src\foo.ts"
url = f"http://127.0.0.1:17891/isDirty?path={quote(path)}"

res = requests.get(url, timeout=1).json()

if res.get("found") and res.get("isDirty"):
    print(f"VS Code側で未保存のため同期中断: {path}", file=sys.stderr)
    sys.exit(2)

🧯 想定されるエッジケース

🟡 VS Codeが起動していない

  • HTTP接続エラーになる
  • 安全側に倒して同期中断がおすすめ

「VS Codeが起動していない=未保存が無い」と解釈するのは危険。


🟡 ファイルが開かれていない

  • found=false が返る

  • 運用で判断:

    • 「開かれていない=安全」とみなす
    • あるいは「判定不能なので中断」

🟡 複数ワークスペース

  • textDocuments は全ワークスペース横断
  • fsPath 一致で問題なし

🧠 設計まとめ

  • 未保存(dirty)はOS/Gitからは絶対に取れない
  • VS Code拡張 + HTTP が最も自然なAPI形態
  • 引数でファイルパスを渡し、即 isDirty を返せる
  • 同期処理の安全装置として非常に強力

「VS Codeを編集中に、外部ツールが勝手に上書きする」という事故を、確実に防止できる構成。