diff --git a/Cargo.lock b/Cargo.lock index a99e38a30b..89eb7efd20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7607,6 +7607,14 @@ dependencies = [ "workspace", ] +[[package]] +name = "perplexity" +version = "0.1.0" +dependencies = [ + "serde", + "zed_extension_api 0.1.0", +] + [[package]] name = "pest" version = "2.7.11" diff --git a/Cargo.toml b/Cargo.toml index e6aa17c503..3a143f33f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -146,6 +146,7 @@ members = [ "extensions/lua", "extensions/ocaml", "extensions/php", + "extensions/perplexity", "extensions/prisma", "extensions/purescript", "extensions/ruff", diff --git a/extensions/perplexity/Cargo.toml b/extensions/perplexity/Cargo.toml new file mode 100644 index 0000000000..ea10214281 --- /dev/null +++ b/extensions/perplexity/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "perplexity" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" + +[lib] +path = "src/perplexity.rs" +crate-type = ["cdylib"] + +[lints] +workspace = true + +[dependencies] +serde = "1" +zed_extension_api = { path = "../../crates/extension_api" } diff --git a/extensions/perplexity/LICENSE-APACHE b/extensions/perplexity/LICENSE-APACHE new file mode 120000 index 0000000000..1cd601d0a3 --- /dev/null +++ b/extensions/perplexity/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/extensions/perplexity/extension.toml b/extensions/perplexity/extension.toml new file mode 100644 index 0000000000..205f8a5cc2 --- /dev/null +++ b/extensions/perplexity/extension.toml @@ -0,0 +1,12 @@ +id = "perplexity" +name = "Perplexity" +version = "0.1.0" +description = "Ask questions to Perplexity AI directly from Zed" +authors = ["Zed Industries "] +repository = "https://github.com/zed-industries/zed-perplexity" +schema_version = 1 + +[slash_commands.perplexity] +description = "Ask a question to Perplexity AI" +requires_argument = true +tooltip_text = "Ask Perplexity" diff --git a/extensions/perplexity/src/perplexity.rs b/extensions/perplexity/src/perplexity.rs new file mode 100644 index 0000000000..1772793cde --- /dev/null +++ b/extensions/perplexity/src/perplexity.rs @@ -0,0 +1,158 @@ +use zed::{ + http_client::HttpMethod, + http_client::HttpRequest, + serde_json::{self, json}, +}; +use zed_extension_api::{self as zed, http_client::RedirectPolicy, Result}; + +struct Perplexity; + +impl zed::Extension for Perplexity { + fn new() -> Self { + Self + } + + fn run_slash_command( + &self, + command: zed::SlashCommand, + argument: Vec, + worktree: Option<&zed::Worktree>, + ) -> zed::Result { + // Check if the command is 'perplexity' + if command.name != "perplexity" { + return Err("Invalid command. Expected 'perplexity'.".into()); + } + + let worktree = worktree.ok_or("Worktree is required")?; + // Join arguments with space as the query + let query = argument.join(" "); + if query.is_empty() { + return Ok(zed::SlashCommandOutput { + text: "Error: Query not provided. Please enter a question or topic.".to_string(), + sections: vec![], + }); + } + + // Get the API key from the environment + let env_vars = worktree.shell_env(); + let api_key = env_vars + .iter() + .find(|(key, _)| key == "PERPLEXITY_API_KEY") + .map(|(_, value)| value.clone()) + .ok_or("PERPLEXITY_API_KEY not found in environment")?; + + // Prepare the request + let request = HttpRequest { + method: HttpMethod::Post, + url: "https://api.perplexity.ai/chat/completions".to_string(), + headers: vec![ + ("Authorization".to_string(), format!("Bearer {}", api_key)), + ("Content-Type".to_string(), "application/json".to_string()), + ], + body: Some( + serde_json::to_vec(&json!({ + "model": "llama-3.1-sonar-small-128k-online", + "messages": [{"role": "user", "content": query}], + "stream": true, + })) + .unwrap(), + ), + redirect_policy: RedirectPolicy::FollowAll, + }; + + // Make the HTTP request + match zed::http_client::fetch_stream(&request) { + Ok(stream) => { + let mut full_content = String::new(); + let mut buffer = String::new(); + while let Ok(Some(chunk)) = stream.next_chunk() { + buffer.push_str(&String::from_utf8_lossy(&chunk)); + for line in buffer.lines() { + if let Some(json) = line.strip_prefix("data: ") { + if let Ok(event) = serde_json::from_str::(json) { + if let Some(choice) = event.choices.first() { + full_content.push_str(&choice.delta.content); + } + } + } + } + buffer.clear(); + } + Ok(zed::SlashCommandOutput { + text: full_content, + sections: vec![], + }) + } + Err(e) => Ok(zed::SlashCommandOutput { + text: format!("API request failed. Error: {}. API Key: {}", e, api_key), + sections: vec![], + }), + } + } + + fn complete_slash_command_argument( + &self, + _command: zed::SlashCommand, + query: Vec, + ) -> zed::Result> { + let suggestions = vec!["How do I develop a Zed extension?"]; + let query = query.join(" ").to_lowercase(); + + Ok(suggestions + .into_iter() + .filter(|suggestion| suggestion.to_lowercase().contains(&query)) + .map(|suggestion| zed::SlashCommandArgumentCompletion { + label: suggestion.to_string(), + new_text: suggestion.to_string(), + run_command: true, + }) + .collect()) + } + + fn language_server_command( + &mut self, + _language_server_id: &zed_extension_api::LanguageServerId, + _worktree: &zed_extension_api::Worktree, + ) -> Result { + Err("Not implemented".into()) + } +} + +#[derive(serde::Deserialize)] +struct StreamEvent { + id: String, + model: String, + created: u64, + usage: Usage, + object: String, + choices: Vec, +} + +#[derive(serde::Deserialize)] +struct Usage { + prompt_tokens: u32, + completion_tokens: u32, + total_tokens: u32, +} + +#[derive(serde::Deserialize)] +struct Choice { + index: u32, + finish_reason: Option, + message: Message, + delta: Delta, +} + +#[derive(serde::Deserialize)] +struct Message { + role: String, + content: String, +} + +#[derive(serde::Deserialize)] +struct Delta { + role: String, + content: String, +} + +zed::register_extension!(Perplexity);