# プログラム設計書 25-removeSpace

## 1. 概要

### このプログラムの目的

このプログラムは、Luaソースコード内の行末にある不要なスペースやタブを一括削除するためのものです。「trailing-space」（末尾の空白）という診断警告に対するクイックフィックスとして、ファイル全体から不要な空白を除去します。

### このプログラムの役割

- **呼び出し元**: LSPコマンド `lua.removeSpace` の実行ハンドラ
- **主な処理**: ファイル内の全行をスキャンし、行末の空白を検出・削除
- **出力**: テキスト編集リクエストをエディタに送信

## 2. ファイル基本情報

| 項目 | 内容 |
|-----|------|
| ファイルパス | `script/core/command/removeSpace.lua` |
| ファイル名 | removeSpace.lua |
| 種別 | ソースコード |
| 使用言語 | Lua |
| 総行数 | 61行 |
| 作成目的 | 行末空白の一括削除機能を提供する |

## 3. 用語解説

このプログラムを理解するために必要な専門用語を解説します。

| 用語 | 読み方 | 意味・説明 |
|-----|--------|----------|
| Trailing Space | トレイリングスペース | 行末にある不要な空白文字（スペースやタブ） |
| Text Edit | テキストエディット | ファイルに対するテキストの編集操作 |
| Offset | オフセット | ファイル先頭からの文字位置（0始まり） |
| Position | ポジション | 行と列で表される位置 |

### 言語固有の概念

- **goto文とラベル**: Luaでは `goto LABEL` と `::LABEL::` で制御フローを移動できます。このコードでは `goto NEXT_LINE` でループの次のイテレーションに移動しています。
- **lines配列**: ファイルの各行の開始オフセットを保持する配列です。`lines[i]` は i 行目の開始位置を示します。

## 4. 依存関係

### このプログラムが使用している外部ファイル

```
removeSpace.lua
    │
    ├─ require 'files' ............. ファイル管理
    │
    ├─ require 'parser.guide' ...... 位置変換ユーティリティ
    │
    ├─ require 'proto' ............. LSPプロトコル通信
    │
    ├─ require 'language' .......... 多言語対応
    │
    └─ require 'proto.converter' ... LSP形式変換
```

| 参照ファイル | パス | 用途 |
|------------|------|------|
| files | `script/files.lua` | ファイルテキストとASTの取得 |
| guide | `script/parser/guide.lua` | オフセット・位置の変換 |
| proto | `script/proto/` | workspace/applyEdit リクエスト送信 |
| language | `script/language.lua` | コマンドラベルの多言語化 |
| converter | `script/proto/converter.lua` | 範囲のLSP形式への変換 |

## 5. データ構造詳細

### 主要な変数・データ項目

#### data パラメータ

| 項目名 | 型 | 用途・説明 |
|-------|--------|----------|
| uri | string | 処理対象ファイルのURI |

**解説**: コマンド実行時に渡されるパラメータです。どのファイルの空白を削除するかを指定します。

#### textEdit 配列

| 項目名 | 型 | 用途・説明 |
|-------|--------|----------|
| range | object | 削除する範囲（start, end） |
| newText | string | 置換後のテキスト（空文字列で削除） |

**解説**: 各行末空白の削除操作を表すオブジェクトの配列です。これがエディタに送信され、実際の編集が適用されます。

## 6. 処理詳細解説

このプログラムの処理内容をステップバイステップで説明します。

### 6.1 処理一覧

| No | 処理名 | 行番号 | 概要 |
|----|-------|--------|------|
| 1 | ファイル情報取得 | 9-14 | ファイルのテキストとASTを取得 |
| 2 | 行ごとの空白検出 | 16-46 | 各行の末尾空白を検出 |
| 3 | 編集リクエスト送信 | 48-59 | エディタに編集を適用 |

### 6.2 ファイル情報取得の詳細（9-14行目）

**この処理は何をするものか**: 処理対象ファイルのテキスト内容と解析情報を取得します。

**ステップバイステップ解説**:

1. **ステップ1: データの取得**（9-11行目）

   ```lua
   local uri   = data.uri
   local text  = files.getText(uri)
   local state = files.getState(uri)
   ```

   まず、パラメータからURIを取得し、そのファイルのテキスト内容とAST（構文解析結果）を取得します。

2. **ステップ2: 存在チェック**（12-14行目）

   ```lua
   if not state or not text then
       return
   end
   ```

   ファイルが読み込まれていない場合は、何もせずに終了します。

### 6.3 行ごとの空白検出の詳細（16-46行目）

**この処理は何をするものか**: ファイルの各行をスキャンし、行末の空白を検出してテキスト編集リストに追加します。

**処理の流れ**:

```
ステップ1: 各行についてループ
    │
    ↓
ステップ2: 行末の空白をチェック
    │
    ↓（空白がある場合）
ステップ3: 文字列/コメント内でないか確認
    │
    ↓（対象外でない場合）
ステップ4: 空白の開始位置を特定
    │
    ↓
ステップ5: 編集情報を追加
```

**ステップバイステップ解説**:

1. **ステップ1: 行ループの開始**（18行目）

   ```lua
   for i = 0, #lines do
   ```

   `lines` 配列（各行の開始オフセット）を使って、全行をループします。Luaの配列は通常1始まりですが、このコードでは0始まりで処理しています。

2. **ステップ2: 行末の空白チェック**（19-25行目）

   ```lua
   local startOffset  = lines[i]
   local finishOffset = text:find('[\r\n]', startOffset) or (#text + 1)
   local lastOffset   = finishOffset - 1
   local lastChar     = text:sub(lastOffset, lastOffset)
   if lastChar ~= ' ' and lastChar ~= '\t' then
       goto NEXT_LINE
   end
   ```

   - 行の開始位置から改行文字を探し、行の終了位置を特定
   - 行末の最後の文字がスペースでもタブでもなければ、次の行へスキップ

3. **ステップ3: 文字列/コメント内のチェック**（26-30行目）

   ```lua
   local lastPos = guide.offsetToPosition(state, lastOffset)
   if guide.isInString(state.ast, lastPos)
   or guide.isInComment(state.ast, lastPos) then
       goto NEXT_LINE
   end
   ```

   行末が文字列リテラルやコメント内にある場合は、その空白は意図的なものかもしれないため、スキップします。

4. **ステップ4: 空白開始位置の特定**（31-38行目）

   ```lua
   local firstOffset = startOffset
   for n = lastOffset - 1, startOffset, -1 do
       local char = text:sub(n, n)
       if char ~= ' ' and char ~= '\t' then
           firstOffset = n + 1
           break
       end
   end
   ```

   行末から逆方向にスキャンして、連続する空白の開始位置を特定します。

5. **ステップ5: 編集情報の追加**（39-43行目）

   ```lua
   local firstPos = guide.offsetToPosition(state, firstOffset) - 1
   textEdit[#textEdit+1] = {
       range = converter.packRange(state, firstPos, lastPos),
       newText = '',
   }
   ```

   空白の範囲を特定し、それを空文字列で置換する編集情報を追加します。

**この処理のポイント**:
- `goto NEXT_LINE` は Lua 5.2+ で使用可能な制御構文です
- 文字列やコメント内の空白は保持されます（意図的な可能性があるため）

### 6.4 編集リクエスト送信の詳細（48-59行目）

**この処理は何をするものか**: 収集した編集情報をエディタに送信し、実際にファイルを編集します。

**ステップバイステップ解説**:

1. **ステップ1: 編集有無のチェック**（48-50行目）

   ```lua
   if #textEdit == 0 then
       return
   end
   ```

   削除すべき空白がなければ、何もせずに終了します。

2. **ステップ2: 編集リクエストの送信**（52-59行目）

   ```lua
   proto.awaitRequest('workspace/applyEdit', {
       label = lang.script.COMMAND_REMOVE_SPACE,
       edit = {
           changes = {
               [uri] = textEdit,
           }
       },
   })
   ```

   LSPの `workspace/applyEdit` リクエストを送信して、エディタに編集を適用します。`label` はアンドゥ履歴に表示される操作名です。

**この処理のポイント**:
- `proto.awaitRequest` は非同期関数で、エディタからの応答を待ちます
- 複数の編集を一度に適用することで、アンドゥも一回で戻せます

## 7. 処理フロー図

プログラム全体の処理の流れを図で示します。

```mermaid
flowchart TD
    A[コマンド実行] --> B[ファイル情報取得]
    B --> C{ファイル存在?}
    C -->|No| Z[終了]
    C -->|Yes| D[行ループ開始]
    D --> E{行末に空白?}
    E -->|No| F[次の行へ]
    E -->|Yes| G{文字列/コメント内?}
    G -->|Yes| F
    G -->|No| H[空白範囲を特定]
    H --> I[編集リストに追加]
    I --> J{最終行?}
    J -->|No| D
    J -->|Yes| K{編集あり?}
    K -->|No| Z
    K -->|Yes| L[workspace/applyEdit送信]
    L --> Z
```

**図の説明**:
- **コマンド実行**: ユーザーがクイックフィックスを選択、またはコマンドパレットから実行
- **行ループ**: 全行をスキャンして空白を検出
- **workspace/applyEdit送信**: エディタに編集を適用

## 8. 外部連携

### 呼び出し関係図

```
[呼び出し元]                    [このプログラム]                    [呼び出し先]
                                     │
code-action.lua ───────────▶ removeSpace.lua ──────────────▶ proto.awaitRequest()
  (lua.removeSpace)                  │                          (編集適用)
                                     │
                                     └──────────────▶ files.getText()
                                                        (テキスト取得)
```

### 呼び出し元（このプログラムを呼び出すもの）

| プログラム名 | 呼び出し契機 | 渡されるデータ |
|------------|------------|--------------|
| code-action.lua | trailing-space 診断のクイックフィックス選択時 | { uri: ファイルURI } |

### 呼び出し先（このプログラムが呼び出すもの）

| プログラム名 | 呼び出し目的 | 渡すデータ | 受け取るデータ |
|------------|------------|----------|--------------|
| files.getText | ファイル内容取得 | URI | テキスト文字列 |
| files.getState | AST取得 | URI | パーサー状態 |
| guide.isInString | 文字列内判定 | AST, 位置 | boolean |
| guide.isInComment | コメント内判定 | AST, 位置 | boolean |
| proto.awaitRequest | 編集適用 | リクエスト名, 編集データ | 応答 |

## 9. エラー処理

このプログラムで発生する可能性があるエラーと、その対処方法を説明します。

### エラー一覧

| エラー種別 | 発生条件 | 対処内容 | 該当行 |
|----------|---------|---------|--------|
| ファイルなし | ファイルが読み込まれていない | 何もせず終了 | 12-14 |
| 編集なし | 削除対象の空白がない | 何もせず終了 | 48-50 |

### エラー発生時の動作

このプログラムは例外を投げません。エラー状態の場合は静かに終了し、ユーザーには何も通知されません。

## 10. このプログラムを読むときのポイント

このプログラムを理解するためのアドバイスです。

### 推奨する読み方

1. **まず全体構造を把握**: ファイル取得 → ループ → 編集送信 の3段階
2. **次にループ内の条件分岐を理解**: 空白検出 → 文字列/コメントチェック → 範囲特定
3. **最後に LSP 通信部分を理解**: workspace/applyEdit の仕組み

### 注意すべきポイント

- `goto NEXT_LINE` は continue 文の代わりです（Luaには continue がありません）
- `::NEXT_LINE::` は goto のラベル定義です
- `lines` 配列は 0 始まりで、各行の開始オフセットを保持しています
- `text:sub(n, n)` は n 番目の1文字を取得します（Lua の文字列は 1 始まり）

### 関連して読むべきファイル

| ファイル | パス | 関連性 |
|---------|------|--------|
| code-action.lua | `script/core/code-action.lua` | このコマンドを呼び出すコードアクション |
| trailing-space.lua | `script/core/diagnostics/trailing-space.lua` | 末尾空白を検出する診断 |

## 11. 備考

- このコマンドは `trailing-space` 診断のクイックフィックスから呼び出されます
- 文字列やコメント内の空白は意図的な可能性があるため、削除対象から除外されています
- 1回の実行でファイル全体の末尾空白を削除できるため、効率的です
