Open Tools API その4:続・ソースエディタへのアクセス ソースエディタの「中身」をいじる

エディタの編集バッファ

ビュー(IOTAEditView)のBufferプロパティがソースエディタ内部のバッファで、ソースファイルの中身を編集する。インターフェースはITOAEditBuffer。
ソースエディタが1つのファイルをオープンしている場合、ビューは複数あっても、バッファは同一のを参照する。ソースエディタ(IOTASourceEditor)、ビュー(IOTAEditView)、バッファ(ITOAEditBuffer)の関係はこんな感じ。

        ┌ ビュー[0] ┐
        ├ ビュー[1] ┤
ソースエディタ ┼ ビュー[2] ┼ バッファ
        │   …   │
        └ ビュー[n] ┘

バッファのEditBlockプロパティがエディタで選択されている範囲。以下は、エディタの選択部分の位置と内容を取得する例。

function GetCurrentSourceEditor: IOTASourceEditor;
var
  i: Integer;
  Editor: IOTAEditor;
  ISourceEditor: IOTASourceEditor;
  CurrentModule: IOTAModule;
begin
  Result := nil;
  CurrentModule := (BorlandIDEServices as IOTAModuleServices).CurrentModule;

  for i := 0 to CurrentModule.GetModuleFileCount - 1 do
  begin
    Editor := CurrentModule.GetModuleFileEditor(i);

    if Supports(Editor, IOTASourceEditor, ISourceEditor) then
    begin
      Result := ISourceEditor;
      Break;
    end;
  end;
end

procedure TfrmEditorStatus.Button2Click(Sender: TObject);
var
  SourceEditor: IOTASourceEditor;
  EditBuffer: IOTAEditBuffer;
  Start, After: TOTACharPos;
  BlockType: TOTABlockType;
begin
  SourceEditor := GetCurrentSourceEditor;  // 現在アクティブなエディタを取得(GExpters由来)
  if SourceEditor = nil then
    Exit;
  Start := SourceEditor.BlockStart;  // 選択範囲の開始位置
  After := SourceEditor.BlockAfter;  // 選択範囲の終了位置

  // 現在編集中のファイルのバッファを取得
  EditBuffer := (BorlandIDEServices as IOTAEditorServices).TopBuffer;
  if SourceEditor = nil then
    Exit;

  // 選択範囲の内容を取得
  Memo1.Clear;
  Memo1.Text := EditBuffer.EditBlock.Text;
end;

バッファへ直接アクセスする

セレクションではなく、バッファを直接アクセスする場合は、編集リーダー(IOTAEditReader)と編集ライター(IOTAEditWriter)を介してアクセスする。バッファはUTF-8で管理しているので、UnicodeStringからは明示的に変換してやらないと文字化けするので注意。
バッファの中身を読み取る手順は、エディタのCreateReaderメソッドで編集リーダーを生成し、GetTextメソッドで中身を取得する。
以下は、GExpertsのユーティリティ関数で、エディタのバッファからTStreamへ中身を読み込むもの。

// 編集リーダーからTStreamへ読み込む
procedure GxSaveReaderToStream(EditReader: IOTAEditReader; Stream: TStream;
  TrailingNull: Boolean);
const
  // Leave typed constant as is - needed for streaming code.
  TerminatingNullChar: AnsiChar = #0;
var
  EditReaderPos: Integer;
  ReadDataSize: Integer;
  Buffer: array [0 .. EditReaderBufferSize] of AnsiChar;
  // Array of bytes, might be UTF-8
begin
  Assert(EditReader <> nil);
  Assert(Stream <> nil);

  EditReaderPos := 0;
  ReadDataSize := EditReader.GetText(EditReaderPos, Buffer,
    EditReaderBufferSize);
  Inc(EditReaderPos, ReadDataSize);
  while ReadDataSize = EditReaderBufferSize do
  begin
    Stream.Write(Buffer, ReadDataSize);
    ReadDataSize := EditReader.GetText(EditReaderPos, Buffer,
      EditReaderBufferSize);
    Inc(EditReaderPos, ReadDataSize);
  end;
  Stream.Write(Buffer, ReadDataSize);
  if TrailingNull then
    Stream.Write(TerminatingNullChar, SizeOf(TerminatingNullChar));
  // The source parsers need this
end;

// ソースエディタの中身をTSTringsにロードする
procedure GxLoadSourceEditorToUnicodeStrings(SourceEditor: IOTASourceEditor; Data: TStrings);
var
  MemStream: TMemoryStream;
begin
  Data.Clear;
  if not Assigned(SourceEditor) then
    raise Exception.Create
      ('No source editor in GxOtaLoadSourceEditorToUnicodeStrings');
  // TODO: Check stream format for forms as text (Ansi with escaped unicode, or UTF-8) in Delphi 2007/2009
  MemStream := TMemoryStream.Create;
  try
    GxSaveReaderToStream(SourceEditor.CreateReader, MemStream, False);
    MemStream.Position := 0;
{$IFDEF UNICODE}
    Data.LoadFromStream(MemStream, TEncoding.UTF8);
{$ELSE}
    if RunningDelphi8OrGreater then
      SynUnicode.LoadFromStream(Data, MemStream, seUTF8)
    else
      SynUnicode.LoadFromStream(Data, MemStream, seAnsi);
{$ENDIF}
  finally
    FreeAndNil(MemStream);
  end;
end;

どうも、CreateReaderで生成した編集リーダーは明示的に破棄する必要はなさそう。
一方、書き込む場合はIOTASourceEditorのCreateUndoableWriter*1メソッドで編集ライターを生成して、DeleteToメソッドで中身を削除したり、Insertメソッドで挿入する。
これまた、GExpertsのコードの一部。この関数は、エディタの中身を文字列で丸々入れ替える例。

procedure GxReplaceEditorText(SourceEditor: IOTASourceEditor; Text: string);
var
  Writer: IOTAEditWriter;
begin
  Assert(Assigned(SourceEditor));
  Writer := SourceEditor.CreateUndoableWriter;
  if not Assigned(Writer) then
    raise Exception.Create('No edit writer');
  Writer.DeleteTo(MaxLongint);
  Writer.Insert(PAnsiChar(AnsiToUtf8(Text)));
  Writer := nil;
end;

文字列をAnsiToUtf8関数で明示的にUTF-8へ変換して、そのポインタを渡している。編集ライターも明示的に破棄する必要は無いみたい。

まとめ

これで、Open Tools APIについてのネタは終わり。他にもOpen Tools APIはプロパティエディタやフォームエディタをカスタマイズしたり、キーバインドも変更できるので、アイデア次第でいろいろできそう。
DelphiC++BuilderIDEVCLで構築されているので、普通にアプリを作る上でのテクニックが十分に使用可能。資料は少ないけどGExpertsのソースコードとToolsAPI.pasはかなり参考になる。試行錯誤の末なので間違いがあるかもしれないので、その場合はアドバイスください。<(_ _)>

*1:CreateWriterメソッドもあるけど、こちらはUNDO不可な編集ライターを生成。