Add "Open source file" option

Available when right-clicking an object in
the object list or when viewing an object

Resolves #99
This commit is contained in:
Luke Street 2024-09-28 10:55:25 -06:00
parent 8fc142d316
commit bb039a1445
8 changed files with 142 additions and 13 deletions

37
Cargo.lock generated
View File

@ -2084,6 +2084,25 @@ version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4"
[[package]]
name = "is-docker"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
dependencies = [
"once_cell",
]
[[package]]
name = "is-wsl"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
dependencies = [
"is-docker",
"once_cell",
]
[[package]] [[package]]
name = "is_ci" name = "is_ci"
version = "1.2.0" version = "1.2.0"
@ -2904,6 +2923,7 @@ dependencies = [
"log", "log",
"notify", "notify",
"objdiff-core", "objdiff-core",
"open",
"path-slash", "path-slash",
"png", "png",
"pollster", "pollster",
@ -2941,6 +2961,17 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "open"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61a877bf6abd716642a53ef1b89fb498923a4afca5c754f9050b4d081c05c4b3"
dependencies = [
"is-wsl",
"libc",
"pathdiff",
]
[[package]] [[package]]
name = "openssl" name = "openssl"
version = "0.10.66" version = "0.10.66"
@ -3066,6 +3097,12 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42" checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42"
[[package]]
name = "pathdiff"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
[[package]] [[package]]
name = "pathfinder_geometry" name = "pathfinder_geometry"
version = "0.5.1" version = "0.5.1"

View File

@ -124,6 +124,10 @@ impl ProjectObject {
pub fn hidden(&self) -> bool { pub fn hidden(&self) -> bool {
self.metadata.as_ref().and_then(|m| m.auto_generated).unwrap_or(false) self.metadata.as_ref().and_then(|m| m.auto_generated).unwrap_or(false)
} }
pub fn source_path(&self) -> Option<&String> {
self.metadata.as_ref().and_then(|m| m.source_path.as_ref())
}
} }
#[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[derive(Default, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]

View File

@ -40,6 +40,7 @@ globset = { version = "0.4", features = ["serde1"] }
log = "0.4" log = "0.4"
notify = { git = "https://github.com/notify-rs/notify", rev = "128bf6230c03d39dbb7f301ff7b20e594e34c3a2" } notify = { git = "https://github.com/notify-rs/notify", rev = "128bf6230c03d39dbb7f301ff7b20e594e34c3a2" }
objdiff-core = { path = "../objdiff-core", features = ["all"] } objdiff-core = { path = "../objdiff-core", features = ["all"] }
open = "5.3"
png = "0.17" png = "0.17"
pollster = "0.3" pollster = "0.3"
regex = "1.10" regex = "1.10"

View File

@ -73,6 +73,7 @@ pub struct ObjectConfig {
pub reverse_fn_order: Option<bool>, pub reverse_fn_order: Option<bool>,
pub complete: Option<bool>, pub complete: Option<bool>,
pub scratch: Option<ScratchConfig>, pub scratch: Option<ScratchConfig>,
pub source_path: Option<String>,
} }
#[inline] #[inline]

View File

@ -61,6 +61,7 @@ impl ObjectConfigV0 {
reverse_fn_order: self.reverse_fn_order, reverse_fn_order: self.reverse_fn_order,
complete: None, complete: None,
scratch: None, scratch: None,
source_path: None,
} }
} }
} }

View File

@ -2,7 +2,7 @@
use std::string::FromUtf16Error; use std::string::FromUtf16Error;
use std::{ use std::{
mem::take, mem::take,
path::{PathBuf, MAIN_SEPARATOR}, path::{Path, PathBuf, MAIN_SEPARATOR},
}; };
#[cfg(all(windows, feature = "wsl"))] #[cfg(all(windows, feature = "wsl"))]
@ -96,6 +96,7 @@ impl ConfigViewState {
reverse_fn_order: None, reverse_fn_order: None,
complete: None, complete: None,
scratch: None, scratch: None,
source_path: None,
}); });
} else if let Ok(obj_path) = path.strip_prefix(target_dir) { } else if let Ok(obj_path) = path.strip_prefix(target_dir) {
let base_path = base_dir.join(obj_path); let base_path = base_dir.join(obj_path);
@ -106,6 +107,7 @@ impl ConfigViewState {
reverse_fn_order: None, reverse_fn_order: None,
complete: None, complete: None,
scratch: None, scratch: None,
source_path: None,
}); });
} }
} }
@ -174,7 +176,10 @@ pub fn config_ui(
) { ) {
let mut state_guard = state.write().unwrap(); let mut state_guard = state.write().unwrap();
let AppState { let AppState {
config: AppConfig { target_obj_dir, base_obj_dir, selected_obj, auto_update_check, .. }, config:
AppConfig {
project_dir, target_obj_dir, base_obj_dir, selected_obj, auto_update_check, ..
},
objects, objects,
object_nodes, object_nodes,
.. ..
@ -318,7 +323,14 @@ pub fn config_ui(
config_state.show_hidden, config_state.show_hidden,
) )
}) { }) {
display_node(ui, &mut new_selected_obj, &node, appearance, node_open); display_node(
ui,
&mut new_selected_obj,
project_dir.as_deref(),
&node,
appearance,
node_open,
);
} }
}); });
} }
@ -333,13 +345,12 @@ pub fn config_ui(
{ {
config_state.queue_build = true; config_state.queue_build = true;
} }
ui.separator();
} }
fn display_object( fn display_object(
ui: &mut egui::Ui, ui: &mut egui::Ui,
selected_obj: &mut Option<ObjectConfig>, selected_obj: &mut Option<ObjectConfig>,
project_dir: Option<&Path>,
name: &str, name: &str,
object: &ProjectObject, object: &ProjectObject,
appearance: &Appearance, appearance: &Appearance,
@ -357,7 +368,7 @@ fn display_object(
} else { } else {
appearance.text_color appearance.text_color
}; };
let clicked = SelectableLabel::new( let response = SelectableLabel::new(
selected, selected,
RichText::new(name) RichText::new(name)
.font(FontId { .font(FontId {
@ -366,11 +377,13 @@ fn display_object(
}) })
.color(color), .color(color),
) )
.ui(ui) .ui(ui);
.clicked(); if get_source_path(project_dir, object).is_some() {
response.context_menu(|ui| object_context_ui(ui, object, project_dir));
}
// Always recreate ObjectConfig if selected, in case the project config changed. // Always recreate ObjectConfig if selected, in case the project config changed.
// ObjectConfig is compared using equality, so this won't unnecessarily trigger a rebuild. // ObjectConfig is compared using equality, so this won't unnecessarily trigger a rebuild.
if selected || clicked { if selected || response.clicked() {
*selected_obj = Some(ObjectConfig { *selected_obj = Some(ObjectConfig {
name: object_name.to_string(), name: object_name.to_string(),
target_path: object.target_path.clone(), target_path: object.target_path.clone(),
@ -378,10 +391,31 @@ fn display_object(
reverse_fn_order: object.reverse_fn_order(), reverse_fn_order: object.reverse_fn_order(),
complete: object.complete(), complete: object.complete(),
scratch: object.scratch.clone(), scratch: object.scratch.clone(),
source_path: object.source_path().cloned(),
}); });
} }
} }
fn get_source_path(project_dir: Option<&Path>, object: &ProjectObject) -> Option<PathBuf> {
project_dir.and_then(|dir| object.source_path().map(|path| dir.join(path)))
}
fn object_context_ui(ui: &mut egui::Ui, object: &ProjectObject, project_dir: Option<&Path>) {
if let Some(source_path) = get_source_path(project_dir, object) {
if ui
.button("Open source file")
.on_hover_text("Open the source file in the default editor")
.clicked()
{
log::info!("Opening file {}", source_path.display());
if let Err(e) = open::that_detached(&source_path) {
log::error!("Failed to open source file: {e}");
}
ui.close_menu();
}
}
}
#[derive(Default, Copy, Clone, PartialEq, Eq, Debug)] #[derive(Default, Copy, Clone, PartialEq, Eq, Debug)]
enum NodeOpen { enum NodeOpen {
#[default] #[default]
@ -394,13 +428,14 @@ enum NodeOpen {
fn display_node( fn display_node(
ui: &mut egui::Ui, ui: &mut egui::Ui,
selected_obj: &mut Option<ObjectConfig>, selected_obj: &mut Option<ObjectConfig>,
project_dir: Option<&Path>,
node: &ProjectObjectNode, node: &ProjectObjectNode,
appearance: &Appearance, appearance: &Appearance,
node_open: NodeOpen, node_open: NodeOpen,
) { ) {
match node { match node {
ProjectObjectNode::File(name, object) => { ProjectObjectNode::File(name, object) => {
display_object(ui, selected_obj, name, object, appearance); display_object(ui, selected_obj, project_dir, name, object, appearance);
} }
ProjectObjectNode::Dir(name, children) => { ProjectObjectNode::Dir(name, children) => {
let contains_obj = selected_obj.as_ref().map(|path| contains_node(node, path)); let contains_obj = selected_obj.as_ref().map(|path| contains_node(node, path));
@ -426,7 +461,7 @@ fn display_node(
.open(open) .open(open)
.show(ui, |ui| { .show(ui, |ui| {
for node in children { for node in children {
display_node(ui, selected_obj, node, appearance, node_open); display_node(ui, selected_obj, project_dir, node, appearance, node_open);
} }
}); });
} }

View File

@ -509,6 +509,18 @@ pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance
); );
} }
}); });
ui.separator();
if ui
.add_enabled(
state.source_path_available,
egui::Button::new("🖹 Source file"),
)
.on_hover_text_at_pointer("Open the source file in the default editor")
.on_disabled_hover_text("Source file metadata missing")
.clicked()
{
state.queue_open_source_path = true;
}
}); });
ui.scope(|ui| { ui.scope(|ui| {

View File

@ -52,6 +52,8 @@ pub struct DiffViewState {
pub scratch_available: bool, pub scratch_available: bool,
pub queue_scratch: bool, pub queue_scratch: bool,
pub scratch_running: bool, pub scratch_running: bool,
pub source_path_available: bool,
pub queue_open_source_path: bool,
} }
#[derive(Default)] #[derive(Default)]
@ -86,6 +88,9 @@ impl DiffViewState {
self.symbol_state.reverse_fn_order = value; self.symbol_state.reverse_fn_order = value;
self.symbol_state.disable_reverse_fn_order = true; self.symbol_state.disable_reverse_fn_order = true;
} }
self.source_path_available = obj_config.source_path.is_some();
} else {
self.source_path_available = false;
} }
self.scratch_available = CreateScratchConfig::is_available(&state.config); self.scratch_available = CreateScratchConfig::is_available(&state.config);
} }
@ -122,6 +127,22 @@ impl DiffViewState {
} }
} }
} }
if self.queue_open_source_path {
self.queue_open_source_path = false;
if let Ok(state) = state.read() {
if let (Some(project_dir), Some(source_path)) = (
&state.config.project_dir,
state.config.selected_obj.as_ref().and_then(|obj| obj.source_path.as_ref()),
) {
let source_path = project_dir.join(source_path);
log::info!("Opening file {}", source_path.display());
open::that_detached(source_path).unwrap_or_else(|err| {
log::error!("Failed to open source file: {err}");
});
}
}
}
} }
} }
@ -518,10 +539,27 @@ pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appea
|ui| { |ui| {
ui.set_width(column_width); ui.set_width(column_width);
ui.horizontal(|ui| {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.label("Build base:");
});
ui.separator();
if ui
.add_enabled(
state.source_path_available,
egui::Button::new("🖹 Source file"),
)
.on_hover_text_at_pointer("Open the source file in the default editor")
.on_disabled_hover_text("Source file metadata missing")
.clicked()
{
state.queue_open_source_path = true;
}
});
ui.scope(|ui| { ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.label("Build base:");
if result.second_status.success { if result.second_status.success {
if result.second_obj.is_none() { if result.second_obj.is_none() {
ui.colored_label(appearance.replace_color, "Missing"); ui.colored_label(appearance.replace_color, "Missing");