From 79cda5cbe55f33066cd080c3fd019aacabce7772 Mon Sep 17 00:00:00 2001 From: Cory Rivera Date: Wed, 2 Mar 2022 10:25:55 -0800 Subject: [PATCH] Add language picker to notebook cells. (#18601) --- .../browser/cellViews/code.component.html | 4 +- .../browser/cellViews/code.component.ts | 87 ++++++++++++++++++- .../notebook/browser/cellViews/code.css | 4 +- .../notebook/browser/notebookStyles.ts | 13 ++- .../workbench/contrib/notebook/test/stubs.ts | 3 + .../services/notebook/browser/models/cell.ts | 1 + .../notebook/browser/notebookService.ts | 2 + .../notebook/browser/notebookServiceImpl.ts | 16 ++++ .../notebook/browser/contrib/coreActions.ts | 2 +- .../src/sql/areas/notebook/notebook.test.ts | 19 ++++ 10 files changed, 144 insertions(+), 7 deletions(-) diff --git a/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.html b/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.html index f38a06bf8e..3bf746e095 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.html +++ b/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.html @@ -14,9 +14,9 @@
-
+
+
{{parametersText}} diff --git a/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.ts b/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.ts index 4ad7eecd33..16bf51ac77 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/cellViews/code.component.ts @@ -22,7 +22,7 @@ import { IModelService } from 'vs/editor/common/services/modelService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { Event, Emitter } from 'vs/base/common/event'; import { CellTypes } from 'sql/workbench/services/notebook/common/contracts'; -import { OVERRIDE_EDITOR_THEMING_SETTING } from 'sql/workbench/services/notebook/browser/notebookService'; +import { INotebookService, OVERRIDE_EDITOR_THEMING_SETTING } from 'sql/workbench/services/notebook/browser/notebookService'; import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement'; import { ILogService } from 'vs/platform/log/common/log'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -36,6 +36,11 @@ import { notebookConstants } from 'sql/workbench/services/notebook/browser/inter import { tryMatchCellMagic } from 'sql/workbench/services/notebook/browser/utils'; import { IColorTheme } from 'vs/platform/theme/common/themeService'; import { localize } from 'vs/nls'; +import { IQuickInputService, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { getIconClasses } from 'vs/editor/common/services/getIconClasses'; +import { URI } from 'vs/base/common/uri'; +import { ILanguagePickInput } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; export const CODE_SELECTOR: string = 'code-component'; const MARKDOWN_CLASS = 'markdown'; @@ -102,7 +107,9 @@ export class CodeComponent extends CellView implements OnInit, OnChanges { @Inject(IModeService) private _modeService: IModeService, @Inject(IConfigurationService) private _configurationService: IConfigurationService, @Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef, - @Inject(ILogService) private readonly logService: ILogService + @Inject(ILogService) private readonly logService: ILogService, + @Inject(IQuickInputService) private _quickInputService: IQuickInputService, + @Inject(INotebookService) private _notebookService: INotebookService, ) { super(); this._register(Event.debounce(this._layoutEmitter.event, (l, e) => e, 250, /*leading=*/false) @@ -145,6 +152,10 @@ export class CodeComponent extends CellView implements OnInit, OnChanges { return this.cellModel.cellGuid; } + get cellLanguageTitle(): string { + return localize('selectCellLanguage', "Select Cell Language Mode"); + } + get parametersText(): string { return localize('parametersText', "Parameters"); } @@ -269,6 +280,7 @@ export class CodeComponent extends CellView implements OnInit, OnChanges { this._register(this.cellModel.onLanguageChanged(language => { let nativeElement = this.languageElement.nativeElement; nativeElement.innerText = this.cellModel.displayLanguage; + nativeElement.ariaLabel = this.cellModel.displayLanguage; this.updateLanguageMode(); this._changeRef.detectChanges(); })); @@ -446,4 +458,75 @@ export class CodeComponent extends CellView implements OnInit, OnChanges { } } } + + public onCellLanguageClick(): void { + this._notebookService.getSupportedLanguagesForProvider(this._model.providerId, this._model.selectedKernelDisplayName) + .then(languages => this.pickCellLanguage(languages)) + .then(selection => { + if (selection?.languageId) { + this._cellModel.setOverrideLanguage(selection.languageId); + } + }) + .catch(err => onUnexpectedError(err)); + } + + private pickCellLanguage(languages: string[]): Promise { + if (languages.length === 0) { + languages = [this._cellModel.language]; + } + + const topItems: ILanguagePickInput[] = []; + const mainItems: ILanguagePickInput[] = []; + languages.forEach(lang => { + let description: string; + if (lang === this._cellModel.language) { + description = localize('cellLanguageDescription', "({0}) - Current Language", lang); + } else { + description = localize('cellLanguageDescriptionConfigured', "({0})", lang); + } + + const languageName = this._modeService.getLanguageName(lang) ?? lang; + const item = { + label: languageName, + iconClasses: getIconClasses(this._modelService, this._modeService, this.getFakeResource(languageName, this._modeService)), + description, + languageId: lang + }; + if (lang === this._cellModel.language) { + topItems.push(item); + } else { + mainItems.push(item); + } + }); + + mainItems.sort((a, b) => { + return a.description.localeCompare(b.description); + }); + + const picks: QuickPickInput[] = [ + ...topItems, + { type: 'separator' }, + ...mainItems + ]; + return this._quickInputService.pick(picks, { placeHolder: this.cellLanguageTitle, canPickMany: false }) as Promise; + } + + /** + * Copied from coreActions.ts + */ + private getFakeResource(lang: string, modeService: IModeService): URI | undefined { + let fakeResource: URI | undefined; + + const extensions = modeService.getExtensions(lang); + if (extensions?.length) { + fakeResource = URI.file(extensions[0]); + } else { + const filenames = modeService.getFilenames(lang); + if (filenames?.length) { + fakeResource = URI.file(filenames[0]); + } + } + + return fakeResource; + } } diff --git a/src/sql/workbench/contrib/notebook/browser/cellViews/code.css b/src/sql/workbench/contrib/notebook/browser/cellViews/code.css index 09bbdb061a..4c3d27f68e 100644 --- a/src/sql/workbench/contrib/notebook/browser/cellViews/code.css +++ b/src/sql/workbench/contrib/notebook/browser/cellViews/code.css @@ -118,7 +118,9 @@ code-component .cellLanguage { padding: 2px 15px; display: inline-block; text-align: center; - font-size: 16px; + font-size: 12px; + border-style: none; + background-color: transparent; } code-component .parameter { diff --git a/src/sql/workbench/contrib/notebook/browser/notebookStyles.ts b/src/sql/workbench/contrib/notebook/browser/notebookStyles.ts index aae8faea0d..3b999a72ce 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebookStyles.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebookStyles.ts @@ -6,7 +6,7 @@ import 'vs/css!./notebook'; import { registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { SIDE_BAR_BACKGROUND, EDITOR_GROUP_HEADER_TABS_BACKGROUND } from 'vs/workbench/common/theme'; -import { activeContrastBorder, contrastBorder, buttonBackground, textLinkForeground, textLinkActiveForeground, textPreformatForeground, textBlockQuoteBackground, textBlockQuoteBorder, buttonForeground } from 'vs/platform/theme/common/colorRegistry'; +import { activeContrastBorder, contrastBorder, buttonBackground, textLinkForeground, textLinkActiveForeground, textPreformatForeground, textBlockQuoteBackground, textBlockQuoteBorder, buttonForeground, foreground } from 'vs/platform/theme/common/colorRegistry'; import { editorLineHighlight, editorLineHighlightBorder } from 'vs/editor/common/view/editorColorRegistry'; import { cellBorder, notebookToolbarIcon, notebookToolbarLines, buttonMenuArrow, dropdownArrow, markdownEditorBackground, codeEditorBackground, codeEditorBackgroundActive, codeEditorLineNumber, codeEditorToolbarIcon, codeEditorToolbarBackground, codeEditorToolbarBorder, toolbarBackground, toolbarIcon, toolbarBottomBorder, notebookToolbarSelectBackground, splitBorder, notebookCellTagBackground, notebookCellTagForeground, notebookFindMatchHighlight, notebookFindRangeHighlight } from 'sql/platform/theme/common/colorRegistry'; import { IDisposable } from 'vs/base/common/lifecycle'; @@ -14,6 +14,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { BareResultsGridInfo, getBareResultsGridInfoStyles } from 'sql/workbench/contrib/query/browser/queryResultsEditor'; import { getZoomLevel } from 'vs/base/browser/browser'; import * as types from 'vs/base/common/types'; +import { cellStatusBarItemHover } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; export function registerNotebookThemes(overrideEditorThemeSetting: boolean, configurationService: IConfigurationService): IDisposable { return registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { @@ -181,6 +182,16 @@ export function registerNotebookThemes(overrideEditorThemeSetting: boolean, conf collector.addRule(`.notebookEditor .notebook-cell.active cell-toolbar-component { background-color: ${notebookToolbarSelectBackgroundColor};}`); } + // Cell language button + const textColor = theme.getColor(foreground); + if (textColor) { + collector.addRule(`code-component .cellLanguage { color: ${textColor}; }`); + } + const cellStatusBarHoverBg = theme.getColor(cellStatusBarItemHover); + if (cellStatusBarHoverBg) { + collector.addRule(`code-component .cellLanguage:hover { background-color: ${cellStatusBarHoverBg}; }`); + } + // Markdown editor toolbar const toolbarBackgroundColor = theme.getColor(toolbarBackground); if (toolbarBackgroundColor) { diff --git a/src/sql/workbench/contrib/notebook/test/stubs.ts b/src/sql/workbench/contrib/notebook/test/stubs.ts index 5f168a5e90..5626398abf 100644 --- a/src/sql/workbench/contrib/notebook/test/stubs.ts +++ b/src/sql/workbench/contrib/notebook/test/stubs.ts @@ -235,6 +235,9 @@ export class ServerManagerStub implements nb.ServerManager { } export class NotebookServiceStub implements INotebookService { + getSupportedLanguagesForProvider(provider: string, kernelDisplayName?: string): Promise { + throw new Error('Method not implemented.'); + } createNotebookInput(options: INotebookShowOptions, resource?: UriComponents): Promise { throw new Error('Method not implemented.'); } diff --git a/src/sql/workbench/services/notebook/browser/models/cell.ts b/src/sql/workbench/services/notebook/browser/models/cell.ts index c171041022..192cd31d4a 100644 --- a/src/sql/workbench/services/notebook/browser/models/cell.ts +++ b/src/sql/workbench/services/notebook/browser/models/cell.ts @@ -418,6 +418,7 @@ export class CellModel extends Disposable implements ICellModel { if (newLanguage !== this._language) { this._language = newLanguage; this._onLanguageChanged.fire(newLanguage); + this.sendChangeToNotebook(NotebookChangeType.CellMetadataUpdated); } } diff --git a/src/sql/workbench/services/notebook/browser/notebookService.ts b/src/sql/workbench/services/notebook/browser/notebookService.ts index a64f425615..c828f4c59f 100644 --- a/src/sql/workbench/services/notebook/browser/notebookService.ts +++ b/src/sql/workbench/services/notebook/browser/notebookService.ts @@ -76,6 +76,8 @@ export interface INotebookService { getStandardKernelsForProvider(provider: string): Promise; + getSupportedLanguagesForProvider(provider: string, kernelDisplayName?: string): Promise; + getOrCreateSerializationManager(providerId: string, uri: URI): Promise; getOrCreateExecuteManager(providerId: string, uri: URI): Thenable; diff --git a/src/sql/workbench/services/notebook/browser/notebookServiceImpl.ts b/src/sql/workbench/services/notebook/browser/notebookServiceImpl.ts index d878bf63d1..34478ac5e5 100644 --- a/src/sql/workbench/services/notebook/browser/notebookServiceImpl.ts +++ b/src/sql/workbench/services/notebook/browser/notebookServiceImpl.ts @@ -494,6 +494,22 @@ export class NotebookService extends Disposable implements INotebookService { return kernels; } + public async getSupportedLanguagesForProvider(provider: string, kernelDisplayName?: string): Promise { + let languages: string[] = []; + let kernels = await this.getStandardKernelsForProvider(provider); + if (kernelDisplayName && kernels) { + kernels = kernels.filter(kernel => kernel.displayName === kernelDisplayName); + } + kernels?.forEach(kernel => { + if (kernel.supportedLanguages) { + languages.push(...kernel.supportedLanguages); + } + }); + // Remove duplicates + languages = [...new Set(languages)]; + return languages; + } + private shutdown(): void { this._executeManagersMap.forEach(manager => { manager.forEach(m => { diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts index de73549fce..0825144caf 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts @@ -1647,7 +1647,7 @@ registerAction2(class ClearCellOutputsAction extends NotebookCellAction { } }); -interface ILanguagePickInput extends IQuickPickItem { +export interface ILanguagePickInput extends IQuickPickItem { // {{SQL CARBON EDIT}} Add export languageId: string; description: string; } diff --git a/test/smoke/src/sql/areas/notebook/notebook.test.ts b/test/smoke/src/sql/areas/notebook/notebook.test.ts index 61302ddc8e..0660a2430c 100644 --- a/test/smoke/src/sql/areas/notebook/notebook.test.ts +++ b/test/smoke/src/sql/areas/notebook/notebook.test.ts @@ -160,6 +160,25 @@ export function setup(opts: minimist.ParsedArgs) { await verifyElementRendered(app, markdownString, imgSelector); }); }); + + describe('Cell Actions', function () { + it('can change cell language', async function () { + const app = this.app as Application; + await app.workbench.sqlNotebook.newUntitledNotebook(); + await app.workbench.sqlNotebook.notebookToolbar.waitForKernel('SQL'); + await app.workbench.sqlNotebook.addCellFromPlaceholder('Code'); + await app.workbench.sqlNotebook.waitForPlaceholderGone(); + + const languagePickerButton = '.notebook-cell.active .cellLanguage'; + await app.code.waitAndClick(languagePickerButton); + + await app.workbench.quickinput.waitForQuickInputElements(names => names[0] === 'SQL'); + await app.code.waitAndClick('.quick-input-widget .quick-input-list .monaco-list-row'); + + let element = await app.code.waitForElement(languagePickerButton); + assert.strictEqual(element.textContent?.trim(), 'SQL'); + }); + }); }); }