PDT(PHP Development Tools)で複数行ペースト時に次行のインデントが変更される

仕事でPHPのコードを編集することがあるのだが、自分が頻繁に使う「トリプルクリックで行選択→ドラッグで数行選択→コピー→別の箇所にペースト」という操作をおこなうと、なぜかペーストした行の次の行のインデントが変わってしまうという現象があり、不便をしている。

内部でどういう処理をしているのかちょっと調べてみた。

プラグインのjarファイルとプラグインの定義

今回関係があるのは、以下の二つのjarファイルだった。

  • plugins/org.eclipse.php.core_3.2.0.201306051924.jar (PDTのコア)
  • plugins/org.eclipse.php.ui_3.2.0.201306051924.jar (エディタなどのUI周りの処理をおこなう)

Eclipseプラグインテキストエディタなどのドキュメントを操作するようなものを作る場合、IAutoEditStrategyの実装クラスを作成してエディタに渡しておくことで、クリップボードの内容の貼り付けやキーボードからの文字入力など、ドキュメントへの更新操作のコマンドをハンドリングすることができるようだ。
PDTでは、MainAutoEditStrategyというのがその実装クラスだった。

org.eclipse.php.ui_3.2.0.201306051924.jarのプラグインの定義ファイル(plugin.xml)をみたところ、以下のような流れでMainAutoEditStrategyを使用するように設定されていた。

  • PHP用のエディタ本体であるorg.eclipse.php.internal.ui.editor.PHPStructuredEditor (plugin.xmlで定義)
    • PHPStructuredEditor用のSourceViewerConfigurationとして、PHPStructuredTextViewerConfigurationが指定されている(plugin.xmlで定義)
      • PHPStructuredTextViewerConfigurationのgetAutoEditStrategies()で、IAutoEditStrategyの実装としてMainAutoEditStrategyを返す

という感じで、エディタに対してMainAutoEditStrategyが渡されている。

実際の処理内容

MainAutoEditStrategyのcustomizeDocumentCommand()の定義を見ると、コマンド操作対象領域のタイプによって処理が振り分けられており、通常のPHPコードは、各種自動インデントの設定に従ってコマンドをハンドリングする各々のAutoEditStrategyに処理が委譲されている。今回の「次行のインデントが勝手に変更される」という挙動は、そのうちの「IndentLineAutoEditStrategy」というクラスが引き起こしているようだ。

IndentLineAutoEditStrategyのcustomizeDocumentCommandメソッドは、以下のような実装になっている。

        public void customizeDocumentCommand(final IDocument document,
                        final DocumentCommand command) {
                if (command.text != null
                                && TextUtilities.endsWith(document.getLegalLineDelimiters(),
                                                command.text) != -1)
                        autoIndentAfterNewLine((IStructuredDocument) document, command);
        }

与えられるテキストが空でなく、テキストの末尾が改行の場合のみ、autoIndentAfterNewLine()を呼び出している。この中身が怪しい。

ここまでの処理を見る限り、ペーストしたテキストの最後が改行の場合、改行キー押下時と同じようにインデントを行っている。(しかも、それはOFFには出来ない。) この挙動は不具合かと思ったが、改行キー押下時と改行コードペースト時で同じように自動インデントを実施しているという意味では、そういうポリシーなら仕方がない気もする。

autoIndentAfterNewLine()の中身も見てみる。挿入対象行の情報を取得している箇所があった。

        private void autoIndentAfterNewLine(final IStructuredDocument document,
                        final DocumentCommand command) {
...
                        final int currentOffset = command.offset;

                        final int lineNumber = document.getLineOfOffset(currentOffset);
...
                        final IRegion lineInfo = document.getLineInformation(lineNumber);

                        final int startOffset = lineInfo.getOffset();
                        final int length = lineInfo.getLength();

挿入対象箇所をcommand.offsetで取得し、それを含む行番号をIStructuredDocumentのgetLineOfOffset()で取得し、行自体の情報をIStructuredDocumentのgetLineInformation()で取得している。

startOffset(行の先頭)と、currentOffset(挿入箇所)があるので、これを比較すれば「挿入箇所が先頭行の場合に限り、インデントを行わない」という処理が書けそうだ。

改造(汚いパッチ)

上のコードに続いて、次のように書けば気に入らない挙動を無効化できそうだ。

                        if (startOffset == command.offset)
                                return;

入力内容の末尾が改行で、かつ挿入先のカーソルが行の先頭の場合は、その行のインデントは変更しない、という風になる。

実際にソースコードを修正して(無理やり)コンパイルし、生成した.classファイルをオリジナルのjarファイルにjar -ufで上書き適用したところ、トリプルクリックで行選択してコピー→ペーストを実行しても、挿入箇所の次の行のインデントが変更されることは無くなった(汚い。)

ちなJDK1.5でコンパイルし、コンパイルにあたってパスを通したのはこの辺。