feat: provide label details by type, symbol, and labels (#237)

* feat: label details by type

* fix: symbol's details and label details

* dev: update snapshot

* fix: make signature stable
This commit is contained in:
Myriad-Dreamin 2024-05-05 20:19:29 +08:00 committed by GitHub
parent 3b93643091
commit 68bcc2b571
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 493 additions and 46 deletions

View file

@ -15,7 +15,7 @@ use typst::{
util::LazyHash,
};
use crate::analysis::resolve_callee;
use crate::analysis::{resolve_callee, FlowSignature};
use crate::syntax::{get_def_target, get_deref_target, DefTarget};
use crate::AnalysisContext;
@ -111,6 +111,8 @@ pub struct PrimarySignature {
pub rest: Option<Arc<ParamSpec>>,
/// The return type.
pub(crate) ret_ty: Option<FlowType>,
/// The signature type.
pub(crate) sig_ty: Option<FlowType>,
_broken: bool,
}
@ -367,12 +369,33 @@ fn analyze_dyn_signature_inner(func: Func) -> Arc<PrimarySignature> {
}
}
let mut named_vec: Vec<(EcoString, FlowType)> = named
.iter()
.map(|e| {
(
e.0.as_ref().into(),
e.1.infer_type.clone().unwrap_or(FlowType::Any),
)
})
.collect::<Vec<_>>();
named_vec.sort_by(|a, b| a.0.cmp(&b.0));
let sig_ty = FlowSignature::new(
pos.iter()
.map(|e| e.infer_type.clone().unwrap_or(FlowType::Any)),
named_vec.into_iter(),
rest.as_ref()
.map(|e| e.infer_type.clone().unwrap_or(FlowType::Any)),
ret_ty.clone(),
);
Arc::new(PrimarySignature {
pos,
named,
rest,
ret_ty,
has_fill_or_size_or_stroke: has_fill || has_stroke || has_size,
sig_ty: Some(FlowType::Func(Box::new(sig_ty))),
_broken: broken,
})
}

View file

