Enable VS Code notebooks with a built-in SQL kernel. (#21995)

This commit is contained in:
Cory Rivera
2023-02-23 16:22:46 -08:00
committed by GitHub
parent 290687a207
commit f53119c2a6
66 changed files with 4962 additions and 318 deletions

View File

@@ -0,0 +1,329 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as azdata from 'azdata';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
interface QueryCompletionHandler {
ownerUri: string;
handler: (results: azdata.BatchSummary[]) => void
}
interface QueryMessageHandler {
ownerUri: string;
handler: (results: azdata.QueryExecuteMessageParams) => void
}
export class SqlNotebookController implements vscode.Disposable {
private readonly _cellUriScheme = 'vscode-notebook-cell';
private readonly _connectionLabel = (serverName: string) => localize('notebookConnection', 'Connected to: {0}', serverName);
private readonly _disconnectedLabel = localize('notebookDisconnected', 'Disconnected');
private readonly _disposables = new Array<vscode.Disposable>();
private readonly _controller: vscode.NotebookController;
private readonly _connectionsMap = new Map<vscode.Uri, azdata.connection.Connection>();
private readonly _executionOrderMap = new Map<vscode.Uri, number>();
private readonly _queryProvider: azdata.QueryProvider;
private readonly _connProvider: azdata.ConnectionProvider;
private readonly _connectionLabelItem: vscode.StatusBarItem;
private _queryCompleteHandler: QueryCompletionHandler;
private _queryMessageHandler: QueryMessageHandler;
private _activeCellUri: string;
constructor() {
this._controller = vscode.notebooks.createNotebookController('sql-controller-id', 'jupyter-notebook', 'SQL');
this._controller.supportedLanguages = ['sql'];
this._controller.supportsExecutionOrder = true;
this._controller.executeHandler = this.execute.bind(this);
const sqlProvider = 'MSSQL';
this._queryProvider = azdata.dataprotocol.getProvider<azdata.QueryProvider>(sqlProvider, azdata.DataProviderType.QueryProvider);
this._queryProvider.registerOnQueryComplete(result => this.handleQueryComplete(result));
this._queryProvider.registerOnMessage(message => this.handleQueryMessage(message));
this._connProvider = azdata.dataprotocol.getProvider<azdata.ConnectionProvider>(sqlProvider, azdata.DataProviderType.ConnectionProvider);
const commandName = 'mssql.changeNotebookConnection';
let changeConnectionCommand = vscode.commands.registerCommand(commandName, async () => await this.changeConnection());
this._disposables.push(changeConnectionCommand);
this._connectionLabelItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right);
this._connectionLabelItem.text = this._disconnectedLabel;
this._connectionLabelItem.tooltip = localize('changeNotebookConnection', 'Change SQL Notebook Connection');
this._connectionLabelItem.command = commandName;
this._disposables.push(this._connectionLabelItem);
// Show connection status if there's a notebook already open when ADS starts
if (vscode.window.activeTextEditor?.document.notebook) {
this._connectionLabelItem.show();
}
let editorChangedEvent = vscode.window.onDidChangeActiveTextEditor(async editor => await this.handleActiveEditorChanged(editor));
this._disposables.push(editorChangedEvent);
let docClosedEvent = vscode.workspace.onDidCloseTextDocument(document => this.handleDocumentClosed(document));
this._disposables.push(docClosedEvent);
}
private handleQueryComplete(result: azdata.QueryExecuteCompleteNotificationResult): void {
if (this._queryCompleteHandler && this._queryCompleteHandler.ownerUri === result.ownerUri) { // Check if handler is undefined separately in case the result URI is also undefined
this._queryCompleteHandler.handler(result.batchSummaries);
}
}
private handleQueryMessage(message: azdata.QueryExecuteMessageParams): void {
if (this._queryMessageHandler && this._queryMessageHandler.ownerUri === message.ownerUri) { // Check if handler is undefined separately in case the result URI is also undefined
this._queryMessageHandler.handler(message);
}
}
private async handleActiveEditorChanged(editor: vscode.TextEditor): Promise<void> {
let notebook = editor?.document.notebook;
if (!notebook) {
// Hide status bar item if the current editor isn't a notebook
this._connectionLabelItem.hide();
} else {
let connection = this._connectionsMap.get(notebook.uri);
if (connection) {
this._connectionLabelItem.text = this._connectionLabel(connection.options['server']);
// If this editor is for a cell, then update the connection for it
this.updateCellConnection(notebook.uri, connection);
} else {
this._connectionLabelItem.text = this._disconnectedLabel;
}
this._connectionLabelItem.show();
}
}
public getConnectionProfile(connection: azdata.connection.Connection): azdata.IConnectionProfile {
let connectionProfile: azdata.IConnectionProfile = {
connectionName: connection.options.connectionName,
serverName: connection.options.server,
databaseName: connection.options.database,
userName: connection.options.user,
password: connection.options.password,
authenticationType: connection.options.authenticationType,
savePassword: connection.options.savePassword,
groupFullName: undefined,
groupId: undefined,
providerName: connection.providerName,
saveProfile: false,
id: connection.connectionId,
options: connection.options
};
return connectionProfile;
}
private handleDocumentClosed(editor: vscode.TextDocument): void {
// Have to check isClosed here since this event is also emitted on doc language changes
if (editor.notebook && editor.isClosed) {
// Remove the connection & execution associations if the doc is closed, but don't close the connection since it might be re-used elsewhere
this._connectionsMap.delete(editor.notebook.uri);
this._executionOrderMap.delete(editor.notebook.uri);
}
}
private updateCellConnection(notebookUri: vscode.Uri, connection: azdata.connection.Connection): void {
let docUri = vscode.window.activeTextEditor?.document.uri;
if (docUri?.scheme === this._cellUriScheme && docUri?.path === notebookUri.path) {
if (this._activeCellUri) {
this._connProvider.disconnect(this._activeCellUri).then(() => undefined, error => console.log(error));
}
this._activeCellUri = docUri.toString();
// Delay connecting in case user is clicking between cells a lot
setTimeout(() => {
if (this._activeCellUri === docUri.toString()) {
let profile = this.getConnectionProfile(connection);
this._connProvider.connect(docUri.toString(), profile).then(
connected => {
if (!connected) {
console.log(`Failed to update cell connection for cell: ${docUri.toString()}`);
}
},
error => {
console.log(error);
});
}
}, 200);
}
}
private async changeConnection(notebook?: vscode.NotebookDocument): Promise<azdata.connection.Connection | undefined> {
let connection: azdata.connection.Connection;
let notebookUri = notebook?.uri ?? vscode.window.activeTextEditor?.document.notebook?.uri;
if (notebookUri) {
connection = await azdata.connection.openConnectionDialog(['MSSQL']);
if (connection) {
this._connectionsMap.set(notebookUri, connection);
this._connectionLabelItem.text = this._connectionLabel(connection.options['server']);
// Connect current notebook cell, if there is one
this.updateCellConnection(notebookUri, connection);
} else {
this._connectionLabelItem.text = this._disconnectedLabel;
}
this._connectionLabelItem.show();
}
return connection;
}
private async execute(cells: vscode.NotebookCell[], notebook: vscode.NotebookDocument, controller: vscode.NotebookController): Promise<void> {
if (this._queryCompleteHandler) {
throw new Error(localize('queryInProgressError', 'Another query is currently in progress. Please wait for that query to complete before running these cells.'));
}
let connection = this._connectionsMap.get(notebook.uri);
if (!connection) {
connection = await this.changeConnection(notebook);
}
let executionOrder = this._executionOrderMap.get(notebook.uri) ?? 0;
for (let cell of cells) {
await this.doExecution(cell, connection, ++executionOrder);
}
this._executionOrderMap.set(notebook.uri, executionOrder);
}
private async doExecution(cell: vscode.NotebookCell, connection: azdata.connection.Connection | undefined, executionOrder: number): Promise<void> {
const execution = this._controller.createNotebookCellExecution(cell);
execution.executionOrder = executionOrder;
execution.start(Date.now());
await execution.clearOutput();
if (!connection) {
await execution.appendOutput([
new vscode.NotebookCellOutput([
vscode.NotebookCellOutputItem.text(localize('noConnectionError', 'No connection provided.'))
])
]);
execution.end(false, Date.now());
return;
}
let cancelHandler: vscode.Disposable;
try {
const ownerUri = await azdata.connection.getUriForConnection(connection.connectionId);
await this._queryProvider.runQueryString(ownerUri, cell.document.getText());
cancelHandler = execution.token.onCancellationRequested(async () => await this._queryProvider.cancelQuery(ownerUri));
let queryComplete = new Promise<void>(resolve => {
let queryCompleteHandler = async (batchSummaries: azdata.BatchSummary[]) => {
let tableHtmlEntries: string[] = [];
for (let batchSummary of batchSummaries) {
if (execution.token.isCancellationRequested) {
break;
}
for (let resultSummary of batchSummary.resultSetSummaries) {
if (execution.token.isCancellationRequested) {
break;
}
if (resultSummary.rowCount > 0) {
// Add column headers
let tableHtml =
`<style>
.output_container .sqlNotebookResults td, .output_container .sqlNotebookResults th {
text-align: left;
}
</style>
<table class="sqlNotebookResults"><thead><tr>`;
for (let column of resultSummary.columnInfo) {
tableHtml += `<th>${htmlEscape(column.columnName)}</th>`;
}
tableHtml += '</tr></thead>';
// Add rows and cells
let subsetResult = await this._queryProvider.getQueryRows({
ownerUri: ownerUri,
batchIndex: batchSummary.id,
resultSetIndex: resultSummary.id,
rowsStartIndex: 0,
rowsCount: resultSummary.rowCount
});
tableHtml += '<tbody>';
for (let row of subsetResult.resultSubset.rows) {
tableHtml += '<tr>';
for (let cell of row) {
tableHtml += `<td>${htmlEscape(cell.displayValue)}</td>`;
}
tableHtml += '</tr>';
}
tableHtml += '</tbody></table>';
tableHtmlEntries.push(tableHtml);
}
}
}
if (execution.token.isCancellationRequested) {
await execution.appendOutput([
new vscode.NotebookCellOutput([
vscode.NotebookCellOutputItem.text(localize('cellExecutionCancelled', 'Cell execution cancelled.'))
])
]);
execution.end(false, Date.now());
} else {
await execution.appendOutput([
new vscode.NotebookCellOutput([
vscode.NotebookCellOutputItem.text(tableHtmlEntries.join('<br><br>'), 'text/html')
])
]);
execution.end(true, Date.now());
}
resolve();
};
this._queryCompleteHandler = { ownerUri: ownerUri, handler: queryCompleteHandler };
});
this._queryMessageHandler = {
ownerUri: ownerUri,
handler: async message => {
await execution.appendOutput([
new vscode.NotebookCellOutput([
vscode.NotebookCellOutputItem.text(message.message.message)
])
]);
}
};
await queryComplete;
} catch (error) {
await execution.appendOutput([
new vscode.NotebookCellOutput([
vscode.NotebookCellOutputItem.error(error)
])
]);
execution.end(false, Date.now());
} finally {
if (cancelHandler) {
cancelHandler.dispose();
}
this._queryCompleteHandler = undefined;
this._queryMessageHandler = undefined;
}
}
dispose() {
this._disposables.forEach(d => d.dispose());
}
}
function htmlEscape(html: string): string {
return html.replace(/[<|>|&|"]/g, function (match) {
switch (match) {
case '<': return '&lt;';
case '>': return '&gt;';
case '&': return '&amp;';
case '"': return '&quot;';
case '\'': return '&#39';
default: return match;
}
});
}