chore: add orgize-{cli,common,lsp} package
This commit is contained in:
parent
6930640866
commit
4cc1130a17
131 changed files with 6577 additions and 56 deletions
104
orgize-lsp/src/code_lens.rs
Normal file
104
orgize-lsp/src/code_lens.rs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
use orgize::{
|
||||
export::{Container, Event, TraversalContext, Traverser},
|
||||
rowan::ast::AstNode,
|
||||
};
|
||||
use orgize_common::{header_argument, property_drawer, property_keyword};
|
||||
use tower_lsp::lsp_types::{CodeLens, Url};
|
||||
|
||||
use crate::org_document::OrgDocument;
|
||||
|
||||
use super::OrgizeCommand;
|
||||
|
||||
pub struct CodeLensTraverser<'a> {
|
||||
pub url: Url,
|
||||
pub doc: &'a OrgDocument,
|
||||
pub lens: Vec<CodeLens>,
|
||||
}
|
||||
|
||||
impl<'a> Traverser for CodeLensTraverser<'a> {
|
||||
fn event(&mut self, event: Event, ctx: &mut TraversalContext) {
|
||||
match event {
|
||||
Event::Enter(Container::SourceBlock(block)) => {
|
||||
let start = block.begin();
|
||||
|
||||
let arg1 = block.parameters().unwrap_or_default();
|
||||
let arg2 = property_drawer(block.syntax()).unwrap_or_default();
|
||||
let arg3 = property_keyword(block.syntax()).unwrap_or_default();
|
||||
|
||||
let range = self.doc.range_of(start, start);
|
||||
|
||||
let tangle = header_argument(&arg1, &arg2, &arg3, ":tangle", "no");
|
||||
|
||||
if header_argument(&arg1, &arg2, &arg3, ":results", "no") != "no" {
|
||||
self.lens.push(CodeLens {
|
||||
range,
|
||||
command: Some(
|
||||
OrgizeCommand::SrcBlockExecute {
|
||||
block_offset: start,
|
||||
url: self.url.clone(),
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
data: None,
|
||||
});
|
||||
}
|
||||
|
||||
if tangle != "no" {
|
||||
self.lens.push(CodeLens {
|
||||
range,
|
||||
command: Some(
|
||||
OrgizeCommand::SrcBlockTangle {
|
||||
block_offset: start,
|
||||
url: self.url.clone(),
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
data: None,
|
||||
});
|
||||
|
||||
self.lens.push(CodeLens {
|
||||
range,
|
||||
command: Some(
|
||||
OrgizeCommand::SrcBlockDetangle {
|
||||
block_offset: start,
|
||||
url: self.url.clone(),
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
data: None,
|
||||
});
|
||||
}
|
||||
|
||||
ctx.skip();
|
||||
}
|
||||
Event::Enter(Container::Headline(headline)) => {
|
||||
if headline.tags().any(|t| t.eq_ignore_ascii_case("TOC")) {
|
||||
let start = headline.begin();
|
||||
|
||||
self.lens.push(CodeLens {
|
||||
range: self.doc.range_of(start, start),
|
||||
command: Some(
|
||||
OrgizeCommand::HeadlineToc {
|
||||
heading_offset: start,
|
||||
url: self.url.clone(),
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
data: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> CodeLensTraverser<'a> {
|
||||
pub fn new(url: Url, doc: &'a OrgDocument) -> Self {
|
||||
CodeLensTraverser {
|
||||
url,
|
||||
lens: vec![],
|
||||
doc,
|
||||
}
|
||||
}
|
||||
}
|
||||
101
orgize-lsp/src/commands/headline_toc.rs
Normal file
101
orgize-lsp/src/commands/headline_toc.rs
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
use orgize::{
|
||||
export::{Container, Event, TraversalContext, Traverser},
|
||||
rowan::ast::AstNode,
|
||||
SyntaxKind,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Write;
|
||||
use tower_lsp::lsp_types::{TextEdit, Url, WorkspaceEdit};
|
||||
|
||||
use crate::Backend;
|
||||
|
||||
impl Backend {
|
||||
pub async fn headline_toc(&self, url: Url, headline_offset: u32) {
|
||||
let uri = url.to_string();
|
||||
|
||||
let Some(doc) = self.documents.get(&uri) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut toc = Toc {
|
||||
indent: 0,
|
||||
output: String::new(),
|
||||
|
||||
headline_offset,
|
||||
edit_range: None,
|
||||
};
|
||||
|
||||
doc.traverse(&mut toc);
|
||||
|
||||
if let Some((start, end)) = toc.edit_range {
|
||||
let mut changes = HashMap::new();
|
||||
|
||||
let range = doc.range_of(start, end);
|
||||
|
||||
changes.insert(
|
||||
url,
|
||||
vec![TextEdit {
|
||||
new_text: toc.output,
|
||||
range,
|
||||
}],
|
||||
);
|
||||
|
||||
let _ = self
|
||||
.client
|
||||
.apply_edit(WorkspaceEdit {
|
||||
changes: Some(changes),
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Toc {
|
||||
output: String,
|
||||
indent: usize,
|
||||
|
||||
headline_offset: u32,
|
||||
|
||||
edit_range: Option<(u32, u32)>,
|
||||
}
|
||||
|
||||
impl Traverser for Toc {
|
||||
fn event(&mut self, event: Event, ctx: &mut TraversalContext) {
|
||||
match event {
|
||||
Event::Enter(Container::Headline(headline)) => {
|
||||
if headline.begin() == self.headline_offset {
|
||||
let start = headline
|
||||
.syntax()
|
||||
.children_with_tokens()
|
||||
.find(|n| n.kind() == SyntaxKind::NEW_LINE)
|
||||
.map(|n| n.text_range().end().into())
|
||||
.unwrap_or(headline.end());
|
||||
|
||||
let end = headline.end();
|
||||
|
||||
self.edit_range = Some((start, end));
|
||||
} else {
|
||||
let title = headline.title().map(|e| e.to_string()).collect::<String>();
|
||||
|
||||
let slug = orgize_common::headline_slug(&headline);
|
||||
|
||||
let _ = writeln!(
|
||||
&mut self.output,
|
||||
"{: >i$}- [[#{slug}][{title}]]",
|
||||
"",
|
||||
i = self.indent
|
||||
);
|
||||
}
|
||||
|
||||
self.indent += 2;
|
||||
}
|
||||
Event::Leave(Container::Headline(_)) => self.indent -= 2,
|
||||
|
||||
Event::Enter(Container::Section(_)) => ctx.skip(),
|
||||
Event::Enter(Container::Document(_)) => self.output += "#+begin_quote\n",
|
||||
Event::Leave(Container::Document(_)) => self.output += "#+end_quote\n\n",
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
116
orgize-lsp/src/commands/mod.rs
Normal file
116
orgize-lsp/src/commands/mod.rs
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
mod headline_toc;
|
||||
mod src_block_detangle;
|
||||
mod src_block_execute;
|
||||
mod src_block_tangle;
|
||||
|
||||
use orgize::rowan::ast::AstNode;
|
||||
use serde_json::{json, Value};
|
||||
use tower_lsp::lsp_types::{Command, ExecuteCommandParams, MessageType, Url};
|
||||
|
||||
use crate::Backend;
|
||||
|
||||
pub enum OrgizeCommand {
|
||||
SrcBlockExecute { url: Url, block_offset: u32 },
|
||||
|
||||
SrcBlockTangle { url: Url, block_offset: u32 },
|
||||
|
||||
SrcBlockDetangle { url: Url, block_offset: u32 },
|
||||
|
||||
HeadlineToc { url: Url, heading_offset: u32 },
|
||||
}
|
||||
|
||||
impl From<OrgizeCommand> for Command {
|
||||
fn from(val: OrgizeCommand) -> Self {
|
||||
match val {
|
||||
OrgizeCommand::SrcBlockExecute { url, block_offset } => Command {
|
||||
command: "orgize.src-block.execute".into(),
|
||||
arguments: Some(vec![json!(url), json!(block_offset)]),
|
||||
title: "Execute".into(),
|
||||
},
|
||||
OrgizeCommand::SrcBlockTangle { url, block_offset } => Command {
|
||||
command: "orgize.src-block.tangle".into(),
|
||||
arguments: Some(vec![json!(url), json!(block_offset)]),
|
||||
title: "Tangle".into(),
|
||||
},
|
||||
OrgizeCommand::SrcBlockDetangle { url, block_offset } => Command {
|
||||
command: "orgize.src-block.detangle".into(),
|
||||
arguments: Some(vec![json!(url), json!(block_offset)]),
|
||||
title: "Detangle".into(),
|
||||
},
|
||||
OrgizeCommand::HeadlineToc {
|
||||
url,
|
||||
heading_offset,
|
||||
} => Command {
|
||||
command: "orgize.headline.toc".into(),
|
||||
arguments: Some(vec![json!(url), json!(heading_offset)]),
|
||||
title: "Generate TOC".into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OrgizeCommand {
|
||||
pub fn all() -> Vec<String> {
|
||||
vec![
|
||||
"orgize.src-block.execute".into(),
|
||||
"orgize.src-block.tangle".into(),
|
||||
"orgize.src-block.detangle".into(),
|
||||
"orgize.src-block.open-tangle-dest".into(),
|
||||
"orgize.headline.toc".into(),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn execute(params: &ExecuteCommandParams, backend: &Backend) -> Option<Value> {
|
||||
let result = match (
|
||||
params.command.as_str(),
|
||||
params.arguments.get(0).and_then(|x| x.as_str()),
|
||||
params.arguments.get(1).and_then(|x| x.as_u64()),
|
||||
) {
|
||||
("orgize.src-block.execute", Some(s), Some(n)) => backend
|
||||
.src_block_execute(s.parse().ok()?, n as u32)
|
||||
.await
|
||||
.map(|_| None),
|
||||
("orgize.src-block.tangle", Some(s), Some(n)) => backend
|
||||
.src_block_tangle(s.parse().ok()?, n as u32)
|
||||
.await
|
||||
.map(|_| None),
|
||||
("orgize.src-block.detangle", Some(s), Some(n)) => backend
|
||||
.src_block_detangle(s.parse().ok()?, n as u32)
|
||||
.await
|
||||
.map(|_| None),
|
||||
("orgize.headline.toc", Some(s), Some(n)) => {
|
||||
backend.headline_toc(s.parse().ok()?, n as u32).await;
|
||||
Ok(None)
|
||||
}
|
||||
("orgize.syntax-tree", Some(s), _) => {
|
||||
if let Some(doc) = backend.documents.get(s) {
|
||||
Ok(Some(json!(format!("{:#?}", doc.org.document().syntax()))))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
("orgize.preview-html", Some(s), _) => {
|
||||
if let Some(doc) = backend.documents.get(s) {
|
||||
Ok(Some(json!(format!("{}", doc.org.to_html()))))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
_ => Ok(None),
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
backend
|
||||
.client
|
||||
.show_message(
|
||||
MessageType::ERROR,
|
||||
format!("Failed to execute {:?}: {}", params.command, err),
|
||||
)
|
||||
.await;
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
53
orgize-lsp/src/commands/src_block_detangle.rs
Normal file
53
orgize-lsp/src/commands/src_block_detangle.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use orgize::{ast::SourceBlock, rowan::ast::AstNode};
|
||||
use tower_lsp::lsp_types::{MessageType, TextEdit, Url, WorkspaceEdit};
|
||||
|
||||
use crate::Backend;
|
||||
|
||||
impl Backend {
|
||||
pub async fn src_block_detangle(&self, url: Url, block_offset: u32) -> anyhow::Result<()> {
|
||||
let uri = url.to_string();
|
||||
|
||||
let Some(doc) = self.documents.get(&uri) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some(block) = doc
|
||||
.org
|
||||
.document()
|
||||
.syntax()
|
||||
.descendants()
|
||||
.filter_map(SourceBlock::cast)
|
||||
.find(|n| n.begin() == block_offset)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Ok(file_path) = url.to_file_path() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if let Some((start, end, new_text)) = orgize_common::detangle(block, &file_path)? {
|
||||
let mut changes = HashMap::new();
|
||||
|
||||
let range = doc.range_of(start as u32, end as u32);
|
||||
|
||||
changes.insert(url, vec![TextEdit { new_text, range }]);
|
||||
|
||||
let _ = self
|
||||
.client
|
||||
.apply_edit(WorkspaceEdit {
|
||||
changes: Some(changes),
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
} else {
|
||||
self.client
|
||||
.show_message(MessageType::WARNING, "Code block can't be detangled.")
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
51
orgize-lsp/src/commands/src_block_execute.rs
Normal file
51
orgize-lsp/src/commands/src_block_execute.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use orgize::{ast::SourceBlock, rowan::ast::AstNode};
|
||||
use tower_lsp::lsp_types::{MessageType, TextEdit, Url, WorkspaceEdit};
|
||||
|
||||
use crate::Backend;
|
||||
|
||||
impl Backend {
|
||||
pub async fn src_block_execute(&self, url: Url, block_offset: u32) -> anyhow::Result<()> {
|
||||
let uri = url.to_string();
|
||||
|
||||
let Some(doc) = self.documents.get(&uri) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some(block) = doc
|
||||
.org
|
||||
.document()
|
||||
.syntax()
|
||||
.descendants()
|
||||
.filter_map(SourceBlock::cast)
|
||||
.find(|n| n.begin() == block_offset)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
if let Some((start, end, new_text)) = orgize_common::execute(block, dir.path())? {
|
||||
let mut changes = HashMap::new();
|
||||
|
||||
let range = doc.range_of(start as u32, end as u32);
|
||||
|
||||
changes.insert(url, vec![TextEdit { new_text, range }]);
|
||||
|
||||
let _ = self
|
||||
.client
|
||||
.apply_edit(WorkspaceEdit {
|
||||
changes: Some(changes),
|
||||
..Default::default()
|
||||
})
|
||||
.await;
|
||||
} else {
|
||||
self.client
|
||||
.show_message(MessageType::WARNING, "Code block can't be executed.")
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
49
orgize-lsp/src/commands/src_block_tangle.rs
Normal file
49
orgize-lsp/src/commands/src_block_tangle.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
use orgize::{ast::SourceBlock, rowan::ast::AstNode};
|
||||
use std::fs;
|
||||
use tower_lsp::lsp_types::{MessageType, Url};
|
||||
|
||||
use crate::Backend;
|
||||
|
||||
impl Backend {
|
||||
pub async fn src_block_tangle(&self, url: Url, block_offset: u32) -> anyhow::Result<()> {
|
||||
let uri = url.to_string();
|
||||
|
||||
let Some(doc) = self.documents.get(&uri) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some(block) = doc
|
||||
.org
|
||||
.document()
|
||||
.syntax()
|
||||
.descendants()
|
||||
.filter_map(SourceBlock::cast)
|
||||
.find(|n| n.begin() == block_offset)
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Ok(file_path) = url.to_file_path() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if let Some((dest, _permission, contents, _mkdir)) =
|
||||
orgize_common::tangle(block, &file_path)?
|
||||
{
|
||||
fs::write(&dest, contents)?;
|
||||
|
||||
self.client
|
||||
.show_message(
|
||||
MessageType::INFO,
|
||||
format!("Wrote to {}", dest.to_string_lossy()),
|
||||
)
|
||||
.await;
|
||||
} else {
|
||||
self.client
|
||||
.show_message(MessageType::WARNING, "Code block can't be tangled.")
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
129
orgize-lsp/src/document_link.rs
Normal file
129
orgize-lsp/src/document_link.rs
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
use orgize::{
|
||||
ast::{Link, SourceBlock},
|
||||
export::{Container, Event, TraversalContext, Traverser},
|
||||
rowan::ast::{support, AstNode},
|
||||
SyntaxKind,
|
||||
};
|
||||
use orgize_common::header_argument;
|
||||
use resolve_path::PathResolveExt;
|
||||
use serde_json::json;
|
||||
use std::path::PathBuf;
|
||||
use tower_lsp::lsp_types::{DocumentLink, Url};
|
||||
|
||||
use crate::org_document::OrgDocument;
|
||||
|
||||
pub struct DocumentLinkTraverser<'a> {
|
||||
pub doc: &'a OrgDocument,
|
||||
pub links: Vec<DocumentLink>,
|
||||
pub path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl<'a> Traverser for DocumentLinkTraverser<'a> {
|
||||
fn event(&mut self, event: Event, ctx: &mut TraversalContext) {
|
||||
let Some(base) = &self.path else {
|
||||
return ctx.skip();
|
||||
};
|
||||
|
||||
match event {
|
||||
Event::Enter(Container::Link(link)) => {
|
||||
if let Some(link) = link_path(link, base, self.doc) {
|
||||
self.links.push(link);
|
||||
}
|
||||
ctx.skip();
|
||||
}
|
||||
Event::Enter(Container::SourceBlock(block)) => {
|
||||
if let Some(link) = block_tangle(block, base, self.doc) {
|
||||
self.links.push(link);
|
||||
}
|
||||
ctx.skip();
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn link_path(link: Link, base: &PathBuf, doc: &OrgDocument) -> Option<DocumentLink> {
|
||||
let path = support::token(link.syntax(), SyntaxKind::LINK_PATH)
|
||||
.or_else(|| support::token(link.syntax(), SyntaxKind::TEXT))?;
|
||||
|
||||
let path_str = path.text();
|
||||
|
||||
let (target, data) = if let Some(file) = path_str.strip_prefix("file:") {
|
||||
let path = file.try_resolve_in(base).ok()?;
|
||||
(Some(Url::from_file_path(path).ok()?), None)
|
||||
} else if path_str.starts_with('/') || path_str.starts_with("./") || path_str.starts_with("~/")
|
||||
{
|
||||
let path = path_str.try_resolve_in(base).ok()?;
|
||||
(Some(Url::from_file_path(path).ok()?), None)
|
||||
} else if path_str.starts_with("http://") || path_str.starts_with("https://") {
|
||||
(Some(Url::parse(path_str).ok()?), None)
|
||||
} else if let Some(id) = path_str.strip_prefix('#') {
|
||||
let url = Url::from_file_path(base).ok()?;
|
||||
(
|
||||
None,
|
||||
Some(json!(vec![
|
||||
"headline-id".to_string(),
|
||||
url.to_string(),
|
||||
id.to_string()
|
||||
])),
|
||||
)
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(DocumentLink {
|
||||
range: doc.range_of(
|
||||
path.text_range().start().into(),
|
||||
path.text_range().end().into(),
|
||||
),
|
||||
tooltip: Some("Jump to link".into()),
|
||||
target,
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
fn block_tangle(block: SourceBlock, base: &PathBuf, doc: &OrgDocument) -> Option<DocumentLink> {
|
||||
let parameters = block
|
||||
.syntax()
|
||||
.children()
|
||||
.find(|e| e.kind() == SyntaxKind::BLOCK_BEGIN)
|
||||
.into_iter()
|
||||
.flat_map(|n| n.children_with_tokens())
|
||||
.filter_map(|n| n.into_token())
|
||||
.find(|n| n.kind() == SyntaxKind::SRC_BLOCK_PARAMETERS)?;
|
||||
|
||||
let tangle = header_argument(parameters.text(), "", "", ":tangle", "no");
|
||||
|
||||
if tangle == "no" {
|
||||
return None;
|
||||
}
|
||||
|
||||
let path = tangle.try_resolve_in(base).ok()?;
|
||||
let url = Url::from_file_path(path).ok()?;
|
||||
|
||||
let start: u32 = parameters.text_range().start().into();
|
||||
|
||||
let index = parameters.text().find(tangle).unwrap_or_default() as u32;
|
||||
|
||||
let len = tangle.len() as u32;
|
||||
|
||||
Some(DocumentLink {
|
||||
range: doc.range_of(start + index, start + index + len),
|
||||
tooltip: Some("Jump to tangle destination".into()),
|
||||
target: Some(url),
|
||||
data: None,
|
||||
})
|
||||
}
|
||||
|
||||
impl<'a> DocumentLinkTraverser<'a> {
|
||||
pub fn new(doc: &'a OrgDocument, path: Option<PathBuf>) -> Self {
|
||||
DocumentLinkTraverser {
|
||||
path,
|
||||
links: vec![],
|
||||
doc,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve() {}
|
||||
81
orgize-lsp/src/document_link_resolve.rs
Normal file
81
orgize-lsp/src/document_link_resolve.rs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
use orgize::export::{Container, Event, TraversalContext, Traverser};
|
||||
use tower_lsp::lsp_types::{DocumentLink, Url};
|
||||
|
||||
use crate::{org_document::OrgDocument, Backend};
|
||||
|
||||
pub fn document_link_resolve(
|
||||
document_link: &DocumentLink,
|
||||
backend: &Backend,
|
||||
) -> Option<DocumentLink> {
|
||||
// don't need to resolve
|
||||
if document_link.target.is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let data = document_link.data.as_ref()?;
|
||||
let data = data.as_array()?;
|
||||
|
||||
match (
|
||||
data.get(0)?.as_str()?,
|
||||
data.get(1)?.as_str()?,
|
||||
data.get(2)?.as_str()?,
|
||||
) {
|
||||
("headline-id", url, id) => {
|
||||
let mut parsed = Url::parse(url).ok()?;
|
||||
|
||||
let doc = backend.documents.get(url)?;
|
||||
let mut h = HeadlineIdTraverser {
|
||||
id: id.to_string(),
|
||||
line_number: None,
|
||||
doc: &doc,
|
||||
};
|
||||
doc.traverse(&mut h);
|
||||
if let Some(line) = h.line_number.take() {
|
||||
// results is zero-based
|
||||
parsed.set_fragment(Some(&(line + 1).to_string()));
|
||||
Some(DocumentLink {
|
||||
target: Some(parsed),
|
||||
data: None,
|
||||
tooltip: None,
|
||||
range: document_link.range,
|
||||
})
|
||||
} else {
|
||||
Some(DocumentLink {
|
||||
target: Some(parsed),
|
||||
data: None,
|
||||
tooltip: None,
|
||||
range: document_link.range,
|
||||
})
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
struct HeadlineIdTraverser<'a> {
|
||||
id: String,
|
||||
line_number: Option<u32>,
|
||||
doc: &'a OrgDocument,
|
||||
}
|
||||
|
||||
impl<'a> Traverser for HeadlineIdTraverser<'a> {
|
||||
fn event(&mut self, event: Event, ctx: &mut TraversalContext) {
|
||||
if self.line_number.is_some() {
|
||||
return ctx.stop();
|
||||
}
|
||||
|
||||
match event {
|
||||
Event::Enter(Container::Document(_)) => {}
|
||||
Event::Enter(Container::Headline(headline)) => {
|
||||
let slug = orgize_common::headline_slug(&headline);
|
||||
|
||||
if slug == self.id {
|
||||
let line = self.doc.line_of(headline.begin());
|
||||
self.line_number = Some(line + 1)
|
||||
}
|
||||
}
|
||||
Event::Enter(Container::Section(_)) => ctx.skip(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
66
orgize-lsp/src/document_symbol.rs
Normal file
66
orgize-lsp/src/document_symbol.rs
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
#![allow(deprecated)]
|
||||
|
||||
use orgize::{
|
||||
export::{Container, Event, TraversalContext, Traverser},
|
||||
rowan::ast::AstNode,
|
||||
SyntaxKind,
|
||||
};
|
||||
use tower_lsp::lsp_types::{DocumentSymbol, SymbolKind};
|
||||
|
||||
use crate::org_document::OrgDocument;
|
||||
|
||||
pub struct DocumentSymbolTraverser<'a> {
|
||||
pub doc: &'a OrgDocument,
|
||||
pub stack: Vec<usize>,
|
||||
pub symbols: Vec<DocumentSymbol>,
|
||||
}
|
||||
|
||||
impl<'a> Traverser for DocumentSymbolTraverser<'a> {
|
||||
fn event(&mut self, event: Event, ctx: &mut TraversalContext) {
|
||||
match event {
|
||||
Event::Enter(Container::Headline(headline)) => {
|
||||
let mut symbols = &mut self.symbols;
|
||||
for &i in &self.stack {
|
||||
symbols = symbols[i].children.get_or_insert(vec![]);
|
||||
}
|
||||
|
||||
let name = headline
|
||||
.syntax()
|
||||
.children_with_tokens()
|
||||
.take_while(|n| n.kind() != SyntaxKind::NEW_LINE)
|
||||
.map(|n| n.to_string())
|
||||
.collect::<String>();
|
||||
|
||||
let start = headline.begin();
|
||||
let end = headline.end() - 1;
|
||||
|
||||
self.stack.push(symbols.len());
|
||||
symbols.push(DocumentSymbol {
|
||||
children: None,
|
||||
name,
|
||||
detail: None,
|
||||
kind: SymbolKind::STRING,
|
||||
tags: Some(vec![]),
|
||||
range: self.doc.range_of(start, end),
|
||||
selection_range: self.doc.range_of(start, end),
|
||||
deprecated: None,
|
||||
});
|
||||
}
|
||||
Event::Leave(Container::Headline(_)) => {
|
||||
self.stack.pop();
|
||||
}
|
||||
Event::Enter(Container::Section(_)) => ctx.skip(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> DocumentSymbolTraverser<'a> {
|
||||
pub fn new(doc: &'a OrgDocument) -> Self {
|
||||
DocumentSymbolTraverser {
|
||||
doc,
|
||||
stack: vec![],
|
||||
symbols: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
94
orgize-lsp/src/folding_range.rs
Normal file
94
orgize-lsp/src/folding_range.rs
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
use orgize::{
|
||||
export::{Container, Event, TraversalContext, Traverser},
|
||||
rowan::ast::AstNode,
|
||||
SyntaxKind, SyntaxNode,
|
||||
};
|
||||
use tower_lsp::lsp_types::{FoldingRange, FoldingRangeKind};
|
||||
|
||||
use crate::org_document::OrgDocument;
|
||||
|
||||
pub struct FoldingRangeTraverser<'a> {
|
||||
pub doc: &'a OrgDocument,
|
||||
pub ranges: Vec<FoldingRange>,
|
||||
}
|
||||
|
||||
impl<'a> Traverser for FoldingRangeTraverser<'a> {
|
||||
fn event(&mut self, event: Event, _: &mut TraversalContext) {
|
||||
let syntax = match &event {
|
||||
Event::Enter(Container::Headline(i)) => i.syntax(),
|
||||
Event::Enter(Container::OrgTable(i)) => i.syntax(),
|
||||
Event::Enter(Container::TableEl(i)) => i.syntax(),
|
||||
Event::Enter(Container::List(i)) => i.syntax(),
|
||||
Event::Enter(Container::Drawer(i)) => i.syntax(),
|
||||
Event::Enter(Container::DynBlock(i)) => i.syntax(),
|
||||
Event::Enter(Container::SpecialBlock(i)) => i.syntax(),
|
||||
Event::Enter(Container::QuoteBlock(i)) => i.syntax(),
|
||||
Event::Enter(Container::CenterBlock(i)) => i.syntax(),
|
||||
Event::Enter(Container::VerseBlock(i)) => i.syntax(),
|
||||
Event::Enter(Container::CommentBlock(i)) => i.syntax(),
|
||||
Event::Enter(Container::ExampleBlock(i)) => i.syntax(),
|
||||
Event::Enter(Container::ExportBlock(i)) => i.syntax(),
|
||||
Event::Enter(Container::SourceBlock(i)) => i.syntax(),
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let (start, end) = if syntax.kind() == SyntaxKind::HEADLINE {
|
||||
let range = syntax.text_range();
|
||||
(range.start().into(), range.end().into())
|
||||
} else {
|
||||
get_block_folding_range(syntax)
|
||||
};
|
||||
|
||||
let start_line = self.doc.line_of(start);
|
||||
let end_line = self.doc.line_of(end - 1);
|
||||
|
||||
if start_line != end_line {
|
||||
self.ranges.push(FoldingRange {
|
||||
start_line,
|
||||
end_line,
|
||||
kind: Some(FoldingRangeKind::Region),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_block_folding_range(syntax: &SyntaxNode) -> (u32, u32) {
|
||||
let start: u32 = syntax.text_range().start().into();
|
||||
|
||||
// don't include blank lines in folding range
|
||||
let end = syntax
|
||||
.children()
|
||||
.take_while(|n| n.kind() != SyntaxKind::BLANK_LINE)
|
||||
.last();
|
||||
|
||||
let end: u32 = end.map(|n| n.text_range().end().into()).unwrap_or(start);
|
||||
|
||||
(start, end)
|
||||
}
|
||||
|
||||
impl<'a> FoldingRangeTraverser<'a> {
|
||||
pub fn new(doc: &'a OrgDocument) -> Self {
|
||||
FoldingRangeTraverser {
|
||||
ranges: vec![],
|
||||
doc,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
let doc = OrgDocument::new("\n* a\n\n* b\n\n");
|
||||
let mut t = FoldingRangeTraverser::new(&doc);
|
||||
doc.traverse(&mut t);
|
||||
assert_eq!(t.ranges[0].start_line, 1);
|
||||
assert_eq!(t.ranges[0].end_line, 2);
|
||||
assert_eq!(t.ranges[1].start_line, 3);
|
||||
assert_eq!(t.ranges[1].end_line, 4);
|
||||
|
||||
let doc = OrgDocument::new("\n\r\n#+begin_src\n#+end_src\n\r\r");
|
||||
let mut t = FoldingRangeTraverser::new(&doc);
|
||||
doc.traverse(&mut t);
|
||||
assert_eq!(t.ranges[0].start_line, 2);
|
||||
assert_eq!(t.ranges[0].end_line, 3);
|
||||
}
|
||||
13
orgize-lsp/src/formatting.rs
Normal file
13
orgize-lsp/src/formatting.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
use tower_lsp::lsp_types::TextEdit;
|
||||
|
||||
use crate::org_document::OrgDocument;
|
||||
|
||||
pub fn formatting(doc: &OrgDocument) -> Vec<TextEdit> {
|
||||
orgize_common::formatting(&doc.org)
|
||||
.into_iter()
|
||||
.map(|(start, end, content)| TextEdit {
|
||||
range: doc.range_of(start as u32, end as u32),
|
||||
new_text: content,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
285
orgize-lsp/src/main.rs
Normal file
285
orgize-lsp/src/main.rs
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
mod code_lens;
|
||||
mod commands;
|
||||
mod document_link;
|
||||
mod document_link_resolve;
|
||||
mod document_symbol;
|
||||
mod folding_range;
|
||||
mod formatting;
|
||||
mod org_document;
|
||||
mod semantic_token;
|
||||
|
||||
use dashmap::DashMap;
|
||||
use document_symbol::DocumentSymbolTraverser;
|
||||
use org_document::OrgDocument;
|
||||
use serde_json::Value;
|
||||
use tower_lsp::lsp_types::*;
|
||||
use tower_lsp::{jsonrpc::Result, Client, LanguageServer, LspService, Server};
|
||||
|
||||
pub use self::code_lens::*;
|
||||
pub use self::commands::*;
|
||||
pub use self::document_link::*;
|
||||
pub use self::folding_range::*;
|
||||
pub use self::semantic_token::*;
|
||||
|
||||
pub struct Backend {
|
||||
client: Client,
|
||||
|
||||
documents: DashMap<String, OrgDocument>,
|
||||
}
|
||||
|
||||
#[tower_lsp::async_trait]
|
||||
impl LanguageServer for Backend {
|
||||
async fn initialize(&self, _: InitializeParams) -> Result<InitializeResult> {
|
||||
Ok(InitializeResult {
|
||||
server_info: None,
|
||||
offset_encoding: None,
|
||||
capabilities: ServerCapabilities {
|
||||
text_document_sync: Some(TextDocumentSyncCapability::Kind(
|
||||
TextDocumentSyncKind::INCREMENTAL,
|
||||
)),
|
||||
execute_command_provider: Some(ExecuteCommandOptions {
|
||||
commands: OrgizeCommand::all(),
|
||||
work_done_progress_options: Default::default(),
|
||||
}),
|
||||
workspace: Some(WorkspaceServerCapabilities {
|
||||
workspace_folders: Some(WorkspaceFoldersServerCapabilities {
|
||||
supported: Some(true),
|
||||
change_notifications: Some(OneOf::Left(true)),
|
||||
}),
|
||||
file_operations: None,
|
||||
}),
|
||||
semantic_tokens_provider: Some(
|
||||
SemanticTokensServerCapabilities::SemanticTokensRegistrationOptions(
|
||||
SemanticTokensRegistrationOptions {
|
||||
text_document_registration_options: {
|
||||
TextDocumentRegistrationOptions {
|
||||
document_selector: Some(vec![DocumentFilter {
|
||||
language: Some("org".to_string()),
|
||||
scheme: Some("file".to_string()),
|
||||
pattern: None,
|
||||
}]),
|
||||
}
|
||||
},
|
||||
semantic_tokens_options: SemanticTokensOptions {
|
||||
work_done_progress_options: WorkDoneProgressOptions::default(),
|
||||
legend: SemanticTokensLegend {
|
||||
token_types: LEGEND_TYPE.into(),
|
||||
token_modifiers: vec![],
|
||||
},
|
||||
range: Some(true),
|
||||
full: Some(SemanticTokensFullOptions::Bool(true)),
|
||||
},
|
||||
static_registration_options: StaticRegistrationOptions::default(),
|
||||
},
|
||||
),
|
||||
),
|
||||
code_lens_provider: Some(CodeLensOptions {
|
||||
resolve_provider: Some(true),
|
||||
}),
|
||||
folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)),
|
||||
document_link_provider: Some(DocumentLinkOptions {
|
||||
resolve_provider: Some(true),
|
||||
work_done_progress_options: WorkDoneProgressOptions::default(),
|
||||
}),
|
||||
document_formatting_provider: Some(OneOf::Left(true)),
|
||||
document_symbol_provider: Some(OneOf::Left(true)),
|
||||
..ServerCapabilities::default()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async fn initialized(&self, _: InitializedParams) {
|
||||
self.client
|
||||
.log_message(MessageType::INFO, "Orgize LSP initialized")
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn shutdown(&self) -> Result<()> {
|
||||
self.client
|
||||
.log_message(MessageType::INFO, "Orgize LSP shutdown")
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn did_open(&self, params: DidOpenTextDocumentParams) {
|
||||
let url = params.text_document.uri.to_string();
|
||||
|
||||
self.documents
|
||||
.insert(url.clone(), OrgDocument::new(params.text_document.text));
|
||||
}
|
||||
|
||||
async fn did_change(&self, params: DidChangeTextDocumentParams) {
|
||||
let url = params.text_document.uri.to_string();
|
||||
|
||||
for change in params.content_changes {
|
||||
if let (Some(mut doc), Some(range)) = (self.documents.get_mut(&url), change.range) {
|
||||
let start = doc.offset_of(range.start);
|
||||
let end = doc.offset_of(range.end);
|
||||
doc.update(start, end, &change.text);
|
||||
} else {
|
||||
self.documents
|
||||
.insert(url.clone(), OrgDocument::new(change.text));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn did_save(&self, _: DidSaveTextDocumentParams) {}
|
||||
|
||||
async fn did_close(&self, _: DidCloseTextDocumentParams) {}
|
||||
|
||||
async fn did_change_configuration(&self, _: DidChangeConfigurationParams) {}
|
||||
|
||||
async fn did_change_workspace_folders(&self, _: DidChangeWorkspaceFoldersParams) {}
|
||||
|
||||
async fn did_change_watched_files(&self, _: DidChangeWatchedFilesParams) {}
|
||||
|
||||
async fn completion(&self, _: CompletionParams) -> Result<Option<CompletionResponse>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn semantic_tokens_full(
|
||||
&self,
|
||||
params: SemanticTokensParams,
|
||||
) -> Result<Option<SemanticTokensResult>> {
|
||||
let uri = params.text_document.uri.to_string();
|
||||
|
||||
let Some(doc) = self.documents.get(&uri) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut traverser = SemanticTokenTraverser::new(&doc);
|
||||
|
||||
doc.traverse(&mut traverser);
|
||||
|
||||
Ok(Some(SemanticTokensResult::Tokens(SemanticTokens {
|
||||
result_id: None,
|
||||
data: traverser.tokens,
|
||||
})))
|
||||
}
|
||||
|
||||
async fn semantic_tokens_range(
|
||||
&self,
|
||||
params: SemanticTokensRangeParams,
|
||||
) -> Result<Option<SemanticTokensRangeResult>> {
|
||||
let uri = params.text_document.uri.to_string();
|
||||
|
||||
let Some(doc) = self.documents.get(&uri) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut traverser = SemanticTokenTraverser::with_range(&doc, params.range);
|
||||
|
||||
doc.traverse(&mut traverser);
|
||||
|
||||
Ok(Some(SemanticTokensRangeResult::Partial(
|
||||
SemanticTokensPartialResult {
|
||||
data: traverser.tokens,
|
||||
},
|
||||
)))
|
||||
}
|
||||
|
||||
async fn document_link(&self, params: DocumentLinkParams) -> Result<Option<Vec<DocumentLink>>> {
|
||||
let uri = params.text_document.uri.to_string();
|
||||
|
||||
let Some(doc) = self.documents.get(&uri) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut traverser =
|
||||
DocumentLinkTraverser::new(&doc, params.text_document.uri.to_file_path().ok());
|
||||
|
||||
doc.traverse(&mut traverser);
|
||||
|
||||
Ok(Some(traverser.links))
|
||||
}
|
||||
|
||||
async fn document_link_resolve(&self, params: DocumentLink) -> Result<DocumentLink> {
|
||||
if let Some(link) = document_link_resolve::document_link_resolve(¶ms, self) {
|
||||
Ok(link)
|
||||
} else {
|
||||
Ok(params)
|
||||
}
|
||||
}
|
||||
|
||||
async fn folding_range(&self, params: FoldingRangeParams) -> Result<Option<Vec<FoldingRange>>> {
|
||||
let uri = params.text_document.uri.to_string();
|
||||
|
||||
let Some(doc) = self.documents.get(&uri) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut traverser = FoldingRangeTraverser::new(&doc);
|
||||
|
||||
doc.traverse(&mut traverser);
|
||||
|
||||
Ok(Some(traverser.ranges))
|
||||
}
|
||||
|
||||
async fn code_lens(&self, params: CodeLensParams) -> Result<Option<Vec<CodeLens>>> {
|
||||
let uri = params.text_document.uri.to_string();
|
||||
|
||||
let Some(doc) = self.documents.get(&uri) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut traverser = CodeLensTraverser::new(params.text_document.uri, &doc);
|
||||
|
||||
doc.traverse(&mut traverser);
|
||||
|
||||
Ok(Some(traverser.lens))
|
||||
}
|
||||
|
||||
async fn code_lens_resolve(&self, params: CodeLens) -> Result<CodeLens> {
|
||||
Ok(params)
|
||||
}
|
||||
|
||||
async fn code_action(&self, _: CodeActionParams) -> Result<Option<CodeActionResponse>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn formatting(&self, params: DocumentFormattingParams) -> Result<Option<Vec<TextEdit>>> {
|
||||
let uri = params.text_document.uri.to_string();
|
||||
|
||||
let Some(doc) = self.documents.get(&uri) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let edits = formatting::formatting(&doc);
|
||||
Ok(Some(edits))
|
||||
}
|
||||
|
||||
async fn execute_command(&self, params: ExecuteCommandParams) -> Result<Option<Value>> {
|
||||
let value = commands::execute(¶ms, self).await;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
async fn document_symbol(
|
||||
&self,
|
||||
params: DocumentSymbolParams,
|
||||
) -> Result<Option<DocumentSymbolResponse>> {
|
||||
let uri = params.text_document.uri.to_string();
|
||||
|
||||
let Some(doc) = self.documents.get(&uri) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut t = DocumentSymbolTraverser::new(&doc);
|
||||
doc.traverse(&mut t);
|
||||
dbg!(&t.symbols);
|
||||
Ok(Some(DocumentSymbolResponse::Nested(t.symbols)))
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let stdin = tokio::io::stdin();
|
||||
let stdout = tokio::io::stdout();
|
||||
|
||||
let (service, socket) = LspService::build(|client| Backend {
|
||||
client,
|
||||
documents: DashMap::new(),
|
||||
})
|
||||
.finish();
|
||||
|
||||
Server::new(stdin, stdout, socket).serve(service).await;
|
||||
}
|
||||
128
orgize-lsp/src/org_document.rs
Normal file
128
orgize-lsp/src/org_document.rs
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
use orgize::{export::Traverser, Org};
|
||||
use std::iter::once;
|
||||
use tower_lsp::lsp_types::{Position, Range};
|
||||
|
||||
pub struct OrgDocument {
|
||||
pub text: String,
|
||||
pub line_starts: Vec<u32>,
|
||||
pub org: Org,
|
||||
}
|
||||
|
||||
impl OrgDocument {
|
||||
pub fn new(text: impl AsRef<str>) -> Self {
|
||||
let text = text.as_ref().to_string();
|
||||
|
||||
OrgDocument {
|
||||
org: Org::parse(&text),
|
||||
line_starts: line_starts(&text),
|
||||
text,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, start: u32, end: u32, text: &str) {
|
||||
self.text
|
||||
.replace_range((start as usize)..(end as usize), text);
|
||||
|
||||
self.line_starts = line_starts(&self.text);
|
||||
|
||||
self.org = Org::parse(&self.text);
|
||||
}
|
||||
|
||||
pub fn position_of(&self, offset: u32) -> Position {
|
||||
let line = self
|
||||
.line_starts
|
||||
.binary_search(&offset)
|
||||
.unwrap_or_else(|i| i - 1);
|
||||
|
||||
let line_start = self.line_starts[line];
|
||||
|
||||
let character = self.text.as_str()[(line_start as usize)..(offset as usize)]
|
||||
.chars()
|
||||
.count();
|
||||
|
||||
Position::new(line as u32, character as u32)
|
||||
}
|
||||
|
||||
pub fn line_of(&self, offset: u32) -> u32 {
|
||||
self.line_starts
|
||||
.binary_search(&offset)
|
||||
.unwrap_or_else(|i| i - 1) as u32
|
||||
}
|
||||
|
||||
pub fn range_of(&self, start_offset: u32, end_offset: u32) -> Range {
|
||||
Range::new(self.position_of(start_offset), self.position_of(end_offset))
|
||||
}
|
||||
|
||||
pub fn offset_of(&self, position: Position) -> u32 {
|
||||
let line_start = self.line_starts[position.line as usize] as usize;
|
||||
|
||||
let index = self.text.as_str()[line_start..]
|
||||
.char_indices()
|
||||
.nth(position.character as usize)
|
||||
.map(|(i, _)| i)
|
||||
.unwrap_or_default();
|
||||
|
||||
(line_start + index) as u32
|
||||
}
|
||||
|
||||
pub fn traverse<H: Traverser>(&self, h: &mut H) {
|
||||
self.org.traverse(h);
|
||||
}
|
||||
}
|
||||
|
||||
fn line_starts(text: &str) -> Vec<u32> {
|
||||
let bytes = text.as_bytes();
|
||||
|
||||
once(0)
|
||||
.chain(
|
||||
memchr::memchr2_iter(b'\r', b'\n', bytes)
|
||||
.filter(|&i| bytes[i] == b'\n' || !matches!(bytes.get(i + 1), Some(b'\n')))
|
||||
.map(|i| (i + 1) as u32),
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
let doc = OrgDocument::new(
|
||||
r#"* toc :toc:
|
||||
|
||||
fsfs
|
||||
fasdfs
|
||||
|
||||
|
||||
|
||||
fasdfs
|
||||
|
||||
*a* _a_ /1/ ~default~ =default= a_a
|
||||
|
||||
# abc
|
||||
|
||||
* abc12121
|
||||
12121
|
||||
|
||||
|
||||
#+begin_src javascript
|
||||
console.log(a);
|
||||
#+end_src
|
||||
|
||||
"#,
|
||||
);
|
||||
|
||||
let start = 12;
|
||||
let start_position = Position {
|
||||
line: 1,
|
||||
character: 0,
|
||||
};
|
||||
let end = 81;
|
||||
let end_position = Position {
|
||||
line: 13,
|
||||
character: 0,
|
||||
};
|
||||
|
||||
assert_eq!(doc.position_of(start), start_position);
|
||||
assert_eq!(doc.position_of(end), end_position);
|
||||
|
||||
assert_eq!(doc.offset_of(start_position), start);
|
||||
assert_eq!(doc.offset_of(end_position), end);
|
||||
}
|
||||
123
orgize-lsp/src/semantic_token.rs
Normal file
123
orgize-lsp/src/semantic_token.rs
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
use orgize::{
|
||||
export::{Container, Event, TraversalContext, Traverser},
|
||||
rowan::{ast::AstNode, TextRange},
|
||||
};
|
||||
use tower_lsp::lsp_types::{Range, SemanticToken, SemanticTokenType};
|
||||
|
||||
use crate::org_document::OrgDocument;
|
||||
|
||||
/// Semantic token types that are used for highlighting
|
||||
pub const LEGEND_TYPE: &[SemanticTokenType] = &[SemanticTokenType::COMMENT];
|
||||
|
||||
pub struct SemanticTokenTraverser<'a> {
|
||||
pub doc: &'a OrgDocument,
|
||||
|
||||
pub range: Option<TextRange>,
|
||||
|
||||
pub tokens: Vec<SemanticToken>,
|
||||
pub previous_line: u32,
|
||||
pub previous_start: u32,
|
||||
}
|
||||
|
||||
impl<'a> Traverser for SemanticTokenTraverser<'a> {
|
||||
fn event(&mut self, event: Event, ctx: &mut TraversalContext) {
|
||||
match event {
|
||||
Event::Enter(Container::Comment(comment)) => {
|
||||
let range = comment.syntax().text_range();
|
||||
|
||||
if self.contains_range(range) {
|
||||
if let Some(token) = self.create_token(
|
||||
comment.begin(),
|
||||
comment.end(),
|
||||
SemanticTokenType::COMMENT,
|
||||
) {
|
||||
self.tokens.push(token);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.skip();
|
||||
}
|
||||
Event::Enter(Container::CommentBlock(comment)) => {
|
||||
let range = comment.syntax().text_range();
|
||||
|
||||
if self.contains_range(range) {
|
||||
if let Some(token) = self.create_token(
|
||||
comment.begin(),
|
||||
comment.end(),
|
||||
SemanticTokenType::COMMENT,
|
||||
) {
|
||||
self.tokens.push(token);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.skip();
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SemanticTokenTraverser<'a> {
|
||||
pub fn new(doc: &'a OrgDocument) -> Self {
|
||||
SemanticTokenTraverser {
|
||||
doc,
|
||||
range: None,
|
||||
previous_line: 0,
|
||||
previous_start: 0,
|
||||
tokens: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_range(doc: &'a OrgDocument, range: Range) -> Self {
|
||||
let start = doc.offset_of(range.start);
|
||||
let end = doc.offset_of(range.end);
|
||||
|
||||
SemanticTokenTraverser {
|
||||
doc,
|
||||
range: Some(TextRange::new(start.into(), end.into())),
|
||||
previous_line: 0,
|
||||
previous_start: 0,
|
||||
tokens: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn contains_range(&self, range: TextRange) -> bool {
|
||||
match self.range {
|
||||
Some(r) => r.contains_range(range),
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn create_token(
|
||||
&mut self,
|
||||
start: u32,
|
||||
end: u32,
|
||||
kind: SemanticTokenType,
|
||||
) -> Option<SemanticToken> {
|
||||
let length = end - start;
|
||||
let token_type = LEGEND_TYPE.iter().position(|item| item == &kind)? as u32;
|
||||
|
||||
let line = self.doc.line_of(start);
|
||||
let first = self.doc.line_of(line);
|
||||
let start = self.doc.line_of(start) - first;
|
||||
|
||||
let delta_line = line - self.previous_line;
|
||||
let delta_start = if delta_line == 0 {
|
||||
start - self.previous_start
|
||||
} else {
|
||||
start
|
||||
};
|
||||
|
||||
self.previous_line = line;
|
||||
self.previous_start = start;
|
||||
|
||||
Some(SemanticToken {
|
||||
delta_line,
|
||||
delta_start,
|
||||
length,
|
||||
token_type,
|
||||
token_modifiers_bitset: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue