1use anyhow::{Context as _, Result, anyhow};
2use language::{Anchor, BufferSnapshot, OffsetRangeExt as _, TextBufferSnapshot};
3use std::ops::Range;
4use std::path::Path;
5use std::sync::Arc;
6
7pub async fn parse_xml_edits<'a>(
8 mut input: &'a str,
9 get_buffer: impl Fn(&Path) -> Option<(&'a BufferSnapshot, &'a [Range<Anchor>])> + Send,
10) -> Result<(&'a BufferSnapshot, Vec<(Range<Anchor>, Arc<str>)>)> {
11 let edits_tag = parse_tag(&mut input, "edits")?.context("No edits tag")?;
12
13 input = edits_tag.body;
14
15 let file_path = edits_tag
16 .attributes
17 .trim_start()
18 .strip_prefix("path")
19 .context("no file attribute on edits tag")?
20 .trim_end()
21 .strip_prefix('=')
22 .context("no value for path attribute")?
23 .trim()
24 .trim_start_matches('"')
25 .trim_end_matches('"');
26
27 let (buffer, context_ranges) = get_buffer(file_path.as_ref())
28 .with_context(|| format!("no buffer for file {file_path}"))?;
29
30 let mut edits = vec![];
31 while let Some(old_text_tag) = parse_tag(&mut input, "old_text")? {
32 let new_text_tag =
33 parse_tag(&mut input, "new_text")?.context("no new_text tag following old_text")?;
34 edits.extend(resolve_new_text_old_text_in_buffer(
35 new_text_tag.body,
36 old_text_tag.body,
37 buffer,
38 context_ranges,
39 )?);
40 }
41
42 Ok((buffer, edits))
43}
44
45fn resolve_new_text_old_text_in_buffer(
46 new_text: &str,
47 old_text: &str,
48 buffer: &TextBufferSnapshot,
49 ranges: &[Range<Anchor>],
50) -> Result<impl Iterator<Item = (Range<Anchor>, Arc<str>)>, anyhow::Error> {
51 let context_offset = if old_text.is_empty() {
52 Ok(0)
53 } else {
54 let mut offset = None;
55 for range in ranges {
56 let range = range.to_offset(buffer);
57 let text = buffer.text_for_range(range.clone()).collect::<String>();
58 for (match_offset, _) in text.match_indices(old_text) {
59 if offset.is_some() {
60 anyhow::bail!("old_text is not unique enough:\n{}", old_text);
61 }
62 offset = Some(range.start + match_offset);
63 }
64 }
65 offset.ok_or_else(|| anyhow!("Failed to match old_text:\n{}", old_text))
66 }?;
67
68 let edits_within_hunk = language::text_diff(&old_text, &new_text);
69 Ok(edits_within_hunk
70 .into_iter()
71 .map(move |(inner_range, inner_text)| {
72 (
73 buffer.anchor_after(context_offset + inner_range.start)
74 ..buffer.anchor_before(context_offset + inner_range.end),
75 inner_text,
76 )
77 }))
78}
79
80struct ParsedTag<'a> {
81 attributes: &'a str,
82 body: &'a str,
83}
84
85fn parse_tag<'a>(input: &mut &'a str, tag: &str) -> Result<Option<ParsedTag<'a>>> {
86 let open_tag = format!("<{}", tag);
87 let close_tag = format!("</{}>", tag);
88 let Some(start_ix) = input.find(&open_tag) else {
89 return Ok(None);
90 };
91 let start_ix = start_ix + open_tag.len();
92 let closing_bracket_ix = start_ix
93 + input[start_ix..]
94 .find('>')
95 .with_context(|| format!("missing > after {tag}"))?;
96 let attributes = &input[start_ix..closing_bracket_ix].trim();
97 let end_ix = closing_bracket_ix
98 + input[closing_bracket_ix..]
99 .find(&close_tag)
100 .with_context(|| format!("no `{close_tag}` tag"))?;
101 let body = &input[closing_bracket_ix + '>'.len_utf8()..end_ix];
102 let body = body.strip_prefix('\n').unwrap_or(body);
103 *input = &input[end_ix + close_tag.len()..];
104 Ok(Some(ParsedTag { attributes, body }))
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use gpui::TestAppContext;
111 use indoc::indoc;
112 use language::Point;
113 use project::{FakeFs, Project};
114 use serde_json::json;
115 use settings::SettingsStore;
116 use util::path;
117
118 #[test]
119 fn test_parse_tags() {
120 let mut input = indoc! {r#"
121 Prelude
122 <tag attr="foo">
123 tag value
124 </tag>
125 "# };
126 let parsed = parse_tag(&mut input, "tag").unwrap().unwrap();
127 assert_eq!(parsed.attributes, "attr=\"foo\"");
128 assert_eq!(parsed.body, "tag value\n");
129 assert_eq!(input, "\n");
130 }
131
132 #[gpui::test]
133 async fn test_parse_xml_edits(cx: &mut TestAppContext) {
134 let fs = init_test(cx);
135
136 let buffer_1_text = indoc! {r#"
137 one two three four
138 five six seven eight
139 nine ten eleven twelve
140 "# };
141
142 fs.insert_tree(
143 path!("/root"),
144 json!({
145 "file1": buffer_1_text,
146 }),
147 )
148 .await;
149
150 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
151 let buffer = project
152 .update(cx, |project, cx| {
153 project.open_local_buffer(path!("/root/file1"), cx)
154 })
155 .await
156 .unwrap();
157 let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
158
159 let edits = indoc! {r#"
160 <edits path="root/file1">
161 <old_text>
162 five six seven eight
163 </old_text>
164 <new_text>
165 five SIX seven eight!
166 </new_text>
167 </edits>
168 "#};
169
170 let (buffer, edits) = parse_xml_edits(edits, |_path| {
171 Some((&buffer_snapshot, &[(Anchor::MIN..Anchor::MAX)] as &[_]))
172 })
173 .await
174 .unwrap();
175
176 let edits = edits
177 .into_iter()
178 .map(|(range, text)| (range.to_point(&buffer), text))
179 .collect::<Vec<_>>();
180 assert_eq!(
181 edits,
182 &[
183 (Point::new(1, 5)..Point::new(1, 8), "SIX".into()),
184 (Point::new(1, 20)..Point::new(1, 20), "!".into())
185 ]
186 );
187 }
188
189 fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
190 cx.update(|cx| {
191 let settings_store = SettingsStore::test(cx);
192 cx.set_global(settings_store);
193 });
194
195 FakeFs::new(cx.background_executor.clone())
196 }
197}