1use super::workspace::{READ_CAP, Workspace, io_problem, not_regular, read_capped};
4
5pub(super) const MAX_LINES: usize = 2000;
7pub(super) const MAX_BYTES: usize = 100 * 1024;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13pub struct Lines {
14 pub start: Option<i64>,
16 pub end: Option<i64>,
18}
19
20impl Lines {
21 #[must_use]
23 pub fn asked(&self) -> bool {
24 self.start.is_some() || self.end.is_some()
25 }
26}
27
28pub(super) fn read(workspace: &Workspace, path: &str, lines: Lines) -> Result<String, String> {
29 let real = workspace.resolve(path)?;
30 let shown = workspace.relative(&real);
31 if real.is_dir() {
32 return Err(format!("`{shown}` is a folder; use list_dir to see what is in it."));
33 }
34 let contents =
35 read_capped(&real).map_err(|error| io_problem(&shown, &error))?.ok_or_else(|| not_regular(&shown))?;
36 let (whole, size) = (contents.whole(), contents.size);
37 let mut bytes = contents.bytes;
38 if !whole && let Some(end) = bytes.iter().rposition(|byte| *byte == b'\n') {
39 bytes.truncate(end + 1);
41 }
42 let text = match String::from_utf8(bytes) {
43 Ok(text) if !text.contains('\0') => text,
44 _ => {
45 return Err(format!("`{shown}` is not a text file ({size} bytes); it cannot be shown."));
46 }
47 };
48 if text.is_empty() {
49 return Ok(format!("`{shown}` is empty."));
50 }
51 let shown_part = if lines.asked() { part(&text, lines) } else { Ok(head(&text)) };
52 if whole {
53 return shown_part;
54 }
55 let cap = format!(
56 "(The file is {size} bytes and only its first {} MB are read, so the lines counted here are of that part.)",
57 READ_CAP / (1024 * 1024)
58 );
59 match shown_part {
60 Ok(text) => Ok(join(&text, &cap)),
61 Err(sentence) => Err(format!("{sentence} {cap}")),
62 }
63}
64
65fn head(text: &str) -> String {
67 let mut end = 0;
68 let mut lines = 0;
69 for line in text.split_inclusive('\n') {
70 if lines == MAX_LINES || end + line.len() > MAX_BYTES {
71 break;
72 }
73 end += line.len();
74 lines += 1;
75 }
76 if end == text.len() {
77 return text.to_owned();
78 }
79 let total = text.lines().count();
80 let note = if lines == 0 {
81 end = text.floor_char_boundary(MAX_BYTES);
83 format!(
84 "(Cut: the file has {total} lines and the first one alone is longer than {} KB; only its start is shown.)",
85 MAX_BYTES / 1024
86 )
87 } else {
88 format!("(Cut: the file has {total} lines; only the first {lines} are shown.)")
89 };
90 join(&text[..end], ¬e)
91}
92
93fn part(text: &str, lines: Lines) -> Result<String, String> {
96 let total = text.lines().count();
97 let (first, last) = bounds(lines, total)?;
98 let mut out = String::new();
99 let mut first_shown = 0;
100 let mut last_shown = 0;
101 let mut shown = 0;
102 let mut cut: Option<String> = None;
103 let mut offset = 0;
104 for (index, line) in text.split_inclusive('\n').enumerate() {
105 let number = index + 1;
106 if number < first {
107 offset += line.len();
108 continue;
109 }
110 if number > last {
111 break;
112 }
113 if shown == MAX_LINES {
114 cut = Some(format!("The {MAX_LINES}-line cap stopped it there."));
115 break;
116 }
117 if out.len() + line.len() > MAX_BYTES {
118 cut = Some(format!("The {} KB cap stopped it there.", MAX_BYTES / 1024));
119 break;
120 }
121 if first_shown == 0 {
122 first_shown = number;
123 }
124 out.push_str(line);
125 last_shown = number;
126 shown += 1;
127 }
128 if first_shown == 0 {
129 let rest = &text[offset..];
132 out.push_str(&rest[..rest.floor_char_boundary(MAX_BYTES)]);
133 first_shown = first;
134 last_shown = first;
135 cut = Some(format!("That one line is longer than {} KB and only its start is shown.", MAX_BYTES / 1024));
136 }
137 let note = note(first_shown, last_shown, total, cut.as_deref());
138 Ok(join(&out, ¬e))
139}
140
141fn bounds(lines: Lines, total: usize) -> Result<(usize, usize), String> {
144 for (field, number) in [("start_line", lines.start), ("end_line", lines.end)] {
145 if number.is_some_and(|number| number < 1) {
146 return Err(format!("Lines are counted from 1, so `{field}` must be 1 or more."));
147 }
148 }
149 let first = usize::try_from(lines.start.unwrap_or(1)).unwrap_or(usize::MAX);
150 let last = match lines.end {
151 Some(end) => {
152 let end = usize::try_from(end).unwrap_or(usize::MAX);
153 if end < first {
154 return Err("`end_line` is before `start_line`; give a range that starts before it ends.".to_owned());
155 }
156 end.min(total)
159 }
160 None => total,
161 };
162 if first > total {
163 return Err(format!("The file has {total} lines, so `start_line` {first} is past its end."));
164 }
165 Ok((first, last))
166}
167
168fn note(first: usize, last: usize, total: usize, cut: Option<&str>) -> String {
171 let above = first.saturating_sub(1);
172 let below = total.saturating_sub(last);
173 let left = match (above > 0, below > 0) {
174 (true, true) => format!("{} above and {} below", count(above), count(below)),
175 (true, false) => format!("{} above", count(above)),
176 (false, true) => format!("{} below", count(below)),
177 (false, false) => String::new(),
178 };
179 let mut note = if left.is_empty() {
180 format!("(The whole file: {}.", count(total))
181 } else {
182 format!("(Lines {first}-{last} of {total}.")
183 };
184 if let Some(cut) = cut {
185 note.push(' ');
186 note.push_str(cut);
187 }
188 if !left.is_empty() {
189 note.push_str(&format!(" Not shown: {left}."));
190 }
191 note.push(')');
192 note
193}
194
195fn count(lines: usize) -> String {
197 format!("{lines} line{}", if lines == 1 { "" } else { "s" })
198}
199
200fn join(lines: &str, note: &str) -> String {
202 let mut out = lines.to_owned();
203 if !out.is_empty() && !out.ends_with('\n') {
204 out.push('\n');
205 }
206 out.push_str(note);
207 out
208}