Skip to content

[Phase 2] Enhance Code Actions with AsciiDoc Quickfixes #35

@vogella

Description

@vogella

Description

Enhance the LSP server's code action support to provide useful quickfixes and refactorings for common AsciiDoc operations. Currently, code actions only detect PLACEHOLDER_TEXT. We need practical actions like fixing broken references, generating TOC, converting to code blocks, etc.

Current State

Current Code Actions:

  • Detects PLACEHOLDER_TEXT pattern
  • Provides quickfix to replace with replacement_text

Missing: Practical AsciiDoc refactorings and fixes

Required Changes

1. Enhance Code Action Provider

File: AsciidocTextDocumentService.java

@Override
public CompletableFuture<List<Either<Command, CodeAction>>> codeAction(CodeActionParams params) {
    String uri = params.getTextDocument().getUri();
    AsciidocDocumentModel model = documentCache.get(uri);
    List<Diagnostic> diagnostics = params.getContext().getDiagnostics();
    
    if (model == null) {
        return CompletableFuture.completedFuture(Collections.emptyList());
    }
    
    List<Either<Command, CodeAction>> actions = new ArrayList<>();
    
    // Actions based on diagnostics (quickfixes)
    for (Diagnostic diagnostic : diagnostics) {
        actions.addAll(getQuickfixActions(diagnostic, uri, model));
    }
    
    // Contextual refactorings (not tied to diagnostics)
    Range range = params.getRange();
    actions.addAll(getRefactoringActions(range, uri, model));
    
    return CompletableFuture.completedFuture(actions);
}

2. Quickfix Actions

private List<Either<Command, CodeAction>> getQuickfixActions(Diagnostic diagnostic, 
                                                              String uri, 
                                                              AsciidocDocumentModel model) {
    List<Either<Command, CodeAction>> actions = new ArrayList<>();
    String code = diagnostic.getCode().getLeft(); // Assuming code is string
    
    switch (code) {
        case "broken-include":
            actions.add(Either.forRight(createIncludeFileAction(diagnostic, uri)));
            break;
        
        case "missing-image":
            actions.add(Either.forRight(createIgnoreMissingImageAction(diagnostic)));
            break;
        
        case "broken-link":
            actions.add(Either.forRight(createFixBrokenLinkAction(diagnostic, uri)));
            break;
        
        case "header-format":
            actions.add(Either.forRight(createFixHeaderFormatAction(diagnostic, uri, model)));
            break;
        
        case "malformed-attribute":
            actions.add(Either.forRight(createFixAttributeAction(diagnostic, uri, model)));
            break;
        
        case "unclosed-block":
            actions.add(Either.forRight(createCloseBlockAction(diagnostic, uri, model)));
            break;
        
        case "undefined-attribute":
            actions.add(Either.forRight(createDefineAttributeAction(diagnostic, uri, model)));
            break;
    }
    
    return actions;
}

3. Create Include File Action

private CodeAction createIncludeFileAction(Diagnostic diagnostic, String uri) {
    CodeAction action = new CodeAction("Create missing include file");
    action.setKind(CodeActionKind.QuickFix);
    action.setDiagnostics(Collections.singletonList(diagnostic));
    
    // Extract file path from diagnostic message
    String message = diagnostic.getMessage();
    String filePath = message.substring(message.indexOf(":") + 2);
    
    // Create file with basic template
    Path docDir = Paths.get(URI.create(uri)).getParent();
    Path newFile = docDir.resolve(filePath);
    
    WorkspaceEdit edit = new WorkspaceEdit();
    
    // Create new file with basic content
    TextDocumentEdit textEdit = new TextDocumentEdit(
        new VersionedTextDocumentIdentifier(newFile.toUri().toString(), null),
        Collections.singletonList(new TextEdit(
            new Range(new Position(0, 0), new Position(0, 0)),
            "= New Document\n\n// TODO: Add content\n"
        ))
    );
    
    edit.setDocumentChanges(Collections.singletonList(Either.forLeft(textEdit)));
    action.setEdit(edit);
    
    return action;
}

4. Fix Header Format Action

private CodeAction createFixHeaderFormatAction(Diagnostic diagnostic, String uri, 
                                                AsciidocDocumentModel model) {
    CodeAction action = new CodeAction("Add space after '='");
    action.setKind(CodeActionKind.QuickFix);
    action.setDiagnostics(Collections.singletonList(diagnostic));
    
    int line = diagnostic.getRange().getStart().getLine();
    String lineContent = model.getLineContent(line);
    
    // Count leading '='
    int eqCount = 0;
    while (eqCount < lineContent.length() && lineContent.charAt(eqCount) == '=') {
        eqCount++;
    }
    
    // Insert space after '='
    String fixed = lineContent.substring(0, eqCount) + " " + lineContent.substring(eqCount);
    
    WorkspaceEdit edit = new WorkspaceEdit();
    TextEdit textEdit = new TextEdit(
        new Range(new Position(line, 0), new Position(line, lineContent.length())),
        fixed
    );
    
    edit.setChanges(Collections.singletonMap(uri, Collections.singletonList(textEdit)));
    action.setEdit(edit);
    
    return action;
}

5. Define Attribute Action

