From c93ce14fc960e35cce879fa75e11df443ba9acc1 Mon Sep 17 00:00:00 2001 From: Eric Hodel Date: Thu, 19 Oct 2023 04:35:32 -0700 Subject: [PATCH] Improved CDPATH (#644) This updated `c` command supports: * Changing to the previous directory with `c -` * Changing to absolute directories * Completion of CDPATH entries * Completion of absolute paths * Completion of children When completing CDPATH entries the parent directory is shown to provide context in case multiple CDPATH entries have the same child directory names. Fixes #244 --- sourced/filesystem/cdpath.nu | 181 ++++++++++++++++++++++++++++++----- 1 file changed, 157 insertions(+), 24 deletions(-) diff --git a/sourced/filesystem/cdpath.nu b/sourced/filesystem/cdpath.nu index bbaa78f..8abcadf 100644 --- a/sourced/filesystem/cdpath.nu +++ b/sourced/filesystem/cdpath.nu @@ -1,36 +1,169 @@ -def-env c [dir = ""] { - let default = if $nu.os-info.name == "windows" { +# You must set $env.CDPATH, try: +# +# $env.CDPATH = [ +# ".", +# "~", +# "~/path/to/repositories", +# ] +# +# The above $env.CDPATH will complete: +# * Entries under the current directory ($env.PWD) +# * Entries in your home directory ($env.HOME) +# * Entries where you check out repositories +# * Children of those entries +# +# This CDPATH implementation also completes absolute paths to help you use `c` +# instead of `cd` +# +# Bugs: +# * `c` does not complete paths that start with "~". This should support both +# directories ("~/.config/nu"), users ("~notme"), and both combined +# ("~notme/.config/nu") +module cdpath { + # $env.CDPATH with unique, expanded, existing paths + def cdpath [] { + $env.CDPATH + | path expand + | uniq + | filter {|| $in | path exists } + } + + # Children of a path + def children [path: string] { + ls -a $path + | where type == "dir" + | get name + } + + # Completion for `c` + # + # `contains` is used instead of `starts-with` to behave similar to fuzzy + # completion behavior. + # + # During completion of a CDPATH entry the description contains the parent + # directory you will complete under. This allows you to tell which entry in + # your CDPATH your are completing to if you have the same directory under + # multiple entries. + def complete [context: string] { + let context_dir = $context | parse "c {context_dir}" | get context_dir | first + let path = $context_dir | path split + let no_trailing_slash = not ($context_dir | str ends-with "/") + + # completion with no context + if ( $path | is-empty ) { + complete_from_cdpath + # Complete an entry in CDPATH + # + # This appends a / to allow continuation to the last step + } else if $no_trailing_slash and (1 == ( $path | length )) { + let first = $path | first + + complete_from_cdpath + | filter {|| $in.value | str contains $first } + | upsert value {|| $"($in.value)/" } + # Complete a child of a CDPATH entry + } else { + let prefix = if 1 == ($path | length) { + $path | first + } else { + $path | first (($path | length) - 1) | path join + } + + let last = if 1 == ($path | length) { + "" + } else { + $path | last + } + + let chosen_path = if ( $path | first) == "/" { + if $no_trailing_slash { + $prefix + } else { + $context_dir + } + } else { + cdpath + | each {|| + $in | path join $prefix + } + | filter {|| + $in | path exists + } + | first + } + + children $chosen_path + | filter {|| + $in | str contains $last + } + | each {|child| + $"($chosen_path | path join $child)/" + } + } + } + + def complete_from_cdpath [] { + cdpath + | each { |path| + children $path + | path basename + | sort + | each { |child| { value: $child, description: $path } } + } + | flatten + | uniq-by value + } + + # Change directory with $env.CDPATH + export def-env c [dir = "": string@complete] { + let span = (metadata $dir).span + let default = if $nu.os-info.name == "windows" { $env.USERPROFILE - } else { + } else { $env.HOME - } + } - let complete_dir = if $dir == "" { + let target_dir = if $dir == "" { + $default + } else if $dir == "-" { + if "OLDPWD" in $env { + $env.OLDPWD + } else { $default + } } else { - $env.CDPATH - | reduce -f "" { |$it, $acc| if $acc == "" { - let new_path = ([$it $dir] | path join) + cdpath + | reduce -f "" { |$it, $acc| + if $acc == "" { + let new_path = ([$it $dir] | path join) if ($new_path | path exists) { - $new_path + $new_path } else { - "" + "" } - } else { $acc }} - } - - let complete_dir = if $complete_dir == "" { - error make --unspanned {msg: "No such path"} - } else if (($complete_dir | path expand | path type) != "dir") { - error make --unspanned {msg: "Not a directory"} - } else { - ($complete_dir | path expand) + } else { + $acc + } + } } - cd $complete_dir + let target_dir = if $target_dir == "" { + let cdpath = $env.CDPATH | str join ", " + + error make { + msg: $"No such child under: ($cdpath)", + label: { + text: "Child directory", + start: $span.start, + end: $span.end, + } + } + } else { + $target_dir + } + + cd $target_dir + } } -# You need to have $env.CDPATH variable declared, my suggestion from config.nu: -# $env.CDPATH = [".", $env.HOME, "/", ([$env.HOME, ".config"] | path join)] -# WINDOWS: -# $env.CDPATH = ["", $env.USERPROFILE, ([$env.USERPROFILE, "AppData\\Roaming\\"] | path join)] +use cdpath c