-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathide.rs
More file actions
335 lines (314 loc) · 12.7 KB
/
Copy pathide.rs
File metadata and controls
335 lines (314 loc) · 12.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
// Copyright (C) 2025 Bryan A. Jones.
//
// This file is part of the CodeChat Editor. The CodeChat Editor is free
// software: you can redistribute it and/or modify it under the terms of the GNU
// General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// The CodeChat Editor is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
// details.
//
// You should have received a copy of the GNU General Public License along with
// the CodeChat Editor. If not, see
// [http://www.gnu.org/licenses](http://www.gnu.org/licenses).
//! `ide.rs` -- Provide interfaces with common IDEs
//! ===============================================
//!
//! This module bridges IDE extensions and the CodeChat Editor's core server.
//! Its central type, `CodeChatEditorServer`, starts the Actix web server in a
//! dedicated OS thread (isolating its async runtime from the IDE's own runtime)
//! and exposes a typed message-passing interface for the extension to use:
//!
//! - **`send_message_*`** methods push editor events (file opened, file
//! changed, cursor moved, …) into the server's translation pipeline.
//! - **`get_message`** / **`get_message_timeout`** retrieve responses from the
//! server (including synthetic timeout errors for unacknowledged messages).
//! - **`stop_server`** gracefully shuts the server down.
//!
//! All internal fields are private so that IDE extensions are forced to use
//! only this public interface, hiding the implementation details of the server.
//! Sub-modules (`filewatcher` — file watcher support, `vscode`) contain
//! IDE-specific logic.
pub mod filewatcher;
pub mod vscode;
// Imports
// -------
//
// ### Standard library
use std::{
collections::HashMap,
net::{IpAddr, Ipv4Addr, SocketAddr},
sync::Arc,
thread,
time::Duration,
};
// ### Third-party
use actix_server::{Server, ServerHandle};
use log::error;
use rand::random;
use tokio::{
runtime::Handle,
select,
sync::{
Mutex,
mpsc::{self, Receiver, Sender},
},
task::JoinHandle,
time::sleep,
};
// ### Local
use crate::{
ide::vscode::{connection_id_raw_to_str, vscode_ide_core},
processing::{CodeChatForWeb, CodeMirror, CodeMirrorDiffable, SourceFileMetadata},
translation::{CreatedTranslationQueues, create_translation_queues},
webserver::{
self, CursorPosition, EditorMessage, EditorMessageContents, INITIAL_IDE_MESSAGE_ID,
MESSAGE_ID_INCREMENT, REPLY_TIMEOUT_MS, ResultErrTypes, ResultOkTypes,
UpdateMessageContents, WebAppState, setup_server,
},
};
// Code
// ----
//
// Using this macro is critical -- otherwise, the Actix system doesn't get
// correctly initialized, which makes calls to `actix_rt::spawn` fail. In
// addition, this ensures that the server runs in a separate thread, rather than
// depending on the extension to yield it time to run in the current thread.
#[actix_web::main]
async fn start_server(
connection_id_raw: String,
app_state_task: WebAppState,
translation_queues: CreatedTranslationQueues,
server: Server,
) -> std::io::Result<()> {
vscode_ide_core(connection_id_raw, app_state_task, translation_queues);
server.await
}
// Provide a class to start and stop the server. All its fields are opaque,
// since only Rust should use them.
pub struct CodeChatEditorServer {
server_handle: ServerHandle,
from_ide_tx: Sender<EditorMessage>,
to_ide_rx: Arc<Mutex<Receiver<EditorMessage>>>,
current_id: Arc<Mutex<f64>>,
pending_messages: Arc<Mutex<HashMap<u64, JoinHandle<()>>>>,
expired_messages_tx: Sender<f64>,
expired_messages_rx: Arc<Mutex<Receiver<f64>>>,
}
impl CodeChatEditorServer {
// Creating the server could fail, so this must return an `io::Result`.
pub fn new() -> std::io::Result<CodeChatEditorServer> {
// Start the server.
let (server, app_state) = setup_server(
// A port of 0 requests the OS to assign an open port.
&SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0),
None,
)?;
let server_handle = server.handle();
// Start a thread to translate between this IDE and a Client.
let connection_id_raw = random::<u64>().to_string();
let connection_id = connection_id_raw_to_str(&connection_id_raw);
let app_state_task = app_state.clone();
let translation_queues = create_translation_queues(connection_id.clone(), &app_state)
.map_err(|err| std::io::Error::other(format!("Unable to create queues: {err}")))?;
thread::spawn(move || {
start_server(
connection_id_raw,
app_state_task,
translation_queues,
server,
)
});
// Get the IDE queues created by this task for use with the `get`/`put`
// methods.
let websocket_queues = app_state
.ide_queues
.lock()
.map_err(|e| std::io::Error::other(format!("Unable to lock queue: {e}")))?
.remove(&connection_id)
.ok_or_else(|| {
std::io::Error::other(format!("Unable to find queue named {connection_id}"))
})?;
let (expired_messages_tx, expired_messages_rx) = mpsc::channel(100);
Ok(CodeChatEditorServer {
server_handle,
from_ide_tx: websocket_queues.from_websocket_tx,
to_ide_rx: Arc::new(Mutex::new(websocket_queues.to_websocket_rx)),
// Use a unique ID for each websocket message sent. See the
// Implementation section on Message IDs for more information.
current_id: Arc::new(Mutex::new(INITIAL_IDE_MESSAGE_ID)),
pending_messages: Arc::new(Mutex::new(HashMap::new())),
expired_messages_tx,
expired_messages_rx: Arc::new(Mutex::new(expired_messages_rx)),
})
}
// This returns an error if the conversion to JSON fails, `None` if the
// queue is closed, or a JSON-encoded string containing the message
// otherwise.
pub async fn get_message(&self) -> Option<EditorMessage> {
// Get a message -- either an expired message result or an incoming
// message.
let mut to_ide_rx = self.to_ide_rx.lock().await;
let mut expired_messages_rx = self.expired_messages_rx.lock().await;
select! {
Some(m) = to_ide_rx.recv() => {
// Cancel the timer on this pending message.
if let Some(task) = self.pending_messages.lock().await.remove(&m.id.to_bits()) {
task.abort();
}
// Return it.
Some(m)
},
Some(id) = expired_messages_rx.recv() =>
// Report this unacknowledged message.
Some(
EditorMessage {
id,
message: EditorMessageContents::Result(Err(webserver::ResultErrTypes::MessageTimeout(id)))
}
),
else => None,
}
}
// Like `get_message`, but with a timeout.
pub async fn get_message_timeout(&self, timeout: Duration) -> Option<EditorMessage> {
select! {
_ = sleep(timeout) => None,
v = self.get_message() => v
}
}
// Send the provided message contents; add in an ID and add this to the list
// of pending messages. This produces a timeout if a matching `Result`
// message isn't received with the timeout.
async fn send_message_timeout(
&self,
editor_message_contents: EditorMessageContents,
) -> std::io::Result<f64> {
// Get and update the current ID.
let id = {
let mut id = self.current_id.lock().await;
let old_id = *id;
*id += MESSAGE_ID_INCREMENT;
old_id
};
// Build the resulting message to send.
let editor_message = EditorMessage {
id,
message: editor_message_contents,
};
// Start a timeout in case the message isn't acknowledged.
let expired_messages_tx = self.expired_messages_tx.clone();
// Important: there's already a Tokio runtime since this is an async
// function. Use that to spawn a new task; there's not an Actix
// System/Arbiter running in this thread.
let waiting_task = Handle::current().spawn(async move {
sleep(REPLY_TIMEOUT_MS).await;
// Since the websocket failed to send a `Result`, produce a timeout
// `Result` for it.
match expired_messages_tx.send(id).await {
Ok(join_handle) => join_handle,
Err(err) => {
error!("Error -- unable to send expired message: {err}");
}
}
});
// Add this to the list of pending message.
self.pending_messages
.lock()
.await
.insert(editor_message.id.to_bits(), waiting_task);
self.send_message_raw(editor_message).await?;
Ok(id)
}
// Send a message with no timeout or other additional steps.
async fn send_message_raw(&self, editor_message: EditorMessage) -> std::io::Result<()> {
self.from_ide_tx
.send(editor_message)
.await
.map_err(|e| std::io::Error::other(e.to_string()))
}
pub async fn send_message_opened(&self, hosted_in_ide: bool) -> std::io::Result<f64> {
self.send_message_timeout(EditorMessageContents::Opened(webserver::IdeType::VSCode(
hosted_in_ide,
)))
.await
}
// Send a `CurrentFile` message. The other parameter (true if text/false if
// binary/None if ignored) is ignored by the server, so it's always sent as
// `None`.
pub async fn send_message_current_file(&self, url: String) -> std::io::Result<f64> {
self.send_message_timeout(EditorMessageContents::CurrentFile(url, None))
.await
}
// Send an `Update` message, optionally with plain text (instead of a diff)
// containing the source code from the IDE.
pub async fn send_message_update_plain(
&self,
file_path: String,
// `null` to send no source code; a string to send the source code.
option_contents: Option<(String, f64)>,
cursor_position: Option<u32>,
scroll_position: Option<f64>,
) -> std::io::Result<f64> {
self.send_message_timeout(EditorMessageContents::Update(UpdateMessageContents {
file_path,
cursor_position: cursor_position.map(CursorPosition::Line),
scroll_position: scroll_position.map(|x| x as f32),
is_re_translation: false,
contents: option_contents.map(|contents| CodeChatForWeb {
metadata: SourceFileMetadata {
// The IDE doesn't need to provide the `mode`; this will
// determined by the server.
mode: "".to_string(),
},
source: CodeMirrorDiffable::Plain(CodeMirror {
doc: contents.0,
doc_blocks: vec![],
}),
version: contents.1,
}),
}))
.await
}
/// Send either an Ok(Void) or an Error result to the Client.
pub async fn send_result(
&self,
id: f64,
message_result: Option<ResultErrTypes>,
) -> std::io::Result<()> {
let editor_message = EditorMessage {
id,
message: webserver::EditorMessageContents::Result(
if let Some(message_result) = message_result {
Err(message_result)
} else {
Ok(ResultOkTypes::Void)
},
),
};
self.send_message_raw(editor_message).await
}
pub async fn send_result_loadfile(
&self,
id: f64,
load_file: Option<(String, f64)>,
) -> std::io::Result<()> {
self.send_message_raw(EditorMessage {
id,
message: EditorMessageContents::Result(Ok(ResultOkTypes::LoadFile(load_file))),
})
.await
}
// This returns after the server shuts down.
pub async fn stop_server(&self) {
self.server_handle.stop(true).await;
// Since the server is closing, don't report any expired message.
self.expired_messages_rx.lock().await.close();
// Stop all running timers, now that no new messages will arrive.
for (_id, join_handle) in self.pending_messages.lock().await.drain() {
join_handle.abort();
}
}
}