@ -10,7 +10,7 @@ use once_cell::sync::Lazy;
use parking_lot::{Mutex, RwLock};
use reflexo::{hash::hash128, vector::ir::DefId};
use typst::{
foundations::{Func, Value},
foundations::{Func, Repr, Value},
syntax::{
ast::{self, AstNode},
LinkedNode, Source, Span, SyntaxKind,
@ -86,6 +86,11 @@ impl TypeCheckInfo {
worker.simplify(ty, principal)
}
pub fn describe(&self, ty: &FlowType) -> Option<String> {
let mut worker = TypeDescriber::default();
worker.describe_root(ty)
}
// todo: distinguish at least, at most
pub fn witness_at_least(&mut self, site: Span, ty: FlowType) {
Self::witness_(site, ty, &mut self.mapping);
@ -1260,6 +1265,167 @@ struct TypeCanoStore {
positives: HashSet<DefId>,
}
#[derive(Default)]
struct TypeDescriber {
described: HashSet<u128>,
results: HashSet<String>,
functions: Vec<FlowSignature>,
}
impl TypeDescriber {
fn describe_root(&mut self, ty: &FlowType) -> Option<String> {
// recursive structure
if self.described.contains(&hash128(ty)) {
return Some("$self".to_string());
}
let res = self.describe(ty);
if !res.is_empty() {
return Some(res);
}
self.described.insert(hash128(ty));
let mut results = std::mem::take(&mut self.results)
.into_iter()
.collect::<Vec<_>>();
let functions = std::mem::take(&mut self.functions);
if !functions.is_empty() {
// todo: union signature
// only first function is described
let f = functions[0].clone();
let mut res = String::new();
res.push('(');
let mut not_first = false;
for ty in f.pos.iter() {
if not_first {
res.push_str(", ");
} else {
not_first = true;
}
res.push_str(self.describe_root(ty).as_deref().unwrap_or("any"));
}
for (k, ty) in f.named.iter() {
if not_first {
res.push_str(", ");
} else {
not_first = true;
}
res.push_str(k);
res.push_str(": ");
res.push_str(self.describe_root(ty).as_deref().unwrap_or("any"));
}
if let Some(r) = &f.rest {
if not_first {
res.push_str(", ");
}
res.push_str("..: ");
res.push_str(self.describe_root(r).as_deref().unwrap_or(""));
res.push_str("[]");
}
res.push_str(") => ");
res.push_str(self.describe_root(&f.ret).as_deref().unwrap_or("any"));
results.push(res);
}
if results.is_empty() {
return None;
}
results.sort();
results.dedup();
Some(results.join(" | "))
}
fn describe_iter(&mut self, ty: &[FlowType]) {
for ty in ty.iter() {
let desc = self.describe(ty);
self.results.insert(desc);
}
}
fn describe(&mut self, ty: &FlowType) -> String {
match ty {
FlowType::Var(..) => {}
FlowType::Union(tys) => {
self.describe_iter(tys);
}
FlowType::Let(lb) => {
self.describe_iter(&lb.lbs);
self.describe_iter(&lb.ubs);
}
FlowType::Func(f) => {
self.functions.push(*f.clone());
}
FlowType::Dict(..) => {
return "dict".to_string();
}
FlowType::Tuple(..) => {
return "array".to_string();
}
FlowType::Array(..) => {
return "array".to_string();
}
FlowType::With(w) => {
return self.describe(&w.0);
}
FlowType::Clause => {}
FlowType::Undef => {}
FlowType::Content => {
return "content".to_string();
}
FlowType::Any => {
return "any".to_string();
}
FlowType::Space => {}
FlowType::None => {
return "none".to_string();
}
FlowType::Infer => {}
FlowType::FlowNone => {
return "none".to_string();
}
FlowType::Auto => {
return "auto".to_string();
}
FlowType::Boolean(None) => {
return "boolean".to_string();
}
FlowType::Boolean(Some(b)) => {
return b.to_string();
}
FlowType::Builtin(b) => {
return b.describe().to_string();
}
FlowType::Value(v) => return v.0.repr().to_string(),
FlowType::ValueDoc(v) => return v.0.repr().to_string(),
FlowType::Field(..) => {
return "field".to_string();
}
FlowType::Element(..) => {
return "element".to_string();
}
FlowType::Args(..) => {
return "args".to_string();
}
FlowType::At(..) => {
return "any".to_string();
}
FlowType::Unary(..) => {
return "any".to_string();
}
FlowType::Binary(..) => {
return "any".to_string();
}
FlowType::If(..) => {
return "any".to_string();
}
}
String::new()
}
}
struct TypeSimplifier<'a, 'b> {
principal: bool,

View file

@ -102,6 +102,40 @@ pub(crate) enum FlowBuiltinType {
Path(PathPreference),
}
impl FlowBuiltinType {
pub(crate) fn describe(&self) -> &'static str {
match self {
FlowBuiltinType::Args => "args",
FlowBuiltinType::Color => "color",
FlowBuiltinType::TextSize => "text.size",
FlowBuiltinType::TextFont => "text.font",
FlowBuiltinType::TextLang => "text.lang",
FlowBuiltinType::TextRegion => "text.region",
FlowBuiltinType::Dir => "dir",
FlowBuiltinType::Length => "length",
FlowBuiltinType::Float => "float",
FlowBuiltinType::Stroke => "stroke",
FlowBuiltinType::Margin => "margin",
FlowBuiltinType::Inset => "inset",
FlowBuiltinType::Outset => "outset",
FlowBuiltinType::Radius => "radius",
FlowBuiltinType::Path(s) => match s {
PathPreference::None => "[any]",
PathPreference::Special => "[any]",
PathPreference::Source => "[source]",
PathPreference::Csv => "[csv]",
PathPreference::Image => "[image]",
PathPreference::Json => "[json]",
PathPreference::Yaml => "[yaml]",
PathPreference::Xml => "[xml]",
PathPreference::Toml => "[toml]",
PathPreference::Bibliography => "[bib]",
PathPreference::RawTheme => "[theme]",
PathPreference::RawSyntax => "[syntax]",
},
}
}
}
use FlowBuiltinType::*;

View file

@ -11,7 +11,7 @@ use typst::{
use crate::analysis::ty::param_mapping;
use super::FlowBuiltinType;
use super::{FlowBuiltinType, TypeDescriber};
struct RefDebug<'a>(&'a FlowType);
@ -171,6 +171,11 @@ impl FlowType {
Some(ty)
}
pub fn describe(&self) -> Option<String> {
let mut worker = TypeDescriber::default();
worker.describe_root(self)
}
pub(crate) fn is_dict(&self) -> bool {
matches!(self, FlowType::Dict(..))
}
@ -412,6 +417,20 @@ impl FlowSignature {
ret,
}
}
pub(crate) fn new(
pos: impl Iterator<Item = FlowType>,
named: impl Iterator<Item = (EcoString, FlowType)>,
rest: Option<FlowType>,
ret_ty: Option<FlowType>,
) -> Self {
FlowSignature {
pos: pos.collect(),
named: named.collect(),
rest,
ret: ret_ty.unwrap_or(FlowType::Any),
}
}
}
impl fmt::Debug for FlowSignature {

View file

@ -11,6 +11,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/base.typ
{
"kind": 3,
"label": "aa",
"labelDetails": {
"description": "() => 1"
},
"textEdit": {
"newText": "aa(${1:})",
"range": {
@ -28,6 +31,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/base.typ
{
"kind": 3,
"label": "aab",
"labelDetails": {
"description": "() => 1"
},
"textEdit": {
"newText": "aab(${1:})",
"range": {
@ -45,6 +51,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/base.typ
{
"kind": 3,
"label": "aabc",
"labelDetails": {
"description": "() => 1"
},
"textEdit": {
"newText": "aabc(${1:})",
"range": {
@ -62,6 +71,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/base.typ
{
"kind": 3,
"label": "aac",
"labelDetails": {
"description": "() => 1"
},
"textEdit": {
"newText": "aac(${1:})",
"range": {
@ -84,6 +96,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/base.typ
{
"kind": 3,
"label": "aabc",
"labelDetails": {
"description": "() => 1"
},
"textEdit": {
"newText": "aabc(${1:})",
"range": {
@ -101,6 +116,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/base.typ
{
"kind": 3,
"label": "aac",
"labelDetails": {
"description": "() => 1"
},
"textEdit": {
"newText": "aac(${1:})",
"range": {

View file

@ -65,6 +65,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/func_args.typ
{
"kind": 7,
"label": "content",
"labelDetails": {
"description": "type"
},
"sortText": "050",
"textEdit": {
"newText": "content",

View file

@ -11,6 +11,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/func_params.typ
{
"kind": 3,
"label": "aa",
"labelDetails": {
"description": "(any, any, any) => none"
},
"textEdit": {
"newText": "aa(${1:})",
"range": {
@ -28,6 +31,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/func_params.typ
{
"kind": 6,
"label": "aab",
"labelDetails": {
"description": "any"
},
"textEdit": {
"newText": "aab",
"range": {
@ -45,6 +51,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/func_params.typ
{
"kind": 6,
"label": "aabc",
"labelDetails": {
"description": "any"
},
"textEdit": {
"newText": "aabc",
"range": {
@ -62,6 +71,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/func_params.typ
{
"kind": 6,
"label": "aac",
"labelDetails": {
"description": "any"
},
"textEdit": {
"newText": "aac",
"range": {
@ -84,6 +96,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/func_params.typ
{
"kind": 6,
"label": "aabc",
"labelDetails": {
"description": "any"
},
"textEdit": {
"newText": "aabc",
"range": {
@ -101,6 +116,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/func_params.typ
{
"kind": 6,
"label": "aac",
"labelDetails": {
"description": "any"
},
"textEdit": {
"newText": "aac",
"range": {

View file

@ -65,6 +65,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/func_with_args.typ
{
"kind": 7,
"label": "content",
"labelDetails": {
"description": "type"
},
"sortText": "050",
"textEdit": {
"newText": "content",

View file

@ -11,6 +11,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/import.typ
{
"kind": 3,
"label": "aab",
"labelDetails": {
"description": "() => any"
},
"textEdit": {
"newText": "aab(${1:})",
"range": {
@ -28,6 +31,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/import.typ
{
"kind": 3,
"label": "aac",
"labelDetails": {
"description": "() => any"
},
"textEdit": {
"newText": "aac(${1:})",
"range": {
@ -50,6 +56,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/import.typ
{
"kind": 3,
"label": "aac",
"labelDetails": {
"description": "() => any"
},
"textEdit": {
"newText": "aac(${1:})",
"range": {

View file

@ -11,6 +11,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/import_self.typ
{
"kind": 9,
"label": "base",
"labelDetails": {
"description": "base.typ"
},
"textEdit": {
"newText": "base",
"range": {

View file

@ -11,6 +11,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/import_self3.typ
{
"kind": 9,
"label": "baz",
"labelDetails": {
"description": "base.typ"
},
"textEdit": {
"newText": "baz",
"range": {

View file

@ -11,6 +11,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/import_self4.typ
{
"kind": 9,
"label": "baz",
"labelDetails": {
"description": "base.typ"
},
"textEdit": {
"newText": "baz",
"range": {

View file

@ -11,6 +11,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/import_star.typ
{
"kind": 3,
"label": "aa",
"labelDetails": {
"description": "() => any"
},
"textEdit": {
"newText": "aa(${1:})",
"range": {
@ -28,6 +31,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/import_star.typ
{
"kind": 3,
"label": "aa.with",
"labelDetails": {
"description": "() => any"
},
"textEdit": {
"newText": "aa.with(${1:})",
"range": {
@ -45,6 +51,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/import_star.typ
{
"kind": 3,
"label": "aab",
"labelDetails": {
"description": "() => any"
},
"textEdit": {
"newText": "aab(${1:})",
"range": {
@ -62,6 +71,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/import_star.typ
{
"kind": 3,
"label": "aabc",
"labelDetails": {
"description": "() => any"
},
"textEdit": {
"newText": "aabc(${1:})",
"range": {
@ -79,6 +91,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/import_star.typ
{
"kind": 3,
"label": "aac",
"labelDetails": {
"description": "() => any"
},
"textEdit": {
"newText": "aac(${1:})",
"range": {
@ -101,6 +116,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/import_star.typ
{
"kind": 3,
"label": "aabc",
"labelDetails": {
"description": "() => any"
},
"textEdit": {
"newText": "aabc(${1:})",
"range": {
@ -118,6 +136,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/import_star.typ
{
"kind": 3,
"label": "aac",
"labelDetails": {
"description": "() => any"
},
"textEdit": {
"newText": "aac(${1:})",
"range": {

View file

@ -11,6 +11,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/item_shadow.typ
{
"kind": 3,
"label": "aa",
"labelDetails": {
"description": "() => 1"
},
"textEdit": {
"newText": "aa(${1:})",
"range": {
@ -28,6 +31,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/item_shadow.typ
{
"kind": 3,
"label": "aac",
"labelDetails": {
"description": "() => 1"
},
"textEdit": {
"newText": "aac(${1:})",
"range": {
@ -50,6 +56,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/item_shadow.typ
{
"kind": 3,
"label": "aac",
"labelDetails": {
"description": "() => 1"
},
"textEdit": {
"newText": "aac(${1:})",
"range": {

View file

@ -11,6 +11,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/let.typ
{
"kind": 3,
"label": "aa",
"labelDetails": {
"description": "() => 1"
},
"textEdit": {
"newText": "aa(${1:})",
"range": {
@ -28,6 +31,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/let.typ
{
"kind": 6,
"label": "aab",
"labelDetails": {
"description": "1"
},
"textEdit": {
"newText": "aab",
"range": {
@ -45,6 +51,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/let.typ
{
"kind": 6,
"label": "aabc",
"labelDetails": {
"description": "1"
},
"textEdit": {
"newText": "aabc",
"range": {
@ -62,6 +71,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/let.typ
{
"kind": 3,
"label": "aac",
"labelDetails": {
"description": "() => 1"
},
"textEdit": {
"newText": "aac(${1:})",
"range": {
@ -84,6 +96,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/let.typ
{
"kind": 6,
"label": "aabc",
"labelDetails": {
"description": "1"
},
"textEdit": {
"newText": "aabc",
"range": {
@ -101,6 +116,9 @@ input_file: crates/tinymist-query/src/fixtures/completion/let.typ
{
"kind": 3,
"label": "aac",
"labelDetails": {
"description": "() => 1"
},
"textEdit": {
"newText": "aac(${1:})",
"range": {

View file

@ -479,39 +479,6 @@ fn field_access_completions(ctx: &mut CompletionContext, value: &Value, styles:
}
}
/// If is printable, return the symbol itself.
/// Otherwise, return the symbol's unicode description.
fn symbol_label_detail(ch: char) -> EcoString {
if !ch.is_whitespace() && !ch.is_control() {
return ch.into();
}
match ch {
' ' => "space".into(),
'\t' => "tab".into(),
'\n' => "newline".into(),
'\r' => "carriage return".into(),
// replacer
'\u{200D}' => "zero width joiner".into(),
'\u{200C}' => "zero width non-joiner".into(),
'\u{200B}' => "zero width space".into(),
'\u{2060}' => "word joiner".into(),
// spaces
'\u{00A0}' => "non-breaking space".into(),
'\u{202F}' => "narrow no-break space".into(),
'\u{2002}' => "en space".into(),
'\u{2003}' => "em space".into(),
'\u{2004}' => "three-per-em space".into(),
'\u{2005}' => "four-per-em space".into(),
'\u{2006}' => "six-per-em space".into(),
'\u{2007}' => "figure space".into(),
'\u{205f}' => "medium mathematical space".into(),
'\u{2008}' => "punctuation space".into(),
'\u{2009}' => "thin space".into(),
'\u{200A}' => "hair space".into(),
_ => format!("\\u{{{:04x}}}", ch as u32).into(),
}
}
/// Complete half-finished labels.
fn complete_open_labels(ctx: &mut CompletionContext) -> bool {
// A label anywhere in code: "(<la|".
@ -1175,12 +1142,33 @@ impl<'a, 'w> CompletionContext<'a, 'w> {
value: &Value,
parens: bool,
docs: Option<&str>,
) {
self.value_completion_(
label,
value,
parens,
match value {
Value::Symbol(s) => Some(symbol_label_detail(s.get())),
_ => None,
},
docs,
);
}
/// Add a completion for a specific value.
fn value_completion_(
&mut self,
label: Option<EcoString>,
value: &Value,
parens: bool,
label_detail: Option<EcoString>,
docs: Option<&str>,
) {
let at = label.as_deref().is_some_and(|field| !is_ident(field));
let label = label.unwrap_or_else(|| value.repr());
let detail = docs.map(Into::into).or_else(|| match value {
Value::Symbol(_) => None,
Value::Symbol(c) => Some(symbol_detail(c.get())),
Value::Func(func) => func.docs().map(plain_docs_sentence),
Value::Type(ty) => Some(plain_docs_sentence(ty.docs())),
v => {
@ -1216,10 +1204,7 @@ impl<'a, 'w> CompletionContext<'a, 'w> {
label,
apply,
detail,
label_detail: match value {
Value::Symbol(s) => Some(symbol_label_detail(s.get())),
_ => None,
},
label_detail,
command,
..Completion::default()
});

View file

@ -5,7 +5,7 @@ use lsp_types::{CompletionItem, CompletionTextEdit, InsertTextFormat, TextEdit};
use once_cell::sync::OnceCell;
use parking_lot::Mutex;
use reflexo::path::{unix_slash, PathClean};
use typst::foundations::{AutoValue, Func, Label, NoneValue, Type, Value};
use typst::foundations::{AutoValue, Func, Label, NoneValue, Repr, Type, Value};
use typst::layout::{Dir, Length};
use typst::syntax::ast::AstNode;
use typst::syntax::{ast, Span, SyntaxKind};
@ -69,6 +69,7 @@ impl<'a, 'w> CompletionContext<'a, 'w> {
let src = self.ctx.source_by_id(id).ok()?;
self.ctx.type_check(src)
})();
let types = types.as_ref();
let mut ancestor = Some(self.leaf.clone());
while let Some(node) = &ancestor {
@ -279,10 +280,38 @@ impl<'a, 'w> CompletionContext<'a, 'w> {
if !filter(None) || name.is_empty() {
continue;
}
let _ = types;
let span = match def_kind {
DefKind::Syntax(span) => span,
DefKind::Instance(span, _) => span,
};
// we don't check literal type here for faster completion
let ty_detail = if let CompletionKind::Symbol(c) = &kind {
Some(symbol_label_detail(*c))
} else {
types
.and_then(|types| {
let ty = types.mapping.get(&span)?;
let ty = types.simplify(ty.clone(), false);
types.describe(&ty).map(From::from)
})
.or_else(|| {
if let DefKind::Instance(_, v) = &def_kind {
Some(describe_value(self.ctx, v))
} else {
None
}
})
};
let detail = if let CompletionKind::Symbol(c) = &kind {
Some(symbol_detail(*c))
} else {
ty_detail.clone()
};
if kind == CompletionKind::Func {
let base = Completion {
kind: kind.clone(),
label_detail: ty_detail,
// todo: only vscode and neovim (0.9.1) support this
command: Some("editor.action.triggerSuggest"),
..Default::default()
@ -345,12 +374,20 @@ impl<'a, 'w> CompletionContext<'a, 'w> {
SurroundingSyntax::Selector | SurroundingSyntax::SetRule
) && !matches!(&v, Value::Func(func) if func.element().is_some());
if !bad_instantiate {
self.value_completion(Some(name), &v, parens, None);
self.value_completion_(
Some(name),
&v,
parens,
ty_detail.clone(),
detail.as_deref(),
);
}
} else {
self.completions.push(Completion {
kind,
label: name,
label_detail: ty_detail.clone(),
detail,
..Completion::default()
});
}
@ -422,6 +459,38 @@ impl<'a, 'w> CompletionContext<'a, 'w> {
}
}
fn describe_value(ctx: &mut AnalysisContext, v: &Value) -> EcoString {
match v {
Value::Func(f) => {
let mut f = f;
while let typst::foundations::func::Repr::With(with_f) = f.inner() {
f = &with_f.0;
}
let sig = analyze_dyn_signature(ctx, f.clone());
sig.primary()
.sig_ty
.as_ref()
.and_then(|e| e.describe())
.unwrap_or_else(|| "function".into())
.into()
}
Value::Module(m) => {
if let Some(fid) = m.file_id() {
let package = fid.package();
let path = unix_slash(fid.vpath().as_rootless_path());
if let Some(package) = package {
return eco_format!("{package}:{path}");
}
return path.into();
}
"module".into()
}
_ => v.ty().repr(),
}
}
fn encolsed_by(parent: &LinkedNode, s: Option<Span>, leaf: &LinkedNode) -> bool {
s.and_then(|s| parent.find(s)?.find(leaf.span())).is_some()
}
@ -1296,6 +1365,49 @@ pub fn complete_path(
)
}
/// If is printable, return the symbol itself.
/// Otherwise, return the symbol's unicode detailed description.
pub fn symbol_detail(ch: char) -> EcoString {
let ld = symbol_label_detail(ch);
if ld.starts_with("\\u") {
return ld;
}
format!("{}, unicode: `\\u{{{:04x}}}`", ld, ch as u32).into()
}
/// If is printable, return the symbol itself.
/// Otherwise, return the symbol's unicode description.
pub fn symbol_label_detail(ch: char) -> EcoString {
if !ch.is_whitespace() && !ch.is_control() {
return ch.into();
}
match ch {
' ' => "space".into(),
'\t' => "tab".into(),
'\n' => "newline".into(),
'\r' => "carriage return".into(),
// replacer
'\u{200D}' => "zero width joiner".into(),
'\u{200C}' => "zero width non-joiner".into(),
'\u{200B}' => "zero width space".into(),
'\u{2060}' => "word joiner".into(),
// spaces
'\u{00A0}' => "non-breaking space".into(),
'\u{202F}' => "narrow no-break space".into(),
'\u{2002}' => "en space".into(),
'\u{2003}' => "em space".into(),
'\u{2004}' => "three-per-em space".into(),
'\u{2005}' => "four-per-em space".into(),
'\u{2006}' => "six-per-em space".into(),
'\u{2007}' => "figure space".into(),
'\u{205f}' => "medium mathematical space".into(),
'\u{2008}' => "punctuation space".into(),
'\u{2009}' => "thin space".into(),
'\u{200A}' => "hair space".into(),
_ => format!("\\u{{{:04x}}}", ch as u32).into(),
}
}
#[cfg(test)]
mod tests {