mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
Autocompletion for raw language tags
This commit is contained in:
parent
a9fdff244a
commit
8e5f446544
@ -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(),
|
||||
|
@ -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();
|
||||
|
24
src/diag.rs
24
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<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>;
|
||||
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 ");
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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`.
|
||||
|
@ -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,
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 == '-'
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
Loading…
x
Reference in New Issue
Block a user