Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions app/assets/bundled/bootstrap/pwsh.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -1031,6 +1031,31 @@ $null = New-Module -Name Warp-Module -ScriptBlock {

function Warp-Finish-Bootstrap {
param([decimal]$rcStartTime, [decimal]$rcEndTime)

# Warp communicates with the shell over a ConPTY channel, which is always UTF-8, but the
# console session decodes the bytes we write into it using the console input code page --
# which Windows PowerShell 5.1 defaults to the system's legacy OEM/ANSI code page (e.g. 857
# on Turkish Windows), not UTF-8. Non-ASCII characters in a command written to the PTY (e.g.
# a path with Turkish characters accepted from autocomplete) then arrive mangled and the
# path no longer resolves. Windows PowerShell 5.1's $OutputEncoding also defaults to strict
# ASCII, turning any non-ASCII character into a literal '?' when piping to a native program.
# Pin all three to BOM-less UTF-8 so non-ASCII text round-trips; a BOM-less encoding instance
# additionally avoids 5.1 prepending a UTF-8 BOM to native stdin once the input code page is
# 65001. This runs after the user's profiles, which could otherwise reset the encodings.
# PowerShell 7 and non-Windows hosts already default to UTF-8, and $IsWindows doesn't exist
# on 5.1, hence the version check. Some hosts disallow changing the encodings, hence the
# catch.
if (($PSVersionTable.PSVersion.Major -le 5) -or $IsWindows) {
try {
$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
$global:OutputEncoding = $utf8NoBom
[Console]::InputEncoding = $utf8NoBom
[Console]::OutputEncoding = $utf8NoBom
} catch {
Write-Verbose "Unable to pin console encodings to UTF-8: $($_.Exception.Message)"
}
}

Warp-Configure-PSReadLine

# This is the closest we can get in PowerShell to a proper preexec hook. We wrap the
Expand Down
128 changes: 128 additions & 0 deletions app/src/terminal/input_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3862,6 +3862,43 @@ fn combined_completions_show_file_paths_after_empty_native_results() {
});
}

#[test]
fn completion_acceptance_preserves_non_ascii_characters() {
App::test((), |mut app| async move {
initialize_app(&mut app);
let terminal =
add_window_with_bootstrapped_terminal(&mut app, None, None /* session_info */).await;
let input = terminal.read(&app, |terminal, _| terminal.input().clone());

// "café" sits left of the completed token, so the byte span (9..13) starts after multi-byte
// characters; "Mäsärästï" is non-ASCII inside the token. "cd café Mäs" is 13 bytes long.
input.update(&mut app, |input, ctx| {
input.clear_buffer_and_reset_undo_stack(ctx);
input.user_insert("cd café Mäs", ctx);
});
input.update(&mut app, |input, ctx| {
input.input_tab(ctx);
input.handle_completion_suggestions_results(
build_suggestion_results(
vec![argument_suggestion("Mäsärästï")],
(9, 13),
MatchStrategy::CaseInsensitive,
),
CompletionsTrigger::Keybinding,
editor_model_snapshot(input, ctx),
ctx,
);
});
input.read(&app, |input, ctx| {
assert_eq!(
input.buffer_text(ctx),
"cd café Mäsärästï ",
"accepting a non-ASCII completion must not mangle any character"
);
});
});
}

#[test]
fn combined_completions_preserve_nonempty_native_results() {
let _native_completions_flag = FeatureFlag::NativeShellCompletions.override_enabled(true);
Expand Down Expand Up @@ -4060,6 +4097,97 @@ fn native_shell_replacement_span_falls_back_to_the_whitespace_token_when_none_is
assert_eq!(results.replacement_span, Span::new(3, 8));
}

#[test]
fn native_shell_replacement_span_is_floored_to_char_boundaries() {
// "cd café Mäs" contains multi-byte characters: 'é' occupies bytes 6..8 and 'ä' bytes 10..12,
// so an end of 11 lands mid-character and would panic if used to slice the buffer.
let buffer_text = "cd café Mäs";

let results = native_shell_suggestion_results(
Vec::new(),
Some(Span::new(9, 11)),
buffer_text,
buffer_text.len(),
);

assert_eq!(results.replacement_span, Span::new(9, 10));
}

#[test]
fn accepting_a_native_completion_preserves_non_ascii_characters() {
let _native_completions_flag = FeatureFlag::NativeShellCompletions.override_enabled(true);

App::test((), |mut app| async move {
initialize_app(&mut app);
app.update(|ctx| {
InputSettings::handle(ctx).update(ctx, |settings, ctx| {
settings
.warp_completions_enabled
.set_value(true, ctx)
.expect("Warp completions setting must update");
settings
.native_shell_completions_enabled
.set_value(true, ctx)
.expect("native completions setting must update");
});
});

let session_info = SessionInfo::new_for_test();
let session_id = session_info.session_id;
let terminal =
add_window_with_bootstrapped_terminal(&mut app, None, Some(session_info)).await;
simulate_directory_for_completion(session_id, &terminal, &mut app, "/usr/bin");

// Mimic a shell reply for "warptool Mäs": the candidates are non-ASCII paths and the span
// is the byte range (9..13) of the token the shell reports, as the pwsh bootstrap emits.
// Two candidates sharing only the short prefix "Mäs" keeps the menu open (rather than
// auto-inserting a single match or the common prefix), so acceptance can be exercised.
app.update(|ctx| {
ctx.subscribe_to_view(&terminal, move |_, event: &TerminalViewEvent, _| {
if let TerminalViewEvent::RunNativeShellCompletions { results_tx, .. } = event {
results_tx
.try_send((
vec![
ShellCompletion::new("Mäsärästü".to_string()),
ShellCompletion::new("Mäsótïlo".to_string()),
],
Some(Span::new(9, 13)),
))
.expect("native completion response receiver must remain open");
}
});
});
let input = terminal.read(&app, |terminal, _| terminal.input().clone());

input.update(&mut app, |input, ctx| {
input.clear_buffer_and_reset_undo_stack(ctx);
input.user_insert("warptool Mäs", ctx);
input.input_tab(ctx);
});

assert_eventually!(
600 => input.read(&app, |input, _| {
input.input_suggestions.read(&app, |suggestions, _| {
let items = suggestions.items().iter().map(|item| item.text()).collect_vec();
items.contains(&"Mäsärästü") && items.contains(&"Mäsótïlo")
})
}),
"gave up waiting for the non-ASCII native suggestions"
);

input.update(&mut app, |input, ctx| {
input.confirm_suggestion("Mäsärästü", ctx);
});
input.read(&app, |input, ctx| {
assert_eq!(
input.buffer_text(ctx),
"warptool Mäsärästü ",
"accepting a non-ASCII completion must insert it verbatim"
);
});
});
}

#[test]
fn test_tab_completion_with_multibyte_chars() {
App::test((), |mut app| async move {
Expand Down
Loading