etherpad-lite/doc/api/editorInfo.md
John McLear 884ac93b4e
feat(editor): add IDE-style line ops (duplicate / delete) (#7564)
* 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>
2026-04-29 09:21:05 +01:00

6.1 KiB

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.