This is an automated email from the ASF dual-hosted git repository.

chanholee 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 8ef350e8b9 [ZEPPELIN-6279] Add Search/Highlight keyword feature for 
Code Editor
8ef350e8b9 is described below

commit 8ef350e8b9738e2c98142be1faf69ec4eb0ced29
Author: hyeyoon Jung <[email protected]>
AuthorDate: Tue Oct 7 20:13:13 2025 +0900

    [ZEPPELIN-6279] Add Search/Highlight keyword feature for Code Editor
    
    ### What is this PR for?
    This PR is a re-implementation of the Search and Replace UI, a feature that 
was available in the Classic UI.
    The core search and highlighting logic has been fully rewritten to work 
with the Monaco Editor, which replaces the legacy Ace Editor library.
    
    ### What type of PR is it?
    Feature
    
    ### Todos
    * [ ] - Replace and Replace All features are not implemented yet.
    * [ ] - The feature to display the total counts and the current index is 
not implemented yet.
    
    ### What is the Jira issue?
    * [ZEPPELIN-6279](https://issues.apache.org/jira/browse/ZEPPELIN-6279)
    
    ### How should this be tested?
    **Manual Testing**
    * Check out the PR branch and check if search and highlight feature works.
    
    ### Screenshots (if appropriate)
    <img width="972" height="280" alt="image" 
src="https://github.com/user-attachments/assets/5e9f0ff1-c796-4e59-8e91-40f3fe1a2ce8";
 />
    
    ### Questions:
    * Does the license files need to update? No
    * Is there breaking changes for older versions? No
    * Does this needs documentation? No
    
    
    Closes #5079 from grcenneat/feat/ZEPPELIN-6279.
    
    Signed-off-by: ChanHo Lee <[email protected]>
---
 .../notebook/action-bar/action-bar.component.html  | 34 ++++++++++++++-
 .../notebook/action-bar/action-bar.component.less  | 48 ++++++++++++++++++++++
 .../notebook/action-bar/action-bar.component.ts    | 35 +++++++++++++++-
 .../workspace/notebook/notebook.component.html     |  1 +
 .../pages/workspace/notebook/notebook.component.ts |  4 ++
 .../code-editor/code-editor.component.less         |  6 +++
 .../paragraph/code-editor/code-editor.component.ts | 33 +++++++++++++++
 .../notebook/paragraph/paragraph.component.ts      |  4 ++
 8 files changed, 162 insertions(+), 3 deletions(-)

diff --git 
a/zeppelin-web-angular/src/app/pages/workspace/notebook/action-bar/action-bar.component.html
 
b/zeppelin-web-angular/src/app/pages/workspace/notebook/action-bar/action-bar.component.html
index deba0b7106..750cb77adc 100644
--- 
a/zeppelin-web-angular/src/app/pages/workspace/notebook/action-bar/action-bar.component.html
+++ 
b/zeppelin-web-angular/src/app/pages/workspace/notebook/action-bar/action-bar.component.html
@@ -179,9 +179,41 @@
       </nz-dropdown-menu>
     </nz-button-group>
     <nz-button-group nzSize="small">
-      <button nz-button nz-tooltip nzTooltipTitle="Search code" 
(click)="searchCode()">
+      <button
+        nz-button
+        nz-dropdown
+        [nzDropdownMenu]="searchMenu"
+        nzTrigger="click"
+        nz-tooltip
+        nzTooltipTitle="Search code"
+        nzTooltipTrigger="hover"
+        (nzVisibleChange)="onSearchMenuOpenChange($event)"
+      >
         <i nz-icon nzType="search" nzTheme="outline"></i>
       </button>
+      <!-- Search Code Dropdown UI -->
+      <nz-dropdown-menu #searchMenu="nzDropdownMenu">
+        <div class="dropdown-menu search-code">
+          <div class="search-code-row">
+            <nz-input-group nzAddOnBefore="Find">
+              <input type="text" nz-input #searchInput 
[(ngModel)]="searchText" (ngModelChange)="searchCode()" />
+            </nz-input-group>
+            <nz-button-group class="search-row-btn-group">
+              <button nz-button (click)="onFindPrevClick(searchText)"><i 
nz-icon nzType="left"></i></button>
+              <button nz-button (click)="onFindNextClick(searchText)"><i 
nz-icon nzType="right"></i></button>
+            </nz-button-group>
+          </div>
+          <div class="search-code-row">
+            <nz-input-group nzAddOnBefore="Replace">
+              <input type="text" nz-input [(ngModel)]="replaceText" />
+            </nz-input-group>
+            <nz-button-group class="search-row-btn-group">
+              <button nz-button (click)="onReplaceClick(searchText, 
replaceText)">Replace</button>
+              <button nz-button (click)="onReplaceAllClick(searchText, 
replaceText)">All</button>
+            </nz-button-group>
+          </div>
+        </div>
+      </nz-dropdown-menu>
     </nz-button-group>
     <nz-button-group nzSize="small" *ngIf="!viewOnly">
       <button
diff --git 
a/zeppelin-web-angular/src/app/pages/workspace/notebook/action-bar/action-bar.component.less
 
b/zeppelin-web-angular/src/app/pages/workspace/notebook/action-bar/action-bar.component.less
index fde2fe3894..e7c7fc69cc 100644
--- 
a/zeppelin-web-angular/src/app/pages/workspace/notebook/action-bar/action-bar.component.less
+++ 
b/zeppelin-web-angular/src/app/pages/workspace/notebook/action-bar/action-bar.component.less
@@ -113,3 +113,51 @@
     }
   }
 });
+
+::ng-deep html.dark .search-code,
+::ng-deep .search-code {
+  padding: 0;
+  &-row {
+    display: flex;
+    .ant-input-group,
+    .ant-input-group-addon,
+    .ant-input {
+      height: 32px;
+      border-radius: 0;
+    }
+    .search-row-btn-group {
+      display: flex;
+      align-items: center;
+      flex-wrap: nowrap;
+    }
+    .ant-btn-group > .ant-btn:first-child,
+    .ant-btn-group > .ant-btn:last-child {
+      border-radius: 0;
+    }
+    &:first-child {
+      .ant-input-group-addon {
+        border-top-left-radius: 4px;
+      }
+      .ant-btn:last-child {
+        border-top-right-radius: 4px;
+      }
+    }
+    &:last-child {
+      .ant-input-group-addon {
+        border-bottom-left-radius: 4px;
+      }
+      .ant-btn:last-child {
+        border-bottom-right-radius: 4px;
+      }
+    }
+
+    nz-input-group + nz-button-group {
+      margin-left: -1px;
+    }
+
+    & ~ & {
+      margin-top: -1px;
+    }
+  }
+}
+
diff --git 
a/zeppelin-web-angular/src/app/pages/workspace/notebook/action-bar/action-bar.component.ts
 
b/zeppelin-web-angular/src/app/pages/workspace/notebook/action-bar/action-bar.component.ts
index 4d05d866bd..3322fe0a5a 100644
--- 
a/zeppelin-web-angular/src/app/pages/workspace/notebook/action-bar/action-bar.component.ts
+++ 
b/zeppelin-web-angular/src/app/pages/workspace/notebook/action-bar/action-bar.component.ts
@@ -14,11 +14,13 @@ import {
   ChangeDetectionStrategy,
   ChangeDetectorRef,
   Component,
+  ElementRef,
   EventEmitter,
   Inject,
   Input,
   OnInit,
-  Output
+  Output,
+  ViewChild
 } from '@angular/core';
 import { ActivatedRoute, Router } from '@angular/router';
 import { NzMessageService } from 'ng-zorro-antd/message';
@@ -51,6 +53,8 @@ export class NotebookActionBarComponent extends 
MessageListenersManager implemen
   >();
   @Output() readonly editorHideChange = new EventEmitter<boolean>();
   @Output() readonly tableHideChange = new EventEmitter<boolean>();
+  @Output() readonly handleSearch = new EventEmitter<string>();
+  @ViewChild('searchInput', { static: false }) searchInputRef?: 
ElementRef<HTMLInputElement>;
   lfOption: Array<'report' | 'default' | 'simple'> = ['default', 'simple', 
'report'];
   isRevisionSupported: boolean = false;
   isNoteParagraphRunning = false;
@@ -58,6 +62,8 @@ export class NotebookActionBarComponent extends 
MessageListenersManager implemen
   editorHide = false;
   commitVisible = false;
   tableHide = false;
