diff --git a/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/formatting/RosettaFormattingService.java b/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/formatting/RosettaFormattingService.java index 38c5999fd..a1045f4cd 100644 --- a/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/formatting/RosettaFormattingService.java +++ b/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/formatting/RosettaFormattingService.java @@ -24,18 +24,15 @@ import org.eclipse.lsp4j.FormattingOptions; import org.eclipse.lsp4j.TextEdit; -import org.eclipse.xtext.formatting.IIndentationInformation; import org.eclipse.xtext.formatting2.IFormatter2; import org.eclipse.xtext.formatting2.regionaccess.ITextReplacement; import org.eclipse.xtext.ide.server.Document; import org.eclipse.xtext.ide.server.formatting.FormattingService; import org.eclipse.xtext.preferences.ITypedPreferenceValues; -import org.eclipse.xtext.preferences.MapBasedPreferenceValues; import org.eclipse.xtext.resource.XtextResource; import org.eclipse.xtext.util.TextRegion; -import com.google.common.base.Strings; -import com.regnosys.rosetta.formatting2.RosettaFormatterPreferenceKeys; +import com.regnosys.rosetta.formatting2.FormattingOptionsAdaptor; /** * This class allows passing additional formatting parameters as defined in @@ -47,55 +44,17 @@ * - expose injected fields to child classes (make them protected) */ public class RosettaFormattingService extends FormattingService { - public static String PREFERENCE_INDENTATION_KEY = "indentation"; - public static String PREFERENCE_MAX_LINE_WIDTH_KEY = "maxLineWidth"; - public static String PREFERENCE_CONDITIONAL_MAX_LINE_WIDTH_KEY = "conditionalMaxLineWidth"; - @Inject private Provider formatter2Provider; - @Inject - private IIndentationInformation indentationInformation; - - protected ITypedPreferenceValues createPreferences(FormattingOptions options) { - MapBasedPreferenceValues preferences = new MapBasedPreferenceValues(); - - String indent = indentationInformation.getIndentString(); - if (options != null) { - if (options.isInsertSpaces()) { - indent = Strings.padEnd("", options.getTabSize(), ' '); - } - } - preferences.put(PREFERENCE_INDENTATION_KEY, indent); - - if (options == null) { - return preferences; - } + private FormattingOptionsAdaptor formattingOptionsAdapter; - Number conditionalMaxLineWidth = options.getNumber(PREFERENCE_CONDITIONAL_MAX_LINE_WIDTH_KEY); - if (conditionalMaxLineWidth != null) { - preferences.put(RosettaFormatterPreferenceKeys.conditionalMaxLineWidth, conditionalMaxLineWidth.intValue()); - } - Number maxLineWidth = options.getNumber(PREFERENCE_MAX_LINE_WIDTH_KEY); - if (maxLineWidth != null) { - preferences.put(RosettaFormatterPreferenceKeys.maxLineWidth, maxLineWidth.intValue()); - if (conditionalMaxLineWidth == null) { - int defaultConditionalMaxLineWidth = RosettaFormatterPreferenceKeys.conditionalMaxLineWidth.toValue(RosettaFormatterPreferenceKeys.conditionalMaxLineWidth.getDefaultValue()); - int defaultMaxLineWidth = RosettaFormatterPreferenceKeys.maxLineWidth.toValue(RosettaFormatterPreferenceKeys.maxLineWidth.getDefaultValue()); - double defaultRatio = (double)defaultConditionalMaxLineWidth / defaultMaxLineWidth; - preferences.put(RosettaFormatterPreferenceKeys.conditionalMaxLineWidth, (int)(maxLineWidth.doubleValue() * defaultRatio)); - } - } - - return preferences; - } - @Override public List format(XtextResource resource, Document document, int offset, int length, FormattingOptions options) { List result = new ArrayList<>(); if (this.formatter2Provider != null) { - ITypedPreferenceValues preferences = createPreferences(options); + ITypedPreferenceValues preferences = formattingOptionsAdapter.createPreferences(options); List replacements = format2(resource, new TextRegion(offset, length), preferences); for (ITextReplacement r : replacements) { result.add(toTextEdit(document, r.getReplacementText(), r.getOffset(), r.getLength())); diff --git a/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/server/RosettaLanguageServer.java b/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/server/RosettaLanguageServer.java new file mode 100644 index 000000000..6aa54234f --- /dev/null +++ b/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/server/RosettaLanguageServer.java @@ -0,0 +1,13 @@ +package com.regnosys.rosetta.ide.server; + +import java.util.concurrent.CompletableFuture; + +import org.eclipse.lsp4j.FormattingOptions; +import org.eclipse.lsp4j.jsonrpc.services.JsonRequest; +import org.eclipse.lsp4j.services.LanguageServer; + +public interface RosettaLanguageServer extends LanguageServer{ + + @JsonRequest + CompletableFuture getDefaultFormattingOptions(); +} diff --git a/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/server/RosettaLanguageServerImpl.java b/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/server/RosettaLanguageServerImpl.java index 609272052..aad31d7cf 100644 --- a/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/server/RosettaLanguageServerImpl.java +++ b/rosetta-ide/src/main/java/com/regnosys/rosetta/ide/server/RosettaLanguageServerImpl.java @@ -23,20 +23,25 @@ import org.eclipse.xtext.resource.IResourceServiceProvider; import org.eclipse.xtext.util.CancelIndicator; +import com.regnosys.rosetta.formatting2.FormattingOptionsAdaptor; import com.regnosys.rosetta.ide.inlayhints.IInlayHintsResolver; import com.regnosys.rosetta.ide.inlayhints.IInlayHintsService; import com.regnosys.rosetta.ide.semantictokens.ISemanticTokensService; import com.regnosys.rosetta.ide.semantictokens.SemanticToken; +import java.io.IOException; import java.util.Arrays; import java.util.List; import java.util.concurrent.CompletableFuture; +import javax.inject.Inject; + /** * TODO: contribute to Xtext. * */ -public class RosettaLanguageServerImpl extends LanguageServerImpl { +public class RosettaLanguageServerImpl extends LanguageServerImpl implements RosettaLanguageServer{ + @Inject FormattingOptionsAdaptor formattingOptionsAdapter; @Override protected ServerCapabilities createServerCapabilities(InitializeParams params) { @@ -170,4 +175,14 @@ protected SemanticTokens semanticTokensRange(SemanticTokensRangeParams params, C public CompletableFuture semanticTokensRange(SemanticTokensRangeParams params) { return this.getRequestManager().runRead((cancelIndicator) -> this.semanticTokensRange(params, cancelIndicator)); } + + @Override + public CompletableFuture getDefaultFormattingOptions() { + try { + return CompletableFuture.completedFuture(formattingOptionsAdapter.readFormattingOptions(null)); + } catch (IOException e) { + // should never happen, since null path always leads to default options being returned + return CompletableFuture.failedFuture(e); + } + } } \ No newline at end of file diff --git a/rosetta-ide/src/test/java/com/regnosys/rosetta/ide/formatting/FormattingTest.xtend b/rosetta-ide/src/test/java/com/regnosys/rosetta/ide/formatting/FormattingTest.xtend index 9c78185f0..7c20e52f0 100644 --- a/rosetta-ide/src/test/java/com/regnosys/rosetta/ide/formatting/FormattingTest.xtend +++ b/rosetta-ide/src/test/java/com/regnosys/rosetta/ide/formatting/FormattingTest.xtend @@ -3,6 +3,7 @@ package com.regnosys.rosetta.ide.formatting import org.junit.jupiter.api.Test import com.regnosys.rosetta.ide.tests.AbstractRosettaLanguageServerTest import org.eclipse.lsp4j.FormattingOptions +import com.regnosys.rosetta.formatting2.FormattingOptionsAdaptor class FormattingTest extends AbstractRosettaLanguageServerTest { @Test @@ -23,7 +24,7 @@ class FormattingTest extends AbstractRosettaLanguageServerTest { testFormatting( [ val options = new FormattingOptions - options.putNumber(RosettaFormattingService.PREFERENCE_MAX_LINE_WIDTH_KEY, 10) + options.putNumber(FormattingOptionsAdaptor.PREFERENCE_MAX_LINE_WIDTH_KEY, 10) it.options = options ], [ @@ -67,7 +68,7 @@ class FormattingTest extends AbstractRosettaLanguageServerTest { testFormatting( [ val options = new FormattingOptions - options.putNumber(RosettaFormattingService.PREFERENCE_CONDITIONAL_MAX_LINE_WIDTH_KEY, 10) + options.putNumber(FormattingOptionsAdaptor.PREFERENCE_CONDITIONAL_MAX_LINE_WIDTH_KEY, 10) it.options = options ], [ diff --git a/rosetta-lang/model/RosettaExpression.xcore b/rosetta-lang/model/RosettaExpression.xcore index 2d6b2fee9..dac8f7362 100644 --- a/rosetta-lang/model/RosettaExpression.xcore +++ b/rosetta-lang/model/RosettaExpression.xcore @@ -244,6 +244,7 @@ class JoinOperation extends RosettaBinaryOperation { class RosettaOnlyExistsExpression extends RosettaExpression { contains RosettaExpression[] args + boolean hasParentheses } /** diff --git a/rosetta-lang/pom.xml b/rosetta-lang/pom.xml index 8686c0ae5..7321b1f45 100644 --- a/rosetta-lang/pom.xml +++ b/rosetta-lang/pom.xml @@ -71,6 +71,10 @@ com.fasterxml.jackson.dataformat jackson-dataformat-yaml + + org.eclipse.lsp4j + org.eclipse.lsp4j + diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/Rosetta.xtext b/rosetta-lang/src/main/java/com/regnosys/rosetta/Rosetta.xtext index f5a54d05c..76a07fba5 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/Rosetta.xtext +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/Rosetta.xtext @@ -789,7 +789,7 @@ RosettaCalcConditionalExpression returns RosettaExpression: ; RosettaCalcOnlyExists returns RosettaExpression: - {RosettaOnlyExistsExpression} (args+=RosettaOnlyExistsElement | ('(' args+=RosettaOnlyExistsElement (',' args+=RosettaOnlyExistsElement)* ')')) 'only' 'exists' + {RosettaOnlyExistsExpression} (args+=RosettaOnlyExistsElement | (hasParentheses?='(' args+=RosettaOnlyExistsElement (',' args+=RosettaOnlyExistsElement)* ')')) 'only' 'exists' ; RosettaOnlyExistsElement returns RosettaExpression: diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/FormattingOptionsAdaptor.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/FormattingOptionsAdaptor.java new file mode 100644 index 000000000..81a591275 --- /dev/null +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/FormattingOptionsAdaptor.java @@ -0,0 +1,94 @@ +package com.regnosys.rosetta.formatting2; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +import org.eclipse.emf.mwe.core.resources.ResourceLoader; +import org.eclipse.lsp4j.FormattingOptions; +import org.eclipse.xtext.preferences.ITypedPreferenceValues; +import org.eclipse.xtext.preferences.MapBasedPreferenceValues; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Strings; + +public class FormattingOptionsAdaptor { + public static String PREFERENCE_INDENTATION_KEY = "indentation"; + public static String PREFERENCE_MAX_LINE_WIDTH_KEY = "maxLineWidth"; + public static String PREFERENCE_CONDITIONAL_MAX_LINE_WIDTH_KEY = "conditionalMaxLineWidth"; + + private static final String DEFAULT_FORMATTING_OPTIONS_PATH = "default-formatting-options.json"; + + public ITypedPreferenceValues createPreferences(FormattingOptions options) { + MapBasedPreferenceValues preferences = new MapBasedPreferenceValues(); + + String indent = "\t"; + if (options != null) { + if (options.isInsertSpaces()) { + indent = Strings.padEnd("", options.getTabSize(), ' '); + } + } + preferences.put(PREFERENCE_INDENTATION_KEY, indent); + + if (options == null) { + return preferences; + } + + Number conditionalMaxLineWidth = options.getNumber(PREFERENCE_CONDITIONAL_MAX_LINE_WIDTH_KEY); + if (conditionalMaxLineWidth != null) { + preferences.put(RosettaFormatterPreferenceKeys.conditionalMaxLineWidth, conditionalMaxLineWidth.intValue()); + } + Number maxLineWidth = options.getNumber(PREFERENCE_MAX_LINE_WIDTH_KEY); + if (maxLineWidth != null) { + preferences.put(RosettaFormatterPreferenceKeys.maxLineWidth, maxLineWidth.intValue()); + if (conditionalMaxLineWidth == null) { + int defaultConditionalMaxLineWidth = RosettaFormatterPreferenceKeys.conditionalMaxLineWidth + .toValue(RosettaFormatterPreferenceKeys.conditionalMaxLineWidth.getDefaultValue()); + int defaultMaxLineWidth = RosettaFormatterPreferenceKeys.maxLineWidth + .toValue(RosettaFormatterPreferenceKeys.maxLineWidth.getDefaultValue()); + double defaultRatio = (double) defaultConditionalMaxLineWidth / defaultMaxLineWidth; + preferences.put(RosettaFormatterPreferenceKeys.conditionalMaxLineWidth, + (int) (maxLineWidth.doubleValue() * defaultRatio)); + } + } + + return preferences; + } + + public FormattingOptions readFormattingOptions(String optionsPath) throws IOException { + InputStream resourceStream; + // If path not given, use default one + if (optionsPath == null) { + // Retrieve resource as an InputStream + resourceStream = ResourceLoader.class.getClassLoader().getResourceAsStream(DEFAULT_FORMATTING_OPTIONS_PATH); + } else { + resourceStream = new FileInputStream(optionsPath); + } + + // Create an ObjectMapper, read JSON into a Map + ObjectMapper objectMapper = new ObjectMapper(); + Map map = null; + map = objectMapper.readValue(resourceStream, Map.class); + + // Create a FormattingOptions object + FormattingOptions formattingOptions = new FormattingOptions(); + + // Populate the FormattingOptions object + for (Map.Entry entry : map.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + + if (value instanceof String) { + formattingOptions.putString(key, (String) value); + } else if (value instanceof Number) { + formattingOptions.putNumber(key, (Number) value); + } else if (value instanceof Boolean) { + formattingOptions.putBoolean(key, (Boolean) value); + } else { + throw new IllegalArgumentException("Unsupported value type for key: " + key); + } + } + return formattingOptions; + } +} diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/FormattingUtil.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/FormattingUtil.java index 7561cc647..0c21d2316 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/FormattingUtil.java +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/FormattingUtil.java @@ -29,7 +29,7 @@ import org.eclipse.xtext.preferences.TypedPreferenceKey; public class FormattingUtil { - private ITextRegionExtensions getTextRegionExt(IFormattableDocument doc) { + public ITextRegionExtensions getTextRegionExt(IFormattableDocument doc) { return doc.getRequest().getTextRegionAccess().getExtensions(); } diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/IFormattedResourceAcceptor.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/IFormattedResourceAcceptor.java new file mode 100644 index 000000000..f122226fa --- /dev/null +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/IFormattedResourceAcceptor.java @@ -0,0 +1,25 @@ +package com.regnosys.rosetta.formatting2; + +import org.eclipse.emf.ecore.resource.Resource; + +/** + * Functional interface for handling a formatted resource and its corresponding + * formatted text. + *

+ * This interface allows customization of how formatted resources are processed, + * such as saving the content, logging it, or using it for validations or + * assertions in tests. + *

+ */ +@FunctionalInterface +public interface IFormattedResourceAcceptor { + + /** + * Accepts a formatted resource and its formatted content for further + * processing. + * + * @param resource the {@link Resource} that was formatted + * @param formattedText the formatted content as a {@link String} + */ + void accept(Resource resource, String formattedText); +} diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/ResourceFormatterService.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/ResourceFormatterService.java index fd6633330..7f9337d58 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/ResourceFormatterService.java +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/ResourceFormatterService.java @@ -7,64 +7,124 @@ import org.eclipse.xtext.resource.XtextResource; public interface ResourceFormatterService { - + /** - * Formats each {@link XtextResource} in the provided collection in-memory. + * Formats each {@link XtextResource} in the provided collection in-memory and + * passes the formatted content to a handler for further processing. *

- * This method iterates over the given collection of resources and applies formatting - * directly to each resource. Formatting may include indentation, spacing adjustments, - * and other stylistic improvements to ensure consistency and readability of the resources. + * This method iterates over the given collection of resources and applies + * formatting directly to each resource. Formatting may include indentation, + * spacing adjustments, and other stylistic improvements to ensure consistency + * and readability of the resources. *

* - * @param resources a collection of {@link XtextResource} objects to be formatted + *

+ * The handler, represented as a {@link java.util.function.BiConsumer}, is + * called with two arguments: the {@link Resource} being formatted, and the + * resulting formatted text as a {@link String}. This allows the caller to + * specify actions such as saving the formatted content, logging it, or + * collecting it for assertions in a test. + *

+ * + * @param resources a collection of {@link XtextResource} objects to be + * formatted + * @param acceptor an {@link IFormattedResourceAcceptor} to process the + * formatted resource and its content */ - default void formatCollection(Collection resources) { - formatCollection(resources, null); + default void formatCollection(Collection resources, IFormattedResourceAcceptor acceptor) { + formatCollection(resources, null, acceptor); } - + /** - * Formats the given {@link XtextResource} in-memory. + * Formats the given {@link XtextResource} in-memory and passes the formatted + * content to a handler for further processing. *

- * This method applies formatting directly to the specified resource. Formatting can include - * adjustments to indentation, spacing, and other stylistic elements to ensure consistency - * and readability of the resource content. + * This method applies formatting directly to the specified resource. Formatting + * can include adjustments to indentation, spacing, and other stylistic elements + * to ensure consistency and readability of the resource content. + *

+ * + *

+ * The handler, represented as a {@link java.util.function.BiConsumer}, is + * called with two arguments: the {@link Resource} being formatted, and the + * resulting formatted text as a {@link String}. This allows the caller to + * specify actions such as saving the formatted content, logging it, or + * collecting it for assertions in a test. *

* * @param resources the {@link XtextResource} to format - * @param preferenceValues an {@link ITypedPreferenceValues} object containing formatting preferences, - * or {@code null} if no preferences are specified + * @param acceptor an {@link IFormattedResourceAcceptor} to process the + * formatted resource and its content */ - default void formatXtextResource(XtextResource resource) { - formatXtextResource(resource, null); + default void formatXtextResource(XtextResource resource, IFormattedResourceAcceptor acceptor) { + formatXtextResource(resource, null, acceptor); } - + /** - * Formats each {@link XtextResource} in the provided collection in-memory, with specified formatting preferences. + * Formats each {@link XtextResource} in the provided collection in-memory, + * applying specified formatting preferences, and passes the formatted content + * to a handler for further processing. *

- * This method iterates over the given collection of resources and applies formatting - * directly to each resource. Formatting may include indentation, spacing adjustments, - * and other stylistic improvements to ensure consistency and readability of the resources. - * The formatting can be customized based on the specified {@link ITypedPreferenceValues}. - * If no preferences are required, {@code preferenceValues} can be set to {@code null}. + * This method iterates over the given collection of resources, formats each + * resource according to the provided preferences, and invokes a handler to + * process the formatted content. Formatting includes indentation, spacing + * adjustments, and other stylistic refinements to ensure consistency and + * readability of the resources. The formatting behavior can be customized based + * on the provided {@link ITypedPreferenceValues}. + *

+ *

+ * The handler, represented as a {@link java.util.function.BiConsumer}, is + * called for each resource with two arguments: the {@link Resource} being + * formatted, and the resulting formatted text as a {@link String}. This allows + * the caller to specify actions such as saving the formatted content, logging + * it, or collecting it for assertions in a test. + *

+ *

+ * If no formatting preferences are required, the {@code preferenceValues} + * parameter can be set to {@code null}. *

* - * @param resources a collection of {@link XtextResource} objects to be formatted - * @param preferenceValues an {@link ITypedPreferenceValues} object containing formatting preferences, - * or {@code null} if no preferences are specified + * @param resources a collection of {@link XtextResource} objects to be + * formatted + * @param preferenceValues an {@link ITypedPreferenceValues} object containing + * formatting preferences, or {@code null} if no + * preferences are specified + * @param acceptor an {@link IFormattedResourceAcceptor} to process the + * formatted resource and its content */ - void formatCollection(Collection resources, ITypedPreferenceValues preferenceValues); - + void formatCollection(Collection resources, ITypedPreferenceValues preferenceValues, + IFormattedResourceAcceptor acceptor); + /** - * Formats the given {@link XtextResource} in-memory. + * Formats the given {@link XtextResource} in-memory, applying specified + * formatting preferences, and passes the formatted content to a handler for + * further processing. *

- * This method applies formatting directly to the specified resource. Formatting can include - * adjustments to indentation, spacing, and other stylistic elements to ensure consistency - * and readability of the resource content. - * The formatting can be customized based on the specified {@link ITypedPreferenceValues}. - * If no preferences are required, {@code preferenceValues} can be set to {@code null}. + * This method formats each resource according to the provided preferences, and + * invokes a handler to process the formatted content. Formatting includes + * indentation, spacing adjustments, and other stylistic refinements to ensure + * consistency and readability of the resources. The formatting behavior can be + * customized based on the provided {@link ITypedPreferenceValues}. *

- * - * @param resource the {@link XtextResource} to format + *

+ * The handler, represented as a {@link java.util.function.BiConsumer}, is + * called with two arguments: the {@link Resource} being formatted, and the + * resulting formatted text as a {@link String}. This allows the caller to + * specify actions such as saving the formatted content, logging it, or + * collecting it for assertions in a test. + *

+ *

+ * If no formatting preferences are required, the {@code preferenceValues} + * parameter can be set to {@code null}. + *

+ * + * @param resource the {@link XtextResource} to be formatted + * @param preferenceValues an {@link ITypedPreferenceValues} object containing + * formatting preferences, or {@code null} if no + * preferences are specified + * @param acceptor an {@link IFormattedResourceAcceptor} to process the + * formatted resource and its content */ - void formatXtextResource(XtextResource resource, ITypedPreferenceValues preferenceValues); + void formatXtextResource(XtextResource resource, ITypedPreferenceValues preferenceValues, + IFormattedResourceAcceptor acceptor); } diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/RosettaExpressionFormatter.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/RosettaExpressionFormatter.xtend index 5e497bd88..38a88b42b 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/RosettaExpressionFormatter.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/RosettaExpressionFormatter.xtend @@ -31,6 +31,10 @@ import com.regnosys.rosetta.rosetta.expression.RosettaOperation import com.regnosys.rosetta.rosetta.expression.ThenOperation import com.regnosys.rosetta.rosetta.expression.RosettaConstructorExpression import com.regnosys.rosetta.rosetta.expression.RosettaDeepFeatureCall +import org.eclipse.xtext.formatting2.regionaccess.ISemanticRegion +import org.eclipse.emf.ecore.EObject +import org.eclipse.xtext.formatting2.regionaccess.IHiddenRegion +import com.regnosys.rosetta.rosetta.expression.ConstructorKeyValuePair class RosettaExpressionFormatter extends AbstractRosettaFormatter2 { @@ -114,38 +118,138 @@ class RosettaExpressionFormatter extends AbstractRosettaFormatter2 { private def dispatch void unsafeFormatExpression(RosettaConstructorExpression expr, extension IFormattableDocument document, FormattingMode mode) { val extension constructorGrammarAccess = rosettaCalcConstructorExpressionAccess - - interior( + + interiorIndentWithoutCurlyBracket( expr.regionFor.keyword(leftCurlyBracketKeyword_2) .prepend[oneSpace] .append[newLine], - expr.regionFor.keyword(rightCurlyBracketKeyword_4) - .prepend[newLine], - [indent] + expr.regionFor.keyword(rightCurlyBracketKeyword_4), + document ) - expr.regionFor.keywords(',').forEach[ - prepend[noSpace] - append[newLine] + val rightCurlyBracketRegion = expr.regionFor.keyword(rightCurlyBracketKeyword_4) + rightCurlyBracketRegion.prepend [ + if(rightCurlyBracketRegion.comesAfter("}") // case '}}' + || (rightCurlyBracketRegion.comesAfter(",") && + rightCurlyBracketRegion.previousSemanticRegion.comesAfter("}")) // case '},}' + ) noSpace else newLine + ] + + expr.regionFor.keywords(',').forEach [ valueExpr | + valueExpr.prepend[noSpace] + if (valueExpr.nextSemanticRegion.text == "}") { + valueExpr.append[noSpace] + } else { + valueExpr.append[newLine] + } ] expr.values.forEach[ - indentInner(document) - regionFor.keyword(':'). - prepend[noSpace] + regionFor.keyword(':') + .prepend[noSpace] .append[oneSpace] value.formatExpression(document, mode) ] } + def comesAfter(ISemanticRegion region, String el) { + if (region !== null && region.previousSemanticRegion !== null) { + val prevRegionElement = region.previousSemanticRegion.text + prevRegionElement == el + } else + false + } + + def comesBefore(ISemanticRegion region, String el) { + if (region !== null && region.nextSemanticRegion !== null) { + val nextRegionElement = region.nextSemanticRegion.text + nextRegionElement == el + } else + false + } + + private def ISemanticRegion findInnermostClosingCurlyBracket(ISemanticRegion region) { + if (region.comesAfter("}")) // case '}}' + { + val prevRegion = region.previousSemanticRegion + findInnermostClosingCurlyBracket(prevRegion) + } else if (region.comesAfter(",") && region.previousSemanticRegion.comesAfter("}")) // case '},}') + { + val prevRegion = region.previousSemanticRegion.previousSemanticRegion + findInnermostClosingCurlyBracket(prevRegion) + + } else { + region + } + } + + private def boolean shouldBracketNotBeIndented(ISemanticRegion region) { + region.text == "}" + && + (region.comesAfter("}") || region.comesBefore("}")) + || + ((region.comesAfter(",") && region.previousSemanticRegion.comesAfter("}")) + || + (region.comesBefore(",") && region.nextSemanticRegion.comesBefore("}")) + ) + } + + def indentInnerWithoutCurlyBracket(EObject expr, extension IFormattableDocument document) { + val ext = getTextRegionExt(document).previousHiddenRegion(expr) + expr.indentInnerWithoutCurlyBracket(ext.nextHiddenRegion, document) + } + + def indentInnerWithoutCurlyBracket(EObject expr, IHiddenRegion firstRegion, extension IFormattableDocument document) { + if (expr === null || firstRegion === null) return + val nextRegion = getTextRegionExt(document).nextHiddenRegion(expr) + val end = nextRegion.previousSemanticRegion + set( + firstRegion, + if (shouldBracketNotBeIndented(end)) + end.findInnermostClosingCurlyBracket.previousHiddenRegion + else + end.nextHiddenRegion, + [indent] + ) + } + + private def void surroundIndentWithoutCurlyBracket(EObject expr, extension IFormattableDocument document) { + if (expr === null) return + val objectRegion = expr.regionForEObject + val end = objectRegion.nextHiddenRegion.previousSemanticRegion + + set( + objectRegion.previousHiddenRegion, + if (shouldBracketNotBeIndented(end)) + end.findInnermostClosingCurlyBracket.previousHiddenRegion + else + end.nextHiddenRegion, + [indent] + ) + } + + private def void interiorIndentWithoutCurlyBracket(ISemanticRegion start, ISemanticRegion end, extension IFormattableDocument document) { + if (start !== null && end !== null) { + set( + start.nextHiddenRegion, + if (shouldBracketNotBeIndented(end)) + end.findInnermostClosingCurlyBracket.previousHiddenRegion + else + end.previousHiddenRegion, + [indent] + ) + } + + } + private def dispatch void unsafeFormatExpression(ListLiteral expr, extension IFormattableDocument document, FormattingMode mode) { expr.regionFor.keywords(',').forEach[ prepend[noSpace] ] - interior( + interiorIndentWithoutCurlyBracket( expr.regionFor.keyword('['), expr.regionFor.keyword(']'), - [indent] + document ) formatInlineOrMultiline(document, expr, mode.singleLineIf(expr.shouldBeOnSingleLine), @@ -175,15 +279,23 @@ class RosettaExpressionFormatter extends AbstractRosettaFormatter2 { private def dispatch void unsafeFormatExpression(RosettaConditionalExpression expr, extension IFormattableDocument document, FormattingMode mode) { val extension conditionalGrammarAccess = rosettaCalcConditionalExpressionAccess + // fix edge case where 'then' inside constructor value is not indented correctly + if (expr.eContainer instanceof ConstructorKeyValuePair) { + surround( + expr.regionFor.keyword(thenKeyword_3), + [indent] + ) + } + expr.regionFor.keywords(ifKeyword_1, thenKeyword_3, fullElseKeyword_5_0_0).forEach[ append[oneSpace] ] val subExprs = #[expr.^if, expr.ifthen, expr.elsethen] - #[expr.^if, expr.ifthen].forEach[ + #[expr.^if, expr.ifthen].forEach [ if (!(it instanceof RosettaUnaryOperation)) { - surround( + surroundIndentWithoutCurlyBracket( it, - [indent] + document ) } ] @@ -201,7 +313,7 @@ class RosettaExpressionFormatter extends AbstractRosettaFormatter2 { expr.regionFor.keyword(fullElseKeyword_5_0_0) .prepend[newLine] if (expr.eContainingFeature == ROSETTA_BINARY_OPERATION__RIGHT) { - expr.indentInner(doc) + expr.indentInnerWithoutCurlyBracket(doc) } expr.^if.formatExpression(doc, mode.stopChain) expr.ifthen.formatExpression(doc, mode.stopChain) @@ -250,11 +362,13 @@ class RosettaExpressionFormatter extends AbstractRosettaFormatter2 { } private def dispatch void unsafeFormatExpression(RosettaSymbolReference expr, extension IFormattableDocument document, FormattingMode mode) { + val extension referenceCallGrammarAccess = rosettaReferenceOrFunctionCallAccess + if (expr.explicitArguments) { expr.regionFor.keywords(',').forEach[ prepend[noSpace] ] - expr.regionFor.keyword('(') + expr.regionFor.keyword(explicitArgumentsLeftParenthesisKeyword_0_2_0_0) .prepend[noSpace] formatInlineOrMultiline(document, expr, mode.singleLineIf(expr.shouldBeOnSingleLine), @@ -269,13 +383,13 @@ class RosettaExpressionFormatter extends AbstractRosettaFormatter2 { expr.args.forEach[formatExpression(doc, mode)] ], [extension doc | // case: long argument list - expr.indentInner(doc) - interior( + expr.indentInnerWithoutCurlyBracket(doc) + interiorIndentWithoutCurlyBracket( expr.regionFor.keyword('(') .append[newLine], expr.regionFor.keyword(')') .prepend[newLine], - [indent] + doc ) expr.regionFor.keywords(',').forEach[ append[newLine] @@ -314,7 +428,7 @@ class RosettaExpressionFormatter extends AbstractRosettaFormatter2 { [extension doc | // case: long operation if (!expr.left.isEmpty) { val afterArgument = expr.left.nextHiddenRegion - expr.indentInner(afterArgument, doc) + expr.indentInnerWithoutCurlyBracket(afterArgument, doc) afterArgument .set[newLine] @@ -324,7 +438,7 @@ class RosettaExpressionFormatter extends AbstractRosettaFormatter2 { false } if (expr.left instanceof RosettaBinaryOperation && !leftIsSameOperation) { - expr.left.indentInner(doc) + expr.left.indentInnerWithoutCurlyBracket(doc) } expr.left.formatExpression(doc, mode.chainIf(leftIsSameOperation)) @@ -374,12 +488,12 @@ class RosettaExpressionFormatter extends AbstractRosettaFormatter2 { } ], [extension doc | // case: long inline function - interior( + interiorIndentWithoutCurlyBracket( left .append[newLine], right .prepend[newLine], - [indent] + doc ) f.body.formatExpression(doc, mode.stopChain) ] @@ -388,9 +502,9 @@ class RosettaExpressionFormatter extends AbstractRosettaFormatter2 { val astRegion = f.regionForEObject val formattableRegion = astRegion.merge(astRegion.previousHiddenRegion).merge(astRegion.nextHiddenRegion) if (!(op instanceof ThenOperation && f.body instanceof RosettaUnaryOperation)) { - surround( + surroundIndentWithoutCurlyBracket( f.body, - [indent] + document ) } formatInlineOrMultiline(document, astRegion, formattableRegion, mode.singleLineIf(op instanceof ThenOperation), @@ -459,7 +573,7 @@ class RosettaExpressionFormatter extends AbstractRosettaFormatter2 { initialArgument = initialArgument.argument } if (!initialArgument.isEmpty) { - expr.indentInner(afterArgument, doc) + expr.indentInnerWithoutCurlyBracket(afterArgument, doc) } afterArgument .set[newLine] diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/RosettaFormatter.xtend b/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/RosettaFormatter.xtend index eb5f60b5a..a091db8bc 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/RosettaFormatter.xtend +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/RosettaFormatter.xtend @@ -56,6 +56,7 @@ import com.regnosys.rosetta.rosetta.TypeParameter import com.regnosys.rosetta.rosetta.TypeCallArgument import javax.inject.Inject import com.regnosys.rosetta.rosetta.RosettaRule +import com.regnosys.rosetta.rosetta.simple.Choice class RosettaFormatter extends AbstractRosettaFormatter2 { @@ -236,6 +237,22 @@ class RosettaFormatter extends AbstractRosettaFormatter2 { ele.regionFor.assignment(nameAssignment_1) .prepend[oneSpace] } + + def dispatch void format(Choice ele, extension IFormattableDocument document) { + ele.regionFor.keyword(choiceAccess.choiceKeyword_0) + .append[oneSpace] + ele.regionFor.keyword(':') + .prepend[noSpace] + ele.indentInner(document) + ele.formatDefinition(document) + ele.attributes.head + .prepend[setNewLines(1, 2, 2)] + .format + ele.attributes.tail.forEach[ + prepend[newLine] + format + ] + } def dispatch void format(Data ele, extension IFormattableDocument document) { diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/XtextResourceFormatter.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/XtextResourceFormatter.java index 1027d86a6..a1af59fbf 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/XtextResourceFormatter.java +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/formatting2/XtextResourceFormatter.java @@ -1,12 +1,8 @@ package com.regnosys.rosetta.formatting2; -import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.UncheckedIOException; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.xtext.formatting2.FormatterRequest; @@ -35,10 +31,11 @@ public class XtextResourceFormatter implements ResourceFormatterService { private TextRegionAccessBuilder regionBuilder; @Override - public void formatCollection(Collection resources, ITypedPreferenceValues preferenceValues) { + public void formatCollection(Collection resources, ITypedPreferenceValues preferenceValues, + IFormattedResourceAcceptor acceptor) { resources.stream().forEach(resource -> { if (resource instanceof XtextResource) { - formatXtextResource((XtextResource) resource, preferenceValues); + formatXtextResource((XtextResource) resource, preferenceValues, acceptor); } else { LOGGER.debug("Resource is not of type XtextResource and will be skipped: " + resource.getURI()); @@ -47,7 +44,15 @@ public void formatCollection(Collection resources, ITypedPreferenceVal } @Override - public void formatXtextResource(XtextResource resource, ITypedPreferenceValues preferenceValues) { + public void formatXtextResource(XtextResource resource, ITypedPreferenceValues preferenceValues, + IFormattedResourceAcceptor acceptor) { + if (!resource.getAllContents().hasNext()) { + LOGGER.info("Resource " + resource.getURI() + " is empty."); + return; + } + + LOGGER.info("Formatting file at location " + resource.getURI()); + // setup request and formatter FormatterRequest req = formatterRequestProvider.get(); req.setPreferences(preferenceValues); @@ -57,20 +62,20 @@ public void formatXtextResource(XtextResource resource, ITypedPreferenceValues p req.setTextRegionAccess(regionAccess); // list contains all the replacements which should be applied to resource - List replacements = formatter.format(req); + List replacements; + try { + replacements = formatter.format(req); // throws exception + } catch (RuntimeException e) { + LOGGER.error("RuntimeException in " + resource.getURI() + ": " + e.getMessage(), e); + replacements = new ArrayList<>(); + } // formatting using TextRegionRewriter ITextRegionRewriter regionRewriter = regionAccess.getRewriter(); String formattedString = regionRewriter.renderToString(regionAccess.regionForDocument(), replacements); - // With the formatted text, update the resource - InputStream resultStream = new ByteArrayInputStream(formattedString.getBytes(StandardCharsets.UTF_8)); - resource.unload(); - try { - resource.load(resultStream, null); - } catch (IOException e) { - throw new UncheckedIOException( - "Since the resource is an in-memory string, this exception is not expected to be ever thrown.", e); - } + // Perform handler operation + acceptor.accept(resource, formattedString); } + } diff --git a/rosetta-lang/src/main/java/com/regnosys/rosetta/serialization/RosettaTransientValueService.java b/rosetta-lang/src/main/java/com/regnosys/rosetta/serialization/RosettaTransientValueService.java index f6cd69328..64f6d8895 100644 --- a/rosetta-lang/src/main/java/com/regnosys/rosetta/serialization/RosettaTransientValueService.java +++ b/rosetta-lang/src/main/java/com/regnosys/rosetta/serialization/RosettaTransientValueService.java @@ -17,6 +17,7 @@ package com.regnosys.rosetta.serialization; import java.util.List; +import java.util.Set; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.EStructuralFeature; @@ -24,29 +25,43 @@ import com.regnosys.rosetta.rosetta.expression.ExpressionPackage; import com.regnosys.rosetta.rosetta.expression.RosettaExpression; +import com.regnosys.rosetta.rosetta.simple.SimplePackage; public class RosettaTransientValueService extends DefaultTransientValueService { - private EStructuralFeature generatedInputWasSetFeature = ExpressionPackage.eINSTANCE.getHasGeneratedInput_GeneratedInputWasSet(); - private EStructuralFeature implicitVariableIsInContextFeature = ExpressionPackage.eINSTANCE.getRosettaSymbolReference_ImplicitVariableIsInContext(); - + private final Set ignoredFeatures; + + public RosettaTransientValueService() { + EStructuralFeature generatedInputWasSetFeature = ExpressionPackage.eINSTANCE + .getHasGeneratedInput_GeneratedInputWasSet(); + EStructuralFeature implicitVariableIsInContextFeature = ExpressionPackage.eINSTANCE + .getRosettaSymbolReference_ImplicitVariableIsInContext(); + EStructuralFeature hardcodedConditionFeature = SimplePackage.eINSTANCE.getChoice__hardcodedConditions(); + EStructuralFeature hardcodedNameFeature = SimplePackage.eINSTANCE.getChoiceOption__hardcodedName(); + EStructuralFeature hardcodedCardinalityFeature = SimplePackage.eINSTANCE + .getChoiceOption__hardcodedCardinality(); + ignoredFeatures = Set.of(generatedInputWasSetFeature, implicitVariableIsInContextFeature, + hardcodedConditionFeature, hardcodedNameFeature, hardcodedCardinalityFeature); + + } + @Override public boolean isCheckElementsIndividually(EObject owner, EStructuralFeature feature) { return true; } - + @Override public boolean isTransient(EObject owner, EStructuralFeature feature, int index) { if (super.isTransient(owner, feature, index)) { return true; } - if (feature.equals(generatedInputWasSetFeature) || feature.equals(implicitVariableIsInContextFeature)) { + if (ignoredFeatures.contains(feature)) { return true; } Object value = owner.eGet(feature); if (index >= 0 && value instanceof List) { - value = ((List)value).get(index); + value = ((List) value).get(index); } - if (value instanceof RosettaExpression && ((RosettaExpression)value).isGenerated()) { + if (value instanceof RosettaExpression && ((RosettaExpression) value).isGenerated()) { return true; } return false; diff --git a/rosetta-maven-plugin/src/main/java/com/regnosys/rosetta/maven/ResourceFormatterMojo.java b/rosetta-maven-plugin/src/main/java/com/regnosys/rosetta/maven/ResourceFormatterMojo.java new file mode 100644 index 000000000..c6b30f416 --- /dev/null +++ b/rosetta-maven-plugin/src/main/java/com/regnosys/rosetta/maven/ResourceFormatterMojo.java @@ -0,0 +1,115 @@ +package com.regnosys.rosetta.maven; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.stream.Collectors; + +import javax.inject.Inject; + +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.resource.ResourceSet; +import org.eclipse.lsp4j.FormattingOptions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.inject.Injector; +import com.regnosys.rosetta.RosettaStandaloneSetup; +import com.regnosys.rosetta.formatting2.FormattingOptionsAdaptor; +import com.regnosys.rosetta.formatting2.ResourceFormatterService; + +/** + *

+ * Formatter Plugin: A Maven plugin to help with the formatting of resources. + *

+ * + *

+ * Given a path to a directory holding {@code .rosetta} resources, it formats + * the files according to set formatting rules. Additionally, you can specify a + * custom configuration file for formatting options using the + * {@code formattingOptionsPath} parameter. If the {@code formattingOptionsPath} + * is not provided, the plugin will use default formatting options. + *

+ * + * + *

+ * To run the goal: + *

    + *
  • {@code mvn com.regnosys.rosetta:rosetta-maven-plugin:version:format -Dpath="path/to/directory"}
  • + *
  • Optionally, provide a custom formatting options file using + * {@code -DformattingOptionsPath="path/to/formattingOptions.json"}
  • + *
+ *

+ * + *

+ * Example with both parameters: + *

    + *
  • {@code mvn com.regnosys.rosetta:rosetta-maven-plugin:version:format -Dpath="path/to/directory" -DformattingOptionsPath="path/to/formattingOptions.json"}
  • + *
+ *

+ */ +@Mojo(name = "format") +public class ResourceFormatterMojo extends AbstractMojo { + private static Logger LOGGER = LoggerFactory.getLogger(ResourceFormatterMojo.class); + + /** + * Path to the directory of files to be formatted + */ + @Parameter(property = "path", required = true) + private String path; + + /** + * Path to the .json file containing formatting options + */ + @Parameter(property = "formattingOptionsPath", required = false) + private String formattingOptionsPath; + + @Inject + private FormattingOptionsAdaptor formattingOptionsAdapter; + + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + Path directory = Paths.get(path); + LOGGER.info("Mojo running on path:" + directory.toString()); + + FormattingOptions formattingOptions = null; + try { + formattingOptions = formattingOptionsAdapter.readFormattingOptions(formattingOptionsPath); + } catch (IOException e) { + LOGGER.error("Config file not found.", e); + } + + Injector inj = new RosettaStandaloneSetup().createInjectorAndDoEMFRegistration(); + ResourceSet resourceSet = inj.getInstance(ResourceSet.class); + ResourceFormatterService formatterService = inj.getInstance(ResourceFormatterService.class); + + List resources; + try { + // Find all .rosetta files in the directory and load them from disk + resources = Files.walk(directory).filter(path -> path.toString().endsWith(".rosetta")) + .map(file -> resourceSet.getResource(URI.createFileURI(file.toString()), true)) + .collect(Collectors.toList()); + } catch (IOException e) { + throw new MojoFailureException("Error processing files: " + e.getMessage(), e); + } + // format resources + formatterService.formatCollection(resources, formattingOptionsAdapter.createPreferences(formattingOptions), + (resource, formattedText) -> { + Path resourcePath = Path.of(resource.getURI().toFileString()); + try { + Files.writeString(resourcePath, formattedText); + LOGGER.info("Content written to file: " + resourcePath); + } catch (IOException e) { + LOGGER.error("Error writing to file.", e); + } + }); + } +} \ No newline at end of file diff --git a/rosetta-runtime/src/main/resources/default-formatting-options.json b/rosetta-runtime/src/main/resources/default-formatting-options.json new file mode 100644 index 000000000..aadbd833e --- /dev/null +++ b/rosetta-runtime/src/main/resources/default-formatting-options.json @@ -0,0 +1,6 @@ +{ + "tabSize":4, + "insertSpaces":true, + "maxLineWidth":92, + "conditionalMaxLineWidth":70 +} \ No newline at end of file diff --git a/rosetta-testing/src/test/java/com/regnosys/rosetta/formatting2/ResourceFormatterServiceTest.java b/rosetta-testing/src/test/java/com/regnosys/rosetta/formatting2/ResourceFormatterServiceTest.java index 9c38fcdea..0a5076583 100644 --- a/rosetta-testing/src/test/java/com/regnosys/rosetta/formatting2/ResourceFormatterServiceTest.java +++ b/rosetta-testing/src/test/java/com/regnosys/rosetta/formatting2/ResourceFormatterServiceTest.java @@ -20,7 +20,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.stream.Collectors; import com.google.common.io.Resources; import com.regnosys.rosetta.tests.RosettaInjectorProvider; @@ -32,14 +31,13 @@ public class ResourceFormatterServiceTest { ResourceFormatterService formatterService; @Inject Provider resourceSetProvider; - @Inject - ISerializer serializer; private void testFormatting(Collection inputUrls, Collection expectedUrls) throws IOException, URISyntaxException { ResourceSet resourceSet = resourceSetProvider.get(); List resources = new ArrayList<>(); - List expected = new ArrayList<>(); + List formattedText = new ArrayList<>(); + List expectedText = new ArrayList<>(); for (String url : inputUrls) { Resource resource = resourceSet.getResource(URI.createURI(Resources.getResource(url).toString()), true); @@ -47,15 +45,14 @@ private void testFormatting(Collection inputUrls, Collection exp } for (String url : expectedUrls) { - expected.add(Files.readString(Path.of(Resources.getResource(url).toURI()))); + expectedText.add(Files.readString(Path.of(Resources.getResource(url).toURI()))); } - formatterService.formatCollection(resources); - - List result = resources.stream().map(resource -> serializer.serialize(resource.getContents().get(0))) - .collect(Collectors.toList()); + formatterService.formatCollection(resources, (resource, formattedContent) -> { + formattedText.add(formattedContent); // Collect formatted content for assertions + }); - Assertions.assertIterableEquals(expected, result); + Assertions.assertIterableEquals(expectedText, formattedText); } @Test @@ -72,4 +69,11 @@ void formatMultipleDocuments() throws IOException, URISyntaxException { List.of("formatting-test/expected/typeAlias.rosetta", "formatting-test/expected/typeAliasWithDocumentation.rosetta")); } + + @Test + void formatNestedConstructor() throws IOException, URISyntaxException { + testFormatting(List.of("formatting-test/input/nestedConstructor.rosetta"), + List.of("formatting-test/expected/nestedConstructor.rosetta")); + } + } diff --git a/rosetta-testing/src/test/java/com/regnosys/rosetta/formatting2/RosettaExpressionFormattingTest.xtend b/rosetta-testing/src/test/java/com/regnosys/rosetta/formatting2/RosettaExpressionFormattingTest.xtend index 32fa3fb31..8761c7826 100644 --- a/rosetta-testing/src/test/java/com/regnosys/rosetta/formatting2/RosettaExpressionFormattingTest.xtend +++ b/rosetta-testing/src/test/java/com/regnosys/rosetta/formatting2/RosettaExpressionFormattingTest.xtend @@ -63,10 +63,10 @@ class RosettaExpressionFormattingTest { SomeType { attr1: "Some expression", attr2: foo - extract - if True - then ["This is a looong", "expression"] - else 42, + extract + if True + then ["This is a looong", "expression"] + else 42, } ''' } @@ -87,15 +87,98 @@ class RosettaExpressionFormattingTest { SomeType { attr1: "Some expression", attr2: foo - extract - if True - then ["This is a looong", "expression"] - else 42, + extract + if True + then ["This is a looong", "expression"] + else 42, ... } ''' } + @Test + def void testConstructorFormat3() { + ''' + SomeType { + attr1: "Some expression", + attr2: Foo { + bar: True + }, + } + ''' -> + ''' + SomeType { + attr1: "Some expression", + attr2: Foo { + bar: True + },} + ''' + } + + @Test + def void testConstructorNestedWithBooleanFormat() { + ''' + Constr1 { + attr1: if True + then False, + attr2: if False + then Constr2 { + attr11: Constr3 { + attr111: 42 + }} + } + ''' -> + ''' + Constr1 { + attr1: if True then False, + attr2: if False + then Constr2 { + attr11: Constr3 { + attr111: 42 + }}} + ''' + } + + @Test + def void testCollapsingBracketsDeepNested() { + ''' + Constr1 { + attr1: if True then False, + attr2: if False + then 42 extract Constr2 { + attr11: Constr3 { + attr111: item + }}} + '''->''' + Constr1 { + attr1: if True then False, + attr2: if False + then 42 + extract + Constr2 { + attr11: Constr3 { + attr111: item + }}} + ''' + } + + @Test + def void testConstructorNestedInUnaryOperation() { + ''' + el1 + extract + Constr1 { + attr1: val1 + } + ''' -> ''' + el1 + extract + Constr1 { + attr1: val1 + } + ''' + } + @Test def void testOperationChainingFormat1() { ''' @@ -761,4 +844,26 @@ class RosettaExpressionFormattingTest { (["This", "is", "a", "loooooooooooooooooooooooong", "list"] count > 10) ''' } + + @Test + def void testFunctionCallInParenthesis() { + ''' + (SomeFunc + ( + )) + ''' -> ''' + (SomeFunc()) + ''' + } + + @Test + def void testConditionalInParenthesis() { + ''' + (if True + then 10) + ''' -> ''' + (if True then 10) + ''' + } + } \ No newline at end of file diff --git a/rosetta-testing/src/test/java/com/regnosys/rosetta/formatting2/RosettaFormattingTest.xtend b/rosetta-testing/src/test/java/com/regnosys/rosetta/formatting2/RosettaFormattingTest.xtend index c38bc8902..9f1289441 100644 --- a/rosetta-testing/src/test/java/com/regnosys/rosetta/formatting2/RosettaFormattingTest.xtend +++ b/rosetta-testing/src/test/java/com/regnosys/rosetta/formatting2/RosettaFormattingTest.xtend @@ -588,4 +588,25 @@ class RosettaFormattingTest { add out -> other: [in1, in1, in1, in1] ''' } + + @Test + def void testChoice() { + ''' + namespace "test" + version "test" + choice Test: + A + + B + + ''' -> ''' + namespace "test" + version "test" + + choice Test: + A + B + ''' + } + } diff --git a/rosetta-testing/src/test/resources/formatting-test/expected/nestedConstructor.rosetta b/rosetta-testing/src/test/resources/formatting-test/expected/nestedConstructor.rosetta new file mode 100644 index 000000000..3209e96fd --- /dev/null +++ b/rosetta-testing/src/test/resources/formatting-test/expected/nestedConstructor.rosetta @@ -0,0 +1,11 @@ +namespace test + +func MyOtherFunc: + output: + result Foo (1..1) + + set result: + Foo { + bar: Bar { + attr: "bar attr" + }} diff --git a/rosetta-testing/src/test/resources/formatting-test/input/nestedConstructor.rosetta b/rosetta-testing/src/test/resources/formatting-test/input/nestedConstructor.rosetta new file mode 100644 index 000000000..5cafd7e54 --- /dev/null +++ b/rosetta-testing/src/test/resources/formatting-test/input/nestedConstructor.rosetta @@ -0,0 +1,12 @@ +namespace test + +func MyOtherFunc: + output: + result Foo (1..1) + + set result: + Foo { + bar: Bar { + attr: "bar attr" + } + } diff --git a/rosetta-testing/src/test/resources/formatting-test/input/onlyExists.rosetta b/rosetta-testing/src/test/resources/formatting-test/input/onlyExists.rosetta new file mode 100644 index 000000000..775ab13ea --- /dev/null +++ b/rosetta-testing/src/test/resources/formatting-test/input/onlyExists.rosetta @@ -0,0 +1,8 @@ +namespace test + +func MyFunc: + output: + result boolean (1..1) + + set result: + (foo -> bar) only exists diff --git a/rosetta-tools/src/main/java/com/regnosys/rosetta/tools/ResourceFormattingTool.java b/rosetta-tools/src/main/java/com/regnosys/rosetta/tools/ResourceFormattingTool.java index 3981db8f8..605881e0b 100644 --- a/rosetta-tools/src/main/java/com/regnosys/rosetta/tools/ResourceFormattingTool.java +++ b/rosetta-tools/src/main/java/com/regnosys/rosetta/tools/ResourceFormattingTool.java @@ -7,16 +7,19 @@ import java.util.List; import java.util.stream.Collectors; +import javax.inject.Inject; + import org.eclipse.emf.common.util.URI; import org.eclipse.emf.ecore.resource.Resource; import org.eclipse.emf.ecore.resource.ResourceSet; +import org.eclipse.lsp4j.FormattingOptions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.inject.Injector; import com.regnosys.rosetta.RosettaStandaloneSetup; +import com.regnosys.rosetta.formatting2.FormattingOptionsAdaptor; import com.regnosys.rosetta.formatting2.ResourceFormatterService; -import com.regnosys.rosetta.formatting2.XtextResourceFormatter; /** * A command-line tool for formatting `.rosetta` files in a specified directory. @@ -36,45 +39,68 @@ *

*/ public class ResourceFormattingTool { + @Inject + private static FormattingOptionsAdaptor formattingOptionsAdapter; + private static Logger LOGGER = LoggerFactory.getLogger(ResourceFormattingTool.class); public static void main(String[] args) { + int maxArgs = 2; + if (args.length == 0) { - System.out.println("Please provide the directory path as an argument."); - System.exit(1); + exitProgram("Please provide the directory path as an argument."); + } + + if (args.length > maxArgs) { + exitProgram("Too many arguments. Please provide maximum " + maxArgs + " arguments."); } Path directory = Paths.get(args[0]); if (!Files.isDirectory(directory)) { - System.out.println("The provided path is not a valid directory."); - System.exit(1); + exitProgram("The provided path is not a valid directory."); + } + + // check if optional parameter was given. If not use default value + FormattingOptions formattingOptions = null; + if(args.length > 1) { + String formattingOptionsPath = args[1]; + try { + formattingOptions = formattingOptionsAdapter.readFormattingOptions(formattingOptionsPath); + } catch (IOException e) { + LOGGER.error("Config file not found.", e); + } } Injector inj = new RosettaStandaloneSetup().createInjectorAndDoEMFRegistration(); ResourceSet resourceSet = inj.getInstance(ResourceSet.class); ResourceFormatterService formatterService = inj.getInstance(ResourceFormatterService.class); + List resources = null; try { // Find all .rosetta files in the directory and load them from disk - List resources = Files.walk(directory) + resources = Files.walk(directory) .filter(path -> path.toString().endsWith(".rosetta")) .map(file -> resourceSet.getResource(URI.createFileURI(file.toString()), true)) .collect(Collectors.toList()); - // format resources - formatterService.formatCollection(resources, null); - - // save each resource - resources.forEach(resource -> { - try { - resource.save(null); - LOGGER.info("Successfully formatted and saved file at location " + resource.getURI()); - } catch (IOException e) { - LOGGER.error("Error saving file at location " + resource.getURI() + ": "+ e.getMessage()); - } - }); } catch (IOException e) { LOGGER.error("Error processing files: " + e.getMessage()); } + + formatterService.formatCollection(resources, formattingOptionsAdapter.createPreferences(formattingOptions), + (resource, formattedText) -> { + Path resourcePath = Path.of(resource.getURI().toFileString()); + try { + Files.writeString(resourcePath, formattedText); + LOGGER.info("Content written to file: " + resourcePath); + } catch (IOException e) { + LOGGER.error("Error writing to file.", e); + } + }); + } + + private static void exitProgram(String msg) { + System.out.println(msg); + System.exit(1); } }