chore: remove orgize-cli & orgize-lsp
This commit is contained in:
parent
edd73e3c6d
commit
42cb1d21bd
54 changed files with 1 additions and 7078 deletions
|
|
@ -1,12 +1,6 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"./orgize",
|
||||
"./orgize-cli",
|
||||
"./orgize-common",
|
||||
"./orgize-lsp",
|
||||
"./orgize-wasm",
|
||||
]
|
||||
members = ["./orgize", "./orgize-wasm"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.10.0-alpha.7"
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
[package]
|
||||
name = "orgize-cli"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
description = "Command line utilities for org-mode files, builtin with 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"
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# Orgize CLI
|
||||
|
||||
Command line utilities for org-mode files, builtin with [`orgize`].
|
||||
|
||||
[`orgize`]: https://crates.io/crates/orgize
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
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();
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
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(())
|
||||
}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
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();
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
use clap::Args;
|
||||
use orgize::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 input = std::fs::read_to_string(&path)?;
|
||||
|
||||
let org = Org::parse(&input);
|
||||
|
||||
let patches = orgize_common::formatting(&org);
|
||||
|
||||
if self.dry_run {
|
||||
diff::print(&input, patches);
|
||||
} else {
|
||||
diff::write_to_file(&input, patches, path)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
mod detangle;
|
||||
mod diff;
|
||||
mod execute_src_block;
|
||||
mod fmt;
|
||||
mod tangle;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use clap_verbosity_flag::{InfoLevel, LevelFilter as CLevelFilter, Verbosity};
|
||||
use tracing::level_filters::LevelFilter;
|
||||
|
||||
/// Command line utility for org-mode files
|
||||
#[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 {
|
||||
/// Tangle source block contents to destination files
|
||||
#[clap(name = "tangle")]
|
||||
Tangle(tangle::Command),
|
||||
|
||||
/// Insert tangled file contents back to source files
|
||||
#[clap(name = "detangle")]
|
||||
Detangle(detangle::Command),
|
||||
|
||||
/// Execute source block
|
||||
#[clap(name = "execute-src-block")]
|
||||
ExecuteSrcBlock(execute_src_block::Command),
|
||||
|
||||
/// Format org-mode files
|
||||
#[clap(name = "fmt")]
|
||||
Format(fmt::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(),
|
||||
Command::Format(cmd) => cmd.run(),
|
||||
}
|
||||
}
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
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();
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
[package]
|
||||
name = "orgize-common"
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
description = "Shared code for orgize-cli 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"
|
||||
|
|
@ -1,110 +0,0 @@
|
|||
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())
|
||||
}
|
||||
|
|
@ -1,142 +0,0 @@
|
|||
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()))
|
||||
}
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
use orgize::{SyntaxKind, SyntaxNode};
|
||||
|
||||
pub fn format(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);
|
||||
|
||||
let Some(first_line) = blank_lines.next() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if first_line.text() != "\n" {
|
||||
edits.push((
|
||||
first_line.text_range().start().into(),
|
||||
first_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(),
|
||||
));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
use crate::test_case;
|
||||
use orgize::ast::SourceBlock;
|
||||
|
||||
test_case!(
|
||||
SourceBlock,
|
||||
"#+begin_src\n#+end_src\n\r\n\n\r",
|
||||
format,
|
||||
"#+begin_src\n#+end_src\n\n"
|
||||
);
|
||||
|
||||
test_case!(
|
||||
SourceBlock,
|
||||
"#+begin_src\n#+end_src\n",
|
||||
format,
|
||||
"#+begin_src\n#+end_src\n"
|
||||
);
|
||||
|
||||
test_case!(
|
||||
SourceBlock,
|
||||
"#+begin_src\n#+end_src",
|
||||
format,
|
||||
"#+begin_src\n#+end_src"
|
||||
);
|
||||
}
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
use std::iter::once;
|
||||
|
||||
use orgize::{ast::ListItem, rowan::ast::AstNode, SyntaxNode};
|
||||
|
||||
pub fn format(node: &SyntaxNode, indent_level: usize, edits: &mut Vec<(usize, usize, String)>) {
|
||||
let mut items = node.children().filter_map(ListItem::cast);
|
||||
|
||||
let Some(first_item) = items.next() else {
|
||||
return;
|
||||
};
|
||||
|
||||
match first_item.bullet().trim_end() {
|
||||
expected_bullet @ ("-" | "+" | "*") => {
|
||||
if first_item.indent() != 3 * indent_level {
|
||||
edits.push((
|
||||
first_item.begin() as usize,
|
||||
first_item.begin() as usize + first_item.indent(),
|
||||
" ".repeat(3 * indent_level),
|
||||
));
|
||||
}
|
||||
|
||||
for item in items {
|
||||
if item.indent() != 3 * indent_level {
|
||||
edits.push((
|
||||
item.begin() as usize,
|
||||
item.begin() as usize + item.indent(),
|
||||
" ".repeat(3 * indent_level),
|
||||
));
|
||||
}
|
||||
|
||||
let bullet = item.bullet();
|
||||
let s = bullet.trim_end();
|
||||
if s != expected_bullet {
|
||||
edits.push((
|
||||
bullet.start() as usize,
|
||||
bullet.start() as usize + s.len(),
|
||||
expected_bullet.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
b => {
|
||||
let c = if b.ends_with(')') { ')' } else { '.' };
|
||||
|
||||
for (index, item) in once(first_item).chain(items).enumerate() {
|
||||
if item.indent() != 3 * indent_level {
|
||||
edits.push((
|
||||
item.begin() as usize,
|
||||
item.begin() as usize + item.indent(),
|
||||
" ".repeat(3 * indent_level),
|
||||
));
|
||||
}
|
||||
|
||||
let expected_bullet = format!("{}{c}", index + 1);
|
||||
let bullet = item.bullet();
|
||||
let s = bullet.trim_end();
|
||||
if s != expected_bullet {
|
||||
edits.push((
|
||||
bullet.start() as usize,
|
||||
bullet.start() as usize + s.len(),
|
||||
expected_bullet,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
use crate::test_case;
|
||||
use orgize::ast::List;
|
||||
|
||||
let format0 =
|
||||
|node: &SyntaxNode, edits: &mut Vec<(usize, usize, String)>| format(node, 0, edits);
|
||||
|
||||
let format2 =
|
||||
|node: &SyntaxNode, edits: &mut Vec<(usize, usize, String)>| format(node, 2, edits);
|
||||
|
||||
test_case!(List, "1. item", format0, "1. item");
|
||||
|
||||
test_case!(
|
||||
List,
|
||||
"0. item\n- item\n+ item",
|
||||
format0,
|
||||
"1. item\n2. item\n3. item"
|
||||
);
|
||||
|
||||
test_case!(
|
||||
List,
|
||||
" + item\n - item\n 1. item",
|
||||
format0,
|
||||
"+ item\n+ item\n+ item"
|
||||
);
|
||||
|
||||
test_case!(
|
||||
List,
|
||||
" + item\n - item\n 1. item",
|
||||
format2,
|
||||
" + item\n + item\n + item"
|
||||
);
|
||||
}
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
use orgize::{
|
||||
export::{Container, Event, TraversalContext, Traverser},
|
||||
rowan::ast::AstNode,
|
||||
Org,
|
||||
};
|
||||
|
||||
mod blank_lines;
|
||||
mod list;
|
||||
mod rule;
|
||||
|
||||
pub fn formatting(org: &Org) -> Vec<(usize, usize, String)> {
|
||||
let mut format = FormattingTraverser::default();
|
||||
|
||||
org.traverse(&mut format);
|
||||
|
||||
format.edits
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct FormattingTraverser {
|
||||
indent_level: usize,
|
||||
edits: Vec<(usize, usize, String)>,
|
||||
}
|
||||
|
||||
impl Traverser for FormattingTraverser {
|
||||
fn event(&mut self, event: Event, _: &mut TraversalContext) {
|
||||
match event {
|
||||
Event::Rule(rule) => {
|
||||
rule::format(rule.syntax(), &mut self.edits);
|
||||
blank_lines::format(rule.syntax(), &mut self.edits);
|
||||
}
|
||||
Event::Clock(clock) => {
|
||||
blank_lines::format(clock.syntax(), &mut self.edits);
|
||||
}
|
||||
|
||||
Event::Enter(Container::Document(document)) => {
|
||||
blank_lines::format(document.syntax(), &mut self.edits);
|
||||
}
|
||||
Event::Enter(Container::Paragraph(paragraph)) => {
|
||||
blank_lines::format(paragraph.syntax(), &mut self.edits);
|
||||
}
|
||||
Event::Enter(Container::List(list)) => {
|
||||
list::format(list.syntax(), self.indent_level, &mut self.edits);
|
||||
blank_lines::format(list.syntax(), &mut self.edits);
|
||||
self.indent_level += 1;
|
||||
}
|
||||
Event::Leave(Container::List(_)) => {
|
||||
self.indent_level -= 1;
|
||||
}
|
||||
Event::Enter(Container::OrgTable(table)) => {
|
||||
blank_lines::format(table.syntax(), &mut self.edits);
|
||||
}
|
||||
Event::Enter(Container::SpecialBlock(block)) => {
|
||||
blank_lines::format(block.syntax(), &mut self.edits);
|
||||
}
|
||||
Event::Enter(Container::QuoteBlock(block)) => {
|
||||
blank_lines::format(block.syntax(), &mut self.edits);
|
||||
}
|
||||
Event::Enter(Container::CenterBlock(block)) => {
|
||||
blank_lines::format(block.syntax(), &mut self.edits);
|
||||
}
|
||||
Event::Enter(Container::VerseBlock(block)) => {
|
||||
blank_lines::format(block.syntax(), &mut self.edits);
|
||||
}
|
||||
Event::Enter(Container::CommentBlock(block)) => {
|
||||
blank_lines::format(block.syntax(), &mut self.edits);
|
||||
}
|
||||
Event::Enter(Container::ExampleBlock(block)) => {
|
||||
blank_lines::format(block.syntax(), &mut self.edits);
|
||||
}
|
||||
Event::Enter(Container::ExportBlock(block)) => {
|
||||
blank_lines::format(block.syntax(), &mut self.edits);
|
||||
}
|
||||
Event::Enter(Container::SourceBlock(block)) => {
|
||||
blank_lines::format(block.syntax(), &mut self.edits);
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_export]
|
||||
macro_rules! test_case {
|
||||
(
|
||||
$n:tt,
|
||||
$input:expr,
|
||||
$fn:expr,
|
||||
$expected:expr
|
||||
) => {{
|
||||
use orgize::rowan::ast::AstNode;
|
||||
|
||||
let org = orgize::Org::parse($input);
|
||||
let node = org.first_node::<$n>().unwrap();
|
||||
let node = node.syntax();
|
||||
|
||||
let mut patches = vec![];
|
||||
|
||||
$fn(&node, &mut patches);
|
||||
|
||||
let input = node.to_string();
|
||||
|
||||
patches.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
let mut i = 0;
|
||||
let mut output = String::new();
|
||||
for (start, end, text) in patches {
|
||||
output.push_str(&input[i..start]);
|
||||
output.push_str(&text);
|
||||
i = end;
|
||||
}
|
||||
output.push_str(&input[i..]);
|
||||
|
||||
assert_eq!(output, $expected);
|
||||
}};
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
use orgize::{SyntaxKind, SyntaxNode};
|
||||
|
||||
pub fn format(node: &SyntaxNode, edits: &mut Vec<(usize, usize, String)>) {
|
||||
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(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
use crate::test_case;
|
||||
use orgize::ast::Rule;
|
||||
|
||||
test_case!(Rule, " ------------\r\n", format, "-----\n");
|
||||
}
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
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"
|
||||
);
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
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;
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
// 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")))
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
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
|
||||
})
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
[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 files, builtin with orgize."
|
||||
exclude = ["editors"]
|
||||
|
||||
[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"
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
# `orgize-lsp`
|
||||
|
||||
Language server for org-mode, builtin with [`orgize`].
|
||||
|
||||
[`orgize`]: https://crates.io/crates/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. Completion
|
||||
|
||||
- Various blocks: `<a`, `<c`, `<C`, `<e`, `<E`, `<h`, `<l`, `<q`, `<s`, `<v`, `<I`
|
||||
|
||||
7. Commands
|
||||
|
||||
- Show syntax tree
|
||||
3
orgize-lsp/editors/vscode/.gitignore
vendored
3
orgize-lsp/editors/vscode/.gitignore
vendored
|
|
@ -1,3 +0,0 @@
|
|||
dist
|
||||
*.vsix
|
||||
node_modules
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
**
|
||||
!dist/
|
||||
!syntaxes/
|
||||
!media/
|
||||
!images/
|
||||
!package.json
|
||||
!org.configuration.json
|
||||
!README.md
|
||||
|
|
@ -1 +0,0 @@
|
|||
../../LICENSE
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
# Orgize LSP
|
||||
|
||||
This extension provides language support for [Org-mode](https://orgmode.org/) files.
|
||||
|
||||
This extension only contains client part so you need to manually install the server:
|
||||
|
||||
```sh
|
||||
$ cargo install orgize-lsp
|
||||
```
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
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,
|
||||
});
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 KiB |
|
|
@ -1,304 +0,0 @@
|
|||
/* https://github.com/microsoft/vscode/blob/01fc3110beb3f6be198f641b19e3c2e83125d2e3/extensions/markdown-language-features/media/markdown.css */
|
||||
|
||||
html,
|
||||
body {
|
||||
font-family: var(
|
||||
--markdown-font-family,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe WPC",
|
||||
"Segoe UI",
|
||||
system-ui,
|
||||
"Ubuntu",
|
||||
"Droid Sans",
|
||||
sans-serif
|
||||
);
|
||||
font-size: var(--markdown-font-size, 14px);
|
||||
padding: 0 26px;
|
||||
line-height: var(--markdown-line-height, 22px);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
body {
|
||||
padding-top: 1em;
|
||||
}
|
||||
|
||||
/* Reset margin top for elements */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p,
|
||||
ol,
|
||||
ul,
|
||||
pre {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: 600;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 16px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
#code-csp-warning {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
color: white;
|
||||
margin: 16px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-family: sans-serif;
|
||||
background-color: #444444;
|
||||
cursor: pointer;
|
||||
padding: 6px;
|
||||
box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
#code-csp-warning:hover {
|
||||
text-decoration: none;
|
||||
background-color: #007acc;
|
||||
box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
body.scrollBeyondLastLine {
|
||||
margin-bottom: calc(100vh - 22px);
|
||||
}
|
||||
|
||||
body.showEditorSelection .code-line {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
body.showEditorSelection :not(tr, ul, ol).code-active-line:before,
|
||||
body.showEditorSelection :not(tr, ul, ol).code-line:hover:before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -12px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.vscode-high-contrast.showEditorSelection
|
||||
:not(tr, ul, ol).code-line
|
||||
.code-line:hover:before {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
body.showEditorSelection li.code-active-line:before,
|
||||
body.showEditorSelection li.code-line:hover:before {
|
||||
left: -30px;
|
||||
}
|
||||
|
||||
.vscode-light.showEditorSelection .code-active-line:before {
|
||||
border-left: 3px solid rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.vscode-light.showEditorSelection .code-line:hover:before {
|
||||
border-left: 3px solid rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.vscode-dark.showEditorSelection .code-active-line:before {
|
||||
border-left: 3px solid rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.vscode-dark.showEditorSelection .code-line:hover:before {
|
||||
border-left: 3px solid rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.vscode-high-contrast.showEditorSelection .code-active-line:before {
|
||||
border-left: 3px solid rgba(255, 160, 0, 0.7);
|
||||
}
|
||||
|
||||
.vscode-high-contrast.showEditorSelection .code-line:hover:before {
|
||||
border-left: 3px solid rgba(255, 160, 0, 1);
|
||||
}
|
||||
|
||||
/* Prevent `sub` and `sup` elements from affecting line height */
|
||||
sub,
|
||||
sup {
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
ul ul:first-child,
|
||||
ul ol:first-child,
|
||||
ol ul:first-child,
|
||||
ol ol:first-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
img,
|
||||
video {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:focus,
|
||||
input:focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
outline: 1px solid -webkit-focus-ring-color;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
li p {
|
||||
margin-bottom: 0.7em;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin-bottom: 0.7em;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
height: 1px;
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin-top: 0;
|
||||
padding-bottom: 0.3em;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5em;
|
||||
padding-bottom: 0.3em;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 0.7em;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
table > tbody > tr + tr > td {
|
||||
border-top: 1px solid;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0;
|
||||
padding: 2px 16px 0 10px;
|
||||
border-left-width: 5px;
|
||||
border-left-style: solid;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: var(
|
||||
--vscode-editor-font-family,
|
||||
"SF Mono",
|
||||
Monaco,
|
||||
Menlo,
|
||||
Consolas,
|
||||
"Ubuntu Mono",
|
||||
"Liberation Mono",
|
||||
"DejaVu Sans Mono",
|
||||
"Courier New",
|
||||
monospace
|
||||
);
|
||||
font-size: 1em;
|
||||
line-height: 1.357em;
|
||||
}
|
||||
|
||||
body.wordWrap pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
pre:not(.hljs),
|
||||
pre.hljs code > div {
|
||||
padding: 16px;
|
||||
border-radius: 3px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
pre code {
|
||||
display: inline-block;
|
||||
color: var(--vscode-editor-foreground);
|
||||
tab-size: 4;
|
||||
background: none;
|
||||
}
|
||||
|
||||
/** Theming */
|
||||
|
||||
pre {
|
||||
background-color: var(--vscode-textCodeBlock-background);
|
||||
border: 1px solid var(--vscode-widget-border);
|
||||
}
|
||||
|
||||
.vscode-high-contrast h1 {
|
||||
border-color: rgb(0, 0, 0);
|
||||
}
|
||||
|
||||
.vscode-light th {
|
||||
border-color: rgba(0, 0, 0, 0.69);
|
||||
}
|
||||
|
||||
.vscode-dark th {
|
||||
border-color: rgba(255, 255, 255, 0.69);
|
||||
}
|
||||
|
||||
.vscode-light h1,
|
||||
.vscode-light h2,
|
||||
.vscode-light hr,
|
||||
.vscode-light td {
|
||||
border-color: rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.vscode-dark h1,
|
||||
.vscode-dark h2,
|
||||
.vscode-dark hr,
|
||||
.vscode-dark td {
|
||||
border-color: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
{
|
||||
"comments": {
|
||||
"lineComment": "#",
|
||||
"blockComment": ["#+BEGIN_COMMENT\n", "\n#+END_COMMENT"]
|
||||
},
|
||||
"brackets": [
|
||||
["{", "}"],
|
||||
["[", "]"],
|
||||
["(", ")"]
|
||||
],
|
||||
"autoClosingPairs": [
|
||||
["{", "}"],
|
||||
["[", "]"],
|
||||
["(", ")"],
|
||||
["\"", "\""]
|
||||
],
|
||||
"surroundingPairs": [
|
||||
["{", "}"],
|
||||
["[", "]"],
|
||||
["(", ")"],
|
||||
["\"", "\""],
|
||||
["'", "'"],
|
||||
["*", "*"],
|
||||
["/", "/"],
|
||||
["_", "_"],
|
||||
["=", "="],
|
||||
["~", "~"]
|
||||
]
|
||||
}
|
||||
|
|
@ -1,156 +0,0 @@
|
|||
{
|
||||
"name": "orgize-lsp",
|
||||
"private": true,
|
||||
"version": "0.10.0-alpha.0",
|
||||
"engines": {
|
||||
"vscode": "^1.75.0"
|
||||
},
|
||||
"displayName": "Orgize LSP",
|
||||
"description": "Language server for org-mode files, builtin with orgize.",
|
||||
"icon": "./images/extension-icon.png",
|
||||
"publisher": "PoiScript",
|
||||
"preview": true,
|
||||
"categories": [
|
||||
"Programming Languages",
|
||||
"Formatters"
|
||||
],
|
||||
"keywords": [
|
||||
"org",
|
||||
"org-mode"
|
||||
],
|
||||
"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",
|
||||
"vscode-uri": "^3.0.8"
|
||||
},
|
||||
"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": "Orgize (debug): Show org syntax tree"
|
||||
},
|
||||
{
|
||||
"command": "orgize.preview-html",
|
||||
"title": "Orgize: Preview in HTML"
|
||||
}
|
||||
],
|
||||
"languages": [
|
||||
{
|
||||
"id": "org",
|
||||
"aliases": [
|
||||
"Org",
|
||||
"Org Markup",
|
||||
"Org Mode"
|
||||
],
|
||||
"extensions": [
|
||||
".org"
|
||||
],
|
||||
"configuration": "./org.configuration.json",
|
||||
"icon": {
|
||||
"light": "./images/language-light-icon.png",
|
||||
"dark": "./images/language-dark-icon.png"
|
||||
}
|
||||
}
|
||||
],
|
||||
"semanticTokenScopes": [
|
||||
{
|
||||
"language": "org",
|
||||
"scopes": {
|
||||
"headlineTodoKeyword": [
|
||||
"invalid.illegal.org"
|
||||
],
|
||||
"headlineDoneKeyword": [
|
||||
"keyword.control.org"
|
||||
],
|
||||
"headlineTags": [
|
||||
"markup.italic.org",
|
||||
"keyword.control.org"
|
||||
],
|
||||
"headlinePriority": [
|
||||
"keyword.control.org"
|
||||
],
|
||||
"timestamp": [
|
||||
"variable.org"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
1275
orgize-lsp/editors/vscode/pnpm-lock.yaml
generated
1275
orgize-lsp/editors/vscode/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,53 +0,0 @@
|
|||
import { ExtensionContext } from "vscode";
|
||||
|
||||
import {
|
||||
Executable,
|
||||
LanguageClient,
|
||||
LanguageClientOptions,
|
||||
ServerOptions,
|
||||
} from "vscode-languageclient/node";
|
||||
|
||||
import SyntaxTreeProvider from "./syntax-tree";
|
||||
import { register } 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());
|
||||
register(context);
|
||||
}
|
||||
|
||||
export function deactivate(): Thenable<void> | undefined {
|
||||
if (!client) {
|
||||
return undefined;
|
||||
}
|
||||
return client.stop();
|
||||
}
|
||||
|
|
@ -1,196 +0,0 @@
|
|||
import {
|
||||
Disposable,
|
||||
ExtensionContext,
|
||||
Uri,
|
||||
ViewColumn,
|
||||
WebviewPanel,
|
||||
commands,
|
||||
window,
|
||||
workspace,
|
||||
} from "vscode";
|
||||
import { Utils } from "vscode-uri";
|
||||
|
||||
import { client } from "./main";
|
||||
|
||||
export const register = (context: ExtensionContext) => {
|
||||
context.subscriptions.push(
|
||||
commands.registerTextEditorCommand("orgize.preview-html", (editor) => {
|
||||
PreviewHtmlPanel.createOrShow(context.extensionUri, editor.document.uri);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
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 _orgUri: Uri;
|
||||
private readonly _extensionUri: Uri;
|
||||
|
||||
private _disposables: Disposable[] = [];
|
||||
|
||||
public static createOrShow(extensionUri: Uri, orgUri: Uri) {
|
||||
const column = window.activeTextEditor.viewColumn! + 1;
|
||||
|
||||
// If we already have a panel, show it.
|
||||
if (PreviewHtmlPanel.currentPanel) {
|
||||
PreviewHtmlPanel.currentPanel._panel.reveal(column);
|
||||
PreviewHtmlPanel.currentPanel._orgUri = orgUri;
|
||||
PreviewHtmlPanel.currentPanel.refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, create a new panel.
|
||||
const panel = window.createWebviewPanel(
|
||||
PreviewHtmlPanel.viewType,
|
||||
"Preview of " + Utils.basename(orgUri),
|
||||
column || ViewColumn.One,
|
||||
{
|
||||
// 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"),
|
||||
...workspace.workspaceFolders.map((folder) => folder.uri),
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
PreviewHtmlPanel.currentPanel = new PreviewHtmlPanel(
|
||||
panel,
|
||||
extensionUri,
|
||||
orgUri
|
||||
);
|
||||
}
|
||||
|
||||
private constructor(panel: WebviewPanel, extensionUri: Uri, orgUri: Uri) {
|
||||
this._panel = panel;
|
||||
this._orgUri = orgUri;
|
||||
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
|
||||
);
|
||||
|
||||
workspace.onDidChangeTextDocument((event) => {
|
||||
if (event.document.uri.fsPath === this._orgUri.fsPath) {
|
||||
this.refresh();
|
||||
}
|
||||
}, this._disposables);
|
||||
|
||||
workspace.onDidOpenTextDocument((document) => {
|
||||
if (document.uri.fsPath === this._orgUri.fsPath) {
|
||||
this.refresh();
|
||||
}
|
||||
}, this._disposables);
|
||||
|
||||
// Update the content based on view changes
|
||||
this._panel.onDidChangeViewState(
|
||||
(e) => {
|
||||
if (this._panel.visible) {
|
||||
this.refresh();
|
||||
}
|
||||
},
|
||||
null,
|
||||
this._disposables
|
||||
);
|
||||
}
|
||||
|
||||
private readonly _delay = 300;
|
||||
private _throttleTimer: any;
|
||||
private _firstUpdate = true;
|
||||
|
||||
public refresh() {
|
||||
// Schedule update if none is pending
|
||||
if (!this._throttleTimer) {
|
||||
if (this._firstUpdate) {
|
||||
this._update();
|
||||
} else {
|
||||
this._throttleTimer = setTimeout(() => this._update(), this._delay);
|
||||
}
|
||||
}
|
||||
|
||||
this._firstUpdate = false;
|
||||
}
|
||||
|
||||
private async _update() {
|
||||
clearTimeout(this._throttleTimer);
|
||||
this._throttleTimer = undefined;
|
||||
|
||||
if (!client) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const content: string = await client.sendRequest(
|
||||
"workspace/executeCommand",
|
||||
{
|
||||
command: "orgize.preview-html",
|
||||
arguments: [this._orgUri.with({ scheme: "file" }).toString()],
|
||||
}
|
||||
);
|
||||
this._panel.webview.html = this._makeHtml(content);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
private _makeHtml(content: string): string {
|
||||
const stylesPath = Uri.joinPath(
|
||||
this._extensionUri,
|
||||
"media",
|
||||
"org-mode.css"
|
||||
);
|
||||
|
||||
return `<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0"
|
||||
/>
|
||||
|
||||
<base
|
||||
href="${this._panel.webview.asWebviewUri(this._orgUri)}"
|
||||
/>
|
||||
|
||||
<link
|
||||
href="${this._panel.webview.asWebviewUri(stylesPath)}"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
${content}
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
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: "" });
|
||||
};
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es2019",
|
||||
"lib": ["ES2019"],
|
||||
"outDir": "../dist",
|
||||
"rootDir": "src",
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", ".vscode-test"]
|
||||
}
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
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",
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
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.first().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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
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(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
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(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
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(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,79 +0,0 @@
|
|||
use tower_lsp::lsp_types::{
|
||||
CompletionItem, CompletionItemKind, CompletionParams, CompletionResponse, CompletionTextEdit,
|
||||
InsertTextFormat, Position, Range, TextEdit,
|
||||
};
|
||||
|
||||
use crate::Backend;
|
||||
|
||||
pub fn completion(params: CompletionParams, backend: &Backend) -> Option<CompletionResponse> {
|
||||
let uri = params.text_document_position.text_document.uri.to_string();
|
||||
|
||||
let Some(doc) = backend.documents.get(&uri) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let offset = doc.offset_of(params.text_document_position.position) as usize;
|
||||
|
||||
if offset < 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let filter_text = doc.text.get((offset - 2)..offset)?;
|
||||
|
||||
let (label, new_text) = match filter_text {
|
||||
"<a" => (
|
||||
"ASCI export block",
|
||||
"#+BEGIN_EXPORT ascii\n${0}\n#+END_EXPORT\n",
|
||||
),
|
||||
"<c" => ("Center block", "#+BEGIN_CENTER\n${0}\n#+END_CENTER\n"),
|
||||
"<C" => ("Comment block", "#+BEGIN_COMMENT\n${0}\n#+END_COMMENT\n"),
|
||||
"<e" => ("Example block", "#+BEGIN_EXAMPLE\n${0}\n#+END_EXAMPLE\n"),
|
||||
"<E" => ("Export block", "#+BEGIN_EXPORT\n${0}\n#+END_EXPORT\n"),
|
||||
"<h" => (
|
||||
"HTML export block",
|
||||
"#+BEGIN_EXPORT html\n${0}\n#+END_EXPORT\n",
|
||||
),
|
||||
"<l" => (
|
||||
"LaTeX export block",
|
||||
"#+BEGIN_EXPORT latex\n${0}\n#+END_EXPORT\n",
|
||||
),
|
||||
"<q" => ("Quote block", "#+BEGIN_QUOTE\n${0}\n#+END_QUOTE\n"),
|
||||
"<s" => ("Source block", "#+BEGIN_SRC ${1}\n${0}\n#+END_SRC\n"),
|
||||
"<v" => ("Verse block", "#+BEGIN_VERSE\n${0}\n#+END_VERSE\n"),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let end = params.text_document_position.position;
|
||||
|
||||
Some(CompletionResponse::Array(vec![CompletionItem {
|
||||
label: label.into(),
|
||||
kind: Some(CompletionItemKind::SNIPPET),
|
||||
insert_text: Some(new_text.into()),
|
||||
insert_text_format: Some(InsertTextFormat::SNIPPET),
|
||||
filter_text: Some(filter_text.into()),
|
||||
text_edit: Some(CompletionTextEdit::Edit(TextEdit {
|
||||
new_text: new_text.into(),
|
||||
range: Range {
|
||||
start: Position::new(end.line, end.character - 2),
|
||||
end,
|
||||
},
|
||||
})),
|
||||
..Default::default()
|
||||
}]))
|
||||
}
|
||||
|
||||
pub fn trigger_characters() -> Vec<String> {
|
||||
vec![
|
||||
"<a".into(),
|
||||
"<c".into(),
|
||||
"<C".into(),
|
||||
"<e".into(),
|
||||
"<E".into(),
|
||||
"<h".into(),
|
||||
"<l".into(),
|
||||
"<q".into(),
|
||||
"<s".into(),
|
||||
"<v".into(),
|
||||
"<I".into(),
|
||||
]
|
||||
}
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
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() {}
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
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.first()?.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(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
#![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![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
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);
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
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<_>>()
|
||||
}
|
||||
|
|
@ -1,294 +0,0 @@
|
|||
mod code_lens;
|
||||
mod commands;
|
||||
mod completion;
|
||||
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: semantic_token::TYPES.into(),
|
||||
token_modifiers: semantic_token::MODIFIERS.into(),
|
||||
},
|
||||
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)),
|
||||
completion_provider: Some(CompletionOptions {
|
||||
resolve_provider: Some(false),
|
||||
trigger_characters: Some(completion::trigger_characters()),
|
||||
..Default::default()
|
||||
}),
|
||||
..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, params: CompletionParams) -> Result<Option<CompletionResponse>> {
|
||||
Ok(completion::completion(params, self))
|
||||
}
|
||||
|
||||
async fn completion_resolve(&self, params: CompletionItem) -> Result<CompletionItem> {
|
||||
Ok(params)
|
||||
}
|
||||
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
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);
|
||||
}
|
||||
|
|
@ -1,147 +0,0 @@
|
|||
use orgize::{
|
||||
export::{Container, Event, TraversalContext, Traverser},
|
||||
rowan::{ast::AstNode, TextRange},
|
||||
SyntaxKind,
|
||||
};
|
||||
use tower_lsp::lsp_types::{Range, SemanticToken, SemanticTokenModifier, SemanticTokenType};
|
||||
|
||||
use crate::org_document::OrgDocument;
|
||||
|
||||
const TIMESTAMP: SemanticTokenType = SemanticTokenType::new("timestamp");
|
||||
const HEADLINE_TODO_KEYWORD: SemanticTokenType = SemanticTokenType::new("headlineTodoKeyword");
|
||||
const HEADLINE_DONE_KEYWORD: SemanticTokenType = SemanticTokenType::new("headlineDoneKeyword");
|
||||
const HEADLINE_PRIORITY: SemanticTokenType = SemanticTokenType::new("headlinePriority");
|
||||
const HEADLINE_TAGS: SemanticTokenType = SemanticTokenType::new("headlineTags");
|
||||
|
||||
pub const TYPES: &[SemanticTokenType] = &[
|
||||
TIMESTAMP,
|
||||
HEADLINE_TODO_KEYWORD,
|
||||
HEADLINE_DONE_KEYWORD,
|
||||
HEADLINE_PRIORITY,
|
||||
HEADLINE_TAGS,
|
||||
];
|
||||
|
||||
pub const MODIFIERS: &[SemanticTokenModifier] = &[];
|
||||
|
||||
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) {
|
||||
macro_rules! m {
|
||||
($range:expr, $ty:expr $(,$modifiers:expr)*) => {{
|
||||
if let Some(token) =
|
||||
self.create_token($range.start().into(), $range.end().into(), $ty)
|
||||
{
|
||||
self.tokens.push(token);
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! s {
|
||||
($range:expr) => {
|
||||
if let Some(range) = self.range {
|
||||
if !range.contains_range($range) {
|
||||
return ctx.skip();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
match event {
|
||||
Event::Enter(Container::Section(section)) => s!(section.syntax().text_range()),
|
||||
Event::Enter(Container::Paragraph(paragraph)) => s!(paragraph.syntax().text_range()),
|
||||
Event::Enter(Container::OrgTable(table)) => s!(table.syntax().text_range()),
|
||||
Event::Enter(Container::List(list)) => s!(list.syntax().text_range()),
|
||||
Event::Enter(Container::Drawer(drawer)) => s!(drawer.syntax().text_range()),
|
||||
Event::Enter(Container::DynBlock(block)) => s!(block.syntax().text_range()),
|
||||
|
||||
Event::Enter(Container::Headline(headline)) => {
|
||||
s!(headline.syntax().text_range());
|
||||
|
||||
for ch in headline.syntax().children_with_tokens() {
|
||||
match ch.kind() {
|
||||
SyntaxKind::HEADLINE_KEYWORD_DONE => {
|
||||
m!(ch.text_range(), HEADLINE_DONE_KEYWORD)
|
||||
}
|
||||
SyntaxKind::HEADLINE_KEYWORD_TODO => {
|
||||
m!(ch.text_range(), HEADLINE_TODO_KEYWORD)
|
||||
}
|
||||
SyntaxKind::HEADLINE_TAGS => m!(ch.text_range(), HEADLINE_TAGS),
|
||||
SyntaxKind::HEADLINE_PRIORITY => m!(ch.text_range(), HEADLINE_PRIORITY),
|
||||
SyntaxKind::NEW_LINE => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Event::Timestamp(timestamp) => m!(timestamp.syntax().text_range(), TIMESTAMP),
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 create_token(
|
||||
&mut self,
|
||||
start: u32,
|
||||
end: u32,
|
||||
kind: SemanticTokenType,
|
||||
) -> Option<SemanticToken> {
|
||||
let length = end - start;
|
||||
let token_type = TYPES.iter().position(|item| item == &kind)? as u32;
|
||||
|
||||
let line = self.doc.line_of(start);
|
||||
|
||||
let start = start - self.doc.line_starts[line as usize];
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue