From 630ccc4d7a5aa74ebb7c0a6f09eb93f977e7dcd8 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Fri, 11 Jul 2025 10:43:40 +0200 Subject: [PATCH 01/32] Rascal LSP formatting API and example. --- .../ParametricTextDocumentService.java | 13 ++++++++ .../library/demo/lang/pico/LanguageServer.rsc | 32 ++++++++++++++++++- .../rascal/library/util/LanguageServer.rsc | 11 +++++++ 3 files changed, 55 insertions(+), 1 deletion(-) 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 81af3c3d9..3f3225944 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 @@ -62,6 +62,7 @@ import org.eclipse.lsp4j.DidCloseTextDocumentParams; import org.eclipse.lsp4j.DidOpenTextDocumentParams; import org.eclipse.lsp4j.DidSaveTextDocumentParams; +import org.eclipse.lsp4j.DocumentFormattingParams; import org.eclipse.lsp4j.DocumentSymbol; import org.eclipse.lsp4j.DocumentSymbolParams; import org.eclipse.lsp4j.ExecuteCommandOptions; @@ -100,6 +101,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; @@ -727,6 +729,17 @@ public CompletableFuture>> codeAction(CodeActio return CodeActions.mergeAndConvertCodeActions(this, dedicatedLanguageName, contribs.getName(), quickfixes, codeActions); } + @Override + public CompletableFuture> formatting(DocumentFormattingParams params) { + logger.debug("formatting: {}", params); + + // convert the `FormattingOptions` map to a `formattingOptions` constructor + // call the `formatting` implementation of the relevant language contribution + // with the resulting string and the input tree, compute a list of `TextEdit`s to return + + return CompletableFuture.completedFuture(Collections.emptyList()); + } + 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/rascal/library/demo/lang/pico/LanguageServer.rsc b/rascal-lsp/src/main/rascal/library/demo/lang/pico/LanguageServer.rsc index 1ec7af63b..c43f346f6 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 @@ -40,6 +40,8 @@ import util::ParseErrorRecovery; import util::Reflective; import lang::pico::\syntax::Main; import DateTime; +import IO; +import String; private Tree (str _input, loc _origin) picoParser(bool allowRecovery) { return ParseTree::parser(#start[Program], allowRecovery=allowRecovery, filters=allowRecovery ? {createParseErrorFilter(false)} : {}); @@ -61,9 +63,37 @@ set[LanguageService] picoLanguageServer(bool allowRecovery) = { codeAction(picoCodeActionService), rename(picoRenamingService, prepareRenameService = picoRenamePreparingService), didRenameFiles(picoFileRenameService), - selectionRange(picoSelectionRangeService) + selectionRange(picoSelectionRangeService), + formatting(picoFormattingService) }; +str picoFormattingService(Tree input, formattingOptions(int tabSize, bool insertSpaces, bool trimTrailingWhiteSpace, bool insertFinalNewLine, bool trimFinalNewLines)) { + str formatted = ""; + str linesep = "\n"; + + println("Warning; `tabSize` () is ignored"); + if (insertSpaces) { + println("Warning; `insertSpaces` is ignored"); + } + + str trimLineTrailingWs(/^\s*$/) = nonWhiteSpace; + default str trimLineTrailingWs(/^\s*$/) = ""; + + if (trimTrailingWhiteSpace) { + formatted = intercalate(linesep, [trimLineTrailingWs(l) | l <- split(linesep, formatted)]); + } + + if (trimFinalNewLines, /^\n+$/ := formatted) { + formatted = textLines; + } + + if (insertFinalNewLine && /\n$/ !:= formatted) { + formatted += "\n"; + } + + return formatted; +} + set[LanguageService] picoLanguageServer() = picoLanguageServer(false); set[LanguageService] picoLanguageServerWithRecovery() = picoLanguageServer(true); diff --git a/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc b/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc index 2716255f5..2d34f9aea 100644 --- a/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc +++ b/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc @@ -277,11 +277,22 @@ data LanguageService , loc (Focus _focus) prepareRenameService = defaultPrepareRenameService) | didRenameFiles(tuple[list[DocumentEdit], set[Message]] (list[DocumentEdit] fileRenames) didRenameFilesService) | selectionRange(list[loc](Focus _focus) selectionRangeService) + | formatting (str (Tree _input, FormattingOptions _opts) formattingService) ; loc defaultPrepareRenameService(Focus _:[Tree tr, *_]) = tr.src when tr.src?; default loc defaultPrepareRenameService(Focus focus) { throw IllegalArgument(focus, "Element under cursor does not have source location"); } +data FormattingOptions + // If LSP adds more options, add them as keyword arguments for backward compatibility + = formattingOptions( + int tabSize + , bool insertSpaces + , bool trimTrailingWhiteSpace + , bool insertFinalNewLine + , bool trimFinalNewLines + ); + @deprecated{Backward compatible with ((parsing)).} @synopsis{Construct a `parsing` ((LanguageService))} LanguageService parser(Parser parser) = parsing(parser); From d3675c44fc34d944ed2156538ea32747e40c394e Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 14 Jul 2025 15:26:02 +0200 Subject: [PATCH 02/32] Provide overridable defaults for common string operations. --- .../library/demo/lang/pico/LanguageServer.rsc | 28 +--- .../rascal/library/util/LanguageServer.rsc | 140 ++++++++++++++++-- 2 files changed, 134 insertions(+), 34 deletions(-) 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 c43f346f6..0cd034d72 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 @@ -67,31 +67,15 @@ set[LanguageService] picoLanguageServer(bool allowRecovery) = { formatting(picoFormattingService) }; -str picoFormattingService(Tree input, formattingOptions(int tabSize, bool insertSpaces, bool trimTrailingWhiteSpace, bool insertFinalNewLine, bool trimFinalNewLines)) { - str formatted = ""; - str linesep = "\n"; - - println("Warning; `tabSize` () is ignored"); - if (insertSpaces) { - println("Warning; `insertSpaces` is ignored"); - } - - str trimLineTrailingWs(/^\s*$/) = nonWhiteSpace; - default str trimLineTrailingWs(/^\s*$/) = ""; - - if (trimTrailingWhiteSpace) { - formatted = intercalate(linesep, [trimLineTrailingWs(l) | l <- split(linesep, formatted)]); +str picoFormattingService(Tree input, set[FormattingOption] opts) { + if (tabSize(int tabSize) <- opts) { + println("Warning; `tabSize` () is ignored"); } - - if (trimFinalNewLines, /^\n+$/ := formatted) { - formatted = textLines; - } - - if (insertFinalNewLine && /\n$/ !:= formatted) { - formatted += "\n"; + if (insertSpaces() <- opts) { + println("Warning; `insertSpaces` is ignored"); } - return formatted; + return ""; } set[LanguageService] picoLanguageServer() = picoLanguageServer(false); diff --git a/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc b/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc index 2d34f9aea..cebfe2506 100644 --- a/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc +++ b/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc @@ -41,10 +41,14 @@ module util::LanguageServer import util::Reflective; import analysis::diff::edits::TextEdits; +import Exception; import IO; -import ParseTree; +import Map; import Message; -import Exception; +import ParseTree; +import Set; +import String; +import Type; @synopsis{Definition of a language server by its meta-data.} @description{ @@ -277,21 +281,133 @@ data LanguageService , loc (Focus _focus) prepareRenameService = defaultPrepareRenameService) | didRenameFiles(tuple[list[DocumentEdit], set[Message]] (list[DocumentEdit] fileRenames) didRenameFilesService) | selectionRange(list[loc](Focus _focus) selectionRangeService) - | formatting (str (Tree _input, FormattingOptions _opts) formattingService) + | formatting (str(Tree _input, set[FormattingOption] _opts) formattingService, + str(str) trimTrailingWhiteSpace = defaultTrimTrailingWhiteSpace, + str(str) insertFinalNewLine = defaultInsertFinalNewLine, + str(str) trimFinalNewLines = defaultTrimFinalNewLines) ; loc defaultPrepareRenameService(Focus _:[Tree tr, *_]) = tr.src when tr.src?; default loc defaultPrepareRenameService(Focus focus) { throw IllegalArgument(focus, "Element under cursor does not have source location"); } -data FormattingOptions - // If LSP adds more options, add them as keyword arguments for backward compatibility - = formattingOptions( - int tabSize - , bool insertSpaces - , bool trimTrailingWhiteSpace - , bool insertFinalNewLine - , bool trimFinalNewLines - ); +data FormattingOption + = tabSize(int) + | insertSpaces() + | trimTrailingWhiteSpace() + | insertFinalNewLine() + | trimFinalNewLines() + ; + +set[str] newLineCharacters = { + "\u000A", // LF + "\u000B", // VT + "\u000C", // FF + "\u000D", // CR + "\u000D\u000A", // CRLF + "\u0085", // NEL + "\u2028", // LS + "\u2029" // PS +}; + +private bool bySize(str a, str b) = size(a) > size(b); + +str mostUsedNewline(str input, set[str] lineseps = newLineCharacters, str(set[str]) tieBreaker = getFirstFrom) { + linesepCounts = (nl: 0 | nl <- lineseps); + for (nl <- reverse(sort(lineseps, bySize))) { + int count = size(findAll(input, nl)); + linesepCounts[nl] = count; + // subtract all occurrences of substrings that we counted before + for (str snl <- substrings(nl), linesepCounts[snl]?) { + linesepCounts[snl] = linesepCounts[snl] - count; + } + + } + byCount = invert(linesepCounts); + return tieBreaker(byCount[max(domain(byCount))]); +} + +set[str] substrings(str input) + = {input[i..i+l] | int i <- [0..size(input)], int l <- [1..size(input)], i + l <= size(input)}; + +test bool mostUsedNewlineTestMixed() + = mostUsedNewline("\r\n\n\r\n\t\t\t\t") == "\r\n"; + +test bool mostUsedNewlineTestTie() + = mostUsedNewline("\n\n\r\n\r\n") == "\n"; + +test bool mostUsedNewlineTestGreedy() + = mostUsedNewline("\r\n\r\n\n") == "\r\n"; + +str defaultInsertFinalNewLine(str input, set[str] lineseps = newLineCharacters) + = any(nl <- lineseps, endsWith(input, nl)) + ? input + : input + mostUsedNewline(input) + ; + +test bool defaultInsertFinalNewLineTestSimple() + = defaultInsertFinalNewLine("a\nb") + == "a\nb\n"; + +test bool defaultInsertFinalNewLineTestNoop() + = defaultInsertFinalNewLine("a\nb\n") + == "a\nb\n"; + +test bool defaultInsertFinalNewLineTestMixed() + = defaultInsertFinalNewLine("a\nb\r\n") + == "a\nb\r\n"; + +str defaultTrimFinalNewLines(str input, set[str] lineseps = newLineCharacters) { + orderedSeps = sort(lineseps, bySize); + while (nl <- orderedSeps, endsWith(input, nl)) { + input = input[0..-size(nl)]; + } + return input; +} + +test bool defaultTrimFinalNewLinesTestSimple() + = defaultTrimFinalNewLines("a\n\n\n") == "a"; + +test bool defaultTrimFinalNewLinesTestEndOnly() + = defaultTrimFinalNewLines("a\n\n\nb\n\n") == "a\n\n\nb"; + +test bool defaultTrimFinalNewLinesTestWhiteSpace() + = defaultTrimFinalNewLines("a\n\n\nb\n\n ") == "a\n\n\nb\n\n "; + +str perLine(str input, str(str) lineFunc, set[str] lineseps = newLineCharacters) { + orderedSeps = sort(lineseps, bySize); + + str result = ""; + int next = 0; + for (int i <- [0..size(input)]) { + // greedily match line separators (longest first) + if (i >= next, str nl <- orderedSeps, nl == input[i..i+size(nl)]) { + line = input[next..i]; + result += lineFunc(line) + nl; + next = i + size(nl); // skip to the start of the next line + } + } + + // last line + if (str nl <- orderedSeps, nl == input[-size(nl)..]) { + line = input[next..next+size(nl)]; + result += lineFunc(line); + } + + return result; +} + +test bool perLineTest() + = perLine("a\nb\r\nc\n\r\n", str(str line) { return line + "x"; }) == "ax\nbx\r\ncx\nx\r\nx"; + +str defaultTrimTrailingWhiteSpace(str input) { + str trimLineTrailingWs(/^\s*$/) = nonWhiteSpace; + default str trimLineTrailingWs(/^\s*$/) = ""; + + return perLine(input, trimLineTrailingWs); +} + +test bool defaultTrimTrailingWhiteSpaceTest() + = defaultTrimTrailingWhiteSpace("a \nb\t\n c \n") == "a\nb\n c\n"; @deprecated{Backward compatible with ((parsing)).} @synopsis{Construct a `parsing` ((LanguageService))} From b32d2f692a0067e3434767a87b6c473698044b61 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 14 Jul 2025 19:06:28 +0200 Subject: [PATCH 03/32] Align names with `DocumentFormattingParams` --- .../rascal/library/util/LanguageServer.rsc | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc b/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc index cebfe2506..061530fd7 100644 --- a/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc +++ b/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc @@ -282,9 +282,9 @@ data LanguageService | didRenameFiles(tuple[list[DocumentEdit], set[Message]] (list[DocumentEdit] fileRenames) didRenameFilesService) | selectionRange(list[loc](Focus _focus) selectionRangeService) | formatting (str(Tree _input, set[FormattingOption] _opts) formattingService, - str(str) trimTrailingWhiteSpace = defaultTrimTrailingWhiteSpace, - str(str) insertFinalNewLine = defaultInsertFinalNewLine, - str(str) trimFinalNewLines = defaultTrimFinalNewLines) + str(str) trimTrailingWhitespace = defaultTrimTrailingWhitespace, + str(str) insertFinalNewline = defaultInsertFinalNewline, + str(str) trimFinalNewlines = defaultTrimFinalNewlines) ; loc defaultPrepareRenameService(Focus _:[Tree tr, *_]) = tr.src when tr.src?; @@ -293,9 +293,9 @@ default loc defaultPrepareRenameService(Focus focus) { throw IllegalArgument(foc data FormattingOption = tabSize(int) | insertSpaces() - | trimTrailingWhiteSpace() - | insertFinalNewLine() - | trimFinalNewLines() + | trimTrailingWhitespace() + | insertFinalNewline() + | trimFinalNewlines() ; set[str] newLineCharacters = { @@ -338,25 +338,25 @@ test bool mostUsedNewlineTestTie() test bool mostUsedNewlineTestGreedy() = mostUsedNewline("\r\n\r\n\n") == "\r\n"; -str defaultInsertFinalNewLine(str input, set[str] lineseps = newLineCharacters) +str defaultInsertFinalNewline(str input, set[str] lineseps = newLineCharacters) = any(nl <- lineseps, endsWith(input, nl)) ? input : input + mostUsedNewline(input) ; -test bool defaultInsertFinalNewLineTestSimple() - = defaultInsertFinalNewLine("a\nb") +test bool defaultInsertFinalNewlineTestSimple() + = defaultInsertFinalNewline("a\nb") == "a\nb\n"; -test bool defaultInsertFinalNewLineTestNoop() - = defaultInsertFinalNewLine("a\nb\n") +test bool defaultInsertFinalNewlineTestNoop() + = defaultInsertFinalNewline("a\nb\n") == "a\nb\n"; -test bool defaultInsertFinalNewLineTestMixed() - = defaultInsertFinalNewLine("a\nb\r\n") +test bool defaultInsertFinalNewlineTestMixed() + = defaultInsertFinalNewline("a\nb\r\n") == "a\nb\r\n"; -str defaultTrimFinalNewLines(str input, set[str] lineseps = newLineCharacters) { +str defaultTrimFinalNewlines(str input, set[str] lineseps = newLineCharacters) { orderedSeps = sort(lineseps, bySize); while (nl <- orderedSeps, endsWith(input, nl)) { input = input[0..-size(nl)]; @@ -364,14 +364,14 @@ str defaultTrimFinalNewLines(str input, set[str] lineseps = newLineCharacters) { return input; } -test bool defaultTrimFinalNewLinesTestSimple() - = defaultTrimFinalNewLines("a\n\n\n") == "a"; +test bool defaultTrimFinalNewlinesTestSimple() + = defaultTrimFinalNewlines("a\n\n\n") == "a"; -test bool defaultTrimFinalNewLinesTestEndOnly() - = defaultTrimFinalNewLines("a\n\n\nb\n\n") == "a\n\n\nb"; +test bool defaultTrimFinalNewlinesTestEndOnly() + = defaultTrimFinalNewlines("a\n\n\nb\n\n") == "a\n\n\nb"; -test bool defaultTrimFinalNewLinesTestWhiteSpace() - = defaultTrimFinalNewLines("a\n\n\nb\n\n ") == "a\n\n\nb\n\n "; +test bool defaultTrimFinalNewlinesTestWhiteSpace() + = defaultTrimFinalNewlines("a\n\n\nb\n\n ") == "a\n\n\nb\n\n "; str perLine(str input, str(str) lineFunc, set[str] lineseps = newLineCharacters) { orderedSeps = sort(lineseps, bySize); @@ -399,15 +399,15 @@ str perLine(str input, str(str) lineFunc, set[str] lineseps = newLineCharacters) test bool perLineTest() = perLine("a\nb\r\nc\n\r\n", str(str line) { return line + "x"; }) == "ax\nbx\r\ncx\nx\r\nx"; -str defaultTrimTrailingWhiteSpace(str input) { +str defaultTrimTrailingWhitespace(str input) { str trimLineTrailingWs(/^\s*$/) = nonWhiteSpace; default str trimLineTrailingWs(/^\s*$/) = ""; return perLine(input, trimLineTrailingWs); } -test bool defaultTrimTrailingWhiteSpaceTest() - = defaultTrimTrailingWhiteSpace("a \nb\t\n c \n") == "a\nb\n c\n"; +test bool defaultTrimTrailingWhitespaceTest() + = defaultTrimTrailingWhitespace("a \nb\t\n c \n") == "a\nb\n c\n"; @deprecated{Backward compatible with ((parsing)).} @synopsis{Construct a `parsing` ((LanguageService))} From f16e17a4a42d6f59243f5459150a8818c0e06b39 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 14 Jul 2025 19:09:09 +0200 Subject: [PATCH 04/32] Implement parametric formatting. --- .../parametric/ILanguageContributions.java | 3 ++ .../InterpretedLanguageContributions.java | 22 +++++++++++++ .../LanguageContributionsMultiplexer.java | 14 ++++++++ .../ParametricTextDocumentService.java | 33 ++++++++++++++++--- .../parametric/ParserOnlyContribution.java | 10 ++++++ .../lsp/parametric/model/RascalADTs.java | 1 + .../vscode/lsp/util/DocumentChanges.java | 2 +- .../rascal/library/util/LanguageServer.rsc | 12 +++++++ 8 files changed, 92 insertions(+), 5 deletions(-) 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 e26fb3fc2..5cb9e1666 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 @@ -34,6 +34,7 @@ import org.rascalmpl.values.IRascalValueFactory; import org.rascalmpl.values.parsetrees.ITree; import org.rascalmpl.vscode.lsp.util.concurrent.InterruptibleFuture; + import io.usethesource.vallang.IConstructor; import io.usethesource.vallang.IList; import io.usethesource.vallang.ISet; @@ -60,6 +61,7 @@ public interface ILanguageContributions { public InterruptibleFuture implementation(IList focus); public InterruptibleFuture codeAction(IList focus); public InterruptibleFuture selectionRange(IList focus); + public InterruptibleFuture formatting(ITree input, ISet formattingOptions); public InterruptibleFuture prepareRename(IList focus); public InterruptibleFuture rename(IList focus, String name); @@ -81,6 +83,7 @@ public interface ILanguageContributions { public CompletableFuture hasCodeAction(); public CompletableFuture hasDidRenameFiles(); public CompletableFuture hasSelectionRange(); + public CompletableFuture hasFormatting(); 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 6b9968cc3..ec6579f8a 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 @@ -26,11 +26,14 @@ */ package org.rascalmpl.vscode.lsp.parametric; +import static org.rascalmpl.vscode.lsp.util.EvaluatorUtil.runEvaluator; + import java.io.IOException; import java.io.StringReader; import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.Nullable; @@ -52,6 +55,7 @@ import org.rascalmpl.vscode.lsp.util.EvaluatorUtil; import org.rascalmpl.vscode.lsp.util.EvaluatorUtil.LSPContext; import org.rascalmpl.vscode.lsp.util.concurrent.InterruptibleFuture; + import io.usethesource.vallang.IBool; import io.usethesource.vallang.IConstructor; import io.usethesource.vallang.IList; @@ -93,6 +97,7 @@ public class InterpretedLanguageContributions implements ILanguageContributions private final CompletableFuture<@Nullable IFunction> rename; private final CompletableFuture<@Nullable IFunction> didRenameFiles; private final CompletableFuture<@Nullable IFunction> selectionRange; + private final CompletableFuture<@Nullable IConstructor> formatting; private final CompletableFuture hasAnalysis; private final CompletableFuture hasBuild; @@ -108,6 +113,7 @@ public class InterpretedLanguageContributions implements ILanguageContributions private final CompletableFuture hasRename; private final CompletableFuture hasDidRenameFiles; private final CompletableFuture hasSelectionRange; + private final CompletableFuture hasFormatting; private final CompletableFuture specialCaseHighlighting; @@ -154,6 +160,7 @@ public InterpretedLanguageContributions(LanguageParameter lang, IBaseTextDocumen this.rename = getFunctionFor(contributions, LanguageContributions.RENAME); this.didRenameFiles = getFunctionFor(contributions, LanguageContributions.DID_RENAME_FILES); this.selectionRange = getFunctionFor(contributions, LanguageContributions.SELECTION_RANGE); + this.formatting = contributions.thenApply(contrib -> getContribution(contrib, LanguageContributions.FORMATTING)); // assign boolean properties once instead of wasting futures all the time this.hasAnalysis = nonNull(this.analysis); @@ -170,6 +177,7 @@ public InterpretedLanguageContributions(LanguageParameter lang, IBaseTextDocumen this.hasRename = nonNull(this.rename); this.hasDidRenameFiles = nonNull(this.didRenameFiles); this.hasSelectionRange = nonNull(this.selectionRange); + this.hasFormatting = nonNull(this.formatting); this.specialCaseHighlighting = getContributionParameter(contributions, LanguageContributions.PARSING, @@ -389,6 +397,15 @@ public InterruptibleFuture selectionRange(IList focus) { return execFunction(LanguageContributions.SELECTION_RANGE, selectionRange, VF.list(), focus); } + @Override + public InterruptibleFuture formatting(ITree input, ISet formattingOptions) { + debug(LanguageContributions.FORMATTING, input != null ? TreeAdapter.getLocation(input) : null, formattingOptions.size()); + return InterruptibleFuture.flatten(formatting.thenApply(formattingContrib -> + runEvaluator(LanguageContributions.FORMATTING, eval, actualEval -> + (IList) actualEval.call(actualEval.getMonitor(), "util::LanguageServer", "formattingWrapper", input, formattingOptions, formattingContrib) + , VF.list(), exec, true, client)), exec); + } + private void debug(String name, Object param) { logger.debug("{}({})", name, param); } @@ -457,6 +474,11 @@ public CompletableFuture hasSelectionRange() { return hasSelectionRange; } + @Override + public CompletableFuture hasFormatting() { + return hasFormatting; + } + @Override public CompletableFuture hasAnalysis() { return hasAnalysis; 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 4917cf8ac..10e70a7d2 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 @@ -70,6 +70,7 @@ private static final CompletableFuture failedInitialization() { private volatile CompletableFuture rename = failedInitialization(); private volatile CompletableFuture didRenameFiles = failedInitialization(); private volatile CompletableFuture selectionRange = failedInitialization(); + private volatile CompletableFuture formatting = failedInitialization(); private volatile CompletableFuture hasAnalysis = failedInitialization(); private volatile CompletableFuture hasBuild = failedInitialization(); @@ -85,6 +86,7 @@ private static final CompletableFuture failedInitialization() { private volatile CompletableFuture hasRename = failedInitialization(); private volatile CompletableFuture hasDidRenameFiles = failedInitialization(); private volatile CompletableFuture hasSelectionRange = failedInitialization(); + private volatile CompletableFuture hasFormatting = failedInitialization(); private volatile CompletableFuture specialCaseHighlighting = failedInitialization(); @@ -162,6 +164,7 @@ private synchronized void calculateRouting() { prepareRename = findFirstOrDefault(ILanguageContributions::hasRename); didRenameFiles = findFirstOrDefault(ILanguageContributions::hasDidRenameFiles); selectionRange = findFirstOrDefault(ILanguageContributions::hasSelectionRange); + formatting = findFirstOrDefault(ILanguageContributions::hasFormatting); hasAnalysis = anyTrue(ILanguageContributions::hasAnalysis); hasBuild = anyTrue(ILanguageContributions::hasBuild); @@ -177,6 +180,7 @@ private synchronized void calculateRouting() { hasDidRenameFiles = anyTrue(ILanguageContributions::hasDidRenameFiles); hasCodeAction = anyTrue(ILanguageContributions::hasCodeAction); hasSelectionRange = anyTrue(ILanguageContributions::hasSelectionRange); + hasFormatting = anyTrue(ILanguageContributions::hasFormatting); // Always use the special-case highlighting status of *the first* // contribution (possibly using the default value in the Rascal ADT if @@ -337,6 +341,16 @@ public InterruptibleFuture selectionRange(IList focus) { return flatten(selectionRange, c -> c.selectionRange(focus)); } + @Override + public InterruptibleFuture formatting(ITree input, ISet formattingOptions) { + return flatten(formatting, c -> c.formatting(input, formattingOptions)); + } + + @Override + public CompletableFuture hasFormatting() { + return hasFormatting; + } + @Override public CompletableFuture hasCodeAction() { return hasCodeAction; 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 3f3225944..9eab14aae 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 @@ -34,6 +34,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; @@ -69,6 +70,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; @@ -143,6 +145,7 @@ import io.usethesource.vallang.IConstructor; import io.usethesource.vallang.IList; import io.usethesource.vallang.ISet; +import io.usethesource.vallang.ISetWriter; import io.usethesource.vallang.ISourceLocation; import io.usethesource.vallang.IString; import io.usethesource.vallang.ITuple; @@ -238,6 +241,7 @@ public void initializeServerCapabilities(ServerCapabilities result) { result.setCodeLensProvider(new CodeLensOptions(false)); result.setRenameProvider(new RenameOptions(true)); result.setExecuteCommandProvider(new ExecuteCommandOptions(Collections.singletonList(getRascalMetaCommandName()))); + result.setDocumentFormattingProvider(true); result.setInlayHintProvider(true); result.setSelectionRangeProvider(true); result.setFoldingRangeProvider(true); @@ -733,11 +737,32 @@ public CompletableFuture>> codeAction(CodeActio public CompletableFuture> formatting(DocumentFormattingParams params) { logger.debug("formatting: {}", params); - // convert the `FormattingOptions` map to a `formattingOptions` constructor - // call the `formatting` implementation of the relevant language contribution - // with the resulting string and the input tree, compute a list of `TextEdit`s to return + final ILanguageContributions contribs = contributions(params.getTextDocument()); - return CompletableFuture.completedFuture(Collections.emptyList()); + // convert the `FormattingOptions` map to a `set[FormattingOption]` + ISet optSet = getFormattingOptions(params.getOptions()); + // call the `formatting` implementation of the relevant language contribution + return getFile(params.getTextDocument()) + .getCurrentTreeAsync() + .thenApply(Versioned::get) + .thenCompose(tree -> contribs.formatting(tree, optSet).get()) + // convert the document changes + .thenApply(l -> DocumentChanges.translateTextEdits(this, l, Map.of())); + } + + private ISet getFormattingOptions(FormattingOptions options) { + var optionType = tf.abstractDataType(typeStore, "FormattingOption"); + + ISetWriter opts = VF.setWriter(); + for (Entry> e : options.entrySet()) { + var opt = e.getValue().map( + s -> VF.constructor(tf.constructor(typeStore, optionType, e.getKey(), tf.stringType()), VF.string(s)), + n -> VF.constructor(tf.constructor(typeStore, optionType, e.getKey(), tf.integerType()), VF.integer(n.longValue())), + b -> VF.constructor(tf.constructor(typeStore, optionType, e.getKey())) + ); + opts.append(opt); + } + return opts.done(); } private CompletableFuture computeCodeActions(final ILanguageContributions contribs, final int startLine, final int startColumn, ITree tree) { diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParserOnlyContribution.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParserOnlyContribution.java index 3b0f6cdcb..72954eb65 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParserOnlyContribution.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParserOnlyContribution.java @@ -189,6 +189,16 @@ public InterruptibleFuture codeAction(IList focus) { return InterruptibleFuture.completedFuture(VF.list()); } + @Override + public InterruptibleFuture formatting(ITree input, ISet formattingOptions) { + return InterruptibleFuture.completedFuture(VF.list()); + } + + @Override + public CompletableFuture hasFormatting() { + return CompletableFuture.completedFuture(false); + } + @Override public InterruptibleFuture implementation(IList focus) { return InterruptibleFuture.completedFuture(VF.set()); 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 09c90f8a3..931f1549a 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 @@ -48,6 +48,7 @@ private LanguageContributions () {} public static final String IMPLEMENTATION = "implementation"; public static final String CODE_ACTION = "codeAction"; public static final String SELECTION_RANGE = "selectionRange"; + 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/util/DocumentChanges.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentChanges.java index 3622d5725..94a92c0d7 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentChanges.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentChanges.java @@ -97,7 +97,7 @@ public static WorkspaceEdit translateDocumentChanges(final IBaseTextDocumentServ return wsEdit; } - private static List translateTextEdits(final IBaseTextDocumentService docService, IList edits, Map changeAnnotations) { + public static List translateTextEdits(final IBaseTextDocumentService docService, IList edits, Map changeAnnotations) { return edits.stream() .map(IConstructor.class::cast) .map(c -> { diff --git a/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc b/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc index 061530fd7..4926bb598 100644 --- a/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc +++ b/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc @@ -409,6 +409,18 @@ str defaultTrimTrailingWhitespace(str input) { test bool defaultTrimTrailingWhitespaceTest() = defaultTrimTrailingWhitespace("a \nb\t\n c \n") == "a\nb\n c\n"; + +list[TextEdit] formattingWrapper(Tree input, set[FormattingOption] opts, f:formatting(format)) { + formatted = format(input, opts); + if (trimTrailingWhitespace() in opts) formatted = f.trimTrailingWhitespace(formatted); + if (trimFinalNewlines() in opts) formatted = f.trimFinalNewlines(formatted); + if (insertFinalNewline() in opts) formatted = f.insertFinalNewline(formatted); + + // wrap complete formatted string in edit + // later, we should calculate more precise, local edits here, to not mess up the history stack in the editor + return [replace(input.src, formatted)]; +} + @deprecated{Backward compatible with ((parsing)).} @synopsis{Construct a `parsing` ((LanguageService))} LanguageService parser(Parser parser) = parsing(parser); From b749dcf12c37862a0d0ba3324dc59fd8b1b132c5 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 16 Jul 2025 11:02:44 +0200 Subject: [PATCH 05/32] Ongoing design work. --- .../rascal/library/demo/lang/pico/LanguageServer.rsc | 2 +- .../src/main/rascal/library/util/LanguageServer.rsc | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) 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 0cd034d72..39057b224 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 @@ -64,7 +64,7 @@ set[LanguageService] picoLanguageServer(bool allowRecovery) = { rename(picoRenamingService, prepareRenameService = picoRenamePreparingService), didRenameFiles(picoFileRenameService), selectionRange(picoSelectionRangeService), - formatting(picoFormattingService) + formatting(picoParser(allowRecovery), picoFormattingService) }; str picoFormattingService(Tree input, set[FormattingOption] opts) { diff --git a/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc b/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc index 4926bb598..8d7b95fb2 100644 --- a/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc +++ b/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc @@ -281,7 +281,8 @@ data LanguageService , loc (Focus _focus) prepareRenameService = defaultPrepareRenameService) | didRenameFiles(tuple[list[DocumentEdit], set[Message]] (list[DocumentEdit] fileRenames) didRenameFilesService) | selectionRange(list[loc](Focus _focus) selectionRangeService) - | formatting (str(Tree _input, set[FormattingOption] _opts) formattingService, + | formatting (Tree (str _input, loc _origin) parsingService, + str(Tree _input, set[FormattingOption] _opts) formattingService, str(str) trimTrailingWhitespace = defaultTrimTrailingWhitespace, str(str) insertFinalNewline = defaultInsertFinalNewline, str(str) trimFinalNewlines = defaultTrimFinalNewlines) @@ -409,16 +410,17 @@ str defaultTrimTrailingWhitespace(str input) { test bool defaultTrimTrailingWhitespaceTest() = defaultTrimTrailingWhitespace("a \nb\t\n c \n") == "a\nb\n c\n"; +list[TextEdit] layoutDiff(Tree a, Tree b, bool copyComments = false); -list[TextEdit] formattingWrapper(Tree input, set[FormattingOption] opts, f:formatting(format)) { - formatted = format(input, opts); +list[TextEdit] formattingWrapper(Tree input, set[FormattingOption] opts, f:formatting(parser, formatter)) { + formatted = formatter(input, opts); if (trimTrailingWhitespace() in opts) formatted = f.trimTrailingWhitespace(formatted); if (trimFinalNewlines() in opts) formatted = f.trimFinalNewlines(formatted); if (insertFinalNewline() in opts) formatted = f.insertFinalNewline(formatted); // wrap complete formatted string in edit // later, we should calculate more precise, local edits here, to not mess up the history stack in the editor - return [replace(input.src, formatted)]; + return layoutDiff(input, parser(formatted, input@\loc.top)); } @deprecated{Backward compatible with ((parsing)).} From f1effe1a603089482f72d20f9e32363eb9939727 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 6 Aug 2025 18:52:17 +0200 Subject: [PATCH 06/32] Experiment with a first formatting implementation using Box. --- .../library/demo/lang/pico/LanguageServer.rsc | 64 +++++++++++++++++-- 1 file changed, 58 insertions(+), 6 deletions(-) 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 39057b224..21e32887b 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 @@ -43,6 +43,10 @@ import DateTime; import IO; import String; +import lang::box::\syntax::Box; +extend lang::box::util::Tree2Box; +import lang::box::util::Box2Text; + private Tree (str _input, loc _origin) picoParser(bool allowRecovery) { return ParseTree::parser(#start[Program], allowRecovery=allowRecovery, filters=allowRecovery ? {createParseErrorFilter(false)} : {}); } @@ -68,16 +72,64 @@ set[LanguageService] picoLanguageServer(bool allowRecovery) = { }; str picoFormattingService(Tree input, set[FormattingOption] opts) { - if (tabSize(int tabSize) <- opts) { - println("Warning; `tabSize` () is ignored"); + int tabSize = 4; + if (tabSize(int ts) <- opts) { + tabSize = ts; } - if (insertSpaces() <- opts) { - println("Warning; `insertSpaces` is ignored"); + box = toBox(input); + box = visit (box) { + case i:I(_) => i[is=tabSize] } - - return ""; + formatted = format(box); + if (insertSpaces() notin opts) { + str spaces = ("" | it + " " | _ <- [0..tabSize]); + // TODO Only do this at the start of the line. Dynamically-sized regex??? + formatted = replaceAll(formatted, spaces, "\t"); + } + return formatted; } +Box toBox((Program) `begin <{Statement ";"}* body> end`, FormatOptions opts = formatOptions()) + = V([ + L("begin"), + I([ + V([ + toBox(decls, opts=opts), + toBox(body, opts=opts) + ], vs=1) + ]), + L("end") + ]); + +Box toBox((Declarations) `declare <{IdType ","}* decls> ;`, FormatOptions opts = formatOptions()) + = V([ + L("declare"), + A([ + R([ + toBox(id, opts=opts), + L(":"), + H([toBox(tp, opts=opts), decl != decls[-1] ? L(",") : L(";")], hs=0) + ]) + | decl:(IdType) ` : ` <- decls + ]) + ]); + +Box toBox(({Statement ";"}*) stmts, FormatOptions opts = formatOptions()) + = V([ + H([ + toBox(stmt, opts=opts), + stmt != stmts[-1] ? L(";") : NULL() + ], hs=0) + | stmt <- stmts + ]); + +Box toBox((Statement) `while do <{Statement ";"}* body> od`, FormatOptions opts = formatOptions()) + = V([ + HOV([L("while"), I([toBox(cond, opts=opts)]), L("do")]), + I([toBox(body, opts=opts)]), + L("od") + ]); + set[LanguageService] picoLanguageServer() = picoLanguageServer(false); set[LanguageService] picoLanguageServerWithRecovery() = picoLanguageServer(true); From 6fc3acf665471f4b44fa05d3057ffbd402893e0b Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 7 Aug 2025 16:19:23 +0200 Subject: [PATCH 07/32] Simplify API and shift responsibilities towards DSL. --- .../InterpretedLanguageContributions.java | 12 +- .../library/demo/lang/pico/LanguageServer.rsc | 2 +- .../src/main/rascal/library/util/Format.rsc | 117 +++++++++++++++ .../rascal/library/util/LanguageServer.rsc | 136 +----------------- 4 files changed, 130 insertions(+), 137 deletions(-) create mode 100644 rascal-lsp/src/main/rascal/library/util/Format.rsc 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 ec6579f8a..b1799e48e 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 @@ -34,6 +34,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; +import org.apache.commons.lang3.tuple.Pair; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.Nullable; @@ -97,7 +98,7 @@ public class InterpretedLanguageContributions implements ILanguageContributions private final CompletableFuture<@Nullable IFunction> rename; private final CompletableFuture<@Nullable IFunction> didRenameFiles; private final CompletableFuture<@Nullable IFunction> selectionRange; - private final CompletableFuture<@Nullable IConstructor> formatting; + private final CompletableFuture<@Nullable IFunction> formatting; private final CompletableFuture hasAnalysis; private final CompletableFuture hasBuild; @@ -160,7 +161,7 @@ public InterpretedLanguageContributions(LanguageParameter lang, IBaseTextDocumen this.rename = getFunctionFor(contributions, LanguageContributions.RENAME); this.didRenameFiles = getFunctionFor(contributions, LanguageContributions.DID_RENAME_FILES); this.selectionRange = getFunctionFor(contributions, LanguageContributions.SELECTION_RANGE); - this.formatting = contributions.thenApply(contrib -> getContribution(contrib, LanguageContributions.FORMATTING)); + this.formatting = getFunctionFor(contributions, LanguageContributions.FORMATTING); // assign boolean properties once instead of wasting futures all the time this.hasAnalysis = nonNull(this.analysis); @@ -400,9 +401,10 @@ public InterruptibleFuture selectionRange(IList focus) { @Override public InterruptibleFuture formatting(ITree input, ISet formattingOptions) { debug(LanguageContributions.FORMATTING, input != null ? TreeAdapter.getLocation(input) : null, formattingOptions.size()); - return InterruptibleFuture.flatten(formatting.thenApply(formattingContrib -> - runEvaluator(LanguageContributions.FORMATTING, eval, actualEval -> - (IList) actualEval.call(actualEval.getMonitor(), "util::LanguageServer", "formattingWrapper", input, formattingOptions, formattingContrib) + return InterruptibleFuture.flatten(formatting.thenCombine(parsing, Pair::of) + .thenApply(pair -> + runEvaluator(LanguageContributions.FORMATTING, eval, actualEval -> + (IList) actualEval.call(actualEval.getMonitor(), "util::LanguageServer", "formatter", input, formattingOptions, pair.getLeft(), pair.getRight()) , VF.list(), exec, true, client)), exec); } 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 21e32887b..affbeebd3 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 @@ -68,7 +68,7 @@ set[LanguageService] picoLanguageServer(bool allowRecovery) = { rename(picoRenamingService, prepareRenameService = picoRenamePreparingService), didRenameFiles(picoFileRenameService), selectionRange(picoSelectionRangeService), - formatting(picoParser(allowRecovery), picoFormattingService) + formatting(picoFormattingService) }; str picoFormattingService(Tree input, set[FormattingOption] opts) { diff --git a/rascal-lsp/src/main/rascal/library/util/Format.rsc b/rascal-lsp/src/main/rascal/library/util/Format.rsc new file mode 100644 index 000000000..21542fc8d --- /dev/null +++ b/rascal-lsp/src/main/rascal/library/util/Format.rsc @@ -0,0 +1,117 @@ +module util::Format + +import List; +import Map; +import Set; +import String; + +set[str] newLineCharacters = { + "\u000A", // LF + "\u000B", // VT + "\u000C", // FF + "\u000D", // CR + "\u000D\u000A", // CRLF + "\u0085", // NEL + "\u2028", // LS + "\u2029" // PS +}; + +private bool bySize(str a, str b) = size(a) > size(b); + +str mostUsedNewline(str input, set[str] lineseps = newLineCharacters, str(set[str]) tieBreaker = getFirstFrom) { + linesepCounts = (nl: 0 | nl <- lineseps); + for (nl <- reverse(sort(lineseps, bySize))) { + int count = size(findAll(input, nl)); + linesepCounts[nl] = count; + // subtract all occurrences of substrings that we counted before + for (str snl <- substrings(nl), linesepCounts[snl]?) { + linesepCounts[snl] = linesepCounts[snl] - count; + } + + } + byCount = invert(linesepCounts); + return tieBreaker(byCount[max(domain(byCount))]); +} + +set[str] substrings(str input) + = {input[i..i+l] | int i <- [0..size(input)], int l <- [1..size(input)], i + l <= size(input)}; + +test bool mostUsedNewlineTestMixed() + = mostUsedNewline("\r\n\n\r\n\t\t\t\t") == "\r\n"; + +test bool mostUsedNewlineTestTie() + = mostUsedNewline("\n\n\r\n\r\n") == "\n"; + +test bool mostUsedNewlineTestGreedy() + = mostUsedNewline("\r\n\r\n\n") == "\r\n"; + +str defaultInsertFinalNewline(str input, set[str] lineseps = newLineCharacters) + = any(nl <- lineseps, endsWith(input, nl)) + ? input + : input + mostUsedNewline(input) + ; + +test bool defaultInsertFinalNewlineTestSimple() + = defaultInsertFinalNewline("a\nb") + == "a\nb\n"; + +test bool defaultInsertFinalNewlineTestNoop() + = defaultInsertFinalNewline("a\nb\n") + == "a\nb\n"; + +test bool defaultInsertFinalNewlineTestMixed() + = defaultInsertFinalNewline("a\nb\r\n") + == "a\nb\r\n"; + +str defaultTrimFinalNewlines(str input, set[str] lineseps = newLineCharacters) { + orderedSeps = sort(lineseps, bySize); + while (nl <- orderedSeps, endsWith(input, nl)) { + input = input[0..-size(nl)]; + } + return input; +} + +test bool defaultTrimFinalNewlinesTestSimple() + = defaultTrimFinalNewlines("a\n\n\n") == "a"; + +test bool defaultTrimFinalNewlinesTestEndOnly() + = defaultTrimFinalNewlines("a\n\n\nb\n\n") == "a\n\n\nb"; + +test bool defaultTrimFinalNewlinesTestWhiteSpace() + = defaultTrimFinalNewlines("a\n\n\nb\n\n ") == "a\n\n\nb\n\n "; + +str perLine(str input, str(str) lineFunc, set[str] lineseps = newLineCharacters) { + orderedSeps = sort(lineseps, bySize); + + str result = ""; + int next = 0; + for (int i <- [0..size(input)]) { + // greedily match line separators (longest first) + if (i >= next, str nl <- orderedSeps, nl == input[i..i+size(nl)]) { + line = input[next..i]; + result += lineFunc(line) + nl; + next = i + size(nl); // skip to the start of the next line + } + } + + // last line + if (str nl <- orderedSeps, nl == input[-size(nl)..]) { + line = input[next..next+size(nl)]; + result += lineFunc(line); + } + + return result; +} + +test bool perLineTest() + = perLine("a\nb\r\nc\n\r\n", str(str line) { return line + "x"; }) == "ax\nbx\r\ncx\nx\r\nx"; + +str defaultTrimTrailingWhitespace(str input) { + str trimLineTrailingWs(/^\s*$/) = nonWhiteSpace; + default str trimLineTrailingWs(/^\s*$/) = ""; + + return perLine(input, trimLineTrailingWs); +} + +test bool defaultTrimTrailingWhitespaceTest() + = defaultTrimTrailingWhitespace("a \nb\t\n c \n") == "a\nb\n c\n"; diff --git a/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc b/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc index 8d7b95fb2..7b1d48f0a 100644 --- a/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc +++ b/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc @@ -43,12 +43,8 @@ import util::Reflective; import analysis::diff::edits::TextEdits; import Exception; import IO; -import Map; import Message; import ParseTree; -import Set; -import String; -import Type; @synopsis{Definition of a language server by its meta-data.} @description{ @@ -281,11 +277,7 @@ data LanguageService , loc (Focus _focus) prepareRenameService = defaultPrepareRenameService) | didRenameFiles(tuple[list[DocumentEdit], set[Message]] (list[DocumentEdit] fileRenames) didRenameFilesService) | selectionRange(list[loc](Focus _focus) selectionRangeService) - | formatting (Tree (str _input, loc _origin) parsingService, - str(Tree _input, set[FormattingOption] _opts) formattingService, - str(str) trimTrailingWhitespace = defaultTrimTrailingWhitespace, - str(str) insertFinalNewline = defaultInsertFinalNewline, - str(str) trimFinalNewlines = defaultTrimFinalNewlines) + | formatting (str(Tree _input, set[FormattingOption] _opts) formattingService) ; loc defaultPrepareRenameService(Focus _:[Tree tr, *_]) = tr.src when tr.src?; @@ -299,129 +291,11 @@ data FormattingOption | trimFinalNewlines() ; -set[str] newLineCharacters = { - "\u000A", // LF - "\u000B", // VT - "\u000C", // FF - "\u000D", // CR - "\u000D\u000A", // CRLF - "\u0085", // NEL - "\u2028", // LS - "\u2029" // PS -}; - -private bool bySize(str a, str b) = size(a) > size(b); - -str mostUsedNewline(str input, set[str] lineseps = newLineCharacters, str(set[str]) tieBreaker = getFirstFrom) { - linesepCounts = (nl: 0 | nl <- lineseps); - for (nl <- reverse(sort(lineseps, bySize))) { - int count = size(findAll(input, nl)); - linesepCounts[nl] = count; - // subtract all occurrences of substrings that we counted before - for (str snl <- substrings(nl), linesepCounts[snl]?) { - linesepCounts[snl] = linesepCounts[snl] - count; - } +private list[TextEdit] layoutDiff(Tree a, Tree b, bool copyComments = false) + = [replace(a@\loc, "")]; - } - byCount = invert(linesepCounts); - return tieBreaker(byCount[max(domain(byCount))]); -} - -set[str] substrings(str input) - = {input[i..i+l] | int i <- [0..size(input)], int l <- [1..size(input)], i + l <= size(input)}; - -test bool mostUsedNewlineTestMixed() - = mostUsedNewline("\r\n\n\r\n\t\t\t\t") == "\r\n"; - -test bool mostUsedNewlineTestTie() - = mostUsedNewline("\n\n\r\n\r\n") == "\n"; - -test bool mostUsedNewlineTestGreedy() - = mostUsedNewline("\r\n\r\n\n") == "\r\n"; - -str defaultInsertFinalNewline(str input, set[str] lineseps = newLineCharacters) - = any(nl <- lineseps, endsWith(input, nl)) - ? input - : input + mostUsedNewline(input) - ; - -test bool defaultInsertFinalNewlineTestSimple() - = defaultInsertFinalNewline("a\nb") - == "a\nb\n"; - -test bool defaultInsertFinalNewlineTestNoop() - = defaultInsertFinalNewline("a\nb\n") - == "a\nb\n"; - -test bool defaultInsertFinalNewlineTestMixed() - = defaultInsertFinalNewline("a\nb\r\n") - == "a\nb\r\n"; - -str defaultTrimFinalNewlines(str input, set[str] lineseps = newLineCharacters) { - orderedSeps = sort(lineseps, bySize); - while (nl <- orderedSeps, endsWith(input, nl)) { - input = input[0..-size(nl)]; - } - return input; -} - -test bool defaultTrimFinalNewlinesTestSimple() - = defaultTrimFinalNewlines("a\n\n\n") == "a"; - -test bool defaultTrimFinalNewlinesTestEndOnly() - = defaultTrimFinalNewlines("a\n\n\nb\n\n") == "a\n\n\nb"; - -test bool defaultTrimFinalNewlinesTestWhiteSpace() - = defaultTrimFinalNewlines("a\n\n\nb\n\n ") == "a\n\n\nb\n\n "; - -str perLine(str input, str(str) lineFunc, set[str] lineseps = newLineCharacters) { - orderedSeps = sort(lineseps, bySize); - - str result = ""; - int next = 0; - for (int i <- [0..size(input)]) { - // greedily match line separators (longest first) - if (i >= next, str nl <- orderedSeps, nl == input[i..i+size(nl)]) { - line = input[next..i]; - result += lineFunc(line) + nl; - next = i + size(nl); // skip to the start of the next line - } - } - - // last line - if (str nl <- orderedSeps, nl == input[-size(nl)..]) { - line = input[next..next+size(nl)]; - result += lineFunc(line); - } - - return result; -} - -test bool perLineTest() - = perLine("a\nb\r\nc\n\r\n", str(str line) { return line + "x"; }) == "ax\nbx\r\ncx\nx\r\nx"; - -str defaultTrimTrailingWhitespace(str input) { - str trimLineTrailingWs(/^\s*$/) = nonWhiteSpace; - default str trimLineTrailingWs(/^\s*$/) = ""; - - return perLine(input, trimLineTrailingWs); -} - -test bool defaultTrimTrailingWhitespaceTest() - = defaultTrimTrailingWhitespace("a \nb\t\n c \n") == "a\nb\n c\n"; - -list[TextEdit] layoutDiff(Tree a, Tree b, bool copyComments = false); - -list[TextEdit] formattingWrapper(Tree input, set[FormattingOption] opts, f:formatting(parser, formatter)) { - formatted = formatter(input, opts); - if (trimTrailingWhitespace() in opts) formatted = f.trimTrailingWhitespace(formatted); - if (trimFinalNewlines() in opts) formatted = f.trimFinalNewlines(formatted); - if (insertFinalNewline() in opts) formatted = f.insertFinalNewline(formatted); - - // wrap complete formatted string in edit - // later, we should calculate more precise, local edits here, to not mess up the history stack in the editor - return layoutDiff(input, parser(formatted, input@\loc.top)); -} +list[TextEdit] formatter(Tree input, set[FormattingOption] opts, str(Tree, set[FormattingOption]) format, Tree(str, loc) parse) + = layoutDiff(input, parse(format(input, opts), input@\loc.top), copyComments = true); @deprecated{Backward compatible with ((parsing)).} @synopsis{Construct a `parsing` ((LanguageService))} From bdb798926d066aa5ee5b0e7ab9b2087dafa5bd1b Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 7 Aug 2025 17:43:24 +0200 Subject: [PATCH 08/32] Improve names in formatting library. --- .../src/main/rascal/library/util/Format.rsc | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/rascal-lsp/src/main/rascal/library/util/Format.rsc b/rascal-lsp/src/main/rascal/library/util/Format.rsc index 21542fc8d..d0039fb19 100644 --- a/rascal-lsp/src/main/rascal/library/util/Format.rsc +++ b/rascal-lsp/src/main/rascal/library/util/Format.rsc @@ -45,25 +45,25 @@ test bool mostUsedNewlineTestTie() test bool mostUsedNewlineTestGreedy() = mostUsedNewline("\r\n\r\n\n") == "\r\n"; -str defaultInsertFinalNewline(str input, set[str] lineseps = newLineCharacters) +str insertFinalNewline(str input, set[str] lineseps = newLineCharacters) = any(nl <- lineseps, endsWith(input, nl)) ? input : input + mostUsedNewline(input) ; -test bool defaultInsertFinalNewlineTestSimple() - = defaultInsertFinalNewline("a\nb") +test bool insertFinalNewlineTestSimple() + = insertFinalNewline("a\nb") == "a\nb\n"; -test bool defaultInsertFinalNewlineTestNoop() - = defaultInsertFinalNewline("a\nb\n") +test bool insertFinalNewlineTestNoop() + = insertFinalNewline("a\nb\n") == "a\nb\n"; -test bool defaultInsertFinalNewlineTestMixed() - = defaultInsertFinalNewline("a\nb\r\n") +test bool insertFinalNewlineTestMixed() + = insertFinalNewline("a\nb\r\n") == "a\nb\r\n"; -str defaultTrimFinalNewlines(str input, set[str] lineseps = newLineCharacters) { +str trimFinalNewline(str input, set[str] lineseps = newLineCharacters) { orderedSeps = sort(lineseps, bySize); while (nl <- orderedSeps, endsWith(input, nl)) { input = input[0..-size(nl)]; @@ -71,14 +71,14 @@ str defaultTrimFinalNewlines(str input, set[str] lineseps = newLineCharacters) { return input; } -test bool defaultTrimFinalNewlinesTestSimple() - = defaultTrimFinalNewlines("a\n\n\n") == "a"; +test bool trimFinalNewlineTestSimple() + = trimFinalNewline("a\n\n\n") == "a"; -test bool defaultTrimFinalNewlinesTestEndOnly() - = defaultTrimFinalNewlines("a\n\n\nb\n\n") == "a\n\n\nb"; +test bool trimFinalNewlineTestEndOnly() + = trimFinalNewline("a\n\n\nb\n\n") == "a\n\n\nb"; -test bool defaultTrimFinalNewlinesTestWhiteSpace() - = defaultTrimFinalNewlines("a\n\n\nb\n\n ") == "a\n\n\nb\n\n "; +test bool trimFinalNewlineTestWhiteSpace() + = trimFinalNewline("a\n\n\nb\n\n ") == "a\n\n\nb\n\n "; str perLine(str input, str(str) lineFunc, set[str] lineseps = newLineCharacters) { orderedSeps = sort(lineseps, bySize); @@ -106,12 +106,12 @@ str perLine(str input, str(str) lineFunc, set[str] lineseps = newLineCharacters) test bool perLineTest() = perLine("a\nb\r\nc\n\r\n", str(str line) { return line + "x"; }) == "ax\nbx\r\ncx\nx\r\nx"; -str defaultTrimTrailingWhitespace(str input) { +str trimTrailingWhitespace(str input) { str trimLineTrailingWs(/^\s*$/) = nonWhiteSpace; default str trimLineTrailingWs(/^\s*$/) = ""; return perLine(input, trimLineTrailingWs); } -test bool defaultTrimTrailingWhitespaceTest() - = defaultTrimTrailingWhitespace("a \nb\t\n c \n") == "a\nb\n c\n"; +test bool trimTrailingWhitespaceTest() + = trimTrailingWhitespace("a \nb\t\n c \n") == "a\nb\n c\n"; From a985a81c0c0b501a0c1f167640299afffb809ed5 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 7 Aug 2025 18:05:14 +0200 Subject: [PATCH 09/32] Add indentation conversion functions. --- .../src/main/rascal/library/util/Format.rsc | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/rascal-lsp/src/main/rascal/library/util/Format.rsc b/rascal-lsp/src/main/rascal/library/util/Format.rsc index d0039fb19..b005e1663 100644 --- a/rascal-lsp/src/main/rascal/library/util/Format.rsc +++ b/rascal-lsp/src/main/rascal/library/util/Format.rsc @@ -33,6 +33,25 @@ str mostUsedNewline(str input, set[str] lineseps = newLineCharacters, str(set[st return tieBreaker(byCount[max(domain(byCount))]); } +tuple[str indentation, str rest] splitIndentation(/^/) + = ; + +str(str) indentSpacesAsTabs(int tabSize) { + str spaces = ("" | it + " " | _ <- [0..tabSize]); + return str(str s) { + parts = splitIndentation(s); + return ""; + }; +} + +str(str) indentTabsAsSpaces(int tabSize) { + str spaces = ("" | it + " " | _ <- [0..tabSize]); + return str(str s) { + parts = splitIndentation(s); + return ""; + }; +} + set[str] substrings(str input) = {input[i..i+l] | int i <- [0..size(input)], int l <- [1..size(input)], i + l <= size(input)}; From 0196d0724a085438dfa3828e6c0c615fa28313c1 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 7 Aug 2025 18:07:16 +0200 Subject: [PATCH 10/32] Do string modifications in Pico formatter. --- .../library/demo/lang/pico/LanguageServer.rsc | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) 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 affbeebd3..a60a40527 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 @@ -34,6 +34,7 @@ The core functionality of this module is built upon these concepts: module demo::lang::pico::LanguageServer import util::LanguageServer; +import util::Format; import util::IDEServices; import ParseTree; import util::ParseErrorRecovery; @@ -81,11 +82,26 @@ str picoFormattingService(Tree input, set[FormattingOption] opts) { case i:I(_) => i[is=tabSize] } formatted = format(box); + formatLine = str(str s) { return s; }; if (insertSpaces() notin opts) { - str spaces = ("" | it + " " | _ <- [0..tabSize]); - // TODO Only do this at the start of the line. Dynamically-sized regex??? - formatted = replaceAll(formatted, spaces, "\t"); + formatLine = formatLine o indentSpacesAsTabs(tabSize); } + if (trimTrailingWhitespace() notin opts) { + println("The Pico formatter does not support leaving trailing whitespace."); + } + + // do line-based processing + formatted = perLine(formatted, formatLine); + + // whole-file processing + if (trimFinalNewlines() notin opts && + /.*/ := "") { + formatted = replaceLast(formatted, "\n", newlines); + } + if (insertFinalNewline() <- opts) { + formatted = insertFinalNewline(formatted); + } + return formatted; } From d1c4e3ec5382deda90b2798347d4ba07ee84d3e8 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 18 Aug 2025 16:23:09 +0200 Subject: [PATCH 11/32] Update formatting API design. --- .../library/demo/lang/pico/LanguageServer.rsc | 44 ++++++++++++------- .../rascal/library/util/LanguageServer.rsc | 22 ++++------ 2 files changed, 35 insertions(+), 31 deletions(-) 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 a60a40527..8d73bb744 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 @@ -47,6 +47,7 @@ import String; import lang::box::\syntax::Box; extend lang::box::util::Tree2Box; import lang::box::util::Box2Text; +import analysis::diff::edits::HiFiLayoutDiff; private Tree (str _input, loc _origin) picoParser(bool allowRecovery) { return ParseTree::parser(#start[Program], allowRecovery=allowRecovery, filters=allowRecovery ? {createParseErrorFilter(false)} : {}); @@ -72,37 +73,46 @@ set[LanguageService] picoLanguageServer(bool allowRecovery) = { formatting(picoFormattingService) }; -str picoFormattingService(Tree input, set[FormattingOption] opts) { - int tabSize = 4; - if (tabSize(int ts) <- opts) { - tabSize = ts; - } +list[TextEdit] picoFormattingService(Tree input, FormattingOptions opts) { + // pico tree to box formatting representation + str original = ""; + print("[original]"); + rprintln(original); + box = toBox(input); - box = visit (box) { - case i:I(_) => i[is=tabSize] - } + box = visit (box) { case i:I(_) => i[is=opts.tabSize] }; + // box to string formatted = format(box); + + //// line-based modifications + // identity operator, to compose with other operators formatLine = str(str s) { return s; }; - if (insertSpaces() notin opts) { - formatLine = formatLine o indentSpacesAsTabs(tabSize); + if (!opts.insertSpaces) { + // replace indentation spaces with tabs + formatLine = indentSpacesAsTabs(opts.tabSize) o formatLine; } - if (trimTrailingWhitespace() notin opts) { - println("The Pico formatter does not support leaving trailing whitespace."); + if (!opts.trimTrailingWhitespace) { + // restore trailing whitespace that was lost during tree->box->text, or find a way to not lose it + println("The Pico formatter does not support maintaining trailing whitespace."); } // do line-based processing formatted = perLine(formatted, formatLine); // whole-file processing - if (trimFinalNewlines() notin opts && - /.*/ := "") { - formatted = replaceLast(formatted, "\n", newlines); + if (/^.*[^\n]$/s := original) { + // replace original final newlines or remove the one introduced by ((format)) (`Box2Text`) + formatted = replaceLast(formatted, "\n", opts.trimFinalNewlines ? "" : newlines); } - if (insertFinalNewline() <- opts) { + if (opts.insertFinalNewline) { + // ensure presence of final newline formatted = insertFinalNewline(formatted); } - return formatted; + // computelayout differences as edits, and restore comments + edits = layoutDiff(input, parse(#start[Program], formatted)); + + return edits; } Box toBox((Program) `begin <{Statement ";"}* body> end`, FormatOptions opts = formatOptions()) diff --git a/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc b/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc index 7b1d48f0a..a115be424 100644 --- a/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc +++ b/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc @@ -277,25 +277,19 @@ data LanguageService , loc (Focus _focus) prepareRenameService = defaultPrepareRenameService) | didRenameFiles(tuple[list[DocumentEdit], set[Message]] (list[DocumentEdit] fileRenames) didRenameFilesService) | selectionRange(list[loc](Focus _focus) selectionRangeService) - | formatting (str(Tree _input, set[FormattingOption] _opts) formattingService) + | formatting (list[TextEdit](Tree _input, FormattingOptions _opts) formattingService) ; loc defaultPrepareRenameService(Focus _:[Tree tr, *_]) = tr.src when tr.src?; default loc defaultPrepareRenameService(Focus focus) { throw IllegalArgument(focus, "Element under cursor does not have source location"); } -data FormattingOption - = tabSize(int) - | insertSpaces() - | trimTrailingWhitespace() - | insertFinalNewline() - | trimFinalNewlines() - ; - -private list[TextEdit] layoutDiff(Tree a, Tree b, bool copyComments = false) - = [replace(a@\loc, "")]; - -list[TextEdit] formatter(Tree input, set[FormattingOption] opts, str(Tree, set[FormattingOption]) format, Tree(str, loc) parse) - = layoutDiff(input, parse(format(input, opts), input@\loc.top), copyComments = true); +data FormattingOptions( + int tabSize = 4 + , bool insertSpaces = true + , bool trimTrailingWhitespace = true + , bool insertFinalNewline = true + , bool trimFinalNewlines = true +) = formattingOptions(); @deprecated{Backward compatible with ((parsing)).} @synopsis{Construct a `parsing` ((LanguageService))} From 49b66d0ad11dda06e0fe0318b1a5dbe7262f4a4f Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 18 Aug 2025 16:26:16 +0200 Subject: [PATCH 12/32] Add `mergeLines` --- .../src/main/rascal/library/util/Format.rsc | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/rascal-lsp/src/main/rascal/library/util/Format.rsc b/rascal-lsp/src/main/rascal/library/util/Format.rsc index b005e1663..bc3e756c6 100644 --- a/rascal-lsp/src/main/rascal/library/util/Format.rsc +++ b/rascal-lsp/src/main/rascal/library/util/Format.rsc @@ -99,29 +99,33 @@ test bool trimFinalNewlineTestEndOnly() test bool trimFinalNewlineTestWhiteSpace() = trimFinalNewline("a\n\n\nb\n\n ") == "a\n\n\nb\n\n "; -str perLine(str input, str(str) lineFunc, set[str] lineseps = newLineCharacters) { +list[tuple[str, str]] separateLines(str input, set[str] lineseps = newLineCharacters) { orderedSeps = sort(lineseps, bySize); - str result = ""; + list[tuple[str, str]] lines = []; int next = 0; for (int i <- [0..size(input)]) { // greedily match line separators (longest first) if (i >= next, str nl <- orderedSeps, nl == input[i..i+size(nl)]) { - line = input[next..i]; - result += lineFunc(line) + nl; + lines += ; next = i + size(nl); // skip to the start of the next line } } // last line if (str nl <- orderedSeps, nl == input[-size(nl)..]) { - line = input[next..next+size(nl)]; - result += lineFunc(line); + lines += ; } - return result; + return lines; } +str mergeLines(list[tuple[str, str]] lines) + = ("" | it + line + sep | <- lines); + +str perLine(str input, str(str) lineFunc, set[str] lineseps = newLineCharacters) + = mergeLines([ | <- separateLines(input, lineseps=lineseps)]); + test bool perLineTest() = perLine("a\nb\r\nc\n\r\n", str(str line) { return line + "x"; }) == "ax\nbx\r\ncx\nx\r\nx"; From 998a115d2e5975b53670ee1beae100ccdc8fe5e7 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 18 Aug 2025 16:29:01 +0200 Subject: [PATCH 13/32] Small fixes. --- .../src/main/rascal/library/util/Format.rsc | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/rascal-lsp/src/main/rascal/library/util/Format.rsc b/rascal-lsp/src/main/rascal/library/util/Format.rsc index bc3e756c6..8702c6b3b 100644 --- a/rascal-lsp/src/main/rascal/library/util/Format.rsc +++ b/rascal-lsp/src/main/rascal/library/util/Format.rsc @@ -16,19 +16,19 @@ set[str] newLineCharacters = { "\u2029" // PS }; -private bool bySize(str a, str b) = size(a) > size(b); +private bool bySize(str a, str b) = size(a) < size(b); str mostUsedNewline(str input, set[str] lineseps = newLineCharacters, str(set[str]) tieBreaker = getFirstFrom) { linesepCounts = (nl: 0 | nl <- lineseps); - for (nl <- reverse(sort(lineseps, bySize))) { + for (nl <- sort(lineseps, bySize)) { int count = size(findAll(input, nl)); linesepCounts[nl] = count; // subtract all occurrences of substrings that we counted before for (str snl <- substrings(nl), linesepCounts[snl]?) { linesepCounts[snl] = linesepCounts[snl] - count; } - } + byCount = invert(linesepCounts); return tieBreaker(byCount[max(domain(byCount))]); } @@ -38,16 +38,16 @@ tuple[str indentation, str rest] splitIndentation(/^/) str(str) indentSpacesAsTabs(int tabSize) { str spaces = ("" | it + " " | _ <- [0..tabSize]); - return str(str s) { - parts = splitIndentation(s); + return str(str line) { + parts = splitIndentation(line); return ""; }; } str(str) indentTabsAsSpaces(int tabSize) { str spaces = ("" | it + " " | _ <- [0..tabSize]); - return str(str s) { - parts = splitIndentation(s); + return str(str line) { + parts = splitIndentation(line); return ""; }; } @@ -83,7 +83,7 @@ test bool insertFinalNewlineTestMixed() == "a\nb\r\n"; str trimFinalNewline(str input, set[str] lineseps = newLineCharacters) { - orderedSeps = sort(lineseps, bySize); + orderedSeps = reverse(sort(lineseps, bySize)); while (nl <- orderedSeps, endsWith(input, nl)) { input = input[0..-size(nl)]; } @@ -100,7 +100,7 @@ test bool trimFinalNewlineTestWhiteSpace() = trimFinalNewline("a\n\n\nb\n\n ") == "a\n\n\nb\n\n "; list[tuple[str, str]] separateLines(str input, set[str] lineseps = newLineCharacters) { - orderedSeps = sort(lineseps, bySize); + orderedSeps = reverse(sort(lineseps, bySize)); list[tuple[str, str]] lines = []; int next = 0; From 551fe2a6c648c6d9a23bed1fb30d3bd63cc75845 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 18 Aug 2025 16:29:47 +0200 Subject: [PATCH 14/32] Break new line ties by order. --- .../src/main/rascal/library/util/Format.rsc | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/rascal-lsp/src/main/rascal/library/util/Format.rsc b/rascal-lsp/src/main/rascal/library/util/Format.rsc index 8702c6b3b..f7dc2af28 100644 --- a/rascal-lsp/src/main/rascal/library/util/Format.rsc +++ b/rascal-lsp/src/main/rascal/library/util/Format.rsc @@ -5,7 +5,7 @@ import Map; import Set; import String; -set[str] newLineCharacters = { +list[str] newLineCharacters = [ "\u000A", // LF "\u000B", // VT "\u000C", // FF @@ -14,11 +14,16 @@ set[str] newLineCharacters = { "\u0085", // NEL "\u2028", // LS "\u2029" // PS -}; +]; private bool bySize(str a, str b) = size(a) < size(b); +private bool(str, str) byIndex(list[str] indices) { + return bool(str a, str b) { + return indexOf(indices, a) < indexOf(indices, b); + }; +} -str mostUsedNewline(str input, set[str] lineseps = newLineCharacters, str(set[str]) tieBreaker = getFirstFrom) { +str mostUsedNewline(str input, list[str] lineseps = newLineCharacters, str(list[str]) tieBreaker = getFirstFrom) { linesepCounts = (nl: 0 | nl <- lineseps); for (nl <- sort(lineseps, bySize)) { int count = size(findAll(input, nl)); @@ -30,7 +35,7 @@ str mostUsedNewline(str input, set[str] lineseps = newLineCharacters, str(set[st } byCount = invert(linesepCounts); - return tieBreaker(byCount[max(domain(byCount))]); + return tieBreaker(sort(byCount[max(domain(byCount))], byIndex(lineseps))); } tuple[str indentation, str rest] splitIndentation(/^/) @@ -64,7 +69,7 @@ test bool mostUsedNewlineTestTie() test bool mostUsedNewlineTestGreedy() = mostUsedNewline("\r\n\r\n\n") == "\r\n"; -str insertFinalNewline(str input, set[str] lineseps = newLineCharacters) +str insertFinalNewline(str input, list[str] lineseps = newLineCharacters) = any(nl <- lineseps, endsWith(input, nl)) ? input : input + mostUsedNewline(input) @@ -82,7 +87,7 @@ test bool insertFinalNewlineTestMixed() = insertFinalNewline("a\nb\r\n") == "a\nb\r\n"; -str trimFinalNewline(str input, set[str] lineseps = newLineCharacters) { +str trimFinalNewline(str input, list[str] lineseps = newLineCharacters) { orderedSeps = reverse(sort(lineseps, bySize)); while (nl <- orderedSeps, endsWith(input, nl)) { input = input[0..-size(nl)]; @@ -99,7 +104,7 @@ test bool trimFinalNewlineTestEndOnly() test bool trimFinalNewlineTestWhiteSpace() = trimFinalNewline("a\n\n\nb\n\n ") == "a\n\n\nb\n\n "; -list[tuple[str, str]] separateLines(str input, set[str] lineseps = newLineCharacters) { +list[tuple[str, str]] separateLines(str input, list[str] lineseps = newLineCharacters) { orderedSeps = reverse(sort(lineseps, bySize)); list[tuple[str, str]] lines = []; @@ -123,7 +128,7 @@ list[tuple[str, str]] separateLines(str input, set[str] lineseps = newLineCharac str mergeLines(list[tuple[str, str]] lines) = ("" | it + line + sep | <- lines); -str perLine(str input, str(str) lineFunc, set[str] lineseps = newLineCharacters) +str perLine(str input, str(str) lineFunc, list[str] lineseps = newLineCharacters) = mergeLines([ | <- separateLines(input, lineseps=lineseps)]); test bool perLineTest() From 84c5a3529ca8abb57cfe5ab18705898b88b1803c Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 18 Aug 2025 16:47:44 +0200 Subject: [PATCH 15/32] Document & fix string/formatting utils. --- .../src/main/rascal/library/util/Format.rsc | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/rascal-lsp/src/main/rascal/library/util/Format.rsc b/rascal-lsp/src/main/rascal/library/util/Format.rsc index f7dc2af28..9a1814b6d 100644 --- a/rascal-lsp/src/main/rascal/library/util/Format.rsc +++ b/rascal-lsp/src/main/rascal/library/util/Format.rsc @@ -16,19 +16,23 @@ list[str] newLineCharacters = [ "\u2029" // PS ]; +@synopsis{Comparator to sort strings by length (ascending).} private bool bySize(str a, str b) = size(a) < size(b); + +@synopsis{Comparator to sort strings by relative position in a reference list.} private bool(str, str) byIndex(list[str] indices) { return bool(str a, str b) { return indexOf(indices, a) < indexOf(indices, b); }; } +@synopsis{Determine the most-used newline character in a string.} str mostUsedNewline(str input, list[str] lineseps = newLineCharacters, str(list[str]) tieBreaker = getFirstFrom) { linesepCounts = (nl: 0 | nl <- lineseps); for (nl <- sort(lineseps, bySize)) { int count = size(findAll(input, nl)); linesepCounts[nl] = count; - // subtract all occurrences of substrings that we counted before + // subtract all occurrences of substrings of newline characters that we counted before for (str snl <- substrings(nl), linesepCounts[snl]?) { linesepCounts[snl] = linesepCounts[snl] - count; } @@ -38,6 +42,7 @@ str mostUsedNewline(str input, list[str] lineseps = newLineCharacters, str(list[ return tieBreaker(sort(byCount[max(domain(byCount))], byIndex(lineseps))); } +@synopsis{Split a string to an indentation prefix and the remainder of the string.} tuple[str indentation, str rest] splitIndentation(/^/) = ; @@ -57,6 +62,7 @@ str(str) indentTabsAsSpaces(int tabSize) { }; } +@synopsis{Compute all possible strict substrings of a string.} set[str] substrings(str input) = {input[i..i+l] | int i <- [0..size(input)], int l <- [1..size(input)], i + l <= size(input)}; @@ -69,10 +75,11 @@ test bool mostUsedNewlineTestTie() test bool mostUsedNewlineTestGreedy() = mostUsedNewline("\r\n\r\n\n") == "\r\n"; +@synopsis{If a string does not end with a newline character, append one. } str insertFinalNewline(str input, list[str] lineseps = newLineCharacters) = any(nl <- lineseps, endsWith(input, nl)) ? input - : input + mostUsedNewline(input) + : input + mostUsedNewline(input, lineseps=lineseps) ; test bool insertFinalNewlineTestSimple() @@ -87,7 +94,8 @@ test bool insertFinalNewlineTestMixed() = insertFinalNewline("a\nb\r\n") == "a\nb\r\n"; -str trimFinalNewline(str input, list[str] lineseps = newLineCharacters) { +@synopsis{Remove all newlines from the end of a string.} +str trimFinalNewlines(str input, list[str] lineseps = newLineCharacters) { orderedSeps = reverse(sort(lineseps, bySize)); while (nl <- orderedSeps, endsWith(input, nl)) { input = input[0..-size(nl)]; @@ -96,14 +104,15 @@ str trimFinalNewline(str input, list[str] lineseps = newLineCharacters) { } test bool trimFinalNewlineTestSimple() - = trimFinalNewline("a\n\n\n") == "a"; + = trimFinalNewlines("a\n\n\n") == "a"; test bool trimFinalNewlineTestEndOnly() - = trimFinalNewline("a\n\n\nb\n\n") == "a\n\n\nb"; + = trimFinalNewlines("a\n\n\nb\n\n") == "a\n\n\nb"; test bool trimFinalNewlineTestWhiteSpace() - = trimFinalNewline("a\n\n\nb\n\n ") == "a\n\n\nb\n\n "; + = trimFinalNewlines("a\n\n\nb\n\n ") == "a\n\n\nb\n\n "; +@synopsis{Split a string in pairs for each line.} list[tuple[str, str]] separateLines(str input, list[str] lineseps = newLineCharacters) { orderedSeps = reverse(sort(lineseps, bySize)); @@ -125,15 +134,18 @@ list[tuple[str, str]] separateLines(str input, list[str] lineseps = newLineChara return lines; } +@synopsis{Concatenate a list of pairs to form a single string.} str mergeLines(list[tuple[str, str]] lines) = ("" | it + line + sep | <- lines); +@synopsis{Process the text of a string per line, maintaining the original newline characters.} str perLine(str input, str(str) lineFunc, list[str] lineseps = newLineCharacters) = mergeLines([ | <- separateLines(input, lineseps=lineseps)]); test bool perLineTest() = perLine("a\nb\r\nc\n\r\n", str(str line) { return line + "x"; }) == "ax\nbx\r\ncx\nx\r\nx"; +@synopsis{Trim trailing non-newline whitespace from each line in a multi-line string.} str trimTrailingWhitespace(str input) { str trimLineTrailingWs(/^\s*$/) = nonWhiteSpace; default str trimLineTrailingWs(/^\s*$/) = ""; From c4b3561506f68815ced71e21b7e797cf2ca06ded Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Tue, 19 Aug 2025 10:54:17 +0200 Subject: [PATCH 16/32] Add rangeFormatting, reuse formatting. --- .../parametric/ILanguageContributions.java | 2 +- .../ParametricTextDocumentService.java | 26 +++++++++++++++---- .../library/demo/lang/pico/LanguageServer.rsc | 11 ++++---- .../rascal/library/util/LanguageServer.rsc | 2 +- 4 files changed, 29 insertions(+), 12 deletions(-) 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 5cb9e1666..8d7f7acba 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 @@ -61,7 +61,7 @@ public interface ILanguageContributions { public InterruptibleFuture implementation(IList focus); public InterruptibleFuture codeAction(IList focus); public InterruptibleFuture selectionRange(IList focus); - public InterruptibleFuture formatting(ITree input, ISet formattingOptions); + public InterruptibleFuture formatting(ITree input, ISourceLocation loc, ISet formattingOptions); public InterruptibleFuture prepareRename(IList focus); public InterruptibleFuture rename(IList focus, String name); 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 9eab14aae..7b849fa04 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 @@ -64,6 +64,7 @@ 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; @@ -735,17 +736,32 @@ public CompletableFuture>> codeAction(CodeActio @Override public CompletableFuture> formatting(DocumentFormattingParams params) { - logger.debug("formatting: {}", params); + logger.debug("Formatting: {}", params); + return format(params.getTextDocument(), null, params.getOptions()); + } - final ILanguageContributions contribs = contributions(params.getTextDocument()); + @Override + public CompletableFuture> rangeFormatting(DocumentRangeFormattingParams params) { + logger.debug("Formatting range: {}", params); + return format(params.getTextDocument(), params.getRange(), params.getOptions()); + } + + private CompletableFuture> format(TextDocumentIdentifier uri, @Nullable Range range, FormattingOptions options) { + final ILanguageContributions contribs = contributions(uri); // convert the `FormattingOptions` map to a `set[FormattingOption]` - ISet optSet = getFormattingOptions(params.getOptions()); + ISet optSet = getFormattingOptions(options); // call the `formatting` implementation of the relevant language contribution - return getFile(params.getTextDocument()) + return getFile(uri) .getCurrentTreeAsync() .thenApply(Versioned::get) - .thenCompose(tree -> contribs.formatting(tree, optSet).get()) + .thenCompose(tree -> { + // range to Rascal loc + ISourceLocation loc = range == null + ? TreeAdapter.getLocation(tree) + : null; // TODO map Range to ISourceLocation + return contribs.formatting(tree, loc, optSet).get(); + }) // convert the document changes .thenApply(l -> DocumentChanges.translateTextEdits(this, l, Map.of())); } 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 8d73bb744..602b40ea7 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 @@ -42,6 +42,7 @@ import util::Reflective; import lang::pico::\syntax::Main; import DateTime; import IO; +import Location; import String; import lang::box::\syntax::Box; @@ -73,14 +74,13 @@ set[LanguageService] picoLanguageServer(bool allowRecovery) = { formatting(picoFormattingService) }; -list[TextEdit] picoFormattingService(Tree input, FormattingOptions opts) { - // pico tree to box formatting representation +list[TextEdit] picoFormattingService(Tree input, loc range, FormattingOptions opts) { str original = ""; - print("[original]"); - rprintln(original); + // pico tree to box formatting representation box = toBox(input); box = visit (box) { case i:I(_) => i[is=opts.tabSize] }; + // box to string formatted = format(box); @@ -112,7 +112,8 @@ list[TextEdit] picoFormattingService(Tree input, FormattingOptions opts) { // computelayout differences as edits, and restore comments edits = layoutDiff(input, parse(#start[Program], formatted)); - return edits; + // TODO Instead of computing all edits and filtering, we can be more efficient by only formatting certain trees. + return [e | e <- edits, isContainedIn(e.range, range)]; } Box toBox((Program) `begin <{Statement ";"}* body> end`, FormatOptions opts = formatOptions()) diff --git a/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc b/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc index a115be424..b3189d61f 100644 --- a/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc +++ b/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc @@ -277,7 +277,7 @@ data LanguageService , loc (Focus _focus) prepareRenameService = defaultPrepareRenameService) | didRenameFiles(tuple[list[DocumentEdit], set[Message]] (list[DocumentEdit] fileRenames) didRenameFilesService) | selectionRange(list[loc](Focus _focus) selectionRangeService) - | formatting (list[TextEdit](Tree _input, FormattingOptions _opts) formattingService) + | formatting (list[TextEdit](Tree _input, loc range, FormattingOptions _opts) formattingService) ; loc defaultPrepareRenameService(Focus _:[Tree tr, *_]) = tr.src when tr.src?; From 17486782677e4877dbd89763fec3d0ed66ffc897 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 25 Aug 2025 16:47:15 +0200 Subject: [PATCH 17/32] Match contribution sig in implementation. --- .../parametric/ILanguageContributions.java | 2 +- .../InterpretedLanguageContributions.java | 13 ++------ .../LanguageContributionsMultiplexer.java | 4 +-- .../ParametricTextDocumentService.java | 30 +++++++++---------- .../parametric/ParserOnlyContribution.java | 2 +- 5 files changed, 21 insertions(+), 30 deletions(-) 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 8d7f7acba..5469f642c 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 @@ -61,7 +61,7 @@ public interface ILanguageContributions { public InterruptibleFuture implementation(IList focus); public InterruptibleFuture codeAction(IList focus); public InterruptibleFuture selectionRange(IList focus); - public InterruptibleFuture formatting(ITree input, ISourceLocation loc, ISet formattingOptions); + public InterruptibleFuture formatting(ITree input, ISourceLocation loc, IConstructor formattingOptions); public InterruptibleFuture prepareRename(IList focus); public InterruptibleFuture rename(IList focus, String name); 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 b1799e48e..9d0f3fd29 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 @@ -26,15 +26,12 @@ */ package org.rascalmpl.vscode.lsp.parametric; -import static org.rascalmpl.vscode.lsp.util.EvaluatorUtil.runEvaluator; - import java.io.IOException; import java.io.StringReader; import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; -import org.apache.commons.lang3.tuple.Pair; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.checkerframework.checker.nullness.qual.Nullable; @@ -399,13 +396,9 @@ public InterruptibleFuture selectionRange(IList focus) { } @Override - public InterruptibleFuture formatting(ITree input, ISet formattingOptions) { - debug(LanguageContributions.FORMATTING, input != null ? TreeAdapter.getLocation(input) : null, formattingOptions.size()); - return InterruptibleFuture.flatten(formatting.thenCombine(parsing, Pair::of) - .thenApply(pair -> - runEvaluator(LanguageContributions.FORMATTING, eval, actualEval -> - (IList) actualEval.call(actualEval.getMonitor(), "util::LanguageServer", "formatter", input, formattingOptions, pair.getLeft(), pair.getRight()) - , VF.list(), exec, true, client)), exec); + public InterruptibleFuture formatting(ITree input, ISourceLocation loc, IConstructor formattingOptions) { + debug(LanguageContributions.FORMATTING, input != null ? TreeAdapter.getLocation(input) : null, formattingOptions); + return execFunction(LanguageContributions.FORMATTING, formatting, VF.list(), input, loc, formattingOptions); } private void debug(String name, Object param) { 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 10e70a7d2..68ff8e011 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 @@ -342,8 +342,8 @@ public InterruptibleFuture selectionRange(IList focus) { } @Override - public InterruptibleFuture formatting(ITree input, ISet formattingOptions) { - return flatten(formatting, c -> c.formatting(input, formattingOptions)); + public InterruptibleFuture formatting(ITree input, ISourceLocation loc, IConstructor formattingOptions) { + return flatten(formatting, c -> c.formatting(input, loc, formattingOptions)); } @Override 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 7b849fa04..65b18b0ef 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 @@ -34,7 +34,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; @@ -146,7 +145,6 @@ import io.usethesource.vallang.IConstructor; import io.usethesource.vallang.IList; import io.usethesource.vallang.ISet; -import io.usethesource.vallang.ISetWriter; import io.usethesource.vallang.ISourceLocation; import io.usethesource.vallang.IString; import io.usethesource.vallang.ITuple; @@ -243,6 +241,7 @@ public void initializeServerCapabilities(ServerCapabilities result) { 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); @@ -750,7 +749,8 @@ private CompletableFuture> format(TextDocumentIdentifie final ILanguageContributions contribs = contributions(uri); // convert the `FormattingOptions` map to a `set[FormattingOption]` - ISet optSet = getFormattingOptions(options); + IConstructor optSet = getFormattingOptions(options); + // call the `formatting` implementation of the relevant language contribution return getFile(uri) .getCurrentTreeAsync() @@ -766,19 +766,17 @@ private CompletableFuture> format(TextDocumentIdentifie .thenApply(l -> DocumentChanges.translateTextEdits(this, l, Map.of())); } - private ISet getFormattingOptions(FormattingOptions options) { - var optionType = tf.abstractDataType(typeStore, "FormattingOption"); - - ISetWriter opts = VF.setWriter(); - for (Entry> e : options.entrySet()) { - var opt = e.getValue().map( - s -> VF.constructor(tf.constructor(typeStore, optionType, e.getKey(), tf.stringType()), VF.string(s)), - n -> VF.constructor(tf.constructor(typeStore, optionType, e.getKey(), tf.integerType()), VF.integer(n.longValue())), - b -> VF.constructor(tf.constructor(typeStore, optionType, e.getKey())) - ); - opts.append(opt); - } - return opts.done(); + 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) { diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParserOnlyContribution.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParserOnlyContribution.java index 72954eb65..c16bfed8e 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParserOnlyContribution.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParserOnlyContribution.java @@ -190,7 +190,7 @@ public InterruptibleFuture codeAction(IList focus) { } @Override - public InterruptibleFuture formatting(ITree input, ISet formattingOptions) { + public InterruptibleFuture formatting(ITree input, ISourceLocation loc, IConstructor formattingOptions) { return InterruptibleFuture.completedFuture(VF.list()); } From 1d56010edc18f0085a72e6ac3d1736c43b198dda Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 25 Aug 2025 16:49:37 +0200 Subject: [PATCH 18/32] Add missing license header. --- .../src/main/rascal/library/util/Format.rsc | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/rascal-lsp/src/main/rascal/library/util/Format.rsc b/rascal-lsp/src/main/rascal/library/util/Format.rsc index 9a1814b6d..598bc42e8 100644 --- a/rascal-lsp/src/main/rascal/library/util/Format.rsc +++ b/rascal-lsp/src/main/rascal/library/util/Format.rsc @@ -1,3 +1,29 @@ +@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 util::Format import List; From 20a71ae7606317c5e3cb9c52bf670767cc4d4fad Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 25 Aug 2025 17:42:34 +0200 Subject: [PATCH 19/32] Document formatting service. --- .../src/main/rascal/library/util/LanguageServer.rsc | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc b/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc index b3189d61f..f3bbdda2c 100644 --- a/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc +++ b/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc @@ -212,6 +212,7 @@ hover documentation, definition with uses, references to declarations, implement * The optional `prepareRename` service argument discovers places in the editor where a ((util::LanguageServer::rename)) is possible. If renameing the location is not supported, it should throw an exception. * The ((didRenameFiles)) service collects ((DocumentEdit))s corresponding to renamed files (e.g. to rename a class when the class file was renamed). The IDE applies the edits after moving the files. It might fail and report why in diagnostics. * The ((selectionRange)) service discovers selections around a cursor, that a user might want to select. It expects the list of source locations to be in ascending order of size (each location should be contained by the next) - similar to ((Focus)) trees. +* 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`. ((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 @@ -283,6 +284,16 @@ data LanguageService loc defaultPrepareRenameService(Focus _:[Tree tr, *_]) = tr.src when tr.src?; default loc defaultPrepareRenameService(Focus focus) { throw IllegalArgument(focus, "Element under cursor does not have source location"); } +@synopsis{Options for formatting of programs.} +@description{ +Options that specify how to format contents of a file. +* `insertSpaces`; if `true`, use spaces for indentation; if `false`, use tabs. +* `tabSize`; if `insertSpaces == true`, use this amount of spaces for a single level of indentation. +* `trimTrailingWhiteSpace`; if `true`, remove any whitespace (except newlines) from ends of formatted lines. +* `insertFinalNewline`; if `true`, and the file does not end with a new line, add one. +* `trimFinalNewlines`; if `true`, and the file ends in one or more new lines, remove them. + Note: formatting with `insertFinalNewline && trimFinalNewlines` is expected to return a file that ends in a single newline. +} data FormattingOptions( int tabSize = 4 , bool insertSpaces = true From cda15311756ca01745a58fd9596959912ab00b2e Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 25 Aug 2025 17:58:44 +0200 Subject: [PATCH 20/32] Use formatter from stdlib. --- .../library/demo/lang/pico/LanguageServer.rsc | 69 +++---------------- 1 file changed, 10 insertions(+), 59 deletions(-) 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 602b40ea7..beef4b5fa 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 @@ -40,6 +40,7 @@ import ParseTree; import util::ParseErrorRecovery; import util::Reflective; import lang::pico::\syntax::Main; +import lang::pico::\syntax::Format; import DateTime; import IO; import Location; @@ -76,87 +77,37 @@ set[LanguageService] picoLanguageServer(bool allowRecovery) = { list[TextEdit] picoFormattingService(Tree input, loc range, FormattingOptions opts) { str original = ""; - - // pico tree to box formatting representation box = toBox(input); - box = visit (box) { case i:I(_) => i[is=opts.tabSize] }; - - // box to string + box = visit (box) { case i:I(_) => i[is=opts.tabSize] } formatted = format(box); - //// line-based modifications - // identity operator, to compose with other operators - formatLine = str(str s) { return s; }; - if (!opts.insertSpaces) { - // replace indentation spaces with tabs - formatLine = indentSpacesAsTabs(opts.tabSize) o formatLine; - } if (!opts.trimTrailingWhitespace) { // restore trailing whitespace that was lost during tree->box->text, or find a way to not lose it println("The Pico formatter does not support maintaining trailing whitespace."); } - // do line-based processing - formatted = perLine(formatted, formatLine); + if (!opts.insertSpaces) { + // replace indentation spaces with tabs + formatted = perLine(formatted, indentSpacesAsTabs(opts.tabSize)); + } - // whole-file processing if (/^.*[^\n]$/s := original) { // replace original final newlines or remove the one introduced by ((format)) (`Box2Text`) formatted = replaceLast(formatted, "\n", opts.trimFinalNewlines ? "" : newlines); } + if (opts.insertFinalNewline) { // ensure presence of final newline formatted = insertFinalNewline(formatted); } - // computelayout differences as edits, and restore comments - edits = layoutDiff(input, parse(#start[Program], formatted)); + // compute layout differences as edits, and restore comments + edits = layoutDiff(input, parse(#start[Program], formatted, input@\loc.top)); - // TODO Instead of computing all edits and filtering, we can be more efficient by only formatting certain trees. + // instead of computing all edits and filtering, we can be more efficient by only formatting certain trees. return [e | e <- edits, isContainedIn(e.range, range)]; } -Box toBox((Program) `begin <{Statement ";"}* body> end`, FormatOptions opts = formatOptions()) - = V([ - L("begin"), - I([ - V([ - toBox(decls, opts=opts), - toBox(body, opts=opts) - ], vs=1) - ]), - L("end") - ]); - -Box toBox((Declarations) `declare <{IdType ","}* decls> ;`, FormatOptions opts = formatOptions()) - = V([ - L("declare"), - A([ - R([ - toBox(id, opts=opts), - L(":"), - H([toBox(tp, opts=opts), decl != decls[-1] ? L(",") : L(";")], hs=0) - ]) - | decl:(IdType) ` : ` <- decls - ]) - ]); - -Box toBox(({Statement ";"}*) stmts, FormatOptions opts = formatOptions()) - = V([ - H([ - toBox(stmt, opts=opts), - stmt != stmts[-1] ? L(";") : NULL() - ], hs=0) - | stmt <- stmts - ]); - -Box toBox((Statement) `while do <{Statement ";"}* body> od`, FormatOptions opts = formatOptions()) - = V([ - HOV([L("while"), I([toBox(cond, opts=opts)]), L("do")]), - I([toBox(body, opts=opts)]), - L("od") - ]); - set[LanguageService] picoLanguageServer() = picoLanguageServer(false); set[LanguageService] picoLanguageServerWithRecovery() = picoLanguageServer(true); From 206a57a755fa50b031727b5636cb11665025910c Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 27 Aug 2025 14:05:33 +0200 Subject: [PATCH 21/32] Change defaults based on discussion with @DavyLandman. --- rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc b/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc index f3bbdda2c..fc27e137b 100644 --- a/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc +++ b/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc @@ -297,9 +297,9 @@ Options that specify how to format contents of a file. data FormattingOptions( int tabSize = 4 , bool insertSpaces = true - , bool trimTrailingWhitespace = true - , bool insertFinalNewline = true - , bool trimFinalNewlines = true + , bool trimTrailingWhitespace = false + , bool insertFinalNewline = false + , bool trimFinalNewlines = false ) = formattingOptions(); @deprecated{Backward compatible with ((parsing)).} From 6cdab513b3b0b63250212d0aa8aea639973c3bec Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 28 Aug 2025 16:09:08 +0200 Subject: [PATCH 22/32] Fix import to renamed module. --- .../src/main/rascal/library/demo/lang/pico/LanguageServer.rsc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 beef4b5fa..2ec2342a4 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 @@ -40,7 +40,7 @@ import ParseTree; import util::ParseErrorRecovery; import util::Reflective; import lang::pico::\syntax::Main; -import lang::pico::\syntax::Format; +import lang::pico::format::Formatting; import DateTime; import IO; import Location; From 05b29555ce97ed5d09bfb3fa28b2fc04222fa095 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Thu, 28 Aug 2025 16:09:36 +0200 Subject: [PATCH 23/32] Format Pico examples. --- .../library/demo/lang/pico/examples/fac.pico | 15 +++++------ .../library/demo/lang/pico/examples/ite.pico | 25 +++++++++---------- 2 files changed, 18 insertions(+), 22 deletions(-) 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..0a26dee3d 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,9 @@ -begin - declare - input : natural, - output : natural, - repnr : natural, - rep : 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 +12,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 From 95618838fe550d6a4a8e85cda199e256f8c4de6d Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 3 Sep 2025 10:41:45 +0200 Subject: [PATCH 24/32] Inline function. --- .../vscode/lsp/rascal/RascalTextDocumentService.java | 3 ++- .../org/rascalmpl/vscode/lsp/util/DocumentChanges.java | 8 ++------ 2 files changed, 4 insertions(+), 7 deletions(-) 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 32510704a..43d8dad56 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 @@ -366,7 +366,8 @@ public CompletableFuture DocumentChanges.locationToRange(this, TreeAdapter.getLocation(cur))) + .thenApply(TreeAdapter::getLocation) + .thenApply(loc -> Locations.toRange(loc, columns)) .thenApply(Either3::forFirst); } diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentChanges.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentChanges.java index 94a92c0d7..9f7a9b0c4 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentChanges.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/util/DocumentChanges.java @@ -101,7 +101,8 @@ public static List translateTextEdits(final IBaseTextDocumentService d return edits.stream() .map(IConstructor.class::cast) .map(c -> { - var range = locationToRange(docService, (ISourceLocation) c.get("range")); + var loc = (ISourceLocation) c.get("range"); + var range = Locations.toRange(loc, docService.getColumnMap(loc)); var replacement = ((IString) c.get("replacement")).getValue(); // Check annotation var kw = c.asWithKeywordParameters(); @@ -125,11 +126,6 @@ public static List translateTextEdits(final IBaseTextDocumentService d .collect(Collectors.toList()); } - public static Range locationToRange(final IBaseTextDocumentService docService, ISourceLocation loc) { - LineColumnOffsetMap columnMap = docService.getColumnMap(loc); - return Locations.toRange(loc, columnMap); - } - private static String getFileURI(IConstructor edit, String label) { return ((ISourceLocation) edit.get(label)).getURI().toString(); } From 7b9203319b5a841cb30f8550117c18656f65e354 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 3 Sep 2025 11:06:22 +0200 Subject: [PATCH 25/32] Use focus instead of Tree+loc. --- .../parametric/ILanguageContributions.java | 2 +- .../InterpretedLanguageContributions.java | 6 +-- .../LanguageContributionsMultiplexer.java | 4 +- .../ParametricTextDocumentService.java | 51 +++++++++++++------ .../parametric/ParserOnlyContribution.java | 2 +- .../library/demo/lang/pico/LanguageServer.rsc | 13 +++-- .../rascal/library/util/LanguageServer.rsc | 2 +- 7 files changed, 52 insertions(+), 28 deletions(-) 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 5469f642c..2601c327c 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 @@ -61,7 +61,7 @@ public interface ILanguageContributions { public InterruptibleFuture implementation(IList focus); public InterruptibleFuture codeAction(IList focus); public InterruptibleFuture selectionRange(IList focus); - public InterruptibleFuture formatting(ITree input, ISourceLocation loc, IConstructor formattingOptions); + public InterruptibleFuture formatting(IList input, IConstructor formattingOptions); public InterruptibleFuture prepareRename(IList focus); public InterruptibleFuture rename(IList focus, String name); 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 9d0f3fd29..6c4f6c3d3 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 @@ -396,9 +396,9 @@ public InterruptibleFuture selectionRange(IList focus) { } @Override - public InterruptibleFuture formatting(ITree input, ISourceLocation loc, IConstructor formattingOptions) { - debug(LanguageContributions.FORMATTING, input != null ? TreeAdapter.getLocation(input) : null, formattingOptions); - return execFunction(LanguageContributions.FORMATTING, formatting, VF.list(), input, loc, formattingOptions); + 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) { 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 68ff8e011..c3f36aabd 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 @@ -342,8 +342,8 @@ public InterruptibleFuture selectionRange(IList focus) { } @Override - public InterruptibleFuture formatting(ITree input, ISourceLocation loc, IConstructor formattingOptions) { - return flatten(formatting, c -> c.formatting(input, loc, formattingOptions)); + public InterruptibleFuture formatting(IList focus, IConstructor formattingOptions) { + return flatten(formatting, c -> c.formatting(focus, formattingOptions)); } @Override 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 65b18b0ef..c1d6498c0 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 @@ -114,6 +114,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.values.IRascalValueFactory; import org.rascalmpl.values.parsetrees.ITree; @@ -413,7 +414,7 @@ private CompletableFuture computeRenameRange(final ILanguageCon public CompletableFuture rename(RenameParams params) { logger.trace("rename for: {}, new name: {}", params.getTextDocument().getUri(), params.getNewName()); final ILanguageContributions contribs = contributions(params.getTextDocument()); - final Position rascalPos = Locations.toRascalPosition(params.getTextDocument(), params.getPosition(), columns);; + final Position rascalPos = Locations.toRascalPosition(params.getTextDocument(), params.getPosition(), columns); return getFile(params.getTextDocument()) .getCurrentTreeAsync() .thenApply(Versioned::get) @@ -736,34 +737,54 @@ public CompletableFuture>> codeAction(CodeActio @Override public CompletableFuture> formatting(DocumentFormattingParams params) { logger.debug("Formatting: {}", params); - return format(params.getTextDocument(), null, params.getOptions()); + + TextDocumentIdentifier uri = params.getTextDocument(); + final ILanguageContributions contribs = contributions(uri); + + // call the `formatting` implementation of the relevant language contribution + return getFile(uri) + .getCurrentTreeAsync() + .thenApply(Versioned::get) + .thenCompose(tree -> { + final var opts = getFormattingOptions(params.getOptions()); + return contribs.formatting(VF.list(tree), opts).get(); + }) + .thenApply(l -> DocumentChanges.translateTextEdits(this, l, Map.of())); } @Override public CompletableFuture> rangeFormatting(DocumentRangeFormattingParams params) { logger.debug("Formatting range: {}", params); - return format(params.getTextDocument(), params.getRange(), params.getOptions()); - } - private CompletableFuture> format(TextDocumentIdentifier uri, @Nullable Range range, FormattingOptions options) { + TextDocumentIdentifier uri = params.getTextDocument(); + Range range = params.getRange(); final ILanguageContributions contribs = contributions(uri); - // convert the `FormattingOptions` map to a `set[FormattingOption]` - IConstructor optSet = getFormattingOptions(options); - // call the `formatting` implementation of the relevant language contribution - return getFile(uri) + var fileState = getFile(uri); + return fileState .getCurrentTreeAsync() .thenApply(Versioned::get) .thenCompose(tree -> { - // range to Rascal loc - ISourceLocation loc = range == null - ? TreeAdapter.getLocation(tree) - : null; // TODO map Range to ISourceLocation - return contribs.formatting(tree, loc, optSet).get(); + // just a range + var start = Locations.toRascalPosition(uri, range.getStart(), columns); + var end = Locations.toRascalPosition(uri, range.getEnd(), columns); + // compute the focus list at the end of the range + var focus = TreeSearch.computeFocusList(tree, end.getLine(), end.getCharacter()) + .stream() + .map(ITree.class::cast) + // check for containment of the start of the range + .filter(t -> Ranges.containsPosition(Locations.toRange(TreeAdapter.getLocation(t), columns), start)) + .collect(VF.listWriter()); + + var opts = getFormattingOptions(params.getOptions()); + return contribs.formatting(focus, opts).get(); }) // convert the document changes - .thenApply(l -> DocumentChanges.translateTextEdits(this, l, Map.of())); + .thenApply(l -> DocumentChanges.translateTextEdits(this, l, Map.of()) + .stream() + .filter(e -> Ranges.containsRange(range, e.getRange())) + .collect(Collectors.toList())); } private IConstructor getFormattingOptions(FormattingOptions options) { diff --git a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParserOnlyContribution.java b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParserOnlyContribution.java index c16bfed8e..75a806289 100644 --- a/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParserOnlyContribution.java +++ b/rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/ParserOnlyContribution.java @@ -190,7 +190,7 @@ public InterruptibleFuture codeAction(IList focus) { } @Override - public InterruptibleFuture formatting(ITree input, ISourceLocation loc, IConstructor formattingOptions) { + public InterruptibleFuture formatting(IList focus, IConstructor formattingOptions) { return InterruptibleFuture.completedFuture(VF.list()); } 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 2ec2342a4..6ebea3f4b 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 @@ -75,9 +75,9 @@ set[LanguageService] picoLanguageServer(bool allowRecovery) = { formatting(picoFormattingService) }; -list[TextEdit] picoFormattingService(Tree input, loc range, FormattingOptions opts) { - str original = ""; - box = toBox(input); +list[TextEdit] picoFormattingService(Focus input, FormattingOptions opts) { + str original = ""; + box = toBox(input[-1]); box = visit (box) { case i:I(_) => i[is=opts.tabSize] } formatted = format(box); @@ -102,10 +102,13 @@ list[TextEdit] picoFormattingService(Tree input, loc range, FormattingOptions op } // compute layout differences as edits, and restore comments - edits = layoutDiff(input, parse(#start[Program], formatted, input@\loc.top)); + edits = layoutDiff(input[-1], parse(#start[Program], formatted, input[-1]@\loc.top)); // instead of computing all edits and filtering, we can be more efficient by only formatting certain trees. - return [e | e <- edits, isContainedIn(e.range, range)]; + loc range = input[0]@\loc; + filteredEdits = [e | e <- edits, isContainedIn(e.range, range)]; + + return filteredEdits; } set[LanguageService] picoLanguageServer() = picoLanguageServer(false); diff --git a/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc b/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc index fc27e137b..8e0c05107 100644 --- a/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc +++ b/rascal-lsp/src/main/rascal/library/util/LanguageServer.rsc @@ -278,7 +278,7 @@ data LanguageService , loc (Focus _focus) prepareRenameService = defaultPrepareRenameService) | didRenameFiles(tuple[list[DocumentEdit], set[Message]] (list[DocumentEdit] fileRenames) didRenameFilesService) | selectionRange(list[loc](Focus _focus) selectionRangeService) - | formatting (list[TextEdit](Tree _input, loc range, FormattingOptions _opts) formattingService) + | formatting (list[TextEdit](Focus _focus, FormattingOptions _opts) formattingService) ; loc defaultPrepareRenameService(Focus _:[Tree tr, *_]) = tr.src when tr.src?; From 244da24c857d28619b215c60589359f60a7174cb Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 8 Sep 2025 12:03:18 +0200 Subject: [PATCH 26/32] Implement range focus with special case for lists. --- .../ParametricTextDocumentService.java | 7 +- .../lsp/util/locations/impl/TreeSearch.java | 75 ++++++++++++++++++- 2 files changed, 75 insertions(+), 7 deletions(-) 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 c1d6498c0..0ad36f27b 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 @@ -770,12 +770,7 @@ public CompletableFuture> rangeFormatting(DocumentRange var start = Locations.toRascalPosition(uri, range.getStart(), columns); var end = Locations.toRascalPosition(uri, range.getEnd(), columns); // compute the focus list at the end of the range - var focus = TreeSearch.computeFocusList(tree, end.getLine(), end.getCharacter()) - .stream() - .map(ITree.class::cast) - // check for containment of the start of the range - .filter(t -> Ranges.containsPosition(Locations.toRange(TreeAdapter.getLocation(t), columns), start)) - .collect(VF.listWriter()); + var focus = TreeSearch.computeFocusList(tree, start.getLine(), start.getCharacter(), end.getLine(), end.getCharacter()); var opts = getFormattingOptions(params.getOptions()); return contribs.formatting(focus, opts).get(); 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..df077cff8 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,6 +90,17 @@ else if (line == loc.getEndLine()) { return true; } + private static boolean rightOf(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). * @@ -99,7 +115,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 +173,61 @@ 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, 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); + + final var commonSuffix = startList.intersect(endList); + if (commonSuffix.equals(startList) || commonSuffix.equals(endList)) { + // We do not have enough information to extend the focus + return commonSuffix; + } + // 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))) { + 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.getListASTArgs(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 -> !inside(TreeAdapter.getLocation(t), startLine, startColumn)) + .takeWhile(t -> rightOf(TreeAdapter.getLocation(t), 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 + var lw = VF.listWriter(); + lw.append(artificialParent); + lw.appendAll(commonSuffix); + return lw.done(); + } } From 69307a7b43e072d19f83be0a827f600bf6296218 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 8 Sep 2025 12:03:45 +0200 Subject: [PATCH 27/32] Fix comment about Rascal charcter encoding. --- .../rascalmpl/vscode/lsp/util/locations/impl/TreeSearch.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 df077cff8..1fd92fd4e 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 @@ -102,7 +102,7 @@ private static boolean rightOf(ISourceLocation loc, int line, int column) { } /** - * 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. From c1b08a84d09c458d0bc73d4e7d4d1a57f6e00582 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 10 Sep 2025 16:56:42 +0200 Subject: [PATCH 28/32] Add basic TreeSearch tests. --- .../swat/rascal/lsp/util/TreeSearchTests.java | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 rascal-lsp/src/test/java/engineering/swat/rascal/lsp/util/TreeSearchTests.java 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..eb6fbd239 --- /dev/null +++ b/rascal-lsp/src/test/java/engineering/swat/rascal/lsp/util/TreeSearchTests.java @@ -0,0 +1,93 @@ +/* + * 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.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 focusEndsWithModule() { + final var focus = computeFocusList(tree, 6, 4); + final var last = focus.get(focus.length() - 1); + assertEquals(tree, last); + } + + @Test + public void listPartialRange() { + final var focus = computeFocusList(tree, 4, 8, 6, 8); + final var selection = (ITree) focus.get(0); + final var originalList = (ITree) focus.get(1); + + assertListLength(selection, 3); + assertListLength(originalList, 4); + } + + + private static void assertListLength(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()); + } +} From 6f5657f89ca03373633882e8d1ac1969de702b0c Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 10 Sep 2025 16:57:07 +0200 Subject: [PATCH 29/32] Consider layout as well. --- .../rascalmpl/vscode/lsp/util/locations/impl/TreeSearch.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1fd92fd4e..b30712452 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 @@ -197,7 +197,7 @@ public static IList computeFocusList(ITree tree, int startLine, int startColumn, 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.getListASTArgs(parent); + final var elements = TreeAdapter.getArgs(parent); final int nElements = elements.length(); logger.trace("Smallest common tree is a {} with {} elements", TreeAdapter.getType(parent), nElements); From 49cf9bea3f86f2e2ef0418d8820df4311ceee182 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 10 Sep 2025 16:57:30 +0200 Subject: [PATCH 30/32] Include partially selected element at end of range. --- .../vscode/lsp/util/locations/impl/TreeSearch.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 b30712452..bf91bd074 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 @@ -90,15 +90,15 @@ else if (line == loc.getEndLine()) { return true; } - private static boolean rightOf(ISourceLocation loc, int line, int column) { + private static boolean rightOfBegin(ISourceLocation loc, int line, int column) { if (!loc.hasLineColumn()) { return false; } - if (line > loc.getEndLine()) { + if (line > loc.getBeginLine()) { return true; } - return line == loc.getEndLine() && column > loc.getEndColumn(); + return line == loc.getBeginLine() && column > loc.getBeginColumn(); } /** @@ -211,7 +211,7 @@ private static IList computeListRangeFocus(final IList commonSuffix, int startLi final var selected = elements.stream() .map(ITree.class::cast) .dropWhile(t -> !inside(TreeAdapter.getLocation(t), startLine, startColumn)) - .takeWhile(t -> rightOf(TreeAdapter.getLocation(t), endLine, endColumn)) + .takeWhile(t -> rightOfBegin(TreeAdapter.getLocation(t), endLine, endColumn)) .collect(VF.listWriter()); final int nSelected = selected.length(); From b23a5ea0acaecebdc8587f89994b583d939c34a5 Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Wed, 10 Sep 2025 16:57:51 +0200 Subject: [PATCH 31/32] Simplify prepending element. --- .../rascalmpl/vscode/lsp/util/locations/impl/TreeSearch.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 bf91bd074..0d04a8ba7 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 @@ -225,9 +225,6 @@ private static IList computeListRangeFocus(final IList commonSuffix, int startLi final var artificialParent = TreeAdapter.setLocation(VF.appl(TreeAdapter.getProduction(parent), selected), selectionLoc); // Build new focus list - var lw = VF.listWriter(); - lw.append(artificialParent); - lw.appendAll(commonSuffix); - return lw.done(); + return commonSuffix.insert(artificialParent); } } From ce462946f7bbbcbd076bfe29d9a12c8b72aa136c Mon Sep 17 00:00:00 2001 From: Toine Hartman Date: Mon, 15 Sep 2025 13:19:24 +0200 Subject: [PATCH 32/32] Test & fix list focus. --- rascal-lsp/pom.xml | 6 +++ .../lsp/util/locations/impl/TreeSearch.java | 38 +++++++++++++--- .../swat/rascal/lsp/util/TreeSearchTests.java | 44 ++++++++++++++++--- 3 files changed, 76 insertions(+), 12 deletions(-) diff --git a/rascal-lsp/pom.xml b/rascal-lsp/pom.xml index aad8ffb08..f2a6395f8 100644 --- a/rascal-lsp/pom.xml +++ b/rascal-lsp/pom.xml @@ -153,6 +153,12 @@ 3.5.3 + + + always + org.rascalmpl.vscode.lsp.log.LogRedirectConfiguration + + org.rascalmpl 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 0d04a8ba7..f2b882d9e 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 @@ -101,6 +101,17 @@ private static boolean rightOfBegin(ISourceLocation loc, int line, int column) { 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-32). * @@ -180,14 +191,16 @@ public static IList computeFocusList(ITree tree, int startLine, int startColumn, 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); - if (commonSuffix.equals(startList) || commonSuffix.equals(endList)) { - // We do not have enough information to extend the focus - return commonSuffix; - } + + 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); } @@ -210,9 +223,22 @@ private static IList computeListRangeFocus(final IList commonSuffix, int startLi // Find the elements in the list that are (partially) selected. final var selected = elements.stream() .map(ITree.class::cast) - .dropWhile(t -> !inside(TreeAdapter.getLocation(t), startLine, startColumn)) - .takeWhile(t -> rightOfBegin(TreeAdapter.getLocation(t), endLine, endColumn)) + .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); 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 index eb6fbd239..762d59e35 100644 --- 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 @@ -27,6 +27,7 @@ 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; @@ -68,26 +69,57 @@ 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 = focus.get(focus.length() - 1); + final var last = (ITree) focus.get(focus.length() - 1); assertEquals(tree, last); } @Test - public void listPartialRange() { - final var focus = computeFocusList(tree, 4, 8, 6, 8); + 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); - assertListLength(selection, 3); - assertListLength(originalList, 4); + 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); + } - private static void assertListLength(final ITree list, int length) { + @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))); } }