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
14pub const EDIT_PLAN_SCHEMA: &str = "weavatrix.edit-plan.v1";
16
17#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
19pub struct Position {
20 pub line: u32,
21 pub character: u32,
22}
23
24#[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#[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#[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#[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#[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#[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#[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}