diff --git a/rascal-lsp/pom.xml b/rascal-lsp/pom.xml index 05444a4fe..de5e9d4c2 100644 --- a/rascal-lsp/pom.xml +++ b/rascal-lsp/pom.xml @@ -64,7 +64,7 @@ org.rascalmpl rascal - 0.43.0-RC6 + 0.43.0-RC8 @@ -153,6 +153,12 @@ 3.5.6 + + + always + org.rascalmpl.vscode.lsp.log.LogRedirectConfiguration + + org.rascalmpl diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ILanguageContributions.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ILanguageContributions.java index 6dc401469..7df394fc4 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ILanguageContributions.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ILanguageContributions.java @@ -62,6 +62,7 @@ public interface ILanguageContributions { public InterruptibleFuture selectionRange(IList focus); public InterruptibleFuture prepareCallHierarchy(IList focus); public InterruptibleFuture incomingOutgoingCalls(IConstructor hierarchyItem, IConstructor direction); + public InterruptibleFuture formatting(IList input, IConstructor formattingOptions); public InterruptibleFuture prepareRename(IList focus); public InterruptibleFuture rename(IList focus, String name); @@ -88,6 +89,7 @@ public interface ILanguageContributions { public CompletableFuture providesSelectionRange(); public CompletableFuture providesCallHierarchy(); public CompletableFuture providesCompletion(); + public CompletableFuture providesFormatting(); public CompletableFuture specialCaseHighlighting(); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/InterpretedLanguageContributions.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/InterpretedLanguageContributions.java index cba9a91f7..75d9cb788 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/InterpretedLanguageContributions.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/InterpretedLanguageContributions.java @@ -99,6 +99,8 @@ public class InterpretedLanguageContributions implements ILanguageContributions private final CompletableFuture<@Nullable IFunction> didRenameFiles; private final CompletableFuture<@Nullable IFunction> selectionRange; private final CompletableFuture<@Nullable IFunction> prepareCallHierarchy; + private final CompletableFuture<@Nullable IFunction> formatting; + private final CompletableFuture<@Nullable IFunction> callHierarchyService; private final CompletableFuture<@Nullable IFunction> completion; private final CompletableFuture completionTriggerCharacters; @@ -119,6 +121,7 @@ public class InterpretedLanguageContributions implements ILanguageContributions private final CompletableFuture providesSelectionRange; private final CompletableFuture providesCallHierarchy; private final CompletableFuture providesCompletion; + private final CompletableFuture providesFormatting; private final CompletableFuture specialCaseHighlighting; @@ -169,6 +172,7 @@ public InterpretedLanguageContributions(LanguageParameter lang, IBaseTextDocumen this.callHierarchyService = getFunctionFor(contributions, LanguageContributions.CALL_HIERARCHY, 1); this.completion = getFunctionFor(contributions, LanguageContributions.COMPLETION); this.completionTriggerCharacters = getContributionParameter(contributions, LanguageContributions.COMPLETION, LanguageContributions.COMPLETION_TRIGGER_CHARACTERS, VF.list(), IList.class); + this.formatting = getFunctionFor(contributions, LanguageContributions.FORMATTING); // assign boolean properties once instead of wasting futures all the time this.providesAnalysis = nonNull(this.analysis); @@ -187,6 +191,7 @@ public InterpretedLanguageContributions(LanguageParameter lang, IBaseTextDocumen this.providesSelectionRange = nonNull(this.selectionRange); this.providesCallHierarchy = nonNull(this.prepareCallHierarchy); this.providesCompletion = nonNull(this.completion); + this.providesFormatting = nonNull(this.formatting); this.specialCaseHighlighting = getContributionParameter(contributions, LanguageContributions.PARSING, @@ -486,6 +491,12 @@ public CompletableFuture completionTriggerCharacters() { return completionTriggerCharacters; } + @Override + public InterruptibleFuture formatting(IList focus, IConstructor formattingOptions) { + debug(LanguageContributions.FORMATTING, focus.size(), formattingOptions); + return execFunction(LanguageContributions.FORMATTING, formatting, VF.list(), focus, formattingOptions); + } + private void debug(String name, Object param) { logger.debug("{}({})", name, param); } @@ -563,6 +574,11 @@ public CompletableFuture providesCompletion() { return providesCompletion; } + @Override + public CompletableFuture providesFormatting() { + return providesFormatting; + } + @Override public CompletableFuture providesAnalysis() { return providesAnalysis; diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/LanguageContributionsMultiplexer.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/LanguageContributionsMultiplexer.java index 62ce162f5..4aefb9652 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/LanguageContributionsMultiplexer.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/LanguageContributionsMultiplexer.java @@ -77,6 +77,7 @@ private static final CompletableFuture failedInitialization() { private volatile CompletableFuture selectionRange = failedInitialization(); private volatile CompletableFuture callHierarchy = failedInitialization(); private volatile CompletableFuture completion = failedInitialization(); + private volatile CompletableFuture formatting = failedInitialization(); private volatile CompletableFuture providesAnalysis = failedInitialization(); private volatile CompletableFuture providesBuild = failedInitialization(); @@ -94,6 +95,7 @@ private static final CompletableFuture failedInitialization() { private volatile CompletableFuture providesSelectionRange = failedInitialization(); private volatile CompletableFuture providesCallHierarchy = failedInitialization(); private volatile CompletableFuture providesCompletion = failedInitialization(); + private volatile CompletableFuture providesFormatting = failedInitialization(); private volatile CompletableFuture specialCaseHighlighting = failedInitialization(); @@ -172,6 +174,7 @@ private synchronized void calculateRouting() { selectionRange = findFirstOrDefault(ILanguageContributions::providesSelectionRange, "selectionRange"); callHierarchy = findFirstOrDefault(ILanguageContributions::providesCallHierarchy, "callHierarchy"); completion = findFirstOrDefault(ILanguageContributions::providesCompletion, "completion"); + formatting = findFirstOrDefault(ILanguageContributions::providesFormatting, "formatting"); providesAnalysis = anyTrue(ILanguageContributions::providesAnalysis); providesBuild = anyTrue(ILanguageContributions::providesBuild); @@ -189,6 +192,7 @@ private synchronized void calculateRouting() { providesSelectionRange = anyTrue(ILanguageContributions::providesSelectionRange); providesCallHierarchy = anyTrue(ILanguageContributions::providesCallHierarchy); providesCompletion = anyTrue(ILanguageContributions::providesCompletion); + providesFormatting = anyTrue(ILanguageContributions::providesFormatting); // Always use the special-case highlighting status of *the first* // contribution (possibly using the default value in the Rascal ADT if @@ -350,6 +354,11 @@ public InterruptibleFuture selectionRange(IList focus) { return flatten(selectionRange, c -> c.selectionRange(focus)); } + @Override + public InterruptibleFuture formatting(IList focus, IConstructor formattingOptions) { + return flatten(formatting, c -> c.formatting(focus, formattingOptions)); + } + @Override public InterruptibleFuture completion(IList focus, IInteger cursorOffset, IConstructor trigger) { return flatten(completion, c -> c.completion(focus, cursorOffset, trigger)); @@ -449,6 +458,11 @@ public CompletableFuture providesCompletion() { return providesCompletion; } + @Override + public CompletableFuture providesFormatting() { + return providesFormatting; + } + @Override public CompletableFuture specialCaseHighlighting() { return specialCaseHighlighting; diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/NoContributions.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/NoContributions.java index b30c31360..26405876a 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/NoContributions.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/NoContributions.java @@ -199,6 +199,11 @@ public CompletableFuture completionTriggerCharacters() { return completable(VF.list()); } + @Override + public InterruptibleFuture formatting(IList input, IConstructor formattingOptions) { + return interruptible(VF.list()); + } + @Override public CompletableFuture providesAnalysis() { return falsy; @@ -279,6 +284,11 @@ public CompletableFuture providesCompletion() { return falsy; } + @Override + public CompletableFuture providesFormatting() { + return falsy; + } + @Override public CompletableFuture specialCaseHighlighting() { return falsy; diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java index ea3ec9226..15fe5ab6b 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParametricTextDocumentService.java @@ -74,6 +74,8 @@ import org.eclipse.lsp4j.DidCloseTextDocumentParams; import org.eclipse.lsp4j.DidOpenTextDocumentParams; import org.eclipse.lsp4j.DidSaveTextDocumentParams; +import org.eclipse.lsp4j.DocumentFormattingParams; +import org.eclipse.lsp4j.DocumentRangeFormattingParams; import org.eclipse.lsp4j.DocumentSymbol; import org.eclipse.lsp4j.DocumentSymbolParams; import org.eclipse.lsp4j.ExecuteCommandOptions; @@ -81,6 +83,7 @@ import org.eclipse.lsp4j.FileRename; import org.eclipse.lsp4j.FoldingRange; import org.eclipse.lsp4j.FoldingRangeRequestParams; +import org.eclipse.lsp4j.FormattingOptions; import org.eclipse.lsp4j.Hover; import org.eclipse.lsp4j.HoverParams; import org.eclipse.lsp4j.ImplementationParams; @@ -112,6 +115,7 @@ import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.TextDocumentItem; import org.eclipse.lsp4j.TextDocumentSyncKind; +import org.eclipse.lsp4j.TextEdit; import org.eclipse.lsp4j.VersionedTextDocumentIdentifier; import org.eclipse.lsp4j.WorkspaceEdit; import org.eclipse.lsp4j.WorkspaceFolder; @@ -122,6 +126,7 @@ import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; import org.eclipse.lsp4j.services.LanguageClient; import org.eclipse.lsp4j.services.LanguageClientAware; +import org.eclipse.lsp4j.util.Ranges; import org.rascalmpl.uri.URIResolverRegistry; import org.rascalmpl.uri.URIUtil; import org.rascalmpl.values.IRascalValueFactory; @@ -239,6 +244,8 @@ public void initializeServerCapabilities(ClientCapabilities clientCapabilities, result.setCodeLensProvider(new CodeLensOptions(false)); result.setRenameProvider(new RenameOptions(true)); result.setExecuteCommandProvider(new ExecuteCommandOptions(Collections.singletonList(getRascalMetaCommandName()))); + result.setDocumentFormattingProvider(true); + result.setDocumentRangeFormattingProvider(true); result.setInlayHintProvider(true); result.setSelectionRangeProvider(true); result.setFoldingRangeProvider(true); @@ -742,6 +749,66 @@ public CompletableFuture>> codeAction(CodeActio return CodeActions.mergeAndConvertCodeActions(this, dedicatedLanguageName, contribs.getName(), quickfixes, codeActions); } + @Override + public CompletableFuture> formatting(DocumentFormattingParams params) { + logger.debug("Formatting: {}", params); + + var uri = Locations.toLoc(params.getTextDocument()); + final ILanguageContributions contribs = contributions(uri); + + // call the `formatting` implementation of the relevant language contribution + return getFile(uri) + .getCurrentTreeAsync(true) + .thenApply(Versioned::get) + .thenCompose(tree -> { + final var opts = getFormattingOptions(params.getOptions()); + return contribs.formatting(VF.list(tree), opts).get(); + }) + .thenApply(l -> DocumentChanges.translateTextEdits(l, getColumnMaps())); + } + + @Override + public CompletableFuture> rangeFormatting(DocumentRangeFormattingParams params) { + logger.debug("Formatting range: {}", params); + + var uri = Locations.toLoc(params.getTextDocument()); + Range range = params.getRange(); + final ILanguageContributions contribs = contributions(uri); + + // call the `formatting` implementation of the relevant language contribution + var fileState = getFile(uri); + return fileState + .getCurrentTreeAsync(true) + .thenApply(Versioned::get) + .thenCompose(tree -> { + // just a range + var r = Locations.setRange(uri, range, getColumnMaps()); + // compute the focus list at the end of the range + var focus = TreeSearch.computeFocusList(tree, r.getBeginLine(), r.getBeginColumn(), r.getEndLine(), r.getEndColumn()); + + var opts = getFormattingOptions(params.getOptions()); + return contribs.formatting(focus, opts).get(); + }) + // convert the document changes + .thenApply(l -> DocumentChanges.translateTextEdits(l, getColumnMaps()) + .stream() + .filter(e -> Ranges.containsRange(range, e.getRange())) + .collect(Collectors.toList())); + } + + private IConstructor getFormattingOptions(FormattingOptions options) { + var optionsType = tf.abstractDataType(typeStore, "FormattingOptions"); + var consType = tf.constructor(typeStore, optionsType, "formattingOptions"); + var opts = Map.of( + "tabSize", VF.integer(options.getTabSize()), + "insertSpaces", VF.bool(options.isInsertSpaces()), + "trimTrailingWhitespace", VF.bool(options.isTrimTrailingWhitespace()), + "insertFinalNewline", VF.bool(options.isInsertFinalNewline()), + "trimFinalNewlines", VF.bool(options.isTrimFinalNewlines()) + ); + return VF.constructor(consType, new IValue[0], opts); + } + private CompletableFuture computeCodeActions(final ILanguageContributions contribs, final int startLine, final int startColumn, ITree tree) { IList focus = TreeSearch.computeFocusList(tree, startLine, startColumn); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/model/RascalADTs.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/model/RascalADTs.java index 84c559766..e54ec9af7 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/model/RascalADTs.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/model/RascalADTs.java @@ -51,6 +51,7 @@ private LanguageContributions () {} public static final String CALL_HIERARCHY = "callHierarchy"; public static final String COMPLETION = "completion"; public static final String COMPLETION_TRIGGER_CHARACTERS = "additionalTriggerCharacters"; + public static final String FORMATTING = "formatting"; public static final String RENAME_SERVICE = "renameService"; public static final String PREPARE_RENAME_SERVICE = "prepareRenameService"; diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java index dc0d62e3a..565405cc4 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalLanguageServices.java @@ -47,6 +47,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.Nullable; +import org.eclipse.lsp4j.FormattingOptions; import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; import org.eclipse.lsp4j.jsonrpc.messages.ResponseError; import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode; @@ -72,6 +73,7 @@ import org.rascalmpl.vscode.lsp.util.EvaluatorUtil; import org.rascalmpl.vscode.lsp.util.EvaluatorUtil.LSPContext; import org.rascalmpl.vscode.lsp.util.RascalServices; +import org.rascalmpl.vscode.lsp.util.Versioned; import org.rascalmpl.vscode.lsp.util.concurrent.InterruptibleFuture; import org.rascalmpl.vscode.lsp.util.locations.Locations; @@ -126,7 +128,7 @@ public RascalLanguageServices(RascalTextDocumentService docService, BaseWorkspac var context = new LSPContext(exec, docService, workspaceService, client); shortRunningTaskEvaluator = makeFutureEvaluator(context, "Rascal tasks", monitor, pcfg, "lang::rascal::lsp::DocumentSymbols", "lang::rascal::lsp::Templates"); - semanticEvaluator = makeFutureEvaluator(context, "Rascal semantics", monitor, compilerPcfg, "lang::rascalcore::check::Summary", "lang::rascal::lsp::refactor::Rename", "lang::rascal::lsp::Actions"); + semanticEvaluator = makeFutureEvaluator(context, "Rascal semantics", monitor, compilerPcfg, "lang::rascalcore::check::Summary", "lang::rascal::lsp::refactor::Rename", "lang::rascal::lsp::Actions", "lang::rascal::lsp::Formatter"); compilerEvaluator = makeFutureEvaluator(context, "Rascal compiler", monitor, compilerPcfg, "lang::rascal::lsp::IDECheckerWrapper"); actionStore = semanticEvaluator.thenApply(e -> ((ModuleEnvironment) e.getModule("lang::rascal::lsp::Actions")).getStore()); rascalTextDocumentService = docService; @@ -375,6 +377,25 @@ public List locateCodeLenses(ITree tree) { return result; } + public CompletableFuture format(IList focus, FormattingOptions options) { + // list[TextEdit] picoFormattingService(Focus focus, FormattingOptions opts) + // TODO: make these final constants, or reuse code from the parametric formatter PR. + // no need to code this twice. + Type FO = tf.abstractDataType(store, "FormattingOptions"); + Type cons = tf.constructor(store, FO, "formattingOptions"); + // TODO: map actual options to keyword fields (already done on the parametric formatter PR) + IConstructor opts = VF.constructor(cons); + return runEvaluator( + "Formatting", + semanticEvaluator, + eval -> (IList) eval.call("rascalFormattingService", focus, opts), + VF.list(), + exec, + false, + client + ).get(); + } + public CompletableFuture parseCodeActions(String command) { return actionStore.thenApply(commandStore -> { try { diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java index a8af868ab..5646833cc 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/rascal/RascalTextDocumentService.java @@ -57,6 +57,8 @@ import org.eclipse.lsp4j.DidCloseTextDocumentParams; import org.eclipse.lsp4j.DidOpenTextDocumentParams; import org.eclipse.lsp4j.DidSaveTextDocumentParams; +import org.eclipse.lsp4j.DocumentFormattingParams; +import org.eclipse.lsp4j.DocumentRangeFormattingParams; import org.eclipse.lsp4j.DocumentSymbol; import org.eclipse.lsp4j.DocumentSymbolParams; import org.eclipse.lsp4j.ExecuteCommandOptions; @@ -91,6 +93,7 @@ import org.eclipse.lsp4j.TextDocumentIdentifier; import org.eclipse.lsp4j.TextDocumentItem; import org.eclipse.lsp4j.TextDocumentSyncKind; +import org.eclipse.lsp4j.TextEdit; import org.eclipse.lsp4j.WorkspaceEdit; import org.eclipse.lsp4j.WorkspaceFolder; import org.eclipse.lsp4j.jsonrpc.ResponseErrorException; @@ -102,6 +105,8 @@ import org.eclipse.lsp4j.services.LanguageClientAware; import org.rascalmpl.library.util.PathConfig; import org.rascalmpl.uri.URIResolverRegistry; +import org.rascalmpl.uri.URIUtil; +import org.rascalmpl.util.locations.ColumnMaps; import org.rascalmpl.values.IRascalValueFactory; import org.rascalmpl.values.parsetrees.ITree; import org.rascalmpl.values.parsetrees.ProductionAdapter; @@ -125,6 +130,7 @@ import org.rascalmpl.vscode.lsp.uri.LSPOpenFileRedirector; import org.rascalmpl.vscode.lsp.util.Versioned; import org.rascalmpl.vscode.lsp.util.concurrent.CompletableFutureUtils; +import org.rascalmpl.vscode.lsp.util.concurrent.InterruptibleFuture; import org.rascalmpl.vscode.lsp.util.locations.Locations; import org.rascalmpl.vscode.lsp.util.locations.impl.TreeSearch; @@ -188,6 +194,8 @@ public void initializeServerCapabilities(ClientCapabilities clientCapabilities, result.setTextDocumentSync(TextDocumentSyncKind.Full); result.setDocumentSymbolProvider(true); result.setHoverProvider(true); + result.setDocumentFormattingProvider(true); + result.setDocumentRangeFormattingProvider(true); result.setSemanticTokensProvider(tokenizer.options()); result.setCodeLensProvider(new CodeLensOptions(false)); result.setFoldingRangeProvider(true); @@ -351,6 +359,34 @@ public CompletableFuture null); } + @Override + public CompletableFuture> formatting(DocumentFormattingParams params) { + logger.debug("textDocument/formatting: {} at {}", params.getTextDocument(), params.getOptions()); + + return getFile(params.getTextDocument()) + .getCurrentTreeAsync(true) + .thenApply(Versioned::get) + .thenCompose(tree -> availableRascalServices().format(VF.list(tree), params.getOptions())) + .thenApply(rascalEdits -> DocumentChanges.translateTextEdits(rascalEdits, getColumnMaps())) + ; + } + + @Override + public CompletableFuture> rangeFormatting(DocumentRangeFormattingParams pr) { + logger.debug("textDocument/rangeFormatting: {} at {}", pr.getTextDocument(), pr.getOptions()); + + return getFile(pr.getTextDocument()) + .getCurrentTreeAsync(true) + .thenApply(Versioned::get) + .thenApply((ITree tree) -> { + ISourceLocation fileLoc = TreeAdapter.getLocation(tree).top(); + return TreeSearch.computeFocusList(tree, Locations.setRange(fileLoc, pr.getRange(), getColumnMaps())); + }) + .thenCompose(focus -> availableRascalServices().format(focus, pr.getOptions())) + .thenApply(rascalEdits -> DocumentChanges.translateTextEdits(rascalEdits, getColumnMaps())) + ; + } + @Override public CompletableFuture rename(RenameParams params) { logger.debug("textDocument/rename: {} at {} to {}", params.getTextDocument(), params.getPosition(), params.getNewName()); diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/locations/impl/TreeSearch.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/locations/impl/TreeSearch.java index 4dc2e594f..f9ae054bb 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/locations/impl/TreeSearch.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/locations/impl/TreeSearch.java @@ -26,6 +26,8 @@ */ package org.rascalmpl.vscode.lsp.util.locations.impl; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.rascalmpl.values.IRascalValueFactory; import org.rascalmpl.values.parsetrees.ITree; import org.rascalmpl.values.parsetrees.TreeAdapter; @@ -40,6 +42,9 @@ */ public class TreeSearch { + private static final Logger logger = LogManager.getLogger(TreeSearch.class); + private static final IRascalValueFactory VF = IRascalValueFactory.getInstance(); + private TreeSearch() {} /** @@ -85,8 +90,30 @@ else if (line == loc.getEndLine()) { return true; } + private static boolean rightOfBegin(ISourceLocation loc, int line, int column) { + if (!loc.hasLineColumn()) { + return false; + } + + if (line > loc.getBeginLine()) { + return true; + } + return line == loc.getBeginLine() && column > loc.getBeginColumn(); + } + + private static boolean rightOfEnd(ISourceLocation loc, int line, int column) { + if (!loc.hasLineColumn()) { + return false; + } + + if (line > loc.getEndLine()) { + return true; + } + return line == loc.getEndLine() && column > loc.getEndColumn(); + } + /** - * Produces a list of trees that are "in focus" at given line and column offset (UTF-24). + * Produces a list of trees that are "in focus" at given line and column offset (UTF-32). * * This log(filesize) algorithm quickly collects the trees along a spine from the * root to the largest lexical or, if that does not exist, the smallest context-free node. @@ -99,7 +126,7 @@ else if (line == loc.getEndLine()) { * @return list of tree that are around the given line/column position, ordered from child to parent. */ public static IList computeFocusList(ITree tree, int line, int column) { - var lw = IRascalValueFactory.getInstance().listWriter(); + var lw = VF.listWriter(); computeFocusList(lw, tree, line, column); return lw.done(); } @@ -157,4 +184,83 @@ private static boolean computeFocusList(IListWriter focus, ITree tree, int line, // cycles and characters do not have locations return false; } + + public static IList computeFocusList(ITree tree, ISourceLocation selection) { + assert TreeAdapter.getLocation(tree).top().equals(selection.top()); + + return computeFocusList(tree, + selection.getBeginLine(), + selection.getBeginColumn(), + selection.getEndLine(), + selection.getEndColumn()); + } + + public static IList computeFocusList(ITree tree, int startLine, int startColumn, int endLine, int endColumn) { + // Compute the focus for both the start end end positions. + // These foci give us information about the structure of the selection. + final var startList = computeFocusList(tree, startLine, startColumn); + final var endList = computeFocusList(tree, endLine, endColumn); + + logger.trace("Focus at range start: {}", startList.length()); + logger.trace("Focus at range end: {}", endList.length()); + + final var commonSuffix = startList.intersect(endList); + + logger.trace("Common focus suffix length: {}", commonSuffix.length()); + // The range spans multiple subtrees. The easy way out is not to focus farther down than + // their smallest common subtree (i.e. `commonSuffix`) - let's see if we can do any better. + if (TreeAdapter.isList((ITree) commonSuffix.get(0))) { + logger.trace("Focus range spans a (partial) list: {}", TreeAdapter.getType((ITree) commonSuffix.get(0))); + return computeListRangeFocus(commonSuffix, startLine, startColumn, endLine, endColumn); + } + + return commonSuffix; + } + + private static IList computeListRangeFocus(final IList commonSuffix, int startLine, int startColumn, int endLine, int endColumn) { + final var parent = (ITree) commonSuffix.get(0); + logger.trace("Computing focus list for {} at range [{}:{}, {}:{}]", TreeAdapter.getType(parent), startLine, startColumn, endLine, endColumn); + final var elements = TreeAdapter.getArgs(parent); + final int nElements = elements.length(); + + logger.trace("Smallest common tree is a {} with {} elements", TreeAdapter.getType(parent), nElements); + if (inside(TreeAdapter.getLocation((ITree) elements.get(0)), startLine, startColumn) && + inside(TreeAdapter.getLocation((ITree) elements.get(nElements - 1)), endLine, endColumn)) { + // The whole list is selected + return commonSuffix; + } + + // Find the elements in the list that are (partially) selected. + final var selected = elements.stream() + .map(ITree.class::cast) + .dropWhile(t -> { + final var l = TreeAdapter.getLocation(t); + // only include layout if the element before it is selected as well + return TreeAdapter.isLayout(t) + ? rightOfBegin(l, startLine, startColumn) + : rightOfEnd(l, startLine, startColumn); + }) + .takeWhile(t -> { + final var l = TreeAdapter.getLocation(t); + // only include layout if the element after it is selected as well + return TreeAdapter.isLayout(t) + ? rightOfEnd(l, endLine, endColumn) + : rightOfBegin(l, endLine, endColumn); + }) + .collect(VF.listWriter()); + + final int nSelected = selected.length(); + + logger.trace("Range covers {} (of {}) elements in the parent list", nSelected, nElements); + final var firstSelected = TreeAdapter.getLocation((ITree) selected.get(0)); + final var lastSelected = TreeAdapter.getLocation((ITree) selected.get(nSelected - 1)); + + final int totalLength = lastSelected.getOffset() - firstSelected.getOffset() + lastSelected.getLength(); + final var selectionLoc = VF.sourceLocation(firstSelected, firstSelected.getOffset(), totalLength, + firstSelected.getBeginLine(), lastSelected.getEndLine(), firstSelected.getBeginColumn(), lastSelected.getEndColumn()); + final var artificialParent = TreeAdapter.setLocation(VF.appl(TreeAdapter.getProduction(parent), selected), selectionLoc); + + // Build new focus list + return commonSuffix.insert(artificialParent); + } } diff --git a/rascal-lsp/src/main/rascal/library/demo/lang/pico/Extensions.rsc b/rascal-lsp/src/main/rascal/library/demo/lang/pico/Extensions.rsc index a26e1088b..dace4209d 100644 --- a/rascal-lsp/src/main/rascal/library/demo/lang/pico/Extensions.rsc +++ b/rascal-lsp/src/main/rascal/library/demo/lang/pico/Extensions.rsc @@ -38,5 +38,5 @@ syntax Expression = call: Id id "(" {Expression ","}* args ")" ; -str typeOf((IdType) `(<{IdType ","}* args>): := `) - = "(): "; +str typeName((IdType) `(<{IdType ","}* args>): := `) + = "(): "; diff --git a/rascal-lsp/src/main/rascal/library/demo/lang/pico/LanguageServer.rsc b/rascal-lsp/src/main/rascal/library/demo/lang/pico/LanguageServer.rsc index e72c331b2..d178f52c4 100644 --- a/rascal-lsp/src/main/rascal/library/demo/lang/pico/LanguageServer.rsc +++ b/rascal-lsp/src/main/rascal/library/demo/lang/pico/LanguageServer.rsc @@ -33,17 +33,22 @@ The core functionality of this module is built upon these concepts: } module demo::lang::pico::LanguageServer -import util::LanguageServer; -import util::IDEServices; -import ParseTree; -import util::ParseErrorRecovery; -import util::Reflective; extend demo::lang::pico::Extensions; + import DateTime; import IO; import List; import Location; +import ParseTree; import String; +import analysis::diff::edits::HiFiLayoutDiff; +import lang::box::util::Box2Text; +import lang::pico::format::Formatting; +import util::Formatters; +import util::IDEServices; +import util::LanguageServer; +import util::ParseErrorRecovery; +import util::Reflective; private Tree (str _input, loc _origin) picoParser(bool allowRecovery) { return ParseTree::parser(#start[Program], allowRecovery=allowRecovery, filters=allowRecovery ? {createParseErrorFilter(false)} : {}); @@ -67,7 +72,8 @@ set[LanguageService] picoLanguageServer() = { didRenameFiles(picoFileRenameService), selectionRange(picoSelectionRangeService), callHierarchy(picoPrepareCallHierarchy, picoCallsService), - completion(picoCompletionService, additionalTriggerCharacters = ["="]) + completion(picoCompletionService, additionalTriggerCharacters = ["="]), + formatting(picoFormattingService) }; @synopsis{This set of contributions runs slower but provides more detail.} @@ -100,16 +106,15 @@ Summary picoBuildService(loc l, start[Program] input) = picoSummaryService(l, in @synopsis{A simple "enum" data type for switching between analysis modes} data PicoSummarizerMode - = analyze() - | build() + = analyze() | build() ; rel[DocumentSymbolKind, loc, Id, str] findDefinitions(Tree input, bool funcScope = false) { rel[DocumentSymbolKind, loc, Id, str] defs = {}; top-down-break visit (input) { - case var:(IdType) `: `: defs += ; + case var:(IdType) `: `: defs += ; case func:(IdType) `(<{IdType ","}* args>): := `: { - defs += ; + defs += ; defs += findDefinitions(args, funcScope = true); } } @@ -259,11 +264,11 @@ CallHierarchyItem callHierarchyItem(start[Program] prog, Id id, loc decl, str tp = callHierarchyItem("", function(), decl, id.src, detail = tp, \data = \data(prog)); CallHierarchyItem callHierarchyItem(start[Program] prog, d:(IdType) `(<{IdType ","}* _>): := `) - = callHierarchyItem("", function(), d.src, id.src, detail = typeOf(d), \data = \data(prog)); + = callHierarchyItem("", function(), d.src, id.src, detail = typeName(d), \data = \data(prog)); data CallHierarchyData = \data(start[Program] prog); -str typeOf((IdType) `: `) = ""; +str typeName((IdType) `: `) = ""; lrel[CallHierarchyItem, loc] picoCallsService(CallHierarchyItem ci, CallDirection dir) { s = picoSummaryService(ci.\data.prog.src.top, ci.\data.prog, analyze()); @@ -303,7 +308,7 @@ list[CompletionItem] picoCompletionService(Focus focus, int cursorOffset, Comple e = isTypingId && !trigger is character ? completionEdit(t.src.begin.column, cc, t.src.end.column, name) : completionEdit(cc, cc, cc, name); - items += completionItem(def is function ? function() : variable(), e, name, labelDetail = ": "); + items += completionItem(def is function ? function() : variable(), e, name, labelDetail = ": "); } } } @@ -311,6 +316,15 @@ list[CompletionItem] picoCompletionService(Focus focus, int cursorOffset, Comple return sort(items, bool(CompletionItem i1, CompletionItem i2) {return i1.label < i2.label; }); } +@synopsis{Whole file formatting for Pico} +list[TextEdit] picoFormattingService([start[Program] tr], FormattingOptions opts) + = treeEdits(#start[Program], toBox, opts=opts)(tr); + +@synopsis{Selection formatting for Pico} +list[TextEdit] picoFormattingService([Tree selection, *_], FormattingOptions opts) + = subTreeEdits(#start[Program], toBox, opts=opts)(selection); + + @synopsis{The main function registers the Pico language with the IDE} @description{ Register the Pico language and the contributions that supply the IDE with features. diff --git a/rascal-lsp/src/main/rascal/library/demo/lang/pico/examples/fac.pico b/rascal-lsp/src/main/rascal/library/demo/lang/pico/examples/fac.pico index ad231294e..394e6968e 100644 --- a/rascal-lsp/src/main/rascal/library/demo/lang/pico/examples/fac.pico +++ b/rascal-lsp/src/main/rascal/library/demo/lang/pico/examples/fac.pico @@ -1,12 +1,12 @@ -begin - declare - input : natural, - output : natural, +begin + declare + input : natural, + output : natural, repnr : natural, rep : natural; - + input := 6; - while input - 1 do + while input - 1 do rep := output; repnr := input; while repnr - 1 do @@ -15,4 +15,4 @@ begin od; input := input - 1 od -end \ No newline at end of file +end diff --git a/rascal-lsp/src/main/rascal/library/demo/lang/pico/examples/ite.pico b/rascal-lsp/src/main/rascal/library/demo/lang/pico/examples/ite.pico index 8e0699861..0557e7bab 100644 --- a/rascal-lsp/src/main/rascal/library/demo/lang/pico/examples/ite.pico +++ b/rascal-lsp/src/main/rascal/library/demo/lang/pico/examples/ite.pico @@ -1,13 +1,12 @@ -begin -declare - input : natural, - output : natural; - - input := 0; - output := 1; - if input then - output := 1 - else - output := 2 - fi -end \ No newline at end of file +begin + declare + input : natural, output : natural + ; + input := 0; + output := 1; + if input then + output := 1 + else + output := 2 + fi +end diff --git a/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc b/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc index e6803426a..70a29f4f8 100644 --- a/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc +++ b/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc @@ -41,10 +41,11 @@ module util::LanguageServer import util::Reflective; extend analysis::diff::edits::AnnotatedTextEdits; +import Exception; import IO; -import ParseTree; import Message; -import Exception; +import ParseTree; +import util::Formatters; @synopsis{Definition of a language server by its meta-data.} @description{ @@ -232,7 +233,8 @@ hover documentation, definition with uses, references to declarations, implement * To keep this API simple, we have left out support for incomplete (partial) completions, so "CompletionList.isIncomplete" will always be set to false. * Again to keep the API simple we have not implemented support for defaults, so CompletionItem, edit range (and commitCharacters if you want them) must be set explicitly on each CompletionItem. -Note: Depending on the capabilities of the client, we will generate "InsertReplaceEdit" items or "TextEdit" items. + Note: Depending on the capabilities of the client, we will generate "InsertReplaceEdit" items or "TextEdit" items. +* The ((formatting)) service determines what edits to do to format (part of) a file. The `range` parameter determines what part of the file to format. For whole-file formatting, `_tree.top == range`. ((util::Formatters::FormattingOptions)) influence how formatting treats whitespace. Many services receive a ((Focus)) parameter. The focus lists the syntactical constructs under the current cursor, from the current leaf all the way up to the root of the tree. This list helps to create functionality that is syntax-directed, and always relevant to the @@ -302,6 +304,7 @@ data LanguageService list[CallHierarchyItem] (Focus _focus) prepareService, lrel[CallHierarchyItem item, loc call] (CallHierarchyItem _ci, CallDirection _dir) callsService) | completion(list[CompletionItem] (Focus _focus, int cursorOffset, CompletionTrigger trigger) completionService, list[str] additionalTriggerCharacters = []) + | formatting (list[TextEdit](Focus _focus, FormattingOptions _opts) formattingService) ; @description{ diff --git a/rascal-lsp/src/main/rascal/lsp/lang/rascal/lsp/Formatter.rsc b/rascal-lsp/src/main/rascal/lsp/lang/rascal/lsp/Formatter.rsc new file mode 100644 index 000000000..f53eeff2a --- /dev/null +++ b/rascal-lsp/src/main/rascal/lsp/lang/rascal/lsp/Formatter.rsc @@ -0,0 +1,49 @@ +@license{ +Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +} +module lang::rascal::lsp::Formatter + +import ParseTree; +import analysis::diff::edits::TextEdits; +import lang::rascal::\syntax::Rascal; +import lang::rascal::format::Rascal; +import util::Formatters; + +// TODO:need to provide `opts` with the function generated by `treeEdits` rather +// than giving it to `treeEdits`, so we can reuse the optimized formatter pipeline +// again and again. +@synopsis{Whole file formatter for Rascal} +list[TextEdit] rascalFormattingService([start[Module] top], FormattingOptions opts) + = treeEdits(#start[Module], toBox, opts = opts)(top); + +@synopsis{Range formatter for Rascal} +default list[TextEdit] rascalFormattingService([*_, Tree selected, *_], FormattingOptions opts) + = subTreeEdits(#start[Module], toBox, opts = opts)(selected) when isSort(selected); + +private bool isSort(sort(_)) = true; +private bool isSort(label(str _, sort(_))) = true; +private default bool isSort(Symbol _) = false; + diff --git a/rascal-lsp/src/test/java/engineering/swat/rascal/lsp/util/TreeSearchTests.java b/rascal-lsp/src/test/java/engineering/swat/rascal/lsp/util/TreeSearchTests.java new file mode 100644 index 000000000..762d59e35 --- /dev/null +++ b/rascal-lsp/src/test/java/engineering/swat/rascal/lsp/util/TreeSearchTests.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2018-2025, NWO-I CWI and Swat.engineering + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package engineering.swat.rascal.lsp.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.rascalmpl.vscode.lsp.util.locations.impl.TreeSearch.computeFocusList; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.rascalmpl.values.IRascalValueFactory; +import org.rascalmpl.values.parsetrees.ITree; +import org.rascalmpl.values.parsetrees.TreeAdapter; +import org.rascalmpl.vscode.lsp.util.RascalServices; + +public class TreeSearchTests { + private static final IRascalValueFactory VF = IRascalValueFactory.getInstance(); + private static final String URI = "unknown:///"; + private static final String CONTENTS = fromLines( + "module TreeTest" // 1 + , "" // 2 + , "int f() {" // 3 + , " int x = 8;" // 4 + , " int y = 54;" // 5 + , " int z = -1;" // 6 + , "" // 7 + , " return x + y + z;" // 8 + , "}" // 9 + ); + + private static ITree tree; + + private static String fromLines(String... lines) { + final var builder = new StringBuilder(); + for (var line : lines) { + builder.append(line); + builder.append("\n"); + } + return builder.toString(); + } + + @BeforeClass + public static void setUpSuite() { + tree = RascalServices.parseRascalModule(VF.sourceLocation(URI), CONTENTS.toCharArray()); + } + + @Test + public void focusStartsWithLexical() { + final var focus = computeFocusList(tree, 8, 13); + final var first = (ITree) focus.get(0); + assertTrue(TreeAdapter.isLexical(first)); + } + + @Test + public void focusEndsWithModule() { + final var focus = computeFocusList(tree, 6, 4); + final var last = (ITree) focus.get(focus.length() - 1); + assertEquals(tree, last); + } + + @Test + public void listRangePartial() { + final var focus = computeFocusList(tree, 5, 8, 6, 8); + final var selection = (ITree) focus.get(0); + final var originalList = (ITree) focus.get(1); + + assertValidListWithLength(selection, 2); + assertValidListWithLength(originalList, 4); + } + + @Test + public void listRangeStartsWithWhitespace() { + final var focus = computeFocusList(tree, 7, 0, 8, 15); + final var selection = (ITree) focus.get(0); + final var originalList = (ITree) focus.get(1); + + assertValidListWithLength(selection, 1); + assertValidListWithLength(originalList, 4); + } + + @Test + public void listRangeEndsWithWhitespace() { + final var focus = computeFocusList(tree, 4, 3, 7, 0); + final var selection = (ITree) focus.get(0); + final var originalList = (ITree) focus.get(1); + + assertValidListWithLength(selection, 3); + assertValidListWithLength(originalList, 4); + } + + private static void assertValidListWithLength(final ITree list, int length) { + assertTrue(String.format("Not a list: %s", TreeAdapter.getType(list)), TreeAdapter.isList(list)); + assertEquals(TreeAdapter.yield(list), length, TreeAdapter.getListASTArgs(list).size()); + + // assert no layout padding + final var args = TreeAdapter.getArgs(list); + assertFalse("List tree malformed: starts with layout", TreeAdapter.isLayout((ITree) args.get(0))); + assertFalse("List tree malformed: ends with layout", TreeAdapter.isLayout((ITree) args.get(args.length() - 1))); + } +} diff --git a/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts b/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts index 998b504e1..e1a0e4fbd 100644 --- a/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts +++ b/rascal-vscode-extension/src/test/vscode-suite/dsl.test.ts @@ -25,7 +25,7 @@ * POSSIBILITY OF SUCH DAMAGE. */ -import { InputBox, SideBarView, TextEditor, VSBrowser, WebDriver, Workbench } from 'vscode-extension-tester'; +import { InputBox, Key, SideBarView, TextEditor, VSBrowser, WebDriver, Workbench } from 'vscode-extension-tester'; import { Delays, expectCompletions, IDEOperations, ignoreFails, printRascalOutputOnFailure, RascalREPL, sleep, TestWorkspace } from './utils'; import { expect } from 'chai'; @@ -356,4 +356,40 @@ end const actualJson = await resultEditor!.getText(); expect(JSON.parse(actualJson)).to.deep.equal(JSON.parse(expectedJson)); }); + + it("formatting files works", async function() { + const editor = await ide.openModule(TestWorkspace.picoFile); + + if (errorRecovery) { + // introduce a parse error + const lineText = (await editor.getTextAtLine(3)).trimEnd(); + await editor.setTextAtLine(3, lineText.substring(0, lineText.length - 1)); + } + + const unformattedText = await editor.getText(); + await bench.executeCommand("editor.action.formatDocument"); + await driver.wait(async () => unformattedText !== await editor.getText(), Delays.normal, "Document should be formatted"); + }); + + it("formatting selection works", async function() { + const editor = await ide.openModule(TestWorkspace.picoFile); + + // some incorrectly formatted code + await editor.setTextAtLine(3, "x : natural, y : natural,"); + + if (errorRecovery) { + // introduce a parse error + const lineText = (await editor.getTextAtLine(3)).trimEnd(); + await editor.setTextAtLine(3, lineText.substring(0, lineText.length - 1)); + } + + const unformattedText = await editor.getText(); + + // select line + await editor.moveCursor(3, 1); + await editor.safeSendKeys(Key.SHIFT, Key.END); + + await bench.executeCommand("editor.action.formatSelection"); + await driver.wait(async () => unformattedText !== await editor.getSelectedText(), Delays.normal, "Selection should be formatted"); + }); });