xml_edits.rs

  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}