Major clean up and improvements to vimwiki compatibility and documentation.
CI / cargo fmt --check (push) Successful in 33s
CI / cargo clippy (push) Successful in 23s
CI / cargo test (push) Successful in 33s
CI / editor keymaps (push) Successful in 1m25s

Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
2026-05-30 18:35:40 +00:00
parent 95645a2b91
commit 8ab6015405
71 changed files with 2496 additions and 1914 deletions
+4 -6
View File
@@ -4,8 +4,6 @@
//! a `Lexer` produces a `TokenStream`, a `Parser` consumes it and produces a
//! `DocumentNode`. A `SyntaxPlugin` is the type-erased facade that the LSP
//! layer holds in a `SyntaxRegistry`.
//!
//! See SPEC.md §6.3 (interface) and §6.6 (lexer strategy).
pub mod registry;
pub mod vimwiki;
@@ -15,7 +13,7 @@ pub use registry::SyntaxRegistry;
use crate::ast::DocumentNode;
/// Eager token buffer produced by a `Lexer`. The newtype keeps the door
/// open to a lazy/streaming variant later (SPEC.md §6.6) without breaking
/// open to a lazy/streaming variant later without breaking
/// callers.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TokenStream<T> {
@@ -89,7 +87,7 @@ pub trait Lexer {
}
/// Parser half of a syntax plugin. Consumes a `TokenStream` and produces a
/// `DocumentNode`. Per SPEC.md §6.7 parsing is resilient — malformed input
/// `DocumentNode`. Parsing is resilient — malformed input
/// becomes `BlockNode::Error(ErrorNode)`, never a hard failure.
pub trait Parser {
type Token;
@@ -104,13 +102,13 @@ pub trait Parser {
/// hold heterogeneous plugins behind a single trait object.
///
/// `Send + Sync` is required — the LSP server stores plugins behind an `Arc`
/// and shares them across request handler tasks (SPEC.md §6.3).
/// and shares them across request handler tasks.
pub trait SyntaxPlugin: Send + Sync {
fn id(&self) -> &str;
fn display_name(&self) -> &str;
/// File extensions associated with this syntax. Each entry includes the
/// leading dot, e.g. `[".wiki"]`. See SPEC.md §6.3.
/// leading dot, e.g. `[".wiki"]`.
fn file_extensions(&self) -> &[&str];
fn parse(&self, text: &str) -> DocumentNode;
+1 -1
View File
@@ -1,4 +1,4 @@
//! Registry of syntax plugins. See SPEC.md §6.3.
//! Registry of syntax plugins.
//!
//! Plugins are stored behind `Arc<dyn SyntaxPlugin>` so the LSP layer can
//! hand a cheap clone to per-document tasks without re-locking the registry.
@@ -1,6 +1,6 @@
//! Vimwiki lexer.
//!
//! Hand-rolled, two-pass per SPEC.md §6.6:
//! Hand-rolled, two-pass:
//!
//! - **Block pass:** scan the source line by line, recognise structural
//! constructs (headings, lists, tables, fences, comments, etc.), emit
@@ -12,7 +12,7 @@
//! Both passes share one `Vec<VimwikiToken>` so the parser sees a single,
//! ordered stream. The lexer is permissive: every delimiter is emitted; the
//! parser pairs them and falls back to literal text on mismatches. Spans are
//! stored as 0-indexed byte offsets per SPEC.md §6.5.
//! stored as 0-indexed byte offsets.
//!
//! Multi-line constructs (`{{{ }}}`, `{{$ }}$`, `%%+ +%%`) flip a
//! `BlockMode` so subsequent lines are treated as raw content until the
@@ -97,13 +97,13 @@ pub enum VimwikiTokenKind {
CommentMultiClose,
CommentMultiLine(String),
// ----- Tags (SPEC §12.3) -----
// ----- Tags -----
/// `:tag1:tag2:` — colon-delimited tag line. The lexer treats this as
/// block-level (must be the entire trimmed content of the line).
/// Scope (file / heading / standalone) is the parser's job.
Tag(Vec<String>),
// ----- Page placeholders (SPEC §9) -----
// ----- Page placeholders -----
PlaceholderTitle(Option<String>),
PlaceholderNohtml,
PlaceholderTemplate(Option<String>),
@@ -129,7 +129,7 @@ pub enum VimwikiTokenKind {
TransclusionSep,
RawUrl(String),
/// Lex error — never aborts the whole document (SPEC §6.7).
/// Lex error — never aborts the whole document.
Error(String),
}
@@ -481,7 +481,7 @@ impl<'src> LexState<'src> {
if trimmed.len() < 4 || !trimmed.bytes().all(|b| b == b'-') {
return false;
}
// SPEC §9: four or more dashes. Allow surrounding whitespace.
// Four or more dashes. Allow surrounding whitespace.
let span = Span::new(self.line_start_pos(), self.line_end_pos(line));
self.push(VimwikiTokenKind::HorizontalRule, span);
self.emit_newline(line);
+1 -1
View File
@@ -10,7 +10,7 @@ use crate::ast::DocumentNode;
use crate::syntax::{Lexer, Parser, SyntaxPlugin};
/// SyntaxPlugin facade: chains `VimwikiLexer` + `VimwikiParser` so the
/// registry can hand out a single trait object per SPEC.md §6.3.
/// registry can hand out a single trait object.
#[derive(Debug, Default, Clone)]
pub struct VimwikiSyntax;
@@ -1,6 +1,6 @@
//! Vimwiki parser.
//!
//! Hand-rolled recursive-descent over `Vec<VimwikiToken>` (SPEC.md §6.7).
//! Hand-rolled recursive-descent over `Vec<VimwikiToken>`.
//! Resilient: never aborts the document. Anything the dispatcher can't
//! claim becomes a `BlockNode::Error(ErrorNode)` so progress is
//! guaranteed.
@@ -28,7 +28,7 @@
//!
//! `[[ ]]` payloads are inspected to choose between `WikiLinkNode`
//! (the default) and `ExternalLinkNode` (when the target is a URL). The
//! `LinkTarget` enum follows SPEC §9: leading `/` is root-relative,
//! `LinkTarget` enum follows the vimwiki conventions: leading `/` is root-relative,
//! `//` is filesystem-absolute, `wiki<N>:` / `wn.<Name>:` are interwiki,
//! `diary:` / `file:` / `local:` are their own kinds, and a trailing `/`
//! marks a directory link.
@@ -69,10 +69,10 @@ impl Parser for VimwikiParser {
struct ParseState<'a> {
toks: &'a [VimwikiToken],
pos: usize,
/// Phase 12: count of `HeadingNode`s already pushed to `children`.
/// Count of `HeadingNode`s already pushed to `children`.
/// `headings_seen - 1` is the most recent heading's index.
headings_seen: usize,
/// Phase 12: source line of the most recently-emitted heading.
/// Source line of the most recently-emitted heading.
/// `tag_line - last_heading_line ∈ {1, 2}` is a header-scope tag.
last_heading_line: Option<u32>,
}
@@ -141,7 +141,7 @@ impl<'a> ParseState<'a> {
}
let before = self.pos;
let block = self.parse_block();
// Phase 12: file-scope tags get accumulated into metadata so
// File-scope tags get accumulated into metadata so
// callers can read page-level tag membership without walking
// the AST. The TagNode itself stays in `children` so renderers
// see it too.
@@ -280,7 +280,7 @@ impl<'a> ParseState<'a> {
let inline = parse_inline_seq(&self.toks[inline_start..self.pos]);
self.advance();
self.eat_newline();
// Record for tag-scope resolution (Phase 12).
// Record for tag-scope resolution.
self.headings_seen += 1;
self.last_heading_line = Some(span_end.line);
return BlockNode::Heading(HeadingNode {