Skip to content

Commit 8046c26

Browse files
authored
Colour for error messages (#59)
Add emphasis colours to error output. Add dependency on the **owo-color** crate to handle embellishing text strings with ANSI colours, and then update the error formatters to use its methods. Includes more of the ongoing effort to improve error messages, specifically related to detecting malformed steps and substeps.
2 parents 72f4bd9 + a9c3624 commit 8046c26

File tree

6 files changed

+116
-37
lines changed

6 files changed

+116
-37
lines changed

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
name = "technique"
33
version = "0.3.3"
44
edition = "2021"
5-
description = "A domain specific lanaguage for procedures."
5+
description = "A domain specific language for procedures."
66
authors = [ "Andrew Cowie" ]
77
repository = "https://github.com/technique-lang/technique"
88
license = "MIT"
99

1010
[dependencies]
1111
clap = { version = "4.5.16", features = [ "wrap_help" ] }
12+
owo-colors = "4"
1213
regex = "1.11.1"
1314
serde = { version = "1.0.209", features = [ "derive" ] }
1415
tinytemplate = "1.2.1"

src/error/display.rs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use std::{fmt, path::Path};
22

3+
use owo_colors::OwoColorize;
4+
35
#[derive(Debug, Clone, PartialEq, Eq)]
46
pub struct TechniqueError<'i> {
57
pub problem: String,
@@ -32,25 +34,30 @@ impl<'i> TechniqueError<'i> {
3234

3335
format!(
3436
r#"
35-
error: {}
37+
{}: {}
3638
{}:{}:{}
3739
38-
{:width$} |
39-
{:width$} | {}
40-
{:width$} | {:>j$}
40+
{:width$} {}
41+
{:width$} {} {}
42+
{:width$} {} {:>j$}
4143
4244
{}
4345
"#,
44-
self.problem,
46+
"error".bright_red(),
47+
self.problem
48+
.bold(),
4549
self.filename
4650
.to_string_lossy(),
4751
line,
4852
column,
4953
' ',
50-
line,
54+
'|'.bright_blue(),
55+
line.bright_blue(),
56+
'|'.bright_blue(),
5157
code,
5258
' ',
53-
'^',
59+
'|'.bright_blue(),
60+
'^'.bright_red(),
5461
self.details
5562
)
5663
.trim_ascii()

src/parsing/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! parser for the Technique language
22
3+
use owo_colors::OwoColorize;
34
use std::path::Path;
45
use tracing::debug;
56

@@ -44,7 +45,7 @@ pub fn parse<'i>(filename: &'i Path, content: &'i str) -> Document<'i> {
4445
} else {
4546
debug!("No content found");
4647
}
47-
eprintln!("Ok");
48+
eprintln!("{}", "ok".bright_green());
4849

4950
document
5051
}

src/parsing/parser.rs

Lines changed: 64 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::path::Path;
2+
use tracing::debug;
23

34
use crate::error::*;
45
use crate::language::*;
@@ -15,7 +16,10 @@ pub fn parse_via_taking<'i>(
1516
let result = input.parse_from_start();
1617
match result {
1718
Ok(technique) => Ok(technique),
18-
Err(error) => Err(make_error(input, error)),
19+
Err(error) => {
20+
debug!(?error);
21+
Err(make_error(input, error))
22+
}
1923
}
2024
}
2125

@@ -52,6 +56,7 @@ pub enum ParsingError<'i> {
5256
InvalidCodeBlock(usize),
5357
InvalidMultiline(usize),
5458
InvalidStep(usize),
59+
InvalidSubstep(usize),
5560
InvalidForeach(usize),
5661
InvalidResponse(usize),
5762
InvalidNumeric(usize),
@@ -79,6 +84,7 @@ impl<'i> ParsingError<'i> {
7984
ParsingError::InvalidCodeBlock(offset) => *offset,
8085
ParsingError::InvalidMultiline(offset) => *offset,
8186
ParsingError::InvalidStep(offset) => *offset,
87+
ParsingError::InvalidSubstep(offset) => *offset,
8288
ParsingError::InvalidForeach(offset) => *offset,
8389
ParsingError::InvalidResponse(offset) => *offset,
8490
ParsingError::InvalidNumeric(offset) => *offset,
@@ -330,15 +336,45 @@ it may be used by output templates when rendering the procedure.
330336
ParsingError::InvalidStep(_) => (
331337
"Invalid step format".to_string(),
332338
r#"
333-
Steps must start with a number or letter (in the case of dependent steps and
334-
sub-steps, respectively) followed by a '.', or a dash (for tasks that can
335-
execute in parallel):
339+
Steps must start with a number or lower-case letter (in the case of dependent
340+
steps and sub-steps, respectively) followed by a '.':
336341
337342
1. First step
338343
2. Second step
339344
a. First substep
340345
b. Second substep
341-
- Parallel task
346+
347+
Steps or substeps that can execute in parallel can instead be marked with a
348+
dash. They can be done in either order, or concurrently:
349+
350+
- Do one thing
351+
- And another
352+
"#
353+
.trim_ascii()
354+
.to_string(),
355+
),
356+
ParsingError::InvalidSubstep(_) => (
357+
"Invalid substep format".to_string(),
358+
r#"
359+
Substeps can be nested below top-level depenent steps or top-level parallel
360+
steps. So both of these are valid:
361+
362+
1. First top-level step.
363+
a. First substep in first dependent step.
364+
b. Second substep in first dependent step.
365+
366+
and
367+
368+
- First top-level step to be done in any order.
369+
a. First substep in first parallel step.
370+
b. Second substep in first parallel step.
371+
372+
The ordinal must be a lowercase letter and not a roman numeral. By convention
373+
substeps are indented by 4 characters, but that is not required.
374+
375+
Note also that the substeps can be consecutively numbered, which allows each
376+
substep to be uniquely identified when they are grouped under different
377+
parallel steps, but again this is not compulsory.
342378
"#
343379
.trim_ascii()
344380
.to_string(),
@@ -1083,7 +1119,7 @@ impl<'i> Parser<'i> {
10831119
if !steps.is_empty() {
10841120
elements.push(Element::Steps(steps));
10851121
}
1086-
} else if is_invalid_step_pattern(content) {
1122+
} else if malformed_step_pattern(content) {
10871123
// Detect and reject invalid step patterns
10881124
return Err(ParsingError::InvalidStep(outer.offset));
10891125
} else {
@@ -1093,13 +1129,13 @@ impl<'i> Parser<'i> {
10931129
!is_step(line)
10941130
&& !is_procedure_title(line)
10951131
&& !is_code_block(line)
1096-
&& !is_invalid_step_pattern(line)
1132+
&& !malformed_step_pattern(line)
10971133
},
10981134
|line| {
10991135
is_step(line)
11001136
|| is_procedure_title(line)
11011137
|| is_code_block(line)
1102-
|| is_invalid_step_pattern(line)
1138+
|| malformed_step_pattern(line)
11031139
},
11041140
|inner| {
11051141
let content = inner.source;
@@ -1695,7 +1731,8 @@ impl<'i> Parser<'i> {
16951731
|| is_subsubstep_dependent(line)
16961732
|| is_role_assignment(line)
16971733
|| is_enum_response(line)
1698-
|| is_invalid_response_pattern(line)
1734+
|| malformed_step_pattern(line)
1735+
|| malformed_response_pattern(line)
16991736
|| is_code_block(line)
17001737
},
17011738
|outer| {
@@ -1917,32 +1954,29 @@ impl<'i> Parser<'i> {
19171954
/// Trim any leading whitespace (space, tab, newline) from the front of
19181955
/// the current parser text.
19191956
fn trim_whitespace(&mut self) {
1920-
let mut l = 0;
1921-
19221957
if self
19231958
.source
19241959
.is_empty()
19251960
{
19261961
return;
19271962
}
19281963

1929-
for c in self
1964+
let bytes = self
19301965
.source
1931-
.chars()
1932-
{
1933-
if c == '\n' {
1934-
l += 1;
1935-
continue;
1936-
} else if c.is_ascii_whitespace() {
1937-
l += 1;
1938-
continue;
1939-
} else {
1940-
break;
1966+
.as_bytes();
1967+
let mut i = 0;
1968+
1969+
while i < bytes.len() {
1970+
match bytes[i] {
1971+
b' ' | b'\t' | b'\n' | b'\r' => {
1972+
i += 1;
1973+
}
1974+
_ => break,
19411975
}
19421976
}
19431977

1944-
self.source = &self.source[l..];
1945-
self.offset += l;
1978+
self.source = &self.source[i..];
1979+
self.offset += i;
19461980
}
19471981

19481982
/// Parse role assignments like @surgeon, @nurse, or @marketing + @sales
@@ -2008,7 +2042,9 @@ impl<'i> Parser<'i> {
20082042
} else if is_code_block(content) {
20092043
let block = self.read_code_scope()?;
20102044
scopes.push(block);
2011-
} else if is_invalid_response_pattern(content) {
2045+
} else if malformed_step_pattern(content) {
2046+
return Err(ParsingError::InvalidSubstep(self.offset));
2047+
} else if malformed_response_pattern(content) {
20122048
return Err(ParsingError::InvalidResponse(self.offset));
20132049
} else if is_enum_response(content) {
20142050
let responses = self.read_responses()?;
@@ -2233,7 +2269,7 @@ fn is_procedure_title(content: &str) -> bool {
22332269
// I'm not sure about anchoring this one on start and end, seeing as how it
22342270
// will be used when scanning.
22352271
fn is_invocation(content: &str) -> bool {
2236-
let re = regex!(r"^\s*(<.+?>\s*(?:\(.*?\))?)\s*$");
2272+
let re = regex!(r"^\s*<");
22372273

22382274
re.is_match(content)
22392275
}
@@ -2291,7 +2327,7 @@ fn is_step(content: &str) -> bool {
22912327
}
22922328

22932329
/// Detect patterns that look like steps but are invalid at the top-level
2294-
fn is_invalid_step_pattern(content: &str) -> bool {
2330+
fn malformed_step_pattern(content: &str) -> bool {
22952331
let re = regex!(r"^\s*([a-zA-Z]|[ivxIVX]+)\.\s+");
22962332
re.is_match(content)
22972333
}
@@ -2336,7 +2372,7 @@ fn is_enum_response(content: &str) -> bool {
23362372
}
23372373

23382374
/// Detect response patterns with double quotes
2339-
fn is_invalid_response_pattern(content: &str) -> bool {
2375+
fn malformed_response_pattern(content: &str) -> bool {
23402376
let re = regex!(r#"^\s*".+?"(\s*\|\s*".+?")*"#);
23412377
re.is_match(content)
23422378
}

tests/parsing/errors.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,4 +238,31 @@ making_coffee :
238238
ParsingError::ExpectedMatchingChar(43, "a function call", '(', ')'),
239239
);
240240
}
241+
242+
#[test]
243+
fn invalid_invocation_in_repeat() {
244+
expect_error(
245+
r#"
246+
making_coffee :
247+
248+
1. { repeat <making_coffee }
249+
"#
250+
.trim_ascii(),
251+
ParsingError::ExpectedMatchingChar(29, "an invocation", '<', '>'),
252+
);
253+
}
254+
255+
#[test]
256+
fn invalid_substep_uppercase() {
257+
expect_error(
258+
r#"
259+
making_coffee :
260+
261+
1. First step
262+
A. This should be lowercase
263+
"#
264+
.trim_ascii(),
265+
ParsingError::InvalidSubstep(37),
266+
);
267+
}
241268
}

0 commit comments

Comments
 (0)