PBToolboxAI v4 ← Site

codeeditor — u_pbt_codeeditor #

← Component reference · Guide contents

Code editor with syntax highlighting: ten languages, line numbers, region folding, search and replace, diagnostic markers in the gutter, and file drop.

▶ See it live — Demo application, Code editor tile: the preview, the code behind it and this page, side by side.


At a glance #

Userobjectu_pbt_codeeditor
Item class— (component without items)
Used forEntering or displaying code, a SQL query, a configuration file: anywhere a multilineedit is not readable enough
Opt-in optionsib_track_caret, ib_allow_drop, ib_folding

Quick start #

// window open event
uo_editor.is_syntax = uo_editor.SYNTAX_SQL
uo_editor.is_text   = "SELECT c.name, SUM(o.amount) AS total~r~n" &
                       + "FROM customer c~r~n" &
                       + "WHERE o.status = 'paid'"
// Local variables
string ls_sql

// Read back what the user has actually typed
ls_sql = uo_editor.of_get_text()

is_text gives the same thing: the user's typing lands there as soon as the input settles (ue_changed event).


Supported languages #

is_syntax accepts one of these constants, or SYNTAX_NONE (empty string) for plain text with no highlighting.

LanguageConstantOther accepted spellings
PowerScriptSYNTAX_POWERSCRIPTpb, powerbuilder
SQLSYNTAX_SQLtsql, plsql
JavaScriptSYNTAX_JAVASCRIPTjs, jsx
JSONSYNTAX_JSONjsonc
C familySYNTAX_C · SYNTAX_CPP · SYNTAX_CSHARP · SYNTAX_JAVAc++, cxx, cs
HTML / XMLSYNTAX_HTML · SYNTAX_XMLhtm, xhtml, svg
CSSSYNTAX_CSSscss, less
PythonSYNTAX_PYTHONpy
YAMLSYNTAX_YAMLyml
MarkdownSYNTAX_MARKDOWNmd, mkd

Languages of the same family share their highlighting (SYNTAX_JAVA colors like SYNTAX_C, SYNTAX_XML like SYNTAX_HTML): the constant you pick documents your intent, the result on screen is the same. The value is case-insensitive, and an unknown name falls back to plain text without raising an error.


Properties #

Content and language #

PropertyTypeDefaultPurpose
is_textstring""The code displayed in the editor. When read back, returns the live content, the user's typing included, with the line endings it was given: a CRLF text reads back in CRLF, a CR-only text in CR, folded or not; a text that mixes them reads back in CRLF. A new text shows from its first line, clears the markers of of_add_marker and is not modified (ib_modified)
is_syntaxstring""Highlighting language: SYNTAX_* constants (see the table above). SYNTAX_NONE = plain text. Reads back as written: SYNTAX_CSHARP stays SYNTAX_CSHARP, even though C# shares the C grammar
ib_readonlybooleanfalseRead-only editor: the user can consult without being able to modify
ib_modifiedbooleanfalsetrue as soon as the text differs from the one declared saved: a new is_text is not modified, typing, of_insert_text or a replace make it modified, undoing back to the saved text sets it back to false. Set it back to false after saving; true forces it

Display #

