Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:28:47.917Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

parser.rs

516 lines · 18.4 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.