[
https://issues.apache.org/jira/browse/GROOVY-12018?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18081878#comment-18081878
]
ASF GitHub Bot commented on GROOVY-12018:
-----------------------------------------
Copilot commented on code in PR #2543:
URL: https://github.com/apache/groovy/pull/2543#discussion_r3263070479
##########
subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyPrinter.groovy:
##########
@@ -0,0 +1,198 @@
+/*
+ * 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.groovy.groovysh.jline
+
+import groovy.transform.CompileStatic
+import org.jline.builtins.ConfigurationPath
+import org.jline.console.Printer
+import org.jline.console.ScriptEngine
+import org.jline.console.impl.DefaultPrinter
+
+import java.nio.file.DirectoryStream
+import java.nio.file.Files
+import java.nio.file.Path
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+
+/**
+ * A {@link DefaultPrinter} that resolves nanorc highlight-style names
+ * case-insensitively.
+ *
+ * JLine matches the {@code /prnt -s STYLE} value (and the
+ * {@code valueStyle}/{@code valueStyleAll} options) against the
+ * {@code syntax "<NAME>"} header of each nanorc grammar with a
+ * case-sensitive {@code String.equals} (in
+ * {@code org.jline.builtins.SyntaxHighlighter}), so {@code -s json}
+ * would not match {@code syntax "JSON"}.
+ *
+ * Rather than blindly transforming the requested style (which would
+ * break selecting a user's existing mixed-case grammars copied into
+ * {@code ~/.groovy} per the groovysh docs), this resolves the requested
+ * name <em>case-insensitively against the actually configured syntax
+ * names</em> — the same {@code jnanorc} {@link DefaultPrinter} itself
+ * uses (user-config first, via {@link ConfigurationPath}) plus the
+ * grammars it {@code include}s. The requested value is rewritten to the
+ * real name only on a unique case-insensitive hit; an exact match or an
+ * unknown name is passed through untouched so JLine's own behaviour is
+ * preserved. Discovery is failsafe per file and overall: a single
+ * malformed nanorc never prevents resolving the others, and any error
+ * leaves the options exactly as JLine would have seen them.
+ */
+@CompileStatic
+class GroovyPrinter extends DefaultPrinter {
+
+ private static final List<String> STYLE_KEYS = [Printer.STYLE,
Printer.VALUE_STYLE, Printer.VALUE_STYLE_ALL]
+ private static final Pattern SYNTAX_HEADER =
Pattern.compile(/(?m)^\s*syntax\s+"([^"]+)"/)
+ private static final Pattern INCLUDE_LINE =
Pattern.compile(/(?m)^\s*include\s+(\S+)/)
+
+ private final ConfigurationPath configPath
+ private Map<String, String> nameByLower // cached lowercase -> actual;
built lazily
+
+ GroovyPrinter(ScriptEngine engine, ConfigurationPath configPath) {
+ super(engine, configPath)
+ this.configPath = configPath
+ }
+
+ @Override
+ void println(Map<String, Object> options, Object object) {
+ super.println(normalizeStyles(options), object)
+ }
+
+ @Override
+ protected void highlightAndPrint(Map<String, Object> options, Throwable
exception) {
+ super.highlightAndPrint(normalizeStyles(options), exception)
+ }
+
+ /**
+ * Returns a copy of {@code options} with style-name values resolved to the
+ * actual configured syntax name (case-insensitively), or the original map
+ * untouched when there is nothing to change (so an immutable or shared
+ * caller map is not mutated). Failsafe: any problem returns {@code
options}
+ * unchanged.
+ */
+ private Map<String, Object> normalizeStyles(Map<String, Object> options) {
+ if (options == null || options.isEmpty()) {
+ return options
+ }
+ Map<String, String> names
+ try {
+ names = syntaxNames()
+ } catch (Exception ignored) {
+ return options
+ }
+ if (names.isEmpty()) {
+ return options
+ }
+ Map<String, Object> copy = null
+ for (String key : STYLE_KEYS) {
+ Object value = options.get(key)
+ if (value instanceof CharSequence && (value as
CharSequence).length() > 0) {
+ String requested = value.toString()
+ String resolved = resolveStyle(requested, names)
+ if (resolved != requested) {
+ if (copy == null) {
+ copy = new LinkedHashMap<String, Object>(options)
+ }
+ copy.put(key, resolved)
+ }
+ }
+ }
+ copy != null ? copy : options
+ }
+
+ /**
+ * Pure resolution (package-private for testing): an exact match or an
+ * unknown name is returned unchanged; otherwise the actual syntax name for
+ * a case-insensitive match is returned.
+ *
+ * @param requested the user-supplied style name
+ * @param nameByLower map of lower-cased syntax name to its actual casing
+ * @return the name JLine should be given
+ */
+ static String resolveStyle(String requested, Map<String, String>
nameByLower) {
+ if (requested == null || requested.isEmpty()) {
+ return requested
+ }
+ if (nameByLower.containsValue(requested)) {
+ return requested // exact match: leave as-is (also covers
user-config names)
+ }
+ String actual = nameByLower.get(requested.toLowerCase(Locale.ROOT))
+ actual != null ? actual : requested
+ }
+
+ /** lowercase-name -> actual syntax name across the resolved jnanorc
and its includes; cached. */
+ private synchronized Map<String, String> syntaxNames() {
+ if (nameByLower != null) {
+ return nameByLower
Review Comment:
The syntax-name cache is never invalidated. `Main` registers
`printer::refresh` with the system highlighter refresh path, but
`GroovyPrinter` inherits `DefaultPrinter.refresh()` without clearing
`nameByLower`, so after a user changes/copies nanorc files and refreshes,
case-insensitive style resolution still uses the old names until groovysh is
restarted.
##########
subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyPrinter.groovy:
##########
@@ -0,0 +1,198 @@
+/*
+ * 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.groovy.groovysh.jline
+
+import groovy.transform.CompileStatic
+import org.jline.builtins.ConfigurationPath
+import org.jline.console.Printer
+import org.jline.console.ScriptEngine
+import org.jline.console.impl.DefaultPrinter
+
+import java.nio.file.DirectoryStream
+import java.nio.file.Files
+import java.nio.file.Path
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+
+/**
+ * A {@link DefaultPrinter} that resolves nanorc highlight-style names
+ * case-insensitively.
+ *
+ * JLine matches the {@code /prnt -s STYLE} value (and the
+ * {@code valueStyle}/{@code valueStyleAll} options) against the
+ * {@code syntax "<NAME>"} header of each nanorc grammar with a
+ * case-sensitive {@code String.equals} (in
+ * {@code org.jline.builtins.SyntaxHighlighter}), so {@code -s json}
+ * would not match {@code syntax "JSON"}.
+ *
+ * Rather than blindly transforming the requested style (which would
+ * break selecting a user's existing mixed-case grammars copied into
+ * {@code ~/.groovy} per the groovysh docs), this resolves the requested
+ * name <em>case-insensitively against the actually configured syntax
+ * names</em> — the same {@code jnanorc} {@link DefaultPrinter} itself
+ * uses (user-config first, via {@link ConfigurationPath}) plus the
+ * grammars it {@code include}s. The requested value is rewritten to the
+ * real name only on a unique case-insensitive hit; an exact match or an
+ * unknown name is passed through untouched so JLine's own behaviour is
+ * preserved. Discovery is failsafe per file and overall: a single
+ * malformed nanorc never prevents resolving the others, and any error
+ * leaves the options exactly as JLine would have seen them.
+ */
+@CompileStatic
+class GroovyPrinter extends DefaultPrinter {
+
+ private static final List<String> STYLE_KEYS = [Printer.STYLE,
Printer.VALUE_STYLE, Printer.VALUE_STYLE_ALL]
+ private static final Pattern SYNTAX_HEADER =
Pattern.compile(/(?m)^\s*syntax\s+"([^"]+)"/)
+ private static final Pattern INCLUDE_LINE =
Pattern.compile(/(?m)^\s*include\s+(\S+)/)
+
+ private final ConfigurationPath configPath
+ private Map<String, String> nameByLower // cached lowercase -> actual;
built lazily
+
+ GroovyPrinter(ScriptEngine engine, ConfigurationPath configPath) {
+ super(engine, configPath)
+ this.configPath = configPath
+ }
+
+ @Override
+ void println(Map<String, Object> options, Object object) {
+ super.println(normalizeStyles(options), object)
+ }
+
+ @Override
+ protected void highlightAndPrint(Map<String, Object> options, Throwable
exception) {
+ super.highlightAndPrint(normalizeStyles(options), exception)
+ }
+
+ /**
+ * Returns a copy of {@code options} with style-name values resolved to the
+ * actual configured syntax name (case-insensitively), or the original map
+ * untouched when there is nothing to change (so an immutable or shared
+ * caller map is not mutated). Failsafe: any problem returns {@code
options}
+ * unchanged.
+ */
+ private Map<String, Object> normalizeStyles(Map<String, Object> options) {
+ if (options == null || options.isEmpty()) {
+ return options
+ }
+ Map<String, String> names
+ try {
+ names = syntaxNames()
+ } catch (Exception ignored) {
+ return options
+ }
+ if (names.isEmpty()) {
+ return options
+ }
+ Map<String, Object> copy = null
+ for (String key : STYLE_KEYS) {
+ Object value = options.get(key)
+ if (value instanceof CharSequence && (value as
CharSequence).length() > 0) {
+ String requested = value.toString()
+ String resolved = resolveStyle(requested, names)
+ if (resolved != requested) {
+ if (copy == null) {
+ copy = new LinkedHashMap<String, Object>(options)
+ }
+ copy.put(key, resolved)
+ }
+ }
+ }
+ copy != null ? copy : options
+ }
+
+ /**
+ * Pure resolution (package-private for testing): an exact match or an
+ * unknown name is returned unchanged; otherwise the actual syntax name for
+ * a case-insensitive match is returned.
+ *
+ * @param requested the user-supplied style name
+ * @param nameByLower map of lower-cased syntax name to its actual casing
+ * @return the name JLine should be given
+ */
+ static String resolveStyle(String requested, Map<String, String>
nameByLower) {
+ if (requested == null || requested.isEmpty()) {
+ return requested
+ }
+ if (nameByLower.containsValue(requested)) {
+ return requested // exact match: leave as-is (also covers
user-config names)
+ }
+ String actual = nameByLower.get(requested.toLowerCase(Locale.ROOT))
+ actual != null ? actual : requested
+ }
+
+ /** lowercase-name -> actual syntax name across the resolved jnanorc
and its includes; cached. */
+ private synchronized Map<String, String> syntaxNames() {
+ if (nameByLower != null) {
+ return nameByLower
+ }
+ Map<String, String> map = new LinkedHashMap<>()
+ Path jnanorc = configPath?.getConfig('jnanorc')
+ if (jnanorc != null && Files.isReadable(jnanorc)) {
+ collectSyntaxNames(jnanorc, map)
+ }
+ nameByLower = map
+ return map
+ }
+
+ /**
+ * Scans the jnanorc itself and every file it {@code include}s for
+ * {@code syntax "NAME"} headers. Per-file and per-include failsafe.
+ */
+ static void collectSyntaxNames(Path jnanorc, Map<String, String> into) {
+ scanForSyntax(jnanorc, into) // jnanorc may declare syntaxes directly
+ String text
+ try {
+ text = new String(Files.readAllBytes(jnanorc))
Review Comment:
This decodes nanorc files with the JVM default charset even though
repository text files are declared UTF-8 in `.editorconfig:18`. On Java 17
runtimes with a non-UTF-8 default charset, non-ASCII include paths or syntax
names can be misread; use an explicit UTF-8 charset here.
##########
subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyPrinter.groovy:
##########
@@ -0,0 +1,198 @@
+/*
+ * 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.groovy.groovysh.jline
+
+import groovy.transform.CompileStatic
+import org.jline.builtins.ConfigurationPath
+import org.jline.console.Printer
+import org.jline.console.ScriptEngine
+import org.jline.console.impl.DefaultPrinter
+
+import java.nio.file.DirectoryStream
+import java.nio.file.Files
+import java.nio.file.Path
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+
+/**
+ * A {@link DefaultPrinter} that resolves nanorc highlight-style names
+ * case-insensitively.
+ *
+ * JLine matches the {@code /prnt -s STYLE} value (and the
+ * {@code valueStyle}/{@code valueStyleAll} options) against the
+ * {@code syntax "<NAME>"} header of each nanorc grammar with a
+ * case-sensitive {@code String.equals} (in
+ * {@code org.jline.builtins.SyntaxHighlighter}), so {@code -s json}
+ * would not match {@code syntax "JSON"}.
+ *
+ * Rather than blindly transforming the requested style (which would
+ * break selecting a user's existing mixed-case grammars copied into
+ * {@code ~/.groovy} per the groovysh docs), this resolves the requested
+ * name <em>case-insensitively against the actually configured syntax
+ * names</em> — the same {@code jnanorc} {@link DefaultPrinter} itself
+ * uses (user-config first, via {@link ConfigurationPath}) plus the
+ * grammars it {@code include}s. The requested value is rewritten to the
+ * real name only on a unique case-insensitive hit; an exact match or an
+ * unknown name is passed through untouched so JLine's own behaviour is
+ * preserved. Discovery is failsafe per file and overall: a single
+ * malformed nanorc never prevents resolving the others, and any error
+ * leaves the options exactly as JLine would have seen them.
+ */
+@CompileStatic
+class GroovyPrinter extends DefaultPrinter {
+
+ private static final List<String> STYLE_KEYS = [Printer.STYLE,
Printer.VALUE_STYLE, Printer.VALUE_STYLE_ALL]
+ private static final Pattern SYNTAX_HEADER =
Pattern.compile(/(?m)^\s*syntax\s+"([^"]+)"/)
+ private static final Pattern INCLUDE_LINE =
Pattern.compile(/(?m)^\s*include\s+(\S+)/)
+
+ private final ConfigurationPath configPath
+ private Map<String, String> nameByLower // cached lowercase -> actual;
built lazily
+
+ GroovyPrinter(ScriptEngine engine, ConfigurationPath configPath) {
+ super(engine, configPath)
+ this.configPath = configPath
+ }
+
+ @Override
+ void println(Map<String, Object> options, Object object) {
+ super.println(normalizeStyles(options), object)
+ }
+
+ @Override
+ protected void highlightAndPrint(Map<String, Object> options, Throwable
exception) {
+ super.highlightAndPrint(normalizeStyles(options), exception)
+ }
+
+ /**
+ * Returns a copy of {@code options} with style-name values resolved to the
+ * actual configured syntax name (case-insensitively), or the original map
+ * untouched when there is nothing to change (so an immutable or shared
+ * caller map is not mutated). Failsafe: any problem returns {@code
options}
+ * unchanged.
+ */
+ private Map<String, Object> normalizeStyles(Map<String, Object> options) {
+ if (options == null || options.isEmpty()) {
+ return options
+ }
+ Map<String, String> names
+ try {
+ names = syntaxNames()
+ } catch (Exception ignored) {
+ return options
+ }
+ if (names.isEmpty()) {
+ return options
+ }
+ Map<String, Object> copy = null
+ for (String key : STYLE_KEYS) {
+ Object value = options.get(key)
+ if (value instanceof CharSequence && (value as
CharSequence).length() > 0) {
+ String requested = value.toString()
+ String resolved = resolveStyle(requested, names)
+ if (resolved != requested) {
+ if (copy == null) {
+ copy = new LinkedHashMap<String, Object>(options)
+ }
+ copy.put(key, resolved)
+ }
+ }
+ }
+ copy != null ? copy : options
+ }
+
+ /**
+ * Pure resolution (package-private for testing): an exact match or an
+ * unknown name is returned unchanged; otherwise the actual syntax name for
+ * a case-insensitive match is returned.
+ *
+ * @param requested the user-supplied style name
+ * @param nameByLower map of lower-cased syntax name to its actual casing
+ * @return the name JLine should be given
+ */
+ static String resolveStyle(String requested, Map<String, String>
nameByLower) {
+ if (requested == null || requested.isEmpty()) {
+ return requested
+ }
+ if (nameByLower.containsValue(requested)) {
+ return requested // exact match: leave as-is (also covers
user-config names)
+ }
+ String actual = nameByLower.get(requested.toLowerCase(Locale.ROOT))
+ actual != null ? actual : requested
+ }
+
+ /** lowercase-name -> actual syntax name across the resolved jnanorc
and its includes; cached. */
+ private synchronized Map<String, String> syntaxNames() {
+ if (nameByLower != null) {
+ return nameByLower
+ }
+ Map<String, String> map = new LinkedHashMap<>()
+ Path jnanorc = configPath?.getConfig('jnanorc')
+ if (jnanorc != null && Files.isReadable(jnanorc)) {
+ collectSyntaxNames(jnanorc, map)
+ }
+ nameByLower = map
+ return map
+ }
+
+ /**
+ * Scans the jnanorc itself and every file it {@code include}s for
+ * {@code syntax "NAME"} headers. Per-file and per-include failsafe.
+ */
+ static void collectSyntaxNames(Path jnanorc, Map<String, String> into) {
Review Comment:
This test/support helper is also public by default in Groovy. To avoid
adding unnecessary public API surface, follow AGENTS.md:98's narrowest-scope
guidance and make it package-scoped if it only needs to be called from
same-package tests.
##########
subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyPrinter.groovy:
##########
@@ -0,0 +1,198 @@
+/*
+ * 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.groovy.groovysh.jline
+
+import groovy.transform.CompileStatic
+import org.jline.builtins.ConfigurationPath
+import org.jline.console.Printer
+import org.jline.console.ScriptEngine
+import org.jline.console.impl.DefaultPrinter
+
+import java.nio.file.DirectoryStream
+import java.nio.file.Files
+import java.nio.file.Path
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+
+/**
+ * A {@link DefaultPrinter} that resolves nanorc highlight-style names
+ * case-insensitively.
+ *
+ * JLine matches the {@code /prnt -s STYLE} value (and the
+ * {@code valueStyle}/{@code valueStyleAll} options) against the
+ * {@code syntax "<NAME>"} header of each nanorc grammar with a
+ * case-sensitive {@code String.equals} (in
+ * {@code org.jline.builtins.SyntaxHighlighter}), so {@code -s json}
+ * would not match {@code syntax "JSON"}.
+ *
+ * Rather than blindly transforming the requested style (which would
+ * break selecting a user's existing mixed-case grammars copied into
+ * {@code ~/.groovy} per the groovysh docs), this resolves the requested
+ * name <em>case-insensitively against the actually configured syntax
+ * names</em> — the same {@code jnanorc} {@link DefaultPrinter} itself
+ * uses (user-config first, via {@link ConfigurationPath}) plus the
+ * grammars it {@code include}s. The requested value is rewritten to the
+ * real name only on a unique case-insensitive hit; an exact match or an
+ * unknown name is passed through untouched so JLine's own behaviour is
+ * preserved. Discovery is failsafe per file and overall: a single
+ * malformed nanorc never prevents resolving the others, and any error
+ * leaves the options exactly as JLine would have seen them.
+ */
+@CompileStatic
+class GroovyPrinter extends DefaultPrinter {
+
+ private static final List<String> STYLE_KEYS = [Printer.STYLE,
Printer.VALUE_STYLE, Printer.VALUE_STYLE_ALL]
+ private static final Pattern SYNTAX_HEADER =
Pattern.compile(/(?m)^\s*syntax\s+"([^"]+)"/)
+ private static final Pattern INCLUDE_LINE =
Pattern.compile(/(?m)^\s*include\s+(\S+)/)
+
+ private final ConfigurationPath configPath
+ private Map<String, String> nameByLower // cached lowercase -> actual;
built lazily
+
+ GroovyPrinter(ScriptEngine engine, ConfigurationPath configPath) {
+ super(engine, configPath)
+ this.configPath = configPath
+ }
+
+ @Override
+ void println(Map<String, Object> options, Object object) {
+ super.println(normalizeStyles(options), object)
+ }
+
+ @Override
+ protected void highlightAndPrint(Map<String, Object> options, Throwable
exception) {
+ super.highlightAndPrint(normalizeStyles(options), exception)
+ }
+
+ /**
+ * Returns a copy of {@code options} with style-name values resolved to the
+ * actual configured syntax name (case-insensitively), or the original map
+ * untouched when there is nothing to change (so an immutable or shared
+ * caller map is not mutated). Failsafe: any problem returns {@code
options}
+ * unchanged.
+ */
+ private Map<String, Object> normalizeStyles(Map<String, Object> options) {
+ if (options == null || options.isEmpty()) {
+ return options
+ }
+ Map<String, String> names
+ try {
+ names = syntaxNames()
+ } catch (Exception ignored) {
+ return options
+ }
+ if (names.isEmpty()) {
+ return options
+ }
+ Map<String, Object> copy = null
+ for (String key : STYLE_KEYS) {
+ Object value = options.get(key)
+ if (value instanceof CharSequence && (value as
CharSequence).length() > 0) {
+ String requested = value.toString()
+ String resolved = resolveStyle(requested, names)
+ if (resolved != requested) {
+ if (copy == null) {
+ copy = new LinkedHashMap<String, Object>(options)
+ }
+ copy.put(key, resolved)
+ }
+ }
+ }
+ copy != null ? copy : options
+ }
+
+ /**
+ * Pure resolution (package-private for testing): an exact match or an
+ * unknown name is returned unchanged; otherwise the actual syntax name for
+ * a case-insensitive match is returned.
+ *
+ * @param requested the user-supplied style name
+ * @param nameByLower map of lower-cased syntax name to its actual casing
+ * @return the name JLine should be given
+ */
+ static String resolveStyle(String requested, Map<String, String>
nameByLower) {
Review Comment:
In Groovy, a method without an explicit visibility modifier is public, so
this helper becomes new public API despite the comment saying it is
package-private. AGENTS.md:98 asks to prefer the narrowest scope because public
API is hard to remove; mark test-only helpers package-scoped instead of
exposing them.
##########
subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyPrinter.groovy:
##########
@@ -0,0 +1,198 @@
+/*
+ * 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.groovy.groovysh.jline
+
+import groovy.transform.CompileStatic
+import org.jline.builtins.ConfigurationPath
+import org.jline.console.Printer
+import org.jline.console.ScriptEngine
+import org.jline.console.impl.DefaultPrinter
+
+import java.nio.file.DirectoryStream
+import java.nio.file.Files
+import java.nio.file.Path
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+
+/**
+ * A {@link DefaultPrinter} that resolves nanorc highlight-style names
+ * case-insensitively.
+ *
+ * JLine matches the {@code /prnt -s STYLE} value (and the
+ * {@code valueStyle}/{@code valueStyleAll} options) against the
+ * {@code syntax "<NAME>"} header of each nanorc grammar with a
+ * case-sensitive {@code String.equals} (in
+ * {@code org.jline.builtins.SyntaxHighlighter}), so {@code -s json}
+ * would not match {@code syntax "JSON"}.
+ *
+ * Rather than blindly transforming the requested style (which would
+ * break selecting a user's existing mixed-case grammars copied into
+ * {@code ~/.groovy} per the groovysh docs), this resolves the requested
+ * name <em>case-insensitively against the actually configured syntax
+ * names</em> — the same {@code jnanorc} {@link DefaultPrinter} itself
+ * uses (user-config first, via {@link ConfigurationPath}) plus the
+ * grammars it {@code include}s. The requested value is rewritten to the
+ * real name only on a unique case-insensitive hit; an exact match or an
+ * unknown name is passed through untouched so JLine's own behaviour is
+ * preserved. Discovery is failsafe per file and overall: a single
+ * malformed nanorc never prevents resolving the others, and any error
+ * leaves the options exactly as JLine would have seen them.
+ */
+@CompileStatic
+class GroovyPrinter extends DefaultPrinter {
+
+ private static final List<String> STYLE_KEYS = [Printer.STYLE,
Printer.VALUE_STYLE, Printer.VALUE_STYLE_ALL]
+ private static final Pattern SYNTAX_HEADER =
Pattern.compile(/(?m)^\s*syntax\s+"([^"]+)"/)
+ private static final Pattern INCLUDE_LINE =
Pattern.compile(/(?m)^\s*include\s+(\S+)/)
+
+ private final ConfigurationPath configPath
+ private Map<String, String> nameByLower // cached lowercase -> actual;
built lazily
+
+ GroovyPrinter(ScriptEngine engine, ConfigurationPath configPath) {
+ super(engine, configPath)
+ this.configPath = configPath
+ }
+
+ @Override
+ void println(Map<String, Object> options, Object object) {
+ super.println(normalizeStyles(options), object)
+ }
+
+ @Override
+ protected void highlightAndPrint(Map<String, Object> options, Throwable
exception) {
+ super.highlightAndPrint(normalizeStyles(options), exception)
+ }
+
+ /**
+ * Returns a copy of {@code options} with style-name values resolved to the
+ * actual configured syntax name (case-insensitively), or the original map
+ * untouched when there is nothing to change (so an immutable or shared
+ * caller map is not mutated). Failsafe: any problem returns {@code
options}
+ * unchanged.
+ */
+ private Map<String, Object> normalizeStyles(Map<String, Object> options) {
+ if (options == null || options.isEmpty()) {
+ return options
+ }
+ Map<String, String> names
+ try {
+ names = syntaxNames()
+ } catch (Exception ignored) {
+ return options
+ }
+ if (names.isEmpty()) {
+ return options
+ }
+ Map<String, Object> copy = null
+ for (String key : STYLE_KEYS) {
+ Object value = options.get(key)
+ if (value instanceof CharSequence && (value as
CharSequence).length() > 0) {
+ String requested = value.toString()
+ String resolved = resolveStyle(requested, names)
+ if (resolved != requested) {
+ if (copy == null) {
+ copy = new LinkedHashMap<String, Object>(options)
+ }
+ copy.put(key, resolved)
+ }
+ }
+ }
+ copy != null ? copy : options
+ }
+
+ /**
+ * Pure resolution (package-private for testing): an exact match or an
+ * unknown name is returned unchanged; otherwise the actual syntax name for
+ * a case-insensitive match is returned.
+ *
+ * @param requested the user-supplied style name
+ * @param nameByLower map of lower-cased syntax name to its actual casing
+ * @return the name JLine should be given
+ */
+ static String resolveStyle(String requested, Map<String, String>
nameByLower) {
+ if (requested == null || requested.isEmpty()) {
+ return requested
+ }
+ if (nameByLower.containsValue(requested)) {
+ return requested // exact match: leave as-is (also covers
user-config names)
+ }
+ String actual = nameByLower.get(requested.toLowerCase(Locale.ROOT))
+ actual != null ? actual : requested
+ }
+
+ /** lowercase-name -> actual syntax name across the resolved jnanorc
and its includes; cached. */
+ private synchronized Map<String, String> syntaxNames() {
+ if (nameByLower != null) {
+ return nameByLower
+ }
+ Map<String, String> map = new LinkedHashMap<>()
+ Path jnanorc = configPath?.getConfig('jnanorc')
+ if (jnanorc != null && Files.isReadable(jnanorc)) {
+ collectSyntaxNames(jnanorc, map)
+ }
+ nameByLower = map
+ return map
+ }
+
+ /**
+ * Scans the jnanorc itself and every file it {@code include}s for
+ * {@code syntax "NAME"} headers. Per-file and per-include failsafe.
+ */
+ static void collectSyntaxNames(Path jnanorc, Map<String, String> into) {
+ scanForSyntax(jnanorc, into) // jnanorc may declare syntaxes directly
+ String text
+ try {
+ text = new String(Files.readAllBytes(jnanorc))
+ } catch (Exception ignored) {
+ return
+ }
+ Path dir = jnanorc.toAbsolutePath().parent
+ Matcher m = INCLUDE_LINE.matcher(text)
+ while (m.find()) {
+ String glob = m.group(1)
+ try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir,
glob)) {
+ for (Path p : stream) {
+ scanForSyntax(p, into) // per-file failsafe inside
+ }
+ } catch (Exception ignored) {
+ // one bad include directive must not stop the others
+ }
+ }
+ }
+
+ /**
+ * Per-file failsafe: a single malformed/unreadable nanorc never breaks
+ * discovery of the rest. First spelling of a name wins (stable order).
+ */
+ private static void scanForSyntax(Path file, Map<String, String> into) {
+ try {
+ if (file == null || !Files.isReadable(file)) {
+ return
+ }
+ String content = new String(Files.readAllBytes(file))
Review Comment:
This second read also uses the platform default charset, while repository
text files are UTF-8 per `.editorconfig:18`. A non-UTF-8 Java 17 runtime can
corrupt non-ASCII syntax names before lower-casing/matching them; decode with
an explicit UTF-8 charset.
##########
subprojects/groovy-groovysh/src/main/groovy/org/apache/groovy/groovysh/jline/GroovyPrinter.groovy:
##########
@@ -0,0 +1,198 @@
+/*
+ * 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.groovy.groovysh.jline
+
+import groovy.transform.CompileStatic
+import org.jline.builtins.ConfigurationPath
+import org.jline.console.Printer
+import org.jline.console.ScriptEngine
+import org.jline.console.impl.DefaultPrinter
+
+import java.nio.file.DirectoryStream
+import java.nio.file.Files
+import java.nio.file.Path
+import java.util.regex.Matcher
+import java.util.regex.Pattern
+
+/**
+ * A {@link DefaultPrinter} that resolves nanorc highlight-style names
+ * case-insensitively.
+ *
+ * JLine matches the {@code /prnt -s STYLE} value (and the
+ * {@code valueStyle}/{@code valueStyleAll} options) against the
+ * {@code syntax "<NAME>"} header of each nanorc grammar with a
+ * case-sensitive {@code String.equals} (in
+ * {@code org.jline.builtins.SyntaxHighlighter}), so {@code -s json}
+ * would not match {@code syntax "JSON"}.
+ *
+ * Rather than blindly transforming the requested style (which would
+ * break selecting a user's existing mixed-case grammars copied into
+ * {@code ~/.groovy} per the groovysh docs), this resolves the requested
+ * name <em>case-insensitively against the actually configured syntax
+ * names</em> — the same {@code jnanorc} {@link DefaultPrinter} itself
+ * uses (user-config first, via {@link ConfigurationPath}) plus the
+ * grammars it {@code include}s. The requested value is rewritten to the
+ * real name only on a unique case-insensitive hit; an exact match or an
+ * unknown name is passed through untouched so JLine's own behaviour is
+ * preserved. Discovery is failsafe per file and overall: a single
+ * malformed nanorc never prevents resolving the others, and any error
+ * leaves the options exactly as JLine would have seen them.
+ */
+@CompileStatic
+class GroovyPrinter extends DefaultPrinter {
+
+ private static final List<String> STYLE_KEYS = [Printer.STYLE,
Printer.VALUE_STYLE, Printer.VALUE_STYLE_ALL]
+ private static final Pattern SYNTAX_HEADER =
Pattern.compile(/(?m)^\s*syntax\s+"([^"]+)"/)
+ private static final Pattern INCLUDE_LINE =
Pattern.compile(/(?m)^\s*include\s+(\S+)/)
+
+ private final ConfigurationPath configPath
+ private Map<String, String> nameByLower // cached lowercase -> actual;
built lazily
+
+ GroovyPrinter(ScriptEngine engine, ConfigurationPath configPath) {
+ super(engine, configPath)
+ this.configPath = configPath
+ }
+
+ @Override
+ void println(Map<String, Object> options, Object object) {
+ super.println(normalizeStyles(options), object)
+ }
+
+ @Override
+ protected void highlightAndPrint(Map<String, Object> options, Throwable
exception) {
+ super.highlightAndPrint(normalizeStyles(options), exception)
+ }
+
+ /**
+ * Returns a copy of {@code options} with style-name values resolved to the
+ * actual configured syntax name (case-insensitively), or the original map
+ * untouched when there is nothing to change (so an immutable or shared
+ * caller map is not mutated). Failsafe: any problem returns {@code
options}
+ * unchanged.
+ */
+ private Map<String, Object> normalizeStyles(Map<String, Object> options) {
+ if (options == null || options.isEmpty()) {
+ return options
+ }
+ Map<String, String> names
+ try {
+ names = syntaxNames()
+ } catch (Exception ignored) {
+ return options
+ }
+ if (names.isEmpty()) {
+ return options
+ }
+ Map<String, Object> copy = null
+ for (String key : STYLE_KEYS) {
+ Object value = options.get(key)
+ if (value instanceof CharSequence && (value as
CharSequence).length() > 0) {
+ String requested = value.toString()
+ String resolved = resolveStyle(requested, names)
+ if (resolved != requested) {
+ if (copy == null) {
+ copy = new LinkedHashMap<String, Object>(options)
+ }
+ copy.put(key, resolved)
+ }
+ }
+ }
+ copy != null ? copy : options
+ }
+
+ /**
+ * Pure resolution (package-private for testing): an exact match or an
+ * unknown name is returned unchanged; otherwise the actual syntax name for
+ * a case-insensitive match is returned.
+ *
+ * @param requested the user-supplied style name
+ * @param nameByLower map of lower-cased syntax name to its actual casing
+ * @return the name JLine should be given
+ */
+ static String resolveStyle(String requested, Map<String, String>
nameByLower) {
+ if (requested == null || requested.isEmpty()) {
+ return requested
+ }
+ if (nameByLower.containsValue(requested)) {
+ return requested // exact match: leave as-is (also covers
user-config names)
+ }
+ String actual = nameByLower.get(requested.toLowerCase(Locale.ROOT))
+ actual != null ? actual : requested
+ }
+
+ /** lowercase-name -> actual syntax name across the resolved jnanorc
and its includes; cached. */
+ private synchronized Map<String, String> syntaxNames() {
+ if (nameByLower != null) {
+ return nameByLower
+ }
+ Map<String, String> map = new LinkedHashMap<>()
+ Path jnanorc = configPath?.getConfig('jnanorc')
+ if (jnanorc != null && Files.isReadable(jnanorc)) {
+ collectSyntaxNames(jnanorc, map)
+ }
+ nameByLower = map
+ return map
+ }
+
+ /**
+ * Scans the jnanorc itself and every file it {@code include}s for
+ * {@code syntax "NAME"} headers. Per-file and per-include failsafe.
+ */
+ static void collectSyntaxNames(Path jnanorc, Map<String, String> into) {
+ scanForSyntax(jnanorc, into) // jnanorc may declare syntaxes directly
+ String text
+ try {
+ text = new String(Files.readAllBytes(jnanorc))
+ } catch (Exception ignored) {
+ return
+ }
+ Path dir = jnanorc.toAbsolutePath().parent
+ Matcher m = INCLUDE_LINE.matcher(text)
+ while (m.find()) {
+ String glob = m.group(1)
+ try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir,
glob)) {
+ for (Path p : stream) {
+ scanForSyntax(p, into) // per-file failsafe inside
+ }
+ } catch (Exception ignored) {
+ // one bad include directive must not stop the others
+ }
+ }
+ }
+
+ /**
+ * Per-file failsafe: a single malformed/unreadable nanorc never breaks
+ * discovery of the rest. First spelling of a name wins (stable order).
+ */
+ private static void scanForSyntax(Path file, Map<String, String> into) {
+ try {
+ if (file == null || !Files.isReadable(file)) {
+ return
+ }
+ String content = new String(Files.readAllBytes(file))
+ Matcher m = SYNTAX_HEADER.matcher(content)
+ while (m.find()) {
+ String name = m.group(1)
+ into.putIfAbsent(name.toLowerCase(Locale.ROOT), name)
Review Comment:
Collapsing syntax names with `putIfAbsent` loses later names that differ
only by case. If a user has both `JSON` and `json`, the later exact name is no
longer in the map, so `resolveStyle('json', ...)` rewrites it to the first
spelling instead of preserving the exact match. Track exact names/ambiguity
separately, or avoid rewriting non-unique case-insensitive matches.
> allow format to be case insensitive for groovysh: /print -s FORMAT
> -------------------------------------------------------------------
>
> Key: GROOVY-12018
> URL: https://issues.apache.org/jira/browse/GROOVY-12018
> Project: Groovy
> Issue Type: Improvement
> Components: Groovysh
> Reporter: Paul King
> Priority: Major
>
--
This message was sent by Atlassian Jira
(v8.20.10#820010)