forked from oracle/javavscode
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[JVSC-249] Fix code folding in VS Code due to line-only folding limit…
…ation Added the backport of the Netbeans PR apache/netbeans#7750 patch which allows supporting LSP clients which have line-only folding support. - This is the case of VS Code as referenced in microsoft/vscode#50840. Closes oracle#249 Signed-off-by: Siddharth Srinivasan <siddharth.srinivasan@oracle.com>
- Loading branch information
Showing
2 changed files
with
294 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,293 @@ | ||
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 c112b4eb73..ff1fe7f903 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 | ||
@@ -44,14 +44,18 @@ import java.net.MalformedURLException; | ||
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.EnumMap; | ||
import java.util.EnumSet; | ||
import java.util.HashMap; | ||
import java.util.HashSet; | ||
+import java.util.Iterator; | ||
import java.util.LinkedHashMap; | ||
import java.util.List; | ||
import java.util.Locale; | ||
@@ -1561,12 +1565,13 @@ public class TextDocumentServiceImpl implements TextDocumentService, LanguageCli | ||
if (source == null) { | ||
return CompletableFuture.completedFuture(Collections.emptyList()); | ||
} | ||
+ final boolean lineFoldingOnly = client.getNbCodeCapabilities().getClientCapabilities().getTextDocument().getFoldingRange().getLineFoldingOnly() == Boolean.TRUE; | ||
CompletableFuture<List<FoldingRange>> 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<FoldingRange>() { | ||
+ JavaElementFoldVisitor<FoldingRange> v = new JavaElementFoldVisitor<>(cc, cc.getCompilationUnit(), cc.getTrees().getSourcePositions(), doc, new FoldCreator<FoldingRange>() { | ||
@Override | ||
public FoldingRange createImportsFold(int start, int end) { | ||
return createFold(start, end, FoldingRangeKind.Imports); | ||
@@ -1611,7 +1616,10 @@ public class TextDocumentServiceImpl implements TextDocumentService, LanguageCli | ||
}); | ||
v.checkInitialFold(); | ||
v.scan(cc.getCompilationUnit(), null); | ||
- result.complete(v.getFolds()); | ||
+ List<FoldingRange> folds = v.getFolds(); | ||
+ if (lineFoldingOnly) | ||
+ folds = convertToLineOnlyFolds(folds); | ||
+ result.complete(folds); | ||
}, true); | ||
} catch (IOException ex) { | ||
result.completeExceptionally(ex); | ||
@@ -1619,6 +1627,76 @@ public class TextDocumentServiceImpl implements TextDocumentService, LanguageCli | ||
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. | ||
+ * <p> | ||
+ * 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: | ||
+ * <em>a fold <b>does not end</b> at the same point <b>where</b> another fold <b>starts</b></em>. | ||
+ * | ||
+ * @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 <a href="https://microsoft.github.io/language-server-protocol/specifications/specification-current/#foldingRangeClientCapabilities"> | ||
+ * LSP FoldingRangeClientCapabilities</a> | ||
+ */ | ||
+ static List<FoldingRange> convertToLineOnlyFolds(List<FoldingRange> 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<FoldingRange> enclosingFolds = new ArrayDeque<>(); | ||
+ for (FoldingRange fold : folds) { | ||
+ FoldingRange last; | ||
+ while ((last = enclosingFolds.peek()) != null && | ||
+ (last.getEndLine() < fold.getEndLine() || | ||
+ (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. | ||
+ } | ||
+ // Remove invalid or duplicate folds | ||
+ Iterator<FoldingRange> it = folds.iterator(); | ||
+ FoldingRange prev = null; | ||
+ while(it.hasNext()) { | ||
+ FoldingRange next = it.next(); | ||
+ if (next.getEndLine() <= next.getStartLine() || | ||
+ (prev != null && prev.equals(next))) { | ||
+ it.remove(); | ||
+ } else { | ||
+ prev = next; | ||
+ } | ||
+ } | ||
+ } | ||
+ return folds; | ||
+ } | ||
+ | ||
+ | ||
@Override | ||
public void didOpen(DidOpenTextDocumentParams params) { | ||
LOG.log(Level.FINER, "didOpen: {0}", params); | ||
diff --git a/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImplTest.java b/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImplTest.java | ||
index 0f2bda50ae..06fd93d3e5 100644 | ||
--- a/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImplTest.java | ||
+++ b/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImplTest.java | ||
@@ -18,14 +18,19 @@ | ||
*/ | ||
package org.netbeans.modules.java.lsp.server.protocol; | ||
|
||
+import java.util.Collections; | ||
+import java.util.List; | ||
import java.util.concurrent.atomic.AtomicInteger; | ||
import javax.swing.event.DocumentEvent; | ||
import javax.swing.event.DocumentListener; | ||
import javax.swing.text.BadLocationException; | ||
import javax.swing.text.Document; | ||
import javax.swing.text.PlainDocument; | ||
+import org.eclipse.lsp4j.FoldingRange; | ||
import org.netbeans.junit.NbTestCase; | ||
|
||
+import static org.netbeans.modules.java.lsp.server.protocol.TextDocumentServiceImpl.convertToLineOnlyFolds; | ||
+ | ||
public class TextDocumentServiceImplTest extends NbTestCase { | ||
|
||
public TextDocumentServiceImplTest(String name) { | ||
@@ -117,4 +122,141 @@ public class TextDocumentServiceImplTest extends NbTestCase { | ||
fail(String.valueOf(e)); | ||
} | ||
} | ||
+ | ||
+ public void testConvertToLineOnlyFolds() { | ||
+ assertNull(convertToLineOnlyFolds(null)); | ||
+ assertEquals(0, convertToLineOnlyFolds(Collections.emptyList()).size()); | ||
+ List<FoldingRange> inputFolds, outputFolds; | ||
+ inputFolds = Collections.singletonList(createRange(10, 20)); | ||
+ assertEquals(inputFolds, convertToLineOnlyFolds(inputFolds)); | ||
+ | ||
+ // test stable sort by start index | ||
+ inputFolds = List.of(createRange(10, 20, 9, 9), createRange(5, 9, 9, 9), createRange(10, 19, 9, 9), createRange(10, 14, 13, 13)); | ||
+ outputFolds = List.of(createRange(5, 9), createRange(10, 20), createRange(10, 19), createRange(10, 14)); | ||
+ assertEquals(outputFolds, convertToLineOnlyFolds(inputFolds)); | ||
+ | ||
+ // test already disjoint folds | ||
+ inputFolds = List.of(createRange(10, 20, 9, 9), createRange(5, 9, 9, 9), createRange(15, 19, 13, 13), createRange(10, 14, 13, 13)); | ||
+ outputFolds = List.of(createRange(5, 9), createRange(10, 20), createRange(10, 14), createRange(15, 19)); | ||
+ assertEquals(outputFolds, convertToLineOnlyFolds(inputFolds)); | ||
+ | ||
+ // test invariant of range.endLine: there exists no otherRange.startLine == range.endLine. | ||
+ inputFolds = List.of(createRange(10, 20, 35, 9), createRange(5, 10, 12, 9), createRange(15, 19, 20, 13), createRange(10, 15, 51, 13)); | ||
+ assertEquals(outputFolds, convertToLineOnlyFolds(inputFolds)); | ||
+ | ||
+ // test a complex example of a full file: | ||
+//import java.util.ArrayList; | ||
+//import java.util.Collection; | ||
+//import java.util.Collections; | ||
+// | ||
+///** | ||
+// * A top-class action performer | ||
+// * | ||
+// * @since 1.1 | ||
+// */ | ||
+//public class TopClass { | ||
+// | ||
+// private final String action; | ||
+// private final int index; | ||
+// | ||
+// /** | ||
+// * @param action Top action to be done | ||
+// */ | ||
+// public TopClass(String action) { | ||
+// this(action, 0); | ||
+// } | ||
+// | ||
+// /** | ||
+// * @param action Top action to be done | ||
+// * @param index Action index | ||
+// */ | ||
+// public TopClass(String action, int index) { | ||
+// this.action = action; | ||
+// this.index = index; | ||
+// } | ||
+// | ||
+// public void doSomethingTopClass(TopClass tc) { | ||
+// // what can we do | ||
+// { | ||
+// if (tc == this) { | ||
+// return; | ||
+// } else if (tc.getClass() == this.getClass()) { | ||
+// } else if (tc.getClass().isAssignableFrom(this.getClass())) { | ||
+// | ||
+// } else { | ||
+// if (true) { | ||
+// switch (tc) { | ||
+// default: { /* this is some comment */ ; } | ||
+// /// some outside default | ||
+// } | ||
+// } else { if (true) { { /* some */ } { /* bad blocks */ } | ||
+// }} | ||
+// /* done */ | ||
+// } | ||
+// } | ||
+// tc.doSomethingTopClass(tc); | ||
+// } | ||
+// | ||
+// public class InnerClass { | ||
+// @Override | ||
+// public String toString() { | ||
+// StringBuilder sb = new StringBuilder(); | ||
+// sb.append("InnerClass{"); | ||
+// sb.append("action=").append(action); | ||
+// sb.append(", index=").append(index); | ||
+// sb.append('}'); | ||
+// return sb.toString(); | ||
+// } | ||
+// } | ||
+//} | ||
+ inputFolds = List.of( | ||
+ createRange(27, 30, 48, 5), | ||
+ createRange(0, 3, 7, 30), | ||
+ createRange(32, 52, 51, 5), | ||
+ createRange(37, 38, 59, 13), | ||
+ createRange(34, 50, 10, 9), | ||
+ createRange(46, 46, 39, 51), | ||
+ createRange(35, 37, 30, 13), | ||
+ createRange(38, 40, 74, 13), | ||
+ createRange(40, 49, 21, 13), | ||
+ createRange(46, 47, 37, 17), | ||
+ createRange(41, 46, 28, 17), | ||
+ createRange(42, 45, 34, 21), | ||
+ createRange(11, 66, 24, 1), | ||
+ createRange(43, 43, 35, 65), | ||
+ createRange(46, 47, 25, 18), | ||
+ createRange(54, 64, 30, 5), | ||
+ createRange(46, 46, 54, 72), | ||
+ createRange(6, 10, 4, 1), | ||
+ createRange(56, 63, 35, 9) | ||
+ ); | ||
+ outputFolds = List.of( | ||
+ createRange(0, 3), | ||
+ createRange(6, 10), | ||
+ createRange(11, 66), | ||
+ createRange(27, 30), | ||
+ createRange(32, 52), | ||
+ createRange(34, 50), | ||
+ createRange(35, 36), | ||
+ createRange(38, 39), | ||
+ createRange(40, 49), | ||
+ createRange(41, 45), | ||
+ createRange(42, 45), | ||
+ createRange(46, 47), | ||
+ createRange(54, 64), | ||
+ createRange(56, 63) | ||
+ ); | ||
+ assertEquals(outputFolds, convertToLineOnlyFolds(inputFolds)); | ||
+ } | ||
+ | ||
+ private static FoldingRange createRange(int startLine, int endLine) { | ||
+ return new FoldingRange(startLine, endLine); | ||
+ } | ||
+ | ||
+ private static FoldingRange createRange(int startLine, int endLine, Integer startColumn, Integer endColumn) { | ||
+ FoldingRange foldingRange = new FoldingRange(startLine, endLine); | ||
+ foldingRange.setStartCharacter(startColumn); | ||
+ foldingRange.setEndCharacter(endColumn); | ||
+ return foldingRange; | ||
+ } | ||
} |