Skip to content

fix(datagrid): scroll long text values in the row inspector - #2373

Merged
datlechin merged 1 commit into
mainfrom
fix/inspector-long-text-scroll
Aug 22, 2026
Merged

fix(datagrid): scroll long text values in the row inspector#2373
datlechin merged 1 commit into
mainfrom
fix/inspector-long-text-scroll

Conversation

@datlechin

@datlechin datlechin commented Aug 21, 2026

Copy link
Copy Markdown
Member

Reported in chat: "string field in inspector detail pane cannot scrollable when have large string in that field".

What was wrong

Three mechanisms, all in the row-details inspector (right panel, Details tab).

1. The control has no scroll view. SwiftUI's TextField is an NSTextField on macOS at every axis setting. Dumping the NSView tree under NSHostingView for TextField(text:, axis: .vertical).lineLimit(3...6) holding a 400-line value gives:

AppKitPlatformViewHost<PlatformViewRepresentableAdaptor<PlatformTextFieldAdaptor>>
  AppKitTextField frame={{0, 0}, {268, 109.5}} [editable=true selectable=true enabled=true]
    NSSimpleLabel frame={{4, 2}, {260, 105.5}}

There is no NSScrollView and no NSScroller anywhere in that subtree, so lineLimit clips and there is nothing to scroll. Focusing the field installs a field editor 12,800pt tall inside the 109.5pt frame. TextEditor produces AppKitScrollView -> PlatformTextView with a live NSScroller instead, and a synthesized wheel event moves it from 0 to 6473.5.

2. Read-only removed the last way out. MultiLineEditorView and SingleLineEditorView applied .disabled(context.isReadOnly), which sets isEnabled = false on the NSTextField, so a read-only value could not be selected, caret-scrolled or copied. FieldDetailView hid the entire field menu on the same flag, so there was no Copy Value either. A large value on a query result with no writable table was unreachable by any route.

3. The routing asked the column type, not the value. ColumnType.isLongText is six exact string comparisons (TEXT, TINYTEXT, MEDIUMTEXT, LONGTEXT, CLOB, NTEXT), so a 40 KB value in VARCHAR(10000), NVARCHAR(MAX), NCLOB, CITEXT or ClickHouse's Nullable(String) got a one-line field. Adding more names cannot fix it: ColumnTypeClassifier.classify computes stripWrappers(rawTypeName) for the lookup and then returns factory(rawTypeName) with the unstripped name, so Nullable(String) is stored literally.

What changed

  • TextValueEditor, a shared NSViewRepresentable over NSTextView in an NSScrollView. Read-only is isEditable = false with isSelectable = true, never .disabled.
  • MultiLineEditorView renders it inside ResizableEditorContainer with a persisted height under rowInspectorTextFieldHeight, the same shape the JSON field already uses, plus an Open in Window button.
  • FieldEditorResolver picks the multi-line editor from the value as well as the type: a line break, or more than 80 characters, which is two lines' worth (between 32 and 46 subheadline characters fit one line at the inspector's minimum width).
  • SingleLineEditorView renders a read-only value as selectable text rather than a disabled field.
  • FieldMenuContent splits canMutate from the copy actions, so a read-only field keeps Copy Value and loses Set NULL, Set DEFAULT, Set EMPTY and SQL Functions, which were previously all-or-nothing.
  • TextViewerWindowController for the pop-out, on a new ValueViewerWindowController extracted from the JSON and PHP controllers, which were near-identical copies of the same window bookkeeping.
  • StartupCommandsEditor and AIRulesEditor, two more hand-rolled copies of the same NSTextView.scrollableTextView() representable, now use TextValueEditor. Net 101 lines removed across the four.

Why not TextEditor

Measured, not assumed. TextEditor on macOS 14 leaves AppKit's substitutions on and SwiftUI has no API to reach them. Reading the NSTextView it produces:

.autocorrectionDisabled(true) -> smartQuotes=true smartDashes=true
                                 textReplacement=true spellCorrection=false

.autocorrectionDisabled(true) clears spelling correction alone. A value editor built on it would save a typed straight quote as U+201C and a typed -- as U+2014, silently rewriting the stored value. .disabled() also leaves the underlying NSTextView editable=true and first-responder eligible, so there is no clean read-only-but-selectable mode. This is why StartupCommandsEditor and AIRulesEditor already hand-rolled the representable, and TextValueEditor is now the one copy.

Two choices worth flagging

The editor is picked from the stored value, once per field, not from the live draft. Recomputing it as you type would swap the view type the moment a draft crossed 80 characters, and SwiftUI rebuilds the child on a switch branch change, so focus and the caret would go with it. The cost is that pasting a very large value into a field that resolved short keeps the text field for that editing session; it stays selectable, copyable and caret-scrollable, and reading the row back after Save gives it the text view.

