intelli-shell 3.4.0

Like IntelliSense, but for shells
# --- PowerShell Integration ---

#Requires -Modules PSReadLine

# Set strict mode
Set-StrictMode -Version Latest

# Ensure PSReadLine module is available
if (-not (Get-Module -Name PSReadLine -ListAvailable)) {
  Write-Warning "PSReadLine module not found. IntelliShell key bindings require PSReadLine."
  return
}
# Import the module if it's not already loaded
Import-Module PSReadLine -ErrorAction SilentlyContinue

# Polyfill for $IsWindows on older PowerShell versions
if (-not (Test-Path 'variable:IsWindows')) {
  # In Windows PowerShell (<= 5.1), we are always on Windows
  New-Variable -Name 'IsWindows' -Value $true -Scope Script
}

# --- Configuration ---
$IntelliSearchChord = if ([string]::IsNullOrEmpty($env:INTELLI_SEARCH_HOTKEY)) { 'Ctrl+Spacebar' } else { $env:INTELLI_SEARCH_HOTKEY }
$IntelliBookmarkChord = if ([string]::IsNullOrEmpty($env:INTELLI_BOOKMARK_HOTKEY)) { 'Ctrl+b' } else { $env:INTELLI_BOOKMARK_HOTKEY }
$IntelliVariableChord = if ([string]::IsNullOrEmpty($env:INTELLI_VARIABLE_HOTKEY)) { 'Ctrl+l' } else { $env:INTELLI_VARIABLE_HOTKEY }
$IntelliFixChord = if ([string]::IsNullOrEmpty($env:INTELLI_FIX_HOTKEY)) { 'Ctrl+x' } else { $env:INTELLI_FIX_HOTKEY }

# Encapsulates the logic for running intelli-shell and updating the buffer
function Invoke-IntelliShellAction {
  param(
    [Parameter(Mandatory=$true)]
    [string]$Subcommand, # The intelli-shell subcommand (e.g., search, new, replace)

    [Parameter(Mandatory=$false)]
    [string[]]$Args # Array of arguments to pass to intelli-shell.exe after the subcommand
  )
  # Define the executable name (assuming it's in PATH)
  $exeName = if ($IsWindows) { 'intelli-shell.exe' } else { 'intelli-shell' }

  # Escape arguments
  $processedArgs = @()
  if ($null -ne $Args) {
    $processedArgs = $Args | ForEach-Object { Escape-ArgumentForCommandLine -Argument $_ }
  }

  # Create a temporary file for the output
  $stdoutTempFilePath = $null
  try {
    $stdoutTempFilePath = [System.IO.Path]::GetTempFileName()

    # Construct the full argument list for intelli-shell
    $fullArgumentList = (@('--extra-line', '--skip-execution', '--file-output', $stdoutTempFilePath, $Subcommand) + $processedArgs) -join ' '

    Write-Verbose "Starting process: $exeName $fullArgumentList"

    # Clear the current line in the buffer first
    [Microsoft.PowerShell.PSConsoleReadLine]::RevertLine()
    [Microsoft.PowerShell.PSConsoleReadLine]::BeginningOfLine()
    [Microsoft.PowerShell.PSConsoleReadLine]::KillLine()

    # Execute intelli-shell
    $process = Start-Process -FilePath $exeName `
      -ArgumentList $fullArgumentList `
      -Wait `
      -NoNewWindow `
      -PassThru

    # If the output file is missing or empty, there's nothing to process (likely a crash)
    if (-not (Test-Path -Path $stdoutTempFilePath) -or (Get-Item $stdoutTempFilePath).Length -eq 0) {
      # Panic report was likely printed, we must start a new prompt line
      [Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine()
      return
    }

    # Read the file content and parse it
    $lines = [System.IO.File]::ReadAllText($stdoutTempFilePath)
    $lines = $lines -split '\r?\n'
    $outStatus = $lines[0]
    $action = if ($lines.Length -gt 1) { $lines[1] } else { '' }
    $command = if ($lines.Length -gt 2) { $lines[2..($lines.Length - 1)] -join "`n" } else { '' }
    
    # If a new prompt is needed but the tool didn't output anything (e.g., Ctrl+C),
    # we must print a newline ourselves to advance the cursor
    if ($process.ExitCode -ne 0 -and $outStatus -eq 'CLEAN') {
      [System.Console]::Error.WriteLine("")
      $newCursorY = [System.Console]::CursorTop
    } elseif ($outStatus -eq 'CLEAN') {
      $promptText = & { prompt }
      $promptLineCount = ($promptText -split "`r?`n").Count
      $newCursorY = [System.Math]::Max(0, [System.Console]::CursorTop - ($promptLineCount - 1))
    } else {
      $newCursorY = [System.Console]::CursorTop
    }
    [System.Console]::OutputEncoding = [System.Text.Encoding]::UTF8
    [Microsoft.PowerShell.PSConsoleReadLine]::InvokePrompt($null, $newCursorY)

    # Determine the content of the buffer
    if ($action -eq 'REPLACE') {
      [Microsoft.PowerShell.PSConsoleReadLine]::Insert($command)
    } elseif ($action -eq 'EXECUTE') {
      [Microsoft.PowerShell.PSConsoleReadLine]::Insert($command)
      [Microsoft.PowerShell.PSConsoleReadLine]::AcceptLine()
    }
  } catch {
    Display-ErrorMessage -Message "An error occurred during IntelliShell action: $_"
  } finally {
    # Clean up temporary file
    if ($null -ne $stdoutTempFilePath -and (Test-Path $stdoutTempFilePath)) {
      Write-Verbose "Removing temporary stdout file: $stdoutTempFilePath"
      Remove-Item $stdoutTempFilePath -Force -ErrorAction SilentlyContinue
    }
  }
}

# Escapes an argument for use in a native process command line
function Escape-ArgumentForCommandLine {
  [CmdletBinding()]
  param(
    [Parameter(Mandatory)]
    [AllowEmptyString()]
    [string]$Argument
  )

  # If the argument is empty, it must be represented as empty double quotes
  if ([string]::IsNullOrEmpty($Argument)) {
    return '""'
  }

  # If the argument contains no special characters, it can be passed as-is without quotes
  if (-not ($Argument -match '[\s"]')) {
    return $Argument
  }

  # This regex-based replacement handles backslashes and quotes:
  # 1. It doubles any backslashes that are at the very end of the string
  # 2. It doubles any backslashes that are followed by a double quote, and then escapes that quote
  $escaped = [regex]::Replace($Argument, '(\\+)$', '$1$1')
  $escaped = [regex]::Replace($escaped, '(\\*)"', '$1$1\"')

  # Finally, wrap the entire escaped string in double quotes
  return '"' + $escaped + '"'
}

# Displays a warning message on a popup
function Display-ErrorMessage {
  param(
    [Parameter(Mandatory=$true)]
    [string]$Message
  )
  if ($IsWindows) {
    # On Windows, use a COM object to show a graphical popup
    try {
      $wshell = New-Object -ComObject WScript.Shell
      $wshell.Popup($Message, 0, "IntelliShell Warning", 48) | Out-Null
    } catch {
      # Fallback to Write-Error if the COM object fails for any reason
      Write-Error "IntelliShell: $Message"
    }
  } else {
    # On Linux/macOS, Write-Warning is the cross-platform equivalent
    Write-Warning "IntelliShell: $Message"
  }
}

# --- Key Handler Definitions ---

Write-Verbose "Setting IntelliShell PSReadLine key handlers..."

# Search Handler
Set-PSReadLineKeyHandler -Chord $IntelliSearchChord -BriefDescription "IntelliShell Search" -Description "Searches for a bookmarked command based on current line" -ScriptBlock {
  $line = $null
  $cursor = $null
  [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)

  # Set command arguments and execute it
  $args = @('-i')
  if (-not [string]::IsNullOrWhiteSpace($line)) {
    $args += $line
  }
  Invoke-IntelliShellAction -Subcommand 'search' -Args $args
}

# Bookmark Handler
Set-PSReadLineKeyHandler -Chord $IntelliBookmarkChord -BriefDescription "IntelliShell Bookmark" -Description "Bookmarks current command line (or opens new bookmark)" -ScriptBlock {
  $line = $null
  $cursor = $null
  [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)

  # Set command arguments and execute it
  $args = @('-i')
  if (-not [string]::IsNullOrWhiteSpace($line)) {
    $args += $line
  }
  Invoke-IntelliShellAction -Subcommand 'new' -Args $args
}

# Variable Replacement Handler
Set-PSReadLineKeyHandler -Chord $IntelliVariableChord -BriefDescription "IntelliShell Variable Replacement" -Description "Triggers variable replacement for current command line" -ScriptBlock {
  $line = $null
  $cursor = $null
  [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)

  # Set command arguments and execute it
  $args = @('-i')
  if (-not [string]::IsNullOrWhiteSpace($line)) {
    $args += $line
  }
  Invoke-IntelliShellAction -Subcommand 'replace' -Args $args
}

# Fix Command Handler
Set-PSReadLineKeyHandler -Chord $IntelliFixChord -BriefDescription "IntelliShell Fix" -Description "Fixes the current command line" -ScriptBlock {
  $line = $null
  $cursor = $null
  [Microsoft.PowerShell.PSConsoleReadLine]::GetBufferState([ref]$line, [ref]$cursor)

  # Initialize an empty array for arguments
  $args = @()

  # Safely get the last commands
  $historyLines = @()
  $historyObjects = Get-History -Count 5 -ErrorAction SilentlyContinue
  if ($null -ne $historyObjects) {
    $historyLines = $historyObjects.CommandLine
  }

  # Only add the history argument if history lines were found
  if ($historyLines.Count -gt 0) {
    $historyString = $historyLines -join "`n"
    $args += @('--history', $historyString)
  }

  # Add the current line if it's not empty
  if (-not [string]::IsNullOrWhiteSpace($line)) {
    $args += $line
  }
  
  # Call the main action function
  Invoke-IntelliShellAction -Subcommand 'fix' -Args $args
}

# Export the execution prompt variable
$env:INTELLI_EXEC_PROMPT = (Get-PSReadlineOption).ContinuationPrompt

Write-Verbose "IntelliShell PSReadLine key handlers set."