Skip to content

[Phase 2] Add Folding Ranges Support #33

@vogella

Description

@vogella

Description

Implement folding ranges in the LSP server to allow users to collapse and expand sections of AsciiDoc documents in the editor. This improves navigation and readability for large documents.

Current State

Status: Not implemented - LSP server doesn't provide folding ranges

Required Changes

1. Add Folding Range Capability

File: AsciidocLanguageServer.java

ServerCapabilities capabilities = new ServerCapabilities();
capabilities.setFoldingRangeProvider(true);

2. Implement Folding Range Provider

File: AsciidocTextDocumentService.java

@Override
public CompletableFuture<List<FoldingRange>> foldingRange(FoldingRangeRequestParams params) {
    String uri = params.getTextDocument().getUri();
    AsciidocDocumentModel model = documentCache.get(uri);
    
    if (model == null) {
        return CompletableFuture.completedFuture(Collections.emptyList());
    }
    
    List<FoldingRange> ranges = new ArrayList<>();
    List<String> lines = model.getLines();
    
    // Find foldable sections
    ranges.addAll(findHeaderFoldingRanges(lines));
    ranges.addAll(findBlockFoldingRanges(lines));
    ranges.addAll(findListFoldingRanges(lines));
    ranges.addAll(findTableFoldingRanges(lines));
    
    return CompletableFuture.completedFuture(ranges);
}

3. Header-Based Folding

private List<FoldingRange> findHeaderFoldingRanges(List<String> lines) {
    List<FoldingRange> ranges = new ArrayList<>();
    Stack<HeaderInfo> headerStack = new Stack<>();
    
    for (int i = 0; i < lines.size(); i++) {
        String line = lines.get(i).trim();
        
        if (line.startsWith("=") && !line.startsWith("====")) {
            int level = getHeaderLevel(line);
            
            // Close all headers of same or higher level
            while (!headerStack.isEmpty() && headerStack.peek().level >= level) {
                HeaderInfo header = headerStack.pop();
                FoldingRange range = new FoldingRange(header.startLine, i - 1);
                range.setKind(FoldingRangeKind.Region);
                ranges.add(range);
            }
            
            // Start new header section
            headerStack.push(new HeaderInfo(level, i));
        }
    }
    
    // Close remaining headers at end of document
    while (!headerStack.isEmpty()) {
        HeaderInfo header = headerStack.pop();
        FoldingRange range = new FoldingRange(header.startLine, lines.size() - 1);
        range.setKind(FoldingRangeKind.Region);
        ranges.add(range);
    }
    
    return ranges;
}

private static class HeaderInfo {
    int level;
    int startLine;
    
    HeaderInfo(int level, int startLine) {
        this.level = level;
        this.startLine = startLine;
    }
}

4. Block Folding

private List<FoldingRange> findBlockFoldingRanges(List<String> lines) {
    List<FoldingRange> ranges = new ArrayList<>();
    Map<String, Integer> blockStarts = new HashMap<>();
    
    for (int i = 0; i < lines.size(); i++) {
        String line = lines.get(i).trim();
        
        // Code blocks (----)
        if (line.equals("----")) {
            if (blockStarts.containsKey("----")) {
                FoldingRange range = new FoldingRange(blockStarts.remove("----"), i);
                range.setKind(FoldingRangeKind.Region);
                ranges.add(range);
            } else {
                blockStarts.put("----", i);
            }
        }
        
        // Example blocks (====)
        else if (line.equals("====")) {
            if (blockStarts.containsKey("====")) {
                FoldingRange range = new FoldingRange(blockStarts.remove("===="), i);
                range.setKind(FoldingRangeKind.Region);
                ranges.add(range);
            } else {
                blockStarts.put("====", i);
            }
        }
        
        // Sidebar blocks (****)
        else if (line.equals("****")) {
            if (blockStarts.containsKey("****")) {
                FoldingRange range = new FoldingRange(blockStarts.remove("****"), i);
                range.setKind(FoldingRangeKind.Region);
                ranges.add(range);
            } else {
                blockStarts.put("****", i);
            }
        }
        
        // Literal blocks (....)
        else if (line.equals("....")) {
            if (blockStarts.containsKey("....")) {
                FoldingRange range = new FoldingRange(blockStarts.remove("...."), i);
                range.setKind(FoldingRangeKind.Region);
                ranges.add(range);
            } else {
                blockStarts.put("....", i);
            }
        }
        
        // Quote blocks (____)
        else if (line.equals("____")) {
            if (blockStarts.containsKey("____")) {
                FoldingRange range = new FoldingRange(blockStarts.remove("____"), i);
                range.setKind(FoldingRangeKind.Region);
                ranges.add(range);
            } else {
                blockStarts.put("____", i);
            }
        }
        
        // Comment blocks (////)
        else if (line.equals("////")) {
            if (blockStarts.containsKey("////")) {
                FoldingRange range = new FoldingRange(blockStarts.remove("////"), i);
                range.setKind(FoldingRangeKind.Comment);
                ranges.add(range);
            } else {
                blockStarts.put("////", i);
            }
        }
    }
    
    return ranges;
}

5. List Folding

private List<FoldingRange> findListFoldingRanges(List<String> lines) {
    List<FoldingRange> ranges = new ArrayList<>();
    Integer listStart = null;
    
    for (int i = 0; i < lines.size(); i++) {
        String line = lines.get(i).trim();
        
        boolean isList = line.startsWith("* ") || 
                        line.startsWith("- ") || 
                        line.matches("^\\d+\\.\\s+.*");
        
        if (isList) {
            if (listStart == null) {
                listStart = i;
            }
        } else if (listStart != null && !line.isEmpty()) {
            // End of list (at least 2 items)
            if (i - listStart > 1) {
                FoldingRange range = new FoldingRange(listStart, i - 1);
                range.setKind(FoldingRangeKind.Region);
                ranges.add(range);
            }
            listStart = null;
        }
    }
    
    // Close list at end of document
    if (listStart != null && lines.size() - listStart > 1) {
        FoldingRange range = new FoldingRange(listStart, lines.size() - 1);
        range.setKind(FoldingRangeKind.Region);
        ranges.add(range);
    }
    
    return ranges;
}

6. Table Folding

private List<FoldingRange> findTableFoldingRanges(List<String> lines) {
    List<FoldingRange> ranges = new ArrayList<>();
    Integer tableStart = null;
    
    for (int i = 0; i < lines.size(); i++) {
        String line = lines.get(i).trim();
        
        if (line.equals("|===")) {
            if (tableStart == null) {
                tableStart = i;
            } else {
                // End of table
                FoldingRange range = new FoldingRange(tableStart, i);
                range.setKind(FoldingRangeKind.Region);
                ranges.add(range);
                tableStart = null;
            }
        }
    }
    
    return ranges;
}

Testing Checklist

Header Folding

  • Fold document sections by header level
  • Nested sections fold correctly
  • Unfolding works properly
  • Test with all header levels (=, ==, ===, etc.)

Block Folding

  • Code blocks (----) can be folded
  • Example blocks (====) can be folded
  • Sidebar blocks (****) can be folded
  • Quote blocks (____) can be folded
  • Comment blocks (////) can be folded
  • Nested blocks handled correctly

List Folding

  • Unordered lists can be folded
  • Ordered lists can be folded
  • Single-item lists don't create fold
  • Multi-level lists fold correctly

Table Folding

  • Tables (|===...===|) can be folded
  • Multiple tables in document work

General

  • Folding indicators appear in editor gutter
  • Fold/unfold actions work via UI
  • Keyboard shortcuts work (if configured)
  • Performance acceptable for large documents
  • Overlapping ranges handled correctly

Files to Modify

  • com.vogella.lsp.asciidoc.server/src/.../AsciidocLanguageServer.java
  • com.vogella.lsp.asciidoc.server/src/.../AsciidocTextDocumentService.java

Dependencies

Success Criteria

  1. ✅ Headers create foldable sections
  2. ✅ Code blocks and other delimited blocks fold
  3. ✅ Lists can be folded
  4. ✅ Tables can be folded
  5. ✅ Nested structures fold correctly
  6. ✅ Fold/unfold works smoothly in Eclipse
  7. ✅ Performance acceptable

Estimated Effort

1-2 days

Priority

Medium - Nice to have, improves large document navigation

Related Issues

Notes

  • Eclipse LSP4E should support folding ranges out of the box
  • Test with large documents (100+ sections) to ensure performance
  • Consider configuration options (fold by default, fold levels, etc.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions