Skip to content

Commit 0c534ec

Browse files
committed
add create project command
1 parent 67ebd22 commit 0c534ec

File tree

4 files changed

+188
-6
lines changed

4 files changed

+188
-6
lines changed

crates/analyzer/src/codegen/snippets.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ pub const CONTRACT_PLAIN: &str = r#"#![cfg_attr(not(feature = "std"), no_std)]
7777
#[ink::contract]
7878
pub mod my_contract {
7979
#[ink(storage)]
80-
pub struct Storage {}
80+
pub struct MyContract {}
8181
8282
impl MyContract {
8383
#[ink(constructor)]

crates/lsp-server/src/dispatch.rs

Lines changed: 104 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
//! LSP server main loop for dispatching requests, notifications and handling responses.
22
33
use crossbeam_channel::Sender;
4+
use lsp_types::request::Request;
45
use std::collections::HashSet;
56

67
use crate::dispatch::routers::{NotificationRouter, RequestRouter};
78
use crate::memory::Memory;
9+
use crate::utils;
10+
use handlers::request::CreateProjectResponse;
811

912
mod actions;
1013
mod handlers;
@@ -41,8 +44,8 @@ pub fn main_loop(
4144
// Handles all other notifications using the dispatcher.
4245
dispatcher.handle_notification(not)?;
4346
}
44-
// We don't currently initiate any requests, so all responses are ignored.
45-
lsp_server::Message::Response(_) => (),
47+
// Handles responses to requests initiated by the server (e.g workspace edits).
48+
lsp_server::Message::Response(resp) => dispatcher.handle_response(resp)?,
4649
}
4750
}
4851

@@ -56,6 +59,9 @@ struct Dispatcher<'a> {
5659
memory: Memory,
5760
}
5861

62+
const INITIALIZE_PROJECT_ID_PREFIX: &str = "initialize-project::";
63+
const SHOW_DOCUMENT_ID_PREFIX: &str = "show-document::";
64+
5965
impl<'a> Dispatcher<'a> {
6066
/// Creates a dispatcher for an LSP server connection.
6167
fn new(
@@ -72,6 +78,7 @@ impl<'a> Dispatcher<'a> {
7278
/// Handles LSP requests and sends responses (if any) as appropriate.
7379
fn handle_request(&mut self, req: lsp_server::Request) -> anyhow::Result<()> {
7480
// Computes request response (if any).
81+
let is_execute_command = req.method == lsp_types::request::ExecuteCommand::METHOD;
7582
let mut router = RequestRouter::new(req, &mut self.memory, &self.client_capabilities);
7683
let result = router
7784
.process::<lsp_types::request::Completion>(handlers::request::handle_completion)
@@ -81,11 +88,71 @@ impl<'a> Dispatcher<'a> {
8188
.process::<lsp_types::request::SignatureHelpRequest>(
8289
handlers::request::handle_signature_help,
8390
)
91+
.process::<lsp_types::request::ExecuteCommand>(
92+
handlers::request::handle_execute_command,
93+
)
8494
.finish();
8595

8696
// Sends response (if any).
87-
if let Some(resp) = result {
88-
self.send(resp.into())?;
97+
if let Some(mut resp) = result {
98+
// Intercept non-empty `createProject` responses and create the project as a workspace edit.
99+
if let Some(project) = is_execute_command
100+
.then_some(resp.result.as_ref())
101+
.flatten()
102+
.and_then(|value| {
103+
serde_json::from_value::<CreateProjectResponse>(value.clone()).ok()
104+
})
105+
{
106+
// Return an empty response.
107+
resp.result = Some(serde_json::Value::Null);
108+
self.send(resp.into())?;
109+
110+
// Create project files using a workspace edit.
111+
let doc_changes = project
112+
.files
113+
.iter()
114+
.flat_map(|(uri, content)| {
115+
vec![
116+
lsp_types::DocumentChangeOperation::Op(lsp_types::ResourceOp::Create(
117+
lsp_types::CreateFile {
118+
uri: uri.clone(),
119+
options: None,
120+
annotation_id: None,
121+
},
122+
)),
123+
lsp_types::DocumentChangeOperation::Edit(lsp_types::TextDocumentEdit {
124+
text_document: lsp_types::OptionalVersionedTextDocumentIdentifier {
125+
uri: uri.clone(),
126+
version: None,
127+
},
128+
edits: vec![lsp_types::OneOf::Left(lsp_types::TextEdit {
129+
range: lsp_types::Range::default(),
130+
new_text: content.to_owned(),
131+
})],
132+
}),
133+
]
134+
})
135+
.collect();
136+
let params = lsp_types::ApplyWorkspaceEditParams {
137+
label: Some("new ink! project".to_string()),
138+
edit: lsp_types::WorkspaceEdit {
139+
document_changes: Some(lsp_types::DocumentChanges::Operations(doc_changes)),
140+
..Default::default()
141+
},
142+
};
143+
let req = lsp_server::Request::new(
144+
lsp_server::RequestId::from(format!(
145+
"{INITIALIZE_PROJECT_ID_PREFIX}{}",
146+
project.uri
147+
)),
148+
lsp_types::request::ApplyWorkspaceEdit::METHOD.to_string(),
149+
params,
150+
);
151+
self.send(req.into())?;
152+
} else {
153+
// Otherwise return response.
154+
self.send(resp.into())?;
155+
}
89156
}
90157

91158
// Process memory changes (if any) made by request handlers.
@@ -116,6 +183,39 @@ impl<'a> Dispatcher<'a> {
116183
Ok(())
117184
}
118185

186+
/// Handles LSP responses.
187+
fn handle_response(&mut self, resp: lsp_server::Response) -> anyhow::Result<()> {
188+
// Open `lib.rs` after project initialization.
189+
if let Some(resp_id) = utils::request_id_as_str(resp.id) {
190+
if resp_id.starts_with(INITIALIZE_PROJECT_ID_PREFIX) {
191+
if let Some(project_uri) = resp_id
192+
.strip_prefix(INITIALIZE_PROJECT_ID_PREFIX)
193+
.and_then(|suffix| lsp_types::Url::parse(suffix).ok())
194+
{
195+
if let Ok(lib_uri) = project_uri.join("lib.rs") {
196+
let params = lsp_types::ShowDocumentParams {
197+
uri: lib_uri.clone(),
198+
external: None,
199+
take_focus: Some(true),
200+
selection: None,
201+
};
202+
let req = lsp_server::Request::new(
203+
lsp_server::RequestId::from(format!(
204+
"{SHOW_DOCUMENT_ID_PREFIX}{}",
205+
lib_uri
206+
)),
207+
lsp_types::request::ShowDocument::METHOD.to_string(),
208+
params,
209+
);
210+
self.send(req.into())?;
211+
}
212+
}
213+
}
214+
}
215+
216+
Ok(())
217+
}
218+
119219
/// Processes changes to state and triggers appropriate actions (if any).
120220
fn process_changes(&mut self) -> anyhow::Result<()> {
121221
// Retrieves document changes (if any).
@@ -216,7 +316,6 @@ mod tests {
216316

217317
// Verifies that LSP requests (from client to server) get appropriate LSP responses (from server to client).
218318
// Creates LSP completion request.
219-
use lsp_types::request::Request;
220319
let completion_request_id = lsp_server::RequestId::from(1);
221320
let completion_request = lsp_server::Request {
222321
id: completion_request_id.clone(),

crates/lsp-server/src/dispatch/handlers/request.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
33
use ink_analyzer::Analysis;
44
use line_index::LineIndex;
5+
use serde::{Deserialize, Serialize};
6+
use std::collections::HashMap;
57

68
use crate::memory::Memory;
79
use crate::translator::PositionTranslationContext;
@@ -209,6 +211,78 @@ pub fn handle_signature_help(
209211
}
210212
}
211213

214+
#[derive(Debug, Serialize, Deserialize)]
215+
pub struct CreateProjectResponse {
216+
pub name: String,
217+
pub uri: lsp_types::Url,
218+
pub files: HashMap<lsp_types::Url, String>,
219+
}
220+
221+
/// Handles execute command request.
222+
pub fn handle_execute_command(
223+
params: lsp_types::ExecuteCommandParams,
224+
_memory: &mut Memory,
225+
_client_capabilities: &lsp_types::ClientCapabilities,
226+
) -> anyhow::Result<Option<serde_json::Value>> {
227+
// Handles create project command.
228+
if params.command == "createProject" {
229+
let args = params
230+
.arguments
231+
.first()
232+
.and_then(serde_json::Value::as_object)
233+
.and_then(|arg| {
234+
arg.get("name").and_then(|it| it.as_str()).zip(
235+
arg.get("root").and_then(|it| it.as_str()).and_then(|it| {
236+
lsp_types::Url::parse(&format!(
237+
"{it}{}",
238+
if it.ends_with('/') { "" } else { "/" }
239+
))
240+
.ok()
241+
}),
242+
)
243+
});
244+
245+
match args {
246+
Some((name, root)) => {
247+
let uris = root
248+
.clone()
249+
.join("lib.rs")
250+
.ok()
251+
.zip(root.join("Cargo.toml").ok());
252+
match uris {
253+
Some((lib_uri, cargo_uri)) => match ink_analyzer::new_project(name.to_string())
254+
{
255+
Ok(project) => {
256+
// Returns create project edits.
257+
Ok(serde_json::to_value(CreateProjectResponse {
258+
name: name.to_owned(),
259+
uri: root,
260+
files: HashMap::from([
261+
(lib_uri, project.lib.plain),
262+
(cargo_uri, project.cargo.plain),
263+
]),
264+
})
265+
.ok())
266+
}
267+
Err(_) => Err(anyhow::format_err!(
268+
"Failed to create ink! project: {name}\n\
269+
ink! project names must begin with an alphabetic character, \
270+
and only contain alphanumeric characters, underscores and hyphens"
271+
)),
272+
},
273+
None => Err(anyhow::format_err!("Failed to create ink! project: {name}")),
274+
}
275+
}
276+
// Error for missing or invalid args.
277+
None => Err(anyhow::format_err!(
278+
"The name and root arguments are required!"
279+
)),
280+
}
281+
} else {
282+
Err(anyhow::format_err!("Unknown command: {}!", params.command))
283+
}
284+
}
285+
212286
#[cfg(test)]
213287
mod tests {
214288
use super::*;

crates/lsp-server/src/utils.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! ink! Language Server utilities.
22
3+
use lsp_server::RequestId;
34
use lsp_types::{ClientCapabilities, CodeActionKind, PositionEncodingKind};
45
use std::collections::HashSet;
56

@@ -94,6 +95,14 @@ pub fn signature_support(client_capabilities: &ClientCapabilities) -> SignatureS
9495
)
9596
}
9697

98+
/// Returns a string representation of the request id
99+
/// but only if its internal representation is a `String` else returns None.
100+
pub fn request_id_as_str(id: RequestId) -> Option<String> {
101+
id.to_string()
102+
.strip_prefix('\"')
103+
.and_then(|it| it.strip_suffix('\"'))
104+
.map(ToString::to_string)
105+
}
97106
#[cfg(test)]
98107
mod tests {
99108
use super::*;

0 commit comments

Comments
 (0)