Released: 2026-07-25
Changes since version 1.3.28 (released 2026-04-18)
TMainMenu can be moved to a TMainMenu plus a TActionList without doing it by hand. It lists the items of the form's main menu and, optionally, of its popup menus, pre-filtered to those that can actually be converted: they must have a name, must not be a separator, must have no submenu and no action assigned yet. You tick the ones you want and pick the action list to add them to (an existing one, or a new TActionList that is created for you). For each selected menu item a TAction is created that takes over the item's caption, shortcut, hint, image index, help context, enabled/visible/checked state and its OnClick handler, and the action is then assigned to the menu item -- which is what makes the IDE drop the now redundant caption, shortcut and OnClick from the DFM. The action names are derived from the menu item names by stripping a configurable prefix (mni, mnu, mi_, mi, ...) and prepending a configurable one (act), so mniFileOpen and mi_FileOpen both become actFileOpen; a prefix is only stripped where it really is one (miscItem is left alone), and names already used on the form are never reused. Available in the GExperts menu and in the form designer's context menu (feature #152)tblLog and dsLog.DataSet points to it, the code ends with dsLog.DataSet := tblLog;. Every published property of every other component on the form is checked, so this also covers e.g. DBGrid1.DataSource, Button1.Action, Edit1.PopupMenu or a label's FocusControl; references from the form itself are assigned via Self. The assignments are appended after the property assignments (and outside the with block, if that option is used). Controlled by the new "Assign references from other components" option in the expert's configuration, which is on by default; turn it off to get the previous behaviour. Nothing is generated when the form itself is selected, because its own code already contains these assignments (feature #160, requested by Irwanto Irwanto)MenuItemName: Shortcut. Menu items whose original shortcut you removed are listed with a shortcut of (none). A hint saying "Shortcut list copied to clipboard" appears above the button for one second to confirm. If you have not configured any custom shortcuts, a message says so and the clipboard is left alone (feature #232, requested by Peter Panettone)@param tag of the XML doc comment (<summary>) directly above the procedure, function or method is renamed too. Only the documented name in the @param tag is changed, never other words in the description. A parameter declared with a leading underscore (e.g. _Bla) is matched even when the doc comment omits the underscore (@param Bla) (feature #233)var" option on the Line Breaks tab. Inline variable declarations inside a begin or try block were never touched, no matter what the "After var, type etc." option was set to. With the new option turned on, var LTest := 1; is split into a var line and the declaration below it, indented by one level. A for loop header keeps its declaration where it is, for var i: Integer := 5 to 10 do is not broken up, and a classic var section is not affected either. The option is off by default and is independent of "After var, type etc.", which continues to have no effect on inline declarations (feature #248, requested by Luka Havrlišan)var, type etc." on the Line Breaks tab. With it turned on, a var, const or type section that was written on a single line stays on that line instead of being broken up, so var Rslt: Integer; is left as it is. Several variables sharing a type still count as one declaration, so var I, J: Integer; is left alone as well, and a trailing comment does not change that. A keyword line that carries more than one declaration is still broken up, because keeping only the first declaration on the var line and pushing the rest below it would look worse than breaking all of them. A section that was already written across several lines is not joined. The option is off by default, so nothing changes unless you turn it on (feature #247, requested by Ian Branch)Item, MenuItem, both configurable) is stripped and the action suffix (Action) is appended instead, so FileItem and FileMenuItem both become FileAction. The tab you leave the dialog on decides which scheme is used and is remembered. As with prefixes, a suffix is only stripped where it really is one, i.e. where it starts a new word in the name (Sanitem is left alone) (feature #244, requested by Keld Hansen)OnClick handler. Which classes it offers is configured as a list of class names, by default TButton, TBitBtn, TSpeedButton and TToolButton, and -- unless you switch that off -- anything derived from them, so third party buttons are covered too; a class name that GExperts knows nothing about (e.g. TRzButton) can simply be added to the list. Only controls that publish an OnClick event and an Action property are offered, because without the latter the IDE could not store the action in the DFM. For each selected button a TAction takes over its caption, hint, image index, help context, enabled and visible state, the group index and down state of a grouped button, and its OnClick handler; the glyph of a TSpeedButton stays where it is, a TAction has nowhere to put it. Buttons without an OnClick handler -- an OK button that only sets a ModalResult, say -- are listed but not preselected, and the same now applies to menu items without a handler. The prefixes and suffixes to strip cover both worlds by default (btn, sbtn, ... and Button, Btn), so btnSave becomes actSave and SaveButton becomes SaveAction. Whether buttons are listed at all can be toggled in the dialog and defaults to on (feature #245, requested by David Cornelius)Source: comment line of every snippet it added to your library. An archive can now name itself in a collection.ini holding a [Collection] section with a Name= entry, and that name is used instead. The two DelphiDabbler collections carry no such file and are unaffected.inc files now show their parent in the caption: &<N> child.inc (via parent.inc). Tabs for files included directly from the active editor still use the old format (&<N> child.inc). Makes it visible why a tab exists for a file the user didn't include themselves (feature #240)FileClose* action blacklist. The // todo: Does this still apply? on the filter (added at r4412 when the expert was created) was a copy-paste from GX_EditorEnhancements.pas, where the original "Close just causes AVs, so we don't allow it" comment dates back to r1 (the 2007 SourceForge initial import). Verified against Delphi 13: FileClose works fine when placed on the IDE Toolbar. The same blacklist in GX_EditorEnhancements.pas and GX_ToolbarConfig.pas stays put -- closing files via the Editor Toolbar still raises an EAbstractError on Delphi 7 (a different failure mode than the original AV, but still broken)TfmOpenFile.btnOKClick records the filename of the last selected entry it opens and calls Ota.OpenFileOrForm on it again after the loop -- a no-op for files that are already activated, but ensures the last file is the visible/active tab when the IDE comes forward via FocusCodeEditorGX_CodeFormatterTypes.pas audited against RAD Studio 13's "Fundamental Syntactic Elements" reference. Added at (operator, as used in raise X at Y), local (Linux/legacy procedure directive), package (package-file declaration, mirrors library's mapping), and winapi (calling-convention directive). All 1478 formatter tests pass identically -- no observable behaviour change; the additions just ensure these tokens are no longer misclassified as identifiers if they appear in newer codeTf_EditPath.TryGetProjectConfigurations no longer raises in the no-project branch; it just returns False, honoring its Try prefix. TEditPathExpert.Configure calls the dialog unconditionally -- with an empty config list, the dialog still shows its <Current Config> sentinel, and OK saves the "use the active config at run time" choice. The main *Edit Unit Search Path* dialog still requires a project (there is no path to edit otherwise); its error message was made accurate -- *"No project is loaded, or this version of Delphi does not support the IOTAProjectOptionsConfigurations interface"* (feature #239)IOTAFormEditor.GetRootComponent.Select(False) (the same fallback GX_TabOrder.pas uses). When the filter matches both the form and components, the form node is skipped and only the components are selected, because the IDE has no "form + components" combined-selection state. Two // todo: markers in GX_SelectComponents.pas are resolved (feature #238)FEditRead: No editor interface exception when invoking Goto with the form designer active, and made the dialog activate the source view on OK. TGotoExpert.Execute now uses View.Buffer.FileName (always the source file) instead of Ota.GetCurrentSourceFile (which returns the .dfm name when the form designer is current and fails to parse), passes the filename explicitly into Tf_Goto.Execute, and calls Ota.MakeSourceVisible before positioning the cursor so the source code view comes forward. Tf_Goto.Create no longer reaches into Ota.GetCurrentSourceFile; positions are loaded by a new LoadUnitPositions method that the class function calls between Create and SetData. Also fixed a latent leak: FUnitPositions was never freed -- added a destructor (bug #496)IGxAction behind it (it only existed to host overflow expert entries as children); the action was only there to "enable the menu item", and its side effect was to surface "More" as a draggable toolbar button with no useful function. The "Configuration..." and "About..." entries do keep their actions because they can reasonably be placed on a toolbar, and they now carry shared-image icons (ImageIndexGear, ImageIndexInfo) so they no longer appear as blank buttons there. No change to the GExperts menu itself (bug #495)UserDirs / RecursiveDirs settings (feature #237)GExperts.chm, Formatter topic, context ID 75) instead of trying to launch the bundled delfor.hlp via WinHelp. WinHelp has been removed from modern Windows for years, so the previous code did nothing visible. The Help button is also visible again (had been hidden in the DFM because the old code did nothing), and F1 is now wired to the same handler through a TAction (consuming the keypress so the default WM_HELP path -- which would call Application.HelpContext against the IDE's help file and produce a "Cannot find file" error -- never fires). The per-tab keyword lookup the old call attempted is not yet mirrored -- the top-level Formatter topic is opened regardless of which tab is activeWordWrap property, but only on the classes where it is a later addition: TCheckBox, TRadioButton, TButton, TBitBtn and TSpeedButton. Delphi 6 and 7 do not know it there, and a DFM containing it makes the form fail to load at run time. WordWrap on a TLabel, TMemo or any other class that has always had it is left alone, so the tool now tracks which object a property belongs to instead of looking at property names onlyStyleElements property now, alongside Margins.* and AlignWithMargins. StyleElements was introduced in Delphi XE3 (theming engine) and is not understood by earlier versions. The IDE adds it automatically to controls touched in newer Delphi versions, so it can easily creep into shared DFMs.gex cache before re-parsing the project. Without this, when a new field is added to the .gex format (e.g. ContainingClassName, IsInImplementation), the user has to manually delete the cache file to see the new data -- AddProject silently loads the stale cache the next time the IDE starts. After Refresh, a fresh cache with all current fields is written by SaveToFileInitDPIScaler call from the constructor (the second one is the right one -- after children are populated, matching the pattern in every other GExperts dialog), and fixed the bookmark-index dropdown to stop at MaxBookmarks - 1 (was adding one extra invalid index when the loop was updated to use the new shared MaxBookmarks constant)INTAIDEThemingServicesNotifier and re-applies the theme to the form on light/dark switches. The owner-drawn listbox now uses Ota.GetThemedColor for all hard-coded system colors (header text, frame, dividers, line text) instead of literal clBtnText/clBtnFace/clBtnHighlight/cl3DDkShadow/clWindowText/clHighLight*. The user-configured list colors (background, bookmark-line highlight, header background, function-name font) are overridden with theme-derived values when IDE theming is active, so the light-mode pinks/greens don't bleed through into dark mode -- the user's saved colors are still honoured when theming is off. The status bar font is set explicitly and StyleElements := [seBorder, seClient] prevents the engine from overriding it. The "Delete all bookmarks?" confirmation dialog is now themed via Ota.InitTheming. After closing the Options dialog, InitThemedColors is re-run so user-saved colors don't leak through in dark mode (bug #491)IOTAEditView.BookmarkPos (the actual upper limit is undocumented; we cap our probe loop at 100). The MaxBookmarks constant moved to GX_OtaUtils.pas and is version-conditional so older Delphi versions still cap at 20TUnitTagsScanner.IsValidTag is exposed for reusewith lvHistoryList.Items.Add do begin ... end block in AddItem with an explicit Item: TListItem local. No behavior change; readability cleanupINTAIDEThemingServicesNotifier and re-applies the theme to the dock form on light/dark switches; the modal Add Tags dialog re-applies the theme in its constructor. The status bar font is set explicitly (with StyleElements := [seBorder, seClient]) so it stays readable in dark mode, and the SynMemo-based tag editor's background and font are overridden with themed system colors so it does not stay bright white on a dark formInitDpiScaler is now called after the memo is added to the form, so the scaler picks it up.svn, .git, .hg) or the Delphi backup folders (__history, __recovery). These never contain source files worth parsing, so skipping them avoids the wasted traversal (bug #513)GpStructuredStorage: it counted the blocks of a partially filled storage by rounding down, one block short, and the on-close cleanup then discarded the still-used last block. This is a separate defect from the folder-delete corruption fixed earlier and, like that one, is version-independentGpStructuredStorage: its on-close cleanup could remove a block-management region from the middle of the file instead of only from the end, which threw off the position of every region after it; it now only trims unused regions from the end. Back-ported together with it is a related defensive hardening in the entry-deletion code that re-checks a file's position after its attribute record has been removed. Both come from the upstream GpStructuredStorage 2.0d and are version-independentvar, const or type section and the procedure that follows it is no longer indented as if it were still part of the section. The comment belongs to the procedure below it and is now left at the margin, in line with that procedure. This affects both // and { } comments and only the built-in "Borland" configuration and the built-in defaults, which are the ones that indent comments. A comment inside a procedure, or one that is not preceded by such a section, was already handled correctly and is unchanged. The same case in an interface section was fixed earlier (bug #511)var keyword is no longer moved to the left margin. var followed by j: Integer; on the next line came out with the declaration at column 0, outside the block it belongs to. An identifier followed by a colon at the start of a line is how a label looks, so the formatter turned the declared variable into one and unindented it accordingly. It now checks whether the line before ended with var or const and leaves such a declaration alone. Only the typed form with a single name was affected: var with s := 5; has no colon, and l, m: Integer; has a comma in front of it, so neither looked like a label (bug #512)Align, Anchors, AutoSize and WordWrap, but also Constraints, BorderStyle or a Caption or Font assigned while AutoSize is active -- were copied in the order the RTTI happened to list them, so whenever one of them came after Width and Height, it resized the new component and the copied dimensions were lost. Left, Top, Width, Height, ClientWidth and ClientHeight are now assigned in a second pass, after all other properties. This also covers the constant values you can configure for a mapping, which are assigned by the same two passes, so a mapping that sets e.g. Align to a constant no longer resizes the component after its dimensions have been copied (bug #150)TfmCodeLib.Create, so the expert could not be used there at all; the 32-bit IDE was fine. The storage library that keeps the snippets (CodeLibrarian.fs) predates 64-bit Delphi and squeezed pointers through a 32-bit integer in two places: TGpFixedMemoryStream computed its buffer address as integer(buffer) + position, and TGpObjectMap kept the objects it maps in a list of integers. Both lost the upper half of every address on Win64, so the first read or write to the storage hit a truncated address. They now use a pointer-sized type. The 32-bit builds are unaffected, and the format of the storage file does not change (bug #508, reported by Ian Branch)StrEndsWith / StrBeginsWith helpers: on Delphi 6 the underlying EndsText / StartsText return True instead of False when the searched-for text is longer than the string being tested, so the importer mistook a SWAG archive for a Code Snippets one. Both helpers now guard that case explicitly, which also fixes any other Delphi 6 use of them with an over-long substringUndeclared identifier: 'SendDebugFmt') when building GExperts with the DEBUG conditional defined but debug info turned off. A debug-output call was guarded by {$IFDEF DEBUG} while the unit providing it was only added to the uses clause under {$IFOPT D+}; the call now uses the same {$IFOPT D+} guard as everywhere elseTButton = class(TButton)) now appear in the class tree instead of being silently left out (and logged as a warning). The tree builder treated a class as a root only when its ancestor was not among the parsed classes, but for such an interposer the ancestor lookup found the interposer itself; it is now also treated as a root (bug #357)< and > of a generic instantiation when a reserved-word type name (e.g. string) appears nested in the generic arguments, as in TDictionary<string, TList<string>>.Create. Plain identifiers like Integer were already handled correctly (bug #501, reported with unit tests and fix supplied by Luka Havrlišan)Execute class function takes the owner control as a parameter and the dialog is created with that owner and explicitly centered before showTfmBaseForm are now centered correctly on their owner at non-100 % DPI scaling. They previously appeared off to the right (and slightly down) on hi-DPI monitors because the form was centered using its pre-scaled width, then DoShow resized it without re-centering. TfmBaseForm.AfterConstruction now rescales the form to the owner's monitor DPI right after construction, so the eventual centering math sees the correct size. Verified at 125 % and 175 % on Delphi 10.2 / 12 / 13 with the Reverse Statement Options and Edit Rule dialogsif whose body contains a nested if/else chain now matches its own else (or terminating ;) instead of the inner else. Pascal's dangling-else binds to the innermost open if; the forward scan didn't track that, so any nested-without-begin/end shape was misidentified+#13+#10+; the cleanup stripped the doubled ++ left by a blank middle line and the trailing + left by a blank last line, but not the leading + left by a blank first line, so lines (blank), Body came out as +#13+#10+Body -- invalid Pascal. A leading + is now stripped too, giving #13+#10+Body. This is the Pascal-side mirror of bug #493; the C++ builder already handled its leading-blank case (bug #494)\n, so two lines Greeting / Body came out as Greeting\nBody -- a literal backslash-n between two expressions, which is not valid C++ (the no-quotes branch of TCppMessageDialogBuilder.GetCode was an empty stub with a "this doesn't really work" todo). Lines are now joined with a quoted, spaced separator -- Greeting + "\n" + Body -- and empty lines are cleaned up: a blank middle line (A, , B) yields A + "\n" + "\n" + B, a trailing blank yields A + "\n", and a single line MyVar is emitted as-is. The empty-operand cleanup matches + + (with the two spaces) rather than bare ++, so a user expression containing the C++ ++ operator is left intact. The Pascal output and the quotes-on C++ output are unchanged (bug #493)IsSubstrHere had no word-boundary check, so the begin keys try/then/else/except/finally/repeat/of matched inside any identifier containing them as substrings -- try inside TryParse/TryAdd/TryGetValue (very common in generic-collection code), then inside Strengthen/Lengthen/Athens. The scanner then looked for the wrong end key, and for try (paired with the rare finally/except) typically ran off the end of the file. (2) When FindKeyPairEnd returned False, the entire Execute procedure aborted -- so a single if X then; with no else (or if Supports(...) then begin ... end chain like the one in the submitter's sample), or any false match that left no plausible end key in the rest of the file, silently skipped every empty block that came after it. The scanner now advances past the unmatched begin key and keeps scanning instead. New unit test suite UnitTests/TestAutoTodo/ with 16 tests covers both causes (bug #483);) on the inserted line: a := 'í'; duplicated as a := 'í' / a := 'í';;. The end-of-line column is now computed from the line's byte length in the IDE encoding, not from the in-memory string length (reported by Antonio Cristovao Gomes)Ota.GetEditorLine corrupted multi-byte UTF-8 bytes when read from the IDE editor. The local LineData: AnsiString declaration carried codepage CP_ACP (system codepage, typically W1252); the cast to UTF8String on the way out then performed an implicit codepage conversion, double-encoding every UTF-8 multi-byte (e.g. í (C3 AD) became à (C3 83 C2 AD)). Reading into IDEEditBufferString instead matches the destination codepage so no conversion occurs. Affects every caller of Ota.GetEditorLineAsString (reported by Antonio Cristovao Gomes against the new Duplicate-multiple-lines flow)TFoo = class(TBar); (no body, no end) caused subsequent class declarations to be misreported as nested inside them, producing tree entries like TControlM.TCustomGroupBoxM.TFrameBaseDS instead of three siblings. FindClassEndIndex now detects short declarations both upfront (the class being looked up is itself short → return -1) and inside the body-scan loop (a nested short declaration must not increment the depth counter) (bug #488, reported and fix supplied by markbd13)X := Lv.Items.Add on a VCL TListView now proposes TListItem; X := Obj.Inner.Value on local nested records/classes resolves through both segments. New helpers LookupMemberInClassDecl / LookupMemberInLocalHierarchy and a new elkMemberOfClass external-lookup kind. TExternalTypeResolver.IsUnitInUses now also matches the trailing segment of a namespace-qualified UCM file basename when the namespace prefix is one of the project's configured unit scope names (from Ota.GetProjectNamespaces, same source UCM's "Add Unit Prefixes" uses) — so a uses entry like ComCtrls matches an indexed Vcl.ComCtrls.pas for a VCL project but does NOT match an unrelated SomeOtherLib.ComCtrls.pas (bug #489)X := Obj.Items.Add where the property type is unknown), the inferer was falling through to "RHS is the variable Obj" and proposing the type of the chain root (TMyObj) instead of leaving the type empty. It now bails out so the user picks the correct typeIOTAActionServices.CloseFile, matching File > Close behaviorCM_DIALOGCHAR to all its child windows including the controls inside our docked forms, so accelerator keys fired on those controls even when the docked tab was not active. Pressing Alt+P on the editor would check the Unit Tags "Scan search path" checkbox instead of opening the IDE's Project menu. TfmIdeDockForm now suppresses the broadcast unless the focused control actually belongs to the form, matching the existing TMenuBar filter and fixing all docked GExperts windows at onceDCC_UnitSearchPath typically contains macros like $(BDS)\lib\$(Platform)\release and project-relative entries like ..\Common; the code passed those strings straight to DirectoryExists, which returned False for any unexpanded $(...) and resolved relatives against the process CWD rather than the project directory. The search-path scan now uses the existing Ota.GetProjectSourcePathStrings helper (the same one UCM has been using) which reads the search path, splits on ;, expands $(BDS)/$(Platform)/env-variable macros via TPathProcessor, and resolves each entry against the project's directory — across Delphi 6 through 13FBookmarks field is preserved. ScanBookmarks's BookmarksChanged check then saw no change vs OTA and skipped the refill. The form now overrides CreateWnd to flag the listbox as needing a refill, which the next 500 ms timer tick honours (bug #490)Constraints.MaxHeight = 1111 / MaxWidth = 1111; at 150% scaling the DPI scaler bumped MaxHeight to 1667 px, which is still smaller than the available area on a 4K monitor. These max constraints were arbitrary (they previously caused the form to be unresizable when set to the design height of 159) and have been removed entirelyChangeSingleLineCommentToBeginEndComment now raises EFormatException if TryGetExpression returns False for a token that has already been confirmed as rtComment. The previous code silently exited with the inline note "This should not be possible -> raise an exception?" -- raising matches the precedent four function calls above (line 436, same file) and surfaces the latent parser bug instead of masking it. No behavior change on valid input; all 1478 formatter tests still passSetSpace([], True) calls in the generic-open < and generic-close > branches of TCodeFormatterFormatter.AdjustSpacing were no-ops -- adding the empty set to FSpaceType never changes anything. The code carried an inline comment noting this and pointing at the intended fix. New tokens default to FSpaceType = [], so the buggy no-op happened to produce the correct output on every existing test case, but the defensive intent ("generic brackets must never have surrounding spaces, regardless of prior state") was not actually enforced. Replaced both with SetSpace([spBefore, spAfter], False) so the invariant survives future refactors of AdjustSpacing. No behavior change today; all 1478 formatter tests still passBookmarkToggle, which removes a bookmark with that ID if one is still present. The buffer replace no longer wipes the bookmark slots, so BookmarkToggle was deleting them instead of restoring them. The restore now uses BookmarkRecord, which sets the bookmark at the cursor unconditionally. The probe loop was also widened from 0..19 to 0..99 because the IDE 13.1 bookmarks tool window exposes more than the legacy 20 slots through BookmarkPos (at least 0..25 confirmed; actual upper limit is undocumented) (bug #487){ <Visibility> '#$5BA3#$8A00' } (the two characters are sengen, "declaration"). The literal uses #$xxxx Unicode escapes so the source file stays pure ASCII. Guarded by {$IFDEF GX_DELPHI2009_UP} because pre-Unicode Delphi (D6/D7/D2007) uses Ansi strings -- the Shift-JIS byte sequence those would require (90 E9 8C BE) is only meaningful on a Japanese-codepage Windows, so the matchers are skipped there. Both the regex and plain-text branches got the new entryINTAEditServicesNotifier ({$IFDEF GX_VER170_up} only) to the GExperts framework's new iinEditorViewActivated notification. The bookmarks dock now re-scans on every Delphi version when the user switches between editor views, not just Delphi 8+. The existing tiUpdateBookmarks timer stays in place -- it catches in-editor bookmark changes (Ctrl+K+N etc.) that no OTA notification surfacesINTAEditServicesNotifier directly with IOTAEditorServices, which is only available on Delphi 8 and up, so on older Delphi versions the dock window kept showing results for the previously-active file until the user re-typed the search expression. It now subscribes through the new TGxEditorViewActivatedNotifier helper, which is wired through the framework's per-editor IOTAEditorNotifier and works on every Delphi versionTGxEditorViewActivatedNotifier. The pre-Delphi 2005 fallback path that polled Ota.GetCurrentSourceFile on a 500ms timer (with a PausePolling mechanism to keep the timer from fighting the user's tab clicks) is gone -- D6/D7 now get the same event-driven "highlight the active file's tab" behavior the newer Delphi versions had, through the framework's per-editor IOTAEditorNotifier. Single code path on every Delphi versionIGxEditorNotification now carries an EditorViewActivated(SourceEditor: IOTASourceEditor) method, dispatched from the previously empty TGxEditorNotifier.ViewActivated via the existing GenericNotification fan-out (iinEditorViewActivated). The existing IGxEditorNotification implementors in CodeProofreader, MacroTemplates and RenameComponents got empty stubs; no behavior change for them. New subscribers (the AutoTODO list, others to follow) can use this in place of registering their own INTAEditServicesNotifier, which makes the same feature work on Delphi 6/7 as well -- the framework's per-editor IOTAEditorNotifier is wired through to subscribers on every Delphi versionTMemo and were previously affected by the VCL quirk where a focused memo swallows Esc: Remove Matching Lines config, Backup Not Found, Filter Exceptions Notification, Feedback Wizard, and Replace Components Map Details. Each now uses the existing TMemoEscFix interposer pattern. Partially addresses bug #492. The companion Ctrl+Enter-to-OK request is not addressed here -- that requires per-dialog TActions with ShortCut = scCtrl + VK_RETURN, since Windows' IsDialogMessage only treats plain Enter as a dialog-navigation key, so the CM_WANTSPECIALKEY hook TMemoEscFix uses for Esc cannot catch modifier+Enter. The TMemoEscFix source carries a comment explaining the failed approach so it isn't retriedactNext action got ShortCut = 16397 so Ctrl+Enter advances pages and finishes the wizard; Remove Matching Lines and Backup Not Found got a new TheActionList with an act_OK that sets ModalResult := mrOk and calls Close respectively; Filter Exceptions Notification got an act_OK in its existing action list mirroring its Default b_Break button (ModalResult := mrOk); Replace Components Map Details got an actApply in its existing action list reusing the existing btnApplyClick handlerTGxEditorNotifier are no longer touched at notifier teardown. Previously the destructor / Destroyed callback ran with the unhook call commented out (it had historically caused IDE crashes on tricky hook-chain orderings), leaving the trampoline pointed at a Self that would shortly be freed. The trampoline now caches the editor's HWND at hook time and EditControlWndProc becomes a transparent forwarder once a new FDisabled flag is set -- skipping the keypress notification (which would touch the now-nil FAssociatedSourceEditor) but still forwarding via CallWindowProc(FOldEditControlWndProc, FEditorHandle, ...) using the cached handle. Modified carries the same disabled guard for symmetry. Lifetime of TGxEditorNotifier is unchanged. This may fix the IDE-close exceptions reported in bug #286 ("Exception on Delphi 6 close") and bug #236 ("Invalid Pointer Operation in 1.3.16" on Delphi 10.4)TabOrder. Components without a TabOrder (labels, panels, non-visual components) previously all tied at 0 and fell through to alphabetical -- with the new layout-based sort the visual ordering is preserved. Position is computed via TControl.ClientToParent(Point(0, 0), GetParentForm(Ctrl)) so cross-parent selections (controls on different panels) are ordered against the form's coordinate system, not each parent's. Non-visual components (anything that isn't a TControl) get (0, 0) and fall through to the alphabetical tiebreaker, same as beforeReplace is a suffix of Typed -- the simplest example is :; -> ; -- now fire correctly. InternalNeedsReplacement checked whether the trailing Length(Replace) characters of the buffer already equalled Replace, intending to detect "we already replaced this." When Replace is a suffix of Typed, the freshly typed trigger looks identical to a completed replacement (the buffer ends with Replace in both cases), so the rule was incorrectly suppressed. The check now splits on the relative lengths of Typed and Replace: when Length(Typed) > Length(Replace) the trigger can't be at the end of the buffer post-replacement (so reaching this point means the user just typed it -- fire), otherwise the existing buffer-ends-with-Replace check is kept to suppress the re-fire that would otherwise create an infinite loop for rules like foo -> foofoo or ; -> ;;. The 2018-06-03 todo flagging this is goneHKCU\Software\<IDE-base>\History Lists\hlGotoLine -- the IDE's own Goto history, which the IDE only flushes to the registry at shutdown (so it was stale during a session) and which tracked the IDE's dialog, not this one. The dropdown now shows the GExperts-Goto's own MRU instead, persisted under the expert's settings (GotoExpert\LineHistoryUnit + GotoExpert\LineHistory via IExpertSettings.ReadString/ReadStrings), capped at 20 entries, MRU-ordered. The entries are the source lines (the cursor's line at the moment Goto was invoked), and each entry shows the content of that line alongside the line number (e.g. 42: begin) for recognition. The history is scoped to the current source unit (the filename is stored alongside the list), so opening Goto in a different unit shows an empty dropdown rather than line numbers that wouldn't make sense in this file. Saved immediately on OK so the history survives IDE crashes; Cancel leaves the registry untouched$(Config) macros in unit search paths and library paths now expand to the queried project's CurrentConfiguration instead of silently using the IDE's active project's configuration. The internal ProcessPaths helper in GX_OtaUtils.pas was always forwarding PlatformName explicitly but leaving ConfigName to be auto-detected by TPathProcessor from Ota.GetCurrentProject. In a single-project setup the active and queried projects are the same and the bug was invisible; in a project group with multiple projects on different configurations (the case Backup Project Group exercises by iterating IProjectGroup.Projects[i]), the non-active projects' $(Config) paths resolved against the wrong project. ProcessPaths now takes a symmetric ConfigName parameter, supplied by each of the 5 callsites via GetProjectConfig(Project) (or '' for the project-agnostic IDE library path)Line N instead of a bare integer, so it's clear what the number is. The clipboard format produced by double-click is unchanged (<filename>:<lineno> -- the conventional file:line form that other tools recognize). The panel's design-time width was bumped from 50 to 80 pixels so Line 99999 doesn't clipOpenExplorerAndSelectFile, the same helper the right-click "Show this file in Windows Explorer" menu item uses). Plain double-click still copies the status bar text to the clipboard (unchanged). The hover tooltip on the status bar now lists the new Ctrl+double-click action alongside the existing plain double-click and right-click lines so the shortcut is discoverableC:\. Also fixes a latent bug in the "remember the last directory" path: after the first export FDirectory was set with ExtractFileDir(fn) (no trailing backslash), so the *next* export's suggested filename was glued onto the directory without a separator (C:\some\dirTitle.dtme). Both the constructor seed and the post-save update now run through AddSlashTGExpertsManager/TEditorExpertManager, which is the registration order from the .dpr (effectively semi-random from a user's perspective and unstable across versions). TfrConfigureExperts.Init now calls FExperts.Sort(CompareExpertsByDisplayName) after Assign; FExperts is the frame's source of truth (filter, save, and click handlers all index into it via the row control's Tag), so a single sort before constructing the rows keeps everything consistentLoadProperty (the property-declaration scanner). The ModifierSet membership (default/stored/read/write/readonly/dispid) was tested twice per loop iteration -- once in the FRName-append guard and once in the if/else that controlled the switch. Restructured so the modifier check runs first; when the token is a modifier we set BuildRName := False and skip everything else, otherwise we run the FRName-append guard and the switch. The + ModifierSet part of the guard is dropped because BuildRName is already off in that branch. Provably equivalent; the inline todo flagging this is now goneContains or Requires used in ordinary code is no longer mangled. Both words are package directives (only keywords inside .dpk files), not reserved words, so the formatter had been lowercasing them and breaking the surrounding indentation -- e.g. if not Contains(S) then came out as if contains (S) then. They are now recognized as identifiers in every ordinary position: a call (Contains(S)), a qualified reference (Requires.Clear), a declaration (Requires: TStrings), an assignment target (Contains := True) or an operand (not Contains, := Requires); the qualified form Self.Contains was already handled correctly (bug #497, reported and fix supplied by Luka Havrlišan)IdentifiersCase, used by the twm configuration), an identifier that shares its spelling with a keyword or directive is no longer forced to the directive's casing. The first-occurrence pool was case-insensitive and included reserved words and directives, so e.g. the enum value in property MyEnum: TMyEnum read FMyEnum write FMyEnum default TMyEnum.Read; came out as TMyEnum.read (merged with the read accessor). Reserved words and directives are now excluded from the pool; their casing is still applied separately (bug #498, reported and fix supplied by Luka Havrlišan)Ota.ConvertColumnBytesToChars, which measured an uninitialized local instead of its argument and so returned 0 for every input -- it had no callers, which is why this went unnoticed (bug #256, reported by maurickow)TNotifierObject descends from TInterfacedObject and ToolsAPI does not override the reference counting), but the experts that install them keep a plain object reference, which does not count. Once registered, the IDE held the only counted reference, so whenever it dropped that one first, the object was freed while the expert still pointed at it; unregistering it afterwards read the notifier index out of freed memory and handed whatever it found to the IDE, which reported it as a list index far out of range -- the reported 1442539864 is $55FB6958, a heap address, on a notifier list of 27 entries. A notifier now holds a reference to itself for as long as it is registered, so it cannot be freed while an expert still points at it, no matter who lets go of it first. Two related defects are fixed in the same place: the notifier index was never initialized, so a notifier that was created but never successfully registered unregistered index 0 -- somebody else's notifier -- when it was freed; and IOTANotifier.Destroyed, the IDE's own signal that it has dropped the notifier, was ignored, so an index the IDE no longer honoured still looked valid. TNotifierObject.Destroyed is not virtual, so the override is bound with an explicit method resolution clause rather than by redeclaration. Affects TBaseIdeNotifier, TBaseDebuggerNotifier and TThemeChangedNotifier, that is every expert installing a notifier: Class Browser, Grep, Bookmarks, Macro Library, Unit Tags, Open File, IDE Shortcuts, ToDo List, the tabs menu and the Uses Clause Manager (bug #374, reported by Tommi Prami)HELP command (? does the same) makes the inspection server list the commands it supports, in sections, each with its arguments and a one-line description, followed by the two rules that were not written down anywhere: a form-id may be a form's Name, its ClassName or its Caption, and every response ends with a line containing only END. An unknown command now says so and points at HELP. This matters because the client is a separate executable from the DLL that answers it: the list of commands the client prints from its own usage text is only what that build of the client knew about, while HELP reports what the loaded GExperts DLL actually implements. Only relevant when GExperts is built with --enable-gxinspectUnitTests/TestClassBrowserParser covers the Class Browser's source parser (TClassItem.LoadClasses in GX_ClassMgr driving the mwParser token list), which until now had no test at all even though it runs over every unit of a project whenever there is no .GEX cache. It builds the real GX_ClassMgr and mwParser units and stubs only the three that pull in the ToolsAPI (which cannot be compiled outside the IDE), none of which the parse path calls. Nine cases cover simple, short (class(TBar);), forward, nested and interface declarations, type aliases to unit-qualified names, class of / of object, and truncated and empty units. A --scan <dir> mode parses every Pascal file below a directory the way the Class Browser does, for reproducing parser crashes outside the IDE (bug #245); it parsed 43000+ files across several source trees without a crashUnitTests/TestStringGridDraw checks that GX_StringGridDrawFix computes the position at which TStringGrid.DrawCell actually draws the cell text. It does not compare against expected numbers, which would only repeat the assumption being tested: for every cell it measures where the grid drew the text, erases the cell, draws the same text again at the computed position and measures a second time, and both must come out identical. The row heights are varied around the height of the text, because that is what the versions that centre the text react to. _runtests.sh builds and runs it once per Delphi version, since a result only says something about the version it was built with; _runtests.sh all currently passes for all 22 installed versions from Delphi 6 to 13UnitTests/TestGpStructuredStorage covers the vendored GpStructuredStorage, the single-file storage the Code Librarian keeps its library in. It guards the two fixes this storage received: bug #508 (on 64-bit the storage truncated pointers and could not even create an empty file) by building and running the same tests both 32-bit and 64-bit, and bug #510 (deleting a folder freed the deleted entry's name string twice and silently corrupted the file). The #510 case reproduces the Code Librarian import/delete pattern -- a sibling branch that must survive plus a branch of many sub-folders imported twice -- and runs the delete under a memory manager that quarantines freed blocks, so the double free is caught as a hard test failure rather than as corruption that only surfaces on reopen; reverting the fix turns exactly that test red. Further cases round-trip files and folders of various sizes. Ten tests, all passing 32-bit and 64-bitGX_StringGridDrawFix no longer guesses where the grid put the cell text. It used to add a fixed offset per IDE version, hand-tuned until the result looked right, which could only ever be correct as long as nothing else changed -- a taller row was enough to break it, because the grid centres the text vertically while a fixed offset does not. It now mirrors what Vcl.Grids actually does, which comes in three variants: Left + 2, Top + 2 up to XE1, a themed and an unthemed variant that are chosen at run time from XE2 to 10.3, and a deflated and centred rectangle from 10.4 on. The choice is still made at run time via GetBorlandIdeVersion because GExperts is built with runtime packages, so the code that draws is the one of the IDE the DLL is loaded into. Callers now only draw the cells that need colours of their own and leave the rest to the gridExternalSource. It was only used by Backup Project, which now uses SynZip like the rest of GExperts, so the tree carries one zip library instead of two (about 14,700 lines less). Its search path entries are gone from all project files, and the note in GX_CondDefine.inc about include files that may need attention for a new Delphi release now points at SynZip's Synopse.inc instead of Abbrevia's AbDefine.inc, which was the file that needed a new compiler version added to it each release. The Delphi 2005 and 2006 projects, which listed Abbrevia but had never had SynZip added when the other projects got it, now have it: both include units that use SynZip, so neither of them could be built until now__BuildEverything.cmd accepts the Delphi version to build the standalone tools with as a parameter (D2007, D102, D103, D104, D11, D12, D13), which runs the whole release build unattended: the version is not asked for, and nothing pauses, not even on error. Without the parameter it asks as before. The mapping and the check that the requested version is installed live in Projects\StandAlone\doSelectDelphi.cmd, which also takes the parameter now, so every caller of it can run unattendedBuildInstall/GExperts.fbp6 and GExperts.fbp7 have been removed. They were last touched in 2010 and 2016, still described the source tree layout used before the move to SourceForge, and listed none of the libraries added since, so they could not build GExperts any more. Installers are built with the Inno Setup scripts in the same directoryTSnippetImporter class, so it no longer depends on the window being open and can be unit tested. The folder creation, unique naming and language attribute now have their own tests against a real storage fileIllegal character: '$EF'). Comments now use -- instead of em-dashes and -> instead of arrows; the French regex matches in GX_eRemoveMatchingLines now use #233 escapes for e-acute (they had previously been corrupted to replacement characters and matched nothing)GX_Goto.pas and GX_ProofreaderCorrection.pas are now --, and a corrupted character (a mangled //TODO marker) in GX_BookmarksConst.pas was restored. Keeps the files pure ASCII so the IDE does not re-save them as UTF-8-with-BOM and break Delphi 6 buildsShowGxMessageBox, EnumFilesOnly (dzlib), RenameIdentifierInSource, CheckNameConflict, ResolveDottedChain, InferTypeFromContext, AnalyzeForDeclaration, the procedure-list filter loop and LoadProjOptionList. The variables are all used safely (dynamic arrays auto-initialize to nil and use sites are within the branch that assigns them), but Delphi 6's flow analysis can't prove it. An explicit SetLength(X, 0) at the top of each function, guarded by {$IFNDEF GX_DELPHI7_UP} (or {$IFNDEF DELPHI7_UP} in the dzlib unit), suppresses the warning without adding any code on D7+TGxEditorViewActivatedNotifier in GX_EditorChangeServices.pas abstracts the IGxEditorNotification boilerplate (~50 lines of identical empty-stub interface methods) for subscribers that only care about the EditorViewActivated event. The owner just creates the helper with a callback and FreeAndNils it on teardown -- registration and unregistration with GxEditorChangeServices are handled internally. The AutoTODO list (TfmToDo) and Editor Bookmarks (TBookmarksExpert) now use it instead of declaring their own TInterfacedObject subclass each, dropping a per-expert class declaration plus seven trivial method bodies. Also fixes a latent bug in the previous boilerplate where the per-expert notifier's GetIndex returned an unset field (always 0) instead of the index that AddNotifier had handed back to the owner -- which would have caused RemoveNotifierIfNecessary to either match nothing or, worse, remove the wrong subscriberTGXStorageFile.SaveStorage and its three call sites. IGpStructuredStorage exposes no Save method and never has; writes to .gex files go through the underlying TStream returned by OpenFile, which is durable when each mutation returns. The method body was always empty (just a commented //FStorage.Save; and a {TODO} asking whether it was disabled on purpose), so the calls were doing nothingBlockSelectionToLineEndType was a byte-for-byte copy in GX_eChangeCase and GX_eSelectionEditorExpert, and the surrounding "replace the selection, stripping the trailing CR/LF that Ota.ReplaceSelection would otherwise duplicate for a multi-line selection" if/else was duplicated too. Both now live as class methods on a new helper class TGxSelectionReplacer in Source/Utils/GX_OtaSelectionReplacer.pas (BlockSelectionToLineEndType + ReplaceSelectionPreserveEol), called by Change Case and by every TSelectionEditorExpert descendant (Align, Sort, Quote, Comment, PasteAs, Reverse Statement, ...). No behavior change. The unexplained EditPos.Col > 2 check is preserved verbatim in the single surviving copy