mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-08-04 10:18:16 +00:00
refactor: complete paths without hacking (#971)
* refactor: complete paths without hacking * dev: move code after refactor
This commit is contained in:
parent
ab234634a9
commit
969cc6d339
7 changed files with 165 additions and 181 deletions
|
@ -333,9 +333,11 @@ impl LocalContext {
|
|||
/// Get all the source files in the workspace.
|
||||
pub fn source_files(&self) -> &Vec<TypstFileId> {
|
||||
self.caches.root_files.get_or_init(|| {
|
||||
self.completion_files(&PathPreference::Source)
|
||||
.copied()
|
||||
.collect()
|
||||
self.completion_files(&PathPreference::Source {
|
||||
allow_package: false,
|
||||
})
|
||||
.copied()
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -305,6 +305,11 @@ impl<'a> PostTypeChecker<'a> {
|
|||
crate::log_debug_ct!("post check target iterated: {:?}", resp.bounds);
|
||||
Some(resp.finalize())
|
||||
}
|
||||
CheckTarget::ImportPath(..) | CheckTarget::IncludePath(..) => Some(Ty::Builtin(
|
||||
crate::ty::BuiltinTy::Path(crate::ty::PathPreference::Source {
|
||||
allow_package: true,
|
||||
}),
|
||||
)),
|
||||
CheckTarget::Normal(target) => {
|
||||
let ty = self.check_context_or(&target, context_ty)?;
|
||||
crate::log_debug_ct!("post check target normal: {ty:?}");
|
||||
|
|
|
@ -7,10 +7,10 @@ use regex::{Captures, Regex};
|
|||
use typst_shim::syntax::LinkedNodeExt;
|
||||
|
||||
use crate::{
|
||||
analysis::{BuiltinTy, InsTy, Ty},
|
||||
analysis::{InsTy, Ty},
|
||||
prelude::*,
|
||||
syntax::{is_ident_like, DerefTarget},
|
||||
upstream::{autocomplete, complete_path, CompletionContext},
|
||||
upstream::{autocomplete, CompletionContext},
|
||||
StatefulRequest,
|
||||
};
|
||||
|
||||
|
@ -104,7 +104,7 @@ impl StatefulRequest for CompletionRequest {
|
|||
// Skip if an error node starts with number (e.g. `1pt`)
|
||||
if matches!(
|
||||
deref_target,
|
||||
Some(DerefTarget::Callee(..) | DerefTarget::VarAccess(..) | DerefTarget::Normal(..),)
|
||||
Some(DerefTarget::Callee(..) | DerefTarget::VarAccess(..) | DerefTarget::Normal(..))
|
||||
) {
|
||||
let node = LinkedNode::new(source.root()).leaf_at_compat(cursor)?;
|
||||
if node.erroneous() {
|
||||
|
@ -122,163 +122,125 @@ impl StatefulRequest for CompletionRequest {
|
|||
}
|
||||
}
|
||||
|
||||
// Do some completion specific to the deref target
|
||||
let mut ident_like = None;
|
||||
let mut completion_result = None;
|
||||
let is_callee = matches!(deref_target, Some(DerefTarget::Callee(..)));
|
||||
match deref_target {
|
||||
Some(DerefTarget::Callee(..) | DerefTarget::VarAccess(..)) => {
|
||||
let node = LinkedNode::new(source.root()).leaf_at_compat(cursor)?;
|
||||
if is_ident_like(&node) {
|
||||
ident_like = Some(node);
|
||||
}
|
||||
}
|
||||
Some(DerefTarget::ImportPath(v) | DerefTarget::IncludePath(v)) => {
|
||||
if !v.text().starts_with(r#""@"#) {
|
||||
completion_result = complete_path(
|
||||
ctx,
|
||||
Some(v),
|
||||
&source,
|
||||
cursor,
|
||||
&crate::analysis::PathPreference::Source,
|
||||
);
|
||||
}
|
||||
}
|
||||
Some(DerefTarget::Normal(SyntaxKind::Str, cano_expr)) => {
|
||||
let parent = cano_expr.parent()?;
|
||||
if matches!(parent.kind(), SyntaxKind::Named | SyntaxKind::Args) {
|
||||
let ty_chk = ctx.type_check(&source);
|
||||
|
||||
let ty = ty_chk.type_of_span(cano_expr.span());
|
||||
crate::log_debug_ct!("check string ty: {ty:?}");
|
||||
if let Some(Ty::Builtin(BuiltinTy::Path(path_filter))) = ty {
|
||||
completion_result =
|
||||
complete_path(ctx, Some(cano_expr), &source, cursor, &path_filter);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(DerefTarget::Label(..) | DerefTarget::Ref(..) | DerefTarget::Normal(..)) => {}
|
||||
None => {}
|
||||
}
|
||||
|
||||
let mut completion_items_rest = None;
|
||||
let is_incomplete = false;
|
||||
|
||||
let mut items = completion_result.or_else(|| {
|
||||
let mut cc_ctx = CompletionContext::new(
|
||||
ctx,
|
||||
doc,
|
||||
&source,
|
||||
cursor,
|
||||
explicit,
|
||||
self.trigger_character,
|
||||
)?;
|
||||
let mut cc_ctx =
|
||||
CompletionContext::new(ctx, doc, &source, cursor, explicit, self.trigger_character)?;
|
||||
|
||||
// Exclude it self from auto completion
|
||||
// e.g. `#let x = (1.);`
|
||||
let self_ty = cc_ctx.leaf.cast::<ast::Expr>().and_then(|exp| {
|
||||
let v = cc_ctx.ctx.mini_eval(exp)?;
|
||||
Some(Ty::Value(InsTy::new(v)))
|
||||
});
|
||||
// Exclude it self from auto completion
|
||||
// e.g. `#let x = (1.);`
|
||||
let self_ty = cc_ctx.leaf.cast::<ast::Expr>().and_then(|exp| {
|
||||
let v = cc_ctx.ctx.mini_eval(exp)?;
|
||||
Some(Ty::Value(InsTy::new(v)))
|
||||
});
|
||||
|
||||
if let Some(self_ty) = self_ty {
|
||||
cc_ctx.seen_types.insert(self_ty);
|
||||
};
|
||||
if let Some(self_ty) = self_ty {
|
||||
cc_ctx.seen_types.insert(self_ty);
|
||||
};
|
||||
|
||||
let (offset, ic, mut completions, completions_items2) = autocomplete(cc_ctx)?;
|
||||
if !completions_items2.is_empty() {
|
||||
completion_items_rest = Some(completions_items2);
|
||||
let (offset, ic, mut completions, completions_items2) = autocomplete(cc_ctx)?;
|
||||
if !completions_items2.is_empty() {
|
||||
completion_items_rest = Some(completions_items2);
|
||||
}
|
||||
// todo: define it well, we were needing it because we wanted to do interactive
|
||||
// path completion, but now we've scanned all the paths at the same time.
|
||||
// is_incomplete = ic;
|
||||
let _ = ic;
|
||||
|
||||
// Filter and determine range to replace
|
||||
let mut from_ident = None;
|
||||
let is_callee = matches!(deref_target, Some(DerefTarget::Callee(..)));
|
||||
if matches!(
|
||||
deref_target,
|
||||
Some(DerefTarget::Callee(..) | DerefTarget::VarAccess(..))
|
||||
) {
|
||||
let node = LinkedNode::new(source.root()).leaf_at_compat(cursor)?;
|
||||
if is_ident_like(&node) && node.offset() == offset {
|
||||
from_ident = Some(node);
|
||||
}
|
||||
// todo: define it well, we were needing it because we wanted to do interactive
|
||||
// path completion, but now we've scanned all the paths at the same time.
|
||||
// is_incomplete = ic;
|
||||
let _ = ic;
|
||||
}
|
||||
let replace_range = if let Some(from_ident) = from_ident {
|
||||
let mut rng = from_ident.range();
|
||||
let ident_prefix = source.text()[rng.start..cursor].to_string();
|
||||
|
||||
let replace_range;
|
||||
if ident_like.as_ref().is_some_and(|i| i.offset() == offset) {
|
||||
let ident_like = ident_like.unwrap();
|
||||
let mut rng = ident_like.range();
|
||||
let ident_prefix = source.text()[rng.start..cursor].to_string();
|
||||
|
||||
completions.retain(|c| {
|
||||
// c.label
|
||||
let mut prefix_matcher = c.label.chars();
|
||||
'ident_matching: for ch in ident_prefix.chars() {
|
||||
for c in prefix_matcher.by_ref() {
|
||||
if c == ch {
|
||||
continue 'ident_matching;
|
||||
}
|
||||
completions.retain(|c| {
|
||||
// c.label
|
||||
let mut prefix_matcher = c.label.chars();
|
||||
'ident_matching: for ch in ident_prefix.chars() {
|
||||
for c in prefix_matcher.by_ref() {
|
||||
if c == ch {
|
||||
continue 'ident_matching;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
});
|
||||
|
||||
// if modifying some arguments, we need to truncate and add a comma
|
||||
if !is_callee && cursor != rng.end && is_arg_like_context(&ident_like) {
|
||||
// extend comma
|
||||
for c in completions.iter_mut() {
|
||||
let apply = match &mut c.apply {
|
||||
Some(w) => w,
|
||||
None => {
|
||||
c.apply = Some(c.label.clone());
|
||||
c.apply.as_mut().unwrap()
|
||||
}
|
||||
};
|
||||
if apply.trim_end().ends_with(',') {
|
||||
continue;
|
||||
}
|
||||
apply.push_str(", ");
|
||||
}
|
||||
|
||||
// Truncate
|
||||
rng.end = cursor;
|
||||
return false;
|
||||
}
|
||||
|
||||
replace_range = ctx.to_lsp_range(rng, &source);
|
||||
} else {
|
||||
replace_range = ctx.to_lsp_range(offset..cursor, &source);
|
||||
true
|
||||
});
|
||||
|
||||
// if modifying some arguments, we need to truncate and add a comma
|
||||
if !is_callee && cursor != rng.end && is_arg_like_context(&from_ident) {
|
||||
// extend comma
|
||||
for c in completions.iter_mut() {
|
||||
let apply = match &mut c.apply {
|
||||
Some(w) => w,
|
||||
None => {
|
||||
c.apply = Some(c.label.clone());
|
||||
c.apply.as_mut().unwrap()
|
||||
}
|
||||
};
|
||||
if apply.trim_end().ends_with(',') {
|
||||
continue;
|
||||
}
|
||||
apply.push_str(", ");
|
||||
}
|
||||
|
||||
// Truncate
|
||||
rng.end = cursor;
|
||||
}
|
||||
|
||||
let completions = completions.iter().map(|typst_completion| {
|
||||
let typst_snippet = typst_completion
|
||||
.apply
|
||||
ctx.to_lsp_range(rng, &source)
|
||||
} else {
|
||||
ctx.to_lsp_range(offset..cursor, &source)
|
||||
};
|
||||
|
||||
let completions = completions.iter().map(|typst_completion| {
|
||||
let typst_snippet = typst_completion
|
||||
.apply
|
||||
.as_ref()
|
||||
.unwrap_or(&typst_completion.label);
|
||||
let lsp_snippet = to_lsp_snippet(typst_snippet);
|
||||
let text_edit = CompletionTextEdit::Edit(TextEdit::new(replace_range, lsp_snippet));
|
||||
|
||||
LspCompletion {
|
||||
label: typst_completion.label.to_string(),
|
||||
kind: Some(completion_kind(typst_completion.kind.clone())),
|
||||
detail: typst_completion.detail.as_ref().map(String::from),
|
||||
sort_text: typst_completion.sort_text.as_ref().map(String::from),
|
||||
filter_text: typst_completion.filter_text.as_ref().map(String::from),
|
||||
label_details: typst_completion.label_detail.as_ref().map(|e| {
|
||||
CompletionItemLabelDetails {
|
||||
detail: None,
|
||||
description: Some(e.to_string()),
|
||||
}
|
||||
}),
|
||||
text_edit: Some(text_edit),
|
||||
additional_text_edits: typst_completion.additional_text_edits.clone(),
|
||||
insert_text_format: Some(InsertTextFormat::SNIPPET),
|
||||
commit_characters: typst_completion
|
||||
.commit_char
|
||||
.as_ref()
|
||||
.unwrap_or(&typst_completion.label);
|
||||
let lsp_snippet = to_lsp_snippet(typst_snippet);
|
||||
let text_edit = CompletionTextEdit::Edit(TextEdit::new(replace_range, lsp_snippet));
|
||||
|
||||
LspCompletion {
|
||||
label: typst_completion.label.to_string(),
|
||||
kind: Some(completion_kind(typst_completion.kind.clone())),
|
||||
detail: typst_completion.detail.as_ref().map(String::from),
|
||||
sort_text: typst_completion.sort_text.as_ref().map(String::from),
|
||||
filter_text: typst_completion.filter_text.as_ref().map(String::from),
|
||||
label_details: typst_completion.label_detail.as_ref().map(|e| {
|
||||
CompletionItemLabelDetails {
|
||||
detail: None,
|
||||
description: Some(e.to_string()),
|
||||
}
|
||||
}),
|
||||
text_edit: Some(text_edit),
|
||||
additional_text_edits: typst_completion.additional_text_edits.clone(),
|
||||
insert_text_format: Some(InsertTextFormat::SNIPPET),
|
||||
commit_characters: typst_completion
|
||||
.commit_char
|
||||
.as_ref()
|
||||
.map(|v| vec![v.to_string()]),
|
||||
command: typst_completion.command.as_ref().map(|c| Command {
|
||||
command: c.to_string(),
|
||||
..Default::default()
|
||||
}),
|
||||
.map(|v| vec![v.to_string()]),
|
||||
command: typst_completion.command.as_ref().map(|c| Command {
|
||||
command: c.to_string(),
|
||||
..Default::default()
|
||||
}
|
||||
});
|
||||
|
||||
Some(completions.collect_vec())
|
||||
})?;
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
});
|
||||
let mut items = completions.collect_vec();
|
||||
|
||||
if let Some(items_rest) = completion_items_rest.as_mut() {
|
||||
items.append(items_rest);
|
||||
|
|
|
@ -519,22 +519,23 @@ pub enum CheckTarget<'a> {
|
|||
container: LinkedNode<'a>,
|
||||
is_before: bool,
|
||||
},
|
||||
ImportPath(LinkedNode<'a>),
|
||||
IncludePath(LinkedNode<'a>),
|
||||
Normal(LinkedNode<'a>),
|
||||
}
|
||||
|
||||
impl<'a> CheckTarget<'a> {
|
||||
pub fn node(&self) -> Option<LinkedNode<'a>> {
|
||||
Some(match self {
|
||||
CheckTarget::Param { target, .. } => match target {
|
||||
ParamTarget::Positional { .. } => return None,
|
||||
ParamTarget::Named(node) => node.clone(),
|
||||
},
|
||||
CheckTarget::Element { target, .. } => match target {
|
||||
CheckTarget::Param { target, .. } | CheckTarget::Element { target, .. } => match target
|
||||
{
|
||||
ParamTarget::Positional { .. } => return None,
|
||||
ParamTarget::Named(node) => node.clone(),
|
||||
},
|
||||
CheckTarget::Paren { container, .. } => container.clone(),
|
||||
CheckTarget::Normal(node) => node.clone(),
|
||||
CheckTarget::ImportPath(node)
|
||||
| CheckTarget::IncludePath(node)
|
||||
| CheckTarget::Normal(node) => node.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -606,8 +607,11 @@ pub fn get_check_target(node: LinkedNode) -> Option<CheckTarget<'_>> {
|
|||
DerefTarget::Callee(callee) => {
|
||||
return get_callee_target(callee, node);
|
||||
}
|
||||
DerefTarget::ImportPath(node) | DerefTarget::IncludePath(node) => {
|
||||
return Some(CheckTarget::Normal(node));
|
||||
DerefTarget::ImportPath(node) => {
|
||||
return Some(CheckTarget::ImportPath(node));
|
||||
}
|
||||
DerefTarget::IncludePath(node) => {
|
||||
return Some(CheckTarget::IncludePath(node));
|
||||
}
|
||||
deref_target => deref_target.node().clone(),
|
||||
};
|
||||
|
@ -882,6 +886,8 @@ mod tests {
|
|||
Some(CheckTarget::Param { .. }) => 'p',
|
||||
Some(CheckTarget::Element { .. }) => 'e',
|
||||
Some(CheckTarget::Paren { .. }) => 'P',
|
||||
Some(CheckTarget::ImportPath(..)) => 'i',
|
||||
Some(CheckTarget::IncludePath(..)) => 'I',
|
||||
Some(CheckTarget::Normal(..)) => 'n',
|
||||
None => ' ',
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ use crate::ty::*;
|
|||
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, EnumIter)]
|
||||
pub enum PathPreference {
|
||||
Source,
|
||||
Source { allow_package: bool },
|
||||
Csv,
|
||||
Image,
|
||||
Json,
|
||||
|
@ -74,7 +74,7 @@ impl PathPreference {
|
|||
});
|
||||
|
||||
match self {
|
||||
PathPreference::Source => &SOURCE_REGSET,
|
||||
PathPreference::Source { .. } => &SOURCE_REGSET,
|
||||
PathPreference::Csv => &CSV_REGSET,
|
||||
PathPreference::Image => &IMAGE_REGSET,
|
||||
PathPreference::Json => &JSON_REGSET,
|
||||
|
@ -363,7 +363,7 @@ impl BuiltinTy {
|
|||
BuiltinTy::Path(s) => match s {
|
||||
PathPreference::None => "[any]",
|
||||
PathPreference::Special => "[any]",
|
||||
PathPreference::Source => "[source]",
|
||||
PathPreference::Source { .. } => "[source]",
|
||||
PathPreference::Csv => "[csv]",
|
||||
PathPreference::Image => "[image]",
|
||||
PathPreference::Json => "[json]",
|
||||
|
|
|
@ -21,7 +21,7 @@ use crate::analysis::{analyze_labels, DynLabel, LocalContext, Ty};
|
|||
|
||||
mod ext;
|
||||
use ext::*;
|
||||
pub use ext::{complete_path, CompletionFeat, PostfixSnippet};
|
||||
pub use ext::{CompletionFeat, PostfixSnippet};
|
||||
|
||||
/// Autocomplete a cursor position in a source file.
|
||||
///
|
||||
|
@ -38,8 +38,8 @@ pub fn autocomplete(
|
|||
mut ctx: CompletionContext,
|
||||
) -> Option<(usize, bool, Vec<Completion>, Vec<lsp_types::CompletionItem>)> {
|
||||
let _ = complete_comments(&mut ctx)
|
||||
|| complete_type(&mut ctx).is_none() && {
|
||||
crate::log_debug_ct!("continue after completing type");
|
||||
|| complete_type_and_syntax(&mut ctx).is_none() && {
|
||||
crate::log_debug_ct!("continue after completing type and syntax");
|
||||
complete_labels(&mut ctx)
|
||||
|| complete_imports(&mut ctx)
|
||||
|| complete_field_accesses(&mut ctx)
|
||||
|
@ -511,24 +511,6 @@ fn complete_labels(ctx: &mut CompletionContext) -> bool {
|
|||
|
||||
/// Complete imports.
|
||||
fn complete_imports(ctx: &mut CompletionContext) -> bool {
|
||||
// In an import path for a package:
|
||||
// "#import "@|",
|
||||
if_chain! {
|
||||
if matches!(
|
||||
ctx.leaf.parent_kind(),
|
||||
Some(SyntaxKind::ModuleImport | SyntaxKind::ModuleInclude)
|
||||
);
|
||||
if let Some(ast::Expr::Str(str)) = ctx.leaf.cast();
|
||||
let value = str.get();
|
||||
if value.starts_with('@');
|
||||
then {
|
||||
let all_versions = value.contains(':');
|
||||
ctx.from = ctx.leaf.offset();
|
||||
ctx.package_completions(all_versions);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// On the colon marker of an import list:
|
||||
// "#import "path.typ":|"
|
||||
if_chain! {
|
||||
|
|
|
@ -1410,8 +1410,8 @@ impl TypeCompletionContext<'_, '_> {
|
|||
// ctx.enrich(" ", "");
|
||||
// }
|
||||
|
||||
/// Complete call and set rule parameters.
|
||||
pub(crate) fn complete_type(ctx: &mut CompletionContext) -> Option<()> {
|
||||
/// Complete code by type or syntax.
|
||||
pub(crate) fn complete_type_and_syntax(ctx: &mut CompletionContext) -> Option<()> {
|
||||
use crate::syntax::get_check_target;
|
||||
use SurroundingSyntax::*;
|
||||
|
||||
|
@ -1438,6 +1438,33 @@ pub(crate) fn complete_type(ctx: &mut CompletionContext) -> Option<()> {
|
|||
}
|
||||
args_node = Some(args.to_untyped().clone());
|
||||
}
|
||||
Some(CheckTarget::ImportPath(path) | CheckTarget::IncludePath(path)) => {
|
||||
let Some(ast::Expr::Str(str)) = path.cast() else {
|
||||
return None;
|
||||
};
|
||||
ctx.from = path.offset();
|
||||
let value = str.get();
|
||||
if value.starts_with('@') {
|
||||
let all_versions = value.contains(':');
|
||||
ctx.package_completions(all_versions);
|
||||
return Some(());
|
||||
} else {
|
||||
let source = ctx.ctx.source_by_id(ctx.root.span().id()?).ok()?;
|
||||
let paths = complete_path(
|
||||
ctx.ctx,
|
||||
Some(path),
|
||||
&source,
|
||||
ctx.cursor,
|
||||
&crate::analysis::PathPreference::Source {
|
||||
allow_package: true,
|
||||
},
|
||||
);
|
||||
// todo: remove completions2
|
||||
ctx.completions2.extend(paths.unwrap_or_default());
|
||||
}
|
||||
|
||||
return Some(());
|
||||
}
|
||||
Some(CheckTarget::Normal(e))
|
||||
if (matches!(e.kind(), SyntaxKind::ContentBlock)
|
||||
&& matches!(ctx.leaf.kind(), SyntaxKind::LeftBracket)) =>
|
||||
|
@ -1610,7 +1637,7 @@ pub(crate) fn complete_type(ctx: &mut CompletionContext) -> Option<()> {
|
|||
Some(())
|
||||
}
|
||||
|
||||
pub fn complete_path(
|
||||
fn complete_path(
|
||||
ctx: &LocalContext,
|
||||
v: Option<LinkedNode>,
|
||||
source: &Source,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue