Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 32 additions & 19 deletions src/domain/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,16 +253,28 @@ impl<'i> Response<'i> {
/// Render an Expression as human-readable text.
/// Returns (expression_text, body_lines) where body_lines captures multiline
/// content separately for distinct styling.
/// The text of a quoted literal, with any interpolation rendered as the
/// expression it came from. Shared by string values and tablet labels.
pub(crate) fn render_pieces(pieces: &[Piece]) -> String {
let mut result = String::new();

for piece in pieces {
match piece {
Piece::Text(t) => result.push_str(t),
Piece::Escaped(c) => result.push(*c),
Piece::Interpolation(e) => result.push_str(&render_expression(e)),
}
}

result
}

fn render_expression_parts(expr: &Expression) -> (String, Vec<String>) {
if let Expression::Execution(func, _) = expr {
let mut body = Vec::new();
for param in &func.parameters {
if let Expression::Multiline(_, lines, _) = param {
body.extend(
lines
.iter()
.map(|s| s.to_string()),
);
if let Expression::Multiline(multiline, _) = param {
body.push(multiline.content());
}
}
if !body.is_empty() {
Expand Down Expand Up @@ -334,26 +346,21 @@ fn render_expression(expr: &Expression) -> String {
args.join(", ")
)
}
Expression::Multiline(_, lines, _) => lines.join("\n"),
Expression::Multiline(multiline, _) => multiline.content(),
Expression::Variable(id, _) => id
.value
.to_string(),
Expression::Binding(inner, _, _) => render_expression(inner),
Expression::String(pieces, _) => {
let mut result = String::new();
for piece in pieces {
match piece {
Piece::Text(t) => result.push_str(t),
Piece::Interpolation(e) => result.push_str(&render_expression(e)),
}
}
result
}
Expression::String(pieces, _) => render_pieces(pieces),
Expression::Response(value, _) => format!("'{}'", value),
Expression::Number(Numeric::Scientific(q), _) => q.to_string(),
Expression::Number(Numeric::Integral(n), _) => n.to_string(),
Expression::Pair(pair, _) => {
format!("\"{}\" = {}", pair.label, render_expression(&pair.value))
format!(
"\"{}\" = {}",
render_pieces(&pair.label),
render_expression(&pair.value)
)
}
Expression::List(elements, _) => {
let items: Vec<_> = elements
Expand All @@ -375,7 +382,13 @@ fn render_expression(expr: &Expression) -> String {
}
let entries: Vec<_> = pairs
.iter()
.map(|pair| format!("\"{}\" = {}", pair.label, render_expression(&pair.value)))
.map(|pair| {
format!(
"\"{}\" = {}",
render_pieces(&pair.label),
render_expression(&pair.value)
)
})
.collect();
format!("[{}]", entries.join(", "))
}
Expand Down
9 changes: 3 additions & 6 deletions src/domain/recipe/adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use std::collections::{HashMap, HashSet};

use crate::domain::Adapter;
use crate::domain::engine::render_pieces;
use crate::language;

use super::types::{Document, Ingredient, Ingredients, Prose, Step};
Expand Down Expand Up @@ -128,9 +129,7 @@ fn collect_ingredients(items: &mut Vec<Ingredient>, scope: &language::Scope, pla
if let Some(pairs) = scope.tablet() {
for pair in pairs {
items.push(Ingredient {
label: pair
.label
.to_string(),
label: render_pieces(&pair.label),
quantity: format_value(&pair.value),
source: place.map(String::from),
});
Expand All @@ -143,9 +142,7 @@ fn collect_ingredients(items: &mut Vec<Ingredient>, scope: &language::Scope, pla
if let Some(pairs) = scope.inline_tablet() {
for pair in pairs {
items.push(Ingredient {
label: pair
.label
.to_string(),
label: render_pieces(&pair.label),
quantity: format_value(&pair.value),
source: place.map(String::from),
});
Expand Down
4 changes: 4 additions & 0 deletions src/editor/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,10 @@ impl TechniqueLanguageServer {
"Invalid quantity symbol".to_string(),
DiagnosticSeverity::ERROR,
),
ParsingError::InvalidEscape(_) => (
"Invalid escape sequence".to_string(),
DiagnosticSeverity::ERROR,
),
ParsingError::UnclosedInterpolation(_) => (
"Unclosed interpolation".to_string(),
DiagnosticSeverity::ERROR,
Expand Down
76 changes: 41 additions & 35 deletions src/formatting/formatter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ impl Substitutions {
}
}

fn escaped_as(c: char) -> char {
match c {
'\n' => 'n',
'\r' => 'r',
'\t' => 't',
other => other,
}
}

// Helper function to convert numbers to superscript
fn to_superscript(num: i8) -> String {
num.to_string()
Expand Down Expand Up @@ -471,7 +480,7 @@ impl<'i> Formatter<'i> {
return Vec::new();
}
match expr {
Expression::Multiline(_, _, _) => {
Expression::Multiline(_, _) => {
// These are not inline, caller should handle specially
Vec::new()
}
Expand Down Expand Up @@ -904,7 +913,7 @@ impl<'i> Formatter<'i> {
line = self.builder();
line.add_word(Syntax::Structure, "}");
}
Expression::Multiline(_, _, _) => {
Expression::Multiline(_, _) => {
line.flush();
self.add_fragment_reference(Syntax::Structure, "{");
self.increase(4);
Expand All @@ -921,7 +930,7 @@ impl<'i> Formatter<'i> {
.parameters
.iter()
.any(|p| {
if let Expression::Multiline(_, _, _) = p {
if let Expression::Multiline(_, _) = p {
true
} else {
false
Expand Down Expand Up @@ -1267,51 +1276,29 @@ impl<'i> Formatter<'i> {
self.add_fragment_reference(Syntax::Variable, identifier.value);
}
Expression::String(pieces, _) => {
self.add_fragment_reference(Syntax::Quote, "\"");
for piece in pieces {
match piece {
Piece::Text(text) => {
// Preserve user string content exactly as written
self.add_fragment_reference(Syntax::String, text);
}
Piece::Interpolation(expr) => {
let fragments = self.render_string_interpolation(expr);
for (syntax, content) in fragments {
self.add_fragment(syntax, content);
}
}
}
}
self.add_fragment_reference(Syntax::Quote, "\"");
self.append_quoted(Syntax::String, pieces);
}
Expression::Response(value, _) => {
self.add_fragment_reference(Syntax::Quote, "'");
self.add_fragment_reference(Syntax::Response, value);
self.add_fragment_reference(Syntax::Quote, "'");
}
Expression::Number(numeric, _) => self.append_numeric(numeric),
Expression::Multiline(lang, lines, _) => {
Expression::Multiline(multiline, _) => {
self.append_char('\n');

self.indent();
self.add_fragment_reference(Syntax::Quote, "```");
if let Some(which) = lang {
if let Some(which) = multiline.language {
self.add_fragment_reference(Syntax::Language, which);
}
self.append_char('\n');

self.increase(4);
for line in lines {
self.indent();
// Break multiline content into words for wrapping
for (i, word) in line
.split_ascii_whitespace()
.enumerate()
{
if i > 0 {
self.add_fragment_reference(Syntax::Multiline, " ");
}
self.add_fragment_reference(Syntax::Multiline, word);
for line in multiline.lines() {
if !line.is_empty() {
self.indent();
self.add_fragment(Syntax::Multiline, line);
}
self.append_char('\n');
}
Expand Down Expand Up @@ -1488,7 +1475,7 @@ impl<'i> Formatter<'i> {

let mut has_multiline = false;
for parameter in &function.parameters {
if let Expression::Multiline(_, _, _) = parameter {
if let Expression::Multiline(_, _) = parameter {
has_multiline = true;
break;
}
Expand All @@ -1512,10 +1499,29 @@ impl<'i> Formatter<'i> {
self.add_fragment_reference(Syntax::Structure, ")");
}

fn append_pair(&mut self, pair: &'i Pair) {
/// Write a quoted literal back out: text verbatim as it was borrowed,
/// escapes in the form they were written, interpolations rendered.
fn append_quoted(&mut self, syntax: Syntax, pieces: &'i [Piece]) {
self.add_fragment_reference(Syntax::Quote, "\"");
self.add_fragment_reference(Syntax::Label, pair.label);
for piece in pieces {
match piece {
Piece::Text(text) => self.add_fragment_reference(syntax, text),
Piece::Escaped(c) => {
self.add_fragment(syntax, Cow::Owned(format!("\\{}", escaped_as(*c))))
}
Piece::Interpolation(expr) => {
let fragments = self.render_string_interpolation(expr);
for (syntax, content) in fragments {
self.add_fragment(syntax, content);
}
}
}
}
self.add_fragment_reference(Syntax::Quote, "\"");
}

fn append_pair(&mut self, pair: &'i Pair) {
self.append_quoted(Syntax::Label, &pair.label);
self.add_fragment_reference(Syntax::Neutral, " ");
self.add_fragment_reference(Syntax::Structure, "=");
self.add_fragment_reference(Syntax::Neutral, " ");
Expand Down
2 changes: 2 additions & 0 deletions src/language/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
// Types representing the Technique language surface syntax

mod error;
mod multiline;
mod quantity;
mod types;

// Re-export all public symbols
pub use error::*;
pub use multiline::*;
pub use quantity::*;
pub use types::*;
87 changes: 87 additions & 0 deletions src/language/multiline.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
//! The content of a ``` fence

use std::borrow::Cow;

#[derive(Debug, Eq, PartialEq)]
pub struct Multiline<'i> {
pub language: Option<&'i str>,
pub lines: Vec<&'i str>,
}

impl<'i> Multiline<'i> {
/// The lines as they were present in the input source but with the leading block
/// indentation removed.
pub fn lines(&self) -> impl Iterator<Item = Cow<'i, str>> + '_ {
let common = self
.lines
.iter()
.filter(|line| {
!line
.trim_ascii()
.is_empty()
})
.map(|line| indent(line))
.min()
.unwrap_or(0);

self.lines
.iter()
.map(move |line| strip(expand(line), common))
}

/// The lines, joined, suitable for use by builtin functions.
pub fn content(&self) -> String {
self.lines()
.collect::<Vec<Cow<'i, str>>>()
.join("\n")
}
}

fn indent(line: &str) -> usize {
let mut column = 0;

for c in line.chars() {
match c {
' ' => column += 1,
'\t' => column = (column / 4 + 1) * 4,
_ => break,
}
}

column
}

/// Expand any tabs present into 4 spaces. If the author needs an actual tab
/// character in a sting literal they can use the `\t` escape.
fn expand(line: &str) -> Cow<'_, str> {
if !line.contains('\t') {
return Cow::Borrowed(line);
}

let mut result = String::with_capacity(line.len() + 8);
let mut column = 0;

for c in line.chars() {
if c == '\t' {
let stop = (column / 4 + 1) * 4;
result.push_str(&" ".repeat(stop - column));
column = stop;
} else {
result.push(c);
column += 1;
}
}

result.into()
}

/// Strip leading block indendation from a line.
fn strip(line: Cow<'_, str>, common: usize) -> Cow<'_, str> {
match line {
Cow::Borrowed(text) => Cow::Borrowed(&text[common.min(text.len())..]),
Cow::Owned(mut text) => {
text.drain(..common.min(text.len()));
Cow::Owned(text)
}
}
}
Loading