From 8e5f446544fd147277ed2e4208c7ea793cc846a7 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sat, 11 Mar 2023 11:46:12 +0100 Subject: [PATCH] Autocompletion for raw language tags --- library/src/lib.rs | 1 + library/src/text/raw.rs | 17 ++++++++ src/diag.rs | 24 ----------- src/eval/args.rs | 4 +- src/eval/array.rs | 4 +- src/eval/cast.rs | 3 +- src/eval/dict.rs | 6 +-- src/eval/library.rs | 2 + src/ide/complete.rs | 55 ++++++++++++++++++++++-- src/model/content.rs | 4 +- src/model/styles.rs | 4 +- src/syntax/lexer.rs | 4 +- src/util/mod.rs | 94 ++++++++++++++++++++++++++--------------- 13 files changed, 146 insertions(+), 76 deletions(-) diff --git a/library/src/lib.rs b/library/src/lib.rs index 94b3e0c44..36afdb2be 100644 --- a/library/src/lib.rs +++ b/library/src/lib.rs @@ -182,6 +182,7 @@ fn items() -> LangItems { } node.pack() }, + raw_languages: text::RawNode::languages, link: |url| meta::LinkNode::from_url(url).pack(), ref_: |target| meta::RefNode::new(target).pack(), heading: |level, title| meta::HeadingNode::new(title).with_level(level).pack(), diff --git a/library/src/text/raw.rs b/library/src/text/raw.rs index 72dd1782c..99fb89d29 100644 --- a/library/src/text/raw.rs +++ b/library/src/text/raw.rs @@ -103,6 +103,23 @@ pub struct RawNode { pub lang: Option, } +impl RawNode { + /// The supported language names and tags. + pub fn languages() -> Vec<(&'static str, Vec<&'static str>)> { + SYNTAXES + .syntaxes() + .iter() + .map(|syntax| { + ( + syntax.name.as_str(), + syntax.file_extensions.iter().map(|s| s.as_str()).collect(), + ) + }) + .chain([("Typst", vec!["typ"]), ("Typst (code)", vec!["typc"])]) + .collect() + } +} + impl Prepare for RawNode { fn prepare(&self, _: &mut Vt, styles: StyleChain) -> SourceResult { let mut node = self.clone().pack(); diff --git a/src/diag.rs b/src/diag.rs index eca6827c6..b12df2097 100644 --- a/src/diag.rs +++ b/src/diag.rs @@ -177,30 +177,6 @@ where } } -/// Format the parts separated with commas and a final "and" or "or". -pub(crate) fn comma_list(buf: &mut String, parts: &[S], last: &str) -where - S: AsRef, -{ - for (i, part) in parts.iter().enumerate() { - match i { - 0 => {} - 1 if parts.len() == 2 => { - buf.push(' '); - buf.push_str(last); - buf.push(' '); - } - i if i + 1 == parts.len() => { - buf.push_str(", "); - buf.push_str(last); - buf.push(' '); - } - _ => buf.push_str(", "), - } - buf.push_str(part.as_ref()); - } -} - /// A result type with a file-related error. pub type FileResult = Result; diff --git a/src/eval/args.rs b/src/eval/args.rs index 17bd9cb40..129da4632 100644 --- a/src/eval/args.rs +++ b/src/eval/args.rs @@ -5,7 +5,7 @@ use ecow::{eco_format, EcoVec}; use super::{Array, Cast, Dict, Str, Value}; use crate::diag::{bail, At, SourceResult}; use crate::syntax::{Span, Spanned}; -use crate::util::pretty_array; +use crate::util::pretty_array_like; /// Evaluated arguments to a function. #[derive(Clone, PartialEq, Hash)] @@ -174,7 +174,7 @@ impl Debug for Args { fn fmt(&self, f: &mut Formatter) -> fmt::Result { let pieces: Vec<_> = self.items.iter().map(|arg| eco_format!("{arg:?}")).collect(); - f.write_str(&pretty_array(&pieces, false)) + f.write_str(&pretty_array_like(&pieces, false)) } } diff --git a/src/eval/array.rs b/src/eval/array.rs index 8da9b3d26..979f15d4c 100644 --- a/src/eval/array.rs +++ b/src/eval/array.rs @@ -6,7 +6,7 @@ use ecow::{eco_format, EcoString, EcoVec}; use super::{ops, Args, Func, Value, Vm}; use crate::diag::{bail, At, SourceResult, StrResult}; -use crate::util::pretty_array; +use crate::util::pretty_array_like; /// Create a new [`Array`] from values. #[macro_export] @@ -343,7 +343,7 @@ impl Array { impl Debug for Array { fn fmt(&self, f: &mut Formatter) -> fmt::Result { let pieces: Vec<_> = self.iter().map(|value| eco_format!("{value:?}")).collect(); - f.write_str(&pretty_array(&pieces, self.len() == 1)) + f.write_str(&pretty_array_like(&pieces, self.len() == 1)) } } diff --git a/src/eval/cast.rs b/src/eval/cast.rs index 806f7e92b..ac23bd3ad 100644 --- a/src/eval/cast.rs +++ b/src/eval/cast.rs @@ -8,6 +8,7 @@ use ecow::EcoString; use super::{Array, Str, Value}; use crate::diag::StrResult; use crate::syntax::Spanned; +use crate::util::separated_list; /// Cast from a value to a specific type. pub trait Cast: Sized { @@ -284,7 +285,7 @@ impl CastInfo { msg.push_str(" nothing"); } - crate::diag::comma_list(&mut msg, &parts, "or"); + msg.push_str(&separated_list(&parts, "or")); if !matching_type { msg.push_str(", found "); diff --git a/src/eval/dict.rs b/src/eval/dict.rs index ececce071..4333a55eb 100644 --- a/src/eval/dict.rs +++ b/src/eval/dict.rs @@ -8,7 +8,7 @@ use ecow::{eco_format, EcoString}; use super::{array, Array, Str, Value}; use crate::diag::StrResult; use crate::syntax::is_ident; -use crate::util::{pretty_array, ArcExt}; +use crate::util::{pretty_array_like, separated_list, ArcExt}; /// Create a new [`Dict`] from key-value pairs. #[macro_export] @@ -125,7 +125,7 @@ impl Dict { if let Some((key, _)) = self.iter().next() { let parts: Vec<_> = expected.iter().map(|s| eco_format!("\"{s}\"")).collect(); let mut msg = format!("unexpected key {key:?}, valid keys are "); - crate::diag::comma_list(&mut msg, &parts, "and"); + msg.push_str(&separated_list(&parts, "and")); return Err(msg.into()); } Ok(()) @@ -149,7 +149,7 @@ impl Debug for Dict { }) .collect(); - f.write_str(&pretty_array(&pieces, false)) + f.write_str(&pretty_array_like(&pieces, false)) } } diff --git a/src/eval/library.rs b/src/eval/library.rs index 75787348c..c37c16fd6 100644 --- a/src/eval/library.rs +++ b/src/eval/library.rs @@ -55,6 +55,8 @@ pub struct LangItems { pub emph: fn(body: Content) -> Content, /// Raw text with optional syntax highlighting: `` `...` ``. pub raw: fn(text: EcoString, tag: Option, block: bool) -> Content, + /// The language names and tags supported by raw text. + pub raw_languages: fn() -> Vec<(&'static str, Vec<&'static str>)>, /// A hyperlink: `https://typst.org`. pub link: fn(url: EcoString) -> Content, /// A reference: `@target`. diff --git a/src/ide/complete.rs b/src/ide/complete.rs index 2e810ee98..a0d5e9a4b 100644 --- a/src/ide/complete.rs +++ b/src/ide/complete.rs @@ -2,10 +2,14 @@ use std::collections::{BTreeSet, HashSet}; use ecow::{eco_format, EcoString}; use if_chain::if_chain; +use unscanny::Scanner; use super::{analyze_expr, analyze_import, plain_docs_sentence, summarize_font_family}; -use crate::eval::{methods_on, CastInfo, Scope, Value}; -use crate::syntax::{ast, LinkedNode, Source, SyntaxKind}; +use crate::eval::{methods_on, CastInfo, Library, Scope, Value}; +use crate::syntax::{ + ast, is_id_continue, is_id_start, is_ident, LinkedNode, Source, SyntaxKind, +}; +use crate::util::separated_list; use crate::World; /// Autocomplete a cursor position in a source file. @@ -104,6 +108,22 @@ fn complete_markup(ctx: &mut CompletionContext) -> bool { } } + // Directly after a raw block. + let mut s = Scanner::new(&ctx.text); + s.jump(ctx.leaf.offset()); + if s.eat_if("```") { + s.eat_while('`'); + let start = s.cursor(); + if s.eat_if(is_id_start) { + s.eat_while(is_id_continue); + } + if s.cursor() == ctx.cursor { + ctx.from = start; + ctx.raw_completions(); + } + return true; + } + // Anywhere: "|". if ctx.explicit { ctx.from = ctx.cursor; @@ -830,9 +850,11 @@ fn code_completions(ctx: &mut CompletionContext, hashtag: bool) { /// Context for autocompletion. struct CompletionContext<'a> { world: &'a (dyn World + 'static), + library: &'a Library, source: &'a Source, global: &'a Scope, math: &'a Scope, + text: &'a str, before: &'a str, after: &'a str, leaf: LinkedNode<'a>, @@ -852,12 +874,15 @@ impl<'a> CompletionContext<'a> { explicit: bool, ) -> Option { let text = source.text(); + let library = world.library(); let leaf = LinkedNode::new(source.root()).leaf_at(cursor)?; Some(Self { world, + library, source, - global: &world.library().global.scope(), - math: &world.library().math.scope(), + global: &library.global.scope(), + math: &library.math.scope(), + text, before: &text[..cursor], after: &text[cursor..], leaf, @@ -908,6 +933,28 @@ impl<'a> CompletionContext<'a> { } } + /// Add completions for raw block tags. + fn raw_completions(&mut self) { + for (name, mut tags) in (self.library.items.raw_languages)() { + let lower = name.to_lowercase(); + if !tags.contains(&lower.as_str()) { + tags.push(lower.as_str()); + } + + tags.retain(|tag| is_ident(tag)); + if tags.is_empty() { + continue; + } + + self.completions.push(Completion { + kind: CompletionKind::Constant, + label: name.into(), + apply: Some(tags[0].into()), + detail: Some(separated_list(&tags, " or ").into()), + }); + } + } + /// Add a completion for a specific value. fn value_completion( &mut self, diff --git a/src/model/content.rs b/src/model/content.rs index b895553c5..17fa786b9 100644 --- a/src/model/content.rs +++ b/src/model/content.rs @@ -12,7 +12,7 @@ use super::{node, Guard, Recipe, Style, StyleMap}; use crate::diag::{SourceResult, StrResult}; use crate::eval::{cast_from_value, Args, FuncInfo, Str, Value, Vm}; use crate::syntax::Span; -use crate::util::pretty_array; +use crate::util::pretty_array_like; use crate::World; /// Composable representation of styled content. @@ -261,7 +261,7 @@ impl Debug for Content { .collect(); f.write_str(name)?; - f.write_str(&pretty_array(&pieces, false)) + f.write_str(&pretty_array_like(&pieces, false)) } } diff --git a/src/model/styles.rs b/src/model/styles.rs index b803bc34c..9a562c758 100644 --- a/src/model/styles.rs +++ b/src/model/styles.rs @@ -8,7 +8,7 @@ use super::{Content, Label, Node, NodeId}; use crate::diag::{SourceResult, Trace, Tracepoint}; use crate::eval::{cast_from_value, Args, Cast, Dict, Func, Regex, Value}; use crate::syntax::Span; -use crate::util::pretty_array; +use crate::util::pretty_array_like; use crate::World; /// A map of style properties. @@ -86,7 +86,7 @@ impl Debug for StyleMap { let pieces: Vec<_> = self.0.iter().map(|value| eco_format!("{value:?}")).collect(); - f.write_str(&pretty_array(&pieces, false)) + f.write_str(&pretty_array_like(&pieces, false)) } } diff --git a/src/syntax/lexer.rs b/src/syntax/lexer.rs index 31608d40c..2d5559177 100644 --- a/src/syntax/lexer.rs +++ b/src/syntax/lexer.rs @@ -644,13 +644,13 @@ pub fn is_ident(string: &str) -> bool { /// Whether a character can start an identifier. #[inline] -pub(super) fn is_id_start(c: char) -> bool { +pub fn is_id_start(c: char) -> bool { c.is_xid_start() || c == '_' } /// Whether a character can continue an identifier. #[inline] -pub(super) fn is_id_continue(c: char) -> bool { +pub fn is_id_continue(c: char) -> bool { c.is_xid_continue() || c == '_' || c == '-' } diff --git a/src/util/mod.rs b/src/util/mod.rs index a3fad8cad..1eb191131 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -11,7 +11,6 @@ use std::hash::Hash; use std::path::{Component, Path, PathBuf}; use std::sync::Arc; -use ecow::EcoString; use siphasher::sip128::{Hasher128, SipHasher}; /// Turn a closure into a struct implementing [`Debug`]. @@ -133,10 +132,66 @@ impl PathExt for Path { } } -/// Format something as a a comma-separated list that support horizontal -/// formatting but falls back to vertical formatting if the pieces are too long. -pub fn pretty_array(pieces: &[EcoString], trailing_comma: bool) -> String { - let list = pretty_comma_list(&pieces, trailing_comma); +/// Format pieces separated with commas and a final "and" or "or". +pub fn separated_list(pieces: &[impl AsRef], last: &str) -> String { + let mut buf = String::new(); + for (i, part) in pieces.iter().enumerate() { + match i { + 0 => {} + 1 if pieces.len() == 2 => { + buf.push(' '); + buf.push_str(last); + buf.push(' '); + } + i if i + 1 == pieces.len() => { + buf.push_str(", "); + buf.push_str(last); + buf.push(' '); + } + _ => buf.push_str(", "), + } + buf.push_str(part.as_ref()); + } + buf +} + +/// Format a comma-separated list. +/// +/// Tries to format horizontally, but falls back to vertical formatting if the +/// pieces are too long. +pub fn pretty_comma_list(pieces: &[impl AsRef], trailing_comma: bool) -> String { + const MAX_WIDTH: usize = 50; + + let mut buf = String::new(); + let len = pieces.iter().map(|s| s.as_ref().len()).sum::() + + 2 * pieces.len().saturating_sub(1); + + if len <= MAX_WIDTH { + for (i, piece) in pieces.iter().enumerate() { + if i > 0 { + buf.push_str(", "); + } + buf.push_str(piece.as_ref()); + } + if trailing_comma { + buf.push(','); + } + } else { + for piece in pieces { + buf.push_str(piece.as_ref().trim()); + buf.push_str(",\n"); + } + } + + buf +} + +/// Format an array-like construct. +/// +/// Tries to format horizontally, but falls back to vertical formatting if the +/// pieces are too long. +pub fn pretty_array_like(parts: &[impl AsRef], trailing_comma: bool) -> String { + let list = pretty_comma_list(&parts, trailing_comma); let mut buf = String::new(); buf.push('('); if list.contains('\n') { @@ -150,35 +205,6 @@ pub fn pretty_array(pieces: &[EcoString], trailing_comma: bool) -> String { buf } -/// Format something as a a comma-separated list that support horizontal -/// formatting but falls back to vertical formatting if the pieces are too long. -pub fn pretty_comma_list(pieces: &[EcoString], trailing_comma: bool) -> String { - const MAX_WIDTH: usize = 50; - - let mut buf = String::new(); - let len = pieces.iter().map(|s| s.len()).sum::() - + 2 * pieces.len().saturating_sub(1); - - if len <= MAX_WIDTH { - for (i, piece) in pieces.iter().enumerate() { - if i > 0 { - buf.push_str(", "); - } - buf.push_str(piece); - } - if trailing_comma { - buf.push(','); - } - } else { - for piece in pieces { - buf.push_str(piece.trim()); - buf.push_str(",\n"); - } - } - - buf -} - /// Indent a string by two spaces. pub fn indent(text: &str, amount: usize) -> String { let mut buf = String::new();