Skip to content

Commit b9fe76f

Browse files
committed
Fix: translate EOLs when loading a file from disk.
1 parent 9bec903 commit b9fe76f

File tree

5 files changed

+132
-21
lines changed

5 files changed

+132
-21
lines changed

docs/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Changelog
2323
* Fix indexing in diffs for characters that use more than one UTF-16 code
2424
unit, such as 😄,👉🏿,👨‍👦, and 🇺🇳.
2525
* Fix data corruption with adjacent doc blocks.
26+
* Translate line endings when loading a file from disk.
2627
* v0.1.23, 2025-Jul-24
2728
* Correct diff errors in IDE with CRLF line endings.
2829
* Upgrade to newest release of MathJax, TinyMCE.

server/src/webserver.rs

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,8 @@ async fn make_simple_http_response(
701701
// If this file is currently being edited, this is the body of an `Update`
702702
// message to send.
703703
Option<UpdateMessageContents>,
704+
// The resulting file contents, if this is a CodeChat Editor file
705+
Option<String>
704706
) {
705707
// Convert the provided URL back into a file name.
706708
let file_path = &http_request.file_path;
@@ -710,6 +712,7 @@ async fn make_simple_http_response(
710712
Err(err) => (
711713
SimpleHttpResponse::Err(SimpleHttpResponseError::Io(err)),
712714
None,
715+
None
713716
),
714717
Ok(mut fc) => {
715718
let file_contents = try_read_as_text(&mut fc).await;
@@ -719,7 +722,7 @@ async fn make_simple_http_response(
719722
file_to_response(
720723
http_request,
721724
current_filepath,
722-
file_contents.as_deref(),
725+
file_contents,
723726
use_pdf_js,
724727
)
725728
.await
@@ -755,7 +758,7 @@ async fn file_to_response(
755758
// `try_canonicalize`.
756759
current_filepath: &Path,
757760
// Contents of this file, if it's text; None if it was binary data.
758-
file_contents: Option<&str>,
761+
file_contents: Option<String>,
759762
// True to use the PDF.js viewer for this file.
760763
use_pdf_js: bool,
761764
) -> (
@@ -765,6 +768,8 @@ async fn file_to_response(
765768
// populate the Client with the parsed `file_contents`. In all other cases,
766769
// return None.
767770
Option<UpdateMessageContents>,
771+
// The `file_contents` if this is a
772+
Option<String>
768773
) {
769774
// Use a lossy conversion, since this is UI display, not filesystem access.
770775
let file_path = &http_request.file_path;
@@ -774,6 +779,7 @@ async fn file_to_response(
774779
file_path.to_path_buf(),
775780
)),
776781
None,
782+
None
777783
);
778784
};
779785
let name = escape_html(&file_name.to_string_lossy());
@@ -791,6 +797,7 @@ async fn file_to_response(
791797
codechat_editor_js_name,
792798
)),
793799
None,
800+
None
794801
);
795802
};
796803
let codechat_editor_css_name = format!("CodeChatEditor{js_test_suffix}.css");
@@ -800,17 +807,18 @@ async fn file_to_response(
800807
codechat_editor_css_name,
801808
)),
802809
None,
810+
file_contents
803811
);
804812
};
805813

806814
// Compare these files, since both have been canonicalized by
807815
// `try_canonical`.
808816
let is_current_file = file_path == current_filepath;
809817
let is_toc = http_request.flags == ProcessingTaskHttpRequestFlags::Toc;
810-
let (translation_results_string, path_to_toc) = if let Some(file_contents_text) = file_contents
818+
let (translation_results_string, path_to_toc) = if let Some(ref file_contents_text) = file_contents
811819
{
812820
if is_current_file || is_toc {
813-
source_to_codechat_for_web_string(file_contents_text, file_path, is_toc)
821+
source_to_codechat_for_web_string(&file_contents_text, file_path, is_toc)
814822
} else {
815823
// If this isn't the current file, then don't parse it.
816824
(TranslationResultsString::Unknown, None)
@@ -852,6 +860,7 @@ async fn file_to_response(
852860
file_name,
853861
))),
854862
None,
863+
None
855864
);
856865
};
857866
return (
@@ -871,13 +880,14 @@ async fn file_to_response(
871880
},
872881
),
873882
None,
883+
None
874884
);
875885
}
876886

877887
let codechat_for_web = match translation_results_string {
878888
// The file type is binary. Ask the HTTP server to serve it raw.
879889
TranslationResultsString::Binary => return
880-
(SimpleHttpResponse::Bin(file_path.to_path_buf()), None)
890+
(SimpleHttpResponse::Bin(file_path.to_path_buf()), None, None)
881891
,
882892
// The file type is unknown. Serve it raw.
883893
TranslationResultsString::Unknown => {
@@ -887,11 +897,12 @@ async fn file_to_response(
887897
mime_guess::from_path(file_path).first_or_text_plain(),
888898
),
889899
None,
900+
None
890901
);
891902
}
892903
// Report a lexer error.
893904
TranslationResultsString::Err(err_string) => {
894-
return (SimpleHttpResponse::Err(SimpleHttpResponseError::LexerError(err_string)), None);
905+
return (SimpleHttpResponse::Err(SimpleHttpResponseError::LexerError(err_string)), None, None);
895906
}
896907
// This is a CodeChat file. The following code wraps the CodeChat for
897908
// web results in a CodeChat Editor Client webpage.
@@ -919,6 +930,7 @@ async fn file_to_response(
919930
</html>"#,
920931
)),
921932
None,
933+
None
922934
);
923935
}
924936
};
@@ -940,6 +952,7 @@ async fn file_to_response(
940952
file_path.to_path_buf(),
941953
)),
942954
None,
955+
None
943956
);
944957
};
945958
let dir = path_display(raw_dir);
@@ -949,6 +962,7 @@ async fn file_to_response(
949962
file_path.to_path_buf(),
950963
)),
951964
None,
965+
None
952966
);
953967
};
954968
// Build and return the webpage.
@@ -996,6 +1010,7 @@ async fn file_to_response(
9961010
cursor_position: None,
9971011
scroll_position: None,
9981012
}),
1013+
file_contents
9991014
)
10001015
}
10011016

