Autocompletion for raw language tags

This commit is contained in:
Laurenz 2023-03-11 11:46:12 +01:00
parent a9fdff244a
commit 8e5f446544
13 changed files with 146 additions and 76 deletions

View File

@ -182,6 +182,7 @@ fn items() -> LangItems {
} }
node.pack() node.pack()
}, },
raw_languages: text::RawNode::languages,
link: |url| meta::LinkNode::from_url(url).pack(), link: |url| meta::LinkNode::from_url(url).pack(),
ref_: |target| meta::RefNode::new(target).pack(), ref_: |target| meta::RefNode::new(target).pack(),
heading: |level, title| meta::HeadingNode::new(title).with_level(level).pack(), heading: |level, title| meta::HeadingNode::new(title).with_level(level).pack(),

View File

@ -103,6 +103,23 @@ pub struct RawNode {
pub lang: Option<EcoString>, pub lang: Option<EcoString>,
} }
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 { impl Prepare for RawNode {
fn prepare(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> { fn prepare(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
let mut node = self.clone().pack(); let mut node = self.clone().pack();

View File

@ -177,30 +177,6 @@ where
} }
} }
/// Format the parts separated with commas and a final "and" or "or".
pub(crate) fn comma_list<S>(buf: &mut String, parts: &[S], last: &str)
where
S: AsRef<str>,
{
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. /// A result type with a file-related error.
pub type FileResult<T> = Result<T, FileError>; pub type FileResult<T> = Result<T, FileError>;

View File

@ -5,7 +5,7 @@ use ecow::{eco_format, EcoVec};
use super::{Array, Cast, Dict, Str, Value}; use super::{Array, Cast, Dict, Str, Value};
use crate::diag::{bail, At, SourceResult}; use crate::diag::{bail, At, SourceResult};
use crate::syntax::{Span, Spanned}; use crate::syntax::{Span, Spanned};
use crate::util::pretty_array; use crate::util::pretty_array_like;
/// Evaluated arguments to a function. /// Evaluated arguments to a function.
#[derive(Clone, PartialEq, Hash)] #[derive(Clone, PartialEq, Hash)]
@ -174,7 +174,7 @@ impl Debug for Args {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let pieces: Vec<_> = let pieces: Vec<_> =
self.items.iter().map(|arg| eco_format!("{arg:?}")).collect(); self.items.iter().map(|arg| eco_format!("{arg:?}")).collect();
f.write_str(&pretty_array(&pieces, false)) f.write_str(&pretty_array_like(&pieces, false))
} }
} }

View File

@ -6,7 +6,7 @@ use ecow::{eco_format, EcoString, EcoVec};
use super::{ops, Args, Func, Value, Vm}; use super::{ops, Args, Func, Value, Vm};
use crate::diag::{bail, At, SourceResult, StrResult}; use crate::diag::{bail, At, SourceResult, StrResult};
use crate::util::pretty_array; use crate::util::pretty_array_like;
/// Create a new [`Array`] from values. /// Create a new [`Array`] from values.
#[macro_export] #[macro_export]
@ -343,7 +343,7 @@ impl Array {
impl Debug for Array { impl Debug for Array {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let pieces: Vec<_> = self.iter().map(|value| eco_format!("{value:?}")).collect(); 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))
} }
} }

View File

@ -8,6 +8,7 @@ use ecow::EcoString;
use super::{Array, Str, Value}; use super::{Array, Str, Value};
use crate::diag::StrResult; use crate::diag::StrResult;
use crate::syntax::Spanned; use crate::syntax::Spanned;
use crate::util::separated_list;
/// Cast from a value to a specific type. /// Cast from a value to a specific type.
pub trait Cast<V = Value>: Sized { pub trait Cast<V = Value>: Sized {
@ -284,7 +285,7 @@ impl CastInfo {
msg.push_str(" nothing"); msg.push_str(" nothing");
} }
crate::diag::comma_list(&mut msg, &parts, "or"); msg.push_str(&separated_list(&parts, "or"));
if !matching_type { if !matching_type {
msg.push_str(", found "); msg.push_str(", found ");

View File

@ -8,7 +8,7 @@ use ecow::{eco_format, EcoString};
use super::{array, Array, Str, Value}; use super::{array, Array, Str, Value};
use crate::diag::StrResult; use crate::diag::StrResult;
use crate::syntax::is_ident; 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. /// Create a new [`Dict`] from key-value pairs.
#[macro_export] #[macro_export]
@ -125,7 +125,7 @@ impl Dict {
if let Some((key, _)) = self.iter().next() { if let Some((key, _)) = self.iter().next() {
let parts: Vec<_> = expected.iter().map(|s| eco_format!("\"{s}\"")).collect(); let parts: Vec<_> = expected.iter().map(|s| eco_format!("\"{s}\"")).collect();
let mut msg = format!("unexpected key {key:?}, valid keys are "); 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()); return Err(msg.into());
} }
Ok(()) Ok(())
@ -149,7 +149,7 @@ impl Debug for Dict {
}) })
.collect(); .collect();
f.write_str(&pretty_array(&pieces, false)) f.write_str(&pretty_array_like(&pieces, false))
} }
} }

View File

@ -55,6 +55,8 @@ pub struct LangItems {
pub emph: fn(body: Content) -> Content, pub emph: fn(body: Content) -> Content,
/// Raw text with optional syntax highlighting: `` `...` ``. /// Raw text with optional syntax highlighting: `` `...` ``.
pub raw: fn(text: EcoString, tag: Option<EcoString>, block: bool) -> Content, pub raw: fn(text: EcoString, tag: Option<EcoString>, 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`. /// A hyperlink: `https://typst.org`.
pub link: fn(url: EcoString) -> Content, pub link: fn(url: EcoString) -> Content,
/// A reference: `@target`. /// A reference: `@target`.

View File

@ -2,10 +2,14 @@ use std::collections::{BTreeSet, HashSet};
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
use if_chain::if_chain; use if_chain::if_chain;
use unscanny::Scanner;
use super::{analyze_expr, analyze_import, plain_docs_sentence, summarize_font_family}; use super::{analyze_expr, analyze_import, plain_docs_sentence, summarize_font_family};
use crate::eval::{methods_on, CastInfo, Scope, Value}; use crate::eval::{methods_on, CastInfo, Library, Scope, Value};
use crate::syntax::{ast, LinkedNode, Source, SyntaxKind}; use crate::syntax::{
ast, is_id_continue, is_id_start, is_ident, LinkedNode, Source, SyntaxKind,
};
use crate::util::separated_list;
use crate::World; use crate::World;
/// Autocomplete a cursor position in a source file. /// 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: "|". // Anywhere: "|".
if ctx.explicit { if ctx.explicit {
ctx.from = ctx.cursor; ctx.from = ctx.cursor;
@ -830,9 +850,11 @@ fn code_completions(ctx: &mut CompletionContext, hashtag: bool) {
/// Context for autocompletion. /// Context for autocompletion.
struct CompletionContext<'a> { struct CompletionContext<'a> {
world: &'a (dyn World + 'static), world: &'a (dyn World + 'static),
library: &'a Library,
source: &'a Source, source: &'a Source,
global: &'a Scope, global: &'a Scope,
math: &'a Scope, math: &'a Scope,
text: &'a str,
before: &'a str, before: &'a str,
after: &'a str, after: &'a str,
leaf: LinkedNode<'a>, leaf: LinkedNode<'a>,
@ -852,12 +874,15 @@ impl<'a> CompletionContext<'a> {
explicit: bool, explicit: bool,
) -> Option<Self> { ) -> Option<Self> {
let text = source.text(); let text = source.text();
let library = world.library();
let leaf = LinkedNode::new(source.root()).leaf_at(cursor)?; let leaf = LinkedNode::new(source.root()).leaf_at(cursor)?;
Some(Self { Some(Self {
world, world,
library,
source, source,
global: &world.library().global.scope(), global: &library.global.scope(),
math: &world.library().math.scope(), math: &library.math.scope(),
text,
before: &text[..cursor], before: &text[..cursor],
after: &text[cursor..], after: &text[cursor..],
leaf, 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. /// Add a completion for a specific value.
fn value_completion( fn value_completion(
&mut self, &mut self,

View File

@ -12,7 +12,7 @@ use super::{node, Guard, Recipe, Style, StyleMap};
use crate::diag::{SourceResult, StrResult}; use crate::diag::{SourceResult, StrResult};
use crate::eval::{cast_from_value, Args, FuncInfo, Str, Value, Vm}; use crate::eval::{cast_from_value, Args, FuncInfo, Str, Value, Vm};
use crate::syntax::Span; use crate::syntax::Span;
use crate::util::pretty_array; use crate::util::pretty_array_like;
use crate::World; use crate::World;
/// Composable representation of styled content. /// Composable representation of styled content.
@ -261,7 +261,7 @@ impl Debug for Content {
.collect(); .collect();
f.write_str(name)?; f.write_str(name)?;
f.write_str(&pretty_array(&pieces, false)) f.write_str(&pretty_array_like(&pieces, false))
} }
} }

View File

@ -8,7 +8,7 @@ use super::{Content, Label, Node, NodeId};
use crate::diag::{SourceResult, Trace, Tracepoint}; use crate::diag::{SourceResult, Trace, Tracepoint};
use crate::eval::{cast_from_value, Args, Cast, Dict, Func, Regex, Value}; use crate::eval::{cast_from_value, Args, Cast, Dict, Func, Regex, Value};
use crate::syntax::Span; use crate::syntax::Span;
use crate::util::pretty_array; use crate::util::pretty_array_like;
use crate::World; use crate::World;
/// A map of style properties. /// A map of style properties.
@ -86,7 +86,7 @@ impl Debug for StyleMap {
let pieces: Vec<_> = let pieces: Vec<_> =
self.0.iter().map(|value| eco_format!("{value:?}")).collect(); self.0.iter().map(|value| eco_format!("{value:?}")).collect();
f.write_str(&pretty_array(&pieces, false)) f.write_str(&pretty_array_like(&pieces, false))
} }
} }

View File

@ -644,13 +644,13 @@ pub fn is_ident(string: &str) -> bool {
/// Whether a character can start an identifier. /// Whether a character can start an identifier.
#[inline] #[inline]
pub(super) fn is_id_start(c: char) -> bool { pub fn is_id_start(c: char) -> bool {
c.is_xid_start() || c == '_' c.is_xid_start() || c == '_'
} }
/// Whether a character can continue an identifier. /// Whether a character can continue an identifier.
#[inline] #[inline]
pub(super) fn is_id_continue(c: char) -> bool { pub fn is_id_continue(c: char) -> bool {
c.is_xid_continue() || c == '_' || c == '-' c.is_xid_continue() || c == '_' || c == '-'
} }

View File

@ -11,7 +11,6 @@ use std::hash::Hash;
use std::path::{Component, Path, PathBuf}; use std::path::{Component, Path, PathBuf};
use std::sync::Arc; use std::sync::Arc;
use ecow::EcoString;
use siphasher::sip128::{Hasher128, SipHasher}; use siphasher::sip128::{Hasher128, SipHasher};
/// Turn a closure into a struct implementing [`Debug`]. /// 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 /// Format pieces separated with commas and a final "and" or "or".
/// formatting but falls back to vertical formatting if the pieces are too long. pub fn separated_list(pieces: &[impl AsRef<str>], last: &str) -> String {
pub fn pretty_array(pieces: &[EcoString], trailing_comma: bool) -> String { let mut buf = String::new();
let list = pretty_comma_list(&pieces, trailing_comma); 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<str>], 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::<usize>()
+ 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<str>], trailing_comma: bool) -> String {
let list = pretty_comma_list(&parts, trailing_comma);
let mut buf = String::new(); let mut buf = String::new();
buf.push('('); buf.push('(');
if list.contains('\n') { if list.contains('\n') {
@ -150,35 +205,6 @@ pub fn pretty_array(pieces: &[EcoString], trailing_comma: bool) -> String {
buf 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::<usize>()
+ 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. /// Indent a string by two spaces.
pub fn indent(text: &str, amount: usize) -> String { pub fn indent(text: &str, amount: usize) -> String {
let mut buf = String::new(); let mut buf = String::new();