Add language picker to notebook cells. (#18601)

This commit is contained in:
Cory Rivera
2022-03-02 10:25:55 -08:00
committed by GitHub
parent c34de52a03
commit 79cda5cbe5
10 changed files with 144 additions and 7 deletions

View File

@@ -14,9 +14,9 @@
</div> </div>
<div style="display: flex; flex-flow: row; justify-content: flex-end;"> <div style="display: flex; flex-flow: row; justify-content: flex-end;">
<collapse-component *ngIf="cellModel.cellType === 'code' && cellModel.source && cellModel.source.length > 1" [cellModel]="cellModel" [activeCellId]="activeCellId"></collapse-component> <collapse-component *ngIf="cellModel.cellType === 'code' && cellModel.source && cellModel.source.length > 1" [cellModel]="cellModel" [activeCellId]="activeCellId"></collapse-component>
<div #cellLanguage class="cellLanguage" *ngIf="cellModel.cellType === 'code' && cellModel.language"> <button #cellLanguage title="{{cellLanguageTitle}}" class="cellLanguage" *ngIf="cellModel.cellType === 'code' && cellModel.language" (click)="onCellLanguageClick()">
{{cellModel.displayLanguage}} {{cellModel.displayLanguage}}
</div> </button>
</div> </div>
<div #parameter class="parameter" *ngIf="cellModel.cellType === 'code' && cellModel.isParameter"> <div #parameter class="parameter" *ngIf="cellModel.cellType === 'code' && cellModel.isParameter">
<span>{{parametersText}}</span> <span>{{parametersText}}</span>

View File

@@ -22,7 +22,7 @@ import { IModelService } from 'vs/editor/common/services/modelService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { Event, Emitter } from 'vs/base/common/event'; import { Event, Emitter } from 'vs/base/common/event';
import { CellTypes } from 'sql/workbench/services/notebook/common/contracts'; 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 { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement';
import { ILogService } from 'vs/platform/log/common/log'; import { ILogService } from 'vs/platform/log/common/log';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; 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 { tryMatchCellMagic } from 'sql/workbench/services/notebook/browser/utils';
import { IColorTheme } from 'vs/platform/theme/common/themeService'; import { IColorTheme } from 'vs/platform/theme/common/themeService';
import { localize } from 'vs/nls'; 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'; export const CODE_SELECTOR: string = 'code-component';
const MARKDOWN_CLASS = 'markdown'; const MARKDOWN_CLASS = 'markdown';
@@ -102,7 +107,9 @@ export class CodeComponent extends CellView implements OnInit, OnChanges {
@Inject(IModeService) private _modeService: IModeService, @Inject(IModeService) private _modeService: IModeService,
@Inject(IConfigurationService) private _configurationService: IConfigurationService, @Inject(IConfigurationService) private _configurationService: IConfigurationService,
@Inject(forwardRef(() => ChangeDetectorRef)) private _changeRef: ChangeDetectorRef, @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(); super();
this._register(Event.debounce(this._layoutEmitter.event, (l, e) => e, 250, /*leading=*/false) 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; return this.cellModel.cellGuid;
} }
get cellLanguageTitle(): string {
return localize('selectCellLanguage', "Select Cell Language Mode");
}
get parametersText(): string { get parametersText(): string {
return localize('parametersText', "Parameters"); return localize('parametersText', "Parameters");
} }
@@ -269,6 +280,7 @@ export class CodeComponent extends CellView implements OnInit, OnChanges {
this._register(this.cellModel.onLanguageChanged(language => { this._register(this.cellModel.onLanguageChanged(language => {
let nativeElement = <HTMLElement>this.languageElement.nativeElement; let nativeElement = <HTMLElement>this.languageElement.nativeElement;
nativeElement.innerText = this.cellModel.displayLanguage; nativeElement.innerText = this.cellModel.displayLanguage;
nativeElement.ariaLabel = this.cellModel.displayLanguage;
this.updateLanguageMode(); this.updateLanguageMode();
this._changeRef.detectChanges(); 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<ILanguagePickInput | undefined> {
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 = <ILanguagePickInput>{
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<ILanguagePickInput | undefined>;
}
/**
* 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;
}
} }

View File

@@ -118,7 +118,9 @@ code-component .cellLanguage {
padding: 2px 15px; padding: 2px 15px;
display: inline-block; display: inline-block;
text-align: center; text-align: center;
font-size: 16px; font-size: 12px;
border-style: none;
background-color: transparent;
} }
code-component .parameter { code-component .parameter {

View File

@@ -6,7 +6,7 @@ import 'vs/css!./notebook';
import { registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; 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 { 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 { 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 { 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'; 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 { BareResultsGridInfo, getBareResultsGridInfoStyles } from 'sql/workbench/contrib/query/browser/queryResultsEditor';
import { getZoomLevel } from 'vs/base/browser/browser'; import { getZoomLevel } from 'vs/base/browser/browser';
import * as types from 'vs/base/common/types'; 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 { export function registerNotebookThemes(overrideEditorThemeSetting: boolean, configurationService: IConfigurationService): IDisposable {
return registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { 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};}`); 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 // Markdown editor toolbar
const toolbarBackgroundColor = theme.getColor(toolbarBackground); const toolbarBackgroundColor = theme.getColor(toolbarBackground);
if (toolbarBackgroundColor) { if (toolbarBackgroundColor) {

View File

@@ -235,6 +235,9 @@ export class ServerManagerStub implements nb.ServerManager {
} }
export class NotebookServiceStub implements INotebookService { export class NotebookServiceStub implements INotebookService {
getSupportedLanguagesForProvider(provider: string, kernelDisplayName?: string): Promise<string[]> {
throw new Error('Method not implemented.');
}
createNotebookInput(options: INotebookShowOptions, resource?: UriComponents): Promise<IEditorInput> { createNotebookInput(options: INotebookShowOptions, resource?: UriComponents): Promise<IEditorInput> {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }

View File

@@ -418,6 +418,7 @@ export class CellModel extends Disposable implements ICellModel {
if (newLanguage !== this._language) { if (newLanguage !== this._language) {
this._language = newLanguage; this._language = newLanguage;
this._onLanguageChanged.fire(newLanguage); this._onLanguageChanged.fire(newLanguage);
this.sendChangeToNotebook(NotebookChangeType.CellMetadataUpdated);
} }
} }

View File

@@ -76,6 +76,8 @@ export interface INotebookService {
getStandardKernelsForProvider(provider: string): Promise<azdata.nb.IStandardKernel[] | undefined>; getStandardKernelsForProvider(provider: string): Promise<azdata.nb.IStandardKernel[] | undefined>;
getSupportedLanguagesForProvider(provider: string, kernelDisplayName?: string): Promise<string[]>;
getOrCreateSerializationManager(providerId: string, uri: URI): Promise<ISerializationManager>; getOrCreateSerializationManager(providerId: string, uri: URI): Promise<ISerializationManager>;
getOrCreateExecuteManager(providerId: string, uri: URI): Thenable<IExecuteManager>; getOrCreateExecuteManager(providerId: string, uri: URI): Thenable<IExecuteManager>;

View File

@@ -494,6 +494,22 @@ export class NotebookService extends Disposable implements INotebookService {
return kernels; return kernels;
} }
public async getSupportedLanguagesForProvider(provider: string, kernelDisplayName?: string): Promise<string[]> {
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 { private shutdown(): void {
this._executeManagersMap.forEach(manager => { this._executeManagersMap.forEach(manager => {
manager.forEach(m => { manager.forEach(m => {

View File

@@ -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; languageId: string;
description: string; description: string;
} }

View File

@@ -160,6 +160,25 @@ export function setup(opts: minimist.ParsedArgs) {
await verifyElementRendered(app, markdownString, imgSelector); 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');
});
});
}); });
} }