Skip to content

Commit 47f0d51

Browse files
feat(ironposh-web): add tab_complete(TabExpansion2) API
1 parent c2bb819 commit 47f0d51

File tree

2 files changed

+117
-1
lines changed

2 files changed

+117
-1
lines changed

crates/ironposh-web/src/client.rs

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ use crate::{
22
error::WasmError,
33
hostcall::handle_host_calls,
44
http_client::GatewayHttpViaWSClient,
5-
types::{SecurityWarningCallback, WasmWinRmConfig},
5+
types::{SecurityWarningCallback, WasmCommandCompletion, WasmWinRmConfig},
66
JsSessionEvent, WasmPowerShellStream,
77
};
8+
use std::convert::TryFrom;
89
use futures::StreamExt;
910
use ironposh_async::RemoteAsyncPowershellClient;
1011
use ironposh_client_core::{connector::WinRmConfig, powershell::PipelineHandle};
@@ -242,6 +243,73 @@ impl WasmPowerShellClient {
242243
Ok(stream)
243244
}
244245

246+
#[wasm_bindgen]
247+
pub async fn tab_complete(
248+
&mut self,
249+
input_script: String,
250+
cursor_column: u32,
251+
) -> Result<WasmCommandCompletion, WasmError> {
252+
use ironposh_client_core::connector::active_session::UserEvent;
253+
254+
fn escape_ps_single_quoted(input: &str) -> String {
255+
input.replace('\'', "''")
256+
}
257+
258+
let escaped = escape_ps_single_quoted(&input_script);
259+
let script = format!(
260+
"TabExpansion2 -inputScript '{escaped}' -cursorColumn {cursor_column}"
261+
);
262+
263+
info!(
264+
cursor_column,
265+
input_len = input_script.len(),
266+
"tab_complete: sending TabExpansion2"
267+
);
268+
269+
let stream = self.client.send_script_raw(script).await?;
270+
let mut stream = stream.boxed();
271+
272+
let mut output: Option<ironposh_psrp::PsValue> = None;
273+
let mut error_message: Option<String> = None;
274+
275+
while let Some(ev) = stream.next().await {
276+
match ev {
277+
UserEvent::PipelineOutput { output: out, .. } => {
278+
if output.is_none() {
279+
output = Some(out.data);
280+
}
281+
}
282+
UserEvent::ErrorRecord { error_record, .. } => {
283+
let concise = error_record.render_concise();
284+
error_message = Some(concise.clone());
285+
warn!(error_message = %concise, "tab_complete: error record");
286+
}
287+
UserEvent::PipelineFinished { .. } => break,
288+
_ => {}
289+
}
290+
}
291+
292+
let Some(ps_value) = output else {
293+
return Err(WasmError::Generic(
294+
error_message.unwrap_or_else(|| "TabExpansion2 returned no output".into()),
295+
));
296+
};
297+
298+
let completion = ironposh_psrp::CommandCompletion::try_from(&ps_value)
299+
.map_err(|e| WasmError::Generic(e.to_string()))?;
300+
301+
info!(
302+
?completion,
303+
cursor_column,
304+
replacement_index = completion.replacement_index,
305+
replacement_length = completion.replacement_length,
306+
matches = completion.completion_matches.len(),
307+
"tab_complete: parsed completion"
308+
);
309+
310+
Ok(WasmCommandCompletion::from(&completion))
311+
}
312+
245313
// pub async fn next_host_call
246314

247315
#[wasm_bindgen]

crates/ironposh-web/src/types/mod.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,3 +426,51 @@ impl From<SessionEvent> for JsSessionEvent {
426426
}
427427
}
428428
}
429+
430+
// =============================================================================
431+
// Tab completion (TabExpansion2 / CommandCompletion)
432+
// =============================================================================
433+
434+
#[derive(Tsify, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
435+
#[tsify(into_wasm_abi, from_wasm_abi)]
436+
pub struct WasmCompletionResult {
437+
pub completion_text: String,
438+
pub list_item_text: String,
439+
pub result_type: String,
440+
pub tool_tip: String,
441+
}
442+
443+
#[derive(Tsify, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
444+
#[tsify(into_wasm_abi, from_wasm_abi)]
445+
pub struct WasmCommandCompletion {
446+
pub current_match_index: i32,
447+
pub replacement_index: i32,
448+
pub replacement_length: i32,
449+
pub completion_matches: Vec<WasmCompletionResult>,
450+
}
451+
452+
impl From<&ironposh_psrp::CompletionResult> for WasmCompletionResult {
453+
fn from(value: &ironposh_psrp::CompletionResult) -> Self {
454+
Self {
455+
completion_text: value.completion_text.clone(),
456+
list_item_text: value.list_item_text.clone(),
457+
result_type: value.result_type.clone(),
458+
tool_tip: value.tool_tip.clone(),
459+
}
460+
}
461+
}
462+
463+
impl From<&ironposh_psrp::CommandCompletion> for WasmCommandCompletion {
464+
fn from(value: &ironposh_psrp::CommandCompletion) -> Self {
465+
Self {
466+
current_match_index: value.current_match_index,
467+
replacement_index: value.replacement_index,
468+
replacement_length: value.replacement_length,
469+
completion_matches: value
470+
.completion_matches
471+
.iter()
472+
.map(WasmCompletionResult::from)
473+
.collect(),
474+
}
475+
}
476+
}

0 commit comments

Comments
 (0)