Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support find widget in notebooks #13982

Merged
merged 4 commits into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading