Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement quick fixes #30

Merged
merged 5 commits into from
May 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* Clear Active File menu item, which clears all issues from the active file.
* VS Code companion: Clear Issues For This File command, which removes any outstanding issues on the current file.
* Support for "quick fixes" in the IDE - suggested fixes that can be applied automatically.

### Changed

* Changes in issue validity are now immediately reflected across the UI.
* The issue view has been redesigned for improved performance and utility.
* The default SonarDelphi version is now [1.5.0](https://github.com/integrated-application-development/sonar-delphi/releases/tag/v1.5.0).

### Fixed

Expand Down
21 changes: 18 additions & 3 deletions client/source/DelphiLint.Analyzer.pas
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ TAnalyzerImpl = class(TInterfacedObject, IAnalyzer)
constructor Create;
destructor Destroy; override;

function GetIssues(FileName: string; Line: Integer = -1): TArray<ILiveIssue>;
function GetIssues(FileName: string; Line: Integer = -1; Column: Integer = -1): TArray<ILiveIssue>;

procedure UpdateIssueLine(FilePath: string; OriginalLine: Integer; NewLine: Integer);

Expand Down Expand Up @@ -326,7 +326,22 @@ function TAnalyzerImpl.GetCurrentAnalysis: TCurrentAnalysis;

//______________________________________________________________________________________________________________________

function TAnalyzerImpl.GetIssues(FileName: string; Line: Integer = -1): TArray<ILiveIssue>;
function TAnalyzerImpl.GetIssues(FileName: string; Line: Integer = -1; Column: Integer = -1): TArray<ILiveIssue>;

function Matches(Issue: ILiveIssue): Boolean;
var
AfterStart: Boolean;
BeforeEnd: Boolean;
begin
Result := (Issue.StartLine <= Line) and (Issue.EndLine >= Line);

if Result and (Column <> -1) then begin
AfterStart := (Issue.StartLine < Line) or (Issue.StartLineOffset <= Column);
BeforeEnd := (Issue.EndLine > Line) or (Issue.EndLineOffset >= Column);
Result := AfterStart and BeforeEnd;
end;
end;

var
SanitizedName: string;
Issue: ILiveIssue;
Expand All @@ -342,7 +357,7 @@ function TAnalyzerImpl.GetIssues(FileName: string; Line: Integer = -1): TArray<I
ResultList := TList<ILiveIssue>.Create;
try
for Issue in FActiveIssues[SanitizedName] do begin
if (Line >= Issue.StartLine) and (Line <= Issue.EndLine) then begin
if Matches(Issue) then begin
ResultList.Add(Issue);
end;
end;
Expand Down
18 changes: 17 additions & 1 deletion client/source/DelphiLint.Context.pas
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ TAnalysisStateChangeContext = record
function GetOnAnalysisStateChanged: TEventNotifier<TAnalysisStateChangeContext>;
function GetCurrentAnalysis: TCurrentAnalysis;
function GetInAnalysis: Boolean;
function GetIssues(FileName: string; Line: Integer = -1): TArray<ILiveIssue>;
function GetIssues(FileName: string; Line: Integer = -1; Column: Integer = -1): TArray<ILiveIssue>;
function GetRule(RuleKey: string; AllowRefresh: Boolean = True): TRule;

procedure UpdateIssueLine(FilePath: string; OriginalLine: Integer; NewLine: Integer);
Expand Down Expand Up @@ -103,10 +103,22 @@ TAnalysisStateChangeContext = record
function AddNotifier(Notifier: IIDEViewHandler): Integer;
procedure RemoveNotifier(Index: Integer);
function GetLeftColumn: Integer;
procedure ReplaceText(
Replacement: string;
StartLine: Integer;
StartColumn: Integer;
EndLine: Integer;
EndColumn: Integer
);
function GetColumn: Integer;
function GetRow: Integer;
function GetContextMenu: TPopupMenu;
function Raw: IInterface; // IOTAEditView

property FileName: string read GetFileName;
property LeftColumn: Integer read GetLeftColumn;
property Column: Integer read GetColumn;
property Row: Integer read GetRow;
end;

IIDESourceEditor = interface
Expand Down Expand Up @@ -236,6 +248,10 @@ TLinePaintContext = record
// From IOTAEditorServices
function AddEditorNotifier(Notifier: IIDEEditorHandler): Integer;
procedure RemoveEditorNotifier(const Index: Integer);
function GetTopView: IIDEEditView;

// From INTAEditorServices
function GetTopEditWindow: TCustomForm;

// From INTAEnvironmentOptionsServices
procedure RegisterAddInOptions(const Options: TAddInOptionsBase);
Expand Down
105 changes: 103 additions & 2 deletions client/source/DelphiLint.Data.pas
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,32 @@ TIssueMetadata = class(TObject)
property Status: TIssueStatus read FStatus;
end;

TQuickFixTextEdit = class(TObject)
private
FReplacement: string;
FRange: TRange;
public
constructor Create(Replacement: string; Range: TRange);
constructor CreateFromJson(Json: TJSONObject);
destructor Destroy; override;

property Replacement: string read FReplacement;
property Range: TRange read FRange;
end;

TQuickFix = class(TObject)
private
FMessage: string;
FTextEdits: TObjectList<TQuickFixTextEdit>;
public
constructor Create(Message: string; TextEdits: TObjectList<TQuickFixTextEdit>);
constructor CreateFromJson(Json: TJSONObject);
destructor Destroy; override;

property Message: string read FMessage;
property TextEdits: TObjectList<TQuickFixTextEdit> read FTextEdits;
end;

//______________________________________________________________________________________________________________________

TLintIssue = class(TObject)
Expand All @@ -115,14 +141,16 @@ TLintIssue = class(TObject)
FFilePath: string;
FRange: TRange;
FMetadata: TIssueMetadata;
FQuickFixes: TObjectList<TQuickFix>;

public
constructor Create(
RuleKey: string;
Message: string;
FilePath: string;
Range: TRange;
Metadata: TIssueMetadata = nil);
Metadata: TIssueMetadata = nil;
QuickFixes: TObjectList<TQuickFix> = nil);
constructor CreateFromJson(Json: TJSONObject);
destructor Destroy; override;

Expand All @@ -131,6 +159,7 @@ TLintIssue = class(TObject)
property FilePath: string read FFilePath;
property Range: TRange read FRange write FRange;
property Metadata: TIssueMetadata read FMetadata write FMetadata;
property QuickFixes: TObjectList<TQuickFix> read FQuickFixes;
end;

//______________________________________________________________________________________________________________________
Expand Down Expand Up @@ -303,7 +332,8 @@ constructor TLintIssue.Create(
Message: string;
FilePath: string;
Range: TRange;
Metadata: TIssueMetadata
Metadata: TIssueMetadata;
QuickFixes: TObjectList<TQuickFix>
);
begin
inherited Create;
Expand All @@ -312,6 +342,10 @@ constructor TLintIssue.Create(
FFilePath := FilePath;
FRange := Range;
FMetadata := Metadata;
FQuickFixes := QuickFixes;
if not Assigned(FQuickFixes) then begin
FQuickFixes := TObjectList<TQuickFix>.Create;
end;
end;

//______________________________________________________________________________________________________________________
Expand All @@ -320,6 +354,8 @@ constructor TLintIssue.CreateFromJson(Json: TJSONObject);
var
RangeJson: TJSONValue;
MetadataJson: TJSONValue;
QuickFixJson: TJSONValue;
QuickFixElement: TJSONValue;
begin
inherited Create;
FRuleKey := Json.GetValue<string>('ruleKey');
Expand All @@ -336,6 +372,14 @@ constructor TLintIssue.CreateFromJson(Json: TJSONObject);
if Assigned(MetadataJson) and (MetadataJson is TJSONObject) then begin
FMetadata := TIssueMetadata.CreateFromJson(MetadataJson as TJSONObject);
end;

FQuickFixes := TObjectList<TQuickFix>.Create;
QuickFixJson := Json.GetValue<TJSONValue>('quickFixes', nil);
if Assigned(QuickFixJson) and (QuickFixJson is TJSONArray) then begin
for QuickFixElement in TJSONArray(QuickFixJson) do begin
FQuickFixes.Add(TQuickFix.CreateFromJson(TJSONObject(QuickFixElement)));
end;
end;
end;

//______________________________________________________________________________________________________________________
Expand All @@ -344,6 +388,7 @@ destructor TLintIssue.Destroy;
begin
FreeAndNil(FRange);
FreeAndNil(FMetadata);
FreeAndNil(FQuickFixes);
inherited;
end;

Expand Down Expand Up @@ -541,4 +586,60 @@ constructor TSonarProjectOptions.Create(Url: string; Token: string; ProjectKey:
Self.ProjectKey := ProjectKey;
end;

//______________________________________________________________________________________________________________________

constructor TQuickFixTextEdit.Create(Replacement: string; Range: TRange);
begin
FReplacement := Replacement;
FRange := Range;
end;

//______________________________________________________________________________________________________________________

constructor TQuickFixTextEdit.CreateFromJson(Json: TJSONObject);
begin
FReplacement := Json.GetValue<string>('replacement');
FRange := TRange.CreateFromJson(Json.GetValue<TJSONObject>('range'));
end;

//______________________________________________________________________________________________________________________

destructor TQuickFixTextEdit.Destroy;
begin
FreeAndNil(FRange);
inherited;
end;

//______________________________________________________________________________________________________________________

constructor TQuickFix.Create(Message: string; TextEdits: TObjectList<TQuickFixTextEdit>);
begin
FMessage := Message;
FTextEdits := TextEdits;
end;

//______________________________________________________________________________________________________________________

constructor TQuickFix.CreateFromJson(Json: TJSONObject);
var
EditsJson: TJSONArray;
EditJson: TJSONValue;
begin
FMessage := Json.GetValue<string>('message');
FTextEdits := TObjectList<TQuickFixTextEdit>.Create;

EditsJson := Json.GetValue<TJSONArray>('textEdits');
for EditJson in EditsJson do begin
FTextEdits.Add(TQuickFixTextEdit.CreateFromJson(TJSONObject(EditJson)));
end;
end;

//______________________________________________________________________________________________________________________

destructor TQuickFix.Destroy;
begin
FreeAndNil(FTextEdits);
inherited;
end;

end.
42 changes: 42 additions & 0 deletions client/source/DelphiLint.Handlers.pas
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ interface
uses
System.Generics.Collections
, Vcl.Graphics
, Vcl.Menus
, DelphiLint.Events
, DelphiLint.Context
, DelphiLint.LiveData
, DelphiLint.PopupHook
;

type
Expand Down Expand Up @@ -97,6 +99,7 @@ TEditorHandler = class(THandler, IIDEEditorHandler)
private
FNotifiers: TList<IIDEHandler>;
FTrackers: TObjectList<TLineTracker>;
FPopupHooks: TDictionary<TPopupMenu, TEditorPopupMenuHook>;
FInitedViews: TList<IInterface>;

FOnActiveFileChanged: TEventNotifier<string>;
Expand All @@ -106,6 +109,8 @@ TEditorHandler = class(THandler, IIDEEditorHandler)
procedure InitView(const View: IIDEEditView);
function IsViewInited(const View: IIDEEditView): Boolean;
procedure OnAnalysisStateChanged(const StateChange: TAnalysisStateChangeContext);

procedure OnHookFreed(const Hook: TEditorPopupMenuHook);
public
constructor Create;
destructor Destroy; override;
Expand Down Expand Up @@ -298,6 +303,8 @@ constructor TEditorHandler.Create;
// Once registered with the IDE, notifiers are reference counted
FNotifiers := TList<IIDEHandler>.Create;
FTrackers := TObjectList<TLineTracker>.Create;
// These are components, nominally owned by the context menu
FPopupHooks := TDictionary<TPopupMenu, TEditorPopupMenuHook>.Create;
FInitedViews := TList<IInterface>.Create;
FOnActiveFileChanged := TEventNotifier<string>.Create;

Expand All @@ -309,11 +316,19 @@ constructor TEditorHandler.Create;
destructor TEditorHandler.Destroy;
var
Notifier: IIDEHandler;
Hook: TEditorPopupMenuHook;
begin
for Notifier in FNotifiers do begin
Notifier.Release;
end;

// Although these hooks are owned by the context menu, we need to free them when the plugin is disabled
for Hook in FPopupHooks.Values do begin
Hook.OnFreed.RemoveListener(OnHookFreed);
FreeAndNil(Hook);
end;
FreeAndNil(FPopupHooks);

if ContextValid then begin
Analyzer.OnAnalysisStateChanged.RemoveListener(OnAnalysisStateChanged);
end;
Expand Down Expand Up @@ -368,6 +383,19 @@ procedure TEditorHandler.InitView(const View: IIDEEditView);
end;
end;

procedure InitPopupHook;
var
PopupHook: TEditorPopupMenuHook;
ContextMenu: TPopupMenu;
begin
ContextMenu := View.GetContextMenu;
if not FPopupHooks.ContainsKey(ContextMenu) then begin
PopupHook := TEditorPopupMenuHook.Create(View.GetContextMenu);
PopupHook.OnFreed.AddListener(OnHookFreed);
FPopupHooks.Add(ContextMenu, PopupHook);
end;
end;

procedure InitNotifier;
var
Notifier: TViewHandler;
Expand All @@ -391,6 +419,7 @@ procedure TEditorHandler.InitView(const View: IIDEEditView);
begin
InitTracker;
InitNotifier;
InitPopupHook;
FInitedViews.Add(View.Raw);
end;

Expand Down Expand Up @@ -436,6 +465,19 @@ procedure TEditorHandler.OnAnalysisStateChanged(const StateChange: TAnalysisStat

//______________________________________________________________________________________________________________________

procedure TEditorHandler.OnHookFreed(const Hook: TEditorPopupMenuHook);
var
Menu: TPopupMenu;
begin
for Menu in FPopupHooks.Keys do begin
if FPopupHooks[Menu] = Hook then begin
FPopupHooks.Remove(Menu);
end;
end;
end;

//______________________________________________________________________________________________________________________

procedure TEditorHandler.OnTrackedLineChanged(const ChangedLine: TChangedLine);
begin
Analyzer.UpdateIssueLine(ChangedLine.Tracker.FilePath, ChangedLine.FromLine, ChangedLine.ToLine);
Expand Down
Loading
Loading