feat(editor): selectable LF/CRLF line endings in code viewer (#3009)

Files saved from the built-in code editor always came out CRLF, which
breaks scripts downloaded to Linux. The editor settings menu now offers
a Line ending choice (LF/CRLF) applied via model.setEOL; the choice is
detected from the file's existing endings on load, and the saved payload
is normalized to the selected sequence.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
pull/3582/head
Tomas Dvorak 2 weeks ago
parent c87f3015de
commit 9c70c554cb

@ -468,7 +468,8 @@
"noEncryption": "Not encrypted",
"ftsStatus": "Full-text index",
"ftsIndexed": "Indexed",
"ftsNotIndexed": "Not indexed"
"ftsNotIndexed": "Not indexed",
"lineEnding": "Line ending"
},
"modals": {
"includePasswordInShareLink": "Include password in share link",

@ -468,7 +468,8 @@
"noEncryption": "未加密",
"ftsStatus": "全文索引",
"ftsIndexed": "已索引",
"ftsNotIndexed": "未索引"
"ftsNotIndexed": "未索引",
"lineEnding": "换行符"
},
"modals": {
"includePasswordInShareLink": "在链接中包含密码",

@ -108,6 +108,7 @@ const CodeViewer = () => {
const [optionAnchorEl, setOptionAnchorEl] = useState<null | HTMLElement>(null);
const [language, setLng] = useState<string | null>(null);
const [wordWrap, setWordWrap] = useState<"off" | "on" | "wordWrapColumn" | "bounded">("off");
const [eol, setEol] = useState<"LF" | "CRLF">("LF");
const saveFunction = useRef<() => void>(() => {});
const closeViewer = useCallback(() => {
@ -141,7 +142,11 @@ const CodeViewer = () => {
setOptionAnchorEl(null);
dispatch(getEntityContent(viewerState.file, viewerState.version))
.then((res) => {
setValue(new TextDecoder(charset).decode(res));
const content = new TextDecoder(charset).decode(res);
setValue(content);
// Preserve the file's existing line endings as the default
// (#3009): CRLF files keep CRLF, everything else uses LF.
setEol(content.includes("\r\n") ? "CRLF" : "LF");
setLoaded(true);
})
.catch(() => {
@ -188,7 +193,8 @@ const CodeViewer = () => {
}
setLoading(true);
dispatch(saveCode(value, viewerState.file, viewerState.version, saveAs))
const eolChar = eol == "LF" ? "\n" : "\r\n";
dispatch(saveCode(value.replace(/\r\n|\r|\n/g, eolChar), viewerState.file, viewerState.version, saveAs))
.then(() => {
setSaved(true);
})
@ -196,7 +202,7 @@ const CodeViewer = () => {
setLoading(false);
});
},
[value, viewerState],
[value, viewerState, eol],
);
const onChange = useCallback((v: string) => {
@ -298,6 +304,18 @@ const CodeViewer = () => {
))}
</CascadingSubmenu>
<DenseDivider />
<CascadingSubmenu popupId={"eol"} title={t("fileManager.lineEnding")}>
{(["LF", "CRLF"] as const).map((e) => (
<SquareMenuItem key={e} onClick={() => setEol(e)} dense>
<ListItemText>{e}</ListItemText>
{e == eol && (
<ListItemIcon>
<Checkmark />
</ListItemIcon>
)}
</SquareMenuItem>
))}
</CascadingSubmenu>
<SquareMenuItem onClick={toggleWordWrap} dense>
<ListItemText>{t("fileManager.wordWrap")}</ListItemText>
{wordWrap === "on" && (
@ -327,6 +345,7 @@ const CodeViewer = () => {
}}
value={value}
language={language ?? ""}
eol={eol}
onChange={(v) => onChange(v as string)}
/>
</Box>

@ -107,6 +107,11 @@ export interface MonacoEditorProps extends MonacoEditorBaseProps {
minHeight?: string | number;
onSave?: React.MutableRefObject<() => void>;
/**
* Desired end-of-line sequence applied to the model via setEOL.
*/
eol?: "LF" | "CRLF";
}
// ============ Diff Editor ============
@ -206,6 +211,7 @@ function MonacoEditor({
className,
uri,
onSave,
eol,
}: MonacoEditorProps) {
const containerElement = useRef<HTMLDivElement | null>(null);
@ -322,6 +328,13 @@ function MonacoEditor({
}
}, [language]);
useEffect(() => {
if (editor.current && eol) {
const model = editor.current.getModel();
model?.setEOL(eol == "LF" ? monaco.editor.EndOfLineSequence.LF : monaco.editor.EndOfLineSequence.CRLF);
}
}, [eol]);
useEffect(() => {
if (editor.current) {
// Don't pass in the model on update because monaco crashes if we pass the model

Loading…
Cancel
Save