From 802bcc4cbc845db2cbb9657e0452232cc19d66a4 Mon Sep 17 00:00:00 2001 From: lance Date: Sat, 25 Jul 2026 21:21:02 +0800 Subject: [PATCH] Add YAML file type support in Explorer perspective Signed-off-by: lance --- .../ui/hopgui/ContentEditorTm4eSupport.java | 109 ++-- .../RuleBasedSourceViewerConfiguration.java | 6 +- .../apache/hop/ui/hopgui/grammars/yaml.json | 610 ++++++++++++++++++ .../hopgui/ContentEditorTm4eSupportTest.java | 19 + .../file/types/yaml/YamlExplorerFileType.java | 67 ++ .../yaml/YamlExplorerFileTypeHandler.java | 37 ++ .../yaml/YamlExplorerFileTypeHandlerTest.java | 68 ++ .../types/yaml/YamlExplorerFileTypeTest.java | 136 ++++ 8 files changed, 1007 insertions(+), 45 deletions(-) create mode 100644 rcp/src/main/resources/org/apache/hop/ui/hopgui/grammars/yaml.json create mode 100644 ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/yaml/YamlExplorerFileType.java create mode 100644 ui/src/main/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/yaml/YamlExplorerFileTypeHandler.java create mode 100644 ui/src/test/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/yaml/YamlExplorerFileTypeHandlerTest.java create mode 100644 ui/src/test/java/org/apache/hop/ui/hopgui/perspective/explorer/file/types/yaml/YamlExplorerFileTypeTest.java 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.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 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 @@ private static boolean isDark() { /** 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 @@ static String scopeForLanguage(String languageId) { 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 @@ private org.eclipse.jface.text.TextAttribute scopeToAttribute(List scope } private RGB scopeToRgb(List 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 @@ private RGB scopeToRgb(List scopes) { } // 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 @@ private java.util.List tokenizeLines( 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 @@ private java.util.List tokenizeLines( 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 @@ /** * 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 @@ private 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\t\t\t\t\t\t\t(\\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 )?\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*\n :\n\t\t\t\t\t\t\t(\\s|$)\n )\n ", + "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+ \\# | \\s* : (\\s|$) | \\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+ \\# | \\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 @@ void pythonGrammarResourceIsOnClasspath() throws Exception { 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 { + + 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 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()); + } +}