# 機能設計書 39-マルチラインサポート

## 概要

本ドキュメントは、CoffeeScript REPLのマルチラインサポート機能の設計を記述する。この機能により、ユーザーはCtrl-Vで複数行入力モードに切り替え、複数行にわたるCoffeeScriptコードを入力してから一括で評価できる。

### 本機能の処理概要

**業務上の目的・背景**：CoffeeScriptはインデントベースの構文を持つため、関数定義やクラス定義など複数行にわたるコードの入力が一般的である。単一行のREPLでは複雑なコードの入力が困難なため、マルチラインモードを提供して対話的な開発体験を向上させる。

**機能の利用シーン**：
- 関数やクラスの定義をREPLで入力したい場合
- if/else、for/while等の制御構造を入力したい場合
- 複数行にわたるオブジェクトリテラルを入力したい場合
- コードブロックをまとめて入力・評価したい場合

**主要な処理内容**：
1. Ctrl-Vキー押下でマルチラインモードを切り替え
2. マルチラインモード中はプロンプトを変更（`.....>`形式）
3. 入力行をバッファに蓄積
4. 再度Ctrl-Vでモード終了、蓄積した行を一括評価
5. 評価前に改行を特殊文字（`\uFF00`）に置換
6. eval関数内で特殊文字を改行に復元

**関連システム・外部連携**：Node.jsのreplモジュール、readlineモジュール

**権限による制御**：特になし。

## 関連画面

本機能はCLI機能のため、画面との直接的な関連はない。ただし、関連ドキュメント画面がある。

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 20 | Annotated Source - REPL | 補助機能 | addMultilineHandlerの解説を含む |
| 35 | Annotated Source - REPL (v2) | 補助機能 | addMultilineHandlerの解説を含む（v2） |

## 機能種別

UI/UX機能 / 入力処理

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| repl | Object | Yes | Node.js REPLインスタンス | - |

### キー入力

| キー | 動作 |
|------|------|
| Ctrl-V | マルチラインモードの切り替え |
| Enter | 現在行を確定（マルチラインモード中はバッファに追加） |

### 入力データソース

キーボード入力（inputStreamからのkeypressイベント）

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| multiline.buffer | String | 蓄積された入力行（改行区切り） |
| evaluatedResult | any | マルチラインコードの評価結果 |

### 出力先

REPLのlineイベント経由でeval関数へ

## 処理フロー

### 処理シーケンス

```
1. addMultilineHandler関数の呼び出し
   └─ REPL起動時にreplインスタンスを渡して呼び出し
2. 初期プロンプトの取得
   └─ repl._prompt または repl.prompt
3. マルチラインオブジェクトの初期化
   └─ enabled: off, initialPrompt, prompt, buffer設定
4. Nodeのlineリスナーをプロキシ
   └─ 元のリスナーを取得・削除し、カスタムリスナーを設定
5. Ctrl-Vキープレスハンドラの設定
   └─ inputStream.on('keypress', ...)
6. マルチラインモード切り替え時の処理
   └─ プロンプト変更、バッファ管理
```

### フローチャート

```mermaid
flowchart TD
    A[REPL起動] --> B[addMultilineHandler repl]
    B --> C[origPrompt取得]
    C --> D[multilineオブジェクト初期化]
    D --> E[nodeLineListener取得・削除]
    E --> F[カスタムlineリスナー設定]
    F --> G[keypressハンドラ設定]
    G --> H[入力待ち]

    H --> I{Ctrl-V?}
    I -->|No| J{Enter?}
    I -->|Yes| K{multiline.enabled?}

    J --> L{multiline.enabled?}
    L -->|Yes| M[buffer += line + '\n']
    M --> N[prompt表示]
    L -->|No| O[nodeLineListener cmd]

    K -->|Yes| P{buffer に改行あり?}
    K -->|No| Q[enabled = on]
    Q --> R[initialPrompt設定]
    R --> H

    P -->|No| S[enabled = off]
    S --> T[origPrompt設定]
    T --> H

    P -->|Yes| U{現在行が空白?}
    U -->|No| H
    U -->|Yes| V[enabled = off]
    V --> W[buffer の改行を \uFF00 に置換]
    W --> X[emit 'line', buffer]
    X --> Y[buffer = '']
    Y --> H
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-39-01 | 初期プロンプト変形 | マルチラインモード開始時、プロンプトを`------>`形式に変更 | Ctrl-V押下時（オフ→オン） |
| BR-39-02 | 継続プロンプト | 2行目以降は`.....>`形式のプロンプト | マルチライン入力中 |
| BR-39-03 | モード切替条件 | バッファに改行がない場合は即座にモード切替可能 | Ctrl-V押下時 |
| BR-39-04 | 評価条件 | バッファに改行があり、現在行が空白の場合のみ評価 | Ctrl-V押下時 |
| BR-39-05 | 改行置換 | 評価前に`\n`を`\uFF00`に置換 | eval発火前 |
| BR-39-06 | 改行復元 | eval関数内で`\uFF00`を`\n`に復元 | eval関数内 |

### 計算ロジック

**プロンプト変形ロジック**:
```coffeescript
# 初期プロンプト（マルチライン開始時）
initialPrompt: origPrompt.replace /^[^> ]*/, (x) -> x.replace /./g, '-'
# 例: 'coffee> ' → '------>'

