| 
 | 1 | +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  | 
 | 2 | +index c112b4eb73..ff1fe7f903 100644  | 
 | 3 | +--- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImpl.java  | 
 | 4 | ++++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImpl.java  | 
 | 5 | +@@ -44,14 +44,18 @@ import java.net.MalformedURLException;  | 
 | 6 | + import java.net.URISyntaxException;  | 
 | 7 | + import java.nio.file.Path;  | 
 | 8 | + import java.nio.file.Paths;  | 
 | 9 | ++import java.util.ArrayDeque;  | 
 | 10 | + import java.util.ArrayList;  | 
 | 11 | + import java.util.Arrays;  | 
 | 12 | + import java.util.Collection;  | 
 | 13 | + import java.util.Collections;  | 
 | 14 | ++import java.util.Comparator;  | 
 | 15 | ++import java.util.Deque;  | 
 | 16 | + import java.util.EnumMap;  | 
 | 17 | + import java.util.EnumSet;  | 
 | 18 | + import java.util.HashMap;  | 
 | 19 | + import java.util.HashSet;  | 
 | 20 | ++import java.util.Iterator;  | 
 | 21 | + import java.util.LinkedHashMap;  | 
 | 22 | + import java.util.List;  | 
 | 23 | + import java.util.Locale;  | 
 | 24 | +@@ -1561,12 +1565,13 @@ public class TextDocumentServiceImpl implements TextDocumentService, LanguageCli  | 
 | 25 | +         if (source == null) {  | 
 | 26 | +             return CompletableFuture.completedFuture(Collections.emptyList());  | 
 | 27 | +         }  | 
 | 28 | ++        final boolean lineFoldingOnly = client.getNbCodeCapabilities().getClientCapabilities().getTextDocument().getFoldingRange().getLineFoldingOnly() == Boolean.TRUE;  | 
 | 29 | +         CompletableFuture<List<FoldingRange>> result = new CompletableFuture<>();  | 
 | 30 | +         try {  | 
 | 31 | +             source.runUserActionTask(cc -> {  | 
 | 32 | +                 cc.toPhase(JavaSource.Phase.RESOLVED);  | 
 | 33 | +                 Document doc = cc.getSnapshot().getSource().getDocument(true);  | 
 | 34 | +-                JavaElementFoldVisitor v = new JavaElementFoldVisitor(cc, cc.getCompilationUnit(), cc.getTrees().getSourcePositions(), doc, new FoldCreator<FoldingRange>() {  | 
 | 35 | ++                JavaElementFoldVisitor<FoldingRange> v = new JavaElementFoldVisitor<>(cc, cc.getCompilationUnit(), cc.getTrees().getSourcePositions(), doc, new FoldCreator<FoldingRange>() {  | 
 | 36 | +                     @Override  | 
 | 37 | +                     public FoldingRange createImportsFold(int start, int end) {  | 
 | 38 | +                         return createFold(start, end, FoldingRangeKind.Imports);  | 
 | 39 | +@@ -1611,7 +1616,10 @@ public class TextDocumentServiceImpl implements TextDocumentService, LanguageCli  | 
 | 40 | +                 });  | 
 | 41 | +                 v.checkInitialFold();  | 
 | 42 | +                 v.scan(cc.getCompilationUnit(), null);  | 
 | 43 | +-                result.complete(v.getFolds());  | 
 | 44 | ++                List<FoldingRange> folds = v.getFolds();  | 
 | 45 | ++                if (lineFoldingOnly)  | 
 | 46 | ++                    folds = convertToLineOnlyFolds(folds);  | 
 | 47 | ++                result.complete(folds);  | 
 | 48 | +             }, true);  | 
 | 49 | +         } catch (IOException ex) {  | 
 | 50 | +             result.completeExceptionally(ex);  | 
 | 51 | +@@ -1619,6 +1627,76 @@ public class TextDocumentServiceImpl implements TextDocumentService, LanguageCli  | 
 | 52 | +         return result;  | 
 | 53 | +     }  | 
 | 54 | +   | 
 | 55 | ++    /**  | 
 | 56 | ++     * Converts a list of code-folds to a line-only Range form, in place of the  | 
 | 57 | ++     * finer-grained form of {@linkplain Position Position-based} (line, column) Ranges.  | 
 | 58 | ++     * <p>  | 
 | 59 | ++     * This is needed for LSP clients that do not support the finer grained Range  | 
 | 60 | ++     * specification. This is expected to be advertised by the client in  | 
 | 61 | ++     * {@code FoldingRangeClientCapabilities.lineFoldingOnly}.  | 
 | 62 | ++     *  | 
 | 63 | ++     * @implSpec The line-only ranges computed uphold the code-folding invariant that:  | 
 | 64 | ++     * <em>a fold <b>does not end</b> at the same point <b>where</b> another fold <b>starts</b></em>.  | 
 | 65 | ++     *  | 
 | 66 | ++     * @implNote This is performed in {@code O(n log n) + O(n)} time and {@code O(n)} space for the returned list.  | 
 | 67 | ++     *  | 
 | 68 | ++     * @param folds List of code-folding ranges computed for a textDocument,  | 
 | 69 | ++     *              containing fine-grained {@linkplain Position Position-based}  | 
 | 70 | ++     *              (line, column) ranges.  | 
 | 71 | ++     * @return List of code-folding ranges computed for a textDocument,  | 
 | 72 | ++     * containing coarse-grained line-only ranges.  | 
 | 73 | ++     *  | 
 | 74 | ++     * @see <a href="https://microsoft.github.io/language-server-protocol/specifications/specification-current/#foldingRangeClientCapabilities">  | 
 | 75 | ++     *     LSP FoldingRangeClientCapabilities</a>  | 
 | 76 | ++     */  | 
 | 77 | ++    static List<FoldingRange> convertToLineOnlyFolds(List<FoldingRange> folds) {  | 
 | 78 | ++        if (folds != null && folds.size() > 1) {  | 
 | 79 | ++            // Ensure that the folds are sorted in increasing order of their start position  | 
 | 80 | ++            folds = new ArrayList<>(folds);  | 
 | 81 | ++            folds.sort(Comparator.comparingInt(FoldingRange::getStartLine)  | 
 | 82 | ++                    .thenComparing(FoldingRange::getStartCharacter));  | 
 | 83 | ++            // Maintain a stack of enclosing folds  | 
 | 84 | ++            Deque<FoldingRange> enclosingFolds = new ArrayDeque<>();  | 
 | 85 | ++            for (FoldingRange fold : folds) {  | 
 | 86 | ++                FoldingRange last;  | 
 | 87 | ++                while ((last = enclosingFolds.peek()) != null &&  | 
 | 88 | ++                        (last.getEndLine() < fold.getEndLine() ||   | 
 | 89 | ++                        (last.getEndLine() == fold.getEndLine() && last.getEndCharacter() < fold.getEndCharacter()))) {  | 
 | 90 | ++                    // The last enclosingFold does not enclose this fold.  | 
 | 91 | ++                    // Due to sortedness of the folds, last also ends before this fold starts.  | 
 | 92 | ++                    enclosingFolds.pop();  | 
 | 93 | ++                    // If needed, adjust last to end on a line prior to this fold start  | 
 | 94 | ++                    if (last.getEndLine() == fold.getStartLine()) {  | 
 | 95 | ++                        last.setEndLine(last.getEndLine() - 1);  | 
 | 96 | ++                    }  | 
 | 97 | ++                    last.setEndCharacter(null);       // null denotes the end of the line.  | 
 | 98 | ++                    last.setStartCharacter(null);     // null denotes the end of the line.  | 
 | 99 | ++                }  | 
 | 100 | ++                enclosingFolds.push(fold);  | 
 | 101 | ++            }  | 
 | 102 | ++            // empty the stack; since each fold completely encloses the next higher one.  | 
 | 103 | ++            FoldingRange fold;  | 
 | 104 | ++            while ((fold = enclosingFolds.poll()) != null) {  | 
 | 105 | ++                fold.setEndCharacter(null);       // null denotes the end of the line.  | 
 | 106 | ++                fold.setStartCharacter(null);     // null denotes the end of the line.  | 
 | 107 | ++            }  | 
 | 108 | ++            // Remove invalid or duplicate folds  | 
 | 109 | ++            Iterator<FoldingRange> it = folds.iterator();  | 
 | 110 | ++            FoldingRange prev = null;  | 
 | 111 | ++            while(it.hasNext()) {  | 
 | 112 | ++                FoldingRange next = it.next();  | 
 | 113 | ++                if (next.getEndLine() <= next.getStartLine() ||   | 
 | 114 | ++                        (prev != null && prev.equals(next))) {  | 
 | 115 | ++                    it.remove();  | 
 | 116 | ++                } else {  | 
 | 117 | ++                    prev = next;  | 
 | 118 | ++                }  | 
 | 119 | ++            }  | 
 | 120 | ++        }  | 
 | 121 | ++        return folds;  | 
 | 122 | ++    }  | 
 | 123 | ++  | 
 | 124 | ++  | 
 | 125 | +     @Override  | 
 | 126 | +     public void didOpen(DidOpenTextDocumentParams params) {  | 
 | 127 | +         LOG.log(Level.FINER, "didOpen: {0}", params);  | 
 | 128 | +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  | 
 | 129 | +index 0f2bda50ae..06fd93d3e5 100644  | 
 | 130 | +--- a/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImplTest.java  | 
 | 131 | ++++ b/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImplTest.java  | 
 | 132 | +@@ -18,14 +18,19 @@  | 
 | 133 | +  */  | 
 | 134 | + package org.netbeans.modules.java.lsp.server.protocol;  | 
 | 135 | +   | 
 | 136 | ++import java.util.Collections;  | 
 | 137 | ++import java.util.List;  | 
 | 138 | + import java.util.concurrent.atomic.AtomicInteger;  | 
 | 139 | + import javax.swing.event.DocumentEvent;  | 
 | 140 | + import javax.swing.event.DocumentListener;  | 
 | 141 | + import javax.swing.text.BadLocationException;  | 
 | 142 | + import javax.swing.text.Document;  | 
 | 143 | + import javax.swing.text.PlainDocument;  | 
 | 144 | ++import org.eclipse.lsp4j.FoldingRange;  | 
 | 145 | + import org.netbeans.junit.NbTestCase;  | 
 | 146 | +   | 
 | 147 | ++import static org.netbeans.modules.java.lsp.server.protocol.TextDocumentServiceImpl.convertToLineOnlyFolds;  | 
 | 148 | ++  | 
 | 149 | + public class TextDocumentServiceImplTest extends NbTestCase {  | 
 | 150 | +   | 
 | 151 | +     public TextDocumentServiceImplTest(String name) {  | 
 | 152 | +@@ -117,4 +122,141 @@ public class TextDocumentServiceImplTest extends NbTestCase {  | 
 | 153 | +             fail(String.valueOf(e));  | 
 | 154 | +         }  | 
 | 155 | +     }  | 
 | 156 | ++      | 
 | 157 | ++    public void testConvertToLineOnlyFolds() {  | 
 | 158 | ++        assertNull(convertToLineOnlyFolds(null));  | 
 | 159 | ++        assertEquals(0, convertToLineOnlyFolds(Collections.emptyList()).size());  | 
 | 160 | ++        List<FoldingRange> inputFolds, outputFolds;  | 
 | 161 | ++        inputFolds = Collections.singletonList(createRange(10, 20));  | 
 | 162 | ++        assertEquals(inputFolds, convertToLineOnlyFolds(inputFolds));  | 
 | 163 | ++  | 
 | 164 | ++        // test stable sort by start index  | 
 | 165 | ++        inputFolds = List.of(createRange(10, 20, 9, 9), createRange(5, 9, 9, 9), createRange(10, 19, 9, 9), createRange(10, 14, 13, 13));  | 
 | 166 | ++        outputFolds = List.of(createRange(5, 9), createRange(10, 20), createRange(10, 19), createRange(10, 14));  | 
 | 167 | ++        assertEquals(outputFolds, convertToLineOnlyFolds(inputFolds));  | 
 | 168 | ++  | 
 | 169 | ++        // test already disjoint folds  | 
 | 170 | ++        inputFolds = List.of(createRange(10, 20, 9, 9), createRange(5, 9, 9, 9), createRange(15, 19, 13, 13), createRange(10, 14, 13, 13));  | 
 | 171 | ++        outputFolds = List.of(createRange(5, 9), createRange(10, 20), createRange(10, 14), createRange(15, 19));  | 
 | 172 | ++        assertEquals(outputFolds, convertToLineOnlyFolds(inputFolds));  | 
 | 173 | ++  | 
 | 174 | ++        // test invariant of range.endLine: there exists no otherRange.startLine == range.endLine.  | 
 | 175 | ++        inputFolds = List.of(createRange(10, 20, 35, 9), createRange(5, 10, 12, 9), createRange(15, 19, 20, 13), createRange(10, 15, 51, 13));  | 
 | 176 | ++        assertEquals(outputFolds, convertToLineOnlyFolds(inputFolds));  | 
 | 177 | ++  | 
 | 178 | ++        // test a complex example of a full file:  | 
 | 179 | ++//import java.util.ArrayList;  | 
 | 180 | ++//import java.util.Collection;  | 
 | 181 | ++//import java.util.Collections;  | 
 | 182 | ++//  | 
 | 183 | ++///**  | 
 | 184 | ++// * A top-class action performer  | 
 | 185 | ++// *  | 
 | 186 | ++// * @since 1.1  | 
 | 187 | ++// */  | 
 | 188 | ++//public class TopClass {  | 
 | 189 | ++//  | 
 | 190 | ++//    private final String action;  | 
 | 191 | ++//    private final int index;  | 
 | 192 | ++//  | 
 | 193 | ++//    /**  | 
 | 194 | ++//     * @param action Top action to be done  | 
 | 195 | ++//     */  | 
 | 196 | ++//    public TopClass(String action) {  | 
 | 197 | ++//        this(action, 0);  | 
 | 198 | ++//    }  | 
 | 199 | ++//  | 
 | 200 | ++//    /**  | 
 | 201 | ++//     * @param action Top action to be done  | 
 | 202 | ++//     * @param index Action index  | 
 | 203 | ++//     */  | 
 | 204 | ++//    public TopClass(String action, int index) {  | 
 | 205 | ++//        this.action = action;  | 
 | 206 | ++//        this.index = index;  | 
 | 207 | ++//    }  | 
 | 208 | ++//  | 
 | 209 | ++//    public void doSomethingTopClass(TopClass tc) {  | 
 | 210 | ++//        // what can we do  | 
 | 211 | ++//        {  | 
 | 212 | ++//            if (tc == this) {  | 
 | 213 | ++//                return;  | 
 | 214 | ++//            } else if (tc.getClass() == this.getClass()) {  | 
 | 215 | ++//            } else if (tc.getClass().isAssignableFrom(this.getClass())) {  | 
 | 216 | ++//  | 
 | 217 | ++//            } else {  | 
 | 218 | ++//                if (true) {  | 
 | 219 | ++//                    switch (tc) {  | 
 | 220 | ++//                        default: { /* this is some comment */ ; }  | 
 | 221 | ++//                        /// some outside default  | 
 | 222 | ++//                    }  | 
 | 223 | ++//                } else { if (true) { { /* some */ } { /* bad blocks */ }  | 
 | 224 | ++//                }}  | 
 | 225 | ++//                /* done  */  | 
 | 226 | ++//            }  | 
 | 227 | ++//        }  | 
 | 228 | ++//        tc.doSomethingTopClass(tc);  | 
 | 229 | ++//    }  | 
 | 230 | ++//  | 
 | 231 | ++//    public class InnerClass {  | 
 | 232 | ++//        @Override  | 
 | 233 | ++//        public String toString() {  | 
 | 234 | ++//            StringBuilder sb = new StringBuilder();  | 
 | 235 | ++//            sb.append("InnerClass{");  | 
 | 236 | ++//            sb.append("action=").append(action);  | 
 | 237 | ++//            sb.append(", index=").append(index);  | 
 | 238 | ++//            sb.append('}');  | 
 | 239 | ++//            return sb.toString();  | 
 | 240 | ++//        }  | 
 | 241 | ++//    }  | 
 | 242 | ++//}  | 
 | 243 | ++        inputFolds = List.of(  | 
 | 244 | ++                createRange(27, 30, 48, 5),  | 
 | 245 | ++                createRange(0, 3, 7, 30),  | 
 | 246 | ++                createRange(32, 52, 51, 5),  | 
 | 247 | ++                createRange(37, 38, 59, 13),  | 
 | 248 | ++                createRange(34, 50, 10, 9),  | 
 | 249 | ++                createRange(46, 46, 39, 51),  | 
 | 250 | ++                createRange(35, 37, 30, 13),  | 
 | 251 | ++                createRange(38, 40, 74, 13),  | 
 | 252 | ++                createRange(40, 49, 21, 13),  | 
 | 253 | ++                createRange(46, 47, 37, 17),  | 
 | 254 | ++                createRange(41, 46, 28, 17),  | 
 | 255 | ++                createRange(42, 45, 34, 21),  | 
 | 256 | ++                createRange(11, 66, 24, 1),  | 
 | 257 | ++                createRange(43, 43, 35, 65),  | 
 | 258 | ++                createRange(46, 47, 25, 18),  | 
 | 259 | ++                createRange(54, 64, 30, 5),  | 
 | 260 | ++                createRange(46, 46, 54, 72),  | 
 | 261 | ++                createRange(6, 10, 4, 1),  | 
 | 262 | ++                createRange(56, 63, 35, 9)  | 
 | 263 | ++        );  | 
 | 264 | ++        outputFolds = List.of(  | 
 | 265 | ++                createRange(0, 3),  | 
 | 266 | ++                createRange(6, 10),  | 
 | 267 | ++                createRange(11, 66),  | 
 | 268 | ++                createRange(27, 30),  | 
 | 269 | ++                createRange(32, 52),  | 
 | 270 | ++                createRange(34, 50),  | 
 | 271 | ++                createRange(35, 36),  | 
 | 272 | ++                createRange(38, 39),  | 
 | 273 | ++                createRange(40, 49),  | 
 | 274 | ++                createRange(41, 45),  | 
 | 275 | ++                createRange(42, 45),  | 
 | 276 | ++                createRange(46, 47),  | 
 | 277 | ++                createRange(54, 64),  | 
 | 278 | ++                createRange(56, 63)  | 
 | 279 | ++        );  | 
 | 280 | ++        assertEquals(outputFolds, convertToLineOnlyFolds(inputFolds));  | 
 | 281 | ++    }  | 
 | 282 | ++      | 
 | 283 | ++    private static FoldingRange createRange(int startLine, int endLine) {  | 
 | 284 | ++        return new FoldingRange(startLine, endLine);  | 
 | 285 | ++    }  | 
 | 286 | ++      | 
 | 287 | ++    private static FoldingRange createRange(int startLine, int endLine, Integer startColumn, Integer endColumn) {  | 
 | 288 | ++        FoldingRange foldingRange = new FoldingRange(startLine, endLine);  | 
 | 289 | ++        foldingRange.setStartCharacter(startColumn);  | 
 | 290 | ++        foldingRange.setEndCharacter(endColumn);  | 
 | 291 | ++        return foldingRange;  | 
 | 292 | ++    }  | 
 | 293 | + }  | 
0 commit comments