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_TEXT pattern 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
Syntax Validation
Attribute Validation
Block Structure
General
Files to Modify
com.vogella.lsp.asciidoc.server/src/.../AsciidocTextDocumentService.java
Dependencies
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
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
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.javaCurrent Diagnostics:
PLACEHOLDER_TEXTpattern onlyMissing:
include::referencesRequired Changes
1. Enhance validateDocument Method
2. Validate Include References
3. Validate Image References
4. Validate Links
5. Validate Syntax
6. Validate Attributes
7. Validate Block Structure
Testing Checklist
Broken References
Syntax Validation
Attribute Validation
Block Structure
General
Files to Modify
com.vogella.lsp.asciidoc.server/src/.../AsciidocTextDocumentService.javaDependencies
Success Criteria
Estimated Effort
2-3 days (comprehensive validation rules and testing)
Priority
High - Prevents common errors and improves document quality
Related Issues
Future Enhancements
Notes