From cff0a9c255ee748de2a055972196850faf09277f Mon Sep 17 00:00:00 2001 From: Jonah Iden Date: Mon, 22 Apr 2024 10:59:17 +0200 Subject: [PATCH] working notebook cell language select (#13615) * working notebook cell language select Also fixing notebook enter in notebook cell editors Signed-off-by: Jonah Iden * Use language display name * review changes Signed-off-by: Jonah Iden * correctly save metadata in notebook model Signed-off-by: Jonah Iden --------- Signed-off-by: Jonah Iden Co-authored-by: Mark Sujew --- packages/editor/src/browser/editor-command.ts | 40 ++--------- .../src/browser/editor-frontend-module.ts | 3 + .../editor-language-quick-pick-service.ts | 68 +++++++++++++++++++ .../notebook-cell-actions-contribution.ts | 30 +++++++- packages/notebook/src/browser/style/index.css | 9 ++- .../browser/view-model/notebook-cell-model.ts | 11 ++- .../src/browser/view-model/notebook-model.ts | 2 +- .../src/browser/view/notebook-cell-editor.tsx | 4 ++ .../browser/view/notebook-code-cell-view.tsx | 22 ++++-- 9 files changed, 146 insertions(+), 43 deletions(-) create mode 100644 packages/editor/src/browser/editor-language-quick-pick-service.ts diff --git a/packages/editor/src/browser/editor-command.ts b/packages/editor/src/browser/editor-command.ts index 81e0c1505d02a..0a721159659a1 100644 --- a/packages/editor/src/browser/editor-command.ts +++ b/packages/editor/src/browser/editor-command.ts @@ -16,15 +16,15 @@ import { inject, injectable, optional, postConstruct } from '@theia/core/shared/inversify'; import { CommandContribution, CommandRegistry, Command } from '@theia/core/lib/common'; -import URI from '@theia/core/lib/common/uri'; -import { CommonCommands, PreferenceService, LabelProvider, ApplicationShell, QuickInputService, QuickPickValue, QuickPickItemOrSeparator } from '@theia/core/lib/browser'; +import { CommonCommands, PreferenceService, LabelProvider, ApplicationShell, QuickInputService, QuickPickValue } from '@theia/core/lib/browser'; import { EditorManager } from './editor-manager'; import { EditorPreferences } from './editor-preferences'; import { ResourceProvider, MessageService } from '@theia/core'; -import { LanguageService, Language } from '@theia/core/lib/browser/language-service'; +import { LanguageService } from '@theia/core/lib/browser/language-service'; import { SUPPORTED_ENCODINGS } from '@theia/core/lib/browser/supported-encodings'; import { EncodingMode } from './editor'; import { nls } from '@theia/core/lib/common/nls'; +import { EditorLanguageQuickPickService } from './editor-language-quick-pick-service'; export namespace EditorCommands { @@ -237,6 +237,9 @@ export class EditorCommandContribution implements CommandContribution { @inject(ResourceProvider) protected readonly resourceProvider: ResourceProvider; + @inject(EditorLanguageQuickPickService) + protected readonly codeLanguageQuickPickService: EditorLanguageQuickPickService; + @postConstruct() protected init(): void { this.editorPreferences.onPreferenceChanged(e => { @@ -293,12 +296,7 @@ export class EditorCommandContribution implements CommandContribution { return; } const current = editor.document.languageId; - const items: Array | QuickPickItemOrSeparator> = [ - { label: nls.localizeByDefault('Auto Detect'), value: 'autoDetect' }, - { type: 'separator', label: nls.localizeByDefault('languages (identifier)') }, - ... (this.languages.languages.map(language => this.toQuickPickLanguage(language, current))).sort((e, e2) => e.label.localeCompare(e2.label)) - ]; - const selectedMode = await this.quickInputService?.showQuickPick(items, { placeholder: nls.localizeByDefault('Select Language Mode') }); + const selectedMode = await this.codeLanguageQuickPickService.pickEditorLanguage(current); if (selectedMode && ('value' in selectedMode)) { if (selectedMode.value === 'autoDetect') { editor.detectLanguage(); @@ -379,30 +377,6 @@ export class EditorCommandContribution implements CommandContribution { } } - protected toQuickPickLanguage(value: Language, current: string): QuickPickValue { - const languageUri = this.toLanguageUri(value); - const icon = this.labelProvider.getIcon(languageUri); - const iconClasses = icon !== '' ? [icon + ' file-icon'] : undefined; - const configured = current === value.id; - return { - value, - label: value.name, - description: nls.localizeByDefault(`({0})${configured ? ' - Configured Language' : ''}`, value.id), - iconClasses - }; - } - protected toLanguageUri(language: Language): URI { - const extension = language.extensions.values().next(); - if (extension.value) { - return new URI('file:///' + extension.value); - } - const filename = language.filenames.values().next(); - if (filename.value) { - return new URI('file:///' + filename.value); - } - return new URI('file:///.txt'); - } - protected isAutoSaveOn(): boolean { const autoSave = this.preferencesService.get(EditorCommandContribution.AUTOSAVE_PREFERENCE); return autoSave !== 'off'; diff --git a/packages/editor/src/browser/editor-frontend-module.ts b/packages/editor/src/browser/editor-frontend-module.ts index a22365ea28e3c..e4a089ae90b2a 100644 --- a/packages/editor/src/browser/editor-frontend-module.ts +++ b/packages/editor/src/browser/editor-frontend-module.ts @@ -38,6 +38,7 @@ import { QuickEditorService } from './quick-editor-service'; import { EditorLanguageStatusService } from './language-status/editor-language-status-service'; import { EditorLineNumberContribution } from './editor-linenumber-contribution'; import { UndoRedoService } from './undo-redo-service'; +import { EditorLanguageQuickPickService } from './editor-language-quick-pick-service'; export default new ContainerModule(bind => { bindEditorPreferences(bind); @@ -84,4 +85,6 @@ export default new ContainerModule(bind => { bind(EditorAccess).to(ActiveEditorAccess).inSingletonScope().whenTargetNamed(EditorAccess.ACTIVE); bind(UndoRedoService).toSelf().inSingletonScope(); + + bind(EditorLanguageQuickPickService).toSelf().inSingletonScope(); }); diff --git a/packages/editor/src/browser/editor-language-quick-pick-service.ts b/packages/editor/src/browser/editor-language-quick-pick-service.ts new file mode 100644 index 0000000000000..081a75b6d0fed --- /dev/null +++ b/packages/editor/src/browser/editor-language-quick-pick-service.ts @@ -0,0 +1,68 @@ +// ***************************************************************************** +// Copyright (C) 2024 TypeFox and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { inject, injectable } from '@theia/core/shared/inversify'; +import { Language, LanguageService } from '@theia/core/lib/browser/language-service'; +import { nls, QuickInputService, QuickPickItemOrSeparator, QuickPickValue, URI } from '@theia/core'; +import { LabelProvider } from '@theia/core/lib/browser'; + +@injectable() +export class EditorLanguageQuickPickService { + @inject(LanguageService) + protected readonly languages: LanguageService; + + @inject(QuickInputService) + protected readonly quickInputService: QuickInputService; + + @inject(LabelProvider) + protected readonly labelProvider: LabelProvider; + + async pickEditorLanguage(current: string): Promise | undefined> { + const items: Array | QuickPickItemOrSeparator> = [ + { label: nls.localizeByDefault('Auto Detect'), value: 'autoDetect' }, + { type: 'separator', label: nls.localizeByDefault('languages (identifier)') }, + ... (this.languages.languages.map(language => this.toQuickPickLanguage(language, current))).sort((e, e2) => e.label.localeCompare(e2.label)) + ]; + const selectedMode = await this.quickInputService?.showQuickPick(items, { placeholder: nls.localizeByDefault('Select Language Mode') }); + return (selectedMode && 'value' in selectedMode) ? selectedMode : undefined; + } + + protected toQuickPickLanguage(value: Language, current: string): QuickPickValue { + const languageUri = this.toLanguageUri(value); + const icon = this.labelProvider.getIcon(languageUri); + const iconClasses = icon !== '' ? [icon + ' file-icon'] : undefined; + const configured = current === value.id; + return { + value, + label: value.name, + description: nls.localizeByDefault(`({0})${configured ? ' - Configured Language' : ''}`, value.id), + iconClasses + }; + } + + protected toLanguageUri(language: Language): URI { + const extension = language.extensions.values().next(); + if (extension.value) { + return new URI('file:///' + extension.value); + } + const filename = language.filenames.values().next(); + if (filename.value) { + return new URI('file:///' + filename.value); + } + return new URI('file:///.txt'); + } + +} diff --git a/packages/notebook/src/browser/contributions/notebook-cell-actions-contribution.ts b/packages/notebook/src/browser/contributions/notebook-cell-actions-contribution.ts index f44a88cd2a054..0c9acfff28703 100644 --- a/packages/notebook/src/browser/contributions/notebook-cell-actions-contribution.ts +++ b/packages/notebook/src/browser/contributions/notebook-cell-actions-contribution.ts @@ -31,6 +31,7 @@ import { CellEditType, CellKind } from '../../common'; import { NotebookEditorWidgetService } from '../service/notebook-editor-widget-service'; import { NotebookCommands } from './notebook-actions-contribution'; import { changeCellType } from './cell-operations'; +import { EditorLanguageQuickPickService } from '@theia/editor/lib/browser/editor-language-quick-pick-service'; export namespace NotebookCellCommands { /** Parameters: notebookModel: NotebookModel | undefined, cell: NotebookCellModel */ @@ -131,6 +132,12 @@ export namespace NotebookCellCommands { label: 'Expand Cell Output', }); + export const CHANGE_CELL_LANGUAGE = Command.toDefaultLocalizedCommand({ + id: 'notebook.cell.changeLanguage', + category: 'Notebook', + label: 'Change Cell Language', + }); + } @injectable() @@ -145,6 +152,9 @@ export class NotebookCellActionContribution implements MenuContribution, Command @inject(NotebookEditorWidgetService) protected notebookEditorWidgetService: NotebookEditorWidgetService; + @inject(EditorLanguageQuickPickService) + protected languageQuickPickService: EditorLanguageQuickPickService; + @postConstruct() protected init(): void { NotebookContextKeys.initNotebookContextKeys(this.contextKeyService); @@ -359,6 +369,24 @@ export class NotebookCellActionContribution implements MenuContribution, Command } }); + commands.registerCommand(NotebookCellCommands.CHANGE_CELL_LANGUAGE, { + isVisible: () => !!this.notebookEditorWidgetService.focusedEditor?.model?.selectedCell, + execute: async (notebook?: NotebookModel, cell?: NotebookCellModel) => { + const selectedCell = cell ?? this.notebookEditorWidgetService.focusedEditor?.model?.selectedCell; + const activeNotebook = notebook ?? this.notebookEditorWidgetService.focusedEditor?.model; + if (selectedCell && activeNotebook) { + const language = await this.languageQuickPickService.pickEditorLanguage(selectedCell.language); + if (language?.value && language.value !== 'autoDetect') { + this.notebookEditorWidgetService.focusedEditor?.model?.applyEdits([{ + editType: CellEditType.CellLanguage, + index: activeNotebook.cells.indexOf(selectedCell), + language: language.value.id + }], true); + } + } + } + }); + } protected editableCellCommandHandler(execute: (notebookModel: NotebookModel, cell: NotebookCellModel, output?: NotebookCellOutputModel) => void): CommandHandler { @@ -382,7 +410,7 @@ export class NotebookCellActionContribution implements MenuContribution, Command { command: NotebookCellCommands.EDIT_COMMAND.id, keybinding: 'Enter', - when: `${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED}`, + when: `!editorTextFocus && ${NOTEBOOK_EDITOR_FOCUSED} && ${NOTEBOOK_CELL_FOCUSED}`, }, { command: NotebookCellCommands.STOP_EDIT_COMMAND.id, diff --git a/packages/notebook/src/browser/style/index.css b/packages/notebook/src/browser/style/index.css index 520cc6d150b42..ce8afa1a658b4 100644 --- a/packages/notebook/src/browser/style/index.css +++ b/packages/notebook/src/browser/style/index.css @@ -109,8 +109,13 @@ flex-grow: 1; } -.notebook-cell-status-right { - margin: 0 5px; +.notebook-cell-language-label { + padding: 0 5px; +} + +.notebook-cell-language-label:hover { + cursor: pointer; + background-color: var(--theia-toolbar-hoverBackground); } .notebook-cell-status-item { diff --git a/packages/notebook/src/browser/view-model/notebook-cell-model.ts b/packages/notebook/src/browser/view-model/notebook-cell-model.ts index dc3f20b1988c3..ea637ae256fbb 100644 --- a/packages/notebook/src/browser/view-model/notebook-cell-model.ts +++ b/packages/notebook/src/browser/view-model/notebook-cell-model.ts @@ -31,6 +31,7 @@ import { NotebookMonacoTextModelService } from '../service/notebook-monaco-text- import { NotebookCellOutputModel } from './notebook-cell-output-model'; import { PreferenceService } from '@theia/core/lib/browser'; import { NOTEBOOK_LINE_NUMBERS } from '../contributions/notebook-preferences'; +import { LanguageService } from '@theia/core/lib/browser/language-service'; export const NotebookCellModelFactory = Symbol('NotebookModelFactory'); export type NotebookCellModelFactory = (props: NotebookCellModelProps) => NotebookCellModel; @@ -121,6 +122,9 @@ export class NotebookCellModel implements NotebookCell, Disposable { @inject(NotebookMonacoTextModelService) protected readonly textModelService: NotebookMonacoTextModelService; + @inject(LanguageService) + protected readonly languageService: LanguageService; + @inject(PreferenceService) protected readonly preferenceService: PreferenceService; @@ -182,16 +186,19 @@ export class NotebookCellModel implements NotebookCell, Disposable { return; } - this.props.language = newLanguage; if (this.textModel) { this.textModel.setLanguageId(newLanguage); } - this.language = newLanguage; + this.props.language = newLanguage; this.onDidChangeLanguageEmitter.fire(newLanguage); this.onDidChangeContentEmitter.fire('language'); } + get languageName(): string { + return this.languageService.getLanguage(this.language)?.name ?? this.language; + } + get uri(): URI { return this.props.uri; } diff --git a/packages/notebook/src/browser/view-model/notebook-model.ts b/packages/notebook/src/browser/view-model/notebook-model.ts index ea8e484ec8c01..4230666db5e63 100644 --- a/packages/notebook/src/browser/view-model/notebook-model.ts +++ b/packages/notebook/src/browser/view-model/notebook-model.ts @@ -141,7 +141,7 @@ export class NotebookModel implements Saveable, Disposable { this.addCellOutputListeners(this.cells); - this.metadata = this.metadata; + this.metadata = this.props.data.metadata; this.nextHandle = this.cells.length; } diff --git a/packages/notebook/src/browser/view/notebook-cell-editor.tsx b/packages/notebook/src/browser/view/notebook-cell-editor.tsx index 46e362fbeb2b6..f0e8de320eed2 100644 --- a/packages/notebook/src/browser/view/notebook-cell-editor.tsx +++ b/packages/notebook/src/browser/view/notebook-cell-editor.tsx @@ -62,6 +62,10 @@ export class CellEditor extends React.Component { this.editor?.getControl().updateOptions(options); })); + this.toDispose.push(this.props.cell.onDidChangeLanguage(language => { + this.editor?.setLanguage(language); + })); + this.toDispose.push(this.props.notebookModel.onDidChangeSelectedCell(() => { if (this.props.notebookModel.selectedCell !== this.props.cell && this.editor?.getControl().hasTextFocus()) { if (document.activeElement && 'blur' in document.activeElement) { diff --git a/packages/notebook/src/browser/view/notebook-code-cell-view.tsx b/packages/notebook/src/browser/view/notebook-code-cell-view.tsx index 292ea421a1819..0cbaf6529f6c6 100644 --- a/packages/notebook/src/browser/view/notebook-code-cell-view.tsx +++ b/packages/notebook/src/browser/view/notebook-code-cell-view.tsx @@ -24,11 +24,11 @@ import { NotebookModel } from '../view-model/notebook-model'; import { CellEditor } from './notebook-cell-editor'; import { CellRenderer } from './notebook-cell-list-view'; import { NotebookCellToolbarFactory } from './notebook-cell-toolbar-factory'; -import { NotebookCellActionContribution } from '../contributions/notebook-cell-actions-contribution'; +import { NotebookCellActionContribution, NotebookCellCommands } from '../contributions/notebook-cell-actions-contribution'; import { CellExecution, NotebookExecutionStateService } from '../service/notebook-execution-state-service'; import { codicon } from '@theia/core/lib/browser'; import { NotebookCellExecutionState } from '../../common'; -import { DisposableCollection, nls } from '@theia/core'; +import { CommandRegistry, DisposableCollection, nls } from '@theia/core'; import { NotebookContextManager } from '../service/notebook-context-manager'; import { NotebookViewportService } from './notebook-viewport-service'; import { EditorPreferences } from '@theia/editor/lib/browser'; @@ -61,6 +61,9 @@ export class NotebookCodeCellRenderer implements CellRenderer { @inject(EditorPreferences) protected readonly editorPreferences: EditorPreferences; + @inject(CommandRegistry) + protected readonly commandRegistry: CommandRegistry; + protected fontInfo: BareFontInfo | undefined; render(notebookModel: NotebookModel, cell: NotebookCellModel, handle: number): React.ReactNode { @@ -76,7 +79,10 @@ export class NotebookCodeCellRenderer implements CellRenderer { notebookContextManager={this.notebookContextManager} notebookViewportService={this.notebookViewportService} fontInfo={this.getOrCreateMonacoFontInfo()} /> - cell.requestFocusEditor()}> + cell.requestFocusEditor()} />
@@ -108,7 +114,9 @@ export class NotebookCodeCellRenderer implements CellRenderer { } export interface NotebookCodeCellStatusProps { + notebook: NotebookModel; cell: NotebookCellModel; + commandRegistry: CommandRegistry; executionStateService: NotebookExecutionStateService; onClick: () => void; } @@ -146,6 +154,10 @@ export class NotebookCodeCellStatus extends React.Component { + this.forceUpdate(); + })); } override componentWillUnmount(): void { @@ -158,7 +170,9 @@ export class NotebookCodeCellStatus extends React.Component
- {this.props.cell.language} + { + this.props.commandRegistry.executeCommand(NotebookCellCommands.CHANGE_CELL_LANGUAGE.id, this.props.notebook, this.props.cell); + }}>{this.props.cell.languageName}
; }