Skip to content

Commit

Permalink
Automatically detect EOL from paste events and output setting
Browse files Browse the repository at this point in the history
  • Loading branch information
n1474335 committed Mar 26, 2024
1 parent c4e7c41 commit 16dfb3f
Show file tree
Hide file tree
Showing 7 changed files with 215 additions and 43 deletions.
39 changes: 39 additions & 0 deletions src/web/utils/editorUtils.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,42 @@ export function escapeControlChars(str, preserveWs=false, lineBreak="\n") {
return n.outerHTML;
});
}

/**
* Convert and EOL sequence to its name
*/
export const eolSeqToCode = {
"\u000a": "LF",
"\u000b": "VT",
"\u000c": "FF",
"\u000d": "CR",
"\u000d\u000a": "CRLF",
"\u0085": "NEL",
"\u2028": "LS",
"\u2029": "PS"
};

/**
* Convert an EOL name to its sequence
*/
export const eolCodeToSeq = {
"LF": "\u000a",
"VT": "\u000b",
"FF": "\u000c",
"CR": "\u000d",
"CRLF": "\u000d\u000a",
"NEL": "\u0085",
"LS": "\u2028",
"PS": "\u2029"
};

export const eolCodeToName = {
"LF": "Line Feed",
"VT": "Vertical Tab",
"FF": "Form Feed",
"CR": "Carriage Return",
"CRLF": "Carriage Return + Line Feed",
"NEL": "Next Line",
"LS": "Line Separator",
"PS": "Paragraph Separator"
};
39 changes: 10 additions & 29 deletions src/web/utils/statusBar.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import {showPanel} from "@codemirror/view";
import {CHR_ENC_SIMPLE_LOOKUP, CHR_ENC_SIMPLE_REVERSE_LOOKUP} from "../../core/lib/ChrEnc.mjs";
import { eolCodeToName, eolSeqToCode } from "./editorUtils.mjs";

/**
* A Status bar extension for CodeMirror
Expand Down Expand Up @@ -92,22 +93,12 @@ class StatusBarPanel {
// preventDefault is required to stop the URL being modified and popState being triggered
e.preventDefault();

const eolLookup = {
"LF": "\u000a",
"VT": "\u000b",
"FF": "\u000c",
"CR": "\u000d",
"CRLF": "\u000d\u000a",
"NEL": "\u0085",
"LS": "\u2028",
"PS": "\u2029"
};
const eolval = eolLookup[e.target.getAttribute("data-val")];

if (eolval === undefined) return;
const eolCode = e.target.getAttribute("data-val");
if (!eolCode) return;

// Call relevant EOL change handler
this.eolHandler(eolval);
this.eolHandler(e.target.getAttribute("data-val"), true);

hideElement(e.target.closest(".cm-status-bar-select-content"));
}

Expand Down Expand Up @@ -223,23 +214,13 @@ class StatusBarPanel {
updateEOL(state) {
if (state.lineBreak === this.eolVal) return;

const eolLookup = {
"\u000a": ["LF", "Line Feed"],
"\u000b": ["VT", "Vertical Tab"],
"\u000c": ["FF", "Form Feed"],
"\u000d": ["CR", "Carriage Return"],
"\u000d\u000a": ["CRLF", "Carriage Return + Line Feed"],
"\u0085": ["NEL", "Next Line"],
"\u2028": ["LS", "Line Separator"],
"\u2029": ["PS", "Paragraph Separator"]
};

const val = this.dom.querySelector(".eol-value");
const button = val.closest(".cm-status-bar-select-btn");
const eolName = eolLookup[state.lineBreak];
val.textContent = eolName[0];
button.setAttribute("title", `End of line sequence:<br>${eolName[1]}`);
button.setAttribute("data-original-title", `End of line sequence:<br>${eolName[1]}`);
const eolCode = eolSeqToCode[state.lineBreak];
const eolName = eolCodeToName[eolCode];
val.textContent = eolCode;
button.setAttribute("title", `End of line sequence:<br>${eolName}`);
button.setAttribute("data-original-title", `End of line sequence:<br>${eolName}`);
this.eolVal = state.lineBreak;
}

Expand Down
9 changes: 5 additions & 4 deletions src/web/waiters/ControlsWaiter.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

import Utils from "../../core/Utils.mjs";
import { eolSeqToCode } from "../utils/editorUtils.mjs";


/**
Expand Down Expand Up @@ -140,16 +141,16 @@ class ControlsWaiter {

const inputChrEnc = this.manager.input.getChrEnc();
const outputChrEnc = this.manager.output.getChrEnc();
const inputEOLSeq = this.manager.input.getEOLSeq();
const outputEOLSeq = this.manager.output.getEOLSeq();
const inputEOL = eolSeqToCode[this.manager.input.getEOLSeq()];
const outputEOL = eolSeqToCode[this.manager.output.getEOLSeq()];

const params = [
includeRecipe ? ["recipe", recipeStr] : undefined,
includeInput && input.length ? ["input", Utils.escapeHtml(input)] : undefined,
inputChrEnc !== 0 ? ["ienc", inputChrEnc] : undefined,
outputChrEnc !== 0 ? ["oenc", outputChrEnc] : undefined,
inputEOLSeq !== "\n" ? ["ieol", inputEOLSeq] : undefined,
outputEOLSeq !== "\n" ? ["oeol", outputEOLSeq] : undefined
inputEOL !== "LF" ? ["ieol", inputEOL] : undefined,
outputEOL !== "LF" ? ["oeol", outputEOL] : undefined
];

const hash = params
Expand Down
88 changes: 84 additions & 4 deletions src/web/waiters/InputWaiter.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import {

import {statusBar} from "../utils/statusBar.mjs";
import {fileDetailsPanel} from "../utils/fileDetails.mjs";
import {renderSpecialChar} from "../utils/editorUtils.mjs";
import {eolCodeToSeq, eolCodeToName, renderSpecialChar} from "../utils/editorUtils.mjs";


/**
Expand All @@ -62,6 +62,7 @@ class InputWaiter {

this.inputTextEl = document.getElementById("input-text");
this.inputChrEnc = 0;
this.eolSetManually = false;
this.initEditor();

this.inputWorker = null;
Expand Down Expand Up @@ -92,6 +93,7 @@ class InputWaiter {
fileDetailsPanel: new Compartment
};

const self = this;
const initialState = EditorState.create({
doc: null,
extensions: [
Expand Down Expand Up @@ -141,6 +143,15 @@ class InputWaiter {
if (e.docChanged && !this.silentInputChange)
this.inputChange(e);
this.silentInputChange = false;
}),

// Event handlers
EditorView.domEventHandlers({
paste(event, view) {
setTimeout(() => {
self.afterPaste(event);
});
}
})
]
});
Expand All @@ -154,12 +165,35 @@ class InputWaiter {
/**
* Handler for EOL change events
* Sets the line separator
* @param {string} eolVal
* @param {string} eol
* @param {boolean} manual - a flag for whether this was set by the user or automatically
*/
eolChange(eolVal) {
const oldInputVal = this.getInput();
eolChange(eol, manual=false) {
const eolVal = eolCodeToSeq[eol];
if (eolVal === undefined) return;

const eolBtn = document.querySelector("#input-text .eol-value");
if (manual) {
this.eolSetManually = true;
eolBtn.classList.remove("font-italic");
} else {
eolBtn.classList.add("font-italic");
}

if (eolVal === this.getEOLSeq()) return;

if (!manual) {
// Pulse
eolBtn.classList.add("pulse");
setTimeout(() => {
eolBtn.classList.remove("pulse");
}, 2000);
// Alert
this.app.alert(`Input EOL separator has been changed to ${eolCodeToName[eol]}`, 5000);
}

// Update the EOL value
const oldInputVal = this.getInput();
this.inputEditorView.dispatch({
effects: this.inputEditorConf.eol.reconfigure(EditorState.lineSeparator.of(eolVal))
});
Expand Down Expand Up @@ -866,6 +900,49 @@ class InputWaiter {
}, delay, "inputChange", this, [e])();
}

/**
* Handler that fires just after input paste events.
* Checks whether the EOL separator or character encoding should be updated.
*
* @param {event} e
*/
afterPaste(e) {
// If EOL has been fixed, skip this.
if (this.eolSetManually) return;

const inputText = this.getInput();

// Detect most likely EOL sequence
const eolCharCounts = {
"LF": inputText.count("\u000a"),
"VT": inputText.count("\u000b"),
"FF": inputText.count("\u000c"),
"CR": inputText.count("\u000d"),
"CRLF": inputText.count("\u000d\u000a"),
"NEL": inputText.count("\u0085"),
"LS": inputText.count("\u2028"),
"PS": inputText.count("\u2029")
};

// If all zero, leave alone
const total = Object.values(eolCharCounts).reduce((acc, curr) => {
return acc + curr;
}, 0);
if (total === 0) return;

// If CRLF not zero and more than half the highest alternative, choose CRLF
const highest = Object.entries(eolCharCounts).reduce((acc, curr) => {
return curr[1] > acc[1] ? curr : acc;
}, ["LF", 0]);
if ((eolCharCounts.CRLF * 2) > highest[1]) {
this.eolChange("CRLF");
return;
}

// Else choose max
this.eolChange(highest[0]);
}

/**
* Handler for input dragover events.
* Gives the user a visual cue to show that items can be dropped here.
Expand Down Expand Up @@ -1199,6 +1276,9 @@ class InputWaiter {
this.manager.output.removeAllOutputs();
this.manager.output.terminateZipWorker();

this.eolSetManually = false;
this.manager.output.eolSetManually = false;

const tabsList = document.getElementById("input-tabs");
const tabsListChildren = tabsList.children;

Expand Down
76 changes: 73 additions & 3 deletions src/web/waiters/OutputWaiter.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import {
import {statusBar} from "../utils/statusBar.mjs";
import {htmlPlugin} from "../utils/htmlWidget.mjs";
import {copyOverride} from "../utils/copyOverride.mjs";
import {renderSpecialChar} from "../utils/editorUtils.mjs";
import {eolCodeToSeq, eolCodeToName, renderSpecialChar} from "../utils/editorUtils.mjs";


/**
Expand Down Expand Up @@ -70,6 +70,7 @@ class OutputWaiter {
this.zipWorker = null;
this.maxTabs = this.manager.tabs.calcMaxTabs();
this.tabTimeout = null;
this.eolSetManually = false;
}

/**
Expand Down Expand Up @@ -146,9 +147,33 @@ class OutputWaiter {
/**
* Handler for EOL change events
* Sets the line separator
* @param {string} eolVal
* @param {string} eol
* @param {boolean} manual - a flag for whether this was set by the user or automatically
*/
async eolChange(eolVal) {
async eolChange(eol, manual=false) {
const eolVal = eolCodeToSeq[eol];
if (eolVal === undefined) return;

const eolBtn = document.querySelector("#output-text .eol-value");
if (manual) {
this.eolSetManually = true;
eolBtn.classList.remove("font-italic");
} else {
eolBtn.classList.add("font-italic");
}

if (eolVal === this.getEOLSeq()) return;

if (!manual) {
// Pulse
eolBtn.classList.add("pulse");
setTimeout(() => {
eolBtn.classList.remove("pulse");
}, 2000);
// Alert
this.app.alert(`Output EOL separator has been changed to ${eolCodeToName[eol]}`, 5000);
}

const currentTabNum = this.manager.tabs.getActiveTab("output");
if (currentTabNum >= 0) {
this.outputs[currentTabNum].eolSequence = eolVal;
Expand Down Expand Up @@ -276,6 +301,9 @@ class OutputWaiter {
// If turning word wrap off, do it before we populate the editor for performance reasons
if (!wrap) this.setWordWrap(wrap);

// Detect suitable EOL sequence
this.detectEOLSequence(data);

// We use setTimeout here to delay the editor dispatch until the next event cycle,
// ensuring all async actions have completed before attempting to set the contents
// of the editor. This is mainly with the above call to setWordWrap() in mind.
Expand Down Expand Up @@ -345,6 +373,48 @@ class OutputWaiter {
});
}

/**
* Checks whether the EOL separator should be updated
*
* @param {string} data
*/
detectEOLSequence(data) {
// If EOL has been fixed, skip this.
if (this.eolSetManually) return;
// If data is too long, skip this.
if (data.length > 1000000) return;

// Detect most likely EOL sequence
const eolCharCounts = {
"LF": data.count("\u000a"),
"VT": data.count("\u000b"),
"FF": data.count("\u000c"),
"CR": data.count("\u000d"),
"CRLF": data.count("\u000d\u000a"),
"NEL": data.count("\u0085"),
"LS": data.count("\u2028"),
"PS": data.count("\u2029")
};

// If all zero, leave alone
const total = Object.values(eolCharCounts).reduce((acc, curr) => {
return acc + curr;
}, 0);
if (total === 0) return;

// If CRLF not zero and more than half the highest alternative, choose CRLF
const highest = Object.entries(eolCharCounts).reduce((acc, curr) => {
return curr[1] > acc[1] ? curr : acc;
}, ["LF", 0]);
if ((eolCharCounts.CRLF * 2) > highest[1]) {
this.eolChange("CRLF");
return;
}

// Else choose max
this.eolChange(highest[0]);
}

/**
* Calculates the maximum number of tabs to display
*/
Expand Down
1 change: 1 addition & 0 deletions tests/browser/00_nightwatch.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ module.exports = {

// Alert bar shows and contains correct content
browser
.waitForElementNotVisible("#snackbar-container")
.click("#copy-output")
.waitForElementVisible("#snackbar-container")
.waitForElementVisible("#snackbar-container .snackbar-content")
Expand Down
Loading

0 comments on commit 16dfb3f

Please sign in to comment.