Skip to main content

weavatrix_edit/
model.rs

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