惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

P
Proofpoint News Feed
罗磊的独立博客
Martin Fowler
Martin Fowler
B
Blog RSS Feed
U
Unit 42
V
V2EX
H
Help Net Security
V
Vulnerabilities – Threatpost
The Register - Security
The Register - Security
IT之家
IT之家
Cloudbric
Cloudbric
K
Kaspersky official blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
Jina AI
Jina AI
Microsoft Azure Blog
Microsoft Azure Blog
大猫的无限游戏
大猫的无限游戏
P
Palo Alto Networks Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
S
SegmentFault 最新的问题
Cisco Talos Blog
Cisco Talos Blog
博客园 - 司徒正美
Cyberwarzone
Cyberwarzone
Spread Privacy
Spread Privacy
P
Proofpoint News Feed
Apple Machine Learning Research
Apple Machine Learning Research
T
Tenable Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
I
Intezer
G
GRAHAM CLULEY
T
Threatpost
C
Check Point Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
H
Hackread – Cybersecurity News, Data Breaches, AI and More
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
Tor Project blog
L
Lohrmann on Cybersecurity
WordPress大学
WordPress大学
N
Netflix TechBlog - Medium
The GitHub Blog
The GitHub Blog
C
CERT Recently Published Vulnerability Notes
Microsoft Security Blog
Microsoft Security Blog
Hugging Face - Blog
Hugging Face - Blog
The Hacker News
The Hacker News
Schneier on Security
Schneier on Security
爱范儿
爱范儿
云风的 BLOG
云风的 BLOG
Security Archives - TechRepublic
Security Archives - TechRepublic
T
The Blog of Author Tim Ferriss
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
P
Privacy & Cybersecurity Law Blog

See you soon

哟,好久不见,无线打印 | See you soon 试试将文章版本化管理吧 | See you soon 使用 Quadlet 将 Podman 中的 Postgres 当作 systemd 服务运行 | See you soon 大他者,那个无时无刻都在盯着你的东西 | See you soon Laws of Software Engineering,软件工程定律 | See you soon 浅记多因素身份认证 | See you soon Linux 内核中的度量单位 | See you soon 重置 GPG 智能密钥 | See you soon 向 NAS 引入 samba | See you soon 无法重复键入的 Fcitx5 | See you soon ZFS 降级事故 | See you soon 记被 XanMod Kernel 和 AppArmor 联合坑的一次踩坑 | See you soon agent 的 skill 与 toolcall | See you soon 记一次服务器被挂恶意挖矿二进制 | See you soon 活着的 Arc | See you soon 令 acme.sh 使用 Cloudflare 的 DNS API 签发与续签证书 | See you soon 于 Tokio 中卸载 CPU Bound 任务 | See you soon 如我所见,梦破碎的时候 | See you soon 74LS 家族手册 | See you soon JDK Projects 备忘录 | See you soon 关于历史 | See you soon 用 curl 下载 OnePlus 的 ROM | See you soon 实用命令切片 | See you soon 再见,Oh My Zsh。 | See you soon 你不应该复用 strings.Builder | See you soon 博客的明日 | See you soon 被 AppArmor 击杀的 Dockge | See you soon AI 时代的自我 | See you soon 支持删除的布隆过滤器 | See you soon 基于栈的虚拟机与基于寄存器的虚拟机 | See you soon RVA23 包含了什么 | See you soon Rust 的边界检查是否已经有很大的进步 | See you soon 使用 Rust 编写操作系统:Barebones | See you soon 使用 Rust 编写操作系统:引言 | See you soon 编译器笔记:rope | See you soon 编译器笔记:CST | See you soon 简单的相似去重算法(基于向量) | See you soon 更加现代的 PaperMC Minecraft 插件设计指南 | See you soon 简单的 CFG 语法分析方法 | See you soon 简单地使用 Caddy 实现 CORS 配置 | See you soon 让 OpenCV 可以被静态链接 | See you soon 图片搜索笔笺 | See you soon 电路板设计笔记-保护 | See you soon 使用 Rust 实现 SnowflakeId | See you soon 农夫乐事 FaQ 启示 | See you soon 使用 Rust 实现拓展系统札记 | See you soon 遗传学定律的代码实现 | See you soon EN:The Journey of Rust and Procps | See you soon ZH:Rust 与 Procps 之旅 | See you soon 使用 debootstrap 与 schroot 构建一个纯净环境 | See you soon GPG添加新的用户信息 | See you soon GSoC2024 笔记:使用 Rust 重新实现 procps | See you soon JWT 小册 | See you soon 使用密钥登录 SSH | See you soon Guava:Cache | See you soon 编译器笔记:增量编译 | See you soon Gradle秘境:添加一个类似modCompileOnly的依赖块 | See you soon Minecraft:原始经济系统设计简述 | See you soon TinyRemapper笔记 | See you soon MicroOS:进阶 | See you soon MicroOS:起步 | See you soon Swapfile 指北 | See you soon GPG 物理密钥的安装与密钥的迁移 | See you soon 用GPG签名告诉大家这就是我的提交 | See you soon DFS:深度优先搜索 | See you soon 文章翻译:从init.vim到init.lua | See you soon 目录遍历攻击 | See you soon 如何挂载.img文件,以及如何使用QEMU模拟arm64环境 | See you soon 在树莓派上编译 OpenCV | See you soon 关于卸载BlueStacks后遇到的问题 | See you soon
Paradoxical 札记 | See you soon
Krysztal Huang · 2025-05-12 · via See you soon

