checkpoint before gfm ast parser design
This commit is contained in:
@@ -22,6 +22,7 @@ pub struct FileTreeRuntimeRow {
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FileTreeRuntimeState {
|
||||
pub active_row_id: Option<String>,
|
||||
pub selection: FileTreeSelectionState,
|
||||
pub drag_row_ids: Vec<String>,
|
||||
pub drag_effect: Option<TreeShellDragEffect>,
|
||||
@@ -70,6 +71,18 @@ pub enum FileTreeRuntimeAction {
|
||||
OpenRow {
|
||||
row_id: String,
|
||||
},
|
||||
FocusNext,
|
||||
FocusPrevious,
|
||||
FocusFirst,
|
||||
FocusLast,
|
||||
OpenFocused,
|
||||
ContextMenuFocused,
|
||||
BeginRenameFocused,
|
||||
DeleteSelection,
|
||||
CopySelection,
|
||||
CutSelection,
|
||||
PasteIntoFocused,
|
||||
Escape,
|
||||
ContextMenuRow {
|
||||
row_id: String,
|
||||
},
|
||||
@@ -90,6 +103,11 @@ pub enum FileTreeIntentEvent {
|
||||
row_id: String,
|
||||
target: FileTreeOpenTarget,
|
||||
},
|
||||
KeyboardCommand {
|
||||
command: FileTreeKeyboardCommand,
|
||||
row_ids: Vec<String>,
|
||||
target_row_id: Option<String>,
|
||||
},
|
||||
InternalDrop {
|
||||
target_row_id: Option<String>,
|
||||
target: Option<FileTreeOpenTarget>,
|
||||
@@ -103,6 +121,16 @@ pub enum FileTreeIntentEvent {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum FileTreeKeyboardCommand {
|
||||
Rename,
|
||||
Delete,
|
||||
Copy,
|
||||
Cut,
|
||||
Paste,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
#[serde(
|
||||
tag = "kind",
|
||||
@@ -241,16 +269,86 @@ impl FileTreeRuntimeState {
|
||||
}
|
||||
FileTreeRuntimeAction::OpenRow { row_id } => {
|
||||
if let Some(target) = resolve_open_target(env, &row_id) {
|
||||
let mut state = self.clone();
|
||||
state.active_row_id = Some(row_id);
|
||||
transition(
|
||||
self.clone(),
|
||||
[FileTreeRuntimeOutput::Intent(FileTreeIntentEvent::Open {
|
||||
target,
|
||||
})],
|
||||
state,
|
||||
[
|
||||
FileTreeRuntimeOutput::DomPatch,
|
||||
FileTreeRuntimeOutput::Intent(FileTreeIntentEvent::Open { target }),
|
||||
],
|
||||
)
|
||||
} else {
|
||||
transition(self.clone(), [])
|
||||
}
|
||||
}
|
||||
FileTreeRuntimeAction::FocusNext => transition(
|
||||
focus_relative_row(self, env, 1),
|
||||
[FileTreeRuntimeOutput::DomPatch],
|
||||
),
|
||||
FileTreeRuntimeAction::FocusPrevious => transition(
|
||||
focus_relative_row(self, env, -1),
|
||||
[FileTreeRuntimeOutput::DomPatch],
|
||||
),
|
||||
FileTreeRuntimeAction::FocusFirst => transition(
|
||||
focus_absolute_row(self, env, 0),
|
||||
[FileTreeRuntimeOutput::DomPatch],
|
||||
),
|
||||
FileTreeRuntimeAction::FocusLast => transition(
|
||||
focus_absolute_row(self, env, env.visible_row_ids.len().saturating_sub(1)),
|
||||
[FileTreeRuntimeOutput::DomPatch],
|
||||
),
|
||||
FileTreeRuntimeAction::OpenFocused => {
|
||||
if let Some(row_id) = self.selection.focused_row_id.clone() {
|
||||
self.reduce(env, FileTreeRuntimeAction::OpenRow { row_id })
|
||||
} else {
|
||||
transition(self.clone(), [])
|
||||
}
|
||||
}
|
||||
FileTreeRuntimeAction::ContextMenuFocused => {
|
||||
if let Some(row_id) = self.selection.focused_row_id.clone() {
|
||||
self.reduce(env, FileTreeRuntimeAction::ContextMenuRow { row_id })
|
||||
} else {
|
||||
transition(self.clone(), [])
|
||||
}
|
||||
}
|
||||
FileTreeRuntimeAction::BeginRenameFocused => keyboard_command_transition(
|
||||
self,
|
||||
FileTreeKeyboardCommand::Rename,
|
||||
selected_or_focused_rows(self),
|
||||
self.selection.focused_row_id.clone(),
|
||||
),
|
||||
FileTreeRuntimeAction::DeleteSelection => keyboard_command_transition(
|
||||
self,
|
||||
FileTreeKeyboardCommand::Delete,
|
||||
selected_or_focused_rows(self),
|
||||
self.selection.focused_row_id.clone(),
|
||||
),
|
||||
FileTreeRuntimeAction::CopySelection => keyboard_command_transition(
|
||||
self,
|
||||
FileTreeKeyboardCommand::Copy,
|
||||
selected_or_focused_rows(self),
|
||||
self.selection.focused_row_id.clone(),
|
||||
),
|
||||
FileTreeRuntimeAction::CutSelection => keyboard_command_transition(
|
||||
self,
|
||||
FileTreeKeyboardCommand::Cut,
|
||||
selected_or_focused_rows(self),
|
||||
self.selection.focused_row_id.clone(),
|
||||
),
|
||||
FileTreeRuntimeAction::PasteIntoFocused => keyboard_command_transition(
|
||||
self,
|
||||
FileTreeKeyboardCommand::Paste,
|
||||
Vec::new(),
|
||||
self.selection.focused_row_id.clone(),
|
||||
),
|
||||
FileTreeRuntimeAction::Escape => {
|
||||
let mut state = self.clone();
|
||||
state.drag_row_ids = Vec::new();
|
||||
state.drag_effect = None;
|
||||
state.drop_target_row_id = None;
|
||||
transition(state, [FileTreeRuntimeOutput::DomPatch])
|
||||
}
|
||||
FileTreeRuntimeAction::ContextMenuRow { row_id } => {
|
||||
if let Some(target) = resolve_open_target(env, &row_id) {
|
||||
transition(
|
||||
@@ -267,6 +365,75 @@ impl FileTreeRuntimeState {
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_relative_row(
|
||||
state: &FileTreeRuntimeState,
|
||||
env: &FileTreeRuntimeEnvironment,
|
||||
offset: isize,
|
||||
) -> FileTreeRuntimeState {
|
||||
if env.visible_row_ids.is_empty() {
|
||||
return state.clone();
|
||||
}
|
||||
let current = state
|
||||
.selection
|
||||
.focused_row_id
|
||||
.as_ref()
|
||||
.and_then(|row_id| {
|
||||
env.visible_row_ids
|
||||
.iter()
|
||||
.position(|candidate| candidate == row_id)
|
||||
})
|
||||
.unwrap_or(0);
|
||||
let next = if offset.is_negative() {
|
||||
current.saturating_sub(offset.unsigned_abs())
|
||||
} else {
|
||||
(current + offset as usize).min(env.visible_row_ids.len() - 1)
|
||||
};
|
||||
focus_absolute_row(state, env, next)
|
||||
}
|
||||
|
||||
fn focus_absolute_row(
|
||||
state: &FileTreeRuntimeState,
|
||||
env: &FileTreeRuntimeEnvironment,
|
||||
index: usize,
|
||||
) -> FileTreeRuntimeState {
|
||||
let mut next = state.clone();
|
||||
next.selection.focused_row_id = env.visible_row_ids.get(index).cloned();
|
||||
next
|
||||
}
|
||||
|
||||
fn selected_or_focused_rows(state: &FileTreeRuntimeState) -> Vec<String> {
|
||||
if !state.selection.selected_row_ids.is_empty() {
|
||||
return state.selection.selected_row_ids.iter().cloned().collect();
|
||||
}
|
||||
state
|
||||
.selection
|
||||
.focused_row_id
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
fn keyboard_command_transition(
|
||||
state: &FileTreeRuntimeState,
|
||||
command: FileTreeKeyboardCommand,
|
||||
row_ids: Vec<String>,
|
||||
target_row_id: Option<String>,
|
||||
) -> FileTreeRuntimeTransition {
|
||||
if row_ids.is_empty() && !matches!(command, FileTreeKeyboardCommand::Paste) {
|
||||
return transition(state.clone(), []);
|
||||
}
|
||||
transition(
|
||||
state.clone(),
|
||||
[FileTreeRuntimeOutput::Intent(
|
||||
FileTreeIntentEvent::KeyboardCommand {
|
||||
command,
|
||||
row_ids,
|
||||
target_row_id,
|
||||
},
|
||||
)],
|
||||
)
|
||||
}
|
||||
|
||||
fn resolve_open_target(
|
||||
env: &FileTreeRuntimeEnvironment,
|
||||
row_id: &str,
|
||||
@@ -309,8 +476,9 @@ fn transition<const N: usize>(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
FileTreeIntentEvent, FileTreeOpenTarget, FileTreeRuntimeAction, FileTreeRuntimeEnvironment,
|
||||
FileTreeRuntimeOutput, FileTreeRuntimeRow, FileTreeRuntimeState,
|
||||
FileTreeIntentEvent, FileTreeKeyboardCommand, FileTreeOpenTarget, FileTreeRuntimeAction,
|
||||
FileTreeRuntimeEnvironment, FileTreeRuntimeOutput, FileTreeRuntimeRow,
|
||||
FileTreeRuntimeState,
|
||||
};
|
||||
use crate::tree_shell::drag_drop_state::TreeShellDragEffect;
|
||||
use crate::tree_shell::filetree_selection::FileTreeSelectionModifiers;
|
||||
@@ -436,6 +604,7 @@ mod tests {
|
||||
},
|
||||
},
|
||||
)));
|
||||
assert_eq!(transition.state.active_row_id.as_deref(), Some("doc:root"));
|
||||
|
||||
let transition = transition.state.reduce(
|
||||
&env(),
|
||||
@@ -551,4 +720,70 @@ mod tests {
|
||||
FileTreeRuntimeOutput::Intent(FileTreeIntentEvent::InternalDrop { .. })
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filetree_runtime_keyboard_contract_separates_active_selection_and_focus() {
|
||||
let state = FileTreeRuntimeState {
|
||||
active_row_id: Some("doc:root".into()),
|
||||
selection: crate::tree_shell::filetree_selection::FileTreeSelectionState::from_selected(
|
||||
&["doc:root".to_string()],
|
||||
),
|
||||
..FileTreeRuntimeState::default()
|
||||
};
|
||||
|
||||
let focused = state.reduce(&env(), FileTreeRuntimeAction::FocusNext);
|
||||
assert_eq!(focused.state.active_row_id.as_deref(), Some("doc:root"));
|
||||
assert_eq!(
|
||||
focused.state.selection.focused_row_id.as_deref(),
|
||||
Some("index:root")
|
||||
);
|
||||
assert!(focused
|
||||
.state
|
||||
.selection
|
||||
.selected_row_ids
|
||||
.contains("doc:root"));
|
||||
|
||||
let opened = focused
|
||||
.state
|
||||
.reduce(&env(), FileTreeRuntimeAction::OpenFocused);
|
||||
assert_eq!(opened.state.active_row_id.as_deref(), Some("index:root"));
|
||||
assert!(opened.outputs.contains(&FileTreeRuntimeOutput::Intent(
|
||||
FileTreeIntentEvent::Open {
|
||||
target: FileTreeOpenTarget::Index {
|
||||
document_id: "root".into(),
|
||||
},
|
||||
},
|
||||
)));
|
||||
|
||||
let rename = focused
|
||||
.state
|
||||
.reduce(&env(), FileTreeRuntimeAction::BeginRenameFocused);
|
||||
assert!(rename.outputs.contains(&FileTreeRuntimeOutput::Intent(
|
||||
FileTreeIntentEvent::KeyboardCommand {
|
||||
command: FileTreeKeyboardCommand::Rename,
|
||||
row_ids: vec!["doc:root".into()],
|
||||
target_row_id: Some("index:root".into()),
|
||||
},
|
||||
)));
|
||||
let cut = focused
|
||||
.state
|
||||
.reduce(&env(), FileTreeRuntimeAction::CutSelection);
|
||||
assert!(cut.outputs.contains(&FileTreeRuntimeOutput::Intent(
|
||||
FileTreeIntentEvent::KeyboardCommand {
|
||||
command: FileTreeKeyboardCommand::Cut,
|
||||
row_ids: vec!["doc:root".into()],
|
||||
target_row_id: Some("index:root".into()),
|
||||
},
|
||||
)));
|
||||
let escaped = FileTreeRuntimeState {
|
||||
drag_row_ids: vec!["doc:root".into()],
|
||||
drag_effect: Some(TreeShellDragEffect::Move),
|
||||
drop_target_row_id: Some("index:root".into()),
|
||||
..focused.state
|
||||
}
|
||||
.reduce(&env(), FileTreeRuntimeAction::Escape);
|
||||
assert!(escaped.state.drag_row_ids.is_empty());
|
||||
assert_eq!(escaped.state.drag_effect, None);
|
||||
assert_eq!(escaped.state.drop_target_row_id, None);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user