clineはVS Code拡張/CLIとして動く自律コーディングAIエージェントです。今日読むのは、CLIのターミナルUI(TUI)で「長いテキストを貼り付けたときに入力欄がぐちゃぐちゃにならないよう、[Pasted +12 lines]のようなプレースホルダーに圧縮する」ためのユーティリティ群です。文字列処理・正規表現・Setによる重複回避など、業務コードで頻出する小技がぎゅっと詰まっているので教材に選びました。
コード
export const LARGE_PASTE_LINE_THRESHOLD = 5;
const PREVIEW_MAX_CHARS = 48;
export function countPastedTextLines(text: string): number {
if (text.length === 0) return 0;
const lines = text.split(/\r\n|\r|\n/);
return lines.at(-1) === "" ? lines.length - 1 : lines.length;
}
export function shouldCompactPastedText(text: string): boolean {
return countPastedTextLines(text) >= LARGE_PASTE_LINE_THRESHOLD;
}
function previewText(text: string): string {
const normalized = text.trim().replace(/\s+/g, " ");
if (!normalized) return "";
return normalized.length > PREVIEW_MAX_CHARS
? normalized.slice(0, PREVIEW_MAX_CHARS).trimEnd()
: normalized;
}
export function formatPastedTextSnippetMarker(text: string): string {
const lineCount = countPastedTextLines(text);
const preview = previewText(text);
const lineLabel = lineCount === 1 ? "line" : "lines";
const prefix = preview ? `${preview}... ` : "";
return `[${prefix}Pasted +${lineCount} ${lineLabel}]`;
}
export function createUniquePastedTextSnippetMarker(
text: string,
existingMarkers: Iterable<string>,
): string {
const base = formatPastedTextSnippetMarker(text);
const existing = new Set(existingMarkers);
if (!existing.has(base)) return base;
for (let suffix = 2; ; suffix += 1) {
const candidate = base.replace(/\]$/, ` #${suffix}]`);
if (!existing.has(candidate)) return candidate;
}
}
引用はリポジトリの実物と機械で照合しています。「// …略…」は省略した行です。
上から順に読む
export const LARGE_PASTE_LINE_THRESHOLD = 5;
const PREVIEW_MAX_CHARS = 48;ファイルの先頭でマジックナンバーを定数化しています。`export`が付いた`LARGE_PASTE_LINE_THRESHOLD`は他のファイルから`import`して使える定数、`export`のない`PREVIEW_MAX_CHARS`はこのファイル内だけで使う定数です。「5行以上なら圧縮する」「プレビューは48文字まで」という閾値をコード中に直接埋め込まず名前を付けておくことで、後で値を変える時に一箇所直せば済みますし、`if (lines >= 5)`とだけ書くよりも意図が読みやすくなります。
export function countPastedTextLines(text: string): number {
if (text.length === 0) return 0;
const lines = text.split(/\r\n|\r|\n/);
return lines.at(-1) === "" ? lines.length - 1 : lines.length;
}引数`text: string`は「textという名前でstring型の値を受け取る」という意味で、戻り値の型`: number`は「この関数はnumber型を返す」という宣言です。`text.split(正規表現)`は文字列を正規表現にマッチした箇所で分割し配列にするメソッドで、`/\r\n|\r|\n/`は「CRLF改行、またはCR単体、またはLF単体」のどれかにマッチする正規表現です(`|`はOR、`\n`は改行、`\r`は復帰)。OSによって改行コードが違う(Windowsは\r\n、Unix系は\n)ので、この3パターンを網羅しています。`lines.at(-1)`は配列の「後ろから1番目」つまり最後の要素を取り出すメソッドで、最後が空文字列なら「末尾に改行が1つあった」とみなして行数を1減らしています(`条件 ? A : B`は条件がtrueならA、falseならBを返す三項演算子です)。
export function shouldCompactPastedText(text: string): boolean {
return countPastedTextLines(text) >= LARGE_PASTE_LINE_THRESHOLD;
}上で定義した関数を呼び出して、行数が閾値以上かどうかをtrue/falseで返すだけの小さな関数です。こうして「行数を数える処理」と「圧縮すべきか判定する処理」を別関数に分けておくと、それぞれ単体でテストしやすくなり、呼び出し側も`shouldCompactPastedText(text)`と書くだけで意図が伝わります。
function previewText(text: string): string {
const normalized = text.trim().replace(/\s+/g, " ");
if (!normalized) return "";この関数には`export`が付いていないので、このファイルの外からは呼べない「内部専用のヘルパー関数」です。`text.trim()`は文字列の前後の空白(スペースや改行)を取り除くメソッド、続けて`.replace(/\s+/g, " ")`は「1文字以上連続する空白文字(`\s+`、改行やタブも含む)」を1つの半角スペースに置き換えています。`/g`は正規表現のフラグで「最初の1箇所だけでなく全部置換する」という意味です。`!normalized`は`normalized`が空文字列(falsy)なら真になるので、空だったら早期に`""`を返しています。
return normalized.length > PREVIEW_MAX_CHARS
? normalized.slice(0, PREVIEW_MAX_CHARS).trimEnd()
: normalized;
}三項演算子が長いので改行して書かれていますが、意味は「文字数がPREVIEW_MAX_CHARS(48)を超えていたら、先頭から48文字だけ`slice(0, 48)`で切り出して末尾の空白を`trimEnd()`で削る。超えていなければそのまま返す」です。長文をプレビュー表示するときに「途中で単語が切れて末尾に半端なスペースが残る」のを`trimEnd()`で防いでいる点が実務的な配慮です。
const lineLabel = lineCount === 1 ? "line" : "lines";
const prefix = preview ? `${preview}... ` : "";`lineCount === 1 ? "line" : "lines"`は英語の単数形/複数形を切り替える三項演算子です。`preview ? \`${preview}... \` : ""`はバッククォートで囲むテンプレートリテラルという書き方で、`${式}`の中にJavaScriptの式を埋め込んで文字列に変換してくれます。previewが空文字列(falsy)ならプレフィックスも空にしています。
return `[${prefix}Pasted +${lineCount} ${lineLabel}]`;
}ここまで作った小さな変数を組み合わせて、実際にユーザーへ見せる文字列(例: `[Hello world... Pasted +12 lines]`)を組み立てています。`+`で文字列連結するより、テンプレートリテラルで一気に組むほうが読みやすいので実務でもよく使われる書き方です。
export function createUniquePastedTextSnippetMarker(
text: string,
existingMarkers: Iterable<string>,
): string {2つ目の引数の型`Iterable<string>`に注目してください。`<string>`の部分はジェネリクスと呼ばれ、「string型の要素を持つ」という意味です。`Iterable`は「`for...of`で1つずつ取り出せるもの」を表す型で、配列(`string[]`)だけでなく`Set<string>`なども当てはまります。つまりこの関数は「呼び出し側が配列を渡してもSetを渡しても受け取れる」ように、あえて具体的な配列型ではなく抽象的なIterable型を使っています。
const base = formatPastedTextSnippetMarker(text);
const existing = new Set(existingMarkers);
if (!existing.has(base)) return base;`new Set(existingMarkers)`は、渡された既存マーカー一覧から重複のないSet(集合)オブジェクトを新しく作っています。Setは`.has(値)`メソッドで「その値が含まれているか」を高速に判定できるのが利点で、配列に対して毎回`.includes()`で探すより効率的です。ここでは「まだ誰も使っていない基本形のマーカーなら、そのまま採用して即returnする」という早期リターンをしています。
for (let suffix = 2; ; suffix += 1) {
const candidate = base.replace(/\]$/, ` #${suffix}]`);
if (!existing.has(candidate)) return candidate;
}
}forループの構文は`for (初期化; 継続条件; 更新)`の3つですが、真ん中の継続条件が空になっています。これは「条件を書かない=ずっとtrue=無限ループ」という意味で、代わりにループの内側にある`if (...) return candidate;`が抜け出す唯一の出口になっています。中身では`base.replace(/\]$/, ...)`で、文字列末尾の`]`(`\]$`の`$`は「文字列の末尾」を表すアンカー)を` #2]`のような文字列に置き換えて、`[... #2]`のような別名の候補を作り、既存になければそれを返します。見つからない限り`suffix`を3, 4, 5...と増やし続けるので、必ずどこかで未使用の名前が見つかります。