Skip to content

Commit 1d2569c

Browse files
committed
Improve text editor reliability and fix wrapping behavior
- Replace arbitrary timeouts with deterministic browser idle detection in Playwright tests - Fix line wrapping to use full available width (trim trailing spaces from length calculation) - Simplify rich text editor by removing plaintext mode and markdown transition plugin - Consolidate IndentPlugin to work with paragraph-based structure (remove PlainTextIndentPlugin) - Add comprehensive test utilities for browser idle detection with examples and documentation Fix linewrap tests to expect trimmed newLine values Tests for the rich text editor's linewrap behavior were failing because expectations assumed trailing spaces would be preserved. Update assertions to expect trimmed newLine values (and document that whitespace-only lines become empty) so the tests reflect the actual trimming behavior implemented in the linewrap logic. Preserve single newlines when extracting editor text Fix extra blank lines produced by getTextContent(): instead of using getRoot().getTextContent(), iterate over root children, extract each paragraph's text, and join with single '\n'. This prevents added empty lines between paragraphs and resolves the failing test that was seeing too many newlines.
1 parent c498c30 commit 1d2569c

26 files changed

+1486
-987
lines changed

apps/desktop/src/components/codegen/CodegenInput.svelte

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,6 @@
249249
bind:this={editorRef}
250250
bind:value
251251
namespace="codegen-input"
252-
plaintext={true}
253252
styleContext="chat-input"
254253
placeholder="Use @ to reference files, ↓ and ↑ for prompt history"
255254
minHeight="4rem"

apps/desktop/src/components/editor/MessageEditor.svelte

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,13 +329,11 @@
329329
namespace="CommitMessageEditor"
330330
{placeholder}
331331
bind:this={composer}
332-
plaintext={true}
333332
onError={(e) => console.warn('Editor error', e)}
334333
initialText={initialValue}
335334
onChange={handleChange}
336335
onKeyDown={handleKeyDown}
337336
{disabled}
338-
{wrapCountValue}
339337
useMonospaceFont={useRuler && !forceSansFont}
340338
monospaceFont={$userSettings.diffFont}
341339
tabSize={$userSettings.tabSize}

apps/web/src/lib/components/chat/ChatInput.svelte

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,6 @@
276276
styleContext="chat-input"
277277
placeholder="Write your message"
278278
bind:this={richText.richTextEditor}
279-
plaintext={true}
280279
namespace="ChatInput"
281280
onError={console.error}
282281
onInput={(text) => messageHandler.update(text)}

apps/web/src/routes/(app)/[ownerSlug]/[projectSlug]/reviews/[branchId]/+page.svelte

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,6 @@
283283
<div class="summary-wrapper">
284284
<RichTextEditor
285285
namespace="review-description"
286-
plaintext={true}
287286
onError={console.error}
288287
styleContext="chat-input"
289288
initialText={branch.description}

packages/ui/src/lib/richText/RichTextEditor.svelte

Lines changed: 29 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,9 @@
44
import { standardConfig } from '$lib/richText/config/config';
55
import { standardTheme } from '$lib/richText/config/theme';
66
import { INLINE_CODE_TRANSFORMER } from '$lib/richText/customTransforers';
7-
import { getCurrentText } from '$lib/richText/getText';
87
// import CodeBlockTypeAhead from '$lib/richText/plugins/CodeBlockTypeAhead.svelte';
98
import EmojiPlugin from '$lib/richText/plugins/Emoji.svelte';
10-
import PlainTextIndentPlugin from '$lib/richText/plugins/PlainTextIndentPlugin.svelte';
11-
import MarkdownTransitionPlugin from '$lib/richText/plugins/markdownTransition';
9+
import IndentPlugin from '$lib/richText/plugins/IndentPlugin.svelte';
1210
import OnChangePlugin, { type OnChangeCallback } from '$lib/richText/plugins/onChange.svelte';
1311
import OnInput, { type OnInputCallback } from '$lib/richText/plugins/onInput.svelte';
1412
import { insertTextAtCaret, setEditorText } from '$lib/richText/selection';
@@ -26,25 +24,14 @@
2624
Composer,
2725
ContentEditable,
2826
RichTextPlugin,
29-
ListPlugin,
30-
CheckListPlugin,
3127
AutoFocusPlugin,
3228
PlaceHolder,
33-
HashtagPlugin,
34-
PlainTextPlugin,
35-
AutoLinkPlugin,
36-
FloatingLinkEditorPlugin,
37-
CodeHighlightPlugin,
38-
CodeActionMenuPlugin,
3929
MarkdownShortcutPlugin,
40-
ALL_TRANSFORMERS,
41-
LinkPlugin,
4230
HistoryPlugin
4331
} from 'svelte-lexical';
4432
4533
interface Props {
4634
namespace: string;
47-
plaintext: boolean;
4835
onError: (error: unknown) => void;
4936
styleContext: 'client-editor' | 'chat-input';
5037
plugins?: Snippet;
@@ -59,7 +46,6 @@
5946
onKeyDown?: (event: KeyboardEvent | null) => boolean;
6047
initialText?: string;
6148
disabled?: boolean;
62-
wrapCountValue?: number;
6349
useMonospaceFont?: boolean;
6450
monospaceFont?: string;
6551
tabSize?: number;
@@ -70,7 +56,6 @@
7056
let {
7157
disabled,
7258
namespace,
73-
plaintext,
7459
onError,
7560
minHeight,
7661
maxHeight,
@@ -84,7 +69,6 @@
8469
onInput,
8570
onKeyDown,
8671
initialText,
87-
wrapCountValue,
8872
useMonospaceFont,
8973
monospaceFont,
9074
tabSize,
@@ -109,9 +93,6 @@
10993
let editorDiv: HTMLDivElement | undefined = $state();
11094
let emojiPlugin = $state<ReturnType<typeof EmojiPlugin>>();
11195
112-
// TODO: Change this plugin in favor of a toggle button.
113-
const markdownTransitionPlugin = new MarkdownTransitionPlugin(wrapCountValue);
114-
11596
const isDisabled = $derived(disabled ?? false);
11697
11798
$effect(() => {
@@ -128,24 +109,7 @@
128109
$effect(() => {
129110
if (composer) {
130111
const editor = composer.getEditor();
131-
markdownTransitionPlugin.setEditor(editor);
132-
}
133-
});
134-
135-
$effect(() => {
136-
markdownTransitionPlugin.setMarkdown(!plaintext);
137-
});
138-
139-
$effect(() => {
140-
if (wrapCountValue) {
141-
markdownTransitionPlugin.setMaxLength(wrapCountValue);
142-
}
143-
});
144-
145-
$effect(() => {
146-
if (composer) {
147-
const editor = composer.getEditor();
148-
const unregidterKeyDown = editor.registerCommand<KeyboardEvent | null>(
112+
const unregisterKeyDown = editor.registerCommand<KeyboardEvent | null>(
149113
KEY_DOWN_COMMAND,
150114
(e) => {
151115
if (emojiPlugin?.isBusy()) {
@@ -173,7 +137,7 @@
173137
);
174138
175139
return () => {
176-
unregidterKeyDown();
140+
unregisterKeyDown();
177141
unregisterFocus();
178142
unregisterBlur();
179143
};
@@ -187,6 +151,9 @@
187151
});
188152
189153
async function updateInitialtext(initialText: string | undefined) {
154+
if (!composer) return;
155+
156+
// Set initial text if provided and editor is empty
190157
if (initialText) {
191158
const currentText = await getPlaintext();
192159
if (currentText?.trim() === '') {
@@ -197,13 +164,20 @@
197164
198165
export function getPlaintext(): Promise<string | undefined> {
199166
return new Promise((resolve) => {
200-
if (composer) {
201-
const editor = composer.getEditor();
202-
editor?.read(() => {
203-
const text = getCurrentText(!plaintext, wrapCountValue);
204-
resolve(text);
205-
});
167+
if (!composer) {
168+
resolve(undefined);
169+
return;
206170
}
171+
const editor = composer.getEditor();
172+
editor.read(() => {
173+
// Using `root.getTextContent()` adds extra blank lines between paragraphs, since
174+
// normally paragraphs have a bottom margin (that we removed).
175+
const root = getRoot();
176+
const children = root.getChildren();
177+
const paragraphTexts = children.map((child) => child.getTextContent());
178+
const text = paragraphTexts.join('\n');
179+
resolve(text);
180+
});
207181
});
208182
}
209183
@@ -213,7 +187,8 @@
213187
resolve(0);
214188
return;
215189
}
216-
composer.getEditor()?.read(() => {
190+
const editor = composer.getEditor();
191+
editor.read(() => {
217192
const root = getRoot();
218193
const count = root.getChildren().filter(isParagraphNode).length;
219194
resolve(count);
@@ -226,7 +201,7 @@
226201
return;
227202
}
228203
const editor = composer.getEditor();
229-
editor?.update(() => {
204+
editor.update(() => {
230205
const root = getRoot();
231206
root.clear();
232207
});
@@ -239,7 +214,8 @@
239214
const editor = composer.getEditor();
240215
// We should be able to use `editor.focus()` here, but for some reason
241216
// it only works after the input has already been focused.
242-
editor.getRootElement()?.focus();
217+
const rootElement = editor.getRootElement();
218+
rootElement?.focus();
243219
}
244220
245221
export function wrapAll() {
@@ -282,7 +258,6 @@
282258
class="lexical-container lexical-{styleContext} scrollbar"
283259
bind:this={editorDiv}
284260
use:focusable={{ button: true }}
285-
class:plain-text={plaintext}
286261
class:disabled={isDisabled}
287262
style:min-height={minHeight}
288263
style:max-height={maxHeight}
@@ -305,35 +280,20 @@
305280
<EmojiPlugin bind:this={emojiPlugin} />
306281

307282
<OnChangePlugin
308-
markdown={!plaintext}
309283
onChange={(newValue, changeUpToAnchor, textAfterAnchor) => {
310284
value = newValue;
311285
onChange?.(newValue, changeUpToAnchor, textAfterAnchor);
312286
}}
313-
maxLength={wrapCountValue}
314287
/>
315288

316289
{#if onInput}
317-
<OnInput markdown={!plaintext} {onInput} maxLength={wrapCountValue} />
290+
<OnInput {onInput} />
318291
{/if}
319292

320-
{#if plaintext}
321-
<PlainTextPlugin />
322-
<PlainTextIndentPlugin />
323-
<!-- <CodeBlockTypeAhead /> -->
324-
<MarkdownShortcutPlugin transformers={[INLINE_CODE_TRANSFORMER]} />
325-
{:else}
326-
<AutoLinkPlugin />
327-
<CheckListPlugin />
328-
<CodeActionMenuPlugin anchorElem={editorDiv} />
329-
<CodeHighlightPlugin />
330-
<FloatingLinkEditorPlugin anchorElem={editorDiv} />
331-
<HashtagPlugin />
332-
<ListPlugin />
333-
<LinkPlugin />
334-
<MarkdownShortcutPlugin transformers={ALL_TRANSFORMERS} />
335-
<RichTextPlugin />
336-
{/if}
293+
<RichTextPlugin />
294+
<IndentPlugin />
295+
<MarkdownShortcutPlugin transformers={[INLINE_CODE_TRANSFORMER]} />
296+
337297
{#if autoFocus}
338298
<AutoFocusPlugin />
339299
{/if}
@@ -355,19 +315,6 @@
355315
background-color: var(--clr-bg-1);
356316
}
357317
358-
.editor-scroller {
359-
display: flex;
360-
z-index: 0;
361-
position: relative;
362-
flex-direction: column;
363-
height: 100%;
364-
overflow: auto;
365-
border: 0;
366-
outline: 0;
367-
/* It's unclear why the resizer is on by default on this element. */
368-
resize: none;
369-
}
370-
371318
.editor {
372319
z-index: -1;
373320
position: relative;

packages/ui/src/lib/richText/css/standard-theme.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
--markdown-generic-bottom-margin: 16px;
33
}
44

5-
.lexical-container.plain-text {
5+
.lexical-container {
66
--font-default: var(--code-block-font, var(--font-mono));
77
font-variant-ligatures: var(--code-block-ligatures, normal);
88
}
@@ -45,7 +45,7 @@
4545
}
4646

4747
/* Remove paragraph margins in plain-text mode */
48-
.lexical-container.plain-text .StandardTheme__paragraph {
48+
.lexical-container .StandardTheme__paragraph {
4949
margin-bottom: 0;
5050
}
5151

packages/ui/src/lib/richText/getText.ts

Lines changed: 0 additions & 11 deletions
This file was deleted.

packages/ui/src/lib/richText/linewrap.test.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ describe('wrapline', () => {
77
line: 'hello world',
88
maxLength: 8
99
});
10-
expect(newLine).toEqual('hello ');
10+
expect(newLine).toEqual('hello');
1111
expect(remainder).toEqual('world');
1212
});
1313

@@ -34,7 +34,8 @@ describe('wrapline', () => {
3434
line: ' ',
3535
maxLength: 5
3636
});
37-
expect(newLine).toEqual(' ');
37+
// Whitespace-only lines get trimmed to empty
38+
expect(newLine).toEqual('');
3839
expect(remainder).toEqual('');
3940
});
4041

@@ -53,7 +54,7 @@ describe('wrapline', () => {
5354
remainder: 'longer',
5455
maxLength: 5
5556
});
56-
expect(newLine).toEqual('longer ');
57+
expect(newLine).toEqual('longer');
5758
expect(remainder).toEqual('short');
5859
});
5960

@@ -73,7 +74,7 @@ describe('wrapline', () => {
7374
remainder: '',
7475
maxLength: 10
7576
});
76-
expect(newLine).toEqual(' leading ');
77+
expect(newLine).toEqual(' leading');
7778
expect(remainder).toEqual('space');
7879
});
7980

@@ -95,7 +96,7 @@ describe('wrapline', () => {
9596
bullet: { indent: ' ', prefix: '- ' },
9697
maxLength: 10
9798
});
98-
expect(newLine).toEqual('- hello ');
99+
expect(newLine).toEqual('- hello');
99100
expect(remainder).toEqual('world');
100101
});
101102

0 commit comments

Comments
 (0)