This is an automated email from the ASF dual-hosted git repository.
voidmatcha pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git
The following commit(s) were added to refs/heads/master by this push:
new 7076ea41c5 [ZEPPELIN-6710] Keep Enter as a newline for no-op Monaco
suggestions
7076ea41c5 is described below
commit 7076ea41c57d1fe3b172fc53ffde1d5229c57ef0
Author: HwangRock <[email protected]>
AuthorDate: Sun Sep 20 11:11:22 2026 +0900
[ZEPPELIN-6710] Keep Enter as a newline for no-op Monaco suggestions
### What is this PR for?
In the New UI paragraph editor (Monaco), pressing Enter can be silently
swallowed while the auto-suggest popup is open, dropping a line break so the
next characters land on the current line instead of a new one.
The trigger is a *no-op* suggestion. When word-based suggestions offer a
candidate identical to the word the user just finished typing, accepting it
changes nothing. With Monaco's `acceptSuggestionOnEnter` left at its default
(`'on'`), Enter accepts that no-op candidate instead of inserting a newline:
the keystroke is consumed, the line break never lands, and rapid multi-line
typing loses lines.
The fix sets `acceptSuggestionOnEnter: 'smart'` on the paragraph editor.
The auto-suggest popup still appears; only the Enter behavior changes, and only
for a focused candidate whose acceptance would not change the text.
#### Why `'smart'` over the alternatives
- **`wordBasedSuggestions: 'off'`** — removes the document-word completion
feature entirely. That is heavier than the bug warrants, and it is not even the
right lever: Monaco's built-in word-based provider already filters out an exact
match of the current token (`editorSimpleWorker.js`, `if (word === leadingWord)
continue`), so turning it off does not target the actual no-op source.
- **Filtering exact-match candidates in the interpreter completion
provider** (`completion.service.ts`) — the most surgical *removal* of the
candidate, but it changes interpreter completion behavior, which ZEPPELIN-6710
explicitly lists as out of scope ("Changing interpreter completion behavior or
the Ctrl+Space-then-Enter accept flow"). It also lives outside
`updateEditorOptions()`, which the issue scopes the change to.
- **`acceptSuggestionOnEnter: 'smart'`** — a one-line change inside
`updateEditorOptions()`, keeps the completion feature intact, and defends the
newline regardless of which provider produced the candidate. It matches the
issue's scope ("Stop Enter from silently discarding a newline when the focused
suggestion would make no textual change") exactly.
#### Trade-offs, and why they are not problems here
- **It defends Enter rather than removing the candidate.** The no-op
candidate still appears in the popup, and Tab/click still accept it. But
accepting a no-op is inert (it changes nothing), so there is no harm left to
prevent — only the *silent newline loss* mattered, and that is what `'smart'`
fixes.
- **`acceptSuggestionOnEnter` is editor-wide** (within the paragraph code
editor). Behavior only changes for the exact combination "no-op candidate +
auto-triggered popup + Enter"; every other completion path — partial-word
accept, Ctrl+Space, Tab — is unchanged. There is no scenario where turning a
no-op accept into a newline is a regression.
- **`'smart'` does not cover `additionalTextEdits` (auto-import) or snippet
candidates** — Monaco treats those as text-changing and still accepts them on
Enter. This does not apply to Zeppelin: interpreter completions are mapped as
plain-string `insertText` with no `insertTextRules` or `additionalTextEdits`
(`completion.service.ts`), so those candidate shapes are never produced.
#### How `'smart'` works internally
Monaco recomputes a context key `suggestionMakesTextEdit` whenever the
focused suggestion changes. Under `'smart'`, it gates on: the popup was
auto-triggered (`state === Auto`, i.e. not a manual Ctrl+Space), no
`additionalTextEdits`, not a snippet, and the replace range length equals the
insert-text length. When that gate passes, it compares the editor text against
the candidate's `insertText`; if they are identical the key is set to `false`.
Enter is bound to `acceptSelectedSuggestion`, which requires `Visible &&
textInputFocus && acceptSuggestionOnEnter && suggestionMakesTextEdit`. With the
key `false`, that binding no longer matches, so Enter falls through to the
editor's default action — a newline. Ctrl+Space (a manual trigger) never enters
the `state === Auto` gate, so interpreter completion's accept-on-Enter is
unaffected; inline ghost-text is a separate `registerInlineCompletionsProvider`
subsystem and is likewise u [...]
### What type of PR is it?
Bug Fix
### What is the Jira issue?
https://issues.apache.org/jira/browse/ZEPPELIN-6710
### How should this be tested?
A new Playwright regression,
`zeppelin-web-angular/e2e/tests/notebook/monaco-enter-newline.spec.ts`, drives
the auto-suggest path (typing to let quickSuggestions open the popup, not a
manual trigger, since `'smart'` only applies to auto-triggered popups). It
asserts:
- a no-op exact-match candidate + Enter keeps the newline (`foobar\nbaz`,
not `foobarbaz`);
- a genuine partial-prefix candidate still accepts on Enter (`'smart'` only
skips no-ops).
Verified: the test passes with the fix, and fails deterministically
(`foobar` and `baz` merge onto one line) when the `'smart'` line is removed —
so the regression actually pins the bug. `tsc`, `prettier`, and `eslint` are
clean.
### Screenshots (if appropriate)
https://github.com/user-attachments/assets/e06c66fb-8cea-4837-bcd3-e84689109695
### Questions:
- Does the documentation need to be updated? No.
Closes #5490 from HwangRock/ZEPPELIN-6710-monaco-enter-newline.
Signed-off-by: YONGJAE LEE <[email protected]>
---
.../tests/notebook/monaco-enter-newline.spec.ts | 78 ++++++++++++++++++++++
.../paragraph/code-editor/code-editor.component.ts | 1 +
2 files changed, 79 insertions(+)
diff --git
a/zeppelin-web-angular/e2e/tests/notebook/monaco-enter-newline.spec.ts
b/zeppelin-web-angular/e2e/tests/notebook/monaco-enter-newline.spec.ts
new file mode 100644
index 0000000000..d094e4a5a2
--- /dev/null
+++ b/zeppelin-web-angular/e2e/tests/notebook/monaco-enter-newline.spec.ts
@@ -0,0 +1,78 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { expect, test } from '@playwright/test';
+import { NotebookKeyboardPage } from 'e2e/models/notebook-keyboard-page';
+import { addPageAnnotationBeforeEach, performLoginIfRequired,
waitForZeppelinReady, PAGES } from '../../utils';
+
+test.describe('Monaco editor: Enter with a no-op exact-match suggestion
(ZEPPELIN-6710)', () => {
+ addPageAnnotationBeforeEach(PAGES.WORKSPACE.NOTEBOOK_PARAGRAPH_CODE_EDITOR);
+
+ test('Enter inserts a newline instead of being swallowed by a completed-word
suggestion', async ({ page }) => {
+ await page.goto('/#/');
+ await waitForZeppelinReady(page);
+ await performLoginIfRequired(page);
+
+ const notePath = `E2E_TEST_FOLDER/MonacoEnterNewline_${Date.now()}`;
+ const createResponse = await page.request.post('/api/notebook', {
+ data: { notePath, defaultInterpreterGroup: 'python',
addingEmptyParagraph: true }
+ });
+ expect(createResponse.ok()).toBeTruthy();
+ const noteId = (await createResponse.json()).body as string;
+
+ try {
+ await page.goto(`/#/notebook/${noteId}`);
+ await expect(page).toHaveURL(new RegExp(`/notebook/${noteId}`));
+
+ const keyboardPage = new NotebookKeyboardPage(page);
+ await expect(keyboardPage.paragraphContainer.first()).toBeVisible({
timeout: 30000 });
+
+ await test.step('Given: a running python paragraph with a variable
already bound in scope', async () => {
+ await keyboardPage.setCodeEditorContent('%python\nfoobar = 1\n');
+ await keyboardPage.pressRunParagraph();
+ await keyboardPage.waitForParagraphExecution(0);
+ });
+
+ await test.step('When: typing the bound variable in full so
quickSuggestions auto-pops its exact, no-op match, then pressing Enter', async
() => {
+ await keyboardPage.tryFocusCodeEditor();
+ await keyboardPage.pressSelectAll();
+ await page.keyboard.press('ArrowRight');
+
+ await page.keyboard.type('foobar', { delay: 100 }); // delay lets
quickSuggestions fire per keystroke
+ await expect(keyboardPage.autocompletePopup).toBeVisible({ timeout:
10000 });
+
+ await page.keyboard.press('Enter');
+ await page.keyboard.type('baz');
+ });
+
+ await test.step('Then: the line break survives and the new text lands on
its own line', async () => {
+ await expect
+ .poll(async () => keyboardPage.getParagraphTextByIndex(0), {
timeout: 15000 })
+ .toBe('%python\nfoobar = 1\nfoobar\nbaz');
+ });
+
+ await test.step('And: Enter still accepts a genuine, non-no-op
completion for a unique partial prefix (smart mode only skips no-ops)', async
() => {
+ await page.keyboard.press('Enter');
+ await page.keyboard.type('fooba', { delay: 100 }); // no python
keyword shares this prefix, so "foobar" is the only candidate
+
+ await expect(keyboardPage.autocompletePopup).toBeVisible({ timeout:
10000 });
+ await page.keyboard.press('Enter');
+
+ await expect
+ .poll(async () => keyboardPage.getParagraphTextByIndex(0), {
timeout: 15000 })
+ .toBe('%python\nfoobar = 1\nfoobar\nbaz\nfoobar');
+ });
+ } finally {
+ await page.request.delete(`/api/notebook/${noteId}`);
+ }
+ });
+});
diff --git
a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts
b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts
index d97ac3f626..4c34acacba 100644
---
a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts
+++
b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.ts
@@ -312,6 +312,7 @@ export class NotebookParagraphCodeEditorComponent
matchBrackets: 'always',
wordWrap: 'on',
inlineSuggest: { enabled: true },
+ acceptSuggestionOnEnter: 'smart',
scrollbar: {
handleMouseWheel: false,
alwaysConsumeMouseWheel: false