diff --git a/language-textmate/src/main/java/io/github/rosemoe/sora/langs/textmate/TextMateLanguage.java b/language-textmate/src/main/java/io/github/rosemoe/sora/langs/textmate/TextMateLanguage.java index 48d5df178..d7e584d75 100644 --- a/language-textmate/src/main/java/io/github/rosemoe/sora/langs/textmate/TextMateLanguage.java +++ b/language-textmate/src/main/java/io/github/rosemoe/sora/langs/textmate/TextMateLanguage.java @@ -35,7 +35,6 @@ import org.eclipse.tm4e.languageconfiguration.internal.model.LanguageConfiguration; import java.io.Reader; -import java.util.Objects; import io.github.rosemoe.sora.lang.EmptyLanguage; import io.github.rosemoe.sora.lang.analysis.AnalyzeManager; @@ -51,7 +50,6 @@ import io.github.rosemoe.sora.text.CharPosition; import io.github.rosemoe.sora.text.ContentReference; import io.github.rosemoe.sora.util.MyCharacter; -import io.github.rosemoe.sora.widget.SymbolPairMatch; public class TextMateLanguage extends EmptyLanguage { @@ -237,7 +235,10 @@ public void updateLanguage(GrammarDefinition grammarDefinition) { @NonNull @Override public AnalyzeManager getAnalyzeManager() { - return Objects.requireNonNullElse(textMateAnalyzer, EmptyAnalyzeManager.INSTANCE); + if (textMateAnalyzer == null) { + return EmptyAnalyzeManager.INSTANCE; + } + return textMateAnalyzer; } @Override diff --git a/language-textmate/src/main/java/org/eclipse/tm4e/core/internal/grammar/LineTokens.java b/language-textmate/src/main/java/org/eclipse/tm4e/core/internal/grammar/LineTokens.java index 8d2c818df..2de3f45df 100644 --- a/language-textmate/src/main/java/org/eclipse/tm4e/core/internal/grammar/LineTokens.java +++ b/language-textmate/src/main/java/org/eclipse/tm4e/core/internal/grammar/LineTokens.java @@ -227,7 +227,7 @@ IToken[] getResult(final StateStack stack, final int lineLength) { this._tokens.getLast().setStartIndex(0); } - return this._tokens.toArray(IToken[]::new); + return this._tokens.toArray(new IToken[0]); } int[] getBinaryResult(final StateStack stack, final int lineLength) { diff --git a/language-textmate/src/main/java/org/eclipse/tm4e/core/internal/rule/RuleFactory.java b/language-textmate/src/main/java/org/eclipse/tm4e/core/internal/rule/RuleFactory.java index 3641ea74d..eb34bccc9 100644 --- a/language-textmate/src/main/java/org/eclipse/tm4e/core/internal/rule/RuleFactory.java +++ b/language-textmate/src/main/java/org/eclipse/tm4e/core/internal/rule/RuleFactory.java @@ -247,7 +247,7 @@ private static CompilePatternsResult _compilePatterns(@Nullable final Collection } } - return new CompilePatternsResult(r.toArray(RuleId[]::new), patterns.size() != r.size()); + return new CompilePatternsResult(r.toArray(new RuleId[0]), patterns.size() != r.size()); } private RuleFactory() { diff --git a/language-textmate/src/main/java/org/eclipse/tm4e/core/internal/utils/MoreCollections.java b/language-textmate/src/main/java/org/eclipse/tm4e/core/internal/utils/MoreCollections.java index 6ddd96c99..7feca33ae 100644 --- a/language-textmate/src/main/java/org/eclipse/tm4e/core/internal/utils/MoreCollections.java +++ b/language-textmate/src/main/java/org/eclipse/tm4e/core/internal/utils/MoreCollections.java @@ -17,6 +17,7 @@ import java.util.List; import java.util.Objects; import java.util.function.Predicate; +import java.util.stream.Collectors; import org.eclipse.jdt.annotation.Nullable; @@ -110,7 +111,9 @@ public static String toStringWithIndex(final List list) { * @return a new list without null elements */ public static List noNulls(final @Nullable List coll) { - return coll == null || coll.isEmpty() ? Collections.emptyList() : coll.stream().filter(Objects::nonNull).toList(); + return coll == null || coll.isEmpty() ? Collections.emptyList() : coll.stream() + .filter(t -> t != null) + .collect(Collectors.toList()); } private MoreCollections() { diff --git a/language-textmate/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/model/LanguageConfiguration.java b/language-textmate/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/model/LanguageConfiguration.java index d4694aa3e..8918b810b 100644 --- a/language-textmate/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/model/LanguageConfiguration.java +++ b/language-textmate/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/model/LanguageConfiguration.java @@ -3,16 +3,27 @@ * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 * which is available at https://www.eclipse.org/legal/epl-2.0/ - * + *

* SPDX-License-Identifier: EPL-2.0 - * + *

* Contributors: * Lucas Bullen (Red Hat Inc.) - initial API and implementation * Sebastian Thomschke (Vegard IT) - improve JSON parsing tolerance, add indentation rules parsing */ package org.eclipse.tm4e.languageconfiguration.internal.model; -import static org.eclipse.tm4e.core.internal.utils.NullSafetyHelper.*; +import static org.eclipse.tm4e.core.internal.utils.NullSafetyHelper.castNullable; +import static org.eclipse.tm4e.core.internal.utils.NullSafetyHelper.lazyNonNull; + +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.tm4e.languageconfiguration.internal.model.EnterAction.IndentAction; import java.io.BufferedReader; import java.io.Reader; @@ -22,16 +33,6 @@ import java.util.Objects; import java.util.stream.Collectors; -import org.eclipse.jdt.annotation.NonNull; -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.eclipse.tm4e.languageconfiguration.internal.model.EnterAction.IndentAction; - -import com.google.gson.GsonBuilder; -import com.google.gson.JsonDeserializer; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; - import io.github.rosemoe.sora.util.Logger; /** @@ -43,415 +44,416 @@ * @see * github.com/microsoft/vscode/blob/main/src/vs/editor/common/languages/languageConfiguration.ts + * @noinspection Convert2MethodRef */ public class LanguageConfiguration { - private static final Logger log = Logger.instance(LanguageConfiguration.class.getName()); - - private static String removeTrailingCommas(String jsonString) { - /* matches: - * -------------- - * }, - * } - * -------------- - * as well as: - * -------------- - * }, - * // foo - * // bar - * } - * -------------- - */ - return jsonString.replaceAll("(,)(\\s*\\n(\\s*\\/\\/.*\\n)*\\s*[\\]}])", "$2"); - } - - /** - * See JSON format at https://code.visualstudio.com/api/language-extensions/language-configuration-guide - * - * @return an instance of {@link LanguageConfiguration} loaded from the VSCode language-configuration.json file - * reader. - */ - @NonNullByDefault({}) - public static @Nullable LanguageConfiguration load(@NonNull final Reader reader) { - // GSON does not support trailing commas so we have to manually remove them -> maybe better switch to jackson json parser? - final var jsonString = removeTrailingCommas(new BufferedReader(reader).lines().collect(Collectors.joining("\n"))); - final LanguageConfiguration langCfg = new GsonBuilder() - .registerTypeAdapter(String.class, (JsonDeserializer) (json, typeOfT, context) -> { - if (json.isJsonObject()) { - /* for example: - * "wordPattern": { - * "pattern": "...", - * "flags": "..." - * }, - */ - final var jsonObj = json.getAsJsonObject(); - return jsonObj.has("pattern") && jsonObj.get("pattern").isJsonPrimitive() // - ? jsonObj.get("pattern").getAsString() - : null; - } - - /* for example: - * "wordPattern": "...", - */ - return json.getAsString(); - }) - - .registerTypeAdapter(OnEnterRule.class, (JsonDeserializer) (json, typeOfT, context) -> { - if (!json.isJsonObject()) { - return null; - } - - final var jsonObj = json.getAsJsonObject(); - final var beforeText = getAsPattern(jsonObj.get("beforeText")); //$NON-NLS-1$ - if (beforeText == null) { - return null; - } - - final var actionElem = jsonObj.get("action"); //$NON-NLS-1$ - if (actionElem != null && actionElem.isJsonObject()) { - final var actionJsonObj = actionElem.getAsJsonObject(); - final var indentActionString = getAsString(actionJsonObj.get("indent")); //$NON-NLS-1$ - if (indentActionString != null) { - final var afterText = getAsPattern(jsonObj.get("afterText")); //$NON-NLS-1$ - final var previousLineText = getAsPattern(jsonObj.get("previousLineText")); //$NON-NLS-1$ - final var indentAction = IndentAction.get(indentActionString); - final var appendText = getAsString(actionJsonObj.get("appendText")); //$NON-NLS-1$ - final var removeText = getAsInteger(actionJsonObj.get("removeText")); //$NON-NLS-1$ - final var action = new EnterAction(indentAction, appendText, removeText); - return new OnEnterRule(beforeText, afterText, previousLineText, action); - } - } - return null; - }) - - .registerTypeAdapter(CommentRule.class, (JsonDeserializer) (json, typeOfT, context) -> { - if (!json.isJsonObject()) { - return null; - } - - // ex: {"lineComment": "//","blockComment": [ "/*", "*/" ]} - final var jsonObj = json.getAsJsonObject(); - final var lineComment = getAsString(jsonObj.get("lineComment")); //$NON-NLS-1$ - final var blockCommentElem = jsonObj.get("blockComment"); //$NON-NLS-1$ - CharacterPair blockComment = null; - if (blockCommentElem != null && blockCommentElem.isJsonArray()) { - final var blockCommentArray = blockCommentElem.getAsJsonArray(); - if (blockCommentArray.size() == 2) { - final var blockCommentStart = getAsString(blockCommentArray.get(0)); - final var blockCommentEnd = getAsString(blockCommentArray.get(1)); - if (blockCommentStart != null && blockCommentEnd != null) { - blockComment = new CharacterPair(blockCommentStart, blockCommentEnd); - } - } - } - - return lineComment == null && blockComment == null - ? null - : new CommentRule(lineComment, blockComment); - }) - - .registerTypeAdapter(CharacterPair.class, (JsonDeserializer) (json, typeOfT, - context) -> { - if (!json.isJsonArray()) { - return null; - } - - // ex: ["{","}"] - final var charsPair = json.getAsJsonArray(); - if (charsPair.size() != 2) { - return null; - } - - final var open = getAsString(charsPair.get(0)); - final var close = getAsString(charsPair.get(1)); - - return open == null || close == null - ? null - : new CharacterPair(open, close); - }) - - .registerTypeAdapter(AutoClosingPair.class, (JsonDeserializer) (json, typeOfT, - context) -> { - String open = null; - String close = null; - if (json.isJsonArray()) { - // ex: ["{","}"] - final var charsPair = json.getAsJsonArray(); - if (charsPair.size() != 2) { - return null; - } - open = getAsString(charsPair.get(0)); - close = getAsString(charsPair.get(1)); - } else if (json.isJsonObject()) { - // ex: {"open":"'","close":"'"} - final var autoClosePair = json.getAsJsonObject(); - open = getAsString(autoClosePair.get("open")); //$NON-NLS-1$ - close = getAsString(autoClosePair.get("close")); //$NON-NLS-1$ - } - - return open == null || close == null - ? null - : new AutoClosingPair(open, close); - }) - - .registerTypeAdapter(AutoClosingPairConditional.class, (JsonDeserializer) ( - json, typeOfT, context) -> { - final var notInList = new ArrayList(2); - String open = null; - String close = null; - if (json.isJsonArray()) { - // ex: ["{","}"] - final var charsPair = json.getAsJsonArray(); - if (charsPair.size() != 2) { - return null; - } - open = getAsString(charsPair.get(0)); - close = getAsString(charsPair.get(1)); - } else if (json.isJsonObject()) { - // ex: {"open":"'","close":"'", "notIn": ["string", "comment"]} - final var autoClosePair = json.getAsJsonObject(); - open = getAsString(autoClosePair.get("open")); //$NON-NLS-1$ - close = getAsString(autoClosePair.get("close")); //$NON-NLS-1$ - final var notInElem = autoClosePair.get("notIn"); //$NON-NLS-1$ - if (notInElem != null && notInElem.isJsonArray()) { - for (final JsonElement elem : notInElem.getAsJsonArray()) { - final var string = getAsString(elem); - if (string != null) { - notInList.add(string); - } - } - } - } - - return open == null || close == null - ? null - : new AutoClosingPairConditional(open, close, notInList); - }) - - .registerTypeAdapter(FoldingRules.class, (JsonDeserializer) (json, typeOfT, context) -> { - if (!json.isJsonObject()) { - return null; - } - - // ex: {"offSide": true, "markers": {"start": "^\\s*/", "end": "^\\s*"}} - final var jsonObj = json.getAsJsonObject(); - final var markersElem = jsonObj.get("markers"); //$NON-NLS-1$ - if (markersElem != null && markersElem.isJsonObject()) { - final var markersObj = markersElem.getAsJsonObject(); - final var startMarker = getAsPattern(markersObj.get("start")); //$NON-NLS-1$ - final var endMarker = getAsPattern(markersObj.get("end")); //$NON-NLS-1$ - if (startMarker != null && endMarker != null) { - final var offSide = getAsBoolean(jsonObj.get("offSide"), false); //$NON-NLS-1$ - return new FoldingRules(offSide, startMarker, endMarker); - } - } - return null; - }) - - .registerTypeAdapter(IndentationRules.class, (JsonDeserializer) (json, typeT, context) -> { - if (!json.isJsonObject()) { - return null; - } - - final var jsonObj = json.getAsJsonObject(); - final var decreaseIndentPattern = getAsPattern(jsonObj.get("decreaseIndentPattern")); - if (decreaseIndentPattern == null) - return null; - final var increaseIndentPattern = getAsPattern(jsonObj.get("increaseIndentPattern")); - if (increaseIndentPattern == null) - return null; - - return new IndentationRules( - decreaseIndentPattern, - increaseIndentPattern, - getAsPattern(jsonObj.get("indentNextLinePattern")), - getAsPattern(jsonObj.get("unIndentedLinePattern"))); - }) - .create() - .fromJson(jsonString, LanguageConfiguration.class); - - if (castNullable(langCfg.autoClosingPairs) == null) { - langCfg.autoClosingPairs = Collections.emptyList(); - } else { - langCfg.autoClosingPairs.removeIf(Objects::isNull); - } - - if (castNullable(langCfg.brackets) == null) { - langCfg.brackets = Collections.emptyList(); - } else { - langCfg.brackets.removeIf(Objects::isNull); - } - - if (castNullable(langCfg.onEnterRules) == null) { - langCfg.onEnterRules = Collections.emptyList(); - } else { - langCfg.onEnterRules.removeIf(Objects::isNull); - } - - if (castNullable(langCfg.surroundingPairs) == null) { - langCfg.surroundingPairs = Collections.emptyList(); - } else { - langCfg.surroundingPairs.removeIf(Objects::isNull); - } - - if (castNullable(langCfg.colorizedBracketPairs) == null) { - langCfg.colorizedBracketPairs = Collections.emptyList(); - } else { - langCfg.colorizedBracketPairs.removeIf(Objects::isNull); - } - return langCfg; - } - - private static @Nullable RegExPattern getAsPattern(@Nullable final JsonElement element) { - if (element == null) { - return null; - } - if (element.isJsonObject()) { - // ex : { "pattern": "^<\\/([_:\\w][_:\\w-.\\d]*)\\s*>", "flags": "i" } - final var pattern = getAsString(((JsonObject) element).get("pattern")); - if (pattern == null) { - return null; - } - final var flags = getAsString(((JsonObject) element).get("flags")); - //return flags != null ? pattern + "(?" + flags + ")" : pattern; - return RegExPattern.of(pattern, flags); - } - // ex : "^<\\/([_:\\w][_:\\w-.\\d]*)\\s*>" - return RegExPattern.ofNullable(getAsString(element), null); - } - - private static @Nullable String getAsString(@Nullable final JsonElement element) { - if (element != null) - try { - return element.getAsString(); - } catch (final Exception ex) { - log.e("Failed to convert JSON element [" + element + "] to String.", ex); - } - return null; - } - - private static boolean getAsBoolean(@Nullable final JsonElement element, final boolean defaultValue) { - if (element != null) - try { - return element.getAsBoolean(); - } catch (final Exception ex) { - log.e("Failed to convert JSON element [" + element + "] to boolean.", ex); - } - return defaultValue; - } - - private static @Nullable Integer getAsInteger(@Nullable final JsonElement element) { - if (element != null) - try { - return element.getAsInt(); - } catch (final Exception ex) { - log.e("Failed to convert JSON element [" + element + "] to Integer.", ex); - } - return null; - } - - private @Nullable CommentRule comments; - - /** - * Returns the language's comments. The comments are used by {@link AutoClosingPairConditional} when - * notIn contains comment - * - * @return the language's comments. - */ - public @Nullable CommentRule getComments() { - return comments; - } - - private List brackets = lazyNonNull(); - - /** - * Returns the language's brackets. This configuration implicitly affects pressing Enter around these brackets. - * - * @return the language's brackets - */ - public List getBrackets() { - return brackets; - } - - private @Nullable String wordPattern; - - /** - * Returns the language's definition of a word. This is the regex used when referring to a word. - * - * @return the language's word pattern. - */ - public @Nullable String getWordPattern() { - return wordPattern; - } - - private @Nullable IndentationRules indentationRules; - - /** - * The language's indentation settings. - */ - public @Nullable IndentationRules getIndentationRules() { - return indentationRules; - } - - private List onEnterRules = lazyNonNull(); - - /** - * Returns the language's rules to be evaluated when pressing Enter. - * - * @return the language's rules to be evaluated when pressing Enter. - */ - public List getOnEnterRules() { - return onEnterRules; - } - - private List autoClosingPairs = lazyNonNull(); - - /** - * Returns the language's auto closing pairs. The 'close' character is automatically inserted with the 'open' - * character is typed. If not set, the configured brackets will be used. - * - * @return the language's auto closing pairs. - */ - public List getAutoClosingPairs() { - return autoClosingPairs; - } - - private List surroundingPairs = lazyNonNull(); - - /** - * Returns the language's surrounding pairs. When the 'open' character is typed on a selection, the selected string - * is surrounded by the open and close characters. If not set, the autoclosing pairs settings will be used. - * - * @return the language's surrounding pairs. - */ - public List getSurroundingPairs() { - return surroundingPairs; - } - - private List colorizedBracketPairs = lazyNonNull(); - - /** - * Defines a list of bracket pairs that are colorized depending on their nesting level. - * If not set, the configured brackets will be used. - */ - public List getColorizedBracketPairs() { - return colorizedBracketPairs; - } - - private @Nullable String autoCloseBefore; - - public @Nullable String getAutoCloseBefore() { - return autoCloseBefore; - } - - private @Nullable FoldingRules folding; - - /** - * Returns the language's folding rules. - * - * @return the language's folding rules. - */ - public @Nullable FoldingRules getFolding() { - return folding; - } + private static final Logger log = Logger.instance(LanguageConfiguration.class.getName()); + + private static String removeTrailingCommas(String jsonString) { + /* matches: + * -------------- + * }, + * } + * -------------- + * as well as: + * -------------- + * }, + * // foo + * // bar + * } + * -------------- + */ + return jsonString.replaceAll("(,)(\\s*\\n(\\s*\\/\\/.*\\n)*\\s*[\\]}])", "$2"); + } + + /** + * See JSON format at https://code.visualstudio.com/api/language-extensions/language-configuration-guide + * + * @return an instance of {@link LanguageConfiguration} loaded from the VSCode language-configuration.json file + * reader. + */ + @NonNullByDefault({}) + public static @Nullable LanguageConfiguration load(@NonNull final Reader reader) { + // GSON does not support trailing commas so we have to manually remove them -> maybe better switch to jackson json parser? + final var jsonString = removeTrailingCommas(new BufferedReader(reader).lines().collect(Collectors.joining("\n"))); + final LanguageConfiguration langCfg = new GsonBuilder() + .registerTypeAdapter(String.class, (JsonDeserializer) (json, typeOfT, context) -> { + if (json.isJsonObject()) { + /* for example: + * "wordPattern": { + * "pattern": "...", + * "flags": "..." + * }, + */ + final var jsonObj = json.getAsJsonObject(); + return jsonObj.has("pattern") && jsonObj.get("pattern").isJsonPrimitive() // + ? jsonObj.get("pattern").getAsString() + : null; + } + + /* for example: + * "wordPattern": "...", + */ + return json.getAsString(); + }) + + .registerTypeAdapter(OnEnterRule.class, (JsonDeserializer) (json, typeOfT, context) -> { + if (!json.isJsonObject()) { + return null; + } + + final var jsonObj = json.getAsJsonObject(); + final var beforeText = getAsPattern(jsonObj.get("beforeText")); //$NON-NLS-1$ + if (beforeText == null) { + return null; + } + + final var actionElem = jsonObj.get("action"); //$NON-NLS-1$ + if (actionElem != null && actionElem.isJsonObject()) { + final var actionJsonObj = actionElem.getAsJsonObject(); + final var indentActionString = getAsString(actionJsonObj.get("indent")); //$NON-NLS-1$ + if (indentActionString != null) { + final var afterText = getAsPattern(jsonObj.get("afterText")); //$NON-NLS-1$ + final var previousLineText = getAsPattern(jsonObj.get("previousLineText")); //$NON-NLS-1$ + final var indentAction = IndentAction.get(indentActionString); + final var appendText = getAsString(actionJsonObj.get("appendText")); //$NON-NLS-1$ + final var removeText = getAsInteger(actionJsonObj.get("removeText")); //$NON-NLS-1$ + final var action = new EnterAction(indentAction, appendText, removeText); + return new OnEnterRule(beforeText, afterText, previousLineText, action); + } + } + return null; + }) + + .registerTypeAdapter(CommentRule.class, (JsonDeserializer) (json, typeOfT, context) -> { + if (!json.isJsonObject()) { + return null; + } + + // ex: {"lineComment": "//","blockComment": [ "/*", "*/" ]} + final var jsonObj = json.getAsJsonObject(); + final var lineComment = getAsString(jsonObj.get("lineComment")); //$NON-NLS-1$ + final var blockCommentElem = jsonObj.get("blockComment"); //$NON-NLS-1$ + CharacterPair blockComment = null; + if (blockCommentElem != null && blockCommentElem.isJsonArray()) { + final var blockCommentArray = blockCommentElem.getAsJsonArray(); + if (blockCommentArray.size() == 2) { + final var blockCommentStart = getAsString(blockCommentArray.get(0)); + final var blockCommentEnd = getAsString(blockCommentArray.get(1)); + if (blockCommentStart != null && blockCommentEnd != null) { + blockComment = new CharacterPair(blockCommentStart, blockCommentEnd); + } + } + } + + return lineComment == null && blockComment == null + ? null + : new CommentRule(lineComment, blockComment); + }) + + .registerTypeAdapter(CharacterPair.class, (JsonDeserializer) (json, typeOfT, + context) -> { + if (!json.isJsonArray()) { + return null; + } + + // ex: ["{","}"] + final var charsPair = json.getAsJsonArray(); + if (charsPair.size() != 2) { + return null; + } + + final var open = getAsString(charsPair.get(0)); + final var close = getAsString(charsPair.get(1)); + + return open == null || close == null + ? null + : new CharacterPair(open, close); + }) + + .registerTypeAdapter(AutoClosingPair.class, (JsonDeserializer) (json, typeOfT, + context) -> { + String open = null; + String close = null; + if (json.isJsonArray()) { + // ex: ["{","}"] + final var charsPair = json.getAsJsonArray(); + if (charsPair.size() != 2) { + return null; + } + open = getAsString(charsPair.get(0)); + close = getAsString(charsPair.get(1)); + } else if (json.isJsonObject()) { + // ex: {"open":"'","close":"'"} + final var autoClosePair = json.getAsJsonObject(); + open = getAsString(autoClosePair.get("open")); //$NON-NLS-1$ + close = getAsString(autoClosePair.get("close")); //$NON-NLS-1$ + } + + return open == null || close == null + ? null + : new AutoClosingPair(open, close); + }) + + .registerTypeAdapter(AutoClosingPairConditional.class, (JsonDeserializer) ( + json, typeOfT, context) -> { + final var notInList = new ArrayList(2); + String open = null; + String close = null; + if (json.isJsonArray()) { + // ex: ["{","}"] + final var charsPair = json.getAsJsonArray(); + if (charsPair.size() != 2) { + return null; + } + open = getAsString(charsPair.get(0)); + close = getAsString(charsPair.get(1)); + } else if (json.isJsonObject()) { + // ex: {"open":"'","close":"'", "notIn": ["string", "comment"]} + final var autoClosePair = json.getAsJsonObject(); + open = getAsString(autoClosePair.get("open")); //$NON-NLS-1$ + close = getAsString(autoClosePair.get("close")); //$NON-NLS-1$ + final var notInElem = autoClosePair.get("notIn"); //$NON-NLS-1$ + if (notInElem != null && notInElem.isJsonArray()) { + for (final JsonElement elem : notInElem.getAsJsonArray()) { + final var string = getAsString(elem); + if (string != null) { + notInList.add(string); + } + } + } + } + + return open == null || close == null + ? null + : new AutoClosingPairConditional(open, close, notInList); + }) + + .registerTypeAdapter(FoldingRules.class, (JsonDeserializer) (json, typeOfT, context) -> { + if (!json.isJsonObject()) { + return null; + } + + // ex: {"offSide": true, "markers": {"start": "^\\s*/", "end": "^\\s*"}} + final var jsonObj = json.getAsJsonObject(); + final var markersElem = jsonObj.get("markers"); //$NON-NLS-1$ + if (markersElem != null && markersElem.isJsonObject()) { + final var markersObj = markersElem.getAsJsonObject(); + final var startMarker = getAsPattern(markersObj.get("start")); //$NON-NLS-1$ + final var endMarker = getAsPattern(markersObj.get("end")); //$NON-NLS-1$ + if (startMarker != null && endMarker != null) { + final var offSide = getAsBoolean(jsonObj.get("offSide"), false); //$NON-NLS-1$ + return new FoldingRules(offSide, startMarker, endMarker); + } + } + return null; + }) + + .registerTypeAdapter(IndentationRules.class, (JsonDeserializer) (json, typeT, context) -> { + if (!json.isJsonObject()) { + return null; + } + + final var jsonObj = json.getAsJsonObject(); + final var decreaseIndentPattern = getAsPattern(jsonObj.get("decreaseIndentPattern")); + if (decreaseIndentPattern == null) + return null; + final var increaseIndentPattern = getAsPattern(jsonObj.get("increaseIndentPattern")); + if (increaseIndentPattern == null) + return null; + + return new IndentationRules( + decreaseIndentPattern, + increaseIndentPattern, + getAsPattern(jsonObj.get("indentNextLinePattern")), + getAsPattern(jsonObj.get("unIndentedLinePattern"))); + }) + .create() + .fromJson(jsonString, LanguageConfiguration.class); + + if (castNullable(langCfg.autoClosingPairs) == null) { + langCfg.autoClosingPairs = Collections.emptyList(); + } else { + langCfg.autoClosingPairs.removeIf(t -> t == null); + } + + if (castNullable(langCfg.brackets) == null) { + langCfg.brackets = Collections.emptyList(); + } else { + langCfg.brackets.removeIf(t -> t == null); + } + + if (castNullable(langCfg.onEnterRules) == null) { + langCfg.onEnterRules = Collections.emptyList(); + } else { + langCfg.onEnterRules.removeIf(t -> t == null); + } + + if (castNullable(langCfg.surroundingPairs) == null) { + langCfg.surroundingPairs = Collections.emptyList(); + } else { + langCfg.surroundingPairs.removeIf(t -> t == null); + } + + if (castNullable(langCfg.colorizedBracketPairs) == null) { + langCfg.colorizedBracketPairs = Collections.emptyList(); + } else { + langCfg.colorizedBracketPairs.removeIf(t -> t == null); + } + return langCfg; + } + + private static @Nullable RegExPattern getAsPattern(@Nullable final JsonElement element) { + if (element == null) { + return null; + } + if (element.isJsonObject()) { + // ex : { "pattern": "^<\\/([_:\\w][_:\\w-.\\d]*)\\s*>", "flags": "i" } + final var pattern = getAsString(((JsonObject) element).get("pattern")); + if (pattern == null) { + return null; + } + final var flags = getAsString(((JsonObject) element).get("flags")); + //return flags != null ? pattern + "(?" + flags + ")" : pattern; + return RegExPattern.of(pattern, flags); + } + // ex : "^<\\/([_:\\w][_:\\w-.\\d]*)\\s*>" + return RegExPattern.ofNullable(getAsString(element), null); + } + + private static @Nullable String getAsString(@Nullable final JsonElement element) { + if (element != null) + try { + return element.getAsString(); + } catch (final Exception ex) { + log.e("Failed to convert JSON element [" + element + "] to String.", ex); + } + return null; + } + + private static boolean getAsBoolean(@Nullable final JsonElement element, final boolean defaultValue) { + if (element != null) + try { + return element.getAsBoolean(); + } catch (final Exception ex) { + log.e("Failed to convert JSON element [" + element + "] to boolean.", ex); + } + return defaultValue; + } + + private static @Nullable Integer getAsInteger(@Nullable final JsonElement element) { + if (element != null) + try { + return element.getAsInt(); + } catch (final Exception ex) { + log.e("Failed to convert JSON element [" + element + "] to Integer.", ex); + } + return null; + } + + private @Nullable CommentRule comments; + + /** + * Returns the language's comments. The comments are used by {@link AutoClosingPairConditional} when + * notIn contains comment + * + * @return the language's comments. + */ + public @Nullable CommentRule getComments() { + return comments; + } + + private List brackets = lazyNonNull(); + + /** + * Returns the language's brackets. This configuration implicitly affects pressing Enter around these brackets. + * + * @return the language's brackets + */ + public List getBrackets() { + return brackets; + } + + private @Nullable String wordPattern; + + /** + * Returns the language's definition of a word. This is the regex used when referring to a word. + * + * @return the language's word pattern. + */ + public @Nullable String getWordPattern() { + return wordPattern; + } + + private @Nullable IndentationRules indentationRules; + + /** + * The language's indentation settings. + */ + public @Nullable IndentationRules getIndentationRules() { + return indentationRules; + } + + private List onEnterRules = lazyNonNull(); + + /** + * Returns the language's rules to be evaluated when pressing Enter. + * + * @return the language's rules to be evaluated when pressing Enter. + */ + public List getOnEnterRules() { + return onEnterRules; + } + + private List autoClosingPairs = lazyNonNull(); + + /** + * Returns the language's auto closing pairs. The 'close' character is automatically inserted with the 'open' + * character is typed. If not set, the configured brackets will be used. + * + * @return the language's auto closing pairs. + */ + public List getAutoClosingPairs() { + return autoClosingPairs; + } + + private List surroundingPairs = lazyNonNull(); + + /** + * Returns the language's surrounding pairs. When the 'open' character is typed on a selection, the selected string + * is surrounded by the open and close characters. If not set, the autoclosing pairs settings will be used. + * + * @return the language's surrounding pairs. + */ + public List getSurroundingPairs() { + return surroundingPairs; + } + + private List colorizedBracketPairs = lazyNonNull(); + + /** + * Defines a list of bracket pairs that are colorized depending on their nesting level. + * If not set, the configured brackets will be used. + */ + public List getColorizedBracketPairs() { + return colorizedBracketPairs; + } + + private @Nullable String autoCloseBefore; + + public @Nullable String getAutoCloseBefore() { + return autoCloseBefore; + } + + private @Nullable FoldingRules folding; + + /** + * Returns the language's folding rules. + * + * @return the language's folding rules. + */ + public @Nullable FoldingRules getFolding() { + return folding; + } } diff --git a/language-textmate/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/supports/CharacterPairSupport.java b/language-textmate/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/supports/CharacterPairSupport.java index 4dad428db..649a143ad 100644 --- a/language-textmate/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/supports/CharacterPairSupport.java +++ b/language-textmate/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/supports/CharacterPairSupport.java @@ -18,6 +18,7 @@ import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.tm4e.languageconfiguration.internal.model.AutoClosingPair; @@ -50,7 +51,7 @@ public CharacterPairSupport(final LanguageConfiguration config) { if (!brackets.isEmpty()) { this.autoClosingPairs = brackets.stream() .map(el -> new AutoClosingPairConditional(el.open, el.close, Collections.emptyList())) - .toList(); + .collect(Collectors.toList()); } else { this.autoClosingPairs = Collections.emptyList(); } diff --git a/language-textmate/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/supports/OnEnterSupport.java b/language-textmate/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/supports/OnEnterSupport.java index f4b0830c9..db801b2e1 100644 --- a/language-textmate/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/supports/OnEnterSupport.java +++ b/language-textmate/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/supports/OnEnterSupport.java @@ -3,21 +3,15 @@ * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 * which is available at https://www.eclipse.org/legal/epl-2.0/ - * + *

* SPDX-License-Identifier: EPL-2.0 - * + *

* Contributors: * Angelo Zerr - initial API and implementation * Sebastian Thomschke (Vegard IT GmbH) - add previousLineText support */ package org.eclipse.tm4e.languageconfiguration.internal.supports; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - import org.eclipse.jdt.annotation.Nullable; import org.eclipse.tm4e.languageconfiguration.internal.model.CharacterPair; import org.eclipse.tm4e.languageconfiguration.internal.model.EnterAction; @@ -25,6 +19,11 @@ import org.eclipse.tm4e.languageconfiguration.internal.model.OnEnterRule; import org.eclipse.tm4e.languageconfiguration.internal.utils.RegExpUtils; +import java.util.Collections; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + /** * On enter support. * @@ -33,107 +32,107 @@ */ public class OnEnterSupport { - private static final List DEFAULT_BRACKETS = List.of( - new CharacterPair("(", ")"), //$NON-NLS-1$ //$NON-NLS-2$ - new CharacterPair("{", "}"), //$NON-NLS-1$ //$NON-NLS-2$ - new CharacterPair("[", "]")); //$NON-NLS-1$ //$NON-NLS-2$ - - private final List brackets; - private final List regExpRules; - - public OnEnterSupport(@Nullable final List brackets, @Nullable final List regExpRules) { - this.brackets = (brackets != null ? brackets : DEFAULT_BRACKETS) - .stream() - .filter(Objects::nonNull) - .map(ProcessedBracketPair::new) - .collect(Collectors.toList()); - - this.regExpRules = regExpRules != null ? regExpRules : Collections.emptyList(); - } - - public @Nullable EnterAction onEnter( - // TODO autoIndent: EditorAutoIndentStrategy, - final String previousLineText, - final String beforeEnterText, - final String afterEnterText) { - // (1): `regExpRules` - // if (autoIndent >= EditorAutoIndentStrategy.Advanced) { - for (final OnEnterRule rule : regExpRules) { - if (!rule.beforeText.matchesPartially(beforeEnterText)) - continue; - - final var afterTextPattern = rule.afterText; - if (afterTextPattern != null && !afterTextPattern.matchesPartially(afterEnterText)) - continue; - - final var previousLinePattern = rule.previousLineText; - if (previousLinePattern != null && !previousLinePattern.matchesPartially(previousLineText)) - continue; - - return rule.action; - } - - // (2): Special indent-outdent - // if (autoIndent >= EditorAutoIndentStrategy.Brackets) { - if (!beforeEnterText.isEmpty() && !afterEnterText.isEmpty()) { - for (final ProcessedBracketPair bracket : brackets) { - if (bracket.matchOpen(beforeEnterText) && bracket.matchClose(afterEnterText)) { - return new EnterAction(IndentAction.IndentOutdent); - } - } - } - - // (3): Open bracket based logic - // if (autoIndent >= EditorAutoIndentStrategy.Brackets) { - if (!beforeEnterText.isEmpty()) { - for (final ProcessedBracketPair bracket : brackets) { - if (bracket.matchOpen(beforeEnterText)) { - return new EnterAction(IndentAction.Indent); - } - } - } - - return null; - } - - private static final class ProcessedBracketPair { - - private static final Pattern B_REGEXP = Pattern.compile("\\B"); //$NON-NLS-1$ - - private final @Nullable Pattern openRegExp; - private final @Nullable Pattern closeRegExp; - - private ProcessedBracketPair(final CharacterPair charPair) { - openRegExp = createOpenBracketRegExp(charPair.open); - closeRegExp = createCloseBracketRegExp(charPair.close); - } - - private boolean matchOpen(final String beforeEnterText) { - return openRegExp != null && openRegExp.matcher(beforeEnterText).find(); - } - - private boolean matchClose(final String afterEnterText) { - return closeRegExp != null && closeRegExp.matcher(afterEnterText).find(); - } - - private static @Nullable Pattern createOpenBracketRegExp(final String bracket) { - final var str = new StringBuilder(RegExpUtils.escapeRegExpCharacters(bracket)); - final var c = String.valueOf(str.charAt(0)); - if (!B_REGEXP.matcher(c).find()) { - str.insert(0, "\\b"); //$NON-NLS-1$ - } - str.append("\\s*$"); //$NON-NLS-1$ - return RegExpUtils.create(str.toString()); - } - - private static @Nullable Pattern createCloseBracketRegExp(final String bracket) { - final var str = new StringBuilder(RegExpUtils.escapeRegExpCharacters(bracket)); - final var c = String.valueOf(str.charAt(str.length() - 1)); - if (!B_REGEXP.matcher(c).find()) { - str.append("\\b"); //$NON-NLS-1$ - } - str.insert(0, "^\\s*"); //$NON-NLS-1$ - return RegExpUtils.create(str.toString()); - } - } + private static final List DEFAULT_BRACKETS = List.of( + new CharacterPair("(", ")"), //$NON-NLS-1$ //$NON-NLS-2$ + new CharacterPair("{", "}"), //$NON-NLS-1$ //$NON-NLS-2$ + new CharacterPair("[", "]")); //$NON-NLS-1$ //$NON-NLS-2$ + + private final List brackets; + private final List regExpRules; + + public OnEnterSupport(@Nullable final List brackets, @Nullable final List regExpRules) { + this.brackets = (brackets != null ? brackets : DEFAULT_BRACKETS) + .stream() + .filter(t -> t != null) + .map(ProcessedBracketPair::new) + .collect(Collectors.toList()); + + this.regExpRules = regExpRules != null ? regExpRules : Collections.emptyList(); + } + + public @Nullable EnterAction onEnter( + // TODO autoIndent: EditorAutoIndentStrategy, + final String previousLineText, + final String beforeEnterText, + final String afterEnterText) { + // (1): `regExpRules` + // if (autoIndent >= EditorAutoIndentStrategy.Advanced) { + for (final OnEnterRule rule : regExpRules) { + if (!rule.beforeText.matchesPartially(beforeEnterText)) + continue; + + final var afterTextPattern = rule.afterText; + if (afterTextPattern != null && !afterTextPattern.matchesPartially(afterEnterText)) + continue; + + final var previousLinePattern = rule.previousLineText; + if (previousLinePattern != null && !previousLinePattern.matchesPartially(previousLineText)) + continue; + + return rule.action; + } + + // (2): Special indent-outdent + // if (autoIndent >= EditorAutoIndentStrategy.Brackets) { + if (!beforeEnterText.isEmpty() && !afterEnterText.isEmpty()) { + for (final ProcessedBracketPair bracket : brackets) { + if (bracket.matchOpen(beforeEnterText) && bracket.matchClose(afterEnterText)) { + return new EnterAction(IndentAction.IndentOutdent); + } + } + } + + // (3): Open bracket based logic + // if (autoIndent >= EditorAutoIndentStrategy.Brackets) { + if (!beforeEnterText.isEmpty()) { + for (final ProcessedBracketPair bracket : brackets) { + if (bracket.matchOpen(beforeEnterText)) { + return new EnterAction(IndentAction.Indent); + } + } + } + + return null; + } + + private static final class ProcessedBracketPair { + + private static final Pattern B_REGEXP = Pattern.compile("\\B"); //$NON-NLS-1$ + + private final @Nullable Pattern openRegExp; + private final @Nullable Pattern closeRegExp; + + private ProcessedBracketPair(final CharacterPair charPair) { + openRegExp = createOpenBracketRegExp(charPair.open); + closeRegExp = createCloseBracketRegExp(charPair.close); + } + + private boolean matchOpen(final String beforeEnterText) { + return openRegExp != null && openRegExp.matcher(beforeEnterText).find(); + } + + private boolean matchClose(final String afterEnterText) { + return closeRegExp != null && closeRegExp.matcher(afterEnterText).find(); + } + + private static @Nullable Pattern createOpenBracketRegExp(final String bracket) { + final var str = new StringBuilder(RegExpUtils.escapeRegExpCharacters(bracket)); + final var c = String.valueOf(str.charAt(0)); + if (!B_REGEXP.matcher(c).find()) { + str.insert(0, "\\b"); //$NON-NLS-1$ + } + str.append("\\s*$"); //$NON-NLS-1$ + return RegExpUtils.create(str.toString()); + } + + private static @Nullable Pattern createCloseBracketRegExp(final String bracket) { + final var str = new StringBuilder(RegExpUtils.escapeRegExpCharacters(bracket)); + final var c = String.valueOf(str.charAt(str.length() - 1)); + if (!B_REGEXP.matcher(c).find()) { + str.append("\\b"); //$NON-NLS-1$ + } + str.insert(0, "^\\s*"); //$NON-NLS-1$ + return RegExpUtils.create(str.toString()); + } + } }