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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions