1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
//! A project: a directory, the sessions running in it, its boards and its
//! articles.
//!
//! The path is the whole identity — it is what every session in the project
//! is spawned with as its `cwd`, and what [`crate::model::state`] persists.
use crate::{
data::{Data, Page, Table},
model::{
article::{self, Article},
session::ChatSession,
watch::Watch,
workspace::Workspace,
},
};
use artifact::{
board::Board,
project::{Project as _, fs},
};
use bezel::gpui::Context;
use std::{
collections::HashMap,
path::{Path, PathBuf},
};
/// How many rows the table pane reads at once. The count beside them is the
/// table's own, so a window that does not reach the end says so.
const PAGE: i64 = 200;
pub struct Project {
pub path: PathBuf,
pub sessions: Vec<ChatSession>,
pub active: Option<u64>,
pub boards: Vec<Board>,
/// Which board the board pane shows.
pub board: Option<usize>,
pub articles: Vec<Article>,
/// Which article the article pane shows.
pub article: Option<usize>,
/// The project's database, once there is one. Opening a project must not
/// write a database into it, so this stays `None` until a table is made.
pub data: Option<Data>,
pub tables: Vec<Table>,
/// Which table the table pane shows.
pub table: Option<usize>,
/// The open table's window of rows, read when it is opened rather than
/// while it is drawn — a query per frame is a query too many.
pub page: Option<Page>,
/// Whether the sidebar shows what is under this project's heading.
pub expanded: bool,
/// Whether it shows what is under the archived divider. Folded away by
/// default: what was put away is not what you came back for.
pub archive_open: bool,
/// The watch on this project's `.cydonia/`, once it is up. Held here so
/// closing the project drops it, which is what takes the watch down.
pub watch: Option<Watch>,
}
impl Project {
pub fn new(path: PathBuf) -> Self {
let mut this = Self {
boards: fs::Project::new(&path).boards(),
articles: article::list(&path),
data: Data::attach(&path),
path,
sessions: Vec::new(),
active: None,
board: None,
article: None,
tables: Vec::new(),
table: None,
page: None,
expanded: true,
archive_open: false,
watch: None,
};
this.reload_tables();
this
}
/// Re-read everything on disk and reconcile it with what is held. The
/// answer to any event under `.cydonia/` — see [`crate::model::watch`] for
/// why the event itself is never read for more than where it landed.
///
/// Answers whether anything a view holds *about* an entry moved: a card's
/// place on a board, a column's place in a table, the editor a document was
/// being read in. Everything else is drawn off the model each frame and
/// nobody keeps a handle on it, so a re-read that only changed those has
/// nothing to announce — and announcing it would drop the edit somebody has
/// open over the echo of their own save.
pub fn reload(&mut self, cx: &mut Context<Workspace>) -> bool {
// The store can appear long after the project was opened, and an agent
// making one is exactly the case this watch is here for.
if self.data.is_none() {
self.data = Data::attach(&self.path);
}
let articles = self.reload_articles(cx);
let boards = self.reload_boards();
let before = self.shape();
self.reload_tables();
articles || boards || before != self.shape()
}
/// What the table pane addresses by position: which tables there are, and
/// what the open one's columns are called.
fn shape(&self) -> (Vec<String>, Vec<String>) {
let keys = self.tables.iter().map(|table| table.key.clone()).collect();
let columns = self
.page
.as_ref()
.map(|page| page.columns.iter().map(|col| col.name.clone()).collect())
.unwrap_or_default();
(keys, columns)
}
/// Re-list the articles and merge the re-read into what is open.
///
/// Held by path across the merge, not by index, and for the reason
/// [`Self::reload_tables`] holds by key: the list is ordered by when each
/// was last written, so one article changing can move every other one.
///
/// Carries out [`Article::adopt`]'s answer — an open document rebuilt over
/// what the file now says has taken the caret with it.
fn reload_articles(&mut self, cx: &mut Context<Workspace>) -> bool {
let open = self.open_article().map(Path::to_path_buf);
let mut held: HashMap<PathBuf, Article> = self
.articles
.drain(..)
.map(|article| (article.path.clone(), article))
.collect();
let mut rebuilt = false;
self.articles = article::list(&self.path)
.into_iter()
.map(|fresh| match held.remove(&fresh.path) {
// Already on screen — the editor over it, its scroll and its
// undo history stay where they are.
Some(mut article) => {
rebuilt |= article.adopt(&fresh, cx);
article
}
None => fresh,
})
.collect();
self.article = open.and_then(|path| self.articles.iter().position(|at| at.path == path));
rebuilt
}
/// The file the article pane is showing, if it is showing one.
fn open_article(&self) -> Option<&Path> {
let article = self.articles.get(self.article?)?;
Some(&article.path)
}
/// The same, for boards. Nothing here is a live surface, so the merge is
/// only about not disturbing a board the re-read did not change — see
/// [`Board::adopt`], whose answer is carried out of here.
fn reload_boards(&mut self) -> bool {
let open = self
.board
.and_then(|ix| self.boards.get(ix))
.map(|board| board.id.clone());
let mut held: HashMap<String, Board> = self
.boards
.drain(..)
.map(|board| (board.id.clone(), board))
.collect();
let mut moved = false;
self.boards = self
.store()
.boards()
.into_iter()
.map(|fresh| match held.remove(&fresh.id) {
Some(mut board) => {
moved |= board.adopt(fresh);
board
}
None => fresh,
})
.collect();
self.board = open.and_then(|id| self.boards.iter().position(|at| at.id == id));
moved
}
/// Re-read what tables exist. The store is the list — nothing here keeps a
/// second copy of it that a failed write could leave standing.
pub fn reload_tables(&mut self) {
// Held by key across the re-read, not by index: a table made or dropped
// beside the open one shifts every index past it, and the pane would be
// left showing whichever table slid into its place.
let open = self
.table
.and_then(|ix| self.tables.get(ix))
.map(|table| table.key.clone());
self.tables = self
.data
.as_ref()
.and_then(|data| data.list().ok())
.unwrap_or_default();
self.table = match open.and_then(|key| self.position(&key)) {
found @ Some(_) => found,
None => self.table.filter(|ix| *ix < self.tables.len()),
};
self.reload_page();
}
fn position(&self, key: &str) -> Option<usize> {
self.tables.iter().position(|table| table.key == key)
}
/// Read the open table's rows.
pub fn reload_page(&mut self) {
let key = self
.table
.and_then(|ix| self.tables.get(ix))
.map(|table| table.key.clone());
self.page = match (key, self.data.as_ref()) {
(Some(key), Some(data)) => data.read(&key, None, false, PAGE, 0).ok(),
_ => None,
};
}
/// Where this project's work is kept. The filesystem, for this app —
/// [`artifact::backend`] is what a different one would be, and nothing above
/// here names a file.
pub fn store(&self) -> fs::Project {
fs::Project::new(&self.path)
}
/// The tab's label: the directory's own name, or the whole path when it
/// has none (`/`).
pub fn name(&self) -> String {
self.path
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| self.path.display().to_string())
}
pub fn session(&self, id: u64) -> Option<&ChatSession> {
self.sessions.iter().find(|chat| chat.id == id)
}
pub fn session_mut(&mut self, id: u64) -> Option<&mut ChatSession> {
self.sessions.iter_mut().find(|chat| chat.id == id)
}
pub fn active_session(&self) -> Option<&ChatSession> {
self.active.and_then(|id| self.session(id))
}
}