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');
+ });
+ });
});
}