Skip to repository content516 lines · 18.4 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T02:32:36.125Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
parser.rs
1use std::sync::Arc;
2
3use crate::{
4 CsvPreviewView,
5 types::TableLikeContent,
6 types::{LineNumber, TableCell},
7};
8use editor::Editor;
9use gpui::{AppContext, Context, Entity, Subscription, Task};
10use std::time::{Duration, Instant};
11use text::BufferSnapshot;
12use ui::{SharedString, table_row::TableRow};
13
14pub(crate) const REPARSE_DEBOUNCE: Duration = Duration::from_millis(200);
15
16pub(crate) struct EditorState {
17 pub editor: Entity<Editor>,
18 pub _subscription: Subscription,
19}
20
21impl CsvPreviewView {
22 pub(crate) fn parse_csv_from_active_editor(
23 &mut self,
24 wait_for_debounce: bool,
25 cx: &mut Context<Self>,
26 ) {
27 let editor = self.active_editor_state.editor.clone();
28 self.is_parsing = true;
29 self.parsing_task = Some(self.parse_csv_in_background(wait_for_debounce, editor, cx));
30 }
31
32 fn parse_csv_in_background(
33 &mut self,
34 wait_for_debounce: bool,
35 editor: Entity<Editor>,
36 cx: &mut Context<Self>,
37 ) -> Task<anyhow::Result<()>> {
38 cx.spawn(async move |view, cx| {
39 if wait_for_debounce {
40 // Smart debouncing: check if cooldown period has already passed
41 let now = Instant::now();
42 let should_wait = view.update(cx, |view, _| {
43 if let Some(last_end) = view.last_parse_end_time {
44 let cooldown_until = last_end + REPARSE_DEBOUNCE;
45 if now < cooldown_until {
46 Some(cooldown_until - now)
47 } else {
48 None // Cooldown already passed, parse immediately
49 }
50 } else {
51 None // First parse, no debounce
52 }
53 })?;
54
55 if let Some(wait_duration) = should_wait {
56 cx.background_executor().timer(wait_duration).await;
57 }
58 }
59
60 let buffer_snapshot = view.update(cx, |_, cx| {
61 editor
62 .read(cx)
63 .buffer()
64 .read(cx)
65 .as_singleton()
66 .map(|b| b.read(cx).text_snapshot())
67 })?;
68
69 let Some(buffer_snapshot) = buffer_snapshot else {
70 return Ok(());
71 };
72
73 let instant = Instant::now();
74 let parsed_csv = cx
75 .background_spawn(async move { from_buffer(&buffer_snapshot) })
76 .await;
77 let parse_duration = instant.elapsed();
78 let parse_end_time: Instant = Instant::now();
79 log::debug!("Parsed CSV in {}ms", parse_duration.as_millis());
80 view.update(cx, move |view, cx| {
81 view.performance_metrics
82 .timings
83 .insert("Parsing", (parse_duration, Instant::now()));
84
85 log::debug!("Parsed {} rows", parsed_csv.rows.len());
86 view.engine.contents = Arc::new(parsed_csv);
87 view.engine.calculate_available_filters();
88 view.sync_column_widths(cx);
89 view.last_parse_end_time = Some(parse_end_time);
90
91 view.is_parsing = false;
92 view.apply_filter_sort(cx);
93 cx.notify();
94 })
95 })
96 }
97}
98
99pub fn from_buffer(buffer_snapshot: &BufferSnapshot) -> TableLikeContent {
100 let text = buffer_snapshot.text();
101
102 if text.trim().is_empty() {
103 return TableLikeContent::default();
104 }
105
106 let (parsed_cells_with_positions, line_numbers) = parse_csv_with_positions(&text);
107 if parsed_cells_with_positions.is_empty() {
108 return TableLikeContent::default();
109 }
110 let raw_headers = parsed_cells_with_positions[0].clone();
111
112 // Calculating the longest row, as CSV might have less headers than max row width
113 let Some(max_number_of_cols) = parsed_cells_with_positions.iter().map(|r| r.len()).max() else {
114 return TableLikeContent::default();
115 };
116
117 // Convert to TableCell objects with buffer positions
118 let headers = create_table_row(&buffer_snapshot, max_number_of_cols, raw_headers);
119
120 let rows = parsed_cells_with_positions
121 .into_iter()
122 .skip(1)
123 .map(|row| create_table_row(&buffer_snapshot, max_number_of_cols, row))
124 .collect();
125
126 let row_line_numbers = line_numbers.into_iter().skip(1).collect();
127
128 TableLikeContent {
129 headers,
130 rows,
131 line_numbers: row_line_numbers,
132 number_of_cols: max_number_of_cols,
133 }
134}
135
136/// Parse CSV and track byte positions for each cell
137fn parse_csv_with_positions(
138 text: &str,
139) -> (
140 Vec<Vec<(SharedString, std::ops::Range<usize>)>>,
141 Vec<LineNumber>,
142) {
143 let mut rows = Vec::new();
144 let mut line_numbers = Vec::new();
145 let mut current_row: Vec<(SharedString, std::ops::Range<usize>)> = Vec::new();
146 let mut current_field = String::new();
147 let mut field_start_offset = 0;
148 let mut current_offset = 0;
149 let mut in_quotes = false;
150 let mut current_line = 1; // 1-based line numbering
151 let mut row_start_line = 1;
152 let mut chars = text.chars().peekable();
153
154 while let Some(ch) = chars.next() {
155 let char_byte_len = ch.len_utf8();
156
157 match ch {
158 '"' => {
159 if in_quotes {
160 if chars.peek() == Some(&'"') {
161 // Escaped quote
162 chars.next();
163 current_field.push('"');
164 current_offset += 1; // Skip the second quote
165 } else {
166 // End of quoted field
167 in_quotes = false;
168 }
169 } else {
170 // Start of quoted field
171 in_quotes = true;
172 if current_field.is_empty() {
173 // Include the opening quote in the range
174 field_start_offset = current_offset;
175 }
176 }
177 }
178 ',' if !in_quotes => {
179 // Field separator
180 let field_end_offset = current_offset;
181 if current_field.is_empty() && !in_quotes {
182 field_start_offset = current_offset;
183 }
184 current_row.push((
185 current_field.clone().into(),
186 field_start_offset..field_end_offset,
187 ));
188 current_field.clear();
189 field_start_offset = current_offset + char_byte_len;
190 }
191 '\n' => {
192 current_line += 1;
193 if !in_quotes {
194 // Row separator (only when not inside quotes)
195 let field_end_offset = current_offset;
196 if current_field.is_empty() && current_row.is_empty() {
197 field_start_offset = 0;
198 }
199 current_row.push((
200 current_field.clone().into(),
201 field_start_offset..field_end_offset,
202 ));
203 current_field.clear();
204
205 // Only add non-empty rows
206 if !current_row.is_empty()
207 && !current_row.iter().all(|(field, _)| field.trim().is_empty())
208 {
209 rows.push(current_row);
210 // Add line number info for this row
211 let line_info = if row_start_line == current_line - 1 {
212 LineNumber::Line(row_start_line)
213 } else {
214 LineNumber::LineRange(row_start_line, current_line - 1)
215 };
216 line_numbers.push(line_info);
217 }
218 current_row = Vec::new();
219 row_start_line = current_line;
220 field_start_offset = current_offset + char_byte_len;
221 } else {
222 // Newline inside quotes - preserve it
223 current_field.push(ch);
224 }
225 }
226 '\r' => {
227 if chars.peek() == Some(&'\n') {
228 // Handle Windows line endings (\r\n): account for \r byte, let \n be handled next
229 current_offset += char_byte_len;
230 continue;
231 } else {
232 // Standalone \r
233 current_line += 1;
234 if !in_quotes {
235 // Row separator (only when not inside quotes)
236 let field_end_offset = current_offset;
237 current_row.push((
238 current_field.clone().into(),
239 field_start_offset..field_end_offset,
240 ));
241 current_field.clear();
242
243 // Only add non-empty rows
244 if !current_row.is_empty()
245 && !current_row.iter().all(|(field, _)| field.trim().is_empty())
246 {
247 rows.push(current_row);
248 // Add line number info for this row
249 let line_info = if row_start_line == current_line - 1 {
250 LineNumber::Line(row_start_line)
251 } else {
252 LineNumber::LineRange(row_start_line, current_line - 1)
253 };
254 line_numbers.push(line_info);
255 }
256 current_row = Vec::new();
257 row_start_line = current_line;
258 field_start_offset = current_offset + char_byte_len;
259 } else {
260 // \r inside quotes - preserve it
261 current_field.push(ch);
262 }
263 }
264 }
265 _ => {
266 if current_field.is_empty() && !in_quotes {
267 field_start_offset = current_offset;
268 }
269 current_field.push(ch);
270 }
271 }
272
273 current_offset += char_byte_len;
274 }
275
276 // Add the last field and row if not empty
277 if !current_field.is_empty() || !current_row.is_empty() {
278 let field_end_offset = current_offset;
279 current_row.push((
280 current_field.clone().into(),
281 field_start_offset..field_end_offset,
282 ));
283 }
284 if !current_row.is_empty() && !current_row.iter().all(|(field, _)| field.trim().is_empty()) {
285 rows.push(current_row);
286 // Add line number info for the last row
287 let line_info = if row_start_line == current_line {
288 LineNumber::Line(row_start_line)
289 } else {
290 LineNumber::LineRange(row_start_line, current_line)
291 };
292 line_numbers.push(line_info);
293 }
294
295 (rows, line_numbers)
296}
297
298fn create_table_row(
299 buffer_snapshot: &BufferSnapshot,
300 max_number_of_cols: usize,
301 row: Vec<(SharedString, std::ops::Range<usize>)>,
302) -> TableRow<TableCell> {
303 let mut raw_row = row
304 .into_iter()
305 .map(|(content, range)| {
306 TableCell::from_buffer_position(content, range.start, range.end, &buffer_snapshot)
307 })
308 .collect::<Vec<_>>();
309
310 let append_elements = max_number_of_cols - raw_row.len();
311 if append_elements > 0 {
312 for _ in 0..append_elements {
313 raw_row.push(TableCell::Virtual);
314 }
315 }
316
317 TableRow::from_vec(raw_row, max_number_of_cols)
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323
324 #[test]
325 fn test_csv_parsing_basic() {
326 let csv_data = "Name,Age,City\nJohn,30,New York\nJane,25,Los Angeles";
327 let parsed = TableLikeContent::from_str(csv_data.to_string());
328
329 assert_eq!(parsed.headers.cols(), 3);
330 assert_eq!(parsed.headers[0].display_value().unwrap().as_ref(), "Name");
331 assert_eq!(parsed.headers[1].display_value().unwrap().as_ref(), "Age");
332 assert_eq!(parsed.headers[2].display_value().unwrap().as_ref(), "City");
333
334 assert_eq!(parsed.rows.len(), 2);
335 assert_eq!(parsed.rows[0][0].display_value().unwrap().as_ref(), "John");
336 assert_eq!(parsed.rows[0][1].display_value().unwrap().as_ref(), "30");
337 assert_eq!(
338 parsed.rows[0][2].display_value().unwrap().as_ref(),
339 "New York"
340 );
341 }
342
343 #[test]
344 fn test_csv_parsing_with_quotes() {
345 let csv_data = r#"Name,Description
346"John Doe","A person with ""special"" characters"
347Jane,"Simple name""#;
348 let parsed = TableLikeContent::from_str(csv_data.to_string());
349
350 assert_eq!(parsed.headers.cols(), 2);
351 assert_eq!(parsed.rows.len(), 2);
352 assert_eq!(
353 parsed.rows[0][1].display_value().unwrap().as_ref(),
354 r#"A person with "special" characters"#
355 );
356 }
357
358 #[test]
359 fn test_csv_parsing_with_newlines_in_quotes() {
360 let csv_data = "Name,Description,Status\n\"John\nDoe\",\"A person with\nmultiple lines\",Active\n\"Jane Smith\",\"Simple\",\"Also\nActive\"";
361 let parsed = TableLikeContent::from_str(csv_data.to_string());
362
363 assert_eq!(parsed.headers.cols(), 3);
364 assert_eq!(parsed.headers[0].display_value().unwrap().as_ref(), "Name");
365 assert_eq!(
366 parsed.headers[1].display_value().unwrap().as_ref(),
367 "Description"
368 );
369 assert_eq!(
370 parsed.headers[2].display_value().unwrap().as_ref(),
371 "Status"
372 );
373
374 assert_eq!(parsed.rows.len(), 2);
375 assert_eq!(
376 parsed.rows[0][0].display_value().unwrap().as_ref(),
377 "John\nDoe"
378 );
379 assert_eq!(
380 parsed.rows[0][1].display_value().unwrap().as_ref(),
381 "A person with\nmultiple lines"
382 );
383 assert_eq!(
384 parsed.rows[0][2].display_value().unwrap().as_ref(),
385 "Active"
386 );
387
388 assert_eq!(
389 parsed.rows[1][0].display_value().unwrap().as_ref(),
390 "Jane Smith"
391 );
392 assert_eq!(
393 parsed.rows[1][1].display_value().unwrap().as_ref(),
394 "Simple"
395 );
396 assert_eq!(
397 parsed.rows[1][2].display_value().unwrap().as_ref(),
398 "Also\nActive"
399 );
400
401 // Check line numbers
402 assert_eq!(parsed.line_numbers.len(), 2);
403 match &parsed.line_numbers[0] {
404 LineNumber::LineRange(start, end) => {
405 assert_eq!(start, &2);
406 assert_eq!(end, &4);
407 }
408 _ => panic!("Expected LineRange for multiline row"),
409 }
410 match &parsed.line_numbers[1] {
411 LineNumber::LineRange(start, end) => {
412 assert_eq!(start, &5);
413 assert_eq!(end, &6);
414 }
415 _ => panic!("Expected LineRange for second multiline row"),
416 }
417 }
418
419 #[test]
420 fn test_empty_csv() {
421 let parsed = TableLikeContent::from_str("".to_string());
422 assert_eq!(parsed.headers.cols(), 0);
423 assert!(parsed.rows.is_empty());
424 }
425
426 #[test]
427 fn test_csv_parsing_quote_offset_handling() {
428 let csv_data = r#"first,"se,cond",third"#;
429 let (parsed_cells, _) = parse_csv_with_positions(csv_data);
430
431 assert_eq!(parsed_cells.len(), 1); // One row
432 assert_eq!(parsed_cells[0].len(), 3); // Three cells
433
434 // first: 0..5 (no quotes)
435 let (content1, range1) = &parsed_cells[0][0];
436 assert_eq!(content1.as_ref(), "first");
437 assert_eq!(*range1, 0..5);
438
439 // "se,cond": 6..15 (includes quotes in range, content without quotes)
440 let (content2, range2) = &parsed_cells[0][1];
441 assert_eq!(content2.as_ref(), "se,cond");
442 assert_eq!(*range2, 6..15);
443
444 // third: 16..21 (no quotes)
445 let (content3, range3) = &parsed_cells[0][2];
446 assert_eq!(content3.as_ref(), "third");
447 assert_eq!(*range3, 16..21);
448 }
449
450 #[test]
451 fn test_csv_parsing_complex_quotes() {
452 let csv_data = r#"id,"name with spaces","description, with commas",status
4531,"John Doe","A person with ""quotes"" and, commas",active
4542,"Jane Smith","Simple description",inactive"#;
455 let (parsed_cells, _) = parse_csv_with_positions(csv_data);
456
457 assert_eq!(parsed_cells.len(), 3); // header + 2 rows
458
459 // Check header row
460 let header_row = &parsed_cells[0];
461 assert_eq!(header_row.len(), 4);
462
463 // id: 0..2
464 assert_eq!(header_row[0].0.as_ref(), "id");
465 assert_eq!(header_row[0].1, 0..2);
466
467 // "name with spaces": 3..21 (includes quotes)
468 assert_eq!(header_row[1].0.as_ref(), "name with spaces");
469 assert_eq!(header_row[1].1, 3..21);
470
471 // "description, with commas": 22..48 (includes quotes)
472 assert_eq!(header_row[2].0.as_ref(), "description, with commas");
473 assert_eq!(header_row[2].1, 22..48);
474
475 // status: 49..55
476 assert_eq!(header_row[3].0.as_ref(), "status");
477 assert_eq!(header_row[3].1, 49..55);
478
479 // Check first data row
480 let first_row = &parsed_cells[1];
481 assert_eq!(first_row.len(), 4);
482
483 // 1: 56..57
484 assert_eq!(first_row[0].0.as_ref(), "1");
485 assert_eq!(first_row[0].1, 56..57);
486
487 // "John Doe": 58..68 (includes quotes)
488 assert_eq!(first_row[1].0.as_ref(), "John Doe");
489 assert_eq!(first_row[1].1, 58..68);
490
491 // Content should be stripped of quotes but include escaped quotes
492 assert_eq!(
493 first_row[2].0.as_ref(),
494 r#"A person with "quotes" and, commas"#
495 );
496 // The range should include the outer quotes: 69..107
497 assert_eq!(first_row[2].1, 69..107);
498
499 // active: 108..114
500 assert_eq!(first_row[3].0.as_ref(), "active");
501 assert_eq!(first_row[3].1, 108..114);
502 }
503}
504
505impl TableLikeContent {
506 #[cfg(test)]
507 pub fn from_str(text: String) -> Self {
508 use text::{Buffer, BufferId, ReplicaId};
509
510 let buffer_id = BufferId::new(1).unwrap();
511 let buffer = Buffer::new(ReplicaId::LOCAL, buffer_id, text);
512 let snapshot = buffer.snapshot();
513 from_buffer(snapshot)
514 }
515}
516