1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
// Copyright (c) ZeroC, Inc.
use crate::grammar::*;
use crate::utils::ptr_util::WeakPtr;
use console::style;
use serde::Serialize;
use std::cmp::{max, min, Ordering};
use std::fmt::{Display, Write};
const EXPANDED_TAB: &str = " ";
/// Stores the row and column numbers of a location in a Slice file.
/// These values are indexed starting at 1 instead of 0 for human readability.
/// Ex: (1,1) is the start of a file: the first column in the first row.
#[derive(Serialize, Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub struct Location {
pub row: usize,
pub col: usize,
}
impl Location {
/// Returns true if this [`Location`] is within the specified [`Span`] (including the span's boundary).
pub fn is_within(&self, span: &Span) -> bool {
self.cmp(&span.start) != Ordering::Less && self.cmp(&span.end) != Ordering::Greater
}
}
impl From<(usize, usize)> for Location {
/// Creates a [Location] from a pair of indices, where the first element represents the line number,
/// and the second element represents the column number.
fn from(x: (usize, usize)) -> Self {
Location { row: x.0, col: x.1 }
}
}
impl Default for Location {
/// Returns a [Location] representing the start of a file: (1,1).
fn default() -> Self {
Location { row: 1, col: 1 }
}
}
#[derive(Serialize, Clone, Debug, PartialEq, Eq)]
pub struct Span {
pub start: Location,
pub end: Location,
pub file: String,
}
impl Span {
pub fn new(start: Location, end: Location, file: &str) -> Self {
let file = file.to_owned();
Span { start, end, file }
}
}
impl std::ops::Add for &Span {
type Output = Span;
fn add(self, rhs: Self) -> Self::Output {
Span {
start: min(self.start, rhs.start),
end: max(self.end, rhs.end),
file: self.file.clone(),
}
}
}
#[derive(Debug)]
pub struct SliceFile {
pub filename: String,
pub relative_path: String,
pub raw_text: String,
pub mode: Option<FileCompilationMode>,
pub module: Option<WeakPtr<Module>>,
pub attributes: Vec<WeakPtr<Attribute>>,
pub contents: Vec<Definition>,
pub is_source: bool,
}
impl SliceFile {
pub fn new(relative_path: String, raw_text: String, is_source: bool) -> Self {
// Extract the name of the slice file without its extension.
let filename = std::path::Path::new(&relative_path)
.file_stem()
.unwrap()
.to_os_string()
.into_string()
.unwrap();
SliceFile {
filename,
relative_path,
raw_text,
mode: None,
module: None,
attributes: Vec::new(),
contents: Vec::new(),
is_source,
}
}
/// Returns the compilation mode used by this file.
///
/// If a mode wasn't explicitly stated, it returns the default mode.
///
/// See [CompilationMode::default()](crate::grammar::CompilationMode::default)
pub fn compilation_mode(&self) -> CompilationMode {
self.mode
.as_ref()
.map_or(CompilationMode::default(), |mode| mode.version)
}
/// Retrieves a formatted snippet from the slice file.
pub(crate) fn get_snippet(&self, start: Location, end: Location) -> String {
debug_assert!(start <= end);
// The number of columns that should be reserved for displaying line numbers to the left of snippets.
// Equal to the number of digits in the longest line number plus one (longest number is always the end).
// Ex: "273 | source code" `line_number_prefix_length` would be 4 for "273 " (4 chars long).
let line_number_prefix_length = end.row.to_string().len() + 1;
// Returns a formatted line prefix of the form: "[line number]<padding>|".
let line_number_prefix = |line_number: Option<usize>| {
// If a line number was provided, use it, otherwise use the empty string.
let number_string: &dyn Display = line_number.as_ref().map_or(&"", |i| i);
// Pad the string with spaces (on the right) so its total length is `line_number_prefix_length`.
let padded_number_string = format!("{number_string:<line_number_prefix_length$}|");
// Style the string and return it.
style(padded_number_string).blue().bold().to_string()
};
// The prefix for lines not showing a line number.
let line_prefix = line_number_prefix(None);
// The lines of text that should be included in the snippet.
let lines = self
.raw_text
.lines()
.enumerate()
.filter(|(line_number, _)| *line_number >= start.row - 1 && *line_number < end.row);
// The formatted snippet.
let mut formatted_snippet = line_prefix.clone() + "\n";
for (i, line) in lines {
// The actual line number
let line_number = i + 1;
let width = line.chars().count();
// The start and end positions of the highlight.
let highlight_start = if line_number == start.row { start.col - 1 } else { 0 };
let highlight_end = if line_number == end.row { end.col - 1 } else { width };
// Expand tabs to 4 spaces so that we can properly compute the highlight length.
let prefix = line_number_prefix(Some(line_number));
let space_separated_line = line.replace('\t', EXPANDED_TAB);
formatted_snippet += &(prefix + " " + &space_separated_line + "\n");
let highlight = get_highlight(line, highlight_start, highlight_end);
writeln!(formatted_snippet, "{line_prefix}{highlight}").expect("failed to write snippet");
}
formatted_snippet + &line_prefix
}
}
pub fn compute_sha256_hash_of_source_files(files: &[SliceFile]) -> String {
use sha2::{Digest, Sha256};
// Filter out any reference files, and sort the source files which remain.
let mut sorted_sources: Vec<&SliceFile> = files.iter().filter(|f| f.is_source).collect();
sorted_sources.sort_by(|a, b| a.filename.cmp(&b.filename));
// Hash the sorted source files.
// Included in the hash are the files' names (no path, just filenames), and their raw (unparsed) content.
let mut hash_engine = Sha256::new();
for file in sorted_sources {
hash_engine.update(&file.filename);
hash_engine.update(&file.raw_text);
}
// Return the hash engine's final result, formatted as a lowercase-hexadecimal string.
// The hash engine guarantees that this string will have 64 chars (representing 32 bytes, or 256 bits).
format!("{:x}", hash_engine.finalize())
}
implement_Attributable_for!(SliceFile);
fn get_highlight(line: &str, highlight_start: usize, highlight_end: usize) -> String {
// The whitespace that should be displayed before the highlight. Tabs are expanded to 4 spaces.
// We always start with one space to separate the highlight from the vertical separator.
let mut whitespace_count = 1;
for c in line.chars().take(highlight_start) {
if c == '\t' {
whitespace_count += EXPANDED_TAB.len();
} else {
whitespace_count += 1;
}
}
// The highlight that should be displayed.
// If it's between 2 characters (same start and end), then we use a single point.
// If the provided range is between 2 locations, highlight everything between them.
let highlight = if highlight_start == highlight_end {
// Subtract 1 from the whitespace count so the '/' is in the column before the highlight starts.
whitespace_count -= 1;
// Point to a single character.
style(r"/\".to_owned()).yellow().bold()
} else {
// Number of tabs between the start and end of the highlight.
let highlight_tab_count = line
.chars()
.skip(highlight_start)
.take(highlight_end - highlight_start)
.filter(|c| *c == '\t')
.count();
// Since tab is only 1 character, we have to account for the extra 3 characters that are displayed
// for each tab.
let highlight_length = (highlight_end - highlight_start) + (highlight_tab_count * (EXPANDED_TAB.len() - 1));
style(format!("{:-<1$}", "", highlight_length)).yellow().bold()
};
" ".repeat(whitespace_count) + &highlight.to_string()
}