Repository: Canop/broot (language: rust)

Issue:
Open broot with a default selected file/directory.
I am using broot as my file manager and file navigator in neovim.
I would like to be able to have the current file preselected when I open broot in neovim.

Currently I achieve this with this command `"broot -c ':select " .. vim.fn.expand("%:") .. "'"` e.g. `broot -c ':select some/dir'`.
This works fine when the directory or file is expanded in broot, but does not work for very nested directories or files.

Would it be possible to auto-expand the tree when passing `some/very/long/and/nested/dir/file.txt` to the :select command?

This would be very helpful as I would be able to navigate in the files that are close to the one that I am currently working with.


Discussion / hints:
Just to be sure: the reason you don't use `broot some/very/long/and/nested/dir/file.txt` is because you want to keep the current tree root ?
Yes, I would still like to be able to search for files in the entire project.
ok, noted.

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8c5a067a..a5ef3ab8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
 ### next
 - 'Size' and 'Deletion date' columns in trash screen. This screen now supports the `:toggle_date`, `:toggle_size`, `:sort_by_date`, and `:sort_by_size` internals.
+- new `:show` internal make the provided path visible and selected, adding lines to the tree if necessary, does nothing if the provided path is not a descendant of the current tree root (this part may change depending on feedback) - Fix #936
 
 ### v1.42.0 - 2024-08-18
 <a name="v1.42.0"></a>
