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 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>
<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}}
</div>
</button>
</div>
<div #parameter class="parameter" *ngIf="cellModel.cellType === 'code' && cellModel.isParameter">
<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 { 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 = <HTMLElement>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<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;
display: inline-block;
text-align: center;
font-size: 16px;
font-size: 12px;
border-style: none;
background-color: transparent;
}
code-component .parameter {

View File

@@ -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) {

View File

@@ -235,6 +235,9 @@ export class ServerManagerStub implements nb.ServerManager {
}
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> {
throw new Error('Method not implemented.');
}

View File

@@ -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);
}
}

View File

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

View File

@@ -494,6 +494,22 @@ export class NotebookService extends Disposable implements INotebookService {
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 {
this._executeManagersMap.forEach(manager => {
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;
description: string;
}

View File

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