unit uEditorTasks;
{$INCLUDE 'FarCtags.inc'}

interface

uses
  Windows, SysUtils, Classes,
  {$INCLUDE 'DelphiFarUnits.inc'} ,
  uCtags, uOptions;

type
  EEditorTasksError = class(Exception)
  end;

  TEditorTasks = class
  private
    fProgress: TFarProgress;
    fEditorID: TIntPtr;
    fOptions: TOptions;
    fCtags: TCtags;
    function GetCtags: TCtags;
    function GetProgress: TFarProgress;
    function GetOptions: TOptions;
  protected
    function SaveEditedFile(out FileName: string; out Temporary: boolean): boolean;
    function LoadTagsFromFile(const FileName: string; Temporary: boolean): boolean;
    function ShowFunctionList(out LineNr: integer): boolean;
    function IsCtagChar(C: Char): boolean;
    function GetCtagUnderCursor(out Ctag: string): boolean;
    procedure RefreshOptions;
    property Progress: TFarProgress read GetProgress;
    property Ctags: TCtags read GetCtags;
    property Options: TOptions read GetOptions;
  public
    constructor Create;
    destructor Destroy; override;
    function Execute: boolean;
    property EditorID: TIntPtr read fEditorID write fEditorID;
  end;

implementation

uses
  uSystem, uFiles,
  uMessages, uFunctionListDialog;

{ TEditorTasks }

constructor TEditorTasks.Create;
begin
  inherited Create;
  fProgress := nil;
  fEditorID := -1;
  fCtags := nil;
end;

destructor TEditorTasks.Destroy;
begin
  FreeAndNil(fProgress);
  FreeAndNil(fOptions);
  FreeAndNil(fCtags);
  inherited;
end;

function TEditorTasks.Execute: boolean;
var
  FileName: string;
  Temporary: boolean;
  LineNr: integer;
begin
  Result := False;
  if not SaveEditedFile(FileName, Temporary) then begin
    TFarPlugin.Instance.ShowMessage([GetMsg(MTitle), GetMsg(MFailedToSaveFile), GetMsg(MOK)], FMSG_WARNING, 1);
    exit;
  end;
  if not LoadTagsFromFile(FileName, Temporary) then
    exit;
  if Ctags.Count = 0 then
    exit;
  if not ShowFunctionList(LineNr) then
    exit;
  if not TFarEditor.SetCursorPosition(EditorID, LineNr-1) then
    exit;
  Result := True;
end;

function TEditorTasks.GetCtags: TCtags;
var
  ModuleName: string;
  CtagsFileName: string;
  n: DWORD;
begin
  if fCtags = nil then begin
    fCtags := TCtags.Create;
    CtagsFileName := '';
    if CtagsFileName = '' then
    begin
      CtagsFileName := ExpandEnvironmentVars(Options.CtagsFileName);
    end;
    if CtagsFileName = '' then
    begin
      SetLength(ModuleName, MAX_PATH);
      n := GetModuleFileName(hInstance, @ModuleName[1], Length(ModuleName));
      if n > 0 then begin
        SetLength(ModuleName, n);
        CtagsFileName := ExtractFilePath(ExpandFileName(ModuleName)) + 'ctags\ctags.exe';
      end;
    end;
    if (CtagsFileName <> '') and FileExists(CtagsFileName) then
      fCtags.ExeFileName := CtagsFileName;
  end;
  Result := fCtags;
end;

function TEditorTasks.GetCtagUnderCursor(out Ctag: string): boolean;
var
  Info: TEditorInfo;
  Line: string;
  LineLen: integer;
  TagStart, TagEnd: integer;
begin
  Result := False;
  if not TFarEditor.GetInfo(EditorID, Info) then
    Exit;
  if not TFarEditor.GetString(EditorID, Info.CurLine, Line) then
    Exit;
  LineLen := Length(Line);
  TagStart := Info.CurPos + 1;
  TagEnd := TagStart;
  if (TagStart < 1) or (TagStart > LineLen) then
    Exit;
  while (TagStart > 0) and IsCtagChar(Line[TagStart]) do
    Dec(TagStart);
  while (TagEnd <= LineLen) and IsCtagChar(Line[TagEnd]) do
    Inc(TagEnd);
  if (TagStart = TagEnd) then
    Exit;
  Ctag := Copy(Line, TagStart + 1, TagEnd - TagStart - 1);
  Result := True;
end;

function TEditorTasks.GetOptions: TOptions;
begin
  if fOptions = nil then
  begin
    fOptions := TOptions.Create;
    fOptions.Load;
  end;
  Result := fOptions;
end;

function TEditorTasks.GetProgress: TFarProgress;
begin
  if fProgress = nil then begin
    fProgress := DelphiFar.TFarProgress.Create;
  end;
  Result := fProgress;
end;

function TEditorTasks.IsCtagChar(C: Char): boolean;
begin
  if C < ' ' then
    Result := False
  else if C >= #128 then
    Result := True
  else
    Result := CharInSet(C, ['0'..'9', 'A'..'Z', 'a'..'z', '_']);
end;

