This is an automated email from the ASF dual-hosted git repository.

hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new 76ee87bd3d issue #7456: TextComposite toolbar with find/replace and 
plugin SPI (#7907)
76ee87bd3d is described below

commit 76ee87bd3dd155e4dd93016deab5d76c4297fe8c
Author: Matt Casters <[email protected]>
AuthorDate: Wed Aug 12 16:37:28 2026 +0200

    issue #7456: TextComposite toolbar with find/replace and plugin SPI (#7907)
    
    * issue #7456: TextComposite toolbar with find/replace and plugin SPI
    
    Add an extensible toolbar on multi-line text editors (SQL, scripts, JSON,
    logs, and so on) with built-in edit actions and find/replace, a styleType
    constructor parameter for GuiToolbarElementFilter, developer documentation,
    and a sample JSON pretty-print toolbar contribution.
    
    * issue #7456: apply spotless formatting
    
    * issue #7456: fix Javadoc link to Lombok-generated getStyleType
---
 core/src/main/java/org/apache/hop/core/Props.java  |   1 +
 docs/hop-dev-manual/modules/ROOT/nav.adoc          |   1 +
 .../modules/ROOT/pages/gui-plugins-toolbars.adoc   | 451 +++++++++++++++++++++
 .../ROOT/pages/hopweb/hopweb-antipatterns.adoc     |   3 +
 docs/hop-dev-manual/modules/ROOT/pages/index.adoc  |   1 +
 .../modules/ROOT/pages/plugin-development.adoc     |   3 +
 .../workflow/actions/eval/ActionEvalDialog.java    |   3 +-
 .../ActionEvalTableContentDialog.java              |   6 +-
 .../ActionSendNagiosPassiveCheckDialog.java        |   4 +-
 .../actions/snmptrap/ActionSNMPTrapDialog.java     |   6 +-
 .../hop/workflow/actions/sql/ActionSqlDialog.java  |   5 +-
 .../actions/waitforsql/ActionWaitForSqlDialog.java |   3 +-
 .../java/org/apache/hop/git/GitPerspective.java    |   1 +
 .../apache/hop/git/info/DiffStyledTextComp.java    |   2 +-
 .../git/info/GitInfoExplorerFileTypeHandler.java   |   1 +
 .../cassandrainput/CassandraInputDialog.java       |   6 +-
 .../mongodbdelete/MongoDbDeleteDialog.java         |   4 +-
 .../mongodbinput/MongoDbInputDialog.java           |   6 +-
 .../hop/workflow/actions/mql/ActionMqlDialog.java  |   6 +-
 .../salesforceinput/SalesforceInputDialog.java     |   7 +-
 .../databasejoin/DatabaseJoinDialog.java           |   5 +-
 .../transforms/drools/RulesAccumulatorDialog.java  |   6 +-
 .../transforms/drools/RulesExecutorDialog.java     |   6 +-
 .../dynamicsqlrow/DynamicSqlRowDialog.java         |   3 +-
 .../transforms/formula/editor/FormulaEditor.java   |   6 +-
 .../transforms/janino/editor/FormulaEditor.java    |   6 +-
 .../transforms/javafilter/JavaFilterDialog.java    |   4 +-
 .../UserDefinedJavaClassDialog.java                |   3 +-
 .../transforms/javascript/ScriptValuesDialog.java  |   3 +-
 .../ui/TextCompositeToolbarJsonFormatButton.java   | 115 ++++++
 .../ui/messages/messages_en_US.properties          |  19 +
 .../transforms/ldapinput/LdapInputDialog.java      |   4 +-
 .../transforms/regexeval/RegexEvalDialog.java      |   4 +-
 .../regexeval/RegexEvalHelperDialog.java           |   6 +-
 .../pipeline/transforms/script/ScriptDialog.java   |   3 +-
 .../hop/pipeline/transforms/sql/ExecSqlDialog.java |   5 +-
 .../hop/pipeline/transforms/ssh/SshDialog.java     |   6 +-
 .../transforms/tableinput/TableInputDialog.java    |   5 +-
 .../transforms/writetolog/WriteToLogDialog.java    |   6 +-
 .../main/java/org/apache/hop/ui/core/PropsUi.java  |   9 +
 .../hop/ui/core/database/dialog/SqlEditor.java     |   5 +-
 .../hop/ui/core/dialog/FindReplaceDialog.java      | 335 +++++++++++++++
 .../ui/core/widget/JavaScriptStyledTextComp.java   |   2 +-
 .../hop/ui/core/widget/JavaStyledTextComp.java     |   2 +-
 .../hop/ui/core/widget/SQLStyledTextComp.java      |   2 +-
 .../hop/ui/core/widget/ScriptStyledTextComp.java   |   2 +-
 .../apache/hop/ui/core/widget/StyledTextComp.java  |  90 +++-
 .../apache/hop/ui/core/widget/StyledTextVar.java   |  97 ++++-
 .../apache/hop/ui/core/widget/TextComposite.java   | 416 ++++++++++++++++++-
 .../apache/hop/ui/core/widget/TextFindSupport.java | 106 +++++
 .../apache/hop/ui/hopgui/dialog/NotePadDialog.java |   5 +-
 .../delegates/HopGuiPipelineLogDelegate.java       |   6 +-
 .../delegates/HopGuiWorkflowLogDelegate.java       |   6 +-
 .../configuration/tabs/ConfigGuiOptionsTab.java    |  17 +
 .../perspective/execution/ExecutionLogPanel.java   |   6 +-
 .../core/dialog/messages/messages_en_US.properties |  14 +
 .../core/widget/messages/messages_en_US.properties |  10 +
 .../hop/ui/core/widget/TextFindSupportTest.java    |  64 +++
 58 files changed, 1856 insertions(+), 73 deletions(-)

diff --git a/core/src/main/java/org/apache/hop/core/Props.java 
b/core/src/main/java/org/apache/hop/core/Props.java
index 1e3a780695..c9888f518e 100644
--- a/core/src/main/java/org/apache/hop/core/Props.java
+++ b/core/src/main/java/org/apache/hop/core/Props.java
@@ -78,6 +78,7 @@ public class Props implements Cloneable {
 
   public static final String STRING_HIDE_VIEWPORT = "HideViewport";
   public static final String STRING_SHOW_TABLE_VIEW_TOOLBAR = 
"ShowTableViewToolbar";
+  public static final String STRING_SHOW_TEXT_COMPOSITE_TOOLBAR = 
"ShowTextCompositeToolbar";
   public static final String STRING_SHOW_EXIT_WARNING = "ShowExitWarning";
 
   public static final String STRING_CUSTOM_PARAMETER = "CustomParameter";
diff --git a/docs/hop-dev-manual/modules/ROOT/nav.adoc 
b/docs/hop-dev-manual/modules/ROOT/nav.adoc
index 4ebe410d66..9dc7cd1497 100644
--- a/docs/hop-dev-manual/modules/ROOT/nav.adoc
+++ b/docs/hop-dev-manual/modules/ROOT/nav.adoc
@@ -25,6 +25,7 @@ under the License.
 * xref:integration-testing.adoc[Integration testing]
 * xref:internationalisation.adoc[Internationalisation (i18n)]
 * xref:plugin-development.adoc[Plugins Development]
+* xref:gui-plugins-toolbars.adoc[GUI plugins and toolbars]
 * xref:marketplace-publish-plugin.adoc[Publishing a plugin to Nexus 
(marketplace)]
 * xref:engine-compatibility.adoc[Engine compatibility]
 * xref:lineage.adoc[Lineage observation hub]
diff --git a/docs/hop-dev-manual/modules/ROOT/pages/gui-plugins-toolbars.adoc 
b/docs/hop-dev-manual/modules/ROOT/pages/gui-plugins-toolbars.adoc
new file mode 100644
index 0000000000..ad857d76c8
--- /dev/null
+++ b/docs/hop-dev-manual/modules/ROOT/pages/gui-plugins-toolbars.adoc
@@ -0,0 +1,451 @@
+////
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+  http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+////
+:description: How Hop GUI toolbars work, how to contribute buttons with 
@GuiPlugin and @GuiToolbarElement, how to use GuiToolbarElementFilter, and how 
TextComposite / TableView expose extensible toolbars to plugins.
+[[GuiPluginsToolbars-GuiPluginsAndToolbars]]
+= GUI plugins and toolbars
+
+Hop has a lightweight system for contributing UI actions from plugins without 
hard-coding every button into the core GUI.
+This page explains how that system works for **toolbars** — the main way 
plugins add icons above tables, multi-line text editors, and other widgets.
+
+You will learn:
+
+* How `@GuiPlugin` classes are discovered
+* How `@GuiToolbarElement` adds a toolbar button
+* How listeners receive the host widget (`TableView`, `TextComposite`, …)
+* How `@GuiToolbarElementFilter` shows or hides buttons (easy to miss)
+* How the **TextComposite** and **TableView** toolbars are designed for 
extension (SQL formatters, JSON pretty-print, export, lineage, …)
+
+Related reading: xref:plugin-development.adoc[Plugin development], 
xref:hopweb/hopweb-antipatterns.adoc[Hop Web antipatterns].
+
+== Mental model
+
+[source]
+----
+@GuiPlugin class
+    └── @GuiToolbarElement(root = "Some-Toolbar-Id", id = "...", image = "...")
+            └── static or instance method  →  invoked when the user clicks
+    └── @GuiToolbarElementFilter(parentId = "Some-Toolbar-Id")   (optional)
+            └── static boolean method(String itemId, Object host)  →  show/hide
+----
+
+1. A **host** widget (for example `TextComposite` or `TableView`) creates a 
toolbar with a fixed **root id** (`ID_TOOLBAR`).
+2. At startup, Hop scans every `@GuiPlugin` and registers methods annotated 
with `@GuiToolbarElement` under that root id.
+3. When the host builds the toolbar, `GuiToolbarWidgets` loads all items for 
that root, applies filters, and wires click listeners.
+4. On click, Hop prefers a **public static** method that takes the host object 
as its single argument (the pattern plugins should use).
+
+== @GuiPlugin discovery
+
+Classes annotated with `@GuiPlugin` are registered as the `GuiPluginType` 
plugin type.
+At GUI startup, `HopGuiEnvironment.initGuiPlugins()` reflects over every such 
class and records:
+
+* `@GuiToolbarElement` methods → toolbar items
+* `@GuiToolbarElementFilter` methods → show/hide rules for a toolbar root
+* Also: menu elements, keyboard shortcuts, tabs, context actions, and similar 
GUI contributions
+
+[source,java]
+----
+@GuiPlugin
+public class MyToolbarContributions {
+  // static methods with @GuiToolbarElement / @GuiToolbarElementFilter
+}
+----
+
+Put the class on the plugin classpath (under `plugins/…`) so the plugin system 
can load it.
+`hop-ui` is typically a `provided` dependency for transform/action plugins 
that already ship dialogs.
+
+IMPORTANT: On Hop Web, reflection at registration time loads every type that 
appears in **method and field signatures** of `@GuiPlugin` classes.
+Do **not** put desktop-only SWT types (for example 
`org.eclipse.swt.custom.StyledText`) in those signatures.
+Use abstractions such as `TextComposite` instead.
+See the `GuiPluginWebCompatibilityTest` and 
xref:hopweb/hopweb-antipatterns.adoc[Hop Web antipatterns].
+
+== Contributing a toolbar button: @GuiToolbarElement
+
+Annotation (package `org.apache.hop.core.gui.plugin.toolbar`):
+
+[cols="1,3",options="header"]
+|===
+| Attribute | Meaning
+
+| `root`
+| **Toolbar root id** this item belongs to (must match the host’s `ID_TOOLBAR` 
/ parent id).
+
+| `id`
+| Unique item id (use a stable, namespaced string so other plugins can refer 
to or filter it).
+
+| `image`
+| SVG path loaded via the plugin classloader (for example `json-input.svg` or 
`ui/images/search.svg`).
+
+| `toolTip`
+| Tooltip; use `i18n::Key.Name` for translation via `BaseMessages` / resource 
bundles.
+
+| `label`
+| Optional text label (buttons are usually icon-only).
+
+| `type`
+| Defaults to `BUTTON`; also supports combo, text, label, checkbox, and so on.
+
+| `separator`
+| If `true`, draws a separator before this item.
+
+| `extraWidth` / `alignRight` / …
+| Layout hints for non-button types.
+|===
+
+=== Preferred listener shape (plugins)
+
+Use a **public static** method with one parameter: the host widget type (or a 
supertype / interface).
+
+[source,java]
+----
+@GuiToolbarElement(
+    root = TextComposite.ID_TOOLBAR,
+    id = "textcomposite-toolbar-20010-format-json",
+    toolTip = "i18n::MyPlugin.FormatJson.ToolTip",
+    separator = true,
+    image = "json-input.svg")
+public static void formatJson(TextComposite text) {
+  // use text.getText(), text.insert(...), text.getStyleType(), ...
+}
+----
+
+`GuiToolbarWidgets` / `BaseGuiWidgets` resolve listeners as follows:
+
+1. If a host object was registered on the toolbar, look for a **static** 
method whose single parameter is assignable from that host’s class.
+2. Otherwise fall back to an instance method on a plugin singleton (no-arg, or 
with `Event`).
+
+Static methods that take the host object are the reliable pattern for 
**per-widget** toolbars (`TableView`, `TextComposite`), because many instances 
exist at once.
+
+=== Built-in items on the host class
+
+Hosts such as `TableView` and `TextComposite` are themselves `@GuiPlugin` 
classes.
+Their built-in actions are also `@GuiToolbarElement` methods (sometimes static 
helpers on the abstract base).
+Plugin ids should use higher numeric ranges (for example `20000+`) so they 
sort after core items.
+
+== Showing or hiding buttons: @GuiToolbarElementFilter
+
+This annotation is powerful and easy to overlook.
+It is the supported way to **conditionally show** toolbar items (for example 
only for SQL editors, only for database metadata lines, only when editable).
+
+=== Contract
+
+[source,java]
+----
+@GuiToolbarElementFilter(parentId = TextComposite.ID_TOOLBAR)
+public static boolean isButtonShown(String itemId, Object guiPluginInstance) {
+  // return true → show the item; false → hide it
+}
+----
+
+[cols="1,3",options="header"]
+|===
+| Rule | Detail
+
+| Annotation
+| `@GuiToolbarElementFilter(parentId = "<toolbar-root-id>")` — `parentId` must 
match the toolbar `root` / host `ID_TOOLBAR`.
+
+| Method shape
+| **Must** be `public static boolean methodName(String itemId, Object 
guiPluginInstance)`.
+Hop looks up the method with reflection using exactly `String.class` and 
`Object.class`.
+A signature of `(String, TextComposite)` will **not** be found.
+
+| `itemId`
+| The `@GuiToolbarElement.id` of the item currently being decided.
+
+| `guiPluginInstance`
+| The **host** object registered with the toolbar (for example the 
`TextComposite` or `TableView` instance), typed as `Object` — cast carefully.
+
+| Return value
+| `true` = show, `false` = hide.
+
+| Multiple filters
+| All registered filters for that toolbar root are consulted.
+If **any** filter returns `false` for an item, the item is hidden.
+|===
+
+=== Critical pitfall: do not hide everyone else’s buttons
+
+Filters run for **every** toolbar item under that root, not only for your 
button.
+If your filter returns `false` for ids it does not own, you will hide undo, 
cut, find, and every other contribution.
+
+Always start with:
+
+[source,java]
+----
+if (!MY_BUTTON_ID.equals(itemId)) {
+  return true; // leave all other items alone
+}
+// then decide for MY_BUTTON_ID only
+----
+
+=== Critical pitfall: filters run mid-construction
+
+`GuiToolbarElementFilter` methods are invoked when the host builds its toolbar 
— for `TextComposite` that is **inside the base constructor**, before 
subclasses create the underlying `Text` / `StyledText` widget.
+
+Safe in a filter:
+
+* Values set **before** `addToolbar()` (for example constructor `styleType`)
+* Identity / type of the host object (`instanceof TextComposite`)
+* Other fields already initialized on the host
+
+**Not** safe in a filter (defer to the button action instead):
+
+* `isEditable()`, `getText()`, `getSelectionText()`, caret position, and 
similar methods that touch the child text control
+* Anything that assumes the host’s full construction has finished
+
+[source,java]
+----
+// Filter: only what is available at construction time
+@GuiToolbarElementFilter(parentId = TextComposite.ID_TOOLBAR)
+public static boolean isButtonShown(String buttonId, Object guiPluginInstance) 
{
+  if (!MY_ID.equals(buttonId)) {
+    return true;
+  }
+  if (!(guiPluginInstance instanceof TextComposite text)) {
+    return false;
+  }
+  return TextComposite.STYLE_TYPE_JSON.equalsIgnoreCase(text.getStyleType());
+}
+
+// Action: runtime checks (editability, content, …)
+@GuiToolbarElement(root = TextComposite.ID_TOOLBAR, id = MY_ID, ...)
+public static void formatJson(TextComposite text) {
+  if (text == null || text.isDisposed() || !text.isEditable()) {
+    return;
+  }
+  // ...
+}
+----
+
+=== Working example (JSON format on TextComposite)
+
+From the JSON transform plugin (`TextCompositeToolbarJsonFormatButton`):
+
+[source,java]
+----
+@GuiToolbarElementFilter(parentId = TextComposite.ID_TOOLBAR)
+public static boolean isButtonShown(String buttonId, Object guiPluginInstance) 
{
+  if (!ID_TOOLBAR_FORMAT_JSON.equals(buttonId)) {
+    return true;
+  }
+  if (!(guiPluginInstance instanceof TextComposite textComposite)) {
+    return false;
+  }
+  // styleType only — isEditable() is checked in formatJson() at click time
+  return 
TextComposite.STYLE_TYPE_JSON.equalsIgnoreCase(textComposite.getStyleType());
+}
+----
+
+Another core example: `MetaSelectionLineClearDbCacheToolbarItem` filters a 
clear-cache button so it only appears for database metadata lines.
+
+== Extensible host: TextComposite toolbar
+
+`org.apache.hop.ui.core.widget.TextComposite` is the multi-line editor used 
for SQL, scripts, logs, formulas, JSON, and more.
+It exposes:
+
+[cols="1,2",options="header"]
+|===
+| API | Purpose
+
+| `TextComposite.ID_TOOLBAR` (`"TextComposite-Toolbar"`)
+| Root id for `@GuiToolbarElement.root` / filter `parentId`.
+
+| Built-in items
+| Undo/redo, cut/copy/paste, select all, find, find/replace (when the global 
“show text editor toolbar” option is on).
+
+| Constructor `styleType` / `getStyleType()`
+| Semantic content type for plugins (must be set in the constructor; see 
below).
+
+| `selectAll()`, `insert()`, `getText()`, `setText()`, …
+| Safe editing API that works on desktop and Hop Web backends.
+|===
+
+=== styleType (do not use instanceof)
+
+On Hop Web, dialogs often replace specialized classes with a plain 
`StyledTextComp`.
+So `instanceof SQLStyledTextComp` is **false** on Hop Web even when the user 
is editing SQL.
+Always use `getStyleType()` when deciding plugin behavior.
+
+IMPORTANT: Toolbar filters run when the `TextComposite` is **constructed** 
(during `addToolbar()`).
+Pass `styleType` as a **constructor argument** so filters see the correct 
value.
+Calling `setStyleType(...)` after construction does **not** rebuild the 
toolbar or re-run filters.
+
+[source,java]
+----
+// Correct: style type is known before the toolbar is built
+wSql =
+    EnvironmentUtils.getInstance().isWeb()
+        ? new StyledTextComp(variables, shell, SWT.MULTI | ..., 
TextComposite.STYLE_TYPE_SQL)
+        : new SQLStyledTextComp(variables, shell, SWT.MULTI | ...); // 
defaults to SQL
+----
+
+Known style type constants:
+
+[cols="1,2",options="header"]
+|===
+| Constant | Typical use
+
+| `STYLE_TYPE_GENERIC`
+| Default / unspecified
+
+| `STYLE_TYPE_SQL`
+| SQL scripts and queries
+
+| `STYLE_TYPE_JAVASCRIPT` / `STYLE_TYPE_JAVA` / `STYLE_TYPE_SCRIPT`
+| Scripting languages
+
+| `STYLE_TYPE_JSON`
+| JSON documents or query payloads
+
+| `STYLE_TYPE_LOG` / `STYLE_TYPE_DIFF` / `STYLE_TYPE_REGEX` / 
`STYLE_TYPE_FORMULA` / `STYLE_TYPE_TEXT`
+| Logs, diffs, expressions, free text
+
+| `STYLE_TYPE_CQL` / `STYLE_TYPE_SOQL` / `STYLE_TYPE_DROOLS`
+| Domain languages
+
+| Free-form strings
+| Plugins may introduce their own labels (for example `"Cypher"`) if needed
+|===
+
+Specialized subclasses set defaults in their constructors (`SQLStyledTextComp` 
→ `SQL`, and so on).
+Call sites that use a plain `StyledTextComp` / `StyledTextVar` (including Hop 
Web stand-ins) must pass the matching `styleType` constructor argument.
+
+=== Ideas enabled by this SPI
+
+Because any plugin can attach to `TextComposite.ID_TOOLBAR` and branch on 
`styleType`, you can ship optional tooling without bloating core dialogs:
+
+* SQL pretty-print / format
+* SQL lineage or “explain”
+* Visual SQL builders
+* JSON / XML formatters (see the sample JSON format button)
+* Language-specific helpers (Cypher, SOQL, Drools, …)
+
+Keep heavy or optional features in plugins; keep the host widget small.
+
+=== Preferences and opt-out
+
+* Global: configuration option **Show text editor toolbar** 
(`PropsUi.isShowTextCompositeToolbar()`).
+* Per instance: construct with `toolbarEnabled = false`, or pass item ids to 
remove.
+
+== Extensible host: TableView toolbar
+
+`TableView` uses the same mechanism with root id `TableView.ID_TOOLBAR` 
(`"TableView-Toolbar"`).
+
+Built-in items cover row insert/delete, clipboard, filter, navigate-to-column, 
undo/redo, and more.
+Plugins contribute the same way — for example export-to-Excel / export-to-CSV 
toolbar buttons in the Excel and text-file plugins:
+
+[source,java]
+----
+@GuiToolbarElement(
+    root = TableView.ID_TOOLBAR,
+    id = "tableview-toolbar-30000-export-to-excel",
+    toolTip = "i18n::ExcelWidget.ExportToolbarButton.ToolTip",
+    separator = true,
+    image = "excelwriter.svg")
+public static void export(TableView tableView) {
+  // ...
+}
+----
+
+== How the host builds the toolbar (for widget authors)
+
+If you own a new composite that should accept plugin buttons:
+
+. Choose a stable public root id, for example `public static final String 
ID_TOOLBAR = "MyWidget-Toolbar";`
+. Annotate the host class (or a related `@GuiPlugin`) with built-in 
`@GuiToolbarElement` methods if needed
+. On construction:
++
+[source,java]
+----
+toolbarWidgets = new GuiToolbarWidgets();
+// Register under the class name elements expect to resolve
+toolbarWidgets.registerGuiPluginObject(MyWidget.class.getName(), this);
+
+IToolbarContainer container =
+    ToolbarFacade.createToolbarContainer(this, SWT.WRAP | SWT.LEFT | 
SWT.HORIZONTAL);
+toolbar = container.getControl();
+// layout toolbar at the top...
+toolbarWidgets.createToolbarWidgets(container, ID_TOOLBAR, removeToolItems);
+----
+. Prefer `ToolbarFacade` / `IToolbarContainer` so desktop (SWT `ToolBar`) and 
Hop Web (composite row layout) share one path.
+
+== i18n for tooltips
+
+Prefer:
+
+[source,java]
+----
+toolTip = "i18n::MyClass.MyButton.ToolTip"
+----
+
+with a messages file under the plugin package, for example:
+
+[source]
+----
+plugins/.../src/main/resources/org/example/myplugin/messages/messages_en_US.properties
+----
+
+[source,properties]
+----
+MyClass.MyButton.ToolTip=Pretty-print JSON
+----
+
+Escape variable-like tokens in properties with single quotes when needed 
(`'${VAR}'`) so they are not treated as interpolation.
+
+== Checklist for a new toolbar contribution
+
+* [ ] Class annotated with `@GuiPlugin`
+* [ ] Button method uses `@GuiToolbarElement(root = Host.ID_TOOLBAR, id = "…", 
image = "…", toolTip = "i18n::…")`
+* [ ] Listener is `public static void name(HostType host)` (or a safe 
supertype)
+* [ ] No desktop-only SWT types in the GuiPlugin class signatures
+* [ ] If visibility depends on context: add `@GuiToolbarElementFilter(parentId 
= Host.ID_TOOLBAR)` with signature `(String, Object)`
+* [ ] Filter returns `true` for all item ids that are **not** yours
+* [ ] Filter only uses state available at construction (`styleType`, not 
`isEditable()` / `getText()`)
+* [ ] Runtime checks (`isEditable()`, empty text, …) live in the action method
+* [ ] For `TextComposite`: pass `styleType` in the constructor; use 
`getStyleType()` in filters/actions (not `instanceof`)
+* [ ] SVG image is on the plugin classpath; i18n key exists in 
`messages_en_US.properties`
+* [ ] Manual check on desktop and, if relevant, Hop Web
+
+== Reference source locations
+
+[cols="1,2",options="header"]
+|===
+| Topic | Location
+
+| Toolbar widgets runtime
+| `ui/.../gui/GuiToolbarWidgets.java`, `BaseGuiWidgets.java`
+
+| Annotations
+| `core/.../gui/plugin/toolbar/GuiToolbarElement.java`, 
`GuiToolbarElementFilter.java`
+
+| Registration scan
+| `ui/.../hopgui/HopGuiEnvironment.initGuiPlugins()`
+
+| Text editor host
+| `ui/.../widget/TextComposite.java`
+
+| Table host
+| `ui/.../widget/TableView.java`
+
+| Sample JSON format button
+| `plugins/transforms/json/.../ui/TextCompositeToolbarJsonFormatButton.java`
+
+| Sample table export buttons
+| `plugins/transforms/excel/.../TableViewExportToExcelToolbarButton.java`, CSV 
equivalent under textfile
+
+| Metadata line filter example
+| `ui/.../database/MetaSelectionLineClearDbCacheToolbarItem.java`
+|===
diff --git 
a/docs/hop-dev-manual/modules/ROOT/pages/hopweb/hopweb-antipatterns.adoc 
b/docs/hop-dev-manual/modules/ROOT/pages/hopweb/hopweb-antipatterns.adoc
index d6e363f414..c30b5be8be 100644
--- a/docs/hop-dev-manual/modules/ROOT/pages/hopweb/hopweb-antipatterns.adoc
+++ b/docs/hop-dev-manual/modules/ROOT/pages/hopweb/hopweb-antipatterns.adoc
@@ -20,6 +20,9 @@ under the License.
 
 Due to the 
https://www.eclipse.org/rap/developers-guide/devguide.php?topic=rwt.html[differences
 between RWT and SWT], there are some anti-patterns that should be avoided when 
developing webSpoon and (webSpoon-) compatible plugins.
 
+GUI toolbar plugins (`@GuiPlugin`) must not put desktop-only SWT types in 
method signatures; use abstractions such as `TextComposite` and content type 
via `getStyleType()`.
+See xref:gui-plugins-toolbars.adoc[GUI plugins and toolbars].
+
 == Avoid sharing session-unique instances among sessions
 
 The *singleton pattern* is a design pattern where only one instance of a class 
is created.
diff --git a/docs/hop-dev-manual/modules/ROOT/pages/index.adoc 
b/docs/hop-dev-manual/modules/ROOT/pages/index.adoc
index f201572d8f..302daf3a99 100644
--- a/docs/hop-dev-manual/modules/ROOT/pages/index.adoc
+++ b/docs/hop-dev-manual/modules/ROOT/pages/index.adoc
@@ -23,6 +23,7 @@ under the License.
 * xref:docker-build.adoc[Docker Build Script]
 * xref:integration-testing.adoc[Integration Testing]
 * xref:plugin-development.adoc[Plugin Development]
+* xref:gui-plugins-toolbars.adoc[GUI plugins and toolbars]
 * xref:lineage.adoc[Lineage observation hub]
 * xref:plugin-samples.adoc[Plugin Samples]
 * xref:svg-files.adoc[SVG Files]
diff --git a/docs/hop-dev-manual/modules/ROOT/pages/plugin-development.adoc 
b/docs/hop-dev-manual/modules/ROOT/pages/plugin-development.adoc
index 8873bd3a1c..41ff1e272a 100644
--- a/docs/hop-dev-manual/modules/ROOT/pages/plugin-development.adoc
+++ b/docs/hop-dev-manual/modules/ROOT/pages/plugin-development.adoc
@@ -20,6 +20,9 @@ under the License.
 
 This page explains how to develop new plugins with references to make 
development easy.
 
+For contributing **toolbar buttons** and other GUI-only extensions 
(`@GuiPlugin`, `@GuiToolbarElement`, `@GuiToolbarElementFilter`), see 
xref:gui-plugins-toolbars.adoc[GUI plugins and toolbars].
+That page covers the TextComposite and TableView extension points (SQL 
formatters, JSON tools, table export, and similar).
+
 == Plugin Registry
 
 Hop keeps a central registry of all the available Hop plugins.
diff --git 
a/plugins/actions/eval/src/main/java/org/apache/hop/workflow/actions/eval/ActionEvalDialog.java
 
b/plugins/actions/eval/src/main/java/org/apache/hop/workflow/actions/eval/ActionEvalDialog.java
index 15a89d9e37..733791ad47 100644
--- 
a/plugins/actions/eval/src/main/java/org/apache/hop/workflow/actions/eval/ActionEvalDialog.java
+++ 
b/plugins/actions/eval/src/main/java/org/apache/hop/workflow/actions/eval/ActionEvalDialog.java
@@ -97,7 +97,8 @@ public class ActionEvalDialog extends ActionDialog {
               action,
               shell,
               SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL,
-              false);
+              false,
+              TextComposite.STYLE_TYPE_JAVASCRIPT);
     } else {
       wScript =
           new JavaScriptStyledTextComp(
diff --git 
a/plugins/actions/evaluatetablecontent/src/main/java/org/apache/hop/workflow/actions/evaluatetablecontent/ActionEvalTableContentDialog.java
 
b/plugins/actions/evaluatetablecontent/src/main/java/org/apache/hop/workflow/actions/evaluatetablecontent/ActionEvalTableContentDialog.java
index ae17fb1ba1..bea65b9c93 100644
--- 
a/plugins/actions/evaluatetablecontent/src/main/java/org/apache/hop/workflow/actions/evaluatetablecontent/ActionEvalTableContentDialog.java
+++ 
b/plugins/actions/evaluatetablecontent/src/main/java/org/apache/hop/workflow/actions/evaluatetablecontent/ActionEvalTableContentDialog.java
@@ -34,6 +34,7 @@ import org.apache.hop.ui.core.dialog.BaseDialog;
 import org.apache.hop.ui.core.dialog.MessageBox;
 import org.apache.hop.ui.core.widget.MetaSelectionLine;
 import org.apache.hop.ui.core.widget.StyledTextComp;
+import org.apache.hop.ui.core.widget.TextComposite;
 import org.apache.hop.ui.core.widget.TextVar;
 import org.apache.hop.ui.workflow.action.ActionDialog;
 import org.apache.hop.workflow.WorkflowMeta;
@@ -408,7 +409,10 @@ public class ActionEvalTableContentDialog extends 
ActionDialog {
 
     wSql =
         new StyledTextComp(
-            action, wCustomGroup, SWT.MULTI | SWT.LEFT | SWT.BORDER | 
SWT.H_SCROLL | SWT.V_SCROLL);
+            action,
+            wCustomGroup,
+            SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL,
+            TextComposite.STYLE_TYPE_SQL);
     PropsUi.setLook(wSql, Props.WIDGET_STYLE_FIXED);
     wSql.addModifyListener(lsMod);
     FormData fdSql = new FormData();
diff --git 
a/plugins/actions/sendnagiospassivecheck/src/main/java/org/apache/hop/workflow/actions/sendnagiospassivecheck/ActionSendNagiosPassiveCheckDialog.java
 
b/plugins/actions/sendnagiospassivecheck/src/main/java/org/apache/hop/workflow/actions/sendnagiospassivecheck/ActionSendNagiosPassiveCheckDialog.java
index 8f30594015..c7b3f710e1 100644
--- 
a/plugins/actions/sendnagiospassivecheck/src/main/java/org/apache/hop/workflow/actions/sendnagiospassivecheck/ActionSendNagiosPassiveCheckDialog.java
+++ 
b/plugins/actions/sendnagiospassivecheck/src/main/java/org/apache/hop/workflow/actions/sendnagiospassivecheck/ActionSendNagiosPassiveCheckDialog.java
@@ -29,6 +29,7 @@ import org.apache.hop.ui.core.dialog.MessageBox;
 import org.apache.hop.ui.core.gui.GuiResource;
 import org.apache.hop.ui.core.widget.LabelTextVar;
 import org.apache.hop.ui.core.widget.StyledTextComp;
+import org.apache.hop.ui.core.widget.TextComposite;
 import org.apache.hop.ui.workflow.action.ActionDialog;
 import org.apache.hop.workflow.WorkflowMeta;
 import org.apache.hop.workflow.action.IAction;
@@ -358,7 +359,8 @@ public class ActionSendNagiosPassiveCheckDialog extends 
ActionDialog {
         new StyledTextComp(
             variables,
             wMessageGroup,
-            SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL);
+            SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL,
+            TextComposite.STYLE_TYPE_TEXT);
     PropsUi.setLook(wMessage);
     wMessage.addModifyListener(lsMod);
     FormData fdMessage = new FormData();
diff --git 
a/plugins/actions/snmptrap/src/main/java/org/apache/hop/workflow/actions/snmptrap/ActionSNMPTrapDialog.java
 
b/plugins/actions/snmptrap/src/main/java/org/apache/hop/workflow/actions/snmptrap/ActionSNMPTrapDialog.java
index b70cbde3be..0384158927 100644
--- 
a/plugins/actions/snmptrap/src/main/java/org/apache/hop/workflow/actions/snmptrap/ActionSNMPTrapDialog.java
+++ 
b/plugins/actions/snmptrap/src/main/java/org/apache/hop/workflow/actions/snmptrap/ActionSNMPTrapDialog.java
@@ -29,6 +29,7 @@ import org.apache.hop.ui.core.dialog.MessageBox;
 import org.apache.hop.ui.core.gui.GuiResource;
 import org.apache.hop.ui.core.widget.LabelTextVar;
 import org.apache.hop.ui.core.widget.StyledTextComp;
+import org.apache.hop.ui.core.widget.TextComposite;
 import org.apache.hop.ui.workflow.action.ActionDialog;
 import org.apache.hop.workflow.WorkflowMeta;
 import org.apache.hop.workflow.action.IAction;
@@ -356,7 +357,10 @@ public class ActionSNMPTrapDialog extends ActionDialog {
 
     wMessage =
         new StyledTextComp(
-            action, wMessageGroup, SWT.MULTI | SWT.LEFT | SWT.BORDER | 
SWT.H_SCROLL | SWT.V_SCROLL);
+            action,
+            wMessageGroup,
+            SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL,
+            TextComposite.STYLE_TYPE_TEXT);
     PropsUi.setLook(wMessage);
     wMessage.addModifyListener(lsMod);
     FormData fdMessage = new FormData();
diff --git 
a/plugins/actions/sql/src/main/java/org/apache/hop/workflow/actions/sql/ActionSqlDialog.java
 
b/plugins/actions/sql/src/main/java/org/apache/hop/workflow/actions/sql/ActionSqlDialog.java
index 9f15331761..d0bd68b3c2 100644
--- 
a/plugins/actions/sql/src/main/java/org/apache/hop/workflow/actions/sql/ActionSqlDialog.java
+++ 
b/plugins/actions/sql/src/main/java/org/apache/hop/workflow/actions/sql/ActionSqlDialog.java
@@ -269,7 +269,10 @@ public class ActionSqlDialog extends ActionDialog {
     wSql =
         EnvironmentUtils.getInstance().isWeb()
             ? new StyledTextComp(
-                variables, shell, SWT.MULTI | SWT.LEFT | SWT.BORDER | 
SWT.H_SCROLL | SWT.V_SCROLL)
+                variables,
+                shell,
+                SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL | 
SWT.V_SCROLL,
+                TextComposite.STYLE_TYPE_SQL)
             : new SQLStyledTextComp(
                 variables, shell, SWT.MULTI | SWT.LEFT | SWT.BORDER | 
SWT.H_SCROLL | SWT.V_SCROLL);
     wSql.addLineStyleListener(getSqlReservedWords());
diff --git 
a/plugins/actions/waitforsql/src/main/java/org/apache/hop/workflow/actions/waitforsql/ActionWaitForSqlDialog.java
 
b/plugins/actions/waitforsql/src/main/java/org/apache/hop/workflow/actions/waitforsql/ActionWaitForSqlDialog.java
index 1940beb5fa..2f5562492c 100644
--- 
a/plugins/actions/waitforsql/src/main/java/org/apache/hop/workflow/actions/waitforsql/ActionWaitForSqlDialog.java
+++ 
b/plugins/actions/waitforsql/src/main/java/org/apache/hop/workflow/actions/waitforsql/ActionWaitForSqlDialog.java
@@ -477,7 +477,8 @@ public class ActionWaitForSqlDialog extends ActionDialog {
             ? new StyledTextComp(
                 action,
                 wCustomGroup,
-                SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL | 
SWT.V_SCROLL)
+                SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL | 
SWT.V_SCROLL,
+                TextComposite.STYLE_TYPE_SQL)
             : new SQLStyledTextComp(
                 action,
                 wCustomGroup,
diff --git 
a/plugins/misc/git/src/main/java/org/apache/hop/git/GitPerspective.java 
b/plugins/misc/git/src/main/java/org/apache/hop/git/GitPerspective.java
index 66699922be..b84d9f7085 100644
--- a/plugins/misc/git/src/main/java/org/apache/hop/git/GitPerspective.java
+++ b/plugins/misc/git/src/main/java/org/apache/hop/git/GitPerspective.java
@@ -1678,6 +1678,7 @@ public class GitPerspective implements IHopPerspective {
       wDiffStyled =
           new DiffStyledTextComp(
               hopGui.getVariables(), parent, SWT.MULTI | SWT.BORDER | 
SWT.H_SCROLL | SWT.V_SCROLL);
+      // styleType STYLE_TYPE_DIFF is set by DiffStyledTextComp constructor
       PropsUi.setLook(wDiffStyled, Props.WIDGET_STYLE_FIXED);
 
       wDiff = wDiffStyled;
diff --git 
a/plugins/misc/git/src/main/java/org/apache/hop/git/info/DiffStyledTextComp.java
 
b/plugins/misc/git/src/main/java/org/apache/hop/git/info/DiffStyledTextComp.java
index fdc444a7e0..4e28b98134 100644
--- 
a/plugins/misc/git/src/main/java/org/apache/hop/git/info/DiffStyledTextComp.java
+++ 
b/plugins/misc/git/src/main/java/org/apache/hop/git/info/DiffStyledTextComp.java
@@ -43,7 +43,7 @@ public class DiffStyledTextComp extends StyledTextVar {
       new Color(Display.getDefault(), 153, 102, 0); // Brown/Orange
 
   public DiffStyledTextComp(IVariables variables, Composite parent, int style) 
{
-    super(variables, parent, style, false, false); // No variable support 
needed
+    super(variables, parent, style, false, false, true, STYLE_TYPE_DIFF); // 
No variable support
     // Set read-only and disable editing
     getTextWidget().setEditable(false);
   }
diff --git 
a/plugins/misc/git/src/main/java/org/apache/hop/git/info/GitInfoExplorerFileTypeHandler.java
 
b/plugins/misc/git/src/main/java/org/apache/hop/git/info/GitInfoExplorerFileTypeHandler.java
index efca243100..32d2014f16 100644
--- 
a/plugins/misc/git/src/main/java/org/apache/hop/git/info/GitInfoExplorerFileTypeHandler.java
+++ 
b/plugins/misc/git/src/main/java/org/apache/hop/git/info/GitInfoExplorerFileTypeHandler.java
@@ -355,6 +355,7 @@ public class GitInfoExplorerFileTypeHandler extends 
BaseExplorerFileTypeHandler
               hopGui.getVariables(),
               wDiffComposite,
               SWT.MULTI | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL);
+      // styleType STYLE_TYPE_DIFF is set by DiffStyledTextComp constructor
       PropsUi.setLook(wDiffStyled, Props.WIDGET_STYLE_FIXED);
       wDiffStyled.setLayoutData(fdDiff);
       wDiff = wDiffStyled;
diff --git 
a/plugins/tech/cassandra/src/main/java/org/apache/hop/pipeline/transforms/cassandrainput/CassandraInputDialog.java
 
b/plugins/tech/cassandra/src/main/java/org/apache/hop/pipeline/transforms/cassandrainput/CassandraInputDialog.java
index 599c033c0d..73c28dc984 100644
--- 
a/plugins/tech/cassandra/src/main/java/org/apache/hop/pipeline/transforms/cassandrainput/CassandraInputDialog.java
+++ 
b/plugins/tech/cassandra/src/main/java/org/apache/hop/pipeline/transforms/cassandrainput/CassandraInputDialog.java
@@ -37,6 +37,7 @@ import org.apache.hop.ui.core.dialog.PreviewRowsDialog;
 import org.apache.hop.ui.core.dialog.ShowMessageDialog;
 import org.apache.hop.ui.core.widget.MetaSelectionLine;
 import org.apache.hop.ui.core.widget.StyledTextComp;
+import org.apache.hop.ui.core.widget.TextComposite;
 import org.apache.hop.ui.core.widget.TextVar;
 import org.apache.hop.ui.pipeline.dialog.PipelinePreviewProgressDialog;
 import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
@@ -182,7 +183,10 @@ public class CassandraInputDialog extends 
BaseTransformDialog {
 
     wCql =
         new StyledTextComp(
-            variables, shell, SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL 
| SWT.V_SCROLL);
+            variables,
+            shell,
+            SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL,
+            TextComposite.STYLE_TYPE_CQL);
     PropsUi.setLook(wCql, Props.WIDGET_STYLE_FIXED);
     FormData fdCql = new FormData();
     fdCql.left = new FormAttachment(0, 0);
diff --git 
a/plugins/tech/mongodb/src/main/java/org/apache/hop/pipeline/transforms/mongodbdelete/MongoDbDeleteDialog.java
 
b/plugins/tech/mongodb/src/main/java/org/apache/hop/pipeline/transforms/mongodbdelete/MongoDbDeleteDialog.java
index 407f6febaa..2d94853b9d 100644
--- 
a/plugins/tech/mongodb/src/main/java/org/apache/hop/pipeline/transforms/mongodbdelete/MongoDbDeleteDialog.java
+++ 
b/plugins/tech/mongodb/src/main/java/org/apache/hop/pipeline/transforms/mongodbdelete/MongoDbDeleteDialog.java
@@ -48,6 +48,7 @@ import org.apache.hop.ui.core.widget.ColumnInfo;
 import org.apache.hop.ui.core.widget.MetaSelectionLine;
 import org.apache.hop.ui.core.widget.StyledTextComp;
 import org.apache.hop.ui.core.widget.TableView;
+import org.apache.hop.ui.core.widget.TextComposite;
 import org.apache.hop.ui.core.widget.TextVar;
 import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
 import org.bson.Document;
@@ -413,7 +414,8 @@ public class MongoDbDeleteDialog extends 
BaseTransformDialog {
         new StyledTextComp(
             variables,
             wFieldsComp,
-            SWT.FULL_SELECTION | SWT.MULTI | SWT.BORDER | SWT.H_SCROLL | 
SWT.V_SCROLL);
+            SWT.FULL_SELECTION | SWT.MULTI | SWT.BORDER | SWT.H_SCROLL | 
SWT.V_SCROLL,
+            TextComposite.STYLE_TYPE_JSON);
     PropsUi.setLook(wstJsonQueryView, Props.WIDGET_STYLE_FIXED);
     wstJsonQueryView.addModifyListener(lsMod);
 
diff --git 
a/plugins/tech/mongodb/src/main/java/org/apache/hop/pipeline/transforms/mongodbinput/MongoDbInputDialog.java
 
b/plugins/tech/mongodb/src/main/java/org/apache/hop/pipeline/transforms/mongodbinput/MongoDbInputDialog.java
index 1cc468a081..d2bd08b814 100644
--- 
a/plugins/tech/mongodb/src/main/java/org/apache/hop/pipeline/transforms/mongodbinput/MongoDbInputDialog.java
+++ 
b/plugins/tech/mongodb/src/main/java/org/apache/hop/pipeline/transforms/mongodbinput/MongoDbInputDialog.java
@@ -50,6 +50,7 @@ import org.apache.hop.ui.core.widget.ColumnInfo;
 import org.apache.hop.ui.core.widget.MetaSelectionLine;
 import org.apache.hop.ui.core.widget.StyledTextComp;
 import org.apache.hop.ui.core.widget.TableView;
+import org.apache.hop.ui.core.widget.TextComposite;
 import org.apache.hop.ui.core.widget.TextVar;
 import org.apache.hop.ui.pipeline.dialog.PipelinePreviewProgressDialog;
 import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
@@ -272,7 +273,10 @@ public class MongoDbInputDialog extends 
BaseTransformDialog {
 
     wJsonQuery =
         new StyledTextComp(
-            variables, wQueryComp, SWT.MULTI | SWT.LEFT | SWT.BORDER | 
SWT.H_SCROLL | SWT.V_SCROLL);
+            variables,
+            wQueryComp,
+            SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL,
+            TextComposite.STYLE_TYPE_JSON);
     PropsUi.setLook(wJsonQuery, Props.WIDGET_STYLE_FIXED);
     wJsonQuery.addModifyListener(lsMod);
 
diff --git 
a/plugins/tech/mongodb/src/main/java/org/apache/hop/workflow/actions/mql/ActionMqlDialog.java
 
b/plugins/tech/mongodb/src/main/java/org/apache/hop/workflow/actions/mql/ActionMqlDialog.java
index 93b44856d8..79eeb9f818 100644
--- 
a/plugins/tech/mongodb/src/main/java/org/apache/hop/workflow/actions/mql/ActionMqlDialog.java
+++ 
b/plugins/tech/mongodb/src/main/java/org/apache/hop/workflow/actions/mql/ActionMqlDialog.java
@@ -264,8 +264,10 @@ public class ActionMqlDialog extends ActionDialog {
 
     wMql =
         new StyledTextComp(
-            variables, shell, SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL 
| SWT.V_SCROLL);
-
+            variables,
+            shell,
+            SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL,
+            TextComposite.STYLE_TYPE_JSON);
     PropsUi.setLook(wMql, Props.WIDGET_STYLE_FIXED);
     FormData fdMql = new FormData();
     fdMql.left = new FormAttachment(0, 0);
diff --git 
a/plugins/tech/salesforce/src/main/java/org/apache/hop/pipeline/transforms/salesforceinput/SalesforceInputDialog.java
 
b/plugins/tech/salesforce/src/main/java/org/apache/hop/pipeline/transforms/salesforceinput/SalesforceInputDialog.java
index 2be97edc74..8f117e1306 100644
--- 
a/plugins/tech/salesforce/src/main/java/org/apache/hop/pipeline/transforms/salesforceinput/SalesforceInputDialog.java
+++ 
b/plugins/tech/salesforce/src/main/java/org/apache/hop/pipeline/transforms/salesforceinput/SalesforceInputDialog.java
@@ -54,6 +54,7 @@ import org.apache.hop.ui.core.widget.LabelTextVar;
 import org.apache.hop.ui.core.widget.MetaSelectionLine;
 import org.apache.hop.ui.core.widget.StyledTextComp;
 import org.apache.hop.ui.core.widget.TableView;
+import org.apache.hop.ui.core.widget.TextComposite;
 import org.apache.hop.ui.core.widget.TextVar;
 import org.apache.hop.ui.pipeline.dialog.PipelinePreviewProgressDialog;
 import org.apache.hop.ui.pipeline.transform.ComponentSelectionListener;
@@ -441,7 +442,8 @@ public class SalesforceInputDialog extends 
SalesforceTransformDialog {
         new StyledTextComp(
             variables,
             wSettingsGroup,
-            SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL);
+            SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL,
+            TextComposite.STYLE_TYPE_SOQL);
     wCondition.setToolTipText(
         BaseMessages.getString(PKG, 
"SalesforceInputDialog.Condition.Tooltip"));
     PropsUi.setLook(wCondition, Props.WIDGET_STYLE_FIXED);
@@ -514,7 +516,8 @@ public class SalesforceInputDialog extends 
SalesforceTransformDialog {
         new StyledTextComp(
             variables,
             wSettingsGroup,
-            SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL);
+            SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL,
+            TextComposite.STYLE_TYPE_SOQL);
     PropsUi.setLook(wQuery, Props.WIDGET_STYLE_FIXED);
     wQuery.addModifyListener(lsMod);
     FormData fdQuery = new FormData();
diff --git 
a/plugins/transforms/databasejoin/src/main/java/org/apache/hop/pipeline/transforms/databasejoin/DatabaseJoinDialog.java
 
b/plugins/transforms/databasejoin/src/main/java/org/apache/hop/pipeline/transforms/databasejoin/DatabaseJoinDialog.java
index d11b0bd5a5..2454a5f1ff 100644
--- 
a/plugins/transforms/databasejoin/src/main/java/org/apache/hop/pipeline/transforms/databasejoin/DatabaseJoinDialog.java
+++ 
b/plugins/transforms/databasejoin/src/main/java/org/apache/hop/pipeline/transforms/databasejoin/DatabaseJoinDialog.java
@@ -168,7 +168,10 @@ public class DatabaseJoinDialog extends 
BaseTransformDialog {
     wSql =
         EnvironmentUtils.getInstance().isWeb()
             ? new StyledTextComp(
-                variables, shell, SWT.MULTI | SWT.LEFT | SWT.BORDER | 
SWT.H_SCROLL | SWT.V_SCROLL)
+                variables,
+                shell,
+                SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL | 
SWT.V_SCROLL,
+                TextComposite.STYLE_TYPE_SQL)
             : new SQLStyledTextComp(
                 variables, shell, SWT.MULTI | SWT.LEFT | SWT.BORDER | 
SWT.H_SCROLL | SWT.V_SCROLL);
     wSql.addLineStyleListener(getSqlReservedWords());
diff --git 
a/plugins/transforms/drools/src/main/java/org/apache/hop/pipeline/transforms/drools/RulesAccumulatorDialog.java
 
b/plugins/transforms/drools/src/main/java/org/apache/hop/pipeline/transforms/drools/RulesAccumulatorDialog.java
index c77fda0590..50fcdd5b82 100644
--- 
a/plugins/transforms/drools/src/main/java/org/apache/hop/pipeline/transforms/drools/RulesAccumulatorDialog.java
+++ 
b/plugins/transforms/drools/src/main/java/org/apache/hop/pipeline/transforms/drools/RulesAccumulatorDialog.java
@@ -31,6 +31,7 @@ import org.apache.hop.ui.core.widget.ColumnInfo;
 import org.apache.hop.ui.core.widget.ColumnsResizer;
 import org.apache.hop.ui.core.widget.StyledTextComp;
 import org.apache.hop.ui.core.widget.TableView;
+import org.apache.hop.ui.core.widget.TextComposite;
 import org.apache.hop.ui.core.widget.TextVar;
 import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
 import org.eclipse.swt.SWT;
@@ -186,7 +187,10 @@ public class RulesAccumulatorDialog extends 
BaseTransformDialog {
 
     wRulesEditor =
         new StyledTextComp(
-            variables, wRulesComp, SWT.MULTI | SWT.LEFT | SWT.BORDER | 
SWT.H_SCROLL | SWT.V_SCROLL);
+            variables,
+            wRulesComp,
+            SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL,
+            TextComposite.STYLE_TYPE_DROOLS);
     PropsUi.setLook(wRulesEditor, Props.WIDGET_STYLE_FIXED);
 
     FormData fdRulesEditor = new FormData();
diff --git 
a/plugins/transforms/drools/src/main/java/org/apache/hop/pipeline/transforms/drools/RulesExecutorDialog.java
 
b/plugins/transforms/drools/src/main/java/org/apache/hop/pipeline/transforms/drools/RulesExecutorDialog.java
index a6e43e433e..4e301f3bde 100644
--- 
a/plugins/transforms/drools/src/main/java/org/apache/hop/pipeline/transforms/drools/RulesExecutorDialog.java
+++ 
b/plugins/transforms/drools/src/main/java/org/apache/hop/pipeline/transforms/drools/RulesExecutorDialog.java
@@ -31,6 +31,7 @@ import org.apache.hop.ui.core.widget.ColumnInfo;
 import org.apache.hop.ui.core.widget.ColumnsResizer;
 import org.apache.hop.ui.core.widget.StyledTextComp;
 import org.apache.hop.ui.core.widget.TableView;
+import org.apache.hop.ui.core.widget.TextComposite;
 import org.apache.hop.ui.core.widget.TextVar;
 import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
 import org.eclipse.swt.SWT;
@@ -189,7 +190,10 @@ public class RulesExecutorDialog extends 
BaseTransformDialog {
 
     wRulesEditor =
         new StyledTextComp(
-            variables, wRulesComp, SWT.MULTI | SWT.LEFT | SWT.BORDER | 
SWT.H_SCROLL | SWT.V_SCROLL);
+            variables,
+            wRulesComp,
+            SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL,
+            TextComposite.STYLE_TYPE_DROOLS);
     PropsUi.setLook(wRulesEditor, Props.WIDGET_STYLE_FIXED);
 
     FormData fdRulesEditor = new FormData();
diff --git 
a/plugins/transforms/dynamicsqlrow/src/main/java/org/apache/hop/pipeline/transforms/dynamicsqlrow/DynamicSqlRowDialog.java
 
b/plugins/transforms/dynamicsqlrow/src/main/java/org/apache/hop/pipeline/transforms/dynamicsqlrow/DynamicSqlRowDialog.java
index f7c1c67e8a..349f05e2f4 100644
--- 
a/plugins/transforms/dynamicsqlrow/src/main/java/org/apache/hop/pipeline/transforms/dynamicsqlrow/DynamicSqlRowDialog.java
+++ 
b/plugins/transforms/dynamicsqlrow/src/main/java/org/apache/hop/pipeline/transforms/dynamicsqlrow/DynamicSqlRowDialog.java
@@ -269,7 +269,8 @@ public class DynamicSqlRowDialog extends 
BaseTransformDialog {
             ? new StyledTextComp(
                 variables,
                 wContent,
-                SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL | 
SWT.V_SCROLL)
+                SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL | 
SWT.V_SCROLL,
+                TextComposite.STYLE_TYPE_SQL)
             : new SQLStyledTextComp(
                 variables,
                 wContent,
diff --git 
a/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/editor/FormulaEditor.java
 
b/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/editor/FormulaEditor.java
index a1c472a05b..3b4897af17 100644
--- 
a/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/editor/FormulaEditor.java
+++ 
b/plugins/transforms/formula/src/main/java/org/apache/hop/pipeline/transforms/formula/editor/FormulaEditor.java
@@ -24,6 +24,7 @@ import org.apache.hop.pipeline.transforms.formula.FormulaMeta;
 import org.apache.hop.pipeline.transforms.formula.function.FunctionDescription;
 import org.apache.hop.pipeline.transforms.formula.function.FunctionLib;
 import org.apache.hop.ui.core.widget.StyledTextComp;
+import org.apache.hop.ui.core.widget.TextComposite;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.browser.Browser;
 import org.eclipse.swt.custom.SashForm;
@@ -222,7 +223,10 @@ public class FormulaEditor extends Dialog implements 
KeyListener {
     //
     expressionEditor =
         new StyledTextComp(
-            variables, rightSash, SWT.MULTI | SWT.LEFT | SWT.BORDER | 
SWT.H_SCROLL | SWT.V_SCROLL);
+            variables,
+            rightSash,
+            SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL,
+            TextComposite.STYLE_TYPE_FORMULA);
     expressionEditor.setText(this.formula);
     expressionEditor.addModifyListener(event -> setStyles());
     expressionEditor.addKeyListener(this);
diff --git 
a/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/janino/editor/FormulaEditor.java
 
b/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/janino/editor/FormulaEditor.java
index 8edcc9e5e9..38b1a99d3a 100644
--- 
a/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/janino/editor/FormulaEditor.java
+++ 
b/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/janino/editor/FormulaEditor.java
@@ -25,6 +25,7 @@ import org.apache.hop.pipeline.transforms.janino.JaninoMeta;
 import org.apache.hop.pipeline.transforms.janino.function.FunctionDescription;
 import org.apache.hop.pipeline.transforms.janino.function.FunctionLib;
 import org.apache.hop.ui.core.widget.StyledTextComp;
+import org.apache.hop.ui.core.widget.TextComposite;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.browser.Browser;
 import org.eclipse.swt.custom.SashForm;
@@ -223,7 +224,10 @@ public class FormulaEditor extends Dialog implements 
KeyListener {
     //
     expressionEditor =
         new StyledTextComp(
-            variables, rightSash, SWT.MULTI | SWT.LEFT | SWT.BORDER | 
SWT.H_SCROLL | SWT.V_SCROLL);
+            variables,
+            rightSash,
+            SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL,
+            TextComposite.STYLE_TYPE_FORMULA);
     expressionEditor.setText(this.formula);
     expressionEditor.addModifyListener(event -> setStyles());
     expressionEditor.addKeyListener(this);
diff --git 
a/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/javafilter/JavaFilterDialog.java
 
b/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/javafilter/JavaFilterDialog.java
index b4d9f006b3..754a0b451b 100644
--- 
a/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/javafilter/JavaFilterDialog.java
+++ 
b/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/javafilter/JavaFilterDialog.java
@@ -35,6 +35,7 @@ import org.apache.hop.ui.core.dialog.ErrorDialog;
 import org.apache.hop.ui.core.dialog.MessageBox;
 import org.apache.hop.ui.core.widget.ColumnInfo;
 import org.apache.hop.ui.core.widget.StyledTextComp;
+import org.apache.hop.ui.core.widget.TextComposite;
 import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.custom.CCombo;
@@ -171,7 +172,8 @@ public class JavaFilterDialog extends BaseTransformDialog {
         new StyledTextComp(
             variables,
             wSettingsGroup,
-            SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL);
+            SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL,
+            TextComposite.STYLE_TYPE_JAVA);
     PropsUi.setLook(wCondition);
     FormData fdCondition = new FormData();
     fdCondition.top = new FormAttachment(wFalseTo, margin);
diff --git 
a/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/UserDefinedJavaClassDialog.java
 
b/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/UserDefinedJavaClassDialog.java
index f61a4377b4..0a7e9bedbb 100644
--- 
a/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/UserDefinedJavaClassDialog.java
+++ 
b/plugins/transforms/janino/src/main/java/org/apache/hop/pipeline/transforms/userdefinedjavaclass/UserDefinedJavaClassDialog.java
@@ -843,7 +843,8 @@ public class UserDefinedJavaClassDialog extends 
BaseTransformDialog {
               variables,
               item.getParent(),
               SWT.MULTI | SWT.LEFT | SWT.H_SCROLL | SWT.V_SCROLL,
-              false);
+              false,
+              TextComposite.STYLE_TYPE_JAVA);
     } else {
       wScript =
           new JavaStyledTextComp(
diff --git 
a/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesDialog.java
 
b/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesDialog.java
index dc1c86b477..50820f9a63 100644
--- 
a/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesDialog.java
+++ 
b/plugins/transforms/javascript/src/main/java/org/apache/hop/pipeline/transforms/javascript/ScriptValuesDialog.java
@@ -602,7 +602,8 @@ public class ScriptValuesDialog extends BaseTransformDialog 
{
               variables,
               item.getParent(),
               SWT.MULTI | SWT.LEFT | SWT.H_SCROLL | SWT.V_SCROLL,
-              false);
+              false,
+              TextComposite.STYLE_TYPE_JAVASCRIPT);
     } else {
       wScript =
           new JavaScriptStyledTextComp(
diff --git 
a/plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/ui/TextCompositeToolbarJsonFormatButton.java
 
b/plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/ui/TextCompositeToolbarJsonFormatButton.java
new file mode 100644
index 0000000000..9ec5262fc1
--- /dev/null
+++ 
b/plugins/transforms/json/src/main/java/org/apache/hop/pipeline/transforms/ui/TextCompositeToolbarJsonFormatButton.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.hop.pipeline.transforms.ui;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hop.core.gui.plugin.GuiPlugin;
+import org.apache.hop.core.gui.plugin.toolbar.GuiToolbarElement;
+import org.apache.hop.core.gui.plugin.toolbar.GuiToolbarElementFilter;
+import org.apache.hop.core.json.HopJson;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.ui.core.dialog.ErrorDialog;
+import org.apache.hop.ui.core.widget.TextComposite;
+import org.eclipse.swt.widgets.Shell;
+
+/**
+ * Sample TextComposite toolbar contribution: pretty-print JSON when the 
editor's {@link
+ * TextComposite#getStyleType()} is {@link TextComposite#STYLE_TYPE_JSON}.
+ */
+@GuiPlugin
+public class TextCompositeToolbarJsonFormatButton {
+  private static final Class<?> PKG = 
TextCompositeToolbarJsonFormatButton.class;
+
+  private static final String ID_TOOLBAR_FORMAT_JSON = 
"textcomposite-toolbar-20010-format-json";
+
+  /**
+   * Only show this toolbar button when the content is JSON. Other toolbar 
item IDs always return
+   * {@code true} so built-in buttons are not hidden.
+   *
+   * <p>Signature must be {@code (String, Object)} for {@link 
GuiToolbarElementFilter} reflection.
+   *
+   * <p><strong>Do not call {@link TextComposite#isEditable()} (or {@code 
getText()}, etc.)
+   * here.</strong> Filters run while the {@link TextComposite} constructor is 
still building the
+   * toolbar, before the underlying text widget exists. Check editability in 
the action method
+   * instead.
+   *
+   * @param buttonId the toolbar button id being evaluated
+   * @param guiPluginInstance the registered TextComposite instance (or other 
owner)
+   * @return whether the button should be shown
+   */
+  @GuiToolbarElementFilter(parentId = TextComposite.ID_TOOLBAR)
+  public static boolean isButtonShown(String buttonId, Object 
guiPluginInstance) {
+    if (!ID_TOOLBAR_FORMAT_JSON.equals(buttonId)) {
+      return true;
+    }
+    if (!(guiPluginInstance instanceof TextComposite textComposite)) {
+      return false;
+    }
+    // styleType is set via the constructor before addToolbar(); isEditable() 
is not safe here.
+    return 
TextComposite.STYLE_TYPE_JSON.equalsIgnoreCase(textComposite.getStyleType());
+  }
+
+  @GuiToolbarElement(
+      root = TextComposite.ID_TOOLBAR,
+      id = ID_TOOLBAR_FORMAT_JSON,
+      toolTip = 
"i18n::TextCompositeToolbarJsonFormatButton.FormatJson.ToolTip",
+      separator = true,
+      image = "json-input.svg")
+  public static void formatJson(TextComposite textComposite) {
+    // Runtime checks (editability, disposed, …) belong here — not in the 
toolbar filter.
+    if (textComposite == null || textComposite.isDisposed() || 
!textComposite.isEditable()) {
+      return;
+    }
+
+    Shell shell = textComposite.getShell();
+    String json = textComposite.getText();
+    if (StringUtils.isBlank(json)) {
+      return;
+    }
+
+    try {
+      ObjectMapper mapper = HopJson.newMapper();
+      JsonNode tree = mapper.readTree(json);
+      String formatted = 
mapper.writerWithDefaultPrettyPrinter().writeValueAsString(tree);
+
+      // Jackson may leave a trailing newline; normalize so compare is stable
+      if (formatted.endsWith("\n") && !json.endsWith("\n")) {
+        formatted = formatted.substring(0, formatted.length() - 1);
+      }
+
+      if (formatted.equals(json)) {
+        return;
+      }
+
+      // selectAll + insert so StyledTextVar undo stack records the previous 
value
+      textComposite.selectAll();
+      textComposite.insert(formatted);
+      textComposite.setCaretPosition(0);
+      textComposite.updateToolbar();
+    } catch (Exception e) {
+      new ErrorDialog(
+          shell,
+          BaseMessages.getString(PKG, 
"TextCompositeToolbarJsonFormatButton.Error.Title"),
+          BaseMessages.getString(PKG, 
"TextCompositeToolbarJsonFormatButton.Error.Message"),
+          e);
+    }
+  }
+}
diff --git 
a/plugins/transforms/json/src/main/resources/org/apache/hop/pipeline/transforms/ui/messages/messages_en_US.properties
 
b/plugins/transforms/json/src/main/resources/org/apache/hop/pipeline/transforms/ui/messages/messages_en_US.properties
new file mode 100644
index 0000000000..58edcb147b
--- /dev/null
+++ 
b/plugins/transforms/json/src/main/resources/org/apache/hop/pipeline/transforms/ui/messages/messages_en_US.properties
@@ -0,0 +1,19 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+TextCompositeToolbarJsonFormatButton.FormatJson.ToolTip=Pretty-print JSON
+TextCompositeToolbarJsonFormatButton.Error.Title=JSON format error
+TextCompositeToolbarJsonFormatButton.Error.Message=The text is not valid JSON 
and could not be formatted.
diff --git 
a/plugins/transforms/ldap/src/main/java/org/apache/hop/pipeline/transforms/ldapinput/LdapInputDialog.java
 
b/plugins/transforms/ldap/src/main/java/org/apache/hop/pipeline/transforms/ldapinput/LdapInputDialog.java
index 88d8b82dbf..1a1f9bb5c7 100644
--- 
a/plugins/transforms/ldap/src/main/java/org/apache/hop/pipeline/transforms/ldapinput/LdapInputDialog.java
+++ 
b/plugins/transforms/ldap/src/main/java/org/apache/hop/pipeline/transforms/ldapinput/LdapInputDialog.java
@@ -43,6 +43,7 @@ import org.apache.hop.ui.core.widget.ComboVar;
 import org.apache.hop.ui.core.widget.PasswordTextVar;
 import org.apache.hop.ui.core.widget.StyledTextComp;
 import org.apache.hop.ui.core.widget.TableView;
+import org.apache.hop.ui.core.widget.TextComposite;
 import org.apache.hop.ui.core.widget.TextVar;
 import org.apache.hop.ui.pipeline.dialog.PipelinePreviewProgressDialog;
 import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
@@ -676,7 +677,8 @@ public class LdapInputDialog extends BaseTransformDialog {
             variables,
             wSearchGroup,
             SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL,
-            true);
+            true,
+            TextComposite.STYLE_TYPE_TEXT);
     wFilterString.setToolTipText(
         BaseMessages.getString(PKG, "LdapInputDialog.FilterString.Tooltip"));
     PropsUi.setLook(wFilterString);
diff --git 
a/plugins/transforms/regexeval/src/main/java/org/apache/hop/pipeline/transforms/regexeval/RegexEvalDialog.java
 
b/plugins/transforms/regexeval/src/main/java/org/apache/hop/pipeline/transforms/regexeval/RegexEvalDialog.java
index d3ee7ddc2d..0680952c73 100644
--- 
a/plugins/transforms/regexeval/src/main/java/org/apache/hop/pipeline/transforms/regexeval/RegexEvalDialog.java
+++ 
b/plugins/transforms/regexeval/src/main/java/org/apache/hop/pipeline/transforms/regexeval/RegexEvalDialog.java
@@ -37,6 +37,7 @@ import org.apache.hop.ui.core.widget.ColumnsResizer;
 import org.apache.hop.ui.core.widget.LabelTextVar;
 import org.apache.hop.ui.core.widget.StyledTextComp;
 import org.apache.hop.ui.core.widget.TableView;
+import org.apache.hop.ui.core.widget.TextComposite;
 import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.custom.CCombo;
@@ -276,7 +277,8 @@ public class RegexEvalDialog extends BaseTransformDialog {
         new StyledTextComp(
             variables,
             wGeneralComp,
-            SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL);
+            SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL,
+            TextComposite.STYLE_TYPE_REGEX);
     wScript.setText(BaseMessages.getString(PKG, 
"RegexEvalDialog.Script.Label"));
     PropsUi.setLook(wScript, Props.WIDGET_STYLE_FIXED);
     wScript.addModifyListener(lsMod);
diff --git 
a/plugins/transforms/regexeval/src/main/java/org/apache/hop/pipeline/transforms/regexeval/RegexEvalHelperDialog.java
 
b/plugins/transforms/regexeval/src/main/java/org/apache/hop/pipeline/transforms/regexeval/RegexEvalHelperDialog.java
index 2965081129..06307e930a 100644
--- 
a/plugins/transforms/regexeval/src/main/java/org/apache/hop/pipeline/transforms/regexeval/RegexEvalHelperDialog.java
+++ 
b/plugins/transforms/regexeval/src/main/java/org/apache/hop/pipeline/transforms/regexeval/RegexEvalHelperDialog.java
@@ -31,6 +31,7 @@ import org.apache.hop.ui.core.dialog.MessageBox;
 import org.apache.hop.ui.core.gui.GuiResource;
 import org.apache.hop.ui.core.gui.WindowProperty;
 import org.apache.hop.ui.core.widget.StyledTextComp;
+import org.apache.hop.ui.core.widget.TextComposite;
 import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.layout.FormAttachment;
@@ -140,7 +141,10 @@ public class RegexEvalHelperDialog extends Dialog {
 
     wRegExScript =
         new StyledTextComp(
-            variables, shell, SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL 
| SWT.V_SCROLL);
+            variables,
+            shell,
+            SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL,
+            TextComposite.STYLE_TYPE_REGEX);
     PropsUi.setLook(wRegExScript);
     FormData fdRegExScript = new FormData();
     fdRegExScript.left = new FormAttachment(0, 0);
diff --git 
a/plugins/transforms/script/src/main/java/org/apache/hop/pipeline/transforms/script/ScriptDialog.java
 
b/plugins/transforms/script/src/main/java/org/apache/hop/pipeline/transforms/script/ScriptDialog.java
index 0eb0075101..26482733a1 100644
--- 
a/plugins/transforms/script/src/main/java/org/apache/hop/pipeline/transforms/script/ScriptDialog.java
+++ 
b/plugins/transforms/script/src/main/java/org/apache/hop/pipeline/transforms/script/ScriptDialog.java
@@ -513,7 +513,8 @@ public class ScriptDialog extends BaseTransformDialog {
               variables,
               item.getParent(),
               SWT.MULTI | SWT.LEFT | SWT.H_SCROLL | SWT.V_SCROLL,
-              false);
+              false,
+              TextComposite.STYLE_TYPE_SCRIPT);
     } else {
       wScript =
           new ScriptStyledTextComp(
diff --git 
a/plugins/transforms/sql/src/main/java/org/apache/hop/pipeline/transforms/sql/ExecSqlDialog.java
 
b/plugins/transforms/sql/src/main/java/org/apache/hop/pipeline/transforms/sql/ExecSqlDialog.java
index 8e35d63e40..d9b18f579d 100644
--- 
a/plugins/transforms/sql/src/main/java/org/apache/hop/pipeline/transforms/sql/ExecSqlDialog.java
+++ 
b/plugins/transforms/sql/src/main/java/org/apache/hop/pipeline/transforms/sql/ExecSqlDialog.java
@@ -155,7 +155,10 @@ public class ExecSqlDialog extends BaseTransformDialog {
     wSql =
         EnvironmentUtils.getInstance().isWeb()
             ? new StyledTextComp(
-                variables, shell, SWT.MULTI | SWT.LEFT | SWT.BORDER | 
SWT.H_SCROLL | SWT.V_SCROLL)
+                variables,
+                shell,
+                SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL | 
SWT.V_SCROLL,
+                TextComposite.STYLE_TYPE_SQL)
             : new SQLStyledTextComp(
                 variables, shell, SWT.MULTI | SWT.LEFT | SWT.BORDER | 
SWT.H_SCROLL | SWT.V_SCROLL);
     final List<String> sqlKeywords = getSqlReservedWords();
diff --git 
a/plugins/transforms/ssh/src/main/java/org/apache/hop/pipeline/transforms/ssh/SshDialog.java
 
b/plugins/transforms/ssh/src/main/java/org/apache/hop/pipeline/transforms/ssh/SshDialog.java
index 1fbb665b25..9dfd89e673 100644
--- 
a/plugins/transforms/ssh/src/main/java/org/apache/hop/pipeline/transforms/ssh/SshDialog.java
+++ 
b/plugins/transforms/ssh/src/main/java/org/apache/hop/pipeline/transforms/ssh/SshDialog.java
@@ -38,6 +38,7 @@ import org.apache.hop.ui.core.dialog.PreviewRowsDialog;
 import org.apache.hop.ui.core.gui.GuiResource;
 import org.apache.hop.ui.core.widget.LabelTextVar;
 import org.apache.hop.ui.core.widget.StyledTextComp;
+import org.apache.hop.ui.core.widget.TextComposite;
 import org.apache.hop.ui.core.widget.TextVar;
 import org.apache.hop.ui.pipeline.dialog.PipelinePreviewProgressDialog;
 import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
@@ -533,7 +534,10 @@ public class SshDialog extends BaseTransformDialog {
 
     wCommand =
         new StyledTextComp(
-            variables, wCommands, SWT.MULTI | SWT.LEFT | SWT.BORDER | 
SWT.H_SCROLL | SWT.V_SCROLL);
+            variables,
+            wCommands,
+            SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL,
+            TextComposite.STYLE_TYPE_TEXT);
     wCommand.setToolTipText(BaseMessages.getString(PKG, 
"SSHDialog.Command.Tooltip"));
     PropsUi.setLook(wCommand);
     wCommand.addModifyListener(lsMod);
diff --git 
a/plugins/transforms/tableinput/src/main/java/org/apache/hop/pipeline/transforms/tableinput/TableInputDialog.java
 
b/plugins/transforms/tableinput/src/main/java/org/apache/hop/pipeline/transforms/tableinput/TableInputDialog.java
index 667c1c961f..f32846160e 100644
--- 
a/plugins/transforms/tableinput/src/main/java/org/apache/hop/pipeline/transforms/tableinput/TableInputDialog.java
+++ 
b/plugins/transforms/tableinput/src/main/java/org/apache/hop/pipeline/transforms/tableinput/TableInputDialog.java
@@ -279,7 +279,10 @@ public class TableInputDialog extends BaseTransformDialog {
     if (EnvironmentUtils.getInstance().isWeb()) {
       wSqlComposite =
           new StyledTextComp(
-              variables, shell, SWT.MULTI | SWT.LEFT | SWT.BORDER | 
SWT.H_SCROLL | SWT.V_SCROLL);
+              variables,
+              shell,
+              SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL,
+              TextComposite.STYLE_TYPE_SQL);
     } else {
       wSqlComposite =
           new SQLStyledTextComp(
diff --git 
a/plugins/transforms/writetolog/src/main/java/org/apache/hop/pipeline/transforms/writetolog/WriteToLogDialog.java
 
b/plugins/transforms/writetolog/src/main/java/org/apache/hop/pipeline/transforms/writetolog/WriteToLogDialog.java
index 8835f026ff..b6dea4d2b6 100644
--- 
a/plugins/transforms/writetolog/src/main/java/org/apache/hop/pipeline/transforms/writetolog/WriteToLogDialog.java
+++ 
b/plugins/transforms/writetolog/src/main/java/org/apache/hop/pipeline/transforms/writetolog/WriteToLogDialog.java
@@ -36,6 +36,7 @@ import org.apache.hop.ui.core.dialog.ErrorDialog;
 import org.apache.hop.ui.core.widget.ColumnInfo;
 import org.apache.hop.ui.core.widget.StyledTextComp;
 import org.apache.hop.ui.core.widget.TableView;
+import org.apache.hop.ui.core.widget.TextComposite;
 import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
 import org.apache.hop.ui.pipeline.transform.ITableItemInsertListener;
 import org.eclipse.swt.SWT;
@@ -172,7 +173,10 @@ public class WriteToLogDialog extends BaseTransformDialog {
 
     wLogMessage =
         new StyledTextComp(
-            variables, shell, SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL 
| SWT.V_SCROLL);
+            variables,
+            shell,
+            SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL,
+            TextComposite.STYLE_TYPE_TEXT);
     PropsUi.setLook(wLogMessage, Props.WIDGET_STYLE_FIXED);
     FormData fdLogMessage = new FormData();
     fdLogMessage.left = new FormAttachment(middle, 0);
diff --git a/ui/src/main/java/org/apache/hop/ui/core/PropsUi.java 
b/ui/src/main/java/org/apache/hop/ui/core/PropsUi.java
index 2501ace11c..cd1119896b 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/PropsUi.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/PropsUi.java
@@ -627,6 +627,15 @@ public class PropsUi extends Props {
     setProperty(STRING_SHOW_TABLE_VIEW_TOOLBAR, show ? YES : NO);
   }
 
+  public boolean isShowTextCompositeToolbar() {
+    String show = getProperty(STRING_SHOW_TEXT_COMPOSITE_TOOLBAR, YES);
+    return YES.equalsIgnoreCase(show); // Default: show the toolbar
+  }
+
+  public void setShowTextCompositeToolbar(boolean show) {
+    setProperty(STRING_SHOW_TEXT_COMPOSITE_TOOLBAR, show ? YES : NO);
+  }
+
   /**
    * Maximum number of characters shown in a preview grid cell before it is 
truncated (double-click
    * the cell to see the full value). 0 disables truncation. Default 50.
diff --git 
a/ui/src/main/java/org/apache/hop/ui/core/database/dialog/SqlEditor.java 
b/ui/src/main/java/org/apache/hop/ui/core/database/dialog/SqlEditor.java
index d0d836dc4c..0a56f7033b 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/database/dialog/SqlEditor.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/database/dialog/SqlEditor.java
@@ -165,7 +165,10 @@ public class SqlEditor {
     if (EnvironmentUtils.getInstance().isWeb()) {
       wScript =
           new StyledTextComp(
-              variables, shell, SWT.MULTI | SWT.LEFT | SWT.BORDER | 
SWT.H_SCROLL | SWT.V_SCROLL);
+              variables,
+              shell,
+              SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL,
+              TextComposite.STYLE_TYPE_SQL);
     } else {
       wScript =
           new SQLStyledTextComp(
diff --git 
a/ui/src/main/java/org/apache/hop/ui/core/dialog/FindReplaceDialog.java 
b/ui/src/main/java/org/apache/hop/ui/core/dialog/FindReplaceDialog.java
new file mode 100644
index 0000000000..43b641ffab
--- /dev/null
+++ b/ui/src/main/java/org/apache/hop/ui/core/dialog/FindReplaceDialog.java
@@ -0,0 +1,335 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.core.dialog;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.ui.core.PropsUi;
+import org.apache.hop.ui.core.gui.GuiResource;
+import org.apache.hop.ui.core.gui.WindowProperty;
+import org.apache.hop.ui.core.widget.TextComposite;
+import org.apache.hop.ui.core.widget.TextFindSupport;
+import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Dialog;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+/**
+ * Find / find-and-replace dialog for multi-line {@link TextComposite} editors 
(SQL, scripts, logs).
+ */
+public class FindReplaceDialog extends Dialog {
+  private static final Class<?> PKG = FindReplaceDialog.class;
+
+  private static String lastFind = "";
+  private static String lastReplace = "";
+  private static boolean lastCaseSensitive = false;
+
+  private final TextComposite textComposite;
+  private final boolean replaceMode;
+  private final PropsUi props;
+
+  private Shell shell;
+  private Text wFind;
+  private Text wReplace;
+  private Button wCaseSensitive;
+  private Label wlStatus;
+
+  private FindReplaceDialog(Shell parent, TextComposite textComposite, boolean 
replaceMode) {
+    super(parent, SWT.NONE);
+    this.textComposite = textComposite;
+    this.replaceMode = replaceMode;
+    this.props = PropsUi.getInstance();
+  }
+
+  /**
+   * Open a find or find-and-replace dialog for the given text composite.
+   *
+   * @param parent parent shell
+   * @param textComposite target editor
+   * @param replaceMode when true, show replace controls
+   */
+  public static void open(Shell parent, TextComposite textComposite, boolean 
replaceMode) {
+    if (parent == null || textComposite == null || textComposite.isDisposed()) 
{
+      return;
+    }
+    new FindReplaceDialog(parent, textComposite, replaceMode).openDialog();
+  }
+
+  private void openDialog() {
+    Shell parent = getParent();
+    shell = new Shell(parent, SWT.DIALOG_TRIM | SWT.RESIZE | 
SWT.APPLICATION_MODAL | SWT.SHEET);
+    PropsUi.setLook(shell);
+    shell.setImage(GuiResource.getInstance().getImageSearch());
+    shell.setText(
+        BaseMessages.getString(
+            PKG,
+            replaceMode
+                ? "FindReplaceDialog.Shell.ReplaceTitle"
+                : "FindReplaceDialog.Shell.FindTitle"));
+
+    FormLayout formLayout = new FormLayout();
+    formLayout.marginWidth = PropsUi.getFormMargin();
+    formLayout.marginHeight = PropsUi.getFormMargin();
+    shell.setLayout(formLayout);
+
+    int margin = PropsUi.getMargin();
+    int middle = props.getMiddlePct();
+
+    // Find
+    Label wlFind = new Label(shell, SWT.RIGHT);
+    PropsUi.setLook(wlFind);
+    wlFind.setText(BaseMessages.getString(PKG, 
"FindReplaceDialog.Find.Label"));
+    FormData fdlFind = new FormData();
+    fdlFind.left = new FormAttachment(0, 0);
+    fdlFind.top = new FormAttachment(0, margin);
+    fdlFind.right = new FormAttachment(middle, -margin);
+    wlFind.setLayoutData(fdlFind);
+
+    wFind = new Text(shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+    PropsUi.setLook(wFind);
+    FormData fdFind = new FormData();
+    fdFind.left = new FormAttachment(middle, 0);
+    fdFind.top = new FormAttachment(wlFind, 0, SWT.CENTER);
+    fdFind.right = new FormAttachment(100, 0);
+    wFind.setLayoutData(fdFind);
+
+    // Replace (optional)
+    Label wlReplace = null;
+    if (replaceMode) {
+      wlReplace = new Label(shell, SWT.RIGHT);
+      PropsUi.setLook(wlReplace);
+      wlReplace.setText(BaseMessages.getString(PKG, 
"FindReplaceDialog.Replace.Label"));
+      FormData fdlReplace = new FormData();
+      fdlReplace.left = new FormAttachment(0, 0);
+      fdlReplace.top = new FormAttachment(wFind, margin);
+      fdlReplace.right = new FormAttachment(middle, -margin);
+      wlReplace.setLayoutData(fdlReplace);
+
+      wReplace = new Text(shell, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+      PropsUi.setLook(wReplace);
+      FormData fdReplace = new FormData();
+      fdReplace.left = new FormAttachment(middle, 0);
+      fdReplace.top = new FormAttachment(wlReplace, 0, SWT.CENTER);
+      fdReplace.right = new FormAttachment(100, 0);
+      wReplace.setLayoutData(fdReplace);
+    }
+
+    // Case sensitive
+    wCaseSensitive = new Button(shell, SWT.CHECK);
+    PropsUi.setLook(wCaseSensitive);
+    wCaseSensitive.setText(BaseMessages.getString(PKG, 
"FindReplaceDialog.CaseSensitive.Label"));
+    FormData fdCase = new FormData();
+    fdCase.left = new FormAttachment(middle, 0);
+    fdCase.top = new FormAttachment(replaceMode ? wReplace : wFind, margin);
+    fdCase.right = new FormAttachment(100, 0);
+    wCaseSensitive.setLayoutData(fdCase);
+
+    // Status line
+    wlStatus = new Label(shell, SWT.LEFT);
+    PropsUi.setLook(wlStatus);
+    FormData fdStatus = new FormData();
+    fdStatus.left = new FormAttachment(0, 0);
+    fdStatus.right = new FormAttachment(100, 0);
+    fdStatus.top = new FormAttachment(wCaseSensitive, margin);
+    wlStatus.setLayoutData(fdStatus);
+
+    // Buttons
+    Button wFindNext = new Button(shell, SWT.PUSH);
+    wFindNext.setText(BaseMessages.getString(PKG, 
"FindReplaceDialog.FindNext.Button"));
+    wFindNext.addListener(SWT.Selection, e -> findNext(true));
+
+    Button wFindPrev = new Button(shell, SWT.PUSH);
+    wFindPrev.setText(BaseMessages.getString(PKG, 
"FindReplaceDialog.FindPrevious.Button"));
+    wFindPrev.addListener(SWT.Selection, e -> findNext(false));
+
+    Button wReplaceOne = null;
+    Button wReplaceAll = null;
+    if (replaceMode) {
+      wReplaceOne = new Button(shell, SWT.PUSH);
+      wReplaceOne.setText(BaseMessages.getString(PKG, 
"FindReplaceDialog.Replace.Button"));
+      wReplaceOne.addListener(SWT.Selection, e -> replaceOne());
+      wReplaceOne.setEnabled(textComposite.isEditable());
+
+      wReplaceAll = new Button(shell, SWT.PUSH);
+      wReplaceAll.setText(BaseMessages.getString(PKG, 
"FindReplaceDialog.ReplaceAll.Button"));
+      wReplaceAll.addListener(SWT.Selection, e -> replaceAll());
+      wReplaceAll.setEnabled(textComposite.isEditable());
+    }
+
+    Button wClose = new Button(shell, SWT.PUSH);
+    wClose.setText(BaseMessages.getString(PKG, "System.Button.Close"));
+    wClose.addListener(SWT.Selection, e -> close());
+
+    Button[] buttons;
+    if (replaceMode) {
+      buttons = new Button[] {wFindNext, wFindPrev, wReplaceOne, wReplaceAll, 
wClose};
+    } else {
+      buttons = new Button[] {wFindNext, wFindPrev, wClose};
+    }
+    BaseTransformDialog.positionBottomButtons(shell, buttons, margin, 
wlStatus);
+
+    // Defaults
+    String selection = textComposite.getSelectionText();
+    if (StringUtils.isNotEmpty(selection) && !selection.contains("\n")) {
+      wFind.setText(selection);
+    } else if (StringUtils.isNotEmpty(lastFind)) {
+      wFind.setText(lastFind);
+    }
+    if (replaceMode && wReplace != null) {
+      wReplace.setText(lastReplace != null ? lastReplace : "");
+    }
+    wCaseSensitive.setSelection(lastCaseSensitive);
+
+    shell.setDefaultButton(wFindNext);
+    wFind.selectAll();
+    wFind.setFocus();
+
+    BaseDialog.defaultShellHandling(shell, c -> findNext(true), c -> close(), 
false);
+  }
+
+  private void findNext(boolean forward) {
+    if (textComposite.isDisposed()) {
+      close();
+      return;
+    }
+    String query = wFind.getText();
+    if (StringUtils.isEmpty(query)) {
+      setStatus(BaseMessages.getString(PKG, 
"FindReplaceDialog.Status.EmptyFind"));
+      return;
+    }
+    rememberOptions();
+
+    String content = textComposite.getText();
+    boolean caseSensitive = wCaseSensitive.getSelection();
+    int caret = textComposite.getCaretPosition();
+    int selectionLen = textComposite.getSelectionCount();
+
+    int found;
+    if (forward) {
+      // Start after current selection when it matches (or after caret)
+      int from = caret;
+      if (selectionLen > 0) {
+        // Caret is typically at the end of the selection; start after it
+        from = Math.max(caret, caret); // caret already end in most cases
+        // If selection equals query, step past it
+        String selected = textComposite.getSelectionText();
+        if (matches(selected, query, caseSensitive)) {
+          from = caret; // end of selection
+        }
+      }
+      found = TextFindSupport.findNext(content, query, from, caseSensitive);
+      if (found < 0 && from > 0) {
+        // wrap
+        found = TextFindSupport.findNext(content, query, 0, caseSensitive);
+      }
+    } else {
+      int from = caret - selectionLen - 1;
+      if (from < 0) {
+        from = content.length();
+      }
+      found = TextFindSupport.findPrevious(content, query, from, 
caseSensitive);
+      if (found < 0) {
+        found = TextFindSupport.findPrevious(content, query, content.length(), 
caseSensitive);
+      }
+    }
+
+    if (found >= 0) {
+      textComposite.setSelection(found, found + query.length());
+      textComposite.setFocus();
+      setStatus("");
+    } else {
+      setStatus(BaseMessages.getString(PKG, 
"FindReplaceDialog.Status.NotFound"));
+    }
+  }
+
+  private void replaceOne() {
+    if (textComposite.isDisposed() || !textComposite.isEditable()) {
+      return;
+    }
+    String query = wFind.getText();
+    if (StringUtils.isEmpty(query)) {
+      setStatus(BaseMessages.getString(PKG, 
"FindReplaceDialog.Status.EmptyFind"));
+      return;
+    }
+    rememberOptions();
+    String selected = textComposite.getSelectionText();
+    if (matches(selected, query, wCaseSensitive.getSelection())) {
+      String replacement = wReplace != null ? wReplace.getText() : "";
+      textComposite.insert(replacement);
+      textComposite.updateToolbar();
+    }
+    findNext(true);
+  }
+
+  private void replaceAll() {
+    if (textComposite.isDisposed() || !textComposite.isEditable()) {
+      return;
+    }
+    String query = wFind.getText();
+    if (StringUtils.isEmpty(query)) {
+      setStatus(BaseMessages.getString(PKG, 
"FindReplaceDialog.Status.EmptyFind"));
+      return;
+    }
+    rememberOptions();
+    String replacement = wReplace != null ? wReplace.getText() : "";
+    TextFindSupport.ReplaceAllResult result =
+        TextFindSupport.replaceAll(
+            textComposite.getText(), query, replacement, 
wCaseSensitive.getSelection());
+    if (result.count() > 0) {
+      int caret = textComposite.getCaretPosition();
+      textComposite.setText(result.text());
+      textComposite.setCaretPosition(Math.min(caret, result.text().length()));
+      textComposite.updateToolbar();
+    }
+    setStatus(
+        BaseMessages.getString(
+            PKG, "FindReplaceDialog.Status.ReplaceAllCount", 
Integer.toString(result.count())));
+  }
+
+  private static boolean matches(String value, String query, boolean 
caseSensitive) {
+    if (value == null || query == null) {
+      return false;
+    }
+    return caseSensitive ? value.equals(query) : value.equalsIgnoreCase(query);
+  }
+
+  private void rememberOptions() {
+    lastFind = wFind.getText();
+    if (wReplace != null) {
+      lastReplace = wReplace.getText();
+    }
+    lastCaseSensitive = wCaseSensitive.getSelection();
+  }
+
+  private void setStatus(String message) {
+    if (wlStatus != null && !wlStatus.isDisposed()) {
+      wlStatus.setText(message != null ? message : "");
+    }
+  }
+
+  private void close() {
+    props.setScreen(new WindowProperty(shell));
+    shell.dispose();
+  }
+}
diff --git 
a/ui/src/main/java/org/apache/hop/ui/core/widget/JavaScriptStyledTextComp.java 
b/ui/src/main/java/org/apache/hop/ui/core/widget/JavaScriptStyledTextComp.java
index 2aa394043d..1d84c364fc 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/core/widget/JavaScriptStyledTextComp.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/core/widget/JavaScriptStyledTextComp.java
@@ -40,7 +40,7 @@ public class JavaScriptStyledTextComp extends StyledTextVar {
       boolean varsSensitive,
       boolean variableIconOnTop) {
 
-    super(variables, parent, style, varsSensitive, variableIconOnTop);
+    super(variables, parent, style, varsSensitive, variableIconOnTop, true, 
STYLE_TYPE_JAVASCRIPT);
   }
 
   @Override
diff --git 
a/ui/src/main/java/org/apache/hop/ui/core/widget/JavaStyledTextComp.java 
b/ui/src/main/java/org/apache/hop/ui/core/widget/JavaStyledTextComp.java
index 7f5d7c32a9..126262653d 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/widget/JavaStyledTextComp.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/widget/JavaStyledTextComp.java
@@ -40,7 +40,7 @@ public class JavaStyledTextComp extends StyledTextVar {
       boolean varsSensitive,
       boolean variableIconOnTop) {
 
-    super(variables, parent, style, varsSensitive, variableIconOnTop);
+    super(variables, parent, style, varsSensitive, variableIconOnTop, true, 
STYLE_TYPE_JAVA);
   }
 
   @Override
diff --git 
a/ui/src/main/java/org/apache/hop/ui/core/widget/SQLStyledTextComp.java 
b/ui/src/main/java/org/apache/hop/ui/core/widget/SQLStyledTextComp.java
index b87c80706c..a38d6bfa60 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/widget/SQLStyledTextComp.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/widget/SQLStyledTextComp.java
@@ -39,7 +39,7 @@ public class SQLStyledTextComp extends StyledTextVar {
       boolean varsSensitive,
       boolean variableIconOnTop) {
 
-    super(variables, parent, style, varsSensitive, variableIconOnTop);
+    super(variables, parent, style, varsSensitive, variableIconOnTop, true, 
STYLE_TYPE_SQL);
   }
 
   @Override
diff --git 
a/ui/src/main/java/org/apache/hop/ui/core/widget/ScriptStyledTextComp.java 
b/ui/src/main/java/org/apache/hop/ui/core/widget/ScriptStyledTextComp.java
index c02361904e..df64ebc60b 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/widget/ScriptStyledTextComp.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/widget/ScriptStyledTextComp.java
@@ -43,7 +43,7 @@ public class ScriptStyledTextComp extends StyledTextVar {
       boolean varsSensitive,
       boolean variableIconOnTop) {
 
-    super(variables, parent, style, varsSensitive, variableIconOnTop);
+    super(variables, parent, style, varsSensitive, variableIconOnTop, true, 
STYLE_TYPE_SCRIPT);
   }
 
   @Override
diff --git a/ui/src/main/java/org/apache/hop/ui/core/widget/StyledTextComp.java 
b/ui/src/main/java/org/apache/hop/ui/core/widget/StyledTextComp.java
index 08f76e404c..cb1d9096da 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/widget/StyledTextComp.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/widget/StyledTextComp.java
@@ -32,8 +32,8 @@ import org.eclipse.swt.events.MouseAdapter;
 import org.eclipse.swt.graphics.Color;
 import org.eclipse.swt.graphics.Font;
 import org.eclipse.swt.layout.FormAttachment;
-import org.eclipse.swt.layout.FormLayout;
 import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
 import org.eclipse.swt.widgets.Label;
 import org.eclipse.swt.widgets.Listener;
 import org.eclipse.swt.widgets.Menu;
@@ -46,11 +46,22 @@ public class StyledTextComp extends TextComposite {
   private final Menu popupMenu;
 
   public StyledTextComp(IVariables variables, Composite parent, int style) {
-    this(variables, parent, style, true, false);
+    this(variables, parent, style, true, false, true, STYLE_TYPE_GENERIC);
+  }
+
+  /** Construct with a semantic {@code styleType} (applied before the toolbar 
is built). */
+  public StyledTextComp(IVariables variables, Composite parent, int style, 
String styleType) {
+    this(variables, parent, style, true, false, true, styleType);
   }
 
   public StyledTextComp(IVariables variables, Composite parent, int style, 
boolean varsSensitive) {
-    this(variables, parent, style, varsSensitive, false);
+    this(variables, parent, style, varsSensitive, false, true, 
STYLE_TYPE_GENERIC);
+  }
+
+  /** Construct with vars sensitivity and a semantic {@code styleType}. */
+  public StyledTextComp(
+      IVariables variables, Composite parent, int style, boolean 
varsSensitive, String styleType) {
+    this(variables, parent, style, varsSensitive, false, true, styleType);
   }
 
   public StyledTextComp(
@@ -59,18 +70,61 @@ public class StyledTextComp extends TextComposite {
       int args,
       boolean varsSensitive,
       boolean variableIconOnTop) {
+    this(variables, parent, args, varsSensitive, variableIconOnTop, true, 
STYLE_TYPE_GENERIC);
+  }
 
-    super(parent, SWT.NONE);
+  public StyledTextComp(
+      IVariables variables,
+      Composite parent,
+      int args,
+      boolean varsSensitive,
+      boolean variableIconOnTop,
+      String styleType) {
+    this(variables, parent, args, varsSensitive, variableIconOnTop, true, 
styleType);
+  }
+
+  public StyledTextComp(
+      IVariables variables,
+      Composite parent,
+      int args,
+      boolean varsSensitive,
+      boolean variableIconOnTop,
+      boolean toolbarEnabled) {
+    this(
+        variables,
+        parent,
+        args,
+        varsSensitive,
+        variableIconOnTop,
+        toolbarEnabled,
+        STYLE_TYPE_GENERIC);
+  }
+
+  public StyledTextComp(
+      IVariables variables,
+      Composite parent,
+      int args,
+      boolean varsSensitive,
+      boolean variableIconOnTop,
+      boolean toolbarEnabled,
+      String styleType) {
+
+    super(parent, SWT.NONE, toolbarEnabled, styleType);
     textWidget = new Text(this, args);
     popupMenu = new Menu(parent.getShell(), SWT.POP_UP);
 
-    this.setLayout(new FormLayout());
-
     buildingStyledTextMenu(popupMenu);
 
+    Control top = getTopControl();
+
     // Default layout without variables
     textWidget.setLayoutData(
-        new FormDataBuilder().top().left().right(100, 0).bottom(100, 
0).result());
+        new FormDataBuilder()
+            .top(top != null ? new FormAttachment(top, 0) : new 
FormAttachment(0, 0))
+            .left()
+            .right(100, 0)
+            .bottom(100, 0)
+            .result());
 
     // Special layout for variables decorator
     if (varsSensitive) {
@@ -81,7 +135,12 @@ public class StyledTextComp extends TextComposite {
         PropsUi.setLook(wIcon);
         wIcon.setToolTipText(BaseMessages.getString(PKG, 
"StyledTextComp.tooltip.InsertVariable"));
         wIcon.setImage(GuiResource.getInstance().getImageVariableMini());
-        wIcon.setLayoutData(new FormDataBuilder().top().right(100, 
0).result());
+        if (top != null) {
+          wIcon.setLayoutData(
+              new FormDataBuilder().top(new FormAttachment(top, 0)).right(100, 
0).result());
+        } else {
+          wIcon.setLayoutData(new FormDataBuilder().top().right(100, 
0).result());
+        }
         textWidget.setLayoutData(
             new FormDataBuilder()
                 .top(new FormAttachment(wIcon, 0, 0))
@@ -95,10 +154,15 @@ public class StyledTextComp extends TextComposite {
         controlDecoration.setToolTipText(
             BaseMessages.getString(PKG, 
"StyledTextComp.tooltip.InsertVariable"));
         PropsUi.setLook(controlDecoration);
-        controlDecoration.setLayoutData(new FormDataBuilder().top().right(100, 
0).result());
+        if (top != null) {
+          controlDecoration.setLayoutData(
+              new FormDataBuilder().top(new FormAttachment(top, 0)).right(100, 
0).result());
+        } else {
+          controlDecoration.setLayoutData(new 
FormDataBuilder().top().right(100, 0).result());
+        }
         textWidget.setLayoutData(
             new FormDataBuilder()
-                .top()
+                .top(top != null ? new FormAttachment(top, 0) : new 
FormAttachment(0, 0))
                 .left()
                 .right(new FormAttachment(controlDecoration, 0, 0))
                 .bottom(100, 0)
@@ -119,7 +183,8 @@ public class StyledTextComp extends TextComposite {
 
   @Override
   public void setCaretPosition(int offset) {
-    // Does not exist in Text widget
+    // Text has no caret offset API; move the selection to a zero-width range.
+    textWidget.setSelection(offset);
   }
 
   @Override
@@ -233,6 +298,9 @@ public class StyledTextComp extends TextComposite {
   @Override
   public void setEnabled(boolean enabled) {
     textWidget.setEnabled(enabled);
+    if (getToolbar() != null && !getToolbar().isDisposed()) {
+      getToolbar().setEnabled(enabled);
+    }
   }
 
   @Override
diff --git a/ui/src/main/java/org/apache/hop/ui/core/widget/StyledTextVar.java 
b/ui/src/main/java/org/apache/hop/ui/core/widget/StyledTextVar.java
index 78e4805de0..95ff36842c 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/widget/StyledTextVar.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/widget/StyledTextVar.java
@@ -38,8 +38,8 @@ import org.eclipse.swt.events.MouseAdapter;
 import org.eclipse.swt.graphics.Color;
 import org.eclipse.swt.graphics.Font;
 import org.eclipse.swt.layout.FormAttachment;
-import org.eclipse.swt.layout.FormLayout;
 import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
 import org.eclipse.swt.widgets.Display;
 import org.eclipse.swt.widgets.Label;
 import org.eclipse.swt.widgets.Listener;
@@ -59,11 +59,22 @@ public class StyledTextVar extends TextComposite {
   private boolean fullSelection = false;
 
   public StyledTextVar(IVariables variables, Composite parent, int style) {
-    this(variables, parent, style, true, false);
+    this(variables, parent, style, true, false, true, STYLE_TYPE_GENERIC);
+  }
+
+  /** Construct with a semantic {@code styleType} (applied before the toolbar 
is built). */
+  public StyledTextVar(IVariables variables, Composite parent, int style, 
String styleType) {
+    this(variables, parent, style, true, false, true, styleType);
   }
 
   public StyledTextVar(IVariables variables, Composite parent, int style, 
boolean varsSensitive) {
-    this(variables, parent, style, varsSensitive, false);
+    this(variables, parent, style, varsSensitive, false, true, 
STYLE_TYPE_GENERIC);
+  }
+
+  /** Construct with vars sensitivity and a semantic {@code styleType}. */
+  public StyledTextVar(
+      IVariables variables, Composite parent, int style, boolean 
varsSensitive, String styleType) {
+    this(variables, parent, style, varsSensitive, false, true, styleType);
   }
 
   public StyledTextVar(
@@ -72,22 +83,67 @@ public class StyledTextVar extends TextComposite {
       int style,
       boolean varsSensitive,
       boolean variableIconOnTop) {
+    this(variables, parent, style, varsSensitive, variableIconOnTop, true, 
STYLE_TYPE_GENERIC);
+  }
+
+  public StyledTextVar(
+      IVariables variables,
+      Composite parent,
+      int style,
+      boolean varsSensitive,
+      boolean variableIconOnTop,
+      String styleType) {
+    this(variables, parent, style, varsSensitive, variableIconOnTop, true, 
styleType);
+  }
+
+  public StyledTextVar(
+      IVariables variables,
+      Composite parent,
+      int style,
+      boolean varsSensitive,
+      boolean variableIconOnTop,
+      boolean toolbarEnabled) {
+    this(
+        variables,
+        parent,
+        style,
+        varsSensitive,
+        variableIconOnTop,
+        toolbarEnabled,
+        STYLE_TYPE_GENERIC);
+  }
 
-    super(parent, SWT.NONE);
+  public StyledTextVar(
+      IVariables variables,
+      Composite parent,
+      int style,
+      boolean varsSensitive,
+      boolean variableIconOnTop,
+      boolean toolbarEnabled,
+      String styleType) {
+
+    super(parent, SWT.NONE, toolbarEnabled, styleType);
 
     undoStack = new LinkedList<>();
     redoStack = new LinkedList<>();
 
     wText = new StyledText(this, style);
     wPopupMenu = new Menu(parent.getShell(), SWT.POP_UP);
-    this.setLayout(new FormLayout());
 
     buildingStyledTextMenu(wPopupMenu);
 
     addUndoRedoSupport();
 
+    Control top = getTopControl();
+
     // Default layout without variables
-    wText.setLayoutData(new FormDataBuilder().top().left().right(100, 
0).bottom(100, 0).result());
+    wText.setLayoutData(
+        new FormDataBuilder()
+            .top(top != null ? new FormAttachment(top, 0) : new 
FormAttachment(0, 0))
+            .left()
+            .right(100, 0)
+            .bottom(100, 0)
+            .result());
 
     // Special layout for variables decorator
     if (varsSensitive) {
@@ -97,7 +153,12 @@ public class StyledTextVar extends TextComposite {
         PropsUi.setLook(wIcon);
         wIcon.setToolTipText(BaseMessages.getString(PKG, 
"StyledTextComp.tooltip.InsertVariable"));
         wIcon.setImage(GuiResource.getInstance().getImageVariableMini());
-        wIcon.setLayoutData(new FormDataBuilder().top().right(100, 
0).result());
+        if (top != null) {
+          wIcon.setLayoutData(
+              new FormDataBuilder().top(new FormAttachment(top, 0)).right(100, 
0).result());
+        } else {
+          wIcon.setLayoutData(new FormDataBuilder().top().right(100, 
0).result());
+        }
         wText.setLayoutData(
             new FormDataBuilder()
                 .top(new FormAttachment(wIcon, 0, 0))
@@ -111,10 +172,15 @@ public class StyledTextVar extends TextComposite {
         controlDecoration.setToolTipText(
             BaseMessages.getString(PKG, 
"StyledTextComp.tooltip.InsertVariable"));
         PropsUi.setLook(controlDecoration);
-        controlDecoration.setLayoutData(new FormDataBuilder().top().right(100, 
0).result());
+        if (top != null) {
+          controlDecoration.setLayoutData(
+              new FormDataBuilder().top(new FormAttachment(top, 0)).right(100, 
0).result());
+        } else {
+          controlDecoration.setLayoutData(new 
FormDataBuilder().top().right(100, 0).result());
+        }
         wText.setLayoutData(
             new FormDataBuilder()
-                .top()
+                .top(top != null ? new FormAttachment(top, 0) : new 
FormAttachment(0, 0))
                 .left()
                 .right(new FormAttachment(controlDecoration, 0, 0))
                 .bottom(100, 0)
@@ -261,6 +327,9 @@ public class StyledTextVar extends TextComposite {
   @Override
   public void setEnabled(boolean enabled) {
     wText.setEnabled(enabled);
+    if (getToolbar() != null && !getToolbar().isDisposed()) {
+      getToolbar().setEnabled(enabled);
+    }
     // StyledText component does not get the "disabled" look, so it needs to 
be applied explicitly
     if (Display.getDefault() != null) {
       wText.setBackground(
@@ -270,6 +339,16 @@ public class StyledTextVar extends TextComposite {
     }
   }
 
+  @Override
+  protected boolean canUndo() {
+    return !undoStack.isEmpty();
+  }
+
+  @Override
+  protected boolean canRedo() {
+    return !redoStack.isEmpty();
+  }
+
   @Override
   public void cut() {
     wText.cut();
diff --git a/ui/src/main/java/org/apache/hop/ui/core/widget/TextComposite.java 
b/ui/src/main/java/org/apache/hop/ui/core/widget/TextComposite.java
index d16a6c2df4..acc34aae23 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/widget/TextComposite.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/widget/TextComposite.java
@@ -17,25 +17,147 @@
 
 package org.apache.hop.ui.core.widget;
 
+import java.util.ArrayList;
 import java.util.List;
+import lombok.Getter;
+import lombok.Setter;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.hop.core.Props;
+import org.apache.hop.core.gui.plugin.GuiPlugin;
+import org.apache.hop.core.gui.plugin.toolbar.GuiToolbarElement;
 import org.apache.hop.core.util.Utils;
 import org.apache.hop.i18n.BaseMessages;
 import org.apache.hop.ui.core.ConstUi;
+import org.apache.hop.ui.core.PropsUi;
+import org.apache.hop.ui.core.dialog.FindReplaceDialog;
 import org.apache.hop.ui.core.gui.GuiResource;
+import org.apache.hop.ui.core.gui.GuiToolbarWidgets;
+import org.apache.hop.ui.core.gui.IToolbarContainer;
+import org.apache.hop.ui.hopgui.ToolbarFacade;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.dnd.Clipboard;
 import org.eclipse.swt.dnd.TextTransfer;
 import org.eclipse.swt.events.MenuDetectListener;
 import org.eclipse.swt.events.ModifyListener;
 import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
 import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
 import org.eclipse.swt.widgets.Menu;
 import org.eclipse.swt.widgets.MenuItem;
 
+/**
+ * Multi-line text editor composite used across Hop dialogs (SQL, scripts, 
logs, etc.).
+ *
+ * <p>Supports an optional toolbar (undo/redo, clipboard, find/replace) that 
plugins can extend via
+ * {@link GuiToolbarElement} with {@link #ID_TOOLBAR} as the root, the same 
way {@link TableView}
+ * toolbars work.
+ *
+ * <p>Toolbar plugins receive a {@link TextComposite} instance. To know what 
kind of text is being
+ * edited, use {@code getStyleType()} (e.g. {@link #STYLE_TYPE_SQL}) rather 
than {@code instanceof}
+ * checks on subclasses — on Hop Web many specialized editors are replaced by 
a plain {@link
+ * StyledTextComp} with the same style type set.
+ *
+ * <pre>{@code
+ * @GuiToolbarElement(root = TextComposite.ID_TOOLBAR, id = "...", image = 
"...")
+ * public static void mySqlAction(TextComposite text) {
+ *   if (!TextComposite.STYLE_TYPE_SQL.equals(text.getStyleType())) {
+ *     return;
+ *   }
+ *   // ...
+ * }
+ * }</pre>
+ */
+@GuiPlugin
 public abstract class TextComposite extends Composite {
   private static final Class<?> PKG = StyledTextComp.class;
 
+  public static final String ID_TOOLBAR = "TextComposite-Toolbar";
+  public static final String ID_TOOLBAR_UNDO = 
"textcomposite-toolbar-10000-undo";
+  public static final String ID_TOOLBAR_REDO = 
"textcomposite-toolbar-10010-redo";
+  public static final String ID_TOOLBAR_CUT = 
"textcomposite-toolbar-10100-cut";
+  public static final String ID_TOOLBAR_COPY = 
"textcomposite-toolbar-10110-copy";
+  public static final String ID_TOOLBAR_PASTE = 
"textcomposite-toolbar-10120-paste";
+  public static final String ID_TOOLBAR_SELECT_ALL = 
"textcomposite-toolbar-10130-select-all";
+  public static final String ID_TOOLBAR_FIND = 
"textcomposite-toolbar-10200-find";
+  public static final String ID_TOOLBAR_FIND_REPLACE = 
"textcomposite-toolbar-10210-find-replace";
+
+  /** Default / unspecified multi-line text. */
+  public static final String STYLE_TYPE_GENERIC = "Generic";
+
+  /** SQL (or SQL-like) script. */
+  public static final String STYLE_TYPE_SQL = "SQL";
+
+  /** JavaScript source. */
+  public static final String STYLE_TYPE_JAVASCRIPT = "JavaScript";
+
+  /** Java source. */
+  public static final String STYLE_TYPE_JAVA = "Java";
+
+  /** Generic scripting language (engine may vary). */
+  public static final String STYLE_TYPE_SCRIPT = "Script";
+
+  /** Execution or application log output. */
+  public static final String STYLE_TYPE_LOG = "Log";
+
+  /** Unified / git-style diff. */
+  public static final String STYLE_TYPE_DIFF = "Diff";
+
+  /** Regular expression. */
+  public static final String STYLE_TYPE_REGEX = "Regex";
+
+  /** Formula / expression language. */
+  public static final String STYLE_TYPE_FORMULA = "Formula";
+
+  /** JSON document or query payload. */
+  public static final String STYLE_TYPE_JSON = "JSON";
+
+  /** Free-form human-readable text / message body. */
+  public static final String STYLE_TYPE_TEXT = "Text";
+
+  /** Cassandra Query Language. */
+  public static final String STYLE_TYPE_CQL = "CQL";
+
+  /** Salesforce Object Query Language. */
+  public static final String STYLE_TYPE_SOQL = "SOQL";
+
+  /** Drools rules. */
+  public static final String STYLE_TYPE_DROOLS = "Drools";
+
+  @Getter private final boolean toolbarEnabled;
+  @Getter @Setter private Control toolbar;
+  @Getter @Setter private GuiToolbarWidgets toolbarWidgets;
+
+  /**
+   * Semantic type of the text being edited (for toolbar plugins and similar 
contributors). Defaults
+   * to {@link #STYLE_TYPE_GENERIC}. Prefer the {@code STYLE_TYPE_*} 
constants; plugins may use
+   * other free-form values.
+   *
+   * <p><strong>Must be set via the constructor</strong> so that {@link
+   * org.apache.hop.core.gui.plugin.toolbar.GuiToolbarElementFilter} sees the 
correct value when the
+   * toolbar is built. Calling {@link #setStyleType(String)} after 
construction does not rebuild the
+   * toolbar or re-run filters.
+   */
+  @Getter private String styleType = STYLE_TYPE_GENERIC;
+
+  private final List<String> removeToolItems;
+  private final PropsUi props;
+
+  /**
+   * Sets the semantic style type of this editor.
+   *
+   * <p>Does <strong>not</strong> rebuild the toolbar or re-evaluate toolbar 
filters. Prefer passing
+   * {@code styleType} to the constructor so plugins that filter on type (for 
example JSON format)
+   * work correctly.
+   *
+   * @param styleType type label; {@code null} resets to {@link 
#STYLE_TYPE_GENERIC}
+   */
+  public void setStyleType(String styleType) {
+    this.styleType = styleType != null ? styleType : STYLE_TYPE_GENERIC;
+  }
+
   /**
    * Constructs a new instance of this class given its parent and a style 
value describing its
    * behavior and appearance.
@@ -51,7 +173,212 @@ public abstract class TextComposite extends Composite {
    * @throws IllegalArgumentException
    */
   public TextComposite(Composite parent, int style) {
+    this(parent, style, true, new ArrayList<>(), STYLE_TYPE_GENERIC);
+  }
+
+  /**
+   * @param parent parent composite
+   * @param style SWT style for this composite (usually {@link SWT#NONE}; the 
text control has its
+   *     own style)
+   * @param toolbarEnabled whether a toolbar may be shown above the text
+   */
+  public TextComposite(Composite parent, int style, boolean toolbarEnabled) {
+    this(parent, style, toolbarEnabled, new ArrayList<>(), STYLE_TYPE_GENERIC);
+  }
+
+  /**
+   * @param parent parent composite
+   * @param style SWT style for this composite
+   * @param toolbarEnabled whether a toolbar may be shown above the text
+   * @param styleType semantic content type ({@link #STYLE_TYPE_SQL}, …); used 
by toolbar filters
+   */
+  public TextComposite(Composite parent, int style, boolean toolbarEnabled, 
String styleType) {
+    this(parent, style, toolbarEnabled, new ArrayList<>(), styleType);
+  }
+
+  /**
+   * @param parent parent composite
+   * @param style SWT style for this composite
+   * @param toolbarEnabled whether a toolbar may be shown above the text
+   * @param removeToolItems toolbar item IDs to hide for this instance
+   */
+  public TextComposite(
+      Composite parent, int style, boolean toolbarEnabled, List<String> 
removeToolItems) {
+    this(parent, style, toolbarEnabled, removeToolItems, STYLE_TYPE_GENERIC);
+  }
+
+  /**
+   * Full constructor. {@code styleType} is applied <em>before</em> the 
toolbar is created so {@link
+   * org.apache.hop.core.gui.plugin.toolbar.GuiToolbarElementFilter} methods 
can read it.
+   *
+   * @param parent parent composite
+   * @param style SWT style for this composite
+   * @param toolbarEnabled whether a toolbar may be shown above the text
+   * @param removeToolItems toolbar item IDs to hide for this instance
+   * @param styleType semantic content type ({@link #STYLE_TYPE_SQL}, …)
+   */
+  public TextComposite(
+      Composite parent,
+      int style,
+      boolean toolbarEnabled,
+      List<String> removeToolItems,
+      String styleType) {
     super(parent, style);
+    this.props = PropsUi.getInstance();
+    this.toolbarEnabled = toolbarEnabled;
+    this.removeToolItems =
+        removeToolItems != null ? new ArrayList<>(removeToolItems) : new 
ArrayList<>();
+    this.styleType = styleType != null ? styleType : STYLE_TYPE_GENERIC;
+
+    FormLayout layout = new FormLayout();
+    layout.marginLeft = 0;
+    layout.marginRight = 0;
+    layout.marginTop = 0;
+    layout.marginBottom = 0;
+    setLayout(layout);
+
+    addToolbar();
+  }
+
+  /**
+   * Control that content (the text widget and variable icon) should attach 
below, or {@code null}
+   * when no toolbar is shown.
+   */
+  public Control getTopControl() {
+    return toolbar;
+  }
+
+  protected void addToolbar() {
+    toolbarWidgets = new GuiToolbarWidgets();
+    // Register under TextComposite so toolbar listeners and plugin static 
methods resolve this
+    // instance for every subclass (StyledTextComp, SQLStyledTextComp, …).
+    toolbarWidgets.registerGuiPluginObject(TextComposite.class.getName(), 
this);
+
+    if (toolbarEnabled && props.isShowTextCompositeToolbar()) {
+      IToolbarContainer toolBarContainer =
+          ToolbarFacade.createToolbarContainer(this, SWT.WRAP | SWT.LEFT | 
SWT.HORIZONTAL);
+      toolbar = toolBarContainer.getControl();
+      FormData fdToolBar = new FormData();
+      fdToolBar.left = new FormAttachment(0, 0);
+      fdToolBar.top = new FormAttachment(0, 0);
+      fdToolBar.right = new FormAttachment(100, 0);
+      toolbar.setLayoutData(fdToolBar);
+      PropsUi.setLook(toolbar, Props.WIDGET_STYLE_TOOLBAR);
+
+      toolbarWidgets.createToolbarWidgets(toolBarContainer, ID_TOOLBAR, 
removeToolItems);
+      toolbar.pack();
+    }
+  }
+
+  /** Refresh enablement of built-in toolbar buttons based on selection and 
editability. */
+  public void updateToolbar() {
+    if (toolbarWidgets == null || toolbar == null || toolbar.isDisposed()) {
+      return;
+    }
+    boolean editable = isEditable();
+    boolean hasSelection = getSelectionCount() > 0;
+    boolean canPaste = editable && checkPaste();
+
+    toolbarWidgets.enableToolbarItem(ID_TOOLBAR_UNDO, canUndo());
+    toolbarWidgets.enableToolbarItem(ID_TOOLBAR_REDO, canRedo());
+    toolbarWidgets.enableToolbarItem(ID_TOOLBAR_CUT, editable && hasSelection);
+    toolbarWidgets.enableToolbarItem(ID_TOOLBAR_COPY, hasSelection);
+    toolbarWidgets.enableToolbarItem(ID_TOOLBAR_PASTE, canPaste);
+    toolbarWidgets.enableToolbarItem(ID_TOOLBAR_SELECT_ALL, getCharCount() > 
0);
+    toolbarWidgets.enableToolbarItem(ID_TOOLBAR_FIND, true);
+    toolbarWidgets.enableToolbarItem(ID_TOOLBAR_FIND_REPLACE, editable);
+  }
+
+  // --- Toolbar actions (static so they resolve for every TextComposite 
subclass) ---
+
+  @GuiToolbarElement(
+      root = ID_TOOLBAR,
+      id = ID_TOOLBAR_UNDO,
+      image = "ui/images/undo.svg",
+      toolTip = "i18n::TextComposite.ToolBarWidget.Undo.ToolTip")
+  public static void toolbarUndo(TextComposite text) {
+    text.undo();
+    text.updateToolbar();
+  }
+
+  @GuiToolbarElement(
+      root = ID_TOOLBAR,
+      id = ID_TOOLBAR_REDO,
+      image = "ui/images/redo.svg",
+      toolTip = "i18n::TextComposite.ToolBarWidget.Redo.ToolTip")
+  public static void toolbarRedo(TextComposite text) {
+    text.redo();
+    text.updateToolbar();
+  }
+
+  @GuiToolbarElement(
+      root = ID_TOOLBAR,
+      id = ID_TOOLBAR_CUT,
+      image = "ui/images/cut.svg",
+      toolTip = "i18n::TextComposite.ToolBarWidget.Cut.ToolTip",
+      separator = true)
+  public static void toolbarCut(TextComposite text) {
+    text.cut();
+    text.updateToolbar();
+  }
+
+  @GuiToolbarElement(
+      root = ID_TOOLBAR,
+      id = ID_TOOLBAR_COPY,
+      image = "ui/images/copy.svg",
+      toolTip = "i18n::TextComposite.ToolBarWidget.Copy.ToolTip")
+  public static void toolbarCopy(TextComposite text) {
+    text.copy();
+    text.updateToolbar();
+  }
+
+  @GuiToolbarElement(
+      root = ID_TOOLBAR,
+      id = ID_TOOLBAR_PASTE,
+      image = "ui/images/paste.svg",
+      toolTip = "i18n::TextComposite.ToolBarWidget.Paste.ToolTip")
+  public static void toolbarPaste(TextComposite text) {
+    text.paste();
+    text.updateToolbar();
+  }
+
+  @GuiToolbarElement(
+      root = ID_TOOLBAR,
+      id = ID_TOOLBAR_SELECT_ALL,
+      image = "ui/images/select-all.svg",
+      toolTip = "i18n::TextComposite.ToolBarWidget.SelectAll.ToolTip")
+  public static void toolbarSelectAll(TextComposite text) {
+    text.selectAll();
+    text.updateToolbar();
+  }
+
+  @GuiToolbarElement(
+      root = ID_TOOLBAR,
+      id = ID_TOOLBAR_FIND,
+      image = "ui/images/search.svg",
+      toolTip = "i18n::TextComposite.ToolBarWidget.Find.ToolTip",
+      separator = true)
+  public static void toolbarFind(TextComposite text) {
+    text.find();
+  }
+
+  @GuiToolbarElement(
+      root = ID_TOOLBAR,
+      id = ID_TOOLBAR_FIND_REPLACE,
+      image = "ui/images/edit.svg",
+      toolTip = "i18n::TextComposite.ToolBarWidget.FindReplace.ToolTip")
+  public static void toolbarFindReplace(TextComposite text) {
+    text.findAndReplace();
+  }
+
+  /** Open the find dialog (Ctrl+F). */
+  public void find() {
+    FindReplaceDialog.open(getShell(), this, false);
+  }
+
+  /** Open the find and replace dialog (Ctrl+H). */
+  public void findAndReplace() {
+    FindReplaceDialog.open(getShell(), this, true);
   }
 
   public abstract void addModifyListener(ModifyListener lsMod);
@@ -262,6 +589,16 @@ public abstract class TextComposite extends Composite {
     return false;
   }
 
+  /** Whether undo is currently available. */
+  protected boolean canUndo() {
+    return false;
+  }
+
+  /** Whether redo is currently available. */
+  protected boolean canRedo() {
+    return false;
+  }
+
   protected void buildingStyledTextMenu(Menu popupMenu) {
 
     if (isSupportUnoRedo()) {
@@ -271,7 +608,12 @@ public abstract class TextComposite extends Composite {
       undoItem.setImage(
           GuiResource.getInstance()
               .getImage("ui/images/undo.svg", ConstUi.SMALL_ICON_SIZE, 
ConstUi.SMALL_ICON_SIZE));
-      undoItem.addListener(SWT.Selection, event -> undo());
+      undoItem.addListener(
+          SWT.Selection,
+          event -> {
+            undo();
+            updateToolbar();
+          });
 
       final MenuItem redoItem = new MenuItem(popupMenu, SWT.PUSH);
       redoItem.setText(
@@ -279,7 +621,12 @@ public abstract class TextComposite extends Composite {
       redoItem.setImage(
           GuiResource.getInstance()
               .getImage("ui/images/redo.svg", ConstUi.SMALL_ICON_SIZE, 
ConstUi.SMALL_ICON_SIZE));
-      redoItem.addListener(SWT.Selection, event -> redo());
+      redoItem.addListener(
+          SWT.Selection,
+          event -> {
+            redo();
+            updateToolbar();
+          });
 
       new MenuItem(popupMenu, SWT.SEPARATOR);
     }
@@ -290,7 +637,12 @@ public abstract class TextComposite extends Composite {
     cutItem.setImage(
         GuiResource.getInstance()
             .getImage("ui/images/cut.svg", ConstUi.SMALL_ICON_SIZE, 
ConstUi.SMALL_ICON_SIZE));
-    cutItem.addListener(SWT.Selection, event -> cut());
+    cutItem.addListener(
+        SWT.Selection,
+        event -> {
+          cut();
+          updateToolbar();
+        });
 
     final MenuItem copyItem = new MenuItem(popupMenu, SWT.PUSH);
     copyItem.setText(
@@ -306,7 +658,12 @@ public abstract class TextComposite extends Composite {
     pasteItem.setImage(
         GuiResource.getInstance()
             .getImage("ui/images/paste.svg", ConstUi.SMALL_ICON_SIZE, 
ConstUi.SMALL_ICON_SIZE));
-    pasteItem.addListener(SWT.Selection, event -> paste());
+    pasteItem.addListener(
+        SWT.Selection,
+        event -> {
+          paste();
+          updateToolbar();
+        });
 
     new MenuItem(popupMenu, SWT.SEPARATOR);
 
@@ -318,7 +675,31 @@ public abstract class TextComposite extends Composite {
         GuiResource.getInstance()
             .getImage(
                 "ui/images/select-all.svg", ConstUi.SMALL_ICON_SIZE, 
ConstUi.SMALL_ICON_SIZE));
-    selectAllItem.addListener(SWT.Selection, event -> selectAll());
+    selectAllItem.addListener(
+        SWT.Selection,
+        event -> {
+          selectAll();
+          updateToolbar();
+        });
+
+    new MenuItem(popupMenu, SWT.SEPARATOR);
+
+    final MenuItem findItem = new MenuItem(popupMenu, SWT.PUSH);
+    findItem.setText(
+        OsHelper.customizeMenuitemText(BaseMessages.getString(PKG, 
"WidgetDialog.Styled.Find")));
+    findItem.setImage(
+        GuiResource.getInstance()
+            .getImage("ui/images/search.svg", ConstUi.SMALL_ICON_SIZE, 
ConstUi.SMALL_ICON_SIZE));
+    findItem.addListener(SWT.Selection, event -> find());
+
+    final MenuItem findReplaceItem = new MenuItem(popupMenu, SWT.PUSH);
+    findReplaceItem.setText(
+        OsHelper.customizeMenuitemText(
+            BaseMessages.getString(PKG, "WidgetDialog.Styled.FindReplace")));
+    findReplaceItem.setImage(
+        GuiResource.getInstance()
+            .getImage("ui/images/edit.svg", ConstUi.SMALL_ICON_SIZE, 
ConstUi.SMALL_ICON_SIZE));
+    findReplaceItem.addListener(SWT.Selection, event -> findAndReplace());
 
     addListener(
         SWT.KeyDown,
@@ -328,21 +709,27 @@ public abstract class TextComposite extends Composite {
               && (event.stateMask & SWT.MOD1) != 0
               && (event.stateMask & SWT.MOD2) != 0) {
             redo();
+            updateToolbar();
           } else if (isSupportUnoRedo()
               && event.keyCode == 'z'
               && (event.stateMask & SWT.MOD1) != 0) {
             undo();
+            updateToolbar();
           } else if (event.keyCode == 'a' && (event.stateMask & SWT.MOD1) != 
0) {
             selectAll();
+            updateToolbar();
           } else if (event.keyCode == 'f' && (event.stateMask & SWT.MOD1) != 
0) {
-            // TODO: implement FIND
-            // find();
+            find();
+            event.doit = false;
           } else if (event.keyCode == 'h' && (event.stateMask & SWT.MOD1) != 
0) {
-            // TODO: implement FIND AND REPLACE
-            // findAndReplace();
+            findAndReplace();
+            event.doit = false;
           }
         });
 
+    addListener(SWT.Modify, event -> updateToolbar());
+    addListener(SWT.Selection, event -> updateToolbar());
+
     addMenuDetectListener(
         event -> {
           pasteItem.setEnabled(checkPaste());
@@ -353,8 +740,19 @@ public abstract class TextComposite extends Composite {
             cutItem.setEnabled(false);
             copyItem.setEnabled(false);
           }
+          findReplaceItem.setEnabled(isEditable());
+          updateToolbar();
         });
 
     setMenu(popupMenu);
+
+    // Initial enablement once the text control exists
+    getDisplay()
+        .asyncExec(
+            () -> {
+              if (!isDisposed()) {
+                updateToolbar();
+              }
+            });
   }
 }
diff --git 
a/ui/src/main/java/org/apache/hop/ui/core/widget/TextFindSupport.java 
b/ui/src/main/java/org/apache/hop/ui/core/widget/TextFindSupport.java
new file mode 100644
index 0000000000..09468a7844
--- /dev/null
+++ b/ui/src/main/java/org/apache/hop/ui/core/widget/TextFindSupport.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.core.widget;
+
+import java.util.Locale;
+import org.apache.commons.lang3.StringUtils;
+
+/** Pure string helpers for find / replace in a {@link TextComposite}. */
+public final class TextFindSupport {
+
+  private TextFindSupport() {}
+
+  /**
+   * Find the next occurrence of {@code query} in {@code text} at or after 
{@code from}.
+   *
+   * @return start index of the match, or -1 if not found
+   */
+  public static int findNext(String text, String query, int from, boolean 
caseSensitive) {
+    if (StringUtils.isEmpty(text) || StringUtils.isEmpty(query)) {
+      return -1;
+    }
+    int start = Math.max(0, from);
+    if (start > text.length()) {
+      return -1;
+    }
+    if (caseSensitive) {
+      return text.indexOf(query, start);
+    }
+    return 
text.toLowerCase(Locale.ROOT).indexOf(query.toLowerCase(Locale.ROOT), start);
+  }
+
+  /**
+   * Find the previous occurrence of {@code query} in {@code text} ending at 
or before {@code from}.
+   *
+   * @return start index of the match, or -1 if not found
+   */
+  public static int findPrevious(String text, String query, int from, boolean 
caseSensitive) {
+    if (StringUtils.isEmpty(text) || StringUtils.isEmpty(query) || from < 0) {
+      return -1;
+    }
+    int end = Math.min(text.length(), from);
+    if (caseSensitive) {
+      return text.lastIndexOf(query, end);
+    }
+    return 
text.toLowerCase(Locale.ROOT).lastIndexOf(query.toLowerCase(Locale.ROOT), end);
+  }
+
+  /**
+   * Replace every occurrence of {@code query} with {@code replacement}.
+   *
+   * @return number of replacements performed
+   */
+  public static ReplaceAllResult replaceAll(
+      String text, String query, String replacement, boolean caseSensitive) {
+    if (StringUtils.isEmpty(text) || StringUtils.isEmpty(query)) {
+      return new ReplaceAllResult(text, 0);
+    }
+    String safeReplacement = replacement != null ? replacement : "";
+    if (caseSensitive) {
+      int count = 0;
+      int from = 0;
+      StringBuilder result = new StringBuilder();
+      int idx;
+      while ((idx = text.indexOf(query, from)) >= 0) {
+        result.append(text, from, idx);
+        result.append(safeReplacement);
+        from = idx + query.length();
+        count++;
+      }
+      result.append(text, from, text.length());
+      return new ReplaceAllResult(result.toString(), count);
+    }
+
+    String lowerText = text.toLowerCase(Locale.ROOT);
+    String lowerQuery = query.toLowerCase(Locale.ROOT);
+    int count = 0;
+    int from = 0;
+    StringBuilder result = new StringBuilder();
+    int idx;
+    while ((idx = lowerText.indexOf(lowerQuery, from)) >= 0) {
+      result.append(text, from, idx);
+      result.append(safeReplacement);
+      from = idx + query.length();
+      count++;
+    }
+    result.append(text, from, text.length());
+    return new ReplaceAllResult(result.toString(), count);
+  }
+
+  public record ReplaceAllResult(String text, int count) {}
+}
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/dialog/NotePadDialog.java 
b/ui/src/main/java/org/apache/hop/ui/hopgui/dialog/NotePadDialog.java
index e7f54b5282..20c33452b9 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/dialog/NotePadDialog.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/dialog/NotePadDialog.java
@@ -28,6 +28,7 @@ import org.apache.hop.ui.core.dialog.BaseDialog;
 import org.apache.hop.ui.core.gui.GuiResource;
 import org.apache.hop.ui.core.gui.WindowProperty;
 import org.apache.hop.ui.core.widget.StyledTextComp;
+import org.apache.hop.ui.core.widget.TextComposite;
 import org.apache.hop.ui.pipeline.transform.BaseTransformDialog;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.custom.CCombo;
@@ -274,8 +275,8 @@ public class NotePadDialog extends Dialog {
         new StyledTextComp(
             variables,
             wNoteContentComp,
-            SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL);
-
+            SWT.MULTI | SWT.LEFT | SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL,
+            TextComposite.STYLE_TYPE_TEXT);
     wDesc.setText("");
     // Standard widget look (theme-aware). Do not paint note fill/font colors 
into the editor —
     // those are canvas-only and break dark mode. Use a fixed-width font for 
Markdown source.
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineLogDelegate.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineLogDelegate.java
index 0ca0089aaa..2110d67393 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineLogDelegate.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/pipeline/delegates/HopGuiPipelineLogDelegate.java
@@ -134,7 +134,8 @@ public class HopGuiPipelineLogDelegate {
           new StyledTextComp(
               pipelineGraph.getVariables(),
               pipelineLogComposite,
-              SWT.READ_ONLY | SWT.BORDER | SWT.MULTI | SWT.V_SCROLL | 
SWT.H_SCROLL);
+              SWT.READ_ONLY | SWT.BORDER | SWT.MULTI | SWT.V_SCROLL | 
SWT.H_SCROLL,
+              TextComposite.STYLE_TYPE_LOG);
     } else {
       pipelineLogText =
           new StyledTextVar(
@@ -142,7 +143,8 @@ public class HopGuiPipelineLogDelegate {
               pipelineLogComposite,
               SWT.READ_ONLY | SWT.BORDER | SWT.MULTI | SWT.V_SCROLL | 
SWT.H_SCROLL,
               false,
-              false);
+              false,
+              TextComposite.STYLE_TYPE_LOG);
       // Error highlighting is applied directly in HopGuiLogBrowser when 
adding lines
     }
     PropsUi.setLook(pipelineLogText);
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/delegates/HopGuiWorkflowLogDelegate.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/delegates/HopGuiWorkflowLogDelegate.java
index 6cec4b4d52..61a5a1c502 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/delegates/HopGuiWorkflowLogDelegate.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/file/workflow/delegates/HopGuiWorkflowLogDelegate.java
@@ -135,7 +135,8 @@ public class HopGuiWorkflowLogDelegate {
           new StyledTextComp(
               workflowGraph.getVariables(),
               workflowLogComposite,
-              SWT.READ_ONLY | SWT.BORDER | SWT.MULTI | SWT.V_SCROLL | 
SWT.H_SCROLL);
+              SWT.READ_ONLY | SWT.BORDER | SWT.MULTI | SWT.V_SCROLL | 
SWT.H_SCROLL,
+              TextComposite.STYLE_TYPE_LOG);
     } else {
       workflowLogText =
           new StyledTextVar(
@@ -143,7 +144,8 @@ public class HopGuiWorkflowLogDelegate {
               workflowLogComposite,
               SWT.READ_ONLY | SWT.BORDER | SWT.MULTI | SWT.V_SCROLL | 
SWT.H_SCROLL,
               false,
-              false);
+              false,
+              TextComposite.STYLE_TYPE_LOG);
       // Error highlighting is applied directly in HopGuiLogBrowser when 
adding lines
     }
     PropsUi.setLook(workflowLogText);
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigGuiOptionsTab.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigGuiOptionsTab.java
index 6d56069a6c..d0b24fe5b9 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigGuiOptionsTab.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/configuration/tabs/ConfigGuiOptionsTab.java
@@ -117,6 +117,7 @@ public class ConfigGuiOptionsTab {
   private Button wMetricsOnTransforms;
   private Button wHideMenuBar;
   private Button wShowTableViewToolbar;
+  private Button wShowTextCompositeToolbar;
   private Text wMaxPreviewCellLength;
   private Button wShowPreviewLineBreaks;
   private Button wMetricsPanelShowUnits;
@@ -221,6 +222,9 @@ public class ConfigGuiOptionsTab {
       wEnableInfiniteMove.setSelection(props.isInfiniteCanvasMoveEnabled());
       wHideMenuBar.setSelection(props.isHidingMenuBar());
       wShowTableViewToolbar.setSelection(props.isShowTableViewToolbar());
+      if (wShowTextCompositeToolbar != null && 
!wShowTextCompositeToolbar.isDisposed()) {
+        
wShowTextCompositeToolbar.setSelection(props.isShowTextCompositeToolbar());
+      }
       if (wMaxPreviewCellLength != null && 
!wMaxPreviewCellLength.isDisposed()) {
         
wMaxPreviewCellLength.setText(Integer.toString(props.getMaxPreviewCellLength()));
       }
@@ -974,6 +978,16 @@ public class ConfigGuiOptionsTab {
             margin);
     lastTablesControl = wShowTableViewToolbar;
 
+    wShowTextCompositeToolbar =
+        createCheckbox(
+            tablesContent,
+            "EnterOptionsDialog.ShowTextCompositeToolbar.Label",
+            "EnterOptionsDialog.ShowTextCompositeToolbar.ToolTip",
+            props.isShowTextCompositeToolbar(),
+            lastTablesControl,
+            margin);
+    lastTablesControl = wShowTextCompositeToolbar;
+
     // Maximum number of characters shown in a preview grid cell before it is 
truncated.
     Control[] maxPreviewCellLengthControls =
         createTextField(
@@ -1431,6 +1445,9 @@ public class ConfigGuiOptionsTab {
     props.setDarkMode(darkMode);
     props.setHidingMenuBar(wHideMenuBar.getSelection());
     props.setShowTableViewToolbar(wShowTableViewToolbar.getSelection());
+    if (wShowTextCompositeToolbar != null && 
!wShowTextCompositeToolbar.isDisposed()) {
+      
props.setShowTextCompositeToolbar(wShowTextCompositeToolbar.getSelection());
+    }
     props.setMaxPreviewCellLength(
         Const.toInt(wMaxPreviewCellLength.getText(), 
props.getMaxPreviewCellLength()));
     
props.setShowPreviewLineBreaksAsSymbols(wShowPreviewLineBreaks.getSelection());
diff --git 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionLogPanel.java
 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionLogPanel.java
index c224578193..0b41d96add 100644
--- 
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionLogPanel.java
+++ 
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/execution/ExecutionLogPanel.java
@@ -107,7 +107,8 @@ public class ExecutionLogPanel {
           new StyledTextComp(
               Variables.getADefaultVariableSpace(),
               composite,
-              SWT.READ_ONLY | SWT.BORDER | SWT.MULTI | SWT.V_SCROLL | 
SWT.H_SCROLL);
+              SWT.READ_ONLY | SWT.BORDER | SWT.MULTI | SWT.V_SCROLL | 
SWT.H_SCROLL,
+              TextComposite.STYLE_TYPE_LOG);
     } else {
       logText =
           new StyledTextVar(
@@ -115,7 +116,8 @@ public class ExecutionLogPanel {
               composite,
               SWT.READ_ONLY | SWT.BORDER | SWT.MULTI | SWT.V_SCROLL | 
SWT.H_SCROLL,
               false,
-              false);
+              false,
+              TextComposite.STYLE_TYPE_LOG);
     }
     PropsUi.setLook(logText);
     FormData fdText = new FormData();
diff --git 
a/ui/src/main/resources/org/apache/hop/ui/core/dialog/messages/messages_en_US.properties
 
b/ui/src/main/resources/org/apache/hop/ui/core/dialog/messages/messages_en_US.properties
index 9701ebb5c2..ba462b6273 100644
--- 
a/ui/src/main/resources/org/apache/hop/ui/core/dialog/messages/messages_en_US.properties
+++ 
b/ui/src/main/resources/org/apache/hop/ui/core/dialog/messages/messages_en_US.properties
@@ -193,6 +193,20 @@ EnterOptionsDialog.ShowCanvasGrid.Label=Show canvas grid
 EnterOptionsDialog.ShowCanvasGrid.ToolTip=If enabled, the canvas grid will be 
visible
 EnterOptionsDialog.ShowTableViewToolbar.Label=Show toolbar
 EnterOptionsDialog.ShowTableViewToolbar.ToolTip=Enabling this option makes a 
toolbar appear above all tables in the Hop GUI.
+EnterOptionsDialog.ShowTextCompositeToolbar.Label=Show text editor toolbar
+EnterOptionsDialog.ShowTextCompositeToolbar.ToolTip=Enabling this option makes 
a toolbar appear above multi-line text editors (SQL, scripts, and similar 
fields) with cut/copy/paste, undo/redo, and find/replace.
+FindReplaceDialog.CaseSensitive.Label=Case sensitive
+FindReplaceDialog.Find.Label=Find
+FindReplaceDialog.FindNext.Button=Find next
+FindReplaceDialog.FindPrevious.Button=Find previous
+FindReplaceDialog.Replace.Button=Replace
+FindReplaceDialog.Replace.Label=Replace with
+FindReplaceDialog.ReplaceAll.Button=Replace all
+FindReplaceDialog.Shell.FindTitle=Find
+FindReplaceDialog.Shell.ReplaceTitle=Find and replace
+FindReplaceDialog.Status.EmptyFind=Enter text to find
+FindReplaceDialog.Status.NotFound=Text not found
+FindReplaceDialog.Status.ReplaceAllCount={0} occurrence(s) replaced
 EnterOptionsDialog.MaxPreviewCellLength.Label=Maximum grid cell length
 EnterOptionsDialog.MaxPreviewCellLength.ToolTip=The maximum number of 
characters shown in a single grid cell. Longer values are only shortened for 
display\: click a cell to see its full value in place, or use the expand icon 
to open it in the multi-line editor. Copying, exporting and saving always use 
the full value. Set to 0 to disable shortening. This has no effect in the web 
version.
 EnterOptionsDialog.ShowPreviewLineBreaks.Label=Show line breaks as symbols in 
grid cells
diff --git 
a/ui/src/main/resources/org/apache/hop/ui/core/widget/messages/messages_en_US.properties
 
b/ui/src/main/resources/org/apache/hop/ui/core/widget/messages/messages_en_US.properties
index 27428e920e..2d4575ff85 100644
--- 
a/ui/src/main/resources/org/apache/hop/ui/core/widget/messages/messages_en_US.properties
+++ 
b/ui/src/main/resources/org/apache/hop/ui/core/widget/messages/messages_en_US.properties
@@ -109,8 +109,18 @@ TextVar.InternalVariable.Message=This is an internal 
variable.
 TextVar.tooltip.InsertVariable=Enter CTRL-SPACE to select a variable to insert
 TextVar.tooltip.ExpandedInteger=This field accepts expanded integer values:\n- 
Grouping: 100,000,000 or 100.000.000 or 100_000_000\n- Suffixes: 100k, 100m, 
1g, 1b  (\u00d71,000 / \u00d71,000,000 / \u00d71,000,000,000)\n- Fractional: 
1.5m\n- Scientific: 1e8, 1x10^8, 10^8\nVariables ($'{'...'}') are resolved 
before parsing.
 TextVar.VariableValue.Message=The value of variable ''{0}'' is \: \n\n\t{1}\n\n
+TextComposite.ToolBarWidget.Copy.ToolTip=Copy selected text
+TextComposite.ToolBarWidget.Cut.ToolTip=Cut selected text
+TextComposite.ToolBarWidget.Find.ToolTip=Find text
+TextComposite.ToolBarWidget.FindReplace.ToolTip=Find and replace text
+TextComposite.ToolBarWidget.Paste.ToolTip=Paste text from clipboard
+TextComposite.ToolBarWidget.Redo.ToolTip=Redo the last action
+TextComposite.ToolBarWidget.SelectAll.ToolTip=Select all text
+TextComposite.ToolBarWidget.Undo.ToolTip=Undo the last action
 WidgetDialog.Styled.Copy=Copy\tCtrl+C
 WidgetDialog.Styled.Cut=Cut\tCtrl+X
+WidgetDialog.Styled.Find=Find\tCtrl+F
+WidgetDialog.Styled.FindReplace=Find/Replace\tCtrl+H
 WidgetDialog.Styled.Paste=Paste\tCtrl+V
 WidgetDialog.Styled.Redo=Redo\tCtrl+Shift+Z
 WidgetDialog.Styled.SelectAll=Select &All\tCtrl+A
diff --git 
a/ui/src/test/java/org/apache/hop/ui/core/widget/TextFindSupportTest.java 
b/ui/src/test/java/org/apache/hop/ui/core/widget/TextFindSupportTest.java
new file mode 100644
index 0000000000..cef9d4e2f0
--- /dev/null
+++ b/ui/src/test/java/org/apache/hop/ui/core/widget/TextFindSupportTest.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.core.widget;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+class TextFindSupportTest {
+
+  @Test
+  void findNextCaseSensitive() {
+    String text = "Select From from FROM dual";
+    assertEquals(7, TextFindSupport.findNext(text, "From", 0, true));
+    assertEquals(12, TextFindSupport.findNext(text, "from", 0, true));
+    assertEquals(-1, TextFindSupport.findNext(text, "missing", 0, true));
+  }
+
+  @Test
+  void findNextCaseInsensitive() {
+    String text = "Select From dual";
+    assertEquals(7, TextFindSupport.findNext(text, "FROM", 0, false));
+    assertEquals(-1, TextFindSupport.findNext(text, "from", 8, false));
+  }
+
+  @Test
+  void findPrevious() {
+    String text = "aaa bbb aaa ccc";
+    assertEquals(8, TextFindSupport.findPrevious(text, "aaa", 10, true));
+    assertEquals(0, TextFindSupport.findPrevious(text, "aaa", 7, true));
+    assertEquals(-1, TextFindSupport.findPrevious(text, "aaa", -1, true));
+  }
+
+  @Test
+  void replaceAllCaseSensitive() {
+    TextFindSupport.ReplaceAllResult result =
+        TextFindSupport.replaceAll("aAa aAa aaa", "aAa", "X", true);
+    assertEquals(2, result.count());
+    assertEquals("X X aaa", result.text());
+  }
+
+  @Test
+  void replaceAllCaseInsensitive() {
+    TextFindSupport.ReplaceAllResult result =
+        TextFindSupport.replaceAll("Hello hello HELLO", "hello", "Hi", false);
+    assertEquals(3, result.count());
+    assertEquals("Hi Hi Hi", result.text());
+  }
+}

Reply via email to