The pop-out is a window, not an Expand in Sidebar. JSON and PHP have both. The inspector's minimum width is 270pt, and every reference client pushes a large value to a bigger surface rather than a narrow side pane, DataGrip explicitly so. The sidebar-expand state is also a stored column index that a table switch can leave pointing at an unrelated column, and this change does not add a third copy of it.

SourceEditor was not reused. JsonEditorView gets scrolling for free from CodeEditSourceEditor, but that is a code editor: tree-sitter parsing, a gutter and code-shaped key handling, for prose in a 270pt pane. The app's own precedent for plain text is the NSTextView representable that StartupCommandsEditor and AIRulesEditor already had.

Why nothing is truncated

The cost of a text view is driven by the longest paragraph, not the total size, so a display cap was considered and rejected. Measured at the inspector's width, layout plus display:

value before, TextField(axis: .vertical) after, NSTextView
100 KB on one line 105 ms 38 ms
1 MB on one line 999 ms 213 ms
5 MB on one line did not finish in 2 minutes 1,029 ms
1 MB over 22,000 lines n/a 1.0 ms

TextKit 1 with allowsNonContiguousLayout is not a rescue: it costs 277 ms at 1 MB and 1,420 ms at 5 MB just to set the string, and the same per keystroke. Wrapping one enormous paragraph is a platform limit, so the change shows the whole value and is faster than what shipped at every size rather than hiding the limit behind a custom truncation notice.

Before / After

Same query, same steps, both real builds, captured from InspectorLongTextFieldUITests driving the app: SELECT hex(zeroblob(999)); against the sample database, one row selected, Details tab open. The value is 1,998 characters and the result set has no writable table, so this is the read-only path the report is about.

Before (dd7b252cf): one greyed line ending in an ellipsis. It is a TextField under .disabled(true), so it takes no first responder: the remaining 1,950 characters cannot be scrolled to, selected or copied, and the field menu was hidden on the same flag so there was no Copy Value either.

After: the value wraps in a scrolling text view, in normal text colour rather than the disabled grey, with the drag handle that resizes it and remembers the height, and the pop-out button that opens it in its own window.

The same difference is machine-checkable, which is what the new UI test asserts. Running it against the base build fails with:

A 1500+ character value must render in a text view, which scrolls, rather than in a
text field, which clips. Text view lengths present: [26]

26 is the query editor. On the base build the 1,998 character value is in no text view at all; on this branch the assertion passes.

Verified

  • verify.sh build: PASS.
  • verify.sh test over FieldEditorResolverTests, ResizableFieldMetricsTests, FieldEditorContextPolicyTests, TextValueEditorDefaultsTests, MultiRowEditStateTests, MultiRowEditStateJsonTests, TableViewCoordinatorPopoverDismissalTests, ColumnTypeTests: 177 cases, 177 passed.
  • verify.sh uitest InspectorLongTextFieldUITests: a 4,000 character value from the sample database must land in a text view, which scrolls, rather than a text field, which clips. The element type is the only thing that separates the fix from the bug, since both report the same accessibility value.
  • swiftlint --strict over TablePro, TableProTests and TableProUITests: 0 violations.

A review pass over the finished diff found six things, all fixed here and covered by the numbers above. The one that mattered was mine: the new empty-state placeholder reported a stored '' as NULL, which a database client must never do, so it now returns nothing where the stored value really is the empty string. The rest: allowsUndo was left off the text view, so Command Z walked past the field and reached the app's row-edit undo; the always-on hover menu sat on top of the type picker's own chevron; Tab wrote a tab character into the value instead of moving to the next field; the popped-out Text window ignored Escape while the JSON and PHP windows close on it; and a sentence promising Cmd+F inside a field was wrong, because that key is bound to performFind:, which NSTextView does not implement, so the sentence is gone.

New unit coverage: the value-driven routing rules including NCLOB, nvarchar(max) and Nullable(String), the threshold boundary, JSON and picker columns keeping their own editors, the read-only menu policy, the placeholder never echoing a stored value, and every automatic substitution being off on the text view.

@mintlify

mintlify Bot commented Aug 21, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
TablePro 🟢 Ready View Preview Aug 21, 2026, 8:58 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@datlechin
datlechin force-pushed the fix/inspector-long-text-scroll branch from ccc2e40 to 0e32aad Compare August 22, 2026 05:12
@datlechin
datlechin merged commit 3dde976 into main Aug 22, 2026
9 of 13 checks passed
@datlechin
datlechin deleted the fix/inspector-long-text-scroll branch August 22, 2026 05:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant