intelli-shell 3.4.0

Like IntelliSense, but for shells
# --- Nushell Integration (using Reedline - Nu's Line Editor) ---
# https://www.nushell.sh/commands/docs/commandline.html
# https://www.nushell.sh/book/line_editor.html#keybindings

# Define key bindings, using defaults if environment variables are not set.
# NOTE: The format can be "modifier keycode" (e.g., "control char_b") or just a keycode for keys without modifiers.
#       See `keybindings list` for available modifiers and keycodes or `keybindings listen` to check.
let intelli_search_key = ($env.INTELLI_SEARCH_HOTKEY? | default "control space")
let intelli_bookmark_key = ($env.INTELLI_BOOKMARK_HOTKEY? | default "control char_b")
let intelli_variable_key = ($env.INTELLI_VARIABLE_HOTKEY? | default "control char_l")
let intelli_fix_key = ($env.INTELLI_FIX_HOTKEY? | default "control char_x")

# Helper function to execute intelli-shell and update the command line buffer.
def _intelli_exec [
  command: string         # The intelli-shell command to run (e.g., "search", "new")
  args: list<string>      # Arguments for the intelli-shell command
] {
  let temp_file = (mktemp -t "intelli-shell.XXXXXX")

  # Clear the buffer with ANSI escapes to be rendered immediately
  let buffer_len = (commandline | str length)
  if $buffer_len > 0 {
    print -n $"\e[($buffer_len)D\e[K"
    commandline edit --replace ""
  }

  # Run intelli-shell
  let exit_code = (try {
    intelli-shell --extra-line --skip-execution --file-output $temp_file $command ...$args
    0
  } catch {
    $env.LAST_EXIT_CODE
  })
  
  # If the output file is missing or empty, there's nothing to process (likely a crash)
  if not (($temp_file | path exists) and ($temp_file | path type) == file and (ls $temp_file | get size | first) > 0b) {
    # Panic report was likely printed, but nu will also display a new prompt automatically
    rm -f $temp_file
    return
  }

  # Read the file content and parse it
  let lines = (open $temp_file | lines)
  rm -f $temp_file
  let out_status = ($lines | get 0)
  let action = if ($lines | length) > 1 { ($lines | get 1) } else { "" }
  let command_out = if ($lines | length) > 2 { ($lines | skip 2 | str join "\n") } else { "" }
  
  # Nu always starts a new prompt, but it does so on the same line if a single line is printed to stderr
  if $out_status == "DIRTY" or $exit_code != 0 {
    # When dirty, we move the cursor one extra line down so that the prompt is not redisplayed over the stderr output
    print -n "\u{1b}[1B"
  } else if $out_status == "CLEAN" {
    # intelli-shell always leaves the cursor at the end of the prompt but nu doesn't properly redisplay it
    # Move the cursor to the beginning of the prompt so that it's properly rendered where it should be
    let prompt_output = (do $env.PROMPT_COMMAND)
    let num_lines = ($prompt_output | lines | length)
    let move_up_code = if $num_lines > 1 {
      let lines_to_move = $num_lines - 1
      $"\u{1b}[($lines_to_move)A"
    } else {
      ""
    }
    print -n $"($move_up_code)\r"
  }

  # Determine the content of the buffer
  if $action == "REPLACE" {
    commandline edit --replace $command_out
  } else if $action == "EXECUTE" {
    commandline edit --replace $command_out --accept
  }
}

# Wrapper function for the search keybinding
def --env "intelli-search" [] {
  _intelli_exec "search" ["-i", (commandline)]
}

# Wrapper function for the bookmark/save keybinding
def --env "intelli-save" [] {
  _intelli_exec "new" ["-i", (commandline)]
}

# Wrapper function for the variable replacement keybinding
def --env "intelli-variable" [] {
  _intelli_exec "replace" ["-i", (commandline)]
}

# Wrapper function for the command fixing keybinding
def --env "intelli-fix" [] {
  let hist = (history | last 5 | each { |it| $it.command } | str join "\n")
  _intelli_exec "fix" ["--history", $hist, (commandline)]
}

# Define the bindings and their corresponding commands in a list for dynamic generation
let keybinding_definitions = [
  { name: "intelli-search", binding: $intelli_search_key, command: "intelli-search" },
  { name: "intelli-bookmark", binding: $intelli_bookmark_key, command: "intelli-save" },
  { name: "intelli-variable", binding: $intelli_variable_key, command: "intelli-variable" },
  { name: "intelli-fix", binding: $intelli_fix_key, command: "intelli-fix" },
]

# Dynamically generate the final keybinding records
let intelli_keybindings = ($keybinding_definitions | each { |kb|
  let binding_parts = ($kb.binding | split row " " -n 2)
  let modifier = if ($binding_parts | length) == 2 {
    ($binding_parts | first)
  } else {
    "none"
  }
  let keycode = ($binding_parts | last)
  {
    name: $kb.name,
    modifier: $modifier,
    keycode: $keycode,
    mode: ["emacs", "vi_insert"]
    event: { 
      send: ExecuteHostCommand
      cmd: $kb.command
    }
  }
})

# Append the new keybindings to the config
$env.config.keybindings ++= $intelli_keybindings

# Bind ESC to kill the whole line if not skipped
if ($env.INTELLI_SKIP_ESC_BIND? | default "0") == "0" {
  $env.config.keybindings ++= [{
    name: "kill-line",
    modifier: "none",
    keycode: "esc",
    mode: ["emacs", "vi_insert"],
    event: [
      { edit: SelectAll }
      { edit: Delete }
    ]
  }]
}