function TEditorTasks.LoadTagsFromFile(const FileName: string; Temporary: boolean): boolean;
begin
  Result := False;
  try
    Progress.Show([GetMsg(MTitle), GetMsg(MLoadingCtags), Progress.BuildProgressFilename(FileName), Progress.BuildProgressBar(0, 0)]);
    try
      try
        Ctags.LoadFromFile(FileName);
        Result := True;
      finally
        Progress.Hide;
      end;
    except
      on E: ECtagsError_CtagsNotFound do begin
        TFarPlugin.Instance.ShowMessage([GetMsg(MTitle), GetMsg(MFailedToRunCtags), Ctags.ExeFileName, GetMsg(MOK)], FMSG_WARNING, 1);
        exit;
      end;
      on E: ECtagsError_ExecutionFailed do begin
        if E.Message <> '' then
          TFarPlugin.Instance.ShowMessage([GetMsg(MTitle), GetMsg(MCtagsFailed), E.Message, Format(GetMsg(MResultCode), [E.ResultCode]), GetMsg(MOK)], FMSG_WARNING, 1)
        else
          TFarPlugin.Instance.ShowMessage([GetMsg(MTitle), GetMsg(MCtagsFailed), Format(GetMsg(MResultCode), [E.ResultCode]), GetMsg(MOK)], FMSG_WARNING, 1);
        exit;
      end;
      on E: ECtagsError do begin
        TFarPlugin.Instance.ShowMessage([GetMsg(MTitle), GetMsg(MCtagsFailed), GetMsg(MOk)], FMSG_WARNING, 1);
        exit;
      end;
    end;
  finally
    if Temporary then
      DeleteFile(FileName);
  end;
end;

procedure TEditorTasks.RefreshOptions;
begin
  FreeAndNil(fOptions);
end;

function TEditorTasks.SaveEditedFile(out FileName: string; out Temporary: boolean): boolean;
var
  EditorInfo: TEditorInfo;
  OriginalFileName: string;
  TempFileStream: TTextStream;
  ProgressStrings: array of string;
  i: Integer;
  s: string;
begin
  Result := False;
  if not TFarEditor.GetInfo(EditorID, EditorInfo) then
    exit;
  if not TFarEditor.GetFileName(EditorID, OriginalFileName) then
    exit;
  // If the editor isn't changed, we can use the underlying file
  if (EditorInfo.CurState and ECSTATE_MODIFIED) = 0 then begin
    FileName := OriginalFileName;
    Temporary := False;
    Result := True;
  end
  // Otherwise save all contents to a temporary file and use that
  else begin
    SetLength(ProgressStrings, 4);
    ProgressStrings[0] := GetMsg(MTitle);
    ProgressStrings[1] := GetMsg(MSavingFile);
    ProgressStrings[2] := Progress.BuildProgressFilename(OriginalFileName);
    ProgressStrings[3] := Progress.BuildProgressBar(0, EditorInfo.TotalLines);
    Progress.Show(ProgressStrings);
    try
      FileName := SystemTempFile(SystemTempDir, 'ctagstemp', ExtractFileExt(OriginalFileName));
      Temporary := True;
      try
        TempFileStream := TTextStream.Create(FileName, fmCreate);
        try
          for i := 0 to Pred(EditorInfo.TotalLines) do begin
            if not TFarEditor.GetString(EditorID, i, s) then
              exit;
            TempFileStream.WriteLine(s);
            if Progress.NeedsRefresh then begin
              ProgressStrings[3] := Progress.BuildProgressBar(i+1, EditorInfo.TotalLines);
              Progress.Show(ProgressStrings);
            end;
          end;
          Result := True;
        finally
          FreeAndNil(TempFileStream);
        end;
      finally
        if not Result then
          DeleteFile(FileName);
      end;
    finally
      Progress.Hide;
    end;
  end;
end;

function TEditorTasks.ShowFunctionList(out LineNr: integer): boolean;
var
  Dlg: TFunctionListDialog;
  EditInfo: TEditorInfo;
  SelectedItem, SelectedLine, Line: integer;
  i: integer;
  Selection: string;
begin
  Result := False;
  Dlg := TFunctionListDialog.Create;
  try
    Dlg.Ctags := Ctags;
    if Options.FunctionSearchDelayEnabled and (Options.FunctionSearchDelay > 0) then
      Dlg.SearchDelay := Options.FunctionSearchDelay;
    Dlg.EditorID := Self.EditorID;
    if TFarEditor.GetSelection(EditorID, Selection) then begin
      Dlg.SearchString := Trim(Selection);
    end
    else if GetCtagUnderCursor(Selection) and Dlg.ValueExists(Selection) then begin
      Dlg.SearchString := Trim(Selection);
    end;
    SelectedItem := 0;
    SelectedLine := 0;
    if TFarEditor.GetInfo(EditorID, EditInfo) then begin
      for i := 0 to Pred(Ctags.Count) do begin
        Line := Ctags[i].Line;
        if Pred(Line) <= EditInfo.CurLine then begin
          if Line >= SelectedLine then begin
            SelectedLine := Ctags[i].Line;
            SelectedItem := i;
          end;
        end;
      end;
    end;
    Dlg.SelectedItem := SelectedItem;
    if Dlg.Execute then begin
      LineNr := Ctags[Dlg.SelectedItem].Line;
      Result := True;
    end;
  finally
    FreeAndNil(Dlg);
  end;
end;

end.
