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

View File

@ -103,6 +103,23 @@ pub struct RawNode {
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 {
fn prepare(&self, _: &mut Vt, styles: StyleChain) -> SourceResult<Content> {
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.
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 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))
}
}

View File

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

View File

@ -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<V = Value>: 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 ");

View File

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

View File

@ -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<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`.
pub link: fn(url: EcoString) -> Content,
/// A reference: `@target`.

View File

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

View File

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

View File

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

View File

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

View File

@ -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<str>], 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<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();
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::<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.
pub fn indent(text: &str, amount: usize) -> String {
let mut buf = String::new();