spiegel-keyman/developer/src/common/delphi/components/KeymanDeveloperDebuggerMemo.pas
Marc Durdin 59f02a6a55 fix(developer): retrieve debug memo anchor without side effects
Use RichEdit's `ITextDocument` interface to retrieve the selection
anchor/caret information, and avoid the side-effects that can arise with
the hacky `EM_GETSEL` / `EM_SETSEL` pattern we used previously.

Changing the selection with `EM_SETSEL` in order to find the anchor
point causes notification messages to be generated that can arrive at an
unexpected time in some text selection scenarios, which ended up with us
having a saved selection in the debugger pointing to the wrong text
range.

The name `Anchor` was a misnomer, because it was actually returning the
caret position, not the anchor for the selection, which is the far side
of the selection from the anchor!

Fixes: #11706
Fixes: KEYMAN-DEVELOPER-18A
2026-03-04 14:01:42 +01:00

186 lines
5.2 KiB
ObjectPascal

(*
Name: KeymanDeveloperDebuggerMemo
Copyright: Copyright (C) 2003-2017 SIL International.
Documentation:
Description:
Create Date: 8 Jun 2012
Modified Date: 8 Jun 2012
Authors: mcdurdin
Related Files:
Dependencies:
Bugs:
Todo:
Notes:
History: 08 Jun 2012 - mcdurdin - I3323 - V9.0 - Extract debug-related code TPlus-Memo into subclass
*)
unit KeymanDeveloperDebuggerMemo; // I3323
interface
uses
System.Classes,
Winapi.ActiveX,
Winapi.Messages,
Winapi.RichEdit,
Winapi.Windows,
Vcl.Controls,
Vcl.ComCtrls,
Vcl.StdCtrls,
Keyman.Developer.UI.RichEdit41;
type
TKeymanDeveloperDebuggerMessageEvent = procedure(Sender: TObject; var Message: TMessage; var Handled: Boolean) of object;
TMemoSelection = record
Start, Finish: Integer;
Caret: Integer; // In a non-empty selection, Anchor is at the other end of the selection
function ToString: string;
end;
TKeymanDeveloperDebuggerMemo = class(TRichEdit41)
private
FOnMessage: TKeymanDeveloperDebuggerMessageEvent;
FAllowUnicodeInput: Boolean;
FIsDebugging: Boolean;
procedure SetAllowUnicode(const Value: Boolean);
function GetSelection: TMemoSelection;
procedure SetSelection(const Value: TMemoSelection);
procedure CNKeyDown(var Message: TWMKeyDown); message CN_KEYDOWN;
protected
procedure CreateHandle; override;
procedure WndProc(var Message: TMessage); override;
public
constructor Create(AOwner: TComponent); override;
function GetTextCR: string;
function GetSelTextCR: string;
published
property AllowUnicode: Boolean read FAllowUnicodeInput write SetAllowUnicode default True;
property OnMessage: TKeymanDeveloperDebuggerMessageEvent read FOnMessage write FOnMessage;
property Selection: TMemoSelection read GetSelection write SetSelection;
property IsDebugging: Boolean read FIsDebugging write FIsDebugging;
end;
procedure Register;
implementation
uses
tom_TLB,
System.SysUtils;
{ TKeymanDeveloperDebuggerMemo }
procedure TKeymanDeveloperDebuggerMemo.CNKeyDown(var Message: TWMKeyDown);
begin
// Shortcuts for actions are handled with CN_KEYDOWN before we get WM_KEYDOWN,
// so if we are debugging we want to filter those out so that the debugger
// gets access to them instead of us. Note that this means that editor
// shortcuts such as Ctrl+A (select all) will not be processed in the normal
// way, so the host of this control needs to look after that.
if IsDebugging
then Exit
else inherited;
end;
constructor TKeymanDeveloperDebuggerMemo.Create(AOwner: TComponent);
begin
FAllowUnicodeInput := True;
inherited Create(AOwner);
PlainText := True;
end;
procedure TKeymanDeveloperDebuggerMemo.CreateHandle;
const
TO_ADVANCEDTYPOGRAPHY = 1;
begin
inherited;
if FAllowUnicodeInput
then SetWindowLongW(Handle, GWL_WNDPROC, GetWindowLong(Handle, GWL_WNDPROC))
else SetWindowLongA(Handle, GWL_WNDPROC, GetWindowLong(Handle, GWL_WNDPROC));
SendMessage(Handle, WM_SETTEXT, 0, NativeUInt(PChar('')));
SendMessage(Handle, EM_SETTEXTMODE, TM_PLAINTEXT or TM_MULTICODEPAGE, 0);
SendMessage(Handle, EM_SETTYPOGRAPHYOPTIONS, TO_ADVANCEDTYPOGRAPHY, TO_ADVANCEDTYPOGRAPHY);
SendMessage(Handle, EM_SETLANGOPTIONS, 0, $0040 {IMF_NOIMPLICITLANG} or $0200 {IMF_NOKBDLIDFIXUP});
end;
function TKeymanDeveloperDebuggerMemo.GetSelection: TMemoSelection;
var
Unk: IUnknown;
TextDoc: ITextDocument;
TextSel: ITextSelection;
begin
SendMessage(Handle, EM_GETSEL, NativeUInt(@Result.Start), NativeUInt(@Result.Finish));
Result.Caret := Result.Finish;
if Result.Finish <> Result.Start then
begin
if SendMessage(Handle, EM_GETOLEINTERFACE, 0, LPARAM(@Unk)) <> 0 then
begin
if Unk.QueryInterface(IID_ITextDocument, TextDoc) = S_OK then
begin
TextSel := TextDoc.Selection;
if TextSel <> nil then
begin
if (TextSel.Flags and tomSelStartActive) <> 0 then
begin
Result.Caret := Result.Start;
end;
end;
end;
end;
end;
end;
function TKeymanDeveloperDebuggerMemo.GetSelTextCR: string;
begin
Result := SelText.Replace(#$0D#$0A, #$0D, [rfReplaceAll]);
end;
function TKeymanDeveloperDebuggerMemo.GetTextCR: string;
begin
Result := string(Text).Replace(#$0D#$0A, #$0D, [rfReplaceAll]);
end;
procedure TKeymanDeveloperDebuggerMemo.SetAllowUnicode(const Value: Boolean);
begin
if Value <> fAllowUnicodeInput then
begin
fAllowUnicodeInput := Value;
if HandleAllocated then RecreateWnd;
end;
end;
procedure TKeymanDeveloperDebuggerMemo.SetSelection(
const Value: TMemoSelection);
begin
if Value.Caret = Value.Start
then SendMessage(Handle, EM_SETSEL, Value.Finish, Value.Start)
else SendMessage(Handle, EM_SETSEL, Value.Start, Value.Finish);
end;
procedure TKeymanDeveloperDebuggerMemo.WndProc(var Message: TMessage);
var
b: Boolean;
begin
b := False;
if Assigned(FOnMessage) then
FOnMessage(Self, Message, b);
if not b then inherited WndProc(Message);
end;
procedure Register;
begin
RegisterComponents('Keyman', [TKeymanDeveloperDebuggerMemo]);
end;
{ TMemoSelection }
function TMemoSelection.ToString: string;
begin
Result := Format('TMemoSelection(start=%d finish=%d caret=%d)', [Start, Finish, Caret]);
end;
end.