Skip to content
This repository was archived by the owner on Feb 18, 2025. It is now read-only.

Commit 8426a61

Browse files
committed
New feature: generate project by AI
1 parent a651674 commit 8426a61

6 files changed

Lines changed: 142 additions & 19 deletions

File tree

crates/cursor-core/src/conversation/models/mod.rs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,9 @@ pub(super) mod user_message;
55
pub(super) mod user_request;
66

77
pub(super) use bot_message::*;
8-
use rand::Rng;
98
pub(super) use request_body::*;
109
pub(super) use user_request::*;
1110

12-
fn random() -> i32 {
13-
rand::thread_rng().gen_range(0..=1000)
14-
}
15-
1611
// Split the code into chunks of 20 line blocks.
1712
pub fn split_code_into_blocks(code: &str) -> Vec<String> {
1813
let lines = code.split("\n");
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
use wasm_bindgen::prelude::*;
2+
3+
#[wasm_bindgen(typescript_custom_section)]
4+
const IPROJECT_HANDLER: &'static str = r#"
5+
interface IProjectHandler {
6+
async createFileRecursive(path: string): void;
7+
makeFileWriter(path: string): IProjectFileWriter | undefined;
8+
}
9+
"#;
10+
11+
#[wasm_bindgen]
12+
extern "C" {
13+
#[wasm_bindgen(typescript_type = "IProjectHandler")]
14+
pub type ProjectHandler;
15+
16+
#[wasm_bindgen(method, structural, js_name = createFileRecursive)]
17+
pub async fn create_file_recursive(this: &ProjectHandler, path: &str);
18+
19+
#[wasm_bindgen(method, structural, js_name = makeFileWriter)]
20+
pub fn make_file_writer(this: &ProjectHandler, path: &str) -> Option<ProjectFileWriter>;
21+
}
22+
23+
#[wasm_bindgen(typescript_custom_section)]
24+
const IPROJECT_FILE_WRITER: &'static str = r#"
25+
interface IProjectFileWriter {
26+
write(contents: string): void;
27+
end(): void;
28+
}
29+
"#;
30+
31+
#[wasm_bindgen]
32+
extern "C" {
33+
#[wasm_bindgen(typescript_type = "IProjectFileWriter")]
34+
pub type ProjectFileWriter;
35+
36+
#[wasm_bindgen(method, structural)]
37+
pub fn write(this: &ProjectFileWriter, contents: &str);
38+
39+
#[wasm_bindgen(method, structural)]
40+
pub fn end(this: &ProjectFileWriter);
41+
}

crates/cursor-core/src/project/mod.rs

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
mod handler;
2+
13
use futures::StreamExt;
24
use node_bridge::{bindings::AbortSignal, prelude::*};
35
use serde_json::json;
@@ -12,6 +14,8 @@ use crate::{
1214
request::stream::{make_stream_request, StreamResponseState},
1315
};
1416

17+
use self::handler::ProjectHandler;
18+
1519
const STEP_MESSAGE: &str = "cursor-step";
1620
const CREATE_MESSAGE: &str = "cursor-create";
1721
const APPEND_MESSAGE: &str = "cursor-append";
@@ -35,7 +39,7 @@ impl Task {
3539
}
3640

3741
#[wasm_bindgen(js_name = generateProject)]
38-
pub async fn generate_project(prompt: &str) -> Result<JsValue, JsValue> {
42+
pub async fn generate_project(prompt: &str, handler: ProjectHandler) -> Result<JsValue, JsValue> {
3943
let prompt = prompt.to_owned();
4044
Ok(get_extension_context()
4145
.with_progress(
@@ -53,6 +57,7 @@ pub async fn generate_project(prompt: &str) -> Result<JsValue, JsValue> {
5357
.into();
5458
let mut data_stream = state.data_stream();
5559
let mut current_task = None;
60+
let mut file_writer = None;
5661
while let Some(data) = data_stream.next().await {
5762
#[cfg(debug_assertions)]
5863
console::log_str(&data);
@@ -65,30 +70,39 @@ pub async fn generate_project(prompt: &str) -> Result<JsValue, JsValue> {
6570
current_task = Some(Task::Step(task.to_owned()));
6671
} else if data.starts_with(CREATE_MESSAGE) {
6772
let task = data[CREATE_MESSAGE.len() + 1..].trim();
68-
current_task = Some(Task::Create(task.to_owned()));
73+
current_task = Some(Task::Create(format!("Creating {}", task)));
74+
75+
// The title of the "create" message is a file path,
76+
// which requires creating a file based on the path.
77+
handler.create_file_recursive(task).await;
6978
} else if data.starts_with(APPEND_MESSAGE) {
7079
let task = data[APPEND_MESSAGE.len() + 1..].trim();
71-
current_task = Some(Task::Append(task.to_owned()));
80+
current_task =
81+
Some(Task::Append(format!("Appending contents to {}", task)));
82+
83+
file_writer = handler.make_file_writer(task);
7284
} else if data.starts_with(END_MESSAGE) {
7385
current_task = None;
86+
file_writer.as_ref().map(|w| w.end());
87+
file_writer = None;
7488
} else if data.starts_with(FINISHED_MESSAGE) {
89+
file_writer.as_ref().map(|w| w.end());
7590
break;
91+
} else {
92+
match &current_task {
93+
Some(Task::Append(_)) => {
94+
if let Some(writer) = file_writer.as_ref() {
95+
writer.write(&data);
96+
}
97+
}
98+
_ => {}
99+
}
76100
}
77101

78102
// The message sent by the report will automatically disappear after a short period of time.
79103
// In order to keep the text displayed on the dialog box, report the title every time data is returned.
80104
if current_task.is_some() {
81105
progress.report(current_task.as_ref().unwrap().title());
82-
continue;
83-
}
84-
match &current_task {
85-
Some(Task::Create(_)) => {
86-
todo!()
87-
}
88-
Some(Task::Append(_)) => {
89-
todo!()
90-
}
91-
_ => {}
92106
}
93107
}
94108
drop(data_stream);

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
{
8585
"command": "aicursor.generateProject",
8686
"title": "Generate Project",
87+
"enablement": "workspaceFolders != null && workspaceFolderCount > 0",
8788
"category": "CodeCursor"
8889
}
8990
],

src/extension/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as vscode from "vscode";
22
import * as crypto from "crypto";
3+
import * as fs from "node:fs";
34

45
import { GenerateSession, getScratchpadManager } from "./generate";
56
import { getGlobalState } from "./globalState";

src/extension/project.ts

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,84 @@
11
import * as vscode from "vscode";
2+
import * as fs from "node:fs";
23
import { generateProject } from "@crates/cursor-core";
34

45
export async function handleGenerateProjectCommand() {
6+
const workspace = getCurrentWorkspace();
7+
if (!workspace) {
8+
return;
9+
}
10+
511
const input = await vscode.window.showInputBox({
612
title: "Generate A New Project",
713
placeHolder: "Instructions for project to generate...",
814
});
915
if (!input) {
1016
return;
1117
}
12-
await generateProject(input);
18+
19+
// Check if the workspace is empty.
20+
const files = await vscode.workspace.fs.readDirectory(workspace.uri);
21+
// Exclude hidden files.
22+
files.filter((file) => !file[0].startsWith("."));
23+
if (files.length > 0) {
24+
const confirmMessage = "Yes, I am sure";
25+
const cancelMessage = "No";
26+
const result = await vscode.window.showWarningMessage(
27+
"The current workspace is not empty. Your existing files may be accidentally modified. Are you sure you want to continue?",
28+
confirmMessage,
29+
cancelMessage
30+
);
31+
if (result !== confirmMessage) {
32+
return;
33+
}
34+
}
35+
36+
await generateProject(input, {
37+
async createFileRecursive(path) {
38+
// `path` is the relative path to the current workspace.
39+
const absolutePath = getAbsolutePath(path);
40+
if (!absolutePath) {
41+
return;
42+
}
43+
// Create a file and recursively create intermediate folders if they do not exist.
44+
const uri = vscode.Uri.file(absolutePath);
45+
const parent = uri.with({
46+
path: uri.path.substring(0, uri.path.lastIndexOf("/")),
47+
});
48+
await vscode.workspace.fs.createDirectory(parent);
49+
await vscode.workspace.fs.writeFile(uri, new Uint8Array());
50+
},
51+
makeFileWriter(path) {
52+
const absolutePath = getAbsolutePath(path);
53+
if (!absolutePath) {
54+
return;
55+
}
56+
// Using the ability of stream writing files provided by NodeJS.
57+
const writeStream = fs.createWriteStream(absolutePath);
58+
return {
59+
write(data) {
60+
writeStream.write(data);
61+
},
62+
end() {
63+
writeStream.end();
64+
},
65+
};
66+
},
67+
});
68+
}
69+
70+
function getAbsolutePath(path: string) {
71+
const workspace = getCurrentWorkspace();
72+
if (!workspace) {
73+
return;
74+
}
75+
return workspace.uri.path + "/" + path;
76+
}
77+
78+
function getCurrentWorkspace() {
79+
const workspaceFolders = vscode.workspace.workspaceFolders;
80+
if (!workspaceFolders) {
81+
return;
82+
}
83+
return workspaceFolders[0];
1384
}

0 commit comments

Comments
 (0)