mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-08-03 17:58:17 +00:00
docs: split and documenting code action worker (#977)
* docs: split and documenting code action worker * dev: update prelude
This commit is contained in:
parent
0d4cd77d2c
commit
678b2d2111
4 changed files with 265 additions and 259 deletions
|
@ -6,6 +6,8 @@ use std::path::Path;
|
|||
pub(crate) use bib::*;
|
||||
pub mod call;
|
||||
pub use call::*;
|
||||
pub mod code_action;
|
||||
pub use code_action::*;
|
||||
pub mod color_exprs;
|
||||
pub use color_exprs::*;
|
||||
pub mod link_exprs;
|
||||
|
|
259
crates/tinymist-query/src/analysis/code_action.rs
Normal file
259
crates/tinymist-query/src/analysis/code_action.rs
Normal file
|
@ -0,0 +1,259 @@
|
|||
//! Provides code actions for the document.
|
||||
|
||||
use regex::Regex;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::syntax::{interpret_mode_at, InterpretMode};
|
||||
|
||||
/// Analyzes the document and provides code actions.
|
||||
pub struct CodeActionWorker<'a> {
|
||||
/// The local analysis context to work with.
|
||||
ctx: &'a mut LocalContext,
|
||||
/// The source document to analyze.
|
||||
source: Source,
|
||||
/// The code actions to provide.
|
||||
pub actions: Vec<CodeActionOrCommand>,
|
||||
/// The lazily calculated local URL to [`Self::source`].
|
||||
local_url: OnceLock<Option<Url>>,
|
||||
}
|
||||
|
||||
impl<'a> CodeActionWorker<'a> {
|
||||
/// Creates a new color action worker.
|
||||
pub fn new(ctx: &'a mut LocalContext, source: Source) -> Self {
|
||||
Self {
|
||||
ctx,
|
||||
source,
|
||||
actions: Vec::new(),
|
||||
local_url: OnceLock::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn local_url(&self) -> Option<&Url> {
|
||||
self.local_url
|
||||
.get_or_init(|| self.ctx.uri_for_id(self.source.id()).ok())
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn local_edits(&self, edits: Vec<TextEdit>) -> Option<WorkspaceEdit> {
|
||||
Some(WorkspaceEdit {
|
||||
changes: Some(HashMap::from_iter([(self.local_url()?.clone(), edits)])),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn local_edit(&self, edit: TextEdit) -> Option<WorkspaceEdit> {
|
||||
self.local_edits(vec![edit])
|
||||
}
|
||||
|
||||
/// Starts to work.
|
||||
pub fn work(&mut self, root: LinkedNode, range: Range<usize>) -> Option<()> {
|
||||
let cursor = (range.start + 1).min(self.source.text().len());
|
||||
let node = root.leaf_at_compat(cursor)?;
|
||||
let mut node = &node;
|
||||
|
||||
let mut heading_resolved = false;
|
||||
let mut equation_resolved = false;
|
||||
|
||||
self.wrap_actions(node, range);
|
||||
|
||||
loop {
|
||||
match node.kind() {
|
||||
// Only the deepest heading is considered
|
||||
SyntaxKind::Heading if !heading_resolved => {
|
||||
heading_resolved = true;
|
||||
self.heading_actions(node);
|
||||
}
|
||||
// Only the deepest equation is considered
|
||||
SyntaxKind::Equation if !equation_resolved => {
|
||||
equation_resolved = true;
|
||||
self.equation_actions(node);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
node = node.parent()?;
|
||||
}
|
||||
}
|
||||
|
||||
fn wrap_actions(&mut self, node: &LinkedNode, range: Range<usize>) -> Option<()> {
|
||||
if range.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let start_mode = interpret_mode_at(Some(node));
|
||||
if !matches!(start_mode, InterpretMode::Markup | InterpretMode::Math) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let edit = self.local_edits(vec![
|
||||
TextEdit {
|
||||
range: self
|
||||
.ctx
|
||||
.to_lsp_range(range.start..range.start, &self.source),
|
||||
new_text: "#[".into(),
|
||||
},
|
||||
TextEdit {
|
||||
range: self.ctx.to_lsp_range(range.end..range.end, &self.source),
|
||||
new_text: "]".into(),
|
||||
},
|
||||
])?;
|
||||
|
||||
let action = CodeActionOrCommand::CodeAction(CodeAction {
|
||||
title: "Wrap with content block".to_string(),
|
||||
kind: Some(CodeActionKind::REFACTOR_REWRITE),
|
||||
edit: Some(edit),
|
||||
..CodeAction::default()
|
||||
});
|
||||
self.actions.push(action);
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn heading_actions(&mut self, node: &LinkedNode) -> Option<()> {
|
||||
let h = node.cast::<ast::Heading>()?;
|
||||
let depth = h.depth().get();
|
||||
|
||||
// Only the marker is replaced, for minimal text change
|
||||
let marker = node
|
||||
.children()
|
||||
.find(|e| e.kind() == SyntaxKind::HeadingMarker)?;
|
||||
let marker_range = marker.range();
|
||||
|
||||
if depth > 1 {
|
||||
// Decrease depth of heading
|
||||
let action = CodeActionOrCommand::CodeAction(CodeAction {
|
||||
title: "Decrease depth of heading".to_string(),
|
||||
kind: Some(CodeActionKind::REFACTOR_REWRITE),
|
||||
edit: Some(self.local_edit(TextEdit {
|
||||
range: self.ctx.to_lsp_range(marker_range.clone(), &self.source),
|
||||
new_text: "=".repeat(depth - 1),
|
||||
})?),
|
||||
..CodeAction::default()
|
||||
});
|
||||
self.actions.push(action);
|
||||
}
|
||||
|
||||
// Increase depth of heading
|
||||
let action = CodeActionOrCommand::CodeAction(CodeAction {
|
||||
title: "Increase depth of heading".to_string(),
|
||||
kind: Some(CodeActionKind::REFACTOR_REWRITE),
|
||||
edit: Some(self.local_edit(TextEdit {
|
||||
range: self.ctx.to_lsp_range(marker_range, &self.source),
|
||||
new_text: "=".repeat(depth + 1),
|
||||
})?),
|
||||
..CodeAction::default()
|
||||
});
|
||||
self.actions.push(action);
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn equation_actions(&mut self, node: &LinkedNode) -> Option<()> {
|
||||
let equation = node.cast::<ast::Equation>()?;
|
||||
let body = equation.body();
|
||||
let is_block = equation.block();
|
||||
|
||||
let body = node.find(body.span())?;
|
||||
let body_range = body.range();
|
||||
let node_end = node.range().end;
|
||||
|
||||
let mut chs = node.children();
|
||||
let chs = chs.by_ref();
|
||||
let first_dollar = chs.take(1).find(|e| e.kind() == SyntaxKind::Dollar)?;
|
||||
let last_dollar = chs.rev().take(1).find(|e| e.kind() == SyntaxKind::Dollar)?;
|
||||
|
||||
// Erroneous equation is skipped.
|
||||
// For example, some unclosed equation.
|
||||
if first_dollar.offset() == last_dollar.offset() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let front_range = self
|
||||
.ctx
|
||||
.to_lsp_range(first_dollar.range().end..body_range.start, &self.source);
|
||||
let back_range = self
|
||||
.ctx
|
||||
.to_lsp_range(body_range.end..last_dollar.range().start, &self.source);
|
||||
|
||||
// Retrieve punctuation to move
|
||||
let mark_after_equation = self
|
||||
.source
|
||||
.text()
|
||||
.get(node_end..)
|
||||
.and_then(|text| {
|
||||
let mut ch = text.chars();
|
||||
let nx = ch.next()?;
|
||||
Some((nx, ch.next()))
|
||||
})
|
||||
.filter(|(ch, ch_next)| {
|
||||
static IS_PUNCTUATION: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"\p{Punctuation}").unwrap());
|
||||
(ch.is_ascii_punctuation()
|
||||
&& ch_next.map_or(true, |ch_next| !ch_next.is_ascii_punctuation()))
|
||||
|| (!ch.is_ascii_punctuation() && IS_PUNCTUATION.is_match(&ch.to_string()))
|
||||
});
|
||||
let punc_modify = if let Some((nx, _)) = mark_after_equation {
|
||||
let ch_range = self
|
||||
.ctx
|
||||
.to_lsp_range(node_end..node_end + nx.len_utf8(), &self.source);
|
||||
let remove_edit = TextEdit {
|
||||
range: ch_range,
|
||||
new_text: "".to_owned(),
|
||||
};
|
||||
Some((nx, remove_edit))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let rewrite_action = |title: &str, new_text: &str| {
|
||||
let mut edits = vec![
|
||||
TextEdit {
|
||||
range: front_range,
|
||||
new_text: new_text.to_owned(),
|
||||
},
|
||||
TextEdit {
|
||||
range: back_range,
|
||||
new_text: if !new_text.is_empty() {
|
||||
if let Some((ch, _)) = &punc_modify {
|
||||
ch.to_string() + new_text
|
||||
} else {
|
||||
new_text.to_owned()
|
||||
}
|
||||
} else {
|
||||
"".to_owned()
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if !new_text.is_empty() {
|
||||
if let Some((_, edit)) = &punc_modify {
|
||||
edits.push(edit.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Some(CodeActionOrCommand::CodeAction(CodeAction {
|
||||
title: title.to_owned(),
|
||||
kind: Some(CodeActionKind::REFACTOR_REWRITE),
|
||||
edit: Some(self.local_edits(edits)?),
|
||||
..CodeAction::default()
|
||||
}))
|
||||
};
|
||||
|
||||
// Prepare actions
|
||||
let a1 = if is_block {
|
||||
rewrite_action("Convert to inline equation", "")?
|
||||
} else {
|
||||
rewrite_action("Convert to block equation", " ")?
|
||||
};
|
||||
let a2 = rewrite_action("Convert to multiple-line block equation", "\n");
|
||||
|
||||
self.actions.push(a1);
|
||||
if let Some(a2) = a2 {
|
||||
self.actions.push(a2);
|
||||
}
|
||||
|
||||
Some(())
|
||||
}
|
||||
}
|
|
@ -1,13 +1,4 @@
|
|||
use lsp_types::TextEdit;
|
||||
use once_cell::sync::{Lazy, OnceCell};
|
||||
use regex::Regex;
|
||||
use typst_shim::syntax::LinkedNodeExt;
|
||||
|
||||
use crate::{
|
||||
prelude::*,
|
||||
syntax::{interpret_mode_at, InterpretMode},
|
||||
SemanticRequest,
|
||||
};
|
||||
use crate::{analysis::CodeActionWorker, prelude::*, SemanticRequest};
|
||||
|
||||
/// The [`textDocument/codeAction`] request is sent from the client to the
|
||||
/// server to compute commands for a given text document and range. These
|
||||
|
@ -86,253 +77,6 @@ impl SemanticRequest for CodeActionRequest {
|
|||
let mut worker = CodeActionWorker::new(ctx, source.clone());
|
||||
worker.work(root, range);
|
||||
|
||||
let res = worker.actions;
|
||||
(!res.is_empty()).then_some(res)
|
||||
}
|
||||
}
|
||||
|
||||
struct CodeActionWorker<'a> {
|
||||
ctx: &'a mut LocalContext,
|
||||
actions: Vec<CodeActionOrCommand>,
|
||||
local_url: OnceCell<Option<Url>>,
|
||||
current: Source,
|
||||
}
|
||||
|
||||
impl<'a> CodeActionWorker<'a> {
|
||||
fn new(ctx: &'a mut LocalContext, current: Source) -> Self {
|
||||
Self {
|
||||
ctx,
|
||||
actions: Vec::new(),
|
||||
local_url: OnceCell::new(),
|
||||
current,
|
||||
}
|
||||
}
|
||||
|
||||
fn local_url(&self) -> Option<&Url> {
|
||||
self.local_url
|
||||
.get_or_init(|| self.ctx.uri_for_id(self.current.id()).ok())
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn local_edits(&self, edits: Vec<TextEdit>) -> Option<WorkspaceEdit> {
|
||||
Some(WorkspaceEdit {
|
||||
changes: Some(HashMap::from_iter([(self.local_url()?.clone(), edits)])),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn local_edit(&self, edit: TextEdit) -> Option<WorkspaceEdit> {
|
||||
self.local_edits(vec![edit])
|
||||
}
|
||||
|
||||
fn wrap_actions(&mut self, node: &LinkedNode, range: Range<usize>) -> Option<()> {
|
||||
if range.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let start_mode = interpret_mode_at(Some(node));
|
||||
if !matches!(start_mode, InterpretMode::Markup | InterpretMode::Math) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let edit = self.local_edits(vec![
|
||||
TextEdit {
|
||||
range: self
|
||||
.ctx
|
||||
.to_lsp_range(range.start..range.start, &self.current),
|
||||
new_text: "#[".into(),
|
||||
},
|
||||
TextEdit {
|
||||
range: self.ctx.to_lsp_range(range.end..range.end, &self.current),
|
||||
new_text: "]".into(),
|
||||
},
|
||||
])?;
|
||||
|
||||
let action = CodeActionOrCommand::CodeAction(CodeAction {
|
||||
title: "Wrap with content block".to_string(),
|
||||
kind: Some(CodeActionKind::REFACTOR_REWRITE),
|
||||
edit: Some(edit),
|
||||
..CodeAction::default()
|
||||
});
|
||||
self.actions.push(action);
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn heading_actions(&mut self, node: &LinkedNode) -> Option<()> {
|
||||
let h = node.cast::<ast::Heading>()?;
|
||||
let depth = h.depth().get();
|
||||
|
||||
// Only the marker is replaced, for minimal text change
|
||||
let marker = node
|
||||
.children()
|
||||
.find(|e| e.kind() == SyntaxKind::HeadingMarker)?;
|
||||
let marker_range = marker.range();
|
||||
|
||||
if depth > 1 {
|
||||
// Decrease depth of heading
|
||||
let action = CodeActionOrCommand::CodeAction(CodeAction {
|
||||
title: "Decrease depth of heading".to_string(),
|
||||
kind: Some(CodeActionKind::REFACTOR_REWRITE),
|
||||
edit: Some(self.local_edit(TextEdit {
|
||||
range: self.ctx.to_lsp_range(marker_range.clone(), &self.current),
|
||||
new_text: "=".repeat(depth - 1),
|
||||
})?),
|
||||
..CodeAction::default()
|
||||
});
|
||||
self.actions.push(action);
|
||||
}
|
||||
|
||||
// Increase depth of heading
|
||||
let action = CodeActionOrCommand::CodeAction(CodeAction {
|
||||
title: "Increase depth of heading".to_string(),
|
||||
kind: Some(CodeActionKind::REFACTOR_REWRITE),
|
||||
edit: Some(self.local_edit(TextEdit {
|
||||
range: self.ctx.to_lsp_range(marker_range, &self.current),
|
||||
new_text: "=".repeat(depth + 1),
|
||||
})?),
|
||||
..CodeAction::default()
|
||||
});
|
||||
self.actions.push(action);
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn equation_actions(&mut self, node: &LinkedNode) -> Option<()> {
|
||||
let equation = node.cast::<ast::Equation>()?;
|
||||
let body = equation.body();
|
||||
let is_block = equation.block();
|
||||
|
||||
let body = node.find(body.span())?;
|
||||
let body_range = body.range();
|
||||
let node_end = node.range().end;
|
||||
|
||||
let mut chs = node.children();
|
||||
let chs = chs.by_ref();
|
||||
let first_dollar = chs.take(1).find(|e| e.kind() == SyntaxKind::Dollar)?;
|
||||
let last_dollar = chs.rev().take(1).find(|e| e.kind() == SyntaxKind::Dollar)?;
|
||||
|
||||
// Erroneous equation is skipped.
|
||||
// For example, some unclosed equation.
|
||||
if first_dollar.offset() == last_dollar.offset() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let front_range = self
|
||||
.ctx
|
||||
.to_lsp_range(first_dollar.range().end..body_range.start, &self.current);
|
||||
let back_range = self
|
||||
.ctx
|
||||
.to_lsp_range(body_range.end..last_dollar.range().start, &self.current);
|
||||
|
||||
// Retrieve punctuation to move
|
||||
let mark_after_equation = self
|
||||
.current
|
||||
.text()
|
||||
.get(node_end..)
|
||||
.and_then(|text| {
|
||||
let mut ch = text.chars();
|
||||
let nx = ch.next()?;
|
||||
Some((nx, ch.next()))
|
||||
})
|
||||
.filter(|(ch, ch_next)| {
|
||||
static IS_PUNCTUATION: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"\p{Punctuation}").unwrap());
|
||||
(ch.is_ascii_punctuation()
|
||||
&& ch_next.map_or(true, |ch_next| !ch_next.is_ascii_punctuation()))
|
||||
|| (!ch.is_ascii_punctuation() && IS_PUNCTUATION.is_match(&ch.to_string()))
|
||||
});
|
||||
let punc_modify = if let Some((nx, _)) = mark_after_equation {
|
||||
let ch_range = self
|
||||
.ctx
|
||||
.to_lsp_range(node_end..node_end + nx.len_utf8(), &self.current);
|
||||
let remove_edit = TextEdit {
|
||||
range: ch_range,
|
||||
new_text: "".to_owned(),
|
||||
};
|
||||
Some((nx, remove_edit))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let rewrite_action = |title: &str, new_text: &str| {
|
||||
let mut edits = vec![
|
||||
TextEdit {
|
||||
range: front_range,
|
||||
new_text: new_text.to_owned(),
|
||||
},
|
||||
TextEdit {
|
||||
range: back_range,
|
||||
new_text: if !new_text.is_empty() {
|
||||
if let Some((ch, _)) = &punc_modify {
|
||||
ch.to_string() + new_text
|
||||
} else {
|
||||
new_text.to_owned()
|
||||
}
|
||||
} else {
|
||||
"".to_owned()
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if !new_text.is_empty() {
|
||||
if let Some((_, edit)) = &punc_modify {
|
||||
edits.push(edit.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Some(CodeActionOrCommand::CodeAction(CodeAction {
|
||||
title: title.to_owned(),
|
||||
kind: Some(CodeActionKind::REFACTOR_REWRITE),
|
||||
edit: Some(self.local_edits(edits)?),
|
||||
..CodeAction::default()
|
||||
}))
|
||||
};
|
||||
|
||||
// Prepare actions
|
||||
let a1 = if is_block {
|
||||
rewrite_action("Convert to inline equation", "")?
|
||||
} else {
|
||||
rewrite_action("Convert to block equation", " ")?
|
||||
};
|
||||
let a2 = rewrite_action("Convert to multiple-line block equation", "\n");
|
||||
|
||||
self.actions.push(a1);
|
||||
if let Some(a2) = a2 {
|
||||
self.actions.push(a2);
|
||||
}
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn work(&mut self, root: LinkedNode, range: Range<usize>) -> Option<()> {
|
||||
let cursor = (range.start + 1).min(self.current.text().len());
|
||||
let node = root.leaf_at_compat(cursor)?;
|
||||
let mut node = &node;
|
||||
|
||||
let mut heading_resolved = false;
|
||||
let mut equation_resolved = false;
|
||||
|
||||
self.wrap_actions(node, range);
|
||||
|
||||
loop {
|
||||
match node.kind() {
|
||||
// Only the deepest heading is considered
|
||||
SyntaxKind::Heading if !heading_resolved => {
|
||||
heading_resolved = true;
|
||||
self.heading_actions(node);
|
||||
}
|
||||
// Only the deepest equation is considered
|
||||
SyntaxKind::Equation if !equation_resolved => {
|
||||
equation_resolved = true;
|
||||
self.equation_actions(node);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
node = node.parent()?;
|
||||
}
|
||||
(!worker.actions.is_empty()).then_some(worker.actions)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ pub use std::collections::HashMap;
|
|||
pub use std::iter;
|
||||
pub use std::ops::Range;
|
||||
pub use std::path::{Path, PathBuf};
|
||||
pub use std::sync::{Arc, LazyLock};
|
||||
pub use std::sync::{Arc, LazyLock, OnceLock};
|
||||
|
||||
pub use ecow::{eco_vec, EcoVec};
|
||||
pub use itertools::{Format, Itertools};
|
||||
|
@ -25,6 +25,7 @@ pub use typst::syntax::{
|
|||
FileId as TypstFileId, LinkedNode, Source, Spanned, SyntaxKind, SyntaxNode,
|
||||
};
|
||||
pub use typst::World;
|
||||
pub use typst_shim::syntax::LinkedNodeExt;
|
||||
|
||||
pub use crate::analysis::{Definition, LocalContext};
|
||||
pub use crate::docs::DefDocs;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue