prompt.rs

  1use fs::Fs;
  2use language::BufferSnapshot;
  3use std::{fmt::Write, ops::Range, path::PathBuf, sync::Arc};
  4use ui::SharedString;
  5use util::paths::PROMPTS_DIR;
  6
  7use gray_matter::{engine::YAML, Matter};
  8use serde::{Deserialize, Serialize};
  9
 10use super::prompt_library::PromptId;
 11
 12pub const PROMPT_DEFAULT_TITLE: &str = "Untitled Prompt";
 13
 14fn standardize_value(value: String) -> String {
 15    value.replace(['\n', '\r', '"', '\''], "")
 16}
 17
 18fn slugify(input: String) -> String {
 19    let mut slug = String::new();
 20    for c in input.chars() {
 21        if c.is_alphanumeric() {
 22            slug.push(c.to_ascii_lowercase());
 23        } else if c.is_whitespace() {
 24            slug.push('-');
 25        } else {
 26            slug.push('_');
 27        }
 28    }
 29    slug
 30}
 31
 32#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
 33pub struct StaticPromptFrontmatter {
 34    title: String,
 35    version: String,
 36    author: String,
 37    #[serde(default)]
 38    languages: Vec<String>,
 39    #[serde(default)]
 40    dependencies: Vec<String>,
 41}
 42
 43impl Default for StaticPromptFrontmatter {
 44    fn default() -> Self {
 45        Self {
 46            title: PROMPT_DEFAULT_TITLE.to_string(),
 47            version: "1.0".to_string(),
 48            author: "You <you@email.com>".to_string(),
 49            languages: vec![],
 50            dependencies: vec![],
 51        }
 52    }
 53}
 54
 55impl StaticPromptFrontmatter {
 56    /// Returns the frontmatter as a markdown frontmatter string
 57    pub fn frontmatter_string(&self) -> String {
 58        let mut frontmatter = format!(
 59            "---\ntitle: \"{}\"\nversion: \"{}\"\nauthor: \"{}\"\n",
 60            standardize_value(self.title.clone()),
 61            standardize_value(self.version.clone()),
 62            standardize_value(self.author.clone()),
 63        );
 64
 65        if !self.languages.is_empty() {
 66            let languages = self
 67                .languages
 68                .iter()
 69                .map(|l| standardize_value(l.clone()))
 70                .collect::<Vec<String>>()
 71                .join(", ");
 72            writeln!(frontmatter, "languages: [{}]", languages).unwrap();
 73        }
 74
 75        if !self.dependencies.is_empty() {
 76            let dependencies = self
 77                .dependencies
 78                .iter()
 79                .map(|d| standardize_value(d.clone()))
 80                .collect::<Vec<String>>()
 81                .join(", ");
 82            writeln!(frontmatter, "dependencies: [{}]", dependencies).unwrap();
 83        }
 84
 85        frontmatter.push_str("---\n");
 86
 87        frontmatter
 88    }
 89}
 90
 91/// A static prompt that can be loaded into the prompt library
 92/// from Markdown with a frontmatter header
 93///
 94/// Examples:
 95///
 96/// ### Globally available prompt
 97///
 98/// ```markdown
 99/// ---
100/// title: Foo
101/// version: 1.0
102/// author: Jane Kim <jane@kim.com
103/// languages: ["*"]
104/// dependencies: []
105/// ---
106///
107/// Foo and bar are terms used in programming to describe generic concepts.
108/// ```
109///
110/// ### Language-specific prompt
111///
112/// ```markdown
113/// ---
114/// title: UI with GPUI
115/// version: 1.0
116/// author: Nate Butler <iamnbutler@gmail.com>
117/// languages: ["rust"]
118/// dependencies: ["gpui"]
119/// ---
120///
121/// When building a UI with GPUI, ensure you...
122/// ```
123#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
124pub struct StaticPrompt {
125    #[serde(skip_deserializing)]
126    id: PromptId,
127    #[serde(skip)]
128    metadata: StaticPromptFrontmatter,
129    content: String,
130    file_name: Option<SharedString>,
131}
132
133impl Default for StaticPrompt {
134    fn default() -> Self {
135        let metadata = StaticPromptFrontmatter::default();
136
137        let content = metadata.clone().frontmatter_string();
138
139        Self {
140            id: PromptId::new(),
141            metadata,
142            content,
143            file_name: None,
144        }
145    }
146}
147
148impl StaticPrompt {
149    pub fn new(content: String, file_name: Option<String>) -> Self {
150        let matter = Matter::<YAML>::new();
151        let result = matter.parse(&content);
152        let file_name = if let Some(file_name) = file_name {
153            let shared_filename: SharedString = file_name.into();
154            Some(shared_filename)
155        } else {
156            None
157        };
158
159        let metadata = result
160            .data
161            .map_or_else(
162                || Err(anyhow::anyhow!("Failed to parse frontmatter")),
163                |data| {
164                    let front_matter: StaticPromptFrontmatter = data.deserialize()?;
165                    Ok(front_matter)
166                },
167            )
168            .unwrap_or_else(|e| {
169                if let Some(file_name) = &file_name {
170                    log::error!("Failed to parse frontmatter for {}: {}", file_name, e);
171                } else {
172                    log::error!("Failed to parse frontmatter: {}", e);
173                }
174                StaticPromptFrontmatter::default()
175            });
176
177        let id = if let Some(file_name) = &file_name {
178            PromptId::from_str(file_name).unwrap_or_default()
179        } else {
180            PromptId::new()
181        };
182
183        StaticPrompt {
184            id,
185            content,
186            file_name,
187            metadata,
188        }
189    }
190
191    pub fn update(&mut self, id: PromptId, content: String) {
192        let mut updated_prompt =
193            StaticPrompt::new(content, self.file_name.clone().map(|s| s.to_string()));
194        updated_prompt.id = id;
195        *self = updated_prompt;
196    }
197}
198
199impl StaticPrompt {
200    /// Returns the prompt's id
201    pub fn id(&self) -> &PromptId {
202        &self.id
203    }
204
205    pub fn file_name(&self) -> Option<&SharedString> {
206        self.file_name.as_ref()
207    }
208
209    /// Sets the file name of the prompt
210    pub fn new_file_name(&self) -> String {
211        let in_name = format!(
212            "{}_{}_{}",
213            standardize_value(self.metadata.title.clone()),
214            standardize_value(self.metadata.version.clone()),
215            standardize_value(self.id.0.to_string())
216        );
217        let out_name = slugify(in_name);
218        out_name
219    }
220
221    /// Returns the prompt's content
222    pub fn content(&self) -> &String {
223        &self.content
224    }
225
226    /// Returns the prompt's metadata
227    pub fn _metadata(&self) -> &StaticPromptFrontmatter {
228        &self.metadata
229    }
230
231    /// Returns the prompt's title
232    pub fn title(&self) -> SharedString {
233        self.metadata.title.clone().into()
234    }
235
236    pub fn body(&self) -> String {
237        let matter = Matter::<YAML>::new();
238        let result = matter.parse(self.content.as_str());
239        result.content.clone()
240    }
241
242    pub fn path(&self) -> Option<PathBuf> {
243        if let Some(file_name) = self.file_name() {
244            let path_str = format!("{}", file_name);
245            Some(PROMPTS_DIR.join(path_str))
246        } else {
247            None
248        }
249    }
250
251    pub async fn save(&self, fs: Arc<dyn Fs>) -> anyhow::Result<()> {
252        let file_name = self.file_name();
253        let new_file_name = self.new_file_name();
254
255        let out_name = if let Some(file_name) = file_name {
256            file_name.to_owned().to_string()
257        } else {
258            format!("{}.md", new_file_name)
259        };
260        let path = PROMPTS_DIR.join(&out_name);
261        let json = self.content.clone();
262
263        fs.atomic_write(path, json).await?;
264
265        Ok(())
266    }
267}
268
269pub fn generate_content_prompt(
270    user_prompt: String,
271    language_name: Option<&str>,
272    buffer: BufferSnapshot,
273    range: Range<usize>,
274    project_name: Option<String>,
275) -> anyhow::Result<String> {
276    let mut prompt = String::new();
277
278    let content_type = match language_name {
279        None | Some("Markdown" | "Plain Text") => {
280            writeln!(prompt, "You are an expert engineer.")?;
281            "Text"
282        }
283        Some(language_name) => {
284            writeln!(prompt, "You are an expert {language_name} engineer.")?;
285            writeln!(
286                prompt,
287                "Your answer MUST always and only be valid {}.",
288                language_name
289            )?;
290            "Code"
291        }
292    };
293
294    if let Some(project_name) = project_name {
295        writeln!(
296            prompt,
297            "You are currently working inside the '{project_name}' project in code editor Zed."
298        )?;
299    }
300
301    // Include file content.
302    for chunk in buffer.text_for_range(0..range.start) {
303        prompt.push_str(chunk);
304    }
305
306    if range.is_empty() {
307        prompt.push_str("<|START|>");
308    } else {
309        prompt.push_str("<|START|");
310    }
311
312    for chunk in buffer.text_for_range(range.clone()) {
313        prompt.push_str(chunk);
314    }
315
316    if !range.is_empty() {
317        prompt.push_str("|END|>");
318    }
319
320    for chunk in buffer.text_for_range(range.end..buffer.len()) {
321        prompt.push_str(chunk);
322    }
323
324    prompt.push('\n');
325
326    if range.is_empty() {
327        writeln!(
328            prompt,
329            "Assume the cursor is located where the `<|START|>` span is."
330        )
331        .unwrap();
332        writeln!(
333            prompt,
334            "{content_type} can't be replaced, so assume your answer will be inserted at the cursor.",
335        )
336        .unwrap();
337        writeln!(
338            prompt,
339            "Generate {content_type} based on the users prompt: {user_prompt}",
340        )
341        .unwrap();
342    } else {
343        writeln!(prompt, "Modify the user's selected {content_type} based upon the users prompt: '{user_prompt}'").unwrap();
344        writeln!(prompt, "You must reply with only the adjusted {content_type} (within the '<|START|' and '|END|>' spans) not the entire file.").unwrap();
345        writeln!(
346            prompt,
347            "Double check that you only return code and not the '<|START|' and '|END|'> spans"
348        )
349        .unwrap();
350    }
351
352    writeln!(prompt, "Never make remarks about the output.").unwrap();
353    writeln!(
354        prompt,
355        "Do not return anything else, except the generated {content_type}."
356    )
357    .unwrap();
358
359    Ok(prompt)
360}