server/src/webserver/filewatcher.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,7 @@ async fn processing_task(file_path: &Path, app_state: web::Data<AppState>, conne
534534
// file, which will still produce an error.
535535
let empty_path = PathBuf::new();
536536
let cfp = current_filepath.as_ref().unwrap_or(&empty_path);
537-
let (simple_http_response, option_update) = make_simple_http_response(&http_request, cfp, false).await;
537+
let (simple_http_response, option_update, _) = make_simple_http_response(&http_request, cfp, false).await;
538538
if let Some(update) = option_update {
539539
// Send the update to the client.
540540
queue_send!(to_websocket_tx.send(EditorMessage {

server/src/webserver/vscode.rs

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ use crate::{
5555
queue_send,
5656
webserver::{
5757
INITIAL_MESSAGE_ID, MESSAGE_ID_INCREMENT, ProcessingTaskHttpRequest, ResultOkTypes,
58-
SyncState, UpdateMessageContents, escape_html, file_to_response, filesystem_endpoint,
59-
get_server_url, html_wrapper, make_simple_http_response, path_to_url, try_canonicalize,
60-
try_read_as_text, url_to_path,
58+
SimpleHttpResponse, SimpleHttpResponseError, SyncState, UpdateMessageContents, escape_html,
59+
file_to_response, filesystem_endpoint, get_server_url, html_wrapper, path_to_url,
60+
try_canonicalize, try_read_as_text, url_to_path,
6161
},
6262
};
6363

@@ -454,21 +454,47 @@ pub async fn vscode_ide_websocket(
454454
// is a PDF file. (TODO: look at the magic
455455
// number also -- "%PDF").
456456
let use_pdf_js = http_request.file_path.extension() == Some(OsStr::new("pdf"));
457-
let (simple_http_response, option_update) = match file_contents_option {
457+
let (simple_http_response, option_update, file_contents) = match file_contents_option {
458458
Some(file_contents) => {
459459
// If there are Windows newlines, replace
460460
// with Unix; this is reversed when the
461461
// file is sent back to the IDE.
462462
eol = find_eol_type(&file_contents);
463-
let file_contents = file_contents.replace("\r\n", "\n");
464-
let ret = file_to_response(&http_request, &current_file, Some(&file_contents), use_pdf_js).await;
465-
source_code = file_contents;
466-
ret
463+
let file_contents = if use_pdf_js { file_contents } else { file_contents.replace("\r\n", "\n") };
464+
file_to_response(&http_request, &current_file, Some(file_contents), use_pdf_js).await
467465
},
468466
None => {
469467
// The file wasn't available in the IDE.
470468
// Look for it in the filesystem.
471-
make_simple_http_response(&http_request, &current_file, use_pdf_js).await
469+
match File::open(&http_request.file_path).await {
470+
Err(err) => (
471+
SimpleHttpResponse::Err(SimpleHttpResponseError::Io(err)),
472+
None,
473+
None
474+
),
475+
Ok(mut fc) => {
476+
let option_file_contents = try_read_as_text(&mut fc).await;
477+
let option_file_contents = if let Some(file_contents) = option_file_contents {
478+
eol = find_eol_type(&file_contents);
479+
let file_contents = if use_pdf_js { file_contents } else { file_contents.replace("\r\n", "\n") };
480+
Some(file_contents)
481+
} else {
482+
None
483+
};
484+
// <a id="binary-file-sniffer"></a>If this
485+
// is a binary file (meaning we can't read
486+
// the contents as UTF-8), send the
487+
// contents as none to signal this isn't a
488+
// text file.
489+
file_to_response(
490+
&http_request,
491+
&current_file,
492+
option_file_contents,
493+
use_pdf_js,
494+
)
495+
.await
496+
}
497+
}
472498
}
473499
};
474500
if let Some(update) = option_update {
@@ -482,6 +508,7 @@ pub async fn vscode_ide_websocket(
482508
};
483509
// We must clone here, since the original is
484510
// placed in the TX queue.
511+
source_code = file_contents.unwrap();
485512
code_mirror_doc = plain.doc.clone();
486513
code_mirror_doc_blocks = Some(plain.doc_blocks.clone());
487514
sync_state = SyncState::Pending(id);

server/src/webserver/vscode/tests.rs

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -831,6 +831,7 @@ async fn test_vscode_ide_websocket4() {
831831
// Message ids: IDE - 4, Server - 3, Client - 2->5.
832832
let file_path_temp = fs::canonicalize(test_dir.join("test.py")).unwrap();
833833
let file_path = simplified(&file_path_temp);
834+
let file_path_str = file_path.to_str().unwrap().to_string();
834835
send_message(
835836
&mut ws_client,
836837
&EditorMessage {
@@ -849,10 +850,7 @@ async fn test_vscode_ide_websocket4() {
849850
read_message(&mut ws_ide).await,
850851
EditorMessage {
851852
id: 2.0,
852-
message: EditorMessageContents::CurrentFile(
853-
file_path.to_str().unwrap().to_string(),
854-
Some(true)
855-
)
853+
message: EditorMessageContents::CurrentFile(file_path_str.clone(), Some(true))
856854
}
857855
);
858856

@@ -917,7 +915,7 @@ async fn test_vscode_ide_websocket4() {
917915
EditorMessage {
918916
id: 6.0,
919917
message: EditorMessageContents::Update(UpdateMessageContents {
920-
file_path: file_path.to_str().unwrap().to_string(),
918+
file_path: file_path_str.clone(),
921919
contents: Some(CodeChatForWeb {
922920
metadata: SourceFileMetadata {
923921
mode: "python".to_string(),
@@ -954,6 +952,76 @@ async fn test_vscode_ide_websocket4() {
954952
}
955953
);
956954

955+
// Send an update from the Client, which should produce a diff.
956+
//
957+
// Message ids: IDE - 4, Server - 9, Client - 5->8.
958+
send_message(
959+
&mut ws_client,
960+
&EditorMessage {
961+
id: 5.0,
962+
message: EditorMessageContents::Update(UpdateMessageContents {
963+
file_path: file_path_str.clone(),
964+
contents: Some(CodeChatForWeb {
965+
metadata: SourceFileMetadata {
966+
mode: "python".to_string(),
967+
},
968+
source: CodeMirrorDiffable::Plain(CodeMirror {
969+
doc: "More\n".to_string(),
970+
doc_blocks: vec![CodeMirrorDocBlock {
971+
from: 5,
972+
to: 6,
973+
indent: "".to_string(),
974+
delimiter: "#".to_string(),
975+
contents: "test.py".to_string(),
976+
}],
977+
}),
978+
}),
979+
cursor_position: None,
980+
scroll_position: None,
981+
}),
982+
},
983+
)
984+
.await;
985+
assert_eq!(
986+
read_message(&mut ws_ide).await,
987+
EditorMessage {
988+
id: 5.0,
989+
message: EditorMessageContents::Update(UpdateMessageContents {
990+
file_path: file_path_str.clone(),
991+
contents: Some(CodeChatForWeb {
992+
metadata: SourceFileMetadata {
993+
mode: "python".to_string(),
994+
},
995+
source: CodeMirrorDiffable::Diff(CodeMirrorDiff {
996+
doc: vec![StringDiff {
997+
from: 0,
998+
to: None,
999+
insert: format!("More{}", if cfg!(windows) { "\r\n" } else { "\n" }),
1000+
}],
1001+
doc_blocks: vec![],
1002+
}),
1003+
}),
1004+
cursor_position: None,
1005+
scroll_position: None,
1006+
})
1007+
}
1008+
);
1009+
send_message(
1010+
&mut ws_ide,
1011+
&EditorMessage {
1012+
id: 5.0,
1013+
message: EditorMessageContents::Result(Ok(ResultOkTypes::Void)),
1014+
},
1015+
)
1016+
.await;
1017+
assert_eq!(
1018+
read_message(&mut ws_client).await,
1019+
EditorMessage {
1020+
id: 5.0,
1021+
message: EditorMessageContents::Result(Ok(ResultOkTypes::Void))
1022+
}
1023+
);
1024+
9571025
check_logger_errors(0);
9581026
// Report any errors produced when removing the temporary directory.
9591027
temp_dir.close().unwrap();

0 commit comments

Comments
 (0)