mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-03-18 11:03:18 -04:00
328 lines
13 KiB
TypeScript
328 lines
13 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* 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 | undefined;
|
|
private _queryMessageHandler: QueryMessageHandler | undefined;
|
|
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) as (cells: vscode.NotebookCell[], notebook: vscode.NotebookDocument, controller: vscode.NotebookController) => void | Thenable<void>;
|
|
|
|
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 | undefined): 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 as string,
|
|
serverName: connection.options.server as string,
|
|
databaseName: connection.options.database as string,
|
|
userName: connection.options.user as string,
|
|
password: connection.options.password as string,
|
|
authenticationType: connection.options.authenticationType as string,
|
|
savePassword: connection.options.savePassword as boolean,
|
|
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 && 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 | undefined;
|
|
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 | undefined;
|
|
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 {
|
|
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 '<';
|
|
case '>': return '>';
|
|
case '&': return '&';
|
|
case '"': return '"';
|
|
case '\'': return ''';
|
|
default: return match;
|
|
}
|
|
});
|
|
}
|