Skip to content

Commit da27047

Browse files
committed
Feat: rewrite sync system to use versions instead of sync status.
1 parent fe7fc66 commit da27047

File tree

13 files changed

+266
-109
lines changed

13 files changed

+266
-109
lines changed

client/src/CodeChatEditor.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ const save_lp = (is_dirty: boolean) => {
348348
update.contents = {
349349
metadata: current_metadata,
350350
source: code_mirror_diffable,
351+
version: Math.random(),
351352
};
352353
}
353354

client/src/CodeChatEditorFramework.mts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ class WebSocketComm {
8181
// IDE and passed back to it, but not otherwise used by the Framework.
8282
current_filename: string | undefined = undefined;
8383

84+
// The version number of the current file. This default value will be overwritten when
85+
// the first `Update` is sent.
86+
version = 0.0;
87+
8488
// True when the iframe is loading, so that an `Update` should be postponed
8589
// until the page load is finished. Otherwise, the page is fully loaded, so
8690
// the `Update` may be applied immediately.
@@ -160,6 +164,17 @@ class WebSocketComm {
160164
const contents = current_update.contents;
161165
const cursor_position = current_update.cursor_position;
162166
if (contents !== undefined) {
167+
// Check and update the version. If this is a diff, ensure the diff was made against the version of the file we have.
168+
if ("Diff" in contents.source) {
169+
if (
170+
contents.source.Diff.version !==
171+
this.version
172+
) {
173+
this.send_result(id, "OutOfSync");
174+
return;
175+
}
176+
}
177+
this.version = contents.version;
163178
// I'd prefer to use a system-maintained value to
164179
// determine the ready state of the iframe, such as
165180
// [readyState](https://developer.mozilla.org/en-US/docs/Web/API/Document/readyState).
@@ -313,6 +328,8 @@ class WebSocketComm {
313328
if (typeof message == "object" && "Update" in message) {
314329
assert(this.current_filename !== undefined);
315330
message.Update.file_path = this.current_filename!;
331+
// Update the version of this file if it's provided.
332+
this.version = message.Update.contents?.version ?? this.version;
316333
}
317334
console_log(
318335
`CodeChat Editor Client: sent message ${id}, ${format_struct(message)}`,

extensions/VSCode/src/extension.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ let quiet_next_error = false;
9393
// True if the editor contents have changed (are dirty) from the perspective of
9494
// the CodeChat Editor (not if the contents are saved to disk).
9595
let is_dirty = false;
96+
// The version of the current file.
97+
let version = 0.0;
9698

9799
// An object to start/stop the CodeChat Editor Server.
98100
let codeChatEditorServer: CodeChatEditorServer | undefined;
@@ -353,6 +355,16 @@ export const activate = (context: vscode.ExtensionContext) => {
353355
);
354356
} else {
355357
assert("Diff" in source);
358+
// If this diff was not made against the text we currently have, reject it.
359+
if (source.Diff.version !== version) {
360+
sendResult(
361+
id,
362+
"Out of sync: incorrect version for diff.",
363+
);
364+
// Send an `Update` with the full text to re-sync the Client.
365+
send_update(true);
366+
break;
367+
}
356368
const diffs = source.Diff.doc;
357369
for (const diff of diffs) {
358370
// Convert from character offsets from the
@@ -384,6 +396,8 @@ export const activate = (context: vscode.ExtensionContext) => {
384396
() =>
385397
(ignore_text_document_change = false),
386398
);
399+
// Now that we've updated our text, update the associated version as well.
400+
version = current_update.contents.version;
387401
}
388402

389403
// Update the cursor and scroll position if
@@ -504,8 +518,13 @@ export const activate = (context: vscode.ExtensionContext) => {
504518
// Look through all open documents to see if we have
505519
// the requested file.
506520
const doc = get_document(load_file);
507-
const load_file_result =
508-
doc === undefined ? null : doc.getText();
521+
const load_file_result: null | [string, number] =
522+
doc === undefined
523+
? null
524+
: [
525+
doc.getText(),
526+
(version = Math.random()),
527+
];
509528
console_log(
510529
`CodeChat Editor extension: Result(LoadFile(${format_struct(load_file_result)}))`,
511530
);
@@ -614,8 +633,8 @@ const send_update = (this_is_dirty: boolean) => {
614633
const scroll_position = ate.visibleRanges[0].start.line + 1;
615634
const file_path = ate.document.fileName;
616635
// Send contents only if necessary.
617-
const option_contents = is_dirty
618-
? ate.document.getText()
636+
const option_contents: null | [string, number] = is_dirty
637+
? [ate.document.getText(), (version = Math.random())]
619638
: null;
620639
is_dirty = false;
621640
console_log(

extensions/VSCode/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ impl CodeChatEditorServer {
8686
&self,
8787
file_path: String,
8888
// `null` to send no source code; a string to send the source code.
89-
option_contents: Option<String>,
89+
option_contents: Option<(String, f64)>,
9090
cursor_position: Option<u32>,
9191
scroll_position: Option<f64>,
9292
) -> std::io::Result<f64> {
@@ -108,7 +108,7 @@ impl CodeChatEditorServer {
108108
pub async fn send_result_loadfile(
109109
&self,
110110
id: f64,
111-
load_file: Option<String>,
111+
load_file: Option<(String, f64)>,
112112
) -> std::io::Result<()> {
113113
self.0.send_result_loadfile(id, load_file).await
114114
}

server/src/ide.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ impl CodeChatEditorServer {
251251
&self,
252252
file_path: String,
253253
// `null` to send no source code; a string to send the source code.
254-
option_contents: Option<String>,
254+
option_contents: Option<(String, f64)>,
255255
cursor_position: Option<u32>,
256256
scroll_position: Option<f64>,
257257
) -> std::io::Result<f64> {
@@ -262,9 +262,10 @@ impl CodeChatEditorServer {
262262
mode: "".to_string(),
263263
},
264264
source: CodeMirrorDiffable::Plain(CodeMirror {
265-
doc: contents,
265+
doc: contents.0,
266266
doc_blocks: vec![],
267267
}),
268+
version: contents.1,
268269
}),
269270
cursor_position,
270271
scroll_position: scroll_position.map(|x| x as f32),
@@ -294,7 +295,7 @@ impl CodeChatEditorServer {
294295
pub async fn send_result_loadfile(
295296
&self,
296297
id: f64,
297-
load_file: Option<String>,
298+
load_file: Option<(String, f64)>,
298299
) -> std::io::Result<()> {
299300
self.send_message_raw(EditorMessage {
300301
id,

server/src/ide/filewatcher.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ use notify_debouncer_full::{
4040
DebounceEventResult, new_debouncer,
4141
notify::{EventKind, RecursiveMode},
4242
};
43+
use rand::random;
4344
use regex::Regex;
4445
use tokio::{
4546
fs::DirEntry,
@@ -534,7 +535,9 @@ async fn processing_task(
534535
source: crate::processing::CodeMirrorDiffable::Plain(CodeMirror {
535536
doc: file_contents,
536537
doc_blocks: vec![],
537-
})
538+
}),
539+
// The filewatcher doesn't store a version, since it only accepts plain (non-diff) results. Provide a version so the Client stays in sync with any diffs.
540+
version: random(),
538541
}),
539542
cursor_position: None,
540543
scroll_position: None,
@@ -845,7 +848,13 @@ mod tests {
845848
send_response(&from_client_tx, id, Ok(ResultOkTypes::Void)).await;
846849

847850
// Check the contents.
848-
let translation_results = source_to_codechat_for_web("", &"py".to_string(), false, false);
851+
let translation_results = source_to_codechat_for_web(
852+
"",
853+
&"py".to_string(),
854+
umc.contents.as_ref().unwrap().version,
855+
false,
856+
false,
857+
);
849858
let codechat_for_web = cast!(cast!(translation_results, Ok), TranslationResults::CodeChat);
850859
assert_eq!(umc.contents, Some(codechat_for_web));
851860

@@ -961,6 +970,7 @@ mod tests {
961970
doc: "".to_string(),
962971
doc_blocks: vec![],
963972
}),
973+
version: 0.0,
964974
}),
965975
cursor_position: None,
966976
scroll_position: None,
@@ -990,6 +1000,7 @@ mod tests {
9901000
doc: "testing".to_string(),
9911001
doc_blocks: vec![],
9921002
}),
1003+
version: 1.0,
9931004
}),
9941005
cursor_position: None,
9951006
scroll_position: None,
@@ -1022,6 +1033,7 @@ mod tests {
10221033
doc: "testing()".to_string(),
10231034
doc_blocks: vec![],
10241035
}),
1036+
version: 2.0,
10251037
}),
10261038
cursor_position: None,
10271039
scroll_position: None,
@@ -1048,8 +1060,10 @@ mod tests {
10481060
fs::write(&file_path, s).unwrap();
10491061
// Wait for the filewatcher to debounce this file write.
10501062
sleep(Duration::from_secs(2)).await;
1063+
// The version is random; don't check it with a fixed value.
1064+
let msg = get_message_as!(to_client_rx, EditorMessageContents::Update);
10511065
assert_eq!(
1052-
get_message_as!(to_client_rx, EditorMessageContents::Update),
1066+
msg,
10531067
(
10541068
INITIAL_IDE_MESSAGE_ID + MESSAGE_ID_INCREMENT,
10551069
UpdateMessageContents {
@@ -1062,6 +1076,7 @@ mod tests {
10621076
doc: "testing()123".to_string(),
10631077
doc_blocks: vec![],
10641078
}),
1079+
version: msg.1.contents.as_ref().unwrap().version,
10651080
}),
10661081
cursor_position: None,
10671082
scroll_position: None,

server/src/ide/vscode/tests.rs

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -516,9 +516,10 @@ async fn test_vscode_ide_websocket8() {
516516
&mut ws_ide,
517517
&EditorMessage {
518518
id: INITIAL_MESSAGE_ID + MESSAGE_ID_INCREMENT,
519-
message: EditorMessageContents::Result(Ok(ResultOkTypes::LoadFile(Some(
519+
message: EditorMessageContents::Result(Ok(ResultOkTypes::LoadFile(Some((
520520
"# testing".to_string(),
521-
)))),
521+
0.0,
522+
))))),
522523
},
523524
)
524525
.await;
@@ -547,6 +548,7 @@ async fn test_vscode_ide_websocket8() {
547548
contents: "<p>testing</p>\n".to_string()
548549
}],
549550
}),
551+
version: 0.0,
550552
}),
551553
cursor_position: None,
552554
scroll_position: None,
@@ -648,6 +650,7 @@ async fn test_vscode_ide_websocket7() {
648650
doc: "# more".to_string(),
649651
doc_blocks: vec![],
650652
}),
653+
version: 0.0,
651654
}),
652655
cursor_position: None,
653656
scroll_position: None,
@@ -674,7 +677,8 @@ async fn test_vscode_ide_websocket7() {
674677
delimiter: "#".to_string(),
675678
contents: "<p>more</p>\n".to_string()
676679
}]
677-
})
680+
}),
681+
version: 0.0,
678682
}),
679683
cursor_position: None,
680684
scroll_position: None,
@@ -720,6 +724,7 @@ async fn test_vscode_ide_websocket7() {
720724
.to_string(),
721725
doc_blocks: vec![],
722726
}),
727+
version: 1.0,
723728
}),
724729
cursor_position: None,
725730
scroll_position: None,
@@ -749,8 +754,10 @@ async fn test_vscode_ide_websocket7() {
749754
indent: "".to_string(),
750755
delimiter: "#".to_string(),
751756
contents: "<p>most</p>\n".to_string()
752-
})]
757+
})],
758+
version: 0.0,
753759
}),
760+
version: 1.0,
754761
}),
755762
cursor_position: None,
756763
scroll_position: None,
@@ -807,6 +814,7 @@ async fn test_vscode_ide_websocket6() {
807814
contents: "less\n".to_string(),
808815
}],
809816
}),
817+
version: 0.0,
810818
}),
811819
cursor_position: None,
812820
scroll_position: None,
@@ -828,6 +836,7 @@ async fn test_vscode_ide_websocket6() {
828836
doc: "# less\n".to_string(),
829837
doc_blocks: vec![],
830838
}),
839+
version: 0.0,
831840
}),
832841
cursor_position: None,
833842
scroll_position: None,
@@ -946,8 +955,11 @@ async fn test_vscode_ide_websocket4() {
946955
// This should also produce an `Update` message sent from the Server.
947956
//
948957
// Message ids: IDE - 0, Server - 2->3, Client - 0.
958+
//
959+
// Since the version is randomly generated, copy that from the received message.
960+
let msg = read_message(&mut ws_client).await;
949961
assert_eq!(
950-
read_message(&mut ws_client).await,
962+
msg,
951963
EditorMessage {
952964
id: INITIAL_MESSAGE_ID + 2.0 * MESSAGE_ID_INCREMENT,
953965
message: EditorMessageContents::Update(UpdateMessageContents {
@@ -966,6 +978,11 @@ async fn test_vscode_ide_websocket4() {
966978
contents: "<p>test.py</p>\n".to_string()
967979
}],
968980
}),
981+
version: cast!(&msg.message, EditorMessageContents::Update)
982+
.contents
983+
.as_ref()
984+
.unwrap()
985+
.version,
969986
}),
970987
cursor_position: None,
971988
scroll_position: None,
@@ -1025,6 +1042,8 @@ async fn test_vscode_ide_websocket4() {
10251042
.await;
10261043
join_handle.join().unwrap();
10271044

1045+
// What makes sense here? If the IDE didn't load the file, either the Client shouldn't edit it or the Client should switch to using a filewatcher for edits.
1046+
/*
10281047
// Send an update from the Client, which should produce a diff.
10291048
//
10301049
// Message ids: IDE - 0, Server - 4, Client - 0->1.
@@ -1048,6 +1067,7 @@ async fn test_vscode_ide_websocket4() {
10481067
contents: "<p>test.py</p>".to_string(),
10491068
}],
10501069
}),
1070+
version: 1.0,
10511071
}),
10521072
cursor_position: None,
10531073
scroll_position: None,
@@ -1072,7 +1092,9 @@ async fn test_vscode_ide_websocket4() {
10721092
insert: format!("More{}", if cfg!(windows) { "\r\n" } else { "\n" }),
10731093
}],
10741094
doc_blocks: vec![],
1095+
version: 0.0,
10751096
}),
1097+
version: 1.0,
10761098
}),
10771099
cursor_position: None,
10781100
scroll_position: None,
@@ -1094,6 +1116,7 @@ async fn test_vscode_ide_websocket4() {
10941116
message: EditorMessageContents::Result(Ok(ResultOkTypes::Void))
10951117
}
10961118
);
1119+
*/
10971120

10981121
check_logger_errors(0);
10991122
// Report any errors produced when removing the temporary directory.

0 commit comments

Comments
 (0)