Skip to content

Commit

Permalink
Support find widget in notebooks (#13982)
Browse files Browse the repository at this point in the history
  • Loading branch information
msujew authored Aug 5, 2024
1 parent 63b57c6 commit 8d247e0
Show file tree
Hide file tree
Showing 16 changed files with 1,007 additions and 56 deletions.
2 changes: 1 addition & 1 deletion dependency-check-baseline.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"npm/npmjs/@types/qs/6.9.11": "Pending https://gitlab.eclipse.org/eclipsefdn/emo-team/iplab/-/issues/13990"
"npm/npmjs/-/advanced-mark.js/2.6.0": "Manually approved"
}
11 changes: 10 additions & 1 deletion packages/core/src/browser/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { environment } from '../common';
import { Disposable, environment } from '../common';

const userAgent = typeof navigator !== 'undefined' ? navigator.userAgent : '';

Expand Down Expand Up @@ -228,3 +228,12 @@ function getMeasurementElement(style?: PartialCSSStyle): HTMLElement {
}
return measureElement;
}

export function onDomEvent<K extends keyof HTMLElementEventMap>(
element: Node,
type: K,
listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => unknown,
options?: boolean | AddEventListenerOptions): Disposable {
element.addEventListener(type, listener, options);
return { dispose: () => element.removeEventListener(type, listener, options) };
}
101 changes: 101 additions & 0 deletions packages/core/src/browser/common-frontend-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,7 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
protected readonly untitledResourceResolver: UntitledResourceResolver;

protected pinnedKey: ContextKey<boolean>;
protected inputFocus: ContextKey<boolean>;

async configure(app: FrontendApplication): Promise<void> {
// FIXME: This request blocks valuable startup time (~200ms).
Expand All @@ -458,6 +459,9 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
this.contextKeyService.createKey<boolean>('isMac', OS.type() === OS.Type.OSX);
this.contextKeyService.createKey<boolean>('isWindows', OS.type() === OS.Type.Windows);
this.contextKeyService.createKey<boolean>('isWeb', !this.isElectron());
this.inputFocus = this.contextKeyService.createKey<boolean>('inputFocus', false);
this.updateInputFocus();
browser.onDomEvent(document, 'focusin', () => this.updateInputFocus());

this.pinnedKey = this.contextKeyService.createKey<boolean>('activeEditorIsPinned', false);
this.updatePinnedKey();
Expand Down Expand Up @@ -513,6 +517,15 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
}
}

protected updateInputFocus(): void {
const activeElement = document.activeElement;
if (activeElement) {
const isInput = activeElement.tagName?.toLowerCase() === 'input'
|| activeElement.tagName?.toLowerCase() === 'textarea';
this.inputFocus.set(isInput);
}
}

protected updatePinnedKey(): void {
const activeTab = this.shell.findTabBar();
const pinningTarget = activeTab && this.shell.findTitle(activeTab);
Expand Down Expand Up @@ -1899,6 +1912,94 @@ export class CommonFrontendContribution implements FrontendApplicationContributi
}, description: 'Status bar warning items foreground color. Warning items stand out from other status bar entries to indicate warning conditions. The status bar is shown in the bottom of the window.'
},

// editor find

{
id: 'editor.findMatchBackground',
defaults: {
light: '#A8AC94',
dark: '#515C6A',
hcDark: undefined,
hcLight: undefined
},
description: 'Color of the current search match.'
},

{
id: 'editor.findMatchForeground',
defaults: {
light: undefined,
dark: undefined,
hcDark: undefined,
hcLight: undefined
},
description: 'Text color of the current search match.'
},
{
id: 'editor.findMatchHighlightBackground',
defaults: {
light: '#EA5C0055',
dark: '#EA5C0055',
hcDark: undefined,
hcLight: undefined
},
description: 'Color of the other search matches. The color must not be opaque so as not to hide underlying decorations.'
},

{
id: 'editor.findMatchHighlightForeground',
defaults: {
light: undefined,
dark: undefined,
hcDark: undefined,
hcLight: undefined
},
description: 'Foreground color of the other search matches.'
},

{
id: 'editor.findRangeHighlightBackground',
defaults: {
dark: '#3a3d4166',
light: '#b4b4b44d',
hcDark: undefined,
hcLight: undefined
},
description: 'Color of the range limiting the search. The color must not be opaque so as not to hide underlying decorations.'
},

{
id: 'editor.findMatchBorder',
defaults: {
light: undefined,
dark: undefined,
hcDark: 'activeContrastBorder',
hcLight: 'activeContrastBorder'
},
description: 'Border color of the current search match.'
},
{
id: 'editor.findMatchHighlightBorder',
defaults: {
light: undefined,
dark: undefined,
hcDark: 'activeContrastBorder',
hcLight: 'activeContrastBorder'
},
description: 'Border color of the other search matches.'
},

{
id: 'editor.findRangeHighlightBorder',
defaults: {
dark: undefined,
light: undefined,
hcDark: Color.transparent('activeContrastBorder', 0.4),
hcLight: Color.transparent('activeContrastBorder', 0.4)
},
description: 'Border color of the range limiting the search. The color must not be opaque so as not to hide underlying decorations.'
},

// Quickinput colors should be aligned with https://code.visualstudio.com/api/references/theme-color#quick-picker
// if not yet contributed by Monaco, check runtime css variables to learn.
{
Expand Down
1 change: 1 addition & 0 deletions packages/notebook/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"@theia/monaco": "1.52.0",
"@theia/monaco-editor-core": "1.83.101",
"@theia/outline-view": "1.52.0",
"advanced-mark.js": "^2.6.0",
"react-perfect-scrollbar": "^1.5.8",
"tslib": "^2.6.2"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ export namespace NotebookCommands {
id: 'notebook.cell.paste',
category: 'Notebook',
});

export const NOTEBOOK_FIND = Command.toDefaultLocalizedCommand({
id: 'notebook.find',
category: 'Notebook',
});
}

export enum CellChangeDirection {
Expand Down Expand Up @@ -251,6 +256,12 @@ export class NotebookActionsContribution implements CommandContribution, MenuCon
}
});

commands.registerCommand(NotebookCommands.NOTEBOOK_FIND, {
execute: () => {
this.notebookEditorWidgetService.focusedEditor?.showFindWidget();
}
});

}

protected editableCommandHandler(execute: (notebookModel: NotebookModel) => void): CommandHandler {
Expand Down Expand Up @@ -326,17 +337,22 @@ export class NotebookActionsContribution implements CommandContribution, MenuCon
{
command: NotebookCommands.CUT_SELECTED_CELL.id,
keybinding: 'ctrlcmd+x',
when: `!editorTextFocus && ${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED}`
when: `${NOTEBOOK_EDITOR_FOCUSED} && !inputFocus`
},
{
command: NotebookCommands.COPY_SELECTED_CELL.id,
keybinding: 'ctrlcmd+c',
when: `!editorTextFocus && ${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED}`
when: `${NOTEBOOK_EDITOR_FOCUSED} && !inputFocus`
},
{
command: NotebookCommands.PASTE_CELL.id,
keybinding: 'ctrlcmd+v',
when: `!editorTextFocus && ${NOTEBOOK_EDITOR_FOCUSED}`
when: `${NOTEBOOK_EDITOR_FOCUSED} && !inputFocus`
},
{
command: NotebookCommands.NOTEBOOK_FIND.id,
keybinding: 'ctrlcmd+f',
when: `${NOTEBOOK_EDITOR_FOCUSED}`
},
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ import { NotebookCellModel } from '../view-model/notebook-cell-model';
import {
NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_TYPE,
NotebookContextKeys, NOTEBOOK_CELL_EXECUTING, NOTEBOOK_EDITOR_FOCUSED,
NOTEBOOK_CELL_FOCUSED
NOTEBOOK_CELL_FOCUSED,
NOTEBOOK_CELL_LIST_FOCUSED
} from './notebook-context-keys';
import { ContextKeyService } from '@theia/core/lib/browser/context-key-service';
import { NotebookExecutionService } from '../service/notebook-execution-service';
Expand Down Expand Up @@ -418,12 +419,12 @@ export class NotebookCellActionContribution implements MenuContribution, Command
{
command: NotebookCellCommands.EDIT_COMMAND.id,
keybinding: 'Enter',
when: `!editorTextFocus && ${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED}`,
when: `!editorTextFocus && !inputFocus && ${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED}`,
},
{
command: NotebookCellCommands.STOP_EDIT_COMMAND.id,
keybinding: KeyCode.createKeyCode({ first: Key.ENTER, modifiers: [KeyModifier.Alt] }).toString(),
when: `editorTextFocus && ${NOTEBOOK_EDITOR_FOCUSED}`,
when: `editorTextFocus && !inputFocus && ${NOTEBOOK_EDITOR_FOCUSED}`,
},
{
command: NotebookCellCommands.STOP_EDIT_COMMAND.id,
Expand All @@ -433,12 +434,12 @@ export class NotebookCellActionContribution implements MenuContribution, Command
{
command: NotebookCellCommands.EXECUTE_SINGLE_CELL_COMMAND.id,
keybinding: KeyCode.createKeyCode({ first: Key.ENTER, modifiers: [KeyModifier.CtrlCmd] }).toString(),
when: `${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED} && ${NOTEBOOK_CELL_TYPE} == 'code'`,
when: `${NOTEBOOK_CELL_LIST_FOCUSED} && ${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED} && ${NOTEBOOK_CELL_TYPE} == 'code'`,
},
{
command: NotebookCellCommands.EXECUTE_SINGLE_CELL_AND_FOCUS_NEXT_COMMAND.id,
keybinding: KeyCode.createKeyCode({ first: Key.ENTER, modifiers: [KeyModifier.Shift] }).toString(),
when: `${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED}`,
when: `${NOTEBOOK_CELL_LIST_FOCUSED} && ${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED}`,
},
{
command: NotebookCellCommands.CLEAR_OUTPUTS_COMMAND.id,
Expand Down
51 changes: 49 additions & 2 deletions packages/notebook/src/browser/notebook-editor-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';
import { NotebookContextManager } from './service/notebook-context-manager';
import { NotebookViewportService } from './view/notebook-viewport-service';
import { NotebookCellCommands } from './contributions/notebook-cell-actions-contribution';
import { NotebookFindWidget } from './view/notebook-find-widget';
import debounce = require('lodash/debounce');
const PerfectScrollbar = require('react-perfect-scrollbar');

export const NotebookEditorWidgetContainerFactory = Symbol('NotebookEditorWidgetContainerFactory');
Expand Down Expand Up @@ -126,7 +128,16 @@ export class NotebookEditorWidget extends ReactWidget implements Navigatable, Sa
protected readonly renderers = new Map<CellKind, CellRenderer>();
protected _model?: NotebookModel;
protected _ready: Deferred<NotebookModel> = new Deferred();
protected _findWidgetVisible = false;
protected _findWidgetRef = React.createRef<NotebookFindWidget>();
protected scrollBarRef = React.createRef<{ updateScroll(): void }>();
protected debounceFind = debounce(() => {
this._findWidgetRef.current?.search({});
}, 30, {
trailing: true,
maxWait: 100,
leading: false
});

get notebookType(): string {
return this.props.notebookType;
Expand Down Expand Up @@ -177,6 +188,11 @@ export class NotebookEditorWidget extends ReactWidget implements Navigatable, Sa
// Wait one frame to ensure that the content has been rendered
animationFrame().then(() => this.scrollBarRef.current?.updateScroll());
}));
this.toDispose.push(this._model.onContentChanged(() => {
if (this._findWidgetVisible) {
this.debounceFind();
}
}));
this.toDispose.push(this._model.onDidChangeReadOnly(readOnly => {
if (readOnly) {
lock(this.title);
Expand Down Expand Up @@ -220,18 +236,41 @@ export class NotebookEditorWidget extends ReactWidget implements Navigatable, Sa
protected render(): ReactNode {
if (this._model) {
return <div className='theia-notebook-main-container'>
<div className='theia-notebook-overlay'>
<NotebookFindWidget
ref={this._findWidgetRef}
hidden={!this._findWidgetVisible}
onClose={() => {
this._findWidgetVisible = false;
this._model?.findMatches({
activeFilters: [],
matchCase: false,
regex: false,
search: '',
wholeWord: false
});
this.update();
}}
onSearch={options => this._model?.findMatches(options) ?? []}
onReplace={(matches, replaceText) => this._model?.replaceAll(matches, replaceText)}
/>
</div>
{this.notebookMainToolbarRenderer.render(this._model, this.node)}
<div className='theia-notebook-viewport' ref={(ref: HTMLDivElement) => this.viewportService.viewportElement = ref}>
<div
className='theia-notebook-viewport'
ref={(ref: HTMLDivElement) => this.viewportService.viewportElement = ref}
>
<PerfectScrollbar className='theia-notebook-scroll-container'
ref={this.scrollBarRef}
onScrollY={(e: HTMLDivElement) => this.viewportService.onScroll(e)}>
<NotebookCellListView renderers={this.renderers}
notebookModel={this._model}
notebookContext={this.notebookContextManager}
toolbarRenderer={this.cellToolbarFactory}
commandRegistry={this.commandRegistry} />
</PerfectScrollbar>
</div>
</div >;
</div>;
} else {
return <div className='theia-notebook-main-container'>
<div className='theia-notebook-main-loading-indicator'></div>
Expand Down Expand Up @@ -260,6 +299,14 @@ export class NotebookEditorWidget extends ReactWidget implements Navigatable, Sa
this.onDidChangeOutputInputFocusEmitter.fire(focused);
}

showFindWidget(): void {
if (!this._findWidgetVisible) {
this._findWidgetVisible = true;
this.update();
}
this._findWidgetRef.current?.focusSearch(this._model?.selectedText);
}

override dispose(): void {
this.notebookContextManager.dispose();
this.onDidChangeModelEmitter.dispose();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { NotebookKernelService } from './notebook-kernel-service';
import {
NOTEBOOK_CELL_EDITABLE,
NOTEBOOK_CELL_EXECUTING, NOTEBOOK_CELL_EXECUTION_STATE,
NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE,
NOTEBOOK_CELL_FOCUSED, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE,
NOTEBOOK_CELL_TYPE, NOTEBOOK_HAS_OUTPUTS, NOTEBOOK_KERNEL, NOTEBOOK_KERNEL_SELECTED,
NOTEBOOK_OUTPUT_INPUT_FOCUSED,
NOTEBOOK_VIEW_TYPE
Expand Down Expand Up @@ -85,7 +85,7 @@ export class NotebookContextManager {

this.scopedStore.setContext(NOTEBOOK_HAS_OUTPUTS, !!widget.model?.cells.find(cell => cell.outputs.length > 0));

// Cell Selection realted keys
// Cell Selection related keys
this.scopedStore.setContext(NOTEBOOK_CELL_FOCUSED, !!widget.model?.selectedCell);
widget.model?.onDidChangeSelectedCell(e => {
this.selectedCellChanged(e.cell);
Expand Down Expand Up @@ -144,8 +144,12 @@ export class NotebookContextManager {
return this.contextKeyService.createOverlay(Object.entries(this.cellContexts.get(cellHandle) ?? {}));
}

onDidEditorTextFocus(focus: boolean): void {
this.scopedStore.setContext('inputFocus', focus);
changeCellFocus(focus: boolean): void {
this.scopedStore.setContext(NOTEBOOK_CELL_FOCUSED, focus);
}

changeCellListFocus(focus: boolean): void {
this.scopedStore.setContext(NOTEBOOK_CELL_LIST_FOCUSED, focus);
}

createContextKeyChangedEvent(affectedKeys: string[]): ContextKeyChangeEvent {
Expand Down
Loading

0 comments on commit 8d247e0

Please sign in to comment.