Skip to content

Text overrides restored file due to trailing newline #9077

Open

Description

@AndyScherzinger

Describe the bug
Text auto saves unchanged file upon opening, no editing, due to a normalizer behavior. See videos:

To Reproduce
From the video, you need to restore a file and needs to have one specific thing, a trailing newline::

  1. restored version : 150 bytes, tail "...old line :)\n\n🌴\n"
  2. what Text wrote : 149 bytes, tail "...old line :)\n\n🌴"
  3. first difference at byte 149: restored "\n" vs written ""

Expected behavior
The serialiser should preserve a trailing newline, or SaveService should not write when the only difference is its own normalization. It's easy to demonstrate without Notes at all - put a markdown file ending in a newline in Files, open it in the Viewer, don't touch it, and a version appears within ~10 seconds.

Screenshots

text_in_files_viewer.mp4
text_editor.mp4

Server details:

  • Nextcloud version: master
  • PHP Version: -
  • Database: -
  • I use the docker dev setup

Client details:

  • OS: Win11pro
  • Browser: FF
  • Browser version: 153.0.4
  • Device: Desktop
Logs

Nextcloud log (data/nextcloud.log)

Insert your Nextcloud log here

Browser log

Insert your browser log here, this could for example include:

a) The javascript console log
b) The network log
c) ...

Activity

  1. micha-wb commented on Oct 5, 2026

    @micha-wb

    I can confirm this on a production instance, with a few additional observations that may help narrow it down.

    Environment

    • Nextcloud Hub 26 Spring (34.0.4), Text 8.0.0
    • Files synced with the macOS desktop client; the Markdown files were written by a script on the client and compared byte by byte with a reference copy kept outside the synced folder
    • Browser: Firefox 156.0.1 (64-Bit)

    What we tested (two Markdown files with identical content, about 1.7 KB each, both ending with a single trailing newline; one named README.md, one named differently)

    Server state Entering the folder in Files Clicking the other .md file, no typing
    defaults README.md rewritten file rewritten
    open_read_only_enabled = 1 README.md still rewritten file still rewritten
    open_read_only_enabled = 1 and workspace_available = 0 unchanged not tested in this state
    workspace_available = 0, Text app disabled unchanged unchanged (plain download)

    Additional observations

    1. The rich workspace triggers it without any click. Just entering a folder in the Files app rewrote that folder's README.md on the server within about a minute. We did not click a file and did not click into the workspace area. workspace_available = 0 stopped this.

    2. open_read_only_enabled = 1 does not prevent the write. With the setting enabled, the file opened read-only as expected (typing was only possible after pressing "Edit"), but the file on the server was rewritten anyway, without "Edit" ever being pressed. The same was true for the rich workspace. So the setting protects against accidental typing, but not against the save described in this issue.

    3. It is not only the trailing newline that changes. Because the whole document is serialized again, the automatic save applies all normalizations at once. In our test file (1719 bytes before, 1747 bytes after):

      • _emphasis_ became *emphasis*
      • an ordered list written as 1. / 1. became 1. / 2.
      • adjacent lists using -, * and + were separated by two blank lines each
      • table columns were re-padded, and a left-alignment marker :--- was reduced to ---
      • a pipe inside inline code in a table cell was escaped with a backslash
      • a closed ATX heading ### Title ### became ### Title
      • two consecutive blank lines were collapsed into one
      • the trailing newline was removed

      Front matter, backslash and two-space hard breaks, fenced code blocks, HTML and HTML comments were left untouched.

    4. A file is only rewritten once. After the first automatic save the file no longer ends with a newline, and opening it again did not change it. This matches the trigger described above.

    Why this matters to us

    The rewritten file is synced to every client, so viewing a folder in the browser silently changes files on all devices. The modification time no longer says "last edited by a person", and any comparison by size or checksum reports a change that nobody made. For files that are tracked or checked elsewhere, this is hard to notice and hard to trace back.

    We have disabled the Text app on this instance for now. I am happy to test a fix or to provide the test file.

  2. kaibae19 commented on Oct 15, 2026

    @kaibae19

    Still reproducible on 35.0.2 / Text 9.0.1 (includes #9350)

    Environment: Nextcloud 35.0.2 (Docker nextcloud:35-fpm, upgraded from 35.0.1 today, occ upgrade successful), Text 9.0.1, PostgreSQL 15. The file lives on External Storage (SFTP). open_read_only_enabled is on.

    Steps:

    1. Create a Markdown file outside Nextcloud (on the SFTP storage) with:
      Some **bold `code` bold** text and a quoted *"emphasis in quotes"* phrase.
      
      1. first
      1. second (both numbered 1.)
      
      | left | right |
      |:---|---:|
      | a | b |
      
      
      ### H ###
    2. In the web UI (after a hard refresh), open it from Files without clicking Edit, wait ~20 s, close the viewer.

    Observed: the file is rewritten. Webserver log for one open:

    15:31:48  PUT  /apps/text/session/file/<id>/create   200
    15:31:51  POST /apps/text/session/<sid>/save          200   ← 3 s after open, before any interaction or close
    15:32:17  POST /apps/text/session/<sid>/close         200
    

    Reproduced 2/2, the second time after a hard refresh (save 1 s after open). Resulting diff:

    -Some **bold `code` bold** text …
    +Some **bold** `code` **bold** text …
    -1. second (both numbered 1.)
    +2. second (both numbered 1.)
    -|:---|---:|
    -| a | b |
    +|------|------:|
    +| a    |     b |
    -### H ###
    +### H
    (and the trailing newline is dropped)

    So with #9350 in place, the initial load still produces a save. It also changes Markdown semantics (emphasis split, column alignment lost), not just whitespace. Happy to test a patch.

  3. kaibae19 commented on Oct 15, 2026

    @kaibae19

    A possible cause (from reading the v35.0.2 source; not yet confirmed with debug logs in the browser)

    The webserver log shows the same order on all three opens:

    15:31:48 create → 15:31:49 push → 15:31:51 save
    15:32:22 create → 15:32:22 push → 15:32:24 save
    15:34:03 create → 15:34:04 push → 15:34:06 save
    

    That would fit this chain:

    1. No document session yet → setInitialYjsState() builds the Y.js state from the markdown (this conversion is where **bold `code` bold** gets split and :--- is lost).
    2. That initial state is pushed like a local edit; the server returns a new version and changesPushed is emitted.
    3. updateVersionWithChanges raises versionWithChanges, so dirty = lastSavedVersion < versionWithChanges becomes true without any edit.
    4. Once synced, the offline-changes branch runs:
      if (this.dirty) { …sendUpdateFromDoc('offline', …); this.saveService.autosave() } — a direct autosave() call that doesn't go through the new addToHistory check from [stable35] fix(autosave): only save after changes in the editor #9350.
    5. AUTOSAVE_DEBOUNCE = 1 s later the re-serialised markdown is saved.

    The #9350 guard itself seems right for the editor path: y-prosemirror's sync transactions are created with addToHistory: false (y-prosemirror 1.3.7 _tr), so the initial load doesn't trigger autosave there. It's the dirty path in step 4 that remains.

    Possible directions: don't count the initial-state push towards versionWithChanges/dirty (e.g. tag it by origin), only take the offline-save branch when there are real offline edits (the IndexedDB dirty flag rather than the computed one), or not push the initial state at all (#9103).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions