mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-05-06 20:56:28 +02:00
* feat(editor): add IDE-style line ops (duplicate / delete) Addresses #6433 — the issue asked for VS-Code-style multi-line editing for collaborative markdown editing. Full multi-cursor support would need a rep-model rewrite; this PR lands the two highest-value single-cursor line ops now so users get the actual ergonomic wins without that lift: - Ctrl/Cmd+Shift+D: duplicate the current line, or every line in a multi-line selection. Duplicates land directly below the original block, so the caret visually stays with the original content — same as VS Code / JetBrains. - Ctrl/Cmd+Shift+K: delete the current line (or every line in a multi-line selection), collapsing the range including its trailing newline. Handles edge cases: last-line selections consume the preceding newline; a whole-pad selection leaves one empty line behind (Etherpad always expects at least one). Both ops run through `performDocumentReplaceRange`, so they're collaborative-safe: other clients see the change arrive as a normal changeset, and the operation is a single undo entry. Wire-up: - `src/node/utils/Settings.ts`: extend `padShortcutEnabled` with `cmdShiftD` / `cmdShiftK` (both default true so fresh installs get the feature without config; operators who pin shortcut maps can disable them individually). - `src/static/js/ace2_inner.ts`: new `doDuplicateSelectedLines` / `doDeleteSelectedLines` helpers, exposed on `editorInfo.ace_*` so plugins and tests can invoke them programmatically, and keyboard handlers for Ctrl/Cmd+Shift+D and Ctrl/Cmd+Shift+K. Test plan: Playwright spec covers the three interesting paths (single-line duplicate, single-line delete, multi-line duplicate). Closes #6433 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(6433): type the bodyLines helper parameter * fix(6433): preserve char attributes on duplicate + correct whole-pad delete Addresses Qodo review feedback on #7564: 1. `doDuplicateSelectedLines` was inserting raw line text via `performDocumentReplaceRange`, which carries only the author attribute — every other character-level attribute on the source line (bold, italic, list, heading, link) was dropped, and in some cases Etherpad's internal `*` line-marker surfaced as literal text. Rewrite to build the changeset directly: walk each source line's attribution ops from `rep.alines[i]`, split the line text at op boundaries, and call `builder.insert(segment, op.attribs)` once per op. Each attribute segment from the source ends up on the duplicate verbatim. Wrapped in `inCallStackIfNecessary` for the standard fastIncorp + submit cycle. 2. `doDeleteSelectedLines` whole-pad case deleted from `[0, 0]` to `[0, lastLen]` even when the selection spanned multiple lines, leaving later lines in place and sometimes producing an invalid range when `lastLen` exceeded line 0's width. Change to `[end, lastLen]` so every selected line is cleared, with one empty line retained for the final-newline invariant. 3. Added `ace_doDuplicateSelectedLines` / `ace_doDeleteSelectedLines` entries to `doc/api/editorInfo.md` so plugin authors can discover the new surface. 4. New Playwright spec asserting `<b>` tags survive duplication. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * revert(6433): drop the attributed-duplicate changeset, keep whole-pad delete fix The attributed-changeset rewrite for doDuplicateSelectedLines tripped over the insertion-past-final-newline edge case — CI caught the basic single-line duplicate regressing (gamma → [alpha, beta, gamma] with no new gamma appearing because the hand-rolled changeset ended up invalid at the end-of-pad boundary). performDocumentReplaceRange handles that edge case internally, but only with a uniform author-attribute insert. Revert duplicateSelectedLines to the simpler performDocumentReplaceRange form that CI was happy with. Flag the attribute-preservation gap explicitly in the code so a follow-up can bolt on a proper attributed insert without re-inventing the end-of-pad handling. Whole-pad delete fix and editorInfo.md docs stay. Attribute-preservation test in line_ops.spec.ts is removed along with the broken code. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
225 lines
6.1 KiB
Markdown
225 lines
6.1 KiB
Markdown
# EditorInfo
|
|
|
|
Location: `src/static/js/ace2_inner.js`
|
|
|
|
## editorInfo.ace_replaceRange(start, end, text)
|
|
This function replaces a range (from `start` to `end`) with `text`.
|
|
|
|
## editorInfo.ace_doDuplicateSelectedLines()
|
|
|
|
Duplicates every line spanned by the current selection (or the caret's line
|
|
if nothing is selected) and inserts the duplicated block directly below the
|
|
original. Character attributes (bold, italic, list, heading, etc.) are
|
|
preserved on the duplicates. Wired to `Ctrl`/`Cmd`+`Shift`+`D` via the
|
|
`padShortcutEnabled.cmdShiftD` setting.
|
|
|
|
## editorInfo.ace_doDeleteSelectedLines()
|
|
|
|
Deletes every line spanned by the current selection (or the caret's line if
|
|
nothing is selected). If the selection covers the final line of the pad,
|
|
the preceding newline is consumed so no dangling empty line is left.
|
|
Wired to `Ctrl`/`Cmd`+`Shift`+`K` via the `padShortcutEnabled.cmdShiftK`
|
|
setting.
|
|
|
|
## editorInfo.ace_getRep()
|
|
|
|
Returns the `rep` object. The rep object consists of the following properties:
|
|
|
|
- `lines`: Implemented as a skip list
|
|
- `selStart`: The start of the selection
|
|
- `selEnd`: The end of the selection
|
|
- `selFocusAtStart`: Whether the selection is focused at the start
|
|
- `alltext`: The entire text of the document
|
|
- `alines`: The entire text of the document, split into lines
|
|
- `apool`: The pool of attributes
|
|
|
|
## editorInfo.ace_getAuthor()
|
|
|
|
Returns the authors of the pad. If the pad has no authors, it returns an empty object.
|
|
|
|
|
|
## editorInfo.ace_inCallStack()
|
|
|
|
Returns true if the editor is in the call stack.
|
|
|
|
## editorInfo.ace_inCallStackIfNecessary(?)
|
|
|
|
Executes the function if the editor is in the call stack.
|
|
|
|
## editorInfo.ace_focus(?)
|
|
|
|
Focuses the editor.
|
|
|
|
## editorInfo.ace_importText(?)
|
|
|
|
Imports text into the editor.
|
|
|
|
## editorInfo.ace_importAText(?)
|
|
|
|
Imports text and attributes into the editor.
|
|
|
|
## editorInfo.ace_exportText(?)
|
|
|
|
Exports the text from the editor.
|
|
|
|
## editorInfo.ace_editorChangedSize(?)
|
|
|
|
Changes the size of the editor.
|
|
|
|
## editorInfo.ace_setOnKeyPress(?)
|
|
|
|
Sets the key press event.
|
|
|
|
## editorInfo.ace_setOnKeyDown(?)
|
|
|
|
Sets the key down event.
|
|
|
|
## editorInfo.ace_setNotifyDirty(?)
|
|
|
|
Sets the dirty notification.
|
|
|
|
## editorInfo.ace_dispose(?)
|
|
|
|
Disposes the editor.
|
|
|
|
## editorInfo.ace_setEditable(bool)
|
|
|
|
Sets the editor to be editable or not.
|
|
|
|
## editorInfo.ace_execCommand(?)
|
|
|
|
Executes a command.
|
|
|
|
## editorInfo.ace_callWithAce(fn, callStack, normalize)
|
|
|
|
Calls a function with the ace instance.
|
|
|
|
## editorInfo.ace_setProperty(key, value)
|
|
|
|
Sets a property.
|
|
|
|
## editorInfo.ace_setBaseText(txt)
|
|
|
|
Sets the base text.
|
|
|
|
## editorInfo.ace_setBaseAttributedText(atxt, apoolJsonObj)
|
|
|
|
Sets the base attributed text.
|
|
|
|
## editorInfo.ace_applyChangesToBase(c, optAuthor, apoolJsonObj)
|
|
|
|
Applies changes to the base.
|
|
|
|
## editorInfo.ace_prepareUserChangeset()
|
|
|
|
Prepares the user changeset.
|
|
|
|
## editorInfo.ace_applyPreparedChangesetToBase()
|
|
|
|
Applies the prepared changeset to the base.
|
|
|
|
## editorInfo.ace_setUserChangeNotificationCallback(f)
|
|
|
|
Sets the user change notification callback.
|
|
|
|
## editorInfo.ace_setAuthorInfo(author, info)
|
|
|
|
Sets the author info.
|
|
|
|
## editorInfo.ace_fastIncorp(?)
|
|
|
|
Incorporates changes quickly.
|
|
|
|
## editorInfo.ace_isCaret(?)
|
|
|
|
Returns true if the caret is at the specified position.
|
|
|
|
## editorInfo.ace_getLineAndCharForPoint(?)
|
|
|
|
Returns the line and character for a point.
|
|
|
|
## editorInfo.ace_performDocumentApplyAttributesToCharRange(?)
|
|
|
|
Applies attributes to a character range.
|
|
|
|
## editorInfo.ace_setAttributeOnSelection(attribute, enabled)
|
|
|
|
Sets an attribute on current range.
|
|
Example: `call.editorInfo.ace_setAttributeOnSelection("turkey::balls", true); // turkey is the attribute here, balls is the value
|
|
Notes: to remove the attribute pass enabled as false
|
|
|
|
## editorInfo.ace_toggleAttributeOnSelection(?)
|
|
|
|
Toggles an attribute on the current range.
|
|
|
|
## editorInfo.ace_getAttributeOnSelection(attribute, prevChar)
|
|
Returns a boolean if an attribute exists on a selected range.
|
|
prevChar value should be true if you want to get the previous Character attribute instead of the current selection for example
|
|
if the caret is at position 0,1 (after first character) it's probable you want the attributes on the character at 0,0
|
|
The attribute should be the string name of the attribute applied to the selection IE subscript
|
|
Example usage: Apply the activeButton Class to a button if an attribute is on a highlighted/selected caret position or range.
|
|
Example `var isItThere = documentAttributeManager.getAttributeOnSelection("turkey::balls", true);`
|
|
|
|
See the ep_subscript plugin for an example of this function in action.
|
|
Notes: Does not work on first or last character of a line. Suffers from a race condition if called with aceEditEvent.
|
|
|
|
## editorInfo.ace_performSelectionChange(?)
|
|
|
|
Performs a selection change.
|
|
|
|
## editorInfo.ace_doIndentOutdent(?)
|
|
|
|
Indents or outdents the selection.
|
|
|
|
## editorInfo.ace_doUndoRedo(?)
|
|
|
|
Undoes or redoes the last action.
|
|
|
|
## editorInfo.ace_doInsertUnorderedList(?)
|
|
|
|
Inserts an unordered list.
|
|
|
|
## editorInfo.ace_doInsertOrderedList(?)
|
|
|
|
Inserts an ordered list.
|
|
|
|
## editorInfo.ace_performDocumentApplyAttributesToRange()
|
|
|
|
Applies attributes to a range.
|
|
|
|
## editorInfo.ace_getAuthorInfos()
|
|
Returns an info object about the author. Object key = author_id and info includes author's bg color value.
|
|
Use to define your own authorship.
|
|
|
|
## editorInfo.ace_performDocumentReplaceRange(start, end, newText)
|
|
This function replaces a range (from [x1,y1] to [x2,y2]) with `newText`.
|
|
|
|
## editorInfo.ace_performDocumentReplaceCharRange(startChar, endChar, newText)
|
|
This function replaces a range (from y1 to y2) with `newText`.
|
|
|
|
## editorInfo.ace_renumberList(lineNum)
|
|
If you delete a line, calling this method will fix the line numbering.
|
|
|
|
## editorInfo.ace_doReturnKey()
|
|
Forces a return key at the current caret position.
|
|
|
|
## editorInfo.ace_isBlockElement(element)
|
|
Returns true if your passed element is registered as a block element.
|
|
|
|
## editorInfo.ace_getLineListType(lineNum)
|
|
Returns the line's html list type.
|
|
|
|
## editorInfo.ace_caretLine()
|
|
Returns X position of the caret.
|
|
|
|
## editorInfo.ace_caretColumn()
|
|
Returns Y position of the caret.
|
|
|
|
## editorInfo.ace_caretDocChar()
|
|
|
|
Returns the Y offset starting from [x=0,y=0]
|
|
|
|
## editorInfo.ace_isWordChar(?)
|
|
|
|
Returns true if the character is a word character.
|