238 lines
6.7 KiB
Rust
238 lines
6.7 KiB
Rust
use std::sync::OnceLock;
|
|
|
|
use regex::Regex;
|
|
use smallvec::SmallVec;
|
|
use util::debug_panic;
|
|
|
|
static START_MARKER: OnceLock<Regex> = OnceLock::new();
|
|
static END_MARKER: OnceLock<Regex> = OnceLock::new();
|
|
|
|
#[derive(Debug)]
|
|
pub enum CreateFileParserEvent {
|
|
NewTextChunk { chunk: String },
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct CreateFileParser {
|
|
state: ParserState,
|
|
buffer: String,
|
|
}
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
enum ParserState {
|
|
Pending,
|
|
WithinText,
|
|
Finishing,
|
|
Finished,
|
|
}
|
|
|
|
impl CreateFileParser {
|
|
pub fn new() -> Self {
|
|
CreateFileParser {
|
|
state: ParserState::Pending,
|
|
buffer: String::new(),
|
|
}
|
|
}
|
|
|
|
pub fn push(&mut self, chunk: Option<&str>) -> SmallVec<[CreateFileParserEvent; 1]> {
|
|
if chunk.is_none() {
|
|
self.state = ParserState::Finishing;
|
|
}
|
|
|
|
let chunk = chunk.unwrap_or_default();
|
|
|
|
self.buffer.push_str(chunk);
|
|
|
|
let mut edit_events = SmallVec::new();
|
|
let start_marker_regex = START_MARKER.get_or_init(|| Regex::new(r"\n?```\S*\n").unwrap());
|
|
let end_marker_regex = END_MARKER.get_or_init(|| Regex::new(r"(^|\n)```\s*$").unwrap());
|
|
loop {
|
|
match &mut self.state {
|
|
ParserState::Pending => {
|
|
if let Some(m) = start_marker_regex.find(&self.buffer) {
|
|
self.buffer.drain(..m.end());
|
|
self.state = ParserState::WithinText;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
ParserState::WithinText => {
|
|
let text = self.buffer.trim_end_matches(&['`', '\n', ' ']);
|
|
let text_len = text.len();
|
|
|
|
if text_len > 0 {
|
|
edit_events.push(CreateFileParserEvent::NewTextChunk {
|
|
chunk: self.buffer.drain(..text_len).collect(),
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
ParserState::Finishing => {
|
|
if let Some(m) = end_marker_regex.find(&self.buffer) {
|
|
self.buffer.drain(m.start()..);
|
|
}
|
|
if !self.buffer.is_empty() {
|
|
if !self.buffer.ends_with('\n') {
|
|
self.buffer.push('\n');
|
|
}
|
|
edit_events.push(CreateFileParserEvent::NewTextChunk {
|
|
chunk: self.buffer.drain(..).collect(),
|
|
});
|
|
}
|
|
self.state = ParserState::Finished;
|
|
break;
|
|
}
|
|
ParserState::Finished => debug_panic!("Can't call parser after finishing"),
|
|
}
|
|
}
|
|
edit_events
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use indoc::indoc;
|
|
use rand::prelude::*;
|
|
use std::cmp;
|
|
|
|
#[gpui::test(iterations = 100)]
|
|
fn test_happy_path(mut rng: StdRng) {
|
|
let mut parser = CreateFileParser::new();
|
|
assert_eq!(
|
|
parse_random_chunks("```\nHello world\n```", &mut parser, &mut rng),
|
|
"Hello world".to_string()
|
|
);
|
|
}
|
|
|
|
#[gpui::test(iterations = 100)]
|
|
fn test_cut_prefix(mut rng: StdRng) {
|
|
let mut parser = CreateFileParser::new();
|
|
assert_eq!(
|
|
parse_random_chunks(
|
|
indoc! {"
|
|
Let me write this file for you:
|
|
|
|
```
|
|
Hello world
|
|
```
|
|
|
|
"},
|
|
&mut parser,
|
|
&mut rng
|
|
),
|
|
"Hello world".to_string()
|
|
);
|
|
}
|
|
|
|
#[gpui::test(iterations = 100)]
|
|
fn test_language_name_on_fences(mut rng: StdRng) {
|
|
let mut parser = CreateFileParser::new();
|
|
assert_eq!(
|
|
parse_random_chunks(
|
|
indoc! {"
|
|
```rust
|
|
Hello world
|
|
```
|
|
|
|
"},
|
|
&mut parser,
|
|
&mut rng
|
|
),
|
|
"Hello world".to_string()
|
|
);
|
|
}
|
|
|
|
#[gpui::test(iterations = 100)]
|
|
fn test_leave_suffix(mut rng: StdRng) {
|
|
let mut parser = CreateFileParser::new();
|
|
assert_eq!(
|
|
parse_random_chunks(
|
|
indoc! {"
|
|
Let me write this file for you:
|
|
|
|
```
|
|
Hello world
|
|
```
|
|
|
|
The end
|
|
"},
|
|
&mut parser,
|
|
&mut rng
|
|
),
|
|
// This output is malformed, so we're doing our best effort
|
|
"Hello world\n```\n\nThe end\n".to_string()
|
|
);
|
|
}
|
|
|
|
#[gpui::test(iterations = 100)]
|
|
fn test_inner_fences(mut rng: StdRng) {
|
|
let mut parser = CreateFileParser::new();
|
|
assert_eq!(
|
|
parse_random_chunks(
|
|
indoc! {"
|
|
Let me write this file for you:
|
|
|
|
```
|
|
```
|
|
Hello world
|
|
```
|
|
```
|
|
"},
|
|
&mut parser,
|
|
&mut rng
|
|
),
|
|
// This output is malformed, so we're doing our best effort
|
|
"```\nHello world\n```\n".to_string()
|
|
);
|
|
}
|
|
|
|
#[gpui::test(iterations = 10)]
|
|
fn test_empty_file(mut rng: StdRng) {
|
|
let mut parser = CreateFileParser::new();
|
|
assert_eq!(
|
|
parse_random_chunks(
|
|
indoc! {"
|
|
```
|
|
```
|
|
"},
|
|
&mut parser,
|
|
&mut rng
|
|
),
|
|
"".to_string()
|
|
);
|
|
}
|
|
|
|
fn parse_random_chunks(input: &str, parser: &mut CreateFileParser, rng: &mut StdRng) -> String {
|
|
let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50));
|
|
let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count);
|
|
chunk_indices.sort();
|
|
chunk_indices.push(input.len());
|
|
|
|
let chunk_indices = chunk_indices
|
|
.into_iter()
|
|
.map(Some)
|
|
.chain(vec![None])
|
|
.collect::<Vec<Option<usize>>>();
|
|
|
|
let mut edit = String::default();
|
|
let mut last_ix = 0;
|
|
for chunk_ix in chunk_indices {
|
|
let mut chunk = None;
|
|
if let Some(chunk_ix) = chunk_ix {
|
|
chunk = Some(&input[last_ix..chunk_ix]);
|
|
last_ix = chunk_ix;
|
|
}
|
|
|
|
for event in parser.push(chunk) {
|
|
match event {
|
|
CreateFileParserEvent::NewTextChunk { chunk } => {
|
|
edit.push_str(&chunk);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
edit
|
|
}
|
|
}
|