diff --git a/src/web/api.rs b/src/web/api.rs
index 9a341de7..b1e683fa 100644
--- a/src/web/api.rs
+++ b/src/web/api.rs
@@ -1,7 +1,7 @@
//! Defines Rojo's HTTP API, all under /api. These endpoints generally return
//! JSON.
-use std::{collections::HashMap, sync::Arc};
+use std::{collections::HashMap, fs, path::PathBuf, sync::Arc};
use futures::{Future, Stream};
@@ -10,12 +10,12 @@ use rbx_dom_weak::RbxId;
use crate::{
serve_session::ServeSession,
- snapshot::{PatchSet, PatchUpdate},
+ snapshot::{InstanceWithMeta, PatchSet, PatchUpdate},
web::{
interface::{
ErrorResponse, Instance, InstanceMetadata as WebInstanceMetadata, InstanceUpdate,
- ReadResponse, ServerInfoResponse, SubscribeMessage, SubscribeResponse, WriteRequest,
- WriteResponse, PROTOCOL_VERSION, SERVER_VERSION,
+ OpenResponse, ReadResponse, ServerInfoResponse, SubscribeMessage, SubscribeResponse,
+ WriteRequest, WriteResponse, PROTOCOL_VERSION, SERVER_VERSION,
},
util::{json, json_ok},
},
@@ -39,6 +39,9 @@ impl Service for ApiService {
(&Method::GET, path) if path.starts_with("/api/subscribe/") => {
self.handle_api_subscribe(request)
}
+ (&Method::POST, path) if path.starts_with("/api/open/") => {
+ self.handle_api_open(request)
+ }
(&Method::POST, "/api/write") if cfg!(feature = "unstable_two_way_sync") => {
self.handle_api_write(request)
@@ -233,4 +236,76 @@ impl ApiService {
instances,
})
}
+
+ /// Open a script with the given ID in the user's default text editor.
+ fn handle_api_open(&self, request: Request
) -> ::Future {
+ let argument = &request.uri().path()["/api/open/".len()..];
+ let requested_id = match RbxId::parse_str(argument) {
+ Some(id) => id,
+ None => {
+ return json(
+ ErrorResponse::bad_request("Invalid instance ID"),
+ StatusCode::BAD_REQUEST,
+ );
+ }
+ };
+
+ let tree = self.serve_session.tree();
+
+ let instance = match tree.get_instance(requested_id) {
+ Some(instance) => instance,
+ None => {
+ return json(
+ ErrorResponse::bad_request("Instance not found"),
+ StatusCode::NOT_FOUND,
+ );
+ }
+ };
+
+ let script_path = match pick_script_path(instance) {
+ Some(path) => path,
+ None => {
+ return json(
+ ErrorResponse::bad_request(
+ "No appropriate file could be found to open this script",
+ ),
+ StatusCode::NOT_FOUND,
+ );
+ }
+ };
+
+ let _ = opener::open(script_path);
+
+ json_ok(&OpenResponse {
+ session_id: self.serve_session.session_id(),
+ })
+ }
+}
+
+/// If this instance is represented by a script, try to find the correct .lua
+/// file to open to edit it.
+fn pick_script_path(instance: InstanceWithMeta<'_>) -> Option {
+ match instance.class_name() {
+ "Script" | "LocalScript" | "ModuleScript" => {}
+ _ => return None,
+ }
+
+ // Pick the first listed relevant path that has an extension of .lua that
+ // exists.
+ instance
+ .metadata()
+ .relevant_paths
+ .iter()
+ .find(|path| {
+ // We should only ever open Lua files to be safe.
+ match path.extension().and_then(|ext| ext.to_str()) {
+ Some("lua") => {}
+ _ => return false,
+ }
+
+ fs::metadata(path)
+ .map(|meta| meta.is_file())
+ .unwrap_or(false)
+ })
+ .map(|path| path.to_owned())
}
diff --git a/src/web/interface.rs b/src/web/interface.rs
index d2b5cd52..f66a3909 100644
--- a/src/web/interface.rs
+++ b/src/web/interface.rs
@@ -128,6 +128,13 @@ pub struct SubscribeResponse<'a> {
pub messages: Vec>,
}
+/// Response body from /api/open/{id}
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct OpenResponse {
+ pub session_id: SessionId,
+}
+
/// General response type returned from all Rojo routes
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]