feat: provide code action to rewrite headings (#240)

* feat: provide code action to rewrite headings

* dev: update snapshot
This commit is contained in:
Myriad-Dreamin 2024-05-05 23:25:03 +08:00 committed by GitHub
parent 6ad9258740
commit b8143e7090
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 157 additions and 9 deletions

View file

@ -0,0 +1,130 @@
use lsp_types::TextEdit;
use once_cell::sync::OnceCell;
use crate::{prelude::*, SemanticRequest};
/// The [`textDocument/codeLens`] request is sent from the client to the server
/// to compute code lenses for a given text document.
///
/// [`textDocument/codeLens`]: https://microsoft.github.io/language-server-protocol/specification#textDocument_codeLens
#[derive(Debug, Clone)]
pub struct CodeActionRequest {
/// The path of the document to request for.
pub path: PathBuf,
/// The range of the document to get code actions for.
pub range: LspRange,
}
impl SemanticRequest for CodeActionRequest {
type Response = Vec<CodeActionOrCommand>;
fn request(self, ctx: &mut AnalysisContext) -> Option<Self::Response> {
let source = ctx.source_by_path(&self.path).ok()?;
let range = ctx.to_typst_range(self.range, &source)?;
let cursor = (range.start + 1).min(source.text().len());
// todo: don't ignore the range end
let root = LinkedNode::new(source.root());
let mut worker = CodeActionWorker::new(ctx, source.clone());
worker.work(root, cursor);
let res = worker.actions;
(!res.is_empty()).then_some(res)
}
}
struct CodeActionWorker<'a, 'w> {
ctx: &'a mut AnalysisContext<'w>,
actions: Vec<CodeActionOrCommand>,
local_url: OnceCell<Option<Url>>,
current: Source,
}
impl<'a, 'w> CodeActionWorker<'a, 'w> {
fn new(ctx: &'a mut AnalysisContext<'w>, 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(|| {
let id = self.current.id();
let path = self.ctx.path_for_id(id).ok()?;
path_to_url(path.as_path()).ok()
})
.as_ref()
}
fn local_edit(&self, edit: TextEdit) -> Option<WorkspaceEdit> {
Some(WorkspaceEdit {
changes: Some(HashMap::from_iter([(
self.local_url()?.clone(),
vec![edit],
)])),
..Default::default()
})
}
fn heading_actions(&mut self, leaf: &LinkedNode) -> Option<()> {
let h = leaf.cast::<ast::Heading>()?;
let depth = h.depth().get();
// Only the marker is replaced, for minimal text change
let marker = leaf
.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 work(&mut self, root: LinkedNode, cursor: usize) -> Option<()> {
let mut node = root.leaf_at(cursor)?;
let mut heading_resolved = false;
loop {
match node.kind() {
// Only the deepest heading is considered
SyntaxKind::Heading if !heading_resolved => {
heading_resolved = true;
self.heading_actions(&node);
}
_ => {}
}
node = node.parent()?.clone();
}
}
}

View file

@ -20,6 +20,8 @@ pub use analysis::AnalysisContext;
use typst::{model::Document as TypstDocument, syntax::Source}; use typst::{model::Document as TypstDocument, syntax::Source};
pub use diagnostics::*; pub use diagnostics::*;
pub(crate) mod code_action;
pub use code_action::*;
pub(crate) mod code_context; pub(crate) mod code_context;
pub use code_context::*; pub use code_context::*;
pub(crate) mod code_lens; pub(crate) mod code_lens;
@ -205,6 +207,7 @@ mod polymorphic {
InlayHint(InlayHintRequest), InlayHint(InlayHintRequest),
DocumentColor(DocumentColorRequest), DocumentColor(DocumentColorRequest),
ColorPresentation(ColorPresentationRequest), ColorPresentation(ColorPresentationRequest),
CodeAction(CodeActionRequest),
CodeLens(CodeLensRequest), CodeLens(CodeLensRequest),
Completion(CompletionRequest), Completion(CompletionRequest),
SignatureHelp(SignatureHelpRequest), SignatureHelp(SignatureHelpRequest),
@ -236,6 +239,7 @@ mod polymorphic {
CompilerQueryRequest::InlayHint(..) => Unique, CompilerQueryRequest::InlayHint(..) => Unique,
CompilerQueryRequest::DocumentColor(..) => PinnedFirst, CompilerQueryRequest::DocumentColor(..) => PinnedFirst,
CompilerQueryRequest::ColorPresentation(..) => ContextFreeUnique, CompilerQueryRequest::ColorPresentation(..) => ContextFreeUnique,
CompilerQueryRequest::CodeAction(..) => Unique,
CompilerQueryRequest::CodeLens(..) => Unique, CompilerQueryRequest::CodeLens(..) => Unique,
CompilerQueryRequest::Completion(..) => Mergeable, CompilerQueryRequest::Completion(..) => Mergeable,
CompilerQueryRequest::SignatureHelp(..) => PinnedFirst, CompilerQueryRequest::SignatureHelp(..) => PinnedFirst,
@ -266,6 +270,7 @@ mod polymorphic {
CompilerQueryRequest::InlayHint(req) => &req.path, CompilerQueryRequest::InlayHint(req) => &req.path,
CompilerQueryRequest::DocumentColor(req) => &req.path, CompilerQueryRequest::DocumentColor(req) => &req.path,
CompilerQueryRequest::ColorPresentation(req) => &req.path, CompilerQueryRequest::ColorPresentation(req) => &req.path,
CompilerQueryRequest::CodeAction(req) => &req.path,
CompilerQueryRequest::CodeLens(req) => &req.path, CompilerQueryRequest::CodeLens(req) => &req.path,
CompilerQueryRequest::Completion(req) => &req.path, CompilerQueryRequest::Completion(req) => &req.path,
CompilerQueryRequest::SignatureHelp(req) => &req.path, CompilerQueryRequest::SignatureHelp(req) => &req.path,
@ -297,6 +302,7 @@ mod polymorphic {
InlayHint(Option<Vec<InlayHint>>), InlayHint(Option<Vec<InlayHint>>),
DocumentColor(Option<Vec<ColorInformation>>), DocumentColor(Option<Vec<ColorInformation>>),
ColorPresentation(Option<Vec<ColorPresentation>>), ColorPresentation(Option<Vec<ColorPresentation>>),
CodeAction(Option<Vec<CodeActionOrCommand>>),
CodeLens(Option<Vec<CodeLens>>), CodeLens(Option<Vec<CodeLens>>),
Completion(Option<CompletionResponse>), Completion(Option<CompletionResponse>),
SignatureHelp(Option<SignatureHelp>), SignatureHelp(Option<SignatureHelp>),

View file

@ -9,13 +9,13 @@ pub use ecow::EcoVec;
pub use itertools::{Format, Itertools}; pub use itertools::{Format, Itertools};
pub use log::{error, trace}; pub use log::{error, trace};
pub use lsp_types::{ pub use lsp_types::{
request::GotoDeclarationResponse, CodeLens, ColorInformation, ColorPresentation, request::GotoDeclarationResponse, CodeAction, CodeActionKind, CodeActionOrCommand, CodeLens,
CompletionResponse, DiagnosticRelatedInformation, DocumentSymbol, DocumentSymbolResponse, ColorInformation, ColorPresentation, CompletionResponse, DiagnosticRelatedInformation,
Documentation, FoldingRange, GotoDefinitionResponse, Hover, InlayHint, LanguageString, DocumentSymbol, DocumentSymbolResponse, Documentation, FoldingRange, GotoDefinitionResponse,
Location as LspLocation, LocationLink, MarkedString, MarkupContent, MarkupKind, Hover, InlayHint, LanguageString, Location as LspLocation, LocationLink, MarkedString,
Position as LspPosition, PrepareRenameResponse, SelectionRange, SemanticTokens, MarkupContent, MarkupKind, Position as LspPosition, PrepareRenameResponse, SelectionRange,
SemanticTokensDelta, SemanticTokensFullDeltaResult, SemanticTokensResult, SignatureHelp, SemanticTokens, SemanticTokensDelta, SemanticTokensFullDeltaResult, SemanticTokensResult,
SignatureInformation, SymbolInformation, Url, WorkspaceEdit, SignatureHelp, SignatureInformation, SymbolInformation, Url, WorkspaceEdit,
}; };
pub use reflexo::vector::ir::DefId; pub use reflexo::vector::ir::DefId;
pub use serde_json::Value as JsonValue; pub use serde_json::Value as JsonValue;

View file

@ -307,6 +307,7 @@ impl TypstLanguageServer {
request_fn!(DocumentColor, Self::document_color), request_fn!(DocumentColor, Self::document_color),
request_fn!(ColorPresentationRequest, Self::color_presentation), request_fn!(ColorPresentationRequest, Self::color_presentation),
request_fn!(HoverRequest, Self::hover), request_fn!(HoverRequest, Self::hover),
request_fn!(CodeActionRequest, Self::code_action),
request_fn!(CodeLensRequest, Self::code_lens), request_fn!(CodeLensRequest, Self::code_lens),
request_fn!(FoldingRangeRequest, Self::folding_range), request_fn!(FoldingRangeRequest, Self::folding_range),
request_fn!(SignatureHelpRequest, Self::signature_help), request_fn!(SignatureHelpRequest, Self::signature_help),
@ -1195,6 +1196,15 @@ impl TypstLanguageServer {
run_query!(self.ColorPresentation(path, color, range)) run_query!(self.ColorPresentation(path, color, range))
} }
fn code_action(
&mut self,
params: CodeActionParams,
) -> LspResult<Option<Vec<CodeActionOrCommand>>> {
let path = as_path(params.text_document);
let range = params.range;
run_query!(self.CodeAction(path, range))
}
fn code_lens(&mut self, params: CodeLensParams) -> LspResult<Option<Vec<CodeLens>>> { fn code_lens(&mut self, params: CodeLensParams) -> LspResult<Option<Vec<CodeLens>>> {
let path = as_path(params.text_document); let path = as_path(params.text_document);
run_query!(self.CodeLens(path)) run_query!(self.CodeLens(path))

View file

@ -479,6 +479,7 @@ impl Init {
}), }),
document_formatting_provider, document_formatting_provider,
inlay_hint_provider: Some(OneOf::Left(true)), inlay_hint_provider: Some(OneOf::Left(true)),
code_action_provider: Some(CodeActionProviderCapability::Simple(true)),
code_lens_provider: Some(CodeLensOptions { code_lens_provider: Some(CodeLensOptions {
resolve_provider: Some(false), resolve_provider: Some(false),
}), }),

View file

@ -263,6 +263,7 @@ impl TypstLanguageServer {
References(req) => query_world!(client, References, req), References(req) => query_world!(client, References, req),
InlayHint(req) => query_world!(client, InlayHint, req), InlayHint(req) => query_world!(client, InlayHint, req),
DocumentColor(req) => query_world!(client, DocumentColor, req), DocumentColor(req) => query_world!(client, DocumentColor, req),
CodeAction(req) => query_world!(client, CodeAction, req),
CodeLens(req) => query_world!(client, CodeLens, req), CodeLens(req) => query_world!(client, CodeLens, req),
Completion(req) => query_state!(client, Completion, req), Completion(req) => query_state!(client, Completion, req),
SignatureHelp(req) => query_world!(client, SignatureHelp, req), SignatureHelp(req) => query_world!(client, SignatureHelp, req),

View file

@ -374,7 +374,7 @@ fn e2e() {
}); });
let hash = replay_log(&tinymist_binary, &root.join("neovim")); let hash = replay_log(&tinymist_binary, &root.join("neovim"));
insta::assert_snapshot!(hash, @"siphash128_13:e271d06cb84deee97e86b0b2763d741a"); insta::assert_snapshot!(hash, @"siphash128_13:8a283a742935063715fbc8af6bff2b88");
} }
{ {
@ -385,7 +385,7 @@ fn e2e() {
}); });
let hash = replay_log(&tinymist_binary, &root.join("vscode")); let hash = replay_log(&tinymist_binary, &root.join("vscode"));
insta::assert_snapshot!(hash, @"siphash128_13:86c5b1c533652019b3d99f7079e19270"); insta::assert_snapshot!(hash, @"siphash128_13:5f118cda0949759f157a2c22b9433775");
} }
} }