From e914d3a3afc121f76d10f09183d627d9ef9890b0 Mon Sep 17 00:00:00 2001 From: Siddharth Srinivasan Date: Fri, 13 Sep 2024 10:58:19 +0530 Subject: [PATCH] Fix code-folding for LSP clients that support line-folding only 1. Added `TextDocumentServiceImpl.convertToLineOnlyFolds` which: - accepts code-folds specified as fine-grained Position-based (line, column) ranges, and, - changes them to coarse-grained line-only ranges. - The line-only ranges computed uphold the code-folding invariant that: *a fold **does not end** at the same point **where** another fold **starts**. * 2. This is used in `TextDocumentServiceImpl.foldingRange()` when the client capabilities have `FoldingRangeClientCapabilities.lineFoldingOnly = true`. Signed-off-by: Siddharth Srinivasan --- .../protocol/TextDocumentServiceImpl.java | 68 ++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImpl.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImpl.java index 66b4e4287732..0e6f7fd6f685 100644 --- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImpl.java +++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImpl.java @@ -43,10 +43,13 @@ import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; +import java.util.Deque; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; @@ -1550,12 +1553,13 @@ public CompletableFuture> foldingRange(FoldingRangeRequestPar if (source == null) { return CompletableFuture.completedFuture(Collections.emptyList()); } + final boolean lineFoldingOnly = client.getNbCodeCapabilities().getClientCapabilities().getTextDocument().getFoldingRange().getLineFoldingOnly() == Boolean.TRUE; CompletableFuture> result = new CompletableFuture<>(); try { source.runUserActionTask(cc -> { cc.toPhase(JavaSource.Phase.RESOLVED); Document doc = cc.getSnapshot().getSource().getDocument(true); - JavaElementFoldVisitor v = new JavaElementFoldVisitor(cc, cc.getCompilationUnit(), cc.getTrees().getSourcePositions(), doc, new FoldCreator() { + JavaElementFoldVisitor v = new JavaElementFoldVisitor<>(cc, cc.getCompilationUnit(), cc.getTrees().getSourcePositions(), doc, new FoldCreator() { @Override public FoldingRange createImportsFold(int start, int end) { return createFold(start, end, FoldingRangeKind.Imports); @@ -1600,7 +1604,10 @@ private FoldingRange createFold(int start, int end, String kind) { }); v.checkInitialFold(); v.scan(cc.getCompilationUnit(), null); - result.complete(v.getFolds()); + List folds = v.getFolds(); + if (lineFoldingOnly) + folds = convertToLineOnlyFolds(folds); + result.complete(folds); }, true); } catch (IOException ex) { result.completeExceptionally(ex); @@ -1608,6 +1615,63 @@ private FoldingRange createFold(int start, int end, String kind) { return result; } + /** + * Converts a list of code-folds to a line-only Range form, in place of the + * finer-grained form of {@linkplain Position Position-based} (line, column) Ranges. + *

+ * This is needed for LSP clients that do not support the finer grained Range + * specification. This is expected to be advertised by the client in + * {@code FoldingRangeClientCapabilities.lineFoldingOnly}. + * + * @implSpec The line-only ranges computed uphold the code-folding invariant that: + * a fold does not end at the same point where another fold starts. + * + * @implNote This is performed in {@code O(n log n) + O(n)} time and {@code O(n)} space for the returned list. + * + * @param folds List of code-folding ranges computed for a textDocument, + * containing fine-grained {@linkplain Position Position-based} + * (line, column) ranges. + * @return List of code-folding ranges computed for a textDocument, + * containing coarse-grained line-only ranges. + * + * @see + * LSP FoldingRangeClientCapabilities + */ + private List convertToLineOnlyFolds(List folds) { + if (folds != null && folds.size() > 1) { + // Ensure that the folds are sorted in increasing order of their start position + folds = new ArrayList<>(folds); + folds.sort(Comparator.comparingInt(FoldingRange::getStartLine) + .thenComparing(FoldingRange::getStartCharacter)); + // Maintain a stack of enclosing folds + Deque enclosingFolds = new ArrayDeque<>(); + for (FoldingRange fold : folds) { + FoldingRange last; + while ((last = enclosingFolds.peek()) != null && + (last.getEndLine() < fold.getEndLine() || last.getEndCharacter() < fold.getEndCharacter())) { + // The last enclosingFold does not enclose this fold. + // Due to sortedness of the folds, last also ends before this fold starts. + enclosingFolds.pop(); + // If needed, adjust last to end on a line prior to this fold start + if (last.getEndLine() == fold.getStartLine()) { + last.setEndLine(last.getEndLine() - 1); + } + last.setEndCharacter(null); // null denotes the end of the line. + last.setStartCharacter(null); // null denotes the end of the line. + } + enclosingFolds.push(fold); + } + // empty the stack; since each fold completely encloses the next higher one. + FoldingRange fold; + while ((fold = enclosingFolds.poll()) != null) { + fold.setEndCharacter(null); // null denotes the end of the line. + fold.setStartCharacter(null); // null denotes the end of the line. + } + } + return folds; + } + + @Override public void didOpen(DidOpenTextDocumentParams params) { LOG.log(Level.FINER, "didOpen: {0}", params);