lazarus/ide/emptymethodsdlg.pas

439 lines
14 KiB
ObjectPascal

{
***************************************************************************
* *
* This source is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This code is distributed in the hope that it will be useful, but *
* WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* General Public License for more details. *
* *
* A copy of the GNU General Public License is available on the World *
* Wide Web at <http://www.gnu.org/copyleft/gpl.html>. You can also *
* obtain it by writing to the Free Software Foundation, *
* Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1335, USA. *
* *
***************************************************************************
Author: Mattias Gaertner
Abstract:
A dialog showing the empty methods of the current class
(at cursor in source editor).
With the ability to remove them automatically.
}
unit EmptyMethodsDlg;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, TypInfo,
// LCL
Forms, Controls, Dialogs, StdCtrls, ButtonPanel,
//LazUtils
LazLoggerBase,
// SynEdit
SynEdit, SynHighlighterPas,
// CodeTools
CodeToolsStructs, CodeCache, CodeToolManager, PascalParserTool, CodeTree,
// IdeIntf
IdeIntfStrConsts, SrcEditorIntf, LazIDEIntf, PropEdits, IDEDialogs,
// IDE
CustomFormEditor, JitForms, Project, LazarusIDEStrConsts, EditorOptions;
type
{ TEmptyMethodsDialog }
TEmptyMethodsDialog = class(TForm)
AllButton: TButton;
PublishedButton: TButton;
ButtonPanel1: TButtonPanel;
PrivateCheckBox: TCheckBox;
ProtectedCheckBox: TCheckBox;
PublicCheckBox: TCheckBox;
PublishedCheckBox: TCheckBox;
SectionsGroupBox: TGroupBox;
MethodsGroupBox: TGroupBox;
MethodsSynEdit: TSynEdit;
SynPasSyn1: TSynPasSyn;
procedure AllButtonClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure OKButtonClick(Sender: TObject);
procedure PrivateCheckBoxChange(Sender: TObject);
procedure PublishedButtonClick(Sender: TObject);
private
FCaret: TPoint;
FCode: TCodeBuffer;
function GetSections: TPascalClassSections;
procedure SetCaret(const AValue: TPoint);
procedure SetCode(const AValue: TCodeBuffer);
procedure SetSections(const AValue: TPascalClassSections);
procedure UpdateList;
public
property Sections: TPascalClassSections read GetSections write SetSections;
property Code: TCodeBuffer read FCode write SetCode;
property Caret: TPoint read FCaret write SetCaret;
end;
function ShowEmptyMethodsDialog: TModalResult;
function RemoveEmptyMethodsInUnit(Code: TCodeBuffer; AClassName: string;
X, Y: integer; Sections: TPascalClassSections): TModalResult;
implementation
{$R *.lfm}
function ShowEmptyMethodsDialog: TModalResult;
var
EmptyMethodsDialog: TEmptyMethodsDialog;
ErrMsg: String;
SrcEdit: TSourceEditorInterface;
Code: TCodeBuffer;
Caret: TPoint;
ListOfPCodeXYPosition: TFPList;
AllEmpty: boolean;
begin
Result:=mrCancel;
ListOfPCodeXYPosition:=TFPList.Create;
try
// init codetools
ErrMsg:=lisSAMIDEIsBusy;
if not LazarusIDE.BeginCodeTools then exit;
// get cursor position
ErrMsg:=lisSAMCursorIsNotInAClassDeclaration;
SrcEdit:=SourceEditorManagerIntf.ActiveEditor;
if SrcEdit=nil then exit;
Code:=TCodeBuffer(SrcEdit.CodeToolsBuffer);
if Code=nil then exit;
Caret:=SrcEdit.CursorTextXY;
ErrMsg:='';
// check cursor is in a class
if not CodeToolBoss.FindEmptyMethods(Code,'',Caret.X,Caret.Y,
AllPascalClassSections,ListOfPCodeXYPosition,AllEmpty)
then begin
DebugLn(['ShowEmptyMethodsDialog CodeToolBoss.FindEmptyMethods failed']);
if CodeToolBoss.ErrorMessage<>'' then begin
ErrMsg:='';
LazarusIDE.DoJumpToCodeToolBossError;
end else begin
IDEMessageDialog(lisEMDNoClass,
Format(lisEMDNoClassAt, [Code.Filename, IntToStr(Caret.Y), IntToStr(
Caret.X)]),
mtError,[mbCancel]);
end;
exit;
end;
CodeToolBoss.FreeListOfPCodeXYPosition(ListOfPCodeXYPosition);
EmptyMethodsDialog:=TEmptyMethodsDialog.Create(nil);
try
EmptyMethodsDialog.Code:=Code;
EmptyMethodsDialog.Caret:=Caret;
EmptyMethodsDialog.UpdateList;
Result:=EmptyMethodsDialog.ShowModal;
finally
EmptyMethodsDialog.Free;
end;
finally
CodeToolBoss.FreeListOfPCodeXYPosition(ListOfPCodeXYPosition);
if ErrMsg<>'' then begin
IDEMessageDialog(lisCCOErrorCaption,
Format(lisEMDUnableToShowEmptyMethodsOfTheCurrentClassBecause,
[LineEnding, ErrMsg]), mtError, [mbCancel]);
end;
end;
end;
function GetInheritedMethod(APersistent: TPersistent; PropInfo: PPropInfo): TMethod;
var
AncestorRoot, AncestorComponent: TComponent;
AncestorMethod: TMethod;
Comp: TComponent;
begin
FillByte(Result{%H-}, SizeOf(Result), 0);
if APersistent is TComponent then
begin
Comp := TComponent(APersistent);
if csAncestor in Comp.ComponentState then
begin
// search for ancestor component
if Assigned(Comp.Owner) then
begin
AncestorRoot := BaseFormEditor1.GetAncestorLookupRoot(Comp);
if Assigned(AncestorRoot) then
AncestorComponent := AncestorRoot.FindComponent(Comp.Name)
else
AncestorComponent := nil;
end
else
AncestorComponent := BaseFormEditor1.GetAncestorInstance(Comp);
if Assigned(AncestorComponent) then
begin
AncestorMethod := GetMethodProp(AncestorComponent, PropInfo);
if IsJITMethod(AncestorMethod) then
Result := AncestorMethod
end;
end;
end;
end;
function RemoveEmptyMethodsInUnit(Code: TCodeBuffer; AClassName: string;
X, Y: integer; Sections: TPascalClassSections): TModalResult;
var
RemovedProcHeads: TStrings;
PropChanged: boolean;
function ExtractClassName: string;
var
ProcName: string;
p: LongInt;
i: Integer;
begin
Result:='';
for i:=RemovedProcHeads.Count-1 downto 0 do
begin
ProcName:=RemovedProcHeads[i];
p:=System.Pos('.',ProcName);
if p<1 then
RemovedProcHeads.Delete(i)
else begin
Result:=copy(ProcName,1,p-1);
RemovedProcHeads[i]:=copy(ProcName,p+1,length(ProcName));
end;
end;
end;
procedure CheckEvents(APersistent: TPersistent);
// Read properties and remove event handlers which were removed from source by Codetools.
var
TypeInfo: PTypeInfo;
PropInfo: PPropInfo;
PropList: PPropList;
PropCount, ic, i: integer;
AMethod: TMethod;
AMethodName: String;
Coll: TCollection;
begin
TypeInfo:=PTypeInfo(APersistent.ClassInfo);
PropCount:=GetPropList(TypeInfo,PropList); // List of properties and their count
try
for ic:=0 to PropCount-1 do // iterate properties
begin
PropInfo:=PropList^[ic];
if PropInfo^.PropType^.Kind=tkMethod then
begin
AMethod:=GetMethodProp(APersistent,PropInfo); // event
AMethodName:=GlobalDesignHook.GetMethodName(AMethod,nil);
if AMethodName<>'' then
begin
i:=RemovedProcHeads.Count-1;
while (i>=0) and (CompareText(RemovedProcHeads[i],AMethodName)<>0) do
dec(i);
if i>=0 then
begin
//DebugLn([' CheckEvents Clearing Property=',PropInfo^.Name,' AMethodName=',AMethodName]);
AMethod := GetInheritedMethod(APersistent, PropInfo);
SetMethodProp(APersistent, PropInfo, AMethod);
PropChanged:=true;
end;
end;
end
else if PropInfo^.PropType^.Kind=tkClass then
begin
Coll := TCollection(GetObjectProp(APersistent, PropInfo, TCollection));
if Assigned(Coll) then
for i := 0 to Coll.Count - 1 do // CollectionItem can have events.
CheckEvents(Coll.Items[i]); // Recurse also because collections can be nested.
end;
end;
finally
FreeMem(PropList);
end;
end;
var
AllEmpty: boolean;
AnUnitInfo: TUnitInfo;
i: Integer;
LookupRoot: TComponent;
CurClassName: String;
begin
Result:=mrCancel;
RemovedProcHeads:=nil;
try
if not CodeToolBoss.RemoveEmptyMethods(Code,AClassName,X,Y,Sections,AllEmpty,
[phpAddClassName,phpDoNotAddSemicolon,phpWithoutParamList,
phpWithoutBrackets,phpWithoutClassKeyword,phpWithoutSemicolon],
RemovedProcHeads)
then begin
DebugLn(['RemoveEmptyMethods failed']);
exit;
end;
if (RemovedProcHeads<>nil) and (RemovedProcHeads.Count>0) then begin
// RemovedProcHeads contains a list of classname.procname, remove classname from the list
CurClassName:=ExtractClassName;
if (CurClassName<>'') and (Project1<>nil) then
begin
AnUnitInfo:=Project1.UnitInfoWithFilename(Code.Filename);
if AnUnitInfo<>nil then
begin
// fix events of designer components
LookupRoot:=AnUnitInfo.Component;
if (LookupRoot<>nil) and (CompareText(LookupRoot.ClassName,CurClassName)=0) then
begin
PropChanged:=false;
CheckEvents(LookupRoot);
for i:=0 to LookupRoot.ComponentCount-1 do
CheckEvents(LookupRoot.Components[i]);
// update objectinspector
if PropChanged and (GlobalDesignHook.LookupRoot=LookupRoot) then
GlobalDesignHook.RefreshPropertyValues;
end;
end;
end;
end;
finally
RemovedProcHeads.Free;
end;
Result:=mrOk;
end;
{ TEmptyMethodsDialog }
procedure TEmptyMethodsDialog.FormCreate(Sender: TObject);
begin
Caption:=lisEMDEmptyMethods;
SectionsGroupBox.Caption:=lisEMDSearchInTheseClassSections;
PrivateCheckBox.Caption:=lisPrivate;
ProtectedCheckBox.Caption:=lisProtected;
PublicCheckBox.Caption:=lisEMDPublic;
PublishedCheckBox.Caption:=lisEMDPublished;
AllButton.Caption:=lisEMDAll;
PublishedButton.Caption:=lisEMDOnlyPublished;
MethodsGroupBox.Caption:=lisEMDFoundEmptyMethods;
Sections:=AllPascalClassSections;
ButtonPanel1.OKButton.Caption:=lisEMDRemoveMethods;
ButtonPanel1.CancelButton.Caption:=lisCancel;
EditorOpts.GetSynEditSettings(MethodsSynEdit);
end;
procedure TEmptyMethodsDialog.OKButtonClick(Sender: TObject);
begin
if LazarusIDE.BeginCodeTools
and (RemoveEmptyMethodsInUnit(Code,'',Caret.X,Caret.Y,Sections)=mrOk) then
ModalResult:=mrOk;
end;
procedure TEmptyMethodsDialog.PrivateCheckBoxChange(Sender: TObject);
begin
UpdateList;
end;
procedure TEmptyMethodsDialog.PublishedButtonClick(Sender: TObject);
begin
Sections:=[pcsPublished];
end;
procedure TEmptyMethodsDialog.SetSections(const AValue: TPascalClassSections);
begin
PrivateCheckBox.Checked:=pcsPrivate in AValue;
ProtectedCheckBox.Checked:=pcsProtected in AValue;
PublicCheckBox.Checked:=pcsPublic in AValue;
PublishedCheckBox.Checked:=pcsPublished in AValue;
end;
procedure TEmptyMethodsDialog.SetCaret(const AValue: TPoint);
begin
FCaret:=AValue;
end;
function TEmptyMethodsDialog.GetSections: TPascalClassSections;
begin
Result:=[];
if PrivateCheckBox.Checked then Include(Result,pcsPrivate);
if ProtectedCheckBox.Checked then Include(Result,pcsProtected);
if PublicCheckBox.Checked then Include(Result,pcsPublic);
if PublishedCheckBox.Checked then Include(Result,pcsPublished);
end;
procedure TEmptyMethodsDialog.SetCode(const AValue: TCodeBuffer);
begin
if FCode=AValue then exit;
FCode:=AValue;
end;
procedure TEmptyMethodsDialog.UpdateList;
var
CurSections: TPascalClassSections;
ListOfPCodeXYPosition: TFPList;
i: Integer;
CodePos: TCodeXYPosition;
Tool: TCodeTool;
CleanPos: integer;
Node: TCodeTreeNode;
NodeText: String;
AllEmpty: boolean;
NewTxt: String;
begin
if (Code=nil) or (Caret.X<1) or (Caret.Y<1) then begin
MethodsSynEdit.Text:='';
exit;
end;
CurSections:=Sections;
ListOfPCodeXYPosition:=TFPList.Create;
try
if (not CodeToolBoss.FindEmptyMethods(Code,'',Caret.X,Caret.Y,
CurSections,ListOfPCodeXYPosition,AllEmpty))
or (not CodeToolBoss.Explore(Code,Tool,false))
then begin
MethodsSynEdit.Text:='CodeToolBoss.FindEmptyMethods failed'#10
+CodeToolBoss.ErrorMessage;
exit;
end;
NewTxt:='';
for i:=0 to ListOfPCodeXYPosition.Count-1 do begin
CodePos:=PCodeXYPosition(ListOfPCodeXYPosition[i])^;
//DebugLn(['TEmptyMethodsDialog.UpdateList ',i,' ',DbgsCXY(CodePos)]);
if Tool.CaretToCleanPos(CodePos,CleanPos)<>0 then begin
DebugLn(['TEmptyMethodsDialog.UpdateList Tool.CaretToCleanPos failed']);
continue;
end;
Node:=Tool.FindDeepestNodeAtPos(CleanPos,false);
if Node=nil then begin
DebugLn(['TEmptyMethodsDialog.UpdateList Tool.FindDeepestNodeAtPos failed']);
continue;
end;
NodeText:=Tool.ExtractProcHead(Node,[phpWithStart,phpWithParameterNames,
phpWithVarModifiers,phpWithDefaultValues,phpWithResultType,
phpWithCallingSpecs,phpWithProcModifiers]);
NewTxt:=NewTxt+NodeText+#10;
end;
MethodsSynEdit.Text:=NewTxt;
finally
CodeToolBoss.FreeListOfPCodeXYPosition(ListOfPCodeXYPosition);
end;
end;
procedure TEmptyMethodsDialog.AllButtonClick(Sender: TObject);
begin
Sections:=AllPascalClassSections;
end;
end.