mattcasters commented on code in PR #8706:
URL: https://github.com/apache/hop/pull/8706#discussion_r4153897824


##########
docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-configuration.adoc:
##########
@@ -28,125 +31,275 @@ The configuration perspective offers a central location 
for all Apache Hop confi
 
 == Usage
 
-The configuration perspective groups configuration options in the following 
tabs:
+The perspective lists its sections in a tree on the left. Selecting one shows 
its options on the right; the search box above the tree finds an option by name 
across all of them.
 
-* **General** options control the behavior of Apache Hop and Hop Gui.
-* **Look & Feel** control how Hop Gui looks like on your desktop or in 
your browser.
-* **Plugin** options provide configuration options to control the behavior of 
your available Apache Hop plugins (including Explorer, 
xref:hop-gui/perspective-search.adoc[Search] limits, and 
xref:hop-gui/perspective-ai-advisor.adoc[AI Assistant] enablement).
-* **System variables** allows you to set and configure Apache Hop global 
variables.
+* **General**: how Apache Hop and Hop GUI behave - confirmations, tooltips and 
dialog positions.
+* **Look & Feel**: how Hop GUI is drawn on your desktop or in your browser.
+* **Plugins**: one entry per plugin that contributes options, including 
xref:hop-gui/perspective-search.adoc[Search], the Explorer perspective and 
xref:hop-gui/perspective-ai-advisor.adoc[AI Assistant]. Every option is listed 
in <<_plugin_options_reference,Plugin options reference>> below.
+* **System variables**: set and inspect the Apache Hop global variables.
+* **Security**: how a session is authenticated, split over General, Basic, 
External and OAuth sub-tabs. Until authentication is turned on the session is 
unrestricted and anyone can change these settings, which the tab says at the 
top.
+* **Keyboard shortcuts**: the shortcuts currently in effect, with the action 
each one triggers.
 
-== Detailed configuration tabs overview
+== Detailed options overview
 
 === General
 
-The General configuration tab allows you to configure the core Apache Hop 
options.
+The General tab holds the options that control how Hop GUI behaves rather than 
how it looks. The tables below follow the sections of the tab.
 
 [%header, width=90%, cols="2,5,1"]
 |===
 |Option|Description|Default
+
 |Hop configuration filename
-a|the location where Apache Hop will store the configuration file. This 
configuration file will contain your project and environment information and 
global variables.
+a|Where Apache Hop stores `hop-config.json`, the file holding your projects, 
environments and global variables. Read-only here.
 
-TIP: set the environment variable `HOP_CONFIG_FOLDER` to set this location 
dynamically to allow easy switching between different Apache Hop versions and 
installations. xref:protips/index.adoc[read more]
+TIP: set the environment variable `HOP_CONFIG_FOLDER` to change this location, 
which makes switching between Hop versions and installations easy. 
xref:protips/index.adoc[read more]
 |`<INSTALLATION_PATH>/config/hop-config.json`
 
 |Preview data batch size
-a|number of rows that will be shown when previewing through the Pipeline run 
toolbar or from transform preview dialogs.
+a|How many rows are fetched when previewing from the pipeline run toolbar or a 
transform preview dialog.
 
-INFO: this setting complements the preview option that is used in the 
xref:pipeline/pipeline-run-configurations/native-local-pipeline-engine.adoc[native
 local pipeline run configuration], which can be accessed through the 
image:transforms/icons/datagrid.svg[data grid, width="16px"]  `preview data` 
icon in the bottom right corner of transforms after a successful run.
+INFO: this complements the preview setting of the 
xref:pipeline/pipeline-run-configurations/native-local-pipeline-engine.adoc[native
 local pipeline run configuration], reachable through the 
image:transforms/icons/datagrid.svg[data grid, width="16px"] `preview data` 
icon in the bottom right corner of a transform after a run.
 |1000
 
-|Use database cache|enable caching for relational databases|yes
+|Use database cache|Cache relational database metadata so repeated lookups of 
tables, fields and types do not go back to the server.|yes
 
-|Open last file at startup|reopen the last used files and select the last used 
file when Hop GUI starts|yes
+|Reopen tabs on startup|Reopen the files that were open when Hop GUI last 
closed, and select the one that was active.|yes
 
 |Enable embedded terminal
-a|Show the embedded terminal in the bottom panel. Clear this on desktops where 
that terminal misbehaves. Search, the database workbench, and the VFS file 
explorer stay available. Hop Web has no embedded terminal. Excluding 
`40010-menu-tools-terminal` in 
xref:hop-gui/disable-ui-elements.adoc[disabledGuiElements.xml] forces it off.
+a|Show the embedded terminal in the bottom panel. Clear this on desktops where 
that terminal misbehaves; search, the database workbench and the VFS file 
explorer stay available either way. Hop Web has no embedded terminal. Excluding 
`40010-menu-tools-terminal` in 
xref:hop-gui/disable-ui-elements.adoc[disabledGuiElements.xml] forces it off.
 |yes
 
-|Autosave changed files|Automatically save changed workflow and pipelines 
files|yes
+|Reload file if changed on filesystem|Reload a pipeline or workflow when it 
changes outside Hop GUI, for example after a `git pull` or an edit in another 
editor.|no
 
-|Automatically split hops
-a|Automatically split hops when a new transform or action is dropped on a hop.
+|Sort field names alphabetically in dropdowns|Sort field names in dropdown 
lists alphabetically. When off, fields keep the order they arrive in from the 
incoming stream.|yes
 
-TIP: Hops will be shown in bold when a transform or action can be dropped on it
-|yes
+|Use global bookmarks in the file dialog|Share the file dialog's bookmarks 
across every project instead of keeping a separate set per project.|yes
+
+|Maximum execution logging text size|The largest amount of log text held for 
the execution information perspective, in characters. Caps memory use on 
long-running or very chatty executions.|2000000
+|===
 
-|Show Copy or Distribute Dialog|Show a pop-up dialog to ask the user to 
confirm whether data should be copied or distributed (round-robin) to 
subsequent transforms in a pipeline|yes
+==== Confirmation dialogs
 
-|Ask user when exiting|Show a pop-up dialog to let the user confirm before 
exiting Hop GUI|yes
+Each option here controls one confirmation pop-up. Turning one off does not 
change what the action does, only whether Hop asks first. Every one of these 
dialogs can also be switched off from the dialog itself, with its *Do not ask 
this again* option.
+
+[%header, width=90%, cols="2,5,1"]
+|===
+|Option|Description|Default
 
-|Clear customer parameters (transforms/plugins|Deletes all custom flags and 
parameters in the dialogs of the transforms and plugins|on click
+|Show confirmation when exiting Hop|Ask for confirmation before closing Hop 
GUI.|yes
 
-|Auto collapse palette tree|Collapse or expand the categories in the context 
dialog|no (expanded)
+|Show copy or distribute dialog when drawing a hop|Ask whether rows should be 
copied to, or distributed round-robin over, the following transforms when a 
second hop is drawn out of a transform.|yes
 
-|Display tooltips|Show tooltips where available in Hop GUI|yes
+|Show confirmation when splitting hops|Ask before splitting a hop when a 
transform or action is dropped onto it. With this off, the hop is split without 
asking. Hops that a dropped item would split are drawn in bold.|yes
 
-|Show help tooltips|Show help tooltips where available in Hop GUI|yes
+|Show confirmation to save file when starting pipeline or workflow|Ask whether 
to save a changed file before running it. With this off, the file is saved 
automatically on every run.|yes
+
+|Reset all to enabled (Includes Action/Transform warnings)|Button. Switches 
every confirmation dialog back on, including the per-transform and per-action 
warnings that were dismissed with *Do not ask this again*.|on click
+|===
+
+==== Tooltips
+
+[%header, width=90%, cols="2,5,1"]
+|===
+|Option|Description|Default
 
-|Use double click on canvas?
-|double-click instead of single click on e.g. transform and action dialogs.
+|Show tooltip when hovering over a hop or Action/Transform|The master switch 
for the canvas tooltips. With this off, none of them appear, whatever is ticked 
under *Tooltips* in the 
xref:hop-gui/perspective-configuration.adoc#_look_feel[Look & Feel] tab.|yes
+
+|Show resolved variables in tooltips|Show what a variable currently resolves 
to in the tooltip, rather than the `+'${VARIABLE}'+` expression as written.|yes
+
+|Reset all to enabled|Button. Switches every tooltip option back on.|on click
+|===
+
+==== Dialog positioning
+
+[%header, width=90%, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|Remember dialog positions|Reopen each dialog at the position and size it was 
last given. With this off, every dialog opens at its default position and 
size.|yes
+
+|Reset dialog positions after restart|Keep remembered positions for the 
current session only, clearing `shells-state.json` on restart. With this off, 
positions are also written to that file and restored the next time Hop GUI 
starts. Only has an effect while *Remember dialog positions* is on.|no
+
+|Clear dialog positions|Button. Forgets every remembered dialog position and 
resets the perspective tree-panel widths to their defaults. Useful after moving 
between monitors of different sizes, when a dialog reopens off-screen.|on click
+|===
+
+=== Look & Feel
 
-enabled: double-click on a transform or action icon to open its properties. A 
single click on the icon opens the context dialog.
-disabled (default): (single) click on a transform or action icon to open the 
context dialog. (single) click on a transform or action name to open its 
properties.
+The Look & Feel tab holds everything about how Hop GUI is drawn: fonts, 
colours, the canvas, and which columns and toolbars are shown. The tables below 
follow the sections of the tab.
+
+Some of these settings only take effect after Hop GUI is restarted; the dialog 
says so where that is the case.
+
+==== General appearance
+
+[%header, width=90%, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|Preferred language
+a|The language Hop GUI is shown in.
+
+TIP: Check the 
https://hop.apache.org/community/contribution-guides/translation-contribution-guide/[Translation
 Contribution Guide] to translate Apache Hop into your own language.
+|English (US)
+
+|Hide the menu bar|Hide the menu bar. With it hidden, the menu is reachable 
from the Apache Hop icon in the upper left corner of Hop GUI.|yes
+
+|Dark mode|Use the dark theme. On Hop Web and Windows this is a straight 
toggle; on macOS and other desktop platforms the theme follows the operating 
system. Changing it in Hop Web reloads the page.|follows the OS
+
+|Follow system dark mode (Hop Web)|Take the light or dark setting from the 
system or browser. While this is on, the *Dark mode* option above is 
disabled.|no

Review Comment:
   **[bug]** The default is on, not off. `ConfigGuiOptionsTab.reloadValues()` 
starts `followSystem` at `true` and only replaces it from the Hop Web audit 
state (`extractBoolean("followSystem", true)`). Opening the configuration 
perspective runs that reload, so a session that has never stored the option 
shows the box checked and disables Dark mode. The `false` passed to 
`createCheckbox` is only the value before that reload.
   
   **Suggestion:** Document the default as yes.



##########
docs/hop-user-manual/modules/ROOT/pages/hop-gui/perspective-configuration.adoc:
##########
@@ -28,125 +31,275 @@ The configuration perspective offers a central location 
for all Apache Hop confi
 
 == Usage
 
-The configuration perspective groups configuration options in the following 
tabs:
+The perspective lists its sections in a tree on the left. Selecting one shows 
its options on the right; the search box above the tree finds an option by name 
across all of them.
 
-* **General** options control the behavior of Apache Hop and Hop Gui.
-* **Look &amp; Feel** control how Hop Gui looks like on your desktop or in 
your browser.
-* **Plugin** options provide configuration options to control the behavior of 
your available Apache Hop plugins (including Explorer, 
xref:hop-gui/perspective-search.adoc[Search] limits, and 
xref:hop-gui/perspective-ai-advisor.adoc[AI Assistant] enablement).
-* **System variables** allows you to set and configure Apache Hop global 
variables.
+* **General**: how Apache Hop and Hop GUI behave - confirmations, tooltips and 
dialog positions.
+* **Look &amp; Feel**: how Hop GUI is drawn on your desktop or in your browser.
+* **Plugins**: one entry per plugin that contributes options, including 
xref:hop-gui/perspective-search.adoc[Search], the Explorer perspective and 
xref:hop-gui/perspective-ai-advisor.adoc[AI Assistant]. Every option is listed 
in <<_plugin_options_reference,Plugin options reference>> below.
+* **System variables**: set and inspect the Apache Hop global variables.
+* **Security**: how a session is authenticated, split over General, Basic, 
External and OAuth sub-tabs. Until authentication is turned on the session is 
unrestricted and anyone can change these settings, which the tab says at the 
top.
+* **Keyboard shortcuts**: the shortcuts currently in effect, with the action 
each one triggers.
 
-== Detailed configuration tabs overview
+== Detailed options overview
 
 === General
 
-The General configuration tab allows you to configure the core Apache Hop 
options.
+The General tab holds the options that control how Hop GUI behaves rather than 
how it looks. The tables below follow the sections of the tab.
 
 [%header, width=90%, cols="2,5,1"]
 |===
 |Option|Description|Default
+
 |Hop configuration filename
-a|the location where Apache Hop will store the configuration file. This 
configuration file will contain your project and environment information and 
global variables.
+a|Where Apache Hop stores `hop-config.json`, the file holding your projects, 
environments and global variables. Read-only here.
 
-TIP: set the environment variable `HOP_CONFIG_FOLDER` to set this location 
dynamically to allow easy switching between different Apache Hop versions and 
installations. xref:protips/index.adoc[read more]
+TIP: set the environment variable `HOP_CONFIG_FOLDER` to change this location, 
which makes switching between Hop versions and installations easy. 
xref:protips/index.adoc[read more]
 |`<INSTALLATION_PATH>/config/hop-config.json`
 
 |Preview data batch size
-a|number of rows that will be shown when previewing through the Pipeline run 
toolbar or from transform preview dialogs.
+a|How many rows are fetched when previewing from the pipeline run toolbar or a 
transform preview dialog.
 
-INFO: this setting complements the preview option that is used in the 
xref:pipeline/pipeline-run-configurations/native-local-pipeline-engine.adoc[native
 local pipeline run configuration], which can be accessed through the 
image:transforms/icons/datagrid.svg[data grid, width="16px"]  `preview data` 
icon in the bottom right corner of transforms after a successful run.
+INFO: this complements the preview setting of the 
xref:pipeline/pipeline-run-configurations/native-local-pipeline-engine.adoc[native
 local pipeline run configuration], reachable through the 
image:transforms/icons/datagrid.svg[data grid, width="16px"] `preview data` 
icon in the bottom right corner of a transform after a run.
 |1000
 
-|Use database cache|enable caching for relational databases|yes
+|Use database cache|Cache relational database metadata so repeated lookups of 
tables, fields and types do not go back to the server.|yes
 
-|Open last file at startup|reopen the last used files and select the last used 
file when Hop GUI starts|yes
+|Reopen tabs on startup|Reopen the files that were open when Hop GUI last 
closed, and select the one that was active.|yes
 
 |Enable embedded terminal
-a|Show the embedded terminal in the bottom panel. Clear this on desktops where 
that terminal misbehaves. Search, the database workbench, and the VFS file 
explorer stay available. Hop Web has no embedded terminal. Excluding 
`40010-menu-tools-terminal` in 
xref:hop-gui/disable-ui-elements.adoc[disabledGuiElements.xml] forces it off.
+a|Show the embedded terminal in the bottom panel. Clear this on desktops where 
that terminal misbehaves; search, the database workbench and the VFS file 
explorer stay available either way. Hop Web has no embedded terminal. Excluding 
`40010-menu-tools-terminal` in 
xref:hop-gui/disable-ui-elements.adoc[disabledGuiElements.xml] forces it off.
 |yes
 
-|Autosave changed files|Automatically save changed workflow and pipelines 
files|yes
+|Reload file if changed on filesystem|Reload a pipeline or workflow when it 
changes outside Hop GUI, for example after a `git pull` or an edit in another 
editor.|no
 
-|Automatically split hops
-a|Automatically split hops when a new transform or action is dropped on a hop.
+|Sort field names alphabetically in dropdowns|Sort field names in dropdown 
lists alphabetically. When off, fields keep the order they arrive in from the 
incoming stream.|yes
 
-TIP: Hops will be shown in bold when a transform or action can be dropped on it
-|yes
+|Use global bookmarks in the file dialog|Share the file dialog's bookmarks 
across every project instead of keeping a separate set per project.|yes
+
+|Maximum execution logging text size|The largest amount of log text held for 
the execution information perspective, in characters. Caps memory use on 
long-running or very chatty executions.|2000000
+|===
 
-|Show Copy or Distribute Dialog|Show a pop-up dialog to ask the user to 
confirm whether data should be copied or distributed (round-robin) to 
subsequent transforms in a pipeline|yes
+==== Confirmation dialogs
 
-|Ask user when exiting|Show a pop-up dialog to let the user confirm before 
exiting Hop GUI|yes
+Each option here controls one confirmation pop-up. Turning one off does not 
change what the action does, only whether Hop asks first. Every one of these 
dialogs can also be switched off from the dialog itself, with its *Do not ask 
this again* option.
+
+[%header, width=90%, cols="2,5,1"]
+|===
+|Option|Description|Default
 
-|Clear customer parameters (transforms/plugins|Deletes all custom flags and 
parameters in the dialogs of the transforms and plugins|on click
+|Show confirmation when exiting Hop|Ask for confirmation before closing Hop 
GUI.|yes
 
-|Auto collapse palette tree|Collapse or expand the categories in the context 
dialog|no (expanded)
+|Show copy or distribute dialog when drawing a hop|Ask whether rows should be 
copied to, or distributed round-robin over, the following transforms when a 
second hop is drawn out of a transform.|yes
 
-|Display tooltips|Show tooltips where available in Hop GUI|yes
+|Show confirmation when splitting hops|Ask before splitting a hop when a 
transform or action is dropped onto it. With this off, the hop is split without 
asking. Hops that a dropped item would split are drawn in bold.|yes
 
-|Show help tooltips|Show help tooltips where available in Hop GUI|yes
+|Show confirmation to save file when starting pipeline or workflow|Ask whether 
to save a changed file before running it. With this off, the file is saved 
automatically on every run.|yes
+
+|Reset all to enabled (Includes Action/Transform warnings)|Button. Switches 
every confirmation dialog back on, including the per-transform and per-action 
warnings that were dismissed with *Do not ask this again*.|on click
+|===
+
+==== Tooltips
+
+[%header, width=90%, cols="2,5,1"]
+|===
+|Option|Description|Default
 
-|Use double click on canvas?
-|double-click instead of single click on e.g. transform and action dialogs.
+|Show tooltip when hovering over a hop or Action/Transform|The master switch 
for the canvas tooltips. With this off, none of them appear, whatever is ticked 
under *Tooltips* in the 
xref:hop-gui/perspective-configuration.adoc#_look_feel[Look & Feel] tab.|yes
+
+|Show resolved variables in tooltips|Show what a variable currently resolves 
to in the tooltip, rather than the `+'${VARIABLE}'+` expression as written.|yes
+
+|Reset all to enabled|Button. Switches every tooltip option back on.|on click
+|===
+
+==== Dialog positioning
+
+[%header, width=90%, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|Remember dialog positions|Reopen each dialog at the position and size it was 
last given. With this off, every dialog opens at its default position and 
size.|yes
+
+|Reset dialog positions after restart|Keep remembered positions for the 
current session only, clearing `shells-state.json` on restart. With this off, 
positions are also written to that file and restored the next time Hop GUI 
starts. Only has an effect while *Remember dialog positions* is on.|no
+
+|Clear dialog positions|Button. Forgets every remembered dialog position and 
resets the perspective tree-panel widths to their defaults. Useful after moving 
between monitors of different sizes, when a dialog reopens off-screen.|on click
+|===
+
+=== Look & Feel
 
-enabled: double-click on a transform or action icon to open its properties. A 
single click on the icon opens the context dialog.
-disabled (default): (single) click on a transform or action icon to open the 
context dialog. (single) click on a transform or action name to open its 
properties.
+The Look & Feel tab holds everything about how Hop GUI is drawn: fonts, 
colours, the canvas, and which columns and toolbars are shown. The tables below 
follow the sections of the tab.
+
+Some of these settings only take effect after Hop GUI is restarted; the dialog 
says so where that is the case.
+
+==== General appearance
+
+[%header, width=90%, cols="2,5,1"]
+|===
+|Option|Description|Default
+
+|Preferred language
+a|The language Hop GUI is shown in.
+
+TIP: Check the 
https://hop.apache.org/community/contribution-guides/translation-contribution-guide/[Translation
 Contribution Guide] to translate Apache Hop into your own language.
+|English (US)
+
+|Hide the menu bar|Hide the menu bar. With it hidden, the menu is reachable 
from the Apache Hop icon in the upper left corner of Hop GUI.|yes
+
+|Dark mode|Use the dark theme. On Hop Web and Windows this is a straight 
toggle; on macOS and other desktop platforms the theme follows the operating 
system. Changing it in Hop Web reloads the page.|follows the OS

Review Comment:
   **[suggestion]** The Default cell says the theme follows the OS, but that is 
only the non-Windows desktop path. On Windows and Hop Web `ConfigGuiOptionsTab` 
uses `props.isDarkMode()`, and `PropsUi.isDarkMode()` falls back to `N`, so 
Windows comes up in the light theme. The sentence already says Windows is a 
toggle; the default column should say the same (light / off on Windows, follows 
the OS on macOS and other desktops).



##########
ui/src/main/java/org/apache/hop/ui/hopgui/welcome/WelcomeDialogOptions.java:
##########
@@ -167,6 +172,6 @@ public void widgetModified(
   @Override
   public void persistContents(GuiCompositeWidgets compositeWidgets) {
     HopConfig.getInstance()
-        .saveOption(WelcomeDialog.HOP_CONFIG_NO_SHOW_OPTION, 
welcomeDialogShowAtStartup);
+        .saveOption(WelcomeDialog.HOP_CONFIG_NO_SHOW_OPTION, 
!welcomeDialogShowAtStartup);

Review Comment:
   **[suggestion]** The constructor inversion is covered by 
`ConfigPluginDefaultsTest`, but this write is not. `persistContents` does not 
use the widget argument, so a unit test can set `welcomeDialogShowAtStartup` 
and call `persistContents(null)`, then assert `doNotShowWelcomeDialog` is the 
opposite. Reverting only this line would put the checkbox bug back without 
failing the new test.
   
   **Suggestion:** Assert both polarities: show stores `false`, hide stores 
`true`.



##########
config-doc/src/main/java/org/apache/hop/config/doc/ConfigDocGenerator.java:
##########
@@ -0,0 +1,526 @@
+/*
+ * 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.config.doc;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.stream.Stream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import org.jboss.jandex.AnnotationInstance;
+import org.jboss.jandex.AnnotationTarget;
+import org.jboss.jandex.AnnotationValue;
+import org.jboss.jandex.ClassInfo;
+import org.jboss.jandex.DotName;
+import org.jboss.jandex.FieldInfo;
+import org.jboss.jandex.Index;
+import org.jboss.jandex.IndexReader;
+
+/**
+ * Generates one AsciiDoc partial per configuration plugin from the 
annotations the configuration
+ * perspective already builds its widgets from. Build time only: it reads the 
Jandex indexes the
+ * build writes into every module and the message bundles next to the classes, 
and instantiates only
+ * the plain configuration objects, never Hop itself.
+ */
+public class ConfigDocGenerator {
+
+  static final DotName CONFIG_PLUGIN =
+      DotName.createSimple("org.apache.hop.core.config.plugin.ConfigPlugin");
+  static final DotName GUI_WIDGET =
+      DotName.createSimple("org.apache.hop.core.gui.plugin.GuiWidgetElement");
+  static final DotName GUI_PLUGIN =
+      DotName.createSimple("org.apache.hop.core.gui.plugin.GuiPlugin");
+  static final String PARENT_ID = "EnterOptionsDialog-GuiWidgetsParent";
+  static final String JANDEX_INDEX = "META-INF/jandex.idx";
+  static final String AGGREGATE = "all-plugins.adoc";
+
+  /** First line of every page this generator owns, and how it tells them from 
hand-written ones. */
+  static final String GENERATED_MARKER =
+      "// Generated from the @ConfigPlugin and @GuiWidgetElement annotations.";
+
+  static final String LICENSE =
+      """
+      ////
+      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.
+      ////
+      """;
+
+  record Option(
+      String order, String label, String toolTip, String type, boolean 
variables, String dflt) {}
+
+  record Plugin(
+      String id, String description, String name, String configKey, 
List<Option> options) {}
+
+  public static void main(String[] args) throws Exception {
+    Path root = Paths.get(args[0]).toAbsolutePath().normalize();
+    Path outDir = Paths.get(args[1]).toAbsolutePath().normalize();
+
+    // Scan this program's own classpath rather than the source tree. Every 
module declaring a
+    // @ConfigPlugin is a declared dependency, so each one is here whether it 
was just built in the
+    // reactor or resolved as a jar from the local repository. Walking target/ 
directories instead
+    // would find nothing for the modules an incremental build left alone.
+    List<Source> sources = new ArrayList<>();
+    for (String entry : 
System.getProperty("java.class.path").split(File.pathSeparator)) {
+      Source source = Source.of(Paths.get(entry));
+      if (source != null) sources.add(source);
+    }
+
+    List<Plugin> plugins = new ArrayList<>();
+    for (Source source : sources) {
+      Index index;
+      try (InputStream in = source.open(JANDEX_INDEX)) {
+        if (in == null) continue;
+        index = new IndexReader(in).read();
+      } catch (Exception e) {
+        continue;
+      }
+      for (AnnotationInstance ai : index.getAnnotations(CONFIG_PLUGIN)) {
+        if (ai.target().kind() != AnnotationTarget.Kind.CLASS) continue;
+        Plugin p = readPlugin(ai.target().asClass(), ai, source);
+        if (p != null) plugins.add(p);
+      }
+    }
+    plugins.sort(Comparator.comparing(Plugin::name, 
String.CASE_INSENSITIVE_ORDER));
+
+    // Only the pages this generator wrote are its business. Anything else in 
the directory was
+    // put there by hand and is left alone, neither counted nor deleted.
+    List<Path> generatedBefore = generatedPages(outDir);
+
+    // Order matters here. An empty classpath means every plugin looks like 
one this generator
+    // cannot see, and the report below would then name all of them and ask 
for fourteen new
+    // dependencies - true in its way, and useless. "There is nothing here at 
all" is the more
+    // fundamental thing to have gone wrong, so it is the one to say.
+    if (plugins.isEmpty() && !generatedBefore.isEmpty()) {
+      throw new IllegalStateException(
+          "No configuration plugins found on the classpath, but "
+              + outDir.getFileName()
+              + " documents "
+              + generatedBefore.size()
+              + ". Refusing to replace those pages with nothing. This means 
the classpath is"
+              + " missing the modules that declare a @ConfigPlugin - build the 
project first.");
+    }
+
+    reportPluginsOutsideTheClasspath(root, plugins);
+
+    Files.createDirectories(outDir);
+    Set<Path> written = new HashSet<>();
+    for (Plugin p : plugins) {
+      Path file = outDir.resolve(p.id() + ".adoc");
+      Files.writeString(file, render(p), StandardCharsets.UTF_8);
+      written.add(file);
+      System.out.printf(
+          "%-34s %2d options -> %s%n", p.name(), p.options().size(), 
root.relativize(file));
+    }
+
+    // One page wants the lot: a section per plugin, in the order the 
perspective's tree shows them.
+    StringBuilder all = new StringBuilder(LICENSE);
+    all.append(GENERATED_MARKER).append("\n// Edit those, not this file.\n");
+    for (Plugin p : plugins) {
+      all.append("\n== ").append(p.name()).append("\n\n").append(body(p));
+    }
+    Path index = outDir.resolve(AGGREGATE);
+    Files.writeString(index, all.toString(), StandardCharsets.UTF_8);
+    written.add(index);
+    System.out.println("aggregate -> " + root.relativize(index));
+
+    // A plugin that was renamed or removed leaves its page behind, and the 
pages that include one
+    // by name would go on rendering it for good. Nothing reads a generated 
page that this run did
+    // not write, so it goes.
+    for (Path stale : generatedBefore) {
+      if (!written.contains(stale)) {
+        Files.delete(stale);
+        System.out.println("removed (no longer generated) -> " + 
root.relativize(stale));
+      }
+    }
+    System.out.println(
+        "\n"
+            + plugins.size()
+            + " configuration plugins, "
+            + plugins.stream().mapToInt(p -> p.options().size()).sum()
+            + " options");
+  }
+
+  /**
+   * Which modules a documented plugin can come from is decided by this 
module's dependencies, and
+   * that list is written by hand. A plugin added to a module that is not on 
it would be absent from
+   * the manual with nothing to say so: the page set would not change, so the 
drift check in CI
+   * would pass, and the option would simply never be written about.
+   *
+   * <p>So: sweep what the build has actually compiled and see whether 
anything turns up that the
+   * classpath did not. This is deliberately only a report - it never adds to 
the pages, because the
+   * set of modules on disk depends on what this particular build chose to 
compile, and the pages
+   * have to come out the same everywhere. An incremental build that has not 
compiled the new module
+   * simply finds nothing, which is why this is best-effort. A full build - 
every push to main is
+   * one - does find it.
+   */
+  static void reportPluginsOutsideTheClasspath(Path root, List<Plugin> 
documented)
+      throws IOException {
+    Set<String> known = new HashSet<>();
+    for (Plugin p : documented) {
+      known.add(p.id());
+    }
+
+    Map<String, String> missing = new TreeMap<>();
+    try (Stream<Path> walk = Files.walk(root)) {
+      for (Path index : 
walk.filter(ConfigDocGenerator::isModuleIndex).toList()) {
+        Path classes = index.getParent().getParent();
+        Source source = Source.of(classes);
+        if (source == null) continue;
+        Index jandex;
+        try (InputStream in = source.open(JANDEX_INDEX)) {
+          if (in == null) continue;
+          jandex = new IndexReader(in).read();
+        } catch (Exception e) {
+          continue;
+        }
+        for (AnnotationInstance ai : jandex.getAnnotations(CONFIG_PLUGIN)) {
+          if (ai.target().kind() != AnnotationTarget.Kind.CLASS) continue;
+          String id = str(ai, "id", "");
+          if (known.contains(id) || 
!hasPerspectiveWidgets(ai.target().asClass())) continue;
+          // target/classes/META-INF/jandex.idx -> the module directory
+          missing.put(id, 
root.relativize(classes.getParent().getParent()).toString());
+        }
+      }
+    }
+    if (missing.isEmpty()) return;
+
+    StringBuilder message =
+        new StringBuilder(
+            "These configuration plugins add options to the configuration 
perspective but are in"
+                + " modules this generator cannot see, so the manual would not 
describe them:\n");
+    missing.forEach(
+        (id, module) ->
+            message.append("  ").append(id).append("  in  
").append(module).append("\n"));
+    message.append(
+        "Add the module as a dependency of config-doc/pom.xml, next to the 
others, and they will"
+            + " be documented on the next build.");
+    throw new IllegalStateException(message.toString());
+  }
+
+  /** target/classes/META-INF/jandex.idx under some module of this tree. */
+  static boolean isModuleIndex(Path path) {
+    return path.endsWith(Paths.get("target", "classes", "META-INF", 
"jandex.idx"));
+  }
+
+  /** Whether the class puts any widget on the configuration perspective. */
+  static boolean hasPerspectiveWidgets(ClassInfo ci) {
+    for (FieldInfo f : ci.fields()) {
+      AnnotationInstance w = f.annotation(GUI_WIDGET);
+      if (w != null && PARENT_ID.equals(str(w, "parentId", ""))) return true;
+    }
+    return false;
+  }
+
+  /**
+   * The .adoc files in the output directory that carry {@link 
#GENERATED_MARKER}. A file without it
+   * was written by hand - a README, a page someone dropped in - and this 
generator neither counts
+   * it nor deletes it.
+   */
+  static List<Path> generatedPages(Path outDir) throws IOException {
+    if (!Files.isDirectory(outDir)) return List.of();
+    List<Path> pages = new ArrayList<>();
+    try (Stream<Path> files = Files.list(outDir)) {
+      for (Path file : files.toList()) {
+        if (!file.getFileName().toString().endsWith(".adoc")) continue;
+        try {
+          if (Files.readString(file, 
StandardCharsets.UTF_8).contains(GENERATED_MARKER)) {
+            pages.add(file);
+          }
+        } catch (IOException e) {
+          // Unreadable, so not something to count on or delete.
+        }
+      }
+    }
+    return pages;
+  }
+
+  static Plugin readPlugin(ClassInfo ci, AnnotationInstance ai, Source source) 
{
+    List<FieldInfo> fields = new ArrayList<>();
+    for (FieldInfo f : ci.fields()) {
+      AnnotationInstance w = f.annotation(GUI_WIDGET);
+      // No default: parentId() itself defaults to "", so falling back to the 
config parent
+      // here would adopt every widget that simply did not name one.
+      if (w != null && PARENT_ID.equals(str(w, "parentId", ""))) fields.add(f);
+    }
+    if (fields.isEmpty()) return null;
+
+    Properties msgs = messages(source, ci.name().packagePrefix());
+    Map<String, Object> defaults = configDefaults(ai);
+
+    List<Option> options = new ArrayList<>();
+    for (FieldInfo f : fields) {
+      AnnotationInstance w = f.annotation(GUI_WIDGET);
+      // The configuration object is the truth where it has this setting; the 
annotation is the
+      // fallback for plugins that keep no such object.
+      Object fromConfig = defaults.get(f.name());
+      String dflt = fromConfig != null ? String.valueOf(fromConfig) : str(w, 
"defaultValue", "");
+      options.add(
+          new Option(
+              str(w, "order", "") + str(w, "id", ""),
+              translate(str(w, "label", ""), msgs),
+              translate(str(w, "toolTip", ""), msgs),
+              w.value("type") == null ? "" : w.value("type").asEnum(),
+              w.value("variables") == null || w.value("variables").asBoolean(),
+              dflt));
+    }
+    options.sort(Comparator.comparing(Option::order));
+    // The configuration perspective labels the tree with the @GuiPlugin 
description, so that is
+    // the name a reader is looking for on the page.
+    AnnotationInstance gui = ci.declaredAnnotation(GUI_PLUGIN);
+    String name = gui == null ? "" : translate(str(gui, "description", ""), 
msgs);
+    if (name.isEmpty()) {
+      name = translate(str(ai, "description", ""), msgs);
+    }
+    return new Plugin(
+        str(ai, "id", ""),
+        translate(str(ai, "description", ""), msgs),
+        name,
+        str(ai, "configKey", ""),
+        options);
+  }
+
+  /** Instantiates the plugin's configClass and reads its fields: the real, 
current defaults. */
+  static Map<String, Object> configDefaults(AnnotationInstance ai) {
+    AnnotationValue cc = ai.value("configClass");
+    if (cc == null) return Map.of();
+    String name = cc.asClass().name().toString();
+    if (name.equals("java.lang.Void")) return Map.of();
+    try {
+      Class<?> c = Class.forName(name);
+      Constructor<?> ctor = c.getDeclaredConstructor();
+      ctor.setAccessible(true);
+      Object o = ctor.newInstance();
+      Map<String, Object> values = new HashMap<>();
+      for (Field f : c.getDeclaredFields()) {
+        if (Modifier.isStatic(f.getModifiers())) continue;
+        f.setAccessible(true);
+        Object v = f.get(o);
+        if (v != null
+            && !(v instanceof Collection<?> c2 && c2.isEmpty())
+            && !(v instanceof Map<?, ?> m && m.isEmpty())) {
+          values.put(f.getName(), v);
+        }
+      }
+      return values;
+    } catch (Throwable t) {
+      System.err.println("  ! could not read defaults from " + name + " : " + 
t);

Review Comment:
   **[suggestion]** A `configClass` that fails to construct is logged and then 
treated as having no defaults, and the build still succeeds. The drift check 
only fails when the generated pages change, so a failure that happens in every 
build (a constructor that starts touching Hop, or a type pulled in behind the 
`*` exclusions) produces blank defaults that stay green once they are committed.
   
   **Suggestion:** Fail the build when `configClass` is not `Void` and 
instantiation throws, instead of falling through to an empty map.



##########
config-doc/src/main/java/org/apache/hop/config/doc/ConfigDocGenerator.java:
##########
@@ -0,0 +1,526 @@
+/*
+ * 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.config.doc;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.stream.Stream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import org.jboss.jandex.AnnotationInstance;
+import org.jboss.jandex.AnnotationTarget;
+import org.jboss.jandex.AnnotationValue;
+import org.jboss.jandex.ClassInfo;
+import org.jboss.jandex.DotName;
+import org.jboss.jandex.FieldInfo;
+import org.jboss.jandex.Index;
+import org.jboss.jandex.IndexReader;
+
+/**
+ * Generates one AsciiDoc partial per configuration plugin from the 
annotations the configuration
+ * perspective already builds its widgets from. Build time only: it reads the 
Jandex indexes the
+ * build writes into every module and the message bundles next to the classes, 
and instantiates only
+ * the plain configuration objects, never Hop itself.
+ */
+public class ConfigDocGenerator {
+
+  static final DotName CONFIG_PLUGIN =
+      DotName.createSimple("org.apache.hop.core.config.plugin.ConfigPlugin");
+  static final DotName GUI_WIDGET =
+      DotName.createSimple("org.apache.hop.core.gui.plugin.GuiWidgetElement");
+  static final DotName GUI_PLUGIN =
+      DotName.createSimple("org.apache.hop.core.gui.plugin.GuiPlugin");
+  static final String PARENT_ID = "EnterOptionsDialog-GuiWidgetsParent";
+  static final String JANDEX_INDEX = "META-INF/jandex.idx";
+  static final String AGGREGATE = "all-plugins.adoc";
+
+  /** First line of every page this generator owns, and how it tells them from 
hand-written ones. */
+  static final String GENERATED_MARKER =
+      "// Generated from the @ConfigPlugin and @GuiWidgetElement annotations.";
+
+  static final String LICENSE =
+      """
+      ////
+      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.
+      ////
+      """;
+
+  record Option(
+      String order, String label, String toolTip, String type, boolean 
variables, String dflt) {}
+
+  record Plugin(
+      String id, String description, String name, String configKey, 
List<Option> options) {}
+
+  public static void main(String[] args) throws Exception {
+    Path root = Paths.get(args[0]).toAbsolutePath().normalize();
+    Path outDir = Paths.get(args[1]).toAbsolutePath().normalize();
+
+    // Scan this program's own classpath rather than the source tree. Every 
module declaring a
+    // @ConfigPlugin is a declared dependency, so each one is here whether it 
was just built in the
+    // reactor or resolved as a jar from the local repository. Walking target/ 
directories instead
+    // would find nothing for the modules an incremental build left alone.
+    List<Source> sources = new ArrayList<>();
+    for (String entry : 
System.getProperty("java.class.path").split(File.pathSeparator)) {
+      Source source = Source.of(Paths.get(entry));
+      if (source != null) sources.add(source);
+    }
+
+    List<Plugin> plugins = new ArrayList<>();
+    for (Source source : sources) {
+      Index index;
+      try (InputStream in = source.open(JANDEX_INDEX)) {
+        if (in == null) continue;
+        index = new IndexReader(in).read();
+      } catch (Exception e) {
+        continue;
+      }
+      for (AnnotationInstance ai : index.getAnnotations(CONFIG_PLUGIN)) {
+        if (ai.target().kind() != AnnotationTarget.Kind.CLASS) continue;
+        Plugin p = readPlugin(ai.target().asClass(), ai, source);
+        if (p != null) plugins.add(p);
+      }
+    }
+    plugins.sort(Comparator.comparing(Plugin::name, 
String.CASE_INSENSITIVE_ORDER));
+
+    // Only the pages this generator wrote are its business. Anything else in 
the directory was
+    // put there by hand and is left alone, neither counted nor deleted.
+    List<Path> generatedBefore = generatedPages(outDir);
+
+    // Order matters here. An empty classpath means every plugin looks like 
one this generator
+    // cannot see, and the report below would then name all of them and ask 
for fourteen new
+    // dependencies - true in its way, and useless. "There is nothing here at 
all" is the more
+    // fundamental thing to have gone wrong, so it is the one to say.
+    if (plugins.isEmpty() && !generatedBefore.isEmpty()) {
+      throw new IllegalStateException(
+          "No configuration plugins found on the classpath, but "
+              + outDir.getFileName()
+              + " documents "
+              + generatedBefore.size()
+              + ". Refusing to replace those pages with nothing. This means 
the classpath is"
+              + " missing the modules that declare a @ConfigPlugin - build the 
project first.");
+    }
+
+    reportPluginsOutsideTheClasspath(root, plugins);
+
+    Files.createDirectories(outDir);
+    Set<Path> written = new HashSet<>();
+    for (Plugin p : plugins) {
+      Path file = outDir.resolve(p.id() + ".adoc");
+      Files.writeString(file, render(p), StandardCharsets.UTF_8);
+      written.add(file);
+      System.out.printf(
+          "%-34s %2d options -> %s%n", p.name(), p.options().size(), 
root.relativize(file));
+    }
+
+    // One page wants the lot: a section per plugin, in the order the 
perspective's tree shows them.
+    StringBuilder all = new StringBuilder(LICENSE);
+    all.append(GENERATED_MARKER).append("\n// Edit those, not this file.\n");
+    for (Plugin p : plugins) {
+      all.append("\n== ").append(p.name()).append("\n\n").append(body(p));
+    }
+    Path index = outDir.resolve(AGGREGATE);
+    Files.writeString(index, all.toString(), StandardCharsets.UTF_8);
+    written.add(index);
+    System.out.println("aggregate -> " + root.relativize(index));
+
+    // A plugin that was renamed or removed leaves its page behind, and the 
pages that include one
+    // by name would go on rendering it for good. Nothing reads a generated 
page that this run did
+    // not write, so it goes.
+    for (Path stale : generatedBefore) {
+      if (!written.contains(stale)) {
+        Files.delete(stale);
+        System.out.println("removed (no longer generated) -> " + 
root.relativize(stale));
+      }
+    }
+    System.out.println(
+        "\n"
+            + plugins.size()
+            + " configuration plugins, "
+            + plugins.stream().mapToInt(p -> p.options().size()).sum()
+            + " options");
+  }
+
+  /**
+   * Which modules a documented plugin can come from is decided by this 
module's dependencies, and
+   * that list is written by hand. A plugin added to a module that is not on 
it would be absent from
+   * the manual with nothing to say so: the page set would not change, so the 
drift check in CI
+   * would pass, and the option would simply never be written about.
+   *
+   * <p>So: sweep what the build has actually compiled and see whether 
anything turns up that the
+   * classpath did not. This is deliberately only a report - it never adds to 
the pages, because the
+   * set of modules on disk depends on what this particular build chose to 
compile, and the pages
+   * have to come out the same everywhere. An incremental build that has not 
compiled the new module
+   * simply finds nothing, which is why this is best-effort. A full build - 
every push to main is
+   * one - does find it.
+   */
+  static void reportPluginsOutsideTheClasspath(Path root, List<Plugin> 
documented)
+      throws IOException {
+    Set<String> known = new HashSet<>();
+    for (Plugin p : documented) {
+      known.add(p.id());
+    }
+
+    Map<String, String> missing = new TreeMap<>();
+    try (Stream<Path> walk = Files.walk(root)) {
+      for (Path index : 
walk.filter(ConfigDocGenerator::isModuleIndex).toList()) {
+        Path classes = index.getParent().getParent();
+        Source source = Source.of(classes);
+        if (source == null) continue;
+        Index jandex;
+        try (InputStream in = source.open(JANDEX_INDEX)) {
+          if (in == null) continue;
+          jandex = new IndexReader(in).read();
+        } catch (Exception e) {
+          continue;
+        }
+        for (AnnotationInstance ai : jandex.getAnnotations(CONFIG_PLUGIN)) {
+          if (ai.target().kind() != AnnotationTarget.Kind.CLASS) continue;
+          String id = str(ai, "id", "");
+          if (known.contains(id) || 
!hasPerspectiveWidgets(ai.target().asClass())) continue;
+          // target/classes/META-INF/jandex.idx -> the module directory
+          missing.put(id, 
root.relativize(classes.getParent().getParent()).toString());
+        }
+      }
+    }
+    if (missing.isEmpty()) return;
+
+    StringBuilder message =
+        new StringBuilder(
+            "These configuration plugins add options to the configuration 
perspective but are in"
+                + " modules this generator cannot see, so the manual would not 
describe them:\n");
+    missing.forEach(
+        (id, module) ->
+            message.append("  ").append(id).append("  in  
").append(module).append("\n"));
+    message.append(
+        "Add the module as a dependency of config-doc/pom.xml, next to the 
others, and they will"
+            + " be documented on the next build.");
+    throw new IllegalStateException(message.toString());
+  }
+
+  /** target/classes/META-INF/jandex.idx under some module of this tree. */
+  static boolean isModuleIndex(Path path) {
+    return path.endsWith(Paths.get("target", "classes", "META-INF", 
"jandex.idx"));
+  }
+
+  /** Whether the class puts any widget on the configuration perspective. */
+  static boolean hasPerspectiveWidgets(ClassInfo ci) {
+    for (FieldInfo f : ci.fields()) {
+      AnnotationInstance w = f.annotation(GUI_WIDGET);
+      if (w != null && PARENT_ID.equals(str(w, "parentId", ""))) return true;
+    }
+    return false;
+  }
+
+  /**
+   * The .adoc files in the output directory that carry {@link 
#GENERATED_MARKER}. A file without it
+   * was written by hand - a README, a page someone dropped in - and this 
generator neither counts
+   * it nor deletes it.
+   */
+  static List<Path> generatedPages(Path outDir) throws IOException {
+    if (!Files.isDirectory(outDir)) return List.of();
+    List<Path> pages = new ArrayList<>();
+    try (Stream<Path> files = Files.list(outDir)) {
+      for (Path file : files.toList()) {
+        if (!file.getFileName().toString().endsWith(".adoc")) continue;
+        try {
+          if (Files.readString(file, 
StandardCharsets.UTF_8).contains(GENERATED_MARKER)) {
+            pages.add(file);
+          }
+        } catch (IOException e) {
+          // Unreadable, so not something to count on or delete.
+        }
+      }
+    }
+    return pages;
+  }
+
+  static Plugin readPlugin(ClassInfo ci, AnnotationInstance ai, Source source) 
{
+    List<FieldInfo> fields = new ArrayList<>();
+    for (FieldInfo f : ci.fields()) {
+      AnnotationInstance w = f.annotation(GUI_WIDGET);
+      // No default: parentId() itself defaults to "", so falling back to the 
config parent
+      // here would adopt every widget that simply did not name one.
+      if (w != null && PARENT_ID.equals(str(w, "parentId", ""))) fields.add(f);
+    }
+    if (fields.isEmpty()) return null;
+
+    Properties msgs = messages(source, ci.name().packagePrefix());
+    Map<String, Object> defaults = configDefaults(ai);
+
+    List<Option> options = new ArrayList<>();
+    for (FieldInfo f : fields) {
+      AnnotationInstance w = f.annotation(GUI_WIDGET);
+      // The configuration object is the truth where it has this setting; the 
annotation is the
+      // fallback for plugins that keep no such object.
+      Object fromConfig = defaults.get(f.name());
+      String dflt = fromConfig != null ? String.valueOf(fromConfig) : str(w, 
"defaultValue", "");

Review Comment:
   **[suggestion]** `String.valueOf` on the config object turns 
`HelpOpenMode.BROWSER` into `BROWSER`. The explorer combo is filled from 
`getLabel()` (`External browser`, `Hop GUI tab`, `Dialog window`), which is 
also what `ExplorerPerspectiveConfigPlugin.getInstance()` copies into the 
widget. The generated Default cell therefore shows a value the dialog never 
displays.
   
   **Suggestion:** If the value is an enum, prefer `getLabel()` when that 
method exists, otherwise `name()`.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to