最近把 neorg 作为笔记和规划系统有点上头,但苦于没一个好用的 Language Server,自己又对编译器前端方面略有了解于是决定写下这篇大概会持续更新的札记

我们的最终目的是实现一个不错的 Language Server 并且可以在 neovim/helix 上跑起来

为什么是 neovim/helix 呢?因为我暂时不会继续用 VSCode 了

流水线设计

考虑到我希望 paradoxical 有如下两个功能

  • 重新格式化 neorg 文档
  • 良好的补全

因此我决定采用如下流水线设计:

Text → Lexer (RawTokenKind) → CST → Typed AST/HIR → Semantic Analysis → LSP Server → Editor

| src | Lazy Lexer | CST | AST |

需要着重说明的是,我们使用 rope 来保存和编辑源文件,这样我们所有的操作的时间复杂度都是

目标

阶段 0

在阶段 0 需要实现最基本的功能:解析

但由于 neorg 的格式内容较多所以在这个阶段只考虑实现一个子集

  • 标题
  • 有序/无序列表
  • 文本段
    • 文本类型 Paragraph:这种类型允许使用 neorg 的修饰符例如 {} // $||$ 等,这些修饰符可以正常执行其对应的功能
    • 逐字类型 VerbatimParagrah: 这种类型的文本段不会处理 neorg 的修饰符,所有输入的修饰符都应该原样输出
  • 标签 Tag

NOTE

请注意,由于 neorg 设计上要求的非歧义解析,所有解析失败的内容都应该回归为 文本类型 Paragraph

Token 设计

基于 Token 的功能和现在的目标,我设计出如下的 RawTokenKind

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]

pub enum RawTokenKind {

Punct(char),

NormalChar(char),

Tag,

Linending(LinendingKind),

}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]

pub enum LinendingKind {

LF,

CRLF,

}

以上的代码省略了对于 Display trait 的实现

Token 解析规则

我们使用 nom 这个库来实现解析,因为 neorg 的文档风格非常适合使用组合子解析器的方式进行解析。

不自己编写递归下降解析器是因为 neorg 的规范实际上比较简单,而且 neorg 的二义性情况很容易被判断,因此我们直接使用组合子解析器来实现解析就好

由于我们需要保留 Token 在文档里的位置信息,因此基于 RawTokenKind 派生出 RawToken,添加 span 字段保留当前 Token 的在文档里的位置信息(以偏移表示)

#[derive(Debug, Clone)]

pub struct RawToken {

pub kind: RawTokenKind,

pub span: Range<usize>,

}

接下来我们开始编写对于几个类型 Token 的组合子

pub const AVAILIABLE_PUNCT: &[char] = &['(', ')', '-', '~', '*', '/', '{', '}', '[', ']'];

fn parse_punct(input: Span) -> IResult<Span, RawToken> {

let (input, pos) = position(input)?;

let (input, matched) = nom::character::complete::one_of(AVAILIABLE_PUNCT)(input)?;

let token = RawToken {

kind: RawTokenKind::Punct(matched),

span: pos.location_offset()..(pos.location_offset() + matched.len_utf8()),

};

Ok((input, token))

}

fn parse_normal_char(input: Span) -> IResult<Span, RawToken> {

let (input, pos) = position(input)?;

let (input, matched) = nom::character::complete::satisfy(|c| {

!AVAILIABLE_PUNCT.contains(&c) && c != '@' && c != '\n' && c != '\r'

})(input)?;

let token = RawToken {

kind: RawTokenKind::NormalChar(matched),

span: pos.location_offset()..(pos.location_offset() + matched.len_utf8()),

};

Ok((input, token))

}

fn parse_tag(input: Span) -> IResult<Span, RawToken> {

let (input, pos) = position(input)?;

let (input, matched) = nom::character::complete::char('@')(input)?;

let token = RawToken {

kind: RawTokenKind::Tag,

span: pos.location_offset()..(pos.location_offset() + matched.len_utf8()),

};

Ok((input, token))

}

fn parse_linending(input: Span) -> IResult<Span, RawToken> {

let (input, pos) = position(input)?;

let (input, ending) = nom::branch::alt((

nom::combinator::map(nom::bytes::complete::tag("\n"), |_| LinendingKind::LF),

nom::combinator::map(nom::bytes::complete::tag("\r\n"), |_| LinendingKind::CRLF),

))

.parse(input)?;

let token = RawToken {

kind: RawTokenKind::Linending(ending),

span: pos.location_offset()..(pos.location_offset() + ending.to_string().len()),

};

Ok((input, token))

}

  • 对于 RawTokenKind::Tag 我们直接提取 @ 字符就好
  • 对于 RawTokenKind::Linending 我们需要判断并且保留文档里的换行符类型
    • \n:UNIX 平台所使用的换行符
    • \r\n:Windows 平台所使用的换行符
  • 对于其他的标点符号,我们引入一个常量 AVAILIABLE_PUNCT 表示可以出现在 RawTokenKind::Punct 内的字符(相当于白名单)
  • 如果在以上情况之外,则归类为 RawTokenKind::NormalChar

因此,我们可以总结出如下的 lexer

let rope = Rope::from_str(source);

let mut tokens = Vec::new();

let mut input = Span::new(source);

let mut parsers = nom::branch::alt((

Self::parse_tag,

Self::parse_linending,

Self::parse_punct,

Self::parse_normal_char,

));

while !input.fragment().is_empty() {

let Ok((rest, token)) = parsers.parse(input) else {

return Err(error::Error::Lexer);

};

tokens.push(token);

input = rest;

}

Ok(Self { text: rope, tokens })

单元测试也是不可少的一部分,因为包括对于中文的解析是略微麻烦也需要验证的

#[cfg(test)]

mod tests {

use super::*;

use crate::token::{LinendingKind, RawTokenKind};

#[test]

fn test_raw_document_parsing() {

let input = "@hello\n-world\r\n";

let doc = RawDocument::new(input).expect("Failed to parse document");

let expected = vec![

RawToken {

kind: RawTokenKind::Tag,

span: 0..1,

},

RawToken {

kind: RawTokenKind::NormalChar('h'),

span: 1..2,

},

RawToken {

kind: RawTokenKind::NormalChar('e'),

span: 2..3,

},

RawToken {

kind: RawTokenKind::NormalChar('l'),

span: 3..4,

},

RawToken {

kind: RawTokenKind::NormalChar('l'),

span: 4..5,

},

RawToken {

kind: RawTokenKind::NormalChar('o'),

span: 5..6,

},

RawToken {

kind: RawTokenKind::Linending(LinendingKind::LF),

span: 6..7,

},

RawToken {

kind: RawTokenKind::Punct('-'),

span: 7..8,

},

RawToken {

kind: RawTokenKind::NormalChar('w'),

span: 8..9,

},

RawToken {

kind: RawTokenKind::NormalChar('o'),

span: 9..10,

},

RawToken {

kind: RawTokenKind::NormalChar('r'),

span: 10..11,

},

RawToken {

kind: RawTokenKind::NormalChar('l'),

span: 11..12,

},

RawToken {

kind: RawTokenKind::NormalChar('d'),

span: 12..13,

},

RawToken {

kind: RawTokenKind::Linending(LinendingKind::CRLF),

span: 13..15,

},

];

assert_eq!(doc.tokens, expected);

}

#[test]

fn test_raw_document_parsing_with_chinese() {

let input = "@你好\n-世界\r\n";

let doc = RawDocument::new(input).expect("Failed to parse document");

let expected = vec![

RawToken {

kind: RawTokenKind::Tag,

span: 0..1,

},

RawToken {

kind: RawTokenKind::NormalChar(''),

span: 1..4,

},

RawToken {

kind: RawTokenKind::NormalChar(''),

span: 4..7,

},

RawToken {

kind: RawTokenKind::Linending(LinendingKind::LF),

span: 7..8,

},

RawToken {

kind: RawTokenKind::Punct('-'),

span: 8..9,

},

RawToken {

kind: RawTokenKind::NormalChar(''),

span: 9..12,

},

RawToken {

kind: RawTokenKind::NormalChar(''),

span: 12..15,

},

RawToken {

kind: RawTokenKind::Linending(LinendingKind::CRLF),

span: 15..17,

},

];

assert_eq!(doc.tokens, expected);

}

}