chore: add orgize-{cli,common,lsp} package

This commit is contained in:
PoiScript 2023-12-20 21:56:10 +08:00
parent 6930640866
commit 4cc1130a17
No known key found for this signature in database
GPG key ID: 22C2B1249D99985E
131 changed files with 6577 additions and 56 deletions

104
orgize-lsp/src/code_lens.rs Normal file
View 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,
}
}
}

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

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

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

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

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

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

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

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

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

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

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

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