1use gpui::SharedString;
8
9#[derive(Debug, Clone, PartialEq, Eq, Default)]
11pub enum ProgressState {
12 #[default]
14 Pending,
15 Running,
17 Complete,
19 Failed(SharedString),
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct Progressive<T> {
29 content: T,
30 state: ProgressState,
31 revision: u64,
32}
33
34impl<T> Progressive<T> {
35 pub fn pending(content: T) -> Self {
37 Self::with_state(content, ProgressState::Pending)
38 }
39
40 pub fn running(content: T) -> Self {
42 Self::with_state(content, ProgressState::Running)
43 }
44
45 pub fn complete(content: T) -> Self {
47 Self::with_state(content, ProgressState::Complete)
48 }
49
50 pub fn failed(content: T, reason: impl Into<SharedString>) -> Self {
52 Self::with_state(content, ProgressState::Failed(reason.into()))
53 }
54
55 fn with_state(content: T, state: ProgressState) -> Self {
56 Self {
57 content,
58 state,
59 revision: 0,
60 }
61 }
62
63 pub fn content(&self) -> &T {
65 &self.content
66 }
67
68 pub fn state(&self) -> &ProgressState {
70 &self.state
71 }
72
73 pub fn revision(&self) -> u64 {
75 self.revision
76 }
77
78 pub fn start(&mut self) {
80 self.set_state(ProgressState::Running);
81 }
82
83 pub fn finish(&mut self) {
85 self.set_state(ProgressState::Complete);
86 }
87
88 pub fn fail(&mut self, reason: impl Into<SharedString>) {
90 self.set_state(ProgressState::Failed(reason.into()));
91 }
92
93 fn set_state(&mut self, state: ProgressState) {
94 if self.state != state {
95 self.state = state;
96 self.bump_revision();
97 }
98 }
99
100 fn bump_revision(&mut self) {
101 self.revision = self.revision.saturating_add(1);
102 }
103}
104
105pub type StreamedContent = Progressive<String>;
120
121impl Progressive<String> {
122 pub fn new() -> Self {
124 Self::running(String::new())
125 }
126
127 pub fn done(text: impl Into<String>) -> Self {
129 Self::complete(text.into())
130 }
131
132 pub fn text(&self) -> &str {
134 &self.content
135 }
136
137 pub fn append(&mut self, chunk: &str) {
139 if !chunk.is_empty() {
140 self.content.push_str(chunk);
141 self.bump_revision();
142 }
143 }
144
145 pub fn replace(&mut self, text: impl Into<String>) {
147 let text = text.into();
148 if self.content != text {
149 self.content = text;
150 self.bump_revision();
151 }
152 }
153
154 pub fn is_streaming(&self) -> bool {
156 self.state == ProgressState::Running
157 }
158}
159
160impl Default for Progressive<String> {
161 fn default() -> Self {
162 Self::new()
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::{ProgressState, Progressive, StreamedContent};
169
170 #[test]
171 fn constructors_preserve_content_and_initial_state() {
172 let pending = Progressive::pending(vec![1, 2]);
173 let running = Progressive::running(vec![3]);
174 let complete = Progressive::complete(vec![4]);
175 let failed = Progressive::failed(vec![5], "offline");
176
177 assert_eq!(pending.content(), &[1, 2]);
178 assert_eq!(pending.state(), &ProgressState::Pending);
179 assert_eq!(running.state(), &ProgressState::Running);
180 assert_eq!(complete.state(), &ProgressState::Complete);
181 assert_eq!(failed.state(), &ProgressState::Failed("offline".into()));
182 assert_eq!(pending.revision(), 0);
183 }
184
185 #[test]
186 fn lifecycle_revision_changes_only_for_real_transitions() {
187 let mut value = Progressive::pending(());
188 value.start();
189 assert_eq!(value.revision(), 1);
190 value.start();
191 assert_eq!(value.revision(), 1);
192 value.finish();
193 assert_eq!(value.revision(), 2);
194 value.finish();
195 assert_eq!(value.revision(), 2);
196 value.fail("late failure");
197 assert_eq!(value.revision(), 3);
198 value.fail("late failure");
199 assert_eq!(value.revision(), 3);
200 }
201
202 #[test]
203 fn text_mutations_track_meaningful_revisions() {
204 let mut text = StreamedContent::new();
205 assert_eq!(text.state(), &ProgressState::Running);
206 text.append("");
207 assert_eq!(text.revision(), 0);
208 text.append("hello");
209 assert_eq!(text.text(), "hello");
210 assert_eq!(text.revision(), 1);
211 text.replace("hello");
212 assert_eq!(text.revision(), 1);
213 text.replace("hello, world");
214 assert_eq!(text.text(), "hello, world");
215 assert_eq!(text.revision(), 2);
216 text.finish();
217 assert!(!text.is_streaming());
218 assert_eq!(text.revision(), 3);
219 }
220}