refactor: complete paths without hacking (#971)

* refactor: complete paths without hacking

* dev: move code after refactor
This commit is contained in:
Myriad-Dreamin 2024-12-10 14:29:24 +08:00 committed by GitHub
parent ab234634a9
commit 969cc6d339
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 165 additions and 181 deletions

View file

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

View file

@ -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:?}");

View file

@ -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);

View file

@ -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 => ' ',
}

View file

@ -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]",

View file

@ -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! {

View file

@ -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,