diff --git a/Cargo.lock b/Cargo.lock index 5ad9fe6450..4d86283942 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -405,6 +405,7 @@ dependencies = [ "strsim 0.11.1", "strum", "telemetry_events", + "terminal_view", "theme", "tiktoken-rs", "toml 0.8.10", diff --git a/crates/assistant/Cargo.toml b/crates/assistant/Cargo.toml index 44a2e65b2f..d3c52e7ab5 100644 --- a/crates/assistant/Cargo.toml +++ b/crates/assistant/Cargo.toml @@ -55,6 +55,7 @@ smol.workspace = true strsim = "0.11" strum.workspace = true telemetry_events.workspace = true +terminal_view.workspace = true theme.workspace = true tiktoken-rs.workspace = true toml.workspace = true diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index dcdb4546bd..52cfb4cb05 100644 --- a/crates/assistant/src/assistant.rs +++ b/crates/assistant/src/assistant.rs @@ -27,7 +27,7 @@ use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; use slash_command::{ active_command, default_command, diagnostics_command, fetch_command, file_command, now_command, - project_command, prompt_command, rustdoc_command, search_command, tabs_command, + project_command, prompt_command, rustdoc_command, search_command, tabs_command, term_command, }; use std::{ fmt::{self, Display}, @@ -314,6 +314,7 @@ fn register_slash_commands(cx: &mut AppContext) { slash_command_registry.register_command(search_command::SearchSlashCommand, true); slash_command_registry.register_command(prompt_command::PromptSlashCommand, true); slash_command_registry.register_command(default_command::DefaultSlashCommand, true); + slash_command_registry.register_command(term_command::TermSlashCommand, true); slash_command_registry.register_command(now_command::NowSlashCommand, true); slash_command_registry.register_command(diagnostics_command::DiagnosticsCommand, true); slash_command_registry.register_command(rustdoc_command::RustdocSlashCommand, false); diff --git a/crates/assistant/src/slash_command.rs b/crates/assistant/src/slash_command.rs index f85ed5c7b3..5f20b08e98 100644 --- a/crates/assistant/src/slash_command.rs +++ b/crates/assistant/src/slash_command.rs @@ -28,6 +28,7 @@ pub mod prompt_command; pub mod rustdoc_command; pub mod search_command; pub mod tabs_command; +pub mod term_command; pub(crate) struct SlashCommandCompletionProvider { commands: Arc, diff --git a/crates/assistant/src/slash_command/term_command.rs b/crates/assistant/src/slash_command/term_command.rs new file mode 100644 index 0000000000..60619afcf5 --- /dev/null +++ b/crates/assistant/src/slash_command/term_command.rs @@ -0,0 +1,105 @@ +use std::sync::atomic::AtomicBool; +use std::sync::Arc; + +use anyhow::Result; +use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandOutputSection}; +use gpui::{AppContext, Task, WeakView}; +use language::{CodeLabel, LspAdapterDelegate}; +use terminal_view::{terminal_panel::TerminalPanel, TerminalView}; +use ui::prelude::*; +use workspace::Workspace; + +use super::create_label_for_command; + +pub(crate) struct TermSlashCommand; + +const LINE_COUNT_ARG: &str = "--line-count"; + +impl SlashCommand for TermSlashCommand { + fn name(&self) -> String { + "term".into() + } + + fn label(&self, cx: &AppContext) -> CodeLabel { + create_label_for_command("term", &[LINE_COUNT_ARG], cx) + } + + fn description(&self) -> String { + "insert terminal output".into() + } + + fn menu_text(&self) -> String { + "Insert terminal output".into() + } + + fn requires_argument(&self) -> bool { + false + } + + fn complete_argument( + self: Arc, + _query: String, + _cancel: Arc, + _workspace: Option>, + _cx: &mut AppContext, + ) -> Task>> { + Task::ready(Ok(vec![LINE_COUNT_ARG.to_string()])) + } + + fn run( + self: Arc, + argument: Option<&str>, + workspace: WeakView, + _delegate: Arc, + cx: &mut WindowContext, + ) -> Task> { + let Some(workspace) = workspace.upgrade() else { + return Task::ready(Err(anyhow::anyhow!("workspace was dropped"))); + }; + let Some(terminal_panel) = workspace.read(cx).panel::(cx) else { + return Task::ready(Err(anyhow::anyhow!("no terminal panel open"))); + }; + let Some(active_terminal) = terminal_panel + .read(cx) + .pane() + .read(cx) + .active_item() + .and_then(|t| t.downcast::()) + else { + return Task::ready(Err(anyhow::anyhow!("no active terminal"))); + }; + + let line_count = argument.and_then(|a| parse_argument(a)).unwrap_or(20); + + let lines = active_terminal + .read(cx) + .model() + .read(cx) + .last_n_non_empty_lines(line_count); + + let mut text = String::new(); + text.push_str("Terminal output:\n"); + text.push_str(&lines.join("\n")); + let range = 0..text.len(); + + Task::ready(Ok(SlashCommandOutput { + text, + sections: vec![SlashCommandOutputSection { + range, + icon: IconName::Terminal, + label: "Terminal".into(), + }], + run_commands_in_text: false, + })) + } +} + +fn parse_argument(argument: &str) -> Option { + let mut args = argument.split(' '); + if args.next() == Some(LINE_COUNT_ARG) { + if let Some(line_count) = args.next().and_then(|s| s.parse::().ok()) { + return Some(line_count); + } + } + None +} diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index dae704d18c..239c1c8463 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -1083,6 +1083,31 @@ impl Terminal { } } + pub fn last_n_non_empty_lines(&self, n: usize) -> Vec { + let term = self.term.clone(); + let terminal = term.lock_unfair(); + + let mut lines = Vec::new(); + let mut current_line = terminal.bottommost_line(); + while lines.len() < n { + let mut line_buffer = String::new(); + for cell in &terminal.grid()[current_line] { + line_buffer.push(cell.c); + } + let line = line_buffer.trim_end(); + if !line.is_empty() { + lines.push(line.to_string()); + } + + if current_line == terminal.topmost_line() { + break; + } + current_line = Line(current_line.0 - 1); + } + lines.reverse(); + lines + } + pub fn focus_in(&self) { if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) { self.write_to_pty("\x1b[I".to_string());