Skip to content

Commit 2fb854b

Browse files
zaneduffieldfourls
authored andcommitted
Relocate bookmarks and breakpoints after format
Before this change, all bookmarks and breakpoints were lost in a formatted file. Now they are relocated, much like editor cursors already are. Fixes #10.
1 parent 9c5e19e commit 2fb854b

File tree

5 files changed

+408
-65
lines changed

5 files changed

+408
-65
lines changed

Diff for: CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
* Support for updating breakpoints and bookmarks after the buffer rewrite.
13+
1014
## [0.1.0] - 2025-02-20
1115

1216
### Added

Diff for: Pasfmt.Cursors.pas

+372
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,372 @@
1+
unit Pasfmt.Cursors;
2+
3+
interface
4+
5+
uses
6+
ToolsAPI,
7+
System.Generics.Collections;
8+
9+
type
10+
TBreakpointData = record
11+
private
12+
// IOTABreakpoint40
13+
FEnabled: Boolean;
14+
FExpression: string;
15+
FFileName: string;
16+
FLineNumber: Integer;
17+
FPassCount: Integer;
18+
19+
// IOTABreakpoint50
20+
FGroupName: string;
21+
FDoBreak: Boolean;
22+
FLogMessage: string;
23+
FEvalExpression: string;
24+
FLogResult: Boolean;
25+
FEnableGroup: string;
26+
FDisableGroup: string;
27+
28+
// IOTABreakpoint80
29+
FDoHandleExceptions: Boolean;
30+
FDoIgnoreExceptions: Boolean;
31+
32+
// IOTABreakpoint120
33+
FStackFramesToLog: Integer;
34+
35+
// IOTABreakpoint
36+
FThreadCondition: string;
37+
38+
class function From(SourceBreakpoint: IOTASourceBreakpoint): TBreakpointData; static;
39+
procedure CopyTo(SourceBreakpoint: IOTABreakpoint);
40+
end;
41+
42+
TCursors = class
43+
private
44+
FOffsets: TArray<Integer>;
45+
FRowsFromTop: TArray<Integer>;
46+
47+
FBookmarkOffsets: TList<Integer>;
48+
FBookmarkIds: TList<Integer>;
49+
50+
FBreakpointOffsets: TList<Integer>;
51+
FBreakpointData: TList<TBreakpointData>;
52+
53+
procedure UpdateBookmarks(Buffer: IOTAEditBuffer);
54+
procedure UpdateBreakpoints(Buffer: IOTAEditBuffer);
55+
procedure UpdateCursors(Buffer: IOTAEditBuffer);
56+
57+
public
58+
constructor Create(Buffer: IOTAEditBuffer);
59+
destructor Destroy; override;
60+
61+
function Serialize: TArray<Integer>;
62+
procedure Deserialize(Offsets: TArray<Integer>);
63+
64+
procedure UpdateBuffer(Buffer: IOTAEditBuffer);
65+
end;
66+
67+
implementation
68+
69+
uses
70+
Pasfmt.Log,
71+
System.SysUtils;
72+
73+
const
74+
// While the user interface only lets you create bookmarks 0 to 9, the ToolsAPI also lets plugins create and use
75+
// bookmarks 10 to 19. I don't know if this is documented anywhere.
76+
CMaxBookmark = 19;
77+
78+
//______________________________________________________________________________________________________________________
79+
80+
class function TBreakpointData.From(SourceBreakpoint: IOTASourceBreakpoint): TBreakpointData;
81+
begin
82+
Result := Default(TBreakpointData);
83+
84+
// IOTABreakpoint40
85+
Result.FEnabled := SourceBreakpoint.Enabled;
86+
Result.FExpression := SourceBreakpoint.Expression;
87+
Result.FFileName := SourceBreakpoint.FileName;
88+
Result.FLineNumber := SourceBreakpoint.LineNumber;
89+
Result.FPassCount := SourceBreakpoint.PassCount;
90+
91+
// IOTABreakpoint50
92+
Result.FGroupName := SourceBreakpoint.GroupName;
93+
Result.FDoBreak := SourceBreakpoint.DoBreak;
94+
Result.FLogMessage := SourceBreakpoint.LogMessage;
95+
Result.FEvalExpression := SourceBreakpoint.EvalExpression;
96+
Result.FLogResult := SourceBreakpoint.LogResult;
97+
Result.FEnableGroup := SourceBreakpoint.EnableGroup;
98+
Result.FDisableGroup := SourceBreakpoint.DisableGroup;
99+
100+
// IOTABreakpoint80
101+
Result.FDoHandleExceptions := SourceBreakpoint.DoHandleExceptions;
102+
Result.FDoIgnoreExceptions := SourceBreakpoint.DoIgnoreExceptions;
103+
104+
// IOTABreakpoint120
105+
Result.FStackFramesToLog := SourceBreakpoint.StackFramesToLog;
106+
107+
// IOTABreakpoint
108+
Result.FThreadCondition := SourceBreakpoint.ThreadCondition;
109+
end;
110+
111+
//______________________________________________________________________________________________________________________
112+
113+
procedure TBreakpointData.CopyTo(SourceBreakpoint: IOTABreakpoint);
114+
begin
115+
// IOTABreakpoint40
116+
SourceBreakpoint.Enabled := Self.FEnabled;
117+
SourceBreakpoint.Expression := Self.FExpression;
118+
SourceBreakpoint.FileName := Self.FFileName;
119+
SourceBreakpoint.LineNumber := Self.FLineNumber;
120+
SourceBreakpoint.PassCount := Self.FPassCount;
121+
122+
// IOTABreakpoint50
123+
SourceBreakpoint.GroupName := Self.FGroupName;
124+
SourceBreakpoint.DoBreak := Self.FDoBreak;
125+
SourceBreakpoint.LogMessage := Self.FLogMessage;
126+
SourceBreakpoint.EvalExpression := Self.FEvalExpression;
127+
SourceBreakpoint.LogResult := Self.FLogResult;
128+
SourceBreakpoint.EnableGroup := Self.FEnableGroup;
129+
SourceBreakpoint.DisableGroup := Self.FDisableGroup;
130+
131+
// IOTABreakpoint80
132+
SourceBreakpoint.DoHandleExceptions := Self.FDoHandleExceptions;
133+
SourceBreakpoint.DoIgnoreExceptions := Self.FDoIgnoreExceptions;
134+
135+
// IOTABreakpoint120
136+
SourceBreakpoint.StackFramesToLog := Self.FStackFramesToLog;
137+
138+
// IOTABreakpoint
139+
SourceBreakpoint.ThreadCondition := Self.FThreadCondition;
140+
end;
141+
142+
//______________________________________________________________________________________________________________________
143+
144+
destructor TCursors.Destroy;
145+
begin
146+
FreeAndNil(FBookmarkOffsets);
147+
FreeAndNil(FBookmarkIds);
148+
FreeAndNil(FBreakpointOffsets);
149+
FreeAndNil(FBreakpointData);
150+
inherited;
151+
end;
152+
153+
//______________________________________________________________________________________________________________________
154+
155+
constructor TCursors.Create(Buffer: IOTAEditBuffer);
156+
var
157+
I: Integer;
158+
EditView: IOTAEditView;
159+
EditPos: TOTAEditPos;
160+
CharPos: TOTACharPos;
161+
Bookmark: Integer;
162+
DebuggerServices: IOTADebuggerServices;
163+
Breakpoint: IOTASourceBreakpoint;
164+
begin
165+
SetLength(FOffsets, Buffer.EditViewCount);
166+
SetLength(FRowsFromTop, Buffer.EditViewCount);
167+
168+
FBookmarkOffsets := TList<Integer>.Create;
169+
FBookmarkIds := TList<Integer>.Create;
170+
FBreakpointOffsets := TList<Integer>.Create;
171+
FBreakpointData := TList<TBreakpointData>.Create;
172+
173+
for I := 0 to Buffer.EditViewCount - 1 do begin
174+
EditView := Buffer.EditViews[I];
175+
176+
EditPos := EditView.CursorPos;
177+
EditView.ConvertPos(True, EditPos, CharPos);
178+
FOffsets[I] := EditView.CharPosToPos(CharPos);
179+
FRowsFromTop[I] := EditView.Position.Row - EditView.TopRow;
180+
end;
181+
182+
if Buffer.EditViewCount <= 0 then begin
183+
Log.Debug('Buffer has no edit views');
184+
Exit;
185+
end;
186+
187+
// bookmarks are shared between all edit views of a buffer
188+
EditView := Buffer.EditViews[0];
189+
190+
for Bookmark := 0 to CMaxBookmark do begin
191+
CharPos := EditView.BookmarkPos[Bookmark];
192+
if CharPos.Line <> 0 then begin
193+
FBookmarkOffsets.Add(EditView.CharPosToPos(CharPos));
194+
FBookmarkIds.Add(Bookmark);
195+
end;
196+
end;
197+
198+
if Supports(BorlandIDEServices, IOTADebuggerServices, DebuggerServices) then begin
199+
for I := 0 to DebuggerServices.SourceBkptCount - 1 do begin
200+
Breakpoint := DebuggerServices.SourceBkpts[I];
201+
if not SameText(Breakpoint.FileName, Buffer.FileName) then
202+
Continue;
203+
204+
CharPos.Line := Breakpoint.LineNumber;
205+
CharPos.CharIndex := 0;
206+
FBreakpointOffsets.Add(EditView.CharPosToPos(CharPos));
207+
FBreakpointData.Add(TBreakpointData.From(Breakpoint));
208+
end;
209+
end;
210+
end;
211+
212+
//______________________________________________________________________________________________________________________
213+
214+
function TCursors.Serialize: TArray<Integer>;
215+
var
216+
Copied: Integer;
217+
List: TList<Integer>;
218+
begin
219+
SetLength(Result, Length(FOffsets) + FBookmarkOffsets.Count + FBreakpointOffsets.Count);
220+
221+
Copied := 0;
222+
223+
if Length(FOffsets) > 0 then
224+
Move(FOffsets[0], Result[Copied], Length(FOffsets) * SizeOf(Integer));
225+
Inc(Copied, Length(FOffsets));
226+
227+
for List in [FBookmarkOffsets, FBreakpointOffsets] do begin
228+
if List.Count > 0 then
229+
Move(List.List[0], Result[Copied], List.Count * SizeOf(Integer));
230+
Inc(Copied, List.Count);
231+
end;
232+
end;
233+
234+
//______________________________________________________________________________________________________________________
235+
236+
procedure TCursors.Deserialize(Offsets: TArray<Integer>);
237+
var
238+
ExpectedCount: Integer;
239+
Copied: Integer;
240+
List: TList<Integer>;
241+
begin
242+
ExpectedCount := Length(FOffsets) + FBookmarkOffsets.Count + FBreakpointOffsets.Count;
243+
if Length(Offsets) <> ExpectedCount then begin
244+
Log.Error(
245+
'Expected %d cursors, found %d. Editor cursors, breakpoints, and bookmarks could not be updated',
246+
[ExpectedCount, Length(Offsets)]
247+
);
248+
Exit;
249+
end;
250+
251+
Copied := 0;
252+
253+
if Length(FOffsets) > 0 then
254+
Move(Offsets[Copied], FOffsets[0], Length(FOffsets) * SizeOf(Integer));
255+
Inc(Copied, Length(FOffsets));
256+
257+
for List in [FBookmarkOffsets, FBreakpointOffsets] do begin
258+
if List.Count > 0 then
259+
Move(Offsets[Copied], List.List[0], List.Count * SizeOf(Integer));
260+
Inc(Copied, List.Count);
261+
end;
262+
end;
263+
264+
//______________________________________________________________________________________________________________________
265+
266+
procedure TCursors.UpdateBookmarks(Buffer: IOTAEditBuffer);
267+
var
268+
EditView: IOTAEditView;
269+
EditPos: TOTAEditPos;
270+
CharPos: TOTACharPos;
271+
272+
BookmarkIdx: Integer;
273+
begin
274+
if Buffer.EditViewCount <= 0 then begin
275+
Log.Debug('Skipping updating bookmarks because the buffer has no edit views');
276+
Exit;
277+
end;
278+
279+
// bookmarks are shared between all edit views of a buffer
280+
EditView := Buffer.EditViews[0];
281+
282+
for BookmarkIdx := 0 to FBookmarkIds.Count - 1 do begin
283+
CharPos := EditView.PosToCharPos(FBookmarkOffsets[BookmarkIdx]);
284+
EditView.ConvertPos(False, EditPos, CharPos);
285+
EditView.CursorPos := EditPos;
286+
287+
EditView.BookmarkToggle(FBookmarkIds[BookmarkIdx]);
288+
end;
289+
end;
290+
291+
//______________________________________________________________________________________________________________________
292+
293+
procedure TCursors.UpdateBreakpoints(Buffer: IOTAEditBuffer);
294+
var
295+
I: Integer;
296+
CharPos: TOTACharPos;
297+
BreakpointData: ^TBreakpointData;
298+
Breakpoint: IOTABreakpoint;
299+
DebuggerServices: IOTADebuggerServices;
300+
begin
301+
if Buffer.EditViewCount <= 0 then begin
302+
Log.Debug('Skipping updating breakpoints because the buffer has no edit views');
303+
Exit;
304+
end;
305+
306+
if not Supports(BorlandIDEServices, IOTADebuggerServices, DebuggerServices) then begin
307+
Log.Debug('Skipping updating breakpoints because BorlandIDEServices does not implement IOTADebuggerServices');
308+
Exit;
309+
end;
310+
311+
// The old breakpoints (sometimes) stick around after a rewrite of the editor, but their positions are wrong and
312+
// they become invisible until manually disabled and re-enabled, so it's better to just recreate them.
313+
for I := DebuggerServices.SourceBkptCount - 1 downto 0 do begin
314+
Breakpoint := DebuggerServices.SourceBkpts[I];
315+
if not SameText(Breakpoint.FileName, Buffer.FileName) then
316+
Continue;
317+
318+
DebuggerServices.RemoveBreakpoint(Breakpoint);
319+
end;
320+
321+
for I := 0 to FBreakpointData.Count - 1 do begin
322+
CharPos := Buffer.EditViews[0].PosToCharPos(FBreakpointOffsets[I]);
323+
BreakpointData := @FBreakpointData.List[I];
324+
BreakpointData^.FLineNumber := CharPos.Line;
325+
326+
Breakpoint :=
327+
DebuggerServices.NewSourceBreakpoint(
328+
BreakpointData^.FFileName,
329+
BreakpointData^.FLineNumber,
330+
DebuggerServices.CurrentProcess
331+
);
332+
BreakpointData^.CopyTo(Breakpoint);
333+
end;
334+
end;
335+
336+
//______________________________________________________________________________________________________________________
337+
338+
procedure TCursors.UpdateCursors(Buffer: IOTAEditBuffer);
339+
var
340+
I: Integer;
341+
EditView: IOTAEditView;
342+
EditPos: TOTAEditPos;
343+
CharPos: TOTACharPos;
344+
begin
345+
for I := 0 to Buffer.EditViewCount - 1 do begin
346+
if (I >= Length(FOffsets)) or (FOffsets[I] < 0) then begin
347+
Continue;
348+
end;
349+
350+
EditView := Buffer.EditViews[I];
351+
352+
CharPos := EditView.PosToCharPos(FOffsets[I]);
353+
EditView.ConvertPos(False, EditPos, CharPos);
354+
EditView.CursorPos := EditPos;
355+
EditView.Scroll((EditView.Position.Row - EditView.TopRow) - FRowsFromTop[I], 0);
356+
EditView.Paint;
357+
end;
358+
end;
359+
360+
//______________________________________________________________________________________________________________________
361+
362+
procedure TCursors.UpdateBuffer(Buffer: IOTAEditBuffer);
363+
begin
364+
UpdateBookmarks(Buffer);
365+
UpdateBreakpoints(Buffer);
366+
367+
// Only update editor cursors after setting bookmarks and breakpoints, because setting the bookmarks involves moving
368+
// the cursor around, and we should only repaint the EditView after all changes are complete.
369+
UpdateCursors(Buffer);
370+
end;
371+
372+
end.

0 commit comments

Comments
 (0)