# 継続プロンプト（2行目以降）
prompt: origPrompt.replace /^[^> ]*>?/, (x) -> x.replace /./g, '.'
# 例: 'coffee> ' → '......'
```

## データベース操作仕様

本機能はデータベース操作を行わない。

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| SYNTAX_ERROR | SyntaxError | 入力されたCoffeeScriptに構文エラー | コードを修正して再入力 |

### リトライ仕様

マルチラインモードはエラー後も継続可能。バッファはクリアされ、新しい入力を受け付ける。

## トランザクション仕様

該当なし。

## パフォーマンス要件

特になし。入力処理はリアルタイムで行われる。

## セキュリティ考慮事項

特になし。

## 備考

**使用例**:
```
coffee> (Ctrl-V を押す)
------>  add = (a, b) ->
......    a + b
......  (Ctrl-V を押す)
[Function: add]
coffee> add 1, 2
3
```

**特殊文字について**:
`\uFF00`（FULLWIDTH IDEOGRAPHIC COMMA）は通常のCoffeeScriptコードでは使用されない文字であり、改行の一時的なプレースホルダとして安全に使用できる。

---

## コードリーディングガイド

本機能を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: addMultilineHandler関数の構造を理解する

まず、関数全体の構造を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | repl.coffee | `src/repl.coffee` | addMultilineHandler関数定義 |

**主要処理フロー**:
- **77-127行目**: addMultilineHandler関数全体
- **78行目**: `{inputStream, outputStream} = repl` - ストリーム取得
- **80行目**: `origPrompt = repl._prompt ? repl.prompt` - 元のプロンプト取得

#### Step 2: multilineオブジェクトの構造を理解する

マルチラインモードの状態管理オブジェクトを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | repl.coffee | `src/repl.coffee` | multilineオブジェクト定義 |

**主要処理フロー**:
- **82-86行目**: multilineオブジェクト
  ```coffeescript
  multiline =
    enabled: off
    initialPrompt: origPrompt.replace /^[^> ]*/, (x) -> x.replace /./g, '-'
    prompt: origPrompt.replace /^[^> ]*>?/, (x) -> x.replace /./g, '.'
    buffer: ''
  ```

**読解のコツ**:
- `enabled: off` はCoffeeScriptの`false`と同義
- 正規表現`/^[^> ]*/`は「先頭から`>`または空白以外の連続」にマッチ

#### Step 3: lineイベントのプロキシを理解する

Nodeのlineイベントハンドラをどのように置き換えているかを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | repl.coffee | `src/repl.coffee` | lineイベントプロキシ |

**主要処理フロー**:
- **89-99行目**: lineイベントプロキシ
  ```coffeescript
  nodeLineListener = repl.listeners('line')[0]
  repl.removeListener 'line', nodeLineListener
  repl.on 'line', (cmd) ->
    if multiline.enabled
      multiline.buffer += "#{cmd}\n"
      repl.setPrompt multiline.prompt
      repl.prompt true
    else
      repl.setPrompt origPrompt
      nodeLineListener cmd
    return
  ```

**読解のコツ**:
- `repl.listeners('line')[0]`で元のリスナーを取得
- マルチラインモード中はバッファに追加、そうでなければ元のリスナーに委譲

#### Step 4: keypressイベントハンドラを理解する

Ctrl-Vの検出とモード切り替えロジックを確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | repl.coffee | `src/repl.coffee` | keypressハンドラ |

**主要処理フロー**:
- **101-127行目**: keypressハンドラ
  ```coffeescript
  inputStream.on 'keypress', (char, key) ->
    return unless key and key.ctrl and not key.meta and not key.shift and key.name is 'v'
    if multiline.enabled
      # マルチラインモードがオンの場合
      unless multiline.buffer.match /\n/
        # バッファに改行がなければ即座にオフに
        multiline.enabled = not multiline.enabled
        repl.setPrompt origPrompt
        repl.prompt true
        return
      return if repl.line? and not repl.line.match /^\s*$/
      # 評価実行
      multiline.enabled = not multiline.enabled
      repl.line = ''
      repl.cursor = 0
      repl.output.cursorTo 0
      repl.output.clearLine 1
      multiline.buffer = multiline.buffer.replace /\n/g, '\uFF00'
      repl.emit 'line', multiline.buffer
      multiline.buffer = ''
    else
      # マルチラインモードがオフの場合 → オンに
      multiline.enabled = not multiline.enabled
      repl.setPrompt multiline.initialPrompt
      repl.prompt true
    return
  ```

**読解のコツ**:
- `key.ctrl and not key.meta and not key.shift and key.name is 'v'` でCtrl-Vを判定
- `repl.line?.match /^\s*$/` で現在行が空白のみかを判定
- `\uFF00`への置換はeval関数内で復元される（マルチラインハック）

#### Step 5: eval関数内での復元処理を理解する

マルチラインハックの復元部分を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | repl.coffee | `src/repl.coffee` | eval関数内の復元処理 |

**主要処理フロー**:
- **19行目**: `input = input.replace /\uFF00/g, '\n'` - マルチラインハックの復元

### プログラム呼び出し階層図

```
start(opts) (repl.coffee:175)
    │
    └─ addMultilineHandler(repl) (repl.coffee:77)
           │
           ├─ repl.listeners('line')[0] → nodeLineListener
           │
           ├─ repl.removeListener('line', nodeLineListener)
           │
           ├─ repl.on('line', カスタムハンドラ)
           │      │
           │      ├─ multiline.enabled → バッファ追加
           │      │      └─ buffer += cmd + '\n'
           │      │
           │      └─ !multiline.enabled → 元のリスナーへ委譲
           │             └─ nodeLineListener(cmd)
           │
           └─ inputStream.on('keypress', Ctrl-Vハンドラ)
                  │
                  ├─ !multiline.enabled → enabled = on
                  │      └─ initialPrompt設定
                  │
                  └─ multiline.enabled
                         │
                         ├─ buffer に改行なし → enabled = off
                         │
                         └─ buffer に改行あり && 現在行空白
                                │
                                ├─ buffer.replace(/\n/g, '\uFF00')
                                │
                                └─ repl.emit('line', buffer)
                                       │
                                       └─ eval関数
                                              │
                                              └─ input.replace(/\uFF00/g, '\n')
```

### データフロー図

```
[入力]                        [処理]                              [出力]

Ctrl-V                   keypressハンドラ               プロンプト変更
     │                        │                               │
     ▼                        ▼                               ▼
enabled = off         enabled = on                    '------>'
     │                        │
     ▼                        ▼
行入力 "add = (a, b) ->"  lineハンドラ                  プロンプト表示
     │                        │                               │
     ▼                        ▼                               ▼
buffer に追加          buffer = "add = (a, b) ->\n"   '......'
     │
     ▼
行入力 "  a + b"
     │
     ▼
buffer に追加          buffer = "add = (a, b) ->\n  a + b\n"
     │
     ▼
Ctrl-V                  改行を \uFF00 に置換
     │                        │
     ▼                        ▼
repl.emit('line')      buffer = "add = (a, b) ->\uFF00  a + b\uFF00"
     │                        │
     ▼                        ▼
eval関数               \uFF00 を \n に復元
     │                        │
     ▼                        ▼
                        通常の評価処理
                              │
                              ▼
                        [Function: add]
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| repl.coffee | `src/repl.coffee` | ソース | addMultilineHandler関数、eval関数（復元処理） |
