feat(terminal): improve PowerShell prompt handling for virtual environments
- Updated the PowerShell prompt initialization to better manage virtual environment prompts, ensuring that the original prompt is preserved and restored correctly. - Added a new test to validate that the virtual environment prompt survives idle repairs and deactivation, preventing recursive prompt issues. - Enhanced the prompt wrapper logic to avoid cycles when dealing with shared delegates, improving overall stability.
This commit is contained in:
1 parent
0d8c0dc5eb
commit
3de2029125
2 files changed
+79
-7
No files matched your search
@@ -399,6 +399,8 @@ fn cmd_prompt_value(session_token: &str) -> String {
|
||||
/// Install a wrapper without touching the user's profile. Metadata is emitted
|
||||
/// before the original prompt, then the incoming `$?`/`$LASTEXITCODE` state is
|
||||
/// restored so prompt themes keep both status and deliberate OSC-title behavior.
|
||||
/// Each wrapper captures its delegate: venv may save an older wrapper as
|
||||
/// `_OLD_VIRTUAL_PROMPT`, so changing a shared delegate would create a cycle.
|
||||
fn pwsh_prompt_init_args(session_token: &str) -> Vec<String> {
|
||||
let init = format!(
|
||||
concat!(
|
||||
@@ -412,14 +414,14 @@ fn pwsh_prompt_init_args(session_token: &str) -> Vec<String> {
|
||||
"[Console]::Out.Write($__nt_esc + ']7;' + $__nt_uri + $__nt_st) }} catch {{ [Console]::Out.Write($__nt_esc + ']7;nyaterm-clear://{0}' + $__nt_st) }} }} else {{ [Console]::Out.Write($__nt_esc + ']7;nyaterm-clear://{0}' + $__nt_st) }} ",
|
||||
"}}; ",
|
||||
"$global:__nyaterm_local_ready_failed = {{ if (-not $global:__nyaterm_local_failure_reported) {{ $global:__nyaterm_local_failure_reported = $true; $__nt_esc = [char]27; [Console]::Out.Write($__nt_esc + ']7777;NyaTermReadyFailed:{0}' + $__nt_esc + '\\') }} }}; ",
|
||||
"$global:__nyaterm_local_prompt_wrapper = {{ ",
|
||||
"$global:__nyaterm_local_install_prompt = {{ param([scriptblock]$__nt_original_prompt); $global:__nyaterm_local_prev_prompt = $__nt_original_prompt; $global:__nyaterm_local_prompt_wrapper = {{ ",
|
||||
"$__nt_success = $?; $__nt_had_last_exit = Test-Path variable:global:LASTEXITCODE; $__nt_last_exit = if ($__nt_had_last_exit) {{ $global:LASTEXITCODE }} else {{ $null }}; & $global:__nyaterm_local_report; ",
|
||||
"if ($__nt_had_last_exit) {{ $global:LASTEXITCODE = $__nt_last_exit }} else {{ Remove-Variable LASTEXITCODE -Scope Global -ErrorAction Ignore }}; if ($__nt_success) {{ & {{}} }} else {{ Write-Error 'NyaTerm prompt status restore' -ErrorAction Ignore }}; ",
|
||||
"$__nt_result = $null; try {{ if ($global:__nyaterm_local_prev_prompt) {{ if ($__nt_success) {{ & {{}} }} else {{ Write-Error 'NyaTerm prompt status restore' -ErrorAction Ignore }}; $__nt_result = & $global:__nyaterm_local_prev_prompt }} else {{ $__nt_result = 'PS ' + $executionContext.SessionState.Path.CurrentLocation.Path + ('>' * ($nestedPromptLevel + 1)) + ' ' }} }} finally {{ ",
|
||||
"$__nt_current_prompt = $function:prompt; if (-not [object]::ReferenceEquals($__nt_current_prompt, $global:__nyaterm_local_prompt_wrapper)) {{ $global:__nyaterm_local_prev_prompt = $__nt_current_prompt; try {{ Set-Item Function:\\global:prompt -Value $global:__nyaterm_local_prompt_wrapper -Force -ErrorAction Stop }} catch {{ & $global:__nyaterm_local_ready_failed }} }} }}; $__nt_result ",
|
||||
"}}; ",
|
||||
"try {{ Set-Item Function:\\global:prompt -Value $global:__nyaterm_local_prompt_wrapper -Force -ErrorAction Stop }} catch {{ & $global:__nyaterm_local_ready_failed }}; ",
|
||||
"try {{ if ($global:__nyaterm_local_prompt_guard) {{ $__nt_old_guard_id = $global:__nyaterm_local_prompt_guard.Id; foreach ($__nt_subscriber in @(Microsoft.PowerShell.Utility\\Get-EventSubscriber -SourceIdentifier PowerShell.OnIdle -ErrorAction Ignore)) {{ if ([object]::ReferenceEquals($__nt_subscriber.Action, $global:__nyaterm_local_prompt_guard)) {{ Microsoft.PowerShell.Utility\\Unregister-Event -SubscriptionId $__nt_subscriber.SubscriptionId -Force -ErrorAction Ignore; break }} }}; Microsoft.PowerShell.Core\\Remove-Job -Id $__nt_old_guard_id -Force -ErrorAction Ignore; $global:__nyaterm_local_prompt_guard = $null }}; $global:__nyaterm_local_prompt_guard = Microsoft.PowerShell.Utility\\Register-EngineEvent -SourceIdentifier PowerShell.OnIdle -Action {{ $__nt_current_prompt = $function:prompt; if (-not [object]::ReferenceEquals($__nt_current_prompt, $global:__nyaterm_local_prompt_wrapper)) {{ $global:__nyaterm_local_prev_prompt = $__nt_current_prompt; try {{ Set-Item Function:\\global:prompt -Value $global:__nyaterm_local_prompt_wrapper -Force -ErrorAction Stop; & $global:__nyaterm_local_report }} catch {{ & $global:__nyaterm_local_ready_failed }} }} }} }} catch {{ & $global:__nyaterm_local_ready_failed }}"
|
||||
"$__nt_result = $null; try {{ if ($__nt_original_prompt) {{ if ($__nt_success) {{ & {{}} }} else {{ Write-Error 'NyaTerm prompt status restore' -ErrorAction Ignore }}; $__nt_result = & $__nt_original_prompt }} else {{ $__nt_result = 'PS ' + $executionContext.SessionState.Path.CurrentLocation.Path + ('>' * ($nestedPromptLevel + 1)) + ' ' }} }} finally {{ ",
|
||||
"$__nt_current_prompt = $function:prompt; if (-not [object]::ReferenceEquals($__nt_current_prompt, $global:__nyaterm_local_prompt_wrapper)) {{ & $global:__nyaterm_local_install_prompt $__nt_current_prompt }} }}; $__nt_result ",
|
||||
"}}.GetNewClosure(); ",
|
||||
"try {{ Set-Item Function:\\global:prompt -Value $global:__nyaterm_local_prompt_wrapper -Force -ErrorAction Stop }} catch {{ & $global:__nyaterm_local_ready_failed }} }}; & $global:__nyaterm_local_install_prompt $global:__nyaterm_local_prev_prompt; ",
|
||||
"try {{ if ($global:__nyaterm_local_prompt_guard) {{ $__nt_old_guard_id = $global:__nyaterm_local_prompt_guard.Id; foreach ($__nt_subscriber in @(Microsoft.PowerShell.Utility\\Get-EventSubscriber -SourceIdentifier PowerShell.OnIdle -ErrorAction Ignore)) {{ if ([object]::ReferenceEquals($__nt_subscriber.Action, $global:__nyaterm_local_prompt_guard)) {{ Microsoft.PowerShell.Utility\\Unregister-Event -SubscriptionId $__nt_subscriber.SubscriptionId -Force -ErrorAction Ignore; break }} }}; Microsoft.PowerShell.Core\\Remove-Job -Id $__nt_old_guard_id -Force -ErrorAction Ignore; $global:__nyaterm_local_prompt_guard = $null }}; $global:__nyaterm_local_prompt_guard = Microsoft.PowerShell.Utility\\Register-EngineEvent -SourceIdentifier PowerShell.OnIdle -Action {{ $__nt_current_prompt = $function:prompt; if (-not [object]::ReferenceEquals($__nt_current_prompt, $global:__nyaterm_local_prompt_wrapper)) {{ & $global:__nyaterm_local_install_prompt $__nt_current_prompt; & $global:__nyaterm_local_report }} }} }} catch {{ & $global:__nyaterm_local_ready_failed }}"
|
||||
),
|
||||
session_token
|
||||
);
|
||||
|
||||
@@ -1593,7 +1593,7 @@ mod tests {
|
||||
assert!(
|
||||
init.find("]2;").unwrap()
|
||||
< init
|
||||
.find("& $global:__nyaterm_local_prev_prompt")
|
||||
.find("& $__nt_original_prompt")
|
||||
.unwrap()
|
||||
);
|
||||
assert!(init.contains("ReferenceEquals"));
|
||||
@@ -1677,6 +1677,76 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[test]
|
||||
fn native_powershell_venv_prompt_survives_idle_repair_and_deactivation() {
|
||||
use std::process::Command;
|
||||
|
||||
let init = build_local_startup_script("powershell.exe", &ready_marker(), true, true)
|
||||
.pwsh_init_args
|
||||
.expect("PowerShell init args")
|
||||
.pop()
|
||||
.expect("PowerShell init script");
|
||||
// Match Activate.ps1's saved prompt and named invocation, and bound the
|
||||
// prefix count so a regression fails instead of overflowing the stack.
|
||||
let harness = format!(
|
||||
r#"$ErrorActionPreference = 'Stop'
|
||||
function global:prompt {{ 'USER> ' }}
|
||||
{init}
|
||||
$global:prefix_count = 0
|
||||
function global:activate_test_venv {{
|
||||
function global:_OLD_VIRTUAL_PROMPT {{ '' }}
|
||||
Copy-Item Function:prompt Function:_OLD_VIRTUAL_PROMPT
|
||||
function global:prompt {{
|
||||
$global:prefix_count++
|
||||
if ($global:prefix_count -gt 8) {{ throw 'recursive venv prompt' }}
|
||||
'(venv) ' + (_OLD_VIRTUAL_PROMPT)
|
||||
}}
|
||||
}}
|
||||
function repair_test_prompt {{
|
||||
$null = New-Event -SourceIdentifier PowerShell.OnIdle
|
||||
for ($i = 0; $i -lt 50; $i++) {{
|
||||
if ([object]::ReferenceEquals($function:prompt, $global:__nyaterm_local_prompt_wrapper)) {{ return }}
|
||||
Start-Sleep -Milliseconds 10
|
||||
}}
|
||||
throw 'idle prompt repair did not run'
|
||||
}}
|
||||
for ($cycle = 0; $cycle -lt 3; $cycle++) {{
|
||||
activate_test_venv
|
||||
repair_test_prompt
|
||||
for ($i = 0; $i -lt 2; $i++) {{
|
||||
$value = prompt
|
||||
if ($value -cne '(venv) USER> ') {{ throw ('unexpected active prompt: ' + $value) }}
|
||||
}}
|
||||
Copy-Item Function:_OLD_VIRTUAL_PROMPT Function:global:prompt
|
||||
Remove-Item Function:_OLD_VIRTUAL_PROMPT
|
||||
repair_test_prompt
|
||||
if ((prompt) -cne 'USER> ') {{ throw 'deactivation did not restore prompt' }}
|
||||
}}
|
||||
if ($global:prefix_count -ne 6) {{ throw ('unexpected prefix count: ' + $global:prefix_count) }}
|
||||
Write-Output 'VENV_PROMPT_OK'
|
||||
"#
|
||||
);
|
||||
let output = Command::new("powershell.exe")
|
||||
.args(["-NoLogo", "-NoProfile", "-Command", &harness])
|
||||
.output()
|
||||
.expect("run Windows PowerShell venv prompt regression");
|
||||
let mut combined = output.stdout;
|
||||
combined.extend_from_slice(&output.stderr);
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&combined)
|
||||
);
|
||||
assert!(
|
||||
combined
|
||||
.windows(b"VENV_PROMPT_OK".len())
|
||||
.any(|window| window == b"VENV_PROMPT_OK"),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&combined)
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[test]
|
||||
fn native_powershell_prompt_wrapper_repairs_self_replacement() {
|
||||
|
||||
Reference in new issue
Block a user