source_span/
position.rs

1use crate::{Metrics, Span};
2use std::fmt;
3
4/// Position in a source file (line and column).
5///
6/// This holds the line and column position of a character in a source file.
7/// Some operations are available to move position in a file. In partular, the
8/// [`next`](Position::next) method computes the next cursor position after
9/// reading a given [`char`].
10///
11/// ## Display
12///
13/// The struct implements two different format traits:
14///
15///  * [`fmt::Display`] will format the position as `line {line} column
16///    {column}`
17///  * [`fmt::Debug`] will format the position as `{line}:{column}`.
18///
19/// Both of them will display lines and columns starting at `1` even though the
20/// internal representation starts at `0`.
21#[derive(Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
22pub struct Position {
23	/// Line number, starting at `0`.
24	pub line: usize,
25
26	/// Column number, starting at `0`.
27	pub column: usize,
28}
29
30impl Position {
31	/// Create a new position given a line and column.
32	///
33	/// Indexes starts at `0`.
34	#[must_use]
35	pub const fn new(line: usize, column: usize) -> Self { Self { line, column } }
36
37	/// Return the maximum position.
38	///
39	/// # Example
40	///
41	/// ```
42	/// use source_span::Position;
43	///
44	/// assert_eq!(
45	/// 	Position::end(),
46	/// 	Position::new(usize::max_value(), usize::max_value())
47	/// 	);
48	/// ```
49	#[must_use]
50	pub const fn end() -> Self {
51		Self {
52			line: usize::max_value(),
53			column: usize::max_value(),
54		}
55	}
56
57	/// Move to the next column.
58	#[must_use]
59	pub const fn next_column(&self) -> Self {
60		Self {
61			line: self.line,
62			column: self.column + 1,
63		}
64	}
65
66	/// Move to the begining of the line.
67	#[must_use]
68	pub const fn reset_column(&self) -> Self {
69		Self {
70			line: self.line,
71			column: 0,
72		}
73	}
74
75	/// Move to the next line, and reset the column position.
76	#[must_use]
77	pub const fn next_line(&self) -> Self {
78		Self {
79			line: self.line + 1,
80			column: 0,
81		}
82	}
83
84	/// Move to the position following the given [`char`] using the given [`Metrics`].
85	///
86	/// ## Control characters
87	///
88	/// This crate is intended to help with incremental lexing/parsing.
89	/// Therefore, any control character moving the cursor backward will be
90	/// ignored: it will be treated as a 0-width character with no
91	/// semantics.
92	///
93	/// ### New lines
94	///
95	/// The `\n` character is interpreted with the Unix semantics, as the new
96	/// line (NL) character. It will reset the column position to `0` and
97	/// move to the next line.
98	///
99	/// ### Tabulations
100	///
101	/// The `\t` will move the cursor to the next horizontal tab-top.
102	/// The length of a tab-stop (in columns) is given by the `metrics` parameter.
103	///
104	/// ## Full-width characters
105	///
106	/// Note that, as for now, double-width characters of full-width characters are *not*
107	/// supported by the [`DefaultMetrics`](`crate::DefaultMetrics`).
108	/// They will move the cursor by only one column as any other
109	/// regular-width character. You are welcome to contribute to handle
110	/// them.
111	#[must_use]
112	pub fn next<M: Metrics>(&self, c: char, metrics: &M) -> Self {
113		match c {
114			'\n' => self.next_line(),
115			'\t' => {
116				let ts = metrics.tab_stop();
117				Self {
118					line: self.line,
119					column: (self.column / ts) * ts + ts,
120				}
121			}
122			c if c.is_control() => *self,
123			_ => {
124				Self {
125					line: self.line,
126					column: self.column + metrics.char_width(c),
127				}
128			}
129		}
130	}
131
132	pub fn shift<M: Metrics>(&mut self, c: char, metrics: &M) { *self = self.next(c, metrics) }
133
134	/// Creates the span ending at this position (excluded) from
135	/// `first` included to `last` included.
136	/// 
137	/// It is assumed that this position follows `last`.
138	/// This is equivalent to `Span::new(first, last, *self)`. 
139	#[must_use]
140	#[inline(always)]
141	pub fn from(&self, first: Self, last: Self) -> Span {
142		Span::new(first, last, *self)
143	}
144
145	/// Creates the span ending at this position (included) from
146	/// `first` included to `end` excluded.
147	/// 
148	/// It is assumed that `end` follows this position.
149	/// This is equivalent to `Span::new(first, *self, end)`. 
150	#[must_use]
151	#[inline(always)]
152	pub fn from_included(&self, first: Self, end: Self) -> Span {
153		Span::new(first, *self, end)
154	}
155
156	/// Creates the span starting from this position to
157	/// `last` included and `end` excluded.
158	/// 
159	/// It is assumed that `end` follows `last`.
160	/// This is equivalent to `Span::new(*self, last, end)`. 
161	#[must_use]
162	#[inline(always)]
163	pub fn to(&self, last: Self, end: Self) -> Span {
164		Span::new(*self, last, end)
165	}
166}
167
168impl fmt::Display for Position {
169	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
170		if self.line == usize::max_value() && self.column == usize::max_value() {
171			write!(f, "line [end] column [end]")
172		} else if self.line == usize::max_value() {
173			write!(f, "line [end] column {}", self.column + 1)
174		} else if self.column == usize::max_value() {
175			write!(f, "line {} column [end]", self.line + 1)
176		} else {
177			write!(f, "line {} column {}", self.line + 1, self.column + 1)
178		}
179	}
180}
181
182impl fmt::Debug for Position {
183	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
184		if self.line == usize::max_value() && self.column == usize::max_value() {
185			write!(f, "[end]:[end]")
186		} else if self.line == usize::max_value() {
187			write!(f, "[end]:{}", self.column + 1)
188		} else if self.column == usize::max_value() {
189			write!(f, "{}:[end]", self.line + 1)
190		} else {
191			write!(f, "{}:{}", self.line + 1, self.column + 1)
192		}
193	}
194}
195
196#[cfg(test)]
197mod tests {
198	use super::*;
199
200	macro_rules! min {
201        ($x: expr) => ($x);
202        ($x: expr, $($z: expr),+ $(,)* ) => (::std::cmp::min($x, min!($($z),*)));
203    }
204
205	macro_rules! max {
206        ($x: expr) => ($x);
207        ($x: expr, $($z: expr),+ $(,)* ) => (::std::cmp::max($x, max!($($z),*)));
208    }
209
210	// An order is a total order if it is (for all a, b and c):
211	// - total and antisymmetric: exactly one of a < b, a == b or a > b is true; and
212	// - transitive, a < b and b < c implies a < c. The same must hold for both ==
213	//   and >.
214	#[test]
215	fn test_ord_position() {
216		assert_eq!(
217			min!(
218				Position::new(1, 2),
219				Position::new(1, 3),
220				Position::new(1, 4),
221				Position::new(1, 2),
222				Position::new(2, 1),
223				Position::new(3, 12),
224				Position::new(4, 4),
225			),
226			Position::new(1, 2)
227		);
228
229		assert_eq!(
230			max!(
231				Position::new(1, 2),
232				Position::new(1, 3),
233				Position::new(1, 4),
234				Position::new(1, 2),
235				Position::new(2, 1),
236				Position::new(3, 12),
237				Position::new(4, 4),
238			),
239			Position::new(4, 4)
240		);
241	}
242
243	#[test]
244	fn test_debug() {
245		assert_eq!(format!("{:?}", Position::new(2, 3)), "3:4".to_string());
246		assert_eq!(
247			format!("{:?}", Position::new(usize::max_value(), 3)),
248			"[end]:4".to_string()
249		);
250		assert_eq!(
251			format!("{:?}", Position::new(3, usize::max_value())),
252			"4:[end]".to_string()
253		);
254		assert_eq!(
255			format!(
256				"{:?}",
257				Position::new(usize::max_value(), usize::max_value())
258			),
259			"[end]:[end]".to_string()
260		);
261	}
262}