Skip to content

[Story] LSP Phase 5: Code actions and autofixes #46

@ekropotin

Description

@ekropotin

User Story

As a developer, I want to automatically fix linting violations using IDE quick-fix actions, so I can resolve issues efficiently without manual editing.

Acceptance Criteria

  • Provides code actions for auto-fixable rules
  • Supports both single-fix and fix-all-in-document actions
  • Maintains document formatting and doesn't break content
  • Clear action titles that describe what will be changed
  • Only offers fixes for violations that can be safely automated
  • Handles edge cases (overlapping fixes, conflicting changes)

Additional Context

Technical Implementation Details:

LSP Capability Registration:

{
  "capabilities": {
    "codeActionProvider": {
      "codeActionKinds": [
        "quickfix",
        "source.fixAll.quickmark"
      ]
    }
  }
}

Auto-Fixable Rules (Initial Set):

  • MD047: Add trailing newline
  • MD009: Remove trailing spaces
  • MD010: Replace tabs with spaces (configurable)
  • MD012: Remove multiple consecutive blank lines
  • MD022: Add missing blank lines around headings
  • MD032: Fix list item indentation
  • MD018: Add space after hash in ATX headings

Code Action Types:

pub enum FixType {
    Single {
        title: String,
        edit: TextEdit,
        rule_code: String,
    },
    Multiple {
        title: String,
        edits: Vec<TextEdit>,
        rule_codes: Vec<String>,
    },
    FixAll {
        title: String,
        edits: Vec<TextEdit>,
        affected_rules: Vec<String>,
    },
}

pub struct AutoFix {
    pub fix_type: FixType,
    pub range: Range,           // LSP Range for the violation
    pub confidence: FixConfidence,
}

pub enum FixConfidence {
    Safe,    // No risk of changing meaning
    Medium,  // Low risk, but user should review
    High,    // Might change document meaning
}

Rule-Specific Fix Implementation:

// Extend RuleViolation to include auto-fix suggestions
pub struct RuleViolation {
    // ... existing fields
    pub auto_fixes: Vec<AutoFix>,  // NEW: Available fixes for this violation
}

// Example: MD047 - Add trailing newline
impl MD047Linter {
    fn generate_fix(&self, context: &Context) -> Option<AutoFix> {
        let content = &context.document_content;
        if \!content.ends_with('\n') {
            Some(AutoFix {
                fix_type: FixType::Single {
                    title: "Add trailing newline".to_string(),
                    edit: TextEdit {
                        range: Range {
                            start: Position::new(content.lines().count() as u32, 
                                               content.lines().last()?.len() as u32),
                            end: Position::new(content.lines().count() as u32, 
                                             content.lines().last()?.len() as u32),
                        },
                        new_text: "\n".to_string(),
                    },
                    rule_code: "MD047".to_string(),
                },
                range: /* violation range */,
                confidence: FixConfidence::Safe,
            })
        } else {
            None
        }
    }
}

Fix Conflict Resolution:

pub struct FixResolver {
    document_content: String,
}

impl FixResolver {
    pub fn resolve_conflicts(&self, fixes: Vec<AutoFix>) -> Vec<AutoFix> {
        // Sort fixes by position (reverse order for safe application)
        let mut sorted_fixes = fixes;
        sorted_fixes.sort_by(|a, b| b.range.start.cmp(&a.range.start));
        
        // Remove overlapping fixes, keeping higher confidence ones
        let mut resolved = Vec::new();
        for fix in sorted_fixes {
            if \!self.conflicts_with_existing(&fix, &resolved) {
                resolved.push(fix);
            }
        }
        
        resolved
    }
}

LSP Code Action Format:

{
  "title": "Fix MD047: Add trailing newline",
  "kind": "quickfix",
  "diagnostics": [/* related diagnostic */],
  "edit": {
    "changes": {
      "file:///path/to/document.md": [
        {
          "range": {
            "start": {"line": 10, "character": 15},
            "end": {"line": 10, "character": 15}
          },
          "newText": "\n"
        }
      ]
    }
  }
}

Safety Considerations:

  • Only fix violations with FixConfidence::Safe by default
  • Preserve existing formatting where possible
  • Validate fixes don't break document structure
  • Test fixes against edge cases (empty documents, only whitespace, etc.)

Performance Optimizations:

  • Cache fix generation results with diagnostic results
  • Lazy fix generation (only when code actions requested)
  • Batch multiple fixes into single workspace edit when possible

This is Phase 5 of a 6-phase LSP implementation plan for QuickMark.

Metadata

Metadata

Assignees

No one assigned

    Labels

    status: triageNeeds review and prioritizationtype: storyNew feature or enhancement

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions