Skip to main content

weavatrix_edit/
model.rs

1use std::collections::BTreeMap;
2
3use blazingly_json::Value;
4use serde::{Deserialize, Serialize};
5
6use crate::{
7    error::EditError,
8    limits::PlanLimits,
9    validation::{ValidatedEditPlan, validate_edit_plan},
10};
11
12pub use crate::provenance::Provenance;
13
14/// Frozen JSON contract consumed by Weavatrix Refactor.
15pub const EDIT_PLAN_SCHEMA: &str = "weavatrix.edit-plan.v1";
16
17/// A 1-based line and 0-based UTF-16 code-unit position.
18#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
19pub struct Position {
20    pub line: u32,
21    pub character: u32,
22}
23
24/// Character-unit convention used for line/character conversion.
25#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
26pub enum PositionEncoding {
27    Utf8,
28    #[default]
29    Utf16,
30    Utf32,
31}
32
33impl Position {
34    #[must_use]
35    pub const fn new(line: u32, character: u32) -> Self {
36        Self { line, character }
37    }
38}
39
40/// A half-open source range.
41#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
42pub struct TextRange {
43    pub start: Position,
44    pub end: Position,
45}
46
47impl TextRange {
48    #[must_use]
49    pub const fn new(start: Position, end: Position) -> Self {
50        Self { start, end }
51    }
52
53    #[must_use]
54    pub const fn empty(position: Position) -> Self {
55        Self::new(position, position)
56    }
57}
58
59/// Completeness claim made by a planner.
60#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
61#[serde(transparent)]
62pub struct Completeness(pub String);
63
64impl Completeness {
65    pub const COMPLETE: &'static str = "COMPLETE";
66    pub const PARTIAL: &'static str = "PARTIAL";
67
68    #[must_use]
69    pub fn new(value: impl Into<String>) -> Self {
70        Self(value.into())
71    }
72
73    #[must_use]
74    pub fn as_str(&self) -> &str {
75        &self.0
76    }
77}
78
79/// One exact replacement over the original source text.
80#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
81#[serde(rename_all = "camelCase")]
82pub struct TextEdit {
83    pub start_line: u32,
84    pub start_char: u32,
85    pub end_line: u32,
86    pub end_char: u32,
87    pub before: String,
88    pub after: String,
89    pub provenance: Provenance,
90    #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
91    pub extensions: BTreeMap<String, Value>,
92}
93
94/// A strict UTF-8 byte-range edit for high-throughput prepared application.
95#[derive(Clone, Debug, Eq, PartialEq)]
96pub struct ByteEdit {
97    pub start: usize,
98    pub end: usize,
99    pub before: String,
100    pub after: String,
101    pub provenance: Provenance,
102}
103
104impl ByteEdit {
105    #[must_use]
106    pub fn replace(
107        range: core::ops::Range<usize>,
108        before: impl Into<String>,
109        after: impl Into<String>,
110        provenance: impl AsRef<str>,
111    ) -> Self {
112        Self {
113            start: range.start,
114            end: range.end,
115            before: before.into(),
116            after: after.into(),
117            provenance: Provenance::new(provenance),
118        }
119    }
120
121    #[must_use]
122    pub fn insert(offset: usize, after: impl Into<String>, provenance: impl AsRef<str>) -> Self {
123        Self::replace(offset..offset, "", after, provenance)
124    }
125
126    #[must_use]
127    pub fn delete(
128        range: core::ops::Range<usize>,
129        before: impl Into<String>,
130        provenance: impl AsRef<str>,
131    ) -> Self {
132        Self::replace(range, before, "", provenance)
133    }
134}
135
136impl TextEdit {
137    #[must_use]
138    pub fn replace(
139        range: TextRange,
140        before: impl Into<String>,
141        after: impl Into<String>,
142        provenance: impl AsRef<str>,
143    ) -> Self {
144        Self {
145            start_line: range.start.line,
146            start_char: range.start.character,
147            end_line: range.end.line,
148            end_char: range.end.character,
149            before: before.into(),
150            after: after.into(),
151            provenance: Provenance::new(provenance),
152            extensions: BTreeMap::new(),
153        }
154    }
155
156    #[must_use]
157    pub fn insert(
158        position: Position,
159        after: impl Into<String>,
160        provenance: impl AsRef<str>,
161    ) -> Self {
162        Self::replace(TextRange::empty(position), "", after, provenance)
163    }
164
165    #[must_use]
166    pub fn delete(
167        range: TextRange,
168        before: impl Into<String>,
169        provenance: impl AsRef<str>,
170    ) -> Self {
171        Self::replace(range, before, "", provenance)
172    }
173
174    #[must_use]
175    pub const fn range(&self) -> TextRange {
176        TextRange::new(
177            Position::new(self.start_line, self.start_char),
178            Position::new(self.end_line, self.end_char),
179        )
180    }
181}
182
183/// All edits for one repository-relative UTF-8 source file.
184#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
185#[serde(rename_all = "camelCase")]
186pub struct FileEdit {
187    pub path: String,
188    pub sha256: String,
189    pub edits: Vec<TextEdit>,
190    #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
191    pub extensions: BTreeMap<String, Value>,
192}
193
194impl FileEdit {
195    #[must_use]
196    pub fn new(path: impl Into<String>, sha256: impl Into<String>, edits: Vec<TextEdit>) -> Self {
197        Self {
198            path: path.into(),
199            sha256: sha256.into(),
200            edits,
201            extensions: BTreeMap::new(),
202        }
203    }
204}
205
206/// Versioned, extensible multi-file edit-plan envelope.
207#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
208#[serde(rename_all = "camelCase")]
209pub struct EditPlan {
210    pub schema_version: String,
211    pub operation: String,
212    pub files: Vec<FileEdit>,
213    #[serde(default, skip_serializing_if = "Option::is_none")]
214    pub completeness: Option<Completeness>,
215    #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
216    pub extensions: BTreeMap<String, Value>,
217}
218
219impl EditPlan {
220    #[must_use]
221    pub fn new(operation: impl Into<String>, files: Vec<FileEdit>) -> Self {
222        Self {
223            schema_version: EDIT_PLAN_SCHEMA.to_owned(),
224            operation: operation.into(),
225            files,
226            completeness: None,
227            extensions: BTreeMap::new(),
228        }
229    }
230
231    pub fn validate(&self) -> Result<ValidatedEditPlan<'_>, EditError> {
232        validate_edit_plan(self, PlanLimits::default())
233    }
234
235    pub fn validate_with(&self, limits: PlanLimits) -> Result<ValidatedEditPlan<'_>, EditError> {
236        validate_edit_plan(self, limits)
237    }
238}