mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-08-04 02:08:17 +00:00
feat: postfix completion on values (#850)
* feat: postfix completion on values * dev: delete an ovo-log
This commit is contained in:
parent
d0b40dbfa6
commit
ab3716d866
6 changed files with 237 additions and 27 deletions
|
@ -76,19 +76,6 @@ impl InteractCodeContextRequest {
|
|||
|
||||
// Get mode
|
||||
let root = LinkedNode::new(source.root());
|
||||
let leaf = root.leaf_at_compat(pos);
|
||||
let mut leaf = leaf.as_ref();
|
||||
Some(loop {
|
||||
log::debug!("leaf for context: {leaf:?}");
|
||||
if let Some(t) = leaf {
|
||||
if let Some(mode) = interpret_mode_at(t.kind()) {
|
||||
break mode;
|
||||
}
|
||||
|
||||
leaf = t.parent();
|
||||
} else {
|
||||
break InterpretMode::Markup;
|
||||
}
|
||||
})
|
||||
Some(interpret_mode_at(root.leaf_at_compat(pos).as_ref()))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ pub mod ty;
|
|||
mod upstream;
|
||||
|
||||
pub use analysis::{LocalContext, LocalContextGuard, LspWorldExt};
|
||||
pub use upstream::{with_vm, CompletionFeat};
|
||||
pub use upstream::{with_vm, CompletionFeat, PostfixSnippet};
|
||||
|
||||
mod diagnostics;
|
||||
pub use diagnostics::*;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typst::foundations::{Func, ParamInfo};
|
||||
|
||||
use crate::prelude::*;
|
||||
|
@ -239,7 +239,7 @@ fn can_be_ident(node: &SyntaxNode) -> bool {
|
|||
}
|
||||
|
||||
/// A mode in which a text document is interpreted.
|
||||
#[derive(Debug, Clone, Copy, Serialize)]
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum InterpretMode {
|
||||
/// The position is in a comment.
|
||||
|
@ -256,7 +256,7 @@ pub enum InterpretMode {
|
|||
Math,
|
||||
}
|
||||
|
||||
pub(crate) fn interpret_mode_at(k: SyntaxKind) -> Option<InterpretMode> {
|
||||
pub(crate) fn interpret_mode_at_kind(k: SyntaxKind) -> Option<InterpretMode> {
|
||||
use SyntaxKind::*;
|
||||
Some(match k {
|
||||
LineComment | BlockComment => InterpretMode::Comment,
|
||||
|
@ -288,6 +288,21 @@ pub(crate) fn interpret_mode_at(k: SyntaxKind) -> Option<InterpretMode> {
|
|||
})
|
||||
}
|
||||
|
||||
pub(crate) fn interpret_mode_at(mut leaf: Option<&LinkedNode>) -> InterpretMode {
|
||||
loop {
|
||||
log::debug!("leaf for context: {leaf:?}");
|
||||
if let Some(t) = leaf {
|
||||
if let Some(mode) = interpret_mode_at_kind(t.kind()) {
|
||||
break mode;
|
||||
}
|
||||
|
||||
leaf = t.parent();
|
||||
} else {
|
||||
break InterpretMode::Markup;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DerefTarget<'a> {
|
||||
Label(LinkedNode<'a>),
|
||||
|
@ -583,7 +598,7 @@ pub fn get_check_target_by_context<'a>(
|
|||
|
||||
fn possible_in_code_trivia(sk: SyntaxKind) -> bool {
|
||||
!matches!(
|
||||
interpret_mode_at(sk),
|
||||
interpret_mode_at_kind(sk),
|
||||
Some(InterpretMode::Markup | InterpretMode::Math | InterpretMode::Comment)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ use crate::analysis::{analyze_labels, DynLabel, LocalContext, Ty};
|
|||
|
||||
mod ext;
|
||||
use ext::*;
|
||||
pub use ext::{complete_path, CompletionFeat};
|
||||
pub use ext::{complete_path, CompletionFeat, PostfixSnippet};
|
||||
|
||||
/// Autocomplete a cursor position in a source file.
|
||||
///
|
||||
|
@ -440,6 +440,8 @@ fn field_access_completions(
|
|||
);
|
||||
}
|
||||
|
||||
ctx.postfix_completions(node, value);
|
||||
|
||||
match value {
|
||||
Value::Symbol(symbol) => {
|
||||
for modifier in symbol.modifiers() {
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::ops::Deref;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use ecow::{eco_format, EcoString};
|
||||
use hashbrown::HashSet;
|
||||
use lsp_types::{CompletionItem, CompletionTextEdit, InsertTextFormat, TextEdit};
|
||||
use reflexo::path::unix_slash;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tinymist_derive::BindTyCtx;
|
||||
use tinymist_world::LspWorld;
|
||||
use typst::foundations::{AutoValue, Func, Label, NoneValue, Scope, Type, Value};
|
||||
use typst::foundations::{AutoValue, Content, Func, Label, NoneValue, Scope, Type, Value};
|
||||
use typst::syntax::ast::AstNode;
|
||||
use typst::syntax::{ast, Span, SyntaxKind, SyntaxNode};
|
||||
use typst::visualize::Color;
|
||||
|
@ -15,14 +18,52 @@ use typst::visualize::Color;
|
|||
use super::{Completion, CompletionContext, CompletionKind};
|
||||
use crate::adt::interner::Interned;
|
||||
use crate::analysis::{func_signature, BuiltinTy, PathPreference, Ty};
|
||||
use crate::syntax::{descending_decls, is_ident_like, CheckTarget, DescentDecl};
|
||||
use crate::syntax::{
|
||||
descending_decls, interpret_mode_at, is_ident_like, CheckTarget, DescentDecl, InterpretMode,
|
||||
};
|
||||
use crate::ty::{Iface, IfaceChecker, InsTy, SigTy, TyCtx, TypeBounds, TypeScheme, TypeVar};
|
||||
use crate::upstream::complete::complete_code;
|
||||
|
||||
use crate::{completion_kind, prelude::*, LspCompletion};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum PostfixSnippetScope {
|
||||
/// Any "dottable" value.
|
||||
Value,
|
||||
/// Any value having content type.
|
||||
Content,
|
||||
}
|
||||
|
||||
/// A parsed snippet
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ParsedSnippet {
|
||||
node_before: EcoString,
|
||||
node_before_before_cursor: Option<EcoString>,
|
||||
node_after: EcoString,
|
||||
}
|
||||
|
||||
/// A postfix completion snippet.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PostfixSnippet {
|
||||
/// The scope of the snippet.
|
||||
pub mode: EcoVec<InterpretMode>,
|
||||
/// The scope of the snippet.
|
||||
pub scope: PostfixSnippetScope,
|
||||
/// The snippet name.
|
||||
pub label: EcoString,
|
||||
/// The snippet name.
|
||||
pub label_detail: Option<EcoString>,
|
||||
/// The snippet detail.
|
||||
pub snippet: EcoString,
|
||||
/// The snippet description.
|
||||
pub description: EcoString,
|
||||
/// Lazily parsed snippet.
|
||||
#[serde(skip)]
|
||||
pub parsed_snippet: OnceLock<Option<ParsedSnippet>>,
|
||||
}
|
||||
|
||||
/// Tinymist's completion features.
|
||||
#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CompletionFeat {
|
||||
/// Whether to enable postfix completion.
|
||||
|
@ -33,20 +74,31 @@ pub struct CompletionFeat {
|
|||
pub postfix_ufcs_left: Option<bool>,
|
||||
/// Whether to enable ufcs completion (right variant).
|
||||
pub postfix_ufcs_right: Option<bool>,
|
||||
/// Postfix snippets.
|
||||
pub postfix_snippets: Option<EcoVec<PostfixSnippet>>,
|
||||
}
|
||||
|
||||
impl CompletionFeat {
|
||||
pub(crate) fn any_ufcs(&self) -> bool {
|
||||
self.ufcs() || self.ufcs_left() || self.ufcs_right()
|
||||
}
|
||||
pub(crate) fn postfix(&self) -> bool {
|
||||
self.postfix.unwrap_or(true)
|
||||
}
|
||||
pub(crate) fn ufcs(&self) -> bool {
|
||||
self.postfix.unwrap_or(true) && self.postfix_ufcs.unwrap_or(true)
|
||||
self.postfix() && self.postfix_ufcs.unwrap_or(true)
|
||||
}
|
||||
pub(crate) fn ufcs_left(&self) -> bool {
|
||||
self.postfix.unwrap_or(true) && self.postfix_ufcs_left.unwrap_or(true)
|
||||
self.postfix() && self.postfix_ufcs_left.unwrap_or(true)
|
||||
}
|
||||
pub(crate) fn ufcs_right(&self) -> bool {
|
||||
self.postfix.unwrap_or(true) && self.postfix_ufcs_right.unwrap_or(true)
|
||||
self.postfix() && self.postfix_ufcs_right.unwrap_or(true)
|
||||
}
|
||||
|
||||
pub(crate) fn postfix_snippets(&self) -> &[PostfixSnippet] {
|
||||
self.postfix_snippets
|
||||
.as_ref()
|
||||
.map_or(DEFAULT_POSTFIX_SNIPPET.deref(), |v| v.as_slice())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -114,6 +166,119 @@ impl<'a> CompletionContext<'a> {
|
|||
Some((src, defines))
|
||||
}
|
||||
|
||||
pub fn postfix_completions(&mut self, node: &LinkedNode, value: &Value) -> Option<()> {
|
||||
if !self.ctx.analysis.completion_feat.postfix() {
|
||||
return None;
|
||||
}
|
||||
let src = self.ctx.source_by_id(self.root.span().id()?).ok()?;
|
||||
|
||||
let _ = node;
|
||||
|
||||
let surrounding_syntax = self.surrounding_syntax();
|
||||
if !matches!(surrounding_syntax, SurroundingSyntax::Regular) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let cursor_mode = interpret_mode_at(Some(node));
|
||||
let is_content = value.ty() == Type::of::<Content>()
|
||||
|| value.ty() == Type::of::<typst::symbols::Symbol>();
|
||||
log::debug!("post snippet is_content: {is_content}");
|
||||
|
||||
let rng = node.range();
|
||||
for snippet in self.ctx.analysis.completion_feat.postfix_snippets() {
|
||||
if !snippet.mode.contains(&cursor_mode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let scope = match snippet.scope {
|
||||
PostfixSnippetScope::Value => true,
|
||||
PostfixSnippetScope::Content => is_content,
|
||||
};
|
||||
if !scope {
|
||||
continue;
|
||||
}
|
||||
log::debug!("post snippet: {}", snippet.label);
|
||||
|
||||
static TYPST_SNIPPET_PLACEHOLDER_RE: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"\$\{(.*?)\}").unwrap());
|
||||
|
||||
let parsed_snippet = snippet.parsed_snippet.get_or_init(|| {
|
||||
let split = TYPST_SNIPPET_PLACEHOLDER_RE
|
||||
.find_iter(&snippet.snippet)
|
||||
.map(|s| (&s.as_str()[2..s.as_str().len() - 1], s.start(), s.end()))
|
||||
.collect::<Vec<_>>();
|
||||
if split.len() > 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let split0 = split[0];
|
||||
let split1 = split.get(1);
|
||||
|
||||
if split0.0.contains("node") {
|
||||
Some(ParsedSnippet {
|
||||
node_before: snippet.snippet[..split0.1].into(),
|
||||
node_before_before_cursor: None,
|
||||
node_after: snippet.snippet[split0.2..].into(),
|
||||
})
|
||||
} else {
|
||||
split1.map(|split1| ParsedSnippet {
|
||||
node_before_before_cursor: Some(snippet.snippet[..split0.1].into()),
|
||||
node_before: snippet.snippet[split0.1..split1.1].into(),
|
||||
node_after: snippet.snippet[split1.2..].into(),
|
||||
})
|
||||
}
|
||||
});
|
||||
log::debug!("post snippet: {} on {:?}", snippet.label, parsed_snippet);
|
||||
let Some(ParsedSnippet {
|
||||
node_before,
|
||||
node_before_before_cursor,
|
||||
node_after,
|
||||
}) = parsed_snippet
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let base = Completion {
|
||||
kind: CompletionKind::Syntax,
|
||||
apply: Some("".into()),
|
||||
label: snippet.label.clone(),
|
||||
label_detail: snippet.label_detail.clone(),
|
||||
detail: Some(snippet.description.clone()),
|
||||
// range: Some(range),
|
||||
..Default::default()
|
||||
};
|
||||
if let Some(node_before_before_cursor) = &node_before_before_cursor {
|
||||
let node_content = node.get().clone().into_text();
|
||||
let before = TextEdit {
|
||||
range: self.ctx.to_lsp_range(rng.start..self.from, &src),
|
||||
new_text: node_before_before_cursor.into(),
|
||||
};
|
||||
|
||||
self.completions.push(Completion {
|
||||
apply: Some(eco_format!("{node_before}{node_content}{node_after}")),
|
||||
additional_text_edits: Some(vec![before]),
|
||||
..base.clone()
|
||||
});
|
||||
} else {
|
||||
let before = TextEdit {
|
||||
range: self.ctx.to_lsp_range(rng.start..rng.start, &src),
|
||||
new_text: node_before.as_ref().into(),
|
||||
};
|
||||
let after = TextEdit {
|
||||
range: self.ctx.to_lsp_range(rng.end..self.from, &src),
|
||||
new_text: "".into(),
|
||||
};
|
||||
self.completions.push(Completion {
|
||||
apply: Some(node_after.clone()),
|
||||
additional_text_edits: Some(vec![before, after]),
|
||||
..base
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
pub fn ufcs_completions(&mut self, node: &LinkedNode, value: &Value) {
|
||||
if !self.ctx.analysis.completion_feat.any_ufcs() {
|
||||
return;
|
||||
|
@ -1437,6 +1602,47 @@ pub fn symbol_label_detail(ch: char) -> EcoString {
|
|||
}
|
||||
}
|
||||
|
||||
static DEFAULT_POSTFIX_SNIPPET: LazyLock<Vec<PostfixSnippet>> = LazyLock::new(|| {
|
||||
vec![
|
||||
PostfixSnippet {
|
||||
scope: PostfixSnippetScope::Content,
|
||||
mode: eco_vec![InterpretMode::Code, InterpretMode::Markup],
|
||||
label: "aleft".into(),
|
||||
label_detail: Some(".align left".into()),
|
||||
snippet: "align(left, ${node})".into(),
|
||||
description: "wrap as with align left".into(),
|
||||
parsed_snippet: OnceLock::new(),
|
||||
},
|
||||
PostfixSnippet {
|
||||
scope: PostfixSnippetScope::Value,
|
||||
mode: eco_vec![InterpretMode::Code, InterpretMode::Markup],
|
||||
label: "if".into(),
|
||||
label_detail: Some(".if".into()),
|
||||
snippet: "if ${node} { ${} }".into(),
|
||||
description: "wrap as if expression".into(),
|
||||
parsed_snippet: OnceLock::new(),
|
||||
},
|
||||
PostfixSnippet {
|
||||
scope: PostfixSnippetScope::Value,
|
||||
mode: eco_vec![InterpretMode::Code, InterpretMode::Markup],
|
||||
label: "return".into(),
|
||||
label_detail: Some(".return".into()),
|
||||
snippet: "return ${node}".into(),
|
||||
description: "wrap as return expression".into(),
|
||||
parsed_snippet: OnceLock::new(),
|
||||
},
|
||||
PostfixSnippet {
|
||||
scope: PostfixSnippetScope::Value,
|
||||
mode: eco_vec![InterpretMode::Code, InterpretMode::Markup],
|
||||
label: "let".into(),
|
||||
label_detail: Some(".let".into()),
|
||||
snippet: "let ${_} = ${node}".into(),
|
||||
description: "wrap as let expression".into(),
|
||||
parsed_snippet: OnceLock::new(),
|
||||
},
|
||||
]
|
||||
});
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
mod tests {
|
||||
|
|
|
@ -110,7 +110,7 @@ impl LanguageState {
|
|||
allow_overlapping_token: const_config.tokens_overlapping_token_support,
|
||||
allow_multiline_token: const_config.tokens_multiline_token_support,
|
||||
remove_html: !self.config.support_html_in_markdown,
|
||||
completion_feat: self.config.completion,
|
||||
completion_feat: self.config.completion.clone(),
|
||||
color_theme: match self.compile_config().color_theme.as_deref() {
|
||||
Some("dark") => tinymist_query::ColorTheme::Dark,
|
||||
_ => tinymist_query::ColorTheme::Light,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue