Recent

Author Topic: Basics of writing SynEdit macros  (Read 921 times)

440bx

  • Hero Member
  • *****
  • Posts: 5559
Basics of writing SynEdit macros
« on: June 08, 2025, 05:49:41 pm »

Introduction

Macros have the potential to save a lot work.  In addition to that, they can prevent "auto-pilot" mistakes common in repetitive tasks.

A SynEdit macro may be a macro that consists of keystrokes that have been recorded.  No programming is needed in this case, simply tell the IDE to start recording, type the text the macro needs to contain, stop recording when done and, simply replay the macro whenever needed.  This thread will not address these macros as they are considered trivial and need no explanation.

A SynEdit macro may also be a program that consists of SynEdit key commands logically controlled by a subset of PascalScript.  This allows for creating much more sophisticate macros, giving the editor power to carry out genuinely sophisticated tasks.

It is very important to keep in mind that programming a macro is slightly different than traditional programming.  This is mostly due to the fact that it is the editor that is being programmed and, the editor is not a file, a console or a window.  For instance, the editor can be thought of as a grid of n varying length lines and, if the caret is placed at a location (X, Y) and a character is typed there, the editor automatically fills any empty space with spaces that may have existed from the last character before X on line Y to the new character that has been placed in position (X, Y).

Basically, when programming the editor, you can place the caret just about anywhere, type something and,  any space that used to be empty between the last existing character and the character ypu typed is automatically filled it with spaces.    This is very convenient and can greatly simplify macro writing.

continued on next post...
« Last Edit: June 09, 2025, 01:56:06 am by 440bx »
(FPC v3.0.4 and Lazarus 1.8.2) or (FPC v3.2.2 and Lazarus v4.0rc3) on Windows 7 SP1 64bit.

440bx

  • Hero Member
  • *****
  • Posts: 5559
Re: Basics of writing SynEdit macros
« Reply #1 on: June 08, 2025, 06:13:33 pm »

PascalScript limitations in macros

PascalScript is a subset of Pascal and when used in a SynEdit macro, there are limitations that must be kept in mind at all times in order to create a working script.

1.  neither local type declarations nor local constants are allowed in macro functions and/or procedures             

2.  set declarations cannot use the .. (range) specifier.  This often makes set declarations impractical. e.g, ['a'..'z'], declaring such a set requires specifying every character, i.e, 'a', 'b', 'c', 'd', etc, all the way to 'z'                                                       

3.  range types are not allowed, e.g.                                   
      type                                                                 
        TRANGE = 0..5;                                                     

4.  typed constants are not allowed                                       

5.  there is no writeln function in macros                                 

6.  if the colon is missing in an assignment token, i.e, "=" instead of ":=", PascalScript emits an "Internal error (20)" which isn't of much help to notice that the colon is missing.                               }

7.  break and continue don't seem to work as expected when used in _nested_ loops (strange/random internal error messages are output.)  They do seem to work as expected in non-nested loops.                           

8.  there is no debugger, there is no unit facility, there is no include file facility, there is no preprocessor facility.  These restrictions affect how the Pascal source is arranged/structured.                   



PascalScript characteristics to always keep in mind:

1. strings in macros are always 1 based       



STRONGLY RECOMMENDED:                                                     

Because of all the language restrictions and the lack of a capable debugging facility it is advisable to keep the Pascal statements very simple.

Typical statements found in a FP program are often too complex for the macro subset of PascalScript.  The more complex the statement is, the harder it becomes to determine which part of the statement is not acceptable to PascalScript.  Statement complexity can also make debugging more difficult than necessary. As the saying goes: keep it simple, it will reward you handsomely.



environment characteristics to _always_ keep in mind:

1. the IDE editor internally holds the lines in an array of Lines that is ZERO based.  That is, the line that is shown as line 1 in the source editor has index 0 in that array (the Lines array.)

2. the Caret coordinate, UNLIKE the lines array, is ONE (1) based.  It is extremely easy to forget this and use a caret's Y coordinate as a line index or vice-versa causing an "off by 1" error.

3. when editing a macro, the message window does _not_ clear old messages when the macro is saved (unlike when compiling, every time a program is compiled, the messages window is cleared.)  For this reason, it is convenient to "manually" clear the messages by right-clicking in the messages window and selecting "clear".  Failure to do this can lead the programmer to believe there are still errors that are not there (they simply are old error messages.)

this is also a reason to keep the Editor Macro window opened because if there is a problem with the macro a red exclamation sign will appear next to it.  It is still a good idea to clear old messages because the red exclamation mark is often insufficient to rectify the problem (the error message is usually a better source of information.)



continued on next post...
(FPC v3.0.4 and Lazarus 1.8.2) or (FPC v3.2.2 and Lazarus v4.0rc3) on Windows 7 SP1 64bit.

440bx

  • Hero Member
  • *****
  • Posts: 5559
Re: Basics of writing SynEdit macros
« Reply #2 on: June 08, 2025, 06:26:34 pm »

The functions available when creating a macro are split between functions provided by SynEdit and functions provided by PascalScript.

PascalScript functions

Code: Pascal  [Select][+][-]
  1.  
  2.  
  3. { IMPORTANT:                                                                  }
  4. {                                                                             }
  5. { in addition to the list of functions below, there are other absolutely      }
  6. { essential functions listed at:                                              }
  7.  
  8. { https://wiki.freepascal.org/Editor_Macros_PascalScript                      }
  9.  
  10. { the information contained in the above page is a MUST read                  }
  11.  
  12.  
  13.  
  14. { CREDIT: list obtained from https://www.elrasoft.com/uuprogs_help/HTML/standardlibrary_id.htm }
  15.  
  16. { sorted on function/procedure name                                           }
  17. { parameters, type and result type, column aligned                            }
  18. { all type identifiers lowercased                                             }
  19. { all definition semicolon ended                                              }
  20.  
  21.  
  22. function  Abs            (    e : extended)                                                         : extended;
  23.  
  24. function  Copy           (    s : string;     ifrom, icount : longint)                              : string;
  25. function  Cos            (    e : extended)                                                         : extended;
  26.  
  27. procedure Delete         (var s                             : string;      ifrom, icount : longint) : string;
  28.  
  29. function  FloatToStr     (    e : extended)                 : string;
  30. function  FloatToStr     (    e : extended)                 : string;
  31.  
  32. function  GetArraylength (var v                             : array)                                : Integer;
  33.  
  34. procedure Insert         (    s : string; var s2            : string;      ipos          : longint) : string;
  35. function  Int            (    e : extended)                                                         : longint;
  36. function  IntToStr       (    i : longint)                                                          : string;
  37.  
  38. function  Length         (    s : string)                                                           : longint;
  39. function  Lowercase      (    s : string)                                                           : string;
  40.  
  41. function  Padl           (    s : string;     I             : longint)                              : string;
  42. function  Padr           (    s : string;     I             : longint)                              : string;
  43. function  Padz           (    s : string;     I             : longint)                              : string;
  44. function  Pi                                                                                        : extended;
  45. function  Pos            (    substr,         s             : string)                               : longint;
  46.  
  47. function  Replicate      (    c : char;       I             : longint)                              : string;
  48. function  Round          (    e : extended)                                                         : longint;
  49. procedure SetArrayLength (var v : array;      i             : Integer);
  50.  
  51. procedure SetLength      (var s : string;     L             : longint);
  52. function  Sin            (    e : extended)                                                         : extended;
  53. function  Sqrt           (    e : extended)                                                         : extended;
  54. function  StrGet         (    s : string;     I             : Integer)                              : char;
  55. function  StrSet         (    c : char;       I             : Integer; var s             : string)  : char;
  56. function  StrToFloat     (    s : string)                                                           : extended;
  57. function  StrToInt       (    s : string)                                                           : longint;
  58. function  StrToIntDef    (    s : string;     def           : longint)                              : longint;
  59. function  stringOfchar   (    c : char;       I             : longint)                              : string;
  60.  
  61. function  Trim           (    s : string)                                                           : string;
  62. function  Trunc          (    e : extended)                                                         : longint;
  63.  
  64. function  Uppercase      (    s : string)                                                           : string;
  65.  
In the above list the function name is usually sufficient to figure out what the function does.



Synedit functions

Unlike the PascalScript functions, the exact operation of a SynEdit operation may require some trial and error. 

At this time, I am not aware of a description of how each function/key command operates.   

Quote

this list of SynEdit functions/key commands was obtained from               
                                                                             }
https://gitlab.com/freepascal.org/lazarus/lazarus/-/blob/main/components/ideintf/idecommands.pas
                                                                             }
which, at the time of this writing is about 5 months old                   

see syneditkeycmds.pp for the most recent list                             


// search
  ecFind
  ecFindAgain
  ecFindNext
  ecFindPrevious
  ecReplace
  ecIncrementalFind
  ecFindProcedureDefinition
  ecFindProcedureMethod
  ecGotoLineNumber
  ecFindNextWordOccurrence
  ecFindPrevWordOccurrence
  ecFindInFiles
  ecJumpToNextSearchResult
  ecJumpToPrevSearchResult
  ecJumpBack
  ecJumpForward
  ecAddJumpPoint
  ecViewJumpHistory
  ecJumpToNextError
  ecJumpToPrevError
  ecProcedureList

  // search code
  ecFindDeclaration
  ecFindBlockOtherEnd
  ecFindBlockStart
  ecOpenFileAtCursor
  ecGotoIncludeDirective
  ecJumpToSection
  ecJumpToInterface
  ecJumpToInterfaceUses
  ecJumpToImplementation
  ecJumpToImplementationUses
  ecJumpToInitialization
  ecJumpToProcedureHeader
  ecJumpToProcedureBegin

  // edit selection
  ecSelectionUpperCase
  ecSelectionLowerCase
  ecSelectionSwapCase
  ecSelectionTabs2Spaces
  ecSelectionEnclose
  ecSelectionComment
  ecSelectionUncomment
  ecSelectionSort
  ecSelectionBreakLines
  ecSelectToBrace
  ecSelectCodeBlock
  ecSelectWord
  ecSelectLine
  ecSelectParagraph
  ecSelectionEncloseIFDEF
  ecToggleComment

  // insert text
  ecInsertCharacter           // not used, moved to external package
  ecInsertGUID
  ecInsertFilename
  ecInsertUserName
  ecInsertDateTime
  ecInsertChangeLogEntry
  ecInsertCVSAuthor
  ecInsertCVSDate
  ecInsertCVSHeader
  ecInsertCVSID
  ecInsertCVSLog
  ecInsertCVSName
  ecInsertCVSRevision
  ecInsertCVSSource
  ecInsertGPLNotice
  ecInsertGPLNoticeTranslated
  ecInsertLGPLNotice
  ecInsertLGPLNoticeTranslated
  ecInsertModifiedLGPLNotice
  ecInsertModifiedLGPLNoticeTranslated
  ecInsertMITNotice
  ecInsertMITNoticeTranslated

  // source tools
  ecWordCompletion
  ecCompleteCode
  ecIdentCompletion
  ecSyntaxCheck
  ecGuessUnclosedBlock
  ecGuessMisplacedIFDEF
  ecConvertDFM2LFM
  ecCheckLFM
  ecConvertDelphiUnit
  ecConvertDelphiProject
  ecConvertDelphiPackage
  ecConvertEncoding
  ecMakeResourceString
  ecDiff
  ecExtractProc
  ecFindIdentifierRefs
  ecRenameIdentifier
  ecInvertAssignment
  ecShowCodeContext
  ecShowAbstractMethods
  ecRemoveEmptyMethods
  ecRemoveUnusedUnits
  ecUseUnit
  ecFindOverloads
  ecFindUsedUnitRefs
  ecCompleteCodeInteractive

  // file menu
  ecNew
  ecNewUnit
  ecNewForm
  ecOpen
  ecRevert
  ecSave
  ecSaveAs
  ecSaveAll
  ecClose
  ecCleanDirectory
  ecRestart
  ecQuit
  ecOpenUnit
  ecOpenRecent
  ecCloseOtherTabs
  ecCloseRightTabs

  // edit menu
  ecMultiPaste

  // IDE navigation
  ecToggleFormUnit
  ecToggleObjectInsp
  ecToggleSourceEditor
  ecToggleCodeExpl
  ecToggleFPDocEditor
  ecToggleMessages
  ecToggleWatches
  ecToggleBreakPoints
  ecToggleDebuggerOut
  ecViewUnitDependencies
  ecViewUnitInfo
  ecToggleLocals
  ecToggleCallStack
  ecToggleSearchResults
  ecViewAnchorEditor
  ecViewTabOrder
  ecToggleCodeBrowser
  ecToggleCompPalette
  ecToggleIDESpeedBtns
  ecViewComponents
  ecToggleRestrictionBrowser
  ecViewTodoList
  ecToggleRegisters
  ecToggleAssembler
  ecToggleDebugEvents
  ecViewPseudoTerminal
  ecViewThreads
  ecViewHistory
  ecViewMacroList
  ecToggleMemViewer

  // sourcenotebook commands
  ecNextEditor
  ecPrevEditor
  ecMoveEditorLeft
  ecMoveEditorRight
  ecMoveEditorLeftmost
  ecMoveEditorRightmost

  ecNextSharedEditor
  ecPrevSharedEditor

  ecNextWindow
  ecPrevWindow
  ecMoveEditorNextWindow
  ecMoveEditorPrevWindow
  ecMoveEditorNewWindow
  ecCopyEditorNextWindow
  ecCopyEditorPrevWindow
  ecCopyEditorNewWindow
  ecPrevEditorInHistory
  ecNextEditorInHistory

  ecGotoEditor1
  ecGotoEditor2
  ecGotoEditor3
  ecGotoEditor4
  ecGotoEditor5
  ecGotoEditor6
  ecGotoEditor7
  ecGotoEditor8
  ecGotoEditor9
  ecGotoEditor0

  ecLockEditor

  // marker
  ecSetFreeBookmark
  ecPrevBookmark
  ecNextBookmark
  ecClearBookmarkForFile
  ecClearAllBookmark

  ecGotoBookmarks
  ecToggleBookmarks

  // Macro
  ecSynMacroRecord
  ecSynMacroPlay

  // run menu
  ecCompile
  ecBuild
  ecQuickCompile
  ecCleanUpAndBuild
  ecBuildManyModes
  ecAbortBuild
  ecRunWithoutDebugging
  ecRun
  ecPause
  ecStepInto
  ecStepOver
  ecStepToCursor
  ecStopProgram
  ecResetDebugger
  ecRunParameters
  ecRunToCursor
  ecRunWithDebugging

  ecBuildFile
  ecRunFile
  ecConfigBuildFile
  ecInspect
  ecEvaluate
  ecAddWatch
  ecShowExecutionPoint
  ecStepOut
  ecStepIntoInstr
  ecStepOverInstr
  ecStepIntoContext
  ecStepOverContext
  ecAddBpSource
  ecAddBpAddress
  ecAddBpDataWatch
  ecAttach
  ecDetach

  // 460++ : used for ecViewHistory (debugger) / ecViewMacroList

  ecToggleBreakPoint
  ecRemoveBreakPoint
  ecToggleBreakPointEnabled
  ecBreakPointProperties


  // project menu
  ecNewProject
  ecNewProjectFromFile
  ecOpenProject
  ecOpenRecentProject
  ecCloseProject
  ecSaveProject
  ecSaveProjectAs
  ecPublishProject
  ecProjectInspector
  ecAddCurUnitToProj
  ecRemoveFromProj
  ecViewProjectUnits
  ecViewProjectForms
  ecViewProjectSource
  ecProjectOptions
  ecProjectChangeBuildMode
  ecProjectResaveFormsWithI18n

  // package menu
  ecOpenPackage
  ecOpenPackageFile
  ecOpenPackageOfCurUnit
  ecOpenRecentPackage
  ecAddCurFileToPkg
  ecNewPkgComponent
  ecPackageGraph
  ecPackageLinks
  ecEditInstallPkgs
  ecConfigCustomComps
  ecNewPackage

  // custom tools menu
  ecExtToolFirst
  ecExtToolLast

  // tools menu
  ecEnvironmentOptions
  ecRescanFPCSrcDir
  ecEditCodeTemplates
  ecCodeToolsDefinesEd

  ecExtToolSettings
  ecManageDesktops
  // ecManageExamples
  ecConfigBuildLazarus
  ecBuildLazarus
  ecBuildAdvancedLazarus

  // window menu
  ecManageSourceEditors

  // help menu
  ecAboutLazarus
  ecOnlineHelp
  ecContextHelp
  ecEditContextHelp
  ecReportingBug
  ecFocusHint
  ecSmartHint

  // designer
  ecDesignerCopy
  ecDesignerCut
  ecDesignerPaste
  ecDesignerSelectParent
  ecDesignerMoveToFront
  ecDesignerMoveToBack
  ecDesignerForwardOne
  ecDesignerBackOne
  ecDesignerToggleNonVisComps


  // SynEdit Plugins
  //   Define fixed values for the IDE. Must be mapped to plugincommands,
  //   when assigned to KeyMap.
  //   Offsets are defined in KeyMapping
  //   See: TKeyCommandRelationList.AssignTo

  ecFirstPlugin
  ecLastPlugin

  // custom commands
  ecLazarusLast

  // TSynPluginTemplateEdit - In cell
  ecIdePTmplEdNextCell
  ecIdePTmplEdNextCellSel
  ecIdePTmplEdNextCellRotate
  ecIdePTmplEdNextCellSelRotate
  ecIdePTmplEdPrevCell
  ecIdePTmplEdPrevCellSel
  ecIdePTmplEdCellHome
  ecIdePTmplEdCellEnd
  ecIdePTmplEdCellSelect
  ecIdePTmplEdFinish
  ecIdePTmplEdEscape
  ecIdePTmplEdNextFirstCell
  ecIdePTmplEdNextFirstCellSel
  ecIdePTmplEdNextFirstCellRotate
  ecIdePTmplEdNextFirstCellSelRotate
  ecIdePTmplEdPrevFirstCell
  ecIdePTmplEdPrevFirstCellSel

  // TSynPluginTemplateEdit - Out off Cell
  ecIdePTmplEdOutNextCell
  ecIdePTmplEdOutNextCellSel
  ecIdePTmplEdOutNextCellRotate
  ecIdePTmplEdOutNextCellSelRotate
  ecIdePTmplEdOutPrevCell
  ecIdePTmplEdOutPrevCellSel
  ecIdePTmplEdOutCellHome
  ecIdePTmplEdOutCellEnd
  ecIdePTmplEdOutCellSelect
  ecIdePTmplEdOutFinish
  ecIdePTmplEdOutEscape
  ecIdePTmplEdOutNextFirstCell
  ecIdePTmplEdOutNextFirstCellSel
  ecIdePTmplEdOutNextFirstCellRotate
  ecIdePTmplEdOutNextFirstCellSelRotate
  ecIdePTmplEdOutPrevFirstCell
  ecIdePTmplEdOutPrevFirstCellSel

  // TSynPluginSyncroEdit - in celll
  ecIdePSyncroEdNextCell
  ecIdePSyncroEdNextCellSel
  ecIdePSyncroEdPrevCell
  ecIdePSyncroEdPrevCellSel
  ecIdePSyncroEdCellHome
  ecIdePSyncroEdCellEnd
  ecIdePSyncroEdCellSelect
  ecIdePSyncroEdEscape
  ecIdePSyncroEdNextFirstCell
  ecIdePSyncroEdNextFirstCellSel
  ecIdePSyncroEdPrevFirstCell
  ecIdePSyncroEdPrevFirstCellSel
  ecIdePSyncroEdGrowCellLeft
  ecIdePSyncroEdShrinkCellLeft
  ecIdePSyncroEdGrowCellRight
  ecIdePSyncroEdShrinkCellRight
  ecIdePSyncroEdAddCell
  ecIdePSyncroEdAddCellCase
  ecIdePSyncroEdAddCellCtx
  ecIdePSyncroEdAddCellCtxCase
  ecIdePSyncroEdDelCell

  // TSynPluginSyncroEdit - Out off cell
  ecIdePSyncroEdOutNextCell
  ecIdePSyncroEdOutNextCellSel
  ecIdePSyncroEdOutPrevCell
  ecIdePSyncroEdOutPrevCellSel
  ecIdePSyncroEdOutCellHome
  ecIdePSyncroEdOutCellEnd
  ecIdePSyncroEdOutCellSelect
  ecIdePSyncroEdOutEscape
  ecIdePSyncroEdOutNextFirstCell
  ecIdePSyncroEdOutNextFirstCellSel
  ecIdePSyncroEdOutPrevFirstCell
  ecIdePSyncroEdOutPrevFirstCellSel
  // grow/shrink 92..95 (reserved)
  ecIdePSyncroEdOutAddCell
  ecIdePSyncroEdOutAddCellCase
  ecIdePSyncroEdOutAddCellCtx
  ecIdePSyncroEdOutAddCellCtxCase
  // del 100 (reserved)

  // TSynPluginSyncroEdit - selecting
  ecIdePSyncroEdSelStart
  ecIdePSyncroEdSelStartCase
  ecIdePSyncroEdSelStartCtx
  ecIdePSyncroEdSelStartCtxCase




continued on next post...
(FPC v3.0.4 and Lazarus 1.8.2) or (FPC v3.2.2 and Lazarus v4.0rc3) on Windows 7 SP1 64bit.

440bx

  • Hero Member
  • *****
  • Posts: 5559
Re: Basics of writing SynEdit macros
« Reply #3 on: June 08, 2025, 06:50:39 pm »
Debugging macros

Unfortunately, there is no macro debugger and, there isn't something as convenient as writeln either.  This makes debugging macros a bit cumbersome. 

What follows are procedure definitions that aid in the debugging of macros.

Code: Pascal  [Select][+][-]
  1. { MACRO Purpose:                                                              }
  2. {                                                                             }
  3. { provide debugging functions to be used in synedit macros                    }
  4.  
  5. { HOW TO USE:                                                                 }
  6. {                                                                             }
  7. { COPY the contents of this file into the new macro's file                    }
  8.  
  9. const                                         { COPY from HERE -------------- }
  10.   { debug message output control                                              }
  11.  
  12.   DEBUG_OUTPUT_MESSAGES = TRUE;
  13.   DEBUG_PAUSE           = TRUE;
  14.  
  15. procedure OutputDebugString
  16.             (
  17.              const InDebugString : string;
  18.              const InOutput      : boolean
  19.             );
  20.   { outputs a string if the InOutput parameter is TRUE and the setting of     }
  21.   { outputting debug messages is also TRUE.  if either one is FALSE, no       }
  22.   { output takes place                                                        }
  23.  
  24. begin
  25.   { if debug messages are globally suppressed, exit                           }
  26.  
  27.   if not DEBUG_OUTPUT_MESSAGES then exit;
  28.  
  29.   if not InOutput              then exit;    { this message is suppressed     }
  30.  
  31.   { neither globally nor specifically suppressed, emit the message            }
  32.  
  33.   ShowMessage(InDebugString);
  34. end;
  35.  
  36.  
  37. procedure Pause(const InPauseMessage : string);
  38.   { pauses execution by prompting the user to press any key.  No pausing will }
  39.   { occur if the global DEBUG_PAUSE is FALSE                                  }
  40.  
  41. var
  42.   InString : string;
  43.  
  44. begin
  45.   if not DEBUG_PAUSE then exit;
  46.  
  47.   if Trim(InPauseMessage) = '' then InPauseMessage := 'execution PAUSED';
  48.  
  49.   InputQuery(InPauseMessage, 'press ENTER/RETURN to continue', InString);
  50.  
  51. end; { ---------------------------------------- to HERE inclusive ----------- }
  52.  
  53. { DON'T copy from this point on down                                          }
  54.  
  55. begin
  56. end.
  57.  
  58. // end of file
  59. // ----------------------------------------------------------------------------
  60.  

The above code provides 2 functions to aid in debugging.

The first function OutputDebugString is very similar to its Windows counterpart.  It takes an additional parameter that determines if the string will be displayed or not.   The reason for this additional parameter is to make it easy to no longer output a debug string.  Once you are satisfied that the code is, to that point working correctly, simply set the second parameter to FALSE and the string will no longer be output. 

This way, all the debugging code can remain in the macro to be used again if the macro fails in some case.

The global variable DEBUG_OUTPUT_MESSAGES is to globally control the output of debug messages.  This allows leaving some important calls to OutputDebugString individually enabled but, no debug string will be displayed because the global setting prevents it.  This way, the most important calls can be left enabled and have their output shown by simply toggling the global setting value.

The second function, Pause, is to as its name implies, pause the macro's execution.  This can be useful to occasionally inspect the progress of the macro.  The pausing can be globally controlled by setting the DEBUG_PAUSE to the appropriate value.

Attached to this post is the code shown above.

Now that we have a basic debugging facility, we can proceed to write some macros.  Learn by example.



continued on next post...



ETA:

corrected a couple of minor typos.

IMPORTANT: the description of Pause says "by prompting the user to press any key" it should say "by prompting the user to press the RETURN/ENTER key"
« Last Edit: June 09, 2025, 02:09:05 am by 440bx »
(FPC v3.0.4 and Lazarus 1.8.2) or (FPC v3.2.2 and Lazarus v4.0rc3) on Windows 7 SP1 64bit.

440bx

  • Hero Member
  • *****
  • Posts: 5559
Re: Basics of writing SynEdit macros
« Reply #4 on: June 08, 2025, 07:23:48 pm »

Note: all code is attached to this post.  There is no need to copy/paste code, just download the attachments.

The following example is a _slightly_ modified example found at: https://wiki.lazarus.freepascal.org/Editor_Macros_PascalScript

The code the macro will act upon is:
Code: Pascal  [Select][+][-]
  1. {$APPTYPE CONSOLE}
  2.  
  3. { original example found at:                                                  }
  4. {                                                                             }
  5. { https://wiki.lazarus.freepascal.org/Editor_Macros_PascalScript              }
  6.  
  7. program _align;
  8.  
  9. { select the block of lines that start with "text" and end with "foo"         }
  10.  
  11.  
  12. var
  13.   text   : string;
  14.     a        : Integer;
  15.    foo          : boolean;
  16.  
  17.  
  18.  
  19. { keep a copy of the original block to make re-running the macro easier and   }
  20. { results repeatable.                                                         }
  21.  
  22. { note that the macro does not align the variable names                       }
  23.  
  24. var
  25.   text   : string;
  26.     a        : Integer;
  27.    foo          : boolean;
  28.  
  29.  
  30.  
  31. begin
  32. end.
  33.  
  34. // end of file
  35. // ----------------------------------------------------------------------------
  36.  

Our first macro will be to align data types.

After playing/running the macro, the aligned declarations will look like this:
Code: Pascal  [Select][+][-]
  1. var
  2.   text          : string;
  3.     a           : Integer;
  4.    foo          : boolean;
  5.  

The macro code that does the alignment is:
Code: Pascal  [Select][+][-]
  1.  
  2.  
  3. const                                         { COPY from HERE -------------- }
  4.   { debug message output control                                              }
  5.  
  6.   DEBUG_OUTPUT_MESSAGES = TRUE;
  7.   DEBUG_PAUSE           = TRUE;
  8.  
  9. procedure OutputDebugString
  10.             (
  11.              const InDebugString : string;
  12.              const InOutput      : boolean
  13.             );
  14.   { outputs a string if the InOutput parameter is TRUE and the setting of     }
  15.   { outputting debug messages is also TRUE.  if either one is FALSE, no       }
  16.   { output takes place                                                        }
  17.  
  18. begin
  19.   { if debug messages are globally suppressed, exit                           }
  20.  
  21.   if not DEBUG_OUTPUT_MESSAGES then exit;
  22.  
  23.   if not InOutput              then exit;    { this message is suppressed     }
  24.  
  25.   { neither globally nor specifically suppressed, emit the message            }
  26.  
  27.   ShowMessage(InDebugString);
  28. end;
  29.  
  30.  
  31. procedure Pause(const InPauseMessage : string);
  32.   { pauses execution by prompting the user to press any key.  No pausing will }
  33.   { occur if the global DEBUG_PAUSE is FALSE                                  }
  34.  
  35. var
  36.   InString : string;
  37.  
  38. begin
  39.   if not DEBUG_PAUSE then exit;
  40.  
  41.   if Trim(InPauseMessage) = '' then InPauseMessage := 'execution PAUSED';
  42.  
  43.   InputQuery(InPauseMessage, 'press ENTER/RETURN to continue', InString);
  44.  
  45. end; { ---------------------------------------- to HERE inclusive ----------- }
  46.  
  47.  
  48.  
  49. { since set definitions cannot use the .. (range), this function test for     }
  50. { membership in the set ['a'..'z', 'A'..'Z', '_']                             }
  51.  
  52. function IsIdent(c: Char): Boolean;
  53. begin
  54.   Result := ((c >= 'a') and (c <= 'z')) or
  55.             ((c >= 'A') and (c <= 'Z')) or
  56.             ((c >= '0') and (c <= '9')) or
  57.             (c = '_');
  58. end;
  59.  
  60. { --------------------------------------------------------------------------- }
  61.  
  62. var
  63.   p1, p2  : TPoint;     { p1 = start of selection, p2 = end of selection      }
  64.   s1, s2  : string;
  65.  
  66.   i, j, k : Integer;
  67.  
  68.  
  69. begin
  70.    { SelAvail: returns TRUE when some text is selected.                       }
  71.  
  72.    if not Caller.SelAvail then exit;  { no selection = nothing to do          }
  73.  
  74.    { BlockBegin and BlockEnd return the selection points                      }
  75.  
  76.    { BlockBegin: initial X,Y coordinate of selected text                      }
  77.    { BlockEnd  : ending  X,Y coordinate of selected text                      }
  78.  
  79.    p1 := Caller.BlockBegin;
  80.    p2 := Caller.BlockEnd;
  81.  
  82.    OutputDebugString('p1.x: ' + IntToStr(p1.x) + '   ' +
  83.                      'p1.y: ' + IntToStr(p1.y) + '   ' +
  84.                      'p2.x: ' + IntToStr(p2.x) + '   ' +
  85.                      'p2.y: ' + IntToStr(p2.y) + '   ',
  86.                      TRUE);
  87.  
  88.    { NOTE: the original macro flipped the values of BlockBegin and BlockEnd   }
  89.    {       if it found that p1.y was greater than p2.y or p2.x was greater    }
  90.    {       than p1.x, this action does not seem to be necessary as testing    }
  91.    {       shows that BlockBegin is always "less" than BlockEnd               }
  92.  
  93.    //   original code to flip values has been commented out as it does not seem
  94.    //   to be necessary
  95.    //
  96.    //   if (p1.y > p2.y) or ((p1.y = p2.y) and (p1.x > p2.x)) then
  97.    //   begin
  98.    //     p1 := Caller.BlockEnd;
  99.    //     p2 := Caller.BlockBegin;
  100.    //   end;
  101.    //
  102.    //   OutputDebugString('p1.x: ' + IntToStr(p1.x) + '   ' +
  103.    //                     'p1.y: ' + IntToStr(p1.y) + '   ' +
  104.    //                     'p2.x: ' + IntToStr(p2.x) + '   ' +
  105.    //                     'p2.y: ' + IntToStr(p2.y) + '   ',
  106.    //                     TRUE);
  107.  
  108.  
  109.    { the following is important to keep in mind:                              }
  110.    {                                                                          }
  111.    { point coordinates are 1 based but the Lines array is zero based          }
  112.  
  113.    s1 := Caller.Lines[p1.y - 1];  // s1 = first line in the selected block
  114.    s2 := '';                      // s2 nothing
  115.  
  116.    OutputDebugString('s1: ' + s1, TRUE);
  117.  
  118.    { NOTE: the above statement are not necessary for the macro to operate     }
  119.    {       properly                                                           }
  120.  
  121.    { the following code in the original macro is commented out as it is       }
  122.    { unnecessary for the macro to function properly                           }
  123.  
  124.    // { skip over whitespace starting from p1.x                                  }
  125.    //
  126.    // i := p1.x
  127.    // while (i <= length(s1)) and (s1[i] in [#9, ' ']) do inc(i);
  128.    //
  129.    // OutputDebugString('i: ' + IntToStr(i), TRUE);
  130.    //
  131.    //
  132.    // { look for a Pascal identifier                                             }
  133.    //
  134.    // j := i;
  135.    // if i <= length(s1) then
  136.    // begin
  137.    //
  138.    //   if IsIdent(s1[i]) then
  139.    //   { we are at the beginning of a Pascal identifier                         }
  140.    //   begin
  141.    //     while (i <= length(s1)) and IsIdent(s1[i]) do inc(i)
  142.    //   end
  143.    //   else
  144.    //   { something that is _not_ a Pascal identifier                            }
  145.    //
  146.    //   begin
  147.    //     while (i <= length(s1)) and not(IsIdent(s1[i]) or (s1[i] in [#9, ' '])) do inc(i);
  148.    //   end
  149.    // end;
  150.    //
  151.    //
  152.    // OutputDebugString('j: ' + IntToStr(j), TRUE);
  153.    //
  154.    //
  155.    // { if we found an identifier, save it in s2                                 }
  156.    //
  157.    // if i > j then s2 := copy(s1, j, i-j);  { s2 = first identifier on line     }
  158.    //
  159.    // OutputDebugString('s2: ' + s2, TRUE);
  160.  
  161.  
  162.    { ask the user for the token to align to (in this case, it should be a ":" }
  163.  
  164.    if not InputQuery( 'Align', 'Token', s2) then exit;
  165.  
  166.    OutputDebugString('s2 (token to align to): ' + s2, TRUE); { new value of s2}
  167.  
  168.    { locate the token to align to in each of the selected lines               }
  169.    { this will provide align-to-token column index                            }
  170.  
  171.    j := 0;
  172.    for i := p1.y to p2.y do
  173.    begin
  174.      s1 := Caller.Lines[i - 1];
  175.      k := pos(s2, s1);
  176.      if (k > j) then j := k;
  177.    end;
  178.  
  179.    OutputDebugString('j (token column number to align to): ' + IntToStr(j),
  180.                      TRUE);
  181.  
  182.    { if the token was not found, exit                                         }
  183.  
  184.    if j < 1 then exit;
  185.  
  186.    { align the tokens to the column                                           }
  187.  
  188.    for i := p1.y to p2.y do
  189.    begin
  190.      s1 := Caller.Lines[i - 1];
  191.      k := pos(s2, s1);
  192.      if (k > 0) and (k < j) then
  193.      begin
  194.        Caller.LogicalCaretXY := Point(k, i);
  195.        while k < j do
  196.        begin
  197.          ecChar(' ');        { add the necessary spaces to align the token    }
  198.          inc(k);
  199.        end;
  200.      end;
  201.    end;
  202. end.
  203.  
  204. // end of file
  205. // ----------------------------------------------------------------------------
  206.  

Notice that the first part of the macro is a copy of the OutputDebugString and Pause functions mentioned in a previous post.  Since there is no include facility in macros, the code needs to be copied and pasted into every macro you wish to debug.

After that, there is the function IsIdent which checks if its character parameter can be part of a valid Pascal identifier.  Note that the function does _not_ use a set because it would have been rather tedious and cumbersome to declare the set of valid characters one by one given that ".." is not available.

The macro requires the lines it will act upon to be selected.  SelAvail ensures there are lines selected.

p1 and p2 are set to the selection points with include (x,y) coordinates for each point.

A call to OutputDebugString shows the p1 and p2 coordinate.

After that, there are a good number of commented out lines.  It looks like, at one time, BlockBegin and BlockEnd might have been reversed depending on how the block of lines was selected (bottom to top instead of top to bottom.)  Testing shows that, at this time, BlockBegin is always above (or to the right) of  BlockEnd thereby making the code that flips the value unnecessary.

Next, the code prompts the user for the token to align to.  For variable declarations, that is a ":".

Once the token is known, a loop follows to determine the column number that will be used to align all the colons.

Once the column number to align to is known, a "for" loop inserts characters in the appropriate places to achieve the alignment.

Note that while the macro vertically aligns the colons, it does not align the beginning of the identifiers.   Another example will do that along with using the minimum amount of whitespace to accomplish the alignment.



additional example on next post...
(FPC v3.0.4 and Lazarus 1.8.2) or (FPC v3.2.2 and Lazarus v4.0rc3) on Windows 7 SP1 64bit.

440bx

  • Hero Member
  • *****
  • Posts: 5559
Re: Basics of writing SynEdit macros
« Reply #5 on: June 08, 2025, 08:09:55 pm »
Our second macro will be to count array elements.  All code is present in this post's attachment.  No need to copy/paste anything.

This macro is a slightly modified version of the one found at:
https://wiki.lazarus.freepascal.org/Editor_Macros_PascalScript

The code the macro will act upon is:
Code: Pascal  [Select][+][-]
  1. {$APPTYPE CONSOLE}
  2.  
  3. program _Macro_ArrayElementCount;
  4.  
  5.  
  6. const foo: array [1..] of integer = (
  7.   1, 2, 3,
  8.   4
  9. );
  10.  
  11.  
  12. (*
  13.  
  14. // save the original to make it easy to restore the test case.  Makes debugging
  15. // results repeatable and easier.
  16.  
  17. // the macro should be invoked with the caret sandwiched between the .. and ]
  18.  
  19.  
  20.  
  21. const foo: array [1..] of integer = (
  22.   1, 2, 3,
  23.   4
  24. );
  25.  
  26.  
  27. *)
  28.  
  29. // NOTE: the macro would be easier to write and the code simpler if some
  30. //       formatting rules were imposed on how the array should be
  31. //       formatted.  For instance, an array such as:
  32. //
  33. // const
  34. //   foo : array[1..] of anytype =
  35. //   (
  36. //    item1,
  37. //    item2,
  38. //    item3,
  39. //    < lots and  >
  40. //    < lots more >
  41. //    < items     >
  42. //   );
  43. //
  44. // not only makes the array definition easier to parse, it is also easier to
  45. // read.
  46. //
  47. // the array above follows these rules:
  48. //
  49. // 1. there are no intervening blank lines anywhere in the definition
  50. // 2. the open parenthesis is on a line by itself
  51. // 3. each item is on a separate line
  52. // 4. the closing parenthesis and ending semicolon are on a separate line
  53. // 5. if a comment is present it must follow the comma
  54. //
  55. // as far as rule #3, if the array has only a few elements then it would be
  56. // easy to make the macro smart enough to figure out that all elements are
  57. // in one line and parse accordingly.  It is worth noting that in this case
  58. // it is rather unlikely a macro is necessary to count the elements!
  59. //
  60. // it would also be fairly simple to remove rule #1 as long as any line that
  61. // has an element has a single element and the first token is the element
  62. // itself always followed by a comma unless it is the last element.
  63. // if there are no intervening blank lines and every element is on its own
  64. // line then a macro is not necessary to count the elements, simply place
  65. // the caret on the first line then the last line and the count is:
  66. // last - first + 1.  It would be very easy to write a macro to do that.
  67. //
  68. // rule #5 implies that anything after a comma is ignored because rule #3
  69. // means there cannot be another item on the line.
  70.  
  71. // formatting discipline goes a very LONG way into making the writing of macros
  72. // simpler and the macros more reliable.
  73.  
  74. begin
  75. end.
  76.  
  77.  
  78. // end of file
  79. // ----------------------------------------------------------------------------
  80.  

The macro that does the element counting is:
Code: Pascal  [Select][+][-]
  1.  
  2.  
  3. const                                         { COPY from HERE -------------- }
  4.   { debug message output control                                              }
  5.  
  6.   DEBUG_OUTPUT_MESSAGES = TRUE;
  7.   DEBUG_PAUSE           = TRUE;
  8.  
  9. procedure OutputDebugString
  10.             (
  11.              const InDebugString : string;
  12.              const InOutput      : boolean
  13.             );
  14.   { outputs a string if the InOutput parameter is TRUE and the setting of     }
  15.   { outputting debug messages is also TRUE.  if either one is FALSE, no       }
  16.   { output takes place                                                        }
  17.  
  18. begin
  19.   { if debug messages are globally suppressed, exit                           }
  20.  
  21.   if not DEBUG_OUTPUT_MESSAGES then exit;
  22.  
  23.   if not InOutput              then exit;    { this message is suppressed     }
  24.  
  25.   { neither globally nor specifically suppressed, emit the message            }
  26.  
  27.   ShowMessage(InDebugString);
  28. end;
  29.  
  30.  
  31. procedure Pause(const InPauseMessage : string);
  32.   { pauses execution by prompting the user to press any key.  No pausing will }
  33.   { occur if the global DEBUG_PAUSE is FALSE                                  }
  34.  
  35. var
  36.   InString : string;
  37.  
  38. begin
  39.   if not DEBUG_PAUSE then exit;
  40.  
  41.   if Trim(InPauseMessage) = '' then InPauseMessage := 'execution PAUSED';
  42.  
  43.   InputQuery(InPauseMessage, 'press ENTER/RETURN to continue', InString);
  44.  
  45. end; { ---------------------------------------- to HERE inclusive ----------- }
  46.  
  47.  
  48. procedure EmitCurrentCaretXY(const InShow : boolean);
  49. var
  50.   CurrentPt   : TPOINT;
  51.  
  52. begin
  53.   { unlike Synedit that allows empty () in calls, PascalScript does not allow }
  54.   { empty () in parameter-less calls.                                         }
  55.  
  56.   CurrentPt := Caller.CaretXY;       { note, no () in CaretXY call            }
  57.   with CurrentPt do
  58.   begin
  59.     OutputDebugString('CurrentPt.x: ' + IntToStr(x) + '    ' +
  60.                       'CurrentPt.y: ' + IntToStr(y),
  61.                       InShow);
  62.   end;
  63. end;
  64.  
  65. procedure EmitCaretPoint
  66.             (
  67.              const InPointName : string;
  68.              const InPoint     : TPOINT;
  69.              const InShow : boolean
  70.             );
  71. begin
  72.   with InPoint do
  73.   begin
  74.     OutputDebugString(InPointName + '.x: ' + IntToStr(x) + '    ' +
  75.                       InPointName + '.y: ' + IntToStr(y),
  76.                       InShow);
  77.   end;
  78. end;
  79.  
  80.  
  81. var
  82.   porigin, p1, p2    : TPOINT;
  83.   s                  : string;
  84.   i,       c1, c2, b : integer;
  85.   t                  : boolean;
  86.   a                  : char;
  87.  
  88. begin
  89.   porigin := Caller.CaretXy;    { column and line where the caret is located  }
  90.  
  91.   EmitCaretPoint('porigin', porigin, TRUE);
  92.  
  93.   { look for an open parenthesis starting at the current caret location       }
  94.  
  95.   ecIncrementalFind();          { can use () for parameter-less calls         }
  96.   ecChar('(');
  97.  
  98.   { after finding the open parenthesis, the caret is AFTER the parenthesis,   }
  99.   { ecLeft brings it just before the parenthesis                              }
  100.  
  101.   ecLeft;
  102.  
  103.   EmitCurrentCaretXY(TRUE);
  104.  
  105.  
  106.   i  :=     1;
  107.   c1 :=     0;
  108.   c2 :=     0;
  109.   b  :=     0;
  110.   t  := FALSE;
  111.  
  112.   repeat
  113.     p1 := Caller.CaretXy;
  114.  
  115.     EmitCaretPoint('p1', p1, TRUE);
  116.  
  117.     ecRight;
  118.  
  119.     EmitCurrentCaretXY(TRUE);
  120.  
  121.  
  122.     s := Caller.LineAtCaret;
  123.  
  124.     OutputDebugString('s: ' + s, TRUE);
  125.  
  126.     p2 := Caller.CaretXy;
  127.  
  128.     EmitCaretPoint('p2: ', p2, TRUE);
  129.  
  130.     if (p1.y = p2.y) and (p1.x = p2.x) then break;
  131.  
  132.     OutputDebugString('p2.x: ' + IntToStr(p2.x) + '    ' +
  133.                       'length(s): ' + IntToStr(Length(s)),
  134.                       TRUE);
  135.  
  136.     if (p2.x > length(s)) then
  137.     begin
  138.       OutputDebugString('p2.x > lenght(s)', TRUE);
  139.  
  140.       t := FALSE; // string ends in line
  141.       ecDown;
  142.       p2 := Caller.CaretXy;
  143.  
  144.       EmitCaretPoint('p1', p1, TRUE);
  145.       EmitCaretPoint('p2', p2, TRUE);
  146.  
  147.       if (p1.y = p2.y) then break;  // last line
  148.  
  149.       OutputDebugString('p1.y <> p2.y', TRUE);
  150.  
  151.       s := Caller.LineAtCaret;
  152.       while s = '' do
  153.       begin
  154.         p1 := Caller.CaretXy;
  155.         ecDown;
  156.         p2 := Caller.CaretXy;
  157.         if (p1.y = p2.y) then break;  // last line
  158.         s := Caller.LineAtCaret;
  159.       end;
  160.  
  161.       if s = '' then break;
  162.       Caller.CaretX := 1;
  163.       p2 := Caller.CaretXy;
  164.     end;
  165.  
  166.     { parse the array definition accounting for possible comments that may be }
  167.     { a part of it.                                                           }
  168.  
  169.     { it is accounting for possible comments that may appear just about       }
  170.     { anywhere that make this macro's implementation a bit complicated.       }
  171.  
  172.     if copy (s, p2.x, 2) = '//' then
  173.     begin
  174.       Caller.CaretX := length(s)+1;
  175.       continue;
  176.     end;
  177.  
  178.     a := s[p2.x];
  179.  
  180.     if copy (s, p2.x, 2) = '(*' then  a := '<';
  181.     if copy (s, p2.x, 2) = '*)' then  a := '>';
  182.  
  183.     if (t) and (copy (s, p2.x, 2) = '''''') then
  184.     begin
  185.       a := ' ';
  186.       ecRight;;
  187.     end;
  188.  
  189.     case a of
  190.       '{' : if (c2 = 0) and (not t)                  then c1 := c1 + 1;
  191.       '}' : if (c2 = 0) and (c1 > 0) and (not t)     then c1 := c1 - 1;
  192.       '<' : if (c1 = 0) and (not t)                  then c2 := c2 + 1;
  193.       '>' : if (c1 = 0) and (c2 > 0) and (not t)     then c2 := c2 - 1;
  194.       '''': if (c1 = 0) and (c2 = 0)                 then  t := not t;
  195.       '(', '[': if (c1 = 0) and (c2 = 0) and (not t) then  b := b + 1;
  196.  
  197.       ')', ']': if (c1 = 0) and (c2 = 0) and (not t) then
  198.       begin
  199.         b := b - 1;
  200.         if b < 0 then break;
  201.       end;
  202.  
  203.       { i has the number of array items, we found a comma and it is not       }
  204.       { part of a comment                                                     }
  205.  
  206.       ',': if (c1 = 0) and (c2 = 0) and (not t)      then  i := i + 1;
  207.     end;
  208.  
  209.   until FALSE;
  210.  
  211.   Caller.CaretXy := porigin;
  212.   Caller.InsertTextAtCaret(inttostr(i), scamAdjust)
  213. end.
  214.  
  215. // end of file
  216. // ----------------------------------------------------------------------------
  217.  

The macro requires the caret to be located between the ".." and "]" before being run/played. 

First thing is to notice that the macro starts with a copy of the debugging functions defined in a previous post.  In addition to that, it defines another 2 debugging helper functions EmitCurrentCaretXY and EmitCaretPoint because the macro does a lot of caret manipulation.

The most important part of this example is that its function could have been accomplished with more ease should a few requirements on how the array is formatted been established.

This macro uses very few SynEdit and PascalScript functions, yet it is not easy to follow because it caters to situations that can easily be prevented by establishing a few requirements up front.

The "ideal" requirements are stated in the code the macro acts upon and for this reason they will not be repeated here.

Also, the attachment has the DEBUG_OUTPUT_MESSAGES global set to FALSE because the loops cause a fair number of messages to be output.  Consider setting it to TRUE after running the macro once.

The lesson to learn from this example is: a macro can be much more complicated than it really needs to be because it attempts to deal with too many situations, many of them uncommon.




additional example on next post...
« Last Edit: June 09, 2025, 02:10:10 am by 440bx »
(FPC v3.0.4 and Lazarus 1.8.2) or (FPC v3.2.2 and Lazarus v4.0rc3) on Windows 7 SP1 64bit.

440bx

  • Hero Member
  • *****
  • Posts: 5559
Re: Basics of writing SynEdit macros
« Reply #6 on: June 08, 2025, 08:40:10 pm »

This is the last example.  This macro packs a punch!

The purpose of this macro is to convert a Windows C API definition into a Pascal API definition.  In other words, the macro converts this:
Code: C  [Select][+][-]
  1.   HANDLE
  2.   IMAGEAPI
  3.   SymFindDebugInfoFile(
  4.       _In_ HANDLE hProcess,
  5.       _In_ PCSTR FileName,
  6.       _Out_writes_(MAX_PATH + 1) PSTR DebugFilePath,
  7.       _In_opt_ PFIND_DEBUG_FILE_CALLBACK Callback,
  8.       _In_opt_ PVOID CallerData
  9.       );
  10.  

into this:

Code: Pascal  [Select][+][-]
  1. function SymFindDebugInfoFile
  2.            (
  3.             { _in_     } InhProcess       : THANDLE;
  4.             { _in_     } InFileName       : pchar;
  5.             { _out_    } OutDebugFilePath : pchar;
  6.             { _in_opt_ } InoptCallback    : PFIND_DEBUG_FILE_CALLBACK;
  7.             { _in_opt_ } InoptCallerData  : pointer
  8.            )
  9.          : THANDLE; stdcall; external ImageHlp;
  10.  

In a single hotkey!

It's important to note that the C definition is exactly as it appears in the C .h file.  IOW, it is a straight, unmodified copy/paste from the .h file.

The Pascal definition is the exact, unmodified, macro output.

Basically, this macro is a "pocket" H2Pas program ;) (for API definitions only!)

The code the macro acts on is:
Code: Pascal  [Select][+][-]
  1. {$APPTYPE CONSOLE}
  2.  
  3. program Macro_C_definition_to_Pascal_definition;
  4.  
  5. (*
  6.  
  7.  
  8.   HANDLE
  9.   IMAGEAPI
  10.   SymFindDebugInfoFile(
  11.       _In_ HANDLE hProcess,
  12.       _In_ PCSTR FileName,
  13.       _Out_writes_(MAX_PATH + 1) PSTR DebugFilePath,
  14.       _In_opt_ PFIND_DEBUG_FILE_CALLBACK Callback,
  15.       _In_opt_ PVOID CallerData
  16.       );
  17.  
  18.        
  19.  
  20.  
  21. *)
  22.  
  23.  
  24. { internal intermediate products                                                 }
  25.  
  26. {  array 1st column                 array 2nd column            array 3rd column }
  27. {  vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv   vvvvvvvvvvvvvvvvvvvvvvvvv   vvvvvvvvvvvvvvvv }
  28.  
  29. // HANDLE
  30. // SymFindDebugInfoFile
  31. //     _In_                         HANDLE                      hProcess
  32. //     _In_                         PCSTR                       FileName
  33. //     _Out_writes_(MAX_PATH + 1)   PSTR                        DebugFilePath
  34. //     _In_opt_                     PFIND_DEBUG_FILE_CALLBACK   Callback
  35. //     _In_opt_                     PVOID                       CallerData
  36.  
  37.  
  38. {  array 1st column                 array 2nd column            array 3rd column }
  39. {  vvvvvvvvvvvvvvvvvvvvvvvvvvvvvv   vvvvvvvvvvvvvvvvvvvvvvvvv   vvvvvvvvvvvvvvvv }
  40.  
  41. // HANDLE
  42. // SymFindDebugInfoFile
  43. //     _In_                         HANDLE                      hProcess
  44. //     _In_                         PCSTR                       FileName
  45. //     _Out_                        PSTR                        DebugFilePath
  46. //     _In_opt_                     PFIND_DEBUG_FILE_CALLBACK   Callback
  47. //     _In_opt_                     PVOID                       CallerData
  48.  
  49. { the final product, a Pascal API definition, is created using the tokens        }
  50. { from the above intermediate product.                                           }
  51.  
  52.  
  53. begin
  54. end.
  55.  

This macro has a few requirements to operate properly:

1 - the definition must be preceded by a blank line
2 - the definition must be followed by a blank line
3 - the first definition line must be the function's return value and it must be a single word/token
4 - the second line must be the calling convention.  that line is IGNORED, this macro assumes the calling convention is ALWAYS stdcall.  if it is not, the resulting Pascal definition will need to be manually modified to specify the correct convention.

5 - the third line must have the function name immediately followed by an open parenthesis (no space between the name and the parenthesis)

6 - the entire C definition is contiguous, in other words, there are no blank lines in it
7 - parameter definitions are always contained in a single line

8 - parameter lines consist of 3 pieces, the first piece is Source-code Annotation Language (SAL) whose first character must be an underscore, the second piece must be a SINGLE WORD type identifier, the third piece must be a parameter name.

9 - the C definition must end with ");" by itself on a single line

The great majority of MS C Windows definitions meet the above requirements.


VERY IMPORTANT:

the macro does NOT check that the above conditions are met. it is the programmer's responsibility to ensure they are met.

the dll name associated with the definition is HARD CODED in the macro, this means the macro needs to be manually modified for API definitions in a different dll.  Modify the EXPORT_DLL variable which holds the dll name. 

ALSO it does NOT check that SAL strings, types and identifiers are valid. garbage-in ensures garbage-out if the macro works at all.

NOTE:

Including the macro's code in this post caused the post limit of 20,000 characters to be exceeded.  For that reason, the macro's code will be in the next post.



continued on next post...
(FPC v3.0.4 and Lazarus 1.8.2) or (FPC v3.2.2 and Lazarus v4.0rc3) on Windows 7 SP1 64bit.

440bx

  • Hero Member
  • *****
  • Posts: 5559
Re: Basics of writing SynEdit macros
« Reply #7 on: June 08, 2025, 09:30:36 pm »
The macro expects the caret to be _anywhere within_ the C definition when it is run/played. IOW, anyplace in lines 8 through 16.

The macro is simply too large to be in a single post.  For this reason, it will be presented in parts to make it acceptable to the forum software.

Macro's mainline:
Code: Pascal  [Select][+][-]
  1.  
  2. { --------------------------------------------------------------------------- }
  3. { MAINLINE                                            ----------------------- }
  4.  
  5. begin
  6.   { for reference purposes, emit the number of lines in the source file       }
  7.  
  8.   OutputDebugString('LinesCount: ' + IntToStr(Caller.LinesCount),
  9.                     FALSE);
  10.  
  11.   InitializeArrays();                    { initialize the arrays              }
  12.   InitializeGlobalVariables();
  13.  
  14.   DetermineDefinitionBeginEnd();
  15.   RestoreCaretToOriginalLocation();
  16.   AddLinesForPascalDefinition();
  17.   SaveCdefinitionLines();
  18.   RemoveTrailingOpenParenthesisAndCommas();
  19.   TokenizeCdefinition();
  20.  
  21.   { make any necessary adjustments to the disposition strings                 }
  22.  
  23.   RemovedUnwantedStringsFromDispositions();
  24.  
  25.   DetermineProcedureOrFunction();
  26.  
  27.   EmitApiName();
  28.   EmitOpenParenthesis();
  29.   DetermineWidestDispositionString();
  30.   EmitDispositions();
  31.   DetermineWidestParameterName();
  32.   EmitParameterNames();
  33.   EmitParameterTypes();
  34.   EmitClosingParenthesis();
  35.   EmitFunctionResult();
  36.  
  37. end.
  38.  

two arrays hold the information needed to go from a C definition to a Pascal definition, they are:
Code: Pascal  [Select][+][-]
  1.  
  2. const
  3.   DEF_LINES_LO         =  1;
  4.   DEF_LINES_HI         = 64;
  5.  
  6. var
  7.   DefinitionLines      : array[DEF_LINES_LO..DEF_LINES_HI] of string;
  8.   DefinitionLinesCount : integer;
  9.  
  10. var
  11.   { this is the above array where each line has been broken into tokens       }
  12.  
  13.   TokenizedLines       : array[DEF_LINES_LO..DEF_LINES_HI]
  14.                             of record
  15.     FunctionResultType           : string;  { index 1 only                    }
  16.     FunctionCallingConvention    : string;  { index 2 only                    }
  17.     FunctionName                 : string;  { index 3 only                    }
  18.  
  19.     { indexes 4 and greater only                                              }
  20.  
  21.     ParmDisposition              : string;  { e.g, _in_opt_                   }
  22.     ParmDispositionPrefix        : string;  { e.g, Inopt                      }
  23.  
  24.     ParmType                     : string;
  25.     ParmName                     : string;
  26.   end;
  27.  
InitializeArrays sets all the strings in the arrays to empty.

The DefinitionLines array holds the C definition, that is, lines 8 through 16.

The TokenizedLines array holds the C definition broken into pieces.  Specifically, the function result type, the function calling convention, the function name and, for each parameter, the parameter disposition (i.e, _in_, _out_, _in_opt_, etc), the parameter type and the parameter name.

In a "normal" Pascal program, some of these fields would be in a variant/union because the first definition line, has the result type and nothing else, i.e, no calling convention, function name, parameter info, etc.  Because of limitations in PascalScript, there is no variant.  This means that line 1 in the TokenizedLines array holds the FunctionResultType and nothing else.  Line 2 holds the calling convention and nothing else.  For each parameter, the FunctionResultType, CallingConvention and Name are empty strings.

Obviously this wastes some memory but, keeps the code simple and _simplicity_ is absolutely necessary for a complex macro to operate properly.

The initialization code is trivial:
Code: Pascal  [Select][+][-]
  1.  
  2. { --------------------------------------------------------------------------- }
  3.  
  4. procedure InitializeArrays();
  5.   { initializes the Definition lines array and the Tokenized lines array      }
  6. var
  7.   i       : integer;
  8.  
  9. begin
  10.   for i := DEF_LINES_LO to DEF_LINES_HI do
  11.   begin
  12.     DefinitionLines[i] := '';
  13.  
  14.     with TokenizedLines[i] do
  15.     begin
  16.       FunctionResultType        := '';
  17.       FunctionCallingConvention := '';
  18.       FunctionName              := '';
  19.  
  20.       ParmDisposition           := '';
  21.       ParmType                  := '';
  22.       ParmName                  := '';
  23.     end;
  24.   end;
  25. end;
  26.  
  27. { --------------------------------------------------------------------------- }
  28.  
  29. procedure InitializeGlobalVariables();
  30. begin
  31.   NewLineString := #13#10#0;
  32.  
  33.   { saving the caret's original location isn't needed in this macro but, may  }
  34.   { be needed in other macros                                                 }
  35.  
  36.   with OriginalCaret do
  37.   begin
  38.     x := Caller.CaretX;
  39.     y := Caller.CaretY;
  40.   end;
  41.  
  42.   OutputDebugString('X: ' + IntToStr(OriginalCaret.X) +
  43.                     'Y: ' + IntToStr(OriginalCaret.Y),
  44.                     FALSE);
  45. end;
  46.  

Note that there are quite a few more global variables but those are initialized in the functions/procedures that use them.

After initialization, the macro determines where the C API defintion starts and ends.
Code: Pascal  [Select][+][-]
  1.  
  2. procedure DetermineDefinitionBeginEnd();
  3. begin
  4.   { locate the definition's top line                                          }
  5.  
  6.   for i := OriginalCaret.Y downto 0 do  { search towards the top of the file  }
  7.   begin
  8.     { proceed backwards from the current caret line up to locate the first    }
  9.     { definition line.  The first definition line is the one that follows a   }
  10.     { blank line                                                              }
  11.  
  12.     { VERY IMPORTANT: the Lines array is zero based while the coordinates are }
  13.     {                 1 based, to make them be in synch, it is necessary to   }
  14.     {                 subtract 1 from the coordinate.  IOW, Y = 1 is line 0   }
  15.  
  16.     { ALSO: don't rely on the loop index having a reliable value when exiting }
  17.  
  18.     FirstLine := i;
  19.     if Trim(Caller.Lines[i - 1]) = '' then
  20.     begin
  21.       break;
  22.    end;
  23.   end;
  24.  
  25.   { we exited when we found an empty line, therefore the first line is the    }
  26.   { line that follows it.                                                     }
  27.  
  28.   inc(FirstLine);     // 1 based
  29.  
  30.   OutputDebugString('First line: ' + IntToStr(FirstLine),
  31.                     TRUE);
  32.  
  33.   { now we need to find the last line                                         }
  34.  
  35.   for i := OriginalCaret.Y to Caller.LinesCount - 1 do
  36.   begin
  37.     LastLine := i;
  38.     if Trim(Caller.Lines[i - 1]) = '' then
  39.     begin
  40.       break;
  41.     end;
  42.   end;
  43.  
  44.   dec(LastLine);      // 1 based
  45.  
  46.   OutputDebugString('Last line: ' + IntToStr(LastLine),
  47.                     TRUE);
  48. end;
  49.  

The macro figures out where the definition starts and ends by looking for an empty line going up from the caret then going down from the caret.  It save the 1 based line index, to make it usable in Caret functions, in the FirstLine and LastLine globals.

Just to be tidy, the macro puts the caret back in its original location.  This isn't really needed but, it's tidy.

The following step is one I recommend be done in all macros whenever possible, which is: do not modify existing lines, instead create new lines that will hold the result of the macro.  That way, the original text isn't damaged by a malfunctioning macro.

Following the above rule, the macro proceeds to add lines that will be used to hold its result (the Pascal definition). 

Code: Pascal  [Select][+][-]
  1. procedure AddLinesForPascalDefinition();
  2. begin
  3.   { now add the lines that will hold the API's Pascal definition              }
  4.  
  5.   LastPt.Y := LastLine + 1;                           { line below last line  }
  6.   LastPt.X := length(Caller.Lines[LastLine - 1]) + 1; { + 1 = past eol        }
  7.  
  8.   Caller.MoveCaretIgnoreEOL(LastPt);  // presume it's the physical caret
  9.  
  10.   AddedLinesCount := (LastLine - FirstLine) + 1
  11.                    + SEPARATING_LINES_COUNT;
  12.  
  13.   { the + 1 is because the caret is currently on the last line                }
  14.  
  15.   for i := 1 to AddedLinesCount do
  16.   begin
  17.     Caller.InsertTextAtCaret(NewLineString, scamEnd);
  18.  
  19.     inc(LastPt.Y);
  20.     Caller.MoveCaretIgnoreEOL(LastPt);  // presume it's the physical caret
  21.   end;
  22.  
  23.   OutputDebugString('Added ' + IntToStr(AddedLinesCount) + ' lines.',
  24.                     TRUE);
  25. end;
  26.  

After adding lines for the Pascal definition, save the C definition lines into its own array.

Code: Pascal  [Select][+][-]
  1. procedure SaveCdefinitionLines();
  2.   { saves the C definition lines into the Definition lines array              }
  3.  
  4. var
  5.   i       : integer;
  6.  
  7. begin
  8.   { save the definition lines in the definition lines array                   }
  9.  
  10.   DefinitionLinesCount := 0;
  11.  
  12.   for i := FirstLine - 1 to LastLine - 1 do
  13.   begin
  14.     inc(DefinitionLinesCount);
  15.  
  16.     DefinitionLines[DefinitionLinesCount] := Caller.Lines[i];
  17.  
  18.     OutputDebugString('Definition line[' + IntToStr(DefinitionLinesCount) + '] = ' +
  19.                       DefinitionLines[DefinitionLinesCount],
  20.                       FALSE);
  21.   end;
  22. end;
  23.  

At this point it should be pointed out that every function/procedure is very simple.  Keep that in mind because that is key to create complex macros that work.

Now that the C definition is safely stored in its array, proceed to "massage" it.  First step is to remove trailing open parentheses (only 1) and commas (usually more than 1).
Code: Pascal  [Select][+][-]
  1.  
  2. procedure RemoveTrailingOpenParenthesisAndCommas();
  3.   { removes trailing open parenthesis and commas in the C definition lines.   }
  4.   { this macro assumes that the C function name is _immediately_ followed by  }
  5.   { an open parenthesis (that is, the open parenthesis is _not_ on a separate }
  6.   { line.  It also assumes that all parameters, except the last one, is       }
  7.   { immediately followed by a comma.  if those assumptions/requirements are   }
  8.   { not met, the behavior of this macro is unpredictable                      }
  9.  
  10. var
  11.   i       : integer;
  12.  
  13. begin
  14.   { remove any trailing open parenthesis and trailing comma that may be in    }
  15.   { the line                                                                  }
  16.  
  17.   for i := 1 to DefinitionLinesCount  do
  18.   begin
  19.     OutputDebugString('Last character of Definition line[' + IntToStr(i) + '] = ' +
  20.                       DefinitionLines[i][length(DefinitionLines[i])],
  21.                       FALSE);
  22.  
  23.     if (DefinitionLines[i][length(DefinitionLines[i])] = ',')   or
  24.        (DefinitionLines[i][length(DefinitionLines[i])] = '(') then
  25.     begin
  26.       { delete the comma or open parenthesis                                  }
  27.  
  28.       Delete(DefinitionLines[i], length(DefinitionLines[i]), 1);
  29.  
  30.       OutputDebugString('Last character of Definition line['          +
  31.                         IntToStr(i) + '] = '                          +
  32.                         DefinitionLines[i][length(DefinitionLines[i])],
  33.                         FALSE);
  34.     end;
  35.   end;
  36. end;
  37.  


Time to start another post before the forum software starts complaining again about exceeding the 20,000 character limit.



continued on next post...

ETA:

Added the missing AddLinesForPascalDefinition() code.
« Last Edit: June 09, 2025, 02:26:28 am by 440bx »
(FPC v3.0.4 and Lazarus 1.8.2) or (FPC v3.2.2 and Lazarus v4.0rc3) on Windows 7 SP1 64bit.

440bx

  • Hero Member
  • *****
  • Posts: 5559
Re: Basics of writing SynEdit macros
« Reply #8 on: June 09, 2025, 12:42:25 am »
continued from previous post...

repost of the mainline for reference purposes, functions and procedures already discussed have been removed.

Code: Pascal  [Select][+][-]
  1. { --------------------------------------------------------------------------- }
  2. { MAINLINE                                            ----------------------- }
  3.  
  4. begin
  5.   < snip >
  6.  
  7.   RemoveTrailingOpenParenthesisAndCommas();
  8.   TokenizeCdefinition();
  9.  
  10.   { make any necessary adjustments to the disposition strings                 }
  11.  
  12.   RemovedUnwantedStringsFromDispositions();
  13.  
  14.   DetermineProcedureOrFunction();
  15.  
  16.   EmitApiName();
  17.   EmitOpenParenthesis();
  18.   DetermineWidestDispositionString();
  19.   EmitDispositions();
  20.   DetermineWidestParameterName();
  21.   EmitParameterNames();
  22.   EmitParameterTypes();
  23.   EmitClosingParenthesis();
  24.   EmitFunctionResult();
  25.  
  26. end.
  27.  

After removing the open parenthensis and trailing commas, the C definition is clean enough to be tokenized.

the TokenizeCdefinition procedure code is:

Code: Pascal  [Select][+][-]
  1. procedure TokenizeCdefinition();
  2.   { tokenizes the C definition.  It does so per line and the definition MUST  }
  3.   { be structured a specific way for the tokenization to be correct.          }
  4. var
  5.   i       : integer;
  6.  
  7. begin
  8.   OutputDebugString('ENTERED TokenizeCdefinition();', FALSE);
  9.  
  10.   for i := 1 to DefinitionLinesCount  do
  11.   begin
  12.     OutputDebugString('i: ' + IntToStr(i) + ' of ' + IntToStr(DefinitionLinesCount),
  13.                       FALSE);
  14.     case i of
  15.       1 : begin
  16.             TokenizedLines[i].FunctionResultType        := Trim(DefinitionLines[i]);
  17.             OutputDebugString(TokenizedLines[i].FunctionResultType,        FALSE);
  18.           end;
  19.  
  20.       2 : begin
  21.             TokenizedLines[i].FunctionCallingConvention := Trim(DefinitionLines[i]);
  22.             OutputDebugString(TokenizedLines[i].FunctionCallingConvention, FALSE);
  23.           end;
  24.  
  25.       3 : begin
  26.             TokenizedLines[i].FunctionName              := Trim(DefinitionLines[i]);
  27.             OutputDebugString(TokenizedLines[i].FunctionName,              FALSE);
  28.           end;
  29.       else
  30.       begin
  31.         { if the last character in the line is a semicolon then it should be  }
  32.         { the closing parenthesis followed by a semicolon ");" which is a     }
  33.         { line we are not interested in.                                      }
  34.  
  35.         if DefinitionLines[i][length(DefinitionLines[i])] = ';' then
  36.         begin
  37.           OutputDebugString('); encountered. loop exited', FALSE);
  38.  
  39.           break;
  40.         end;
  41.  
  42.         { this is a parameter line, which consists of 3 parts.  break the     }
  43.         { line into its 3 constituent parts that are separated by spaces      }
  44.  
  45.         TokenEnd   := length(DefinitionLines[i]);
  46.         TokenStart := length(DefinitionLines[i]);
  47.  
  48.         TokenIndex := 3;   { index of parameter name                          }
  49.  
  50.         while TokenStart > 0 do
  51.         begin
  52.           if DefinitionLines[i][TokenStart] <> ' ' then
  53.           begin
  54.             dec(TokenStart);
  55.  
  56.             continue;
  57.           end;
  58.  
  59.           { we are here because TokenEnd is pointing to a space, save the     }
  60.           { token in the appropriate string                                   }
  61.  
  62.           case TokenIndex of
  63.             3 : begin
  64.                   TokenizedLines[i].ParmName := Copy(DefinitionLines[i],
  65.                                                      TokenStart + 1,
  66.                                                      TokenEnd - TokenStart);
  67.  
  68.                   OutputDebugString('Parameter name: ' +
  69.                                     TokenizedLines[i].ParmName,
  70.                                     FALSE);
  71.  
  72.                   { consume all the spaces                                    }
  73.  
  74.                   while DefinitionLines[i][TokenStart] = ' ' do
  75.                   begin
  76.                     dec(TokenStart);
  77.                   end;
  78.  
  79.                   TokenEnd := TokenStart; { first non-space character         }
  80.  
  81.                   dec(TokenIndex);      { next token is the parameter type    }
  82.                 end;
  83.  
  84.             2 : begin
  85.                   TokenizedLines[i].ParmType := Copy(DefinitionLines[i],
  86.                                                      TokenStart + 1,
  87.                                                      TokenEnd - TokenStart);
  88.  
  89.                   OutputDebugString('Parameter type: ' +
  90.                                     TokenizedLines[i].ParmType,
  91.                                     FALSE);
  92.  
  93.                   { consume all the spaces                                    }
  94.  
  95.                   while DefinitionLines[i][TokenStart] = ' ' do
  96.                   begin
  97.                     dec(TokenStart);
  98.                   end;
  99.  
  100.                   TokenEnd := TokenStart;
  101.  
  102.                   dec(TokenIndex);      { next token is the parameter type    }
  103.  
  104.                 end;
  105.  
  106.             1 : begin
  107.                   TokenizedLines[i].ParmDisposition := Copy(DefinitionLines[i],
  108.                                                        TokenStart + 1,
  109.                                                        TokenEnd - TokenStart);
  110.  
  111.                   { we have to account for the possibility that the           }
  112.                   { disposition may include additional information we are not }
  113.                   { interested in.  In those cases the last character of the  }
  114.                   { disposition is _not_ an underscore (as it normally is)    }
  115.  
  116.                   with TokenizedLines[i] do
  117.                   begin
  118.                     if ParmDisposition[length(ParmDisposition)] <> '_' then
  119.                     begin
  120.                       { locate the last underscore in the disposition         }
  121.  
  122.                       LastUnderscore := LocateLastUnderscore(DefinitionLines[i]);
  123.  
  124.                       OutputDebugString('Last underscore index: ' +
  125.                                         IntToStr(LastUnderscore),
  126.                                         FALSE);
  127.  
  128.                       { string indexes are 1 based                            }
  129.  
  130.                       ParmDisposition := Copy(DefinitionLines[i],
  131.                                               1,                {  1 based    }
  132.                                               LastUnderscore);
  133.                     end;
  134.  
  135.                     { we want the disposition in lowercase                    }
  136.  
  137.                     ParmDisposition := Lowercase(ParmDisposition);
  138.                     ParmDisposition := Trim(ParmDisposition);
  139.                   end;
  140.  
  141.                   OutputDebugString('Parameter disposition: ' +
  142.                                     TokenizedLines[i].ParmDisposition,
  143.                                     FALSE);
  144.  
  145.                   { consume all the spaces                                    }
  146.  
  147.                   while (TokenStart                     > 0)   and
  148.                         (DefinitionLines[i][TokenStart] = ' ')  do
  149.                   begin
  150.                     dec(TokenStart);
  151.  
  152.                     OutputDebugString('TokenStart: ' +
  153.                                       IntToStr(TokenStart),
  154.                                       FALSE);
  155.                   end;
  156.  
  157.                   { we are done processing this parameter line                }
  158.  
  159.                   TokenStart := 0;
  160.                 end;
  161.           end; { case TokenIndex of }
  162.  
  163.         end; { for rsi }
  164.       end; { else : definition line greater than 3 }
  165.     end; { case i of }
  166.   end; { for i := 1 to DefinitionLinesCount  do }
  167.  
  168.   OutputDebugString('EXITED  TokenizeCdefinition();', FALSE);
  169. end;
  170.  
The first 3 lines are trivial. The first line is a single token that is the function result.  The second line is also a single token that is the calling convention (which this macro doesn't use), the third line is also a single token (the open parenthesis has been removed) which is the function name.  All that is needed is to trim each string to have the proper token.

Once the first three lines are scanned, the parameter lines need to be scanned.

Parameter lines are scanned from right to left.  IOW, from the end towards the beginning because that makes it much simpler.

The last identifier on a parameter line is the C parameter name.  All that is needed is to scan towards the beginning of the line until a space is found.  Once the space is found, the parameter name is simply the string from (space + 1) to the end of the line.   This is taken care of by the Copy function, see line 64.

the type identifier precedes the parameter name.  Finding its ending position is a simple matter of consuming all the spaces between the parameter name and the parameter type.  Once that X coordinate (TokenEnd) is known then scanning backwards until a space is found gives the X coordinate of the starting position (TokenStart) which is used by the Copy function (see line 85) to save the parameter type.

determining the parameter disposition follows the same process but, requires one additional step.  Sometimes the disposition will have parts that are not of interest, in this example, the third parameter contains "(MAX_PATH + 1)" which we are _not_ interested in.  We remove that portion by determining the X coordinate of the last underscore, once that is determined, again a copy (see line 130) saves the disposition.

What's notable about this procedure is that two functions, Trim and Copy, were all that's needed to break the lines into tokens.   Part of the reason for the simplicity is that the function was not acting on text, instead it was acting on strings in an array.

The TokenizeCdefinition may still leave some unwanted characters in the disposition.  In this example, while the procedure was able to remove the unwanted "(MAX_PATH + 1)", it left the "writes_" which is not part of the parameter disposition.  Removing such unwanted parts is done by RemovedUnwantedStringsFromDispositions() and a helper function.  They are:

Code: Pascal  [Select][+][-]
  1. const
  2.   PARAMETERS_MIN_INDEX         = 4;
  3.  
  4. const
  5.   { strings to be removed from the ParmDisposition                            }
  6.  
  7.   UNWANTED_DISPOSITION_STRING_01 = 'writes_';
  8.  
  9.  
  10. function UnwantedStrings
  11.            (
  12.             const InString  : string;
  13.               var OutPos    : integer;
  14.               var OutLength : integer
  15.            )
  16.          : boolean;
  17.   { determines if the InString parameter contains one of the undesirable      }
  18.   { disposition strings                                                       }
  19.  
  20. begin
  21.   result    := TRUE;     { presume there is an undesirable string             }
  22.  
  23.   OutPos    :=    0;
  24.   OutLength :=    0;
  25.  
  26.   OutPos := Pos(UNWANTED_DISPOSITION_STRING_01, InString);
  27.  
  28.   if OutPos <> 0 then
  29.   begin
  30.     OutLength := length(UNWANTED_DISPOSITION_STRING_01);
  31.     exit;
  32.   end;
  33.  
  34.   result := FALSE;       { we get here when none of the strings were found    }
  35. end;
  36.  
  37. {                                                    ------------------------ }
  38.  
  39. procedure RemovedUnwantedStringsFromDispositions();
  40.  
  41.   { removes unwanted strings that may appear in the parameter disposition     }
  42.   { ParmDisposition                                                           }
  43.  
  44. var
  45.   i   : integer;     { index into TokenizedLines array                        }
  46.  
  47.   p   : integer;     { position of unwanted string                            }
  48.   l   : integer;     { length   of     "      "                               }
  49.  
  50. begin
  51.   { used a while instead of a "for" because using the index "i" caused        }
  52.   { problems even while still inside the "for" loop.                          }
  53.  
  54.   for i := PARAMETERS_MIN_INDEX to DefinitionLinesCount do
  55.   begin
  56.     with TokenizedLines[i] do
  57.     begin
  58.       { look for unwanted strings in the parameter disposition                }
  59.  
  60.       if UnwantedStrings(ParmDisposition, p, l) then
  61.       begin
  62.         OutputDebugString('i: ' + IntToStr(i) + '    ' +
  63.                           'p: ' + IntToStr(p) + '    ' +
  64.                           'l: ' + IntToStr(l),
  65.                           FALSE);
  66.  
  67.         OutputDebugString('Parameter disposition: '                           +
  68.                           TokenizedLines[i].ParmDisposition,
  69.                           FALSE);
  70.  
  71.         Delete(TokenizedLines[i].ParmDisposition, p, l);
  72.  
  73.         OutputDebugString('Parameter disposition: '                           +
  74.                           TokenizedLines[i].ParmDisposition,
  75.                           FALSE);
  76.       end;
  77.     end; { with TokenizedLines[i] do }
  78.   end; { for }
  79. end;
  80.  

The "UnwantedStrings" function is called by RemovedUnwantedStringsFromDispositions() to determine if the disposition contains an unwanted string and, if it does, its position and length in the string.  With that information, the unwanted string is Delete-ed from the disposition.

Note that using FPC instead of PascalScript, it would be quite simple and straightforward to consolidate these 2 routines but, limitations such as not being able to declare typed constants (to create an array of unwanted strings) and PascalScript's problem with "break" and "continue" in nested loops required breaking the logic into 2 routines.

In spite of that, it is worth noting again that the PascalScript's Pos and Delete functions were all that was needed to get the task done.

Now that the C definition is tokenized, the last piece of information needed before we can proceed to build/emit the Pascal definition, is to determine if the C definition is a function or a procedure.  This is necessary because a procedure only needs a semicolon after the closing parenthesis whereas a function requires a colon and a result type before emitting the semicolon. 

Making that determinatioin is "DetermineProcedureOrFunction()" purpose. 

Code: Pascal  [Select][+][-]
  1. procedure DetermineProcedureOrFunction();
  2.   { determines if the API definitions is that of a function or a procedure  }
  3.  
  4. begin
  5.   FunctionApi := TRUE;    { presume it is a function                        }
  6.  
  7.    if Uppercase(TokenizedLines[FUNCTION_RESULT_INDEX].FunctionResultType) = 'VOID' then
  8.   begin
  9.     { presume that procedures are declared as void functions                }
  10.  
  11.     FunctionApi := FALSE;
  12.     exit;
  13.   end;
  14. end;
  15.  

All that's needed to implement that function is to use PascalScript's Uppercase function.

Now we have all the information necessary to start emitting the Pascal definition.

First, we emit the function name, using EmitApiName()

Code: Pascal  [Select][+][-]
  1. var
  2.   { the Pascal API output is relative to the LastLine                         }
  3.  
  4.   FirstLine            : integer;
  5.   LastLine             : integer;
  6.  
  7. const
  8.   { number of lines we'll use to separate the C definition from the Pascal    }
  9.   { definition                                                                }
  10.  
  11.   SEPARATING_LINES_COUNT    = 2;
  12.  
  13.   API_NAME_LINE             = 1;
  14.   API_OPEN_PARENTHESIS_LINE = 2;
  15.   API_FIRST_PARAMETER_LINE  = 3;
  16.  
  17.   FUNCTION_TOKEN            = 'function ';
  18.   PROCEDURE_TOKEN           = 'procedure ';
  19.  
  20. procedure EmitApiName();
  21.   { outputs the function/procedure name line                                  }
  22.  
  23. var
  24.   OutputPt  : TPOINT;
  25.  
  26. begin
  27.   with OutputPt do
  28.   begin
  29.     x := 1;
  30.     y := LastLine + SEPARATING_LINES_COUNT + API_NAME_LINE;
  31.   end;
  32.  
  33.   Caller.MoveCaretIgnoreEOL(OutputPt);
  34.  
  35.   case FunctionApi of
  36.     TRUE:
  37.     begin
  38.       Caller.InsertTextAtCaret(FUNCTION_TOKEN, scamEnd);
  39.     end;
  40.  
  41.     FALSE:
  42.     begin
  43.       Caller.InsertTextAtCaret(PROCEDURE_TOKEN, scamEnd);
  44.     end;
  45.   end;
  46.  
  47.   { now put the function name                                                 }
  48.  
  49.   Caller.InsertTextAtCaret(TokenizedLines[FUNCTION_NAME_INDEX].FunctionName,
  50.                            scamEnd);
  51. end; { procedure EmitApiName(); }
  52.  

The notable characteristic about the Emit... family of functions is that they use PascalScript's functions   Caller.MoveCaretIgnoreEOL and Caller.InsertTextAtCaret to place the caret where needed and then put text at that location.  Every line is identified using a mix of the variable LastLine (of C definition) + the number of separating lines from the C definition to the Pascal definition + a constant that identifies the particular line that is being emitted.

Generally speaking, the Emit... family a function all have a similar pattern.



continued on next post...
(FPC v3.0.4 and Lazarus 1.8.2) or (FPC v3.2.2 and Lazarus v4.0rc3) on Windows 7 SP1 64bit.

440bx

  • Hero Member
  • *****
  • Posts: 5559
Re: Basics of writing SynEdit macros
« Reply #9 on: June 09, 2025, 01:55:37 am »
continued from previous post...

repost of the mainline for reference purposes, functions and procedures already discussed have been removed.

Code: Pascal  [Select][+][-]
  1. { --------------------------------------------------------------------------- }
  2. { MAINLINE                                            ----------------------- }
  3.  
  4. begin
  5.   < snip >
  6.  
  7.   EmitApiName();     { done in previous post }
  8.  
  9.   EmitOpenParenthesis();
  10.   DetermineWidestDispositionString();
  11.   EmitDispositions();
  12.   DetermineWidestParameterName();
  13.   EmitParameterNames();
  14.   EmitParameterTypes();
  15.   EmitClosingParenthesis();
  16.   EmitFunctionResult();
  17.  
  18. end.
  19.  

after emitting the function name, it is necessary to emit the opening parenthesis.  The "hardest" part is placing it in the right spot.

Code: Pascal  [Select][+][-]
  1. const
  2.   PARENTHESIS_FUNCTION_X      = 12;
  3.   PARENTHESIS_PROCEDURE_X     = 13;
  4.  
  5.   PARENTHESIS_OPEN            = '(';
  6.   PARENTHESIS_CLOSE           = ')';
  7.   PARENTHESIS_CLOSE_SEMICOLON = ');';
  8.  
  9. procedure EmitOpenParenthesis();
  10. var
  11.   OutputPt  : TPOINT;
  12.  
  13. begin
  14.   with OutputPt do
  15.   begin
  16.     Y := LastLine + SEPARATING_LINES_COUNT + API_OPEN_PARENTHESIS_LINE;
  17.  
  18.     case FunctionApi of
  19.       TRUE : X := PARENTHESIS_FUNCTION_X;
  20.       FALSE: X := PARENTHESIS_PROCEDURE_X;
  21.     end;
  22.   end;
  23.  
  24.   Caller.MoveCaretIgnoreEOL(OutputPt);
  25.  
  26.   { now that we have the caret's coordinate, emit the parenthesis           }
  27.  
  28.   Caller.InsertTextAtCaret(PARENTHESIS_OPEN, scamEnd);
  29. end;
  30.  

The X coordinate of the parenthesis is dependent on whether the definition is for a function or a procedure.  It is a simple matter of defining the appropriate constants (unfortunately global since PascalScript does not allow local constants in macros.) 

The next procedure is one that should have been done _before_ emitting anything but, it takes care of a detail that was originally overlooked.  The originally overlooked detail is that the parameter name is a combination of the C parameter name and its disposition.  Therefore, in order to keep everything vertically aligned and not waste any whitespace, it is necessary to determine the widest of the combined string (parameter disposition + parameter name)   The original macro naively added the widest parameter name + the widest disposition and, while that works, it wastes whitespace which is visually unappealing.  DetermineWidestDispositionString() solves that problem.

Code: Pascal  [Select][+][-]
  1. var
  2.   WidestDispositionString : integer;
  3.   WidestDispositionPrefix : integer;
  4.  
  5.   BraceOpenX              : integer;
  6.   BraceCloseX             : integer;
  7.  
  8. {                                                    ------------------------ }
  9.  
  10. procedure DispositionStringToDispositionPrefix
  11.             (
  12.                    InDispositionString  : string;
  13.                var OutDispositionPrefix : string
  14.             );
  15.   { removes the underscores from the disposition string and uppercases the    }
  16.   { first character                                                           }
  17.  
  18. var
  19.   i         : integer;
  20.   First     : string;
  21.  
  22. begin
  23.   OutputDebugString('InDispositionString: ' + InDispositionString, FALSE);
  24.  
  25.   { loop to remove all the underscores in the disposition string              }
  26.  
  27.   i := 1;
  28.   while i <= length(InDispositionString) do
  29.   begin
  30.     if InDispositionString[i] = '_' then
  31.     begin
  32.       { remove the underscore                                                 }
  33.  
  34.       Delete(InDispositionString, i, 1);
  35.  
  36.       continue;
  37.     end;
  38.  
  39.     inc(i);
  40.   end;
  41.  
  42.   { now we need to uppercase the first character                              }
  43.  
  44.   First := Copy(InDispositionString, 1, 1);
  45.   First := Uppercase(First);
  46.  
  47.   OutputDebugString('Uppercased First: ' + First, FALSE);
  48.  
  49.   Delete(InDispositionString, 1, 1);
  50.   OutDispositionPrefix := First + InDispositionString;
  51.  
  52.   OutputDebugString('OutDispositionPrefix: ' + OutDispositionPrefix, FALSE);
  53. end;
  54.  
  55. {                                                    ------------------------ }
  56.  
  57. procedure DetermineWidestDispositionString();
  58. var
  59.   i            : integer;
  60.   PrefixString : string;
  61.  
  62. begin
  63.   WidestDispositionString := 0;
  64.   WidestDispositionPrefix := 0;
  65.  
  66.   for i := PARAMETERS_FIRST_INDEX to DefinitionLinesCount - 1 do
  67.   begin
  68.     with TokenizedLines[i] do
  69.     begin
  70.       if Length(ParmDisposition) > WidestDispositionString then
  71.       begin
  72.         WidestDispositionString := Length(ParmDisposition);
  73.       end;
  74.  
  75.       DispositionStringToDispositionPrefix(ParmDisposition, PrefixString);
  76.  
  77.       { update the width of the widest prefix                                 }
  78.  
  79.       if Length(PrefixString) > WidestDispositionPrefix then
  80.       begin
  81.         WidestDispositionPrefix := Length(PrefixString);
  82.       end;
  83.  
  84.       { save this parameter's name prefix                                     }
  85.  
  86.       ParmDispositionPrefix := PrefixString;
  87.     end;
  88.   end;
  89.  
  90.   OutputDebugString('WidestDispositionPrefix: ' + IntToStr(WidestDispositionPrefix),
  91.                     FALSE);
  92.  
  93.   { once the widest disposition string is known, it is possible to            }
  94.   { calculate the X coordinates of the open and close braces                  }
  95.  
  96.   case FunctionApi of
  97.     TRUE : BraceOpenX := PARENTHESIS_FUNCTION_X  + 1;
  98.     FALSE: BraceOpenX := PARENTHESIS_PROCEDURE_X + 1;
  99.   end;
  100.  
  101.   { add 2 to account for the space after the opening brace and the space      }
  102.   { after the disposition string and 1 more to account for the open brace     }
  103.   { itself (3 = 2 spaces + open brace)                                        }
  104.  
  105.   BraceCloseX := BraceOpenX + WidestDispositionString + 3;
  106. end;
  107.  

There are two widest items to determine.  the widest disposition string which is the string that is between an open and close brace and the widest parameter name (as already explained above.)   The former is very easy to determine.  The latter is done in two steps. 

The first step is to determine the parameter prefix which is the parameter disposition with the underscores removed and the first character capitalized.  That's referred to as the DispositionPrefix. 

The widest parameter name is the widest string resulting from concatenating the parameter name with its disposition prefix.  To simplify things a bit, DispositionStringToDispositionPrefix() determines what the disposition prefix is for a given parameter.  The prefix will later be used to determine the widest parameter name (combination of prefix + C parameter name.)  Note that all that's needed to implement these functions are the PascalScript functions Delete, Copy, Uppercase and Length.

Once the widest disposition string has been calculated, the disposition string can be emited.  This is what EmitDispositions() does.

Code: Pascal  [Select][+][-]
  1. const
  2.   BRACE_OPEN  = '{ ';
  3.   BRACE_CLOSE = '}';
  4.  
  5. procedure EmitDispositions();
  6. var
  7.   OutputPt       : TPOINT;
  8.   i              : integer;
  9.   Parameter      : integer;     { 0 based parameter ordinal                   }
  10.  
  11. begin
  12.   Parameter := 0;
  13.  
  14.   for i := PARAMETERS_FIRST_INDEX to DefinitionLinesCount - 1 do
  15.   begin
  16.     with OutputPt do
  17.     begin
  18.       x := BraceOpenX;
  19.       y := LastLine + SEPARATING_LINES_COUNT + API_FIRST_PARAMETER_LINE +
  20.            Parameter;
  21.     end;
  22.  
  23.     Caller.MoveCaretIgnoreEOL(OutputPt);
  24.  
  25.     Caller.InsertTextAtCaret(BRACE_OPEN, scamEnd);    { open  brace           }
  26.  
  27.     Caller.InsertTextAtCaret(TokenizedLines[i].ParmDisposition, scamEnd);
  28.  
  29.     OutputPt.X := BraceCloseX;
  30.     Caller.MoveCaretIgnoreEOL(OutputPt);
  31.     Caller.InsertTextAtCaret(BRACE_CLOSE, scamEnd);   { close brace           }
  32.  
  33.     inc(Parameter);
  34.   end;
  35. end;
  36.  

As is the case with other Emit... functions, all that's needed to implement this function are Caller.MoveCaretIgnoreEOL and  Caller.InsertTextAtCaret.   It is simple because _what_ needs to be output has already been determined by other functions.

while DetermineWidestDispositionString() did all the ground work that is needed to calculate the widest parameter name, it did not concern itself with the widest parameter name, it concerned itself only with determining the widest disposition string.  The determination of the widest parameter name is done by the "surprisingly" named DetermineWidestParameterName() procedure.

Code: Pascal  [Select][+][-]
  1. var
  2.   { the parameter name width is the width of the parameter name plus the      }
  3.   { width of its disposition prefix.  the widest of that addition is the      }
  4.   { widest of all parameter names                                             }
  5.  
  6.   WidestParameterName  : integer;
  7.  
  8. procedure DetermineWidestParameterName();
  9. var
  10.   i       : integer;
  11.  
  12. begin
  13.   WidestParameterName := 0;
  14.  
  15.   for i := PARAMETERS_FIRST_INDEX to DefinitionLinesCount - 1 do
  16.   begin
  17.     with TokenizedLines[i] do
  18.     begin
  19.       if Length(ParmName) + Length(ParmDispositionPrefix) > WidestParameterName then
  20.       begin
  21.         WidestParameterName := Length(ParmName) + Length(ParmDispositionPrefix);
  22.       end;
  23.     end;
  24.   end;
  25. end;
  26.  

As it has already been mentioned, the widest parameter name is the widest of the combination of Disposition prefix + C parameter name. Only the PascalScript's function Length is needed for this procedure.

Once the widest parameter name is known, everything needed to emit the parameter names is known.

Code: Pascal  [Select][+][-]
  1. var
  2.   ColonX         : integer;
  3.  
  4. const
  5.   COLON_CHAR     = ':'#0;
  6.  
  7. procedure EmitParameterNames();
  8. var
  9.   OutputPt        : TPOINT;
  10.   i               : integer;
  11.   ParameterIndex  : integer;     { 0 based parameter ordinal                  }
  12.   ParameterName   : string;      { Parameter prefix + C definition name       }
  13.  
  14.  
  15. begin
  16.   ParameterIndex := 0;
  17.  
  18.   ColonX          := BraceCloseX         + 2 +
  19.                      WidestParameterName + 1;
  20.  
  21.   OutputDebugString('WidestDispositionPrefix: ' + IntToStr(WidestDispositionPrefix) + '  ' +
  22.                     'WidestParameterName: '     + IntToStr(WidestParameterName)     + '  ' +
  23.                     'BraceCloseX: '             + IntToStr(BraceCloseX)             + '  ' +
  24.                     'ColonX: '                  + IntToStr(ColonX),
  25.                     FALSE);
  26.  
  27.   for i := PARAMETERS_FIRST_INDEX to DefinitionLinesCount - 1 do
  28.   begin
  29.     with OutputPt do
  30.     begin
  31.       x := BraceCloseX + 2;
  32.       y := LastLine + SEPARATING_LINES_COUNT + API_FIRST_PARAMETER_LINE +
  33.            ParameterIndex;
  34.     end;
  35.  
  36.     Caller.MoveCaretIgnoreEOL(OutputPt);
  37.  
  38.     with TokenizedLines[i] do
  39.     begin
  40.       ParameterName := ParmDispositionPrefix + ParmName;
  41.     end;
  42.  
  43.     Caller.InsertTextAtCaret(ParameterName, scamEnd);
  44.  
  45.     OutputPt.X := ColonX;
  46.     Caller.MoveCaretIgnoreEOL(OutputPt);
  47.     Caller.InsertTextAtCaret(COLON_CHAR, scamEnd);
  48.  
  49.     inc(ParameterIndex);
  50.   end;
  51. end;
  52.  
As with other Emit... functions, this function is simply calls to Caller.MoveCaretIgnoreEOL and Caller.InsertTextAtCaret.

After the parameter names have been emitted, it is time to emit the parameter types.

EmitParameterTypes code:

Code: Pascal  [Select][+][-]
  1. const
  2.   { PTT = Parameter Types Table                                               }
  3.  
  4.   PTT_LO              = 1;
  5.   PTT_HI              = 1;
  6.  
  7. type
  8.   TPARAMETERS_TYPES_TABLE = array[PTT_LO..PTT_HI]
  9.                                of record
  10.     ptt_c_type                      : string;
  11.     ptt_pascal_type                 : string;
  12.   end;
  13.  
  14. var
  15.   ParameterTypesTable  : TPARAMETERS_TYPES_TABLE;
  16.  
  17. procedure TranslateParameterType
  18.             (
  19.                    InCParameterType       : string;
  20.                var OutPascalParameterType : string
  21.             );
  22. begin
  23.   { initialize                                                                }
  24.  
  25.   InCParameterType       := Uppercase(InCParameterType);
  26.   OutPascalParameterType := '';
  27.  
  28.   { look for a match                                                          }
  29.  
  30.   { having the "exit" in a separate condition makes it easier to copy/paste   }
  31.   { to additional type translations                                           }
  32.  
  33.   if InCParameterType = 'HANDLE'  then OutPascalParameterType := 'THANDLE';
  34.   if OutPascalParameterType <> '' then exit;
  35.  
  36.  
  37.   if InCParameterType = 'PCSTR'   then OutPascalParameterType := 'pchar';
  38.   if OutPascalParameterType <> '' then exit;
  39.  
  40.   if InCParameterType = 'PSTR'    then OutPascalParameterType := 'pchar';
  41.   if OutPascalParameterType <> '' then exit;
  42.  
  43.   if InCParameterType = 'PVOID'   then OutPascalParameterType := 'pointer';
  44.   if OutPascalParameterType <> '' then exit;
  45.  
  46.   { add additional type translations here                                     }
  47.  
  48.   { etc...                                                                    }
  49.  
  50.  
  51.  
  52.   { no match, make the Pascal parameter type equal the C parameter type       }
  53.  
  54.   OutPascalParameterType := InCParameterType;
  55. end;
  56.  
  57. {                                                    ------------------------ }
  58.  
  59. var
  60.   { this could have been a constant... oh well                                }
  61.  
  62.   Semicolon : string;
  63.  
  64. procedure EmitParameterTypes();
  65. var
  66.   OutputPt            : TPOINT;
  67.   ParameterIndex      : integer;
  68.  
  69.   i                   : integer;
  70.  
  71.   PascalParameterType : string;
  72.  
  73. begin
  74.   PascalParameterType := '**unset**';    { to make any problems obvious       }
  75.   Semicolon := ';';
  76.  
  77.   for i := PARAMETERS_FIRST_INDEX to DefinitionLinesCount - 1 do
  78.   begin
  79.     with OutputPt do
  80.     begin
  81.       x := ColonX + 2;
  82.       y := LastLine + SEPARATING_LINES_COUNT + API_FIRST_PARAMETER_LINE +
  83.            ParameterIndex;
  84.     end;
  85.  
  86.     Caller.MoveCaretIgnoreEOL(OutputPt);
  87.  
  88.     with TokenizedLines[i] do
  89.     begin
  90.       TranslateParameterType(ParmType, PascalParameterType);
  91.  
  92.       Caller.InsertTextAtCaret(PascalParameterType, scamEnd);
  93.  
  94.       if i < DefinitionLinesCount - 1 then
  95.       begin
  96.         Caller.InsertTextAtCaret(Semicolon, scamEnd);
  97.       end;
  98.  
  99.       inc(ParameterIndex);
  100.     end;
  101.   end;
  102. end;
  103.  
about the only thing that is notable about EmitParameterTypes() is that it translates C types into Pascal types, e.g, PVOID into pointer, HANDLE into THANDLE, etc.

The rest is more of the same, Caller.MoveCaretIgnoreEOL and Caller.InsertTextAtCaret.
 
Now that the parameters disposition, type and name have been emitted, all that's left is to emit the closing parenthesis and the function result (if any)

EmitClosingParenthesis() routine:

Code: Pascal  [Select][+][-]
  1. procedure EmitClosingParenthesis();
  2. var
  3.   OutputPt  : TPOINT;
  4.  
  5. begin
  6.   OutputPt.Y := LastLine + SEPARATING_LINES_COUNT + API_FIRST_PARAMETER_LINE +
  7.                 (DefinitionLinesCount - PARAMETERS_FIRST_INDEX);
  8.  
  9.   case FunctionApi of
  10.     TRUE :
  11.     begin
  12.       OutputPt.X := PARENTHESIS_FUNCTION_X;
  13.       Caller.MoveCaretIgnoreEOL(OutputPt);
  14.       Caller.InsertTextAtCaret(PARENTHESIS_CLOSE, scamEnd);
  15.     end;
  16.  
  17.     FALSE:
  18.     begin
  19.       OutputPt.X := PARENTHESIS_PROCEDURE_X;
  20.       Caller.MoveCaretIgnoreEOL(OutputPt);
  21.       Caller.InsertTextAtCaret(PARENTHESIS_CLOSE_SEMICOLON, scamEnd);
  22.     end;
  23.   end;
  24. end;
  25.  

Just look at the code, it's just more of the same.


EmitFunctionResult() routine:
Code: Pascal  [Select][+][-]
  1.  
  2. const
  3.   SPACE_CHAR     = ' ';
  4.   SEMICOLON_CHAR = ';';
  5.  
  6. const
  7.   EXPORT_DATA    = ' stdcall; external ';
  8.  
  9.   { the following needs to be customized or prompted for                      }
  10.  
  11.   EXPORT_DLL     = 'ImageHlp;';
  12.  
  13.  
  14. procedure EmitFunctionResult();
  15. var
  16.   OutputPt         : TPOINT;
  17.  
  18.   PascalReturnType : string;
  19.   CReturnType      : string;
  20.  
  21. begin
  22.   { if it's a procedure, there is no result to emit                           }
  23.  
  24.   if not FunctionApi then exit;
  25.  
  26.   with OutputPt do
  27.   begin
  28.     x := length(FUNCTION_TOKEN) + 1;
  29.     y := LastLine + SEPARATING_LINES_COUNT + API_FIRST_PARAMETER_LINE +
  30.          (DefinitionLinesCount - PARAMETERS_FIRST_INDEX) + 1;
  31.   end;
  32.  
  33.   Caller.MoveCaretIgnoreEOL(OutputPt);
  34.   Caller.InsertTextAtCaret(COLON_CHAR, scamEnd);
  35.  
  36.   OutputPt.X := OutputPt.X + 2;
  37.   Caller.MoveCaretIgnoreEOL(OutputPt);
  38.  
  39.   CReturnType := TokenizedLines[1].FunctionResultType;
  40.  
  41.   TranslateParameterType(CReturnType, PascalReturnType);
  42.  
  43.   Caller.InsertTextAtCaret(PascalReturnType + SEMICOLON_CHAR, scamEnd);
  44.   Caller.InsertTextAtCaret(EXPORT_DATA, scamEnd);
  45.   Caller.InsertTextAtCaret(EXPORT_DLL,  scamEnd);
  46. end;
  47.  
You may not know the drill but, by now, you should know the code.

The attachment has the macro and the example program the macro acts on.  The majority of calls to OutputDebugString are suppressed but, a few calls have been left active to emphasize the most important points/parts.

Also, very educational, once the macro has fully played do repeated undo(s).  That will reverse-play the macro and show how the undo facility creates its undo blocks depending on the macro code. Not only its educational, it's fun to watch.   Undo like an Egyptian. :)

What is truly remarkable is that it took roughly less than a dozen PascalScript and SynEdit functions to implement a macro that does a somewhat elaborate conversion from a C API definition to a Pascal definition.




finally... there is no more code to post.

Hopefully, these examples will be educational and useful to some.
(FPC v3.0.4 and Lazarus 1.8.2) or (FPC v3.2.2 and Lazarus v4.0rc3) on Windows 7 SP1 64bit.

440bx

  • Hero Member
  • *****
  • Posts: 5559
Re: Basics of writing SynEdit macros
« Reply #10 on: June 09, 2025, 02:08:53 pm »
In the array element counting example, I mentioned that requiring the array to have one item per line would significantly simplify determining how many elements there are in the array.  Basically a simple line count from first to last element would be all that's needed.

This gave me the idea of writing a macro that simply reports the number of lines selected, which is something I've personally needed on a number of occasions.

The macro is:
Code: Pascal  [Select][+][-]
  1. var
  2.   p1, p2 : TPOINT;
  3.  
  4. begin
  5.   p2 := Caller.BlockEnd;
  6.   p1 := Caller.BlockBegin;
  7.  
  8.   case Caller.SelAvail of
  9.     TRUE:
  10.     begin
  11.       ShowMessage('lines: ' + IntToStr(p2.y - p1.y + 1));
  12.     end;
  13.  
  14.     FALSE:
  15.     begin
  16.       ShowMessage('zero lines selected');
  17.     end;
  18.   end;
  19. end.                
  20.  
This macro counts all selected lines, it would be easy to modify it to ignore blank lines.  Hint: use a "for" loop and the "Trim" function.

Note that if a line is _partially_ selected, it still counts as one fully selected line.

HTH.
« Last Edit: June 09, 2025, 02:49:25 pm by 440bx »
(FPC v3.0.4 and Lazarus 1.8.2) or (FPC v3.2.2 and Lazarus v4.0rc3) on Windows 7 SP1 64bit.

n7800

  • Sr. Member
  • ****
  • Posts: 394
Re: Basics of writing SynEdit macros
« Reply #11 on: June 09, 2025, 07:13:12 pm »
Nice tutorial! I hope this is a draft for a future wiki page? There is a tutorials category there.

440bx

  • Hero Member
  • *****
  • Posts: 5559
Re: Basics of writing SynEdit macros
« Reply #12 on: June 09, 2025, 07:29:56 pm »
Nice tutorial!
Thank you n7800

I hope this is a draft for a future wiki page? There is a tutorials category there.
I was looking into that but, I've run into a little problem. 

I forgot what my password for the wiki is.  I asked for a password reset email twice and didn't get anything.  I figured that maybe I didn't have a wiki  account so I tried to create one and was told my user name (440bx) is already in use (I doubt someone else used that handle, it would be the first time.)

I figured I'd wait a few more hours to see if by any chance I get a password reset email but, I really doubt that's going to happen since they usually come within seconds.

Bottom line is, I'll definitely consider putting the information on the wiki but, somehow I got to get in there first. <chuckle>
(FPC v3.0.4 and Lazarus 1.8.2) or (FPC v3.2.2 and Lazarus v4.0rc3) on Windows 7 SP1 64bit.

n7800

  • Sr. Member
  • ****
  • Posts: 394
Re: Basics of writing SynEdit macros
« Reply #13 on: June 09, 2025, 08:37:09 pm »
I figured I'd wait a few more hours to see if by any chance I get a password reset email but, I really doubt that's going to happen since they usually come within seconds.

Unfortunately, the wiki has always had access issues. Apparently, like many others, you were accidentally blocked...

There are instructions on who to write to to fix the problem. You can write a personal message on this forum or via the mailing list.

440bx

  • Hero Member
  • *****
  • Posts: 5559
Re: Basics of writing SynEdit macros
« Reply #14 on: June 09, 2025, 09:28:08 pm »
Thank you @n7800, I will attempt to have the problem resolved.
(FPC v3.0.4 and Lazarus 1.8.2) or (FPC v3.2.2 and Lazarus v4.0rc3) on Windows 7 SP1 64bit.

 

TinyPortal © 2005-2018