This is an automated email from the ASF dual-hosted git repository.
hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new 0c16330bc9 Add YAML file type support in Explorer perspective (#7632)
0c16330bc9 is described below
commit 0c16330bc90d56bb36a36569477831e29809f0ee
Author: Lance <[email protected]>
AuthorDate: Mon Jul 27 15:42:52 2026 +0800
Add YAML file type support in Explorer perspective (#7632)
Signed-off-by: lance <[email protected]>
---
.../hop/ui/hopgui/ContentEditorTm4eSupport.java | 109 ++--
.../hopgui/RuleBasedSourceViewerConfiguration.java | 6 +-
.../org/apache/hop/ui/hopgui/grammars/yaml.json | 610 +++++++++++++++++++++
.../ui/hopgui/ContentEditorTm4eSupportTest.java | 19 +
.../file/types/yaml/YamlExplorerFileType.java | 67 +++
.../types/yaml/YamlExplorerFileTypeHandler.java | 37 ++
.../yaml/YamlExplorerFileTypeHandlerTest.java | 68 +++
.../file/types/yaml/YamlExplorerFileTypeTest.java | 136 +++++
8 files changed, 1007 insertions(+), 45 deletions(-)
diff --git
a/rcp/src/main/java/org/apache/hop/ui/hopgui/ContentEditorTm4eSupport.java
b/rcp/src/main/java/org/apache/hop/ui/hopgui/ContentEditorTm4eSupport.java
index 5725c07b68..89a348063f 100644
--- a/rcp/src/main/java/org/apache/hop/ui/hopgui/ContentEditorTm4eSupport.java
+++ b/rcp/src/main/java/org/apache/hop/ui/hopgui/ContentEditorTm4eSupport.java
@@ -28,6 +28,7 @@ import java.util.List;
import java.util.Map;
import org.apache.hop.ui.core.PropsUi;
import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.TextAttribute;
import org.eclipse.jface.text.rules.IToken;
import org.eclipse.jface.text.rules.Token;
import org.eclipse.swt.graphics.Color;
@@ -53,6 +54,7 @@ final class ContentEditorTm4eSupport {
private static final String SCOPE_XML = "text.xml";
private static final String SCOPE_SQL = "source.sql";
private static final String SCOPE_PYTHON = "source.python";
+ private static final String SCOPE_YAML = "source.yaml";
/** Maps TM4E scope names to grammar resource filenames (classpath-relative
to grammars/). */
private static final Map<String, String> GRAMMAR_FILES =
@@ -60,7 +62,8 @@ final class ContentEditorTm4eSupport {
SCOPE_JSON, "json.json",
SCOPE_XML, "xml.json",
SCOPE_SQL, "sql.json",
- SCOPE_PYTHON, "python.json");
+ SCOPE_PYTHON, "python.json",
+ SCOPE_YAML, "yaml.json");
// Same palette as before (light/dark) for consistency
private static final RGB L_COMMENT = new RGB(128, 128, 128);
@@ -101,20 +104,18 @@ final class ContentEditorTm4eSupport {
/** Returns the grammar scope name for the given language id, or null if not
supported by TM4E. */
static String scopeForLanguage(String languageId) {
- if (languageId == null) return null;
- switch (languageId.toLowerCase(java.util.Locale.ROOT)) {
- case "json":
- return SCOPE_JSON;
- case "xml":
- return SCOPE_XML;
- case "sql":
- return SCOPE_SQL;
- case "python":
- case "py":
- return SCOPE_PYTHON;
- default:
- return null;
+ if (languageId == null) {
+ return null;
}
+
+ return switch (languageId.toLowerCase(java.util.Locale.ROOT)) {
+ case "json" -> SCOPE_JSON;
+ case "xml" -> SCOPE_XML;
+ case "sql" -> SCOPE_SQL;
+ case "python", "py" -> SCOPE_PYTHON;
+ case "yaml", "yml" -> SCOPE_YAML;
+ default -> null;
+ };
}
/**
@@ -124,10 +125,14 @@ final class ContentEditorTm4eSupport {
static org.eclipse.jface.text.source.SourceViewerConfiguration
createConfiguration(
String languageId, Display display) {
String scopeName = scopeForLanguage(languageId);
- if (scopeName == null) return null;
+ if (scopeName == null) {
+ return null;
+ }
ContentEditorTm4eSupport support = new ContentEditorTm4eSupport(display);
IGrammar grammar = support.getGrammar(scopeName);
- if (grammar == null) return null;
+ if (grammar == null) {
+ return null;
+ }
return ContentEditorTm4eSupport.createReconciler(support, grammar);
}
@@ -210,7 +215,9 @@ final class ContentEditorTm4eSupport {
}
private RGB scopeToRgb(List<String> scopes) {
- if (scopes == null || scopes.isEmpty()) return isDark() ? D_DEFAULT :
L_DEFAULT;
+ if (scopes == null || scopes.isEmpty()) {
+ return isDark() ? D_DEFAULT : L_DEFAULT;
+ }
String scope = String.join(" ", scopes);
boolean dark = isDark();
@@ -219,33 +226,59 @@ final class ContentEditorTm4eSupport {
}
// Comments (all languages)
- if (scope.contains("comment")) return dark ? D_COMMENT : L_COMMENT;
+ if (scope.contains("comment")) {
+ return dark ? D_COMMENT : L_COMMENT;
+ }
// JSON keys: must be checked before "string" - VS Code uses
"support.type.property-name.json"
- if (scope.contains("support.type.property-name") ||
scope.contains("property-name"))
+ if (scope.contains("support.type.property-name") ||
scope.contains("property-name")) {
+ return dark ? D_JSON_KEY : L_JSON_KEY;
+ }
+
+ // YAML keys: TextMate uses entity.name.tag.yaml (often with
string.unquoted.*)
+ if (scope.contains("entity.name.tag.yaml")) {
return dark ? D_JSON_KEY : L_JSON_KEY;
+ }
- // Strings (JSON: string.quoted.double; XML/SQL: string.quoted.*)
- if (scope.contains("string")) return dark ? D_STRING : L_STRING;
+ // Strings (JSON: string.quoted.double; XML/SQL: string.quoted.*; YAML:
string.*)
+ if (scope.contains("string")) {
+ return dark ? D_STRING : L_STRING;
+ }
// Numbers and constants
- if (scope.contains("constant.numeric") || scope.contains("number"))
+ if (scope.contains("constant.numeric") || scope.contains("number")) {
return dark ? D_NUMBER : L_NUMBER;
- if (scope.contains("constant.language")) return dark ? D_CONSTANT :
L_CONSTANT;
- if (scope.contains("constant.other")) return dark ? D_CONSTANT :
L_CONSTANT;
+ }
+ if (scope.contains("constant.language")) {
+ return dark ? D_CONSTANT : L_CONSTANT;
+ }
+ if (scope.contains("constant.other")) {
+ return dark ? D_CONSTANT : L_CONSTANT;
+ }
// Keywords (SQL, etc.)
if (scope.contains("keyword")
|| scope.contains("support.function")
- || scope.contains("support.type")) return dark ? D_KEYWORD : L_KEYWORD;
- if (scope.contains("storage.type") || scope.contains("storage.modifier"))
+ || scope.contains("support.type")) {
+ return dark ? D_KEYWORD : L_KEYWORD;
+ }
+ if (scope.contains("storage.type") || scope.contains("storage.modifier")) {
return dark ? D_KEYWORD : L_KEYWORD;
- if (scope.contains("entity.name.function")) return dark ? D_KEYWORD :
L_KEYWORD;
+ }
+ if (scope.contains("entity.name.function")) {
+ return dark ? D_KEYWORD : L_KEYWORD;
+ }
// XML: <? ?> then tags then attribute names
- if (scope.contains("meta.tag.preprocessor")) return dark ? D_XML_HEADER :
L_XML_HEADER;
- if (scope.contains("entity.name.tag") || scope.contains(".tag")) return
dark ? D_TAG : L_TAG;
- if (scope.contains("entity.other.attribute-name")) return dark ?
D_JSON_KEY : L_JSON_KEY;
+ if (scope.contains("meta.tag.preprocessor")) {
+ return dark ? D_XML_HEADER : L_XML_HEADER;
+ }
+ if (scope.contains("entity.name.tag") || scope.contains(".tag")) {
+ return dark ? D_TAG : L_TAG;
+ }
+ if (scope.contains("entity.other.attribute-name")) {
+ return dark ? D_JSON_KEY : L_JSON_KEY;
+ }
return dark ? D_DEFAULT : L_DEFAULT;
}
@@ -356,7 +389,9 @@ final class ContentEditorTm4eSupport {
int rangeEnd = Math.min(rangeOffset + rangeLength, len);
for (int lineIndex = 0; lineIndex < lines.length; lineIndex++) {
- if (lineIndex >= MAX_LINES_TO_TOKENIZE) break;
+ if (lineIndex >= MAX_LINES_TO_TOKENIZE) {
+ break;
+ }
String raw = lines[lineIndex];
int rawLen = raw.length();
@@ -409,16 +444,6 @@ final class ContentEditorTm4eSupport {
return result;
}
- private static final class ColoredToken {
- final int offset;
- final int length;
- final org.eclipse.jface.text.TextAttribute attribute;
-
- ColoredToken(int offset, int length,
org.eclipse.jface.text.TextAttribute attribute) {
- this.offset = offset;
- this.length = length;
- this.attribute = attribute;
- }
- }
+ private record ColoredToken(int offset, int length, TextAttribute
attribute) {}
}
}
diff --git
a/rcp/src/main/java/org/apache/hop/ui/hopgui/RuleBasedSourceViewerConfiguration.java
b/rcp/src/main/java/org/apache/hop/ui/hopgui/RuleBasedSourceViewerConfiguration.java
index fef246f722..123ede5fc3 100644
---
a/rcp/src/main/java/org/apache/hop/ui/hopgui/RuleBasedSourceViewerConfiguration.java
+++
b/rcp/src/main/java/org/apache/hop/ui/hopgui/RuleBasedSourceViewerConfiguration.java
@@ -22,7 +22,7 @@ import org.eclipse.swt.widgets.Display;
/**
* Builds a {@link SourceViewerConfiguration} for the content editor. Uses
TM4E with TextMate
- * grammars for syntax highlighting of JSON, XML, SQL, and Python; other
languages get a plain
+ * grammars for syntax highlighting of JSON, XML, SQL, Python, and YAML; other
languages get a plain
* config.
*/
public final class RuleBasedSourceViewerConfiguration {
@@ -31,9 +31,9 @@ public final class RuleBasedSourceViewerConfiguration {
/**
* Creates a configuration for the given language. Uses TM4E when a grammar
is available (json,
- * xml, sql, python); otherwise returns a plain configuration with no syntax
highlighting.
+ * xml, sql, python, yaml); otherwise returns a plain configuration with no
syntax highlighting.
*
- * @param languageId language id (e.g. "json", "xml", "sql"), or null for
plain text
+ * @param languageId language id (e.g. "json", "xml", "sql", "yaml"), or
null for plain text
* @return configuration, never null
*/
public static SourceViewerConfiguration create(String languageId) {
diff --git a/rcp/src/main/resources/org/apache/hop/ui/hopgui/grammars/yaml.json
b/rcp/src/main/resources/org/apache/hop/ui/hopgui/grammars/yaml.json
new file mode 100644
index 0000000000..bf7bd52b8b
--- /dev/null
+++ b/rcp/src/main/resources/org/apache/hop/ui/hopgui/grammars/yaml.json
@@ -0,0 +1,610 @@
+{
+ "information_for_contributors": [
+ "This file was obtained from
https://github.com/redhat-developer/vscode-yaml/blob/main/syntaxes/yaml.tmLanguage.json",
+ "That file was converted from
https://github.com/textmate/yaml.tmbundle/blob/master/Syntaxes/YAML.tmLanguage",
+ "If you want to provide a fix or improvement, please create a
pull request against the original repository.",
+ "Once accepted there, we are happy to receive an update
request."
+ ],
+ "version":
"https://github.com/textmate/yaml.tmbundle/commit/e54ceae3b719506dba7e481a77cea4a8b576ae46",
+ "name": "YAML",
+ "scopeName": "source.yaml",
+ "patterns": [
+ {
+ "include": "#comment"
+ },
+ {
+ "include": "#property"
+ },
+ {
+ "include": "#directive"
+ },
+ {
+ "match": "^---",
+ "name": "entity.other.document.begin.yaml"
+ },
+ {
+ "match": "^\\.{3}",
+ "name": "entity.other.document.end.yaml"
+ },
+ {
+ "include": "#node"
+ }
+ ],
+ "repository": {
+ "block-collection": {
+ "patterns": [
+ {
+ "include": "#block-sequence"
+ },
+ {
+ "include": "#block-mapping"
+ }
+ ]
+ },
+ "block-mapping": {
+ "patterns": [
+ {
+ "include": "#block-pair"
+ }
+ ]
+ },
+ "block-node": {
+ "patterns": [
+ {
+ "include": "#prototype"
+ },
+ {
+ "include": "#block-scalar"
+ },
+ {
+ "include": "#block-collection"
+ },
+ {
+ "include": "#flow-scalar-plain-out"
+ },
+ {
+ "include": "#flow-node"
+ }
+ ]
+ },
+ "block-pair": {
+ "patterns": [
+ {
+ "begin": "\\?",
+ "beginCaptures": {
+ "1": {
+ "name":
"punctuation.definition.key-value.begin.yaml"
+ }
+ },
+ "end": "(?=\\?)|^ *(:)|(:)",
+ "endCaptures": {
+ "1": {
+ "name":
"punctuation.separator.key-value.mapping.yaml"
+ },
+ "2": {
+ "name":
"invalid.illegal.expected-newline.yaml"
+ }
+ },
+ "name": "meta.block-mapping.yaml",
+ "patterns": [
+ {
+ "include": "#block-node"
+ }
+ ]
+ },
+ {
+ "begin": "(?x)\n
(?=\n (?x:\n
[^\\s[-?:,\\[\\]{}#&*!|>'\"%@`]]\n | [?:-] \\S\n
)\n (\n
[^\\s:]\n | : \\S\n
| \\s+ (?![#\\s])\n )*\n
\\s*\n :\n\ [...]
+ "end": "(?x)\n
(?=\n \\s* $\n | \\s+
\\#\n | \\s* : (\\s|$)\n )\n
",
+ "patterns": [
+ {
+ "include":
"#flow-scalar-plain-out-implicit-type"
+ },
+ {
+ "begin": "(?x)\n
[^\\s[-?:,\\[\\]{}#&*!|>'\"%@`]]\n
| [?:-] \\S\n ",
+ "beginCaptures": {
+ "0": {
+ "name":
"entity.name.tag.yaml"
+ }
+ },
+ "contentName":
"entity.name.tag.yaml",
+ "end": "(?x)\n
(?=\n \\s* $\n
| \\s+ \\#\n |
\\s* : (\\s|$)\n )\n
",
+ "name":
"string.unquoted.plain.out.yaml"
+ }
+ ]
+ },
+ {
+ "match": ":(?=\\s|$)",
+ "name":
"punctuation.separator.key-value.mapping.yaml"
+ }
+ ]
+ },
+ "block-scalar": {
+ "begin": "(?:(\\|)|(>))([1-9])?([-+])?(.*\\n?)",
+ "beginCaptures": {
+ "1": {
+ "name":
"keyword.control.flow.block-scalar.literal.yaml"
+ },
+ "2": {
+ "name":
"keyword.control.flow.block-scalar.folded.yaml"
+ },
+ "3": {
+ "name":
"constant.numeric.indentation-indicator.yaml"
+ },
+ "4": {
+ "name":
"storage.modifier.chomping-indicator.yaml"
+ },
+ "5": {
+ "patterns": [
+ {
+ "include": "#comment"
+ },
+ {
+ "match": ".+",
+ "name":
"invalid.illegal.expected-comment-or-newline.yaml"
+ }
+ ]
+ }
+ },
+ "end": "^(?=\\S)|(?!\\G)",
+ "patterns": [
+ {
+ "begin": "^([ ]+)(?! )",
+ "end": "^(?!\\1|\\s*$)",
+ "name": "string.unquoted.block.yaml"
+ }
+ ]
+ },
+ "block-sequence": {
+ "match": "(-)(?!\\S)",
+ "name":
"punctuation.definition.block.sequence.item.yaml"
+ },
+ "comment": {
+ "begin": "(?:(^[ \\t]*)|[ \\t]+)(?=#\\p{Print}*$)",
+ "beginCaptures": {
+ "1": {
+ "name":
"punctuation.whitespace.comment.leading.yaml"
+ }
+ },
+ "end": "(?!\\G)",
+ "patterns": [
+ {
+ "begin": "#",
+ "beginCaptures": {
+ "0": {
+ "name":
"punctuation.definition.comment.yaml"
+ }
+ },
+ "end": "\\n",
+ "name": "comment.line.number-sign.yaml"
+ }
+ ]
+ },
+ "directive": {
+ "begin": "^%",
+ "beginCaptures": {
+ "0": {
+ "name":
"punctuation.definition.directive.begin.yaml"
+ }
+ },
+ "end": "(?=$|[ \\t]+($|#))",
+ "name": "meta.directive.yaml",
+ "patterns": [
+ {
+ "captures": {
+ "1": {
+ "name":
"keyword.other.directive.yaml.yaml"
+ },
+ "2": {
+ "name":
"constant.numeric.yaml-version.yaml"
+ }
+ },
+ "match": "\\G(YAML)[ \\t]+(\\d+\\.\\d+)"
+ },
+ {
+ "captures": {
+ "1": {
+ "name":
"keyword.other.directive.tag.yaml"
+ },
+ "2": {
+ "name":
"storage.type.tag-handle.yaml"
+ },
+ "3": {
+ "name":
"support.type.tag-prefix.yaml"
+ }
+ },
+ "match": "(?x)\n
\\G\n (TAG)\n (?:[ \\t]+\n
((?:!(?:[0-9A-Za-z\\-]*!)?))\n
(?:[ \\t]+ (\n ! (?x:
%[0-9A-Fa-f]{2} | [0-9A-Za-z\\-#;/?:@&=+$,_.!~*'()\\[\\]] )*\n
| (?![,!\\[\\]{}]) (?x: %[0-9A-Fa-f]{2} |
[0-9A-Za-z\\-#;/?:@&=+$,_.!~*'()\\[\\]] )+\n )\n
[...]
+ },
+ {
+ "captures": {
+ "1": {
+ "name":
"support.other.directive.reserved.yaml"
+ },
+ "2": {
+ "name":
"string.unquoted.directive-name.yaml"
+ },
+ "3": {
+ "name":
"string.unquoted.directive-parameter.yaml"
+ }
+ },
+ "match": "(?x) \\G (\\w+) (?:[ \\t]+
(\\w+) (?:[ \\t]+ (\\w+))? )?"
+ },
+ {
+ "match": "\\S+",
+ "name":
"invalid.illegal.unrecognized.yaml"
+ }
+ ]
+ },
+ "flow-alias": {
+ "captures": {
+ "1": {
+ "name":
"keyword.control.flow.alias.yaml"
+ },
+ "2": {
+ "name":
"punctuation.definition.alias.yaml"
+ },
+ "3": {
+ "name": "variable.other.alias.yaml"
+ },
+ "4": {
+ "name":
"invalid.illegal.character.anchor.yaml"
+ }
+ },
+ "match":
"((\\*))([^\\s\\[\\]\/{\/},]+)([^\\s\\]},]\\S*)?"
+ },
+ "flow-collection": {
+ "patterns": [
+ {
+ "include": "#flow-sequence"
+ },
+ {
+ "include": "#flow-mapping"
+ }
+ ]
+ },
+ "flow-mapping": {
+ "begin": "\\{",
+ "beginCaptures": {
+ "0": {
+ "name":
"punctuation.definition.mapping.begin.yaml"
+ }
+ },
+ "end": "\\}",
+ "endCaptures": {
+ "0": {
+ "name":
"punctuation.definition.mapping.end.yaml"
+ }
+ },
+ "name": "meta.flow-mapping.yaml",
+ "patterns": [
+ {
+ "include": "#prototype"
+ },
+ {
+ "match": ",",
+ "name":
"punctuation.separator.mapping.yaml"
+ },
+ {
+ "include": "#flow-pair"
+ }
+ ]
+ },
+ "flow-node": {
+ "patterns": [
+ {
+ "include": "#prototype"
+ },
+ {
+ "include": "#flow-alias"
+ },
+ {
+ "include": "#flow-collection"
+ },
+ {
+ "include": "#flow-scalar"
+ }
+ ]
+ },
+ "flow-pair": {
+ "patterns": [
+ {
+ "begin": "\\?",
+ "beginCaptures": {
+ "0": {
+ "name":
"punctuation.definition.key-value.begin.yaml"
+ }
+ },
+ "end": "(?=[},\\]])",
+ "name": "meta.flow-pair.explicit.yaml",
+ "patterns": [
+ {
+ "include": "#prototype"
+ },
+ {
+ "include": "#flow-pair"
+ },
+ {
+ "include": "#flow-node"
+ },
+ {
+ "begin":
":(?=\\s|$|[\\[\\]{},])",
+ "beginCaptures": {
+ "0": {
+ "name":
"punctuation.separator.key-value.mapping.yaml"
+ }
+ },
+ "end": "(?=[},\\]])",
+ "patterns": [
+ {
+
"include": "#flow-value"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "begin": "(?x)\n
(?=\n (?:\n
[^\\s[-?:,\\[\\]{}#&*!|>'\"%@`]]\n | [?:-]
[^\\s[\\[\\]{},]]\n )\n
(\n [^\\s:[\\[\\]{},]]\n
| : [^\\s[\\[\\]{},]]\n | \\s+
(?![#\\s])\n )*\n \\s*
[...]
+ "end": "(?x)\n
(?=\n \\s* $\n | \\s+
\\#\n | \\s* : (\\s|$)\n
| \\s* : [\\[\\]{},]\n | \\s* [\\[\\]{},]\n
)\n ",
+ "name": "meta.flow-pair.key.yaml",
+ "patterns": [
+ {
+ "include":
"#flow-scalar-plain-in-implicit-type"
+ },
+ {
+ "begin": "(?x)\n
[^\\s[-?:,\\[\\]{}#&*!|>'\"%@`]]\n
| [?:-] [^\\s[\\[\\]{},]]\n ",
+ "beginCaptures": {
+ "0": {
+ "name":
"entity.name.tag.yaml"
+ }
+ },
+ "contentName":
"entity.name.tag.yaml",
+ "end": "(?x)\n
(?=\n \\s* $\n
| \\s+ \\#\n |
\\s* : (\\s|$)\n | \\s* : [\\[\\]{},]\n
| \\s* [\\[\\]{},]\n
)\n ",
+ "name":
"string.unquoted.plain.in.yaml"
+ }
+ ]
+ },
+ {
+ "include": "#flow-node"
+ },
+ {
+ "begin": ":(?=\\s|$|[\\[\\]{},])",
+ "captures": {
+ "0": {
+ "name":
"punctuation.separator.key-value.mapping.yaml"
+ }
+ },
+ "end": "(?=[},\\]])",
+ "name": "meta.flow-pair.yaml",
+ "patterns": [
+ {
+ "include": "#flow-value"
+ }
+ ]
+ }
+ ]
+ },
+ "flow-scalar": {
+ "patterns": [
+ {
+ "include": "#flow-scalar-double-quoted"
+ },
+ {
+ "include": "#flow-scalar-single-quoted"
+ },
+ {
+ "include": "#flow-scalar-plain-in"
+ }
+ ]
+ },
+ "flow-scalar-double-quoted": {
+ "begin": "\"",
+ "beginCaptures": {
+ "0": {
+ "name":
"punctuation.definition.string.begin.yaml"
+ }
+ },
+ "end": "\"",
+ "endCaptures": {
+ "0": {
+ "name":
"punctuation.definition.string.end.yaml"
+ }
+ },
+ "name": "string.quoted.double.yaml",
+ "patterns": [
+ {
+ "match": "\\\\([0abtnvfre
\"/\\\\N_Lp]|x\\d\\d|u\\d{4}|U\\d{8})",
+ "name": "constant.character.escape.yaml"
+ },
+ {
+ "match": "\\\\\\n",
+ "name":
"constant.character.escape.double-quoted.newline.yaml"
+ }
+ ]
+ },
+ "flow-scalar-plain-in": {
+ "patterns": [
+ {
+ "include":
"#flow-scalar-plain-in-implicit-type"
+ },
+ {
+ "begin": "(?x)\n
[^\\s[-?:,\\[\\]{}#&*!|>'\"%@`]]\n | [?:-]
[^\\s[\\[\\]{},]]\n ",
+ "end": "(?x)\n
(?=\n \\s* $\n | \\s+
\\#\n | \\s* : (\\s|$)\n
| \\s* : [\\[\\]{},]\n | \\s* [\\[\\]{},]\n
)\n ",
+ "name": "string.unquoted.plain.in.yaml"
+ }
+ ]
+ },
+ "flow-scalar-plain-in-implicit-type": {
+ "patterns": [
+ {
+ "captures": {
+ "1": {
+ "name":
"constant.language.null.yaml"
+ },
+ "2": {
+ "name":
"constant.language.boolean.yaml"
+ },
+ "3": {
+ "name":
"constant.numeric.integer.yaml"
+ },
+ "4": {
+ "name":
"constant.numeric.float.yaml"
+ },
+ "5": {
+ "name":
"keyword.operator.yaml"
+ }
+ },
+ "match": "(?x) (?x:(null|Null|NULL|~)
|
(y|Y|yes|Yes|YES|n|N|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF)
| ((?:[-+]? [0-9]+) | (?:0o [0-7]+) | (?:0x [0-9a-fA-F]+) | (?:[-+]? [1-9]
[0-9_]* (?: :[0-5]?[0-9])+)) | ((?:[-+]? (?: \\. [0-9]+ | [0-9]+ (?: \\.
[0-9]* )? ) (?: [eE] [-+]? [0-9]+ )?) | (?:[-+]? [0-9] [0-9_]* (?:
:[0-5]?[0-9])+ \\. [0-9_]*) | (?:[-+]? (?: \\.inf | \\.Inf | \\.INF )) |
(?:\\.nan | \\.NaN | \\.NAN) ) | (<<) ) (?: (?= \\s* $ | \\s+ [...]
+ }
+ ]
+ },
+ "flow-scalar-plain-out": {
+ "patterns": [
+ {
+ "include":
"#flow-scalar-plain-out-implicit-type"
+ },
+ {
+ "begin": "(?x)\n
[^\\s[-?:,\\[\\]{}#&*!|>'\"%@`]]\n | [?:-] \\S\n
",
+ "end": "(?x)\n
(?=\n \\s* $\n | \\s+
\\#\n | \\s* : (\\s|$)\n )\n
",
+ "name": "string.unquoted.plain.out.yaml"
+ }
+ ]
+ },
+ "flow-scalar-plain-out-implicit-type": {
+ "patterns": [
+ {
+ "captures": {
+ "1": {
+ "name":
"constant.language.null.yaml"
+ },
+ "2": {
+ "name":
"constant.language.boolean.yaml"
+ },
+ "3": {
+ "name":
"constant.numeric.integer.yaml"
+ },
+ "4": {
+ "name":
"constant.numeric.float.yaml"
+ },
+ "5": {
+ "name":
"keyword.operator.yaml"
+ }
+ },
+ "match": "(?x) (?x:(null|Null|NULL|~)
|
(y|Y|yes|Yes|YES|n|N|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF)
| ((?:[-+]? [0-9]+) | (?:0o [0-7]+) | (?:0x [0-9a-fA-F]+) | (?:[-+]? [1-9]
[0-9_]* (?: :[0-5]?[0-9])+)) | ((?:[-+]? (?: \\. [0-9]+ | [0-9]+ (?: \\.
[0-9]* )? ) (?: [eE] [-+]? [0-9]+ )?) | (?:[-+]? [0-9] [0-9_]* (?:
:[0-5]?[0-9])+ \\. [0-9_]*) | (?:[-+]? (?: \\.inf | \\.Inf | \\.INF )) |
(?:\\.nan | \\.NaN | \\.NAN) ) | (<<) ) (?x: (?= \\s* $ | \\s+ [...]
+ }
+ ]
+ },
+ "flow-scalar-single-quoted": {
+ "begin": "'",
+ "beginCaptures": {
+ "0": {
+ "name":
"punctuation.definition.string.begin.yaml"
+ }
+ },
+ "end": "'(?!')",
+ "endCaptures": {
+ "0": {
+ "name":
"punctuation.definition.string.end.yaml"
+ }
+ },
+ "name": "string.quoted.single.yaml",
+ "patterns": [
+ {
+ "match": "''",
+ "name":
"constant.character.escape.single-quoted.yaml"
+ }
+ ]
+ },
+ "flow-sequence": {
+ "begin": "\\[",
+ "beginCaptures": {
+ "0": {
+ "name":
"punctuation.definition.sequence.begin.yaml"
+ }
+ },
+ "end": "\\]",
+ "endCaptures": {
+ "0": {
+ "name":
"punctuation.definition.sequence.end.yaml"
+ }
+ },
+ "name": "meta.flow-sequence.yaml",
+ "patterns": [
+ {
+ "include": "#prototype"
+ },
+ {
+ "match": ",",
+ "name":
"punctuation.separator.sequence.yaml"
+ },
+ {
+ "include": "#flow-pair"
+ },
+ {
+ "include": "#flow-node"
+ }
+ ]
+ },
+ "flow-value": {
+ "patterns": [
+ {
+ "begin": "\\G(?![},\\]])",
+ "end": "(?=[},\\]])",
+ "name": "meta.flow-pair.value.yaml",
+ "patterns": [
+ {
+ "include": "#flow-node"
+ }
+ ]
+ }
+ ]
+ },
+ "node": {
+ "patterns": [
+ {
+ "include": "#block-node"
+ }
+ ]
+ },
+ "property": {
+ "begin": "(?=!|&)",
+ "end": "(?!\\G)",
+ "name": "meta.property.yaml",
+ "patterns": [
+ {
+ "captures": {
+ "1": {
+ "name":
"keyword.control.property.anchor.yaml"
+ },
+ "2": {
+ "name":
"punctuation.definition.anchor.yaml"
+ },
+ "3": {
+ "name":
"entity.name.type.anchor.yaml"
+ },
+ "4": {
+ "name":
"invalid.illegal.character.anchor.yaml"
+ }
+ },
+ "match":
"\\G((&))([^\\s\\[\\]/{/},]+)(\\S+)?"
+ },
+ {
+ "match": "(?x)\n
\\G\n (?:\n ! < (?:
%[0-9A-Fa-f]{2} | [0-9A-Za-z\\-#;/?:@&=+$,_.!~*'()\\[\\]] )+ >\n
| (?:!(?:[0-9A-Za-z\\-]*!)?) (?: %[0-9A-Fa-f]{2} |
[0-9A-Za-z\\-#;/?:@&=+$_.~*'()] )+\n | !\n
)\n (?=\\ |\\t|$)\n ",
+ "name": "storage.type.tag-handle.yaml"
+ },
+ {
+ "match": "\\S+",
+ "name":
"invalid.illegal.tag-handle.yaml"
+ }
+ ]
+ },
+ "prototype": {
+ "patterns": [
+ {
+ "include": "#comment"
+ },
+ {
+ "include": "#property"
+ }
+ ]
+ }
+ }
+}
diff --git
a/rcp/src/test/java/org/apache/hop/ui/hopgui/ContentEditorTm4eSupportTest.java
b/rcp/src/test/java/org/apache/hop/ui/hopgui/ContentEditorTm4eSupportTest.java
index 63cfc77a72..cca9c3b590 100644
---
a/rcp/src/test/java/org/apache/hop/ui/hopgui/ContentEditorTm4eSupportTest.java
+++
b/rcp/src/test/java/org/apache/hop/ui/hopgui/ContentEditorTm4eSupportTest.java
@@ -44,4 +44,23 @@ class ContentEditorTm4eSupportTest {
assertTrue(in.read() >= 0, "python.json should not be empty");
}
}
+
+ @Test
+ void scopeForLanguage_yaml_returnsSourceYaml() {
+ assertEquals("source.yaml",
ContentEditorTm4eSupport.scopeForLanguage("yaml"));
+ }
+
+ @Test
+ void scopeForLanguage_yml_returnsSourceYaml() {
+ assertEquals("source.yaml",
ContentEditorTm4eSupport.scopeForLanguage("yml"));
+ }
+
+ @Test
+ void yamlGrammarResourceIsOnClasspath() throws Exception {
+ try (InputStream in =
+
ContentEditorTm4eSupport.class.getResourceAsStream("grammars/yaml.json")) {
+ assertNotNull(in, "grammars/yaml.json should be on the classpath");
+ assertTrue(in.read() >= 0, "yaml.json should not be empty");
+ }
+ }
}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/yaml/YamlExplorerFileType.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/yaml/YamlExplorerFileType.java
new file mode 100644
index 0000000000..76dc936794
--- /dev/null
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/yaml/YamlExplorerFileType.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.hopgui.perspective.explorer.file.types.yaml;
+
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.ui.hopgui.HopGui;
+import org.apache.hop.ui.hopgui.file.HopFileTypePlugin;
+import org.apache.hop.ui.hopgui.file.IHopFileType;
+import org.apache.hop.ui.hopgui.file.IHopFileTypeHandler;
+import org.apache.hop.ui.hopgui.file.empty.EmptyHopFileTypeHandler;
+import org.apache.hop.ui.hopgui.perspective.explorer.ExplorerFile;
+import org.apache.hop.ui.hopgui.perspective.explorer.ExplorerPerspective;
+import
org.apache.hop.ui.hopgui.perspective.explorer.file.capabilities.FileTypeCapabilities;
+import
org.apache.hop.ui.hopgui.perspective.explorer.file.types.text.BaseTextExplorerFileType;
+
+@HopFileTypePlugin(
+ id = "YamlExplorerFileType",
+ name = "YAML File Type",
+ description = "YAML file handling in the explorer perspective",
+ image = "ui/images/file.svg")
+public class YamlExplorerFileType extends
BaseTextExplorerFileType<YamlExplorerFileTypeHandler> {
+
+ public YamlExplorerFileType() {
+ super(
+ "YAML File",
+ ".yaml",
+ new String[] {"*.yml", "*.yaml"},
+ new String[] {"YAML files", "YAML files"},
+ FileTypeCapabilities.getCapabilities(
+ IHopFileType.CAPABILITY_SAVE,
+ IHopFileType.CAPABILITY_SAVE_AS,
+ IHopFileType.CAPABILITY_CLOSE,
+ IHopFileType.CAPABILITY_FILE_HISTORY,
+ IHopFileType.CAPABILITY_COPY,
+ IHopFileType.CAPABILITY_CUT,
+ IHopFileType.CAPABILITY_PASTE,
+ IHopFileType.CAPABILITY_SELECT));
+ }
+
+ @Override
+ public YamlExplorerFileTypeHandler createFileTypeHandler(
+ HopGui hopGui, ExplorerPerspective perspective, ExplorerFile file) {
+ return new YamlExplorerFileTypeHandler(hopGui, perspective, file);
+ }
+
+ @Override
+ public IHopFileTypeHandler newFile(HopGui hopGui, IVariables
parentVariableSpace)
+ throws HopException {
+ return new EmptyHopFileTypeHandler();
+ }
+}
diff --git
a/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/yaml/YamlExplorerFileTypeHandler.java
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/yaml/YamlExplorerFileTypeHandler.java
new file mode 100644
index 0000000000..107a1b1975
--- /dev/null
+++
b/ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/yaml/YamlExplorerFileTypeHandler.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.hopgui.perspective.explorer.file.types.yaml;
+
+import org.apache.hop.ui.hopgui.HopGui;
+import org.apache.hop.ui.hopgui.perspective.explorer.ExplorerFile;
+import org.apache.hop.ui.hopgui.perspective.explorer.ExplorerPerspective;
+import
org.apache.hop.ui.hopgui.perspective.explorer.file.types.text.BaseTextExplorerFileTypeHandler;
+
+/** Handler for YAML files in the file explorer perspective. */
+public class YamlExplorerFileTypeHandler extends
BaseTextExplorerFileTypeHandler {
+
+ public YamlExplorerFileTypeHandler(
+ HopGui hopGui, ExplorerPerspective perspective, ExplorerFile
explorerFile) {
+ super(hopGui, perspective, explorerFile);
+ }
+
+ @Override
+ protected String getLanguageId() {
+ return "yaml";
+ }
+}
diff --git
a/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/yaml/YamlExplorerFileTypeHandlerTest.java
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/yaml/YamlExplorerFileTypeHandlerTest.java
new file mode 100644
index 0000000000..98a3dabcde
--- /dev/null
+++
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/yaml/YamlExplorerFileTypeHandlerTest.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.hopgui.perspective.explorer.file.types.yaml;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import org.apache.hop.ui.hopgui.HopGui;
+import org.apache.hop.ui.hopgui.perspective.explorer.ExplorerFile;
+import org.apache.hop.ui.hopgui.perspective.explorer.ExplorerPerspective;
+import
org.apache.hop.ui.hopgui.perspective.explorer.file.IExplorerFileTypeHandler;
+import
org.apache.hop.ui.hopgui.perspective.explorer.file.types.text.BaseTextExplorerFileTypeHandler;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/** Unit test for {@link YamlExplorerFileTypeHandler} */
+class YamlExplorerFileTypeHandlerTest {
+
+ private YamlExplorerFileTypeHandler handler;
+
+ @BeforeEach
+ void setUp() {
+ handler = new YamlExplorerFileTypeHandler(null, null, null);
+ }
+
+ @Test
+ void testHandlerExtendsBaseTextExplorerFileTypeHandler() {
+ assertInstanceOf(BaseTextExplorerFileTypeHandler.class, handler);
+ }
+
+ @Test
+ void testHandlerImplementsIExplorerFileTypeHandler() {
+ assertInstanceOf(IExplorerFileTypeHandler.class, handler);
+ }
+
+ @Test
+ void testHandlerHasCorrectConstructor() throws Exception {
+ Constructor<YamlExplorerFileTypeHandler> constructor =
+ YamlExplorerFileTypeHandler.class.getConstructor(
+ HopGui.class, ExplorerPerspective.class, ExplorerFile.class);
+ assertNotNull(constructor);
+ }
+
+ @Test
+ void testHandlerReturnsYamlLanguageId() throws Exception {
+ Method method =
BaseTextExplorerFileTypeHandler.class.getDeclaredMethod("getLanguageId");
+ method.setAccessible(true);
+ assertEquals("yaml", method.invoke(handler));
+ }
+}
diff --git
a/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/yaml/YamlExplorerFileTypeTest.java
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/yaml/YamlExplorerFileTypeTest.java
new file mode 100644
index 0000000000..6523a3e922
--- /dev/null
+++
b/ui/src/test/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/yaml/YamlExplorerFileTypeTest.java
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.hop.ui.hopgui.perspective.explorer.file.types.yaml;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.hop.ui.hopgui.file.IHopFileType;
+import org.apache.hop.ui.hopgui.file.empty.EmptyHopFileTypeHandler;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/** Unit test for {@link YamlExplorerFileType} */
+class YamlExplorerFileTypeTest {
+
+ private YamlExplorerFileType fileType;
+
+ @BeforeEach
+ void setUp() {
+ fileType = new YamlExplorerFileType();
+ }
+
+ @Test
+ void testConstructor() {
+ assertNotNull(fileType);
+ }
+
+ @Test
+ void testGetName() {
+ assertEquals("YAML File", fileType.getName());
+ }
+
+ @Test
+ void testGetDefaultFileExtension() {
+ assertEquals(".yaml", fileType.getDefaultFileExtension());
+ }
+
+ @Test
+ void testGetFilterExtensions() {
+ String[] extensions = fileType.getFilterExtensions();
+ assertNotNull(extensions);
+ assertEquals(2, extensions.length);
+ assertEquals("*.yml", extensions[0]);
+ assertEquals("*.yaml", extensions[1]);
+ }
+
+ @Test
+ void testGetFilterNames() {
+ String[] names = fileType.getFilterNames();
+ assertNotNull(names);
+ assertEquals(2, names.length);
+ assertEquals("YAML files", names[0]);
+ assertEquals("YAML files", names[1]);
+ }
+
+ @Test
+ void testFilterExtensionsMatchFilterNames() {
+ String[] extensions = fileType.getFilterExtensions();
+ String[] names = fileType.getFilterNames();
+ assertEquals(extensions.length, names.length);
+ for (int i = 0; i < extensions.length; i++) {
+ assertNotNull(extensions[i]);
+ assertNotNull(names[i]);
+ assertFalse(extensions[i].isEmpty());
+ assertFalse(names[i].isEmpty());
+ }
+ }
+
+ @Test
+ void testHasExpectedCapabilities() {
+ assertTrue(fileType.hasCapability(IHopFileType.CAPABILITY_SAVE));
+ assertTrue(fileType.hasCapability(IHopFileType.CAPABILITY_SAVE_AS));
+ assertTrue(fileType.hasCapability(IHopFileType.CAPABILITY_CLOSE));
+ assertTrue(fileType.hasCapability(IHopFileType.CAPABILITY_FILE_HISTORY));
+ assertTrue(fileType.hasCapability(IHopFileType.CAPABILITY_COPY));
+ assertTrue(fileType.hasCapability(IHopFileType.CAPABILITY_CUT));
+ assertTrue(fileType.hasCapability(IHopFileType.CAPABILITY_PASTE));
+ assertTrue(fileType.hasCapability(IHopFileType.CAPABILITY_SELECT));
+ assertTrue(fileType.hasCapability(IHopFileType.CAPABILITY_SEARCH));
+ assertFalse(fileType.hasCapability(IHopFileType.CAPABILITY_NEW));
+ assertFalse(fileType.hasCapability(IHopFileType.CAPABILITY_START));
+ }
+
+ @Test
+ void testGetCapabilities() {
+ assertNotNull(fileType.getCapabilities());
+ assertFalse(fileType.getCapabilities().isEmpty());
+ }
+
+ @Test
+ void testGetFileTypeImage() {
+ assertEquals("ui/images/file.svg", fileType.getFileTypeImage());
+ }
+
+ @Test
+ void testCreateFileTypeHandlerReturnsYamlHandler() {
+ assertInstanceOf(
+ YamlExplorerFileTypeHandler.class,
fileType.createFileTypeHandler(null, null, null));
+ }
+
+ @Test
+ void testNewFileReturnsEmptyHandler() throws Exception {
+ assertInstanceOf(EmptyHopFileTypeHandler.class, fileType.newFile(null,
null));
+ }
+
+ @Test
+ void testIsHandledByYmlAndYamlExtensions() throws Exception {
+ assertTrue(fileType.isHandledBy("config.yml", false));
+ assertTrue(fileType.isHandledBy("config.yaml", false));
+ assertFalse(fileType.isHandledBy("config.json", false));
+ assertFalse(fileType.isHandledBy("config.txt", false));
+ }
+
+ @Test
+ void testSupportsOpening() {
+ assertTrue(fileType.supportsOpening());
+ }
+}