Compare commits

...

2 Commits

Author SHA1 Message Date
João Marcos
1cd9c3f3cb biggest wip of my life 2025-04-25 21:17:04 -03:00
João Marcos
bceb10a16b style 2025-04-25 14:39:44 -03:00
51 changed files with 762 additions and 381 deletions

1
Cargo.lock generated
View File

@@ -3192,6 +3192,7 @@ version = "0.1.0"
dependencies = [
"collections",
"gpui",
"itertools 0.14.0",
"linkme",
"parking_lot",
"theme",

View File

@@ -497,6 +497,7 @@ impl Render for ContextPillPreview {
// TODO: Component commented out due to new dependency on `Project`.
/*
impl Component for AddedContext {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::Agent
}

View File

@@ -98,11 +98,16 @@ impl RenderOnce for UsageBanner {
}
impl Component for UsageBanner {
type InitialState = ();
fn sort_name() -> &'static str {
"AgentUsageBanner"
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, _cx: &mut App) -> AnyElement {
let trial_limit = Plan::ZedProTrial.model_requests_limit();
let trial_examples = vec![
single_example(

View File

@@ -24,6 +24,8 @@ use ui::{Disclosure, Tooltip, Window, prelude::*};
use util::ResultExt;
use workspace::Workspace;
pub struct EditFileTool;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct EditFileToolInput {
/// A user-friendly markdown description of what's being replaced. This will be shown in the UI.
@@ -75,8 +77,6 @@ struct PartialInput {
new_string: String,
}
pub struct EditFileTool;
const DEFAULT_UI_TEXT: &str = "Editing file";
impl Tool for EditFileTool {
@@ -627,7 +627,6 @@ mod tests {
#[test]
fn still_streaming_ui_text_with_path() {
let tool = EditFileTool;
let input = json!({
"path": "src/main.rs",
"display_description": "",
@@ -635,12 +634,11 @@ mod tests {
"new_string": "new code"
});
assert_eq!(tool.still_streaming_ui_text(&input), "src/main.rs");
assert_eq!(EditFileTool.still_streaming_ui_text(&input), "src/main.rs");
}
#[test]
fn still_streaming_ui_text_with_description() {
let tool = EditFileTool;
let input = json!({
"path": "",
"display_description": "Fix error handling",
@@ -648,12 +646,14 @@ mod tests {
"new_string": "new code"
});
assert_eq!(tool.still_streaming_ui_text(&input), "Fix error handling");
assert_eq!(
EditFileTool.still_streaming_ui_text(&input),
"Fix error handling",
);
}
#[test]
fn still_streaming_ui_text_with_path_and_description() {
let tool = EditFileTool;
let input = json!({
"path": "src/main.rs",
"display_description": "Fix error handling",
@@ -661,12 +661,14 @@ mod tests {
"new_string": "new code"
});
assert_eq!(tool.still_streaming_ui_text(&input), "Fix error handling");
assert_eq!(
EditFileTool.still_streaming_ui_text(&input),
"Fix error handling",
);
}
#[test]
fn still_streaming_ui_text_no_path_or_description() {
let tool = EditFileTool;
let input = json!({
"path": "",
"display_description": "",
@@ -674,14 +676,19 @@ mod tests {
"new_string": "new code"
});
assert_eq!(tool.still_streaming_ui_text(&input), DEFAULT_UI_TEXT);
assert_eq!(
EditFileTool.still_streaming_ui_text(&input),
DEFAULT_UI_TEXT,
);
}
#[test]
fn still_streaming_ui_text_with_null() {
let tool = EditFileTool;
let input = serde_json::Value::Null;
assert_eq!(tool.still_streaming_ui_text(&input), DEFAULT_UI_TEXT);
assert_eq!(
EditFileTool.still_streaming_ui_text(&input),
DEFAULT_UI_TEXT,
);
}
}

View File

@@ -1,21 +1,24 @@
use crate::schema::json_schema_for;
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::{ActionLog, Tool, ToolResult};
use futures::io::BufReader;
use futures::{AsyncBufReadExt, AsyncReadExt, FutureExt};
use gpui::{AnyWindowHandle, App, AppContext, Entity, Task};
use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
use component::Component;
use futures::{
AsyncBufReadExt, SinkExt, StreamExt,
channel::mpsc::{UnboundedReceiver, UnboundedSender, unbounded},
io::BufReader,
stream::SelectAll,
};
use gpui::{AnyElement, AnyWindowHandle, App, AppContext, Entity, Task, WeakEntity, Window};
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
use project::Project;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::future;
use util::get_system_shell;
use std::{path::Path, process::Stdio, sync::Arc, time::Duration};
use ui::{ComponentScope, IconName, RegisterComponent, prelude::*};
use util::{command::new_smol_command, get_system_shell, markdown::MarkdownString};
use workspace::Workspace;
use std::path::Path;
use std::sync::Arc;
use ui::IconName;
use util::command::new_smol_command;
use util::markdown::MarkdownString;
const COMMAND_OUTPUT_LIMIT: usize = 16 * 1024;
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct TerminalToolInput {
@@ -25,6 +28,7 @@ pub struct TerminalToolInput {
cd: String,
}
#[derive(RegisterComponent)]
pub struct TerminalTool;
impl Tool for TerminalTool {
@@ -86,176 +90,186 @@ impl Tool for TerminalTool {
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
let project = project.read(cx);
let input_path = Path::new(&input.cd);
let working_dir = if input.cd == "." {
// Accept "." as meaning "the one worktree" if we only have one worktree.
let mut worktrees = project.worktrees(cx);
let only_worktree = match worktrees.next() {
Some(worktree) => worktree,
None => {
return Task::ready(Err(anyhow!("No worktrees found in the project"))).into();
}
};
if worktrees.next().is_some() {
return Task::ready(Err(anyhow!(
"'.' is ambiguous in multi-root workspaces. Please specify a root directory explicitly."
))).into();
}
only_worktree.read(cx).abs_path()
} else if input_path.is_absolute() {
// Absolute paths are allowed, but only if they're in one of the project's worktrees.
if !project
.worktrees(cx)
.any(|worktree| input_path.starts_with(&worktree.read(cx).abs_path()))
{
return Task::ready(Err(anyhow!(
"The absolute path must be within one of the project's worktrees"
)))
.into();
}
input_path.into()
} else {
let Some(worktree) = project.worktree_for_root_name(&input.cd, cx) else {
return Task::ready(Err(anyhow!(
"`cd` directory {} not found in the project",
&input.cd
)))
.into();
};
worktree.read(cx).abs_path()
let working_dir = match working_dir(cx, &input, &project, input_path) {
Ok(dir) => dir,
Err(err) => return Task::ready(Err(anyhow!(err))).into(),
};
cx.background_spawn(run_command_limited(working_dir, input.command))
.into()
let (line_sender, line_receiver) = unbounded();
let output = spawn_command_and_stream(working_dir, input.command, line_sender, cx);
let output = match output {
Ok(ok) => ok,
Err(err) => return Task::ready(Err(err)).into(),
};
let card = cx.new(|cx| TerminalToolCard::new(line_receiver, cx));
ToolResult {
output,
card: Some(card.into()),
}
}
}
const LIMIT: usize = 16 * 1024;
async fn run_command_limited(working_dir: Arc<Path>, command: String) -> Result<String> {
/// Run a command until completion and return the output.
///
/// Also stream each line through a channel that can be accessed via the returned
/// receiver, the channel will only receive updates if the future is awaited.
fn spawn_command_and_stream(
working_dir: Arc<Path>,
command: String,
mut line_sender: UnboundedSender<Result<String>>,
cx: &mut App,
) -> Result<Task<Result<String>>> {
let shell = get_system_shell();
let mut cmd = new_smol_command(&shell)
.arg("-c")
.arg(&command)
.args(["-c", &command])
.current_dir(working_dir)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.context("Failed to execute terminal command")?;
let mut combined_buffer = String::with_capacity(LIMIT + 1);
let mut out_reader = BufReader::new(cmd.stdout.take().context("Failed to get stdout")?);
let mut out_tmp_buffer = String::with_capacity(512);
let mut err_reader = BufReader::new(cmd.stderr.take().context("Failed to get stderr")?);
let mut err_tmp_buffer = String::with_capacity(512);
let mut out_line = Box::pin(
out_reader
.read_line(&mut out_tmp_buffer)
.left_future()
.fuse(),
let mut line_stream = SelectAll::new();
line_stream.push(
BufReader::new(cmd.stdout.take().context("Failed to get stdout")?)
.lines()
.boxed(),
);
let mut err_line = Box::pin(
err_reader
.read_line(&mut err_tmp_buffer)
.left_future()
.fuse(),
line_stream.push(
BufReader::new(cmd.stderr.take().context("Failed to get stderr")?)
.lines()
.boxed(),
);
let mut has_stdout = true;
let mut has_stderr = true;
while (has_stdout || has_stderr) && combined_buffer.len() < LIMIT + 1 {
futures::select_biased! {
read = out_line => {
drop(out_line);
combined_buffer.extend(out_tmp_buffer.drain(..));
if read? == 0 {
out_line = Box::pin(future::pending().right_future().fuse());
has_stdout = false;
} else {
out_line = Box::pin(out_reader.read_line(&mut out_tmp_buffer).left_future().fuse());
let fut = cx.background_spawn(async move {
let mut combined_output = String::with_capacity(COMMAND_OUTPUT_LIMIT + 1);
while let Some(line) = line_stream.next().await {
let line = match line {
Ok(line) => line,
Err(err) => {
let err = format!("Failed to read line: {err}");
// TODO: unwrap
line_sender.send(Err(anyhow!(err.clone()))).await.unwrap();
return Err(anyhow!(err));
}
};
let truncated = combined_output.len() + line.len() > COMMAND_OUTPUT_LIMIT;
let line = if truncated {
let remaining_capacity = COMMAND_OUTPUT_LIMIT.saturating_sub(combined_output.len());
&line[..remaining_capacity]
} else {
&line
};
combined_output.push_str(line);
combined_output.push('\n');
// TODO: unwrap
line_sender
.send(Ok(line.to_owned()))
.await
.context("Failed to send terminal output text")
.unwrap();
if truncated {
// TODO
break;
}
read = err_line => {
drop(err_line);
combined_buffer.extend(err_tmp_buffer.drain(..));
if read? == 0 {
err_line = Box::pin(future::pending().right_future().fuse());
has_stderr = false;
} else {
err_line = Box::pin(err_reader.read_line(&mut err_tmp_buffer).left_future().fuse());
}
}
};
}
drop((out_line, err_line));
let truncated = combined_buffer.len() > LIMIT;
combined_buffer.truncate(LIMIT);
consume_reader(out_reader, truncated).await?;
consume_reader(err_reader, truncated).await?;
let status = cmd.status().await.context("Failed to get command status")?;
let output_string = if truncated {
// Valid to find `\n` in UTF-8 since 0-127 ASCII characters are not used in
// multi-byte characters.
let last_line_ix = combined_buffer.bytes().rposition(|b| b == b'\n');
let combined_buffer = &combined_buffer[..last_line_ix.unwrap_or(combined_buffer.len())];
format!(
"Command output too long. The first {} bytes:\n\n{}",
combined_buffer.len(),
output_block(&combined_buffer),
)
} else {
output_block(&combined_buffer)
};
let output_with_status = if status.success() {
if output_string.is_empty() {
"Command executed successfully.".to_string()
} else {
output_string.to_string()
}
} else {
format!(
"Command failed with exit code {} (shell: {}).\n\n{}",
status.code().unwrap_or(-1),
shell,
output_string,
)
};
Ok(output_with_status)
Ok(output_block(&combined_output))
});
Ok(fut)
// drop((out_line, err_line));
// let truncated = combined_buffer.len() > COMMAND_OUTPUT_LIMIT;
// combined_buffer.truncate(COMMAND_OUTPUT_LIMIT);
// consume_reader(out_reader, truncated).await?;
// consume_reader(err_reader, truncated).await?;
// let status = cmd.status().await.context("Failed to get command status")?;
// let output_string = if truncated {
// // Valid to find `\n` in UTF-8 since 0-127 ASCII characters are not used in
// // multi-byte characters.
// let last_line_ix = combined_buffer.bytes().rposition(|b| b == b'\n');
// let combined_buffer = &combined_buffer[..last_line_ix.unwrap_or(combined_buffer.len())];
// format!(
// "Command output too long. The first {} bytes:\n\n{}",
// combined_buffer.len(),
// output_block(&combined_buffer),
// )
// } else {
// output_block(&combined_buffer)
// };
// let output_with_status = if status.success() {
// if output_string.is_empty() {
// "Command executed successfully.".to_string()
// } else {
// output_string.to_string()
// }
// } else {
// format!(
// "Command failed with exit code {} (shell: {}).\n\n{}",
// status.code().unwrap_or(-1),
// shell,
// output_string,
// )
// };
// Ok(output_with_status)
}
async fn consume_reader<T: AsyncReadExt + Unpin>(
mut reader: BufReader<T>,
truncated: bool,
) -> Result<(), std::io::Error> {
loop {
let skipped_bytes = reader.fill_buf().await?;
if skipped_bytes.is_empty() {
break;
}
let skipped_bytes_len = skipped_bytes.len();
reader.consume_unpin(skipped_bytes_len);
fn working_dir(
cx: &mut App,
input: &TerminalToolInput,
project: &Entity<Project>,
input_path: &Path,
) -> Result<Arc<Path>, &'static str> {
let project = project.read(cx);
if input.cd == "." {
// Accept "." as meaning "the one worktree" if we only have one worktree.
let mut worktrees = project.worktrees(cx);
// Should only skip if we went over the limit
debug_assert!(truncated);
let only_worktree = match worktrees.next() {
Some(worktree) => worktree,
None => return Err("No worktrees found in the project"),
};
if worktrees.next().is_some() {
return Err(
"'.' is ambiguous in multi-root workspaces. Please specify a root directory explicitly.",
);
}
Ok(only_worktree.read(cx).abs_path())
} else if input_path.is_absolute() {
// Absolute paths are allowed, but only if they're in one of the project's worktrees.
if !project
.worktrees(cx)
.any(|worktree| input_path.starts_with(&worktree.read(cx).abs_path()))
{
return Err("The absolute path must be within one of the project's worktrees");
}
Ok(input_path.into())
} else {
let Some(worktree) = project.worktree_for_root_name(&input.cd, cx) else {
return Err("`cd` directory {} not found in the project");
};
Ok(worktree.read(cx).abs_path())
}
Ok(())
}
fn output_block(output: &str) -> String {
@@ -266,107 +280,234 @@ fn output_block(output: &str) -> String {
)
}
struct TerminalToolCard {
read_failed: bool,
combined_contents: String,
_task: Task<()>,
}
impl TerminalToolCard {
fn new(mut line_receiver: UnboundedReceiver<Result<String>>, cx: &mut Context<Self>) -> Self {
let _task = cx.spawn(async move |this, cx| {
while let Some(line) = line_receiver.next().await {
let is_entity_released = this
.update(cx, |card, cx| {
let line = match line {
Ok(line) => line,
// TODO: don't we need to log these??
Err(_) => {
card.read_failed = true;
return; // stop receiving
}
};
card.combined_contents += &line;
cx.notify();
})
.is_err();
if is_entity_released {
return;
}
}
});
Self {
read_failed: false,
combined_contents: String::new(),
_task,
}
}
}
impl ToolCard for TerminalToolCard {
fn render(
&mut self,
_status: &ToolUseStatus,
_window: &mut Window,
_workspace: WeakEntity<Workspace>,
_cx: &mut Context<Self>,
) -> impl IntoElement {
format!("text: {}", self.combined_contents)
}
}
impl Component for TerminalTool {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::Agent
}
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), window: &mut Window, cx: &mut App) -> Option<AnyElement> {
enum TerminalToolCardPreviewOperation {
Sleep(u64),
SendLine(&'static str),
}
use TerminalToolCardPreviewOperation::*;
const OPERATIONS: &[TerminalToolCardPreviewOperation] = &[
SendLine("$ ./imaginary-script.sh"),
Sleep(100),
SendLine(""),
Sleep(200),
SendLine(" This"),
Sleep(16),
SendLine(" takes"),
Sleep(1000),
SendLine(" LONG"),
Sleep(100),
SendLine(" to"),
Sleep(300),
SendLine(" finish."),
];
let (mut tx, rx) = unbounded();
let executor = cx.background_executor().clone();
let ccccard = cx.new(|cx| TerminalToolCard::new(rx, cx));
cx.background_spawn(async move {
for operation in OPERATIONS {
match operation {
&Sleep(millis) => executor.timer(Duration::from_millis(millis)).await,
&SendLine(line) => {
let _ = tx.send(Ok(line.to_owned())).await;
}
}
}
})
.detach();
// TODO: add one where it receives a read failure.
Some(
v_flex()
.gap_6()
.children(vec![example_group(vec![single_example(
"No failures (todo naming)",
div()
.size_full()
.child(ccccard.update(cx, |tool, cx| {
tool.render(
&ToolUseStatus::Pending,
window,
WeakEntity::new_invalid(),
cx,
)
.into_any_element()
}))
.into_any_element(),
)])])
.into_any_element(),
)
}
}
#[cfg(test)]
#[cfg(not(windows))]
mod tests {
use gpui::TestAppContext;
use super::*;
// #[gpui::test(iterations = 10)]
// async fn test_run_command_simple(cx: &mut TestAppContext) {
// cx.executor().allow_parking();
#[gpui::test(iterations = 10)]
async fn test_run_command_simple(cx: &mut TestAppContext) {
cx.executor().allow_parking();
// let result =
// spawn_command_and_stream(Path::new(".").into(), "echo 'Hello, World!'".to_string())
// .await;
let result =
run_command_limited(Path::new(".").into(), "echo 'Hello, World!'".to_string()).await;
// assert!(result.is_ok());
// assert_eq!(result.unwrap(), "```\nHello, World!\n```");
// }
assert!(result.is_ok());
assert_eq!(result.unwrap(), "```\nHello, World!\n```");
}
// #[gpui::test(iterations = 10)]
// async fn test_interleaved_stdout_stderr(cx: &mut TestAppContext) {
// cx.executor().allow_parking();
#[gpui::test(iterations = 10)]
async fn test_interleaved_stdout_stderr(cx: &mut TestAppContext) {
cx.executor().allow_parking();
// let command = "echo 'stdout 1' && sleep 0.01 && echo 'stderr 1' >&2 && sleep 0.01 && echo 'stdout 2' && sleep 0.01 && echo 'stderr 2' >&2";
// let result = spawn_command_and_stream(Path::new(".").into(), command.to_string()).await;
let command = "echo 'stdout 1' && sleep 0.01 && echo 'stderr 1' >&2 && sleep 0.01 && echo 'stdout 2' && sleep 0.01 && echo 'stderr 2' >&2";
let result = run_command_limited(Path::new(".").into(), command.to_string()).await;
// assert!(result.is_ok());
// assert_eq!(
// result.unwrap(),
// "```\nstdout 1\nstderr 1\nstdout 2\nstderr 2\n```"
// );
// }
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
"```\nstdout 1\nstderr 1\nstdout 2\nstderr 2\n```"
);
}
// #[gpui::test(iterations = 10)]
// async fn test_multiple_output_reads(cx: &mut TestAppContext) {
// cx.executor().allow_parking();
#[gpui::test(iterations = 10)]
async fn test_multiple_output_reads(cx: &mut TestAppContext) {
cx.executor().allow_parking();
// // Command with multiple outputs that might require multiple reads
// let result = spawn_command_and_stream(
// Path::new(".").into(),
// "echo '1'; sleep 0.01; echo '2'; sleep 0.01; echo '3'".to_string(),
// )
// .await;
// Command with multiple outputs that might require multiple reads
let result = run_command_limited(
Path::new(".").into(),
"echo '1'; sleep 0.01; echo '2'; sleep 0.01; echo '3'".to_string(),
)
.await;
// assert!(result.is_ok());
// assert_eq!(result.unwrap(), "```\n1\n2\n3\n```");
// }
assert!(result.is_ok());
assert_eq!(result.unwrap(), "```\n1\n2\n3\n```");
}
// #[gpui::test(iterations = 10)]
// async fn test_output_truncation_single_line(cx: &mut TestAppContext) {
// cx.executor().allow_parking();
#[gpui::test(iterations = 10)]
async fn test_output_truncation_single_line(cx: &mut TestAppContext) {
cx.executor().allow_parking();
// let cmd = format!(
// "echo '{}'; sleep 0.01;",
// "X".repeat(COMMAND_OUTPUT_LIMIT * 2)
// );
let cmd = format!("echo '{}'; sleep 0.01;", "X".repeat(LIMIT * 2));
// let result = spawn_command_and_stream(Path::new(".").into(), cmd).await;
let result = run_command_limited(Path::new(".").into(), cmd).await;
// assert!(result.is_ok());
// let output = result.unwrap();
assert!(result.is_ok());
let output = result.unwrap();
// let content_start = output.find("```\n").map(|i| i + 4).unwrap_or(0);
// let content_end = output.rfind("\n```").unwrap_or(output.len());
// let content_length = content_end - content_start;
let content_start = output.find("```\n").map(|i| i + 4).unwrap_or(0);
let content_end = output.rfind("\n```").unwrap_or(output.len());
let content_length = content_end - content_start;
// // Output should be exactly the limit
// assert_eq!(content_length, COMMAND_OUTPUT_LIMIT);
// }
// Output should be exactly the limit
assert_eq!(content_length, LIMIT);
}
// #[gpui::test(iterations = 10)]
// async fn test_output_truncation_multiline(cx: &mut TestAppContext) {
// cx.executor().allow_parking();
#[gpui::test(iterations = 10)]
async fn test_output_truncation_multiline(cx: &mut TestAppContext) {
cx.executor().allow_parking();
// let cmd = format!("echo '{}'; ", "X".repeat(120)).repeat(160);
// let result = spawn_command_and_stream(Path::new(".").into(), cmd).await;
let cmd = format!("echo '{}'; ", "X".repeat(120)).repeat(160);
let result = run_command_limited(Path::new(".").into(), cmd).await;
// assert!(result.is_ok());
// let output = result.unwrap();
assert!(result.is_ok());
let output = result.unwrap();
// assert!(output.starts_with("Command output too long. The first 16334 bytes:\n\n"));
assert!(output.starts_with("Command output too long. The first 16334 bytes:\n\n"));
// let content_start = output.find("```\n").map(|i| i + 4).unwrap_or(0);
// let content_end = output.rfind("\n```").unwrap_or(output.len());
// let content_length = content_end - content_start;
let content_start = output.find("```\n").map(|i| i + 4).unwrap_or(0);
let content_end = output.rfind("\n```").unwrap_or(output.len());
let content_length = content_end - content_start;
// assert!(content_length <= COMMAND_OUTPUT_LIMIT);
// }
assert!(content_length <= LIMIT);
}
// #[gpui::test(iterations = 10)]
// async fn test_command_failure(cx: &mut TestAppContext) {
// cx.executor().allow_parking();
#[gpui::test(iterations = 10)]
async fn test_command_failure(cx: &mut TestAppContext) {
cx.executor().allow_parking();
// let result = spawn_command_and_stream(Path::new(".").into(), "exit 42".to_string()).await;
let result = run_command_limited(Path::new(".").into(), "exit 42".to_string()).await;
// assert!(result.is_ok());
// let output = result.unwrap();
assert!(result.is_ok());
let output = result.unwrap();
// // Extract the shell name from path for cleaner test output
// let shell_path = std::env::var("SHELL").unwrap_or("bash".to_string());
// Extract the shell name from path for cleaner test output
let shell_path = std::env::var("SHELL").unwrap_or("bash".to_string());
let expected_output = format!(
"Command failed with exit code 42 (shell: {}).\n\n```\n\n```",
shell_path
);
assert_eq!(output, expected_output);
}
// let expected_output = format!(
// "Command failed with exit code 42 (shell: {}).\n\n```\n\n```",
// shell_path
// );
// assert_eq!(output, expected_output);
// }
}

View File

@@ -23,7 +23,6 @@ pub struct WebSearchToolInput {
query: String,
}
#[derive(RegisterComponent)]
pub struct WebSearchTool;
impl Tool for WebSearchTool {
@@ -84,6 +83,7 @@ impl Tool for WebSearchTool {
}
}
#[derive(RegisterComponent)]
struct WebSearchToolCard {
response: Option<Result<WebSearchResponse>>,
_task: Task<()>,
@@ -185,16 +185,17 @@ impl ToolCard for WebSearchToolCard {
}
}
impl Component for WebSearchTool {
impl Component for WebSearchToolCard {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::Agent
}
fn sort_name() -> &'static str {
"ToolWebSearch"
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
fn preview(_state: &mut (), window: &mut Window, cx: &mut App) -> Option<AnyElement> {
let in_progress_search = cx.new(|cx| WebSearchToolCard {
response: None,
_task: cx.spawn(async move |_this, cx| {

View File

@@ -14,6 +14,7 @@ path = "src/component.rs"
[dependencies]
collections.workspace = true
gpui.workspace = true
itertools.workspace = true
linkme.workspace = true
parking_lot.workspace = true
theme.workspace = true

View File

@@ -1,3 +1,5 @@
use std::any::Any;
use std::collections::BTreeMap;
use std::fmt::Display;
use std::ops::{Deref, DerefMut};
use std::sync::LazyLock;
@@ -7,11 +9,14 @@ use gpui::{
AnyElement, App, IntoElement, RenderOnce, SharedString, Window, div, pattern_slash, prelude::*,
px, rems,
};
use itertools::Itertools;
use linkme::distributed_slice;
use parking_lot::RwLock;
use theme::ActiveTheme;
pub trait Component {
type InitialState;
fn scope() -> ComponentScope {
ComponentScope::None
}
@@ -37,7 +42,14 @@ pub trait Component {
fn description() -> Option<&'static str> {
None
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
/// State for `preview`, should be `()` for stateless components.
fn initial_state(_cx: &mut App) -> Self::InitialState;
/// Render the component.
fn preview(
_initial_state: &mut Self::InitialState,
_window: &mut Window,
_cx: &mut App,
) -> Option<AnyElement> {
None
}
}
@@ -46,28 +58,16 @@ pub trait Component {
pub static __ALL_COMPONENTS: [fn()] = [..];
pub static COMPONENT_DATA: LazyLock<RwLock<ComponentRegistry>> =
LazyLock::new(|| RwLock::new(ComponentRegistry::new()));
LazyLock::new(|| RwLock::new(BTreeMap::default()));
pub struct ComponentRegistry {
components: Vec<(
ComponentScope,
// name
&'static str,
// sort name
&'static str,
// description
Option<&'static str>,
)>,
previews: HashMap<&'static str, fn(&mut Window, &mut App) -> Option<AnyElement>>,
}
pub type ComponentRegistry = BTreeMap<&'static str, ComponentRegistryItem>;
impl ComponentRegistry {
fn new() -> Self {
ComponentRegistry {
components: Vec::new(),
previews: HashMap::default(),
}
}
#[derive(Clone)]
pub struct ComponentRegistryItem {
scope: ComponentScope,
sort_name: &'static str,
description: Option<&'static str>,
preview_helper_creator: PreviewHelperCreator,
}
pub fn init() {
@@ -77,24 +77,69 @@ pub fn init() {
}
}
pub fn register_component<T: Component>() {
let component_data = (T::scope(), T::name(), T::sort_name(), T::description());
let mut data = COMPONENT_DATA.write();
data.components.push(component_data);
data.previews.insert(T::name(), T::preview);
pub fn register_component<T: Component + 'static>() {
let component_data = ComponentRegistryItem {
scope: T::scope(),
sort_name: T::sort_name(),
description: T::description(),
preview_helper_creator: PreviewHelperCreator::new::<T>(),
};
COMPONENT_DATA.write().insert(T::name(), component_data);
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ComponentId(pub &'static str);
#[derive(Clone)]
struct PreviewHelperCreator {
state_initializer: fn(&mut App) -> Box<dyn Any>,
preview_fn: fn(Box<dyn Any>, &mut Window, &mut App) -> Option<AnyElement>,
}
impl PreviewHelperCreator {
fn new<T: Component + Any>() -> Self {
PreviewHelperCreator {
state_initializer: state_initializer_type_erased::<T>,
preview_fn: access_state_and_preview::<T>,
}
}
}
fn state_initializer_type_erased<T: Component + 'static>(cx: &mut App) -> Box<dyn Any> {
Box::new(T::initial_state(cx))
}
fn access_state_and_preview<T: Component + 'static>(
mut initial_state: Box<dyn Any>,
window: &mut Window,
cx: &mut App,
) -> Option<AnyElement> {
let mut state = initial_state.downcast_mut::<T::InitialState>()?;
T::preview(&mut state, window, cx)
}
impl PreviewHelperCreator {
fn create(&self, cx: &mut App) -> PreviewHelper {
PreviewHelper {
state: (self.state_initializer)(cx),
preview_fn: self.preview_fn,
}
}
}
struct PreviewHelper {
state: Box<dyn Any>,
preview_fn: fn(Box<dyn Any>, &mut Window, &mut App) -> Option<AnyElement>,
}
pub struct ComponentMetadata {
id: ComponentId,
name: SharedString,
sort_name: SharedString,
scope: ComponentScope,
description: Option<SharedString>,
preview: Option<fn(&mut Window, &mut App) -> Option<AnyElement>>,
preview_helper: PreviewHelper,
}
impl ComponentMetadata {
@@ -125,33 +170,43 @@ impl ComponentMetadata {
pub fn description(&self) -> Option<SharedString> {
self.description.clone()
}
pub fn preview(&self) -> Option<fn(&mut Window, &mut App) -> Option<AnyElement>> {
self.preview
}
// pub fn preview_helper(&self, cx: &mut App) -> PreviewHelper {
// self.preview_helper_creator.create(cx)
// }
}
pub struct AllComponents(pub HashMap<ComponentId, ComponentMetadata>);
pub struct AllComponents(HashMap<ComponentId, ComponentMetadata>);
impl AllComponents {
pub fn new() -> Self {
AllComponents(HashMap::default())
pub fn new(cx: &mut App) -> Self {
let data = COMPONENT_DATA.read();
let mut map = HashMap::new();
for (name, item) in data.iter() {
let ComponentRegistryItem {
scope,
sort_name,
description,
preview_helper_creator,
} = item.clone();
let id = ComponentId(name);
map.insert(
id.clone(),
ComponentMetadata {
id,
name: SharedString::new(name.to_owned()),
sort_name: SharedString::new(sort_name.to_owned()),
scope,
description: description.map(Into::into),
preview_helper: preview_helper_creator.create(cx),
},
);
}
Self(map)
}
pub fn all_previews(&self) -> Vec<&ComponentMetadata> {
self.0.values().filter(|c| c.preview.is_some()).collect()
}
pub fn all_previews_sorted(&self) -> Vec<ComponentMetadata> {
let mut previews: Vec<ComponentMetadata> =
self.all_previews().into_iter().cloned().collect();
previews.sort_by_key(|a| a.name());
previews
}
pub fn all(&self) -> Vec<&ComponentMetadata> {
self.0.values().collect()
}
pub fn all_sorted(&self) -> Vec<ComponentMetadata> {
let mut components: Vec<ComponentMetadata> = self.all().into_iter().cloned().collect();
components.sort_by_key(|a| a.name());
components
pub fn all_sorted(&self) -> Vec<&ComponentMetadata> {
self.values().sorted_by_key(|a| a.name()).collect()
}
}
@@ -168,29 +223,6 @@ impl DerefMut for AllComponents {
}
}
pub fn components() -> AllComponents {
let data = COMPONENT_DATA.read();
let mut all_components = AllComponents::new();
for (scope, name, sort_name, description) in &data.components {
let preview = data.previews.get(name).cloned();
let component_name = SharedString::new_static(name);
let sort_name = SharedString::new_static(sort_name);
let id = ComponentId(name);
all_components.insert(
id.clone(),
ComponentMetadata {
id,
name: component_name,
sort_name,
scope: scope.clone(),
description: description.map(Into::into),
preview,
},
);
}
all_components
}
// #[derive(Debug, Clone, PartialEq, Eq, Hash)]
// pub enum ComponentStatus {
// WorkInProgress,

View File

@@ -191,8 +191,8 @@ impl ComponentPreview {
cx.notify();
}
fn get_component(&self, ix: usize) -> ComponentMetadata {
self.components[ix].clone()
fn get_component(&self, ix: usize) -> Option<ComponentMetadata> {
self.components.get(ix)
}
fn filtered_components(&self) -> Vec<ComponentMetadata> {

View File

@@ -4494,11 +4494,16 @@ impl RenderOnce for PanelRepoFooter {
}
impl Component for PanelRepoFooter {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::VersionControl
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
let unknown_upstream = None;
let no_remote_upstream = Some(UpstreamTracking::Gone);
let ahead_of_upstream = Some(

View File

@@ -493,11 +493,16 @@ impl RenderOnce for GitStatusIcon {
// View this component preview using `workspace: open component-preview`
impl Component for GitStatusIcon {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::VersionControl
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn tracked_file_status(code: StatusCode) -> FileStatus {
FileStatus::Tracked(git::status::TrackedStatus {
index_status: code,

View File

@@ -1142,11 +1142,16 @@ mod preview {
// View this component preview using `workspace: open component-preview`
impl Component for ProjectDiffEmptyState {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::VersionControl
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
let unknown_upstream: Option<UpstreamTracking> = None;
let ahead_of_upstream: Option<UpstreamTracking> = Some(
UpstreamTrackingStatus {

View File

@@ -135,11 +135,16 @@ impl Focusable for StatusToast {
impl EventEmitter<DismissEvent> for StatusToast {}
impl Component for StatusToast {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::Notification
}
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, cx: &mut App) -> Option<AnyElement> {
let text_example = StatusToast::new("Operation completed", cx, |this, _| this);
let action_example = StatusToast::new("Update ready to install", cx, |this, _cx| {

View File

@@ -514,7 +514,7 @@ impl Project {
terminal_handle: &Entity<Terminal>,
cx: &mut App,
) {
terminal_handle.update(cx, |terminal, _| terminal.input_bytes(command.into_bytes()));
terminal_handle.update(cx, |terminal, _| terminal.input(command));
}
pub fn local_terminal_handles(&self) -> &Vec<WeakEntity<terminal::Terminal>> {

View File

@@ -1216,15 +1216,11 @@ impl Terminal {
}
///Write the Input payload to the tty.
fn write_to_pty(&self, input: String) {
self.pty_tx.notify(input.into_bytes());
fn write_to_pty(&self, input: impl Into<Vec<u8>>) {
self.pty_tx.notify(input.into());
}
fn write_bytes_to_pty(&self, input: Vec<u8>) {
self.pty_tx.notify(input);
}
pub fn input(&mut self, input: String) {
pub fn input(&mut self, input: impl Into<Vec<u8>>) {
self.events
.push_back(InternalEvent::Scroll(AlacScroll::Bottom));
self.events.push_back(InternalEvent::SetSelection(None));
@@ -1232,14 +1228,6 @@ impl Terminal {
self.write_to_pty(input);
}
pub fn input_bytes(&mut self, input: Vec<u8>) {
self.events
.push_back(InternalEvent::Scroll(AlacScroll::Bottom));
self.events.push_back(InternalEvent::SetSelection(None));
self.write_bytes_to_pty(input);
}
pub fn toggle_vi_mode(&mut self) {
self.events.push_back(InternalEvent::ToggleViMode);
}

View File

@@ -1036,7 +1036,7 @@ impl InputHandler for TerminalInputHandler {
cx: &mut App,
) {
self.terminal.update(cx, |terminal, _| {
terminal.input(text.into());
terminal.input(text);
});
self.workspace

View File

@@ -1133,7 +1133,7 @@ async fn wait_for_terminals_tasks(
})
.ok()
});
let _: Vec<_> = join_all(pending_tasks).await;
join_all(pending_tasks).await;
}
fn add_paths_to_terminal(

View File

@@ -221,6 +221,7 @@ impl RenderOnce for AvatarAvailabilityIndicator {
// View this component preview using `workspace: open component-preview`
impl Component for Avatar {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::Collaboration
}
@@ -229,7 +230,11 @@ impl Component for Avatar {
Some(Avatar::DOCS)
}
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, cx: &mut App) -> Option<AnyElement> {
let example_avatar = "https://avatars.githubusercontent.com/u/1714999?v=4";
Some(

View File

@@ -137,11 +137,16 @@ impl RenderOnce for Banner {
}
impl Component for Banner {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::Notification
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
let severity_examples = vec![
single_example(
"Default",

View File

@@ -466,6 +466,7 @@ impl RenderOnce for Button {
// View this component preview using `workspace: open component-preview`
impl Component for Button {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::Input
}
@@ -478,7 +479,11 @@ impl Component for Button {
Some("A button triggers an event or action.")
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()

View File

@@ -120,6 +120,7 @@ impl RenderOnce for ButtonIcon {
}
impl Component for ButtonIcon {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::Input
}
@@ -132,7 +133,11 @@ impl Component for ButtonIcon {
Some("An icon component specifically designed for use within buttons.")
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()

View File

@@ -590,6 +590,7 @@ impl RenderOnce for ButtonLike {
}
impl Component for ButtonLike {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::Input
}
@@ -603,7 +604,11 @@ impl Component for ButtonLike {
Some(ButtonLike::DOCS)
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()

View File

@@ -210,6 +210,7 @@ impl RenderOnce for IconButton {
}
impl Component for IconButton {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::Input
}
@@ -218,7 +219,11 @@ impl Component for IconButton {
"ButtonB"
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()

View File

@@ -155,6 +155,7 @@ impl RenderOnce for ToggleButton {
}
impl Component for ToggleButton {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::Input
}
@@ -163,7 +164,11 @@ impl Component for ToggleButton {
"ButtonC"
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()

View File

@@ -87,6 +87,7 @@ impl RenderOnce for ContentGroup {
}
impl Component for ContentGroup {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::Layout
}
@@ -95,7 +96,11 @@ impl Component for ContentGroup {
Some(ContentGroup::DOCS)
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
example_group(vec![
single_example(

View File

@@ -94,6 +94,7 @@ impl RenderOnce for Disclosure {
}
impl Component for Disclosure {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::Navigation
}
@@ -104,7 +105,11 @@ impl Component for Disclosure {
)
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()

View File

@@ -160,6 +160,7 @@ impl Divider {
}
impl Component for Divider {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::Layout
}
@@ -170,7 +171,11 @@ impl Component for Divider {
)
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()

View File

@@ -73,6 +73,7 @@ impl RenderOnce for DropdownMenu {
}
impl Component for DropdownMenu {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::Input
}
@@ -87,7 +88,11 @@ impl Component for DropdownMenu {
)
}
fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), window: &mut Window, cx: &mut App) -> Option<AnyElement> {
let menu = ContextMenu::build(window, cx, |this, _, _| {
this.entry("Option 1", None, |_, _| {})
.entry("Option 2", None, |_, _| {})

View File

@@ -88,6 +88,7 @@ pub const EXAMPLE_FACES: [&'static str; 6] = [
];
impl Component for Facepile {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::Collaboration
}
@@ -98,7 +99,11 @@ impl Component for Facepile {
)
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()

View File

@@ -266,6 +266,7 @@ impl RenderOnce for IconWithIndicator {
}
impl Component for Icon {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::Images
}
@@ -276,7 +277,11 @@ impl Component for Icon {
)
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()

View File

@@ -25,6 +25,7 @@ impl RenderOnce for DecoratedIcon {
}
impl Component for DecoratedIcon {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::Images
}
@@ -35,7 +36,11 @@ impl Component for DecoratedIcon {
)
}
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, cx: &mut App) -> Option<AnyElement> {
let decoration_x = IconDecoration::new(
IconDecorationKind::X,
cx.theme().colors().surface_background,

View File

@@ -84,6 +84,7 @@ impl RenderOnce for Vector {
}
impl Component for Vector {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::Images
}
@@ -96,7 +97,11 @@ impl Component for Vector {
Some("A vector image component that can be displayed at specific sizes.")
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()

View File

@@ -85,6 +85,7 @@ impl RenderOnce for Indicator {
}
impl Component for Indicator {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::Status
}
@@ -95,7 +96,11 @@ impl Component for Indicator {
)
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()

View File

@@ -445,6 +445,7 @@ fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode
}
impl Component for KeyBinding {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::Typography
}
@@ -459,7 +460,11 @@ impl Component for KeyBinding {
)
}
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()

View File

@@ -206,6 +206,7 @@ impl RenderOnce for KeybindingHint {
}
impl Component for KeybindingHint {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::None
}
@@ -214,7 +215,11 @@ impl Component for KeybindingHint {
Some("Displays a keyboard shortcut hint with optional prefix and suffix text")
}
fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), window: &mut Window, cx: &mut App) -> Option<AnyElement> {
let enter_fallback = gpui::KeyBinding::new("enter", menu::Confirm, None);
let enter = KeyBinding::for_action(&menu::Confirm, window, cx)
.unwrap_or(KeyBinding::new(enter_fallback, cx));

View File

@@ -136,6 +136,7 @@ impl RenderOnce for HighlightedLabel {
}
impl Component for HighlightedLabel {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::Typography
}
@@ -148,7 +149,11 @@ impl Component for HighlightedLabel {
Some("A label with highlighted characters based on specified indices.")
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()

View File

@@ -204,6 +204,7 @@ impl RenderOnce for Label {
}
impl Component for Label {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::Typography
}
@@ -212,7 +213,11 @@ impl Component for Label {
Some("A text label component that supports various styles, sizes, and formatting options.")
}
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()

View File

@@ -247,6 +247,7 @@ impl RenderOnce for LabelLike {
}
impl Component for LabelLike {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::Typography
}
@@ -261,7 +262,11 @@ impl Component for LabelLike {
)
}
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()

View File

@@ -77,6 +77,7 @@ impl ParentElement for AlertModal {
}
impl Component for AlertModal {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::Notification
}
@@ -85,7 +86,11 @@ impl Component for AlertModal {
Some("A modal dialog that presents an alert message with primary and dismiss actions.")
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()

View File

@@ -81,6 +81,7 @@ impl RenderOnce for ProgressBar {
}
impl Component for ProgressBar {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::Status
}
@@ -89,7 +90,11 @@ impl Component for ProgressBar {
Some(Self::DOCS)
}
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, cx: &mut App) -> Option<AnyElement> {
let max_value = 180.0;
Some(

View File

@@ -37,6 +37,7 @@ impl RenderOnce for SettingsContainer {
}
impl Component for SettingsContainer {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::Layout
}
@@ -49,7 +50,11 @@ impl Component for SettingsContainer {
Some("A container for organizing and displaying settings in a structured manner.")
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()

View File

@@ -38,6 +38,7 @@ impl RenderOnce for SettingsGroup {
}
impl Component for SettingsGroup {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::Layout
}
@@ -50,7 +51,11 @@ impl Component for SettingsGroup {
Some("A group of settings with a header, used to organize related settings.")
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()

View File

@@ -178,6 +178,7 @@ impl RenderOnce for Tab {
}
impl Component for Tab {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::None
}
@@ -188,7 +189,11 @@ impl Component for Tab {
)
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()

View File

@@ -153,6 +153,7 @@ impl RenderOnce for TabBar {
}
impl Component for TabBar {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::Navigation
}
@@ -165,7 +166,11 @@ impl Component for TabBar {
Some("A horizontal bar containing tabs for navigation between different views or sections.")
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()

View File

@@ -152,6 +152,7 @@ where
}
impl Component for Table {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::Layout
}
@@ -160,7 +161,11 @@ impl Component for Table {
Some("A table component for displaying data in rows and columns with optional styling.")
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()

View File

@@ -599,6 +599,7 @@ impl RenderOnce for SwitchWithLabel {
}
impl Component for Checkbox {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::Input
}
@@ -607,7 +608,11 @@ impl Component for Checkbox {
Some("A checkbox component that can be used for multiple choice selections")
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()
@@ -704,6 +709,7 @@ impl Component for Checkbox {
}
impl Component for Switch {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::Input
}
@@ -712,7 +718,11 @@ impl Component for Switch {
Some("A switch component that represents binary states like on/off")
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()
@@ -823,6 +833,7 @@ impl Component for Switch {
}
impl Component for CheckboxWithLabel {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::Input
}
@@ -831,7 +842,11 @@ impl Component for CheckboxWithLabel {
Some("A checkbox component with an attached label")
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_6()

View File

@@ -227,6 +227,7 @@ impl Render for LinkPreview {
}
impl Component for Tooltip {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::None
}
@@ -237,7 +238,11 @@ impl Component for Tooltip {
)
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
example_group(vec![single_example(
"Text only",

View File

@@ -98,6 +98,7 @@ impl<E: Styled> DefaultAnimations for E {}
struct Animation {}
impl Component for Animation {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::None
}
@@ -106,7 +107,11 @@ impl Component for Animation {
Some("Demonstrates various animation patterns and transitions available in the UI system.")
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
let container_size = 128.0;
let element_size = 32.0;
let left_offset = element_size - container_size / 2.0;

View File

@@ -125,6 +125,7 @@ impl From<Hsla> for Color {
}
impl Component for Color {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::None
}
@@ -133,7 +134,15 @@ impl Component for Color {
Some(Color::DOCS)
}
fn preview(_window: &mut gpui::Window, _cx: &mut App) -> Option<gpui::AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(
_state: &mut (),
_window: &mut gpui::Window,
_cx: &mut App,
) -> Option<gpui::AnyElement> {
Some(
v_flex()
.gap_6()

View File

@@ -234,6 +234,7 @@ impl Headline {
}
impl Component for Headline {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::Typography
}
@@ -242,7 +243,11 @@ impl Component for Headline {
Some("A headline element used to emphasize text and create visual hierarchy in the UI.")
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), _window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
v_flex()
.gap_1()

View File

@@ -168,11 +168,16 @@ impl Render for SingleLineInput {
}
impl Component for SingleLineInput {
type InitialState = ();
fn scope() -> ComponentScope {
ComponentScope::Input
}
fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
fn initial_state(_cx: &mut App) -> Self::InitialState {
()
}
fn preview(_state: &mut (), window: &mut Window, cx: &mut App) -> Option<AnyElement> {
let input_1 =
cx.new(|cx| SingleLineInput::new(window, cx, "placeholder").label("Some Label"));