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

Reply via email to