chore: add orgize-{cli,common,lsp} package
This commit is contained in:
parent
6930640866
commit
4cc1130a17
131 changed files with 6577 additions and 56 deletions
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
|
|
@ -45,12 +45,12 @@ jobs:
|
|||
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
|
||||
- name: Build
|
||||
run: wasm-pack build -t web -d ./dist --out-name orgize ./wasm/
|
||||
run: wasm-pack build -t web -d ./dist --out-name orgize ./orgize-wasm/
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v2
|
||||
with:
|
||||
path: "./wasm"
|
||||
path: "./orgize-wasm"
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
|
|
|
|||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -2,6 +2,6 @@
|
|||
**/*.rs.bk
|
||||
Cargo.lock
|
||||
|
||||
benches/*.org
|
||||
.vscode
|
||||
.gdb_history
|
||||
perf.data*
|
||||
|
|
|
|||
54
Cargo.toml
54
Cargo.toml
|
|
@ -1,49 +1,21 @@
|
|||
[package]
|
||||
name = "orgize"
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
".",
|
||||
"./orgize",
|
||||
"./orgize-cli",
|
||||
"./orgize-common",
|
||||
"./orgize-lsp",
|
||||
"./orgize-wasm",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.10.0-alpha.6"
|
||||
authors = ["PoiScript <poiscript@gmail.com>"]
|
||||
description = "A Rust library for parsing org-mode files."
|
||||
repository = "https://github.com/PoiScript/orgize"
|
||||
readme = "README.md"
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
keywords = ["orgmode", "org-mode", "emacs", "parser"]
|
||||
exclude = ["/wasm", "/.github"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
indexmap = ["dep:indexmap"]
|
||||
chrono = ["dep:chrono"]
|
||||
|
||||
[workspace]
|
||||
members = [".", "./wasm"]
|
||||
|
||||
[dependencies]
|
||||
bytecount = "0.6"
|
||||
chrono = { version = "0.4", optional = true }
|
||||
indexmap = { version = "2.1", optional = true }
|
||||
jetscii = "0.5"
|
||||
memchr = "2.5"
|
||||
nom = { version = "7.1", default-features = false, features = ["std"] }
|
||||
rowan = "0.15"
|
||||
tracing = "0.1"
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = "0.5"
|
||||
insta = "1.29"
|
||||
slugify = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["fmt"] }
|
||||
|
||||
[profile.dev.package]
|
||||
insta.opt-level = 3
|
||||
similar.opt-level = 3
|
||||
|
||||
[profile.bench]
|
||||
debug = true
|
||||
|
||||
[[bench]]
|
||||
name = "parse"
|
||||
harness = false
|
||||
|
|
|
|||
25
orgize-cli/Cargo.toml
Normal file
25
orgize-cli/Cargo.toml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
[package]
|
||||
name = "orgize-cli"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
description = "CLI tools for org-mode file, powered by orgize."
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.75"
|
||||
clap = { version = "4.4.11", features = ["derive"] }
|
||||
clap-verbosity-flag = "2.1.0"
|
||||
jetscii = "0.5.3"
|
||||
nom = "7.1.3"
|
||||
orgize = { path = "../orgize" }
|
||||
orgize-common = { path = "../orgize-common" }
|
||||
resolve-path = "0.1.0"
|
||||
tempfile = "3.8.1"
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = { version = "0.3", features = ["fmt"] }
|
||||
|
||||
[[bin]]
|
||||
name = "orgize"
|
||||
path = "src/main.rs"
|
||||
80
orgize-cli/src/detangle.rs
Normal file
80
orgize-cli/src/detangle.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
use clap::Args;
|
||||
use orgize::{
|
||||
export::{Container, Event, TraversalContext, Traverser},
|
||||
Org,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::diff;
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct Command {
|
||||
path: Vec<PathBuf>,
|
||||
|
||||
#[arg(short, long)]
|
||||
dry_run: bool,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub fn run(self) -> anyhow::Result<()> {
|
||||
for path in self.path {
|
||||
if !path.exists() {
|
||||
tracing::error!("{:?} is not existed", path);
|
||||
}
|
||||
|
||||
let orgi = std::fs::read_to_string(&path)?;
|
||||
|
||||
let mut t = DetangleTraverser {
|
||||
results: Vec::new(),
|
||||
org_file_path: path,
|
||||
};
|
||||
|
||||
let org = Org::parse(&orgi);
|
||||
org.traverse(&mut t);
|
||||
|
||||
if self.dry_run {
|
||||
diff::print(&orgi, t.results);
|
||||
} else {
|
||||
diff::write_to_file(&orgi, t.results, t.org_file_path)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct DetangleTraverser {
|
||||
results: Vec<(usize, usize, String)>,
|
||||
org_file_path: PathBuf,
|
||||
}
|
||||
|
||||
impl Traverser for DetangleTraverser {
|
||||
fn event(&mut self, event: Event, ctx: &mut TraversalContext) {
|
||||
match event {
|
||||
Event::Enter(Container::SourceBlock(block)) => {
|
||||
if let Ok(Some((start, end, content))) =
|
||||
orgize_common::detangle(block, &self.org_file_path)
|
||||
{
|
||||
self.results.push((start, end, content));
|
||||
}
|
||||
|
||||
ctx.skip();
|
||||
}
|
||||
|
||||
// skip some containers for performance
|
||||
Event::Enter(Container::List(_))
|
||||
| Event::Enter(Container::OrgTable(_))
|
||||
| Event::Enter(Container::SpecialBlock(_))
|
||||
| Event::Enter(Container::QuoteBlock(_))
|
||||
| Event::Enter(Container::CenterBlock(_))
|
||||
| Event::Enter(Container::VerseBlock(_))
|
||||
| Event::Enter(Container::CommentBlock(_))
|
||||
| Event::Enter(Container::ExampleBlock(_))
|
||||
| Event::Enter(Container::ExportBlock(_)) => {
|
||||
ctx.skip();
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
46
orgize-cli/src/diff.rs
Normal file
46
orgize-cli/src/diff.rs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::builder::styling::{AnsiColor, Color, Style};
|
||||
|
||||
pub fn print(orgi: &str, mut patches: Vec<(usize, usize, String)>) {
|
||||
patches.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
let mut off = 0;
|
||||
|
||||
for (start, end, content) in patches {
|
||||
print!("{}", &orgi[off..(start)]);
|
||||
|
||||
if orgi[start..end] != content {
|
||||
let style = Style::new().fg_color(Color::Ansi(AnsiColor::Cyan).into());
|
||||
print!("{}{}{}", style.render(), &content, style.render_reset());
|
||||
} else {
|
||||
print!("{}", &content);
|
||||
}
|
||||
|
||||
off = end;
|
||||
}
|
||||
|
||||
print!("{}", &orgi[off..]);
|
||||
}
|
||||
|
||||
pub fn write_to_file(
|
||||
orgi: &str,
|
||||
mut patches: Vec<(usize, usize, String)>,
|
||||
path: PathBuf,
|
||||
) -> anyhow::Result<()> {
|
||||
patches.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
let file = &mut OpenOptions::new().write(true).open(path)?;
|
||||
let mut off = 0;
|
||||
|
||||
for (start, end, content) in patches {
|
||||
write!(file, "{}{}", &orgi[off..start], &content)?;
|
||||
off = end;
|
||||
}
|
||||
|
||||
write!(file, "{}", &orgi[off..])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
86
orgize-cli/src/execute_src_block.rs
Normal file
86
orgize-cli/src/execute_src_block.rs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
use clap::Args;
|
||||
use orgize::{
|
||||
export::{Container, Event, TraversalContext, Traverser},
|
||||
Org,
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::diff;
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct Command {
|
||||
path: Vec<PathBuf>,
|
||||
|
||||
#[arg(short, long)]
|
||||
dry_run: bool,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub fn run(self) -> anyhow::Result<()> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
|
||||
tracing::debug!("Create tempdir {:?}", dir.path().to_string_lossy());
|
||||
|
||||
for path in self.path {
|
||||
if !path.exists() {
|
||||
tracing::error!("{:?} is not existed", path);
|
||||
}
|
||||
|
||||
let mut t = ExecuteTraverser {
|
||||
results: Vec::new(),
|
||||
dir: &dir,
|
||||
};
|
||||
|
||||
let orgi = std::fs::read_to_string(&path)?;
|
||||
let org = Org::parse(&orgi);
|
||||
org.traverse(&mut t);
|
||||
|
||||
t.results.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
if self.dry_run {
|
||||
diff::print(&orgi, t.results);
|
||||
} else {
|
||||
diff::write_to_file(&orgi, t.results, path)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct ExecuteTraverser<'a> {
|
||||
results: Vec<(usize, usize, String)>,
|
||||
|
||||
dir: &'a tempfile::TempDir,
|
||||
}
|
||||
|
||||
impl<'a> Traverser for ExecuteTraverser<'a> {
|
||||
fn event(&mut self, event: Event, ctx: &mut TraversalContext) {
|
||||
match event {
|
||||
Event::Enter(Container::SourceBlock(block)) => {
|
||||
if let Ok(Some((start, end, content))) =
|
||||
orgize_common::execute(block, self.dir.path())
|
||||
{
|
||||
self.results.push((start, end, content));
|
||||
}
|
||||
|
||||
ctx.skip();
|
||||
}
|
||||
|
||||
// skip some containers for performance
|
||||
Event::Enter(Container::List(_))
|
||||
| Event::Enter(Container::OrgTable(_))
|
||||
| Event::Enter(Container::SpecialBlock(_))
|
||||
| Event::Enter(Container::QuoteBlock(_))
|
||||
| Event::Enter(Container::CenterBlock(_))
|
||||
| Event::Enter(Container::VerseBlock(_))
|
||||
| Event::Enter(Container::CommentBlock(_))
|
||||
| Event::Enter(Container::ExampleBlock(_))
|
||||
| Event::Enter(Container::ExportBlock(_)) => {
|
||||
ctx.skip();
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
54
orgize-cli/src/main.rs
Normal file
54
orgize-cli/src/main.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
mod detangle;
|
||||
mod diff;
|
||||
mod execute_src_block;
|
||||
mod tangle;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use clap_verbosity_flag::{InfoLevel, LevelFilter as CLevelFilter, Verbosity};
|
||||
use tracing::level_filters::LevelFilter;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[clap(name = "orgize-tools", version)]
|
||||
pub struct App {
|
||||
#[clap(subcommand)]
|
||||
command: Command,
|
||||
|
||||
#[command(flatten)]
|
||||
verbose: Verbosity<InfoLevel>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum Command {
|
||||
#[clap(name = "tangle")]
|
||||
Tangle(tangle::Command),
|
||||
|
||||
#[clap(name = "detangle")]
|
||||
Detangle(detangle::Command),
|
||||
|
||||
#[clap(name = "execute-src-block")]
|
||||
ExecuteSrcBlock(execute_src_block::Command),
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let parsed = App::parse();
|
||||
|
||||
tracing_subscriber::fmt()
|
||||
.with_max_level(match parsed.verbose.log_level_filter() {
|
||||
CLevelFilter::Off => LevelFilter::OFF,
|
||||
CLevelFilter::Error => LevelFilter::ERROR,
|
||||
CLevelFilter::Warn => LevelFilter::WARN,
|
||||
CLevelFilter::Info => LevelFilter::INFO,
|
||||
CLevelFilter::Debug => LevelFilter::DEBUG,
|
||||
CLevelFilter::Trace => LevelFilter::TRACE,
|
||||
})
|
||||
.without_time()
|
||||
.with_file(false)
|
||||
.with_line_number(false)
|
||||
.init();
|
||||
|
||||
match parsed.command {
|
||||
Command::Tangle(cmd) => cmd.run(),
|
||||
Command::Detangle(cmd) => cmd.run(),
|
||||
Command::ExecuteSrcBlock(cmd) => cmd.run(),
|
||||
}
|
||||
}
|
||||
111
orgize-cli/src/tangle.rs
Normal file
111
orgize-cli/src/tangle.rs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
use clap::{
|
||||
builder::styling::{AnsiColor, Color, Style},
|
||||
Args,
|
||||
};
|
||||
use orgize::{
|
||||
export::{Container, Event, TraversalContext, Traverser},
|
||||
Org,
|
||||
};
|
||||
use std::fs;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct Command {
|
||||
path: Vec<PathBuf>,
|
||||
|
||||
#[arg(short, long)]
|
||||
dry_run: bool,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub fn run(self) -> anyhow::Result<()> {
|
||||
let mut t = TangleTraverser::default();
|
||||
|
||||
for path in self.path {
|
||||
if !path.exists() {
|
||||
tracing::error!("{:?} is not existed", path);
|
||||
}
|
||||
|
||||
let string = std::fs::read_to_string(&path)?;
|
||||
let org = Org::parse(string);
|
||||
t.org_file_path = path;
|
||||
t.count = 0;
|
||||
org.traverse(&mut t);
|
||||
tracing::info!(
|
||||
"Found {} code block from {}",
|
||||
t.count,
|
||||
t.org_file_path.to_string_lossy()
|
||||
);
|
||||
}
|
||||
|
||||
if self.dry_run {
|
||||
for (path, (permission, content, mkdir)) in t.results {
|
||||
let style = Style::new()
|
||||
.fg_color(Color::Ansi(AnsiColor::BrightYellow).into())
|
||||
.underline()
|
||||
.bold();
|
||||
print!(
|
||||
"{}{}{}",
|
||||
style.render(),
|
||||
path.to_string_lossy(),
|
||||
style.render_reset(),
|
||||
);
|
||||
if let Some(permission) = permission {
|
||||
print!(" (permission: {:o})", permission);
|
||||
}
|
||||
if mkdir {
|
||||
print!(" (mkdir: yes)");
|
||||
}
|
||||
println!("\n{}", content);
|
||||
}
|
||||
} else {
|
||||
for (path, (_, contents, _)) in t.results {
|
||||
fs::write(&path, contents)?;
|
||||
tracing::info!("Wrote to {}", path.to_string_lossy());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct TangleTraverser {
|
||||
results: HashMap<PathBuf, (Option<u32>, String, bool)>,
|
||||
count: usize,
|
||||
org_file_path: PathBuf,
|
||||
}
|
||||
|
||||
impl Traverser for TangleTraverser {
|
||||
fn event(&mut self, event: Event, ctx: &mut TraversalContext) {
|
||||
match event {
|
||||
Event::Enter(Container::SourceBlock(block)) => {
|
||||
if let Ok(Some((path, permission, content, mkdir))) =
|
||||
orgize_common::tangle(block, &self.org_file_path)
|
||||
{
|
||||
let value = self.results.entry(path).or_default();
|
||||
value.0 = permission;
|
||||
value.1.push_str(&content);
|
||||
value.2 = mkdir;
|
||||
}
|
||||
|
||||
ctx.skip();
|
||||
}
|
||||
|
||||
// skip some containers for performance
|
||||
Event::Enter(Container::List(_))
|
||||
| Event::Enter(Container::OrgTable(_))
|
||||
| Event::Enter(Container::SpecialBlock(_))
|
||||
| Event::Enter(Container::QuoteBlock(_))
|
||||
| Event::Enter(Container::CenterBlock(_))
|
||||
| Event::Enter(Container::VerseBlock(_))
|
||||
| Event::Enter(Container::CommentBlock(_))
|
||||
| Event::Enter(Container::ExampleBlock(_))
|
||||
| Event::Enter(Container::ExportBlock(_)) => {
|
||||
ctx.skip();
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
orgize-common/Cargo.toml
Normal file
16
orgize-common/Cargo.toml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
[package]
|
||||
name = "orgize-common"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
description = "Shared code between orgize-lsp and orgize-lsp."
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.75"
|
||||
jetscii = "0.5.3"
|
||||
nom = "7.1.3"
|
||||
orgize = { path = "../orgize" }
|
||||
resolve-path = "0.1.0"
|
||||
tracing = "0.1.40"
|
||||
110
orgize-common/src/detangle.rs
Normal file
110
orgize-common/src/detangle.rs
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
use orgize::{
|
||||
ast::{Headline, SourceBlock},
|
||||
rowan::ast::AstNode,
|
||||
SyntaxKind,
|
||||
};
|
||||
use resolve_path::PathResolveExt;
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
use crate::{
|
||||
header_argument::{header_argument, property_drawer, property_keyword},
|
||||
utils::language_comments,
|
||||
};
|
||||
|
||||
pub fn detangle(
|
||||
block: SourceBlock,
|
||||
file_path: &PathBuf,
|
||||
) -> anyhow::Result<Option<(usize, usize, String)>> {
|
||||
let arg1 = block.parameters().unwrap_or_default();
|
||||
let arg2 = property_drawer(block.syntax()).unwrap_or_default();
|
||||
let arg3 = property_keyword(block.syntax()).unwrap_or_default();
|
||||
let language = block.language().unwrap_or_default();
|
||||
|
||||
let tangle = header_argument(&arg1, &arg2, &arg3, ":tangle", "no");
|
||||
|
||||
if tangle == "no" {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let comments = header_argument(&arg1, &arg2, &arg3, ":comments", "no");
|
||||
|
||||
let parent = block
|
||||
.syntax()
|
||||
.ancestors()
|
||||
.find(|n| n.kind() == SyntaxKind::HEADLINE || n.kind() == SyntaxKind::DOCUMENT);
|
||||
|
||||
let nth = parent
|
||||
.as_ref()
|
||||
.and_then(|n| n.children().position(|c| &c == block.syntax()))
|
||||
.unwrap_or(1);
|
||||
|
||||
let headline_title = parent.and_then(Headline::cast).map(|headline| {
|
||||
headline
|
||||
.title()
|
||||
.fold(String::new(), |a, n| a + &n.to_string())
|
||||
});
|
||||
|
||||
let dest_path = tangle.try_resolve_in(file_path)?.to_path_buf();
|
||||
|
||||
let content = fs::read_to_string(dest_path)?;
|
||||
|
||||
let Some(begin) = block
|
||||
.syntax()
|
||||
.children()
|
||||
.find(|n| n.kind() == SyntaxKind::BLOCK_CONTENT)
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let text_range = begin.text_range();
|
||||
|
||||
if comments == "yes" || comments == "link" || comments == "noweb" || comments == "both" {
|
||||
let begin_comments = format!(
|
||||
"[[file:{path}::*{title}][{title}:{nth}]]",
|
||||
title = headline_title.as_deref().unwrap_or("No heading"),
|
||||
path = file_path.to_string_lossy(),
|
||||
);
|
||||
let end_comments = format!(
|
||||
"{title}:{nth} ends here",
|
||||
title = headline_title.as_deref().unwrap_or("No heading"),
|
||||
);
|
||||
|
||||
let mut block_content = String::new();
|
||||
|
||||
for line in content
|
||||
.lines()
|
||||
.skip_while(|line| trim_comments(line, &language).unwrap_or_default() != begin_comments)
|
||||
.skip(1)
|
||||
{
|
||||
if trim_comments(line, &language).unwrap_or_default() == end_comments {
|
||||
return Ok(Some((
|
||||
text_range.start().into(),
|
||||
text_range.end().into(),
|
||||
block_content,
|
||||
)));
|
||||
} else {
|
||||
block_content += line;
|
||||
block_content += "\n";
|
||||
}
|
||||
}
|
||||
|
||||
tracing::warn!(
|
||||
"Cannot found contents wrapped by comments for code block {path}*{title}:{nth}.",
|
||||
title = headline_title.as_deref().unwrap_or("No heading"),
|
||||
path = file_path.to_string_lossy(),
|
||||
);
|
||||
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some((
|
||||
text_range.start().into(),
|
||||
text_range.end().into(),
|
||||
content,
|
||||
)))
|
||||
}
|
||||
|
||||
fn trim_comments<'a>(input: &'a str, language: &str) -> Option<&'a str> {
|
||||
let (begin, end) = language_comments(language)?;
|
||||
Some(input.trim().strip_prefix(begin)?.strip_suffix(end)?.trim())
|
||||
}
|
||||
142
orgize-common/src/execute_src_block.rs
Normal file
142
orgize-common/src/execute_src_block.rs
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
use orgize::{
|
||||
ast::{AffiliatedKeyword, SourceBlock},
|
||||
rowan::ast::AstNode,
|
||||
SyntaxKind,
|
||||
};
|
||||
use std::{fs::File, io::Write, iter::once, path::Path, process};
|
||||
|
||||
use crate::{
|
||||
header_argument::{header_argument, property_drawer, property_keyword},
|
||||
utils::language_execute_command,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Format {
|
||||
Code,
|
||||
List,
|
||||
Verbatim,
|
||||
Html,
|
||||
Latex,
|
||||
Raw,
|
||||
}
|
||||
|
||||
pub fn execute(block: SourceBlock, path: &Path) -> anyhow::Result<Option<(usize, usize, String)>> {
|
||||
let arg1 = block.parameters().unwrap_or_default();
|
||||
let arg2 = property_drawer(block.syntax()).unwrap_or_default();
|
||||
let arg3 = property_keyword(block.syntax()).unwrap_or_default();
|
||||
let language = block.language().unwrap_or_default();
|
||||
let results = header_argument(&arg1, &arg2, &arg3, ":results", "no");
|
||||
|
||||
if results == "no" {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let Some(command) = language_execute_command(&language) else {
|
||||
anyhow::bail!("{language:?} is not supported.")
|
||||
};
|
||||
|
||||
let mut segs = results.split(&[' ', '\t']).filter(|x| !x.is_empty());
|
||||
|
||||
let format = match (segs.next(), segs.next()) {
|
||||
(Some("output"), Some("code")) | (Some("code"), None) => Format::Code,
|
||||
(Some("output"), Some("list")) | (Some("list"), None) => Format::List,
|
||||
(Some("output"), Some("scalar"))
|
||||
| (Some("scalar"), None)
|
||||
| (Some("output"), Some("verbatim"))
|
||||
| (Some("verbatim"), None) => Format::Verbatim,
|
||||
(Some("output"), Some("html")) | (Some("html"), None) => Format::Html,
|
||||
(Some("output"), Some("latex")) | (Some("latex"), None) => Format::Latex,
|
||||
(Some("output"), Some("raw")) | (Some("raw"), None) => Format::Raw,
|
||||
(Some("value"), _) => anyhow::bail!("{language:?} is not supported."),
|
||||
_ => return Ok(None),
|
||||
};
|
||||
|
||||
let results = collect_output(command, &block.value(), format, path)?;
|
||||
|
||||
if let Some((start, end)) = find_existing_results(&block) {
|
||||
Ok(Some((start, end, results)))
|
||||
} else {
|
||||
let start = block.end() as usize;
|
||||
Ok(Some((start, start, format!("\n#+RESULTS:\n{}\n", results))))
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_output(
|
||||
command: &str,
|
||||
value: &str,
|
||||
format: Format,
|
||||
path: &Path,
|
||||
) -> anyhow::Result<String> {
|
||||
let path = path.join("orgize-temporary");
|
||||
|
||||
let mut file = File::create(&path)?;
|
||||
|
||||
file.write_all(value.as_bytes())?;
|
||||
|
||||
let output = process::Command::new(command).arg(path).output()?;
|
||||
|
||||
let output = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
match format {
|
||||
Format::Code => Ok(once("#+begin_src")
|
||||
.chain(output.lines())
|
||||
.chain(once("#+end_src"))
|
||||
.fold(String::new(), |acc, line| acc + line + "\n")),
|
||||
|
||||
Format::Html => Ok(once("#+begin_export html")
|
||||
.chain(output.lines())
|
||||
.chain(once("#+end_export"))
|
||||
.fold(String::new(), |acc, line| acc + line + "\n")),
|
||||
|
||||
Format::Latex => Ok(once("#+begin_export latex")
|
||||
.chain(output.lines())
|
||||
.chain(once("#+end_export"))
|
||||
.fold(String::new(), |acc, line| acc + line + "\n")),
|
||||
|
||||
Format::List => Ok(output
|
||||
.lines()
|
||||
.fold(String::new(), |acc, line| acc + "- " + line + "\n")),
|
||||
|
||||
Format::Verbatim => Ok(output
|
||||
.lines()
|
||||
.fold(String::new(), |acc, line| acc + ": " + line + "\n")),
|
||||
|
||||
Format::Raw => Ok(output.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn find_existing_results(block: &SourceBlock) -> Option<(usize, usize)> {
|
||||
let results = block
|
||||
.syntax()
|
||||
.next_sibling()
|
||||
.filter(|n| {
|
||||
matches!(
|
||||
n.kind(),
|
||||
SyntaxKind::ORG_TABLE
|
||||
| SyntaxKind::FIXED_WIDTH
|
||||
| SyntaxKind::LIST
|
||||
| SyntaxKind::SOURCE_BLOCK
|
||||
| SyntaxKind::EXPORT_BLOCK
|
||||
)
|
||||
})
|
||||
.filter(|n| {
|
||||
n.children()
|
||||
.filter_map(AffiliatedKeyword::cast)
|
||||
.any(|k| k.key().eq_ignore_ascii_case("results"))
|
||||
})?;
|
||||
|
||||
let mut iter = results
|
||||
.children_with_tokens()
|
||||
.skip_while(|n| n.kind() == SyntaxKind::AFFILIATED_KEYWORD)
|
||||
.take_while(|n| n.kind() != SyntaxKind::BLANK_LINE);
|
||||
|
||||
let first = iter.next();
|
||||
|
||||
let last = iter.last();
|
||||
|
||||
let start = first.as_ref().map(|n| n.text_range().start())?;
|
||||
|
||||
let end = last.or(first).map(|x| x.text_range().end())?;
|
||||
|
||||
Some((start.into(), end.into()))
|
||||
}
|
||||
133
orgize-common/src/formatting.rs
Normal file
133
orgize-common/src/formatting.rs
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
use orgize::{
|
||||
ast::Rule,
|
||||
export::{Container, Event, TraversalContext, Traverser},
|
||||
rowan::ast::AstNode,
|
||||
Org, SyntaxKind, SyntaxNode,
|
||||
};
|
||||
|
||||
pub fn formatting(org: &Org) -> Vec<(usize, usize, String)> {
|
||||
let mut format = FormattingTraverser { edits: vec![] };
|
||||
|
||||
org.traverse(&mut format);
|
||||
|
||||
format.edits
|
||||
}
|
||||
|
||||
struct FormattingTraverser {
|
||||
edits: Vec<(usize, usize, String)>,
|
||||
}
|
||||
|
||||
impl Traverser for FormattingTraverser {
|
||||
fn event(&mut self, event: Event, _: &mut TraversalContext) {
|
||||
match event {
|
||||
Event::Rule(rule) => {
|
||||
format_rule(&rule, &mut self.edits);
|
||||
format_blank_lines(rule.syntax(), &mut self.edits);
|
||||
}
|
||||
Event::Clock(clock) => {
|
||||
format_blank_lines(clock.syntax(), &mut self.edits);
|
||||
}
|
||||
|
||||
Event::Enter(Container::Document(document)) => {
|
||||
format_blank_lines(document.syntax(), &mut self.edits);
|
||||
}
|
||||
Event::Enter(Container::Paragraph(paragraph)) => {
|
||||
format_blank_lines(paragraph.syntax(), &mut self.edits);
|
||||
}
|
||||
Event::Enter(Container::List(list)) => {
|
||||
format_blank_lines(list.syntax(), &mut self.edits);
|
||||
}
|
||||
Event::Enter(Container::OrgTable(table)) => {
|
||||
format_blank_lines(table.syntax(), &mut self.edits);
|
||||
}
|
||||
Event::Enter(Container::SpecialBlock(block)) => {
|
||||
format_blank_lines(block.syntax(), &mut self.edits);
|
||||
}
|
||||
Event::Enter(Container::QuoteBlock(block)) => {
|
||||
format_blank_lines(block.syntax(), &mut self.edits);
|
||||
}
|
||||
Event::Enter(Container::CenterBlock(block)) => {
|
||||
format_blank_lines(block.syntax(), &mut self.edits);
|
||||
}
|
||||
Event::Enter(Container::VerseBlock(block)) => {
|
||||
format_blank_lines(block.syntax(), &mut self.edits);
|
||||
}
|
||||
Event::Enter(Container::CommentBlock(block)) => {
|
||||
format_blank_lines(block.syntax(), &mut self.edits);
|
||||
}
|
||||
Event::Enter(Container::ExampleBlock(block)) => {
|
||||
format_blank_lines(block.syntax(), &mut self.edits);
|
||||
}
|
||||
Event::Enter(Container::ExportBlock(block)) => {
|
||||
format_blank_lines(block.syntax(), &mut self.edits);
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn format_rule(rule: &Rule, edits: &mut Vec<(usize, usize, String)>) {
|
||||
let node = rule.syntax();
|
||||
|
||||
for token in node.children_with_tokens().filter_map(|e| e.into_token()) {
|
||||
if token.kind() == SyntaxKind::WHITESPACE && !token.text().is_empty() {
|
||||
edits.push((
|
||||
token.text_range().start().into(),
|
||||
token.text_range().end().into(),
|
||||
"".into(),
|
||||
));
|
||||
}
|
||||
|
||||
if token.kind() == SyntaxKind::TEXT && token.text().len() != 5 {
|
||||
edits.push((
|
||||
token.text_range().start().into(),
|
||||
token.text_range().end().into(),
|
||||
"-----".into(),
|
||||
));
|
||||
}
|
||||
|
||||
if token.kind() == SyntaxKind::NEW_LINE && token.text() != "\n" {
|
||||
edits.push((
|
||||
token.text_range().start().into(),
|
||||
token.text_range().end().into(),
|
||||
"\n".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn format_blank_lines(node: &SyntaxNode, edits: &mut Vec<(usize, usize, String)>) {
|
||||
let mut blank_lines = node
|
||||
.children_with_tokens()
|
||||
.filter_map(|e| e.into_token())
|
||||
.filter(|n| n.kind() == SyntaxKind::BLANK_LINE);
|
||||
|
||||
if let Some(line) = blank_lines.next() {
|
||||
if line.text() != "\n" {
|
||||
edits.push((
|
||||
line.text_range().start().into(),
|
||||
line.text_range().end().into(),
|
||||
"\n".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
match (blank_lines.next(), blank_lines.last()) {
|
||||
(Some(first), Some(last)) => {
|
||||
edits.push((
|
||||
first.text_range().start().into(),
|
||||
last.text_range().end().into(),
|
||||
"".into(),
|
||||
));
|
||||
}
|
||||
(Some(first), None) => {
|
||||
edits.push((
|
||||
first.text_range().start().into(),
|
||||
first.text_range().end().into(),
|
||||
"".into(),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
111
orgize-common/src/header_argument.rs
Normal file
111
orgize-common/src/header_argument.rs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
use jetscii::Substring;
|
||||
use nom::{
|
||||
bytes::complete::take_while1,
|
||||
character::complete::{space0, space1},
|
||||
InputTake,
|
||||
};
|
||||
use orgize::{
|
||||
ast::{Headline, Keyword, Token},
|
||||
rowan::ast::AstNode,
|
||||
SyntaxKind, SyntaxNode,
|
||||
};
|
||||
|
||||
pub fn header_argument<'a>(
|
||||
arg1: &'a str,
|
||||
arg2: &'a str,
|
||||
arg3: &'a str,
|
||||
key: &str,
|
||||
default: &'static str,
|
||||
) -> &'a str {
|
||||
extract_header_args(arg1, key)
|
||||
.or_else(|_| extract_header_args(arg2, key))
|
||||
.or_else(|_| extract_header_args(arg3, key))
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
pub fn property_keyword(node: &SyntaxNode) -> Option<Token> {
|
||||
node.ancestors()
|
||||
.find(|n| n.kind() == SyntaxKind::DOCUMENT)
|
||||
.and_then(|n| n.first_child())
|
||||
.filter(|n| n.kind() == SyntaxKind::SECTION)
|
||||
.and_then(|n| {
|
||||
n.children()
|
||||
.filter_map(Keyword::cast)
|
||||
.filter(|kw| kw.key().eq_ignore_ascii_case("PROPERTY"))
|
||||
.map(|kw| kw.value())
|
||||
.find(|value| value.trim_start().starts_with("header-args "))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn property_drawer(node: &SyntaxNode) -> Option<Token> {
|
||||
node.ancestors()
|
||||
.find_map(Headline::cast)
|
||||
.and_then(|hdl| hdl.properties())
|
||||
.and_then(|drawer| drawer.get("header-args"))
|
||||
}
|
||||
|
||||
pub fn extract_header_args<'a>(input: &'a str, key: &str) -> Result<&'a str, nom::Err<()>> {
|
||||
let mut i = input;
|
||||
|
||||
while !i.is_empty() {
|
||||
let (input, _) = space0(i)?;
|
||||
let (input, name) = take_while1(|c| c != ' ' && c != '\t')(input)?;
|
||||
|
||||
if !name.eq_ignore_ascii_case(key) {
|
||||
debug_assert!(input.len() < i.len(), "{} < {}", input.len(), i.len());
|
||||
i = input;
|
||||
continue;
|
||||
}
|
||||
|
||||
let (input, _) = space1(input)?;
|
||||
|
||||
if let Some(idx) = Substring::new(" :")
|
||||
.find(input)
|
||||
.or_else(|| Substring::new("\t:").find(input))
|
||||
{
|
||||
let idx = input[0..idx]
|
||||
.rfind(|c| c != ' ' && c != '\t')
|
||||
.map(|i| i + 1)
|
||||
.unwrap_or(idx);
|
||||
|
||||
let (_, value) = input.take_split(idx);
|
||||
|
||||
return Ok(value.trim());
|
||||
} else {
|
||||
return Ok(input.trim());
|
||||
}
|
||||
}
|
||||
|
||||
Err(nom::Err::Error(()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_header_args() {
|
||||
assert!(extract_header_args("", ":tangle").is_err());
|
||||
assert!(extract_header_args(" :noweb yes", ":tangle1").is_err());
|
||||
assert!(extract_header_args(":tangle", ":tangle").is_err());
|
||||
|
||||
assert_eq!(extract_header_args(":tangle ", ":tangle").unwrap(), "");
|
||||
|
||||
assert_eq!(
|
||||
extract_header_args(":tangle emacs.d/init.el", ":tangle").unwrap(),
|
||||
"emacs.d/init.el"
|
||||
);
|
||||
assert_eq!(
|
||||
extract_header_args(" :tangle emacs.d/init.el", ":tangle").unwrap(),
|
||||
"emacs.d/init.el"
|
||||
);
|
||||
assert_eq!(
|
||||
extract_header_args(" :tangle emacs.d/init.el :noweb yes", ":tangle").unwrap(),
|
||||
"emacs.d/init.el"
|
||||
);
|
||||
assert_eq!(
|
||||
extract_header_args(" :noweb yes :tangle emacs.d/init.el", ":tangle").unwrap(),
|
||||
"emacs.d/init.el"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
extract_header_args(":results output code", ":results").unwrap(),
|
||||
"output code"
|
||||
);
|
||||
}
|
||||
13
orgize-common/src/lib.rs
Normal file
13
orgize-common/src/lib.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
mod detangle;
|
||||
mod execute_src_block;
|
||||
mod formatting;
|
||||
mod header_argument;
|
||||
mod tangle;
|
||||
mod utils;
|
||||
|
||||
pub use detangle::detangle;
|
||||
pub use execute_src_block::execute;
|
||||
pub use formatting::formatting;
|
||||
pub use header_argument::*;
|
||||
pub use tangle::tangle;
|
||||
pub use utils::headline_slug;
|
||||
130
orgize-common/src/tangle.rs
Normal file
130
orgize-common/src/tangle.rs
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
// TODO: :noweb support
|
||||
|
||||
use orgize::{
|
||||
ast::{Headline, SourceBlock},
|
||||
rowan::{ast::AstNode, Direction},
|
||||
SyntaxKind,
|
||||
};
|
||||
use resolve_path::PathResolveExt;
|
||||
use std::fmt::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::{
|
||||
header_argument::{header_argument, property_drawer, property_keyword},
|
||||
utils::language_comments,
|
||||
};
|
||||
|
||||
pub fn tangle(
|
||||
block: SourceBlock,
|
||||
path: &PathBuf,
|
||||
) -> anyhow::Result<Option<(PathBuf, Option<u32>, String, bool)>> {
|
||||
let arg1 = block.parameters().unwrap_or_default();
|
||||
let arg2 = property_drawer(block.syntax()).unwrap_or_default();
|
||||
let arg3 = property_keyword(block.syntax()).unwrap_or_default();
|
||||
let language = block.language().unwrap_or_default();
|
||||
|
||||
let tangle = header_argument(&arg1, &arg2, &arg3, ":tangle", "no");
|
||||
|
||||
if tangle == "no" {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let comments = header_argument(&arg1, &arg2, &arg3, ":comments", "no");
|
||||
let padline = header_argument(&arg1, &arg2, &arg3, ":padline", "no");
|
||||
let shebang = header_argument(&arg1, &arg2, &arg3, ":shebang", "no");
|
||||
let mode = header_argument(
|
||||
&arg1,
|
||||
&arg2,
|
||||
&arg3,
|
||||
":tangle-mode",
|
||||
if shebang == "yea" { "o755" } else { "no" },
|
||||
);
|
||||
let is_mkdir = header_argument(&arg1, &arg2, &arg3, ":mkdir", "no");
|
||||
|
||||
let parent = block
|
||||
.syntax()
|
||||
.ancestors()
|
||||
.find(|n| n.kind() == SyntaxKind::HEADLINE || n.kind() == SyntaxKind::DOCUMENT);
|
||||
|
||||
let nth = parent
|
||||
.as_ref()
|
||||
.and_then(|n| n.children().position(|c| &c == block.syntax()))
|
||||
.unwrap_or(1);
|
||||
|
||||
let headline_title = parent.and_then(Headline::cast).map(|headline| {
|
||||
headline
|
||||
.title()
|
||||
.fold(String::new(), |a, n| a + &n.to_string())
|
||||
});
|
||||
|
||||
let path = tangle.try_resolve_in(path)?.to_path_buf();
|
||||
|
||||
let mut permission = None;
|
||||
let mut content = String::new();
|
||||
|
||||
if mode != "no"
|
||||
&& mode.len() == 4
|
||||
&& mode.starts_with('o')
|
||||
&& mode.bytes().skip(1).all(|b| (b'0'..=b'7').contains(&b))
|
||||
{
|
||||
permission = u32::from_str_radix(&mode[1..], 8).ok();
|
||||
}
|
||||
|
||||
if shebang != "no" && !shebang.is_empty() {
|
||||
content.push_str(shebang);
|
||||
}
|
||||
|
||||
if comments == "org" || comments == "both" {
|
||||
if let Some((begin, end)) = language_comments(&language) {
|
||||
let start = block
|
||||
.syntax()
|
||||
.siblings(Direction::Prev)
|
||||
.skip(1) // skip self
|
||||
.take_while(|n| n.kind() != SyntaxKind::SOURCE_BLOCK)
|
||||
.last();
|
||||
|
||||
for sibling in start
|
||||
.into_iter()
|
||||
.flat_map(|start| start.siblings(Direction::Next))
|
||||
.take_while(|node| node != block.syntax())
|
||||
{
|
||||
for line in sibling.to_string().lines() {
|
||||
if line.is_empty() {
|
||||
let _ = writeln!(content);
|
||||
} else {
|
||||
let _ = writeln!(content, "{begin} {line} {end}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if comments == "yes" || comments == "link" || comments == "noweb" || comments == "both" {
|
||||
if let Some((begin, end)) = language_comments(&language) {
|
||||
let _ = writeln!(
|
||||
content,
|
||||
"{begin} [[file:{path}::*{title}][{title}:{nth}]] {end}",
|
||||
title = headline_title.as_deref().unwrap_or("No heading"),
|
||||
path = path.to_string_lossy(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
content.push_str(&block.value());
|
||||
|
||||
if padline != "no" {
|
||||
let _ = writeln!(content);
|
||||
}
|
||||
|
||||
if comments == "yes" || comments == "link" || comments == "noweb" || comments == "both" {
|
||||
if let Some((begin, end)) = language_comments(&language) {
|
||||
let _ = writeln!(
|
||||
content,
|
||||
"{begin} {title}:{nth} ends here {end}",
|
||||
title = headline_title.as_deref().unwrap_or("No heading"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Some((path, permission, content, is_mkdir != "no")))
|
||||
}
|
||||
35
orgize-common/src/utils.rs
Normal file
35
orgize-common/src/utils.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
use orgize::ast::Headline;
|
||||
|
||||
pub fn language_comments(language: &str) -> Option<(&str, &str)> {
|
||||
match language {
|
||||
"c" | "cpp" | "c++" | "go" | "js" | "javascript" | "ts" | "typescript" | "rust"
|
||||
| "vera" | "jsonc" => Some(("//", "")),
|
||||
"toml" | "tml" | "yaml" | "yml" | "conf" | "gitconfig" | "conf-toml" | "sh" | "shell"
|
||||
| "bash" | "zsh" | "fish" => Some(("#", "")),
|
||||
"lua" | "sql" => Some(("--", "")),
|
||||
"lisp" | "emacs-lisp" | "elisp" => Some((";;", "")),
|
||||
"xml" | "html" | "svg" => Some(("<!--", "-->")),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn language_execute_command(language: &str) -> Option<&str> {
|
||||
match language {
|
||||
"js" | "javascript" => Some("node"),
|
||||
"sh" | "bash" => Some("bash"),
|
||||
"py" | "python" => Some("python"),
|
||||
"fish" => Some("fish"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn headline_slug(headline: &Headline) -> String {
|
||||
headline.title().fold(String::new(), |mut acc, elem| {
|
||||
for ch in elem.to_string().chars() {
|
||||
if ch.is_ascii_graphic() {
|
||||
acc.push(ch);
|
||||
}
|
||||
}
|
||||
acc
|
||||
})
|
||||
}
|
||||
25
orgize-lsp/Cargo.toml
Normal file
25
orgize-lsp/Cargo.toml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
[package]
|
||||
name = "orgize-lsp"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
description = "Language server for Org-mode, powered by orgize."
|
||||
|
||||
[dependencies]
|
||||
orgize = { path = "../orgize" }
|
||||
orgize-common = { path = "../orgize-common" }
|
||||
tokio = { version = "1.17", features = ["full"] }
|
||||
tower-lsp = { version = "0.20", features = ["proposed"] }
|
||||
dashmap = "5.1"
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
anyhow = "1.0"
|
||||
tempfile = "3.8"
|
||||
resolve-path = "0.1"
|
||||
memchr = "2.6"
|
||||
|
||||
[[bin]]
|
||||
name = "orgize-lsp"
|
||||
path = "src/main.rs"
|
||||
50
orgize-lsp/README.md
Normal file
50
orgize-lsp/README.md
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# `orgize-lsp`
|
||||
|
||||
Language server for org-mode, powered by orgize.
|
||||
|
||||
## Install
|
||||
|
||||
### Server
|
||||
|
||||
```sh
|
||||
$ cargo install --path .
|
||||
```
|
||||
|
||||
### Client (vscode)
|
||||
|
||||
```sh
|
||||
$ pnpm run -C editors/vscode package --no-dependencies
|
||||
$ code --install-extension ./editors/vscode/orgize-lsp.vsix --force
|
||||
```
|
||||
|
||||
## Supported features
|
||||
|
||||
1. Folding range
|
||||
|
||||
- Fold headline, list, table, blocks
|
||||
|
||||
2. Document symbols
|
||||
|
||||
- Headings
|
||||
|
||||
3. Formatting
|
||||
|
||||
4. Document link
|
||||
|
||||
- File links
|
||||
|
||||
- Source block `:tangle` arguments
|
||||
|
||||
- Internal links
|
||||
|
||||
5. Code lens
|
||||
|
||||
- Generate toc heading
|
||||
|
||||
- Tangle/detanlge source block
|
||||
|
||||
- Evaluate source block
|
||||
|
||||
6. Commands
|
||||
|
||||
- Show syntax tree
|
||||
3
orgize-lsp/editors/vscode/.gitignore
vendored
Normal file
3
orgize-lsp/editors/vscode/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
dist
|
||||
*.vsix
|
||||
node_modules
|
||||
6
orgize-lsp/editors/vscode/.vscodeignore
Normal file
6
orgize-lsp/editors/vscode/.vscodeignore
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
**
|
||||
!dist/
|
||||
!syntaxes/
|
||||
!package.json
|
||||
!org.configuration.json
|
||||
!README.md
|
||||
1
orgize-lsp/editors/vscode/LICENSE
Symbolic link
1
orgize-lsp/editors/vscode/LICENSE
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../LICENSE
|
||||
13
orgize-lsp/editors/vscode/build.mjs
Normal file
13
orgize-lsp/editors/vscode/build.mjs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import * as esbuild from "esbuild";
|
||||
|
||||
await esbuild.build({
|
||||
bundle: true,
|
||||
entryPoints: ["src/main.ts"],
|
||||
external: ["vscode"],
|
||||
outfile: "dist/main.js",
|
||||
format: "cjs",
|
||||
platform: "node",
|
||||
target: "node16",
|
||||
minify: true,
|
||||
treeShaking: true,
|
||||
});
|
||||
29
orgize-lsp/editors/vscode/org.configuration.json
Normal file
29
orgize-lsp/editors/vscode/org.configuration.json
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"comments": {
|
||||
"lineComment": "#",
|
||||
"blockComment": ["#+BEGIN_COMMENT\n", "\n#+END_COMMENT"]
|
||||
},
|
||||
"brackets": [
|
||||
["{", "}"],
|
||||
["[", "]"],
|
||||
["(", ")"]
|
||||
],
|
||||
"autoClosingPairs": [
|
||||
["{", "}"],
|
||||
["[", "]"],
|
||||
["(", ")"],
|
||||
["\"", "\""]
|
||||
],
|
||||
"surroundingPairs": [
|
||||
["{", "}"],
|
||||
["[", "]"],
|
||||
["(", ")"],
|
||||
["\"", "\""],
|
||||
["'", "'"],
|
||||
["*", "*"],
|
||||
["/", "/"],
|
||||
["_", "_"],
|
||||
["=", "="],
|
||||
["~", "~"]
|
||||
]
|
||||
}
|
||||
111
orgize-lsp/editors/vscode/package.json
Normal file
111
orgize-lsp/editors/vscode/package.json
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
{
|
||||
"name": "orgize-lsp",
|
||||
"private": true,
|
||||
"version": "0.0.0-dev",
|
||||
"engines": {
|
||||
"vscode": "^1.75.0"
|
||||
},
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PoiScript/orgize"
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "node build.mjs",
|
||||
"package": "vsce package -o orgize-lsp.vsix --skip-license --no-dependencies",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"typecheck": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"vscode-languageclient": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "~16.11.68",
|
||||
"@types/vscode": "~1.75.1",
|
||||
"@vscode/test-electron": "^2.3.8",
|
||||
"@vscode/vsce": "^2.22.0",
|
||||
"esbuild": "^0.18.20",
|
||||
"ovsx": "^0.8.3",
|
||||
"prettier": "^3.1.1",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"main": "./dist/main",
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "orgize.syntax-tree",
|
||||
"title": "(Debug) Show org syntax tree"
|
||||
}
|
||||
],
|
||||
"languages": [
|
||||
{
|
||||
"id": "org",
|
||||
"aliases": [
|
||||
"Org",
|
||||
"Org Markup",
|
||||
"Org Mode"
|
||||
],
|
||||
"extensions": [
|
||||
".org"
|
||||
],
|
||||
"configuration": "./org.configuration.json"
|
||||
}
|
||||
],
|
||||
"grammars": [
|
||||
{
|
||||
"language": "org",
|
||||
"scopeName": "source.org",
|
||||
"path": "./syntaxes/org.tmLanguage.json",
|
||||
"embeddedLanguages": {
|
||||
"meta.embedded.block.html": "html",
|
||||
"source.js": "javascript",
|
||||
"source.css": "css",
|
||||
"meta.embedded.block.frontmatter": "yaml",
|
||||
"meta.embedded.block.css": "css",
|
||||
"meta.embedded.block.ini": "ini",
|
||||
"meta.embedded.block.java": "java",
|
||||
"meta.embedded.block.lua": "lua",
|
||||
"meta.embedded.block.makefile": "makefile",
|
||||
"meta.embedded.block.perl": "perl",
|
||||
"meta.embedded.block.r": "r",
|
||||
"meta.embedded.block.ruby": "ruby",
|
||||
"meta.embedded.block.php": "php",
|
||||
"meta.embedded.block.sql": "sql",
|
||||
"meta.embedded.block.vs_net": "vs_net",
|
||||
"meta.embedded.block.xml": "xml",
|
||||
"meta.embedded.block.xsl": "xsl",
|
||||
"meta.embedded.block.yaml": "yaml",
|
||||
"meta.embedded.block.dosbatch": "dosbatch",
|
||||
"meta.embedded.block.clojure": "clojure",
|
||||
"meta.embedded.block.coffee": "coffee",
|
||||
"meta.embedded.block.c": "c",
|
||||
"meta.embedded.block.cpp": "cpp",
|
||||
"meta.embedded.block.diff": "diff",
|
||||
"meta.embedded.block.dockerfile": "dockerfile",
|
||||
"meta.embedded.block.go": "go",
|
||||
"meta.embedded.block.groovy": "groovy",
|
||||
"meta.embedded.block.pug": "jade",
|
||||
"meta.embedded.block.javascript": "javascript",
|
||||
"meta.embedded.block.json": "json",
|
||||
"meta.embedded.block.jsonc": "jsonc",
|
||||
"meta.embedded.block.latex": "latex",
|
||||
"meta.embedded.block.less": "less",
|
||||
"meta.embedded.block.objc": "objc",
|
||||
"meta.embedded.block.scss": "scss",
|
||||
"meta.embedded.block.perl6": "perl6",
|
||||
"meta.embedded.block.powershell": "powershell",
|
||||
"meta.embedded.block.python": "python",
|
||||
"meta.embedded.block.rust": "rust",
|
||||
"meta.embedded.block.scala": "scala",
|
||||
"meta.embedded.block.shellscript": "shellscript",
|
||||
"meta.embedded.block.typescript": "typescript",
|
||||
"meta.embedded.block.typescriptreact": "typescriptreact",
|
||||
"meta.embedded.block.csharp": "csharp",
|
||||
"meta.embedded.block.fsharp": "fsharp"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
1268
orgize-lsp/editors/vscode/pnpm-lock.yaml
generated
Normal file
1268
orgize-lsp/editors/vscode/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
53
orgize-lsp/editors/vscode/src/main.ts
Normal file
53
orgize-lsp/editors/vscode/src/main.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { ExtensionContext } from "vscode";
|
||||
|
||||
import {
|
||||
Executable,
|
||||
LanguageClient,
|
||||
LanguageClientOptions,
|
||||
ServerOptions,
|
||||
} from "vscode-languageclient/node";
|
||||
|
||||
import SyntaxTreeProvider from "./syntax-tree";
|
||||
import PreviewHtmlProvider from "./preview-html";
|
||||
|
||||
export let client: LanguageClient;
|
||||
|
||||
export function activate(context: ExtensionContext) {
|
||||
// If the extension is launched in debug mode then the debug server options are used
|
||||
// Otherwise the run options are used
|
||||
const run: Executable = {
|
||||
command: "orgize-lsp",
|
||||
};
|
||||
|
||||
const serverOptions: ServerOptions = {
|
||||
run,
|
||||
debug: run,
|
||||
};
|
||||
|
||||
// Options to control the language client
|
||||
const clientOptions: LanguageClientOptions = {
|
||||
// Register the server for plain text documents
|
||||
documentSelector: [{ scheme: "file", language: "org" }],
|
||||
};
|
||||
|
||||
// Create the language client and start the client.
|
||||
client = new LanguageClient(
|
||||
"orgize-lsp",
|
||||
"Orgize LSP",
|
||||
serverOptions,
|
||||
clientOptions
|
||||
);
|
||||
|
||||
// Start the client. This will also launch the server
|
||||
client.start();
|
||||
|
||||
context.subscriptions.push(SyntaxTreeProvider.register());
|
||||
context.subscriptions.push(PreviewHtmlProvider.register());
|
||||
}
|
||||
|
||||
export function deactivate(): Thenable<void> | undefined {
|
||||
if (!client) {
|
||||
return undefined;
|
||||
}
|
||||
return client.stop();
|
||||
}
|
||||
234
orgize-lsp/editors/vscode/src/preview-html.ts
Normal file
234
orgize-lsp/editors/vscode/src/preview-html.ts
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
import {
|
||||
Disposable,
|
||||
ExtensionContext,
|
||||
TextDocumentContentProvider,
|
||||
Uri,
|
||||
ViewColumn,
|
||||
Webview,
|
||||
WebviewOptions,
|
||||
WebviewPanel,
|
||||
commands,
|
||||
window,
|
||||
workspace,
|
||||
} from "vscode";
|
||||
|
||||
import { client } from "./main";
|
||||
|
||||
export const register = (context: ExtensionContext) => {
|
||||
const provider = new PreviewHtmlProvider();
|
||||
|
||||
context.subscriptions.push(
|
||||
workspace.registerTextDocumentContentProvider(
|
||||
"orgize-lsp-preview",
|
||||
provider
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default class PreviewHtmlProvider
|
||||
implements TextDocumentContentProvider
|
||||
{
|
||||
static readonly scheme = "orgize-preview-html";
|
||||
|
||||
static register(): Disposable {
|
||||
const provider = new PreviewHtmlProvider();
|
||||
|
||||
// register content provider for scheme `references`
|
||||
// register document link provider for scheme `references`
|
||||
const providerRegistrations = workspace.registerTextDocumentContentProvider(
|
||||
PreviewHtmlProvider.scheme,
|
||||
provider
|
||||
);
|
||||
|
||||
// register command that crafts an uri with the `references` scheme,
|
||||
// open the dynamic document, and shows it in the next editor
|
||||
const commandRegistration = commands.registerTextEditorCommand(
|
||||
"orgize.preview-html",
|
||||
(editor) => {
|
||||
return workspace
|
||||
.openTextDocument(encode(editor.document.uri))
|
||||
.then((doc) => window.showTextDocument(doc, editor.viewColumn! + 1));
|
||||
}
|
||||
);
|
||||
|
||||
return Disposable.from(
|
||||
provider,
|
||||
commandRegistration,
|
||||
providerRegistrations
|
||||
);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
// this._subscriptions.dispose();
|
||||
// this._documents.clear();
|
||||
// this._editorDecoration.dispose();
|
||||
// this._onDidChange.dispose();
|
||||
}
|
||||
|
||||
async provideTextDocumentContent(uri: Uri): Promise<string> {
|
||||
if (!client) {
|
||||
return "LSP server is not ready...";
|
||||
}
|
||||
|
||||
return client.sendRequest("workspace/executeCommand", {
|
||||
command: "orgize.syntax-tree",
|
||||
arguments: [uri.toString()],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class PreviewHtmlPanel {
|
||||
/**
|
||||
* Track the currently panel. Only allow a single panel to exist at a time.
|
||||
*/
|
||||
public static currentPanel: PreviewHtmlPanel | undefined;
|
||||
|
||||
public static readonly viewType = "orgizePreviewHtml";
|
||||
|
||||
private readonly _panel: WebviewPanel;
|
||||
private readonly _extensionUri: Uri;
|
||||
private _disposables: Disposable[] = [];
|
||||
|
||||
public static createOrShow(uri: Uri) {
|
||||
const column = window.activeTextEditor
|
||||
? window.activeTextEditor.viewColumn
|
||||
: undefined;
|
||||
|
||||
// If we already have a panel, show it.
|
||||
if (PreviewHtmlPanel.currentPanel) {
|
||||
PreviewHtmlPanel.currentPanel._panel.reveal(column);
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, create a new panel.
|
||||
const panel = window.createWebviewPanel(
|
||||
PreviewHtmlPanel.viewType,
|
||||
"Preview of " + uri.fsPath,
|
||||
column || ViewColumn.One,
|
||||
getWebviewOptions(uri)
|
||||
);
|
||||
|
||||
PreviewHtmlPanel.currentPanel = new PreviewHtmlPanel(panel, uri);
|
||||
}
|
||||
|
||||
public static revive(panel: WebviewPanel, extensionUri: Uri) {
|
||||
PreviewHtmlPanel.currentPanel = new PreviewHtmlPanel(panel, extensionUri);
|
||||
}
|
||||
|
||||
private constructor(panel: WebviewPanel, extensionUri: Uri) {
|
||||
this._panel = panel;
|
||||
this._extensionUri = extensionUri;
|
||||
|
||||
// Set the webview's initial html content
|
||||
this._update();
|
||||
|
||||
// Listen for when the panel is disposed
|
||||
// This happens when the user closes the panel or when the panel is closed programmatically
|
||||
this._panel.onDidDispose(
|
||||
() => {
|
||||
this.dispose();
|
||||
},
|
||||
null,
|
||||
this._disposables
|
||||
);
|
||||
|
||||
// Update the content based on view changes
|
||||
this._panel.onDidChangeViewState(
|
||||
(e) => {
|
||||
if (this._panel.visible) {
|
||||
this._update();
|
||||
}
|
||||
},
|
||||
null,
|
||||
this._disposables
|
||||
);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
PreviewHtmlPanel.currentPanel = undefined;
|
||||
|
||||
// Clean up our resources
|
||||
this._panel.dispose();
|
||||
|
||||
while (this._disposables.length) {
|
||||
const x = this._disposables.pop();
|
||||
if (x) {
|
||||
x.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _update() {
|
||||
const webview = this._panel.webview;
|
||||
this._panel.webview.html = this._getHtmlForWebview(webview);
|
||||
}
|
||||
|
||||
private _getHtmlForWebview(webview: Webview): string {
|
||||
// // Local path to main script run in the webview
|
||||
// const scriptPathOnDisk = Uri.joinPath(
|
||||
// this._extensionUri,
|
||||
// "media",
|
||||
// "main.js"
|
||||
// );
|
||||
|
||||
// // And the uri we use to load this script in the webview
|
||||
// const scriptUri = webview.asWebviewUri(scriptPathOnDisk);
|
||||
|
||||
// // Local path to css styles
|
||||
// const styleResetPath = Uri.joinPath(
|
||||
// this._extensionUri,
|
||||
// "media",
|
||||
// "reset.css"
|
||||
// );
|
||||
|
||||
// const stylesPathMainPath = Uri.joinPath(
|
||||
// this._extensionUri,
|
||||
// "media",
|
||||
// " css"
|
||||
// );
|
||||
|
||||
// // Uri to load styles into webview
|
||||
// const stylesResetUri = webview.asWebviewUri(styleResetPath);
|
||||
// const stylesMainUri = webview.asWebviewUri(stylesPathMainPath);
|
||||
|
||||
// Use a nonce to only allow specific scripts to be run
|
||||
// const nonce = getNonce();
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>Cat Coding</title>
|
||||
</head>
|
||||
<body>
|
||||
<img width="300" />
|
||||
<h1 id="lines-of-code-counter">0</h1>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
}
|
||||
|
||||
const getWebviewOptions = (extensionUri: Uri): WebviewOptions => {
|
||||
return {
|
||||
// Enable javascript in the webview
|
||||
enableScripts: true,
|
||||
|
||||
// And restrict the webview to only loading content from our extension's `media` directory.
|
||||
localResourceRoots: [Uri.joinPath(extensionUri, "media")],
|
||||
};
|
||||
};
|
||||
|
||||
const encode = (uri: Uri): Uri => {
|
||||
return uri.with({
|
||||
scheme: PreviewHtmlProvider.scheme,
|
||||
query: uri.path,
|
||||
path: "tree.syntax",
|
||||
});
|
||||
};
|
||||
|
||||
const decode = (uri: Uri): Uri => {
|
||||
return uri.with({ scheme: "file", path: uri.query, query: "" });
|
||||
};
|
||||
77
orgize-lsp/editors/vscode/src/syntax-tree.ts
Normal file
77
orgize-lsp/editors/vscode/src/syntax-tree.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import {
|
||||
Disposable,
|
||||
TextDocumentContentProvider,
|
||||
Uri,
|
||||
commands,
|
||||
window,
|
||||
workspace,
|
||||
} from "vscode";
|
||||
import { client } from "./main";
|
||||
|
||||
export default class SyntaxTreeProvider implements TextDocumentContentProvider {
|
||||
static readonly scheme = "orgize-syntax-tree";
|
||||
|
||||
static register(): Disposable {
|
||||
const provider = new SyntaxTreeProvider();
|
||||
|
||||
// register content provider for scheme `references`
|
||||
// register document link provider for scheme `references`
|
||||
const providerRegistrations = workspace.registerTextDocumentContentProvider(
|
||||
SyntaxTreeProvider.scheme,
|
||||
provider
|
||||
);
|
||||
|
||||
// register command that crafts an uri with the `references` scheme,
|
||||
// open the dynamic document, and shows it in the next editor
|
||||
const commandRegistration = commands.registerTextEditorCommand(
|
||||
"orgize.syntax-tree",
|
||||
(editor) => {
|
||||
return workspace
|
||||
.openTextDocument(encode(editor.document.uri))
|
||||
.then((doc) => window.showTextDocument(doc, editor.viewColumn! + 1));
|
||||
}
|
||||
);
|
||||
|
||||
return Disposable.from(
|
||||
provider,
|
||||
commandRegistration,
|
||||
providerRegistrations
|
||||
);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
// this._subscriptions.dispose();
|
||||
// this._documents.clear();
|
||||
// this._editorDecoration.dispose();
|
||||
// this._onDidChange.dispose();
|
||||
}
|
||||
|
||||
async provideTextDocumentContent(uri: Uri): Promise<string> {
|
||||
if (!client) {
|
||||
return "LSP server is not ready...";
|
||||
}
|
||||
|
||||
const result = await client.sendRequest("workspace/executeCommand", {
|
||||
command: "orgize.syntax-tree",
|
||||
arguments: [decode(uri).toString()],
|
||||
});
|
||||
|
||||
if (typeof result === "string") {
|
||||
return result;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
const encode = (uri: Uri): Uri => {
|
||||
return uri.with({
|
||||
scheme: SyntaxTreeProvider.scheme,
|
||||
query: uri.path,
|
||||
path: "tree.syntax",
|
||||
});
|
||||
};
|
||||
|
||||
const decode = (uri: Uri): Uri => {
|
||||
return uri.with({ scheme: "file", path: uri.query, query: "" });
|
||||
};
|
||||
2060
orgize-lsp/editors/vscode/syntaxes/org.tmLanguage.json
Normal file
2060
orgize-lsp/editors/vscode/syntaxes/org.tmLanguage.json
Normal file
File diff suppressed because it is too large
Load diff
12
orgize-lsp/editors/vscode/tsconfig.json
Normal file
12
orgize-lsp/editors/vscode/tsconfig.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es2019",
|
||||
"lib": ["ES2019"],
|
||||
"outDir": "../dist",
|
||||
"rootDir": "src",
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", ".vscode-test"]
|
||||
}
|
||||
8
orgize-lsp/justfile
Normal file
8
orgize-lsp/justfile
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
a: c s
|
||||
|
||||
c:
|
||||
pnpm run -C client-vscode package --no-dependencies
|
||||
pnpm run -C client-vscode install --no-dependencies
|
||||
|
||||
s:
|
||||
cargo install --path ./server --offline
|
||||
104
orgize-lsp/src/code_lens.rs
Normal file
104
orgize-lsp/src/code_lens.rs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
use orgize::{
|
||||
export::{Container, Event, TraversalContext, Traverser},
|
||||
rowan::ast::AstNode,
|
||||
};
|
||||
use orgize_common::{header_argument, property_drawer, property_keyword};
|
||||
use tower_lsp::lsp_types::{CodeLens, Url};
|
||||
|
||||
use crate::org_document::OrgDocument;
|
||||
|
||||
use super::OrgizeCommand;
|
||||
|
||||
pub struct CodeLensTraverser<'a> {
|
||||
pub url: Url,
|
||||
pub doc: &'a OrgDocument,
|
||||
pub lens: Vec<CodeLens>,
|
||||
}
|
||||
|
||||
impl<'a> Traverser for CodeLensTraverser<'a> {
|
||||
fn event(&mut self, event: Event, ctx: &mut TraversalContext) {
|
||||
match event {
|
||||
Event::Enter(Container::SourceBlock(block)) => {
|
||||
let start = block.begin();
|
||||
|
||||
let arg1 = block.parameters().unwrap_or_default();
|
||||
let arg2 = property_drawer(block.syntax()).unwrap_or_default();
|
||||
let arg3 = property_keyword(block.syntax()).unwrap_or_default();
|
||||
|
||||
let range = self.doc.range_of(start, start);
|
||||
|
||||
let tangle = header_argument(&arg1, &arg2, &arg3, ":tangle", "no");
|
||||
|
||||
if header_argument(&arg1, &arg2, &arg3, ":results", "no") != "no" {
|
||||
self.lens.push(CodeLens {
|
||||
range,
|
||||
command: Some(
|
||||
OrgizeCommand::SrcBlockExecute {
|
||||
block_offset: start,
|
||||
url: self.url.clone(),
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
data: None,
|
||||
});
|
||||
}
|
||||
|
||||
if tangle != "no" {
|
||||
self.lens.push(CodeLens {
|
||||
range,
|
||||
command: Some(
|
||||
OrgizeCommand::SrcBlockTangle {
|
||||
block_offset: start,
|
||||
url: self.url.clone(),
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
data: None,
|
||||
});
|
||||
|
||||
self.lens.push(CodeLens {
|
||||
range,
|
||||
command: Some(
|
||||
OrgizeCommand::SrcBlockDetangle {
|
||||
block_offset: start,
|
||||
url: self.url.clone(),
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
data: None,
|
||||
});
|
||||
}
|
||||
|
||||
ctx.skip();
|
||||
}
|
||||
Event::Enter(Container::Headline(headline)) => {
|
||||
if headline.tags().any(|t| t.eq_ignore_ascii_case("TOC")) {
|
||||
let start = headline.begin();
|
||||
|
||||
self.lens.push(CodeLens {
|
||||
range: self.doc.range_of(start, start),
|
||||
command: Some(
|
||||
OrgizeCommand::HeadlineToc {
|
||||
heading_offset: start,
|
||||
url: self.url.clone(),
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
data: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> CodeLensTraverser<'a> {
|
||||
pub fn new(url: Url, doc: &'a OrgDocument) -> Self {
|
||||
CodeLensTraverser {
|
||||
url,
|
||||
lens: vec![],
|
||||
doc,
|
||||
}
|
||||
}
|
||||
}
|
||||
101
orgize-lsp/src/commands/headline_toc.rs
Normal file
101
orgize-lsp/src/commands/headline_toc.rs
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
use orgize::{
|
||||
export::{Container, Event, TraversalContext, Traverser},
|
||||
rowan::ast::AstNode,
|
||||
SyntaxKind,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Write;
|
||||
use tower_lsp::lsp_types::{TextEdit, Url, WorkspaceEdit};
|
||||
|
||||
use crate::Backend;
|
||||
|
||||
impl Backend {
|
||||
pub async fn headline_toc(&self, url: Url, headline_offset: u32) {
|
||||
let uri = url.to_string();
|
||||
|
||||
let Some(doc) = self.documents.get(&uri) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut toc = Toc {
|
||||
indent: 0,
|
||||
output: String::new(),
|
||||
|
||||
headline_offset,
|
||||
edit_range: None,
|
||||
};
|
||||
|
||||
doc.traverse(&mut toc);
|
||||
|
||||
if let Some((start, end)) = toc.edit_range {
|
||||
let mut changes = HashMap::new();
|
||||
|
||||
let range = doc.range_of(start, end);
|
||||
|
||||
changes.insert(
|
||||
url,
|
||||
vec![TextEdit {
|
||||
new_text: toc.output,
|
||||
range,
|
||||
}],
|
||||
);
|
||||
|
||||
let _ = self
|
||||
.client
|
||||
.apply_edit(WorkspaceEdit {
|
||||
changes: Some(changes),
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Toc {
|
||||
output: String,
|
||||
indent: usize,
|
||||
|
||||
headline_offset: u32,
|
||||
|
||||
edit_range: Option<(u32, u32)>,
|
||||
}
|
||||
|
||||
impl Traverser for Toc {
|
||||
fn event(&mut self, event: Event, ctx: &mut TraversalContext) {
|
||||
match event {
|
||||
Event::Enter(Container::Headline(headline)) => {
|
||||
if headline.begin() == self.headline_offset {
|
||||
let start = headline
|
||||
.syntax()
|
||||
.children_with_tokens()
|
||||
.find(|n| n.kind() == SyntaxKind::NEW_LINE)
|
||||
.map(|n| n.text_range().end().into())
|
||||
.unwrap_or(headline.end());
|
||||
|
||||
let end = headline.end();
|
||||
|
||||
self.edit_range = Some((start, end));
|
||||
} else {
|
||||
let title = headline.title().map(|e| e.to_string()).collect::<String>();
|
||||
|
||||
let slug = orgize_common::headline_slug(&headline);
|
||||
|
||||
let _ = writeln!(
|
||||
&mut self.output,
|
||||
"{: >i$}- [[#{slug}][{title}]]",
|
||||
"",
|
||||
i = self.indent
|
||||
);
|
||||
}
|
||||
|
||||
self.indent += 2;
|
||||
}
|
||||
Event::Leave(Container::Headline(_)) => self.indent -= 2,
|
||||
|
||||
Event::Enter(Container::Section(_)) => ctx.skip(),
|
||||
Event::Enter(Container::Document(_)) => self.output += "#+begin_quote\n",
|
||||
Event::Leave(Container::Document(_)) => self.output += "#+end_quote\n\n",
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
116
orgize-lsp/src/commands/mod.rs
Normal file
116
orgize-lsp/src/commands/mod.rs
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
mod headline_toc;
|
||||
mod src_block_detangle;
|
||||
mod src_block_execute;
|
||||
mod src_block_tangle;
|
||||
|
||||
use orgize::rowan::ast::AstNode;
|
||||
use serde_json::{json, Value};
|
||||
use tower_lsp::lsp_types::{Command, ExecuteCommandParams, MessageType, Url};
|
||||
|
||||
use crate::Backend;
|
||||
|
||||
pub enum OrgizeCommand {
|
||||
SrcBlockExecute { url: Url, block_offset: u32 },
|
||||
|
||||
SrcBlockTangle { url: Url, block_offset: u32 },
|
||||
|
||||
SrcBlockDetangle { url: Url, block_offset: u32 },
|
||||
|
||||
HeadlineToc { url: Url, heading_offset: u32 },
|
||||
}
|
||||
|
||||
impl From<OrgizeCommand> for Command {
|
||||
fn from(val: OrgizeCommand) -> Self {
|
||||
match val {
|
||||
OrgizeCommand::SrcBlockExecute { url, block_offset } => Command {
|
||||
command: "orgize.src-block.execute".into(),
|
||||
arguments: Some(vec![json!(url), json!(block_offset)]),
|
||||
title: "Execute".into(),
|
||||
},
|
||||
OrgizeCommand::SrcBlockTangle { url, block_offset } => Command {
|
||||
command: "orgize.src-block.tangle".into(),
|
||||
arguments: Some(vec![json!(url), json!(block_offset)]),
|
||||
title: "Tangle".into(),
|
||||
},
|
||||
OrgizeCommand::SrcBlockDetangle { url, block_offset } => Command {
|
||||
command: "orgize.src-block.detangle".into(),
|
||||
arguments: Some(vec![json!(url), json!(block_offset)]),
|
||||
title: "Detangle".into(),
|
||||
},
|
||||
OrgizeCommand::HeadlineToc {
|
||||
url,
|
||||
heading_offset,
|
||||
} => Command {
|
||||
command: "orgize.headline.toc".into(),
|
||||
arguments: Some(vec![json!(url), json!(heading_offset)]),
|
||||
title: "Generate TOC".into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OrgizeCommand {
|
||||
pub fn all() -> Vec<String> {
|
||||
vec![
|
||||
"orgize.src-block.execute".into(),
|
||||
"orgize.src-block.tangle".into(),
|
||||
"orgize.src-block.detangle".into(),
|
||||
"orgize.src-block.open-tangle-dest".into(),
|
||||
"orgize.headline.toc".into(),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn execute(params: &ExecuteCommandParams, backend: &Backend) -> Option<Value> {
|
||||
let result = match (
|
||||
params.command.as_str(),
|
||||
params.arguments.get(0).and_then(|x| x.as_str()),
|
||||
params.arguments.get(1).and_then(|x| x.as_u64()),
|
||||
) {
|
||||
("orgize.src-block.execute", Some(s), Some(n)) => backend
|
||||
.src_block_execute(s.parse().ok()?, n as u32)
|
||||
.await
|
||||
.map(|_| None),
|
||||
("orgize.src-block.tangle", Some(s), Some(n)) => backend
|
||||
.src_block_tangle(s.parse().ok()?, n as u32)
|
||||
.await
|
||||
.map(|_| None),
|
||||
("orgize.src-block.detangle", Some(s), Some(n)) => backend
|
||||
.src_block_detangle(s.parse().ok()?, n as u32)
|
||||
.await
|
||||
.map(|_| None),
|
||||
("orgize.headline.toc", Some(s), Some(n)) => {
|
||||
backend.headline_toc(s.parse().ok()?, n as u32).await;
|
||||
Ok(None)
|
||||
}
|
||||
("orgize.syntax-tree", Some(s), _) => {
|
||||
if let Some(doc) = backend.documents.get(s) {
|
||||
Ok(Some(json!(format!("{:#?}", doc.org.document().syntax()))))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
("orgize.preview-html", Some(s), _) => {
|
||||
if let Some(doc) = backend.documents.get(s) {
|
||||
Ok(Some(json!(format!("{}", doc.org.to_html()))))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
_ => Ok(None),
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
backend
|
||||
.client
|
||||
.show_message(
|
||||
MessageType::ERROR,
|
||||
format!("Failed to execute {:?}: {}", params.command, err),
|
||||
)
|
||||
.await;
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
53
orgize-lsp/src/commands/src_block_detangle.rs
Normal file
53
orgize-lsp/src/commands/src_block_detangle.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use orgize::{ast::SourceBlock, rowan::ast::AstNode};
|
||||
use tower_lsp::lsp_types::{MessageType, TextEdit, Url, WorkspaceEdit};
|
||||
|
||||
use crate::Backend;
|
||||
|
||||
impl Backend {
|
||||
pub async fn src_block_detangle(&self, url: Url, block_offset: u32) -> anyhow::Result<()> {
|
||||
let uri = url.to_string();
|
||||
|
||||
let Some(doc) = self.documents.get(&uri) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some(block) = doc
|
||||
.org
|
||||
.document()
|
||||
.syntax()
|
||||
.descendants()
|
||||
.filter_map(SourceBlock::cast)
|
||||
.find(|n| n.begin() == block_offset)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Ok(file_path) = url.to_file_path() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if let Some((start, end, new_text)) = orgize_common::detangle(block, &file_path)? {
|
||||
let mut changes = HashMap::new();
|
||||
|
||||
let range = doc.range_of(start as u32, end as u32);
|
||||
|
||||
changes.insert(url, vec![TextEdit { new_text, range }]);
|
||||
|
||||
let _ = self
|
||||
.client
|
||||
.apply_edit(WorkspaceEdit {
|
||||
changes: Some(changes),
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
} else {
|
||||
self.client
|
||||
.show_message(MessageType::WARNING, "Code block can't be detangled.")
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
51
orgize-lsp/src/commands/src_block_execute.rs
Normal file
51
orgize-lsp/src/commands/src_block_execute.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use orgize::{ast::SourceBlock, rowan::ast::AstNode};
|
||||
use tower_lsp::lsp_types::{MessageType, TextEdit, Url, WorkspaceEdit};
|
||||
|
||||
use crate::Backend;
|
||||
|
||||
impl Backend {
|
||||
pub async fn src_block_execute(&self, url: Url, block_offset: u32) -> anyhow::Result<()> {
|
||||
let uri = url.to_string();
|
||||
|
||||
let Some(doc) = self.documents.get(&uri) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some(block) = doc
|
||||
.org
|
||||
.document()
|
||||
.syntax()
|
||||
.descendants()
|
||||
.filter_map(SourceBlock::cast)
|
||||
.find(|n| n.begin() == block_offset)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
if let Some((start, end, new_text)) = orgize_common::execute(block, dir.path())? {
|
||||
let mut changes = HashMap::new();
|
||||
|
||||
let range = doc.range_of(start as u32, end as u32);
|
||||
|
||||
changes.insert(url, vec![TextEdit { new_text, range }]);
|
||||
|
||||
let _ = self
|
||||
.client
|
||||
.apply_edit(WorkspaceEdit {
|
||||
changes: Some(changes),
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
} else {
|
||||
self.client
|
||||
.show_message(MessageType::WARNING, "Code block can't be executed.")
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
49
orgize-lsp/src/commands/src_block_tangle.rs
Normal file
49
orgize-lsp/src/commands/src_block_tangle.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
use orgize::{ast::SourceBlock, rowan::ast::AstNode};
|
||||
use std::fs;
|
||||
use tower_lsp::lsp_types::{MessageType, Url};
|
||||
|
||||
use crate::Backend;
|
||||
|
||||
impl Backend {
|
||||
pub async fn src_block_tangle(&self, url: Url, block_offset: u32) -> anyhow::Result<()> {
|
||||
let uri = url.to_string();
|
||||
|
||||
let Some(doc) = self.documents.get(&uri) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some(block) = doc
|
||||
.org
|
||||
.document()
|
||||
.syntax()
|
||||
.descendants()
|
||||
.filter_map(SourceBlock::cast)
|
||||
.find(|n| n.begin() == block_offset)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Ok(file_path) = url.to_file_path() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if let Some((dest, _permission, contents, _mkdir)) =
|
||||
orgize_common::tangle(block, &file_path)?
|
||||
{
|
||||
fs::write(&dest, contents)?;
|
||||
|
||||
self.client
|
||||
.show_message(
|
||||
MessageType::INFO,
|
||||
format!("Wrote to {}", dest.to_string_lossy()),
|
||||
)
|
||||
.await;
|
||||
} else {
|
||||
self.client
|
||||
.show_message(MessageType::WARNING, "Code block can't be tangled.")
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
129
orgize-lsp/src/document_link.rs
Normal file
129
orgize-lsp/src/document_link.rs
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
use orgize::{
|
||||
ast::{Link, SourceBlock},
|
||||
export::{Container, Event, TraversalContext, Traverser},
|
||||
rowan::ast::{support, AstNode},
|
||||
SyntaxKind,
|
||||
};
|
||||
use orgize_common::header_argument;
|
||||
use resolve_path::PathResolveExt;
|
||||
use serde_json::json;
|
||||
use std::path::PathBuf;
|
||||
use tower_lsp::lsp_types::{DocumentLink, Url};
|
||||
|
||||
use crate::org_document::OrgDocument;
|
||||
|
||||
pub struct DocumentLinkTraverser<'a> {
|
||||
pub doc: &'a OrgDocument,
|
||||
pub links: Vec<DocumentLink>,
|
||||
pub path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl<'a> Traverser for DocumentLinkTraverser<'a> {
|
||||
fn event(&mut self, event: Event, ctx: &mut TraversalContext) {
|
||||
let Some(base) = &self.path else {
|
||||
return ctx.skip();
|
||||
};
|
||||
|
||||
match event {
|
||||
Event::Enter(Container::Link(link)) => {
|
||||
if let Some(link) = link_path(link, base, self.doc) {
|
||||
self.links.push(link);
|
||||
}
|
||||
ctx.skip();
|
||||
}
|
||||
Event::Enter(Container::SourceBlock(block)) => {
|
||||
if let Some(link) = block_tangle(block, base, self.doc) {
|
||||
self.links.push(link);
|
||||
}
|
||||
ctx.skip();
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn link_path(link: Link, base: &PathBuf, doc: &OrgDocument) -> Option<DocumentLink> {
|
||||
let path = support::token(link.syntax(), SyntaxKind::LINK_PATH)
|
||||
.or_else(|| support::token(link.syntax(), SyntaxKind::TEXT))?;
|
||||
|
||||
let path_str = path.text();
|
||||
|
||||
let (target, data) = if let Some(file) = path_str.strip_prefix("file:") {
|
||||
let path = file.try_resolve_in(base).ok()?;
|
||||
(Some(Url::from_file_path(path).ok()?), None)
|
||||
} else if path_str.starts_with('/') || path_str.starts_with("./") || path_str.starts_with("~/")
|
||||
{
|
||||
let path = path_str.try_resolve_in(base).ok()?;
|
||||
(Some(Url::from_file_path(path).ok()?), None)
|
||||
} else if path_str.starts_with("http://") || path_str.starts_with("https://") {
|
||||
(Some(Url::parse(path_str).ok()?), None)
|
||||
} else if let Some(id) = path_str.strip_prefix('#') {
|
||||
let url = Url::from_file_path(base).ok()?;
|
||||
(
|
||||
None,
|
||||
Some(json!(vec![
|
||||
"headline-id".to_string(),
|
||||
url.to_string(),
|
||||
id.to_string()
|
||||
])),
|
||||
)
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(DocumentLink {
|
||||
range: doc.range_of(
|
||||
path.text_range().start().into(),
|
||||
path.text_range().end().into(),
|
||||
),
|
||||
tooltip: Some("Jump to link".into()),
|
||||
target,
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
fn block_tangle(block: SourceBlock, base: &PathBuf, doc: &OrgDocument) -> Option<DocumentLink> {
|
||||
let parameters = block
|
||||
.syntax()
|
||||
.children()
|
||||
.find(|e| e.kind() == SyntaxKind::BLOCK_BEGIN)
|
||||
.into_iter()
|
||||
.flat_map(|n| n.children_with_tokens())
|
||||
.filter_map(|n| n.into_token())
|
||||
.find(|n| n.kind() == SyntaxKind::SRC_BLOCK_PARAMETERS)?;
|
||||
|
||||
let tangle = header_argument(parameters.text(), "", "", ":tangle", "no");
|
||||
|
||||
if tangle == "no" {
|
||||
return None;
|
||||
}
|
||||
|
||||
let path = tangle.try_resolve_in(base).ok()?;
|
||||
let url = Url::from_file_path(path).ok()?;
|
||||
|
||||
let start: u32 = parameters.text_range().start().into();
|
||||
|
||||
let index = parameters.text().find(tangle).unwrap_or_default() as u32;
|
||||
|
||||
let len = tangle.len() as u32;
|
||||
|
||||
Some(DocumentLink {
|
||||
range: doc.range_of(start + index, start + index + len),
|
||||
tooltip: Some("Jump to tangle destination".into()),
|
||||
target: Some(url),
|
||||
data: None,
|
||||
})
|
||||
}
|
||||
|
||||
impl<'a> DocumentLinkTraverser<'a> {
|
||||
pub fn new(doc: &'a OrgDocument, path: Option<PathBuf>) -> Self {
|
||||
DocumentLinkTraverser {
|
||||
path,
|
||||
links: vec![],
|
||||
doc,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve() {}
|
||||
81
orgize-lsp/src/document_link_resolve.rs
Normal file
81
orgize-lsp/src/document_link_resolve.rs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
use orgize::export::{Container, Event, TraversalContext, Traverser};
|
||||
use tower_lsp::lsp_types::{DocumentLink, Url};
|
||||
|
||||
use crate::{org_document::OrgDocument, Backend};
|
||||
|
||||
pub fn document_link_resolve(
|
||||
document_link: &DocumentLink,
|
||||
backend: &Backend,
|
||||
) -> Option<DocumentLink> {
|
||||
// don't need to resolve
|
||||
if document_link.target.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let data = document_link.data.as_ref()?;
|
||||
let data = data.as_array()?;
|
||||
|
||||
match (
|
||||
data.get(0)?.as_str()?,
|
||||
data.get(1)?.as_str()?,
|
||||
data.get(2)?.as_str()?,
|
||||
) {
|
||||
("headline-id", url, id) => {
|
||||
let mut parsed = Url::parse(url).ok()?;
|
||||
|
||||
let doc = backend.documents.get(url)?;
|
||||
let mut h = HeadlineIdTraverser {
|
||||
id: id.to_string(),
|
||||
line_number: None,
|
||||
doc: &doc,
|
||||
};
|
||||
doc.traverse(&mut h);
|
||||
if let Some(line) = h.line_number.take() {
|
||||
// results is zero-based
|
||||
parsed.set_fragment(Some(&(line + 1).to_string()));
|
||||
Some(DocumentLink {
|
||||
target: Some(parsed),
|
||||
data: None,
|
||||
tooltip: None,
|
||||
range: document_link.range,
|
||||
})
|
||||
} else {
|
||||
Some(DocumentLink {
|
||||
target: Some(parsed),
|
||||
data: None,
|
||||
tooltip: None,
|
||||
range: document_link.range,
|
||||
})
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
struct HeadlineIdTraverser<'a> {
|
||||
id: String,
|
||||
line_number: Option<u32>,
|
||||
doc: &'a OrgDocument,
|
||||
}
|
||||
|
||||
impl<'a> Traverser for HeadlineIdTraverser<'a> {
|
||||
fn event(&mut self, event: Event, ctx: &mut TraversalContext) {
|
||||
if self.line_number.is_some() {
|
||||
return ctx.stop();
|
||||
}
|
||||
|
||||
match event {
|
||||
Event::Enter(Container::Document(_)) => {}
|
||||
Event::Enter(Container::Headline(headline)) => {
|
||||
let slug = orgize_common::headline_slug(&headline);
|
||||
|
||||
if slug == self.id {
|
||||
let line = self.doc.line_of(headline.begin());
|
||||
self.line_number = Some(line + 1)
|
||||
}
|
||||
}
|
||||
Event::Enter(Container::Section(_)) => ctx.skip(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
66
orgize-lsp/src/document_symbol.rs
Normal file
66
orgize-lsp/src/document_symbol.rs
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
#![allow(deprecated)]
|
||||
|
||||
use orgize::{
|
||||
export::{Container, Event, TraversalContext, Traverser},
|
||||
rowan::ast::AstNode,
|
||||
SyntaxKind,
|
||||
};
|
||||
use tower_lsp::lsp_types::{DocumentSymbol, SymbolKind};
|
||||
|
||||
use crate::org_document::OrgDocument;
|
||||
|
||||
pub struct DocumentSymbolTraverser<'a> {
|
||||
pub doc: &'a OrgDocument,
|
||||
pub stack: Vec<usize>,
|
||||
pub symbols: Vec<DocumentSymbol>,
|
||||
}
|
||||
|
||||
impl<'a> Traverser for DocumentSymbolTraverser<'a> {
|
||||
fn event(&mut self, event: Event, ctx: &mut TraversalContext) {
|
||||
match event {
|
||||
Event::Enter(Container::Headline(headline)) => {
|
||||
let mut symbols = &mut self.symbols;
|
||||
for &i in &self.stack {
|
||||
symbols = symbols[i].children.get_or_insert(vec![]);
|
||||
}
|
||||
|
||||
let name = headline
|
||||
.syntax()
|
||||
.children_with_tokens()
|
||||
.take_while(|n| n.kind() != SyntaxKind::NEW_LINE)
|
||||
.map(|n| n.to_string())
|
||||
.collect::<String>();
|
||||
|
||||
let start = headline.begin();
|
||||
let end = headline.end() - 1;
|
||||
|
||||
self.stack.push(symbols.len());
|
||||
symbols.push(DocumentSymbol {
|
||||
children: None,
|
||||
name,
|
||||
detail: None,
|
||||
kind: SymbolKind::STRING,
|
||||
tags: Some(vec![]),
|
||||
range: self.doc.range_of(start, end),
|
||||
selection_range: self.doc.range_of(start, end),
|
||||
deprecated: None,
|
||||
});
|
||||
}
|
||||
Event::Leave(Container::Headline(_)) => {
|
||||
self.stack.pop();
|
||||
}
|
||||
Event::Enter(Container::Section(_)) => ctx.skip(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> DocumentSymbolTraverser<'a> {
|
||||
pub fn new(doc: &'a OrgDocument) -> Self {
|
||||
DocumentSymbolTraverser {
|
||||
doc,
|
||||
stack: vec![],
|
||||
symbols: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
94
orgize-lsp/src/folding_range.rs
Normal file
94
orgize-lsp/src/folding_range.rs
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
use orgize::{
|
||||
export::{Container, Event, TraversalContext, Traverser},
|
||||
rowan::ast::AstNode,
|
||||
SyntaxKind, SyntaxNode,
|
||||
};
|
||||
use tower_lsp::lsp_types::{FoldingRange, FoldingRangeKind};
|
||||
|
||||
use crate::org_document::OrgDocument;
|
||||
|
||||
pub struct FoldingRangeTraverser<'a> {
|
||||
pub doc: &'a OrgDocument,
|
||||
pub ranges: Vec<FoldingRange>,
|
||||
}
|
||||
|
||||
impl<'a> Traverser for FoldingRangeTraverser<'a> {
|
||||
fn event(&mut self, event: Event, _: &mut TraversalContext) {
|
||||
let syntax = match &event {
|
||||
Event::Enter(Container::Headline(i)) => i.syntax(),
|
||||
Event::Enter(Container::OrgTable(i)) => i.syntax(),
|
||||
Event::Enter(Container::TableEl(i)) => i.syntax(),
|
||||
Event::Enter(Container::List(i)) => i.syntax(),
|
||||
Event::Enter(Container::Drawer(i)) => i.syntax(),
|
||||
Event::Enter(Container::DynBlock(i)) => i.syntax(),
|
||||
Event::Enter(Container::SpecialBlock(i)) => i.syntax(),
|
||||
Event::Enter(Container::QuoteBlock(i)) => i.syntax(),
|
||||
Event::Enter(Container::CenterBlock(i)) => i.syntax(),
|
||||
Event::Enter(Container::VerseBlock(i)) => i.syntax(),
|
||||
Event::Enter(Container::CommentBlock(i)) => i.syntax(),
|
||||
Event::Enter(Container::ExampleBlock(i)) => i.syntax(),
|
||||
Event::Enter(Container::ExportBlock(i)) => i.syntax(),
|
||||
Event::Enter(Container::SourceBlock(i)) => i.syntax(),
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let (start, end) = if syntax.kind() == SyntaxKind::HEADLINE {
|
||||
let range = syntax.text_range();
|
||||
(range.start().into(), range.end().into())
|
||||
} else {
|
||||
get_block_folding_range(syntax)
|
||||
};
|
||||
|
||||
let start_line = self.doc.line_of(start);
|
||||
let end_line = self.doc.line_of(end - 1);
|
||||
|
||||
if start_line != end_line {
|
||||
self.ranges.push(FoldingRange {
|
||||
start_line,
|
||||
end_line,
|
||||
kind: Some(FoldingRangeKind::Region),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_block_folding_range(syntax: &SyntaxNode) -> (u32, u32) {
|
||||
let start: u32 = syntax.text_range().start().into();
|
||||
|
||||
// don't include blank lines in folding range
|
||||
let end = syntax
|
||||
.children()
|
||||
.take_while(|n| n.kind() != SyntaxKind::BLANK_LINE)
|
||||
.last();
|
||||
|
||||
let end: u32 = end.map(|n| n.text_range().end().into()).unwrap_or(start);
|
||||
|
||||
(start, end)
|
||||
}
|
||||
|
||||
impl<'a> FoldingRangeTraverser<'a> {
|
||||
pub fn new(doc: &'a OrgDocument) -> Self {
|
||||
FoldingRangeTraverser {
|
||||
ranges: vec![],
|
||||
doc,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
let doc = OrgDocument::new("\n* a\n\n* b\n\n");
|
||||
let mut t = FoldingRangeTraverser::new(&doc);
|
||||
doc.traverse(&mut t);
|
||||
assert_eq!(t.ranges[0].start_line, 1);
|
||||
assert_eq!(t.ranges[0].end_line, 2);
|
||||
assert_eq!(t.ranges[1].start_line, 3);
|
||||
assert_eq!(t.ranges[1].end_line, 4);
|
||||
|
||||
let doc = OrgDocument::new("\n\r\n#+begin_src\n#+end_src\n\r\r");
|
||||
let mut t = FoldingRangeTraverser::new(&doc);
|
||||
doc.traverse(&mut t);
|
||||
assert_eq!(t.ranges[0].start_line, 2);
|
||||
assert_eq!(t.ranges[0].end_line, 3);
|
||||
}
|
||||
13
orgize-lsp/src/formatting.rs
Normal file
13
orgize-lsp/src/formatting.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
use tower_lsp::lsp_types::TextEdit;
|
||||
|
||||
use crate::org_document::OrgDocument;
|
||||
|
||||
pub fn formatting(doc: &OrgDocument) -> Vec<TextEdit> {
|
||||
orgize_common::formatting(&doc.org)
|
||||
.into_iter()
|
||||
.map(|(start, end, content)| TextEdit {
|
||||
range: doc.range_of(start as u32, end as u32),
|
||||
new_text: content,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
285
orgize-lsp/src/main.rs
Normal file
285
orgize-lsp/src/main.rs
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
mod code_lens;
|
||||
mod commands;
|
||||
mod document_link;
|
||||
mod document_link_resolve;
|
||||
mod document_symbol;
|
||||
mod folding_range;
|
||||
mod formatting;
|
||||
mod org_document;
|
||||
mod semantic_token;
|
||||
|
||||
use dashmap::DashMap;
|
||||
use document_symbol::DocumentSymbolTraverser;
|
||||
use org_document::OrgDocument;
|
||||
use serde_json::Value;
|
||||
use tower_lsp::lsp_types::*;
|
||||
use tower_lsp::{jsonrpc::Result, Client, LanguageServer, LspService, Server};
|
||||
|
||||
pub use self::code_lens::*;
|
||||
pub use self::commands::*;
|
||||
pub use self::document_link::*;
|
||||
pub use self::folding_range::*;
|
||||
pub use self::semantic_token::*;
|
||||
|
||||
pub struct Backend {
|
||||
client: Client,
|
||||
|
||||
documents: DashMap<String, OrgDocument>,
|
||||
}
|
||||
|
||||
#[tower_lsp::async_trait]
|
||||
impl LanguageServer for Backend {
|
||||
async fn initialize(&self, _: InitializeParams) -> Result<InitializeResult> {
|
||||
Ok(InitializeResult {
|
||||
server_info: None,
|
||||
offset_encoding: None,
|
||||
capabilities: ServerCapabilities {
|
||||
text_document_sync: Some(TextDocumentSyncCapability::Kind(
|
||||
TextDocumentSyncKind::INCREMENTAL,
|
||||
)),
|
||||
execute_command_provider: Some(ExecuteCommandOptions {
|
||||
commands: OrgizeCommand::all(),
|
||||
work_done_progress_options: Default::default(),
|
||||
}),
|
||||
workspace: Some(WorkspaceServerCapabilities {
|
||||
workspace_folders: Some(WorkspaceFoldersServerCapabilities {
|
||||
supported: Some(true),
|
||||
change_notifications: Some(OneOf::Left(true)),
|
||||
}),
|
||||
file_operations: None,
|
||||
}),
|
||||
semantic_tokens_provider: Some(
|
||||
SemanticTokensServerCapabilities::SemanticTokensRegistrationOptions(
|
||||
SemanticTokensRegistrationOptions {
|
||||
text_document_registration_options: {
|
||||
TextDocumentRegistrationOptions {
|
||||
document_selector: Some(vec![DocumentFilter {
|
||||
language: Some("org".to_string()),
|
||||
scheme: Some("file".to_string()),
|
||||
pattern: None,
|
||||
}]),
|
||||
}
|
||||
},
|
||||
semantic_tokens_options: SemanticTokensOptions {
|
||||
work_done_progress_options: WorkDoneProgressOptions::default(),
|
||||
legend: SemanticTokensLegend {
|
||||
token_types: LEGEND_TYPE.into(),
|
||||
token_modifiers: vec![],
|
||||
},
|
||||
range: Some(true),
|
||||
full: Some(SemanticTokensFullOptions::Bool(true)),
|
||||
},
|
||||
static_registration_options: StaticRegistrationOptions::default(),
|
||||
},
|
||||
),
|
||||
),
|
||||
code_lens_provider: Some(CodeLensOptions {
|
||||
resolve_provider: Some(true),
|
||||
}),
|
||||
folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)),
|
||||
document_link_provider: Some(DocumentLinkOptions {
|
||||
resolve_provider: Some(true),
|
||||
work_done_progress_options: WorkDoneProgressOptions::default(),
|
||||
}),
|
||||
document_formatting_provider: Some(OneOf::Left(true)),
|
||||
document_symbol_provider: Some(OneOf::Left(true)),
|
||||
..ServerCapabilities::default()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async fn initialized(&self, _: InitializedParams) {
|
||||
self.client
|
||||
.log_message(MessageType::INFO, "Orgize LSP initialized")
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn shutdown(&self) -> Result<()> {
|
||||
self.client
|
||||
.log_message(MessageType::INFO, "Orgize LSP shutdown")
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn did_open(&self, params: DidOpenTextDocumentParams) {
|
||||
let url = params.text_document.uri.to_string();
|
||||
|
||||
self.documents
|
||||
.insert(url.clone(), OrgDocument::new(params.text_document.text));
|
||||
}
|
||||
|
||||
async fn did_change(&self, params: DidChangeTextDocumentParams) {
|
||||
let url = params.text_document.uri.to_string();
|
||||
|
||||
for change in params.content_changes {
|
||||
if let (Some(mut doc), Some(range)) = (self.documents.get_mut(&url), change.range) {
|
||||
let start = doc.offset_of(range.start);
|
||||
let end = doc.offset_of(range.end);
|
||||
doc.update(start, end, &change.text);
|
||||
} else {
|
||||
self.documents
|
||||
.insert(url.clone(), OrgDocument::new(change.text));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn did_save(&self, _: DidSaveTextDocumentParams) {}
|
||||
|
||||
async fn did_close(&self, _: DidCloseTextDocumentParams) {}
|
||||
|
||||
async fn did_change_configuration(&self, _: DidChangeConfigurationParams) {}
|
||||
|
||||
async fn did_change_workspace_folders(&self, _: DidChangeWorkspaceFoldersParams) {}
|
||||
|
||||
async fn did_change_watched_files(&self, _: DidChangeWatchedFilesParams) {}
|
||||
|
||||
async fn completion(&self, _: CompletionParams) -> Result<Option<CompletionResponse>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn semantic_tokens_full(
|
||||
&self,
|
||||
params: SemanticTokensParams,
|
||||
) -> Result<Option<SemanticTokensResult>> {
|
||||
let uri = params.text_document.uri.to_string();
|
||||
|
||||
let Some(doc) = self.documents.get(&uri) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut traverser = SemanticTokenTraverser::new(&doc);
|
||||
|
||||
doc.traverse(&mut traverser);
|
||||
|
||||
Ok(Some(SemanticTokensResult::Tokens(SemanticTokens {
|
||||
result_id: None,
|
||||
data: traverser.tokens,
|
||||
})))
|
||||
}
|
||||
|
||||
async fn semantic_tokens_range(
|
||||
&self,
|
||||
params: SemanticTokensRangeParams,
|
||||
) -> Result<Option<SemanticTokensRangeResult>> {
|
||||
let uri = params.text_document.uri.to_string();
|
||||
|
||||
let Some(doc) = self.documents.get(&uri) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut traverser = SemanticTokenTraverser::with_range(&doc, params.range);
|
||||
|
||||
doc.traverse(&mut traverser);
|
||||
|
||||
Ok(Some(SemanticTokensRangeResult::Partial(
|
||||
SemanticTokensPartialResult {
|
||||
data: traverser.tokens,
|
||||
},
|
||||
)))
|
||||
}
|
||||
|
||||
async fn document_link(&self, params: DocumentLinkParams) -> Result<Option<Vec<DocumentLink>>> {
|
||||
let uri = params.text_document.uri.to_string();
|
||||
|
||||
let Some(doc) = self.documents.get(&uri) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut traverser =
|
||||
DocumentLinkTraverser::new(&doc, params.text_document.uri.to_file_path().ok());
|
||||
|
||||
doc.traverse(&mut traverser);
|
||||
|
||||
Ok(Some(traverser.links))
|
||||
}
|
||||
|
||||
async fn document_link_resolve(&self, params: DocumentLink) -> Result<DocumentLink> {
|
||||
if let Some(link) = document_link_resolve::document_link_resolve(¶ms, self) {
|
||||
Ok(link)
|
||||
} else {
|
||||
Ok(params)
|
||||
}
|
||||
}
|
||||
|
||||
async fn folding_range(&self, params: FoldingRangeParams) -> Result<Option<Vec<FoldingRange>>> {
|
||||
let uri = params.text_document.uri.to_string();
|
||||
|
||||
let Some(doc) = self.documents.get(&uri) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut traverser = FoldingRangeTraverser::new(&doc);
|
||||
|
||||
doc.traverse(&mut traverser);
|
||||
|
||||
Ok(Some(traverser.ranges))
|
||||
}
|
||||
|
||||
async fn code_lens(&self, params: CodeLensParams) -> Result<Option<Vec<CodeLens>>> {
|
||||
let uri = params.text_document.uri.to_string();
|
||||
|
||||
let Some(doc) = self.documents.get(&uri) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut traverser = CodeLensTraverser::new(params.text_document.uri, &doc);
|
||||
|
||||
doc.traverse(&mut traverser);
|
||||
|
||||
Ok(Some(traverser.lens))
|
||||
}
|
||||
|
||||
async fn code_lens_resolve(&self, params: CodeLens) -> Result<CodeLens> {
|
||||
Ok(params)
|
||||
}
|
||||
|
||||
async fn code_action(&self, _: CodeActionParams) -> Result<Option<CodeActionResponse>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn formatting(&self, params: DocumentFormattingParams) -> Result<Option<Vec<TextEdit>>> {
|
||||
let uri = params.text_document.uri.to_string();
|
||||
|
||||
let Some(doc) = self.documents.get(&uri) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let edits = formatting::formatting(&doc);
|
||||
Ok(Some(edits))
|
||||
}
|
||||
|
||||
async fn execute_command(&self, params: ExecuteCommandParams) -> Result<Option<Value>> {
|
||||
let value = commands::execute(¶ms, self).await;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
async fn document_symbol(
|
||||
&self,
|
||||
params: DocumentSymbolParams,
|
||||
) -> Result<Option<DocumentSymbolResponse>> {
|
||||
let uri = params.text_document.uri.to_string();
|
||||
|
||||
let Some(doc) = self.documents.get(&uri) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut t = DocumentSymbolTraverser::new(&doc);
|
||||
doc.traverse(&mut t);
|
||||
dbg!(&t.symbols);
|
||||
Ok(Some(DocumentSymbolResponse::Nested(t.symbols)))
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let stdin = tokio::io::stdin();
|
||||
let stdout = tokio::io::stdout();
|
||||
|
||||
let (service, socket) = LspService::build(|client| Backend {
|
||||
client,
|
||||
documents: DashMap::new(),
|
||||
})
|
||||
.finish();
|
||||
|
||||
Server::new(stdin, stdout, socket).serve(service).await;
|
||||
}
|
||||
128
orgize-lsp/src/org_document.rs
Normal file
128
orgize-lsp/src/org_document.rs
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
use orgize::{export::Traverser, Org};
|
||||
use std::iter::once;
|
||||
use tower_lsp::lsp_types::{Position, Range};
|
||||
|
||||
pub struct OrgDocument {
|
||||
pub text: String,
|
||||
pub line_starts: Vec<u32>,
|
||||
pub org: Org,
|
||||
}
|
||||
|
||||
impl OrgDocument {
|
||||
pub fn new(text: impl AsRef<str>) -> Self {
|
||||
let text = text.as_ref().to_string();
|
||||
|
||||
OrgDocument {
|
||||
org: Org::parse(&text),
|
||||
line_starts: line_starts(&text),
|
||||
text,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, start: u32, end: u32, text: &str) {
|
||||
self.text
|
||||
.replace_range((start as usize)..(end as usize), text);
|
||||
|
||||
self.line_starts = line_starts(&self.text);
|
||||
|
||||
self.org = Org::parse(&self.text);
|
||||
}
|
||||
|
||||
pub fn position_of(&self, offset: u32) -> Position {
|
||||
let line = self
|
||||
.line_starts
|
||||
.binary_search(&offset)
|
||||
.unwrap_or_else(|i| i - 1);
|
||||
|
||||
let line_start = self.line_starts[line];
|
||||
|
||||
let character = self.text.as_str()[(line_start as usize)..(offset as usize)]
|
||||
.chars()
|
||||
.count();
|
||||
|
||||
Position::new(line as u32, character as u32)
|
||||
}
|
||||
|
||||
pub fn line_of(&self, offset: u32) -> u32 {
|
||||
self.line_starts
|
||||
.binary_search(&offset)
|
||||
.unwrap_or_else(|i| i - 1) as u32
|
||||
}
|
||||
|
||||
pub fn range_of(&self, start_offset: u32, end_offset: u32) -> Range {
|
||||
Range::new(self.position_of(start_offset), self.position_of(end_offset))
|
||||
}
|
||||
|
||||
pub fn offset_of(&self, position: Position) -> u32 {
|
||||
let line_start = self.line_starts[position.line as usize] as usize;
|
||||
|
||||
let index = self.text.as_str()[line_start..]
|
||||
.char_indices()
|
||||
.nth(position.character as usize)
|
||||
.map(|(i, _)| i)
|
||||
.unwrap_or_default();
|
||||
|
||||
(line_start + index) as u32
|
||||
}
|
||||
|
||||
pub fn traverse<H: Traverser>(&self, h: &mut H) {
|
||||
self.org.traverse(h);
|
||||
}
|
||||
}
|
||||
|
||||
fn line_starts(text: &str) -> Vec<u32> {
|
||||
let bytes = text.as_bytes();
|
||||
|
||||
once(0)
|
||||
.chain(
|
||||
memchr::memchr2_iter(b'\r', b'\n', bytes)
|
||||
.filter(|&i| bytes[i] == b'\n' || !matches!(bytes.get(i + 1), Some(b'\n')))
|
||||
.map(|i| (i + 1) as u32),
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
let doc = OrgDocument::new(
|
||||
r#"* toc :toc:
|
||||
|
||||
fsfs
|
||||
fasdfs
|
||||
|
||||
|
||||
|
||||
fasdfs
|
||||
|
||||
*a* _a_ /1/ ~default~ =default= a_a
|
||||
|
||||
# abc
|
||||
|
||||
* abc12121
|
||||
12121
|
||||
|
||||
|
||||
#+begin_src javascript
|
||||
console.log(a);
|
||||
#+end_src
|
||||
|
||||
"#,
|
||||
);
|
||||
|
||||
let start = 12;
|
||||
let start_position = Position {
|
||||
line: 1,
|
||||
character: 0,
|
||||
};
|
||||
let end = 81;
|
||||
let end_position = Position {
|
||||
line: 13,
|
||||
character: 0,
|
||||
};
|
||||
|
||||
assert_eq!(doc.position_of(start), start_position);
|
||||
assert_eq!(doc.position_of(end), end_position);
|
||||
|
||||
assert_eq!(doc.offset_of(start_position), start);
|
||||
assert_eq!(doc.offset_of(end_position), end);
|
||||
}
|
||||
123
orgize-lsp/src/semantic_token.rs
Normal file
123
orgize-lsp/src/semantic_token.rs
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
use orgize::{
|
||||
export::{Container, Event, TraversalContext, Traverser},
|
||||
rowan::{ast::AstNode, TextRange},
|
||||
};
|
||||
use tower_lsp::lsp_types::{Range, SemanticToken, SemanticTokenType};
|
||||
|
||||
use crate::org_document::OrgDocument;
|
||||
|
||||
/// Semantic token types that are used for highlighting
|
||||
pub const LEGEND_TYPE: &[SemanticTokenType] = &[SemanticTokenType::COMMENT];
|
||||
|
||||
pub struct SemanticTokenTraverser<'a> {
|
||||
pub doc: &'a OrgDocument,
|
||||
|
||||
pub range: Option<TextRange>,
|
||||
|
||||
pub tokens: Vec<SemanticToken>,
|
||||
pub previous_line: u32,
|
||||
pub previous_start: u32,
|
||||
}
|
||||
|
||||
impl<'a> Traverser for SemanticTokenTraverser<'a> {
|
||||
fn event(&mut self, event: Event, ctx: &mut TraversalContext) {
|
||||
match event {
|
||||
Event::Enter(Container::Comment(comment)) => {
|
||||
let range = comment.syntax().text_range();
|
||||
|
||||
if self.contains_range(range) {
|
||||
if let Some(token) = self.create_token(
|
||||
comment.begin(),
|
||||
comment.end(),
|
||||
SemanticTokenType::COMMENT,
|
||||
) {
|
||||
self.tokens.push(token);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.skip();
|
||||
}
|
||||
Event::Enter(Container::CommentBlock(comment)) => {
|
||||
let range = comment.syntax().text_range();
|
||||
|
||||
if self.contains_range(range) {
|
||||
if let Some(token) = self.create_token(
|
||||
comment.begin(),
|
||||
comment.end(),
|
||||
SemanticTokenType::COMMENT,
|
||||
) {
|
||||
self.tokens.push(token);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.skip();
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SemanticTokenTraverser<'a> {
|
||||
pub fn new(doc: &'a OrgDocument) -> Self {
|
||||
SemanticTokenTraverser {
|
||||
doc,
|
||||
range: None,
|
||||
previous_line: 0,
|
||||
previous_start: 0,
|
||||
tokens: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_range(doc: &'a OrgDocument, range: Range) -> Self {
|
||||
let start = doc.offset_of(range.start);
|
||||
let end = doc.offset_of(range.end);
|
||||
|
||||
SemanticTokenTraverser {
|
||||
doc,
|
||||
range: Some(TextRange::new(start.into(), end.into())),
|
||||
previous_line: 0,
|
||||
previous_start: 0,
|
||||
tokens: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn contains_range(&self, range: TextRange) -> bool {
|
||||
match self.range {
|
||||
Some(r) => r.contains_range(range),
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn create_token(
|
||||
&mut self,
|
||||
start: u32,
|
||||
end: u32,
|
||||
kind: SemanticTokenType,
|
||||
) -> Option<SemanticToken> {
|
||||
let length = end - start;
|
||||
let token_type = LEGEND_TYPE.iter().position(|item| item == &kind)? as u32;
|
||||
|
||||
let line = self.doc.line_of(start);
|
||||
let first = self.doc.line_of(line);
|
||||
let start = self.doc.line_of(start) - first;
|
||||
|
||||
let delta_line = line - self.previous_line;
|
||||
let delta_start = if delta_line == 0 {
|
||||
start - self.previous_start
|
||||
} else {
|
||||
start
|
||||
};
|
||||
|
||||
self.previous_line = line;
|
||||
self.previous_start = start;
|
||||
|
||||
Some(SemanticToken {
|
||||
delta_line,
|
||||
delta_start,
|
||||
length,
|
||||
token_type,
|
||||
token_modifiers_bitset: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
0
wasm/.gitignore → orgize-wasm/.gitignore
vendored
0
wasm/.gitignore → orgize-wasm/.gitignore
vendored
15
orgize-wasm/Cargo.toml
Normal file
15
orgize-wasm/Cargo.toml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "orgize-wasm"
|
||||
publish = false
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
orgize = { path = "../orgize" }
|
||||
wasm-bindgen = "0.2"
|
||||
39
orgize/Cargo.toml
Normal file
39
orgize/Cargo.toml
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
[package]
|
||||
name = "orgize"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
description = "A Rust library for parsing org-mode files."
|
||||
repository.workspace = true
|
||||
readme = "README.md"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
keywords = ["orgmode", "org-mode", "emacs", "parser"]
|
||||
exclude = ["/wasm", "/.github"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
indexmap = ["dep:indexmap"]
|
||||
chrono = ["dep:chrono"]
|
||||
|
||||
[dependencies]
|
||||
bytecount = "0.6"
|
||||
chrono = { version = "0.4", optional = true }
|
||||
indexmap = { version = "2.1", optional = true }
|
||||
jetscii = "0.5"
|
||||
memchr = "2.5"
|
||||
nom = { version = "7.1", default-features = false, features = ["std"] }
|
||||
rowan = "0.15"
|
||||
tracing = "0.1"
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = "0.5"
|
||||
insta = "1.29"
|
||||
slugify = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["fmt"] }
|
||||
|
||||
[[bench]]
|
||||
name = "parse"
|
||||
harness = false
|
||||
71
orgize/README.md
Normal file
71
orgize/README.md
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
# Orgize
|
||||
|
||||
[](https://crates.io/crates/orgize)
|
||||
[](https://docs.rs/orgize)
|
||||
[](https://github.com/PoiScript/orgize/actions/workflows/ci.yml)
|
||||

|
||||
|
||||
A Rust library for parsing org-mode files.
|
||||
|
||||
Live Demo: <https://poiscript.github.io/orgize/>
|
||||
|
||||
## Parse
|
||||
|
||||
To parse a org-mode string, simply invoking the `Org::parse` function:
|
||||
|
||||
```rust
|
||||
use orgize::{Org, rowan::ast::AstNode};
|
||||
|
||||
let org = Org::parse("* DONE Title :tag:");
|
||||
assert_eq!(
|
||||
format!("{:#?}", org.document().syntax()),
|
||||
r#"DOCUMENT@0..18
|
||||
HEADLINE@0..18
|
||||
HEADLINE_STARS@0..1 "*"
|
||||
WHITESPACE@1..2 " "
|
||||
HEADLINE_KEYWORD_DONE@2..6 "DONE"
|
||||
WHITESPACE@6..7 " "
|
||||
HEADLINE_TITLE@7..13
|
||||
TEXT@7..13 "Title "
|
||||
HEADLINE_TAGS@13..18
|
||||
COLON@13..14 ":"
|
||||
TEXT@14..17 "tag"
|
||||
COLON@17..18 ":"
|
||||
"#);
|
||||
```
|
||||
|
||||
use `ParseConfig::parse` to specific a custom parse config
|
||||
|
||||
```rust
|
||||
use orgize::{Org, ParseConfig, ast::Headline};
|
||||
|
||||
let config = ParseConfig {
|
||||
// custom todo keywords
|
||||
todo_keywords: (vec!["TASK".to_string()], vec![]),
|
||||
..Default::default()
|
||||
};
|
||||
let org = config.parse("* TASK Title 1");
|
||||
let hdl = org.first_node::<Headline>().unwrap();
|
||||
assert_eq!(hdl.todo_keyword().unwrap(), "TASK");
|
||||
```
|
||||
|
||||
## Render to html
|
||||
|
||||
Call the `Org::to_html` function to export org element tree to html:
|
||||
|
||||
```rust
|
||||
use orgize::Org;
|
||||
|
||||
assert_eq!(
|
||||
Org::parse("* title\n*section*").to_html(),
|
||||
"<main><h1>title</h1><section><p><b>section</b></p></section></main>"
|
||||
);
|
||||
```
|
||||
|
||||
Checkout `examples/html-slugify.rs` on how to customizing html export process.
|
||||
|
||||
## Features
|
||||
|
||||
- **`chrono`**: adds the ability to convert `Timestamp` into `chrono::NaiveDateTime`, disabled by default.
|
||||
|
||||
- **`indexmap`**: adds the ability to convert `PropertyDrawer` properties into `IndexMap`, disabled by default.
|
||||
1
orgize/benches/.gitignore
vendored
Normal file
1
orgize/benches/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
*.org
|
||||
0
fuzz/.gitignore → orgize/fuzz/.gitignore
vendored
0
fuzz/.gitignore → orgize/fuzz/.gitignore
vendored
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue