diff --git a/CHANGELOG.md b/CHANGELOG.md index caba19c..2a46e97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/client/source/DelphiLint.Analyzer.pas b/client/source/DelphiLint.Analyzer.pas index ae9a749..5528178 100644 --- a/client/source/DelphiLint.Analyzer.pas +++ b/client/source/DelphiLint.Analyzer.pas @@ -66,7 +66,7 @@ TAnalyzerImpl = class(TInterfacedObject, IAnalyzer) constructor Create; destructor Destroy; override; - function GetIssues(FileName: string; Line: Integer = -1): TArray; + function GetIssues(FileName: string; Line: Integer = -1; Column: Integer = -1): TArray; procedure UpdateIssueLine(FilePath: string; OriginalLine: Integer; NewLine: Integer); @@ -326,7 +326,22 @@ function TAnalyzerImpl.GetCurrentAnalysis: TCurrentAnalysis; //______________________________________________________________________________________________________________________ -function TAnalyzerImpl.GetIssues(FileName: string; Line: Integer = -1): TArray; +function TAnalyzerImpl.GetIssues(FileName: string; Line: Integer = -1; Column: Integer = -1): TArray; + + 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; @@ -342,7 +357,7 @@ function TAnalyzerImpl.GetIssues(FileName: string; Line: Integer = -1): TArray.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; diff --git a/client/source/DelphiLint.Context.pas b/client/source/DelphiLint.Context.pas index 69b5612..a47e0ba 100644 --- a/client/source/DelphiLint.Context.pas +++ b/client/source/DelphiLint.Context.pas @@ -67,7 +67,7 @@ TAnalysisStateChangeContext = record function GetOnAnalysisStateChanged: TEventNotifier; function GetCurrentAnalysis: TCurrentAnalysis; function GetInAnalysis: Boolean; - function GetIssues(FileName: string; Line: Integer = -1): TArray; + function GetIssues(FileName: string; Line: Integer = -1; Column: Integer = -1): TArray; function GetRule(RuleKey: string; AllowRefresh: Boolean = True): TRule; procedure UpdateIssueLine(FilePath: string; OriginalLine: Integer; NewLine: Integer); @@ -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 @@ -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); diff --git a/client/source/DelphiLint.Data.pas b/client/source/DelphiLint.Data.pas index 4393ec1..2f853e1 100644 --- a/client/source/DelphiLint.Data.pas +++ b/client/source/DelphiLint.Data.pas @@ -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; + public + constructor Create(Message: string; TextEdits: TObjectList); + constructor CreateFromJson(Json: TJSONObject); + destructor Destroy; override; + + property Message: string read FMessage; + property TextEdits: TObjectList read FTextEdits; + end; + //______________________________________________________________________________________________________________________ TLintIssue = class(TObject) @@ -115,6 +141,7 @@ TLintIssue = class(TObject) FFilePath: string; FRange: TRange; FMetadata: TIssueMetadata; + FQuickFixes: TObjectList; public constructor Create( @@ -122,7 +149,8 @@ TLintIssue = class(TObject) Message: string; FilePath: string; Range: TRange; - Metadata: TIssueMetadata = nil); + Metadata: TIssueMetadata = nil; + QuickFixes: TObjectList = nil); constructor CreateFromJson(Json: TJSONObject); destructor Destroy; override; @@ -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 read FQuickFixes; end; //______________________________________________________________________________________________________________________ @@ -303,7 +332,8 @@ constructor TLintIssue.Create( Message: string; FilePath: string; Range: TRange; - Metadata: TIssueMetadata + Metadata: TIssueMetadata; + QuickFixes: TObjectList ); begin inherited Create; @@ -312,6 +342,10 @@ constructor TLintIssue.Create( FFilePath := FilePath; FRange := Range; FMetadata := Metadata; + FQuickFixes := QuickFixes; + if not Assigned(FQuickFixes) then begin + FQuickFixes := TObjectList.Create; + end; end; //______________________________________________________________________________________________________________________ @@ -320,6 +354,8 @@ constructor TLintIssue.CreateFromJson(Json: TJSONObject); var RangeJson: TJSONValue; MetadataJson: TJSONValue; + QuickFixJson: TJSONValue; + QuickFixElement: TJSONValue; begin inherited Create; FRuleKey := Json.GetValue('ruleKey'); @@ -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.Create; + QuickFixJson := Json.GetValue('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; //______________________________________________________________________________________________________________________ @@ -344,6 +388,7 @@ destructor TLintIssue.Destroy; begin FreeAndNil(FRange); FreeAndNil(FMetadata); + FreeAndNil(FQuickFixes); inherited; end; @@ -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('replacement'); + FRange := TRange.CreateFromJson(Json.GetValue('range')); +end; + +//______________________________________________________________________________________________________________________ + +destructor TQuickFixTextEdit.Destroy; +begin + FreeAndNil(FRange); + inherited; +end; + +//______________________________________________________________________________________________________________________ + +constructor TQuickFix.Create(Message: string; TextEdits: TObjectList); +begin + FMessage := Message; + FTextEdits := TextEdits; +end; + +//______________________________________________________________________________________________________________________ + +constructor TQuickFix.CreateFromJson(Json: TJSONObject); +var + EditsJson: TJSONArray; + EditJson: TJSONValue; +begin + FMessage := Json.GetValue('message'); + FTextEdits := TObjectList.Create; + + EditsJson := Json.GetValue('textEdits'); + for EditJson in EditsJson do begin + FTextEdits.Add(TQuickFixTextEdit.CreateFromJson(TJSONObject(EditJson))); + end; +end; + +//______________________________________________________________________________________________________________________ + +destructor TQuickFix.Destroy; +begin + FreeAndNil(FTextEdits); + inherited; +end; + end. diff --git a/client/source/DelphiLint.Handlers.pas b/client/source/DelphiLint.Handlers.pas index 7865a1b..27daf3c 100644 --- a/client/source/DelphiLint.Handlers.pas +++ b/client/source/DelphiLint.Handlers.pas @@ -22,9 +22,11 @@ interface uses System.Generics.Collections , Vcl.Graphics + , Vcl.Menus , DelphiLint.Events , DelphiLint.Context , DelphiLint.LiveData + , DelphiLint.PopupHook ; type @@ -97,6 +99,7 @@ TEditorHandler = class(THandler, IIDEEditorHandler) private FNotifiers: TList; FTrackers: TObjectList; + FPopupHooks: TDictionary; FInitedViews: TList; FOnActiveFileChanged: TEventNotifier; @@ -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; @@ -298,6 +303,8 @@ constructor TEditorHandler.Create; // Once registered with the IDE, notifiers are reference counted FNotifiers := TList.Create; FTrackers := TObjectList.Create; + // These are components, nominally owned by the context menu + FPopupHooks := TDictionary.Create; FInitedViews := TList.Create; FOnActiveFileChanged := TEventNotifier.Create; @@ -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; @@ -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; @@ -391,6 +419,7 @@ procedure TEditorHandler.InitView(const View: IIDEEditView); begin InitTracker; InitNotifier; + InitPopupHook; FInitedViews.Add(View.Raw); end; @@ -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); diff --git a/client/source/DelphiLint.IDEContext.pas b/client/source/DelphiLint.IDEContext.pas index d73cb0f..c257b8b 100644 --- a/client/source/DelphiLint.IDEContext.pas +++ b/client/source/DelphiLint.IDEContext.pas @@ -94,6 +94,10 @@ TToolsApiServices = class(TInterfacedObject, IIDEServices) // 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); @@ -155,6 +159,16 @@ TToolsApiEditView = class(TToolsApiWrapper, IIDEEditView) 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; end; TToolsApiSourceEditor = class(TToolsApiWrapper, IIDESourceEditor) @@ -432,6 +446,34 @@ function TToolsApiServices.GetToolBar(ToolBarId: string): TToolBar; //______________________________________________________________________________________________________________________ +function TToolsApiServices.GetTopEditWindow: TCustomForm; +var + Window: INTAEditWindow; +begin + Result := nil; + + Window := (BorlandIDEServices as INTAEditorServices).GetTopEditWindow; + if Assigned(Window) then begin + Result := Window.GetForm; + end; +end; + +//______________________________________________________________________________________________________________________ + +function TToolsApiServices.GetTopView: IIDEEditView; +var + TopView: IOTAEditView; +begin + TopView := (BorlandIDEServices as IOTAEditorServices).GetTopView; + + Result := nil; + if Assigned(TopView) then begin + Result := TToolsApiEditView.Create(TopView); + end; +end; + +//______________________________________________________________________________________________________________________ + function TToolsApiServices.CreateDockableForm(const CustomForm: TCustomDockableFormBase): TCustomForm; begin Result := (BorlandIDEServices as INTAServices).CreateDockableForm(CustomForm); @@ -568,6 +610,11 @@ function TToolsApiEditView.GetLineTracker: IIDEEditLineTracker; Result := TToolsApiEditLineTracker.Create(FRaw.Buffer.GetEditLineTracker); end; +function TToolsApiEditView.GetRow: Integer; +begin + Result := FRaw.Buffer.EditPosition.Row; +end; + procedure TToolsApiEditView.GoToPosition(const Line: Integer; const Column: Integer); var ScrollLine: Integer; @@ -594,6 +641,47 @@ procedure TToolsApiEditView.RemoveNotifier(Index: Integer); FRaw.RemoveNotifier(Index); end; +procedure TToolsApiEditView.ReplaceText(Replacement: string; StartLine, StartColumn, EndLine, EndColumn: Integer); + + procedure MoveCursor(const Line: Integer; const Column: Integer); + var + Pos: TOTAEditPos; + begin + Pos.Col := Column; + Pos.Line := Line; + FRaw.SetCursorPos(Pos); + end; + +begin + FRaw.Buffer.EditBlock.Reset; + MoveCursor(StartLine, StartColumn + 1); + FRaw.Buffer.EditBlock.BeginBlock; + MoveCursor(EndLine, EndColumn + 1); + FRaw.Buffer.EditBlock.EndBlock; + + FRaw.Buffer.EditPosition.InsertText(Replacement); + MoveCursor(StartLine, StartColumn + 1); +end; + +function TToolsApiEditView.GetColumn: Integer; +begin + Result := FRaw.Buffer.EditPosition.Column; +end; + +function TToolsApiEditView.GetContextMenu: TPopupMenu; +var + Window: INTAEditWindow; +begin + Window := FRaw.GetEditWindow; + + if Assigned(Window) then begin + Result := Window.Form.FindComponent('EditorLocalMenu') as TPopupMenu; + end + else begin + Result := nil; + end; +end; + function TToolsApiEditView.GetFileName: string; begin Result := FRaw.Buffer.FileName; diff --git a/client/source/DelphiLint.IssueActions.pas b/client/source/DelphiLint.IssueActions.pas index 45634ed..01e873c 100644 --- a/client/source/DelphiLint.IssueActions.pas +++ b/client/source/DelphiLint.IssueActions.pas @@ -26,6 +26,9 @@ interface ; type + +//______________________________________________________________________________________________________________________ + TIssueMenuItemFactory = class(TObject) private FIssue: ILiveIssue; @@ -35,8 +38,11 @@ TIssueMenuItemFactory = class(TObject) destructor Destroy; override; function HideIssue(Owner: TComponent): TMenuItem; + function ApplyQuickFix(Owner: TComponent): TMenuItem; end; +//______________________________________________________________________________________________________________________ + TIssueLinkedMenuItem = class abstract(TMenuItem) private FIssue: ILiveIssue; @@ -48,13 +54,42 @@ TIssueLinkedMenuItem = class abstract(TMenuItem) property LinkedIssue: ILiveIssue read FIssue; end; +//______________________________________________________________________________________________________________________ + TUntetherMenuItem = class(TIssueLinkedMenuItem) protected procedure DoOnClick(Sender: TObject); override; end; +//______________________________________________________________________________________________________________________ + + TQuickFixMenuItem = class(TIssueLinkedMenuItem) + private + FIndex: Integer; + + procedure CalculateReplacementMetrics( + TextEdit: TLiveTextEdit; + out AddedLines: Integer; + out AddedColumns: Integer + ); + procedure OffsetTextEdits(QuickFix: TLiveQuickFix; EditIndex: Integer); + function GetQuickFix: TLiveQuickFix; + protected + procedure DoOnClick(Sender: TObject); override; + public + constructor CreateLinked(AOwner: TComponent; Issue: ILiveIssue; Index: Integer); + property QuickFix: TLiveQuickFix read GetQuickFix; + end; + +//______________________________________________________________________________________________________________________ + implementation +uses + System.SysUtils + , DelphiLint.Context + ; + //______________________________________________________________________________________________________________________ constructor TIssueMenuItemFactory.Create(Issue: ILiveIssue); @@ -76,12 +111,54 @@ destructor TIssueMenuItemFactory.Destroy; function TIssueMenuItemFactory.HideIssue(Owner: TComponent): TMenuItem; begin Result := TUntetherMenuItem.CreateLinked(Owner, FIssue); - Result.Caption := 'Hide'; + Result.Name := 'HideIssue'; + Result.Caption := 'Hide Issue'; Result.Enabled := FIssue.IsTethered; end; //______________________________________________________________________________________________________________________ +function TIssueMenuItemFactory.ApplyQuickFix(Owner: TComponent): TMenuItem; + + function QuickFixItem(Index: Integer; IndexInName: Boolean): TQuickFixMenuItem; + begin + Result := TQuickFixMenuItem.CreateLinked(Owner, FIssue, Index); + Result.Name := Format('ApplyQuickFix_%d', [Index]); + Result.Enabled := Result.QuickFix.Tethered; + + if IndexInName then begin + Result.Caption := Format('&%d: %s', [Index + 1, Result.QuickFix.Message]); + end + else begin + Result.Caption := Format('&Quick Fix: %s', [Result.QuickFix.Message]); + end; + end; + +var + I: Integer; + FixCount: Integer; +begin + FixCount := FIssue.QuickFixes.Count; + + if FixCount = 0 then begin + Result := nil; + end + else if FixCount = 1 then begin + Result := QuickFixItem(0, False); + end + else begin + Result := TMenuItem.Create(Owner); + Result.Name := 'DelphiLintApplyQuickFixGroup'; + Result.Caption := Format('&Quick Fix (%d available)', [FixCount]); + + for I := 0 to FixCount - 1 do begin + Result.Add(QuickFixItem(I, True)); + end; + end; +end; + +//______________________________________________________________________________________________________________________ + constructor TIssueLinkedMenuItem.CreateLinked(AOwner: TComponent; Issue: ILiveIssue); begin inherited Create(AOwner); @@ -99,4 +176,139 @@ procedure TUntetherMenuItem.DoOnClick(Sender: TObject); //______________________________________________________________________________________________________________________ +constructor TQuickFixMenuItem.CreateLinked(AOwner: TComponent; Issue: ILiveIssue; Index: Integer); +begin + inherited CreateLinked(AOwner, Issue); + FIndex := Index; +end; + +//______________________________________________________________________________________________________________________ + +procedure TQuickFixMenuItem.DoOnClick(Sender: TObject); +var + QuickFix: TLiveQuickFix; + TextEdit: TLiveTextEdit; + View: IIDEEditView; + I: Integer; +begin + if FIssue.QuickFixes.Count < FIndex then begin + Log.Warn('Quick fix menu item wanted nonexistent quick fix at index %d', [FIndex]); + Exit; + end; + + QuickFix := FIssue.QuickFixes[FIndex]; + View := LintContext.IDEServices.GetTopView; + + if not QuickFix.Tethered then begin + Log.Warn('Attempted to apply an untethered quick fix'); + Exit; + end + else if not Assigned(View) then begin + Log.Warn('No top view found when applying quick fix'); + Exit; + end; + + for I := 0 to QuickFix.TextEdits.Count - 1 do begin + TextEdit := QuickFix.TextEdits[I]; + + View.ReplaceText( + TextEdit.Replacement, + QuickFix.Issue.StartLine + TextEdit.RelativeStartLine, + TextEdit.StartLineOffset, + QuickFix.Issue.StartLine + TextEdit.RelativeEndLine, + TextEdit.EndLineOffset + ); + + OffsetTextEdits(QuickFix, I); + end; + + QuickFix.Untether; + View.Paint; +end; + +//______________________________________________________________________________________________________________________ + +function TQuickFixMenuItem.GetQuickFix: TLiveQuickFix; +begin + Result := nil; + if (FIndex >= 0) and (FIndex < FIssue.QuickFixes.Count) then begin + Result := FIssue.QuickFixes[FIndex]; + end; +end; + +//______________________________________________________________________________________________________________________ + +procedure TQuickFixMenuItem.CalculateReplacementMetrics( + TextEdit: TLiveTextEdit; + out AddedLines: Integer; + out AddedColumns: Integer +); +var + RangeLength: Integer; + RangeHeight: Integer; + ReplacementLastLineLength: Integer; + Lines: TStringList; +begin + if TextEdit.RelativeStartLine = TextEdit.RelativeEndLine then begin + RangeHeight := 1; + RangeLength := TextEdit.EndLineOffset - TextEdit.StartLineOffset; + end + else begin + RangeHeight := TextEdit.RelativeEndLine - TextEdit.RelativeStartLine; + RangeLength := TextEdit.EndLineOffset; + end; + + Lines := TStringList.Create; + try + Lines.Text := TextEdit.Replacement; + + if Lines.Count <> 0 then begin + ReplacementLastLineLength := Length(Lines[Lines.Count - 1]); + AddedColumns := ReplacementLastLineLength - RangeLength; + AddedLines := Lines.Count - RangeHeight; + end + else begin + AddedColumns := -RangeLength; + AddedLines := 0; + end; + finally + FreeAndNil(Lines); + end; +end; + +//______________________________________________________________________________________________________________________ + +procedure TQuickFixMenuItem.OffsetTextEdits(QuickFix: TLiveQuickFix; EditIndex: Integer); +var + AppliedEdit: TLiveTextEdit; + I: Integer; + LinesAdded: Integer; + ColumnsAdded: Integer; + SuccessorEdit: TLiveTextEdit; +begin + AppliedEdit := QuickFix.TextEdits[EditIndex]; + CalculateReplacementMetrics(AppliedEdit, LinesAdded, ColumnsAdded); + + for I := EditIndex + 1 to QuickFix.TextEdits.Count - 1 do begin + SuccessorEdit := QuickFix.TextEdits[I]; + + if SuccessorEdit.RelativeStartLine >= AppliedEdit.RelativeEndLine then begin + if (SuccessorEdit.RelativeStartLine = AppliedEdit.RelativeEndLine) + and(SuccessorEdit.StartLineOffset > AppliedEdit.EndLineOffset) + then begin + SuccessorEdit.StartLineOffset := SuccessorEdit.StartLineOffset + ColumnsAdded; + end; + + if (SuccessorEdit.RelativeEndLine = AppliedEdit.RelativeEndLine) + and (SuccessorEdit.EndLineOffset > AppliedEdit.EndLineOffset) + then begin + SuccessorEdit.EndLineOffset := SuccessorEdit.EndLineOffset + ColumnsAdded; + end; + + SuccessorEdit.RelativeStartLine := SuccessorEdit.RelativeStartLine + LinesAdded; + SuccessorEdit.RelativeEndLine := SuccessorEdit.RelativeEndLine + LinesAdded; + end; + end; +end; + end. diff --git a/client/source/DelphiLint.LiveData.pas b/client/source/DelphiLint.LiveData.pas index 8337197..3bf1659 100644 --- a/client/source/DelphiLint.LiveData.pas +++ b/client/source/DelphiLint.LiveData.pas @@ -22,9 +22,11 @@ interface uses DelphiLint.Data , DelphiLint.Events + , System.Generics.Collections ; type + TLiveQuickFix = class; ILiveIssue = interface ['{AC60181F-D4C3-46B2-8B02-02FDD1D511D6}'] @@ -44,6 +46,7 @@ interface function IsTethered: Boolean; function GetLinesMoved: Integer; procedure SetLinesMoved(NewStartLine: Integer); + function QuickFixes: TObjectList; procedure NewLineMoveSession; procedure UpdateTether(LineNum: Integer; LineText: string); @@ -55,6 +58,46 @@ interface property OnUntethered: TEventNotifier read GetOnUntethered; end; +//______________________________________________________________________________________________________________________ + + TLiveTextEdit = class(TObject) + private + FReplacement: string; + FStartLine: Integer; + FEndLine: Integer; + FStartLineOffset: Integer; + FEndLineOffset: Integer; + public + constructor Create(TextEdit: TQuickFixTextEdit; IssueLine: Integer); + + property Replacement: string read FReplacement; + property RelativeStartLine: Integer read FStartLine write FStartLine; + property RelativeEndLine: Integer read FEndLine write FEndLine; + property StartLineOffset: Integer read FStartLineOffset write FStartLineOffset; + property EndLineOffset: Integer read FEndLineOffset write FEndLineOffset; + end; + + TLiveIssueImpl = class; + +//______________________________________________________________________________________________________________________ + + TLiveQuickFix = class(TObject) + private + FMessage: string; + FTextEdits: TObjectList; + FTethered: Boolean; + FIssue: TLiveIssueImpl; + public + constructor Create(QuickFix: TQuickFix; Issue: TLiveIssueImpl); + procedure Untether; + destructor Destroy; override; + + property Message: string read FMessage; + property TextEdits: TObjectList read FTextEdits; + property Tethered: Boolean read FTethered; + property Issue: TLiveIssueImpl read FIssue; + end; + //______________________________________________________________________________________________________________________ TLiveIssueImpl = class(TInterfacedObject, ILiveIssue) @@ -74,6 +117,7 @@ TLiveIssueImpl = class(TInterfacedObject, ILiveIssue) FTethered: Boolean; FOnUntethered: TEventNotifier; FLines: TArray; + FQuickFixes: TObjectList; public constructor Create(Issue: TLintIssue; IssueLines: TArray; HasMetadata: Boolean = False); destructor Destroy; override; @@ -89,6 +133,7 @@ TLiveIssueImpl = class(TInterfacedObject, ILiveIssue) function CreationDate: string; function Status: TIssueStatus; function HasMetadata: Boolean; + function QuickFixes: TObjectList; function StartLine: Integer; function EndLine: Integer; @@ -118,6 +163,8 @@ implementation //______________________________________________________________________________________________________________________ constructor TLiveIssueImpl.Create(Issue: TLintIssue; IssueLines: TArray; HasMetadata: Boolean = False); +var + QuickFix: TQuickFix; begin inherited Create; @@ -161,6 +208,11 @@ constructor TLiveIssueImpl.Create(Issue: TLintIssue; IssueLines: TArray; FLinesMoved := 0; FLines := IssueLines; FTethered := True; + + FQuickFixes := TObjectList.Create; + for QuickFix in Issue.QuickFixes do begin + FQuickFixes.Add(TLiveQuickFix.Create(QuickFix, Self)); + end; end; //______________________________________________________________________________________________________________________ @@ -168,17 +220,24 @@ constructor TLiveIssueImpl.Create(Issue: TLintIssue; IssueLines: TArray; destructor TLiveIssueImpl.Destroy; begin FreeAndNil(FOnUntethered); + FreeAndNil(FQuickFixes); inherited; end; //______________________________________________________________________________________________________________________ procedure TLiveIssueImpl.Untether; +var + QuickFix: TLiveQuickFix; begin if FTethered then begin FTethered := False; FOnUntethered.Notify(StartLine); end; + + for QuickFix in FQuickFixes do begin + QuickFix.Untether; + end; end; //______________________________________________________________________________________________________________________ @@ -288,6 +347,8 @@ function TLiveIssueImpl.IsTethered: Boolean; Result := FTethered; end; +//______________________________________________________________________________________________________________________ + function TLiveIssueImpl.Message: string; begin Result := FMessage; @@ -302,19 +363,78 @@ procedure TLiveIssueImpl.NewLineMoveSession; FLinesMoved := 0; end; +//______________________________________________________________________________________________________________________ + function TLiveIssueImpl.OriginalEndLine: Integer; begin Result := FEndLine; end; +//______________________________________________________________________________________________________________________ + function TLiveIssueImpl.OriginalStartLine: Integer; begin Result := FStartLine; end; +//______________________________________________________________________________________________________________________ + function TLiveIssueImpl.RuleKey: string; begin Result := FRuleKey; end; -end. +//______________________________________________________________________________________________________________________ + +function TLiveIssueImpl.QuickFixes: TObjectList; +begin + Result := FQuickFixes; +end; + +//______________________________________________________________________________________________________________________ + +constructor TLiveQuickFix.Create(QuickFix: TQuickFix; Issue: TLiveIssueImpl); +var + TextEdit: TQuickFixTextEdit; +begin + inherited Create; + + FTethered := True; + FMessage := QuickFix.Message; + FTextEdits := TObjectList.Create; + FIssue := Issue; + + for TextEdit in QuickFix.TextEdits do begin + FTextEdits.Add(TLiveTextEdit.Create(TextEdit, Issue.StartLine)); + end; +end; + +//______________________________________________________________________________________________________________________ + +procedure TLiveQuickFix.Untether; +begin + FTethered := False; +end; + +//______________________________________________________________________________________________________________________ + +destructor TLiveQuickFix.Destroy; +begin + FreeAndNil(FTextEdits); + inherited; +end; + +//______________________________________________________________________________________________________________________ + +constructor TLiveTextEdit.Create(TextEdit: TQuickFixTextEdit; IssueLine: Integer); +begin + inherited Create; + + FReplacement := TextEdit.Replacement; + FStartLine := TextEdit.Range.StartLine - IssueLine; + FEndLine := TextEdit.Range.EndLine - IssueLine; + FStartLineOffset := TextEdit.Range.StartLineOffset; + FEndLineOffset := TextEdit.Range.EndLineOffset; +end; + +end. \ No newline at end of file diff --git a/client/source/DelphiLint.PopupHook.pas b/client/source/DelphiLint.PopupHook.pas new file mode 100644 index 0000000..8bc05cd --- /dev/null +++ b/client/source/DelphiLint.PopupHook.pas @@ -0,0 +1,159 @@ +{ +DelphiLint Client +Copyright (C) 2024 Integrated Application Development + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This program 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 +Lesser General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +} +unit DelphiLint.PopupHook; + +interface + +uses + System.Classes + , Vcl.Menus + , DelphiLint.Events + ; + +type + TEditorPopupMenuHook = class(TComponent) + private + FMenu: TPopupMenu; + FBaseOnPopup: TNotifyEvent; + FOnFreed: TEventNotifier; + + procedure PopulatePopup(Sender: TObject); + procedure ClearTaggedItems; + procedure TagItem(Item: TMenuItem); + public + constructor Create(Owner: TComponent); override; + destructor Destroy; override; + + property OnFreed: TEventNotifier read FOnFreed; + end; + +implementation + +uses + System.SysUtils + , DelphiLint.Context + , DelphiLint.LiveData + , DelphiLint.IssueActions + ; + +const + CDelphiLintMenuItemTag = 856248; + +//______________________________________________________________________________________________________________________ + +procedure TEditorPopupMenuHook.ClearTaggedItems; +var + I: Integer; +begin + for I := FMenu.Items.Count - 1 downto 0 do begin + if FMenu.Items[I].Tag = CDelphiLintMenuItemTag then begin + FMenu.Items[I].Free; + end; + end; +end; + +//______________________________________________________________________________________________________________________ + +constructor TEditorPopupMenuHook.Create(Owner: TComponent); +begin + inherited; + + Assert(Owner is TPopupMenu, 'Popup menu hook owner is not a TPopupMenu'); + FMenu := TPopupMenu(Owner); + + FBaseOnPopup := FMenu.OnPopup; + FMenu.OnPopup := PopulatePopup; + + FOnFreed := TEventNotifier.Create; +end; + +//______________________________________________________________________________________________________________________ + +destructor TEditorPopupMenuHook.Destroy; +begin + FOnFreed.Notify(Self); + FreeAndNil(FOnFreed); + ClearTaggedItems; + FMenu.OnPopup := FBaseOnPopup; + inherited; +end; + +//______________________________________________________________________________________________________________________ + +procedure TEditorPopupMenuHook.PopulatePopup(Sender: TObject); + + function Separator(Owner: TComponent): TMenuItem; + begin + Result := TMenuItem.Create(Owner); + Result.Name := 'DelphiLintSeparator'; + Result.Caption := '-'; + TagItem(Result); + end; + +var + TopView: IIDEEditView; + Issues: TArray; + Issue: ILiveIssue; + MenuItemFactory: TIssueMenuItemFactory; + MenuItem: TMenuItem; + SeparatorAdded: Boolean; +begin + if Assigned(FBaseOnPopup) then begin + FBaseOnPopup(Sender); + end; + + ClearTaggedItems; + + if not ContextValid then begin + Exit; + end; + + SeparatorAdded := False; + TopView := LintContext.IDEServices.GetTopView; + if Assigned(TopView) then begin + Issues := Analyzer.GetIssues(TopView.FileName, TopView.Row, TopView.Column); + + for Issue in Issues do begin + if not SeparatorAdded then begin + FMenu.Items.Add(Separator(FMenu)); + SeparatorAdded := True; + end; + + MenuItemFactory := TIssueMenuItemFactory.Create(Issue); + try + // Apply quick fix + MenuItem := MenuItemFactory.ApplyQuickFix(FMenu); + if Assigned(MenuItem) then begin + TagItem(MenuItem); + FMenu.Items.Add(MenuItem); + end; + finally + FreeAndNil(MenuItemFactory); + end; + end; + end; +end; + +//______________________________________________________________________________________________________________________ + +procedure TEditorPopupMenuHook.TagItem(Item: TMenuItem); +begin + Item.Tag := CDelphiLintMenuItemTag; +end; + +end. diff --git a/client/source/DelphiLint.Settings.pas b/client/source/DelphiLint.Settings.pas index 2814a49..b6da5f7 100644 --- a/client/source/DelphiLint.Settings.pas +++ b/client/source/DelphiLint.Settings.pas @@ -169,7 +169,7 @@ function TLintSettings.GetJavaExe(Index: Integer): string; function TLintSettings.GetDefaultSonarDelphiVersion: string; begin - Result := '1.4.0'; + Result := '1.5.0'; end; //______________________________________________________________________________________________________________________ diff --git a/client/source/DelphiLint.ToolFrame.pas b/client/source/DelphiLint.ToolFrame.pas index 6b671dd..a545512 100644 --- a/client/source/DelphiLint.ToolFrame.pas +++ b/client/source/DelphiLint.ToolFrame.pas @@ -222,6 +222,7 @@ function TLintToolFrame.CreateIssuePopup(Index: Integer): TPopupMenu; var Issue: ILiveIssue; MenuItemFactory: TIssueMenuItemFactory; + Item: TMenuItem; I: Integer; begin if (Index < 0) or (Index >= FIssues.Count) then begin @@ -240,6 +241,11 @@ function TLintToolFrame.CreateIssuePopup(Index: Integer): TPopupMenu; try Result.Items.Add(DummyMenuItem(Result)); Result.Items.Add(MenuItemFactory.HideIssue(Result)); + + Item := MenuItemFactory.ApplyQuickFix(Result); + if Assigned(Item) then begin + Result.Items.Add(Item); + end; finally FreeAndNil(MenuItemFactory); end; diff --git a/client/source/DelphiLintClient280.dpk b/client/source/DelphiLintClient280.dpk index 47e105e..738156f 100644 --- a/client/source/DelphiLintClient280.dpk +++ b/client/source/DelphiLintClient280.dpk @@ -66,7 +66,8 @@ contains DelphiLint.HtmlGen in 'DelphiLint.HtmlGen.pas', DelphiLint.ExtWebView2 in 'DelphiLint.ExtWebView2.pas', DelphiLint.LiveData in 'DelphiLint.LiveData.pas', - DelphiLint.IssueActions in 'DelphiLint.IssueActions.pas'; + DelphiLint.IssueActions in 'DelphiLint.IssueActions.pas', + DelphiLint.PopupHook in 'DelphiLint.PopupHook.pas'; {$R DelphiLintClientAdditional.res} diff --git a/client/source/DelphiLintClient280.dproj b/client/source/DelphiLintClient280.dproj index 306c2f8..a0b01b9 100644 --- a/client/source/DelphiLintClient280.dproj +++ b/client/source/DelphiLintClient280.dproj @@ -138,6 +138,7 @@ $(PreBuildEvent)]]> + BITMAP DL_SPLASH diff --git a/client/source/DelphiLintClient290.dpk b/client/source/DelphiLintClient290.dpk index 2a55688..0fce52d 100644 --- a/client/source/DelphiLintClient290.dpk +++ b/client/source/DelphiLintClient290.dpk @@ -66,7 +66,8 @@ contains DelphiLint.HtmlGen in 'DelphiLint.HtmlGen.pas', DelphiLint.ExtWebView2 in 'DelphiLint.ExtWebView2.pas', DelphiLint.LiveData in 'DelphiLint.LiveData.pas', - DelphiLint.IssueActions in 'DelphiLint.IssueActions.pas'; + DelphiLint.IssueActions in 'DelphiLint.IssueActions.pas', + DelphiLint.PopupHook in 'DelphiLint.PopupHook.pas'; {$R DelphiLintClientAdditional.res} diff --git a/client/source/DelphiLintClient290.dproj b/client/source/DelphiLintClient290.dproj index bf7af48..8044708 100644 --- a/client/source/DelphiLintClient290.dproj +++ b/client/source/DelphiLintClient290.dproj @@ -139,6 +139,7 @@ $(PreBuildEvent)]]> + BITMAP DL_SPLASH diff --git a/client/test/DelphiLintTest.Handlers.pas b/client/test/DelphiLintTest.Handlers.pas index c4f0333..b414bc9 100644 --- a/client/test/DelphiLintTest.Handlers.pas +++ b/client/test/DelphiLintTest.Handlers.pas @@ -82,6 +82,7 @@ implementation uses System.SysUtils + , Vcl.Menus , DelphiLint.Handlers , DelphiLint.Context , DelphiLintTest.MockContext @@ -408,6 +409,7 @@ procedure TEditorHandlerTest.TestActivatedViewDoesNotDoubleInitTracker; TMock.Construct(MockLineTracker); MockLineTracker.MockedFileName := CFileName; MockView.MockedLineTracker := MockLineTracker; + MockView.MockedContextMenu := TPopupMenu.Create(nil); Handler := TEditorHandler.Create; try @@ -449,6 +451,7 @@ procedure TEditorHandlerTest.TestNewViewInitsTracker(NewType: string); TMock.Construct(MockLineTracker); MockLineTracker.MockedFileName := CFileName; MockView.MockedLineTracker := MockLineTracker; + MockView.MockedContextMenu := TPopupMenu.Create(nil); Handler := TEditorHandler.Create; try @@ -499,6 +502,7 @@ procedure TEditorHandlerTest.TestIssueLineUpdatedWhenTrackedLineChanged; TMock.Construct(MockLineTracker); MockLineTracker.MockedFileName := 'abc.pas'; MockView.MockedLineTracker := MockLineTracker; + MockView.MockedContextMenu := TPopupMenu.Create(nil); Handler := TEditorHandler.Create; try diff --git a/client/test/DelphiLintTest.MockContext.pas b/client/test/DelphiLintTest.MockContext.pas index 68f029d..3deb165 100644 --- a/client/test/DelphiLintTest.MockContext.pas +++ b/client/test/DelphiLintTest.MockContext.pas @@ -72,8 +72,9 @@ TMockAnalyzer = class(THookedObject, IAnalyzer) function GetOnAnalysisStateChanged: TEventNotifier; function GetCurrentAnalysis: TCurrentAnalysis; function GetInAnalysis: Boolean; - function GetIssues(FileName: string; Line: Integer = -1): TArray; + function GetIssues(FileName: string; Line: Integer = -1; Column: Integer = -1): TArray; function GetRule(RuleKey: string; AllowRefresh: Boolean = True): TRule; + function GetQuickFixesAtPosition(FileName: string; Line: Integer; Column: Integer): TArray; procedure UpdateIssueLine(FilePath: string; OriginalLine: Integer; NewLine: Integer); @@ -97,7 +98,8 @@ TMockIDEObject = class(TInterfacedObject) TEditViewCallType = ( evcPaint, - evcGoToPosition + evcGoToPosition, + evcReplaceText ); TMockEditView = class(TMockIDEObject, IIDEEditView) @@ -107,6 +109,9 @@ TMockEditView = class(TMockIDEObject, IIDEEditView) FNotifiers: TDictionary; FNextId: Integer; FLeftColumn: Integer; + FColumn: Integer; + FRow: Integer; + FContextMenu: TPopupMenu; public constructor Create; destructor Destroy; override; @@ -118,11 +123,24 @@ TMockEditView = class(TMockIDEObject, IIDEEditView) 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; property MockedFileName: string read FFileName write FFileName; property MockedLineTracker: IIDEEditLineTracker read FLineTracker write FLineTracker; property MockedNotifiers: TDictionary read FNotifiers write FNotifiers; property MockedLeftColumn: Integer read FLeftColumn write FLeftColumn; + property MockedRow: Integer read FRow write FRow; + property MockedColumn: Integer read FColumn write FColumn; + property MockedContextMenu: TPopupMenu read FContextMenu write FContextMenu; end; TMockSourceEditor = class(TMockIDEObject, IIDESourceEditor) @@ -230,6 +248,7 @@ TMockIDE = class(TObject) FPluginLicenseStatus: string; FPluginRegistered: Boolean; FMainMenu: TMainMenu; + FTopEditWindow: TCustomForm; public constructor Create; destructor Destroy; override; @@ -247,6 +266,7 @@ TMockIDE = class(TObject) property PluginRegistered: Boolean read FPluginRegistered write FPluginRegistered; property MainMenu: TMainMenu read FMainMenu write FMainMenu; + property TopEditWindow: TCustomForm read FTopEditWindow write FTopEditWindow; end; TMockIDEServices = class(THookedObject, IIDEServices) @@ -261,6 +281,7 @@ TMockIDEServices = class(THookedObject, IIDEServices) FActiveProject: IIDEProject; FCurrentModule: IIDEModule; FModules: TList; + FTopView: IIDEEditView; public constructor Create; destructor Destroy; override; @@ -270,6 +291,7 @@ TMockIDEServices = class(THookedObject, IIDEServices) procedure MockToolBar(Id: string; ToolBar: TToolBar); procedure MockActiveProject(Project: IIDEProject); procedure MockCurrentModule(Module: IIDEModule); + procedure MockTopView(TopView: IIDEEditView); procedure MockModules(Modules: TList); // From IOTAIDEThemingServices @@ -323,6 +345,10 @@ TMockIDEServices = class(THookedObject, IIDEServices) // 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); @@ -451,7 +477,7 @@ function TMockAnalyzer.GetInAnalysis: Boolean; //______________________________________________________________________________________________________________________ -function TMockAnalyzer.GetIssues(FileName: string; Line: Integer): TArray; +function TMockAnalyzer.GetIssues(FileName: string; Line: Integer = -1; Column: Integer = -1): TArray; var ReturnIssues: TList; LineNum: Integer; @@ -486,6 +512,24 @@ function TMockAnalyzer.GetOnAnalysisStateChanged: DelphiLint.Events.TEventNotifi //______________________________________________________________________________________________________________________ +function TMockAnalyzer.GetQuickFixesAtPosition(FileName: string; Line, Column: Integer): TArray; +var + Issues: TArray; + Issue: ILiveIssue; + QuickFixes: TList; +begin + QuickFixes := TList.Create; + + Issues := GetIssues(FileName, Line, Column); + for Issue in Issues do begin + QuickFixes.AddRange(Issue.QuickFixes); + end; + + Result := QuickFixes.ToArray; +end; + +//______________________________________________________________________________________________________________________ + function TMockAnalyzer.GetRule(RuleKey: string; AllowRefresh: Boolean): TRule; begin if not FRules.ContainsKey(RuleKey) then begin @@ -797,6 +841,13 @@ procedure TMockIDEServices.MockToolBar(Id: string; ToolBar: TToolBar); //______________________________________________________________________________________________________________________ +procedure TMockIDEServices.MockTopView(TopView: IIDEEditView); +begin + FTopView := TopView; +end; + +//______________________________________________________________________________________________________________________ + constructor TMockIDEServices.Create; begin inherited; @@ -998,6 +1049,20 @@ function TMockIDEServices.GetToolBar(ToolBarId: string): TToolBar; //______________________________________________________________________________________________________________________ +function TMockIDEServices.GetTopEditWindow: TCustomForm; +begin + Result := FIDE.TopEditWindow; +end; + +//______________________________________________________________________________________________________________________ + +function TMockIDEServices.GetTopView: IIDEEditView; +begin + Result := FTopView; +end; + +//______________________________________________________________________________________________________________________ + procedure TMockIDEServices.RegisterAddInOptions(const Options: TAddInOptionsBase); begin NotifyEvent(iscRegisterAddInOptions, [Options]); @@ -1073,6 +1138,7 @@ destructor TMockIDE.Destroy; FreeAndNil(FPluginInfos); FreeAndNil(FPluginIcon); FreeAndNil(FMainMenu); + FreeAndNil(FTopEditWindow); inherited; end; @@ -1117,6 +1183,7 @@ constructor TMockEditView.Create; destructor TMockEditView.Destroy; begin FreeAndNil(FNotifiers); + FreeAndNil(FContextMenu); inherited; end; @@ -1127,6 +1194,16 @@ function TMockEditView.AddNotifier(Notifier: IIDEViewHandler): Integer; FNextId := FNextId + 1; end; +function TMockEditView.GetColumn: Integer; +begin + Result := FColumn; +end; + +function TMockEditView.GetContextMenu: TPopupMenu; +begin + Result := FContextMenu; +end; + function TMockEditView.GetFileName: string; begin Result := FFileName; @@ -1142,6 +1219,11 @@ function TMockEditView.GetLineTracker: IIDEEditLineTracker; Result := FLineTracker; end; +function TMockEditView.GetRow: Integer; +begin + Result := FRow; +end; + procedure TMockEditView.GoToPosition(const Line: Integer; const Column: Integer); begin NotifyEvent(evcGoToPosition, [Line, Column]); @@ -1157,6 +1239,11 @@ procedure TMockEditView.RemoveNotifier(Index: Integer); FNotifiers.Remove(Index); end; +procedure TMockEditView.ReplaceText(Replacement: string; StartLine, StartColumn, EndLine, EndColumn: Integer); +begin + NotifyEvent(evcReplaceText, [Replacement, StartLine, StartColumn, EndLine, EndColumn]); +end; + //______________________________________________________________________________________________________________________ constructor TMockSourceEditor.Create; diff --git a/companion/delphilint-vscode/src/settings.ts b/companion/delphilint-vscode/src/settings.ts index da53d5a..91d4e3a 100644 --- a/companion/delphilint-vscode/src/settings.ts +++ b/companion/delphilint-vscode/src/settings.ts @@ -130,7 +130,7 @@ export function getSonarDelphiVersion(): string { if (override) { return override; } else { - return "1.4.0"; + return "1.5.0"; } } diff --git a/server/delphilint-server/src/main/java/au/com/integradev/delphilint/analysis/DelphiIssue.java b/server/delphilint-server/src/main/java/au/com/integradev/delphilint/analysis/DelphiIssue.java index 15b337a..0686803 100644 --- a/server/delphilint-server/src/main/java/au/com/integradev/delphilint/analysis/DelphiIssue.java +++ b/server/delphilint-server/src/main/java/au/com/integradev/delphilint/analysis/DelphiIssue.java @@ -18,6 +18,8 @@ package au.com.integradev.delphilint.analysis; import au.com.integradev.delphilint.remote.IssueStatus; +import java.util.List; +import java.util.stream.Collectors; import org.sonarsource.sonarlint.core.analysis.api.Issue; public class DelphiIssue { @@ -26,6 +28,7 @@ public class DelphiIssue { private String file; private TextRange range; private RemoteMetadata metadata; + private List quickFixes; public DelphiIssue(Issue issue, RemoteMetadata metadata) { var textRange = issue.getTextRange(); @@ -41,6 +44,8 @@ public DelphiIssue(Issue issue, RemoteMetadata metadata) { textRange.getEndLine(), textRange.getEndLineOffset()); this.metadata = metadata; + this.quickFixes = + issue.quickFixes().stream().map(DelphiQuickFix::new).collect(Collectors.toList()); } public DelphiIssue( @@ -76,6 +81,10 @@ public RemoteMetadata getMetadata() { return metadata; } + public List getQuickFixes() { + return quickFixes; + } + public static class RemoteMetadata { private final String assignee; private final String creationDate; diff --git a/server/delphilint-server/src/main/java/au/com/integradev/delphilint/analysis/DelphiQuickFix.java b/server/delphilint-server/src/main/java/au/com/integradev/delphilint/analysis/DelphiQuickFix.java new file mode 100644 index 0000000..7b8b665 --- /dev/null +++ b/server/delphilint-server/src/main/java/au/com/integradev/delphilint/analysis/DelphiQuickFix.java @@ -0,0 +1,61 @@ +/* + * DelphiLint Server + * Copyright (C) 2024 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package au.com.integradev.delphilint.analysis; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.sonarsource.sonarlint.core.analysis.api.ClientInputFileEdit; +import org.sonarsource.sonarlint.core.analysis.api.QuickFix; + +public class DelphiQuickFix { + private static final Logger LOG = LogManager.getLogger(DelphiQuickFix.class); + + private final String message; + private final List textEdits; + + public DelphiQuickFix(QuickFix quickFix) { + message = quickFix.message(); + + if (quickFix.inputFileEdits().size() > 1) { + LOG.warn("A quick fix attempted to edit more than one file, which is unsupported"); + } + + textEdits = + quickFix.inputFileEdits().stream() + .findFirst() + .map(DelphiQuickFix::convertInputFileEdit) + .orElse(Collections.emptyList()); + } + + private static List convertInputFileEdit(ClientInputFileEdit fileEdit) { + return fileEdit.textEdits().stream() + .map(edit -> new DelphiTextEdit(edit.newText(), new TextRange(edit.range()))) + .collect(Collectors.toList()); + } + + public String message() { + return message; + } + + public List textEdits() { + return textEdits; + } +} diff --git a/server/delphilint-server/src/main/java/au/com/integradev/delphilint/analysis/DelphiTextEdit.java b/server/delphilint-server/src/main/java/au/com/integradev/delphilint/analysis/DelphiTextEdit.java new file mode 100644 index 0000000..17a7445 --- /dev/null +++ b/server/delphilint-server/src/main/java/au/com/integradev/delphilint/analysis/DelphiTextEdit.java @@ -0,0 +1,36 @@ +/* + * DelphiLint Server + * Copyright (C) 2024 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package au.com.integradev.delphilint.analysis; + +public class DelphiTextEdit { + private String replacement; + private TextRange range; + + public DelphiTextEdit(String replacement, TextRange range) { + this.replacement = replacement; + this.range = range; + } + + public String replacement() { + return replacement; + } + + public TextRange range() { + return range; + } +} diff --git a/server/delphilint-server/src/main/java/au/com/integradev/delphilint/server/message/ResponseAnalyzeResult.java b/server/delphilint-server/src/main/java/au/com/integradev/delphilint/server/message/ResponseAnalyzeResult.java index abc684b..e4f6a2e 100644 --- a/server/delphilint-server/src/main/java/au/com/integradev/delphilint/server/message/ResponseAnalyzeResult.java +++ b/server/delphilint-server/src/main/java/au/com/integradev/delphilint/server/message/ResponseAnalyzeResult.java @@ -18,12 +18,16 @@ package au.com.integradev.delphilint.server.message; import au.com.integradev.delphilint.analysis.DelphiIssue; +import au.com.integradev.delphilint.analysis.DelphiQuickFix; import au.com.integradev.delphilint.analysis.TextRange; import au.com.integradev.delphilint.server.message.data.IssueData; import au.com.integradev.delphilint.server.message.data.IssueMetadataData; +import au.com.integradev.delphilint.server.message.data.QuickFixData; +import au.com.integradev.delphilint.server.message.data.TextEditData; import com.fasterxml.jackson.annotation.JsonProperty; import java.nio.file.Path; import java.util.Collection; +import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -43,34 +47,46 @@ public static ResponseAnalyzeResult fromIssueSet(Collection delphiI delphiIssues.stream() .map( delphiIssue -> { - TextRange range = null; - if (delphiIssue.getTextRange() != null) { - range = - new TextRange( - delphiIssue.getTextRange().getStartLine(), - delphiIssue.getTextRange().getStartOffset(), - delphiIssue.getTextRange().getEndLine(), - delphiIssue.getTextRange().getEndOffset()); - } - - IssueMetadataData metadata = null; - if (delphiIssue.getMetadata() != null) { - metadata = - new IssueMetadataData( - delphiIssue.getMetadata().getAssignee(), - delphiIssue.getMetadata().getCreationDate(), - delphiIssue.getMetadata().getStatus()); - } - return new IssueData( delphiIssue.getRuleKey(), delphiIssue.getMessage(), delphiIssue.getFile(), - range, - metadata); + transformRange(delphiIssue.getTextRange()), + transformMetadata(delphiIssue.getMetadata()), + transformQuickFixes(delphiIssue.getQuickFixes())); }) .collect(Collectors.toSet()); return new ResponseAnalyzeResult(issues); } + + private static TextRange transformRange(TextRange range) { + if (range == null) { + return null; + } + + return new TextRange( + range.getStartLine(), range.getStartOffset(), range.getEndLine(), range.getEndOffset()); + } + + private static IssueMetadataData transformMetadata(DelphiIssue.RemoteMetadata metadata) { + if (metadata == null) { + return null; + } + + return new IssueMetadataData( + metadata.getAssignee(), metadata.getCreationDate(), metadata.getStatus()); + } + + private static List transformQuickFixes(List quickFixes) { + return quickFixes.stream() + .map( + quickFix -> + new QuickFixData( + quickFix.message(), + quickFix.textEdits().stream() + .map(edit -> new TextEditData(edit.replacement(), edit.range())) + .collect(Collectors.toList()))) + .collect(Collectors.toList()); + } } diff --git a/server/delphilint-server/src/main/java/au/com/integradev/delphilint/server/message/data/IssueData.java b/server/delphilint-server/src/main/java/au/com/integradev/delphilint/server/message/data/IssueData.java index f8e1c1d..5d7bf95 100644 --- a/server/delphilint-server/src/main/java/au/com/integradev/delphilint/server/message/data/IssueData.java +++ b/server/delphilint-server/src/main/java/au/com/integradev/delphilint/server/message/data/IssueData.java @@ -19,6 +19,7 @@ import au.com.integradev.delphilint.analysis.TextRange; import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; public class IssueData { @JsonProperty private String ruleKey; @@ -26,14 +27,21 @@ public class IssueData { @JsonProperty private String file; @JsonProperty private TextRange range; @JsonProperty private IssueMetadataData metadata; + @JsonProperty private List quickFixes; public IssueData( - String ruleKey, String message, String file, TextRange range, IssueMetadataData metadata) { + String ruleKey, + String message, + String file, + TextRange range, + IssueMetadataData metadata, + List quickFixes) { this.ruleKey = ruleKey; this.message = message; this.file = file; this.range = range; this.metadata = metadata; + this.quickFixes = quickFixes; } public String getRuleKey() { @@ -55,4 +63,12 @@ public void setFile(String file) { public TextRange getRange() { return range; } + + public IssueMetadataData getMetadata() { + return metadata; + } + + public List getQuickFixes() { + return quickFixes; + } } diff --git a/server/delphilint-server/src/main/java/au/com/integradev/delphilint/server/message/data/QuickFixData.java b/server/delphilint-server/src/main/java/au/com/integradev/delphilint/server/message/data/QuickFixData.java new file mode 100644 index 0000000..8c230e3 --- /dev/null +++ b/server/delphilint-server/src/main/java/au/com/integradev/delphilint/server/message/data/QuickFixData.java @@ -0,0 +1,39 @@ +/* + * DelphiLint Server + * Copyright (C) 2024 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package au.com.integradev.delphilint.server.message.data; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; + +public class QuickFixData { + @JsonProperty private String message; + @JsonProperty private List textEdits; + + public QuickFixData(String message, List textEdits) { + this.message = message; + this.textEdits = textEdits; + } + + public String getMessage() { + return message; + } + + public List getTextEdits() { + return textEdits; + } +} diff --git a/server/delphilint-server/src/main/java/au/com/integradev/delphilint/server/message/data/TextEditData.java b/server/delphilint-server/src/main/java/au/com/integradev/delphilint/server/message/data/TextEditData.java new file mode 100644 index 0000000..3143209 --- /dev/null +++ b/server/delphilint-server/src/main/java/au/com/integradev/delphilint/server/message/data/TextEditData.java @@ -0,0 +1,39 @@ +/* + * DelphiLint Server + * Copyright (C) 2024 Integrated Application Development + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package au.com.integradev.delphilint.server.message.data; + +import au.com.integradev.delphilint.analysis.TextRange; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class TextEditData { + @JsonProperty private String replacement; + @JsonProperty private TextRange range; + + public TextEditData(String replacement, TextRange range) { + this.replacement = replacement; + this.range = range; + } + + public String replacement() { + return replacement; + } + + public TextRange range() { + return range; + } +}