Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/rustgis-web/src/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ impl eframe::App for WebShell {
embed.exchange(&mut self.app, ctx);
}
self.app.logic(ctx);
self.dirty.set(self.app.session.dirty);
self.dirty.set(self.app.session.dirty());
// The shortcuts can be changed in the app: the key listener follows, rebuilt only when
// the keymap changed.
let print = self.app.shortcuts_fingerprint();
Expand Down
2 changes: 1 addition & 1 deletion apps/rustgis/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ fn main() -> eframe::Result {
for e in open_files(&mut app, &files) {
eprintln!("rustgis: {e}");
}
app.session.dirty = false;
app.session.mark_saved();
let mut viewport = egui::ViewportBuilder::default()
.with_title("RustGIS")
.with_app_id("org.rustgis.RustGIS")
Expand Down
2 changes: 1 addition & 1 deletion crates/engine/src/cmd/design.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ pub fn commands() -> Vec<C> {
.iter()
.filter_map(|(id, c)| s.map().layer(*id).map(|l| json!({"layer": l.name, "added": c.added, "modified": c.modified, "deleted": c.deleted})))
.collect();
Ok(json!({"unsaved": s.dirty, "layers": rows}))
Ok(json!({"unsaved": s.dirty(), "layers": rows}))
})
.view(),
C::new("edit.template", "Manage Templates", "Edit › Manage Edits", template).when(need_feature_layer).params(
Expand Down
2 changes: 1 addition & 1 deletion crates/engine/src/cmd/history.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ fn script(s: &mut Session, _: &Value) -> CmdResult {
// there until the project is saved again.
if s.path.is_none() {
text.push_str("# The project hasn't been saved yet: save it as this file first.\n");
} else if s.dirty {
} else if s.dirty() {
text.push_str("# The project has changes that aren't saved: save it first, so its layers are there.\n");
}
text.push_str(&format!("import rustgis\n\ns = rustgis.Session({})\n", quoted(&project_file(s))));
Expand Down
6 changes: 4 additions & 2 deletions crates/engine/src/cmd/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,12 @@ fn open_text(s: &mut Session, p: &Value) -> CmdResult {
Ok(Value::Null)
}

/// Saves the project to `path` (`.rgis` added when it has no extension) and makes it the
/// project's file. The project as it is now is the saved state undo can return to.
fn write(s: &mut Session, path: PathBuf) -> CmdResult {
let path = if path.extension().is_none() { path.with_extension(crate::project::EXTENSION) } else { path };
let stripped = s.project.save(&path)?;
s.dirty = false;
s.mark_saved();
s.edit_counts.clear();
s.notify(format!("Saved {}", path.display()), false);
if stripped > 0 {
Expand Down Expand Up @@ -295,7 +297,7 @@ fn info(s: &mut Session, p: &Value) -> CmdResult {
Ok(json!({
"name": s.project.name,
"path": s.path.as_ref().map(|p| p.display().to_string()),
"unsaved": s.dirty,
"unsaved": s.dirty(),
"maps": s.project.maps.iter().map(|m| m.name.clone()).collect::<Vec<_>>(),
"activeMap": m.name,
"basemap": m.basemap.code(),
Expand Down
76 changes: 62 additions & 14 deletions crates/engine/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -416,8 +416,9 @@ pub struct Notice {

const UNDO_LIMIT: usize = 40;

/// An undo or redo step: the project before it, the layout open then, its label and who made it.
type Step = (Project, Option<usize>, &'static str, Option<String>);
/// An undo or redo step: the project before it, the layout open then, its label, who made it,
/// and which state of the project it is ([`Session::dirty`]).
type Step = (Project, Option<usize>, &'static str, Option<String>, u64);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nits, confidence high. Grouped here because they are small:

  • The undo step is now a 5-tuple, and the tests destructure it as (snap, _, _, _, _). A small struct with named fields (state, label, ...) would read better and survive the next addition.
  • state (current state) and states (count of states issued, line 483) are easy to confuse. Something like last_state or issued would be clearer.
  • saveprompt.rs repeats app.session.dirty() && app.opens_files() in asks, close_requested and unanswered. One helper would keep them in step.
  • The kept_line split in autosave.rs means existing translations of the full "didn't close properly" sentence are now orphaned. Acceptable under AGENTS.md, but worth noting for the translator.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Taken in part, in 863e984:

  • Repeated predicate: dirty() && opens_files() is now one helper, unsaved_here, used by asks, close_requested and unanswered.
  • Step tuple: it was already a 4-tuple; this adds the state. A named struct would be a refactor of the undo stack better done on its own.
  • state and states: they sit together, and each has a doc comment saying which is which.
  • Orphaned translations: none exist. No catalog in crates/ui-egui/src/i18n/*.tsv had the full "didn't close properly last time. A copy of…" sentences, so nothing is lost.


/// What a project holds in the session beside itself: what [`Session::clear_history`] forgets
/// when another project starts, and what opening one sets (its file, layer, tool…). When another
Expand All @@ -436,7 +437,8 @@ struct PutAside {
/// The projection those cameras are in: without it, the extent history given back would be
/// taken for another projection's and cleared.
history_projection: String,
dirty: bool,
state: u64,
saved: Option<u64>,
trusted_tasks: std::collections::HashSet<String>,
trusted_models: std::collections::HashSet<String>,
trusted_notebooks: std::collections::HashSet<String>,
Expand Down Expand Up @@ -475,7 +477,13 @@ struct PutAside {
pub struct Session {
pub project: Project,
pub path: Option<PathBuf>,
pub dirty: bool,
/// Which state of the project is open: a new one after each change, an earlier one again
/// after undo. Each is given once in a session (`states` counts them).
state: u64,
states: u64,
/// The state last saved, or opened from its file; `None` when no state of the open project is
/// (recovered work). The project has unsaved changes while another is open ([`Session::dirty`]).
saved: Option<u64>,
/// Favourite, recent and saved data sources (kept with the app's preferences, not the
/// project).
pub sources: sources::Sources,
Expand Down Expand Up @@ -703,7 +711,9 @@ impl Session {
Session {
project,
path: None,
dirty: false,
state: 0,
states: 0,
saved: Some(0),
sources: sources::Sources::default(),
connections: connections::Connections::default(),
s3_bindings: std::collections::BTreeMap::new(),
Expand Down Expand Up @@ -952,6 +962,7 @@ impl Session {
/// and restored if `f` fails or panics.
pub(crate) fn guarded(&mut self, label: &'static str, mutates: bool, f: impl FnOnce(&mut Session) -> CmdResult) -> CmdResult {
let snapshot = mutates.then(|| self.project.clone());
let state = self.state;
// The merge key asked for applies to this command only, whatever it does; and to a
// command run by itself, not to those it runs.
let merge = if self.depth == 0 { self.merge.take() } else { None };
Expand Down Expand Up @@ -1012,7 +1023,7 @@ impl Session {
// back to before the first of them. Only a command run by itself merges.
let key = merge;
if key.is_none() || key != self.merged || self.undo.is_empty() {
self.undo.push((snap, open_layout, label, self.actor.clone()));
self.undo.push((snap, open_layout, label, self.actor.clone(), state));
if self.undo.len() > UNDO_LIMIT {
self.undo.remove(0);
}
Expand All @@ -1021,7 +1032,7 @@ impl Session {
self.merged = key;
}
self.redo.clear();
self.dirty = true;
self.state = self.new_state();
}
(Err(_), Some(snap)) => {
// The project as it was before the command, so the layout open then, whatever
Expand All @@ -1034,6 +1045,7 @@ impl Session {
self.give_back(opened);
}
self.opened = opened;
self.state = state;
if let Some(c) = counts {
self.edit_counts = c;
}
Expand Down Expand Up @@ -1090,7 +1102,8 @@ impl Session {
forward: take(&mut self.forward),
last_camera: self.last_camera.take(),
history_projection: self.history_projection.clone(),
dirty: std::mem::replace(&mut self.dirty, false),
state: self.state,
saved: self.saved,
trusted_tasks: take(&mut self.trusted_tasks),
trusted_models: take(&mut self.trusted_models),
trusted_notebooks: take(&mut self.trusted_notebooks),
Expand Down Expand Up @@ -1135,7 +1148,8 @@ impl Session {
forward,
last_camera,
history_projection,
dirty,
state,
saved,
trusted_tasks,
trusted_models,
trusted_notebooks,
Expand Down Expand Up @@ -1167,7 +1181,8 @@ impl Session {
} = *a;
(self.undo, self.redo, self.back, self.forward, self.last_camera) = (undo, redo, back, forward, last_camera);
self.history_projection = history_projection;
(self.dirty, self.project_starts, self.history_from_file) = (dirty, project_starts, history_from_file);
(self.state, self.saved) = (state, saved);
(self.project_starts, self.history_from_file) = (project_starts, history_from_file);
(self.trusted_tasks, self.trusted_models, self.trusted_notebooks) = (trusted_tasks, trusted_models, trusted_notebooks);
(self.active_layer, self.table_layer, self.table_standalone, self.tool) = (active_layer, table_layer, table_standalone, tool);
(self.layer_selection, self.layer_selection_of) = (layer_selection, layer_selection_of);
Expand Down Expand Up @@ -1241,30 +1256,63 @@ impl Session {
pub fn clear_history(&mut self) {
drop(self.take_project_state());
self.project_starts = self.project_starts.wrapping_add(1);
// A state of its own: undo back past a replacement made within a command (a task's step)
// reaches a state of the project before, which isn't this one's saved state.
self.state = self.new_state();
self.saved = Some(self.state);
}

/// Whether the project has changes that aren't saved: it is in another state than the one
/// last saved or opened. Undo or redo back to that state leaves none.
pub fn dirty(&self) -> bool {
self.saved != Some(self.state)
}

/// Which state the project is in: another after each change, an earlier one after undo. A
/// front end compares it to tell whether the project changed since it last looked.
pub fn state(&self) -> u64 {
self.state
}

/// The project as it is now is saved (written to its file, or downloaded in the browser).
pub fn mark_saved(&mut self) {
self.saved = Some(self.state);
}

/// The project has changes that aren't saved, whatever undo does (work recovered after a
/// crash: no state of it is in a file).
pub fn mark_unsaved(&mut self) {
self.saved = None;
}

/// A state of the project not given before in this session, for a change just made.
fn new_state(&mut self) -> u64 {
self.states = self.states.wrapping_add(1);
self.states
}

pub(crate) fn undo(&mut self) -> bool {
self.merged = None;
let Some((p, saved, label, actor)) = self.undo.pop() else { return false };
let Some((p, saved, label, actor, state)) = self.undo.pop() else { return false };
let (open, was) = (self.open_layout_name(), self.active_layout);
let cur = std::mem::replace(&mut self.project, p);
self.project.keep_table_ids(&cur);
self.reopen_layout(open, saved);
self.keep_time_position(&cur);
self.redo.push((cur, was, label, actor));
self.redo.push((cur, was, label, actor, std::mem::replace(&mut self.state, state)));
self.touch_all_geometry();
true
}

pub(crate) fn redo(&mut self) -> bool {
self.merged = None;
let Some((p, saved, label, actor)) = self.redo.pop() else { return false };
let Some((p, saved, label, actor, state)) = self.redo.pop() else { return false };
let (open, was) = (self.open_layout_name(), self.active_layout);
let cur = std::mem::replace(&mut self.project, p);
self.project.keep_table_ids(&cur);
self.reopen_layout(open, saved);
self.keep_time_position(&cur);
self.undo.push((cur, was, label, actor));
self.undo.push((cur, was, label, actor, std::mem::replace(&mut self.state, state)));
self.touch_all_geometry();
true
}
Expand Down
2 changes: 1 addition & 1 deletion crates/engine/src/sample.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ fn countries_in(w: Worldview) -> &'static str {
/// that is unsaved, unchanged and has nothing to undo, and only a layer named `Countries` holding
/// exactly the built-in countries of `from`. Returns whether a layer was swapped.
pub fn swap_countries(s: &mut crate::Session, from: Worldview, to: Worldview) -> bool {
if from == to || s.dirty || s.path.is_some() || s.can_undo() {
if from == to || s.dirty() || s.path.is_some() || s.can_undo() {
return false;
}
let read = |w| read_geojson(countries_in(w), "Countries").ok().map(|(ds, _)| ds);
Expand Down
Loading
Loading