-
Notifications
You must be signed in to change notification settings - Fork 4
Open
Labels
enhancementNew feature or requestNew feature or request
Description
Description
Enhance the LSP server's diagnostic capabilities to detect and report broken references in AsciiDoc documents. Currently, diagnostics only detect PLACEHOLDER_TEXT. We need real-time validation for broken includes, missing images, malformed syntax, and other common errors.
Current State
LSP Implementation (current - basic)
File: AsciidocTextDocumentService.java
Current Diagnostics:
- Detects
PLACEHOLDER_TEXTpattern only - Creates warnings with code action support
Missing:
- Broken
include::references - Missing image files
- Malformed syntax
- Undefined attributes
- Unclosed blocks
Required Changes
1. Enhance validateDocument Method
private void validateDocument(String uri) {
AsciidocDocumentModel model = documentCache.get(uri);
if (model == null) {
return;
}
List<Diagnostic> diagnostics = new ArrayList<>();
List<String> lines = model.getLines();
for (int i = 0; i < lines.size(); i++) {
String line = lines.get(i);
// Validate includes
diagnostics.addAll(validateIncludes(line, i, uri));
// Validate images
diagnostics.addAll(validateImages(line, i, uri));
// Validate links
diagnostics.addAll(validateLinks(line, i, uri));
// Validate syntax
diagnostics.addAll(validateSyntax(line, i));
// Validate attributes
diagnostics.addAll(validateAttributes(line, i, model));
}
// Validate block structure
diagnostics.addAll(validateBlockStructure(lines));
// Publish diagnostics
publishDiagnostics(uri, diagnostics);
}2. Validate Include References
private List<Diagnostic> validateIncludes(String line, int lineNum, String docUri) {
List<Diagnostic> diagnostics = new ArrayList<>();
Pattern pattern = Pattern.compile("include::(.*?)\\[");
Matcher matcher = pattern.matcher(line);
while (matcher.find()) {
String includePath = matcher.group(1);
// Resolve path
Path docDir = Paths.get(URI.create(docUri)).getParent();
Path includeFile = resolveIncludePath(docDir, includePath);
if (!Files.exists(includeFile)) {
Diagnostic diagnostic = new Diagnostic();
diagnostic.setRange(new Range(
new Position(lineNum, matcher.start()),
new Position(lineNum, matcher.end())
));
diagnostic.setSeverity(DiagnosticSeverity.Error);
diagnostic.setMessage("Include file not found: " + includePath);
diagnostic.setCode("broken-include");
diagnostic.setSource("asciidoc");
diagnostics.add(diagnostic);
} else if (!Files.isReadable(includeFile)) {
Diagnostic diagnostic = new Diagnostic();
diagnostic.setRange(new Range(
new Position(lineNum, matcher.start()),
new Position(lineNum, matcher.end())
));
diagnostic.setSeverity(DiagnosticSeverity.Warning);
diagnostic.setMessage("Include file is not readable: " + includePath);
diagnostic.setCode("unreadable-include");
diagnostic.setSource("asciidoc");
diagnostics.add(diagnostic);
}
}
return diagnostics;
}3. Validate Image References
private List<Diagnostic> validateImages(String line, int lineNum, String docUri) {
List<Diagnostic> diagnostics = new ArrayList<>();
Pattern pattern = Pattern.compile("image::(.*?)\\[");
Matcher matcher = pattern.matcher(line);
while (matcher.find()) {
String imagePath = matcher.group(1);
// Resolve in img/ subdirectory
Path docDir = Paths.get(URI.create(docUri)).getParent();
Path imageFile = docDir.resolve("img").resolve(imagePath);
if (!Files.exists(imageFile)) {
Diagnostic diagnostic = new Diagnostic();
diagnostic.setRange(new Range(
new Position(lineNum, matcher.start()),
new Position(lineNum, matcher.end())
));
diagnostic.setSeverity(DiagnosticSeverity.Warning);
diagnostic.setMessage("Image file not found: " + imagePath);
diagnostic.setCode("missing-image");
diagnostic.setSource("asciidoc");
diagnostics.add(diagnostic);
}
}
return diagnostics;
}4. Validate Links
private List<Diagnostic> validateLinks(String line, int lineNum, String docUri) {
List<Diagnostic> diagnostics = new ArrayList<>();
Pattern pattern = Pattern.compile("link:(.*?)\\[");
Matcher matcher = pattern.matcher(line);
while (matcher.find()) {
String target = matcher.group(1);
// Skip external URLs
if (target.startsWith("http://") || target.startsWith("https://")) {
continue;
}
// Validate internal file links
Path docDir = Paths.get(URI.create(docUri)).getParent();
Path targetFile = resolveIncludePath(docDir, target);
if (!Files.exists(targetFile)) {
Diagnostic diagnostic = new Diagnostic();
diagnostic.setRange(new Range(
new Position(lineNum, matcher.start()),
new Position(lineNum, matcher.end())
));
diagnostic.setSeverity(DiagnosticSeverity.Warning);
diagnostic.setMessage("Link target not found: " + target);
diagnostic.setCode("broken-link");
diagnostic.setSource("asciidoc");
diagnostics.add(diagnostic);
}
}
return diagnostics;
}5. Validate Syntax
private List<Diagnostic> validateSyntax(String line, int lineNum) {
List<Diagnostic> diagnostics = new ArrayList<>();
String trimmed = line.trim();
// Check headers: should have space after '='
if (trimmed.startsWith("=") && !trimmed.startsWith("====")) {
int level = 0;
while (level < trimmed.length() && trimmed.charAt(level) == '=') {
level++;
}
if (level < trimmed.length() && trimmed.charAt(level) != ' ') {
Diagnostic diagnostic = new Diagnostic();
diagnostic.setRange(new Range(
new Position(lineNum, 0),
new Position(lineNum, line.length())
));
diagnostic.setSeverity(DiagnosticSeverity.Information);
diagnostic.setMessage("Header should have space after '='");
diagnostic.setCode("header-format");
diagnostic.setSource("asciidoc");
diagnostics.add(diagnostic);
}
}
// Check for malformed attributes
if (trimmed.startsWith(":") && !trimmed.endsWith(":")) {
int colonPos = trimmed.indexOf(':', 1);
if (colonPos == -1) {
Diagnostic diagnostic = new Diagnostic();
diagnostic.setRange(new Range(
new Position(lineNum, 0),
new Position(lineNum, line.length())
));
diagnostic.setSeverity(DiagnosticSeverity.Warning);
diagnostic.setMessage("Attribute definition should end with ':'");
diagnostic.setCode("malformed-attribute");
diagnostic.setSource("asciidoc");
diagnostics.add(diagnostic);
}
}
// Check for unclosed inline formatting
validateInlineFormatting(line, lineNum, diagnostics);
return diagnostics;
}
private void validateInlineFormatting(String line, int lineNum, List<Diagnostic> diagnostics) {
// Check for unclosed bold (*...*), italic (_..._), monospace (`...`)
checkUnclosedDelimiter(line, lineNum, '*', "bold", diagnostics);
checkUnclosedDelimiter(line, lineNum, '_', "italic", diagnostics);
checkUnclosedDelimiter(line, lineNum, '`', "monospace", diagnostics);
}
private void checkUnclosedDelimiter(String line, int lineNum, char delimiter,
String formatName, List<Diagnostic> diagnostics) {
int count = 0;
for (char c : line.toCharArray()) {
if (c == delimiter) count++;
}
if (count % 2 != 0) {
Diagnostic diagnostic = new Diagnostic();
diagnostic.setRange(new Range(
new Position(lineNum, 0),
new Position(lineNum, line.length())
));
diagnostic.setSeverity(DiagnosticSeverity.Hint);
diagnostic.setMessage("Unclosed " + formatName + " delimiter (" + delimiter + ")");
diagnostic.setCode("unclosed-formatting");
diagnostic.setSource("asciidoc");
diagnostics.add(diagnostic);
}
}6. Validate Attributes
private List<Diagnostic> validateAttributes(String line, int lineNum, AsciidocDocumentModel model) {
List<Diagnostic> diagnostics = new ArrayList<>();
// Find attribute references {attribute-name}
Pattern pattern = Pattern.compile("\\{([^}]+)\\}");
Matcher matcher = pattern.matcher(line);
// Extract defined attributes from document
Set<String> definedAttributes = extractDefinedAttributes(model);
while (matcher.find()) {
String attrName = matcher.group(1);
// Skip built-in attributes
if (isBuiltInAttribute(attrName)) {
continue;
}
if (!definedAttributes.contains(attrName)) {
Diagnostic diagnostic = new Diagnostic();
diagnostic.setRange(new Range(
new Position(lineNum, matcher.start()),
new Position(lineNum, matcher.end())
));
diagnostic.setSeverity(DiagnosticSeverity.Information);
diagnostic.setMessage("Undefined attribute: " + attrName);
diagnostic.setCode("undefined-attribute");
diagnostic.setSource("asciidoc");
diagnostics.add(diagnostic);
}
}
return diagnostics;
}
private Set<String> extractDefinedAttributes(AsciidocDocumentModel model) {
Set<String> attributes = new HashSet<>();
Pattern pattern = Pattern.compile("^:([^:]+):");
for (String line : model.getLines()) {
Matcher matcher = pattern.matcher(line.trim());
if (matcher.find()) {
attributes.add(matcher.group(1));
}
}
return attributes;
}
private boolean isBuiltInAttribute(String name) {
// Common built-in AsciiDoc attributes
Set<String> builtIn = Set.of(
"author", "email", "revdate", "revnumber", "revremark",
"doctitle", "docdate", "doctime", "docdatetime",
"localdate", "localtime", "localdatetime",
"cpp", "blank", "sp", "nbsp", "zwsp", "wj",
"apos", "quot", "lsquo", "rsquo", "ldquo", "rdquo",
"deg", "plus", "brvbar", "vbar", "amp", "lt", "gt"
);
return builtIn.contains(name);
}7. Validate Block Structure
private List<Diagnostic> validateBlockStructure(List<String> lines) {
List<Diagnostic> diagnostics = new ArrayList<>();
Stack<BlockInfo> blockStack = new Stack<>();
for (int i = 0; i < lines.size(); i++) {
String line = lines.get(i).trim();
// Check for block delimiters
if (line.equals("----")) {
toggleBlock(blockStack, "code", i, "----", diagnostics);
} else if (line.equals("====")) {
toggleBlock(blockStack, "example", i, "====", diagnostics);
} else if (line.equals("****")) {
toggleBlock(blockStack, "sidebar", i, "****", diagnostics);
} else if (line.equals("....")) {
toggleBlock(blockStack, "literal", i, "....", diagnostics);
} else if (line.equals("____")) {
toggleBlock(blockStack, "quote", i, "____", diagnostics);
} else if (line.equals("////")) {
toggleBlock(blockStack, "comment", i, "////", diagnostics);
}
}
// Check for unclosed blocks
while (!blockStack.isEmpty()) {
BlockInfo block = blockStack.pop();
Diagnostic diagnostic = new Diagnostic();
diagnostic.setRange(new Range(
new Position(block.startLine, 0),
new Position(block.startLine, block.delimiter.length())
));
diagnostic.setSeverity(DiagnosticSeverity.Error);
diagnostic.setMessage("Unclosed " + block.type + " block");
diagnostic.setCode("unclosed-block");
diagnostic.setSource("asciidoc");
diagnostics.add(diagnostic);
}
return diagnostics;
}
private void toggleBlock(Stack<BlockInfo> stack, String type, int lineNum,
String delimiter, List<Diagnostic> diagnostics) {
if (!stack.isEmpty() && stack.peek().delimiter.equals(delimiter)) {
stack.pop(); // Close block
} else {
stack.push(new BlockInfo(type, lineNum, delimiter)); // Open block
}
}
private static class BlockInfo {
String type;
int startLine;
String delimiter;
BlockInfo(String type, int startLine, String delimiter) {
this.type = type;
this.startLine = startLine;
this.delimiter = delimiter;
}
}Testing Checklist
Broken References
- Include non-existent file - shows error
- Include unreadable file - shows warning
- Reference missing image - shows warning
- Link to non-existent file - shows warning
- Fix broken reference - diagnostic disappears
Syntax Validation
- Header without space after '=' - shows hint
- Malformed attribute definition - shows warning
- Unclosed bold (*) - shows hint
- Unclosed italic (_) - shows hint
- Unclosed monospace (`) - shows hint
Attribute Validation
- Reference undefined attribute - shows info
- Built-in attributes don't trigger warning
- Define attribute - references no longer flagged
Block Structure
- Unclosed code block (----) - shows error
- Unclosed example block (====) - shows error
- Unclosed comment block (////) - shows error
- Properly closed blocks - no errors
General
- Diagnostics update on save/edit
- Error markers appear in Problems view
- Quick fixes work (if implemented)
- Performance acceptable for large files
Files to Modify
com.vogella.lsp.asciidoc.server/src/.../AsciidocTextDocumentService.java
Dependencies
- Requires: [Phase 1] Connect LSP Server to .adoc Content Type #27 (LSP connected to .adoc content type)
Success Criteria
- ✅ Broken includes detected and reported
- ✅ Missing images detected and reported
- ✅ Broken internal links detected
- ✅ Syntax errors detected (headers, attributes, formatting)
- ✅ Unclosed blocks detected
- ✅ Undefined attributes flagged (with built-in exclusions)
- ✅ Diagnostics update in real-time
- ✅ Performance acceptable (< 200ms for validation)
Estimated Effort
2-3 days (comprehensive validation rules and testing)
Priority
High - Prevents common errors and improves document quality
Related Issues
- Parent: Migration Plan: AsciiDoc LSP Integration #26 - Migration Plan: AsciiDoc LSP Integration
- Depends on: [Phase 1] Connect LSP Server to .adoc Content Type #27 - Connect LSP to .adoc content type
Future Enhancements
- Code actions to fix diagnostics (create missing file, fix formatting, etc.)
- Configurable severity levels (user preferences)
- Custom validation rules via settings
- Integration with Asciidoctor for deep validation
Notes
- Consider performance impact of file system checks on every edit
- May want to debounce validation (e.g., 500ms after last edit)
- Path resolution should match completion/links for consistency
- Be careful with false positives - better to under-report than over-report
Metadata
Metadata
Assignees
Labels
enhancementNew feature or requestNew feature or request