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}