private CodeAction createDefineAttributeAction(Diagnostic diagnostic, String uri, 
                                                AsciidocDocumentModel model) {
    // Extract attribute name from diagnostic
    String message = diagnostic.getMessage();
    String attrName = message.substring(message.indexOf(":") + 2);
    
    CodeAction action = new CodeAction("Define attribute '" + attrName + "'");
    action.setKind(CodeActionKind.QuickFix);
    action.setDiagnostics(Collections.singletonList(diagnostic));
    
    // Find document header (after title, before first section)
    int insertLine = findAttributeInsertionPoint(model);
    
    WorkspaceEdit edit = new WorkspaceEdit();
    TextEdit textEdit = new TextEdit(
        new Range(new Position(insertLine, 0), new Position(insertLine, 0)),
        ":" + attrName + ": \n"
    );
    
    edit.setChanges(Collections.singletonMap(uri, Collections.singletonList(textEdit)));
    action.setEdit(edit);
    
    return action;
}

private int findAttributeInsertionPoint(AsciidocDocumentModel model) {
    List<String> lines = model.getLines();
    
    // Look for first section header (==)
    for (int i = 0; i < lines.size(); i++) {
        if (lines.get(i).trim().startsWith("==")) {
            return i;
        }
    }
    
    // Otherwise insert after document title
    for (int i = 0; i < lines.size(); i++) {
        if (lines.get(i).trim().startsWith("=")) {
            return i + 1;
        }
    }
    
    return 0; // Insert at top if no headers found
}

6. Refactoring Actions

private List<Either<Command, CodeAction>> getRefactoringActions(Range range, String uri, 
                                                                  AsciidocDocumentModel model) {
    List<Either<Command, CodeAction>> actions = new ArrayList<>();
    
    int line = range.getStart().getLine();
    String lineContent = model.getLineContent(line);
    
    // Convert selection to code block
    if (!lineContent.trim().startsWith("[source")) {
        actions.add(Either.forRight(createConvertToCodeBlockAction(range, uri, model)));
    }
    
    // Generate table of contents
    if (line == 0) {
        actions.add(Either.forRight(createGenerateTocAction(uri, model)));
    }
    
    // Convert to include (if multiple lines selected)
    if (range.getEnd().getLine() - range.getStart().getLine() > 5) {
        actions.add(Either.forRight(createExtractToIncludeAction(range, uri, model)));
    }
    
    // Add image alt text
    if (lineContent.contains("image::") && lineContent.contains("[]")) {
        actions.add(Either.forRight(createAddAltTextAction(line, uri, model)));
    }
    
    return actions;
}

7. Convert to Code Block Action

private CodeAction createConvertToCodeBlockAction(Range range, String uri, 
                                                    AsciidocDocumentModel model) {
    CodeAction action = new CodeAction("Convert to code block");
    action.setKind(CodeActionKind.Refactor);
    
    WorkspaceEdit edit = new WorkspaceEdit();
    List<TextEdit> edits = new ArrayList<>();
    
    // Insert [source] and ---- before
    edits.add(new TextEdit(
        new Range(new Position(range.getStart().getLine(), 0), 
                  new Position(range.getStart().getLine(), 0)),
        "[source]\n----\n"
    ));
    
    // Insert ---- after
    edits.add(new TextEdit(
        new Range(new Position(range.getEnd().getLine() + 1, 0), 
                  new Position(range.getEnd().getLine() + 1, 0)),
        "----\n"
    ));
    
    edit.setChanges(Collections.singletonMap(uri, edits));
    action.setEdit(edit);
    
    return action;
}

8. Generate Table of Contents Action

private CodeAction createGenerateTocAction(String uri, AsciidocDocumentModel model) {
    CodeAction action = new CodeAction("Generate table of contents");
    action.setKind(CodeActionKind.Refactor);
    
    // Build TOC from headers
    StringBuilder toc = new StringBuilder("\n== Table of Contents\n\n");
    List<String> lines = model.getLines();
    
    for (int i = 0; i < lines.size(); i++) {
        String line = lines.get(i).trim();
        if (line.startsWith("==") && !line.startsWith("===")) {
            String title = line.substring(2).trim();
            toc.append("* <<").append(sanitizeAnchor(title)).append(",").append(title).append(">>\n");
        }
    }
    
    toc.append("\n");
    
    WorkspaceEdit edit = new WorkspaceEdit();
    TextEdit textEdit = new TextEdit(
        new Range(new Position(1, 0), new Position(1, 0)),
        toc.toString()
    );
    
    edit.setChanges(Collections.singletonMap(uri, Collections.singletonList(textEdit)));
    action.setEdit(edit);
    
    return action;
}

private String sanitizeAnchor(String title) {
    return title.toLowerCase()
                .replaceAll("[^a-z0-9]+", "-")
                .replaceAll("^-|-$", "");
}

Testing Checklist

Quickfix Actions

  • "Create missing include file" works
  • "Add space after '='" fixes headers
  • "Define attribute" inserts definition correctly
  • "Close block" adds missing delimiter
  • All quickfixes appear in Problems view

Refactoring Actions

  • "Convert to code block" wraps selection
  • "Generate TOC" creates valid TOC
  • "Extract to include" creates new file
  • "Add alt text" inserts placeholder

General

  • Code actions appear on Ctrl+1 (Quick Assist)
  • Apply action modifies document correctly
  • Undo/redo works properly
  • Performance acceptable

Files to Modify

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

Dependencies

Success Criteria

  1. ✅ Quickfixes for all diagnostic codes
  2. ✅ Useful refactoring actions
  3. ✅ Code actions integrate with Eclipse UI
  4. ✅ Actions apply correctly
  5. ✅ Good UX (clear action titles)

Estimated Effort

2-3 days

Priority

Medium - Enhances productivity significantly

Related Issues

Notes

  • LSP code actions map to Eclipse Quick Assist (Ctrl+1)
  • WorkspaceEdit support varies in LSP4E - test thoroughly
  • Consider user preferences for action behavior
  • File creation actions may need special permissions

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