unit NewSort;

{ Sort selected lines of text in Delphi's source code editor.
  This unit does exactly the same thing as SortSel.pas, but
  it uses the new Tools API.
  
  Copyright © 1998 Tempest Software, Inc.
}

interface

// Sort the selection in the current file.
procedure SortSelection;

implementation

uses Windows, Classes, SysUtils, ToolsAPI;

resourcestring
  sCloseViews = 'Close all views but one';
  
type
  // Keep track of buffer positions for each line of text.
  // An edit writer's positions are relative to the original file.
  // When writing the sorted text, it is important to keep track
  // of the original position for each line.
  TPos = class
  private
    fLeft, fRight: LongInt;
  public
    constructor Create(Left, Right: LongInt);
    property Left: LongInt read fLeft;
    property Right: LongInt read fRight;
  end;
  TPosList = class(TList)
  public
    procedure AddPos(Left, Right: LongInt);
    destructor Destroy; override;
  end;

{ TPos }
constructor TPos.Create(Left, Right: Integer);
begin
  inherited Create;
  fLeft := Left;
  fRight := Right;
end;

{ TPosList }
procedure TPosList.AddPos(Left, Right: LongInt);
begin
  inherited Add(TPos.Create(Left, Right));
end;

destructor TPosList.Destroy;
var
  I: Integer;
  Obj: TObject;
begin
  for I := 0 to Count-1 do
  begin
    Obj := Self[I];
    Self[I] := nil;
    Obj.Free;
  end;
  inherited;
end;


// Sort the columnar selection by extracting the selected text
// in each line, sorting the text, and replacing the text in
// each line with the sorted text.
procedure SortColumns(const Editor: IOTASourceEditor; const View: IOTAEditView);
var
  TopLeft: TOTAEditPos;
  BottomRight: TOTAEditPos;
  Left, Right: TOTAEditPos;
  LeftChar, RightChar: TOTACharPos;
  LeftPos, RightPos: LongInt;
  Reader: IOTAEditReader;
  Writer: IOTAEditWriter;
  Text: string;
  Strings: TStringList;
  PosList: TPosList;
  Line: LongInt;
  I: Integer;
begin
  // Get the corners of the selected region.
  TopLeft     := TOTAEditPos(Editor.BlockStart);
  BottomRight := TOTAEditPos(Editor.BlockAfter);

  Strings := TStringList.Create;
  try
    PosList := TPosList.Create;
    try
      // Get the selected text.
      Reader := Editor.CreateReader;
      Left.Col := TopLeft.Col;
      Right.Col := BottomRight.Col;
      for Line := TopLeft.Line to BottomRight.Line do
      begin
        // Convert the edit positions on the current line to
        // buffer positions. Delphi requires the intermediate
        // step of character positions.
        Left.Line := Line;
        Right.Line := Line;
        View.ConvertPos(True, Left,  LeftChar);
        View.ConvertPos(True, Right, RightChar);
        // include the character at RightChar
        Inc(RightChar.CharIndex);
        LeftPos  := View.CharPosToPos(LeftChar);
        RightPos := View.CharPosToPos(RightChar);
        SetLength(Text, RightPos - LeftPos);
        Reader.GetText(LeftPos, PChar(Text), Length(Text));
        // If the text includes the end of the line characters,
        // strip the CR and LF.
        while (Length(Text) > 0) and (Text[Length(Text)] in [#13, #10]) do
        begin
          Delete(Text, Length(Text), 1);
          Dec(RightPos);
        end;
        Strings.Add(Text);
        PosList.AddPos(LeftPos, RightPos);
      end;
      Reader := nil;

      // Now sort the text.
      Strings.Sort;

      // And write the sorted text, line by line.
      Writer := Editor.CreateUndoableWriter;
      I := 0;
      for Line := TopLeft.Line to BottomRight.Line do
      begin
        with TPos(PosList[I]) do
        begin
          Writer.CopyTo(Left);
          Writer.DeleteTo(Right);
        end;
        Writer.Insert(PChar(Strings[I]));
        Inc(I);
      end;
      Writer := nil;
    finally
      PosList.Free;
    end;
  finally
    Strings.Free;
  end;

  // Set the cursor to the start of the sorted text.
  View.CursorPos := TopLeft;

  // Make sure the top of the sorted text is visible.
  // Scroll the edit window if ncessary.
  if (TopLeft.Line < View.TopPos.Line) or
     (BottomRight.Line >= View.TopPos.Line + View.ViewSize.CY)
  then
    View.TopPos := TopLeft;

  // Select the newly inserted, sorted text.
  Editor.BlockVisible := False;
  Editor.BlockType    := btColumn;
  Editor.BlockStart   := TOTACharPos(TopLeft);
  Editor.BlockAfter   := TOTACharPos(BottomRight);
  Editor.BlockVisible := True;
end;

// Sort the selection as lines.
procedure SortLines(const Editor: IOTASourceEditor; const View: IOTAEditView);
var
  BlockStart: TOTACharPos;
  BlockAfter: TOTACharPos;
  StartPos, EndPos: LongInt;
  Reader: IOTAEditReader;
  Writer: IOTAEditWriter;
  TopPos, CursorPos: TOTAEditPos;
  Text: string;
  Strings: TStringList;
begin
  // Get the limits of the selected text.
  BlockStart := Editor.BlockStart;
  BlockAfter := Editor.BlockAfter;

  // Sort entire lines, so modify the positions accordingly.
  BlockStart.CharIndex := 0;   // start of line
  if BlockAfter.CharIndex > 0 then
  begin
    // Select the entire line by setting the After position
    // to the start of the next line.
    BlockAfter.CharIndex := 0;
    Inc(BlockAfter.Line);
  end;

  // Convert the character positions to buffer positions.
  StartPos := View.CharPosToPos(BlockStart);
  EndPos   := View.CharPosToPos(BlockAfter);

  // Get the selected text.
  Reader := Editor.CreateReader;
  SetLength(Text, EndPos - StartPos - 1);
  Reader.GetText(StartPos, PChar(Text), Length(Text));
  Reader := nil;

  // Sort the text. Use a TStringList because it's easy.
  Strings := TStringList.Create;
  try
    Strings.Text := Text;
    Strings.Sort;
    Text := Strings.Text;
  finally
    Strings.Free;
  end;

  // Replace the selection with the sorted text.
  Writer := Editor.CreateUndoableWriter;
  Writer.CopyTo(StartPos);
  Writer.DeleteTo(EndPos);
  Writer.Insert(PChar(Text));
  Writer := nil;

  // Set the cursor to the start of the sorted text.
  View.ConvertPos(False, CursorPos, BlockStart);
  View.CursorPos := CursorPos;

  // Make sure the top of the sorted text is visible.
  // Scroll the edit window if ncessary.
  if (BlockStart.Line < View.TopPos.Line) or
     (BlockAfter.Line >= View.TopPos.Line + View.ViewSize.CY) then
  begin
    View.ConvertPos(False, TopPos, BlockStart);
    View.TopPos := TopPos;
  end;

  // Select the newly inserted, sorted text.
  Editor.BlockVisible := False;
  Editor.BlockType    := btNonInclusive;
  Editor.BlockStart   := BlockStart;
  Editor.BlockAfter   := BlockAfter;
  Editor.BlockVisible := True;
end;


// Sort the selection in the current file.
procedure SortSelection;
var
  ModuleServices: IOTAModuleServices;
  Module: IOTAModule;
  Intf: IOTAEditor;
  Editor: IOTASourceEditor;
  View: IOTAEditView;
  I: Integer;
begin
  ModuleServices := BorlandIDEServices as IOTAModuleServices;
  // Get the module interface for the current file.
  Module := ModuleServices.CurrentModule;
  // If no file is open, Module is nil.
  if Module = nil then
    Exit;

  // Get the interface to the source editor.
  for I := 0 to Module.GetModuleFileCount-1 do
  begin
    Intf := Module.GetModuleFileEditor(I);
    if Intf.QueryInterface(IOTASourceEditor, Editor) = S_OK then
      Break;
  end;
  // If the file is not a source file, Editor is nil.
  if Editor = nil then
    Exit;

  // The expert cannot tell which view is active, so force
  // the user to have only one view at a time.
  if Editor.EditViewCount > 1 then
    raise Exception.Create(sCloseViews);

  View := Editor.EditViews[0];

  // Columnar selections work entirely differently.
  if Editor.BlockType = btColumn then
    SortColumns(Editor, View)
  else
    SortLines(Editor, View);

  // Bring the focus back to the source editor window.
  Editor.Show;
end;

end.