+  searchText = '';
+  replaceText = '';
   cronOption = [
     { name: 'None', value: undefined },
     { name: '1m', value: '0 0/1 * * * ?' },
@@ -219,7 +225,32 @@ export class NotebookActionBarComponent extends 
MessageListenersManager implemen
   }
 
   searchCode() {
-    // TODO(hsuanxyz)
+    this.handleSearch.emit(this.searchText);
+  }
+
+  onSearchMenuOpenChange(open: boolean) {
+    if (open) {
+      setTimeout(() => {
+        this.searchInputRef?.nativeElement?.focus();
+      }, 0);
+    } else {
+      this.searchText = '';
+      this.searchCode();
+    }
+  }
+
+  // TODO: Implement logic to find the previous search match in the notebook 
editor
+  onFindPrevClick(searchText: string) {}
+
+  // TODO: Implement logic to find the next search match in the notebook editor
+  onFindNextClick(searchText: string) {}
+
+  // TODO: Implement logic to replace the current search match with the 
replacement text
+  onReplaceClick(searchText: string, replaceText: string) {}
+
+  // TODO: Implement logic to replace all search matches with the replacement 
text
+  onReplaceAllClick(searchText: string, replaceText: string) {
+    this.handleSearch.emit(searchText);
   }
 
   deleteNote() {
diff --git 
a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.html 
b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.html
index f5f9e0a1a3..f75ff5104b 100644
--- 
a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.html
+++ 
b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.html
@@ -23,6 +23,7 @@
     [currentRevision]="currentRevision"
     (tableHideChange)="setAllParagraphTableHide($event)"
     (editorHideChange)="setAllParagraphEditorHide($event)"
+    (handleSearch)="onParagraphSearch($event)"
     #actionBar
   ></zeppelin-notebook-action-bar>
   <div class="flex-container">
diff --git 
a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts 
b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts
index 33e8e9385d..a905d26617 100644
--- 
a/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts
+++ 
b/zeppelin-web-angular/src/app/pages/workspace/notebook/notebook.component.ts
@@ -258,6 +258,10 @@ export class NotebookComponent extends 
MessageListenersManager implements OnInit
     this.cdr.markForCheck();
   }
 
+  onParagraphSearch(term: string) {
+    this.listOfNotebookParagraphComponent.forEach(comp => 
comp.highlightMatches(term || ''));
+  }
+
   saveParagraph(id: string) {
     const paragraphFound = 
this.listOfNotebookParagraphComponent.toArray().find(p => p.paragraph.id === 
id);
     if (!paragraphFound) {
diff --git 
a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.less
 
b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.less
index 8f61bd5b93..b6e9083f25 100644
--- 
a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.less
+++ 
b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/code-editor/code-editor.component.less
@@ -33,3 +33,9 @@
   }
 
 });
+
+::ng-deep .editor-search-highlight {
+  background: yellow;
+  color: black;
+  border-radius: 2px;
+}
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 d046e24850..7562e72d90 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
@@ -34,6 +34,7 @@ import { NotebookParagraphControlComponent } from 
'../control/control.component'
 
 type IStandaloneCodeEditor = MonacoEditor.IStandaloneCodeEditor;
 type IEditor = MonacoEditor.IEditor;
+type DecorationIdentifier = 
ReturnType<monaco.editor.ICodeEditor['deltaDecorations']>[number];
 
 @Component({
   selector: 'zeppelin-notebook-paragraph-code-editor',
@@ -62,6 +63,7 @@ export class NotebookParagraphCodeEditorComponent
   @Output() readonly initKeyBindings = new 
EventEmitter<IStandaloneCodeEditor>();
   private editor?: IStandaloneCodeEditor;
   private monacoDisposables: IDisposable[] = [];
+  private highlightDecorations: DecorationIdentifier[] = [];
   height = 18;
   interpreterName?: string;
 
@@ -346,6 +348,37 @@ export class NotebookParagraphCodeEditorComponent
     }
   }
 
+  highlightMatches(term: string) {
+    if (!this.editor || !term) {
+      // Remove previous highlights if term is empty
+      this.highlightDecorations = 
this.editor?.deltaDecorations(this.highlightDecorations, []) || [];
+      return;
+    }
+    const model = this.editor.getModel();
+    if (!model) {
+      return;
+    }
+    const text = model.getValue();
+    const newDecorations = [];
+    let startIndex = 0;
+    while (term && text) {
+      const idx = text.indexOf(term, startIndex);
+      if (idx === -1) {
+        break;
+      }
+      const startPos = model.getPositionAt(idx);
+      const endPos = model.getPositionAt(idx + term.length);
+      newDecorations.push({
+        range: new monaco.Range(startPos.lineNumber, startPos.column, 
endPos.lineNumber, endPos.column),
+        options: {
+          inlineClassName: 'editor-search-highlight'
+        }
+      });
+      startIndex = idx + term.length;
+    }
+    this.highlightDecorations = 
this.editor.deltaDecorations(this.highlightDecorations, newDecorations);
+  }
+
   constructor(
     private cdr: ChangeDetectorRef,
     private ngZone: NgZone,
diff --git 
a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts
 
b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts
index 5e9d7bf820..f817e7e148 100644
--- 
a/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts
+++ 
b/zeppelin-web-angular/src/app/pages/workspace/notebook/paragraph/paragraph.component.ts
@@ -125,6 +125,10 @@ export class NotebookParagraphComponent extends 
ParagraphBase
     }
   }
 
+  highlightMatches(searchText: string) {
+    this.notebookParagraphCodeEditorComponent?.highlightMatches(searchText);
+  }
+
   textChanged(text: string) {
     this.dirtyText = text;
     this.paragraph.text = text;

Reply via email to