PropertyTypeDefaultPurpose
ib_line_numbersbooleantrueShows or hides the line number gutter, on the left; hidden, a narrow gutter keeps the fold markers and the diagnostics of of_add_marker
ib_current_linebooleantrueHighlights the line where the caret sits; in ib_wrap mode the band covers every row of the line
ib_foldingbooleanfalseOpt-in: allows the #region … #endregion regions (//#region in PowerScript, JavaScript or C) to be folded from the gutter: #region starts folded, #regionopen starts open; the markers stay even with ib_line_numbers = false. It is a reading mode: while it is on the editor takes no typing, because the input area then holds the folded text and not the source; turning it on over closed regions also forgets the Ctrl+Z history. A marker is reached with the keyboard (Tab); Enter or Space folds or unfolds it
ib_wrapbooleanfalseWraps long lines instead of scrolling sideways
ii_tab_sizeinteger4Number of columns a tab character takes up, from 1 to 12 (any other value falls back to 4); Enter after an opening brace indents by this width
is_font_familystring""Editor font (empty = the theme's monospaced font)
ii_font_sizeinteger0Font size in pixels (0 = the theme's size)
PropertyTypeDefaultPurpose
ib_search_enabledbooleantrueEnables the built-in search bar (Ctrl+F, Ctrl+H to replace — see “Keyboard”); while false, of_find returns -4
ib_find_match_casebooleanfalseSearch option: finds only the text with the same case. The Aa toggle of the bar is the same switch; applies to of_find, of_replace and of_replace_all
ib_find_whole_wordbooleanfalseSearch option: finds the text only as a whole word (the _ belongs to the word: ls_a is not found inside ls_ab). The ab toggle of the bar
ib_find_regexbooleanfalseSearch option: the text is a regular expression (JavaScript syntax); the replacement of of_replace may then use $1, $& and $<name>. An invalid expression finds nothing, frames the box in red and makes of_replace return -5. The .* toggle of the bar
il_doc_linelong0Brings the given line to the middle of the view and marks it with an accent band (numbered from 1, like the gutter) — the line a documentation or a search result points at. The user's caret and selection do not move (il_caret_line moves the caret). A line hidden in a folded region opens that region; the band is painted in ib_wrap mode too. 0 clears the band; a line past the end marks nothing and reads back as written; any new is_text clears the band
il_caret_linelong1The line of the caret, from 1, read live (folded: the gutter's). Writing it puts the caret at the start of that line and brings it on screen — the “go to line” of a compiler error; a line hidden in a folded region opens that region, a line past the end stops at the last one. With ib_track_caret, ue_caret_changed follows, as for a click — nothing when the caret stays where it is
il_caret_columnlong1The column of the caret, from 1, read live: the rank of the character in its line (a tab counts for one). Writing it moves the caret along its line; past the end of the line it stops at that end. With ib_track_caret, ue_caret_changed follows, as for a click
ib_track_caretbooleanfalseOpt-in: raises ue_caret_changed every time the caret moves — a gesture of the user or an order of your code; a new document (is_text, a load) says nothing about the caret
ib_allow_dropbooleanfalseOpt-in: accepts files dropped from Windows Explorer; the full paths arrive through ue_drop_files
is_theme_stylestring""Visual style of the component (THEME_STYLE_* constants); empty = the application's, followed at every change
is_theme_modestring""Light or dark variant (THEME_MODE_* constants); empty = the application's, followed at every change
il_theme_accentlong-1Accent colour of this component (-1 = the application accent, or the theme's)
is_tooltipstring""Simple tooltip shown when hovering the component
is_super_tooltip_titlestring""Title of the rich tooltip (takes precedence over is_tooltip)
is_super_tooltip_textstring""Text of the rich tooltip (rich markup accepted)
is_super_tooltip_imagestring""Image of the rich tooltip

Methods #

MethodPurpose
of_get_text ( ) → stringReturns the live content — the same value as is_text, for code that prefers a method call
of_find (string as_text)Opens the search bar, fills it in and highlights every occurrence of the text, following the ib_find_* options (folded, in the visible lines only). The first occurrence from the caret is brought on screen; the caret itself only moves when the user closes the bar. An empty text empties the box and clears the highlights. Returns 0 once applied, -4 when ib_search_enabled is false, -2 if the component is not created
of_insert_text (string as_text)Inserts text at the caret, exactly as if the user had typed it: the current selection is replaced, the caret lands after the insertion and the insertion is brought on screen. This is the method behind an insert snippet button, where is_text would throw away the work in progress. During a search, the insertion goes to the user's caret, never over the match. When the focus is in the editor or in its search bar, the insertion goes through the browser's own edit path: Ctrl+Z still undoes it. Raises ue_changed, and with ib_track_caret ue_caret_changed follows, as for typing. Nothing is inserted while the editor is read-only (ib_readonly) or folded (ib_folding). Returns 0 once applied, -4 when read-only or folded, -5 on an empty text, -2 if the component is not created
of_replace (string as_find, string as_replace)Replaces the first occurrence of as_find from the caret (wrapping to the top) and brings it on screen; the ib_find_* options apply, and with ib_find_regex the replacement may use $1, $& and $<name>. One Ctrl+Z undoes it. With ib_track_caret, ue_caret_changed follows when the caret moves. Returns the number of occurrences replaced (1, or 0 when there is none), -4 when read-only or folded, -5 on an empty search or an invalid expression, -2 if the component is not created
of_replace_all (string as_find, string as_replace)Replaces every occurrence of as_find, as one undo step. Same options and same codes as of_replace; returns how many occurrences were replaced
of_selected_text ( ) → stringReturns the text the user has selected, read live ("" without a selection), with the line endings of is_text
of_select_range (long al_from_line, long al_from_col, long al_to_line, long al_to_col)Selects from (line, column) to (line, column), all from 1, and brings the selection on screen; the caret sits at the end given last, and a column past the end of its line stops at that end. With ib_track_caret, ue_caret_changed follows, as for a drag. Returns 0 once applied, -5 on a line outside the document or a column below 1, -4 while the editor is folded, -2 if the component is not created
of_line_count ( )Returns how many lines the document has, folded or not, read live — the upper bound of a “go to line” box
of_add_marker (long al_line, string as_kind, string as_tooltip)Marks a line of the gutter with a diagnostic — MARKER_ERROR, MARKER_WARNING or MARKER_INFO — and a tooltip (markup accepted): what a compiler says, where it says it. Several markers may share a line: the worst kind gives the icon, the tooltip lists them all. They belong to the document: a new is_text clears them. Returns 0 once applied, -5 on a line outside the document or an unknown kind, -2 if the component is not created
of_remove_marker (long al_line)Removes every marker of one line. Returns 0 once removed, -5 when the line carries none, -2 if the component is not created
of_clear_markers ( )Removes every marker of the gutter. Returns 0 once applied, -2 if the component is not created
of_marker_count ( )Returns how many markers the gutter carries, read live (two on one line count for two)
of_reset ( )Returns every property to its default and empties the editor. Returns 0 once applied, -2 when the component is not created
of_set_redraw (boolean)Batches a burst of changes into a single render. Returns 0 once applied, -2 when the component is not created
of_save_as_png (string) · of_save_as_jpg (string)Exports the rendering as an image. Returns 0 once the image is written, -4 when writing fails, -2 when the component is not created

Events #

EventRaised when
ue_changed ( )The user has modified the content and the typing has settled — also raised after of_insert_text or a replace. The event carries nothing: filling it meant re-reading the whole document on every settled keystroke, for an application that most often only wants to know that it changed. The one that wants the code asks for it — is_text or of_get_text(); ib_modified says whether it differs from the saved text
ue_caret_changed (long al_line, long al_col)The caret has moved (key, click, drag, typing, or your code); line and column counted from 1, the line being the gutter's when the editor is folded, the column the rank of the character in its line (a tab counts for one). Your code raises it like the gesture (il_caret_line, il_caret_column, of_select_range, of_insert_text, of_replace); nothing when the caret stays where it is, and a new document (is_text, a load) says nothing about the caret — requires ib_track_caret = true
ue_find_result (long al_count, long al_index)A search has completed, or moves to another occurrence: al_count occurrences found, al_index = rank of the one currently highlighted (from 1). While typing, it is raised only when the count changes; closing the bar does not raise it; nor does an empty search
ue_drop_files (string as_files[])Files have been dropped from Windows: full paths, one entry per file. Requires ib_allow_drop = true
ue_drag_enter ( )A file drag enters the editor (ib_allow_drop)
ue_drag_leave ( )The file drag leaves the editor
ue_ready ( )The component has finished loading; everything sent beforehand has been replayed
ue_runtime_missing ( )The WebView2 runtime is missing: the component stays empty
ue_bg_color (long al_color)The component has computed its theme background color; the userobject has already adopted it (backcolor)

Keyboard #

KeyEffect
TabIndents to the next stop (ii_tab_size); several lines selected: indents the whole block
Shift+TabOutdents the line or the block
EnterNew line at the same indentation, one level deeper after {, ( or [
Ctrl+FOpens the search bar, filled with the selection (ib_search_enabled)
Ctrl+HOpens the bar with its replace row (typeable editor only)
F3 · Shift+F3Next · previous match, from the code or the bar
Enter · Shift+Enter (bar)Next · previous match; in the replace box, Enter replaces the current match and Ctrl+Enter replaces them all
EscapeCloses the bar and puts the caret on the current match
Tab, then Enter or Space (gutter)Reaches a fold marker, folds or unfolds it (ib_folding)

The column reported by ue_caret_changed and il_caret_column is the rank of the character in its line: a tab counts for one, whatever its width on screen.


Examples #

A SQL query editor #

// Freeze the drawing while the editor is set up
uo_query.of_set_redraw(/*on*/ false)

// SQL colouring, then the query to show
uo_query.is_syntax = uo_query.SYNTAX_SQL
uo_query.is_text   = "-- Top customers by revenue collected~r~n" &
                       + "SELECT c.name, SUM(o.amount) AS total~r~n" &
                       + "FROM customer c~r~n" &
                       + "  INNER JOIN orders o ON o.cust_id = c.id~r~n" &
                       + "WHERE o.status = 'paid'~r~n" &
                       + "GROUP BY c.name~r~n" &
                       + "ORDER BY total DESC"

// One repaint, everything at once
uo_query.of_set_redraw(/*on*/ true)
// clicked event of cb_execute
string ls_sql

// Run the query as typed
ls_sql = uo_query.of_get_text()     // what the user really typed
of_execute(ls_sql)

A read-only viewer #

Ideal for presenting generated code, a log, or an excerpt the user has to read without modifying it.

// PowerScript colouring
uo_preview.is_syntax = uo_preview.SYNTAX_POWERSCRIPT

// A read-only viewer: no caret, no gutter, no highlight
uo_preview.ib_readonly     = true      // consultation only, no editing caret
uo_preview.ib_line_numbers = false     // hides the number gutter
uo_preview.ib_current_line = false     // no current line highlight
uo_preview.ib_wrap         = true      // wraps rather than scrolls
uo_preview.ii_tab_size     = 2         // tabs displayed over 2 columns

// The code to show
uo_preview.is_text = of_generate_code()

Tracking the caret position in a status bar #

// Report every caret move (ue_caret_changed)
uo_editor.ib_track_caret = true       // explicit opt-in: otherwise no event at all
// ue_caret_changed event of uo_editor: (long al_line, long al_col)
uo_status.of_panel(/*key*/ "pos").is_text = "Line " + String(al_line) + ", col. " + String(al_col)

Without ib_track_caret, the caret reports nothing: this trigger fires very often and stays off until you ask for it.

Searching and jumping to a line #

// Opens the search bar and highlights every occurrence
uo_editor.of_find(/*text*/ "ll_total")
// ue_find_result event of uo_editor: (long al_count, long al_index)
if al_count = 0 then
    uo_status.of_panel(/*key*/ "main").is_text = "No match"
else
    uo_status.of_panel(/*key*/ "main").is_text = String(al_index) + " / " + String(al_count)
end if
// Go to the line a compiler flagged and mark it in the gutter
uo_editor.of_add_marker(/*line*/ ll_error_line, /*kind*/ uo_editor.MARKER_ERROR, /*tooltip*/ ls_error_text)
uo_editor.il_caret_line = ll_error_line     // the caret goes there, the line comes on screen
uo_editor.of_focus_webview()                // the user fixes it at once

Opening a file dropped from Explorer #

// Accept files dropped from the Explorer
uo_editor.ib_allow_drop = true
// ue_drop_files event of uo_editor: (string as_files[])
string ls_content
integer li_file

// as_files[1] carries the FULL path of the first dropped file
li_file = FileOpen(as_files[1], StreamMode!, Read!)
if li_file > 0 then
    FileReadEx(li_file, ls_content)
    FileClose(li_file)

    // Colour by the file's extension, then show its content
    uo_editor.is_syntax = of_syntax_for_extension(as_files[1])
    uo_editor.is_text   = ls_content
end if

Reacting to changes #

// ue_changed event of uo_editor: ( )
cb_save.enabled = uo_editor.ib_modified     // undoing back to the saved text sets it back to false
// clicked event of cb_save
if of_save_script(uo_editor.is_text) = 1 then
    uo_editor.ib_modified = false     // the saved text becomes the reference
    cb_save.enabled = false
end if

The event is only raised once the typing has settled: continuous input does not generate one event per keystroke.

Renaming a variable throughout the script #

// Local variables
long ll_count

// Whole words only: ll_total2 is another variable
uo_editor.ib_find_whole_word = true
ll_count = uo_editor.of_replace_all(/*find*/ "ll_total", /*replace*/ "ldc_amount")   // one Ctrl+Z undoes them all

Best practices #

Inherited from the common base #

These members exist on every visual component — they are not specific to this one. They are detailed once, in the transverse chapters; this table only says where to read them.

MembersRoleDetailed in
of_resetPut the component back to zero3.6 Resetting a component: of_reset()
of_register_shortcut · of_clear_shortcutsThe component's keyboard chords3.5 Keyboard shortcuts
of_is_created · of_is_ready · of_get_last_errorWhether it was born, whether it is ready, what failed3.7 Diagnostics
of_save_as_png · of_save_as_jpgExport the rendering as an image3.8 Exporting the rendering as an image
of_set_redrawGroup changes into a single repaint3.10 Best practices
of_preload_iconsIcons shown with no delayInstant display: of_icon
of_set_translationTranslate one of the component's labels5.2 Adapting a label: of_set_translation
of_focus_webviewGive the component the focus6.4 Keyboard and focus
of_print · of_print_to_pdfPrint, or write a PDF6.9 Printing
of_set_property · of_get_property · of_component_nameDriving a property by its name3.1 The property engine

Two helpers are not inherited: of_icon and of_escape_markup live on n_pbt_utils. Declare one — n_pbt_utils lnv_utils, nothing to create — and call them on it.


← Component reference · Guide contents