hi_doc/
lib.rs

1extern crate hi_doc_jumprope as jumprope;
2
3use std::{
4	collections::{BTreeMap, BTreeSet, HashMap, HashSet},
5	ops::RangeInclusive,
6};
7
8mod segment;
9use annotation::{Annotation, AnnotationId, Opts};
10use anomaly_fixer::{apply_fixup, fixup_byte_to_char, fixup_char_to_display};
11pub use formatting::Text;
12use rand::{rngs::SmallRng, Rng, SeedableRng};
13use random_color::{
14	options::{Gamut, Luminosity},
15	RandomColor,
16};
17use range_map::{Range, RangeSet};
18use segment::SegmentBuffer;
19use single_line::LineAnnotation;
20
21pub use crate::formatting::Formatting;
22
23mod annotation;
24mod anomaly_fixer;
25pub(crate) mod associated_data;
26mod chars;
27mod formatting;
28mod inline;
29mod single_line;
30
31#[derive(Clone, Debug)]
32struct RawLine {
33	data: Text,
34}
35
36#[derive(Debug)]
37struct AnnotationLine {
38	prefix: Text,
39	line: Text,
40	/// There will be lines drawn to connect lines with the same annotation id specified
41	annotation: Option<AnnotationId>,
42}
43
44#[derive(Debug)]
45struct GapLine {
46	prefix: Text,
47	line: Text,
48}
49
50#[derive(Debug)]
51struct TextLine {
52	prefix: Text,
53	line_num: usize,
54	line: Text,
55	/// Is this line allowed to be hidden by fold?
56	fold: bool,
57	annotation: Option<AnnotationId>,
58	annotations: Vec<LineAnnotation>,
59	top_annotations: Vec<(Option<AnnotationId>, Text)>,
60	bottom_annotations: Vec<(Option<AnnotationId>, Text)>,
61}
62impl TextLine {
63	#[allow(dead_code)]
64	fn add_prefix(&mut self, this: Text, annotations: Text) {
65		self.prefix.extend([this]);
66		for (_, ele) in self.bottom_annotations.iter_mut() {
67			ele.splice(0..0, Some(annotations.clone()));
68		}
69	}
70	fn len(&self) -> usize {
71		self.line.len()
72	}
73	fn is_empty(&self) -> bool {
74		self.line.is_empty()
75	}
76	// fn trim_end(&mut self) {
77	// 	self.line.truncate(self.line.trim_end().len());
78	// }
79}
80
81fn cons_slices<T>(mut slice: &mut [T], test: impl Fn(&T) -> bool) -> Vec<&mut [T]> {
82	let mut out = Vec::new();
83
84	while !slice.is_empty() {
85		let mut skip = 0;
86		while !slice.get(skip).map(&test).unwrap_or(true) {
87			skip += 1;
88		}
89		let mut take = 0;
90		while slice.get(skip + take).map(&test).unwrap_or(false) {
91			take += 1;
92		}
93		let (_skipped, rest) = slice.split_at_mut(skip);
94		let (taken, rest) = rest.split_at_mut(take);
95		if !taken.is_empty() {
96			out.push(taken);
97		}
98		slice = rest;
99	}
100
101	out
102}
103
104#[derive(Debug)]
105enum Line {
106	Text(TextLine),
107	Annotation(AnnotationLine),
108	Raw(RawLine),
109	Nop,
110	Gap(GapLine),
111}
112impl Line {
113	fn text_mut(&mut self) -> Option<&mut Text> {
114		Some(match self {
115			Line::Text(t) => &mut t.line,
116			Line::Gap(t) => &mut t.line,
117			Line::Annotation(t) => &mut t.line,
118			_ => return None,
119		})
120	}
121	fn is_text(&self) -> bool {
122		matches!(self, Self::Text(_))
123	}
124	fn is_annotation(&self) -> bool {
125		matches!(self, Self::Annotation(_))
126	}
127	fn as_annotation(&self) -> Option<&AnnotationLine> {
128		match self {
129			Self::Annotation(a) => Some(a),
130			_ => None,
131		}
132	}
133	fn is_gap(&self) -> bool {
134		matches!(self, Self::Gap(_))
135	}
136	fn as_text_mut(&mut self) -> Option<&mut TextLine> {
137		match self {
138			Line::Text(t) => Some(t),
139			_ => None,
140		}
141	}
142	#[allow(dead_code)]
143	fn as_gap_mut(&mut self) -> Option<&mut GapLine> {
144		match self {
145			Line::Gap(t) => Some(t),
146			_ => None,
147		}
148	}
149	fn as_text(&self) -> Option<&TextLine> {
150		match self {
151			Line::Text(t) => Some(t),
152			_ => None,
153		}
154	}
155	fn as_raw(&self) -> Option<&RawLine> {
156		match self {
157			Line::Raw(r) => Some(r),
158			_ => None,
159		}
160	}
161	fn is_nop(&self) -> bool {
162		matches!(self, Self::Nop)
163	}
164}
165
166#[derive(Debug)]
167pub struct Source {
168	lines: Vec<Line>,
169}
170
171fn cleanup_nops(source: &mut Source) {
172	let mut i = 0;
173	while i < source.lines.len() {
174		if source.lines[i].is_nop() {
175			source.lines.remove(i);
176		} else {
177			i += 1;
178		}
179	}
180}
181
182/// Remove NOP/empty annotation lines
183fn cleanup(source: &mut Source) {
184	for slice in cons_slices(&mut source.lines, Line::is_text) {
185		for line in slice
186			.iter_mut()
187			.take_while(|l| l.as_text().unwrap().is_empty())
188		{
189			*line = Line::Nop;
190		}
191		for line in slice
192			.iter_mut()
193			.rev()
194			.take_while(|l| l.as_text().unwrap().is_empty())
195		{
196			*line = Line::Nop;
197		}
198	}
199	cleanup_nops(source);
200	for slice in cons_slices(&mut source.lines, Line::is_gap) {
201		if slice.len() == 1 {
202			continue;
203		}
204		for ele in slice.iter_mut().skip(1) {
205			*ele = Line::Nop;
206		}
207	}
208	cleanup_nops(source);
209}
210
211fn fold(source: &mut Source, opts: &Opts) {
212	for slice in cons_slices(&mut source.lines, Line::is_text) {
213		'line: for i in 0..slice.len() {
214			for j in i.saturating_sub(opts.context_lines)..=(i + opts.context_lines) {
215				let Some(ctx) = slice.get(j) else {
216					continue;
217				};
218				let Line::Text(t) = ctx else {
219					continue;
220				};
221				if t.fold {
222					continue;
223				}
224				continue 'line;
225			}
226			slice[i] = Line::Gap(GapLine {
227				prefix: Text::new(),
228				line: Text::new(),
229			});
230		}
231	}
232	cleanup(source);
233}
234
235fn draw_line_numbers(source: &mut Source) {
236	for lines in &mut cons_slices(&mut source.lines, |l| {
237		l.is_annotation() || l.is_text() || l.is_gap()
238	}) {
239		let max_num = lines
240			.iter()
241			.filter_map(|l| match l {
242				Line::Text(t) => Some(t.line_num),
243				_ => None,
244			})
245			.max()
246			.unwrap_or(0);
247		let max_len = max_num.to_string().len();
248		let prefix_segment =
249			SegmentBuffer::segment(" ".repeat(max_len - 1), Formatting::line_number());
250		for line in lines.iter_mut() {
251			match line {
252				Line::Text(t) => t.prefix.extend([SegmentBuffer::segment(
253					format!("{:>width$} ", t.line_num, width = max_len),
254					Formatting::line_number(),
255				)]),
256				Line::Annotation(a) => a.prefix.extend([
257					prefix_segment.clone(),
258					SegmentBuffer::segment("· ", Formatting::line_number()),
259				]),
260				Line::Gap(a) => a.prefix.extend([
261					prefix_segment.clone(),
262					SegmentBuffer::segment("⋮ ", Formatting::line_number()),
263				]),
264				_ => unreachable!(),
265			}
266		}
267	}
268}
269
270fn draw_line_connections(
271	source: &mut Source,
272	annotation_formats: HashMap<AnnotationId, Formatting>,
273) {
274	for lines in &mut cons_slices(&mut source.lines, |l| {
275		l.is_annotation() || l.is_text() || l.is_gap()
276	}) {
277		#[derive(Debug)]
278		struct Connection {
279			range: Range<usize>,
280			connected: Vec<usize>,
281		}
282
283		let mut connected_annotations = HashMap::new();
284		for (i, line) in lines.iter().enumerate() {
285			let annotation = if let Some(annotation) = line.as_annotation() {
286				annotation.annotation
287			} else if let Some(text) = line.as_text() {
288				text.annotation
289			} else {
290				None
291			};
292			if let Some(annotation) = annotation {
293				let conn = connected_annotations
294					.entry(annotation)
295					.or_insert(Connection {
296						range: Range::new(i, i),
297						connected: Vec::new(),
298					});
299				conn.range.start = conn.range.start.min(i);
300				conn.range.end = conn.range.end.max(i);
301				conn.connected.push(i);
302			}
303		}
304		let mut grouped = connected_annotations
305			.iter()
306			.map(|(k, v)| (*k, vec![v.range].into_iter().collect::<RangeSet<usize>>()))
307			.collect::<Vec<_>>();
308
309		grouped.sort_by_key(|a| a.1.num_elements());
310		let grouped = single_line::group_nonconflicting(&grouped, &HashSet::new());
311
312		for group in grouped {
313			for annotation in group {
314				let annotation_fmt = annotation_formats
315					.get(&annotation)
316					.expect("id is used in string but not defined")
317					.clone()
318					.decoration();
319				let conn = connected_annotations.get(&annotation).expect("exists");
320				let range = conn.range;
321				let mut max_index = usize::MAX;
322				for line in range.start..=range.end {
323					match &lines[line] {
324						Line::Text(t) if t.line.chars().all(|c| c.is_whitespace()) => {}
325						Line::Text(t) => {
326							let whitespaces =
327								t.line.chars().take_while(|i| i.is_whitespace()).count();
328							max_index = max_index.min(whitespaces)
329						}
330						Line::Annotation(t) if t.line.chars().all(|c| c.is_whitespace()) => {}
331						Line::Annotation(t) => {
332							let whitespaces =
333								t.line.chars().take_while(|i| i.is_whitespace()).count();
334							max_index = max_index.min(whitespaces)
335						}
336						Line::Gap(_) => {}
337						_ => unreachable!(),
338					}
339				}
340				while max_index < 2 {
341					let seg = Some(SegmentBuffer::segment(
342						" ".repeat(2 - max_index),
343						annotation_fmt.clone(),
344					));
345					for line in lines.iter_mut() {
346						match line {
347							Line::Text(t) => t.line.splice(0..0, seg.clone()),
348							Line::Annotation(t) => t.line.splice(0..0, seg.clone()),
349							Line::Gap(t) => t.line.splice(0..0, seg.clone()),
350							_ => unreachable!(),
351						}
352					}
353					max_index = 2;
354				}
355				if max_index >= 2 {
356					let offset = max_index - 2;
357
358					for line in range.start..=range.end {
359						use chars::line::*;
360						let char = if range.start == range.end {
361							RANGE_EMPTY
362						} else if line == range.start {
363							RANGE_START
364						} else if line == range.end {
365							RANGE_END
366						} else if conn.connected.contains(&line) {
367							RANGE_CONNECTION
368						} else {
369							RANGE_CONTINUE
370						};
371						let text = lines[line].text_mut().expect("only with text reachable");
372						if text.len() <= offset {
373							text.resize(offset + 1, ' ', annotation_fmt.clone());
374						}
375						text.splice(
376							offset..offset + 1,
377							Some(SegmentBuffer::segment(
378								char.to_string(),
379								annotation_fmt.clone(),
380							)),
381						);
382
383						if conn.connected.contains(&line) {
384							for i in offset + 1..text.len() {
385								let (char, fmt) = text.get(i).expect("in bounds");
386								if !text.get(i).expect("in bounds").0.is_whitespace()
387									&& !fmt.decoration
388								{
389									break;
390								}
391								if let Some((keep_style, replacement)) = cross(char) {
392									text.splice(
393										i..=i,
394										Some(SegmentBuffer::segment(
395											replacement.to_string(),
396											if keep_style {
397												fmt.clone()
398											} else {
399												annotation_fmt.clone()
400											},
401										)),
402									)
403								}
404							}
405						}
406					}
407				}
408			}
409		}
410	}
411}
412
413fn generate_annotations(source: &mut Source, opts: &Opts) {
414	for line in source
415		.lines
416		.iter_mut()
417		.flat_map(Line::as_text_mut)
418		.filter(|t| !t.annotations.is_empty())
419	{
420		let hide_ranges_for = if opts.apply_to_orig {
421			let parsed = inline::group_singleline(&line.annotations);
422			assert!(line.annotation.is_none());
423			line.annotation = parsed.annotation;
424			inline::apply_inline_annotations(&mut line.line, &parsed.inline, parsed.right);
425
426			line.annotations
427				.retain(|a| !parsed.processed.contains(&a.id));
428			line.fold = false;
429
430			parsed.hide_ranges_for
431		} else {
432			HashSet::new()
433		};
434
435		let char_to_display_fixup = fixup_char_to_display(line.line.chars());
436		let mut extra = single_line::generate_range_annotations(
437			line.annotations.clone(),
438			&char_to_display_fixup,
439			&hide_ranges_for,
440			false,
441		);
442		extra.reverse();
443		// TODO: instead of writing generated annotations into lines, return them from this function, and apply later
444		line.top_annotations = extra;
445		line.annotations.truncate(0);
446	}
447}
448
449fn apply_annotations(source: &mut Source) {
450	// Top
451	{
452		let mut insertions = vec![];
453		for (i, line) in source
454			.lines
455			.iter_mut()
456			.enumerate()
457			.flat_map(|(i, l)| l.as_text_mut().map(|t| (i, t)))
458		{
459			for buf in line.top_annotations.drain(..) {
460				insertions.push((i + 1, buf))
461			}
462		}
463		insertions.reverse();
464		for (i, (annotation, line)) in insertions {
465			source.lines.insert(
466				i - 1,
467				Line::Annotation(AnnotationLine {
468					line,
469					annotation,
470					prefix: SegmentBuffer::new(),
471				}),
472			);
473		}
474	}
475	// Bottom
476	{
477		let mut insertions = vec![];
478		for (i, line) in source
479			.lines
480			.iter_mut()
481			.enumerate()
482			.flat_map(|(i, l)| l.as_text_mut().map(|t| (i, t)))
483		{
484			for buf in line.bottom_annotations.drain(..) {
485				insertions.push((i + 1, buf))
486			}
487		}
488		insertions.reverse();
489		for (i, (annotation, line)) in insertions {
490			source.lines.insert(
491				i,
492				Line::Annotation(AnnotationLine {
493					line,
494					annotation,
495					prefix: SegmentBuffer::new(),
496				}),
497			);
498		}
499	}
500}
501
502fn process(
503	source: &mut Source,
504	annotation_formats: HashMap<AnnotationId, Formatting>,
505	opts: &Opts,
506) {
507	cleanup(source);
508	// Format inline annotations
509	generate_annotations(source, opts);
510	// Make gaps in files
511	if opts.fold {
512		fold(source, opts)
513	}
514	// Expand annotation buffers
515	apply_annotations(source);
516	// Connect annotation lines
517	draw_line_connections(source, annotation_formats);
518	// Apply line numbers
519	draw_line_numbers(source);
520	// To raw
521	{
522		for line in &mut source.lines {
523			match line {
524				Line::Text(t) => {
525					let mut buf = SegmentBuffer::new();
526					buf.extend([t.prefix.clone(), t.line.clone()]);
527					*line = Line::Raw(RawLine { data: buf });
528				}
529				Line::Annotation(t) => {
530					let mut buf = SegmentBuffer::new();
531					buf.extend([t.prefix.clone(), t.line.clone()]);
532					*line = Line::Raw(RawLine { data: buf })
533				}
534				Line::Gap(t) => {
535					let mut buf = SegmentBuffer::new();
536					buf.extend([t.prefix.clone(), t.line.clone()]);
537					*line = Line::Raw(RawLine { data: buf })
538				}
539				Line::Raw(_) | Line::Nop => {}
540			}
541		}
542	}
543	cleanup(source);
544}
545
546fn linestarts(str: &str) -> BTreeSet<usize> {
547	let mut linestarts = BTreeSet::new();
548	for (i, c) in str.chars().enumerate() {
549		if c == '\n' {
550			linestarts.insert(i + 1);
551		}
552	}
553	linestarts
554}
555struct LineCol {
556	line: usize,
557	column: usize,
558}
559fn offset_to_linecol(mut offset: usize, linestarts: &BTreeSet<usize>) -> LineCol {
560	let mut line = 0;
561	let last_offset = linestarts
562		.range(..=offset)
563		.inspect(|_| line += 1)
564		.last()
565		.copied()
566		.unwrap_or(0);
567	offset -= last_offset;
568	LineCol {
569		line,
570		column: offset,
571	}
572}
573
574fn parse(txt: &str, annotations: &[Annotation], opts: &Opts) -> Source {
575	let (txt, byte_to_char_fixup) = fixup_byte_to_char(txt, opts.tab_width);
576	let mut annotations = annotations.to_vec();
577
578	// Convert byte offsets to char offsets
579	for annotation in annotations.iter_mut() {
580		let ranges: RangeSet<usize> = annotation
581			.ranges
582			.ranges()
583			.map(|r| {
584				let mut start = r.start;
585				let mut end = r.end;
586				apply_fixup(&mut start, &byte_to_char_fixup);
587				apply_fixup(&mut end, &byte_to_char_fixup);
588				Range::new(start, end)
589			})
590			.collect();
591		annotation.ranges = ranges;
592	}
593	let linestarts = linestarts(&txt);
594
595	let mut lines: Vec<Line> = txt
596		.split('\n')
597		.map(|s| s.to_string())
598		.enumerate()
599		.map(|(num, line)| TextLine {
600			line_num: num + 1,
601			line: SegmentBuffer::segment(
602				// Reserve 1 char for the spans pointing to EOL
603				line.chars().chain([' ']).collect::<String>(),
604				Formatting::default(),
605			),
606			annotation: None,
607			prefix: SegmentBuffer::new(),
608			annotations: Vec::new(),
609			bottom_annotations: Vec::new(),
610			top_annotations: Vec::new(),
611			fold: true,
612		})
613		.map(Line::Text)
614		.collect();
615
616	for (aid, annotation) in annotations.iter().enumerate() {
617		let mut line_ranges: BTreeMap<usize, RangeSet<usize>> = BTreeMap::new();
618		for range in annotation.ranges.ranges() {
619			let start = offset_to_linecol(range.start, &linestarts);
620			let end = offset_to_linecol(range.end, &linestarts);
621
622			if start.line == end.line {
623				let set = line_ranges.entry(start.line).or_insert_with(RangeSet::new);
624				*set = set.union(&[Range::new(start.column, end.column)].into_iter().collect());
625			} else {
626				{
627					let set = line_ranges.entry(start.line).or_insert_with(RangeSet::new);
628					let line = lines[start.line].as_text().expect("annotation OOB");
629					*set = set.union(
630						&[Range::new(start.column, line.len() - 1)]
631							.into_iter()
632							.collect(),
633					);
634				}
635				{
636					let set = line_ranges.entry(end.line).or_insert_with(RangeSet::new);
637					*set = set.union(&[Range::new(0, end.column)].into_iter().collect());
638				}
639			}
640		}
641		let left = line_ranges.len() > 1;
642		let line_ranges_len = line_ranges.len();
643
644		for (i, (line, ranges)) in line_ranges.into_iter().enumerate() {
645			let last = i == line_ranges_len - 1;
646			let line = lines[line].as_text_mut().expect("annotation OOB");
647			line.annotations.push(LineAnnotation {
648				id: AnnotationId(aid),
649				priority: annotation.priority,
650				ranges,
651				formatting: annotation.formatting.clone(),
652				left,
653				right: if last {
654					annotation.text.clone()
655				} else {
656					Text::new()
657				},
658			});
659			line.fold = false;
660		}
661	}
662
663	let mut source = Source { lines };
664
665	let annotation_formats = annotations
666		.iter()
667		.enumerate()
668		.map(|(aid, a)| (AnnotationId(aid), a.formatting.clone()))
669		.collect();
670
671	process(&mut source, annotation_formats, opts);
672
673	source
674}
675
676pub fn source_to_ansi(source: &Source) -> String {
677	let mut out = String::new();
678	for line in &source.lines {
679		let line = line
680			.as_raw()
681			.expect("after processing all lines should turn raw");
682		let data = line.data.clone();
683		formatting::text_to_ansi(&data, &mut out);
684		out.push('\n');
685	}
686	out
687}
688
689pub struct FormattingGenerator {
690	rand: SmallRng,
691}
692impl FormattingGenerator {
693	pub fn new(src: &[u8]) -> Self {
694		let mut rng_seed = [0; 8];
695		// let seed = seed.to_value();
696		for chunk in src.chunks(8) {
697			for (s, c) in rng_seed.iter_mut().zip(chunk.iter()) {
698				*s ^= *c;
699			}
700		}
701
702		Self {
703			rand: SmallRng::seed_from_u64(u64::from_be_bytes(rng_seed)),
704		}
705	}
706	fn next(&mut self) -> RandomColor {
707		let mut color = RandomColor::new();
708		color.seed(self.rand.gen::<u64>());
709		color.luminosity(Luminosity::Bright);
710		color
711	}
712}
713
714pub struct SnippetBuilder {
715	src: String,
716	generator: FormattingGenerator,
717	annotations: Vec<Annotation>,
718}
719impl SnippetBuilder {
720	pub fn new(src: impl AsRef<str>) -> Self {
721		Self {
722			src: src.as_ref().to_string(),
723			generator: FormattingGenerator::new(src.as_ref().as_bytes()),
724			annotations: Vec::new(),
725		}
726	}
727	fn custom(&mut self, custom_color: Gamut, text: Text) -> AnnotationBuilder<'_> {
728		let mut color = self.generator.next();
729		color.hue(custom_color);
730		let formatting = Formatting::rgb(color.to_rgb_array());
731		// FIXME: apply_meta is not implemented
732		// let [r, g, b] = color.luminosity(Luminosity::Light).to_rgb_array();
733		// text.apply_meta(
734		// 	0..text.len(),
735		// 	&AddColorToUncolored(u32::from_be_bytes([r, g, b, 0])),
736		// );
737		AnnotationBuilder {
738			snippet: self,
739			priority: 0,
740			formatting,
741			ranges: Vec::new(),
742			text,
743		}
744	}
745	pub fn error(&mut self, text: Text) -> AnnotationBuilder<'_> {
746		self.custom(Gamut::Red, text)
747	}
748	pub fn warning(&mut self, text: Text) -> AnnotationBuilder<'_> {
749		self.custom(Gamut::Orange, text)
750	}
751	pub fn note(&mut self, text: Text) -> AnnotationBuilder<'_> {
752		self.custom(Gamut::Green, text)
753	}
754	pub fn info(&mut self, text: Text) -> AnnotationBuilder<'_> {
755		self.custom(Gamut::Blue, text)
756	}
757	pub fn build(self) -> Source {
758		parse(
759			&self.src,
760			&self.annotations,
761			&Opts {
762				apply_to_orig: true,
763				fold: true,
764				tab_width: 4,
765				context_lines: 2,
766			},
767		)
768	}
769}
770
771#[must_use]
772pub struct AnnotationBuilder<'s> {
773	snippet: &'s mut SnippetBuilder,
774	priority: usize,
775	formatting: Formatting,
776	ranges: Vec<Range<usize>>,
777	text: Text,
778}
779
780impl AnnotationBuilder<'_> {
781	pub fn range(mut self, range: RangeInclusive<usize>) -> Self {
782		assert!(
783			*range.end() < self.snippet.src.len(),
784			"out of bounds annotation"
785		);
786		self.ranges.push(Range::new(*range.start(), *range.end()));
787		self
788	}
789	pub fn ranges(mut self, ranges: impl IntoIterator<Item = RangeInclusive<usize>>) -> Self {
790		for range in ranges {
791			self = self.range(range);
792		}
793		self
794	}
795	pub fn build(self) {
796		self.snippet.annotations.push(Annotation {
797			priority: self.priority,
798			formatting: self.formatting,
799			ranges: self.ranges.into_iter().collect(),
800			text: self.text,
801		});
802	}
803}
804
805#[cfg(test)]
806mod tests {
807	use super::*;
808
809	fn default<T: Default>() -> T {
810		Default::default()
811	}
812
813	#[test]
814	fn readme() {
815		let mut snippet = SnippetBuilder::new(include_str!("../../../fixtures/std.jsonnet"));
816		snippet
817			.error(Text::segment("Local defs", default()))
818			.ranges([4..=8, 3142..=3146])
819			.build();
820		snippet
821			.warning(Text::segment("Local name", default()))
822			.range(10..=12)
823			.build();
824		snippet
825			.info(Text::segment("Equals", default()))
826			.range(14..=14)
827			.build();
828		snippet
829			.note(Text::segment("Connected definition", default()))
830			.ranges([3133..=3135, 6155..=6157])
831			.build();
832		snippet
833			.note(Text::segment("Another connected definition", default()))
834			.ranges([5909..=5913, 6062..=6066, 6242..=6244])
835			.build();
836		let s = snippet.build();
837		println!("{}", source_to_ansi(&s))
838	}
839
840	#[test]
841	fn test_fmt() {
842		let mut snippet = SnippetBuilder::new(include_str!("../../../fixtures/std.jsonnet"));
843		snippet
844			.info(Text::segment("Hello world", default()))
845			.range(2832..=3135)
846			.build();
847		snippet
848			.warning(Text::segment("Conflict", default()))
849			.range(2838..=2847)
850			.build();
851		snippet
852			.error(Text::segment("Still has text", default()))
853			.range(2839..=2846)
854			.build();
855		let s = snippet.build();
856		println!("{}", source_to_ansi(&s))
857	}
858
859	#[test]
860	fn fullwidth_marker() {
861		let mut snippet = SnippetBuilder::new("ABC");
862		snippet
863			.info(Text::segment("a", default()))
864			.range(0..=2)
865			.build();
866		snippet
867			.info(Text::segment("b", default()))
868			.range(3..=5)
869			.build();
870		snippet
871			.info(Text::segment("c", default()))
872			.range(6..=8)
873			.build();
874		let s = snippet.build();
875		println!("{}", source_to_ansi(&s))
876	}
877
878	#[test]
879	fn fullwidth_marker_apply() {
880		let s = parse(
881			"ABC",
882			&[
883				Annotation {
884					priority: 0,
885					formatting: Formatting::color(0xff000000),
886					ranges: [Range::new(0, 2)].into_iter().collect(),
887					text: Text::segment("a", default()),
888				},
889				Annotation {
890					priority: 0,
891					formatting: Formatting::color(0x00ff0000),
892					ranges: [Range::new(3, 5)].into_iter().collect(),
893					text: Text::segment("b", default()),
894				},
895				Annotation {
896					priority: 0,
897					formatting: Formatting::color(0x0000ff00),
898					ranges: [Range::new(6, 8)].into_iter().collect(),
899					text: Text::segment("c", default()),
900				},
901			],
902			&Opts {
903				apply_to_orig: true,
904				fold: true,
905				tab_width: 4,
906				context_lines: 2,
907			},
908		);
909		println!("{}", source_to_ansi(&s))
910	}
911
912	#[test]
913	fn tab_in_normal_and_fullwidth() {
914		let s = parse(
915			"A\tB\n\tB\na\tb\n\tb",
916			&[
917				Annotation {
918					priority: 0,
919					formatting: Formatting::color(0xff000000),
920					ranges: [Range::new(17, 17)].into_iter().collect(),
921					text: Text::segment("Line start", default()),
922				},
923				Annotation {
924					priority: 0,
925					formatting: Formatting::color(0x00ff0000),
926					ranges: [Range::new(18, 18)].into_iter().collect(),
927					text: Text::segment("Aligned", default()),
928				},
929			],
930			&Opts {
931				apply_to_orig: false,
932				fold: false,
933				tab_width: 4,
934				context_lines: 2,
935			},
936		);
937		dbg!(&s);
938		println!("{}", source_to_ansi(&s))
939	}
940
941	#[test]
942	fn example_from_annotate_snippets() {
943		let src = r#") -> Option<String> {
944	for ann in annotations {
945		match (ann.range.0, ann.range.1) {
946			(None, None) => continue,
947			(Some(start), Some(end)) if start > end_index => continue,
948			(Some(start), Some(end)) if start >= start_index => {
949				let label = if let Some(ref label) = ann.label {
950					format!(" {}", label)
951				} else {
952					String::from("")
953				};
954				return Some(format!(
955					"{}{}{}",
956					" ".repeat(start - start_index),
957					"^".repeat(end - start),
958					label
959				));
960			}
961			_ => continue,
962		}
963	}"#;
964		let mut snippet = SnippetBuilder::new(src);
965		snippet
966			.error(Text::segment(
967				"expected `Option<String>` because of return type",
968				default(),
969			))
970			.range(5..=18)
971			.build();
972		snippet
973			.note(Text::segment(
974				"expected enum `std::option::Option`",
975				default(),
976			))
977			.range(22..=510)
978			.build();
979		let s = snippet.build();
980		println!("{}", source_to_ansi(&s))
981	}
982}