diff --git a/shell/bash-preexec.sh b/shell/bash-preexec.sh new file mode 100644 index 0000000..9dc3540 --- /dev/null +++ b/shell/bash-preexec.sh @@ -0,0 +1,380 @@ +# bash-preexec.sh -- Bash support for ZSH-like 'preexec' and 'precmd' functions. +# https://github.com/rcaloras/bash-preexec +# +# +# 'preexec' functions are executed before each interactive command is +# executed, with the interactive command as its argument. The 'precmd' +# function is executed before each prompt is displayed. +# +# Author: Ryan Caloras (ryan@bashhub.com) +# Forked from Original Author: Glyph Lefkowitz +# +# V0.5.0 +# + +# General Usage: +# +# 1. Source this file at the end of your bash profile so as not to interfere +# with anything else that's using PROMPT_COMMAND. +# +# 2. Add any precmd or preexec functions by appending them to their arrays: +# e.g. +# precmd_functions+=(my_precmd_function) +# precmd_functions+=(some_other_precmd_function) +# +# preexec_functions+=(my_preexec_function) +# +# 3. Consider changing anything using the DEBUG trap or PROMPT_COMMAND +# to use preexec and precmd instead. Preexisting usages will be +# preserved, but doing so manually may be less surprising. +# +# Note: This module requires two Bash features which you must not otherwise be +# using: the "DEBUG" trap, and the "PROMPT_COMMAND" variable. If you override +# either of these after bash-preexec has been installed it will most likely break. + +# Tell shellcheck what kind of file this is. +# shellcheck shell=bash + +# Make sure this is bash that's running and return otherwise. +# Use POSIX syntax for this line: +if [ -z "${BASH_VERSION-}" ]; then + return 1; +fi + +# We only support Bash 3.1+. +# Note: BASH_VERSINFO is first available in Bash-2.0. +if [[ -z "${BASH_VERSINFO-}" ]] || (( BASH_VERSINFO[0] < 3 || (BASH_VERSINFO[0] == 3 && BASH_VERSINFO[1] < 1) )); then + return 1 +fi + +# Avoid duplicate inclusion +if [[ -n "${bash_preexec_imported:-}" ]]; then + return 0 +fi +bash_preexec_imported="defined" + +# WARNING: This variable is no longer used and should not be relied upon. +# Use ${bash_preexec_imported} instead. +# shellcheck disable=SC2034 +__bp_imported="${bash_preexec_imported}" + +# Should be available to each precmd and preexec +# functions, should they want it. $? and $_ are available as $? and $_, but +# $PIPESTATUS is available only in a copy, $BP_PIPESTATUS. +# TODO: Figure out how to restore PIPESTATUS before each precmd or preexec +# function. +__bp_last_ret_value="$?" +BP_PIPESTATUS=("${PIPESTATUS[@]}") +__bp_last_argument_prev_command="$_" + +__bp_inside_precmd=0 +__bp_inside_preexec=0 + +# Initial PROMPT_COMMAND string that is removed from PROMPT_COMMAND post __bp_install +__bp_install_string=$'__bp_trap_string="$(trap -p DEBUG)"\ntrap - DEBUG\n__bp_install' + +# Fails if any of the given variables are readonly +# Reference https://stackoverflow.com/a/4441178 +__bp_require_not_readonly() { + local var + for var; do + if ! ( unset "$var" 2> /dev/null ); then + echo "bash-preexec requires write access to ${var}" >&2 + return 1 + fi + done +} + +# Remove ignorespace and or replace ignoreboth from HISTCONTROL +# so we can accurately invoke preexec with a command from our +# history even if it starts with a space. +__bp_adjust_histcontrol() { + local histcontrol + histcontrol="${HISTCONTROL:-}" + histcontrol="${histcontrol//ignorespace}" + # Replace ignoreboth with ignoredups + if [[ "$histcontrol" == *"ignoreboth"* ]]; then + histcontrol="ignoredups:${histcontrol//ignoreboth}" + fi; + export HISTCONTROL="$histcontrol" +} + +# This variable describes whether we are currently in "interactive mode"; +# i.e. whether this shell has just executed a prompt and is waiting for user +# input. It documents whether the current command invoked by the trace hook is +# run interactively by the user; it's set immediately after the prompt hook, +# and unset as soon as the trace hook is run. +__bp_preexec_interactive_mode="" + +# These arrays are used to add functions to be run before, or after, prompts. +declare -a precmd_functions +declare -a preexec_functions + +# Trims leading and trailing whitespace from $2 and writes it to the variable +# name passed as $1 +__bp_trim_whitespace() { + local var=${1:?} text=${2:-} + text="${text#"${text%%[![:space:]]*}"}" # remove leading whitespace characters + text="${text%"${text##*[![:space:]]}"}" # remove trailing whitespace characters + printf -v "$var" '%s' "$text" +} + + +# Trims whitespace and removes any leading or trailing semicolons from $2 and +# writes the resulting string to the variable name passed as $1. Used for +# manipulating substrings in PROMPT_COMMAND +__bp_sanitize_string() { + local var=${1:?} text=${2:-} sanitized + __bp_trim_whitespace sanitized "$text" + sanitized=${sanitized%;} + sanitized=${sanitized#;} + __bp_trim_whitespace sanitized "$sanitized" + printf -v "$var" '%s' "$sanitized" +} + +# This function is installed as part of the PROMPT_COMMAND; +# It sets a variable to indicate that the prompt was just displayed, +# to allow the DEBUG trap to know that the next command is likely interactive. +__bp_interactive_mode() { + __bp_preexec_interactive_mode="on"; +} + + +# This function is installed as part of the PROMPT_COMMAND. +# It will invoke any functions defined in the precmd_functions array. +__bp_precmd_invoke_cmd() { + # Save the returned value from our last command, and from each process in + # its pipeline. Note: this MUST be the first thing done in this function. + # BP_PIPESTATUS may be unused, ignore + # shellcheck disable=SC2034 + + __bp_last_ret_value="$?" BP_PIPESTATUS=("${PIPESTATUS[@]}") + + # Don't invoke precmds if we are inside an execution of an "original + # prompt command" by another precmd execution loop. This avoids infinite + # recursion. + if (( __bp_inside_precmd > 0 )); then + return + fi + local __bp_inside_precmd=1 + + # Invoke every function defined in our function array. + local precmd_function + for precmd_function in "${precmd_functions[@]}"; do + + # Only execute this function if it actually exists. + # Test existence of functions with: declare -[Ff] + if type -t "$precmd_function" 1>/dev/null; then + __bp_set_ret_value "$__bp_last_ret_value" "$__bp_last_argument_prev_command" + # Quote our function invocation to prevent issues with IFS + "$precmd_function" + fi + done + + __bp_set_ret_value "$__bp_last_ret_value" +} + +# Sets a return value in $?. We may want to get access to the $? variable in our +# precmd functions. This is available for instance in zsh. We can simulate it in bash +# by setting the value here. +__bp_set_ret_value() { + return ${1:+"$1"} +} + +__bp_in_prompt_command() { + + local prompt_command_array IFS=$'\n;' + read -rd '' -a prompt_command_array <<< "${PROMPT_COMMAND[*]:-}" + + local trimmed_arg + __bp_trim_whitespace trimmed_arg "${1:-}" + + local command trimmed_command + for command in "${prompt_command_array[@]:-}"; do + __bp_trim_whitespace trimmed_command "$command" + if [[ "$trimmed_command" == "$trimmed_arg" ]]; then + return 0 + fi + done + + return 1 +} + +# This function is installed as the DEBUG trap. It is invoked before each +# interactive prompt display. Its purpose is to inspect the current +# environment to attempt to detect if the current command is being invoked +# interactively, and invoke 'preexec' if so. +__bp_preexec_invoke_exec() { + + # Save the contents of $_ so that it can be restored later on. + # https://stackoverflow.com/questions/40944532/bash-preserve-in-a-debug-trap#40944702 + __bp_last_argument_prev_command="${1:-}" + # Don't invoke preexecs if we are inside of another preexec. + if (( __bp_inside_preexec > 0 )); then + return + fi + local __bp_inside_preexec=1 + + # Checks if the file descriptor is not standard out (i.e. '1') + # __bp_delay_install checks if we're in test. Needed for bats to run. + # Prevents preexec from being invoked for functions in PS1 + if [[ ! -t 1 && -z "${__bp_delay_install:-}" ]]; then + return + fi + + if [[ -n "${COMP_LINE:-}" ]]; then + # We're in the middle of a completer. This obviously can't be + # an interactively issued command. + return + fi + if [[ -z "${__bp_preexec_interactive_mode:-}" ]]; then + # We're doing something related to displaying the prompt. Let the + # prompt set the title instead of me. + return + else + # If we're in a subshell, then the prompt won't be re-displayed to put + # us back into interactive mode, so let's not set the variable back. + # In other words, if you have a subshell like + # (sleep 1; sleep 2) + # You want to see the 'sleep 2' as a set_command_title as well. + if [[ 0 -eq "${BASH_SUBSHELL:-}" ]]; then + __bp_preexec_interactive_mode="" + fi + fi + + if __bp_in_prompt_command "${BASH_COMMAND:-}"; then + # If we're executing something inside our prompt_command then we don't + # want to call preexec. Bash prior to 3.1 can't detect this at all :/ + __bp_preexec_interactive_mode="" + return + fi + + local this_command + this_command=$( + export LC_ALL=C + HISTTIMEFORMAT='' builtin history 1 | sed '1 s/^ *[0-9][0-9]*[* ] //' + ) + + # Sanity check to make sure we have something to invoke our function with. + if [[ -z "$this_command" ]]; then + return + fi + + # Invoke every function defined in our function array. + local preexec_function + local preexec_function_ret_value + local preexec_ret_value=0 + for preexec_function in "${preexec_functions[@]:-}"; do + + # Only execute each function if it actually exists. + # Test existence of function with: declare -[fF] + if type -t "$preexec_function" 1>/dev/null; then + __bp_set_ret_value "${__bp_last_ret_value:-}" + # Quote our function invocation to prevent issues with IFS + "$preexec_function" "$this_command" + preexec_function_ret_value="$?" + if [[ "$preexec_function_ret_value" != 0 ]]; then + preexec_ret_value="$preexec_function_ret_value" + fi + fi + done + + # Restore the last argument of the last executed command, and set the return + # value of the DEBUG trap to be the return code of the last preexec function + # to return an error. + # If `extdebug` is enabled a non-zero return value from any preexec function + # will cause the user's command not to execute. + # Run `shopt -s extdebug` to enable + __bp_set_ret_value "$preexec_ret_value" "$__bp_last_argument_prev_command" +} + +__bp_install() { + # Exit if we already have this installed. + if [[ "${PROMPT_COMMAND[*]:-}" == *"__bp_precmd_invoke_cmd"* ]]; then + return 1; + fi + + trap '__bp_preexec_invoke_exec "$_"' DEBUG + + # Preserve any prior DEBUG trap as a preexec function + local prior_trap + # we can't easily do this with variable expansion. Leaving as sed command. + # shellcheck disable=SC2001 + prior_trap=$(sed "s/[^']*'\(.*\)'[^']*/\1/" <<<"${__bp_trap_string:-}") + unset __bp_trap_string + if [[ -n "$prior_trap" ]]; then + eval '__bp_original_debug_trap() { + '"$prior_trap"' + }' + preexec_functions+=(__bp_original_debug_trap) + fi + + # Adjust our HISTCONTROL Variable if needed. + __bp_adjust_histcontrol + + # Issue #25. Setting debug trap for subshells causes sessions to exit for + # backgrounded subshell commands (e.g. (pwd)& ). Believe this is a bug in Bash. + # + # Disabling this by default. It can be enabled by setting this variable. + if [[ -n "${__bp_enable_subshells:-}" ]]; then + + # Set so debug trap will work be invoked in subshells. + set -o functrace > /dev/null 2>&1 + shopt -s extdebug > /dev/null 2>&1 + fi; + + local existing_prompt_command + # Remove setting our trap install string and sanitize the existing prompt command string + existing_prompt_command="${PROMPT_COMMAND:-}" + # Edge case of appending to PROMPT_COMMAND + existing_prompt_command="${existing_prompt_command//$__bp_install_string/:}" # no-op + existing_prompt_command="${existing_prompt_command//$'\n':$'\n'/$'\n'}" # remove known-token only + existing_prompt_command="${existing_prompt_command//$'\n':;/$'\n'}" # remove known-token only + __bp_sanitize_string existing_prompt_command "$existing_prompt_command" + if [[ "${existing_prompt_command:-:}" == ":" ]]; then + existing_prompt_command= + fi + + # Install our hooks in PROMPT_COMMAND to allow our trap to know when we've + # actually entered something. + PROMPT_COMMAND='__bp_precmd_invoke_cmd' + PROMPT_COMMAND+=${existing_prompt_command:+$'\n'$existing_prompt_command} + if (( BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 1) )); then + PROMPT_COMMAND+=('__bp_interactive_mode') + else + # shellcheck disable=SC2179 # PROMPT_COMMAND is not an array in bash <= 5.0 + PROMPT_COMMAND+=$'\n__bp_interactive_mode' + fi + + # Add two functions to our arrays for convenience + # of definition. + precmd_functions+=(precmd) + preexec_functions+=(preexec) + + # Invoke our two functions manually that were added to $PROMPT_COMMAND + __bp_precmd_invoke_cmd + __bp_interactive_mode +} + +# Sets an installation string as part of our PROMPT_COMMAND to install +# after our session has started. This allows bash-preexec to be included +# at any point in our bash profile. +__bp_install_after_session_init() { + # bash-preexec needs to modify these variables in order to work correctly + # if it can't, just stop the installation + __bp_require_not_readonly PROMPT_COMMAND HISTCONTROL HISTTIMEFORMAT || return + + local sanitized_prompt_command + __bp_sanitize_string sanitized_prompt_command "${PROMPT_COMMAND:-}" + if [[ -n "$sanitized_prompt_command" ]]; then + # shellcheck disable=SC2178 # PROMPT_COMMAND is not an array in bash <= 5.0 + PROMPT_COMMAND=${sanitized_prompt_command}$'\n' + fi; + # shellcheck disable=SC2179 # PROMPT_COMMAND is not an array in bash <= 5.0 + PROMPT_COMMAND+=${__bp_install_string} +} + +# Run our install so long as we're not delaying it. +if [[ -z "${__bp_delay_install:-}" ]]; then + __bp_install_after_session_init +fi; \ No newline at end of file diff --git a/shell/shellIntegration-rc.zsh b/shell/shellIntegration-rc.zsh index 20dc91d..90f1eca 100644 --- a/shell/shellIntegration-rc.zsh +++ b/shell/shellIntegration-rc.zsh @@ -11,9 +11,42 @@ __is_prompt_end() { builtin printf '\e]6973;PE\a' } +__is_escape_value() { + builtin emulate -L zsh + + # Process text byte by byte, not by codepoint. + builtin local LC_ALL=C str="$1" i byte token out='' + + for (( i = 0; i < ${#str}; ++i )); do + byte="${str:$i:1}" + + # Escape backslashes and semi-colons + if [ "$byte" = "\\" ]; then + token="\\\\" + elif [ "$byte" = ";" ]; then + token="\\x3b" + else + token="$byte" + fi + + out+="$token" + done + + builtin print -r "$out" +} + +__is_update_cwd() { + builtin printf '\e]6973;CWD;%s\a' "$(__vsc_escape_value "${PWD}")" +} + __is_update_prompt() { __is_prior_prompt="$PS1" PS1="%{$(__is_prompt_start)%}$PS1%{$(__is_prompt_end)%}" } -__is_update_prompt \ No newline at end of file +__is_precmd() { + __is_update_cwd +} + +__is_update_prompt +add-zsh-hook precmd __is_precmd \ No newline at end of file diff --git a/shell/shellIntegration.bash b/shell/shellIntegration.bash index 1aa3b1e..9a88bc2 100644 --- a/shell/shellIntegration.bash +++ b/shell/shellIntegration.bash @@ -12,6 +12,10 @@ elif [ -r ~/.profile ]; then . ~/.profile fi +if [ -r ~/.inshellisense/bash-preexec.sh ]; then + . ~/.inshellisense/bash-preexec.sh +fi + __is_prompt_start() { builtin printf '\e]6973;PS\a' } @@ -20,6 +24,36 @@ __is_prompt_end() { builtin printf '\e]6973;PE\a' } +__is_escape_value() { + # Process text byte by byte, not by codepoint. + builtin local LC_ALL=C str="${1}" i byte token out='' + + for (( i=0; i < "${#str}"; ++i )); do + byte="${str:$i:1}" + + # Escape backslashes and semi-colons + if [ "$byte" = "\\" ]; then + token="\\\\" + elif [ "$byte" = ";" ]; then + token="\\x3b" + else + token="$byte" + fi + + out+="$token" + done + + builtin printf '%s\n' "${out}" +} + +__is_update_cwd() { + builtin printf '\e]6973;CWD;%s\a' "$(__is_escape_value "$PWD")" +} + +if [[ -n "${bash_preexec_imported:-}" ]]; then + precmd_functions+=(__is_update_cwd) +fi + __is_update_prompt() { if [[ "$__is_custom_PS1" == "" || "$__is_custom_PS1" != "$PS1" ]]; then __is_original_PS1=$PS1 diff --git a/shell/shellIntegration.fish b/shell/shellIntegration.fish index 13bf8d7..1fcb9bc 100644 --- a/shell/shellIntegration.fish +++ b/shell/shellIntegration.fish @@ -2,5 +2,13 @@ function __is_copy_function; functions $argv[1] | sed "s/^function $argv[1]/func function __is_prompt_start; printf '\e]6973;PS\a'; end function __is_prompt_end; printf '\e]6973;PE\a'; end +function __is_escape_value + echo $argv \ + | string replace --all '\\' '\\\\' \ + | string replace --all ';' '\\x3b' \ + ; +end +function __is_update_cwd --on-event fish_prompt; set __is_cwd (__is_escape_value "$PWD"); printf "\e]6973;CWD;$__is_cwd\a"; end + __is_copy_function fish_prompt is_user_prompt function fish_prompt; printf (__is_prompt_start); printf (is_user_prompt); printf (__is_prompt_end); end \ No newline at end of file diff --git a/shell/shellIntegration.ps1 b/shell/shellIntegration.ps1 index e267f52..ab2e4fb 100644 --- a/shell/shellIntegration.ps1 +++ b/shell/shellIntegration.ps1 @@ -1,8 +1,17 @@ $Global:__IsOriginalPrompt = $function:Prompt +function Global:__IS-Escape-Value([string]$value) { + [regex]::Replace($value, '[\\\n;]', { param($match) + -Join ( + [System.Text.Encoding]::UTF8.GetBytes($match.Value) | ForEach-Object { '\x{0:x2}' -f $_ } + ) + }) +} + function Global:Prompt() { $Result = "$([char]0x1b)]6973;PS`a" $Result += $Global:__IsOriginalPrompt.Invoke() $Result += "$([char]0x1b)]6973;PE`a" + $Result += if ($pwd.Provider.Name -eq 'FileSystem') { "$([char]0x1b)]6973;CWD;$(__IS-Escape-Value $pwd.ProviderPath)`a" } return $Result } \ No newline at end of file diff --git a/src/commands/complete.ts b/src/commands/complete.ts index d68f5f1..c322468 100644 --- a/src/commands/complete.ts +++ b/src/commands/complete.ts @@ -5,7 +5,7 @@ import { Command } from "commander"; import { getSuggestions } from "../runtime/runtime.js"; const action = async (input: string) => { - const suggestions = await getSuggestions(input); + const suggestions = await getSuggestions(input, process.cwd()); process.stdout.write(JSON.stringify(suggestions)); }; diff --git a/src/commands/root.ts b/src/commands/root.ts index ab622c8..0acf5aa 100644 --- a/src/commands/root.ts +++ b/src/commands/root.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import { render, renderConfirmation } from "../ui/ui-root.js"; -import { Shell, supportedShells as shells, setupZshDotfiles } from "../utils/shell.js"; +import { Shell, supportedShells as shells, setupZshDotfiles, setupBashPreExec } from "../utils/shell.js"; import { inferShell } from "../utils/shell.js"; import { loadConfig } from "../utils/config.js"; import { Command } from "commander"; @@ -36,6 +36,8 @@ export const action = (program: Command) => async (options: RootCommandOptions) } if (shell == Shell.Zsh) { await setupZshDotfiles(); + } else if (shell == Shell.Bash) { + await setupBashPreExec(); } await render(shell); }; diff --git a/src/isterm/pty.ts b/src/isterm/pty.ts index 1ec10d0..fdacb8d 100644 --- a/src/isterm/pty.ts +++ b/src/isterm/pty.ts @@ -36,11 +36,13 @@ export class ISTerm implements IPty { readonly onData: IEvent; readonly onExit: IEvent<{ exitCode: number; signal?: number }>; shellBuffer?: string; + cwd: string = ""; readonly #pty: IPty; readonly #ptyEmitter: EventEmitter; readonly #term: xterm.Terminal; readonly #commandManager: CommandManager; + readonly #shell: Shell; constructor({ shell, cols, rows, env, shellTarget, shellArgs }: ISTermOptions & { shellTarget: string }) { this.#pty = pty.spawn(shellTarget, shellArgs ?? [], { @@ -58,6 +60,7 @@ export class ISTerm implements IPty { this.#term = new xterm.Terminal({ allowProposedApi: true, rows, cols }); this.#term.parser.registerOscHandler(IsTermOscPs, (data) => this._handleIsSequence(data)); this.#commandManager = new CommandManager(this.#term, shell); + this.#shell = shell; this.#ptyEmitter = new EventEmitter(); this.#pty.onData((data) => { @@ -77,6 +80,25 @@ export class ISTerm implements IPty { this.onExit = this.#pty.onExit; } + private _deserializeIsMessage(message: string): string { + return message.replaceAll(/\\(\\|x([0-9a-f]{2}))/gi, (_match: string, op: string, hex?: string) => (hex ? String.fromCharCode(parseInt(hex, 16)) : op)); + } + + private _sanitizedCwd(cwd: string): string { + if (cwd.match(/^['"].*['"]$/)) { + cwd = cwd.substring(1, cwd.length - 1); + } + // Convert a drive prefix to windows style when using Git Bash + if (os.platform() === "win32" && this.#shell == Shell.Bash && cwd && cwd.match(/^\/[A-z]{1}\//)) { + cwd = `${cwd[1]}:\\` + cwd.substring(3, cwd.length); + } + // Make the drive letter uppercase on Windows (see vscode #9448) + if (os.platform() === "win32" && cwd && cwd[1] === ":") { + return cwd[0].toUpperCase() + cwd.substring(1); + } + return cwd; + } + private _handleIsSequence(data: string): boolean { const argsIndex = data.indexOf(";"); const sequence = argsIndex === -1 ? data : data.substring(0, argsIndex); @@ -87,6 +109,13 @@ export class ISTerm implements IPty { case IstermOscPt.PromptEnded: this.#commandManager.handlePromptEnd(); break; + case IstermOscPt.CurrentWorkingDirectory: { + const cwd = data.split(";").at(1); + if (cwd != null) { + this.cwd = this._sanitizedCwd(this._deserializeIsMessage(cwd)); + } + break; + } default: return false; } diff --git a/src/runtime/generator.ts b/src/runtime/generator.ts index 96ca02b..6baf43c 100644 --- a/src/runtime/generator.ts +++ b/src/runtime/generator.ts @@ -4,10 +4,10 @@ import { runTemplates } from "./template.js"; import { buildExecuteShellCommand } from "./utils.js"; -const getGeneratorContext = (): Fig.GeneratorContext => { +const getGeneratorContext = (cwd: string): Fig.GeneratorContext => { return { environmentVariables: Object.fromEntries(Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] != null)), - currentWorkingDirectory: process.cwd(), + currentWorkingDirectory: cwd, currentProcess: "", // TODO: define current process sshPrefix: "", // deprecated, should be empty isDangerous: false, @@ -16,7 +16,7 @@ const getGeneratorContext = (): Fig.GeneratorContext => { }; // TODO: add support for caching, trigger, & getQueryTerm -export const runGenerator = async (generator: Fig.Generator, tokens: string[]): Promise => { +export const runGenerator = async (generator: Fig.Generator, tokens: string[], cwd: string): Promise => { const { script, postProcess, scriptTimeout, splitOn, custom, template } = generator; const executeShellCommand = buildExecuteShellCommand(scriptTimeout ?? 5000); @@ -32,11 +32,11 @@ export const runGenerator = async (generator: Fig.Generator, tokens: string[]): } if (custom) { - suggestions.push(...(await custom(tokens, executeShellCommand, getGeneratorContext()))); + suggestions.push(...(await custom(tokens, executeShellCommand, getGeneratorContext(cwd)))); } if (template != null) { - suggestions.push(...(await runTemplates(template))); + suggestions.push(...(await runTemplates(template, cwd))); } return suggestions; } catch (e) { diff --git a/src/runtime/runtime.ts b/src/runtime/runtime.ts index 5770269..1b757b0 100644 --- a/src/runtime/runtime.ts +++ b/src/runtime/runtime.ts @@ -55,7 +55,7 @@ const lazyLoadSpecLocation = async (location: Fig.SpecLocation): Promise => { +export const getSuggestions = async (cmd: string, cwd: string): Promise => { const activeCmd = parseCommand(cmd); const rootToken = activeCmd.at(0); if (activeCmd.length === 0 || !rootToken?.complete) { @@ -67,7 +67,7 @@ export const getSuggestions = async (cmd: string): Promise => { @@ -172,26 +173,27 @@ const runOption = async ( const isPersistent = persistentOptions.some((o) => (typeof o.name === "string" ? o.name === activeToken.token : o.name.includes(activeToken.token))); if ((option.args instanceof Array && option.args.length > 0) || option.args != null) { const args = option.args instanceof Array ? option.args : [option.args]; - return runArg(tokens.slice(1), args, subcommand, persistentOptions, acceptedTokens.concat(activeToken), true, false); + return runArg(tokens.slice(1), args, subcommand, cwd, persistentOptions, acceptedTokens.concat(activeToken), true, false); } - return runSubcommand(tokens.slice(1), subcommand, persistentOptions, acceptedTokens.concat({ ...activeToken, isPersistent })); + return runSubcommand(tokens.slice(1), subcommand, cwd, persistentOptions, acceptedTokens.concat({ ...activeToken, isPersistent })); }; const runArg = async ( tokens: CommandToken[], args: Fig.Arg[], subcommand: Fig.Subcommand, + cwd: string, persistentOptions: Fig.Option[], acceptedTokens: CommandToken[], fromOption: boolean, fromVariadic: boolean, ): Promise => { if (args.length === 0) { - return runSubcommand(tokens, subcommand, persistentOptions, acceptedTokens, true, !fromOption); + return runSubcommand(tokens, subcommand, cwd, persistentOptions, acceptedTokens, true, !fromOption); } else if (tokens.length === 0) { - return await getArgDrivenRecommendation(args, subcommand, persistentOptions, undefined, acceptedTokens, fromVariadic); + return await getArgDrivenRecommendation(args, subcommand, persistentOptions, undefined, acceptedTokens, fromVariadic, cwd); } else if (!tokens.at(0)?.complete) { - return await getArgDrivenRecommendation(args, subcommand, persistentOptions, tokens[0].token, acceptedTokens, fromVariadic); + return await getArgDrivenRecommendation(args, subcommand, persistentOptions, tokens[0].token, acceptedTokens, fromVariadic, cwd); } const activeToken = tokens[0]; @@ -199,20 +201,20 @@ const runArg = async ( if (activeToken.isOption) { const option = getOption(activeToken, persistentOptions.concat(subcommand.options ?? [])); if (option != null) { - return runOption(tokens, option, subcommand, persistentOptions, acceptedTokens); + return runOption(tokens, option, subcommand, cwd, persistentOptions, acceptedTokens); } return; } const nextSubcommand = await genSubcommand(activeToken.token, subcommand); if (nextSubcommand != null) { - return runSubcommand(tokens.slice(1), nextSubcommand, persistentOptions, getPersistentTokens(acceptedTokens.concat(activeToken))); + return runSubcommand(tokens.slice(1), nextSubcommand, cwd, persistentOptions, getPersistentTokens(acceptedTokens.concat(activeToken))); } } const activeArg = args[0]; if (activeArg.isVariadic) { - return runArg(tokens.slice(1), args, subcommand, persistentOptions, acceptedTokens.concat(activeToken), fromOption, true); + return runArg(tokens.slice(1), args, subcommand, cwd, persistentOptions, acceptedTokens.concat(activeToken), fromOption, true); } else if (activeArg.isCommand) { if (tokens.length <= 0) { return; @@ -221,23 +223,24 @@ const runArg = async ( if (spec == null) return; const subcommand = getSubcommand(spec); if (subcommand == null) return; - return runSubcommand(tokens.slice(1), subcommand); + return runSubcommand(tokens.slice(1), subcommand, cwd); } - return runArg(tokens.slice(1), args.slice(1), subcommand, persistentOptions, acceptedTokens.concat(activeToken), fromOption, false); + return runArg(tokens.slice(1), args.slice(1), subcommand, cwd, persistentOptions, acceptedTokens.concat(activeToken), fromOption, false); }; const runSubcommand = async ( tokens: CommandToken[], subcommand: Fig.Subcommand, + cwd: string, persistentOptions: Fig.Option[] = [], acceptedTokens: CommandToken[] = [], argsDepleted = false, argsUsed = false, ): Promise => { if (tokens.length === 0) { - return getSubcommandDrivenRecommendation(subcommand, persistentOptions, undefined, argsDepleted, argsUsed, acceptedTokens); + return getSubcommandDrivenRecommendation(subcommand, persistentOptions, undefined, argsDepleted, argsUsed, acceptedTokens, cwd); } else if (!tokens.at(0)?.complete) { - return getSubcommandDrivenRecommendation(subcommand, persistentOptions, tokens[0].token, argsDepleted, argsUsed, acceptedTokens); + return getSubcommandDrivenRecommendation(subcommand, persistentOptions, tokens[0].token, argsDepleted, argsUsed, acceptedTokens, cwd); } const activeToken = tokens[0]; @@ -247,7 +250,7 @@ const runSubcommand = async ( if (activeToken.isOption) { const option = getOption(activeToken, allOptions); if (option != null) { - return runOption(tokens, option, subcommand, persistentOptions, acceptedTokens); + return runOption(tokens, option, subcommand, cwd, persistentOptions, acceptedTokens); } return; } @@ -257,6 +260,7 @@ const runSubcommand = async ( return runSubcommand( tokens.slice(1), nextSubcommand, + cwd, getPersistentOptions(persistentOptions, subcommand.options), getPersistentTokens(acceptedTokens.concat(activeToken)), ); @@ -268,8 +272,8 @@ const runSubcommand = async ( const args = getArgs(subcommand.args); if (args.length != 0) { - return runArg(tokens, args, subcommand, allOptions, acceptedTokens, false, false); + return runArg(tokens, args, subcommand, cwd, allOptions, acceptedTokens, false, false); } // if the subcommand has no args specified, fallback to the subcommand and ignore this item - return runSubcommand(tokens.slice(1), subcommand, persistentOptions, acceptedTokens.concat(activeToken)); + return runSubcommand(tokens.slice(1), subcommand, cwd, persistentOptions, acceptedTokens.concat(activeToken)); }; diff --git a/src/runtime/suggestion.ts b/src/runtime/suggestion.ts index 5ee2d90..6fb750e 100644 --- a/src/runtime/suggestion.ts +++ b/src/runtime/suggestion.ts @@ -133,10 +133,11 @@ const generatorSuggestions = async ( acceptedTokens: CommandToken[], filterStrategy: FilterStrategy | undefined, partialCmd: string | undefined, + cwd: string, ): Promise => { const generators = generator instanceof Array ? generator : generator ? [generator] : []; const tokens = acceptedTokens.map((t) => t.token); - const suggestions = (await Promise.all(generators.map((gen) => runGenerator(gen, tokens)))).flat(); + const suggestions = (await Promise.all(generators.map((gen) => runGenerator(gen, tokens, cwd)))).flat(); return filter(suggestions, filterStrategy, partialCmd, undefined); }; @@ -144,8 +145,9 @@ const templateSuggestions = async ( templates: Fig.Template | undefined, filterStrategy: FilterStrategy | undefined, partialCmd: string | undefined, + cwd: string, ): Promise => { - return filter(await runTemplates(templates ?? []), filterStrategy, partialCmd, undefined); + return filter(await runTemplates(templates ?? [], cwd), filterStrategy, partialCmd, undefined); }; const suggestionSuggestions = ( @@ -192,6 +194,7 @@ export const getSubcommandDrivenRecommendation = async ( argsDepleted: boolean, argsFromSubcommand: boolean, acceptedTokens: CommandToken[], + cwd: string, ): Promise => { if (argsDepleted && argsFromSubcommand) { return; @@ -206,9 +209,9 @@ export const getSubcommandDrivenRecommendation = async ( } if (argLength != 0) { const activeArg = subcommand.args instanceof Array ? subcommand.args[0] : subcommand.args; - suggestions.push(...(await generatorSuggestions(activeArg?.generators, acceptedTokens, activeArg?.filterStrategy, partialCmd))); + suggestions.push(...(await generatorSuggestions(activeArg?.generators, acceptedTokens, activeArg?.filterStrategy, partialCmd, cwd))); suggestions.push(...suggestionSuggestions(activeArg?.suggestions, activeArg?.filterStrategy, partialCmd)); - suggestions.push(...(await templateSuggestions(activeArg?.template, activeArg?.filterStrategy, partialCmd))); + suggestions.push(...(await templateSuggestions(activeArg?.template, activeArg?.filterStrategy, partialCmd, cwd))); } return { @@ -228,13 +231,14 @@ export const getArgDrivenRecommendation = async ( partialCmd: string | undefined, acceptedTokens: CommandToken[], variadicArgBound: boolean, + cwd: string, ): Promise => { const activeArg = args[0]; const allOptions = persistentOptions.concat(subcommand.options ?? []); const suggestions = [ - ...(await generatorSuggestions(args[0].generators, acceptedTokens, activeArg?.filterStrategy, partialCmd)), + ...(await generatorSuggestions(args[0].generators, acceptedTokens, activeArg?.filterStrategy, partialCmd, cwd)), ...suggestionSuggestions(args[0].suggestions, activeArg?.filterStrategy, partialCmd), - ...(await templateSuggestions(args[0].template, activeArg?.filterStrategy, partialCmd)), + ...(await templateSuggestions(args[0].template, activeArg?.filterStrategy, partialCmd, cwd)), ]; if (activeArg.isOptional || (activeArg.isVariadic && variadicArgBound)) { diff --git a/src/runtime/template.ts b/src/runtime/template.ts index c2e6af3..96cb0fa 100644 --- a/src/runtime/template.ts +++ b/src/runtime/template.ts @@ -2,15 +2,14 @@ // Licensed under the MIT License. import fsAsync from "node:fs/promises"; -import process from "node:process"; -const filepathsTemplate = async (): Promise => { - const files = await fsAsync.readdir(process.cwd(), { withFileTypes: true }); +const filepathsTemplate = async (cwd: string): Promise => { + const files = await fsAsync.readdir(cwd, { withFileTypes: true }); return files.filter((f) => f.isFile() || f.isDirectory()).map((f) => ({ name: f.name, priority: 90 })); }; -const foldersTemplate = async (): Promise => { - const files = await fsAsync.readdir(process.cwd(), { withFileTypes: true }); +const foldersTemplate = async (cwd: string): Promise => { + const files = await fsAsync.readdir(cwd, { withFileTypes: true }); return files.filter((f) => f.isDirectory()).map((f) => ({ name: f.name, priority: 90 })); }; @@ -24,16 +23,16 @@ const helpTemplate = (): Fig.Suggestion[] => { return []; }; -export const runTemplates = async (template: Fig.TemplateStrings[] | Fig.Template): Promise => { +export const runTemplates = async (template: Fig.TemplateStrings[] | Fig.Template, cwd: string): Promise => { const templates = template instanceof Array ? template : [template]; return ( await Promise.all( templates.map(async (t) => { switch (t) { case "filepaths": - return await filepathsTemplate(); + return await filepathsTemplate(cwd); case "folders": - return await foldersTemplate(); + return await foldersTemplate(cwd); case "history": return historyTemplate(); case "help": diff --git a/src/tests/runtime/runtime.test.ts b/src/tests/runtime/runtime.test.ts index 49a12bb..9d4cc85 100644 --- a/src/tests/runtime/runtime.test.ts +++ b/src/tests/runtime/runtime.test.ts @@ -26,7 +26,7 @@ describe(`parseCommand`, () => { testData.forEach(({ command, name, skip, maxSuggestions }) => { if (skip) return; test(name, async () => { - const suggestions = await getSuggestions(command); + const suggestions = await getSuggestions(command, process.cwd()); if (suggestions != null && suggestions.suggestions != null) { suggestions.suggestions = suggestions?.suggestions.slice(0, maxSuggestions); } diff --git a/src/ui/suggestionManager.ts b/src/ui/suggestionManager.ts index 8c0b8f9..e9c8c42 100644 --- a/src/ui/suggestionManager.ts +++ b/src/ui/suggestionManager.ts @@ -44,7 +44,7 @@ export class SuggestionManager { return; } this.#command = commandText; - const suggestionBlob = await getSuggestions(commandText); + const suggestionBlob = await getSuggestions(commandText, this.#term.cwd); this.#suggestBlob = suggestionBlob; } diff --git a/src/utils/ansi.ts b/src/utils/ansi.ts index 95d3b33..6169606 100644 --- a/src/utils/ansi.ts +++ b/src/utils/ansi.ts @@ -13,6 +13,7 @@ const IS_OSC = OSC + IsTermOscPs + ";"; export enum IstermOscPt { PromptStarted = "PS", PromptEnded = "PE", + CurrentWorkingDirectory = "CWD", } export const IstermPromptStart = IS_OSC + IstermOscPt.PromptStarted + BEL; diff --git a/src/utils/shell.ts b/src/utils/shell.ts index 94f2158..d78e944 100644 --- a/src/utils/shell.ts +++ b/src/utils/shell.ts @@ -25,6 +25,16 @@ export const supportedShells = [Shell.Bash, process.platform == "win32" ? Shell. export const userZdotdir = process.env?.ZDOTDIR ?? os.homedir() ?? `~`; export const zdotdir = path.join(os.tmpdir(), `is-zsh`); +const configFolder = ".inshellisense"; + +export const setupBashPreExec = async () => { + const shellFolderPath = path.join(path.dirname(url.fileURLToPath(import.meta.url)), "..", "..", "shell"); + const globalConfigPath = path.join(os.homedir(), configFolder); + if (!fs.existsSync(globalConfigPath)) { + await fsAsync.mkdir(globalConfigPath, { recursive: true }); + } + await fsAsync.cp(path.join(shellFolderPath, "bash-preexec.sh"), path.join(globalConfigPath, "bash-preexec.sh")); +}; export const setupZshDotfiles = async () => { const shellFolderPath = path.join(path.dirname(url.fileURLToPath(import.meta.url)), "..", "..", "shell");