diff --git a/src/browser/browser_state.rs b/src/browser/browser_state.rs
index 5f4b7301..51881087 100644
--- a/src/browser/browser_state.rs
+++ b/src/browser/browser_state.rs
@@ -24,8 +24,6 @@ use {
 pub struct BrowserState {
     pub tree: Tree,
     pub filtered_tree: Option<Tree>,
-    // pub pending_pattern: InputPattern, // a pattern (or not) which has not yet be applied
-    // pub total_search_required: bool,   // whether the pending pattern should be in total search mode
     mode: Mode, // whether we're in 'input' or 'normal' mode
     pending_task: Option<BrowserTask>, // note: there are some other pending task, see
 }
@@ -563,6 +561,32 @@ impl PanelState for BrowserState {
                 self.displayed_tree_mut().try_select_last(page_height);
                 CmdResult::Keep
             }
+            Internal::show => {
+                let path = internal_path::determine_path(
+                    internal_exec,
+                    input_invocation,
+                    trigger_type,
+                    self.displayed_tree(),
+                    app_state,
+                    cc,
+                );
+                match path {
+                    Some(path) => {
+                        let res = self.displayed_tree_mut().show_path(&path, con);
+                        match res {
+                            Ok(()) => {
+                                let page_height = BrowserState::page_height(screen);
+                                self.displayed_tree_mut().make_selection_visible(page_height);
+                                CmdResult::Keep
+                            }
+                            Err(e) => CmdResult::DisplayError(format!("{e}")),
+                        }
+                    }
+                    None => {
+                        CmdResult::Keep
+                    }
+                }
+            }
             Internal::stage_all_directories => {
                 let pattern = self.displayed_tree().options.pattern.clone();
                 let file_type_condition = FileTypeCondition::Directory;
diff --git a/src/errors.rs b/src/errors.rs
index 4f90169d..bcafbf94 100644
--- a/src/errors.rs
+++ b/src/errors.rs
@@ -10,11 +10,10 @@ use {
 custom_error! {pub ProgramError
     AmbiguousVerbName {name: String} = "Ambiguous name: More than one verb matches {name:?}",
     ArgParse {bad: String, valid: String} = "{bad:?} can't be parsed (valid values: {valid:?})",
-    Conf {source: ConfError} = "Bad configuration: {source}",
     ConfFile {path:String, details: ConfError} = "Bad configuration file {path:?} : {details}",
+    Conf {source: ConfError} = "Bad configuration: {source}",
     ImageError {source: ImageError } = "{source}",
     InternalError {details: String} = "Internal error: {details}", // should not happen
-    InvalidGlobError {pattern: String} = "Invalid glob: {pattern}",
     Io {source: io::Error} = "IO Error : {source}",
     LaunchError {program: String, source: io::Error} = "Unable to launch {program}: {source}",
     Lfs {details: String} = "Failed to fetch mounts: {details}",
@@ -24,6 +23,7 @@ custom_error! {pub ProgramError
     Svg {source: SvgError} = "SVG error: {source}",
     SyntectCrashed { details: String } = "Syntect crashed on {details:?}",
     Termimad {source: termimad::Error} = "Termimad Error : {source}",
+    Trash {message: String} = "Trash error: {message}",
     TreeBuild {source: TreeBuildError} = "{source}",
     UnknowShell {shell: String} = "Unknown shell: {shell}",
     UnknownVerb {name: String} = "No verb matches {name:?}",
@@ -32,7 +32,6 @@ custom_error! {pub ProgramError
     UnprintableFile = "File can't be printed", // has characters that can't be printed without escaping
     Unrecognized {token: String} = "Unrecognized: {token}",
     ZeroLenFile = "File seems empty",
-    Trash {message: String} = "Trash error: {message}",
 }
 
 custom_error! {pub ShellInstallError
@@ -67,11 +66,14 @@ impl<Ok> IoToShellInstallError<Ok> for Result<Ok, io::Error> {
 }
 
 custom_error! {pub TreeBuildError
-    NotADirectory { path: String } = "Not a directory: {path}",
     FileNotFound { path: String } = "File not found: {path}",
     Interrupted = "Task Interrupted",
     InvalidUtf8 { path: String } = "Invalid UTF-8 in {path}",
+    //Io {source: io::Error} = "IO Error : {source}",
+    NotADirectory { path: String } = "Not a directory: {path}",
+    NotARootDescendant { path: String } = "Not a descendant of the root: {path}",
     TooManyMatches { max: usize } = "Too many matches (max allowed: {max})",
+    UnconsistentData { message:String } = "Unconsistent data: {message}", // maybe refresh ?
 }
 
 custom_error! {pub ConfError
diff --git a/src/tree/mod.rs b/src/tree/mod.rs
index 4f3ccc58..371fa53c 100644
--- a/src/tree/mod.rs
+++ b/src/tree/mod.rs
@@ -8,7 +8,7 @@ mod tree_options;
 pub use {
     sort::Sort,
     tree::Tree,
-    tree_line::TreeLine,
+    tree_line::*,
     tree_line_type::TreeLineType,
     tree_options::TreeOptions,
 };
diff --git a/src/tree/tree.rs b/src/tree/tree.rs
index ab07fc91..cb9d70ab 100644
--- a/src/tree/tree.rs
+++ b/src/tree/tree.rs
@@ -2,12 +2,12 @@ use {
     super::*,
     crate::{
         app::AppContext,
-        errors,
+        errors::TreeBuildError,
         file_sum::FileSum,
         git::TreeGitStatus,
         task_sync::ComputationResult,
         task_sync::Dam,
-        tree_build::{BId, BuildReport, TreeBuilder},
+        tree_build::{BuildReport, TreeBuilder},
     },
     rustc_hash::FxHashMap,
     std::{
@@ -17,13 +17,14 @@ use {
     },
 };
 
-/// The tree which may be displayed, with onle line per visible line of the panel.
+/// The tree which may be displayed, with one line per visible line of the panel.
 ///
 /// In the tree structure, every "node" is just a line, there's
 ///  no link from a child to its parent or from a parent to its children.
 #[derive(Debug, Clone)]
 pub struct Tree {
-    pub lines: Box<[TreeLine]>,
+    pub lines: Vec<TreeLine>,
+    pub next_line_id: usize,
     pub selection: usize, // there's always a selection (starts with root, which is 0)
     pub options: TreeOptions,
     pub scroll: usize, // the number of lines at the top hidden because of scrolling
@@ -34,12 +35,13 @@ pub struct Tree {
 
 impl Tree {
 
+
     /// rebuild the tree with the same root, height, and options
     pub fn refresh(
         &mut self,
         page_height: usize,
         con: &AppContext,
-    ) -> Result<(), errors::TreeBuildError> {
+    ) -> Result<(), TreeBuildError> {
         let builder = TreeBuilder::from(
             self.root().to_path_buf(),
             self.options.clone(),
@@ -73,19 +75,19 @@ impl Tree {
         //  - we want a case insensitive sort
         //  - we still don't want to confuse the children of AA and Aa
         //  - a node can come from a not parent node, when we followed a link
-        let mut bid_parents: FxHashMap<BId, BId> = FxHashMap::default();
-        let mut bid_lines: FxHashMap<BId, &TreeLine> = FxHashMap::default();
+        let mut id_parents: FxHashMap<TreeLineId, TreeLineId> = FxHashMap::default();
+        let mut id_lines: FxHashMap<TreeLineId, &TreeLine> = FxHashMap::default();
         for line in &self.lines[..] {
-            if let Some(parent_bid) = line.parent_bid {
-                bid_parents.insert(line.bid, parent_bid);
+            if let Some(parent_id) = line.parent_id {
+                id_parents.insert(line.id, parent_id);
             }
-            bid_lines.insert(line.bid, line);
+            id_lines.insert(line.id, line);
         }
-        let mut sort_paths: FxHashMap<BId, String> = FxHashMap::default();
+        let mut sort_paths: FxHashMap<TreeLineId, String> = FxHashMap::default();
         for line in &self.lines[1..] {
             let mut sort_path = String::new();
-            let mut bid = line.bid;
-            while let Some(l) = bid_lines.get(&bid) {
+            let mut id = line.id;
+            while let Some(l) = id_lines.get(&id) {
                 let lower_name = l.path.file_name().map_or(
                     "".to_string(),
                     |name| name.to_string_lossy().to_lowercase(),
@@ -111,18 +113,18 @@ impl Tree {
                     "{}{}-{}/{}",
                     sort_prefix,
                     lower_name,
-                    bid.index(), // to be sure to separate paths having the same lowercase
+                    id, // to be sure to separate paths having the same lowercase
                     sort_path,
                 );
-                if let Some(&parent_bid) = bid_parents.get(&bid) {
-                    bid = parent_bid;
+                if let Some(&parent_id) = id_parents.get(&id) {
+                    id = parent_id;
                 } else {
                     break;
                 }
             }
-            sort_paths.insert(line.bid, sort_path);
+            sort_paths.insert(line.id, sort_path);
         }
-        self.lines[1..].sort_by_key(|line| sort_paths.get(&line.bid).unwrap());
+        self.lines[1..].sort_by_key(|line| sort_paths.get(&line.id).unwrap());
 
         let mut best_index = 0; // index of the line with the best score
         for i in 1..self.lines.len() {
@@ -139,12 +141,12 @@ impl Tree {
         for end_index in (1..self.lines.len()).rev() {
             let depth = (self.lines[end_index].depth - 1) as usize;
             let start_index = {
-                let parent_index = match self.lines[end_index].parent_bid {
-                    Some(parent_bid) => {
+                let parent_index = match self.lines[end_index].parent_id {
+                    Some(parent_id) => {
                         let mut index = end_index;
                         loop {
                             index -= 1;
-                            if self.lines[index].bid == parent_bid {
+                            if self.lines[index].id == parent_id {
                                 break;
                             }
                             if index == 0 {
@@ -534,5 +536,106 @@ impl Tree {
             sum
         }
     }
+
+    /// Add to the tree the lines which are in the given path but not already in the tree.
+    ///
+    /// Fail if the path is not a descendant of the tree root.
+    fn add_lines_to_path(
+        &mut self,
+        target_path: &Path,
+        con: &AppContext,
+    ) -> Result<(), TreeBuildError> {
+        let mut path = target_path;
+        let mut paths_to_add = Vec::new();
+        // find the closest parent already in the tree
+        let mut present_ancestor_idx = loop {
+            let idx = self
+                .lines
+                .iter()
+                .position(|line| line.path == path);
+            if let Some(idx) = idx {
+                break idx;
+            }
+            paths_to_add.push(path);
+            let Some(parent) = path.parent() else {
+                warn!("no ancestor in the tree for {:?}", path);
+                return Err(TreeBuildError::NotARootDescendant {
+                    path: path.display().to_string(),
+                });
+            };
+            path = parent;
+        };
+
+        let present_ancestor = &mut self.lines[present_ancestor_idx];
+
+        //debug!("present ancestor: {:#?}", &present_ancestor);
+        if present_ancestor.line_type.is_pruning() {
+            info!("unpruning {:?}", &present_ancestor.path);
+            present_ancestor.unprune();
+            // we should in exchange prune another one ?
+        }
+
+        debug!("show -> paths to add: {:?}", paths_to_add);
+        if paths_to_add.is_empty() {
+            return Ok(());
+        }
+        present_ancestor.nb_kept_children += 1;
+
+        // adding the new lines
+        while let Some(path_to_add) = paths_to_add.pop() {
+            info!("adding {:?}", path_to_add);
+            let new_line_id = self.next_line_id;
+            self.next_line_id += 1;
+            let parent = &self.lines[present_ancestor_idx];
+            let depth = parent.depth + 1;
+
+            // The 1 kept_children here might be a trick to avoid the file
+            // being changed to Pruning in the after_lines_changed method...
+            let nb_kept_children = 1;
+
+            let subpath =path_to_add
+                .strip_prefix(self.root())
+                .map_err(|_| {
+                    // not supposed to happen at this point as we're adding a descendant
+                    TreeBuildError::NotARootDescendant {
+                        path: path.display().to_string(),
+                    }
+                })?
+                .to_string_lossy()
+                .to_string();
+
+            let line = TreeLineBuilder {
+                id: new_line_id,
+                path: path_to_add.to_path_buf(),
+                subpath,
+                parent_id: Some(parent.id),
+                depth,
+                unlisted: 0,
+                nb_kept_children,
+                has_error: false,
+                score: 1,
+                direct_match: true,
+            }.build(con)?;
+
+            present_ancestor_idx = self.lines.len();
+            self.lines.push(line);
+        }
+        self.after_lines_changed();
+        Ok(())
+    }
+
+    pub fn show_path(
+        &mut self,
+        path: &Path,
+        con: &AppContext,
+    ) -> Result<(), TreeBuildError> {
+        self.add_lines_to_path(path, con)?;
+        let selected = self.try_select_path(path);
+        if !selected {
+            warn!("failed to select {:?}", path);
+        }
+        Ok(())
+    }
+
 }
 
diff --git a/src/tree/tree_line.rs b/src/tree/tree_line.rs
index 45b1084d..30d27dde 100644
--- a/src/tree/tree_line.rs
+++ b/src/tree/tree_line.rs
@@ -1,29 +1,41 @@
 use {
     super::*,
     crate::{
-        app::{Selection, SelectionType},
+        app::{
+            AppContext,
+            Selection,
+            SelectionType,
+        },
+        errors::TreeBuildError,
         file_sum::FileSum,
         git::LineGitStatus,
-        tree_build::BId,
     },
     lazy_regex::regex_captures,
     std::{
         fs,
-        path::{Path, PathBuf},
+        path::{
+            Path,
+            PathBuf,
+        },
     },
 };
 
 #[cfg(unix)]
-use {std::os::unix::fs::MetadataExt, umask::Mode};
+use {
+    std::os::unix::fs::MetadataExt,
+    umask::Mode,
+};
 
 #[cfg(windows)]
 use is_executable::IsExecutable;
 
+pub type TreeLineId = usize;
+
 /// a line in the representation of the file hierarchy
 #[derive(Debug, Clone)]
 pub struct TreeLine {
-    pub bid: BId,
-    pub parent_bid: Option<BId>,
+    pub id: TreeLineId,
+    pub parent_id: Option<TreeLineId>,
     pub left_branches: Box<[bool]>, // a depth-sized array telling whether a branch pass
     pub depth: u16,
     pub path: PathBuf,
@@ -41,16 +53,81 @@ pub struct TreeLine {
     pub git_status: Option<LineGitStatus>,
 }
 
-impl TreeLine {
+pub struct TreeLineBuilder {
+    pub path: PathBuf,
+    pub subpath: String,
+    pub id: TreeLineId,
+    pub parent_id: Option<TreeLineId>,
+    pub depth: u16,
+    pub unlisted: usize,
+    pub nb_kept_children: usize,
+    pub has_error: bool,
+    pub score: i32,
+    pub direct_match: bool,
+}
+
+impl TreeLineBuilder {
+    pub fn build(
+        self,
+        con: &AppContext,
+    ) -> Result<TreeLine, TreeBuildError> {
+        let Self {
+            path,
+            subpath,
+            id,
+            parent_id,
+            depth,
+            unlisted,
+            nb_kept_children,
+            has_error,
+            score,
+            direct_match,
+        } = self;
+        let metadata = fs::symlink_metadata(&path).map_err(|_| TreeBuildError::FileNotFound {
+            path: path.to_string_lossy().to_string(),
+        })?;
+        let line_type = TreeLineType::new(&path, metadata.file_type());
+        let name = path
+            .file_name()
+            .and_then(|os_str| os_str.to_str())
+            .unwrap_or("")
+            .replace('\n', "");
+        let icon = con.icons.as_ref().map(|icon_plugin| {
+            let extension = TreeLine::extension_from_name(&name);
+            let double_extension =
+                extension.and_then(|_| TreeLine::double_extension_from_name(&name));
+            icon_plugin.get_icon(&line_type, &name, double_extension, extension)
+        });
+
+        Ok(TreeLine {
+            id,
+            parent_id,
+            left_branches: vec![false; depth as usize].into_boxed_slice(),
+            depth,
+            icon,
+            name,
+            subpath,
+            path,
+            line_type,
+            has_error,
+            nb_kept_children,
+            unlisted,
+            score,
+            direct_match,
+            sum: None,
+            metadata,
+            git_status: None,
+        })
+    }
+}
 
+impl TreeLine {
     pub fn double_extension_from_name(name: &str) -> Option<&str> {
-        regex_captures!(r"\.([^.]+\.[^.]+)", name)
-            .map(|(_, de)| de)
+        regex_captures!(r"\.([^.]+\.[^.]+)", name).map(|(_, de)| de)
     }
 
     pub fn extension_from_name(name: &str) -> Option<&str> {
-        regex_captures!(r"\.([^.]+)$", name)
-            .map(|(_, ext)| ext)
+        regex_captures!(r"\.([^.]+)$", name).map(|(_, ext)| ext)
     }
 
     pub fn is_selectable(&self) -> bool {
@@ -66,7 +143,10 @@ impl TreeLine {
     pub fn is_file(&self) -> bool {
         matches!(&self.line_type, TreeLineType::File)
     }
-    pub fn is_of(&self, selection_type: SelectionType) -> bool {
+    pub fn is_of(
+        &self,
+        selection_type: SelectionType,
+    ) -> bool {
         match selection_type {
             SelectionType::Any => true,
             SelectionType::File => self.is_file(),
@@ -134,6 +214,11 @@ impl TreeLine {
             _ => &self.path,
         }
     }
+    pub fn unprune(&mut self) {
+        self.line_type = TreeLineType::new(&self.path, self.metadata.file_type());
+        self.name = self
+            .path
+            .file_name()
+            .map_or_else(|| "???".to_string(), |n| n.to_string_lossy().to_string());
+    }
 }
-
-
diff --git a/src/tree/tree_line_type.rs b/src/tree/tree_line_type.rs
index 6b7e8967..ae5866bd 100644
--- a/src/tree/tree_line_type.rs
+++ b/src/tree/tree_line_type.rs
@@ -35,6 +35,10 @@ pub fn read_link(path: &Path) -> io::Result<PathBuf> {
 
 impl TreeLineType {
 
+    pub fn is_pruning(&self) -> bool {
+        matches!(self, Self::Pruning)
+    }
+
     fn resolve(direct_target: &Path) -> io::Result<Self> {
         let mut final_target = direct_target.to_path_buf();
         let mut final_metadata = fs::symlink_metadata(&final_target)?;
@@ -74,7 +78,7 @@ impl TreeLineType {
         })
     }
 
-    pub fn new(path: &Path, ft: &fs::FileType) -> Self {
+    pub fn new(path: &Path, ft: fs::FileType) -> Self {
         if ft.is_dir() {
             Self::Dir
         } else if ft.is_symlink() {
diff --git a/src/tree_build/builder.rs b/src/tree_build/builder.rs
index 9f383677..9abea318 100644
--- a/src/tree_build/builder.rs
+++ b/src/tree_build/builder.rs
@@ -434,9 +434,7 @@ impl<'c> TreeBuilder<'c> {
         let mut remove_queue: BinaryHeap<SortableBId> = BinaryHeap::new();
         for id in &out_blines[1..] {
             let bline = &self.blines[*id];
-            if bline.has_match
-                && bline.nb_kept_children == 0
-                && (bline.depth > 1 || self.trim_root)
+            if bline.has_match && bline.nb_kept_children == 0 && (bline.depth > 1 || self.trim_root)
             {
                 remove_queue.push(SortableBId {
                     id: *id,
@@ -470,15 +468,7 @@ impl<'c> TreeBuilder<'c> {
         bid: BId,
     ) -> Result<TreeLine, TreeBuildError> {
         let bline = &self.blines[bid];
-        let line_type = TreeLineType::new(&bline.path, &bline.file_type);
-        let unlisted = bline
-            .children
-            .as_ref()
-            .map_or(0, |children| children.len() - bline.next_child_idx);
-        let metadata =
-            fs::symlink_metadata(&bline.path).map_err(|_| TreeBuildError::FileNotFound {
-                path: bline.path.to_string_lossy().to_string(),
-            })?;
+        let path = bline.path.clone();
         let subpath = if bline.depth == 0 {
             bline.path.to_string_lossy().to_string()
         } else {
@@ -491,38 +481,23 @@ impl<'c> TreeBuilder<'c> {
                 .to_string_lossy()
                 .to_string()
         };
-        let name = bline
-            .path
-            .file_name()
-            .and_then(|os_str| os_str.to_str())
-            .unwrap_or("")
-            .replace('\n', "");
-        let icon = self.con.icons.as_ref().map(|icon_plugin| {
-            let extension = TreeLine::extension_from_name(&name);
-            let double_extension =
-                extension.and_then(|_| TreeLine::double_extension_from_name(&name));
-            icon_plugin.get_icon(&line_type, &name, double_extension, extension)
-        });
-
-        Ok(TreeLine {
-            bid,
-            parent_bid: bline.parent_id,
-            left_branches: vec![false; bline.depth as usize].into_boxed_slice(),
-            depth: bline.depth,
-            icon,
-            name,
+        let unlisted = bline
+            .children
+            .as_ref()
+            .map_or(0, |children| children.len() - bline.next_child_idx);
+        TreeLineBuilder {
+            path,
             subpath,
-            path: bline.path.clone(),
-            line_type,
-            has_error: bline.has_error,
-            nb_kept_children: bline.nb_kept_children as usize,
+            id: bid.index(),
+            parent_id: bline.parent_id.map(|bid| bid.index()),
+            depth: bline.depth,
             unlisted,
+            nb_kept_children: bline.nb_kept_children as usize,
+            has_error: bline.has_error,
             score: bline.score,
             direct_match: bline.direct_match,
-            sum: None,
-            metadata,
-            git_status: None,
-        })
+        }
+        .build(self.con)
     }
 
     /// make a tree from the builder's specific structure
@@ -548,8 +523,10 @@ impl<'c> TreeBuilder<'c> {
                 }
             }
         }
+        let next_line_id = lines.iter().map(|line| line.id).max().unwrap_or(0) + 1;
         let mut tree = Tree {
-            lines: lines.into_boxed_slice(),
+            lines,
+            next_line_id,
             selection: 0,
             options: self.options.clone(),
             scroll: 0,
@@ -564,7 +541,7 @@ impl<'c> TreeBuilder<'c> {
             tree.git_status = ComputationResult::NotComputed;
             // it would make no sense to keep only files having a git status and
             // not display that type
-            for line in tree.lines.iter_mut() {
+            for line in &mut tree.lines {
                 line.git_status = computer.line_status(&line.path);
             }
         }
@@ -580,13 +557,12 @@ impl<'c> TreeBuilder<'c> {
         total_search: bool,
         dam: &Dam,
     ) -> Result<Tree, TreeBuildError> {
-        self.gather_lines(total_search, dam).map(|blines_ids| {
-            debug!("blines before trimming: {}", blines_ids.len());
-            if !self.total_search {
-                self.trim_excess(&blines_ids);
-            }
-            self.take_as_tree(&blines_ids)
-        })
+        let blines_ids = self.gather_lines(total_search, dam)?;
+        debug!("blines before trimming: {}", blines_ids.len());
+        if !self.total_search {
+            self.trim_excess(&blines_ids);
+        }
+        Ok(self.take_as_tree(&blines_ids))
     }
 
     pub fn build_paths<F>(
diff --git a/src/verb/internal.rs b/src/verb/internal.rs
index cdaae716..a0f5a7e3 100644
--- a/src/verb/internal.rs
+++ b/src/verb/internal.rs
@@ -123,6 +123,7 @@ Internals! {
     root_down: "move tree root down" true,
     root_up: "move tree root up" true,
     select: "select a file by path" true,
+    show: "reveal and select a file by path" true,
     select_first: "select the first item" false,
     select_last: "select the last item" false,
     set_panel_width: "set the width of a panel" false,
@@ -170,6 +171,7 @@ impl Internal {
             Internal::apply_flags => r"-(?P<flags>\w+)?",
             Internal::focus => r"focus (?P<path>.*)?",
             Internal::select => r"select (?P<path>.*)?",
+            Internal::show => r"show (?P<path>.*)?",
             Internal::line_down => r"line_down (?P<count>\d*)?",
             Internal::line_up => r"line_up (?P<count>\d*)?",
             Internal::line_down_no_cycle => r"line_down_no_cycle (?P<count>\d*)?",
diff --git a/src/verb/internal_path.rs b/src/verb/internal_path.rs
new file mode 100644
index 00000000..9b85cde9
--- /dev/null
+++ b/src/verb/internal_path.rs
@@ -0,0 +1,139 @@
+//! utility functions to help handle some internals which require a path
+
+use {
+    super::*,
+    crate::{
+        app::*,
+        browser::BrowserState,
+        command::TriggerType,
+        display::Screen,
+        path::{self, PathAnchor},
+        tree::Tree,
+    },
+    std::path::{Path, PathBuf},
+};
+
+
+/// general implementation for verbs on the form ":some_internal path"
+pub fn determine_path(
+    internal_exec: &InternalExecution,
+    input_invocation: Option<&VerbInvocation>,
+    trigger_type: TriggerType,
+    tree: &Tree,
+    app_state: & AppState,
+    cc: &CmdContext,
+) -> Option<PathBuf> {
+    info!(
+        "internal_path::determine_path internal_exec={:?} input_invocation={:?} trygger_type={:?}",
+        internal_exec,
+        input_invocation,
+        trigger_type,
+    );
+    let input_arg = input_invocation.as_ref()
+        .and_then(|invocation| invocation.args.as_ref());
+    match trigger_type {
+        TriggerType::Input(verb) => {
+            let path = path_from_input(
+                verb,
+                internal_exec,
+                &tree.selected_line().path,
+                input_arg,
+                app_state,
+                cc.app.con,
+            );
+            Some(path)
+        }
+        _ => {
+            // the :select internal was triggered by a key
+            if let Some(arg) = &internal_exec.arg {
+                // the internal_execution specifies the path to use
+                // (it may come from a configured verb whose execution is
+                //  `:select some/path`).
+                // The given path may be relative hence the need for the
+                // state's selection
+                let path = path::path_from(
+                    &tree.selected_line().path,
+                    PathAnchor::Unspecified,
+                    arg,
+                );
+                Some(path)
+            } else {
+                // there's nothing really to do here
+                None
+            }
+        }
+    }
+}
+
+
+/// Compute the path to go to in case of the internal being triggered from
+/// the input.
+///
+/// This path depends on the verb (which may hardcore the path or have a
+/// pattern), from the selection,
+fn path_from_input(
+    verb: &Verb,
+    internal_exec: &InternalExecution,
+    base_path: &Path, // either the selected path or the root path
+    input_arg: Option<&String>,
+    app_state: &AppState,
+    con: &AppContext,
+) -> PathBuf {
+    match (input_arg, internal_exec.arg.as_ref()) {
+        (Some(input_arg), Some(verb_arg)) => {
+            // The verb probably defines some pattern which uses the input.
+            // For example:
+            // {
+            //     invocation: "gotar {path}"
+            //     execution: ":select {path}/target"
+            // }
+            // (or that input is useless)
+            let path_builder = ExecutionStringBuilder::with_invocation(
+                verb.invocation_parser.as_ref(),
+                SelInfo::from_path(base_path),
+                app_state,
+                Some(input_arg),
+            );
+            path_builder.path(verb_arg, con)
+        }
+        (Some(input_arg), None) => {
+            // the verb defines nothing
+            // The :select internal execution was triggered from the
+            // input (which must be a kind of alias for :select)
+            // so we do exactly what the input asks for
+            path::path_from(base_path, PathAnchor::Unspecified, input_arg)
+        }
+        (None, Some(verb_arg)) => {
+            // the verb defines the path where to go..
+            // the internal_execution specifies the path to use
+            // (it may come from a configured verb whose execution is
+            //  `:select some/path`).
+            // The given path may be relative hence the need for the
+            // state's selection
+            // (we assume a check before ensured it doesn't need an input)
+            path::path_from(base_path, PathAnchor::Unspecified, verb_arg)
+        }
+        (None, None) => {
+            // This doesn't really make sense: we're selecting the currently
+            // selected path
+            base_path.to_path_buf()
+        }
+    }
+
+}
+
+pub fn on_path(
+    path: PathBuf,
+    tree: &mut Tree,
+    screen: Screen,
+    in_new_panel: bool,
+) -> CmdResult {
+    debug!("executing :select on path {:?}", &path);
+    if in_new_panel {
+        warn!("bang in :select isn't supported yet");
+    }
+    if tree.try_select_path(&path) {
+        tree.make_selection_visible(BrowserState::page_height(screen));
+    }
+    CmdResult::Keep
+}
diff --git a/src/verb/internal_select.rs b/src/verb/internal_select.rs
index 947d3fa4..c04c4f1b 100644
--- a/src/verb/internal_select.rs
+++ b/src/verb/internal_select.rs
@@ -7,10 +7,9 @@ use {
         browser::BrowserState,
         command::TriggerType,
         display::Screen,
-        path::{self, PathAnchor},
         tree::Tree,
     },
-    std::path::{Path, PathBuf},
+    std::path::PathBuf,
 };
 
 
@@ -24,110 +23,21 @@ pub fn on_internal(
     app_state: & AppState,
     cc: &CmdContext,
 ) -> CmdResult {
-    let screen = cc.app.screen;
-    info!(
-        "internal_select.on_internal internal_exec={:?} input_invocation={:?} trygger_type={:?}",
+    let Some(path) = internal_path::determine_path(
         internal_exec,
         input_invocation,
         trigger_type,
-    );
+        tree,
+        app_state,
+        cc,
+    ) else {
+        return CmdResult::Keep;
+    };
+    let screen = cc.app.screen;
     let bang = input_invocation
             .map(|inv| inv.bang)
             .unwrap_or(internal_exec.bang);
-    let input_arg = input_invocation.as_ref()
-        .and_then(|invocation| invocation.args.as_ref());
-    match trigger_type {
-        TriggerType::Input(verb) => {
-            let path = path_from_input(
-                verb,
-                internal_exec,
-                &tree.selected_line().path,
-                input_arg,
-                app_state,
-                cc.app.con,
-            );
-            on_path(path, tree, screen, bang)
-        }
-        _ => {
-            // the :select internal was triggered by a key
-            if let Some(arg) = &internal_exec.arg {
-                // the internal_execution specifies the path to use
-                // (it may come from a configured verb whose execution is
-                //  `:select some/path`).
-                // The given path may be relative hence the need for the
-                // state's selection
-                let path = path::path_from(
-                    &tree.selected_line().path,
-                    PathAnchor::Unspecified,
-                    arg,
-                );
-                let bang = input_invocation
-                    .map(|inv| inv.bang)
-                    .unwrap_or(internal_exec.bang);
-                on_path(path, tree, screen, bang)
-            } else {
-                // there's nothing really to do here
-                CmdResult::Keep
-            }
-        }
-    }
-}
-
-
-/// Compute the path to go to in case of the internal being triggered from
-/// the input.
-///
-/// This path depends on the verb (which may hardcore the path or have a
-/// pattern), from the selection,
-fn path_from_input(
-    verb: &Verb,
-    internal_exec: &InternalExecution,
-    base_path: &Path, // either the selected path or the root path
-    input_arg: Option<&String>,
-    app_state: &AppState,
-    con: &AppContext,
-) -> PathBuf {
-    match (input_arg, internal_exec.arg.as_ref()) {
-        (Some(input_arg), Some(verb_arg)) => {
-            // The verb probably defines some patttern which uses the input.
-            // For example:
-            // {
-            //     invocation: "gotar {path}"
-            //     execution: ":select {path}/target"
-            // }
-            // (or that input is useless)
-            let path_builder = ExecutionStringBuilder::with_invocation(
-                verb.invocation_parser.as_ref(),
-                SelInfo::from_path(base_path),
-                app_state,
-                Some(input_arg),
-            );
-            path_builder.path(verb_arg, con)
-        }
-        (Some(input_arg), None) => {
-            // the verb defines nothing
-            // The :select internal execution was triggered from the
-            // input (which must be a kind of alias for :select)
-            // so we do exactly what the input asks for
-            path::path_from(base_path, PathAnchor::Unspecified, input_arg)
-        }
-        (None, Some(verb_arg)) => {
-            // the verb defines the path where to go..
-            // the internal_execution specifies the path to use
-            // (it may come from a configured verb whose execution is
-            //  `:select some/path`).
-            // The given path may be relative hence the need for the
-            // state's selection
-            // (we assume a check before ensured it doesn't need an input)
-            path::path_from(base_path, PathAnchor::Unspecified, verb_arg)
-        }
-        (None, None) => {
-            // This doesn't really make sense: we're selecting the currently
-            // selected path
-            base_path.to_path_buf()
-        }
-    }
-
+    on_path(path, tree, screen, bang)
 }
 
 pub fn on_path(
diff --git a/src/verb/mod.rs b/src/verb/mod.rs
index f90d05d3..7e619035 100644
--- a/src/verb/mod.rs
+++ b/src/verb/mod.rs
@@ -8,6 +8,7 @@ mod internal;
 mod internal_execution;
 pub mod internal_focus;
 pub mod internal_select;
+pub mod internal_path;
 mod invocation_parser;
 mod sequence_execution;
 mod verb;
diff --git a/src/verb/verb_store.rs b/src/verb/verb_store.rs
index 88c72232..07b2aa91 100644
--- a/src/verb/verb_store.rs
+++ b/src/verb/verb_store.rs
@@ -290,6 +290,7 @@ impl VerbStore {
         self.add_internal(select_first);
         self.add_internal(select_last);
         self.add_internal(select);
+        self.add_internal(show);
         self.add_internal(clear_stage).with_shortcut("cls");
         self.add_internal(stage)
             .with_key(key!('+'));
diff --git a/website/docs/conf_verbs.md b/website/docs/conf_verbs.md
index deb85a48..4be4fd35 100644
--- a/website/docs/conf_verbs.md
+++ b/website/docs/conf_verbs.md
@@ -438,6 +438,7 @@ invocation | default key | default shortcut | behavior / details
 :select_last | - | - | select the last line
 :set_panel_width | - | - | ex: `:set_panel_width 1 150` sets the width of the second panel to 150 "characters"
 :set_syntax_theme | - | - | set the [syntect theme](../conf_file/#syntax-theme) of code preview, eg `:set SolarizedDark`
+:show | - | - | similar to `:select` but will add missing lines to the tree. Does nothing if the provided path isn't a descendant of the current root
 :sort_by_count | - | sc | sort by count (only one level of the tree is displayed)
 :sort_by_date | - | sd | sort by date
 :sort_by_size | - | ss | sort by size


Test patch: