chore: remove orgize-cli & orgize-lsp

This commit is contained in:
PoiScript 2024-03-06 15:04:09 +08:00
parent edd73e3c6d
commit 42cb1d21bd
No known key found for this signature in database
GPG key ID: 22C2B1249D99985E
54 changed files with 1 additions and 7078 deletions

View file

@ -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"

View file

@ -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"

View file

@ -1,5 +0,0 @@
# Orgize CLI
Command line utilities for org-mode files, builtin with [`orgize`].
[`orgize`]: https://crates.io/crates/orgize

View file

@ -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();
}
_ => {}
}
}
}

View file

@ -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(())
}

View file

@ -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();
}
_ => {}
}
}
}

View file

@ -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(())
}
}

View file

@ -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(),
}
}

View file

@ -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();
}
_ => {}
}
}
}

View file

@ -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"

View file

@ -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())
}

View file

@ -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()))
}

View file

@ -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"
);
}

View file

@ -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"
);
}

View file

@ -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);
}};
}

View file

@ -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");
}

View file

@ -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"
);
}

View file

@ -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;

View file

@ -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")))
}

View file

@ -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
})
}

View file

@ -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"

View file

@ -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

View file

@ -1,3 +0,0 @@
dist
*.vsix
node_modules

View file

@ -1,8 +0,0 @@
**
!dist/
!syntaxes/
!media/
!images/
!package.json
!org.configuration.json
!README.md

View file

@ -1 +0,0 @@
../../LICENSE

View file

@ -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
```

View file

@ -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

View file

@ -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);
}

View file

@ -1,29 +0,0 @@
{
"comments": {
"lineComment": "#",
"blockComment": ["#+BEGIN_COMMENT\n", "\n#+END_COMMENT"]
},
"brackets": [
["{", "}"],
["[", "]"],
["(", ")"]
],
"autoClosingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
["\"", "\""]
],
"surroundingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
["\"", "\""],
["'", "'"],
["*", "*"],
["/", "/"],
["_", "_"],
["=", "="],
["~", "~"]
]
}

View file

@ -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"
}
}
]
}
}

File diff suppressed because it is too large Load diff

View file

@ -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();
}

View file

@ -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();
}
}
}
}

View file

@ -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

View file

@ -1,12 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2019",
"lib": ["ES2019"],
"outDir": "../dist",
"rootDir": "src",
"sourceMap": true
},
"include": ["src"],
"exclude": ["node_modules", ".vscode-test"]
}

View file

@ -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,
}
}
}

View file

@ -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",
_ => {}
}
}
}

View file

@ -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
}
}
}

View file

@ -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(())
}
}

View file

@ -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(())
}
}

View file

@ -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(())
}
}

View file

@ -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(),
]
}

View file

@ -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() {}

View file

@ -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(),
_ => {}
}
}
}

View file

@ -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![],
}
}
}

View file

@ -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);
}

View file

@ -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<_>>()
}

View file

@ -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(&params, 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(&params, 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;
}

View file

@ -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);
}

View file

@ -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,
})
}
}