0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

diffチェッカーをバージョンアップした話 — AI に diff させるのはトークンの無駄だった

0
Posted at

はじめに

「コードの差分を確認したい」というとき、ChatGPT や Claude にコードを貼り付けて比べてもらっていませんか?

diff は確定的な処理です。どこが違うかは計算で一瞬わかる。それをわざわざ LLM に推論させてトークンを消費するのは過剰です。しかも LLM は長いコードを省略したり、見落としたりすることがあります。

差分確認は専用ツールに任せ、AI はもっと高度な判断に使う。

そう考えて diff チェッカーをリニューアルしました。


実装した主な改善点

1. 並列表示(サイドバイサイド diff)

unified diff(+- 記号)から左右並列表示に変更しました。

# before: unified diff(初学者には読みにくい)
- border-radius: 50%
+ border-radius: 5px

# after: 左右並列(直感的)
左(元コード)         右(自分のコード)
border-radius: 50%  |  border-radius: 5px

実装は diff.jsdiffLines() を使い、removed と added が連続する場合は対応する行としてペアリングします。

while (i < parts.length) {
  const part = parts[i];
  const next = parts[i + 1];

  if (part.removed && next && next.added) {
    // 変更行:1:1でペアリングして単語diff→文字diffへ
    const removedLines = splitLines(part.value);
    const addedLines = splitLines(next.value);
    const pairLen = Math.min(removedLines.length, addedLines.length);
    // ...
    i += 2;
  }
}

2. ハイブリッド diff(行→単語→文字)

行単位だけでは「行のどこが違うか」がわかりません。3段階で精度を上げました。

// 変更行のペアに対して単語レベル diff
const words = Diff.diffWords(removedLines[j], addedLines[j]);

words.forEach((w, wi) => {
  if (w.removed && words[wi + 1]?.added) {
    // 削除側:文字レベルで精密に(消えた文字だけ赤)
    Diff.diffChars(w.value, words[wi + 1].value).forEach(({ added, removed, value }) => {
      if (removed) h1 += `<span class="char-removed">${escapeHTML(value)}</span>`;
      else if (!added) h1 += escapeHTML(value);
    });
    // 追加側:単語丸ごと緑
    h2 += `<span class="char-added">${escapeHTML(words[wi + 1].value)}</span>`;
  }
});

結果:border vs boder の場合、削除側は d だけ赤、追加側は boder 丸ごと緑になります。

3. 全角スペース検出(センチネル方式)

全角スペース(\u3000)を通常の空白として扱うと、diffWords がトークン境界として認識してしまい、誤った側に表示されるバグが発生します。

解決策として、Unicode プライベート領域のセンチネル文字\uE000)に置換してから diff を実行します。

const FW = "\uE000"; // U+E000: プライベート領域(空白でないため diffWords が境界と認識しない)

const normalizeCode = (text, ignoreWhitespace) => {
  let normalized = text
    .replace(/\r\n/g, "\n")
    .replace(/\r/g, "\n")
    .replace(/\u3000/g, FW); // 全角スペース → センチネル
  if (!ignoreWhitespace) return normalized;
  return normalized.split("\n").map((l) => l.trimEnd()).join("\n");
};

// 表示時にセンチネルを □ マークに変換
const escapeHTML = (str) =>
  str
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/\uE000/g, '<span class="fullwidth-marker">□</span>');

これにより:

  • 全角スペースが my code 側にある → 右パネルのみ □ が表示される ✓
  • 両側にある → 両パネルに □ が表示される ✓
  • ページ上部に警告バナーを表示 ✓

4. ファイルのドラッグ&ドロップ

FileReader API でテキストファイルを直接読み込めるようにしました。

const handleDrop = (target) => (e) => {
  e.preventDefault();
  target.classList.remove("drag-over");
  const file = e.dataTransfer.files[0];
  if (!file) return;
  const reader = new FileReader();
  reader.onload = (ev) => {
    target.innerHTML = escapeHTML(ev.target.result);
  };
  reader.readAsText(file);
};

5. ペースト処理を Selection API に移行

document.execCommand('insertText') は非推奨のため、Selection API に置き換えました。

const handlePaste = (e) => {
  e.preventDefault();
  const text = (e.clipboardData || window.clipboardData).getData("text");
  const sel = window.getSelection();
  if (!sel.rangeCount) return;
  const range = sel.getRangeAt(0);
  range.deleteContents();
  const tpl = document.createElement("template");
  tpl.innerHTML = escapeHTML(text);
  range.insertNode(tpl.content);
  range.collapse(false);
};

GitHub Actions で FTP 自動デプロイ

レンタルサーバー(ConoHa WING)へのデプロイを GitHub Actions で自動化しました。

name: Deploy via FTP

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: FTP Deploy
        uses: SamKirkland/FTP-Deploy-Action@v4.3.4
        with:
          server: ${{ secrets.FTP_SERVER }}
          username: ${{ secrets.FTP_USERNAME }}
          password: ${{ secrets.FTP_PASSWORD }}
          local-dir: ./public/
          server-dir: ${{ secrets.FTP_SERVER_DIR }}
          exclude: |
            **/.git*
            **/.git*/**
            **/node_modules/**
            **/.github/**

main へ push するだけで自動デプロイされます。local-dir: ./public/ を指定することで、サーバーサイドのコードを誤ってアップロードしないようにしています。


まとめ

改善点 実装
並列diff表示 diffLines() + removed/added ペアリング
文字レベルハイライト diffWords() + diffChars() のハイブリッド
全角スペース検出 \uE000 センチネル方式
ファイルD&D FileReader API
ペースト処理 Selection API
自動デプロイ GitHub Actions + FTP

diff に AI を使っていたトークン、もっと価値あることに使いましょう。


SEOスコアチェックツール: SEO_CHECK — RINIAディレクターツール。
Web制作・SEO関連の技術情報サイト: CodeQuest.work

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?